diff --git a/ImportScolars.py b/ImportScolars.py
new file mode 100644
index 0000000000000000000000000000000000000000..c819551e7e0515309f55c80018bcdfdb6902fc23
--- /dev/null
+++ b/ImportScolars.py
@@ -0,0 +1,781 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+""" Importation des etudiants à partir de fichiers CSV
+"""
+
+import os, sys, time, pdb
+
+from sco_utils import *
+from notesdb import *
+from notes_log import log
+import scolars
+import sco_formsemestre
+import sco_groups
+import sco_excel
+import sco_groups_view
+import sco_news
+from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC
+from sco_formsemestre_inscriptions import do_formsemestre_inscription_with_modules
+from gen_tables import GenTable
+
+# format description (relative to Product directory))
+FORMAT_FILE = "misc/format_import_etudiants.txt"
+
+# Champs modifiables via "Import données admission"
+ADMISSION_MODIFIABLE_FIELDS = (
+    "code_nip",
+    "code_ine",
+    "date_naissance",
+    "lieu_naissance",
+    "bac",
+    "specialite",
+    "annee_bac",
+    "math",
+    "physique",
+    "anglais",
+    "francais",
+    "type_admission",
+    "boursier_prec",
+    "qualite",
+    "rapporteur",
+    "score",
+    "commentaire",
+    "classement",
+    "apb_groupe",
+    "apb_classement_gr",
+    "nomlycee",
+    "villelycee",
+    "codepostallycee",
+    "codelycee",
+    # Adresse:
+    "email",
+    "emailperso",
+    "domicile",
+    "codepostaldomicile",
+    "villedomicile",
+    "paysdomicile",
+    "telephone",
+    "telephonemobile",
+    # Debouche
+    "debouche",
+    # Groupes
+    "groupes",
+)
+
+# ----
+
+
+def sco_import_format(with_codesemestre=True):
+    "returns tuples (Attribut, Type, Table, AllowNulls, Description)"
+    r = []
+    for l in open(SCO_SRCDIR + "/" + FORMAT_FILE):
+        l = l.strip()
+        if l and l[0] != "#":
+            fs = l.split(";")
+            if len(fs) < 5:
+                # Bug: invalid format file (fatal)
+                raise ScoException(
+                    "file %s has invalid format (expected %d fields, got %d) (%s)"
+                    % (FORMAT_FILE, 5, len(fs), l)
+                )
+            fieldname = (
+                fs[0].strip().lower().split()[0]
+            )  # titre attribut: normalize, 1er mot seulement (nom du champ en BD)
+            typ, table, allow_nulls, description = [x.strip() for x in fs[1:5]]
+            aliases = [x.strip() for x in fs[5:] if x.strip()]
+            if fieldname not in aliases:
+                aliases.insert(0, fieldname)  # prepend
+            if with_codesemestre or fs[0] != "codesemestre":
+                r.append((fieldname, typ, table, allow_nulls, description, aliases))
+    return r
+
+
+def sco_import_format_dict(with_codesemestre=True):
+    """ Attribut: { 'type': , 'table', 'allow_nulls' , 'description' }
+    """
+    fmt = sco_import_format(with_codesemestre=with_codesemestre)
+    R = collections.OrderedDict()
+    for l in fmt:
+        R[l[0]] = {
+            "type": l[1],
+            "table": l[2],
+            "allow_nulls": l[3],
+            "description": l[4],
+            "aliases": l[5],
+        }
+    return R
+
+
+def sco_import_generate_excel_sample(
+    fmt,
+    with_codesemestre=True,
+    only_tables=None,
+    with_groups=True,
+    exclude_cols=[],
+    extra_cols=[],
+    group_ids=[],
+    context=None,
+    REQUEST=None,
+):
+    """Generates an excel document based on format fmt
+    (format is the result of sco_import_format())
+    If not None, only_tables can specify a list of sql table names
+    (only columns from these tables will be generated)
+    If group_ids, liste les etudiants de ces groupes
+    """
+    style = sco_excel.Excel_MakeStyle(bold=True)
+    style_required = sco_excel.Excel_MakeStyle(bold=True, color="red")
+    titles = []
+    titlesStyles = []
+    for l in fmt:
+        name = strlower(l[0])
+        if (not with_codesemestre) and name == "codesemestre":
+            continue  # pas de colonne codesemestre
+        if only_tables is not None and strlower(l[2]) not in only_tables:
+            continue  # table non demandée
+        if name in exclude_cols:
+            continue  # colonne exclue
+        if int(l[3]):
+            titlesStyles.append(style)
+        else:
+            titlesStyles.append(style_required)
+        titles.append(name)
+    if with_groups and "groupes" not in titles:
+        titles.append("groupes")
+        titlesStyles.append(style)
+    titles += extra_cols
+    titlesStyles += [style] * len(extra_cols)
+    if group_ids and context:
+        groups_infos = sco_groups_view.DisplayedGroupsInfos(
+            context, group_ids, REQUEST=REQUEST
+        )
+        members = groups_infos.members
+        log(
+            "sco_import_generate_excel_sample: group_ids=%s  %d members"
+            % (group_ids, len(members))
+        )
+        titles = ["etudid"] + titles
+        titlesStyles = [style] + titlesStyles
+        # rempli table avec données actuelles
+        lines = []
+        for i in members:
+            etud = context.getEtudInfo(etudid=i["etudid"], filled=True)[0]
+            l = []
+            for field in titles:
+                if field == "groupes":
+                    sco_groups.etud_add_group_infos(
+                        context, etud, groups_infos.formsemestre, sep=";"
+                    )
+                    l.append(etud["partitionsgroupes"])
+                else:
+                    key = strlower(field).split()[0]
+                    l.append(etud.get(key, ""))
+            lines.append(l)
+    else:
+        lines = [[]]  # empty content, titles only
+    return sco_excel.Excel_SimpleTable(
+        titles=titles, titlesStyles=titlesStyles, SheetName="Etudiants", lines=lines
+    )
+
+
+def students_import_excel(
+    context,
+    csvfile,
+    REQUEST=None,
+    formsemestre_id=None,
+    check_homonyms=True,
+    require_ine=False,
+):
+    "import students from Excel file"
+    diag = scolars_import_excel_file(
+        csvfile,
+        context.Notes,
+        REQUEST,
+        formsemestre_id=formsemestre_id,
+        check_homonyms=check_homonyms,
+        require_ine=require_ine,
+        exclude_cols=["photo_filename"],
+    )
+    if REQUEST:
+        if formsemestre_id:
+            dest = "formsemestre_status?formsemestre_id=%s" % formsemestre_id
+        else:
+            dest = REQUEST.URL1
+        H = [context.sco_header(REQUEST, page_title="Import etudiants")]
+        H.append("<ul>")
+        for d in diag:
+            H.append("<li>%s</li>" % d)
+        H.append("</ul>")
+        H.append("<p>Import terminé !</p>")
+        H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % dest)
+        return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+def scolars_import_excel_file(
+    datafile,
+    context,
+    REQUEST,
+    formsemestre_id=None,
+    check_homonyms=True,
+    require_ine=False,
+    exclude_cols=[],
+):
+    """Importe etudiants depuis fichier Excel
+    et les inscrit dans le semestre indiqué (et à TOUS ses modules)
+    """
+    log("scolars_import_excel_file: formsemestre_id=%s" % formsemestre_id)
+    cnx = context.GetDBConnexion(autocommit=False)
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    annee_courante = time.localtime()[0]
+    always_require_ine = context.get_preference("always_require_ine")
+    exceldata = datafile.read()
+    if not exceldata:
+        raise ScoValueError("Ficher excel vide ou invalide")
+    diag, data = sco_excel.Excel_to_list(exceldata)
+    if not data:  # probably a bug
+        raise ScoException("scolars_import_excel_file: empty file !")
+
+    formsemestre_to_invalidate = Set()
+
+    # 1-  --- check title line
+    titles = {}
+    fmt = sco_import_format()
+    for l in fmt:
+        tit = strlower(l[0]).split()[0]  # titles in lowercase, and take 1st word
+        if (
+            (not formsemestre_id) or (tit != "codesemestre")
+        ) and tit not in exclude_cols:
+            titles[tit] = l[1:]  # title : (Type, Table, AllowNulls, Description)
+
+    # log("titles=%s" % titles)
+    # remove quotes, downcase and keep only 1st word
+    try:
+        fs = [strlower(stripquotes(s)).split()[0] for s in data[0]]
+    except:
+        raise ScoValueError("Titres de colonnes invalides (ou vides ?)")
+    # log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
+
+    # check columns titles
+    if len(fs) != len(titles):
+        missing = {}.fromkeys(titles.keys())
+        unknown = []
+        for f in fs:
+            if missing.has_key(f):
+                del missing[f]
+            else:
+                unknown.append(f)
+        raise ScoValueError(
+            "Nombre de colonnes incorrect (devrait être %d, et non %d) <br/> (colonnes manquantes: %s, colonnes invalides: %s)"
+            % (len(titles), len(fs), missing.keys(), unknown)
+        )
+    titleslist = []
+    for t in fs:
+        if not titles.has_key(t):
+            raise ScoValueError('Colonne invalide: "%s"' % t)
+        titleslist.append(t)  #
+    # ok, same titles
+    # Start inserting data, abort whole transaction in case of error
+    created_etudids = []
+    NbImportedHomonyms = 0
+    GroupIdInferers = {}
+    try:  # --- begin DB transaction
+        linenum = 0
+        for line in data[1:]:
+            linenum += 1
+            # Read fields, check and convert type
+            values = {}
+            fs = line
+            # remove quotes
+            for i in range(len(fs)):
+                if fs[i] and (
+                    (fs[i][0] == '"' and fs[i][-1] == '"')
+                    or (fs[i][0] == "'" and fs[i][-1] == "'")
+                ):
+                    fs[i] = fs[i][1:-1]
+            for i in range(len(fs)):
+                val = fs[i].strip()
+                typ, table, an, descr, aliases = tuple(titles[titleslist[i]])
+                # log('field %s: %s %s %s %s'%(titleslist[i], table, typ, an, descr))
+                if not val and not an:
+                    raise ScoValueError(
+                        "line %d: null value not allowed in column %s"
+                        % (linenum, titleslist[i])
+                    )
+                if val == "":
+                    val = None
+                else:
+                    if typ == "real":
+                        val = val.replace(",", ".")  # si virgule a la française
+                        try:
+                            val = float(val)
+                        except:
+                            raise ScoValueError(
+                                "valeur nombre reel invalide (%s) sur line %d, colonne %s"
+                                % (val, linenum, titleslist[i])
+                            )
+                    elif typ == "integer":
+                        try:
+                            # on doit accepter des valeurs comme "2006.0"
+                            val = val.replace(",", ".")  # si virgule a la française
+                            val = float(val)
+                            if val % 1.0 > 1e-4:
+                                raise ValueError()
+                            val = int(val)
+                        except:
+                            raise ScoValueError(
+                                "valeur nombre entier invalide (%s) sur ligne %d, colonne %s"
+                                % (val, linenum, titleslist[i])
+                            )
+                # xxx Ad-hoc checks (should be in format description)
+                if strlower(titleslist[i]) == "sexe":
+                    try:
+                        val = scolars.normalize_sexe(val)
+                    except:
+                        raise ScoValueError(
+                            "valeur invalide pour 'SEXE' (doit etre 'M' ou 'MME' ou 'H' ou 'F', pas '%s') ligne %d, colonne %s"
+                            % (val, linenum, titleslist[i])
+                        )
+                # Excel date conversion:
+                if strlower(titleslist[i]) == "date_naissance":
+                    if val:
+                        if re.match("^[0-9]*\.?[0-9]*$", str(val)):
+                            val = sco_excel.xldate_as_datetime(float(val))
+                # INE
+                if (
+                    strlower(titleslist[i]) == "code_ine"
+                    and always_require_ine
+                    and not val
+                ):
+                    raise ScoValueError(
+                        "Code INE manquant sur ligne %d, colonne %s"
+                        % (linenum, titleslist[i])
+                    )
+
+                # --
+                values[titleslist[i]] = val
+            skip = False
+            is_new_ine = values["code_ine"] and _is_new_ine(cnx, values["code_ine"])
+            if require_ine and not is_new_ine:
+                log("skipping %s (code_ine=%s)" % (values["nom"], values["code_ine"]))
+                skip = True
+
+            if not skip:
+                if values["code_ine"] and not is_new_ine:
+                    raise ScoValueError("Code INE dupliqué (%s)" % values["code_ine"])
+                # Check nom/prenom
+                ok, NbHomonyms = scolars.check_nom_prenom(
+                    cnx, nom=values["nom"], prenom=values["prenom"]
+                )
+                if not ok:
+                    raise ScoValueError(
+                        "nom ou prénom invalide sur la ligne %d" % (linenum)
+                    )
+                if NbHomonyms:
+                    NbImportedHomonyms += 1
+                # Insert in DB tables
+                formsemestre_to_invalidate.add(
+                    _import_one_student(
+                        context,
+                        cnx,
+                        REQUEST,
+                        formsemestre_id,
+                        values,
+                        GroupIdInferers,
+                        annee_courante,
+                        created_etudids,
+                        linenum,
+                    )
+                )
+
+        # Verification proportion d'homonymes: si > 10%, abandonne
+        log("scolars_import_excel_file: detected %d homonyms" % NbImportedHomonyms)
+        if check_homonyms and NbImportedHomonyms > len(created_etudids) / 10:
+            log("scolars_import_excel_file: too many homonyms")
+            raise ScoValueError(
+                "Il y a trop d'homonymes (%d étudiants)" % NbImportedHomonyms
+            )
+    except:
+        cnx.rollback()
+        log("scolars_import_excel_file: aborting transaction !")
+        # Nota: db transaction is sometimes partly commited...
+        # here we try to remove all created students
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        for etudid in created_etudids:
+            log("scolars_import_excel_file: deleting etudid=%s" % etudid)
+            cursor.execute(
+                "delete from notes_moduleimpl_inscription where etudid=%(etudid)s",
+                {"etudid": etudid},
+            )
+            cursor.execute(
+                "delete from notes_formsemestre_inscription where etudid=%(etudid)s",
+                {"etudid": etudid},
+            )
+            cursor.execute(
+                "delete from scolar_events where etudid=%(etudid)s", {"etudid": etudid}
+            )
+            cursor.execute(
+                "delete from adresse where etudid=%(etudid)s", {"etudid": etudid}
+            )
+            cursor.execute(
+                "delete from admissions where etudid=%(etudid)s", {"etudid": etudid}
+            )
+            cursor.execute(
+                "delete from group_membership where etudid=%(etudid)s",
+                {"etudid": etudid},
+            )
+            cursor.execute(
+                "delete from identite where etudid=%(etudid)s", {"etudid": etudid}
+            )
+        cnx.commit()
+        log("scolars_import_excel_file: re-raising exception")
+        raise
+
+    diag.append("Import et inscription de %s étudiants" % len(created_etudids))
+
+    sco_news.add(
+        context,
+        REQUEST,
+        typ=NEWS_INSCR,
+        text="Inscription de %d étudiants"  # peuvent avoir ete inscrits a des semestres differents
+        % len(created_etudids),
+        object=formsemestre_id,
+    )
+
+    log("scolars_import_excel_file: completing transaction")
+    cnx.commit()
+
+    # Invalide les caches des semestres dans lesquels on a inscrit des etudiants:
+    context.Notes._inval_cache(formsemestre_id_list=formsemestre_to_invalidate)
+
+    return diag
+
+
+def _import_one_student(
+    context,
+    cnx,
+    REQUEST,
+    formsemestre_id,
+    values,
+    GroupIdInferers,
+    annee_courante,
+    created_etudids,
+    linenum,
+):
+    """
+    Import d'un étudiant et inscription dans le semestre.
+    Return: id du semestre dans lequel il a été inscrit.
+    """
+    log(
+        "scolars_import_excel_file: formsemestre_id=%s values=%s"
+        % (formsemestre_id, str(values))
+    )
+    # Identite
+    args = values.copy()
+    etudid = scolars.identite_create(cnx, args, context=context, REQUEST=REQUEST)
+    created_etudids.append(etudid)
+    # Admissions
+    args["etudid"] = etudid
+    args["annee"] = annee_courante
+    adm_id = scolars.admission_create(cnx, args)
+    # Adresse
+    args["typeadresse"] = "domicile"
+    args["description"] = "(infos admission)"
+    adresse_id = scolars.adresse_create(cnx, args)
+    # Inscription au semestre
+    args["etat"] = "I"  # etat insc. semestre
+    if formsemestre_id:
+        args["formsemestre_id"] = formsemestre_id
+    else:
+        args["formsemestre_id"] = values["codesemestre"]
+        formsemestre_id = values["codesemestre"]
+    # recupere liste des groupes:
+    if formsemestre_id not in GroupIdInferers:
+        GroupIdInferers[formsemestre_id] = sco_groups.GroupIdInferer(
+            context, formsemestre_id
+        )
+    gi = GroupIdInferers[formsemestre_id]
+    if args["groupes"]:
+        groupes = args["groupes"].split(";")
+    else:
+        groupes = []
+    group_ids = [gi[group_name] for group_name in groupes]
+    group_ids = {}.fromkeys(group_ids).keys()  # uniq
+    if None in group_ids:
+        raise ScoValueError(
+            "groupe invalide sur la ligne %d (groupe %s)" % (linenum, groupes)
+        )
+
+    do_formsemestre_inscription_with_modules(
+        context,
+        args["formsemestre_id"],
+        etudid,
+        group_ids,
+        etat="I",
+        REQUEST=REQUEST,
+        method="import_csv_file",
+    )
+    return args["formsemestre_id"]
+
+
+def _is_new_ine(cnx, code_ine):
+    "True if this code is not in DB"
+    etuds = scolars.identite_list(cnx, {"code_ine": code_ine})
+    return not etuds
+
+
+# ------ Fonction ré-écrite en nov 2016 pour lire des fichiers sans etudid (fichiers APB)
+def scolars_import_admission(
+    datafile, context, REQUEST, formsemestre_id=None, type_admission=None
+):
+    """Importe données admission depuis un fichier Excel quelconque
+    par exemple ceux utilisés avec APB
+
+    Cherche dans ce fichier les étudiants qui correspondent à des inscrits du 
+    semestre formsemestre_id.
+    Le fichier n'a pas l'INE ni le NIP ni l'etudid, la correspondance se fait 
+    via les noms/prénoms qui doivent être égaux (la casse, les accents et caractères spéciaux 
+    étant ignorés).
+
+    On tolère plusieurs variantes pour chaque nom de colonne (ici aussi, la casse, les espaces 
+    et les caractères spéciaux sont ignorés. Ainsi, la colonne "Prénom:" sera considéré comme "prenom".
+
+    Le parametre type_admission remplace les valeurs vides (dans la base ET dans le fichier importé) du champ type_admission.
+    Si une valeur existe ou est présente dans le fichier importé, ce paramètre est ignoré.
+    
+    TODO:
+    - choix onglet du classeur
+    """
+
+    log("scolars_import_admission: formsemestre_id=%s" % formsemestre_id)
+    members = sco_groups.get_group_members(
+        context, sco_groups.get_default_group(context, formsemestre_id)
+    )
+    etuds_by_nomprenom = {}  # { nomprenom : etud }
+    diag = []
+    for m in members:
+        np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"]))
+        if np in etuds_by_nomprenom:
+            msg = "Attention: hononymie pour %s %s" % (m["nom"], m["prenom"])
+            log(msg)
+            diag.append(msg)
+        etuds_by_nomprenom[np] = m
+
+    exceldata = datafile.read()
+    diag2, data = sco_excel.Excel_to_list(exceldata, convert_to_string=False)
+    if not data:
+        raise ScoException("scolars_import_admission: empty file !")
+    diag += diag2
+    cnx = context.GetDBConnexion()
+
+    titles = data[0]
+    # idx -> ('field', convertor)
+    fields = adm_get_fields(titles, formsemestre_id)
+    idx_nom = None
+    idx_prenom = None
+    for idx in fields:
+        if fields[idx][0] == "nom":
+            idx_nom = idx
+        if fields[idx][0] == "prenom":
+            idx_prenom = idx
+    if (idx_nom is None) or (idx_prenom is None):
+        log("fields indices=" + ", ".join([str(x) for x in fields]))
+        log("fields titles =" + ", ".join([fields[x][0] for x in fields]))
+        raise FormatError(
+            "scolars_import_admission: colonnes nom et prenom requises",
+            dest_url="form_students_import_infos_admissions?formsemestre_id=%s"
+            % formsemestre_id,
+        )
+
+    modifiable_fields = Set(ADMISSION_MODIFIABLE_FIELDS)
+
+    nline = 2  # la premiere ligne de donnees du fichier excel est 2
+    n_import = 0
+    for line in data[1:]:
+        # Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
+        nom = adm_normalize_string(line[idx_nom])
+        prenom = adm_normalize_string(line[idx_prenom])
+        if not (nom, prenom) in etuds_by_nomprenom:
+            log(
+                "unable to find %s %s among members" % (line[idx_nom], line[idx_prenom])
+            )
+        else:
+            etud = etuds_by_nomprenom[(nom, prenom)]
+            cur_adm = scolars.admission_list(cnx, args={"etudid": etud["etudid"]})[0]
+            # peuple les champs presents dans le tableau
+            args = {}
+            for idx in fields:
+                field_name, convertor = fields[idx]
+                if field_name in modifiable_fields:
+                    try:
+                        val = convertor(line[idx])
+                    except ValueError:
+                        raise FormatError(
+                            'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"'
+                            % (nline, field_name, line[idx]),
+                            dest_url="form_students_import_infos_admissions?formsemestre_id=%s"
+                            % formsemestre_id,
+                        )
+                    if val is not None:  # note: ne peut jamais supprimer une valeur
+                        args[field_name] = val
+            if args:
+                args["etudid"] = etud["etudid"]
+                args["adm_id"] = cur_adm["adm_id"]
+                # Type admission: traitement particulier
+                if not cur_adm["type_admission"] and not args.get("type_admission"):
+                    args["type_admission"] = type_admission
+                scolars.etudident_edit(cnx, args)
+                adr = scolars.adresse_list(cnx, args={"etudid": etud["etudid"]})
+                if adr:
+                    args["adresse_id"] = adr[0]["adresse_id"]
+                    scolars.adresse_edit(
+                        cnx, args
+                    )  # ne passe pas le contexte: pas de notification ici
+                else:
+                    args["typeadresse"] = "domicile"
+                    args["description"] = "(infos admission)"
+                    adresse_id = scolars.adresse_create(cnx, args)
+                # log('import_adm: %s' % args )
+                # Change les groupes si nécessaire:
+                if args["groupes"]:
+                    gi = sco_groups.GroupIdInferer(context, formsemestre_id)
+                    groupes = args["groupes"].split(";")
+                    group_ids = [gi[group_name] for group_name in groupes]
+                    group_ids = {}.fromkeys(group_ids).keys()  # uniq
+                    if None in group_ids:
+                        raise ScoValueError(
+                            "groupe invalide sur la ligne %d (groupe %s)"
+                            % (nline, groupes)
+                        )
+
+                    for group_id in group_ids:
+                        sco_groups.change_etud_group_in_partition(
+                            context, args["etudid"], group_id, REQUEST=REQUEST
+                        )
+                #
+                diag.append("import de %s" % (etud["nomprenom"]))
+                n_import += 1
+        nline += 1
+    diag.append("%d lignes importées" % n_import)
+    if n_import > 0:
+        context._inval_cache(formsemestre_id=formsemestre_id)
+    return diag
+
+
+_ADM_PATTERN = re.compile(r"[\W]+", re.UNICODE)  # supprime tout sauf alphanum
+
+
+def adm_normalize_string(s):  # normalize unicode title
+    return suppression_diacritics(_ADM_PATTERN.sub("", s.strip().lower())).replace(
+        "_", ""
+    )
+
+
+def adm_get_fields(titles, formsemestre_id):
+    """Cherche les colonnes importables dans les titres (ligne 1) du fichier excel
+    return: { idx : (field_name, convertor) }
+    """
+    # log('adm_get_fields: titles=%s' % titles)
+    Fmt = sco_import_format_dict()
+    fields = {}
+    idx = 0
+    for title in titles:
+        title_n = adm_normalize_string(title)
+        for k in Fmt:
+            for v in Fmt[k]["aliases"]:
+                if adm_normalize_string(v) == title_n:
+                    typ = Fmt[k]["type"]
+                    if typ == "real":
+                        convertor = adm_convert_real
+                    elif typ == "integer" or typ == "int":
+                        convertor = adm_convert_int
+                    else:
+                        convertor = adm_convert_text
+                    # doublons ?
+                    if k in [x[0] for x in fields.values()]:
+                        raise FormatError(
+                            'scolars_import_admission: titre "%s" en double (ligne 1)'
+                            % (title),
+                            dest_url="form_students_import_infos_admissions_apb?formsemestre_id=%s"
+                            % formsemestre_id,
+                        )
+                    fields[idx] = (k, convertor)
+        idx += 1
+
+    return fields
+
+
+def adm_convert_text(v):
+    if type(v) == FloatType:
+        return "{:g}".format(v)  # evite "1.0"
+    return v
+
+
+def adm_convert_int(v):
+    if type(v) != IntType and not v:
+        return None
+    return int(float(v))  # accept "10.0"
+
+
+def adm_convert_real(v):
+    if type(v) != FloatType and not v:
+        return None
+    return float(v)
+
+
+def adm_table_description_format(context):
+    """Table HTML (ou autre format) decrivant les donnees d'admissions importables
+    """
+    Fmt = sco_import_format_dict(with_codesemestre=False)
+    for k in Fmt:
+        Fmt[k]["attribute"] = k
+        Fmt[k]["aliases_str"] = ", ".join(Fmt[k]["aliases"])
+        if not Fmt[k]["allow_nulls"]:
+            Fmt[k]["required"] = "*"
+        if k in ADMISSION_MODIFIABLE_FIELDS:
+            Fmt[k]["writable"] = "oui"
+        else:
+            Fmt[k]["writable"] = "non"
+    titles = {
+        "attribute": "Attribut",
+        "type": "Type",
+        "required": "Requis",
+        "writable": "Modifiable",
+        "description": "Description",
+        "aliases_str": "Titres (variantes)",
+    }
+    columns_ids = ("attribute", "type", "writable", "description", "aliases_str")
+
+    tab = GenTable(
+        titles=titles,
+        columns_ids=columns_ids,
+        rows=Fmt.values(),
+        html_sortable=True,
+        html_class="table_leftalign",
+        preferences=context.get_preferences(),
+    )
+    return tab
diff --git a/SuppressAccents.py b/SuppressAccents.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0b4d34fe357c57721c81e515b89078935ab7098
--- /dev/null
+++ b/SuppressAccents.py
@@ -0,0 +1,206 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Suppression des accents d'une chaine
+
+Source: http://wikipython.flibuste.net/moin.py/JouerAvecUnicode#head-1213938516c633958921591439c33d202244e2f4
+"""
+
+_reptable = {}
+
+
+def _fill_reptable():
+    _corresp = [
+        (
+            u"A",
+            [0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x0100, 0x0102, 0x0104],
+        ),
+        (u"AE", [0x00C6]),
+        (
+            u"a",
+            [0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x0101, 0x0103, 0x0105],
+        ),
+        (u"ae", [0x00E6]),
+        (u"C", [0x00C7, 0x0106, 0x0108, 0x010A, 0x010C]),
+        (u"c", [0x00E7, 0x0107, 0x0109, 0x010B, 0x010D]),
+        (u"D", [0x00D0, 0x010E, 0x0110]),
+        (u"d", [0x00F0, 0x010F, 0x0111]),
+        (
+            u"E",
+            [0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x0112, 0x0114, 0x0116, 0x0118, 0x011A],
+        ),
+        (
+            u"e",
+            [
+                0x00E8,
+                0xE9,
+                0x00E9,
+                0x00EA,
+                0xEB,
+                0x00EB,
+                0x0113,
+                0x0115,
+                0x0117,
+                0x0119,
+                0x011B,
+            ],
+        ),
+        (u"G", [0x011C, 0x011E, 0x0120, 0x0122]),
+        (u"g", [0x011D, 0x011F, 0x0121, 0x0123]),
+        (u"H", [0x0124, 0x0126]),
+        (u"h", [0x0125, 0x0127]),
+        (
+            u"I",
+            [0x00CC, 0x00CD, 0x00CE, 0x00CF, 0x0128, 0x012A, 0x012C, 0x012E, 0x0130],
+        ),
+        (
+            u"i",
+            [0x00EC, 0x00ED, 0x00EE, 0x00EF, 0x0129, 0x012B, 0x012D, 0x012F, 0x0131],
+        ),
+        (u"IJ", [0x0132]),
+        (u"ij", [0x0133]),
+        (u"J", [0x0134]),
+        (u"j", [0x0135]),
+        (u"K", [0x0136]),
+        (u"k", [0x0137, 0x0138]),
+        (u"L", [0x0139, 0x013B, 0x013D, 0x013F, 0x0141]),
+        (u"l", [0x013A, 0x013C, 0x013E, 0x0140, 0x0142]),
+        (u"N", [0x00D1, 0x0143, 0x0145, 0x0147, 0x014A]),
+        (u"n", [0x00F1, 0x0144, 0x0146, 0x0148, 0x0149, 0x014B]),
+        (
+            u"O",
+            [0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D8, 0x014C, 0x014E, 0x0150],
+        ),
+        (
+            u"o",
+            [0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F8, 0x014D, 0x014F, 0x0151],
+        ),
+        (u"OE", [0x0152]),
+        (u"oe", [0x0153]),
+        (u"R", [0x0154, 0x0156, 0x0158]),
+        (u"r", [0x0155, 0x0157, 0x0159]),
+        (u"S", [0x015A, 0x015C, 0x015E, 0x0160]),
+        (u"s", [0x015B, 0x015D, 0x015F, 0x01610, 0x017F, 0x0218]),
+        (u"T", [0x0162, 0x0164, 0x0166]),
+        (u"t", [0x0163, 0x0165, 0x0167]),
+        (
+            u"U",
+            [
+                0x00D9,
+                0x00DA,
+                0x00DB,
+                0x00DC,
+                0x0168,
+                0x016A,
+                0x016C,
+                0x016E,
+                0x0170,
+                0x172,
+            ],
+        ),
+        (
+            u"u",
+            [
+                0x00F9,
+                0x00FA,
+                0x00FB,
+                0x00FC,
+                0x0169,
+                0x016B,
+                0x016D,
+                0x016F,
+                0x0171,
+                0xB5,
+            ],
+        ),
+        (u"W", [0x0174]),
+        (u"w", [0x0175]),
+        (u"Y", [0x00DD, 0x0176, 0x0178]),
+        (u"y", [0x00FD, 0x00FF, 0x0177]),
+        (u"Z", [0x0179, 0x017B, 0x017D]),
+        (u"z", [0x017A, 0x017C, 0x017E]),
+        (
+            u"",
+            [
+                0x80,
+                0x81,
+                0x82,
+                0x83,
+                0x84,
+                0x85,
+                0x86,
+                0x87,
+                0x88,
+                0x89,
+                0x8A,
+                0x8B,
+                0x8C,
+                0x8D,
+                0x8E,
+                0x8F,
+                0x90,
+                0x91,
+                0x92,
+                0x93,
+                0x94,
+                0x95,
+                0x96,
+                0x97,
+                0x98,
+                0x99,
+                0x9A,
+                0x9B,
+                0x9C,
+                0x9D,
+                0x9E,
+                0x9F,
+            ],
+        ),  # misc controls
+        (u" ", [0x00A0]),  # &nbsp
+        (u"!", [0xA1]),  # &iexcl;
+        (u"c", [0xA2]),  # cent
+        (u"L", [0xA3]),  # pound
+        (u"o", [0xA4]),  # currency symbol
+        (u"Y", [0xA5]),  # yen
+        (u"|", [0xA6]),  # Broken Bar	&brvbar;
+        (u"S", [0xA7]),  # section
+        (u"", [0xA8]),  # diaeresis ¨
+        (u"", [0xA9]),  # copyright
+        (u'"', [0xAB, 0xBA]),  # &laquo;, &raquo; <<, >>
+        (u" ", [0xAC]),  # Math Not Sign
+        (u"", [0xAD]),  # DashPunctuation
+        (u"(r)", [0xAE]),  # registred
+        (u"-", [0xAF]),  # macron
+        (u"", [0xB0]),  # degre
+        (u"+-", [0xB1]),  # +-
+        (u"2", [0x00B2, 0xB2]),  # deux exposant
+        (u"3", [0xB3]),  # 3 exposant
+        (u".", [0xB7]),  # &middot;,
+        (u"1/4", [0xBC]),  # 1/4
+        (u"1/2", [0xBD]),  # 1/2
+        (u"3/4", [0xBE]),  # 3/4
+        (u"e", [0x20AC]),  # euro
+        (u"--", [0x2013]),  # EN DASH
+        (u"'", [0x2018, 0x2019, 0x201A]),  # LEFT, RIGHT SINGLE QUOTATION MARK
+        (u" ", [0x2020]),  # dagger
+    ]
+    global _reptable
+    for repchar, codes in _corresp:
+        for code in codes:
+            _reptable[code] = repchar
+
+
+_fill_reptable()
+
+
+def suppression_diacritics(s):
+    """Suppression des accents et autres marques.
+
+    @param s: le texte à nettoyer.
+    @type s: str ou unicode
+    @return: le texte nettoyé de ses marques diacritiques.
+    @rtype: unicode
+    """
+    if isinstance(s, str):
+        s = unicode(s, "utf8", "replace")
+    return s.translate(_reptable)
diff --git a/TODO b/TODO
new file mode 100644
index 0000000000000000000000000000000000000000..4041532f07cd29b446ebd2a4c6e90932b7185044
--- /dev/null
+++ b/TODO
@@ -0,0 +1,238 @@
+
+  NOTES EN VRAC / Brouillon / Trucs obsoletes
+
+
+#do_moduleimpl_list\(\{"([a-z_]*)"\s*:\s*(.*)\}\)
+#do_moduleimpl_list( $1 = $2 )
+
+#do_moduleimpl_list\([\s\n]*args[\s\n]*=[\s\n]*\{"([a-z_]*)"[\s\n]*:[\s\n]*(.*)[\s\n]*\}[\s\n]*\)
+
+Upgrade JavaScript
+ - jquery-ui-1.12.1  introduit un problème d'affichage de la barre de menu.
+    Il faudrait la revoir entièrement pour upgrader.
+    On reste donc à jquery-ui-1.10.4.custom
+    Or cette version est incompatible avec jQuery 3 (messages d'erreur dans la console)
+    On reste donc avec jQuery 1.12.14
+
+
+Suivi des requêtes utilisateurs:
+ table sql: id, ip, authuser, request
+
+
+* Optim:
+porcodeb4, avant memorisation des moy_ue:
+S1 SEM14133 cold start: min 9s, max 12s, avg > 11s
+            inval (add note): 1.33s (pas de recalcul des autres)
+            inval (add abs) : min8s, max 12s (recalcule tout :-()
+LP SEM14946 cold start: 0.7s - 0.86s
+
+
+
+----------------- LISTE OBSOLETE (très ancienne, à trier) -----------------------
+BUGS
+----
+
+ - formsemestre_inscription_with_modules
+     si inscription 'un etud deja inscrit, IntegrityError
+
+FEATURES REQUESTS
+-----------------
+
+* Bulletins:
+  . logos IUT et Univ sur bull PDF
+  . nom departement: nom abbrégé (CJ) ou complet (Carrière Juridiques)
+  . bulletin: deplacer la barre indicateur (cf OLDGEA S2: gêne)
+  . bulletin: click nom titre -> ficheEtud
+
+  . formsemestre_pagebulletin_dialog: marges en mm: accepter "2,5" et "2.5"
+    et valider correctement le form !
+
+* Jury
+  . recapcomplet: revenir avec qq lignes au dessus de l'étudiant en cours
+
+
+* Divers
+  . formsemestre_editwithmodules: confirmer suppression modules
+      (et pour l'instant impossible si evaluations dans le module)
+
+* Modules et UE optionnelles:
+  . UE capitalisées: donc dispense possible dans semestre redoublé.
+      traitable en n'inscrivant pas l'etudiant au modules
+      de cette UE: faire interface utilisateur
+
+  . page pour inscription d'un etudiant a un module
+  . page pour visualiser les modules auquel un etudiant est inscrit,
+    et le desinscrire si besoin.
+
+  . ficheEtud  indiquer si inscrit au module sport
+
+* Absences
+  . EtatAbsences : verifier dates (en JS)
+  . Listes absences pdf et listes groupes pdf + emargements (cf mail Nathalie)
+  . absences par demi-journées sur EtatAbsencesDate (? à vérifier)
+  . formChoixSemestreGroupe: utilisé par Absences/index_html
+       a améliorer
+
+
+* Notes et évaluations:
+  . Exception "Not an OLE file": generer page erreur plus explicite
+  . Dates evaluation: utiliser JS pour calendrier
+  . Saisie des notes: si une note invalide, l'indiquer dans le listing (JS ?)
+  . et/ou: notes invalides: afficher les noms des etudiants concernes
+    dans le message d'erreur.
+  . upload excel: message erreur peu explicite:
+          * Feuille "Saisie notes", 17 lignes
+          * Erreur: la feuille contient 1 notes invalides
+          * Notes invalides pour les id: ['10500494']
+          (pas de notes modifiées)
+          Notes chargées. <<< CONTRADICTOIRE !!
+
+  . recap complet semestre:
+       Options:
+           - choix groupes
+           - critère de tri (moy ou alphab)
+           - nb de chiffres a afficher
+
+       + definir des "catégories" d'évaluations (eg "théorie","pratique")
+         afin de n'afficher que des moyennes "de catégorie" dans
+         le bulletin.
+
+  . liste des absents à une eval et croisement avec BD absences
+
+  . notes_evaluation_listenotes
+    - afficher groupes, moyenne, #inscrits, #absents, #manquantes dans l'en-tete.
+    - lien vers modif notes (selon role)
+
+  . Export excel des notes d'evaluation: indiquer date, et autres infos en haut.
+  . Génération PDF listes notes
+  . Page recap notes moyennes par groupes (choisir type de groupe?)
+
+  . (GEA) edition tableau notes avec tous les evals d'un module
+        (comme notes_evaluation_listenotes mais avec tt les evals)
+
+
+* Non prioritaire:
+ . optimiser scolar_news_summary
+ . recapitulatif des "nouvelles"
+     - dernieres notes
+     - changement de statuts (demissions,inscriptions)
+     - annotations
+     - entreprises
+
+ . notes_table: pouvoir changer decision sans invalider tout le cache
+ . navigation: utiliser Session pour montrer historique pages vues ?
+
+
+
+------------------------------------------------------------------------
+
+
+A faire:
+ - fiche etud: code dec jury sur ligne 1
+               si ancien, indiquer autorisation inscription sous le parcours
+
+ - saisie notes: undo
+ - saisie notes: validation
+- ticket #18:
+UE capitalisées: donc dispense possible dans semestre redoublé. Traitable en n'inscrivant pas l'etudiant aux modules de cette UE: faire interface utilisateur.
+
+Prévoir d'entrer une UE capitalisée avec sa note, date d'obtention et un commentaire. Coupler avec la désincription aux modules (si l'étudiant a été inscrit avec ses condisciples).
+
+
+ - Ticket #4: Afin d'éviter les doublons, vérifier qu'il n'existe pas d'homonyme proche lors de la création manuelle d'un étudiant.  (confirmé en ScoDoc 6, vérifier aussi les imports Excel)
+
+ - Ticket #74: Il est possible d'inscrire un étudiant sans prénom par un import excel !!!
+
+ - Ticket #64: saisir les absences pour la promo entiere (et pas par groupe). Des fois, je fais signer une feuille de presence en amphi a partir de la liste de tous les etudiants. Ensuite pour reporter les absents par groupe, c'est galere.
+
+ - Ticket #62: Lors des exports Excel, le format des cellules n'est pas reconnu comme numérique sous Windows (pas de problèmes avec Macintosh et Linux).
+
+A confirmer et corriger.
+
+ - Ticket #75: On peut modifier une décision de jury (et les autorisations de passage associées), mais pas la supprimer purement et simplement.
+Ajoute ce choix dans les "décisions manuelles".
+
+ - Ticket #37: Page recap notes moyennes par groupes
+Construire une page avec les moyennes dans chaque UE ou module par groupe d'étudiants.
+Et aussi pourquoi pas ventiler par type de bac, sexe, parcours (nombre de semestre de parcours) ?
+redemandé par CJ: à faire avant mai 2008 !
+
+ - Ticket #75: Synchro Apogée: choisir les etudiants
+Sur la page de syncho Apogée (formsemestre_synchro_etuds), on peut choisir (cocher) les étudiants Apogée à importer. mais on ne peut pas le faire s'ils sont déjà dans ScoDoc: il faudrait ajouter des checkboxes dans toutes les listes.
+
+ - Ticket #9: Format des valeurs de marges des bulletins.
+formsemestre_pagebulletin_dialog: marges en mm: accepter "2,5" et "2.5" et valider correctement le form !
+
+ - Ticket #17: Suppression modules dans semestres
+formsemestre_editwithmodules: confirmer suppression modules
+
+ - Ticket #29: changer le stoquage des photos, garder une version HD.
+
+ - bencher NotesTable sans calcul de moyennes. Etudier un cache des moyennes de modules.
+ - listes d'utilisateurs (modules): remplacer menus par champs texte + completions javascript
+ - documenter archives sur Wiki
+ - verifier paquet Debian pour font pdf (reportab: helvetica ... plante si font indisponible)
+ - chercher comment obtenir une page d'erreur correcte pour les pages POST
+    (eg: si le font n'existe pas, archive semestre echoue sans page d'erreur)
+    ? je ne crois pas que le POST soit en cause. HTTP status=500
+     ne se produit pas avec Safari
+ - essayer avec IE / Win98
+ - faire apparaitre les diplômés sur le graphe des parcours
+ - démission: formulaire: vérifier que la date est bien dans le semestre
+
+ + graphe parcours: aligner en colonnes selon les dates (de fin), placer les diplomes
+   dans la même colone que le semestre terminal.
+
+ - modif gestion utilisateurs (donner droits en fct du dept. d'appartenance, bug #57)
+ - modif form def. utilisateur (dept appartenance)
+ - utilisateurs: source externe
+ - archivage des semestres
+
+
+        o-------------------------------------o
+
+* Nouvelle gestion utilisateurs:
+  objectif: dissocier l'authentification de la notion "d'enseignant"
+  On a une source externe "d'utilisateurs" (annuaire LDAP ou base SQL)
+  qui permet seulement de:
+     - authentifier un utilisateur (login, passwd)
+     - lister un utilisateur: login => firstname, lastname, email
+     - lister les utilisateurs
+
+  et une base interne ScoDoc "d'acteurs" (enseignants, administratifs).
+  Chaque acteur est défini par:
+     - actor_id, firstname, lastname
+       date_creation, date_expiration,
+       roles, departement,
+       email (+flag indiquant s'il faut utiliser ce mail ou celui de
+       l'utilisateur ?)
+       state (on, off) (pour desactiver avant expiration ?)
+       user_id (login)  => lien avec base utilisateur
+
+  On offrira une source d'utilisateurs SQL (base partagée par tous les dept.
+  d'une instance ScoDoc), mais dans la plupart des cas les gens utiliseront
+  un annuaire LDAP.
+
+  La base d'acteurs remplace ScoUsers. Les objets ScoDoc (semestres,
+  modules etc) font référence à des acteurs (eg responsable_id est un actor_id).
+
+  Le lien entre les deux ?
+  Loger un utilisateur => authentification utilisateur + association d'un acteur
+  Cela doit se faire au niveau d'un UserFolder Zope, pour avoir les
+  bons rôles et le contrôle d'accès adéquat.
+  (Il faut donc coder notre propre UserFolder).
+  On ne peut associer qu'un acteur à l'état 'on' et non expiré.
+
+  Opérations ScoDoc:
+   - paramétrage: choisir et paramétrer source utilisateurs
+   - ajouter utilisateur: choisir un utilisateur dans la liste
+     et lui associer un nouvel acteur (choix des rôles, des dates)
+     + éventuellement: synchro d'un ensemble d'utilisateurs, basé sur
+     une requête (eg LDAP) précise (quelle interface utilisateur proposer ?)
+
+   - régulièrement (cron) aviser quelqu'un (le chef) de l'expiration des acteurs.
+   - changer etat d'un acteur (on/off)
+
+
+        o-------------------------------------o
+
diff --git a/TrivialFormulator.py b/TrivialFormulator.py
new file mode 100644
index 0000000000000000000000000000000000000000..788d95da1c661bfa95152f01c173d2d44461cc1e
--- /dev/null
+++ b/TrivialFormulator.py
@@ -0,0 +1,769 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+
+"""Simple form generator/validator
+
+   E. Viennet 2005 - 2008
+
+   v 1.2
+"""
+
+from types import *
+
+
+def TrivialFormulator(
+    form_url,
+    values,
+    formdescription=(),
+    initvalues={},
+    method="post",
+    enctype=None,
+    submitlabel="OK",
+    name=None,
+    formid="tf",
+    cssclass="",
+    cancelbutton=None,
+    submitbutton=True,
+    submitbuttonattributes=[],
+    top_buttons=False,  # place buttons at top of form
+    bottom_buttons=True,  # buttons after form
+    html_foot_markup="",
+    readonly=False,
+    is_submitted=False,
+):
+    """
+    form_url : URL for this form
+    initvalues : dict giving default values
+    values : dict with all HTML form variables (may start empty)
+    is_submitted:  handle form as if already submitted
+
+    Returns (status, HTML form, values)
+         status = 0 (html to display),
+                  1 (ok, validated values in "values")
+                  -1 cancel (if cancelbutton specified)
+         HTML form: html string (form to insert in your web page)
+         values: None or, when the form is submitted and correctly filled,
+                 a dictionnary with the requeted values.                 
+    formdescription: sequence [ (field, description), ... ]
+        where description is a dict with following (optional) keys:
+          default    : default value for this field ('')
+          title      : text titre (default to field name)
+          allow_null : if true, field can be left empty (default true)
+          type       : 'string', 'int', 'float' (default to string), 'list' (only for hidden)
+          readonly   : default False. if True, no form element, display current value.
+          convert_numbers: covert int and float values (from string)
+          allowed_values : list of possible values (default: any value)
+          validator : function validating the field (called with (value,field)).
+          min_value : minimum value (for floats and ints)
+          max_value : maximum value (for floats and ints)
+          explanation: text string to display next the input widget
+          title_buble: help bubble on field title (needs bubble.js or equivalent)
+          comment : comment, showed under input widget
+          withcheckbox: if true, place a checkbox at the left of the input
+                        elem. Checked items will be returned in 'tf-checked'
+          attributes: a liste of strings to put in the HTML form element
+          template: HTML template for element 
+          HTML elements:
+             input_type : 'text', 'textarea', 'password',
+                          'radio', 'menu', 'checkbox',
+                          'hidden', 'separator', 'file', 'date', 'boolcheckbox',
+                          'text_suggest'
+                         (default text)
+             size : text field width
+             rows, cols: textarea geometry
+             labels : labels for radio or menu lists (associated to allowed_values)
+             vertical: for checkbox; if true, vertical layout
+             disabled_items: for checkbox, dict such that disabled_items[i] true if disabled checkbox
+          To use text_suggest elements, one must:
+            - specify options in text_suggest_options (a dict)
+            - HTML page must load JS AutoSuggest.js and CSS autosuggest_inquisitor.css
+            - bodyOnLoad must call JS function init_tf_form(formid)
+    """
+    method = method.lower()
+    if method == "get":
+        enctype = None
+    t = TF(
+        form_url,
+        values,
+        formdescription,
+        initvalues,
+        method,
+        enctype,
+        submitlabel,
+        name,
+        formid,
+        cssclass,
+        cancelbutton=cancelbutton,
+        submitbutton=submitbutton,
+        submitbuttonattributes=submitbuttonattributes,
+        top_buttons=top_buttons,
+        bottom_buttons=bottom_buttons,
+        html_foot_markup=html_foot_markup,
+        readonly=readonly,
+        is_submitted=is_submitted,
+    )
+    form = t.getform()
+    if t.canceled():
+        res = -1
+    elif t.submitted() and t.result:
+        res = 1
+    else:
+        res = 0
+    return res, form, t.result
+
+
+class TF:
+    def __init__(
+        self,
+        form_url,
+        values,
+        formdescription=[],
+        initvalues={},
+        method="POST",
+        enctype=None,
+        submitlabel="OK",
+        name=None,
+        formid="tf",
+        cssclass="",
+        cancelbutton=None,
+        submitbutton=True,
+        submitbuttonattributes=[],
+        top_buttons=False,  # place buttons at top of form
+        bottom_buttons=True,  # buttons after form
+        html_foot_markup="",  # html snippet put at the end, just after the table
+        readonly=False,
+        is_submitted=False,
+    ):
+        self.form_url = form_url
+        self.values = values
+        self.formdescription = list(formdescription)
+        self.initvalues = initvalues
+        self.method = method
+        self.enctype = enctype
+        self.submitlabel = submitlabel
+        if name:
+            self.name = name
+        else:
+            self.name = formid  # 'tf'
+        self.formid = formid
+        self.cssclass = cssclass
+        self.cancelbutton = cancelbutton
+        self.submitbutton = submitbutton
+        self.submitbuttonattributes = submitbuttonattributes
+        self.top_buttons = top_buttons
+        self.bottom_buttons = bottom_buttons
+        self.html_foot_markup = html_foot_markup
+        self.readonly = readonly
+        self.result = None
+        self.is_submitted = is_submitted
+        if readonly:
+            self.top_buttons = self.bottom_buttons = False
+            self.cssclass += " readonly"
+
+    def submitted(self):
+        "true if form has been submitted"
+        if self.is_submitted:
+            return True
+        return self.values.get("%s-submitted" % self.formid, False)
+
+    def canceled(self):
+        "true if form has been canceled"
+        return self.values.get("%s_cancel" % self.formid, False)
+
+    def getform(self):
+        "return HTML form"
+        R = []
+        msg = None
+        self.setdefaultvalues()
+        if self.submitted() and not self.readonly:
+            msg = self.checkvalues()
+        # display error message
+        R.append(tf_error_message(msg))
+        # form or view
+        if self.readonly:
+            R = R + self._ReadOnlyVersion(self.formdescription)
+        else:
+            R = R + self._GenForm()
+        #
+        return "\n".join(R)
+
+    __str__ = getform
+    __repr__ = getform
+
+    def setdefaultvalues(self):
+        "set default values and convert numbers to strings"
+        for (field, descr) in self.formdescription:
+            # special case for boolcheckbox
+            if descr.get("input_type", None) == "boolcheckbox" and self.submitted():
+                if not self.values.has_key(field):
+                    self.values[field] = 0
+                else:
+                    self.values[field] = 1
+            if not self.values.has_key(field):
+                if descr.has_key("default"):  # first: default in form description
+                    self.values[field] = descr["default"]
+                else:  # then: use initvalues dict
+                    self.values[field] = self.initvalues.get(field, "")
+                if self.values[field] == None:
+                    self.values[field] = ""
+
+            # convert numbers
+            if type(self.values[field]) == type(1) or type(self.values[field]) == type(
+                1.0
+            ):
+                self.values[field] = str(self.values[field])
+        #
+        if not self.values.has_key("tf-checked"):
+            if self.submitted():
+                # si rien n'est coché, tf-checked n'existe plus dans la reponse
+                self.values["tf-checked"] = []
+            else:
+                self.values["tf-checked"] = self.initvalues.get("tf-checked", [])
+        self.values["tf-checked"] = [str(x) for x in self.values["tf-checked"]]
+
+    def checkvalues(self):
+        "check values. Store .result and returns msg"
+        ok = 1
+        msg = []
+        for (field, descr) in self.formdescription:
+            val = self.values[field]
+            # do not check "unckecked" items
+            if descr.get("withcheckbox", False):
+                if not field in self.values["tf-checked"]:
+                    continue
+            # null values
+            allow_null = descr.get("allow_null", True)
+            if not allow_null:
+                if val == "" or val == None:
+                    msg.append(
+                        "Le champ '%s' doit être renseigné" % descr.get("title", field)
+                    )
+                    ok = 0
+            # type
+            typ = descr.get("type", "string")
+            if val != "" and val != None:
+                # check only non-null values
+                if typ[:3] == "int":
+                    try:
+                        val = int(val)
+                        self.values[field] = val
+                    except:
+                        msg.append(
+                            "La valeur du champ '%s' doit être un nombre entier" % field
+                        )
+                        ok = 0
+                elif typ == "float" or typ == "real":
+                    self.values[field] = self.values[field].replace(",", ".")
+                    try:
+                        val = float(val.replace(",", "."))  # allow ,
+                        self.values[field] = val
+                    except:
+                        msg.append(
+                            "La valeur du champ '%s' doit être un nombre" % field
+                        )
+                        ok = 0
+                if typ[:3] == "int" or typ == "float" or typ == "real":
+                    if descr.has_key("min_value") and val < descr["min_value"]:
+                        msg.append(
+                            "La valeur (%d) du champ '%s' est trop petite (min=%s)"
+                            % (val, field, descr["min_value"])
+                        )
+                        ok = 0
+
+                    if descr.has_key("max_value") and val > descr["max_value"]:
+                        msg.append(
+                            "La valeur (%s) du champ '%s' est trop grande (max=%s)"
+                            % (val, field, descr["max_value"])
+                        )
+                        ok = 0
+
+            # allowed values
+            if descr.has_key("allowed_values"):
+                if descr.get("input_type", None) == "checkbox":
+                    # for checkboxes, val is a list
+                    for v in val:
+                        if not v in descr["allowed_values"]:
+                            msg.append(
+                                "valeur invalide (%s) pour le champ '%s'" % (val, field)
+                            )
+                            ok = 0
+                elif descr.get("input_type", None) == "boolcheckbox":
+                    pass
+                elif not val in descr["allowed_values"]:
+                    msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
+                    ok = 0
+            if descr.has_key("validator"):
+                if not descr["validator"](val, field):
+                    msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
+                    ok = 0
+            # boolean checkbox
+            if descr.get("input_type", None) == "boolcheckbox":
+                if int(val):
+                    self.values[field] = 1
+                else:
+                    self.values[field] = 0
+                # open('/tmp/toto','a').write('checkvalues: val=%s (%s) values[%s] = %s\n' % (val, type(val), field, self.values[field]))
+            if descr.get("convert_numbers", False):
+                if typ[:3] == "int":
+                    self.values[field] = int(self.values[field])
+                elif typ == "float" or typ == "real":
+                    self.values[field] = float(self.values[field].replace(",", "."))
+        if ok:
+            self.result = self.values
+        else:
+            self.result = None
+        return msg
+
+    def _GenForm(self, method="", enctype=None, form_url=""):
+        values = self.values
+        add_no_enter_js = False  # add JS function to prevent 'enter' -> submit
+        # form template
+
+        # default template for each input element
+        itemtemplate = """<tr%(item_dom_attr)s>
+        <td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td>
+        </tr>
+        """
+        hiddenitemtemplate = "%(elem)s"
+        separatortemplate = '<tr%(item_dom_attr)s><td colspan="2">%(label)s</td></tr>'
+        # ---- build form
+        buttons_markup = ""
+        if self.submitbutton:
+            buttons_markup += (
+                '<input type="submit" name="%s_submit" id="%s_submit" value="%s" %s/>'
+                % (
+                    self.formid,
+                    self.formid,
+                    self.submitlabel,
+                    " ".join(self.submitbuttonattributes),
+                )
+            )
+        if self.cancelbutton:
+            buttons_markup += (
+                ' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s"/>'
+                % (self.formid, self.formid, self.cancelbutton)
+            )
+
+        R = []
+        suggest_js = []
+        if self.enctype is None:
+            if self.method == "post":
+                enctype = "multipart/form-data"
+            else:
+                enctype = "application/x-www-form-urlencoded"
+        if self.cssclass:
+            klass = ' class="%s"' % self.cssclass
+        else:
+            klass = ""
+        name = self.name
+        R.append(
+            '<form action="%s" method="%s" id="%s" enctype="%s" name="%s" %s>'
+            % (self.form_url, self.method, self.formid, enctype, name, klass)
+        )
+        R.append('<input type="hidden" name="%s-submitted" value="1"/>' % self.formid)
+        if self.top_buttons:
+            R.append(buttons_markup + "<p></p>")
+        R.append('<table class="tf">')
+        idx = 0
+        for idx in range(len(self.formdescription)):
+            (field, descr) = self.formdescription[idx]
+            nextitemname = None
+            if idx < len(self.formdescription) - 2:
+                nextitemname = self.formdescription[idx + 1][0]
+            if descr.get("readonly", False):
+                R.append(self._ReadOnlyElement(field, descr))
+                continue
+            wid = self.name + "_" + field
+            size = descr.get("size", 12)
+            rows = descr.get("rows", 5)
+            cols = descr.get("cols", 60)
+            title = descr.get("title", field.capitalize())
+            title_bubble = descr.get("title_bubble", None)
+            withcheckbox = descr.get("withcheckbox", False)
+            input_type = descr.get("input_type", "text")
+            item_dom_id = descr.get("dom_id", "")
+            if item_dom_id:
+                item_dom_attr = ' id="%s"' % item_dom_id
+            else:
+                item_dom_attr = ""
+            # choix du template
+            etempl = descr.get("template", None)
+            if etempl is None:
+                if input_type == "hidden":
+                    etempl = hiddenitemtemplate
+                elif input_type == "separator":
+                    etempl = separatortemplate
+                    R.append(etempl % {"label": title, "item_dom_attr": item_dom_attr})
+                    continue
+                else:
+                    etempl = itemtemplate
+            lab = []
+            lem = []
+            if withcheckbox and input_type != "hidden":
+                if field in values["tf-checked"]:
+                    checked = 'checked="checked"'
+                else:
+                    checked = ""
+                lab.append(
+                    '<input type="checkbox" name="%s:list" value="%s" onclick="tf_enable_elem(this)" %s/>'
+                    % ("tf-checked", field, checked)
+                )
+            if title_bubble:
+                lab.append(
+                    '<a class="discretelink" href="" title="%s">%s</a>'
+                    % (title_bubble, title)
+                )
+            else:
+                lab.append(title)
+            #
+            attribs = " ".join(descr.get("attributes", []))
+            if (
+                withcheckbox and not checked
+            ) or not descr.get(  # desactive les element non coches:
+                "enabled", True
+            ):
+                attribs += ' disabled="true"'
+            #
+            if input_type == "text":
+                lem.append(
+                    '<input type="text" name="%s" size="%d" id="%s" %s'
+                    % (field, size, wid, attribs)
+                )
+                if descr.get("return_focus_next", False):  # and nextitemname:
+                    # JS code to focus on next element on 'enter' key
+                    # ceci ne marche que pour desactiver enter sous IE (pas Firefox)
+                    # lem.append('''onKeyDown="if(event.keyCode==13){
+                    # event.cancelBubble = true; event.returnValue = false;}"''')
+                    lem.append('onkeypress="return enter_focus_next(this, event);"')
+                    add_no_enter_js = True
+                #                    lem.append('onchange="document.%s.%s.focus()"'%(name,nextitemname))
+                #                    lem.append('onblur="document.%s.%s.focus()"'%(name,nextitemname))
+                lem.append(('value="%(' + field + ')s" />') % values)
+            elif input_type == "password":
+                lem.append(
+                    '<input type="password" name="%s" id="%s" size="%d" %s'
+                    % (field, wid, size, attribs)
+                )
+                lem.append(('value="%(' + field + ')s" />') % values)
+            elif input_type == "radio":
+                labels = descr.get("labels", descr["allowed_values"])
+                for i in range(len(labels)):
+                    if descr["allowed_values"][i] == values[field]:
+                        checked = 'checked="checked"'
+                    else:
+                        checked = ""
+                    lem.append(
+                        '<input type="radio" name="%s" value="%s" %s %s>%s</input>'
+                        % (
+                            field,
+                            descr["allowed_values"][i],
+                            checked,
+                            attribs,
+                            labels[i],
+                        )
+                    )
+            elif input_type == "menu":
+                lem.append('<select name="%s" id="%s" %s>' % (field, wid, attribs))
+                labels = descr.get("labels", descr["allowed_values"])
+                for i in range(len(labels)):
+                    if str(descr["allowed_values"][i]) == str(values[field]):
+                        selected = "selected"
+                    else:
+                        selected = ""
+                    lem.append(
+                        '<option value="%s" %s>%s</option>'
+                        % (descr["allowed_values"][i], selected, labels[i])
+                    )
+                lem.append("</select>")
+            elif input_type == "checkbox" or input_type == "boolcheckbox":
+                if input_type == "checkbox":
+                    labels = descr.get("labels", descr["allowed_values"])
+                else:  # boolcheckbox
+                    labels = [""]
+                    descr["allowed_values"] = ["0", "1"]
+                vertical = descr.get("vertical", False)
+                disabled_items = descr.get("disabled_items", {})
+                if vertical:
+                    lem.append("<table>")
+                for i in range(len(labels)):
+                    if input_type == "checkbox":
+                        # from notes_log import log # debug only
+                        # log('checkbox: values[%s] = "%s"' % (field,repr(values[field]) ))
+                        # log("descr['allowed_values'][%s] = '%s'" % (i, repr(descr['allowed_values'][i])))
+                        if descr["allowed_values"][i] in values[field]:
+                            checked = 'checked="checked"'
+                        else:
+                            checked = ""
+                    else:  # boolcheckbox
+                        # open('/tmp/toto','a').write('GenForm: values[%s] = %s (%s)\n' % (field, values[field], type(values[field])))
+                        try:
+                            v = int(values[field])
+                        except:
+                            v = 0
+                        if v:
+                            checked = 'checked="checked"'
+                        else:
+                            checked = ""
+                    if vertical:
+                        lem.append("<tr><td>")
+                    if disabled_items.get(i, False):
+                        disab = 'disabled="1"'
+                        ilab = (
+                            '<span class="tf-label-disabled">'
+                            + labels[i]
+                            + "</span> <em>(non modifiable)</em>"
+                        )
+                    else:
+                        disab = ""
+                        ilab = "<span>" + labels[i] + "</span>"
+                    lem.append(
+                        '<input type="checkbox" name="%s:list" value="%s" %s %s %s>%s</input>'
+                        % (
+                            field,
+                            descr["allowed_values"][i],
+                            attribs,
+                            disab,
+                            checked,
+                            ilab,
+                        )
+                    )
+                    if vertical:
+                        lem.append("</tr></td>")
+                if vertical:
+                    lem.append("</table>")
+            elif input_type == "textarea":
+                lem.append(
+                    '<textarea name="%s" id="%s" rows="%d" cols="%d" %s>%s</textarea>'
+                    % (field, wid, rows, cols, attribs, values[field])
+                )
+            elif input_type == "hidden":
+                if descr.get("type", "") == "list":
+                    for v in values[field]:
+                        lem.append(
+                            '<input type="hidden" name="%s:list" value="%s" %s />'
+                            % (field, v, attribs)
+                        )
+                else:
+                    lem.append(
+                        '<input type="hidden" name="%s" id="%s" value="%s" %s />'
+                        % (field, wid, values[field], attribs)
+                    )
+            elif input_type == "separator":
+                pass
+            elif input_type == "file":
+                lem.append(
+                    '<input type="file" name="%s" size="%s" value="%s" %s/>'
+                    % (field, size, values[field], attribs)
+                )
+            elif input_type == "date":  # JavaScript widget for date input
+                lem.append(
+                    '<input type="text" name="%s" size="10" value="%s" class="datepicker"/>'
+                    % (field, values[field])
+                )
+            elif input_type == "text_suggest":
+                lem.append(
+                    '<input type="text" name="%s" id="%s" size="%d" %s'
+                    % (field, field, size, attribs)
+                )
+                lem.append(('value="%(' + field + ')s" />') % values)
+                suggest_js.append(
+                    """var %s_opts = %s;
+                    var %s_as = new bsn.AutoSuggest('%s', %s_opts);
+                    """
+                    % (
+                        field,
+                        dict2js(descr.get("text_suggest_options", {})),
+                        field,
+                        field,
+                        field,
+                    )
+                )
+            else:
+                raise ValueError("unkown input_type for form (%s)!" % input_type)
+            explanation = descr.get("explanation", "")
+            if explanation:
+                lem.append('<span class="tf-explanation">%s</span>' % explanation)
+            comment = descr.get("comment", "")
+            if comment:
+                lem.append('<br/><span class="tf-comment">%s</span>' % comment)
+            R.append(
+                etempl
+                % {
+                    "label": "\n".join(lab),
+                    "elem": "\n".join(lem),
+                    "item_dom_attr": item_dom_attr,
+                }
+            )
+        R.append("</table>")
+
+        R.append(self.html_foot_markup)
+
+        if self.bottom_buttons:
+            R.append("<br/>" + buttons_markup)
+
+        if add_no_enter_js:
+            R.append(
+                """<script type="text/javascript">
+            function enter_focus_next (elem, event) {
+        var cod = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
+        var enter = false;
+        if (event.keyCode == 13)
+            enter = true;
+        if (event.which == 13)
+            enter = true;
+        if (event.charCode == 13)
+            enter = true;
+        if (enter) {
+            var focused = false;
+            var i;
+            for (i = 0; i < elem.form.elements.length; i++)
+                if (elem == elem.form.elements[i])
+                    break;
+            i = i + 1;
+            while (i < elem.form.elements.length) {
+                if ((elem.form.elements[i].type == "text") 
+                    && (!(elem.form.elements[i].disabled))
+                    && ($(elem.form.elements[i]).is(':visible')))
+                {
+                    elem.form.elements[i].focus();
+                    focused = true;
+                    break;
+                }
+                i = i + 1;
+            }
+            if (!focused) {
+                elem.blur();
+            }
+            return false;
+        } 
+        else
+            return true;
+    }</script>
+            """
+            )  # enter_focus_next, ne focus que les champs text
+        if suggest_js:
+            # nota: formid is currently ignored
+            # => only one form with text_suggest field on a page.
+            R.append(
+                """<script type="text/javascript">
+            function init_tf_form(formid) {                
+                %s
+            }
+            </script>"""
+                % "\n".join(suggest_js)
+            )
+        # Javascript common to all forms:
+        R.append(
+            """<script type="text/javascript">
+    // controle par la checkbox
+    function tf_enable_elem(checkbox) {
+      var oid = checkbox.value;
+      if (oid) {
+         var elem = document.getElementById(oid);
+         if (elem) {
+             if (checkbox.checked) {
+                 elem.disabled = false;
+             } else {
+                 elem.disabled = true;
+             }
+         }
+      }
+    }
+
+    // Selections etendues avec shift (use jquery.field)
+    $('input[name="tf-checked:list"]').createCheckboxRange();
+        </script>"""
+        )
+        R.append("</form>")
+        return R
+
+    def _ReadOnlyElement(self, field, descr):
+        "Generate HTML for an element, read-only"
+        R = []
+        title = descr.get("title", field.capitalize())
+        withcheckbox = descr.get("withcheckbox", False)
+        input_type = descr.get("input_type", "text")
+        klass = descr.get("cssclass", "")
+        klass = " " + klass
+        if input_type == "hidden":
+            return ""
+
+        R.append('<tr class="tf-ro-tr%s">' % klass)
+
+        if input_type == "separator":  # separator
+            R.append('<td colspan="2">%s' % title)
+        else:
+            R.append('<td class="tf-ro-fieldlabel%s">' % klass)
+            R.append("%s</td>" % title)
+            R.append('<td class="tf-ro-field%s">' % klass)
+
+        if input_type == "text" or input_type == "text_suggest":
+            R.append(("%(" + field + ")s") % self.values)
+        elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"):
+            if input_type == "boolcheckbox":
+                labels = descr.get(
+                    "labels", descr.get("allowed_values", ["oui", "non"])
+                )
+                # XXX open('/tmp/log', 'w').write('%s labels=%s, val=%s\ndescr=%s\n'%(field, labels, self.values[field], descr))
+                R.append(labels[int(self.values[field])])
+                if int(self.values[field]):
+                    R.append('<input type="hidden" name="%s" value="1"/>' % field)
+            else:
+                labels = descr.get("labels", descr["allowed_values"])
+                for i in range(len(labels)):
+                    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])
+        elif input_type == "separator" or input_type == "hidden":
+            pass
+        elif input_type == "file":
+            R.append("'%s'" % self.values[field])
+        else:
+            raise ValueError("unkown input_type for form (%s)!" % input_type)
+
+        explanation = descr.get("explanation", "")
+        if explanation:
+            R.append('<span class="tf-explanation">%s</span>' % explanation)
+
+        R.append("</td></tr>")
+
+        return "\n".join(R)
+
+    def _ReadOnlyVersion(self, formdescription):
+        "Generate HTML for read-only view of the form"
+        R = ['<table class="tf-ro">']
+        for (field, descr) in formdescription:
+            R.append(self._ReadOnlyElement(field, descr))
+        R.append("</table>")
+        return R
+
+
+def dict2js(d):
+    """convert Python dict to JS code"""
+    r = []
+    for k in d:
+        v = d[k]
+        if type(v) == BooleanType:
+            if v:
+                v = "true"
+            else:
+                v = "false"
+        elif type(v) == StringType:
+            v = '"' + v + '"'
+
+        r.append("%s: %s" % (k, v))
+    return "{" + ",\n".join(r) + "}"
+
+
+def tf_error_message(msg):
+    """html for form error message"""
+    if not msg:
+        return ""
+    if type(msg) == StringType:
+        msg = [msg]
+    return (
+        '<ul class="tf-msg"><li class="tf-msg">%s</li></ul>'
+        % '</li><li class="tf-msg">'.join(msg)
+    )
diff --git a/VERSION.py b/VERSION.py
new file mode 100644
index 0000000000000000000000000000000000000000..853ccfbb83fa7ab6a403ee2766dff7970306cbeb
--- /dev/null
+++ b/VERSION.py
@@ -0,0 +1,349 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+SCOVERSION = "7.18"
+
+SCONAME = "ScoDoc"
+
+SCONEWS = """
+<h4>Année 2020</h4>
+<ul>
+<li>Nouveau site <a href="https://scodoc.org">scodoc.org</a> pour la documentation.</li>
+<li>Enregistrement de semestres extérieurs</li>
+<li>Améliorations PV de Jury</li>
+<li>Contributions J.-M. Place: aide au diagnostic problèmes export Apogée
+</li>
+</ul>
+<h4>Année 2019</h4>
+<ul>
+<li>Support Linux Debian 10</li>
+<li>Petites améliorations: import groupes, droits de suppression notes pour vacataires, etc.</li>
+<li>Exports listes pour Moodle</li>
+<li>Fonction facilitant l'envoi de données pour l'assistance technique</li>
+</ul>
+<h4>Année 2018</h4>
+<ul>
+<li>Affichage date finalisation inscription Apogée</li>
+<li>Co-responsables de semestres</li>
+<li>Amélioration page d'accueil département</li>
+<li>Corrections diverses et petites améliorations</li>
+<li>Avis de poursuites d'études plus robustes et configurables</li>
+</ul>
+<h4>Année 2017</h4>
+<ul>
+<li>Bonus/Malus sur les moyennes d'UE</li>
+<li>Enregistrement des informations sur le devenir de l'étudiant</li>
+<li>Export global des résultats d'un département (utile pour les rapports d'évaluation)</li>
+<li>Compatibilité Linux Debian 9, et modernisation de certains composants</li>
+<li>Génération des avis de poursuite d'études</li>
+<li>Toilettage page liste programme pédagogiques</li>
+<li>Amélioration exports résultats vers Apogée</li>
+<li>Amélioration calcul des ECTS</li>
+<li>Possibilité d'utilisation des adresses mail personnelles des étudiant</li>
+<li>Corrections diverses</li>
+</ul>
+
+<h4>Année 2016</h4>
+<ul>
+<li>Import des données d'admissions depuis fichiers APB ou autres</li>
+<li>Nouveau formulaire saisie des notes</li>
+<li>Export des résultats vers Apogée pour un ensemble de semestre</li>
+<li>Enregistrement du classement lors de l'admission</li>
+<li>Modification du calcul des coefficients des UE capitalisées</li>
+</ul>
+
+<h4>Année 2015</h4>
+<ul>
+<li>Exports fichiers Apogée</li>
+<li>Recherche étudiants sur plusieurs départements</li>
+<li>Corrections diverses</li>
+</ul>
+
+
+<h4>Année 2014</h4>
+<ul>
+<li>Nouvelle interface pour listes groupes, photos, feuilles d'émargement.</li>
+</ul>
+<h4>Année 2013</h4>
+<ul>
+<li>Modernisation de nombreux composants logiciels (ScoDoc 7)</li>
+<li>Saisie des absences par matières</li>
+</ul>
+<h4>Année 2012</h4>
+<ul>
+<li>Table lycées d'origine avec carte google</li>
+<li>Amélioration des PV de jury (logos, ...)</li>
+<li>Accélération du code de calcul des semestres</li>
+<li>Changement documentation en ligne (nouveau site web)</li>
+</ul>
+
+
+<h4>Année 2011</h4>
+<ul>
+<li>Amélioration de la présentation des bulletins de notes, et possibilité de définir de nouveaux formats</li>
+<li>Possibilité de modifier les moyennes d'UE via un "bonus" (sport/culture)</li>
+<li>Ajout parcours spécifique pour UCAC (Cameroun)</li>
+<li>Possibilité d'indiquer des mentions sur les PV</li>
+<li>Evaluations de "rattrapage"</li>
+<li>Support pour installation en Linux Debian "Squeeze"</li>
+<li>Corrections diverses</li>
+</ul>
+
+
+<h4>Novembre 2010</h4>
+<ul>
+<li>Possibilité d'indiquer des évaluations avec publication immédiate des notes (même si incomplètes)</li>
+</ul>
+
+<h4>Octobre 2010</h4>
+<ul>
+<li>Nouvelle API JSON</li>
+<li>Possibilité d'associer 2 étapes Apogée au même semestre</li>
+<li>Table "poursuite études"</li>
+<li>Possibilité d'envoyer un mail auto aux étudiants absentéistes<li>
+</ul>
+
+<h4>Août 2010</h4>
+<ul>
+<li>Définitions de parcours (DUT, LP, ...) avec prise en compte des spécificités (par ex., certaines barres d'UE différentes en LP)</li>
+</ul>
+
+<h4>Avril - Juin 2010</h4>
+<ul>
+<li>Formules utilisateur pour le calcul des moyennes d'UE</li>
+<li>Nouveau système de notification des absences par mail</li>
+<li>Affichage optionnel des valeurs mini et maxi des moyennes sur les bulletins</li>
+<li>Nouveau code de décision jury semestre: "RAT" : en attente de rattrapage</li>
+</ul>
+
+<h4>Janvier 2010</h4>
+<ul>
+<li>Suivez l'actualité du développement sur Twitter: <a href="https://twitter.com/ScoDoc">@ScoDoc</a></li>
+<li>Nouveau menu "Groupes" pour faciliter la prise en main</li>
+<li>Possibilité de définir des règles ad hoc de calcul des moyennes de modules (formules)</li>
+<li>Possibilité d'inclure des images (logos) dans les bulletins PDF</li>
+<li>Bandeau "provisoire" sur les bulletins en cours de semestre</li>
+<li>Possibilite de valider (capitaliser) une UE passee hors ScoDoc</li>
+<li>Amelioration de l'édition des programmes (formations)</li>
+<li>Nombreuses améliorations mineures</li>
+</ul>
+
+<h4>Novembre 2009</h4>
+<ul>
+<li>Gestion des partitions et groupes en nombres quelconques</li>
+<li>Nouvelle gestion des photos</li>
+<lI>Imports d'étudiants excel incrémentaux</li>
+<li>Optimisations et petites améliorations</li>
+</ul>
+
+<h4>Septembre 2009</h4>
+<ul>
+<li>Traitement de "billets d'absences" (saisis par les étudiants sur le portail)</li>
+</ul>
+
+<h4>Juin 2009</h4>
+<ul>
+<li>Nouveau système plus flexibles de gestion des préférences (ou "paramètres")</li>
+<li>Possiblité d'associer une nouvelle version de programme à un semestre</li>
+<li>Corrections et améliorations diverses</h4>
+</ul>
+
+<h4>Juillet 2008: version 6.0</h4>
+<ul>
+<li>Installeur automatisé pour Linux</li>
+<li>Amélioration ergonomie (barre menu pages semestres)</li>
+<li>Refonte fiche étudiant (parcours)</li>
+<li>Archivage des documents (PV)</li>
+<li>Nouvel affichage des notes des évaluations</li>
+<li>Nombreuses corrections et améliorations</li>
+</ul>
+
+<h4>Juin 2008</h4>
+<ul>
+<li>Rangs sur les bulletins</li>
+</ul>
+
+<h4>Février 2008</h4>
+<ul>
+<li>Statistiques et suivis de cohortes (chiffres et graphes)</li>
+<li>Nombreuses petites corrections suites aux jurys de janvier</li>
+</ul>
+
+<h4>Janvier 2008</h4>
+<ul>
+<li>Personnalisation des régles de calculs notes d'option (sport, culture)</li>
+<li>Edition de PV de jury individuel</li>
+</ul>
+
+<h4>Novembre 2007</h4>
+<ul>
+<li>Vérification des absences aux évaluations</li>
+<li>Import des photos depuis portail, trombinoscopes en PDF</li>
+</ul>
+
+<h4>Septembre 2007</h4>
+<ul>
+<li>Importation des etudiants depuis étapes Apogée</li>
+<li>Inscription de groupes à des modules (options ou parcours)</li>
+<li>Listes de étapes Apogée (importées du portail)</li>
+</ul>
+
+<h4>Juillet 2007</h4>
+<ul>
+<li>Import utilisateurs depuis Excel</li>
+<li>Nouvelle gestion des passage d'un semestre à l'autre</li>
+</ul>
+
+<h4>Juin 2007: version 5.0</h4>
+<ul>
+<li>Suivi des parcours et règles de décision des jurys DUT</li>
+<li>Capitalisation des UEs</li>
+<li>Edition des PV de jurys et courriers aux étudiants</li>
+<li>Feuilles (excel) pour préparation jurys</li>
+<li>Nombreuses petites améliorations</li>
+</ul>
+
+<h4>Avril 2007</h4>
+<ul>
+<li>Paramètres de mise en page des bulletins en PDF</li>
+</ul>
+
+<h4>Février 2007</h4>
+
+<ul>
+<li>Possibilité de ne <em>pas</em> publier les bulletins sur le portail</li>
+<li>Gestion des notes "en attente" (publication d'évaluations sans correction de toutes les copies)</li>
+<li>Amélioration formulaire saisie absences, saisie absences par semestre.</li>
+</ul>
+
+<h4>Janvier 2007</h4>
+<ul>
+<li>Possibilité d'initialiser les notes manquantes d'une évaluation</li>
+<li>Recupération des codes NIP depuis Apogée</li>
+<li>Gestion des compensations inter-semestre DUT (en cours de développement)</li>
+<li>Export trombinoscope en archive zip</li>
+</ul>
+
+<h4>Octobre 2006</h4>
+<ul>
+<li>Réorganisation des pages d'accueil</li>
+<li>Ajout des "nouvelles" (dernières opérations), avec flux RSS</li>
+<li>Import/Export XML des formations, duplication d'une formation (versions)</li>
+<li>Bulletins toujours sur une seule feuille (passage à ReportLab 2.0)</li>
+<li>Suppression d'un utilisateur</il>
+</ul>
+<h4>Septembre 2006</h4>
+<ul>
+<li>Page pour suppression des groupes.</li>
+<li>Amélioration gestion des utilisateurs</li>
+<li>"Verrouillage" des semestres</li>
+<li>Liste d'enseignants (chargés de TD) associés à un module (et pouvant saisir des notes)</li>
+<li>Noms de types de groupes (TD, TP, ...) modifiables</li>
+<li>Tableau rudimentaire donnant la répartition des bacs dans un semestre</li>
+<li>Amélioration mise en page des listes au format excel</li>
+<li>Annulation des démissions</li>
+</ul>
+
+<h4>Juillet 2006</h4>
+<ul>
+<li>Dialogue permettant au directeur des études de modifier
+les options d'un semestre</li>
+<li>Option pour ne pas afficher les UE validées sur les bulletins</li>
+</ul>
+
+<h4>30 juin 2006</h4>
+<ul>
+<li>Option pour ne pas afficher les décisions sur les bulletins</li>
+<li>Génération feuilles pour préparation jury</li>
+<li>Gestion des modules optionnels</li>
+<li>Prise en compte note "activités culturelles ou sportives"</li>
+<li>Amélioration tableau de bord semestre</li>
+<li>Import listes étudiants depuis Excel (avec code Apogée)</li>
+</ul>
+
+<h4>12 juin 2006</h4>
+<ul>
+<li>Formulaire dynamique d'affectation aux groupes</li>
+<li>Tri des tableaux (listes, récapitulatif)</li>
+<li>Export XML des infos sur un etudiant et des groupes</li>
+</ul>
+
+<h4>12 mai 2006</h4>
+<ul>
+<li>Possibilité de suppression d'un semestre</li>
+<li>Export XML du tableau recapitulatif des notes du semestre</li>
+<li>Possibilité de supression d'une formation complète</li>
+</ul>
+
+<h4>24 avril 2006</h4>
+<ul>
+<li>Export bulletins en XML (expérimental)</li>
+<li>Flag "gestion_absence" sur les semestres de formation</li>
+</ul>
+
+<h4>4 mars 2006</h4>
+<ul>
+<li>Formulaire d'inscription au semestre suivant.</li>
+<li>Format "nombre" dans les feuilles excel exportées.</li>
+</ul>
+
+<h4>23 février 2006</h4>
+<ul>
+<li>Décisions jury sur bulletins.</li>
+</ul>
+
+<h4>17 janvier 2006</h4>
+<ul>
+<li>Ajout et édition d'appréciations sur les bulletins.</li>
+</ul>
+<h4>12 janvier 2006</h4>
+<ul>
+<li>Envoi des bulletins en PDF par mail aux étudiants.</li>
+</ul>
+
+<h4>6 janvier 2006</h4>
+<ul>
+<li>Affichage des ex-aequos.</li>
+<li>Classeurs bulletins PDF en différentes versions.</li>
+<li>Corrigé gestion des notes des démissionnaires.</li>
+</ul>
+
+<h4>1er janvier 2006</h4>
+<ul>
+<li>Import du projet dans Subversion / LIPN.</li>
+<li>Lecture des feuilles de notes Excel.</li>
+</ul>
+
+<h4>31 décembre 2005</h4>
+<ul>
+<li>Listes générées au format Excel au lieu de CSV.</li>
+<li>Bug fix (création/saisie evals).</li>
+</ul>
+
+<h4>29 décembre 2005</h4>
+<ul>
+<li>Affichage des moyennes de chaque groupe dans tableau de bord module.
+</ul>
+
+<h4>26 décembre 2005</h4>
+<ul>
+<li>Révision inscription/édition <em>individuelle</em> d'étudiants.</li>
+<li>Amélioration fiche étudiant (cosmétique, liste formations, actions).</li>
+<li>Listings notes d'évaluations anonymes (utilité douteuse ?).</li>
+<li>Amélioration formulaire saisie notes ('enter' -> champ suivant).</li>
+</ul>
+
+<h4>24 décembre 2005</h4>
+<ul>
+<li>Génération de bulletins PDF
+</li>
+<li>Suppression de notes (permet donc de supprimer une évaluation)
+</li>
+<li>Bulletins en versions courtes (seulement moyennes de chaque module), longues
+(toutes les notes) et intermédiaire (moyenne de chaque module plus notes dans les évaluations sélectionnées).
+</li>
+<li>Notes moyennes sous les barres en rouge dans le tableau récapitulatif (seuil=10 sur la moyenne générale, et 8 sur chaque UE).
+</li>
+<li>Colonne "groupe de TD" dans le tableau récapitulatif des notes.
+</ul>
+"""
diff --git a/ZAbsences.py b/ZAbsences.py
new file mode 100644
index 0000000000000000000000000000000000000000..1bdd11b7acf251e30c257a30514b2d3d54a38842
--- /dev/null
+++ b/ZAbsences.py
@@ -0,0 +1,2516 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+""" Gestion des absences (v4)
+
+C'est la partie la plus ancienne de ScoDoc, et elle est à revoir.
+
+L'API de plus bas niveau est en gros:
+
+ AnnuleAbsencesDatesNoJust(etudid, dates)
+ CountAbs(etudid, debut, fin, matin=None, moduleimpl_id=None)
+ CountAbsJust(etudid, debut, fin, matin=None, moduleimpl_id=None)
+ ListeAbsJust(etudid, datedebut)  [pas de fin ?]
+ ListeAbsNonJust(etudid, datedebut)  [pas de fin ?]
+ ListeJustifs(etudid, datedebut, datefin=None, only_no_abs=True)
+
+ ListeAbsJour(date, am=True, pm=True, is_abs=None, is_just=None)
+ ListeAbsNonJustJour(date, am=True, pm=True)
+
+
+"""
+
+import urllib
+
+from sco_zope import *
+
+# ---------------
+
+from notesdb import *
+from notes_log import log
+from scolog import logdb
+from sco_utils import *
+
+# import notes_users
+from TrivialFormulator import TrivialFormulator, TF
+from gen_tables import GenTable
+import scolars
+import sco_formsemestre
+import sco_groups
+import sco_groups_view
+import sco_excel
+import sco_abs_notification, sco_abs_views
+import sco_compute_moy
+import string, re
+import time, calendar
+from mx.DateTime import DateTime as mxDateTime
+from mx.DateTime.ISO import ParseDateTimeUTC
+
+
+def _toboolean(x):
+    "convert a value to boolean (ensure backward compat with OLD intranet code)"
+    if type(x) == type(""):
+        x = x.lower()
+    if x and x != "false":  # backward compat...
+        return True
+    else:
+        return False
+
+
+def MonthNbDays(month, year):
+    "returns nb of days in month"
+    if month > 7:
+        month = month + 1
+    if month % 2:
+        return 31
+    elif month == 2:
+        if calendar.isleap(year):
+            return 29
+        else:
+            return 28
+    else:
+        return 30
+
+
+class ddmmyyyy:
+    """immutable dates"""
+
+    def __init__(self, date=None, fmt="ddmmyyyy", work_saturday=False):
+        self.work_saturday = work_saturday
+        if date is None:
+            return
+        try:
+            if fmt == "ddmmyyyy":
+                self.day, self.month, self.year = string.split(date, "/")
+            elif fmt == "iso":
+                self.year, self.month, self.day = string.split(date, "-")
+            else:
+                raise ValueError("invalid format spec. (%s)" % fmt)
+            self.year = string.atoi(self.year)
+            self.month = string.atoi(self.month)
+            self.day = string.atoi(self.day)
+        except:
+            raise ScoValueError("date invalide: %s" % date)
+        # accept years YYYY or YY, uses 1970 as pivot
+        if self.year < 1970:
+            if self.year > 100:
+                raise ScoInvalidDateError("Année invalide: %s" % self.year)
+            if self.year < 70:
+                self.year = self.year + 2000
+            else:
+                self.year = self.year + 1900
+        if self.month < 1 or self.month > 12:
+            raise ScoInvalidDateError("Mois invalide: %s" % self.month)
+
+        if self.day < 1 or self.day > MonthNbDays(self.month, self.year):
+            raise ScoInvalidDateError("Jour invalide: %s" % self.day)
+
+        # weekday in 0-6, where 0 is monday
+        self.weekday = calendar.weekday(self.year, self.month, self.day)
+
+        self.time = time.mktime((self.year, self.month, self.day, 0, 0, 0, 0, 0, 0))
+
+    def iswork(self):
+        "returns true if workable day"
+        if self.work_saturday:
+            nbdays = 6
+        else:
+            nbdays = 5
+        if (
+            self.weekday >= 0 and self.weekday < nbdays
+        ):  # monday-friday or monday-saturday
+            return 1
+        else:
+            return 0
+
+    def __repr__(self):
+        return "'%02d/%02d/%04d'" % (self.day, self.month, self.year)
+
+    def __str__(self):
+        return "%02d/%02d/%04d" % (self.day, self.month, self.year)
+
+    def ISO(self):
+        "iso8601 representation of the date"
+        return "%04d-%02d-%02d" % (self.year, self.month, self.day)
+
+    def next(self, days=1):
+        "date for the next day (nota: may be a non workable day)"
+        day = self.day + days
+        month = self.month
+        year = self.year
+
+        while day > MonthNbDays(month, year):
+            day = day - MonthNbDays(month, year)
+            month = month + 1
+            if month > 12:
+                month = 1
+                year = year + 1
+        return self.__class__(
+            "%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday
+        )
+
+    def prev(self, days=1):
+        "date for previous day"
+        day = self.day - days
+        month = self.month
+        year = self.year
+        while day <= 0:
+            month = month - 1
+            if month == 0:
+                month = 12
+                year = year - 1
+            day = day + MonthNbDays(month, year)
+
+        return self.__class__(
+            "%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday
+        )
+
+    def next_monday(self):
+        "date of next monday"
+        return self.next((7 - self.weekday) % 7)
+
+    def prev_monday(self):
+        "date of last monday, but on sunday, pick next monday"
+        if self.weekday == 6:
+            return self.next_monday()
+        else:
+            return self.prev(self.weekday)
+
+    def __cmp__(self, other):
+        """return a negative integer if self < other, 
+        zero if self == other, a positive integer if self > other"""
+        return int(self.time - other.time)
+
+    def __hash__(self):
+        "we are immutable !"
+        return hash(self.time) ^ hash(str(self))
+
+
+# d = ddmmyyyy( '21/12/99' )
+
+
+def YearTable(
+    context,
+    year,
+    events=[],
+    firstmonth=9,
+    lastmonth=7,
+    halfday=0,
+    dayattributes="",
+    pad_width=8,
+):
+    """Generate a calendar table
+    events = list of tuples (date, text, color, href [,halfday])
+             where date is a string in ISO format (yyyy-mm-dd)
+             halfday is boolean (true: morning, false: afternoon)
+    text  = text to put in calendar (must be short, 1-5 cars) (optional)
+    if halfday, generate 2 cells per day (morning, afternoon)
+    """
+    T = [
+        '<table id="maincalendar" class="maincalendar" border="3" cellpadding="1" cellspacing="1" frame="box">'
+    ]
+    T.append("<tr>")
+    month = firstmonth
+    while 1:
+        T.append('<td valign="top">')
+        T.append(MonthTableHead(month))
+        T.append(
+            MonthTableBody(
+                month,
+                year,
+                events,
+                halfday,
+                dayattributes,
+                context.is_work_saturday(),
+                pad_width=pad_width,
+            )
+        )
+        T.append(MonthTableTail())
+        T.append("</td>")
+        if month == lastmonth:
+            break
+        month = month + 1
+        if month > 12:
+            month = 1
+            year = year + 1
+    T.append("</table>")
+    return string.join(T, "\n")
+
+
+# ---------------
+
+
+class ZAbsences(
+    ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit
+):
+
+    "ZAbsences object"
+
+    meta_type = "ZAbsences"
+    security = ClassSecurityInfo()
+
+    # This is the list of the methods associated to 'tabs' in the ZMI
+    # Be aware that The first in the list is the one shown by default, so if
+    # the 'View' tab is the first, you will never see your tabs by cliquing
+    # on the object.
+    manage_options = (
+        ({"label": "Contents", "action": "manage_main"},)
+        + PropertyManager.manage_options  # add the 'Properties' tab
+        + ({"label": "View", "action": "index_html"},)
+        + Item.manage_options  # add the 'Undo' & 'Owner' tab
+        + RoleManager.manage_options  # add the 'Security' tab
+    )
+
+    # no permissions, only called from python
+    def __init__(self, id, title):
+        "initialise a new instance"
+        self.id = id
+        self.title = title
+
+    # The form used to edit this object
+    def manage_editZAbsences(self, title, RESPONSE=None):
+        "Changes the instance values"
+        self.title = title
+        self._p_changed = 1
+        RESPONSE.redirect("manage_editForm")
+
+    # --------------------------------------------------------------------
+    #
+    #   ABSENCES (top level)
+    #
+    # --------------------------------------------------------------------
+    # used to view content of the object
+    security.declareProtected(ScoView, "index_html")
+    index_html = sco_abs_views.absences_index_html
+
+    security.declareProtected(ScoView, "EtatAbsences")
+    EtatAbsences = sco_abs_views.EtatAbsences
+
+    security.declareProtected(ScoView, "CalAbs")
+    CalAbs = sco_abs_views.CalAbs
+
+    security.declareProtected(ScoAbsChange, "SignaleAbsenceEtud")
+    SignaleAbsenceEtud = sco_abs_views.SignaleAbsenceEtud
+    security.declareProtected(ScoAbsChange, "doSignaleAbsence")
+    doSignaleAbsence = sco_abs_views.doSignaleAbsence
+
+    security.declareProtected(ScoAbsChange, "JustifAbsenceEtud")
+    JustifAbsenceEtud = sco_abs_views.JustifAbsenceEtud
+    security.declareProtected(ScoAbsChange, "doJustifAbsence")
+    doJustifAbsence = sco_abs_views.doJustifAbsence
+
+    security.declareProtected(ScoAbsChange, "AnnuleAbsenceEtud")
+    AnnuleAbsenceEtud = sco_abs_views.AnnuleAbsenceEtud
+    security.declareProtected(ScoAbsChange, "doAnnuleAbsence")
+    doAnnuleAbsence = sco_abs_views.doAnnuleAbsence
+    security.declareProtected(ScoAbsChange, "doAnnuleJustif")
+    doAnnuleJustif = sco_abs_views.doAnnuleJustif
+
+    security.declareProtected(ScoView, "ListeAbsEtud")
+    ListeAbsEtud = sco_abs_views.ListeAbsEtud
+
+    # --------------------------------------------------------------------
+    #
+    #   SQL METHODS
+    #
+    # --------------------------------------------------------------------
+
+    def _AddAbsence(
+        self,
+        etudid,
+        jour,
+        matin,
+        estjust,
+        REQUEST,
+        description=None,
+        moduleimpl_id=None,
+    ):
+        "Ajoute une absence dans la bd"
+        # unpublished
+        if self._isFarFutur(jour):
+            raise ScoValueError("date absence trop loin dans le futur !")
+        estjust = _toboolean(estjust)
+        matin = _toboolean(matin)
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=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 )",
+            vars(),
+        )
+        logdb(
+            REQUEST,
+            cnx,
+            "AddAbsence",
+            etudid=etudid,
+            msg="JOUR=%(jour)s,MATIN=%(matin)s,ESTJUST=%(estjust)s,description=%(description)s,moduleimpl_id=%(moduleimpl_id)s"
+            % vars(),
+        )
+        cnx.commit()
+        invalidateAbsEtudDate(self, etudid, jour)
+        sco_abs_notification.abs_notify(self, etudid, jour)
+
+    def _AddJustif(self, etudid, jour, matin, REQUEST, description=None):
+        "Ajoute un justificatif dans la base"
+        # unpublished
+        if self._isFarFutur(jour):
+            raise ScoValueError("date justificatif trop loin dans le futur !")
+        matin = _toboolean(matin)
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "insert into absences (etudid,jour,estabs,estjust,matin, description) values (%(etudid)s,%(jour)s, FALSE, TRUE, %(matin)s, %(description)s )",
+            vars(),
+        )
+        logdb(
+            REQUEST,
+            cnx,
+            "AddJustif",
+            etudid=etudid,
+            msg="JOUR=%(jour)s,MATIN=%(matin)s" % vars(),
+        )
+        cnx.commit()
+        invalidateAbsEtudDate(self, etudid, jour)
+
+    def _AnnuleAbsence(self, etudid, jour, matin, moduleimpl_id=None, REQUEST=None):
+        """Annule une absence ds base
+        Si moduleimpl_id, n'annule que pour ce module
+        """
+        # unpublished
+        matin = _toboolean(matin)
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        req = "delete from absences where jour=%(jour)s and matin=%(matin)s and etudid=%(etudid)s and estabs"
+        if moduleimpl_id:
+            req += " and moduleimpl_id=%(moduleimpl_id)s"
+        cursor.execute(req, vars())
+        logdb(
+            REQUEST,
+            cnx,
+            "AnnuleAbsence",
+            etudid=etudid,
+            msg="JOUR=%(jour)s,MATIN=%(matin)s,moduleimpl_id=%(moduleimpl_id)s"
+            % vars(),
+        )
+        cnx.commit()
+        invalidateAbsEtudDate(self, etudid, jour)
+
+    def _AnnuleJustif(self, etudid, jour, matin, REQUEST=None):
+        "Annule un justificatif"
+        # unpublished
+        matin = _toboolean(matin)
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "delete from absences where jour=%(jour)s and matin=%(matin)s and etudid=%(etudid)s and ESTJUST AND NOT ESTABS",
+            vars(),
+        )
+        cursor.execute(
+            "update absences set estjust=false where jour=%(jour)s and matin=%(matin)s and etudid=%(etudid)s",
+            vars(),
+        )
+        logdb(
+            REQUEST,
+            cnx,
+            "AnnuleJustif",
+            etudid=etudid,
+            msg="JOUR=%(jour)s,MATIN=%(matin)s" % vars(),
+        )
+        cnx.commit()
+        invalidateAbsEtudDate(self, etudid, jour)
+
+    # Fonction inutile à supprimer (gestion moduleimpl_id incorrecte):
+    # def _AnnuleAbsencesPeriodNoJust(self, etudid, datedebut, datefin,
+    #                                 moduleimpl_id=None, REQUEST=None):
+    #     """Supprime les absences entre ces dates (incluses).
+    #     mais ne supprime pas les justificatifs.
+    #     """
+    #     # unpublished
+    #     cnx = self.GetDBConnexion()
+    #     cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    #     # supr les absences non justifiees
+    #     cursor.execute("delete from absences where etudid=%(etudid)s and (not estjust) and moduleimpl_id=(moduleimpl_id)s and jour BETWEEN %(datedebut)s AND %(datefin)s",
+    #                    vars() )
+    #     # s'assure que les justificatifs ne sont pas "absents"
+    #     cursor.execute("update absences set estabs=FALSE where  etudid=%(etudid)s and jour and moduleimpl_id=(moduleimpl_id)s  BETWEEN %(datedebut)s AND %(datefin)s", vars())
+    #     logdb(REQUEST, cnx, 'AnnuleAbsencesPeriodNoJust', etudid=etudid,
+    #           msg='%(datedebut)s - %(datefin)s - (moduleimpl_id)s'%vars())
+    #     cnx.commit()
+    #     invalidateAbsEtudDate(self, etudid, datedebut)
+    #     invalidateAbsEtudDate(self, etudid, datefin) # si un semestre commence apres datedebut et termine avant datefin, il ne sera pas invalide. Tant pis ;-)
+
+    security.declareProtected(ScoAbsChange, "AnnuleAbsencesDatesNoJust")
+
+    def AnnuleAbsencesDatesNoJust(
+        self, etudid, dates, moduleimpl_id=None, REQUEST=None
+    ):
+        """Supprime les absences aux dates indiquées
+        mais ne supprime pas les justificatifs.
+        """
+        # log('AnnuleAbsencesDatesNoJust: moduleimpl_id=%s' % moduleimpl_id)
+        if not dates:
+            return
+        date0 = dates[0]
+        if len(date0.split(":")) == 2:
+            # am/pm is present
+            for date in dates:
+                jour, ampm = date.split(":")
+                if ampm == "am":
+                    matin = 1
+                elif ampm == "pm":
+                    matin = 0
+                else:
+                    raise ValueError("invalid ampm !")
+                self._AnnuleAbsence(etudid, jour, matin, moduleimpl_id, REQUEST)
+            return
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        # 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",
+                vars(),
+            )
+            invalidateAbsEtudDate(self, 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",
+                vars(),
+            )
+        if dates:
+            date0 = dates[0]
+        else:
+            date0 = None
+        if len(dates) > 1:
+            date1 = dates[1]
+        else:
+            date1 = None
+        logdb(
+            REQUEST,
+            cnx,
+            "AnnuleAbsencesDatesNoJust",
+            etudid=etudid,
+            msg="%s - %s - %s" % (date0, date1, moduleimpl_id),
+        )
+        cnx.commit()
+
+    security.declareProtected(ScoView, "CountAbs")
+
+    def CountAbs(self, etudid, debut, fin, matin=None, moduleimpl_id=None):
+        """CountAbs
+        matin= 1 ou 0.
+        """
+        if matin != None:
+            matin = _toboolean(matin)
+            ismatin = " AND A.MATIN = %(matin)s "
+        else:
+            ismatin = ""
+        if moduleimpl_id:
+            modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s "
+        else:
+            modul = ""
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            """SELECT COUNT(*) AS NbAbs FROM (
+    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
+          ) AS tmp
+          """,
+            vars(),
+        )
+        res = cursor.fetchone()[0]
+        return res
+
+    security.declareProtected(ScoView, "CountAbsJust")
+
+    def CountAbsJust(self, etudid, debut, fin, matin=None, moduleimpl_id=None):
+        if matin != None:
+            matin = _toboolean(matin)
+            ismatin = " AND A.MATIN = %(matin)s "
+        else:
+            ismatin = ""
+        if moduleimpl_id:
+            modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s "
+        else:
+            modul = ""
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            """SELECT COUNT(*) AS NbAbsJust FROM (
+  SELECT DISTINCT A.JOUR, A.MATIN
+  FROM ABSENCES A, ABSENCES B
+  WHERE A.ETUDID = %(etudid)s
+      AND A.ETUDID = B.ETUDID 
+      AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN
+      AND A.JOUR BETWEEN %(debut)s AND %(fin)s
+      AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST)"""
+            + ismatin
+            + modul
+            + """
+) AS tmp
+        """,
+            vars(),
+        )
+        res = cursor.fetchone()[0]
+        return res
+
+    def _ListeAbsDate(self, etudid, beg_date, end_date):
+        # Liste des absences et justifs entre deux dates
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            """SELECT jour, matin, estabs, estjust, description FROM ABSENCES A 
+     WHERE A.ETUDID = %(etudid)s
+     AND A.jour >= %(beg_date)s 
+     AND A.jour <= %(end_date)s 
+         """,
+            vars(),
+        )
+        Abs = cursor.dictfetchall()
+        # log('ListeAbsDate: abs=%s' % Abs)
+        # remove duplicates
+        A = {}  # { (jour, matin) : abs }
+        for a in Abs:
+            jour, matin = a["jour"], a["matin"]
+            if (jour, matin) in A:
+                # garde toujours la description
+                a["description"] = a["description"] or A[(jour, matin)]["description"]
+                # et la justif:
+                a["estjust"] = a["estjust"] or A[(jour, matin)]["estjust"]
+                a["estabs"] = a["estabs"] or A[(jour, matin)]["estabs"]
+                A[(jour, matin)] = a
+            else:
+                A[(jour, matin)] = a
+            if A[(jour, matin)]["description"] is None:
+                A[(jour, matin)]["description"] = ""
+            # add hours: matin = 8:00 - 12:00, apresmidi = 12:00 - 18:00
+            dat = "%04d-%02d-%02d" % (a["jour"].year, a["jour"].month, a["jour"].day)
+            if a["matin"]:
+                A[(jour, matin)]["begin"] = dat + " 08:00:00"
+                A[(jour, matin)]["end"] = dat + " 11:59:59"
+            else:
+                A[(jour, matin)]["begin"] = dat + " 12:00:00"
+                A[(jour, matin)]["end"] = dat + " 17:59:59"
+        # sort
+        R = A.values()
+        R.sort(key=lambda x: (x["begin"]))
+        # log('R=%s' % R)
+        return R
+
+    security.declareProtected(ScoView, "ListeAbsJust")
+
+    def ListeAbsJust(self, etudid, datedebut):
+        "Liste des absences justifiees (par ordre chronologique)"
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            """SELECT DISTINCT A.ETUDID, A.JOUR, A.MATIN FROM ABSENCES A, ABSENCES B
+ WHERE A.ETUDID = %(etudid)s
+ AND A.ETUDID = B.ETUDID 
+ AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN AND A.JOUR >= %(datedebut)s
+ AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST)
+ ORDER BY A.JOUR
+        """,
+            vars(),
+        )
+        A = cursor.dictfetchall()
+        for a in A:
+            a["description"] = self._GetAbsDescription(a, cursor=cursor)
+        return A
+
+    security.declareProtected(ScoView, "ListeAbsNonJust")
+
+    def ListeAbsNonJust(self, etudid, datedebut):
+        "Liste des absences NON justifiees (par ordre chronologique)"
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            """SELECT ETUDID, JOUR, MATIN FROM ABSENCES A 
+    WHERE A.ETUDID = %(etudid)s
+    AND A.estabs 
+    AND A.jour >= %(datedebut)s
+    EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B 
+    WHERE B.estjust 
+    AND B.ETUDID = %(etudid)s
+    ORDER BY JOUR
+        """,
+            vars(),
+        )
+        A = cursor.dictfetchall()
+        for a in A:
+            a["description"] = self._GetAbsDescription(a, cursor=cursor)
+        return A
+
+    security.declareProtected(ScoView, "ListeAbsJust")
+
+    def ListeJustifs(self, etudid, datedebut, datefin=None, only_no_abs=False):
+        """Liste des justificatifs (sans absence relevée) à partir d'une date,
+        ou, si datefin spécifié, entre deux dates.
+        Si only_no_abs: seulement les justificatifs correspondant aux jours sans absences relevées.
+        """
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        req = """SELECT DISTINCT ETUDID, JOUR, MATIN FROM ABSENCES A
+ WHERE A.ETUDID = %(etudid)s
+ AND A.ESTJUST
+ AND A.JOUR >= %(datedebut)s"""
+        if datefin:
+            req += """AND A.JOUR <= %(datefin)s"""
+        if only_no_abs:
+            req += """
+ EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B 
+ WHERE B.estabs
+ AND B.ETUDID = %(etudid)s
+        """
+        cursor.execute(req, vars())
+        A = cursor.dictfetchall()
+        for a in A:
+            a["description"] = self._GetAbsDescription(a, cursor=cursor)
+
+        return A
+
+    def _GetAbsDescription(self, a, cursor=None):
+        "Description associee a l'absence"
+        if not cursor:
+            cnx = self.GetDBConnexion()
+            cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        a = a.copy()
+        # a['jour'] = a['jour'].date()
+        if a["matin"]:  # devrait etre booleen... :-(
+            a["matin"] = True
+        else:
+            a["matin"] = False
+        cursor.execute(
+            """select * from absences where etudid=%(etudid)s and jour=%(jour)s and matin=%(matin)s order by entry_date desc""",
+            a,
+        )
+        A = cursor.dictfetchall()
+        desc = None
+        module = ""
+        for a in A:
+            if a["description"]:
+                desc = a["description"]
+            if a["moduleimpl_id"] and a["moduleimpl_id"] != "NULL":
+                # Trouver le nom du module
+                Mlist = self.Notes.do_moduleimpl_withmodule_list(
+                    moduleimpl_id=a["moduleimpl_id"]
+                )
+                if Mlist:
+                    M = Mlist[0]
+                    module += "%s " % M["module"]["code"]
+
+        if desc:
+            return "(%s) %s" % (desc, module)
+            return desc
+        if module:
+            return module
+        return ""
+
+    security.declareProtected(ScoView, "ListeAbsJour")
+
+    def ListeAbsJour(self, date, am=True, pm=True, is_abs=True, is_just=None):
+        """Liste des absences et/ou justificatifs ce jour.
+        is_abs: None (peu importe), True, False
+        is_just: idem
+        """
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        req = """SELECT DISTINCT etudid, jour, matin FROM ABSENCES A 
+    WHERE A.jour = %(date)s
+    """
+        if is_abs != None:
+            req += " AND A.estabs = %(is_abs)s"
+        if is_just != None:
+            req += " AND A.estjust = %(is_just)s"
+        if not am:
+            req += " AND NOT matin "
+        if not pm:
+            req += " AND matin"
+
+        cursor.execute(req, {"date": date, "is_just": is_just, "is_abs": is_abs})
+        A = cursor.dictfetchall()
+        for a in A:
+            a["description"] = self._GetAbsDescription(a, cursor=cursor)
+        return A
+
+    security.declareProtected(ScoView, "ListeAbsNonJustJour")
+
+    def ListeAbsNonJustJour(self, date, am=True, pm=True):
+        "Liste des absences non justifiees ce jour"
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        reqa = ""
+        if not am:
+            reqa += " AND NOT matin "
+        if not pm:
+            reqa += " AND matin "
+        req = (
+            """SELECT  etudid, jour, matin FROM ABSENCES A 
+    WHERE A.estabs 
+    AND A.jour = %(date)s
+    """
+            + reqa
+            + """EXCEPT SELECT etudid, jour, matin FROM ABSENCES B 
+    WHERE B.estjust AND B.jour = %(date)s"""
+            + reqa
+        )
+
+        cursor.execute(req, {"date": date})
+        A = cursor.dictfetchall()
+        for a in A:
+            a["description"] = self._GetAbsDescription(a, cursor=cursor)
+        return A
+
+    security.declareProtected(ScoAbsChange, "doSignaleAbsenceGrSemestre")
+
+    def doSignaleAbsenceGrSemestre(
+        self,
+        moduleimpl_id=None,
+        abslist=[],
+        dates="",
+        etudids="",
+        destination=None,
+        REQUEST=None,
+    ):
+        """Enregistre absences aux dates indiquees (abslist et dates).
+        dates est une liste de dates ISO (séparées par des ',').
+        Efface les absences aux dates indiquées par dates,
+        ou bien ajoute celles de abslist.
+        """
+        if etudids:
+            etudids = etudids.split(",")
+        else:
+            etudids = []
+        if dates:
+            dates = dates.split(",")
+        else:
+            dates = []
+
+        # 1- Efface les absences
+        if dates:
+            for etudid in etudids:
+                self.AnnuleAbsencesDatesNoJust(etudid, dates, moduleimpl_id, REQUEST)
+            return "Absences effacées"
+
+        # 2- Ajoute les absences
+        if abslist:
+            self._add_abslist(abslist, REQUEST, moduleimpl_id)
+            return "Absences ajoutées"
+
+        return ""
+
+    def _add_abslist(self, abslist, REQUEST, moduleimpl_id=None):
+        for a in abslist:
+            etudid, jour, ampm = a.split(":")
+            if ampm == "am":
+                matin = 1
+            elif ampm == "pm":
+                matin = 0
+            else:
+                raise ValueError("invalid ampm !")
+            # ajoute abs si pas deja absent
+            if self.CountAbs(etudid, jour, jour, matin, moduleimpl_id) == 0:
+                self._AddAbsence(etudid, jour, matin, 0, REQUEST, "", moduleimpl_id)
+
+    #
+    security.declareProtected(ScoView, "CalSelectWeek")
+
+    def CalSelectWeek(self, year=None, REQUEST=None):
+        "display calendar allowing week selection"
+        if not year:
+            year = AnneeScolaire(REQUEST)
+        sems = sco_formsemestre.do_formsemestre_list(self)
+        if not sems:
+            js = ""
+        else:
+            js = 'onmouseover="highlightweek(this);" onmouseout="deselectweeks();" onclick="wclick(this);"'
+        C = YearTable(self, int(year), dayattributes=js)
+        return C
+
+    # --- Misc tools.... ------------------
+
+    def _isFarFutur(self, jour):
+        # check si jour est dans le futur "lointain"
+        # pour autoriser les saisies dans le futur mais pas a plus de 6 mois
+        y, m, d = [int(x) for x in jour.split("-")]
+        j = datetime.date(y, m, d)
+        # 6 mois ~ 182 jours:
+        return j - datetime.date.today() > datetime.timedelta(182)
+
+    security.declareProtected(ScoView, "is_work_saturday")
+
+    def is_work_saturday(self):
+        "Vrai si le samedi est travaillé"
+        return int(self.get_preference("work_saturday"))
+
+    def day_names(self):
+        """Returns week day names.
+        If work_saturday property is set, include saturday
+        """
+        if self.is_work_saturday():
+            return ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"]
+        else:
+            return ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi"]
+
+    security.declareProtected(ScoView, "ListMondays")
+
+    def ListMondays(self, year=None, REQUEST=None):
+        """return list of mondays (ISO dates), from september to june
+        """
+        if not year:
+            year = AnneeScolaire(REQUEST)
+        d = ddmmyyyy("1/9/%d" % year, work_saturday=self.is_work_saturday())
+        while d.weekday != 0:
+            d = d.next()
+        end = ddmmyyyy("1/7/%d" % (year + 1), work_saturday=self.is_work_saturday())
+        L = [d]
+        while d < end:
+            d = d.next(days=7)
+            L.append(d)
+        return map(lambda x: x.ISO(), L)
+
+    security.declareProtected(ScoView, "NextISODay")
+
+    def NextISODay(self, date):
+        "return date after date"
+        d = ddmmyyyy(date, fmt="iso", work_saturday=self.is_work_saturday())
+        return d.next().ISO()
+
+    security.declareProtected(ScoView, "DateRangeISO")
+
+    def DateRangeISO(self, date_beg, date_end, workable=1):
+        """returns list of dates in [date_beg,date_end]
+        workable = 1 => keeps only workable days"""
+        if not date_beg:
+            raise ScoValueError("pas de date spécifiée !")
+        if not date_end:
+            date_end = date_beg
+        r = []
+        cur = ddmmyyyy(date_beg, work_saturday=self.is_work_saturday())
+        end = ddmmyyyy(date_end, work_saturday=self.is_work_saturday())
+        while cur <= end:
+            if (not workable) or cur.iswork():
+                r.append(cur)
+            cur = cur.next()
+
+        return map(lambda x: x.ISO(), r)
+
+    # ------------ HTML Interfaces
+    security.declareProtected(ScoAbsChange, "SignaleAbsenceGrHebdo")
+
+    def SignaleAbsenceGrHebdo(
+        self, datelundi, group_ids=[], destination="", moduleimpl_id=None, REQUEST=None
+    ):
+        "Saisie hebdomadaire des absences"
+        if not moduleimpl_id:
+            moduleimp_id = None
+
+        groups_infos = sco_groups_view.DisplayedGroupsInfos(
+            self, group_ids, REQUEST=REQUEST
+        )
+        if not groups_infos.members:
+            return (
+                self.sco_header(page_title="Saisie des absences", REQUEST=REQUEST)
+                + "<h3>Aucun étudiant !</h3>"
+                + self.sco_footer(REQUEST)
+            )
+
+        base_url = "SignaleAbsenceGrHebdo?datelundi=%s&amp;%s&amp;destination=%s" % (
+            datelundi,
+            groups_infos.groups_query_args,
+            urllib.quote(destination),
+        )
+
+        formsemestre_id = groups_infos.formsemestre_id
+        etuds = [
+            self.getEtudInfo(etudid=m["etudid"], filled=True)[0]
+            for m in groups_infos.members
+        ]
+        nt = self.Notes._getNotesCache().get_NotesTable(self.Notes, formsemestre_id)
+        sem = sco_formsemestre.do_formsemestre_list(
+            self, {"formsemestre_id": formsemestre_id}
+        )[0]
+
+        # calcule dates jours de cette semaine
+        datessem = [DateDMYtoISO(datelundi)]
+        for jour in self.day_names()[1:]:
+            datessem.append(self.NextISODay(datessem[-1]))
+
+        #
+        if groups_infos.tous_les_etuds_du_sem:
+            gr_tit = "en"
+        else:
+            if len(groups_infos.group_ids) > 1:
+                p = "des groupes"
+            else:
+                p = "du groupe"
+            gr_tit = (
+                p + '<span class="fontred">' + groups_infos.groups_titles + "</span>"
+            )
+
+        H = [
+            self.sco_header(
+                page_title="Saisie hebdomadaire des absences",
+                init_qtip=True,
+                javascripts=["js/etud_info.js", "js/abs_ajax.js"],
+                no_side_bar=1,
+                REQUEST=REQUEST,
+            ),
+            """<table border="0" cellspacing="16"><tr><td>
+              <h2>Saisie des absences %s %s, 
+              <span class="fontred">semaine du lundi %s</span></h2>
+
+              <p><a href="index_html">Annuler</a></p>
+
+              <p>
+              <form action="doSignaleAbsenceGrHebdo" method="post" action="%s">              
+              """
+            % (gr_tit, sem["titre_num"], datelundi, REQUEST.URL0),
+        ]
+        #
+        modimpls_list = []
+        # Initialize with first student
+        ues = nt.get_ues(etudid=etuds[0]["etudid"])
+        for ue in ues:
+            modimpls_list += nt.get_modimpls(ue_id=ue["ue_id"])
+
+        # Add modules other students are subscribed to
+        for etud in etuds[1:]:
+            modimpls_etud = []
+            ues = nt.get_ues(etudid=etud["etudid"])
+            for ue in ues:
+                modimpls_etud += nt.get_modimpls(ue_id=ue["ue_id"])
+            modimpls_list += [m for m in modimpls_etud if m not in modimpls_list]
+
+        menu_module = ""
+        for modimpl in modimpls_list:
+            if modimpl["moduleimpl_id"] == moduleimpl_id:
+                sel = "selected"
+            else:
+                sel = ""
+            menu_module += (
+                """<option value="%(modimpl_id)s" %(sel)s>%(modname)s</option>\n"""
+                % {
+                    "modimpl_id": modimpl["moduleimpl_id"],
+                    "modname": modimpl["module"]["code"]
+                    + " "
+                    + (modimpl["module"]["abbrev"] or modimpl["module"]["titre"]),
+                    "sel": sel,
+                }
+            )
+        if moduleimpl_id:
+            sel = ""
+        else:
+            sel = "selected"  # aucun module specifie
+
+        H.append(
+            """
+ Module concerné par ces absences (optionnel): <select id="moduleimpl_id" name="moduleimpl_id" onchange="document.location='%(url)s&amp;moduleimpl_id='+document.getElementById('moduleimpl_id').value">
+    <option value="" %(sel)s>non spécifié</option>
+    %(menu_module)s
+    </select>
+    </p>"""
+            % {"menu_module": menu_module, "url": base_url, "sel": sel}
+        )
+
+        H += self._gen_form_saisie_groupe(
+            etuds, self.day_names(), datessem, destination, None, moduleimpl_id
+        )
+
+        H.append(self.sco_footer(REQUEST))
+        return "\n".join(H)
+
+    security.declareProtected(ScoAbsChange, "SignaleAbsenceGrSemestre")
+
+    def SignaleAbsenceGrSemestre(
+        self,
+        datedebut,
+        datefin,
+        destination="",
+        group_ids=[],  # list of groups to display
+        nbweeks=4,  # ne montre que les nbweeks dernieres semaines
+        moduleimpl_id=None,
+        REQUEST=None,
+    ):
+        """Saisie des absences sur une journée sur un semestre (ou intervalle de dates) entier
+        """
+        # log('SignaleAbsenceGrSemestre: moduleimpl_id=%s destination=%s' % (moduleimpl_id, destination))
+        groups_infos = sco_groups_view.DisplayedGroupsInfos(
+            self, group_ids, REQUEST=REQUEST
+        )
+        if not groups_infos.members:
+            return (
+                self.sco_header(page_title="Saisie des absences", REQUEST=REQUEST)
+                + "<h3>Aucun étudiant !</h3>"
+                + self.sco_footer(REQUEST)
+            )
+
+        formsemestre_id = groups_infos.formsemestre_id
+        etuds = [
+            self.getEtudInfo(etudid=m["etudid"], filled=True)[0]
+            for m in groups_infos.members
+        ]
+
+        if not moduleimpl_id:
+            moduleimp_id = None
+        base_url_noweeks = (
+            "SignaleAbsenceGrSemestre?datedebut=%s&amp;datefin=%s&amp;%s&amp;destination=%s"
+            % (
+                datedebut,
+                datefin,
+                groups_infos.groups_query_args,
+                urllib.quote(destination),
+            )
+        )
+        base_url = (
+            base_url_noweeks + "&amp;nbweeks=%s" % nbweeks
+        )  # sans le moduleimpl_id
+
+        if etuds:
+            nt = self.Notes._getNotesCache().get_NotesTable(self.Notes, formsemestre_id)
+            sem = sco_formsemestre.do_formsemestre_list(
+                self, {"formsemestre_id": formsemestre_id}
+            )[0]
+
+        jourdebut = ddmmyyyy(datedebut, work_saturday=self.is_work_saturday())
+        jourfin = ddmmyyyy(datefin, work_saturday=self.is_work_saturday())
+        today = ddmmyyyy(
+            time.strftime("%d/%m/%Y", time.localtime()),
+            work_saturday=self.is_work_saturday(),
+        )
+        today.next()
+        if jourfin > today:  # ne propose jamais les semaines dans le futur
+            jourfin = today
+        if jourdebut > today:
+            raise ScoValueError("date de début dans le futur (%s) !" % jourdebut)
+        #
+        if not jourdebut.iswork() or jourdebut > jourfin:
+            raise ValueError(
+                "date debut invalide (%s, ouvrable=%d)"
+                % (str(jourdebut), jourdebut.iswork())
+            )
+        # calcule dates
+        dates = []  # ddmmyyyy instances
+        d = ddmmyyyy(datedebut, work_saturday=self.is_work_saturday())
+        while d <= jourfin:
+            dates.append(d)
+            d = d.next(7)  # avance d'une semaine
+        #
+        msg = "Montrer seulement les 4 dernières semaines"
+        nwl = 4
+        if nbweeks:
+            nbweeks = int(nbweeks)
+            if nbweeks > 0:
+                dates = dates[-nbweeks:]
+                msg = "Montrer toutes les semaines"
+                nwl = 0
+        url_link_semaines = base_url_noweeks + "&amp;nbweeks=%s" % nwl
+        if moduleimpl_id:
+            url_link_semaines += "&amp;moduleimpl_id=" + moduleimpl_id
+        #
+        colnames = [str(x) for x in dates]
+        dates = [x.ISO() for x in dates]
+        dayname = self.day_names()[jourdebut.weekday]
+
+        if groups_infos.tous_les_etuds_du_sem:
+            gr_tit = "en"
+        else:
+            if len(groups_infos.group_ids) > 1:
+                p = "des groupes"
+            else:
+                p = "du groupe"
+            gr_tit = (
+                p + '<span class="fontred">' + groups_infos.groups_titles + "</span>"
+            )
+
+        H = [
+            self.sco_header(
+                page_title="Saisie des absences",
+                init_qtip=True,
+                javascripts=["js/etud_info.js", "js/abs_ajax.js"],
+                no_side_bar=1,
+                REQUEST=REQUEST,
+            ),
+            """<table border="0" cellspacing="16"><tr><td>
+              <h2>Saisie des absences %s %s, 
+              les <span class="fontred">%s</span></h2>
+              <p>
+              <a href="%s">%s</a>
+              <form action="doSignaleAbsenceGrSemestre" method="post">              
+              """
+            % (gr_tit, sem["titre_num"], dayname, url_link_semaines, msg),
+        ]
+        #
+        if etuds:
+            modimpls_list = []
+            # Initialize with first student
+            ues = nt.get_ues(etudid=etuds[0]["etudid"])
+            for ue in ues:
+                modimpls_list += nt.get_modimpls(ue_id=ue["ue_id"])
+
+            # Add modules other students are subscribed to
+            for etud in etuds[1:]:
+                modimpls_etud = []
+                ues = nt.get_ues(etudid=etud["etudid"])
+                for ue in ues:
+                    modimpls_etud += nt.get_modimpls(ue_id=ue["ue_id"])
+                modimpls_list += [m for m in modimpls_etud if m not in modimpls_list]
+
+            menu_module = ""
+            for modimpl in modimpls_list:
+                if modimpl["moduleimpl_id"] == moduleimpl_id:
+                    sel = "selected"
+                else:
+                    sel = ""
+                menu_module += (
+                    """<option value="%(modimpl_id)s" %(sel)s>%(modname)s</option>\n"""
+                    % {
+                        "modimpl_id": modimpl["moduleimpl_id"],
+                        "modname": modimpl["module"]["code"]
+                        + " "
+                        + (modimpl["module"]["abbrev"] or modimpl["module"]["titre"]),
+                        "sel": sel,
+                    }
+                )
+            if moduleimpl_id:
+                sel = ""
+            else:
+                sel = "selected"  # aucun module specifie
+            H.append(
+                """<p>
+    Module concerné par ces absences (optionnel): <select id="moduleimpl_id" name="moduleimpl_id" onchange="document.location='%(url)s&amp;moduleimpl_id='+document.getElementById('moduleimpl_id').value">
+    <option value="" %(sel)s>non spécifié</option>
+    %(menu_module)s
+    </select>
+</p>"""
+                % {"menu_module": menu_module, "url": base_url, "sel": sel}
+            )
+
+        H += self._gen_form_saisie_groupe(
+            etuds, colnames, dates, destination, dayname, moduleimpl_id
+        )
+        H.append(self.sco_footer(REQUEST))
+        return "\n".join(H)
+
+    def _gen_form_saisie_groupe(
+        self, etuds, colnames, dates, destination="", dayname="", moduleimpl_id=None
+    ):
+        H = [
+            """
+        <script type="text/javascript">
+        function colorize(obj) {
+             if (obj.checked) {
+                 obj.parentNode.className = 'absent';
+             } else {
+                 obj.parentNode.className = 'present';
+             }
+        }
+        function on_toggled(obj, etudid, dat) {
+            colorize(obj);
+            if (obj.checked) {
+                ajaxFunction('add', etudid, dat);
+            } else {
+                ajaxFunction('remove', etudid, dat);
+            }
+        }
+        </script>
+        <div id="AjaxDiv"></div>
+        <br/>
+        <table rules="cols" frame="box" class="abs_form_table">
+        <tr><th class="formabs_contetud">%d étudiants</th>
+        """
+            % len(etuds)
+        ]
+        # Titres colonnes
+        if dayname:
+            for jour in colnames:
+                H.append(
+                    '<th colspan="2" width="100px" style="padding-left: 5px; padding-right: 5px;">'
+                    + dayname
+                    + "</th>"
+                )
+            H.append("</tr><tr><td>&nbsp;</td>")
+
+        for jour in colnames:
+            H.append(
+                '<th colspan="2" width="100px" style="padding-left: 5px; padding-right: 5px;">'
+                + jour
+                + "</th>"
+            )
+
+        H.append("</tr><tr><td>&nbsp;</td>")
+        H.append("<th>AM</th><th>PM</th>" * len(colnames))
+        H.append("</tr>")
+        #
+        if not etuds:
+            H.append(
+                '<tr><td><span class="redboldtext">Aucun étudiant inscrit !</span></td></tr>'
+            )
+        i = 1
+        for etud in etuds:
+            i += 1
+            etudid = etud["etudid"]
+            # UE capitalisee dans semestre courant ?
+            cap = []
+            if etud["cursem"]:
+                nt = self.Notes._getNotesCache().get_NotesTable(
+                    self.Notes, etud["cursem"]["formsemestre_id"]
+                )  # > get_ues, get_etud_ue_status
+                for ue in nt.get_ues():
+                    status = nt.get_etud_ue_status(etudid, ue["ue_id"])
+                    if status["is_capitalized"]:
+                        cap.append(ue["acronyme"])
+            if cap:
+                capstr = ' <span class="capstr">(%s cap.)</span>' % ", ".join(cap)
+            else:
+                capstr = ""
+
+            tr_class = ("row_1", "row_2", "row_3")[i % 3]
+            td_matin_class = ("matin_1", "matin_2", "matin_3")[i % 3]
+
+            H.append(
+                '<tr class="%s"><td><b class="etudinfo" id="%s"><a class="discretelink" href="ficheEtud?etudid=%s" target="new">%s</a></b>%s</td>'
+                % (tr_class, etudid, etudid, etud["nomprenom"], capstr)
+            )
+            for date in dates:
+                # matin
+                if self.CountAbs(etudid, date, date, True, moduleimpl_id=moduleimpl_id):
+                    checked = "checked"
+                else:
+                    checked = ""
+                H.append(
+                    '<td class="%s"><input type="checkbox" name="abslist:list" value="%s" %s onclick="on_toggled(this, \'%s\', \'%s\')"/></td>'
+                    % (
+                        td_matin_class,
+                        etudid + ":" + date + ":" + "am",
+                        checked,
+                        etudid,
+                        date + ":am",
+                    )
+                )
+                # apres midi
+                if self.CountAbs(
+                    etudid, date, date, False, moduleimpl_id=moduleimpl_id
+                ):
+                    checked = "checked"
+                else:
+                    checked = ""
+                H.append(
+                    '<td><input type="checkbox" name="abslist:list" value="%s" %s onclick="on_toggled(this, \'%s\', \'%s\')"/></td>'
+                    % (etudid + ":" + date + ":" + "pm", checked, etudid, date + ":pm")
+                )
+            H.append("</tr>")
+        H.append("</table>")
+        # place la liste des etudiants et les dates pour pouvoir effacer les absences
+        H.append(
+            '<input type="hidden" name="etudids" value="%s"/>'
+            % ",".join([etud["etudid"] for etud in etuds])
+        )
+        H.append('<input type="hidden" name="datedebut" value="%s"/>' % dates[0])
+        H.append('<input type="hidden" name="datefin" value="%s"/>' % dates[-1])
+        H.append('<input type="hidden" name="dates" value="%s"/>' % ",".join(dates))
+        H.append(
+            '<input type="hidden" name="destination" value="%s"/>'
+            % urllib.quote(destination)
+        )
+        #
+        # version pour formulaire avec AJAX (Yann LB)
+        H.append(
+            """
+            <p><input type="button" value="Retour" onClick="window.location='%s'"/>
+            </p>
+            </form>
+            </p>
+            </td></tr></table>
+            <p class="help">Les cases cochées correspondent à des absences.
+            Les absences saisies ne sont pas justifiées (sauf si un justificatif a été entré
+            par ailleurs).
+            </p><p class="help">Si vous "décochez" une case,  l'absence correspondante sera supprimée.
+            Attention, les modifications sont automatiquement entregistrées au fur et à mesure.
+            </p>
+        """
+            % destination
+        )
+        return H
+
+    def _TablesAbsEtud(
+        self,
+        etudid,
+        datedebut,
+        with_evals=True,
+        format="html",
+        absjust_only=0,
+        REQUEST=None,
+    ):
+        """Tables des absences justifiees et non justifiees d'un étudiant sur l'année en cours
+        """
+        absjust = self.ListeAbsJust(etudid=etudid, datedebut=datedebut)
+        absnonjust = self.ListeAbsNonJust(etudid=etudid, datedebut=datedebut)
+        # examens ces jours là ?
+        if with_evals:
+            cnx = self.GetDBConnexion()
+            cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+            for a in absnonjust + absjust:
+                cursor.execute(
+                    """select eval.*
+                from notes_evaluation eval, notes_moduleimpl_inscription mi, notes_moduleimpl m
+                where eval.jour = %(jour)s and eval.moduleimpl_id = m.moduleimpl_id
+                and mi.moduleimpl_id = m.moduleimpl_id and mi.etudid = %(etudid)s""",
+                    {"jour": a["jour"].strftime("%Y-%m-%d"), "etudid": etudid},
+                )
+                a["evals"] = cursor.dictfetchall()
+                cursor.execute(
+                    """SELECT mi.moduleimpl_id
+                from  absences abs, notes_moduleimpl_inscription mi, notes_moduleimpl m
+                where abs.matin = %(matin)s and abs.jour = %(jour)s and abs.etudid=%(etudid)s and abs.moduleimpl_id=mi.moduleimpl_id and mi.moduleimpl_id=m.moduleimpl_id 
+                and mi.etudid = %(etudid)s""",
+                    {
+                        "matin": bool(a["matin"]),
+                        "jour": a["jour"].strftime("%Y-%m-%d"),
+                        "etudid": etudid,
+                    },
+                )
+                a["absent"] = cursor.dictfetchall()
+
+        def matin(x):
+            if x:
+                return "matin"
+            else:
+                return "après midi"
+
+        def descr_exams(a):
+            if not a.has_key("evals"):
+                return ""
+            ex = []
+            for ev in a["evals"]:
+                mod = self.Notes.do_moduleimpl_withmodule_list(
+                    moduleimpl_id=ev["moduleimpl_id"]
+                )[0]
+                if format == "html":
+                    ex.append(
+                        '<a href="Notes/moduleimpl_status?moduleimpl_id=%s">%s</a>'
+                        % (mod["moduleimpl_id"], mod["module"]["code"])
+                    )
+                else:
+                    ex.append(mod["module"]["code"])
+            if ex:
+                return ", ".join(ex)
+            return ""
+
+        def descr_abs(a):
+            ex = []
+            for ev in a.get("absent", []):
+                mod = self.Notes.do_moduleimpl_withmodule_list(
+                    moduleimpl_id=ev["moduleimpl_id"]
+                )[0]
+                if format == "html":
+                    ex.append(
+                        '<a href="Notes/moduleimpl_status?moduleimpl_id=%s">%s</a>'
+                        % (mod["moduleimpl_id"], mod["module"]["code"])
+                    )
+                else:
+                    ex.append(mod["module"]["code"])
+            if ex:
+                return ", ".join(ex)
+            return ""
+
+        # ajoute date formatée et évaluations
+        for L in (absnonjust, absjust):
+            for a in L:
+                if with_evals:
+                    a["exams"] = descr_exams(a)
+                a["datedmy"] = a["jour"].strftime("%d/%m/%Y")
+                a["matin_o"] = int(a["matin"])
+                a["matin"] = matin(a["matin"])
+                index = a["description"].find(")")
+                if index != -1:
+                    a["motif"] = a["description"][1:index]
+                else:
+                    a["motif"] = ""
+                a["description"] = descr_abs(a) or ""
+
+        # ajoute lien pour justifier
+        if format == "html":
+            for a in absnonjust:
+                a["justlink"] = "<em>justifier</em>"
+                a["_justlink_target"] = (
+                    "doJustifAbsence?etudid=%s&amp;datedebut=%s&amp;datefin=%s&amp;demijournee=%s"
+                    % (etudid, a["datedmy"], a["datedmy"], a["matin_o"])
+                )
+        #
+        titles = {
+            "datedmy": "Date",
+            "matin": "",
+            "exams": "Examens ce jour",
+            "justlink": "",
+            "description": "Modules",
+            "motif": "Motif",
+        }
+        columns_ids = ["datedmy", "matin"]
+        if with_evals:
+            columns_ids.append("exams")
+
+        columns_ids.append("description")
+        columns_ids.append("motif")
+        if format == "html":
+            columns_ids.append("justlink")
+
+        return titles, columns_ids, absnonjust, absjust
+
+    security.declareProtected(ScoView, "EtatAbsencesGr")  # ported from dtml
+
+    def EtatAbsencesGr(
+        self,
+        group_ids=[],  # list of groups to display
+        debut="",
+        fin="",
+        with_boursier=True,  # colonne boursier
+        format="html",
+        REQUEST=None,
+    ):
+        """Liste les absences de groupes
+        """
+        datedebut = DateDMYtoISO(debut)
+        datefin = DateDMYtoISO(fin)
+        # Informations sur les groupes à afficher:
+        groups_infos = sco_groups_view.DisplayedGroupsInfos(
+            self, group_ids, REQUEST=REQUEST
+        )
+        formsemestre_id = groups_infos.formsemestre_id
+        sem = groups_infos.formsemestre
+
+        # Construit tableau (etudid, statut, nomprenom, nbJust, nbNonJust, NbTotal)
+        T = []
+        for m in groups_infos.members:
+            etud = self.getEtudInfo(etudid=m["etudid"], filled=True)[0]
+            nbabs = self.CountAbs(etudid=etud["etudid"], debut=datedebut, fin=datefin)
+            nbabsjust = self.CountAbsJust(
+                etudid=etud["etudid"], debut=datedebut, fin=datefin
+            )
+            nbjustifs_noabs = len(
+                self.ListeJustifs(
+                    etudid=etud["etudid"], datedebut=datedebut, only_no_abs=True
+                )
+            )
+            # retrouve sem dans etud['sems']
+            s = None
+            for s in etud["sems"]:
+                if s["formsemestre_id"] == formsemestre_id:
+                    break
+            if not s or s["formsemestre_id"] != formsemestre_id:
+                raise ValueError(
+                    "EtatAbsencesGr: can't retreive sem"
+                )  # bug or malicious arg
+            T.append(
+                {
+                    "etudid": etud["etudid"],
+                    "etatincursem": s["ins"]["etat"],
+                    "nomprenom": etud["nomprenom"],
+                    "nbabsjust": nbabsjust,
+                    "nbabsnonjust": nbabs - nbabsjust,
+                    "nbabs": nbabs,
+                    "nbjustifs_noabs": nbjustifs_noabs,
+                    "_nomprenom_target": "CalAbs?etudid=%s" % etud["etudid"],
+                    "_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % etud["etudid"],
+                    "boursier": etud["boursier"],
+                }
+            )
+            if s["ins"]["etat"] == "D":
+                T[-1]["_css_row_class"] = "etuddem"
+                T[-1]["nomprenom"] += " (dem)"
+        columns_ids = [
+            "nomprenom",
+            "nbjustifs_noabs",
+            "nbabsjust",
+            "nbabsnonjust",
+            "nbabs",
+        ]
+        if with_boursier:
+            columns_ids[1:1] = ["boursier"]
+        if groups_infos.tous_les_etuds_du_sem:
+            gr_tit = ""
+        else:
+            if len(groups_infos.group_ids) > 1:
+                p = "des groupes"
+            else:
+                p = "du groupe"
+            if format == "html":
+                h = ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
+            else:
+                h = groups_infos.groups_titles
+            gr_tit = p + h
+
+        title = "Etat des absences %s" % gr_tit
+        if format == "xls" or format == "xml":
+            columns_ids = ["etudid"] + columns_ids
+        tab = GenTable(
+            columns_ids=columns_ids,
+            rows=T,
+            preferences=self.get_preferences(formsemestre_id),
+            titles={
+                "etatincursem": "Etat",
+                "nomprenom": "Nom",
+                "nbabsjust": "Justifiées",
+                "nbabsnonjust": "Non justifiées",
+                "nbabs": "Total",
+                "nbjustifs_noabs": "Justifs non utilisés",
+                "boursier": "Bourse",
+            },
+            html_sortable=True,
+            html_class="table_leftalign",
+            html_header=self.sco_header(
+                REQUEST,
+                page_title=title,
+                init_qtip=True,
+                javascripts=["js/etud_info.js"],
+            ),
+            html_title=self.Notes.html_sem_header(
+                REQUEST, "%s" % title, sem, with_page_header=False
+            )
+            + "<p>Période du %s au %s (nombre de <b>demi-journées</b>)<br/>"
+            % (debut, fin),
+            base_url="%s&amp;formsemestre_id=%s&amp;debut=%s&amp;fin=%s"
+            % (groups_infos.base_url, formsemestre_id, debut, fin),
+            filename="etat_abs_"
+            + make_filename(
+                "%s de %s" % (groups_infos.groups_filename, sem["titreannee"])
+            ),
+            caption=title,
+            html_next_section="""</table>
+<p class="help">
+Justifs non utilisés: nombre de demi-journées avec justificatif mais sans absences relevées.
+</p>
+<p class="help">
+Cliquez sur un nom pour afficher le calendrier des absences<br/>
+ou entrez une date pour visualiser les absents un jour donné&nbsp;:
+</p>
+<div style="margin-bottom: 10px;">
+<form action="EtatAbsencesDate" method="get" action="%s">
+<input type="hidden" name="formsemestre_id" value="%s">
+%s
+<input type="text" name="date" size="10" class="datepicker"/>
+<input type="submit" name="" value="visualiser les absences">
+</form></div>
+                        """
+            % (REQUEST.URL0, formsemestre_id, groups_infos.get_form_elem()),
+        )
+        return tab.make_page(self, format=format, REQUEST=REQUEST)
+
+    security.declareProtected(ScoView, "EtatAbsencesDate")  # ported from dtml
+
+    def EtatAbsencesDate(
+        self, group_ids=[], date=None, REQUEST=None  # list of groups to display
+    ):
+        """Etat des absences pour un groupe à une date donnée
+        """
+        # Informations sur les groupes à afficher:
+        groups_infos = sco_groups_view.DisplayedGroupsInfos(
+            self, group_ids, REQUEST=REQUEST
+        )
+        formsemestre_id = groups_infos.formsemestre_id
+        sem = sco_formsemestre.do_formsemestre_list(
+            self, {"formsemestre_id": formsemestre_id}
+        )[0]
+        H = [self.sco_header(page_title="Etat des absences", REQUEST=REQUEST)]
+        if date:
+            dateiso = DateDMYtoISO(date)
+            nbetud = 0
+            t_nbabsjustam = 0
+            t_nbabsam = 0
+            t_nbabsjustpm = 0
+            t_nbabspm = 0
+            etuds = self.getEtudInfoGroupes(groups_infos.group_ids)
+            H.append("<h2>Etat des absences le %s</h2>" % date)
+            H.append(
+                """<table border="0" cellspacing="4" cellpadding="0">
+             <tr><th>&nbsp;</th>
+            <th style="width: 10em;">Matin</th><th style="width: 10em;">Après-midi</th></tr>
+            """
+            )
+            for etud in groups_infos.members:
+                nbabsam = self.CountAbs(
+                    etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=1
+                )
+                nbabspm = self.CountAbs(
+                    etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=0
+                )
+                if (nbabsam != 0) or (nbabspm != 0):
+                    nbetud += 1
+                    nbabsjustam = self.CountAbsJust(
+                        etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=1
+                    )
+                    nbabsjustpm = self.CountAbsJust(
+                        etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=0
+                    )
+                    H.append(
+                        """<tr bgcolor="#FFFFFF"><td>
+                     <a href="CalAbs?etudid=%(etudid)s"><font color="#A00000">%(nomprenom)s</font></a></td><td align="center">"""
+                        % etud
+                    )  # """
+                    if nbabsam != 0:
+                        if nbabsjustam:
+                            H.append("Just.")
+                            t_nbabsjustam += 1
+                        else:
+                            H.append("Abs.")
+                            t_nbabsam += 1
+                    else:
+                        H.append("")
+                    H.append('</td><td align="center">')
+                    if nbabspm != 0:
+                        if nbabsjustpm:
+                            H.append("Just.")
+                            t_nbabsjustam += 1
+                        else:
+                            H.append("Abs.")
+                            t_nbabspm += 1
+                    else:
+                        H.append("")
+                    H.append("</td></tr>")
+            H.append(
+                """<tr bgcolor="#FFFFFF"><td></td><td>%d abs, %d just.</td><td>%d abs, %d just.</td></tr>"""
+                % (t_nbabsam, t_nbabsjustam, t_nbabspm, t_nbabsjustpm)
+            )
+            H.append("</table>")
+            if nbetud == 0:
+                H.append("<p>Aucune absence !</p>")
+        else:
+            H.append(
+                """<h2>Erreur: vous n'avez pas choisi de date !</h2>
+              <a class="stdlink" href="%s">Continuer</a>"""
+                % REQUEST.HTTP_REFERER
+            )
+
+        return "\n".join(H) + self.sco_footer(REQUEST)
+
+    # ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail)
+    security.declareProtected(ScoAbsAddBillet, "AddBilletAbsence")
+
+    def AddBilletAbsence(
+        self,
+        begin,
+        end,
+        description,
+        etudid=False,
+        code_nip=None,
+        code_ine=None,
+        justified=True,
+        REQUEST=None,
+        xml_reply=True,
+    ):
+        """Memorise un "billet"
+        begin et end sont au format ISO (eg "1999-01-08 04:05:06")
+        """
+        t0 = time.time()
+        # check etudid
+        etuds = self.getEtudInfo(
+            etudid=etudid, code_nip=code_nip, REQUEST=REQUEST, filled=True
+        )
+        if not etuds:
+            return log_unknown_etud(self, REQUEST=REQUEST)
+        etud = etuds[0]
+        # check dates
+        begin_date = ParseDateTimeUTC(begin)  # may raises ValueError
+        end_date = ParseDateTimeUTC(end)
+        if begin_date > end_date:
+            raise ValueError("invalid dates")
+        #
+        justified = int(justified)
+        #
+        cnx = self.GetDBConnexion()
+        billet_id = billet_absence_create(
+            cnx,
+            {
+                "etudid": etud["etudid"],
+                "abs_begin": begin,
+                "abs_end": end,
+                "description": description,
+                "etat": 0,
+                "justified": justified,
+            },
+        )
+        if xml_reply:
+            # Renvoie le nouveau billet en XML
+            if REQUEST:
+                REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+
+            billets = billet_absence_list(cnx, {"billet_id": billet_id})
+            tab = self._tableBillets(billets, etud=etud)
+            log(
+                "AddBilletAbsence: new billet_id=%s (%gs)"
+                % (billet_id, time.time() - t0)
+            )
+            return tab.make_page(self, REQUEST=REQUEST, format="xml")
+        else:
+            return billet_id
+
+    security.declareProtected(ScoAbsAddBillet, "AddBilletAbsenceForm")
+
+    def AddBilletAbsenceForm(self, etudid, REQUEST=None):
+        """Formulaire ajout billet (pour tests seulement, le vrai formulaire accessible aux etudiants
+        étant sur le portail étudiant).
+        """
+        etud = self.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+        H = [
+            self.sco_header(
+                REQUEST, page_title="Billet d'absence de %s" % etud["nomprenom"]
+            )
+        ]
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (
+                ("etudid", {"input_type": "hidden"}),
+                ("begin", {"input_type": "date"}),
+                ("end", {"input_type": "date"}),
+                (
+                    "justified",
+                    {"input_type": "boolcheckbox", "default": 0, "title": "Justifiée"},
+                ),
+                ("description", {"input_type": "textarea"}),
+            ),
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + self.sco_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+        else:
+            e = tf[2]["begin"].split("/")
+            begin = e[2] + "-" + e[1] + "-" + e[0] + " 00:00:00"
+            e = tf[2]["end"].split("/")
+            end = e[2] + "-" + e[1] + "-" + e[0] + " 00:00:00"
+            log(
+                self.AddBilletAbsence(
+                    begin,
+                    end,
+                    tf[2]["description"],
+                    etudid=etudid,
+                    xml_reply=True,
+                    justified=tf[2]["justified"],
+                )
+            )
+            return REQUEST.RESPONSE.redirect("listeBilletsEtud?etudid=" + etudid)
+
+    def _tableBillets(self, billets, etud=None, title=""):
+        for b in billets:
+            if b["abs_begin"].hour < 12:
+                m = " matin"
+            else:
+                m = " après midi"
+            b["abs_begin_str"] = b["abs_begin"].strftime("%d/%m/%Y") + m
+            if b["abs_end"].hour < 12:
+                m = " matin"
+            else:
+                m = " après midi"
+            b["abs_end_str"] = b["abs_end"].strftime("%d/%m/%Y") + m
+            if b["etat"] == 0:
+                if b["justified"] == 0:
+                    b["etat_str"] = "à traiter"
+                else:
+                    b["etat_str"] = "à justifier"
+                b["_etat_str_target"] = (
+                    "ProcessBilletAbsenceForm?billet_id=%s" % b["billet_id"]
+                )
+                if etud:
+                    b["_etat_str_target"] += "&amp;etudid=%s" % etud["etudid"]
+                b["_billet_id_target"] = b["_etat_str_target"]
+            else:
+                b["etat_str"] = "ok"
+            if not etud:
+                # ajoute info etudiant
+                e = self.getEtudInfo(etudid=b["etudid"], filled=1)
+                if not e:
+                    b["nomprenom"] = "???"  # should not occur
+                else:
+                    b["nomprenom"] = e[0]["nomprenom"]
+                b["_nomprenom_target"] = "ficheEtud?etudid=%s" % b["etudid"]
+        if etud and not title:
+            title = "Billets d'absence déclarés par %(nomprenom)s" % etud
+        else:
+            title = title
+        columns_ids = ["billet_id"]
+        if not etud:
+            columns_ids += ["nomprenom"]
+        columns_ids += ["abs_begin_str", "abs_end_str", "description", "etat_str"]
+
+        tab = GenTable(
+            titles={
+                "billet_id": "Numéro",
+                "abs_begin_str": "Début",
+                "abs_end_str": "Fin",
+                "description": "Raison de l'absence",
+                "etat_str": "Etat",
+            },
+            columns_ids=columns_ids,
+            page_title=title,
+            html_title="<h2>%s</h2>" % title,
+            preferences=self.get_preferences(),
+            rows=billets,
+            html_sortable=True,
+        )
+        return tab
+
+    security.declareProtected(ScoView, "listeBilletsEtud")
+
+    def listeBilletsEtud(self, etudid=False, REQUEST=None, format="html"):
+        """Liste billets pour un etudiant
+        """
+        etuds = self.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)
+        if not etuds:
+            return log_unknown_etud(self, format=format, REQUEST=REQUEST)
+
+        etud = etuds[0]
+        cnx = self.GetDBConnexion()
+        billets = billet_absence_list(cnx, {"etudid": etud["etudid"]})
+        tab = self._tableBillets(billets, etud=etud)
+        return tab.make_page(self, REQUEST=REQUEST, format=format)
+
+    security.declareProtected(ScoView, "XMLgetBilletsEtud")
+
+    def XMLgetBilletsEtud(self, etudid=False, REQUEST=None):
+        """Liste billets pour un etudiant
+        """
+        if not self.get_preference("handle_billets_abs"):
+            return ""
+        t0 = time.time()
+        r = self.listeBilletsEtud(etudid, REQUEST=REQUEST, format="xml")
+        log("XMLgetBilletsEtud (%gs)" % (time.time() - t0))
+        return r
+
+    security.declareProtected(ScoView, "listeBillets")
+
+    def listeBillets(self, REQUEST=None):
+        """Page liste des billets non traités et formulaire recherche d'un billet"""
+        cnx = self.GetDBConnexion()
+        billets = billet_absence_list(cnx, {"etat": 0})
+        tab = self._tableBillets(billets)
+        T = tab.html()
+        H = [
+            self.sco_header(REQUEST, page_title="Billet d'absence non traités"),
+            "<h2>Billets d'absence en attente de traitement (%d)</h2>" % len(billets),
+        ]
+
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (("billet_id", {"input_type": "text", "title": "Numéro du billet"}),),
+            submitbutton=False,
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + T + self.sco_footer(REQUEST)
+        else:
+            return REQUEST.RESPONSE.redirect(
+                "ProcessBilletAbsenceForm?billet_id=" + tf[2]["billet_id"]
+            )
+
+    security.declareProtected(ScoAbsChange, "deleteBilletAbsence")
+
+    def deleteBilletAbsence(self, billet_id, REQUEST=None, dialog_confirmed=False):
+        """Supprime un billet.
+        """
+        cnx = self.GetDBConnexion()
+        billets = billet_absence_list(cnx, {"billet_id": billet_id})
+        if not billets:
+            return REQUEST.RESPONSE.redirect(
+                "listeBillets?head_message=Billet%%20%s%%20inexistant !" % billet_id
+            )
+        if not dialog_confirmed:
+            tab = self._tableBillets(billets)
+            return self.confirmDialog(
+                """<h2>Supprimer ce billet ?</h2>""" + tab.html(),
+                dest_url="",
+                REQUEST=REQUEST,
+                cancel_url="listeBillets",
+                parameters={"billet_id": billet_id},
+            )
+
+        billet_absence_delete(cnx, billet_id)
+
+        return REQUEST.RESPONSE.redirect("listeBillets?head_message=Billet%20supprimé")
+
+    def _ProcessBilletAbsence(self, billet, estjust, description, REQUEST):
+        """Traite un billet: ajoute absence(s) et éventuellement justificatifs,
+        et change l'état du billet à 1.
+        NB: actuellement, les heures ne sont utilisées que pour déterminer si matin et/ou après midi.
+        """
+        cnx = self.GetDBConnexion()
+        if billet["etat"] != 0:
+            log("billet=%s" % billet)
+            log("billet deja traité !")
+            return -1
+        n = 0  # nombre de demi-journées d'absence ajoutées
+        # 1-- ajout des absences (et justifs)
+        datedebut = billet["abs_begin"].strftime("%d/%m/%Y")
+        datefin = billet["abs_end"].strftime("%d/%m/%Y")
+        dates = self.DateRangeISO(datedebut, datefin)
+        # commence apres midi ?
+        if dates and billet["abs_begin"].hour > 11:
+            self._AddAbsence(
+                billet["etudid"], dates[0], 0, estjust, REQUEST, description=description
+            )
+            n += 1
+            dates = dates[1:]
+        # termine matin ?
+        if dates and billet["abs_end"].hour < 12:
+            self._AddAbsence(
+                billet["etudid"],
+                dates[-1],
+                1,
+                estjust,
+                REQUEST,
+                description=description,
+            )
+            n += 1
+            dates = dates[:-1]
+
+        for jour in dates:
+            self._AddAbsence(
+                billet["etudid"], jour, 0, estjust, REQUEST, description=description
+            )
+            self._AddAbsence(
+                billet["etudid"], jour, 1, estjust, REQUEST, description=description
+            )
+            n += 2
+
+        # 2- change etat du billet
+        billet_absence_edit(cnx, {"billet_id": billet["billet_id"], "etat": 1})
+
+        return n
+
+    security.declareProtected(ScoAbsChange, "ProcessBilletAbsenceForm")
+
+    def ProcessBilletAbsenceForm(self, billet_id, REQUEST=None):
+        """Formulaire traitement d'un billet"""
+        cnx = self.GetDBConnexion()
+        billets = billet_absence_list(cnx, {"billet_id": billet_id})
+        if not billets:
+            return REQUEST.RESPONSE.redirect(
+                "listeBillets?head_message=Billet%%20%s%%20inexistant !" % billet_id
+            )
+        billet = billets[0]
+        etudid = billet["etudid"]
+        etud = self.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+
+        H = [
+            self.sco_header(
+                REQUEST,
+                page_title="Traitement billet d'absence de %s" % etud["nomprenom"],
+            ),
+            '<h2>Traitement du billet %s : <a class="discretelink" href="ficheEtud?etudid=%s">%s</a></h2>'
+            % (billet_id, etudid, etud["nomprenom"]),
+        ]
+
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (
+                ("billet_id", {"input_type": "hidden"}),
+                (
+                    "etudid",
+                    {"input_type": "hidden"},
+                ),  # pour centrer l'UI sur l'étudiant
+                (
+                    "estjust",
+                    {"input_type": "boolcheckbox", "title": "Absences justifiées"},
+                ),
+                ("description", {"input_type": "text", "size": 42, "title": "Raison"}),
+            ),
+            initvalues={
+                "description": billet["description"],
+                "estjust": billet["justified"],
+                "etudid": etudid,
+            },
+            submitlabel="Enregistrer ces absences",
+        )
+        if tf[0] == 0:
+            tab = self._tableBillets([billet], etud=etud)
+            H.append(tab.html())
+            if billet["justified"] == 1:
+                H.append(
+                    """<p>L'étudiant pense pouvoir justifier cette absence.<br/><em>Vérifiez le justificatif avant d'enregistrer.</em></p>"""
+                )
+            F = (
+                """<p><a class="stdlink" href="deleteBilletAbsence?billet_id=%s">Supprimer ce billet</a> (utiliser en cas d'erreur, par ex. billet en double)</p>"""
+                % billet_id
+            )
+            F += '<p><a class="stdlink" href="listeBillets">Liste de tous les billets en attente</a></p>'
+
+            return "\n".join(H) + "<br/>" + tf[1] + F + self.sco_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+        else:
+            n = self._ProcessBilletAbsence(
+                billet, tf[2]["estjust"], tf[2]["description"], REQUEST
+            )
+            if tf[2]["estjust"]:
+                j = "justifiées"
+            else:
+                j = "non justifiées"
+            H.append('<div class="head_message">')
+            if n > 0:
+                H.append("%d absences (1/2 journées) %s ajoutées" % (n, j))
+            elif n == 0:
+                H.append("Aucun jour d'absence dans les dates indiquées !")
+            elif n < 0:
+                H.append("Ce billet avait déjà été traité !")
+            H.append(
+                '</div><p><a class="stdlink" href="listeBillets">Autre billets en attente</a></p><h4>Billets déclarés par %s</h4>'
+                % (etud["nomprenom"])
+            )
+            billets = billet_absence_list(cnx, {"etudid": etud["etudid"]})
+            tab = self._tableBillets(billets, etud=etud)
+            H.append(tab.html())
+            return "\n".join(H) + self.sco_footer(REQUEST)
+
+    security.declareProtected(ScoView, "XMLgetAbsEtud")
+
+    def XMLgetAbsEtud(self, beg_date="", end_date="", REQUEST=None):
+        """returns list of absences in date interval"""
+        t0 = time.time()
+        etud = self.getEtudInfo(REQUEST=REQUEST)[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 = self._ListeAbsDate(etud["etudid"], beg_date, end_date)
+
+        REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+        doc = jaxml.XML_document(encoding=SCO_ENCODING)
+        doc.absences(etudid=etud["etudid"], beg_date=beg_date, end_date=end_date)
+        doc._push()
+        for a in Abs:
+            if a["estabs"]:  # ne donne pas les justifications si pas d'absence
+                doc._push()
+                doc.abs(
+                    begin=a["begin"],
+                    end=a["end"],
+                    description=a["description"],
+                    justified=a["estjust"],
+                )
+                doc._pop()
+        doc._pop()
+        log("XMLgetAbsEtud (%gs)" % (time.time() - t0))
+        return repr(doc)
+
+
+_billet_absenceEditor = EditableTable(
+    "billet_absence",
+    "billet_id",
+    (
+        "billet_id",
+        "etudid",
+        "abs_begin",
+        "abs_end",
+        "description",
+        "etat",
+        "entry_date",
+        "justified",
+    ),
+    sortkey="entry_date desc",
+)
+
+billet_absence_create = _billet_absenceEditor.create
+billet_absence_delete = _billet_absenceEditor.delete
+billet_absence_list = _billet_absenceEditor.list
+billet_absence_edit = _billet_absenceEditor.edit
+
+# ------ HTML Calendar functions (see YearTable function)
+
+# MONTH/DAY NAMES:
+
+MONTHNAMES = (
+    "Janvier",
+    "F&eacute;vrier",
+    "Mars",
+    "Avril",
+    "Mai",
+    "Juin",
+    "Juillet",
+    "Aout",
+    "Septembre",
+    "Octobre",
+    "Novembre",
+    "D&eacute;cembre",
+)
+
+MONTHNAMES_ABREV = (
+    "Jan.",
+    "F&eacute;v.",
+    "Mars",
+    "Avr.",
+    "Mai&nbsp;",
+    "Juin",
+    "Juil",
+    "Aout",
+    "Sept",
+    "Oct.",
+    "Nov.",
+    "D&eacute;c.",
+)
+
+DAYNAMES = ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche")
+
+DAYNAMES_ABREV = ("L", "M", "M", "J", "V", "S", "D")
+
+# COLORS:
+
+WHITE = "#FFFFFF"
+GRAY1 = "#EEEEEE"
+GREEN3 = "#99CC99"
+WEEKDAYCOLOR = GRAY1
+WEEKENDCOLOR = GREEN3
+
+
+def MonthTableHead(month):
+    color = WHITE
+    return """<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
+     <tr bgcolor="%s"><td class="calcol" colspan="2" align="center">%s</td></tr>\n""" % (
+        color,
+        MONTHNAMES_ABREV[month - 1],
+    )
+
+
+def MonthTableTail():
+    return "</table>\n"
+
+
+def MonthTableBody(
+    month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8
+):
+    # log('XXX events=%s' % events)
+    firstday, nbdays = calendar.monthrange(year, month)
+    localtime = time.localtime()
+    current_weeknum = time.strftime("%U", localtime)
+    current_year = localtime[0]
+    T = []
+    # cherche date du lundi de la 1ere semaine de ce mois
+    monday = ddmmyyyy("1/%d/%d" % (month, year))
+    while monday.weekday != 0:
+        monday = monday.prev()
+
+    if work_saturday:
+        weekend = ("D",)
+    else:
+        weekend = ("S", "D")
+
+    if not halfday:
+        for d in range(1, nbdays + 1):
+            weeknum = time.strftime(
+                "%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y")
+            )
+            day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
+            if day in weekend:
+                bgcolor = WEEKENDCOLOR
+                weekclass = "wkend"
+                attrs = ""
+            else:
+                bgcolor = WEEKDAYCOLOR
+                weekclass = "wk" + str(monday).replace("/", "_")
+                attrs = trattributes
+            color = None
+            legend = ""
+            href = ""
+            descr = ""
+            # event this day ?
+            # each event is a tuple (date, text, color, href)
+            #  where date is a string in ISO format (yyyy-mm-dd)
+            for ev in events:
+                ev_year = int(ev[0][:4])
+                ev_month = int(ev[0][5:7])
+                ev_day = int(ev[0][8:10])
+                if year == ev_year and month == ev_month and ev_day == d:
+                    if ev[1]:
+                        legend = ev[1]
+                    if ev[2]:
+                        color = ev[2]
+                    if ev[3]:
+                        href = ev[3]
+                    if len(ev) > 4 and ev[4]:
+                        descr = ev[4]
+            #
+            cc = []
+            if color != None:
+                cc.append('<td bgcolor="%s" class="calcell">' % color)
+            else:
+                cc.append('<td class="calcell">')
+
+            if href:
+                href = 'href="%s"' % href
+            if descr:
+                descr = 'title="%s"' % descr
+            if href or descr:
+                cc.append("<a %s %s>" % (href, descr))
+
+            if legend or d == 1:
+                if pad_width != None:
+                    n = pad_width - len(legend)  # pad to 8 cars
+                    if n > 0:
+                        legend = "&nbsp;" * (n / 2) + legend + "&nbsp;" * ((n + 1) / 2)
+            else:
+                legend = "&nbsp;"  # empty cell
+            cc.append(legend)
+            if href or descr:
+                cc.append("</a>")
+            cc.append("</td>")
+            cell = string.join(cc, "")
+            if day == "D":
+                monday = monday.next(7)
+            if (
+                weeknum == current_weeknum
+                and current_year == year
+                and weekclass != "wkend"
+            ):
+                weekclass += " currentweek"
+            T.append(
+                '<tr bgcolor="%s" class="%s" %s><td class="calday">%d%s</td>%s</tr>'
+                % (bgcolor, weekclass, attrs, d, day, cell)
+            )
+    else:
+        # Calendar with 2 cells / day
+        for d in range(1, nbdays + 1):
+            weeknum = time.strftime(
+                "%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y")
+            )
+            day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
+            if day in weekend:
+                bgcolor = WEEKENDCOLOR
+                weekclass = "wkend"
+                attrs = ""
+            else:
+                bgcolor = WEEKDAYCOLOR
+                weekclass = "wk" + str(monday).replace("/", "_")
+                attrs = trattributes
+            if (
+                weeknum == current_weeknum
+                and current_year == year
+                and weekclass != "wkend"
+            ):
+                weeknum += " currentweek"
+
+            if day == "D":
+                monday = monday.next(7)
+            T.append(
+                '<tr bgcolor="%s" class="wk%s" %s><td class="calday">%d%s</td>'
+                % (bgcolor, weekclass, attrs, d, day)
+            )
+            cc = []
+            for morning in (1, 0):
+                color = None
+                legend = ""
+                href = ""
+                descr = ""
+                for ev in events:
+                    ev_year = int(ev[0][:4])
+                    ev_month = int(ev[0][5:7])
+                    ev_day = int(ev[0][8:10])
+                    if ev[4] != None:
+                        ev_half = int(ev[4])
+                    else:
+                        ev_half = 0
+                    if (
+                        year == ev_year
+                        and month == ev_month
+                        and ev_day == d
+                        and morning == ev_half
+                    ):
+                        if ev[1]:
+                            legend = ev[1]
+                        if ev[2]:
+                            color = ev[2]
+                        if ev[3]:
+                            href = ev[3]
+                        if len(ev) > 5 and ev[5]:
+                            descr = ev[5]
+                #
+                if color != None:
+                    cc.append('<td bgcolor="%s" class="calcell">' % (color))
+                else:
+                    cc.append('<td class="calcell">')
+                if href:
+                    href = 'href="%s"' % href
+                if descr:
+                    descr = 'title="%s"' % descr
+                if href or descr:
+                    cc.append("<a %s %s>" % (href, descr))
+                if legend or d == 1:
+                    n = 3 - len(legend)  # pad to 3 cars
+                    if n > 0:
+                        legend = "&nbsp;" * (n / 2) + legend + "&nbsp;" * ((n + 1) / 2)
+                else:
+                    legend = "&nbsp;&nbsp;&nbsp;"  # empty cell
+                cc.append(legend)
+                if href or descr:
+                    cc.append("</a>")
+                cc.append("</td>\n")
+            T.append(string.join(cc, "") + "</tr>")
+    return string.join(T, "\n")
+
+
+# --------------------------------------------------------------------
+#
+# Zope Product Administration
+#
+# --------------------------------------------------------------------
+def manage_addZAbsences(
+    self, id="id_ZAbsences", title="The Title for ZAbsences Object", REQUEST=None
+):
+    "Add a ZAbsences instance to a folder."
+    self._setObject(id, ZAbsences(id, title))
+    if REQUEST is not None:
+        return self.manage_main(self, REQUEST)
+        # return self.manage_editForm(self, REQUEST)
+
+
+# The form used to get the instance id from the user.
+# manage_addZAbsencesForm = DTMLFile('dtml/manage_addZAbsencesForm', globals())
+
+
+# --------------------------------------------------------------------
+#
+# Cache absences
+#
+# On cache simplement (à la demande) le nombre d'absences de chaque etudiant
+# dans un semestre donné.
+# Toute modification du semestre (invalidation) invalide le cache
+#  (simple mécanisme de "listener" sur le cache de semestres)
+# Toute modification des absences d'un étudiant invalide les caches des semestres
+# concernés à cette date (en général un seul semestre)
+#
+# On ne cache pas la liste des absences car elle est rarement utilisée (calendrier,
+#  absences à une date donnée).
+#
+# --------------------------------------------------------------------
+class CAbsSemEtud:
+    """Comptes d'absences d'un etudiant dans un semestre"""
+
+    def __init__(self, context, sem, etudid):
+        self.context = context
+        self.sem = sem
+        self.etudid = etudid
+        self._loaded = False
+        formsemestre_id = sem["formsemestre_id"]
+        context.Notes._getNotesCache().add_listener(
+            self.invalidate, formsemestre_id, (etudid, formsemestre_id)
+        )
+
+    def CountAbs(self):
+        if not self._loaded:
+            self.load()
+        return self._CountAbs
+
+    def CountAbsJust(self):
+        if not self._loaded:
+            self.load()
+        return self._CountAbsJust
+
+    def load(self):
+        "Load state from DB"
+        # log('loading CAbsEtudSem(%s,%s)' % (self.etudid, self.sem['formsemestre_id']))
+        # Reload sem, it may have changed
+        self.sem = sco_formsemestre.get_formsemestre(
+            self.context, self.sem["formsemestre_id"]
+        )
+        debut_sem = DateDMYtoISO(self.sem["date_debut"])
+        fin_sem = DateDMYtoISO(self.sem["date_fin"])
+
+        self._CountAbs = self.context.Absences.CountAbs(
+            etudid=self.etudid, debut=debut_sem, fin=fin_sem
+        )
+        self._CountAbsJust = self.context.Absences.CountAbsJust(
+            etudid=self.etudid, debut=debut_sem, fin=fin_sem
+        )
+        self._loaded = True
+
+    def invalidate(self, args=None):
+        "Notify me that DB has been modified"
+        # log('invalidate CAbsEtudSem(%s,%s)' % (self.etudid, self.sem['formsemestre_id']))
+        self._loaded = False
+
+
+# Accès au cache des absences
+ABS_CACHE_INST = {}  # { DeptId : { formsemestre_id : { etudid :  CAbsEtudSem } } }
+
+
+def getAbsSemEtud(context, sem, etudid):
+    AbsSemEtuds = getAbsSemEtuds(context, sem)
+    if not etudid in AbsSemEtuds:
+        AbsSemEtuds[etudid] = CAbsSemEtud(context, sem, etudid)
+    return AbsSemEtuds[etudid]
+
+
+def getAbsSemEtuds(context, sem):
+    u = context.GetDBConnexionString()  # identifie le dept de facon fiable
+    if not u in ABS_CACHE_INST:
+        ABS_CACHE_INST[u] = {}
+    C = ABS_CACHE_INST[u]
+    if sem["formsemestre_id"] not in C:
+        C[sem["formsemestre_id"]] = {}
+    return C[sem["formsemestre_id"]]
+
+
+def invalidateAbsEtudDate(context, etudid, date):
+    """Doit etre appelé à chaque modification des absences pour cet étudiant et cette date.
+    Invalide cache absence et PDF bulletins si nécessaire.
+    date: date au format ISO
+    """
+    # Semestres a cette date:
+    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+    sems = [
+        sem
+        for sem in etud["sems"]
+        if sem["date_debut_iso"] <= date and sem["date_fin_iso"] >= date
+    ]
+
+    # Invalide les PDF et les abscences:
+    for sem in sems:
+        # Inval cache bulletin et/ou note_table
+        if sco_compute_moy.formsemestre_expressions_use_abscounts(
+            context, sem["formsemestre_id"]
+        ):
+            pdfonly = False  # seules certaines formules utilisent les absences
+        else:
+            pdfonly = (
+                True  # efface toujours le PDF car il affiche en général les absences
+            )
+
+        context.Notes._inval_cache(
+            pdfonly=pdfonly, formsemestre_id=sem["formsemestre_id"]
+        )
+
+        # Inval cache compteurs absences:
+        AbsSemEtuds = getAbsSemEtuds(context, sem)
+        if etudid in AbsSemEtuds:
+            AbsSemEtuds[etudid].invalidate()
diff --git a/ZEntreprises.py b/ZEntreprises.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ee42a84e44046a946d4a869a05abd764b40e8d1
--- /dev/null
+++ b/ZEntreprises.py
@@ -0,0 +1,898 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+""" Gestion des relations avec les entreprises
+"""
+import urllib
+
+from sco_zope import *
+
+# ---------------
+
+from notesdb import *
+from notes_log import log
+from scolog import logdb
+from sco_utils import *
+import html_sidebar
+
+from TrivialFormulator import TrivialFormulator, TF
+import scolars
+import string, re
+import time, calendar
+
+
+def _format_nom(nom):
+    "formatte nom (filtre en entree db) d'une entreprise"
+    if not nom:
+        return nom
+    nom = nom.decode(SCO_ENCODING)
+    return (nom[0].upper() + nom[1:]).encode(SCO_ENCODING)
+
+
+class EntreprisesEditor(EditableTable):
+    def delete(self, cnx, oid):
+        "delete correspondants and contacts, then self"
+        # first, delete all correspondants and contacts
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "delete from entreprise_contact where entreprise_id=%(entreprise_id)s",
+            {"entreprise_id": oid},
+        )
+        cursor.execute(
+            "delete from entreprise_correspondant where entreprise_id=%(entreprise_id)s",
+            {"entreprise_id": oid},
+        )
+        cnx.commit()
+        EditableTable.delete(self, cnx, oid)
+
+    def list(
+        self,
+        cnx,
+        args={},
+        operator="and",
+        test="=",
+        sortkey=None,
+        sort_on_contact=False,
+        ZEntrepriseInstance=None,
+    ):
+        # list, then sort on date of last contact
+        R = EditableTable.list(
+            self, cnx, args=args, operator=operator, test=test, sortkey=sortkey
+        )
+        if sort_on_contact:
+            for r in R:
+                c = ZEntrepriseInstance.do_entreprise_contact_list(
+                    args={"entreprise_id": r["entreprise_id"]}, disable_formatting=True
+                )
+                if c:
+                    r["date"] = max([x["date"] or datetime.date.min for x in c])
+                else:
+                    r["date"] = datetime.date.min
+            # sort
+            R.sort(lambda r1, r2: cmp(r2["date"], r1["date"]))
+            for r in R:
+                r["date"] = DateISOtoDMY(r["date"])
+        return R
+
+    def list_by_etud(
+        self, cnx, args={}, sort_on_contact=False, disable_formatting=False
+    ):
+        "cherche rentreprise ayant eu contact avec etudiant"
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "select E.*, I.nom as etud_nom, I.prenom as etud_prenom, C.date from entreprises E, entreprise_contact C, identite I where C.entreprise_id = E.entreprise_id and C.etudid = I.etudid and I.nom ~* %(etud_nom)s ORDER BY E.nom",
+            args,
+        )
+        titles, res = [x[0] for x in cursor.description], cursor.dictfetchall()
+        R = []
+        for r in res:
+            r["etud_prenom"] = r["etud_prenom"] or ""
+            d = {}
+            for key in r:
+                v = r[key]
+                # format value
+                if not disable_formatting and self.output_formators.has_key(key):
+                    v = self.output_formators[key](v)
+                d[key] = v
+            R.append(d)
+        # sort
+        if sort_on_contact:
+            R.sort(
+                lambda r1, r2: cmp(
+                    r2["date"] or datetime.date.min, r1["date"] or datetime.date.min
+                )
+            )
+        for r in R:
+            r["date"] = DateISOtoDMY(r["date"] or datetime.date.min)
+        return R
+
+
+_entreprisesEditor = EntreprisesEditor(
+    "entreprises",
+    "entreprise_id",
+    (
+        "entreprise_id",
+        "nom",
+        "adresse",
+        "ville",
+        "codepostal",
+        "pays",
+        "contact_origine",
+        "secteur",
+        "privee",
+        "localisation",
+        "qualite_relation",
+        "plus10salaries",
+        "note",
+        "date_creation",
+    ),
+    sortkey="nom",
+    input_formators={"nom": _format_nom},
+)
+
+# -----------  Correspondants
+_entreprise_correspEditor = EditableTable(
+    "entreprise_correspondant",
+    "entreprise_corresp_id",
+    (
+        "entreprise_corresp_id",
+        "entreprise_id",
+        "civilite",
+        "nom",
+        "prenom",
+        "fonction",
+        "phone1",
+        "phone2",
+        "mobile",
+        "fax",
+        "mail1",
+        "mail2",
+        "note",
+    ),
+    sortkey="nom",
+)
+
+
+# -----------  Contacts
+_entreprise_contactEditor = EditableTable(
+    "entreprise_contact",
+    "entreprise_contact_id",
+    (
+        "entreprise_contact_id",
+        "date",
+        "type_contact",
+        "entreprise_id",
+        "entreprise_corresp_id",
+        "etudid",
+        "description",
+        "enseignant",
+    ),
+    sortkey="date",
+    output_formators={"date": DateISOtoDMY},
+    input_formators={"date": DateDMYtoISO},
+)
+
+# ---------------
+
+
+class ZEntreprises(
+    ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit
+):
+
+    "ZEntreprises object"
+
+    meta_type = "ZEntreprises"
+    security = ClassSecurityInfo()
+
+    # This is the list of the methods associated to 'tabs' in the ZMI
+    # Be aware that The first in the list is the one shown by default, so if
+    # the 'View' tab is the first, you will never see your tabs by cliquing
+    # on the object.
+    manage_options = (
+        ({"label": "Contents", "action": "manage_main"},)
+        + PropertyManager.manage_options  # add the 'Properties' tab
+        + (
+            # this line is kept as an example with the files :
+            #   dtml/manage_editZScolarForm.dtml
+            #   html/ZScolar-edit.stx
+            #    {'label': 'Properties', 'action': 'manage_editForm',},
+            {"label": "View", "action": "index_html"},
+        )
+        + Item.manage_options  # add the 'Undo' & 'Owner' tab
+        + RoleManager.manage_options  # add the 'Security' tab
+    )
+
+    # no permissions, only called from python
+    def __init__(self, id, title):
+        "initialise a new instance"
+        self.id = id
+        self.title = title
+
+    # The form used to edit this object
+    def manage_editZEntreprises(self, title, RESPONSE=None):
+        "Changes the instance values"
+        self.title = title
+        self._p_changed = 1
+        RESPONSE.redirect("manage_editForm")
+
+    # Ajout (dans l'instance) d'un dtml modifiable par Zope
+    def defaultDocFile(self, id, title, file):
+        f = open(file_path + "/dtml-editable/" + file + ".dtml")
+        file = f.read()
+        f.close()
+        self.manage_addDTMLMethod(id, title, file)
+
+    security.declareProtected(ScoEntrepriseView, "entreprise_header")
+
+    def entreprise_header(self, REQUEST=None, page_title=""):
+        "common header for all Entreprises pages"
+        authuser = REQUEST.AUTHENTICATED_USER
+        # _read_only is used to modify pages properties (links, buttons)
+        # Python methods (do_xxx in this class) are also protected individualy)
+        if authuser.has_permission(ScoEntrepriseChange, self):
+            REQUEST.set("_read_only", False)
+        else:
+            REQUEST.set("_read_only", True)
+        return self.sco_header(REQUEST, container=self, page_title=page_title)
+
+    security.declareProtected(ScoEntrepriseView, "entreprise_footer")
+
+    def entreprise_footer(self, REQUEST):
+        "common entreprise footer"
+        return self.sco_footer(REQUEST)
+
+    security.declareProtected(ScoEntrepriseView, "sidebar")
+
+    def sidebar(self, REQUEST):
+        "barre gauche (overide std sco sidebar)"
+        # rewritten from legacy DTML code
+        context = self
+        params = {"ScoURL": context.ScoURL()}
+
+        H = [
+            """<div id="sidebar-container">
+            <div class="sidebar">""",
+            html_sidebar.sidebar_common(context, REQUEST),
+            """<h2 class="insidebar"><a href="%(ScoURL)s/Entreprises" class="sidebar">Entreprises</a></h2>
+<ul class="insidebar">"""
+            % params,
+        ]
+        if not REQUEST["_read_only"]:
+            H.append(
+                """<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_create" class="sidebar">Nouvelle entreprise</a> </li>"""
+                % params
+            )
+
+        H.append(
+            """<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_contact_list" class="sidebar">Contacts</a> </li></ul> """
+            % params
+        )
+
+        # --- entreprise selectionnée:
+        if REQUEST.form.has_key("entreprise_id"):
+            entreprise_id = REQUEST.form["entreprise_id"]
+            E = context.do_entreprise_list(args={"entreprise_id": entreprise_id})
+            if E:
+                E = E[0]
+                params.update(E)
+                H.append(
+                    """<div class="entreprise-insidebar">
+      <h3 class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_edit?entreprise_id=%(entreprise_id)s" class="sidebar">%(nom)s</a></h2>
+      <ul class="insidebar">
+      <li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_correspondant_list?entreprise_id=%(entreprise_id)s" class="sidebar">Corresp.</a></li>"""
+                    % params
+                )  # """
+                if not REQUEST["_read_only"]:
+                    H.append(
+                        """<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_correspondant_create?entreprise_id=%(entreprise_id)s" class="sidebar">Nouveau Corresp.</a></li>"""
+                        % params
+                    )
+                H.append(
+                    """<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_contact_list?entreprise_id=%(entreprise_id)s" class="sidebar">Contacts</a></li>"""
+                    % params
+                )
+                if not REQUEST["_read_only"]:
+                    H.append(
+                        """<li class="insidebar"><a href="%(ScoURL)s/Entreprises/entreprise_contact_create?entreprise_id=%(entreprise_id)s" class="sidebar">Nouveau "contact"</a></li>"""
+                        % params
+                    )
+                H.append("</ul></div>")
+
+        #
+        H.append("""<br/><br/>%s""" % icontag("entreprise_side_img"))
+        if REQUEST["_read_only"]:
+            H.append("""<br/><em>(Lecture seule)</em>""")
+        H.append("""</div> </div> <!-- end of sidebar -->""")
+        return "".join(H)
+
+    # --------------------------------------------------------------------
+    #
+    #   Entreprises : Methodes en DTML
+    #
+    # --------------------------------------------------------------------
+    # used to view content of the object
+    security.declareProtected(ScoEntrepriseView, "index_html")
+    index_html = DTMLFile("dtml/entreprises/index_html", globals())
+
+    security.declareProtected(ScoEntrepriseView, "entreprise_contact_list")
+    entreprise_contact_list = DTMLFile(
+        "dtml/entreprises/entreprise_contact_list", globals()
+    )
+    security.declareProtected(ScoEntrepriseView, "entreprise_correspondant_list")
+    entreprise_correspondant_list = DTMLFile(
+        "dtml/entreprises/entreprise_correspondant_list", globals()
+    )
+    # les methodes "edit" sont aussi en ScoEntrepriseView car elles permettent
+    # la visualisation (via variable _read_only positionnee dans entreprise_header)
+    security.declareProtected(ScoEntrepriseView, "entreprise_contact_edit")
+    entreprise_contact_edit = DTMLFile(
+        "dtml/entreprises/entreprise_contact_edit", globals()
+    )
+    security.declareProtected(ScoEntrepriseView, "entreprise_correspondant_edit")
+    entreprise_correspondant_edit = DTMLFile(
+        "dtml/entreprises/entreprise_correspondant_edit", globals()
+    )
+
+    # Acces en modification:
+    security.declareProtected(ScoEntrepriseChange, "entreprise_contact_create")
+    entreprise_contact_create = DTMLFile(
+        "dtml/entreprises/entreprise_contact_create", globals()
+    )
+    security.declareProtected(ScoEntrepriseChange, "entreprise_contact_delete")
+    entreprise_contact_delete = DTMLFile(
+        "dtml/entreprises/entreprise_contact_delete", globals()
+    )
+    security.declareProtected(ScoEntrepriseChange, "entreprise_correspondant_create")
+    entreprise_correspondant_create = DTMLFile(
+        "dtml/entreprises/entreprise_correspondant_create", globals()
+    )
+    security.declareProtected(ScoEntrepriseChange, "entreprise_correspondant_delete")
+    entreprise_correspondant_delete = DTMLFile(
+        "dtml/entreprises/entreprise_correspondant_delete", globals()
+    )
+    security.declareProtected(ScoEntrepriseChange, "entreprise_delete")
+    entreprise_delete = DTMLFile("dtml/entreprises/entreprise_delete", globals())
+
+    # --------------------------------------------------------------------
+    #
+    #   Entreprises : Methodes en Python
+    #
+    # --------------------------------------------------------------------
+    security.declareProtected(ScoEntrepriseChange, "do_entreprise_create")
+
+    def do_entreprise_create(self, args):
+        "entreprise_create"
+        cnx = self.GetDBConnexion()
+        r = _entreprisesEditor.create(cnx, args)
+        return r
+
+    security.declareProtected(ScoEntrepriseChange, "do_entreprise_delete")
+
+    def do_entreprise_delete(self, oid):
+        "entreprise_delete"
+        cnx = self.GetDBConnexion()
+        _entreprisesEditor.delete(cnx, oid)
+
+    security.declareProtected(ScoEntrepriseView, "do_entreprise_list")
+
+    def do_entreprise_list(self, **kw):
+        "entreprise_list"
+        cnx = self.GetDBConnexion()
+        kw["ZEntrepriseInstance"] = self
+        return _entreprisesEditor.list(cnx, **kw)
+
+    security.declareProtected(ScoEntrepriseView, "do_entreprise_list_by_etud")
+
+    def do_entreprise_list_by_etud(self, **kw):
+        "entreprise_list_by_etud"
+        cnx = self.GetDBConnexion()
+        return _entreprisesEditor.list_by_etud(cnx, **kw)
+
+    security.declareProtected(ScoEntrepriseView, "do_entreprise_edit")
+
+    def do_entreprise_edit(self, *args, **kw):
+        "entreprise_edit"
+        cnx = self.GetDBConnexion()
+        _entreprisesEditor.edit(cnx, *args, **kw)
+
+    security.declareProtected(ScoEntrepriseChange, "do_entreprise_correspondant_create")
+
+    def do_entreprise_correspondant_create(self, args):
+        "entreprise_correspondant_create"
+        cnx = self.GetDBConnexion()
+        r = _entreprise_correspEditor.create(cnx, args)
+        return r
+
+    security.declareProtected(ScoEntrepriseChange, "do_entreprise_correspondant_delete")
+
+    def do_entreprise_correspondant_delete(self, oid):
+        "entreprise_correspondant_delete"
+        cnx = self.GetDBConnexion()
+        _entreprise_correspEditor.delete(cnx, oid)
+
+    security.declareProtected(ScoEntrepriseView, "do_entreprise_correspondant_list")
+
+    def do_entreprise_correspondant_list(self, **kw):
+        "entreprise_correspondant_list"
+        cnx = self.GetDBConnexion()
+        return _entreprise_correspEditor.list(cnx, **kw)
+
+    security.declareProtected(ScoEntrepriseView, "do_entreprise_correspondant_edit")
+
+    def do_entreprise_correspondant_edit(self, *args, **kw):
+        "entreprise_correspondant_edit"
+        cnx = self.GetDBConnexion()
+        _entreprise_correspEditor.edit(cnx, *args, **kw)
+
+    def do_entreprise_correspondant_listnames(self, args={}):
+        "-> liste des noms des correspondants (pour affichage menu)"
+        cnx = self.GetDBConnexion()
+        C = self.do_entreprise_correspondant_list(args=args)
+        return [
+            (x["prenom"] + " " + x["nom"], str(x["entreprise_corresp_id"])) for x in C
+        ]
+
+    security.declareProtected(ScoEntrepriseChange, "do_entreprise_contact_create")
+
+    def do_entreprise_contact_create(self, args):
+        "entreprise_contact_create"
+        cnx = self.GetDBConnexion()
+        r = _entreprise_contactEditor.create(cnx, args)
+        return r
+
+    security.declareProtected(ScoEntrepriseChange, "do_entreprise_contact_delete")
+
+    def do_entreprise_contact_delete(self, oid):
+        "entreprise_contact_delete"
+        cnx = self.GetDBConnexion()
+        _entreprise_contactEditor.delete(cnx, oid)
+
+    security.declareProtected(ScoEntrepriseView, "do_entreprise_contact_list")
+
+    def do_entreprise_contact_list(self, **kw):
+        "entreprise_contact_list"
+        cnx = self.GetDBConnexion()
+        return _entreprise_contactEditor.list(cnx, **kw)
+
+    security.declareProtected(ScoEntrepriseView, "do_entreprise_contact_edit")
+
+    def do_entreprise_contact_edit(self, *args, **kw):
+        "entreprise_contact_edit"
+        cnx = self.GetDBConnexion()
+        _entreprise_contactEditor.edit(cnx, *args, **kw)
+
+    #
+    security.declareProtected(ScoEntrepriseView, "do_entreprise_check_etudiant")
+
+    def do_entreprise_check_etudiant(self, etudiant):
+        """Si etudiant est vide, ou un ETUDID valide, ou un nom unique,
+        retourne (1, ETUDID).
+        Sinon, retourne (0, 'message explicatif')
+        """
+        etudiant = etudiant.strip().translate(
+            None, "'()"
+        )  # suppress parens and quote from name
+        if not etudiant:
+            return 1, None
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "select etudid, nom, prenom from identite where upper(nom) ~ upper(%(etudiant)s) or etudid=%(etudiant)s",
+            {"etudiant": etudiant},
+        )
+        r = cursor.fetchall()
+        if len(r) < 1:
+            return 0, 'Aucun etudiant ne correspond à "%s"' % etudiant
+        elif len(r) > 10:
+            return (
+                0,
+                "<b>%d etudiants</b> correspondent à ce nom (utilisez le code)"
+                % len(r),
+            )
+        elif len(r) > 1:
+            e = ['<ul class="entreprise_etud_list">']
+            for x in r:
+                e.append(
+                    "<li>%s %s (code %s)</li>"
+                    % (strupper(x[1]), x[2] or "", x[0].strip())
+                )
+            e.append("</ul>")
+            return (
+                0,
+                "Les étudiants suivants correspondent: préciser le nom complet ou le code\n"
+                + "\n".join(e),
+            )
+        else:  # une seule reponse !
+            return 1, r[0][0].strip()
+
+    security.declareProtected(ScoEntrepriseView, "do_entreprise_list_by_contact")
+
+    def do_entreprise_list_by_contact(
+        self, args={}, operator="and", test="=", sortkey=None
+    ):
+        """Recherche dans entreprises, avec date de contact"""
+        # (fonction ad-hoc car requete sur plusieurs tables)
+        raise NotImplementedError
+        # XXXXX fonction non achevee , non testee...
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        vals = dictfilter(args, self.dbfields)
+        # DBSelect
+        what = ["*"]
+        operator = " " + operator + " "
+        cond = " E.entreprise_id = C.entreprise_id "
+        if vals:
+            cond += " where " + operator.join(
+                ["%s%s%%(%s)s" % (x, test, x) for x in vals.keys() if vals[x] != None]
+            )
+            cnuls = " and ".join(
+                ["%s is NULL" % x for x in vals.keys() if vals[x] is None]
+            )
+            if cnuls:
+                cond = cond + " and " + cnuls
+        else:
+            cond += ""
+        cursor.execute(
+            "select distinct"
+            + ", ".join(what)
+            + " from entreprises E,  entreprise_contact C "
+            + cond
+            + orderby,
+            vals,
+        )
+        titles, res = [x[0] for x in cursor.description], cursor.fetchall()
+        #
+        R = []
+        for r in res:
+            d = {}
+            for i in range(len(titles)):
+                v = r[i]
+                # value not formatted ! (see EditableTable.list())
+                d[titles[i]] = v
+            R.append(d)
+        return R
+
+    # -------- Formulaires: traductions du DTML
+    security.declareProtected(ScoEntrepriseChange, "entreprise_create")
+
+    def entreprise_create(self, REQUEST=None):
+        """Form. création entreprise"""
+        context = self
+        H = [
+            self.entreprise_header(REQUEST, page_title="Création d'une entreprise"),
+            """<h2 class="entreprise_new">Création d'une entreprise</h2>""",
+        ]
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (
+                ("nom", {"size": 25, "title": "Nom de l'entreprise"}),
+                (
+                    "adresse",
+                    {"size": 30, "title": "Adresse", "explanation": "(numéro, rue)"},
+                ),
+                ("codepostal", {"size": 8, "title": "Code Postal"}),
+                ("ville", {"size": 30, "title": "Ville"}),
+                ("pays", {"size": 30, "title": "Pays", "default": "France"}),
+                (
+                    "localisation",
+                    {
+                        "input_type": "menu",
+                        "labels": ["Ile de France", "Province", "Etranger"],
+                        "allowed_values": ["IDF", "Province", "Etranger"],
+                    },
+                ),
+                ("secteur", {"size": 30, "title": "Secteur d'activités"}),
+                (
+                    "privee",
+                    {
+                        "input_type": "menu",
+                        "title": "Statut",
+                        "labels": [
+                            "Entreprise privee",
+                            "Entreprise Publique",
+                            "Association",
+                        ],
+                        "allowed_values": ["privee", "publique", "association"],
+                    },
+                ),
+                (
+                    "plus10salaries",
+                    {
+                        "title": "Masse salariale",
+                        "type": "integer",
+                        "input_type": "menu",
+                        "labels": [
+                            "10 salariés ou plus",
+                            "Moins de 10 salariés",
+                            "Inconnue",
+                        ],
+                        "allowed_values": [1, 0, -1],
+                    },
+                ),
+                (
+                    "qualite_relation",
+                    {
+                        "title": "Qualité relation IUT/Entreprise",
+                        "input_type": "menu",
+                        "default": "-1",
+                        "labels": [
+                            "Très bonne",
+                            "Bonne",
+                            "Moyenne",
+                            "Mauvaise",
+                            "Inconnue",
+                        ],
+                        "allowed_values": ["100", "75", "50", "25", "-1"],
+                    },
+                ),
+                ("contact_origine", {"size": 30, "title": "Origine du contact"}),
+                (
+                    "note",
+                    {"input_type": "textarea", "rows": 3, "cols": 40, "title": "Note"},
+                ),
+            ),
+            cancelbutton="Annuler",
+            submitlabel="Ajouter cette entreprise",
+            readonly=REQUEST["_read_only"],
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + context.entreprise_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+        else:
+            self.do_entreprise_create(tf[2])
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+
+    security.declareProtected(ScoEntrepriseView, "entreprise_edit")
+
+    def entreprise_edit(self, entreprise_id, REQUEST=None, start=1):
+        """Form. edit entreprise"""
+        context = self
+        authuser = REQUEST.AUTHENTICATED_USER
+        readonly = not authuser.has_permission(ScoEntrepriseChange, self)
+        F = self.do_entreprise_list(args={"entreprise_id": entreprise_id})[0]
+        H = [
+            self.entreprise_header(REQUEST, page_title="Entreprise"),
+            """<h2 class="entreprise">%(nom)s</h2>""" % F,
+        ]
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (
+                ("entreprise_id", {"default": entreprise_id, "input_type": "hidden"}),
+                ("start", {"default": 1, "input_type": "hidden"}),
+                (
+                    "date_creation",
+                    {"default": time.strftime("%Y-%m-%d"), "input_type": "hidden"},
+                ),
+                ("nom", {"size": 25, "title": "Nom de l'entreprise"}),
+                (
+                    "adresse",
+                    {"size": 30, "title": "Adresse", "explanation": "(numéro, rue)"},
+                ),
+                ("codepostal", {"size": 8, "title": "Code Postal"}),
+                ("ville", {"size": 30, "title": "Ville"}),
+                ("pays", {"size": 30, "title": "Pays", "default": "France"}),
+                (
+                    "localisation",
+                    {
+                        "input_type": "menu",
+                        "labels": ["Ile de France", "Province", "Etranger"],
+                        "allowed_values": ["IDF", "Province", "Etranger"],
+                    },
+                ),
+                ("secteur", {"size": 30, "title": "Secteur d'activités"}),
+                (
+                    "privee",
+                    {
+                        "input_type": "menu",
+                        "title": "Statut",
+                        "labels": [
+                            "Entreprise privee",
+                            "Entreprise Publique",
+                            "Association",
+                        ],
+                        "allowed_values": ["privee", "publique", "association"],
+                    },
+                ),
+                (
+                    "plus10salaries",
+                    {
+                        "title": "Masse salariale",
+                        "input_type": "menu",
+                        "labels": [
+                            "10 salariés ou plus",
+                            "Moins de 10 salariés",
+                            "Inconnue",
+                        ],
+                        "allowed_values": ["1", "0", "-1"],
+                    },
+                ),
+                (
+                    "qualite_relation",
+                    {
+                        "title": "Qualité relation IUT/Entreprise",
+                        "input_type": "menu",
+                        "labels": [
+                            "Très bonne",
+                            "Bonne",
+                            "Moyenne",
+                            "Mauvaise",
+                            "Inconnue",
+                        ],
+                        "allowed_values": ["100", "75", "50", "25", "-1"],
+                    },
+                ),
+                ("contact_origine", {"size": 30, "title": "Origine du contact"}),
+                (
+                    "note",
+                    {"input_type": "textarea", "rows": 3, "cols": 40, "title": "Note"},
+                ),
+            ),
+            cancelbutton="Annuler",
+            initvalues=F,
+            submitlabel="Modifier les valeurs",
+            readonly=readonly,
+        )
+
+        if tf[0] == 0:
+            H.append(tf[1])
+            Cl = self.do_entreprise_correspondant_list(
+                args={"entreprise_id": F["entreprise_id"]}
+            )
+            Cts = self.do_entreprise_contact_list(
+                args={"entreprise_id": F["entreprise_id"]}
+            )
+            if not readonly:
+                H.append(
+                    """<p>%s&nbsp;<a class="entreprise_delete" href="entreprise_delete?entreprise_id=%s">Supprimer cette entreprise</a> </p>"""
+                    % (
+                        icontag("delete_img", title="delete", border="0"),
+                        F["entreprise_id"],
+                    )
+                )
+            if len(Cl):
+                H.append(
+                    """<h3>%d correspondants dans l'entreprise %s (<a href="entreprise_correspondant_list?entreprise_id=%s">liste complète</a>) :</h3>
+<ul>"""
+                    % (len(Cl), F["nom"], F["entreprise_id"])
+                )
+                for c in Cl:
+                    H.append(
+                        """<li><a href="entreprise_correspondant_edit?entreprise_corresp_id=%s">"""
+                        % c["entreprise_corresp_id"]
+                    )
+                    if c["nom"]:
+                        nom = (
+                            c["nom"]
+                            .decode(SCO_ENCODING)
+                            .lower()
+                            .capitalize()
+                            .encode(SCO_ENCODING)
+                        )
+                    else:
+                        nom = ""
+                    if c["prenom"]:
+                        prenom = (
+                            c["prenom"]
+                            .decode(SCO_ENCODING)
+                            .lower()
+                            .capitalize()
+                            .encode(SCO_ENCODING)
+                        )
+                    else:
+                        prenom = ""
+                    H.append(
+                        """%s %s</a>&nbsp;(%s)</li>""" % (nom, prenom, c["fonction"])
+                    )
+                H.append("</ul>")
+            if len(Cts):
+                H.append(
+                    """<h3>%d contacts avec l'entreprise %s (<a href="entreprise_contact_list?entreprise_id=%s">liste complète</a>) :</h3><ul>"""
+                    % (len(Cts), F["nom"], F["entreprise_id"])
+                )
+                for c in Cts:
+                    H.append(
+                        """<li><a href="entreprise_contact_edit?entreprise_contact_id=%s">%s</a>&nbsp;&nbsp;&nbsp;"""
+                        % (c["entreprise_contact_id"], c["date"])
+                    )
+                    if c["type_contact"]:
+                        H.append(c["type_contact"])
+                    if c["etudid"]:
+                        etud = self.getEtudInfo(etudid=c["etudid"], filled=1)
+                        if etud:
+                            etud = etud[0]
+                            H.append(
+                                """<a href="%s/ficheEtud?etudid=%s">%s</a>"""
+                                % (self.ScoURL(), c["etudid"], etud["nomprenom"])
+                            )
+                    if c["description"]:
+                        H.append("(%s)" % c["description"])
+                    H.append("</li>")
+                H.append("</ul>")
+            return "\n".join(H) + context.entreprise_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1 + "?start=" + start)
+        else:
+            self.do_entreprise_edit(tf[2])
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1 + "?start=" + start)
+
+    # --- Misc tools.... ------------------
+    security.declareProtected(ScoEntrepriseView, "str_abbrev")
+
+    def str_abbrev(self, s, maxlen):
+        "abreviation"
+        if s == None:
+            return "?"
+        if len(s) < maxlen:
+            return s
+        return s[: maxlen - 3] + "..."
+
+    security.declareProtected(ScoEntrepriseView, "setPageSizeCookie")
+
+    def setPageSizeCookie(self, REQUEST=None):
+        "set page size cookie"
+        RESPONSE = REQUEST.RESPONSE
+        #
+        if REQUEST.form.has_key("entreprise_page_size"):
+            RESPONSE.setCookie(
+                "entreprise_page_size",
+                REQUEST.form["entreprise_page_size"],
+                path="/",
+                expires="Wed, 31-Dec-2025 23:55:00 GMT",
+            )
+        RESPONSE.redirect(REQUEST.form["target_url"])
+
+    security.declareProtected(ScoEntrepriseView, "make_link_create_corr")
+
+    def make_link_create_corr(self, entreprise_id):
+        "yet another stupid code snippet"
+        return (
+            '<a href="entreprise_correspondant_create?entreprise_id='
+            + str(entreprise_id)
+            + '">créer un nouveau correspondant</a>'
+        )
+
+
+# --------------------------------------------------------------------
+#
+# Zope Product Administration
+#
+# --------------------------------------------------------------------
+def manage_addZEntreprises(
+    self, id="id_ZEntreprises", title="The Title for ZEntreprises Object", REQUEST=None
+):
+    "Add a ZEntreprises instance to a folder."
+    self._setObject(id, ZEntreprises(id, title))
+    if REQUEST is not None:
+        return self.manage_main(self, REQUEST)
+        # return self.manage_editForm(self, REQUEST)
+
+
+# The form used to get the instance id from the user.
+# manage_addZAbsencesForm = DTMLFile('dtml/manage_addZAbsencesForm', globals())
diff --git a/ZNotes.py b/ZNotes.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9946c86bacc1345d676add69075d805efed1564
--- /dev/null
+++ b/ZNotes.py
@@ -0,0 +1,3647 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Interface Zope <-> Notes
+"""
+
+from sets import Set
+
+from sco_zope import *
+
+# ---------------
+
+from notesdb import *
+from notes_log import log, sendAlarm
+import scolog
+from scolog import logdb
+from sco_utils import *
+import htmlutils
+import sco_excel
+
+# import notes_users
+from gen_tables import GenTable
+import sco_cache
+import scolars
+import sco_news
+from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC
+
+import sco_formsemestre
+import sco_formsemestre_edit
+import sco_formsemestre_status
+import sco_formsemestre_inscriptions
+import sco_formsemestre_custommenu
+import sco_moduleimpl_status
+import sco_moduleimpl_inscriptions
+import sco_evaluations
+import sco_groups
+import sco_edit_ue
+import sco_edit_formation
+import sco_edit_matiere
+import sco_edit_module
+import sco_tag_module
+import sco_bulletins
+import sco_bulletins_pdf
+import sco_recapcomplet
+import sco_liste_notes
+import sco_saisie_notes
+import sco_placement
+import sco_undo_notes
+import sco_formations
+import sco_report
+import sco_lycee
+import sco_poursuite_dut
+import pe_view
+import sco_debouche
+import sco_ue_external
+import sco_cost_formation
+import sco_formsemestre_validation
+import sco_parcours_dut
+import sco_codes_parcours
+import sco_pvjury
+import sco_pvpdf
+import sco_prepajury
+import sco_inscr_passage
+import sco_synchro_etuds
+import sco_archives
+import sco_apogee_csv
+import sco_etape_apogee_view
+import sco_apogee_compare
+import sco_semset
+import sco_export_results
+import sco_formsemestre_exterieurs
+
+from sco_pdf import PDFLOCK
+from notes_table import *
+import VERSION
+
+#
+# Cache global: chaque instance, repérée par sa connexion db, a un cache
+# qui est recréé à la demande
+#
+CACHE_formsemestre_inscription = {}
+CACHE_evaluations = {}
+
+# ---------------
+
+
+class ZNotes(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit):
+
+    "ZNotes object"
+
+    meta_type = "ZNotes"
+    security = ClassSecurityInfo()
+
+    # This is the list of the methods associated to 'tabs' in the ZMI
+    # Be aware that The first in the list is the one shown by default, so if
+    # the 'View' tab is the first, you will never see your tabs by cliquing
+    # on the object.
+    manage_options = (
+        ({"label": "Contents", "action": "manage_main"},)
+        + PropertyManager.manage_options  # add the 'Properties' tab
+        + (
+            # this line is kept as an example with the files :
+            #     dtml/manage_editZScolarForm.dtml
+            #     html/ZScolar-edit.stx
+            #   {'label': 'Properties', 'action': 'manage_editForm',},
+            {"label": "View", "action": "index_html"},
+        )
+        + Item.manage_options  # add the 'Undo' & 'Owner' tab
+        + RoleManager.manage_options  # add the 'Security' tab
+    )
+
+    # no permissions, only called from python
+    def __init__(self, id, title):
+        "initialise a new instance of ZNotes"
+        self.id = id
+        self.title = title
+
+    # The form used to edit this object
+    def manage_editZNotes(self, title, RESPONSE=None):
+        "Changes the instance values"
+        self.title = title
+        self._p_changed = 1
+        RESPONSE.redirect("manage_editForm")
+
+    def _getNotesCache(self):
+        "returns CacheNotesTable instance for us"
+        u = self.GetDBConnexionString()  # identifie le dept de facon fiable
+        if not NOTES_CACHE_INST.has_key(u):
+            log("getNotesCache: creating cache for %s" % u)
+            NOTES_CACHE_INST[u] = CacheNotesTable()
+        return NOTES_CACHE_INST[u]
+
+    def _inval_cache(
+        self, formsemestre_id=None, pdfonly=False, formsemestre_id_list=None
+    ):  # >
+        "expire cache pour un semestre (ou tous si pas d'argument)"
+        if formsemestre_id_list:
+            for formsemestre_id in formsemestre_id_list:
+                self._getNotesCache().inval_cache(
+                    self, formsemestre_id=formsemestre_id, pdfonly=pdfonly
+                )
+                # Affecte aussi cache inscriptions
+                self.get_formsemestre_inscription_cache().inval_cache(
+                    key=formsemestre_id
+                )  # >
+        else:
+            self._getNotesCache().inval_cache(
+                self, formsemestre_id=formsemestre_id, pdfonly=pdfonly
+            )  # >
+            # Affecte aussi cache inscriptions
+            self.get_formsemestre_inscription_cache().inval_cache(
+                key=formsemestre_id
+            )  # >
+
+    security.declareProtected(ScoView, "clearcache")
+
+    def clearcache(self, REQUEST=None):
+        "Efface les caches de notes (utile pendant developpement slt)"
+        log("*** clearcache request")
+        # Debugging code: compare results before and after cache reconstruction
+        # (_should_ be identicals !)
+        # Compare XML representation
+        cache = self._getNotesCache()
+        formsemestre_ids = cache.get_cached_formsemestre_ids()
+        docs_before = []
+        for formsemestre_id in formsemestre_ids:
+            docs_before.append(
+                sco_recapcomplet.do_formsemestre_recapcomplet(
+                    self, REQUEST, formsemestre_id, format="xml", xml_nodate=True
+                )
+            )
+        #
+        cache.inval_cache(self)  # >
+        # Rebuild cache (useful only to debug)
+        docs_after = []
+        for formsemestre_id in formsemestre_ids:
+            docs_after.append(
+                sco_recapcomplet.do_formsemestre_recapcomplet(
+                    self, REQUEST, formsemestre_id, format="xml", xml_nodate=True
+                )
+            )
+        if docs_before != docs_after:
+            log("clearcache: inconsistency !")
+            txt = "before=" + repr(docs_before) + "\n\nafter=" + repr(docs_after) + "\n"
+            log(txt)
+            sendAlarm(self, "clearcache: inconsistency !", txt)
+
+    # --------------------------------------------------------------------
+    #
+    #    NOTES (top level)
+    #
+    # --------------------------------------------------------------------
+    # XXX essai
+    security.declareProtected(ScoView, "gloups")
+
+    def gloups(self, REQUEST):
+        "essai gloups"
+        return ""
+        # return pdfbulletins.essaipdf(REQUEST)
+        # return sendPDFFile(REQUEST, pdfbulletins.pdftrombino(0,0), 'toto.pdf' )
+
+    # Python methods:
+    security.declareProtected(ScoView, "formsemestre_status")
+    formsemestre_status = sco_formsemestre_status.formsemestre_status
+
+    security.declareProtected(ScoImplement, "formsemestre_createwithmodules")
+    formsemestre_createwithmodules = (
+        sco_formsemestre_edit.formsemestre_createwithmodules
+    )
+
+    security.declareProtected(ScoView, "formsemestre_editwithmodules")
+    formsemestre_editwithmodules = sco_formsemestre_edit.formsemestre_editwithmodules
+
+    security.declareProtected(ScoView, "formsemestre_clone")
+    formsemestre_clone = sco_formsemestre_edit.formsemestre_clone
+
+    security.declareProtected(ScoChangeFormation, "formsemestre_associate_new_version")
+    formsemestre_associate_new_version = (
+        sco_formsemestre_edit.formsemestre_associate_new_version
+    )
+
+    security.declareProtected(ScoImplement, "formsemestre_delete")
+    formsemestre_delete = sco_formsemestre_edit.formsemestre_delete
+    security.declareProtected(ScoImplement, "formsemestre_delete2")
+    formsemestre_delete2 = sco_formsemestre_edit.formsemestre_delete2
+
+    security.declareProtected(ScoView, "formsemestre_recapcomplet")
+    formsemestre_recapcomplet = sco_recapcomplet.formsemestre_recapcomplet
+
+    security.declareProtected(ScoView, "moduleimpl_status")
+    moduleimpl_status = sco_moduleimpl_status.moduleimpl_status
+
+    security.declareProtected(ScoView, "formsemestre_description")
+    formsemestre_description = sco_formsemestre_status.formsemestre_description
+
+    security.declareProtected(ScoView, "formsemestre_lists")
+    formsemestre_lists = sco_formsemestre_status.formsemestre_lists
+
+    security.declareProtected(ScoView, "formsemestre_status_menubar")
+    formsemestre_status_menubar = sco_formsemestre_status.formsemestre_status_menubar
+    security.declareProtected(ScoChangeFormation, "formation_create")
+    formation_create = sco_edit_formation.formation_create
+    security.declareProtected(ScoChangeFormation, "formation_delete")
+    formation_delete = sco_edit_formation.formation_delete
+    security.declareProtected(ScoChangeFormation, "formation_edit")
+    formation_edit = sco_edit_formation.formation_edit
+
+    security.declareProtected(ScoView, "formsemestre_bulletinetud")
+    formsemestre_bulletinetud = sco_bulletins.formsemestre_bulletinetud
+
+    security.declareProtected(ScoView, "formsemestre_evaluations_cal")
+    formsemestre_evaluations_cal = sco_evaluations.formsemestre_evaluations_cal
+    security.declareProtected(ScoView, "formsemestre_evaluations_delai_correction")
+    formsemestre_evaluations_delai_correction = (
+        sco_evaluations.formsemestre_evaluations_delai_correction
+    )
+
+    security.declareProtected(ScoView, "module_evaluation_renumber")
+    module_evaluation_renumber = sco_evaluations.module_evaluation_renumber
+    security.declareProtected(ScoView, "module_evaluation_move")
+    module_evaluation_move = sco_evaluations.module_evaluation_move
+
+    security.declareProtected(ScoView, "formsemestre_list_saisies_notes")
+    formsemestre_list_saisies_notes = sco_undo_notes.formsemestre_list_saisies_notes
+
+    security.declareProtected(ScoChangeFormation, "ue_create")
+    ue_create = sco_edit_ue.ue_create
+    security.declareProtected(ScoChangeFormation, "ue_delete")
+    ue_delete = sco_edit_ue.ue_delete
+    security.declareProtected(ScoChangeFormation, "ue_edit")
+    ue_edit = sco_edit_ue.ue_edit
+    security.declareProtected(ScoView, "ue_list")
+    ue_list = sco_edit_ue.ue_list
+    security.declareProtected(ScoView, "ue_sharing_code")
+    ue_sharing_code = sco_edit_ue.ue_sharing_code
+    security.declareProtected(ScoChangeFormation, "edit_ue_set_code_apogee")
+    edit_ue_set_code_apogee = sco_edit_ue.edit_ue_set_code_apogee
+    security.declareProtected(ScoView, "formation_table_recap")
+    formation_table_recap = sco_edit_ue.formation_table_recap
+    security.declareProtected(ScoChangeFormation, "formation_add_malus_modules")
+    formation_add_malus_modules = sco_edit_module.formation_add_malus_modules
+
+    security.declareProtected(ScoChangeFormation, "matiere_create")
+    matiere_create = sco_edit_matiere.matiere_create
+    security.declareProtected(ScoChangeFormation, "matiere_delete")
+    matiere_delete = sco_edit_matiere.matiere_delete
+    security.declareProtected(ScoChangeFormation, "matiere_edit")
+    matiere_edit = sco_edit_matiere.matiere_edit
+
+    security.declareProtected(ScoChangeFormation, "module_create")
+    module_create = sco_edit_module.module_create
+    security.declareProtected(ScoChangeFormation, "module_delete")
+    module_delete = sco_edit_module.module_delete
+    security.declareProtected(ScoChangeFormation, "module_edit")
+    module_edit = sco_edit_module.module_edit
+    security.declareProtected(ScoChangeFormation, "edit_module_set_code_apogee")
+    edit_module_set_code_apogee = sco_edit_module.edit_module_set_code_apogee
+    security.declareProtected(ScoView, "module_list")
+    module_list = sco_edit_module.module_list
+    # Tags
+    security.declareProtected(ScoView, "module_tag_search")
+    module_tag_search = sco_tag_module.module_tag_search
+    security.declareProtected(
+        ScoView, "module_tag_set"
+    )  # should be ScoEditFormationTags, but not present in old installs => check in method
+    module_tag_set = sco_tag_module.module_tag_set
+
+    #
+    security.declareProtected(ScoView, "index_html")
+
+    def index_html(self, REQUEST=None):
+        "Page accueil formations"
+        lockicon = icontag(
+            "lock32_img", title="Comporte des semestres verrouillés", border="0"
+        )
+        suppricon = icontag(
+            "delete_small_img", border="0", alt="supprimer", title="Supprimer"
+        )
+        editicon = icontag(
+            "edit_img", border="0", alt="modifier", title="Modifier titres et code"
+        )
+
+        editable = REQUEST.AUTHENTICATED_USER.has_permission(ScoChangeFormation, self)
+
+        H = [
+            self.sco_header(REQUEST, page_title="Programmes formations"),
+            """<h2>Programmes pédagogiques</h2>
+              """,
+        ]
+        T = sco_formations.formation_list_table(self, REQUEST=REQUEST)
+
+        H.append(T.html())
+
+        if editable:
+            H.append(
+                """<p><a class="stdlink" href="formation_create">Créer une formation</a></p>
+      <p><a class="stdlink" href="formation_import_xml_form">Importer une formation (xml)</a></p>
+         <p class="help">Une "formation" est un programme pédagogique structuré en UE, matières et modules. Chaque semestre se réfère à une formation. La modification d'une formation affecte tous les semestres qui s'y réfèrent.</p>
+            """
+            )
+
+        H.append(self.sco_footer(REQUEST))
+        return "\n".join(H)
+
+    # --------------------------------------------------------------------
+    #
+    #    Notes Methods
+    #
+    # --------------------------------------------------------------------
+
+    # --- Formations
+    _formationEditor = EditableTable(
+        "notes_formations",
+        "formation_id",
+        (
+            "formation_id",
+            "acronyme",
+            "titre",
+            "titre_officiel",
+            "version",
+            "formation_code",
+            "type_parcours",
+            "code_specialite",
+        ),
+        sortkey="acronyme",
+    )
+
+    security.declareProtected(ScoChangeFormation, "do_formation_create")
+
+    def do_formation_create(self, args, REQUEST):
+        "create a formation"
+        cnx = self.GetDBConnexion()
+        # check unique acronyme/titre/version
+        a = args.copy()
+        if a.has_key("formation_id"):
+            del a["formation_id"]
+        F = self.formation_list(args=a)
+        if len(F) > 0:
+            log(
+                "do_formation_create: error: %d formations matching args=%s"
+                % (len(F), a)
+            )
+            raise ScoValueError("Formation non unique (%s) !" % str(a))
+        # Si pas de formation_code, l'enleve (default SQL)
+        if args.has_key("formation_code") and not args["formation_code"]:
+            del args["formation_code"]
+        #
+        r = self._formationEditor.create(cnx, args)
+
+        sco_news.add(
+            self,
+            REQUEST,
+            typ=NEWS_FORM,
+            text="Création de la formation %(titre)s (%(acronyme)s)" % args,
+        )
+        return r
+
+    security.declareProtected(ScoChangeFormation, "do_formation_delete")
+
+    def do_formation_delete(self, oid, REQUEST):
+        """delete a formation (and all its UE, matieres, modules)
+        XXX delete all ues, will break if there are validations ! USE WITH CARE !
+        """
+        F = self.formation_list(args={"formation_id": oid})[0]
+        if self.formation_has_locked_sems(oid):
+            raise ScoLockedFormError()
+        cnx = self.GetDBConnexion()
+        # delete all UE in this formation
+        ues = self.do_ue_list({"formation_id": oid})
+        for ue in ues:
+            self._do_ue_delete(ue["ue_id"], REQUEST=REQUEST, force=True)
+
+        self._formationEditor.delete(cnx, oid)
+
+        # news
+        sco_news.add(
+            self,
+            REQUEST,
+            typ=NEWS_FORM,
+            object=oid,
+            text="Suppression de la formation %(acronyme)s" % F,
+        )
+
+    security.declareProtected(ScoView, "formation_list")
+
+    def formation_list(self, format=None, REQUEST=None, formation_id=None, args={}):
+        """List formation(s) with given id, or matching args
+        (when args is given, formation_id is ignored).
+        """
+        # logCallStack()
+        if not args:
+            if formation_id is None:
+                args = {}
+            else:
+                args = {"formation_id": formation_id}
+        cnx = self.GetDBConnexion()
+        r = self._formationEditor.list(cnx, args=args)
+        # log('%d formations found' % len(r))
+        return sendResult(REQUEST, r, name="formation", format=format)
+
+    security.declareProtected(ScoView, "formation_export")
+
+    def formation_export(
+        self, formation_id, export_ids=False, format=None, REQUEST=None
+    ):
+        "Export de la formation au format indiqué (xml ou json)"
+        return sco_formations.formation_export(
+            self, formation_id, export_ids=export_ids, format=format, REQUEST=REQUEST
+        )
+
+    security.declareProtected(ScoChangeFormation, "formation_import_xml")
+
+    def formation_import_xml(self, file, REQUEST):
+        "import d'une formation en XML"
+        log("formation_import_xml")
+        doc = file.read()
+        return sco_formations.formation_import_xml(self, REQUEST, doc)
+
+    security.declareProtected(ScoChangeFormation, "formation_import_xml_form")
+
+    def formation_import_xml_form(self, REQUEST):
+        "form import d'une formation en XML"
+        H = [
+            self.sco_header(page_title="Import d'une formation", REQUEST=REQUEST),
+            """<h2>Import d'une formation</h2>
+        <p>Création d'une formation (avec UE, matières, modules)
+        à partir un fichier XML (réservé aux utilisateurs avertis)</p>
+        """,
+        ]
+        footer = self.sco_footer(REQUEST)
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (("xmlfile", {"input_type": "file", "title": "Fichier XML", "size": 30}),),
+            submitlabel="Importer",
+            cancelbutton="Annuler",
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + footer
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+        else:
+            formation_id, junk, junk = self.formation_import_xml(
+                tf[2]["xmlfile"], REQUEST
+            )
+
+            return (
+                "\n".join(H)
+                + """<p>Import effectué !</p>
+            <p><a class="stdlink" href="ue_list?formation_id=%s">Voir la formation</a></p>"""
+                % formation_id
+                + footer
+            )
+
+    security.declareProtected(ScoChangeFormation, "formation_create_new_version")
+
+    def formation_create_new_version(self, formation_id, redirect=True, REQUEST=None):
+        "duplicate formation, with new version number"
+        xml = sco_formations.formation_export(
+            self, formation_id, export_ids=True, format="xml"
+        )
+        new_id, modules_old2new, ues_old2new = sco_formations.formation_import_xml(
+            self, REQUEST, xml
+        )
+        # news
+        F = self.formation_list(args={"formation_id": new_id})[0]
+        sco_news.add(
+            self,
+            REQUEST,
+            typ=NEWS_FORM,
+            object=new_id,
+            text="Nouvelle version de la formation %(acronyme)s" % F,
+        )
+        if redirect:
+            return REQUEST.RESPONSE.redirect(
+                "ue_list?formation_id=" + new_id + "&amp;msg=Nouvelle version !"
+            )
+        else:
+            return new_id, modules_old2new, ues_old2new
+
+    # --- UE
+    _ueEditor = EditableTable(
+        "notes_ue",
+        "ue_id",
+        (
+            "ue_id",
+            "formation_id",
+            "acronyme",
+            "numero",
+            "titre",
+            "type",
+            "ue_code",
+            "ects",
+            "is_external",
+            "code_apogee",
+            "coefficient",
+        ),
+        sortkey="numero",
+        input_formators={"type": int_null_is_zero},
+        output_formators={
+            "numero": int_null_is_zero,
+            "ects": float_null_is_null,
+            "coefficient": float_null_is_zero,
+        },
+    )
+
+    security.declareProtected(ScoChangeFormation, "do_ue_create")
+
+    def do_ue_create(self, args, REQUEST):
+        "create an ue"
+        cnx = self.GetDBConnexion()
+        # check duplicates
+        ues = self.do_ue_list(
+            {"formation_id": args["formation_id"], "acronyme": args["acronyme"]}
+        )
+        if ues:
+            raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"])
+        # create
+        r = self._ueEditor.create(cnx, args)
+
+        # news
+        F = self.formation_list(args={"formation_id": args["formation_id"]})[0]
+        sco_news.add(
+            self,
+            REQUEST,
+            typ=NEWS_FORM,
+            object=args["formation_id"],
+            text="Modification de la formation %(acronyme)s" % F,
+        )
+        return r
+
+    def _do_ue_delete(self, ue_id, delete_validations=False, REQUEST=None, force=False):
+        "delete UE and attached matieres (but not modules (it should ?))"
+        cnx = self.GetDBConnexion()
+        log(
+            "do_ue_delete: ue_id=%s, delete_validations=%s"
+            % (ue_id, delete_validations)
+        )
+        # check
+        ue = self.do_ue_list({"ue_id": ue_id})
+        if not ue:
+            raise ScoValueError("UE inexistante !")
+        ue = ue[0]
+        if self.ue_is_locked(ue["ue_id"]):
+            raise ScoLockedFormError()
+        # Il y a-t-il des etudiants ayant validé cette UE ?
+        # si oui, propose de supprimer les validations
+        validations = sco_parcours_dut.scolar_formsemestre_validation_list(
+            cnx, args={"ue_id": ue_id}
+        )
+        if validations and not delete_validations and not force:
+            return self.confirmDialog(
+                "<p>%d étudiants ont validé l'UE %s (%s)</p><p>Si vous supprimez cette UE, ces validations vont être supprimées !</p>"
+                % (len(validations), ue["acronyme"], ue["titre"]),
+                dest_url="",
+                REQUEST=REQUEST,
+                target_variable="delete_validations",
+                cancel_url="ue_list?formation_id=%s" % ue["formation_id"],
+                parameters={"ue_id": ue_id, "dialog_confirmed": 1},
+            )
+        if delete_validations:
+            log("deleting all validations of UE %s" % ue_id)
+            SimpleQuery(
+                self,
+                "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
+                {"ue_id": ue_id},
+            )
+
+        # delete all matiere in this UE
+        mats = self.do_matiere_list({"ue_id": ue_id})
+        for mat in mats:
+            self.do_matiere_delete(mat["matiere_id"], REQUEST)
+        # delete uecoef and events
+        SimpleQuery(
+            self,
+            "DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s",
+            {"ue_id": ue_id},
+        )
+        SimpleQuery(
+            self, "DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue_id}
+        )
+        cnx = self.GetDBConnexion()
+        self._ueEditor.delete(cnx, ue_id)
+        self._inval_cache()  # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement utilisé: acceptable de tout invalider ?)
+        # news
+        F = self.formation_list(args={"formation_id": ue["formation_id"]})[0]
+        sco_news.add(
+            self,
+            REQUEST,
+            typ=NEWS_FORM,
+            object=ue["formation_id"],
+            text="Modification de la formation %(acronyme)s" % F,
+        )
+        #
+        if not force:
+            return REQUEST.RESPONSE.redirect(
+                REQUEST.URL1 + "/ue_list?formation_id=" + str(ue["formation_id"])
+            )
+        else:
+            return None
+
+    security.declareProtected(ScoView, "do_ue_list")
+
+    def do_ue_list(self, *args, **kw):
+        "list UEs"
+        cnx = self.GetDBConnexion()
+        return self._ueEditor.list(cnx, *args, **kw)
+
+    # --- Matieres
+    _matiereEditor = EditableTable(
+        "notes_matieres",
+        "matiere_id",
+        ("matiere_id", "ue_id", "numero", "titre"),
+        sortkey="numero",
+        output_formators={"numero": int_null_is_zero},
+    )
+
+    security.declareProtected(ScoChangeFormation, "do_matiere_create")
+
+    def do_matiere_create(self, args, REQUEST):
+        "create a matiere"
+        cnx = self.GetDBConnexion()
+        # check
+        ue = self.do_ue_list({"ue_id": args["ue_id"]})[0]
+        # create matiere
+        r = self._matiereEditor.create(cnx, args)
+
+        # news
+        F = self.formation_list(args={"formation_id": ue["formation_id"]})[0]
+        sco_news.add(
+            self,
+            REQUEST,
+            typ=NEWS_FORM,
+            object=ue["formation_id"],
+            text="Modification de la formation %(acronyme)s" % F,
+        )
+        return r
+
+    security.declareProtected(ScoChangeFormation, "do_matiere_delete")
+
+    def do_matiere_delete(self, oid, REQUEST):
+        "delete matiere and attached modules"
+        cnx = self.GetDBConnexion()
+        # check
+        mat = self.do_matiere_list({"matiere_id": oid})[0]
+        ue = self.do_ue_list({"ue_id": mat["ue_id"]})[0]
+        locked = self.matiere_is_locked(mat["matiere_id"])
+        if locked:
+            log("do_matiere_delete: mat=%s" % mat)
+            log("do_matiere_delete: ue=%s" % ue)
+            log("do_matiere_delete: locked sems: %s" % locked)
+            raise ScoLockedFormError()
+        log("do_matiere_delete: matiere_id=%s" % oid)
+        # delete all modules in this matiere
+        mods = self.do_module_list({"matiere_id": oid})
+        for mod in mods:
+            self.do_module_delete(mod["module_id"], REQUEST)
+        self._matiereEditor.delete(cnx, oid)
+
+        # news
+        F = self.formation_list(args={"formation_id": ue["formation_id"]})[0]
+        sco_news.add(
+            self,
+            REQUEST,
+            typ=NEWS_FORM,
+            object=ue["formation_id"],
+            text="Modification de la formation %(acronyme)s" % F,
+        )
+
+    security.declareProtected(ScoView, "do_matiere_list")
+
+    def do_matiere_list(self, *args, **kw):
+        "list matieres"
+        cnx = self.GetDBConnexion()
+        return self._matiereEditor.list(cnx, *args, **kw)
+
+    security.declareProtected(ScoChangeFormation, "do_matiere_edit")
+
+    def do_matiere_edit(self, *args, **kw):
+        "edit a matiere"
+        cnx = self.GetDBConnexion()
+        # check
+        mat = self.do_matiere_list({"matiere_id": args[0]["matiere_id"]})[0]
+        ue = self.do_ue_list({"ue_id": mat["ue_id"]})[0]
+        if self.matiere_is_locked(mat["matiere_id"]):
+            raise ScoLockedFormError()
+        # edit
+        self._matiereEditor.edit(cnx, *args, **kw)
+        self._inval_cache()  # > modif matiere
+
+    security.declareProtected(ScoView, "do_matiere_formation_id")
+
+    def do_matiere_formation_id(self, matiere_id):
+        "get formation_id from matiere"
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "select UE.formation_id from notes_matieres M, notes_ue UE where M.matiere_id = %(matiere_id)s and M.ue_id = UE.ue_id",
+            {"matiere_id": matiere_id},
+        )
+        res = cursor.fetchall()
+        return res[0][0]
+
+    # --- Modules
+    _moduleEditor = EditableTable(
+        "notes_modules",
+        "module_id",
+        (
+            "module_id",
+            "titre",
+            "code",
+            "abbrev",
+            "heures_cours",
+            "heures_td",
+            "heures_tp",
+            "coefficient",
+            "ue_id",
+            "matiere_id",
+            "formation_id",
+            "semestre_id",
+            "numero",
+            "code_apogee",
+            "module_type"
+            #'ects'
+        ),
+        sortkey="numero, code, titre",
+        output_formators={
+            "heures_cours": float_null_is_zero,
+            "heures_td": float_null_is_zero,
+            "heures_tp": float_null_is_zero,
+            "numero": int_null_is_zero,
+            "coefficient": float_null_is_zero,
+            "module_type": int_null_is_zero
+            #'ects' : float_null_is_null
+        },
+    )
+
+    security.declareProtected(ScoChangeFormation, "do_module_create")
+
+    def do_module_create(self, args, REQUEST):
+        "create a module"
+        # create
+        cnx = self.GetDBConnexion()
+        r = self._moduleEditor.create(cnx, args)
+
+        # news
+        F = self.formation_list(args={"formation_id": args["formation_id"]})[0]
+        sco_news.add(
+            self,
+            REQUEST,
+            typ=NEWS_FORM,
+            object=args["formation_id"],
+            text="Modification de la formation %(acronyme)s" % F,
+        )
+        return r
+
+    security.declareProtected(ScoChangeFormation, "do_module_delete")
+
+    def do_module_delete(self, oid, REQUEST):
+        "delete module"
+        mod = self.do_module_list({"module_id": oid})[0]
+        if self.module_is_locked(mod["module_id"]):
+            raise ScoLockedFormError()
+
+        # S'il y a des moduleimpls, on ne peut pas detruire le module !
+        mods = self.do_moduleimpl_list(module_id=oid)
+        if mods:
+            err_page = self.confirmDialog(
+                message="""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>""",
+                helpmsg="""Il faut d'abord supprimer le semestre. Mais il est peut être préférable de laisser ce programme intact et d'en créer une nouvelle version pour la modifier.""",
+                dest_url="ue_list",
+                parameters={"formation_id": mod["formation_id"]},
+                REQUEST=REQUEST,
+            )
+            raise ScoGenError(err_page)
+        # delete
+        cnx = self.GetDBConnexion()
+        self._moduleEditor.delete(cnx, oid)
+
+        # news
+        F = self.formation_list(args={"formation_id": mod["formation_id"]})[0]
+        sco_news.add(
+            self,
+            REQUEST,
+            typ=NEWS_FORM,
+            object=mod["formation_id"],
+            text="Modification de la formation %(acronyme)s" % F,
+        )
+
+    security.declareProtected(ScoView, "do_module_list")
+
+    def do_module_list(self, *args, **kw):
+        "list modules"
+        cnx = self.GetDBConnexion()
+        return self._moduleEditor.list(cnx, *args, **kw)
+
+    security.declareProtected(ScoChangeFormation, "do_module_edit")
+
+    def do_module_edit(self, val):
+        "edit a module"
+        # check
+        mod = self.do_module_list({"module_id": val["module_id"]})[0]
+        if self.module_is_locked(mod["module_id"]):
+            # formation verrouillée: empeche de modifier certains champs:
+            protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id")
+            for f in protected_fields:
+                if f in val:
+                    del val[f]
+        # edit
+        cnx = self.GetDBConnexion()
+        self._moduleEditor.edit(cnx, val)
+
+        sems = sco_formsemestre.do_formsemestre_list(
+            self, args={"formation_id": mod["formation_id"]}
+        )
+        if sems:
+            self._inval_cache(
+                formsemestre_id_list=[s["formsemestre_id"] for s in sems]
+            )  # > modif module
+
+    #
+    security.declareProtected(ScoView, "formation_has_locked_sems")
+
+    def formation_has_locked_sems(self, formation_id):
+        "True if there is a locked formsemestre in this formation"
+        sems = sco_formsemestre.do_formsemestre_list(
+            self, args={"formation_id": formation_id, "etat": "0"}
+        )
+        return sems
+
+    security.declareProtected(ScoView, "formation_count_sems")
+
+    def formation_count_sems(self, formation_id):
+        "Number of formsemestre in this formation (locked or not)"
+        sems = sco_formsemestre.do_formsemestre_list(
+            self, args={"formation_id": formation_id}
+        )
+        return len(sems)
+
+    security.declareProtected(ScoView, "module_count_moduleimpls")
+
+    def module_count_moduleimpls(self, module_id):
+        "Number of moduleimpls using this module"
+        mods = self.do_moduleimpl_list(module_id=module_id)
+        return len(mods)
+
+    security.declareProtected(ScoView, "module_is_locked")
+
+    def module_is_locked(self, module_id):
+        """True if UE should not be modified
+        (used in a locked formsemestre)
+        """
+        r = SimpleDictFetch(
+            self,
+            """SELECT mi.* from notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
+            WHERE mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id
+            AND mi.module_id = %(module_id)s AND sem.etat = 0
+            """,
+            {"module_id": module_id},
+        )
+        return len(r) > 0
+
+    security.declareProtected(ScoView, "matiere_is_locked")
+
+    def matiere_is_locked(self, matiere_id):
+        """True if matiere should not be modified
+        (contains modules used in a locked formsemestre)
+        """
+        r = SimpleDictFetch(
+            self,
+            """SELECT ma.* from notes_matieres ma, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
+            WHERE ma.matiere_id = mod.matiere_id AND mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id
+            AND ma.matiere_id = %(matiere_id)s AND sem.etat = 0
+            """,
+            {"matiere_id": matiere_id},
+        )
+        return len(r) > 0
+
+    security.declareProtected(ScoView, "ue_is_locked")
+
+    def ue_is_locked(self, ue_id):
+        """True if module should not be modified
+        (contains modules used in a locked formsemestre)
+        """
+        r = SimpleDictFetch(
+            self,
+            """SELECT ue.* FROM notes_ue ue, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
+               WHERE ue.ue_id = mod.ue_id
+               AND mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id
+               AND ue.ue_id = %(ue_id)s AND sem.etat = 0
+            """,
+            {"ue_id": ue_id},
+        )
+        return len(r) > 0
+
+    security.declareProtected(ScoChangeFormation, "module_move")
+
+    def module_move(self, module_id, after=0, REQUEST=None, redirect=1):
+        """Move before/after previous one (decrement/increment numero)"""
+        module = self.do_module_list({"module_id": module_id})[0]
+        redirect = int(redirect)
+        after = int(after)  # 0: deplace avant, 1 deplace apres
+        if after not in (0, 1):
+            raise ValueError('invalid value for "after"')
+        formation_id = module["formation_id"]
+        others = self.do_module_list({"matiere_id": module["matiere_id"]})
+        # log('others=%s' % others)
+        if len(others) > 1:
+            idx = [p["module_id"] for p in others].index(module_id)
+            # log('module_move: after=%s idx=%s' % (after, idx))
+            neigh = None  # object to swap with
+            if after == 0 and idx > 0:
+                neigh = others[idx - 1]
+            elif after == 1 and idx < len(others) - 1:
+                neigh = others[idx + 1]
+            if neigh:  #
+                # swap numero between partition and its neighbor
+                # log('moving module %s' % module_id)
+                cnx = self.GetDBConnexion()
+                module["numero"], neigh["numero"] = neigh["numero"], module["numero"]
+                if module["numero"] == neigh["numero"]:
+                    neigh["numero"] -= 2 * after - 1
+                self._moduleEditor.edit(cnx, module)
+                self._moduleEditor.edit(cnx, neigh)
+
+        # redirect to ue_list page:
+        if redirect:
+            return REQUEST.RESPONSE.redirect("ue_list?formation_id=" + formation_id)
+
+    def ue_move(self, ue_id, after=0, REQUEST=None, redirect=1):
+        """Move UE before/after previous one (decrement/increment numero)"""
+        o = self.do_ue_list({"ue_id": ue_id})[0]
+        # log('ue_move %s (#%s) after=%s' % (ue_id, o['numero'], after))
+        redirect = int(redirect)
+        after = int(after)  # 0: deplace avant, 1 deplace apres
+        if after not in (0, 1):
+            raise ValueError('invalid value for "after"')
+        formation_id = o["formation_id"]
+        others = self.do_ue_list({"formation_id": formation_id})
+        if len(others) > 1:
+            idx = [p["ue_id"] for p in others].index(ue_id)
+            neigh = None  # object to swap with
+            if after == 0 and idx > 0:
+                neigh = others[idx - 1]
+            elif after == 1 and idx < len(others) - 1:
+                neigh = others[idx + 1]
+            if neigh:  #
+                # swap numero between partition and its neighbor
+                # log('moving ue %s (neigh #%s)' % (ue_id, neigh['numero']))
+                cnx = self.GetDBConnexion()
+                o["numero"], neigh["numero"] = neigh["numero"], o["numero"]
+                if o["numero"] == neigh["numero"]:
+                    neigh["numero"] -= 2 * after - 1
+                self._ueEditor.edit(cnx, o)
+                self._ueEditor.edit(cnx, neigh)
+        # redirect to ue_list page
+        if redirect:
+            return REQUEST.RESPONSE.redirect(
+                "ue_list?formation_id=" + o["formation_id"]
+            )
+
+    # --- Semestres de formation
+
+    security.declareProtected(ScoImplement, "do_formsemestre_create")
+
+    def do_formsemestre_create(self, args, REQUEST, silent=False):
+        "create a formsemestre"
+        cnx = self.GetDBConnexion()
+        formsemestre_id = sco_formsemestre._formsemestreEditor.create(cnx, args)
+        if args["etapes"]:
+            args["formsemestre_id"] = formsemestre_id
+            sco_formsemestre.write_formsemestre_etapes(self, args)
+        if args["responsables"]:
+            args["formsemestre_id"] = formsemestre_id
+            sco_formsemestre.write_formsemestre_responsables(self, args)
+
+        # create default partition
+        partition_id = sco_groups.partition_create(
+            self, formsemestre_id, default=True, redirect=0, REQUEST=REQUEST
+        )
+        _group_id = sco_groups.createGroup(
+            self, partition_id, default=True, REQUEST=REQUEST
+        )
+
+        # news
+        if not args.has_key("titre"):
+            args["titre"] = "sans titre"
+        args["formsemestre_id"] = formsemestre_id
+        args["url"] = (
+            "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args
+        )
+        if not silent:
+            sco_news.add(
+                self,
+                REQUEST,
+                typ=NEWS_SEM,
+                text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
+                url=args["url"],
+            )
+        return formsemestre_id
+
+    def formsemestre_list(
+        self,
+        format=None,
+        REQUEST=None,
+        formsemestre_id=None,
+        formation_id=None,
+        etape_apo=None,
+    ):
+        """List formsemestres in given format.
+        kw can specify some conditions: examples:
+           formsemestre_list( format='json', formation_id='F777', REQUEST=REQUEST)
+        """
+        # XAPI: new json api
+        args = {}
+        L = locals()
+        for argname in ("formsemestre_id", "formation_id", "etape_apo"):
+            if L[argname] is not None:
+                args[argname] = L[argname]
+        sems = sco_formsemestre.do_formsemestre_list(self, args=args)
+        # log('formsemestre_list: format="%s", %s semestres found' % (format,len(sems)))
+        return sendResult(REQUEST, sems, name="formsemestre", format=format)
+
+    security.declareProtected(ScoView, "XMLgetFormsemestres")
+
+    def XMLgetFormsemestres(self, etape_apo=None, formsemestre_id=None, REQUEST=None):
+        """List all formsemestres matching etape, XML format
+        DEPRECATED: use formsemestre_list()
+        """
+        log("Warning: calling deprecated XMLgetFormsemestres")
+        args = {}
+        if etape_apo:
+            args["etape_apo"] = etape_apo
+        if formsemestre_id:
+            args["formsemestre_id"] = formsemestre_id
+        if REQUEST:
+            REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+        doc = jaxml.XML_document(encoding=SCO_ENCODING)
+        doc.formsemestrelist()
+        for sem in sco_formsemestre.do_formsemestre_list(self, args=args):
+            doc._push()
+            doc.formsemestre(sem)
+            doc._pop()
+        return repr(doc)
+
+    security.declareProtected(ScoImplement, "do_formsemestre_edit")
+    do_formsemestre_edit = sco_formsemestre.do_formsemestre_edit
+
+    security.declareProtected(ScoView, "formsemestre_edit_options")
+    formsemestre_edit_options = sco_formsemestre_edit.formsemestre_edit_options
+
+    security.declareProtected(ScoView, "formsemestre_change_lock")
+    formsemestre_change_lock = sco_formsemestre_edit.formsemestre_change_lock
+
+    security.declareProtected(ScoView, "formsemestre_change_publication_bul")
+    formsemestre_change_publication_bul = (
+        sco_formsemestre_edit.formsemestre_change_publication_bul
+    )
+
+    security.declareProtected(ScoView, "view_formsemestre_by_etape")
+    view_formsemestre_by_etape = sco_formsemestre.view_formsemestre_by_etape
+
+    def _check_access_diretud(
+        self, formsemestre_id, REQUEST, required_permission=ScoImplement
+    ):
+        """Check if access granted: responsable or ScoImplement
+        Return True|False, HTML_error_page
+        """
+        authuser = REQUEST.AUTHENTICATED_USER
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        header = self.sco_header(page_title="Accès interdit", REQUEST=REQUEST)
+        footer = self.sco_footer(REQUEST)
+        if (str(authuser) not in sem["responsables"]) and not authuser.has_permission(
+            required_permission, self
+        ):
+            return (
+                False,
+                "\n".join(
+                    [
+                        header,
+                        "<h2>Opération non autorisée pour %s</h2>" % authuser,
+                        "<p>Responsable de ce semestre : <b>%s</b></p>"
+                        % ", ".join(sem["responsables"]),
+                        footer,
+                    ]
+                ),
+            )
+        else:
+            return True, ""
+
+    security.declareProtected(ScoView, "formsemestre_custommenu_edit")
+
+    def formsemestre_custommenu_edit(self, REQUEST, formsemestre_id):
+        "Dialogue modif menu"
+        # accessible à tous !
+        return sco_formsemestre_custommenu.formsemestre_custommenu_edit(
+            self, formsemestre_id, REQUEST=REQUEST
+        )
+
+    security.declareProtected(ScoView, "formsemestre_custommenu_html")
+    formsemestre_custommenu_html = (
+        sco_formsemestre_custommenu.formsemestre_custommenu_html
+    )
+
+    security.declareProtected(ScoView, "html_sem_header")
+
+    def html_sem_header(
+        self,
+        REQUEST,
+        title,
+        sem=None,
+        with_page_header=True,
+        with_h2=True,
+        page_title=None,
+        **args
+    ):
+        "Titre d'une page semestre avec lien vers tableau de bord"
+        # sem now unused and thus optional...
+        if with_page_header:
+            h = self.sco_header(
+                REQUEST, page_title="%s" % (page_title or title), **args
+            )
+        else:
+            h = ""
+        if with_h2:
+            return h + """<h2 class="formsemestre">%s</h2>""" % (title)
+        else:
+            return h
+
+    # --- Gestion des "Implémentations de Modules"
+    # Un "moduleimpl" correspond a la mise en oeuvre d'un module
+    # dans une formation spécifique, à une date spécifique.
+    _moduleimplEditor = EditableTable(
+        "notes_moduleimpl",
+        "moduleimpl_id",
+        (
+            "moduleimpl_id",
+            "module_id",
+            "formsemestre_id",
+            "responsable_id",
+            "computation_expr",
+        ),
+    )
+
+    _modules_enseignantsEditor = EditableTable(
+        "notes_modules_enseignants",
+        "modules_enseignants_id",
+        ("modules_enseignants_id", "moduleimpl_id", "ens_id"),
+    )
+
+    security.declareProtected(ScoImplement, "do_moduleimpl_create")
+
+    def do_moduleimpl_create(self, args):
+        "create a moduleimpl"
+        cnx = self.GetDBConnexion()
+        r = self._moduleimplEditor.create(cnx, args)
+        self._inval_cache(
+            formsemestre_id=args["formsemestre_id"]
+        )  # > creation moduleimpl
+        return r
+
+    security.declareProtected(ScoImplement, "do_moduleimpl_delete")
+
+    def do_moduleimpl_delete(self, oid, formsemestre_id=None):
+        "delete moduleimpl (desinscrit tous les etudiants)"
+        cnx = self.GetDBConnexion()
+        # --- desinscription des etudiants
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        req = "DELETE FROM notes_moduleimpl_inscription WHERE moduleimpl_id=%(moduleimpl_id)s"
+        cursor.execute(req, {"moduleimpl_id": oid})
+        # --- suppression des enseignants
+        cursor.execute(
+            "DELETE FROM notes_modules_enseignants WHERE moduleimpl_id=%(moduleimpl_id)s",
+            {"moduleimpl_id": oid},
+        )
+        # --- suppression des references dans les absences
+        cursor.execute(
+            "UPDATE absences SET moduleimpl_id=NULL WHERE moduleimpl_id=%(moduleimpl_id)s",
+            {"moduleimpl_id": oid},
+        )
+        # --- destruction du moduleimpl
+        self._moduleimplEditor.delete(cnx, oid)
+        self._inval_cache(formsemestre_id=formsemestre_id)  # > moduleimpl_delete
+
+    security.declareProtected(ScoView, "do_moduleimpl_list")
+
+    def do_moduleimpl_list(
+        self, moduleimpl_id=None, formsemestre_id=None, module_id=None, REQUEST=None
+    ):
+        "list moduleimpls"
+        args = locals()
+        cnx = self.GetDBConnexion()
+        modimpls = self._moduleimplEditor.list(cnx, args)  # *args, **kw)
+        # Ajoute la liste des enseignants
+        for mo in modimpls:
+            mo["ens"] = self.do_ens_list(args={"moduleimpl_id": mo["moduleimpl_id"]})
+        return return_text_if_published(modimpls, REQUEST)
+
+    security.declareProtected(ScoImplement, "do_moduleimpl_edit")
+
+    def do_moduleimpl_edit(self, args, formsemestre_id=None, cnx=None):
+        "edit a moduleimpl"
+        if not cnx:
+            cnx = self.GetDBConnexion()
+        self._moduleimplEditor.edit(cnx, args)
+
+        self._inval_cache(formsemestre_id=formsemestre_id)  # > modif moduleimpl
+
+    security.declareProtected(ScoView, "do_moduleimpl_withmodule_list")
+
+    def do_moduleimpl_withmodule_list(
+        self, moduleimpl_id=None, formsemestre_id=None, module_id=None, REQUEST=None
+    ):
+        """Liste les moduleimpls et ajoute dans chacun le module correspondant
+        Tri la liste par semestre/UE/numero_matiere/numero_module
+        """
+        args = locals()
+        del args["self"]
+        del args["REQUEST"]
+        modimpls = self.do_moduleimpl_list(**args)
+        for mo in modimpls:
+            mo["module"] = self.do_module_list(args={"module_id": mo["module_id"]})[0]
+            mo["ue"] = self.do_ue_list(args={"ue_id": mo["module"]["ue_id"]})[0]
+            mo["matiere"] = self.do_matiere_list(
+                args={"matiere_id": mo["module"]["matiere_id"]}
+            )[0]
+
+        # tri par semestre/UE/numero_matiere/numero_module
+
+        extr = lambda x: (
+            x["ue"]["numero"],
+            x["ue"]["ue_id"],
+            x["matiere"]["numero"],
+            x["matiere"]["matiere_id"],
+            x["module"]["numero"],
+            x["module"]["code"],
+        )
+
+        modimpls.sort(lambda x, y: cmp(extr(x), extr(y)))
+        # log('after sort args=%s' % args)
+        # log( ',\n'.join( [ str(extr(m)) for m in modimpls ] ))
+        # log('after sort: Mlist=\n' + ',\n'.join( [ str(m) for m in  modimpls ] ) + '\n')
+        return return_text_if_published(modimpls, REQUEST)
+
+    security.declareProtected(ScoView, "do_ens_list")
+
+    def do_ens_list(self, *args, **kw):
+        "liste les enseignants d'un moduleimpl (pas le responsable)"
+        cnx = self.GetDBConnexion()
+        ens = self._modules_enseignantsEditor.list(cnx, *args, **kw)
+        return ens
+
+    security.declareProtected(ScoImplement, "do_ens_edit")
+
+    def do_ens_edit(self, *args, **kw):
+        "edit ens"
+        cnx = self.GetDBConnexion()
+        self._modules_enseignantsEditor.edit(cnx, *args, **kw)
+
+    security.declareProtected(ScoImplement, "do_ens_create")
+
+    def do_ens_create(self, args):
+        "create ens"
+        cnx = self.GetDBConnexion()
+        r = self._modules_enseignantsEditor.create(cnx, args)
+        return r
+
+    security.declareProtected(ScoImplement, "do_ens_delete")
+
+    def do_ens_delete(self, oid):
+        "delete ens"
+        cnx = self.GetDBConnexion()
+        r = self._modules_enseignantsEditor.delete(cnx, oid)
+        return r
+
+    # --- dialogue modif enseignants/moduleimpl
+    security.declareProtected(ScoView, "edit_enseignants_form")
+
+    def edit_enseignants_form(self, REQUEST, moduleimpl_id):
+        "modif liste enseignants/moduleimpl"
+        M, sem = self.can_change_ens(REQUEST, moduleimpl_id)
+        # --
+        header = self.html_sem_header(
+            REQUEST,
+            'Enseignants du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
+            % (moduleimpl_id, M["module"]["titre"]),
+            page_title="Enseignants du module %s" % M["module"]["titre"],
+            javascripts=["libjs/AutoSuggest.js"],
+            cssstyles=["css/autosuggest_inquisitor.css"],
+            bodyOnLoad="init_tf_form('')",
+        )
+        footer = self.sco_footer(REQUEST)
+
+        # Liste des enseignants avec forme pour affichage / saisie avec suggestion
+        userlist = self.Users.get_userlist()
+        login2display = {}  # user_name : forme pour affichage = "NOM Prenom (login)"
+        for u in userlist:
+            login2display[u["user_name"]] = u["nomplogin"]
+            allowed_user_names = login2display.values()
+
+        H = [
+            "<ul><li><b>%s</b> (responsable)</li>"
+            % login2display.get(M["responsable_id"], M["responsable_id"])
+        ]
+        for ens in M["ens"]:
+            H.append(
+                '<li>%s (<a class="stdlink" href="edit_enseignants_form_delete?moduleimpl_id=%s&amp;ens_id=%s">supprimer</a>)</li>'
+                % (
+                    login2display.get(ens["ens_id"], ens["ens_id"]),
+                    moduleimpl_id,
+                    ens["ens_id"],
+                )
+            )
+        H.append("</ul>")
+        F = """<p class="help">Les enseignants d'un module ont le droit de
+        saisir et modifier toutes les notes des évaluations de ce module.
+        </p>
+        <p class="help">Pour changer le responsable du module, passez par la
+        page "<a class="stdlink" href="formsemestre_editwithmodules?formation_id=%s&amp;formsemestre_id=%s">Modification du semestre</a>", accessible uniquement au responsable de la formation (chef de département)
+        </p>
+        """ % (
+            sem["formation_id"],
+            M["formsemestre_id"],
+        )
+
+        modform = [
+            ("moduleimpl_id", {"input_type": "hidden"}),
+            (
+                "ens_id",
+                {
+                    "input_type": "text_suggest",
+                    "size": 50,
+                    "title": "Ajouter un enseignant",
+                    "allowed_values": allowed_user_names,
+                    "allow_null": False,
+                    "text_suggest_options": {
+                        "script": "Users/get_userlist_xml?",
+                        "varname": "start",
+                        "json": False,
+                        "noresults": "Valeur invalide !",
+                        "timeout": 60000,
+                    },
+                },
+            ),
+        ]
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            modform,
+            submitlabel="Ajouter enseignant",
+            cancelbutton="Annuler",
+        )
+        if tf[0] == 0:
+            return header + "\n".join(H) + tf[1] + F + footer
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(
+                "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
+            )
+        else:
+            ens_id = self.Users.get_user_name_from_nomplogin(tf[2]["ens_id"])
+            if not ens_id:
+                H.append(
+                    '<p class="help">Pour ajouter un enseignant, choisissez un nom dans le menu</p>'
+                )
+            else:
+                # et qu'il n'est pas deja:
+                if (
+                    ens_id in [x["ens_id"] for x in M["ens"]]
+                    or ens_id == M["responsable_id"]
+                ):
+                    H.append(
+                        '<p class="help">Enseignant %s déjà dans la liste !</p>'
+                        % ens_id
+                    )
+                else:
+                    self.do_ens_create(
+                        {"moduleimpl_id": moduleimpl_id, "ens_id": ens_id}
+                    )
+                    return REQUEST.RESPONSE.redirect(
+                        "edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id
+                    )
+            return header + "\n".join(H) + tf[1] + F + footer
+
+    security.declareProtected(ScoView, "edit_moduleimpl_resp")
+
+    def edit_moduleimpl_resp(self, REQUEST, moduleimpl_id):
+        """Changement d'un enseignant responsable de module
+        Accessible par Admin et dir des etud si flag resp_can_change_ens
+        """
+        M, sem = self.can_change_module_resp(REQUEST, moduleimpl_id)
+        H = [
+            self.html_sem_header(
+                REQUEST,
+                'Modification du responsable du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
+                % (moduleimpl_id, M["module"]["titre"]),
+                sem,
+                javascripts=["libjs/AutoSuggest.js"],
+                cssstyles=["css/autosuggest_inquisitor.css"],
+                bodyOnLoad="init_tf_form('')",
+            )
+        ]
+        help = """<p class="help">Taper le début du nom de l'enseignant.</p>"""
+        # Liste des enseignants avec forme pour affichage / saisie avec suggestion
+        userlist = self.Users.get_userlist()
+        login2display = {}  # user_name : forme pour affichage = "NOM Prenom (login)"
+        for u in userlist:
+            login2display[u["user_name"]] = u["nomplogin"]
+        allowed_user_names = login2display.values()
+
+        initvalues = M
+        initvalues["responsable_id"] = login2display.get(
+            M["responsable_id"], M["responsable_id"]
+        )
+        form = [
+            ("moduleimpl_id", {"input_type": "hidden"}),
+            (
+                "responsable_id",
+                {
+                    "input_type": "text_suggest",
+                    "size": 50,
+                    "title": "Responsable du module",
+                    "allowed_values": allowed_user_names,
+                    "allow_null": False,
+                    "text_suggest_options": {
+                        "script": "Users/get_userlist_xml?",
+                        "varname": "start",
+                        "json": False,
+                        "noresults": "Valeur invalide !",
+                        "timeout": 60000,
+                    },
+                },
+            ),
+        ]
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            form,
+            submitlabel="Changer responsable",
+            cancelbutton="Annuler",
+            initvalues=initvalues,
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + help + self.sco_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(
+                "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
+            )
+        else:
+            responsable_id = self.Users.get_user_name_from_nomplogin(
+                tf[2]["responsable_id"]
+            )
+            if (
+                not responsable_id
+            ):  # presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps)
+                return REQUEST.RESPONSE.redirect(
+                    "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
+                )
+            self.do_moduleimpl_edit(
+                {"moduleimpl_id": moduleimpl_id, "responsable_id": responsable_id},
+                formsemestre_id=sem["formsemestre_id"],
+            )
+            return REQUEST.RESPONSE.redirect(
+                "moduleimpl_status?moduleimpl_id="
+                + moduleimpl_id
+                + "&amp;head_message=responsable%20modifié"
+            )
+
+    _expr_help = """<p class="help">Expérimental: formule de calcul de la moyenne %(target)s</p>
+        <p class="help">Dans la formule, les variables suivantes sont définies:</p>
+        <ul class="help">
+        <li><tt>moy</tt> la moyenne, calculée selon la règle standard (moyenne pondérée)</li>
+        <li><tt>moy_is_valid</tt> vrai si la moyenne est valide (numérique)</li>
+        <li><tt>moy_val</tt> la valeur de la moyenne (nombre, valant 0 si invalide)</li>
+        <li><tt>notes</tt> vecteur des notes (/20) aux %(objs)s</li>
+        <li><tt>coefs</tt> vecteur des coefficients des %(objs)s, les coefs des %(objs)s sans notes (ATT, EXC) étant mis à zéro</li>
+        <li><tt>cmask</tt> vecteur de 0/1, 0 si le coef correspondant a été annulé</li>
+        <li>Nombre d'absences: <tt>nb_abs</tt>, <tt>nb_abs_just</tt>, <tt>nb_abs_nojust</tt> (en demi-journées)</li>
+        </ul>
+        <p class="help">Les éléments des vecteurs sont ordonnés dans l'ordre des %(objs)s%(ordre)s.</p>
+        <p class="help">Les fonctions suivantes sont utilisables: <tt>abs, cmp, dot, len, map, max, min, pow, reduce, round, sum, ifelse</tt></p>
+        <p class="help">La notation <tt>V(1,2,3)</tt> représente un vecteur <tt>(1,2,3)</tt></p>
+        <p class="help">Vous pouvez désactiver la formule (et revenir au mode de calcul "classique") 
+        en supprimant le texte ou en faisant précéder la première ligne par <tt>#</tt></p>
+    """
+
+    security.declareProtected(ScoView, "edit_moduleimpl_expr")
+
+    def edit_moduleimpl_expr(self, REQUEST, moduleimpl_id):
+        """Edition formule calcul moyenne module
+        Accessible par Admin, dir des etud et responsable module
+        """
+        M, sem = self.can_change_ens(REQUEST, moduleimpl_id)
+        H = [
+            self.html_sem_header(
+                REQUEST,
+                'Modification règle de calcul du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
+                % (moduleimpl_id, M["module"]["titre"]),
+                sem,
+            ),
+            self._expr_help
+            % {
+                "target": "du module",
+                "objs": "évaluations",
+                "ordre": " (le premier élément est la plus ancienne évaluation)",
+            },
+        ]
+        initvalues = M
+        form = [
+            ("moduleimpl_id", {"input_type": "hidden"}),
+            (
+                "computation_expr",
+                {
+                    "title": "Formule de calcul",
+                    "input_type": "textarea",
+                    "rows": 4,
+                    "cols": 60,
+                    "explanation": "formule de calcul (expérimental)",
+                },
+            ),
+        ]
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            form,
+            submitlabel="Modifier formule de calcul",
+            cancelbutton="Annuler",
+            initvalues=initvalues,
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + self.sco_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(
+                "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
+            )
+        else:
+            self.do_moduleimpl_edit(
+                {
+                    "moduleimpl_id": moduleimpl_id,
+                    "computation_expr": tf[2]["computation_expr"],
+                },
+                formsemestre_id=sem["formsemestre_id"],
+            )
+            self._inval_cache(
+                formsemestre_id=sem["formsemestre_id"]
+            )  # > modif regle calcul
+            return REQUEST.RESPONSE.redirect(
+                "moduleimpl_status?moduleimpl_id="
+                + moduleimpl_id
+                + "&amp;head_message=règle%20de%20calcul%20modifiée"
+            )
+
+    security.declareProtected(ScoView, "view_module_abs")
+
+    def view_module_abs(self, REQUEST, moduleimpl_id, format="html"):
+        """Visulalisation des absences a un module
+        """
+        M = self.do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
+        sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
+        debut_sem = DateDMYtoISO(sem["date_debut"])
+        fin_sem = DateDMYtoISO(sem["date_fin"])
+        list_insc = self.do_moduleimpl_listeetuds(moduleimpl_id)
+
+        T = []
+        for etudid in list_insc:
+            nb_abs = self.Absences.CountAbs(
+                etudid=etudid, debut=debut_sem, fin=fin_sem, moduleimpl_id=moduleimpl_id
+            )
+            if nb_abs:
+                nb_abs_just = self.Absences.CountAbsJust(
+                    etudid=etudid,
+                    debut=debut_sem,
+                    fin=fin_sem,
+                    moduleimpl_id=moduleimpl_id,
+                )
+                etud = self.getEtudInfo(etudid=etudid, filled=True)[0]
+                T.append(
+                    {
+                        "nomprenom": etud["nomprenom"],
+                        "just": nb_abs_just,
+                        "nojust": nb_abs - nb_abs_just,
+                        "total": nb_abs,
+                        "_nomprenom_target": "ficheEtud?etudid=%s" % etudid,
+                    }
+                )
+
+        H = [
+            self.html_sem_header(
+                REQUEST,
+                'Absences du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
+                % (moduleimpl_id, M["module"]["titre"]),
+                page_title="Absences du module %s" % (M["module"]["titre"]),
+                sem=sem,
+            )
+        ]
+        if not T and format == "html":
+            return (
+                "\n".join(H)
+                + "<p>Aucune absence signalée</p>"
+                + self.sco_footer(REQUEST)
+            )
+
+        tab = GenTable(
+            titles={
+                "nomprenom": "Nom",
+                "just": "Just.",
+                "nojust": "Non Just.",
+                "total": "Total",
+            },
+            columns_ids=("nomprenom", "just", "nojust", "total"),
+            rows=T,
+            html_class="table_leftalign",
+            base_url="%s?moduleimpl_id=%s" % (REQUEST.URL0, moduleimpl_id),
+            filename="absmodule_" + make_filename(M["module"]["titre"]),
+            caption="Absences dans le module %s" % M["module"]["titre"],
+            preferences=self.get_preferences(),
+        )
+
+        if format != "html":
+            return tab.make_page(self, format=format, REQUEST=REQUEST)
+
+        return "\n".join(H) + tab.html() + self.sco_footer(REQUEST)
+
+    security.declareProtected(ScoView, "edit_ue_expr")
+
+    def edit_ue_expr(self, REQUEST, formsemestre_id, ue_id):
+        """Edition formule calcul moyenne UE"""
+        # Check access
+        sem = sco_formsemestre_edit.can_edit_sem(self, REQUEST, formsemestre_id)
+        if not sem:
+            raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
+        cnx = self.GetDBConnexion()
+        #
+        ue = self.do_ue_list({"ue_id": ue_id})[0]
+        H = [
+            self.html_sem_header(
+                REQUEST,
+                "Modification règle de calcul de l'UE %s (%s)"
+                % (ue["acronyme"], ue["titre"]),
+                sem,
+            ),
+            self._expr_help % {"target": "de l'UE", "objs": "modules", "ordre": ""},
+        ]
+        el = sco_compute_moy.formsemestre_ue_computation_expr_list(
+            cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
+        )
+        if el:
+            initvalues = el[0]
+        else:
+            initvalues = {}
+        form = [
+            ("ue_id", {"input_type": "hidden"}),
+            ("formsemestre_id", {"input_type": "hidden"}),
+            (
+                "computation_expr",
+                {
+                    "title": "Formule de calcul",
+                    "input_type": "textarea",
+                    "rows": 4,
+                    "cols": 60,
+                    "explanation": "formule de calcul (expérimental)",
+                },
+            ),
+        ]
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            form,
+            submitlabel="Modifier formule de calcul",
+            cancelbutton="Annuler",
+            initvalues=initvalues,
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + self.sco_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(
+                "formsemestre_status?formsemestre_id=" + formsemestre_id
+            )
+        else:
+            if el:
+                el[0]["computation_expr"] = tf[2]["computation_expr"]
+                sco_compute_moy.formsemestre_ue_computation_expr_edit(cnx, el[0])
+            else:
+                sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, tf[2])
+
+            self._inval_cache(formsemestre_id=formsemestre_id)  # > modif regle calcul
+            return REQUEST.RESPONSE.redirect(
+                "formsemestre_status?formsemestre_id="
+                + formsemestre_id
+                + "&amp;head_message=règle%20de%20calcul%20modifiée"
+            )
+
+    security.declareProtected(ScoView, "formsemestre_enseignants_list")
+
+    def formsemestre_enseignants_list(self, REQUEST, formsemestre_id, format="html"):
+        """Liste les enseignants intervenants dans le semestre (resp. modules et chargés de TD)
+        et indique les absences saisies par chacun.
+        """
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        # resp. de modules:
+        mods = self.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+        sem_ens = {}
+        for mod in mods:
+            if not mod["responsable_id"] in sem_ens:
+                sem_ens[mod["responsable_id"]] = {"mods": [mod]}
+            else:
+                sem_ens[mod["responsable_id"]]["mods"].append(mod)
+        # charges de TD:
+        for mod in mods:
+            for ensd in mod["ens"]:
+                if not ensd["ens_id"] in sem_ens:
+                    sem_ens[ensd["ens_id"]] = {"mods": [mod]}
+                else:
+                    sem_ens[ensd["ens_id"]]["mods"].append(mod)
+        # compte les absences ajoutées par chacun dans tout le semestre
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        for ens in sem_ens:
+            cursor.execute(
+                "select * from scolog L, notes_formsemestre_inscription I where method='AddAbsence' and authenticated_user=%(authenticated_user)s and L.etudid = I.etudid and  I.formsemestre_id=%(formsemestre_id)s and date > %(date_debut)s and date < %(date_fin)s",
+                {
+                    "authenticated_user": ens,
+                    "formsemestre_id": formsemestre_id,
+                    "date_debut": DateDMYtoISO(sem["date_debut"]),
+                    "date_fin": DateDMYtoISO(sem["date_fin"]),
+                },
+            )
+
+            events = cursor.dictfetchall()
+            sem_ens[ens]["nbabsadded"] = len(events)
+
+        # description textuelle des modules
+        for ens in sem_ens:
+            sem_ens[ens]["descr_mods"] = ", ".join(
+                [x["module"]["code"] for x in sem_ens[ens]["mods"]]
+            )
+
+        # ajoute infos sur enseignant:
+        for ens in sem_ens:
+            sem_ens[ens].update(self.Users.user_info(ens))
+            if sem_ens[ens]["email"]:
+                sem_ens[ens]["_email_target"] = "mailto:%s" % sem_ens[ens]["email"]
+
+        sem_ens_list = sem_ens.values()
+        sem_ens_list.sort(lambda x, y: cmp(x["nomprenom"], y["nomprenom"]))
+
+        # --- Generate page with table
+        title = "Enseignants de " + sem["titremois"]
+        T = GenTable(
+            columns_ids=["nom_fmt", "prenom_fmt", "descr_mods", "nbabsadded", "email"],
+            titles={
+                "nom_fmt": "Nom",
+                "prenom_fmt": "Prénom",
+                "email": "Mail",
+                "descr_mods": "Modules",
+                "nbabsadded": "Saisies Abs.",
+            },
+            rows=sem_ens_list,
+            html_sortable=True,
+            html_class="table_leftalign",
+            filename=make_filename("Enseignants-" + sem["titreannee"]),
+            html_title=self.html_sem_header(
+                REQUEST, "Enseignants du semestre", sem, with_page_header=False
+            ),
+            base_url="%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id),
+            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=self.get_preferences(formsemestre_id),
+        )
+        return T.make_page(
+            self, page_title=title, title=title, REQUEST=REQUEST, format=format
+        )
+
+    security.declareProtected(ScoView, "edit_enseignants_form_delete")
+
+    def edit_enseignants_form_delete(self, REQUEST, moduleimpl_id, ens_id):
+        "remove ens"
+        M, sem = self.can_change_ens(REQUEST, moduleimpl_id)
+        # search ens_id
+        ok = False
+        for ens in M["ens"]:
+            if ens["ens_id"] == ens_id:
+                ok = True
+                break
+        if not ok:
+            raise ScoValueError("invalid ens_id (%s)" % ens_id)
+        self.do_ens_delete(ens["modules_enseignants_id"])
+        return REQUEST.RESPONSE.redirect(
+            "edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id
+        )
+
+    security.declareProtected(ScoView, "can_change_ens")
+
+    def can_change_ens(self, REQUEST, moduleimpl_id, raise_exc=True):
+        "check if current user can modify ens list (raise exception if not)"
+        M = self.do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
+        # -- check lock
+        sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
+        if sem["etat"] != "1":
+            if raise_exc:
+                raise ScoValueError("Modification impossible: semestre verrouille")
+            else:
+                return False
+        # -- check access
+        authuser = REQUEST.AUTHENTICATED_USER
+        uid = str(authuser)
+        # admin, resp. module ou resp. semestre
+        if (
+            uid != M["responsable_id"]
+            and not authuser.has_permission(ScoImplement, self)
+            and (uid not in sem["responsables"])
+        ):
+            if raise_exc:
+                raise AccessDenied("Modification impossible pour %s" % uid)
+            else:
+                return False
+        return M, sem
+
+    security.declareProtected(ScoView, "can_change_module_resp")
+
+    def can_change_module_resp(self, REQUEST, moduleimpl_id):
+        """Check if current user can modify module resp. (raise exception if not).
+        = Admin, et dir des etud. (si option l'y autorise)
+        """
+        M = self.do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
+        # -- check lock
+        sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
+        if sem["etat"] != "1":
+            raise ScoValueError("Modification impossible: semestre verrouille")
+        # -- check access
+        authuser = REQUEST.AUTHENTICATED_USER
+        uid = str(authuser)
+        # admin ou resp. semestre avec flag resp_can_change_resp
+        if not authuser.has_permission(ScoImplement, self) and (
+            (uid not in sem["responsables"]) or (not sem["resp_can_change_ens"])
+        ):
+            raise AccessDenied("Modification impossible pour %s" % uid)
+        return M, sem
+
+    # --- Gestion des inscriptions aux modules
+    _formsemestre_inscriptionEditor = EditableTable(
+        "notes_formsemestre_inscription",
+        "formsemestre_inscription_id",
+        ("formsemestre_inscription_id", "etudid", "formsemestre_id", "etat"),
+        sortkey="formsemestre_id",
+    )
+
+    security.declareProtected(ScoEtudInscrit, "do_formsemestre_inscription_create")
+
+    def do_formsemestre_inscription_create(self, args, REQUEST, method=None):
+        "create a formsemestre_inscription (and sco event)"
+        cnx = self.GetDBConnexion()
+        log("do_formsemestre_inscription_create: args=%s" % str(args))
+        sems = sco_formsemestre.do_formsemestre_list(
+            self, {"formsemestre_id": args["formsemestre_id"]}
+        )
+        if len(sems) != 1:
+            raise ScoValueError(
+                "code de semestre invalide: %s" % args["formsemestre_id"]
+            )
+        sem = sems[0]
+        # check lock
+        if sem["etat"] != "1":
+            raise ScoValueError("inscription: semestre verrouille")
+        #
+        r = self._formsemestre_inscriptionEditor.create(cnx, args)
+        # Evenement
+        scolars.scolar_events_create(
+            cnx,
+            args={
+                "etudid": args["etudid"],
+                "event_date": time.strftime("%d/%m/%Y"),
+                "formsemestre_id": args["formsemestre_id"],
+                "event_type": "INSCRIPTION",
+            },
+        )
+        # Log etudiant
+        logdb(
+            REQUEST,
+            cnx,
+            method=method,
+            etudid=args["etudid"],
+            msg="inscription en semestre %s" % args["formsemestre_id"],
+            commit=False,
+        )
+        #
+        self._inval_cache(
+            formsemestre_id=args["formsemestre_id"]
+        )  # > inscription au semestre
+        return r
+
+    security.declareProtected(ScoImplement, "do_formsemestre_inscription_delete")
+
+    def do_formsemestre_inscription_delete(self, oid, formsemestre_id=None):
+        "delete formsemestre_inscription"
+        cnx = self.GetDBConnexion()
+        self._formsemestre_inscriptionEditor.delete(cnx, oid)
+
+        self._inval_cache(
+            formsemestre_id=formsemestre_id
+        )  # > desinscription du semestre
+
+    security.declareProtected(ScoView, "do_formsemestre_inscription_list")
+
+    def do_formsemestre_inscription_list(self, *args, **kw):
+        "list formsemestre_inscriptions"
+        cnx = self.GetDBConnexion()
+        return self._formsemestre_inscriptionEditor.list(cnx, *args, **kw)
+
+    security.declareProtected(ScoView, "do_formsemestre_inscription_listinscrits")
+
+    def do_formsemestre_inscription_listinscrits(self, formsemestre_id):
+        """Liste les inscrits (état I) à ce semestre et cache le résultat"""
+        cache = self.get_formsemestre_inscription_cache()
+        r = cache.get(formsemestre_id)
+        if r != None:
+            return r
+        # retreive list
+        r = self.do_formsemestre_inscription_list(
+            args={"formsemestre_id": formsemestre_id, "etat": "I"}
+        )
+        cache.set(formsemestre_id, r)
+        return r
+
+    security.declareProtected(ScoImplement, "do_formsemestre_inscription_edit")
+
+    def do_formsemestre_inscription_edit(self, args=None, formsemestre_id=None):
+        "edit a formsemestre_inscription"
+        cnx = self.GetDBConnexion()
+        self._formsemestre_inscriptionEditor.edit(cnx, args)
+        self._inval_cache(
+            formsemestre_id=formsemestre_id
+        )  # > modif inscription semestre (demission ?)
+
+    # Cache inscriptions semestres
+    def get_formsemestre_inscription_cache(self):
+        u = self.GetDBConnexionString()
+        if CACHE_formsemestre_inscription.has_key(u):
+            return CACHE_formsemestre_inscription[u]
+        else:
+            log("get_formsemestre_inscription_cache: new simpleCache")
+            CACHE_formsemestre_inscription[u] = sco_cache.simpleCache()
+            return CACHE_formsemestre_inscription[u]
+
+    security.declareProtected(ScoImplement, "formsemestre_desinscription")
+
+    def formsemestre_desinscription(
+        self, etudid, formsemestre_id, REQUEST=None, dialog_confirmed=False
+    ):
+        """desinscrit l'etudiant de ce semestre (et donc de tous les modules).
+        A n'utiliser qu'en cas d'erreur de saisie.
+        S'il s'agit d'un semestre extérieur et qu'il n'y a plus d'inscrit, 
+        le semestre sera supprimé.
+        """
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        # -- check lock
+        if sem["etat"] != "1":
+            raise ScoValueError("desinscription impossible: semestre verrouille")
+
+        # -- Si décisions de jury, désinscription interdite
+        nt = self._getNotesCache().get_NotesTable(self, formsemestre_id)
+        if nt.etud_has_decision(etudid):
+            raise ScoValueError(
+                """Désinscription impossible: l'étudiant a une décision de jury 
+                (la supprimer avant si nécessaire: 
+                <a href="formsemestre_validation_suppress_etud?etudid=%s&amp;formsemestre_id=%s">
+                supprimer décision jury</a>
+                )
+                """
+                % (etudid, formsemestre_id)
+            )
+        if not dialog_confirmed:
+            etud = self.getEtudInfo(etudid=etudid, filled=1)[0]
+            if sem["modalite"] != "EXT":
+                msg_ext = """
+                <p>%s sera désinscrit de tous les modules du semestre %s (%s - %s).</p>
+                <p>Cette opération ne doit être utilisée que pour corriger une <b>erreur</b> !
+                Un étudiant réellement inscrit doit le rester, le faire éventuellement <b>démissionner<b>.
+                </p>
+                """ % (
+                    etud["nomprenom"],
+                    sem["titre_num"],
+                    sem["date_debut"],
+                    sem["date_fin"],
+                )
+            else:  # semestre extérieur
+                msg_ext = """
+                <p>%s sera désinscrit du semestre extérieur %s (%s - %s).</p>
+                """ % (
+                    etud["nomprenom"],
+                    sem["titre_num"],
+                    sem["date_debut"],
+                    sem["date_fin"],
+                )
+                inscrits = self.do_formsemestre_inscription_list(
+                    args={"formsemestre_id": formsemestre_id}
+                )
+                nbinscrits = len(inscrits)
+                if nbinscrits <= 1:
+                    msg_ext = """<p class="warning">Attention: le semestre extérieur sera supprimé
+                    car il n'a pas d'autre étudiant inscrit.
+                    </p>
+                    """
+            return self.confirmDialog(
+                """<h2>Confirmer la demande de desinscription ?</h2>""" + msg_ext,
+                dest_url="",
+                REQUEST=REQUEST,
+                cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
+                parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
+            )
+
+        self.do_formsemestre_desinscription(etudid, formsemestre_id, REQUEST=REQUEST)
+
+        return (
+            self.sco_header(REQUEST)
+            + '<p>Etudiant désinscrit !</p><p><a class="stdlink" href="%s/ficheEtud?etudid=%s">retour à la fiche</a>'
+            % (self.ScoURL(), etudid)
+            + self.sco_footer(REQUEST)
+        )
+
+    def do_formsemestre_desinscription(self, etudid, formsemestre_id, REQUEST=None):
+        """Désinscription d'un étudiant.
+        Si semestre extérieur et dernier inscrit, suppression de ce semestre.
+        """
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        # -- check lock
+        if sem["etat"] != "1":
+            raise ScoValueError("desinscription impossible: semestre verrouille")
+
+        # -- Si decisions de jury, desinscription interdite
+        nt = self._getNotesCache().get_NotesTable(self, formsemestre_id)
+        if nt.etud_has_decision(etudid):
+            raise ScoValueError(
+                "desinscription impossible: l'étudiant a une décision de jury (la supprimer avant si nécessaire)"
+            )
+
+        insem = self.do_formsemestre_inscription_list(
+            args={"formsemestre_id": formsemestre_id, "etudid": etudid}
+        )
+        if not insem:
+            raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)
+        insem = insem[0]
+        # -- desinscription de tous les modules
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "select moduleimpl_inscription_id from notes_moduleimpl_inscription Im, notes_moduleimpl M  where Im.etudid=%(etudid)s and Im.moduleimpl_id = M.moduleimpl_id and M.formsemestre_id = %(formsemestre_id)s",
+            {"etudid": etudid, "formsemestre_id": formsemestre_id},
+        )
+        res = cursor.fetchall()
+        moduleimpl_inscription_ids = [x[0] for x in res]
+        for moduleimpl_inscription_id in moduleimpl_inscription_ids:
+            self.do_moduleimpl_inscription_delete(
+                moduleimpl_inscription_id, formsemestre_id=formsemestre_id
+            )
+        # -- desincription du semestre
+        self.do_formsemestre_inscription_delete(
+            insem["formsemestre_inscription_id"], formsemestre_id=formsemestre_id
+        )
+        # --- Semestre extérieur
+        if sem["modalite"] == "EXT":
+            inscrits = self.do_formsemestre_inscription_list(
+                args={"formsemestre_id": formsemestre_id}
+            )
+            nbinscrits = len(inscrits)
+            if nbinscrits == 0:
+                log(
+                    "do_formsemestre_desinscription: suppression du semestre extérieur %s"
+                    % formsemestre_id
+                )
+                sco_formsemestre_edit.do_formsemestre_delete(
+                    self, formsemestre_id, REQUEST=REQUEST
+                )
+
+        if REQUEST:
+            logdb(
+                REQUEST,
+                cnx,
+                method="formsemestre_desinscription",
+                etudid=etudid,
+                msg="desinscription semestre %s" % formsemestre_id,
+                commit=False,
+            )
+
+    # --- Inscriptions aux modules
+    _moduleimpl_inscriptionEditor = EditableTable(
+        "notes_moduleimpl_inscription",
+        "moduleimpl_inscription_id",
+        ("moduleimpl_inscription_id", "etudid", "moduleimpl_id"),
+    )
+
+    security.declareProtected(ScoEtudInscrit, "do_moduleimpl_inscription_create")
+
+    def do_moduleimpl_inscription_create(
+        self, args, REQUEST=None, formsemestre_id=None
+    ):
+        "create a moduleimpl_inscription"
+        cnx = self.GetDBConnexion()
+        log("do_moduleimpl_inscription_create: " + str(args))
+        r = self._moduleimpl_inscriptionEditor.create(cnx, args)
+        self._inval_cache(formsemestre_id=formsemestre_id)  # > moduleimpl_inscription
+        if REQUEST:
+            logdb(
+                REQUEST,
+                cnx,
+                method="moduleimpl_inscription",
+                etudid=args["etudid"],
+                msg="inscription module %s" % args["moduleimpl_id"],
+                commit=False,
+            )
+        return r
+
+    security.declareProtected(ScoImplement, "do_moduleimpl_inscription_delete")
+
+    def do_moduleimpl_inscription_delete(self, oid, formsemestre_id=None):
+        "delete moduleimpl_inscription"
+        cnx = self.GetDBConnexion()
+        self._moduleimpl_inscriptionEditor.delete(cnx, oid)
+        self._inval_cache(formsemestre_id=formsemestre_id)  # > moduleimpl_inscription
+
+    security.declareProtected(ScoView, "do_moduleimpl_inscription_list")
+
+    def do_moduleimpl_inscription_list(
+        self, moduleimpl_id=None, etudid=None, REQUEST=None
+    ):
+        "list moduleimpl_inscriptions"
+        args = locals()
+        cnx = self.GetDBConnexion()
+        return return_text_if_published(
+            self._moduleimpl_inscriptionEditor.list(cnx, args), REQUEST
+        )
+
+    security.declareProtected(ScoView, "do_moduleimpl_listeetuds")
+
+    def do_moduleimpl_listeetuds(self, moduleimpl_id):
+        "retourne liste des etudids inscrits a ce module"
+        req = "select distinct Im.etudid from notes_moduleimpl_inscription Im, notes_formsemestre_inscription Isem, notes_moduleimpl M where Isem.etudid=Im.etudid and Im.moduleimpl_id=M.moduleimpl_id and M.moduleimpl_id = %(moduleimpl_id)s"
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(req, {"moduleimpl_id": moduleimpl_id})
+        res = cursor.fetchall()
+        return [x[0] for x in res]
+
+    security.declareProtected(ScoEtudInscrit, "do_moduleimpl_inscrit_tout_semestre")
+
+    def do_moduleimpl_inscrit_tout_semestre(self, moduleimpl_id, formsemestre_id):
+        "inscrit tous les etudiants inscrit au semestre a ce module"
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        req = """INSERT INTO notes_moduleimpl_inscription
+                             (moduleimpl_id, etudid)
+                    SELECT %(moduleimpl_id)s, I.etudid
+                    FROM  notes_formsemestre_inscription I
+                    WHERE I.formsemestre_id=%(formsemestre_id)s"""
+        args = {"moduleimpl_id": moduleimpl_id, "formsemestre_id": formsemestre_id}
+        cursor.execute(req, args)
+
+    security.declareProtected(ScoEtudInscrit, "do_moduleimpl_inscrit_etuds")
+
+    def do_moduleimpl_inscrit_etuds(
+        self, moduleimpl_id, formsemestre_id, etudids, reset=False, REQUEST=None
+    ):
+        """Inscrit les etudiants (liste d'etudids) a ce module.
+        Si reset, desinscrit tous les autres.
+        """
+        # Verifie qu'ils sont tous bien inscrits au semestre
+        for etudid in etudids:
+            insem = self.do_formsemestre_inscription_list(
+                args={"formsemestre_id": formsemestre_id, "etudid": etudid}
+            )
+            if not insem:
+                raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)
+
+        # Desinscriptions
+        if reset:
+            cnx = self.GetDBConnexion()
+            cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+            cursor.execute(
+                "delete from notes_moduleimpl_inscription where moduleimpl_id = %(moduleimpl_id)s",
+                {"moduleimpl_id": moduleimpl_id},
+            )
+        # Inscriptions au module:
+        inmod_set = Set(
+            [
+                x["etudid"]
+                for x in self.do_moduleimpl_inscription_list(
+                    moduleimpl_id=moduleimpl_id
+                )
+            ]
+        )
+        for etudid in etudids:
+            # deja inscrit ?
+            if not etudid in inmod_set:
+                self.do_moduleimpl_inscription_create(
+                    {"moduleimpl_id": moduleimpl_id, "etudid": etudid},
+                    REQUEST=REQUEST,
+                    formsemestre_id=formsemestre_id,
+                )
+
+        self._inval_cache(formsemestre_id=formsemestre_id)  # > moduleimpl_inscrit_etuds
+
+    security.declareProtected(ScoEtudInscrit, "etud_desinscrit_ue")
+
+    def etud_desinscrit_ue(self, etudid, formsemestre_id, ue_id, REQUEST=None):
+        """Desinscrit l'etudiant de tous les modules de cette UE dans ce semestre.
+        """
+        sco_moduleimpl_inscriptions.do_etud_desinscrit_ue(
+            self, etudid, formsemestre_id, ue_id, REQUEST=REQUEST
+        )
+        return REQUEST.RESPONSE.redirect(
+            self.ScoURL()
+            + "/Notes/moduleimpl_inscriptions_stats?formsemestre_id="
+            + formsemestre_id
+        )
+
+    security.declareProtected(ScoEtudInscrit, "etud_inscrit_ue")
+
+    def etud_inscrit_ue(self, etudid, formsemestre_id, ue_id, REQUEST=None):
+        """Inscrit l'etudiant de tous les modules de cette UE dans ce semestre.
+        """
+        sco_moduleimpl_inscriptions.do_etud_inscrit_ue(
+            self, etudid, formsemestre_id, ue_id, REQUEST=REQUEST
+        )
+        return REQUEST.RESPONSE.redirect(
+            self.ScoURL()
+            + "/Notes/moduleimpl_inscriptions_stats?formsemestre_id="
+            + formsemestre_id
+        )
+
+    # --- Inscriptions
+    security.declareProtected(
+        ScoEtudInscrit, "formsemestre_inscription_with_modules_form"
+    )
+    formsemestre_inscription_with_modules_form = (
+        sco_formsemestre_inscriptions.formsemestre_inscription_with_modules_form
+    )
+
+    security.declareProtected(
+        ScoEtudInscrit, "formsemestre_inscription_with_modules_etud"
+    )
+    formsemestre_inscription_with_modules_etud = (
+        sco_formsemestre_inscriptions.formsemestre_inscription_with_modules_etud
+    )
+
+    security.declareProtected(ScoEtudInscrit, "formsemestre_inscription_with_modules")
+    formsemestre_inscription_with_modules = (
+        sco_formsemestre_inscriptions.formsemestre_inscription_with_modules
+    )
+
+    security.declareProtected(ScoEtudInscrit, "formsemestre_inscription_option")
+    formsemestre_inscription_option = (
+        sco_formsemestre_inscriptions.formsemestre_inscription_option
+    )
+
+    security.declareProtected(ScoEtudInscrit, "do_moduleimpl_incription_options")
+    do_moduleimpl_incription_options = (
+        sco_formsemestre_inscriptions.do_moduleimpl_incription_options
+    )
+
+    security.declareProtected(ScoView, "formsemestre_inscrits_ailleurs")
+    formsemestre_inscrits_ailleurs = (
+        sco_formsemestre_inscriptions.formsemestre_inscrits_ailleurs
+    )
+
+    security.declareProtected(ScoEtudInscrit, "moduleimpl_inscriptions_edit")
+    moduleimpl_inscriptions_edit = (
+        sco_moduleimpl_inscriptions.moduleimpl_inscriptions_edit
+    )
+
+    security.declareProtected(ScoView, "moduleimpl_inscriptions_stats")
+    moduleimpl_inscriptions_stats = (
+        sco_moduleimpl_inscriptions.moduleimpl_inscriptions_stats
+    )
+
+    # --- Evaluations
+    _evaluationEditor = EditableTable(
+        "notes_evaluation",
+        "evaluation_id",
+        (
+            "evaluation_id",
+            "moduleimpl_id",
+            "jour",
+            "heure_debut",
+            "heure_fin",
+            "description",
+            "note_max",
+            "coefficient",
+            "visibulletin",
+            "publish_incomplete",
+            "evaluation_type",
+            "numero",
+        ),
+        sortkey="numero desc, jour desc, heure_debut desc",  # plus recente d'abord
+        output_formators={
+            "jour": DateISOtoDMY,
+            "visibulletin": str,
+            "publish_incomplete": str,
+            "numero": int_null_is_zero,
+        },
+        input_formators={
+            "jour": DateDMYtoISO,
+            "heure_debut": TimetoISO8601,  # converti par do_evaluation_list
+            "heure_fin": TimetoISO8601,  # converti par do_evaluation_list
+            "visibulletin": int,
+            "publish_incomplete": int,
+        },
+    )
+
+    def _evaluation_check_write_access(self, REQUEST, moduleimpl_id=None):
+        """Vérifie que l'on a le droit de modifier, créer ou détruire une
+        évaluation dans ce module.
+        Sinon, lance une exception.
+        (nb: n'implique pas le droit de saisir ou modifier des notes)
+        """
+        # acces pour resp. moduleimpl et resp. form semestre (dir etud)
+        if moduleimpl_id is None:
+            raise ValueError("no moduleimpl specified")  # bug
+        authuser = REQUEST.AUTHENTICATED_USER
+        uid = str(authuser)
+        M = self.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+        sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
+
+        if (
+            (not authuser.has_permission(ScoEditAllEvals, self))
+            and uid != M["responsable_id"]
+            and uid not in sem["responsables"]
+        ):
+            if sem["ens_can_edit_eval"]:
+                for ens in M["ens"]:
+                    if ens["ens_id"] == uid:
+                        return  # ok
+            raise AccessDenied("Modification évaluation impossible pour %s" % (uid,))
+
+    security.declareProtected(ScoEnsView, "do_evaluation_create")
+
+    def do_evaluation_create(
+        self,
+        moduleimpl_id=None,
+        jour=None,
+        heure_debut=None,
+        heure_fin=None,
+        description=None,
+        note_max=None,
+        coefficient=None,
+        visibulletin=None,
+        publish_incomplete=None,
+        evaluation_type=None,
+        numero=None,
+        REQUEST=None,
+        **kw
+    ):
+        """Create an evaluation
+        """
+        args = locals()
+        log("do_evaluation_create: args=" + str(args))
+        self._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id)
+        self._check_evaluation_args(args)
+        # Check numeros
+        sco_evaluations.module_evaluation_renumber(
+            self, moduleimpl_id, REQUEST=REQUEST, only_if_unumbered=True
+        )
+        if not "numero" in args or args["numero"] is None:
+            n = None
+            # determine le numero avec la date
+            # Liste des eval existantes triees par date, la plus ancienne en tete
+            ModEvals = self.do_evaluation_list(
+                args={"moduleimpl_id": moduleimpl_id},
+                sortkey="jour asc, heure_debut asc",
+            )
+            if args["jour"]:
+                next_eval = None
+                t = (DateDMYtoISO(args["jour"]), TimetoISO8601(args["heure_debut"]))
+                for e in ModEvals:
+                    if (DateDMYtoISO(e["jour"]), TimetoISO8601(e["heure_debut"])) > t:
+                        next_eval = e
+                        break
+                if next_eval:
+                    n = sco_evaluations.module_evaluation_insert_before(
+                        self, ModEvals, next_eval, REQUEST
+                    )
+                else:
+                    n = None  # a placer en fin
+            if n is None:  # pas de date ou en fin:
+                if ModEvals:
+                    log(pprint.pformat(ModEvals[-1]))
+                    n = ModEvals[-1]["numero"] + 1
+                else:
+                    n = 0  # the only one
+            # log("creating with numero n=%d" % n)
+            args["numero"] = n
+
+        #
+        cnx = self.GetDBConnexion()
+        r = self._evaluationEditor.create(cnx, args)
+
+        # news
+        M = self.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+        mod = self.do_module_list(args={"module_id": M["module_id"]})[0]
+        mod["moduleimpl_id"] = M["moduleimpl_id"]
+        mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
+        sco_news.add(
+            self,
+            REQUEST,
+            typ=NEWS_NOTE,
+            object=moduleimpl_id,
+            text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>'
+            % mod,
+            url=mod["url"],
+        )
+
+        return r
+
+    def _check_evaluation_args(self, args):
+        "Check coefficient, dates and duration, raises exception if invalid"
+        moduleimpl_id = args["moduleimpl_id"]
+        # check bareme
+        note_max = args.get("note_max", None)
+        if note_max is None:
+            raise ScoValueError("missing note_max")
+        try:
+            note_max = float(note_max)
+        except ValueError:
+            raise ScoValueError("Invalid note_max value")
+        if note_max < 0:
+            raise ScoValueError("Invalid note_max value (must be positive or null)")
+        # check coefficient
+        coef = args.get("coefficient", None)
+        if coef is None:
+            raise ScoValueError("missing coefficient")
+        try:
+            coef = float(coef)
+        except ValueError:
+            raise ScoValueError("Invalid coefficient value")
+        if coef < 0:
+            raise ScoValueError("Invalid coefficient value (must be positive or null)")
+        # check date
+        jour = args.get("jour", None)
+        args["jour"] = jour
+        if jour:
+            M = self.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+            sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
+            d, m, y = [int(x) for x in sem["date_debut"].split("/")]
+            date_debut = datetime.date(y, m, d)
+            d, m, y = [int(x) for x in sem["date_fin"].split("/")]
+            date_fin = datetime.date(y, m, d)
+            # passe par DateDMYtoISO pour avoir date pivot
+            y, m, d = [int(x) for x in DateDMYtoISO(jour).split("-")]
+            jour = datetime.date(y, m, d)
+            if (jour > date_fin) or (jour < date_debut):
+                raise ScoValueError(
+                    "La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
+                    % (d, m, y)
+                )
+        heure_debut = args.get("heure_debut", None)
+        args["heure_debut"] = heure_debut
+        heure_fin = args.get("heure_fin", None)
+        args["heure_fin"] = heure_fin
+        if jour and ((not heure_debut) or (not heure_fin)):
+            raise ScoValueError("Les heures doivent être précisées")
+        d = TimeDuration(heure_debut, heure_fin)
+        if d and ((d < 0) or (d > 60 * 12)):
+            raise ScoValueError("Heures de l'évaluation incohérentes !")
+
+    security.declareProtected(ScoEnsView, "evaluation_delete")
+
+    def evaluation_delete(self, REQUEST, evaluation_id):
+        """Form delete evaluation"""
+        El = self.do_evaluation_list(args={"evaluation_id": evaluation_id})
+        if not El:
+            raise ValueError("Evalution inexistante ! (%s)" % evaluation_id)
+        E = El[0]
+        M = self.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+        Mod = self.do_module_list(args={"module_id": M["module_id"]})[0]
+        tit = "Suppression de l'évaluation %(description)s (%(jour)s)" % E
+        etat = sco_evaluations.do_evaluation_etat(self, evaluation_id)
+        H = [
+            self.html_sem_header(REQUEST, tit, with_h2=False),
+            """<h2 class="formsemestre">Module <tt>%(code)s</tt> %(titre)s</h2>"""
+            % Mod,
+            """<h3>%s</h3>""" % tit,
+            """<p class="help">Opération <span class="redboldtext">irréversible</span>. Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées.</p>""",
+        ]
+        warning = False
+        if etat["nb_notes_total"]:
+            warning = True
+            nb_desinscrits = etat["nb_notes_total"] - etat["nb_notes"]
+            H.append(
+                """<div class="ue_warning"><span>Il y a %s notes"""
+                % etat["nb_notes_total"]
+            )
+            if nb_desinscrits:
+                H.append(
+                    """ (dont %s d'étudiants qui ne sont plus inscrits)"""
+                    % nb_desinscrits
+                )
+            H.append(""" dans l'évaluation</span>""")
+            if etat["nb_notes"] == 0:
+                H.append(
+                    """<p>Vous pouvez quand même supprimer l'évaluation, les notes des étudiants désincrits seront effacées.</p>"""
+                )
+
+        if etat["nb_notes"]:
+            H.append(
+                """<p>Suppression impossible (effacer les notes d'abord)</p><p><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%s">retour au tableau de bord du module</a></p></div>"""
+                % E["moduleimpl_id"]
+            )
+            return "\n".join(H) + self.sco_footer(REQUEST)
+        if warning:
+            H.append("""</div>""")
+
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (("evaluation_id", {"input_type": "hidden"}),),
+            initvalues=E,
+            submitlabel="Confirmer la suppression",
+            cancelbutton="Annuler",
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + self.sco_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(
+                self.ScoURL()
+                + "/Notes/moduleimpl_status?moduleimpl_id="
+                + E["moduleimpl_id"]
+            )
+        else:
+            sco_evaluations.do_evaluation_delete(self, REQUEST, E["evaluation_id"])
+            return (
+                "\n".join(H)
+                + """<p>OK, évaluation supprimée.</p>
+            <p><a class="stdlink" href="%s">Continuer</a></p>"""
+                % (
+                    self.ScoURL()
+                    + "/Notes/moduleimpl_status?moduleimpl_id="
+                    + E["moduleimpl_id"]
+                )
+                + self.sco_footer(REQUEST)
+            )
+
+    security.declareProtected(ScoView, "do_evaluation_list")
+
+    def do_evaluation_list(self, args, sortkey=None):
+        """List evaluations, sorted by numero (or most recent date first).
+
+        Ajoute les champs:
+        'duree' : '2h30'
+        'matin' : 1 (commence avant 12:00) ou 0
+        'apresmidi' : 1 (termine après 12:00) ou 0
+        'descrheure' : ' de 15h00 à 16h30'
+        """
+        cnx = self.GetDBConnexion()
+        evals = self._evaluationEditor.list(cnx, args, sortkey=sortkey)
+        # calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi
+        for e in evals:
+            heure_debut_dt = e["heure_debut"] or datetime.time(
+                8, 00
+            )  # au cas ou pas d'heure (note externe?)
+            heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
+            e["heure_debut"] = TimefromISO8601(e["heure_debut"])
+            e["heure_fin"] = TimefromISO8601(e["heure_fin"])
+            e["jouriso"] = DateDMYtoISO(e["jour"])
+            heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
+            d = TimeDuration(heure_debut, heure_fin)
+            if d is not None:
+                m = d % 60
+                e["duree"] = "%dh" % (d / 60)
+                if m != 0:
+                    e["duree"] += "%02d" % m
+            else:
+                e["duree"] = ""
+            if heure_debut and (not heure_fin or heure_fin == heure_debut):
+                e["descrheure"] = " à " + heure_debut
+            elif heure_debut and heure_fin:
+                e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
+            else:
+                e["descrheure"] = ""
+            # matin, apresmidi: utile pour se referer aux absences:
+            if heure_debut_dt < datetime.time(12, 00):
+                e["matin"] = 1
+            else:
+                e["matin"] = 0
+            if heure_fin_dt > datetime.time(12, 00):
+                e["apresmidi"] = 1
+            else:
+                e["apresmidi"] = 0
+
+        return evals
+
+    security.declareProtected(ScoView, "do_evaluation_list_in_formsemestre")
+
+    def do_evaluation_list_in_formsemestre(self, formsemestre_id):
+        "list evaluations in this formsemestre"
+        cnx = self.GetDBConnexion()
+        mods = self.do_moduleimpl_list(formsemestre_id=formsemestre_id)
+        evals = []
+        for mod in mods:
+            evals += self.do_evaluation_list(
+                args={"moduleimpl_id": mod["moduleimpl_id"]}
+            )
+        return evals
+
+    security.declareProtected(ScoEnsView, "do_evaluation_edit")
+
+    def do_evaluation_edit(self, REQUEST, args):
+        "edit a evaluation"
+        evaluation_id = args["evaluation_id"]
+        the_evals = self.do_evaluation_list({"evaluation_id": evaluation_id})
+        if not the_evals:
+            raise ValueError("evaluation inexistante !")
+        moduleimpl_id = the_evals[0]["moduleimpl_id"]
+        args["moduleimpl_id"] = moduleimpl_id
+        self._check_evaluation_args(args)
+        self._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id)
+        cnx = self.GetDBConnexion()
+        self._evaluationEditor.edit(cnx, args)
+        # inval cache pour ce semestre
+        M = self.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+        self._inval_cache(
+            formsemestre_id=M["formsemestre_id"]
+        )  # > evaluation_edit (coef, ...)
+
+    security.declareProtected(ScoEnsView, "evaluation_edit")
+
+    def evaluation_edit(self, evaluation_id, REQUEST):
+        "form edit evaluation"
+        return sco_evaluations.evaluation_create_form(
+            self, evaluation_id=evaluation_id, REQUEST=REQUEST, edit=True
+        )
+
+    security.declareProtected(ScoEnsView, "evaluation_create")
+
+    def evaluation_create(self, moduleimpl_id, REQUEST):
+        "form create evaluation"
+        return sco_evaluations.evaluation_create_form(
+            self, moduleimpl_id=moduleimpl_id, REQUEST=REQUEST, edit=False
+        )
+
+    security.declareProtected(ScoView, "evaluation_listenotes")
+
+    def evaluation_listenotes(self, REQUEST=None):
+        """Affichage des notes d'une évaluation"""
+        if REQUEST.form.get("format", "html") == "html":
+            H = self.sco_header(
+                REQUEST,
+                cssstyles=["css/verticalhisto.css"],
+                javascripts=["js/etud_info.js"],
+                init_qtip=True,
+            )
+            F = self.sco_footer(REQUEST)
+        else:
+            H, F = "", ""
+        B = self.do_evaluation_listenotes(REQUEST)
+        return H + B + F
+
+    security.declareProtected(ScoView, "do_evaluation_listenotes")
+    do_evaluation_listenotes = sco_liste_notes.do_evaluation_listenotes
+
+    security.declareProtected(ScoView, "evaluation_list_operations")
+    evaluation_list_operations = sco_undo_notes.evaluation_list_operations
+
+    security.declareProtected(ScoView, "evaluation_check_absences_html")
+    evaluation_check_absences_html = sco_liste_notes.evaluation_check_absences_html
+
+    security.declareProtected(ScoView, "formsemestre_check_absences_html")
+    formsemestre_check_absences_html = sco_liste_notes.formsemestre_check_absences_html
+
+    # --- Placement des étudiants pour l'évaluation
+    security.declareProtected(ScoEnsView, "placement_eval_selectetuds")
+    placement_eval_selectetuds = sco_placement.placement_eval_selectetuds
+
+    security.declareProtected(ScoEnsView, "do_placement")
+    do_placement = sco_placement.do_placement
+
+    # --- Saisie des notes
+    security.declareProtected(ScoEnsView, "saisie_notes_tableur")
+    saisie_notes_tableur = sco_saisie_notes.saisie_notes_tableur
+
+    security.declareProtected(ScoEnsView, "feuille_saisie_notes")
+    feuille_saisie_notes = sco_saisie_notes.feuille_saisie_notes
+
+    security.declareProtected(ScoEnsView, "saisie_notes")
+    saisie_notes = sco_saisie_notes.saisie_notes
+
+    security.declareProtected(ScoEnsView, "save_note")
+    save_note = sco_saisie_notes.save_note
+
+    security.declareProtected(ScoEnsView, "do_evaluation_set_missing")
+    do_evaluation_set_missing = sco_saisie_notes.do_evaluation_set_missing
+
+    security.declareProtected(ScoView, "evaluation_suppress_alln")
+    evaluation_suppress_alln = sco_saisie_notes.evaluation_suppress_alln
+
+    security.declareProtected(ScoView, "can_edit_notes")
+
+    def can_edit_notes(self, authuser, moduleimpl_id, allow_ens=True):
+        """True if authuser can enter or edit notes in this module.
+        If allow_ens, grant access to all ens in this module
+        
+        Si des décisions de jury ont déjà été saisies dans ce semestre, 
+        seul le directeur des études peut saisir des notes (et il ne devrait pas).
+        """
+        uid = str(authuser)
+        M = self.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+        sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"])
+        if sem["etat"] != "1":
+            return False  # semestre verrouillé
+
+        if sco_parcours_dut.formsemestre_has_decisions(self, sem["formsemestre_id"]):
+            # il y a des décisions de jury dans ce semestre !
+            return (
+                authuser.has_permission(ScoEditAllNotes, self)
+                or uid in sem["responsables"]
+            )
+        else:
+            if (
+                (not authuser.has_permission(ScoEditAllNotes, self))
+                and uid != M["responsable_id"]
+                and uid not in sem["responsables"]
+            ):
+                # enseignant (chargé de TD) ?
+                if allow_ens:
+                    for ens in M["ens"]:
+                        if ens["ens_id"] == uid:
+                            return True
+                return False
+            else:
+                return True
+
+    security.declareProtected(ScoEditAllNotes, "dummy_ScoEditAllNotes")
+
+    def dummy_ScoEditAllNotes(self):
+        "dummy method, necessary to declare permission ScoEditAllNotes"
+        return True
+
+    security.declareProtected(ScoEditAllEvals, "dummy_ScoEditAllEvals")
+
+    def dummy_ScoEditAllEvals(self):
+        "dummy method, necessary to declare permission ScoEditAllEvals"
+        return True
+
+    security.declareProtected(ScoSuperAdmin, "dummy_ScoSuperAdmin")
+
+    def dummy_ScoSuperAdmin(self):
+        "dummy method, necessary to declare permission ScoSuperAdmin"
+        return True
+
+    security.declareProtected(ScoEtudChangeGroups, "dummy_ScoEtudChangeGroups")
+
+    def dummy_ScoEtudChangeGroups(self):
+        "dummy method, necessary to declare permission ScoEtudChangeGroups"
+        return True
+
+    security.declareProtected(ScoEtudSupprAnnotations, "dummy_ScoEtudSupprAnnotations")
+
+    def dummy_ScoEtudSupprAnnotations(self):
+        "dummy method, necessary to declare permission ScoEtudSupprAnnotations"
+        return True
+
+    security.declareProtected(ScoEditFormationTags, "dummy_ScoEditFormationTags")
+
+    def dummy_ScoEditFormationTags(self):
+        "dummy method, necessary to declare permission ScoEditFormationTags"
+        return True
+
+    # cache notes evaluations
+    def get_evaluations_cache(self):
+        u = self.GetDBConnexionString()
+        if CACHE_evaluations.has_key(u):
+            return CACHE_evaluations[u]
+        else:
+            log("get_evaluations_cache: new simpleCache")
+            CACHE_evaluations[u] = sco_cache.simpleCache()
+            return CACHE_evaluations[u]
+
+    def _notes_getall(
+        self, evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
+    ):
+        """get tt les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
+        Attention: inclue aussi les notes des étudiants qui ne sont plus inscrits au module.
+        """
+        # log('_notes_getall( e=%s fs=%s )' % (evaluation_id, filter_suppressed))
+        do_cache = (
+            filter_suppressed and table == "notes_notes" and (by_uid is None)
+        )  # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
+        if do_cache:
+            cache = self.get_evaluations_cache()
+            r = cache.get(evaluation_id)
+            if r != None:
+                return r
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cond = " where evaluation_id=%(evaluation_id)s"
+        if by_uid:
+            cond += " and uid=%(by_uid)s"
+
+        cursor.execute(
+            "select * from " + table + cond,
+            {"evaluation_id": evaluation_id, "by_uid": by_uid},
+        )
+        res = cursor.dictfetchall()
+        d = {}
+        if filter_suppressed:
+            for x in res:
+                if x["value"] != NOTES_SUPPRESS:
+                    d[x["etudid"]] = x
+        else:
+            for x in res:
+                d[x["etudid"]] = x
+        if do_cache:
+            cache.set(evaluation_id, d)
+        return d
+
+    # --- Bulletins
+    security.declareProtected(ScoView, "formsemestre_bulletins_pdf")
+
+    def formsemestre_bulletins_pdf(
+        self, formsemestre_id, REQUEST, version="selectedevals"
+    ):
+        "Publie les bulletins dans un classeur PDF"
+        pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
+            self, formsemestre_id, REQUEST, version=version
+        )
+        return sendPDFFile(REQUEST, pdfdoc, filename)
+
+    security.declareProtected(ScoView, "etud_bulletins_pdf")
+
+    def etud_bulletins_pdf(self, etudid, REQUEST, version="selectedevals"):
+        "Publie tous les bulletins d'un etudiants dans un classeur PDF"
+        pdfdoc, filename = sco_bulletins_pdf.get_etud_bulletins_pdf(
+            self, etudid, REQUEST, version=version
+        )
+        return sendPDFFile(REQUEST, pdfdoc, filename)
+
+    security.declareProtected(ScoView, "formsemestre_bulletins_pdf_choice")
+    formsemestre_bulletins_pdf_choice = sco_bulletins.formsemestre_bulletins_pdf_choice
+
+    security.declareProtected(ScoView, "formsemestre_bulletins_mailetuds_choice")
+    formsemestre_bulletins_mailetuds_choice = (
+        sco_bulletins.formsemestre_bulletins_mailetuds_choice
+    )
+
+    security.declareProtected(ScoView, "formsemestre_bulletins_mailetuds")
+
+    def formsemestre_bulletins_mailetuds(
+        self,
+        formsemestre_id,
+        REQUEST,
+        version="long",
+        dialog_confirmed=False,
+        prefer_mail_perso=0,
+    ):
+        "envoi a chaque etudiant (inscrit et ayant un mail) son bulletin"
+        prefer_mail_perso = int(prefer_mail_perso)
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        nt = self._getNotesCache().get_NotesTable(
+            self, formsemestre_id
+        )  # > get_etudids
+        etudids = nt.get_etudids()
+        #
+        if not sco_bulletins.can_send_bulletin_by_mail(self, formsemestre_id, REQUEST):
+            raise AccessDenied("vous n'avez pas le droit d'envoyer les bulletins")
+        # Confirmation dialog
+        if not dialog_confirmed:
+            return self.confirmDialog(
+                "<h2>Envoyer les %d bulletins par e-mail aux étudiants ?"
+                % len(etudids),
+                dest_url="",
+                REQUEST=REQUEST,
+                cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
+                parameters={
+                    "version": version,
+                    "formsemestre_id": formsemestre_id,
+                    "prefer_mail_perso": prefer_mail_perso,
+                },
+            )
+
+        # Make each bulletin
+        nb_send = 0
+        for etudid in etudids:
+            h, i = sco_bulletins.do_formsemestre_bulletinetud(
+                self,
+                formsemestre_id,
+                etudid,
+                version=version,
+                prefer_mail_perso=prefer_mail_perso,
+                format="pdfmail",
+                nohtml=True,
+                REQUEST=REQUEST,
+            )
+            if h:
+                nb_send += 1
+        #
+        return (
+            self.sco_header(REQUEST)
+            + '<p>%d bulletins sur %d envoyés par mail !</p><p><a class="stdlink" href="formsemestre_status?formsemestre_id=%s">continuer</a></p>'
+            % (nb_send, len(etudids), formsemestre_id)
+            + self.sco_footer(REQUEST)
+        )
+
+    security.declareProtected(ScoView, "formsemestre_bulletins_mailetuds")
+    external_ue_create_form = sco_ue_external.external_ue_create_form
+
+    security.declareProtected(ScoEnsView, "appreciation_add_form")
+
+    def appreciation_add_form(
+        self,
+        etudid=None,
+        formsemestre_id=None,
+        id=None,  # si id, edit
+        suppress=False,  # si true, supress id
+        REQUEST=None,
+    ):
+        "form ajout ou edition d'une appreciation"
+        cnx = self.GetDBConnexion()
+        authuser = REQUEST.AUTHENTICATED_USER
+        if id:  # edit mode
+            apps = scolars.appreciations_list(cnx, args={"id": id})
+            if not apps:
+                raise ScoValueError("id d'appreciation invalide !")
+            app = apps[0]
+            formsemestre_id = app["formsemestre_id"]
+            etudid = app["etudid"]
+        if REQUEST.form.has_key("edit"):
+            edit = int(REQUEST.form["edit"])
+        elif id:
+            edit = 1
+        else:
+            edit = 0
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        # check custom access permission
+        can_edit_app = (str(authuser) in sem["responsables"]) or (
+            authuser.has_permission(ScoEtudInscrit, self)
+        )
+        if not can_edit_app:
+            raise AccessDenied("vous n'avez pas le droit d'ajouter une appreciation")
+        #
+        bull_url = "formsemestre_bulletinetud?formsemestre_id=%s&amp;etudid=%s" % (
+            formsemestre_id,
+            etudid,
+        )
+        if suppress:
+            scolars.appreciations_delete(cnx, id)
+            logdb(REQUEST, cnx, method="appreciation_suppress", etudid=etudid, msg="")
+            return REQUEST.RESPONSE.redirect(bull_url)
+        #
+        etud = self.getEtudInfo(etudid=etudid, filled=1)[0]
+        if id:
+            a = "Edition"
+        else:
+            a = "Ajout"
+        H = [
+            self.sco_header(REQUEST)
+            + "<h2>%s d'une appréciation sur %s</h2>" % (a, etud["nomprenom"])
+        ]
+        F = self.sco_footer(REQUEST)
+        descr = [
+            ("edit", {"input_type": "hidden", "default": edit}),
+            ("etudid", {"input_type": "hidden"}),
+            ("formsemestre_id", {"input_type": "hidden"}),
+            ("id", {"input_type": "hidden"}),
+            ("comment", {"title": "", "input_type": "textarea", "rows": 4, "cols": 60}),
+        ]
+        if id:
+            initvalues = {
+                "etudid": etudid,
+                "formsemestre_id": formsemestre_id,
+                "comment": app["comment"],
+            }
+        else:
+            initvalues = {}
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            descr,
+            initvalues=initvalues,
+            cancelbutton="Annuler",
+            submitlabel="Ajouter appréciation",
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + "\n" + tf[1] + F
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(bull_url)
+        else:
+            args = {
+                "etudid": etudid,
+                "formsemestre_id": formsemestre_id,
+                "author": str(authuser),
+                "comment": tf[2]["comment"],
+                "zope_authenticated_user": str(authuser),
+                "zope_remote_addr": REQUEST.REMOTE_ADDR,
+            }
+            if edit:
+                args["id"] = id
+                scolars.appreciations_edit(cnx, args)
+            else:  # nouvelle
+                scolars.appreciations_create(cnx, args, has_uniq_values=False)
+            # log
+            logdb(
+                REQUEST,
+                cnx,
+                method="appreciation_add",
+                etudid=etudid,
+                msg=tf[2]["comment"],
+            )
+            # ennuyeux mais necessaire (pour le PDF seulement)
+            self._inval_cache(
+                pdfonly=True, formsemestre_id=formsemestre_id
+            )  # > appreciation_add
+            return REQUEST.RESPONSE.redirect(bull_url)
+
+    security.declareProtected(ScoView, "can_change_groups")
+
+    def can_change_groups(self, REQUEST, formsemestre_id):
+        "Vrai si utilisateur peut changer les groupes dans ce semestre"
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        if sem["etat"] != "1":
+            return False  # semestre verrouillé
+        authuser = REQUEST.AUTHENTICATED_USER
+        if authuser.has_permission(ScoEtudChangeGroups, self):
+            return True  # admin, chef dept
+        uid = str(authuser)
+        if uid in sem["responsables"]:
+            return True
+        return False
+
+    def _can_edit_pv(self, REQUEST, formsemestre_id):
+        "Vrai si utilisateur peut editer un PV de jury de ce semestre"
+
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        if self._is_chef_or_diretud(REQUEST, sem):
+            return True
+        # Autorise les secrétariats, repérés via la permission ScoEtudChangeAdr
+        # (ceci nous évite d'ajouter une permission Zope aux installations existantes)
+        authuser = REQUEST.AUTHENTICATED_USER
+        return authuser.has_permission(ScoEtudChangeAdr, self)
+
+    # --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES
+    def _can_validate_sem(self, REQUEST, formsemestre_id):
+        "Vrai si utilisateur peut saisir decision de jury dans ce semestre"
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        if sem["etat"] != "1":
+            return False  # semestre verrouillé
+
+        return self._is_chef_or_diretud(REQUEST, sem)
+
+    def _is_chef_or_diretud(self, REQUEST, sem):
+        "Vrai si utilisateur est admin, chef dept ou responsable du semestre"
+        authuser = REQUEST.AUTHENTICATED_USER
+        if authuser.has_permission(ScoImplement, self):
+            return True  # admin, chef dept
+        uid = str(authuser)
+        if uid in sem["responsables"]:
+            return True
+
+        return False
+
+    security.declareProtected(ScoView, "formsemestre_validation_etud_form")
+
+    def formsemestre_validation_etud_form(
+        self,
+        formsemestre_id,
+        etudid=None,
+        etud_index=None,
+        check=0,
+        desturl="",
+        sortcol=None,
+        REQUEST=None,
+    ):
+        "Formulaire choix jury pour un étudiant"
+        readonly = not self._can_validate_sem(REQUEST, formsemestre_id)
+        return sco_formsemestre_validation.formsemestre_validation_etud_form(
+            self,
+            formsemestre_id,
+            etudid=etudid,
+            etud_index=etud_index,
+            check=check,
+            readonly=readonly,
+            desturl=desturl,
+            sortcol=sortcol,
+            REQUEST=REQUEST,
+        )
+
+    security.declareProtected(ScoView, "formsemestre_validation_etud")
+
+    def formsemestre_validation_etud(
+        self,
+        formsemestre_id,
+        etudid=None,
+        codechoice=None,
+        desturl="",
+        sortcol=None,
+        REQUEST=None,
+    ):
+        "Enregistre choix jury pour un étudiant"
+        if not self._can_validate_sem(REQUEST, formsemestre_id):
+            return self.confirmDialog(
+                message="<p>Opération non autorisée pour %s</h2>"
+                % REQUEST.AUTHENTICATED_USER,
+                dest_url=self.ScoURL(),
+                REQUEST=REQUEST,
+            )
+
+        return sco_formsemestre_validation.formsemestre_validation_etud(
+            self,
+            formsemestre_id,
+            etudid=etudid,
+            codechoice=codechoice,
+            desturl=desturl,
+            sortcol=sortcol,
+            REQUEST=REQUEST,
+        )
+
+    security.declareProtected(ScoView, "formsemestre_validation_etud_manu")
+
+    def formsemestre_validation_etud_manu(
+        self,
+        formsemestre_id,
+        etudid=None,
+        code_etat="",
+        new_code_prev="",
+        devenir="",
+        assidu=False,
+        desturl="",
+        sortcol=None,
+        REQUEST=None,
+    ):
+        "Enregistre choix jury pour un étudiant"
+        if not self._can_validate_sem(REQUEST, formsemestre_id):
+            return self.confirmDialog(
+                message="<p>Opération non autorisée pour %s</h2>"
+                % REQUEST.AUTHENTICATED_USER,
+                dest_url=self.ScoURL(),
+                REQUEST=REQUEST,
+            )
+
+        return sco_formsemestre_validation.formsemestre_validation_etud_manu(
+            self,
+            formsemestre_id,
+            etudid=etudid,
+            code_etat=code_etat,
+            new_code_prev=new_code_prev,
+            devenir=devenir,
+            assidu=assidu,
+            desturl=desturl,
+            sortcol=sortcol,
+            REQUEST=REQUEST,
+        )
+
+    security.declareProtected(ScoView, "formsemestre_validate_previous_ue")
+
+    def formsemestre_validate_previous_ue(
+        self, formsemestre_id, etudid=None, REQUEST=None
+    ):
+        "Form. saisie UE validée hors ScoDoc "
+        if not self._can_validate_sem(REQUEST, formsemestre_id):
+            return self.confirmDialog(
+                message="<p>Opération non autorisée pour %s</h2>"
+                % REQUEST.AUTHENTICATED_USER,
+                dest_url=self.ScoURL(),
+                REQUEST=REQUEST,
+            )
+        return sco_formsemestre_validation.formsemestre_validate_previous_ue(
+            self, formsemestre_id, etudid, REQUEST=REQUEST
+        )
+
+    security.declareProtected(ScoView, "formsemestre_ext_create_form")
+    formsemestre_ext_create_form = (
+        sco_formsemestre_exterieurs.formsemestre_ext_create_form
+    )
+
+    security.declareProtected(ScoView, "formsemestre_ext_edit_ue_validations")
+
+    def formsemestre_ext_edit_ue_validations(
+        self, formsemestre_id, etudid=None, REQUEST=None
+    ):
+        "Form. edition UE semestre extérieur"
+        if not self._can_validate_sem(REQUEST, formsemestre_id):
+            return self.confirmDialog(
+                message="<p>Opération non autorisée pour %s</h2>"
+                % REQUEST.AUTHENTICATED_USER,
+                dest_url=self.ScoURL(),
+                REQUEST=REQUEST,
+            )
+        return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations(
+            self, formsemestre_id, etudid, REQUEST=REQUEST
+        )
+
+    security.declareProtected(ScoView, "get_etud_ue_cap_html")
+    get_etud_ue_cap_html = sco_formsemestre_validation.get_etud_ue_cap_html
+
+    security.declareProtected(ScoView, "etud_ue_suppress_validation")
+
+    def etud_ue_suppress_validation(self, etudid, formsemestre_id, ue_id, REQUEST=None):
+        """Suppress a validation (ue_id, etudid) and redirect to formsemestre"""
+        if not self._can_validate_sem(REQUEST, formsemestre_id):
+            return self.confirmDialog(
+                message="<p>Opération non autorisée pour %s</h2>"
+                % REQUEST.AUTHENTICATED_USER,
+                dest_url=self.ScoURL(),
+                REQUEST=REQUEST,
+            )
+        return sco_formsemestre_validation.etud_ue_suppress_validation(
+            self, etudid, formsemestre_id, ue_id, REQUEST=REQUEST
+        )
+
+    security.declareProtected(ScoView, "formsemestre_validation_auto")
+
+    def formsemestre_validation_auto(self, formsemestre_id, REQUEST):
+        "Formulaire saisie automatisee des decisions d'un semestre"
+        if not self._can_validate_sem(REQUEST, formsemestre_id):
+            return self.confirmDialog(
+                message="<p>Opération non autorisée pour %s</h2>"
+                % REQUEST.AUTHENTICATED_USER,
+                dest_url=self.ScoURL(),
+                REQUEST=REQUEST,
+            )
+
+        return sco_formsemestre_validation.formsemestre_validation_auto(
+            self, formsemestre_id, REQUEST
+        )
+
+    security.declareProtected(ScoView, "formsemestre_validation_auto")
+
+    def do_formsemestre_validation_auto(self, formsemestre_id, REQUEST):
+        "Formulaire saisie automatisee des decisions d'un semestre"
+        if not self._can_validate_sem(REQUEST, formsemestre_id):
+            return self.confirmDialog(
+                message="<p>Opération non autorisée pour %s</h2>"
+                % REQUEST.AUTHENTICATED_USER,
+                dest_url=self.ScoURL(),
+                REQUEST=REQUEST,
+            )
+
+        return sco_formsemestre_validation.do_formsemestre_validation_auto(
+            self, formsemestre_id, REQUEST
+        )
+
+    security.declareProtected(ScoView, "formsemestre_fix_validation_ues")
+
+    def formsemestre_fix_validation_ues(self, formsemestre_id, REQUEST=None):
+        "Verif/reparation codes UE"
+        if not self._can_validate_sem(REQUEST, formsemestre_id):
+            return self.confirmDialog(
+                message="<p>Opération non autorisée pour %s</h2>"
+                % REQUEST.AUTHENTICATED_USER,
+                dest_url=self.ScoURL(),
+                REQUEST=REQUEST,
+            )
+
+        return sco_formsemestre_validation.formsemestre_fix_validation_ues(
+            self, formsemestre_id, REQUEST
+        )
+
+    security.declareProtected(ScoView, "formsemestre_validation_suppress_etud")
+
+    def formsemestre_validation_suppress_etud(
+        self, formsemestre_id, etudid, REQUEST=None, dialog_confirmed=False
+    ):
+        """Suppression des decisions de jury pour un etudiant.
+        """
+        if not self._can_validate_sem(REQUEST, formsemestre_id):
+            return self.confirmDialog(
+                message="<p>Opération non autorisée pour %s</h2>"
+                % REQUEST.AUTHENTICATED_USER,
+                dest_url=self.ScoURL(),
+                REQUEST=REQUEST,
+            )
+        if not dialog_confirmed:
+            sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+            etud = self.getEtudInfo(etudid=etudid, filled=1)[0]
+            nt = self._getNotesCache().get_NotesTable(
+                self, formsemestre_id
+            )  # > get_etud_decision_sem
+            decision_jury = nt.get_etud_decision_sem(etudid)
+            if decision_jury:
+                existing = (
+                    "<p>Décision existante: %(code)s du %(event_date)s</p>"
+                    % decision_jury
+                )
+            else:
+                existing = ""
+            return self.confirmDialog(
+                """<h2>Confirmer la suppression des décisions du semestre %s (%s - %s) pour %s ?</h2>%s
+                <p>Cette opération est irréversible.
+                </p>
+                """
+                % (
+                    sem["titre_num"],
+                    sem["date_debut"],
+                    sem["date_fin"],
+                    etud["nomprenom"],
+                    existing,
+                ),
+                OK="Supprimer",
+                dest_url="",
+                REQUEST=REQUEST,
+                cancel_url="formsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s"
+                % (formsemestre_id, etudid),
+                parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
+            )
+
+        sco_formsemestre_validation.formsemestre_validation_suppress_etud(
+            self, formsemestre_id, etudid
+        )
+        return REQUEST.RESPONSE.redirect(
+            self.ScoURL()
+            + "/Notes/formsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s&amp;head_message=Décision%%20supprimée"
+            % (formsemestre_id, etudid)
+        )
+
+    # ------------- PV de JURY et archives
+    security.declareProtected(ScoView, "formsemestre_pvjury")
+    formsemestre_pvjury = sco_pvjury.formsemestre_pvjury
+
+    security.declareProtected(ScoView, "formsemestre_lettres_individuelles")
+    formsemestre_lettres_individuelles = sco_pvjury.formsemestre_lettres_individuelles
+    security.declareProtected(ScoView, "formsemestre_pvjury_pdf")
+    formsemestre_pvjury_pdf = sco_pvjury.formsemestre_pvjury_pdf
+
+    security.declareProtected(ScoView, "feuille_preparation_jury")
+    feuille_preparation_jury = sco_prepajury.feuille_preparation_jury
+
+    security.declareProtected(ScoView, "formsemestre_archive")
+    formsemestre_archive = sco_archives.formsemestre_archive
+
+    security.declareProtected(ScoView, "formsemestre_delete_archive")
+    formsemestre_delete_archive = sco_archives.formsemestre_delete_archive
+
+    security.declareProtected(ScoView, "formsemestre_list_archives")
+    formsemestre_list_archives = sco_archives.formsemestre_list_archives
+
+    security.declareProtected(ScoView, "formsemestre_get_archived_file")
+    formsemestre_get_archived_file = sco_archives.formsemestre_get_archived_file
+
+    security.declareProtected(ScoEditApo, "view_apo_csv")
+    view_apo_csv = sco_etape_apogee_view.view_apo_csv
+
+    security.declareProtected(ScoEditApo, "view_apo_csv_store")
+    view_apo_csv_store = sco_etape_apogee_view.view_apo_csv_store
+
+    security.declareProtected(ScoEditApo, "view_apo_csv_download_and_store")
+    view_apo_csv_download_and_store = (
+        sco_etape_apogee_view.view_apo_csv_download_and_store
+    )
+
+    security.declareProtected(ScoEditApo, "view_apo_csv_delete")
+    view_apo_csv_delete = sco_etape_apogee_view.view_apo_csv_delete
+
+    security.declareProtected(ScoEditApo, "view_scodoc_etuds")
+    view_scodoc_etuds = sco_etape_apogee_view.view_scodoc_etuds
+
+    security.declareProtected(ScoEditApo, "view_apo_etuds")
+    view_apo_etuds = sco_etape_apogee_view.view_apo_etuds
+
+    security.declareProtected(ScoEditApo, "apo_semset_maq_status")
+    apo_semset_maq_status = sco_etape_apogee_view.apo_semset_maq_status
+
+    security.declareProtected(ScoEditApo, "apo_csv_export_results")
+    apo_csv_export_results = sco_etape_apogee_view.apo_csv_export_results
+
+    # sco_semset
+    security.declareProtected(ScoEditApo, "semset_page")
+    semset_page = sco_semset.semset_page
+
+    security.declareProtected(ScoEditApo, "do_semset_create")
+    do_semset_create = sco_semset.do_semset_create
+
+    security.declareProtected(ScoEditApo, "do_semset_delete")
+    do_semset_delete = sco_semset.do_semset_delete
+
+    security.declareProtected(ScoEditApo, "edit_semset_set_title")
+    edit_semset_set_title = sco_semset.edit_semset_set_title
+
+    security.declareProtected(ScoEditApo, "do_semset_add_sem")
+    do_semset_add_sem = sco_semset.do_semset_add_sem
+
+    security.declareProtected(ScoEditApo, "do_semset_remove_sem")
+    do_semset_remove_sem = sco_semset.do_semset_remove_sem
+
+    # sco_export_result
+    security.declareProtected(ScoEditApo, "scodoc_table_results")
+    scodoc_table_results = sco_export_results.scodoc_table_results
+
+    security.declareProtected(ScoView, "apo_compare_csv_form")
+    apo_compare_csv_form = sco_apogee_compare.apo_compare_csv_form
+
+    security.declareProtected(ScoView, "apo_compare_csv")
+    apo_compare_csv = sco_apogee_compare.apo_compare_csv
+
+    # ------------- INSCRIPTIONS: PASSAGE D'UN SEMESTRE A UN AUTRE
+    security.declareProtected(ScoEtudInscrit, "formsemestre_inscr_passage")
+    formsemestre_inscr_passage = sco_inscr_passage.formsemestre_inscr_passage
+
+    security.declareProtected(ScoView, "formsemestre_synchro_etuds")
+    formsemestre_synchro_etuds = sco_synchro_etuds.formsemestre_synchro_etuds
+
+    # ------------- RAPPORTS STATISTIQUES
+    security.declareProtected(ScoView, "formsemestre_report_counts")
+    formsemestre_report_counts = sco_report.formsemestre_report_counts
+
+    security.declareProtected(ScoView, "formsemestre_suivi_cohorte")
+    formsemestre_suivi_cohorte = sco_report.formsemestre_suivi_cohorte
+
+    security.declareProtected(ScoView, "formsemestre_suivi_parcours")
+    formsemestre_suivi_parcours = sco_report.formsemestre_suivi_parcours
+
+    security.declareProtected(ScoView, "formsemestre_etuds_lycees")
+    formsemestre_etuds_lycees = sco_lycee.formsemestre_etuds_lycees
+
+    security.declareProtected(ScoView, "scodoc_table_etuds_lycees")
+    scodoc_table_etuds_lycees = sco_lycee.scodoc_table_etuds_lycees
+
+    security.declareProtected(ScoView, "formsemestre_graph_parcours")
+    formsemestre_graph_parcours = sco_report.formsemestre_graph_parcours
+
+    security.declareProtected(ScoView, "formsemestre_poursuite_report")
+    formsemestre_poursuite_report = sco_poursuite_dut.formsemestre_poursuite_report
+
+    security.declareProtected(ScoView, "pe_view_sem_recap")
+    pe_view_sem_recap = pe_view.pe_view_sem_recap
+
+    security.declareProtected(ScoView, "report_debouche_date")
+    report_debouche_date = sco_debouche.report_debouche_date
+
+    security.declareProtected(ScoView, "formsemestre_estim_cost")
+    formsemestre_estim_cost = sco_cost_formation.formsemestre_estim_cost
+
+    # --------------------------------------------------------------------
+    # DEBUG
+    security.declareProtected(ScoView, "check_sem_integrity")
+
+    def check_sem_integrity(self, formsemestre_id, REQUEST):
+        """Debug.
+        Check that ue and module formations are consistents
+        """
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+
+        modimpls = self.do_moduleimpl_list(formsemestre_id=formsemestre_id)
+        bad_ue = []
+        bad_sem = []
+        for modimpl in modimpls:
+            mod = self.do_module_list({"module_id": modimpl["module_id"]})[0]
+            ue = self.do_ue_list({"ue_id": mod["ue_id"]})[0]
+            if ue["formation_id"] != mod["formation_id"]:
+                modimpl["mod"] = mod
+                modimpl["ue"] = ue
+                bad_ue.append(modimpl)
+            if sem["formation_id"] != mod["formation_id"]:
+                bad_sem.append(modimpl)
+                modimpl["mod"] = mod
+
+        return (
+            self.sco_header(REQUEST=REQUEST)
+            + "<p>formation_id=%s" % sem["formation_id"]
+            + "<h2>Inconsistent UE/MOD:</h2>"
+            + "<br/>".join([str(x) for x in bad_ue])
+            + "<h2>Inconsistent SEM/MOD:</h2>"
+            + "<br/>".join([str(x) for x in bad_sem])
+            + self.sco_footer(REQUEST)
+        )
+
+    security.declareProtected(ScoView, "check_form_integrity")
+
+    def check_form_integrity(self, formation_id, fix=False, REQUEST=None):
+        "debug"
+        log("check_form_integrity: formation_id=%s  fix=%s" % (formation_id, fix))
+        F = self.formation_list(args={"formation_id": formation_id})[0]
+        ues = self.do_ue_list(args={"formation_id": formation_id})
+        bad = []
+        for ue in ues:
+            mats = self.do_matiere_list(args={"ue_id": ue["ue_id"]})
+            for mat in mats:
+                mods = self.do_module_list({"matiere_id": mat["matiere_id"]})
+                for mod in mods:
+                    if mod["ue_id"] != ue["ue_id"]:
+                        if fix:
+                            # fix mod.ue_id
+                            log(
+                                "fix: mod.ue_id = %s (was %s)"
+                                % (ue["ue_id"], mod["ue_id"])
+                            )
+                            mod["ue_id"] = ue["ue_id"]
+                            self.do_module_edit(mod)
+                        bad.append(mod)
+                    if mod["formation_id"] != formation_id:
+                        bad.append(mod)
+        if bad:
+            txth = "<br/>".join([str(x) for x in bad])
+            txt = "\n".join([str(x) for x in bad])
+            log(
+                "check_form_integrity: formation_id=%s\ninconsistencies:" % formation_id
+            )
+            log(txt)
+            # Notify by e-mail
+            sendAlarm(self, "Notes: formation incoherente !", txt)
+        else:
+            txth = "OK"
+            log("ok")
+        return self.sco_header(REQUEST=REQUEST) + txth + self.sco_footer(REQUEST)
+
+    security.declareProtected(ScoView, "check_formsemestre_integrity")
+
+    def check_formsemestre_integrity(self, formsemestre_id, REQUEST=None):
+        "debug"
+        log("check_formsemestre_integrity: formsemestre_id=%s" % (formsemestre_id))
+        # verifie que tous les moduleimpl d'un formsemestre
+        # se réfèrent à un module dont l'UE appartient a la même formation
+        # Ancien bug: les ue_id étaient mal copiés lors des création de versions
+        # de formations
+        diag = []
+
+        Mlist = self.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+        for mod in Mlist:
+            if mod["module"]["ue_id"] != mod["matiere"]["ue_id"]:
+                diag.append(
+                    "moduleimpl %s: module.ue_id=%s != matiere.ue_id=%s"
+                    % (
+                        mod["moduleimpl_id"],
+                        mod["module"]["ue_id"],
+                        mod["matiere"]["ue_id"],
+                    )
+                )
+            if mod["ue"]["formation_id"] != mod["module"]["formation_id"]:
+                diag.append(
+                    "moduleimpl %s: ue.formation_id=%s != mod.formation_id=%s"
+                    % (
+                        mod["moduleimpl_id"],
+                        mod["ue"]["formation_id"],
+                        mod["module"]["formation_id"],
+                    )
+                )
+        if diag:
+            sendAlarm(
+                self,
+                "Notes: formation incoherente dans semestre %s !" % formsemestre_id,
+                "\n".join(diag),
+            )
+            log("check_formsemestre_integrity: formsemestre_id=%s" % formsemestre_id)
+            log("inconsistencies:\n" + "\n".join(diag))
+        else:
+            diag = ["OK"]
+            log("ok")
+        return (
+            self.sco_header(REQUEST=REQUEST)
+            + "<br/>".join(diag)
+            + self.sco_footer(REQUEST)
+        )
+
+    security.declareProtected(ScoView, "check_integrity_all")
+
+    def check_integrity_all(self, REQUEST=None):
+        "debug: verifie tous les semestres et tt les formations"
+        # formations
+        for F in self.formation_list():
+            self.check_form_integrity(F["formation_id"], REQUEST=REQUEST)
+        # semestres
+        for sem in sco_formsemestre.do_formsemestre_list(self):
+            self.check_formsemestre_integrity(sem["formsemestre_id"], REQUEST=REQUEST)
+        return (
+            self.sco_header(REQUEST=REQUEST)
+            + "<p>empty page: see logs and mails</p>"
+            + self.sco_footer(REQUEST)
+        )
+
+    # --------------------------------------------------------------------
+
+
+# --------------------------------------------------------------------
+#
+# Zope Product Administration
+#
+# --------------------------------------------------------------------
+def manage_addZNotes(
+    self, id="id_ZNotes", title="The Title for ZNotes Object", REQUEST=None
+):
+    "Add a ZNotes instance to a folder."
+    self._setObject(id, ZNotes(id, title))
+    if REQUEST is not None:
+        return self.manage_main(self, REQUEST)
+        # return self.manage_editForm(self, REQUEST)
+
+
+# The form used to get the instance id from the user.
+manage_addZNotesForm = DTMLFile("dtml/manage_addZNotesForm", globals())
diff --git a/ZScoDoc.py b/ZScoDoc.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3f1d8902c83fbddbb9adb1b4131dd33e5d1b904
--- /dev/null
+++ b/ZScoDoc.py
@@ -0,0 +1,954 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Site ScoDoc pour plusieurs departements: 
+      gestion de l'installation et des creation de départements.
+
+   Chaque departement est géré par un ZScolar sous ZScoDoc.
+"""
+
+import time, string, glob, re, inspect
+import urllib, urllib2, cgi, xml
+
+try:
+    from cStringIO import StringIO
+except:
+    from StringIO import StringIO
+from zipfile import ZipFile
+import os.path, glob
+import traceback
+
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email.MIMEBase import MIMEBase
+from email.Header import Header
+from email import Encoders
+
+from sco_zope import *
+
+#
+try:
+    import Products.ZPsycopgDA.DA as ZopeDA
+except:
+    import ZPsycopgDA.DA as ZopeDA  # interp.py
+
+from sco_utils import *
+from notes_log import log
+import sco_find_etud
+from ZScoUsers import pwdFascistCheck
+
+
+class ZScoDoc(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit):
+
+    "ZScoDoc object"
+
+    meta_type = "ZScoDoc"
+    security = ClassSecurityInfo()
+    file_path = Globals.package_home(globals())
+
+    # This is the list of the methods associated to 'tabs' in the ZMI
+    # Be aware that The first in the list is the one shown by default, so if
+    # the 'View' tab is the first, you will never see your tabs by cliquing
+    # on the object.
+    manage_options = (
+        ({"label": "Contents", "action": "manage_main"},)
+        + PropertyManager.manage_options  # add the 'Properties' tab
+        + (
+            # this line is kept as an example with the files :
+            #     dtml/manage_editZScolarForm.dtml
+            #     html/ZScolar-edit.stx
+            #     {'label': 'Properties', 'action': 'manage_editForm',},
+            {"label": "View", "action": "index_html"},
+        )
+        + Item.manage_options  # add the 'Undo' & 'Owner' tab
+        + RoleManager.manage_options  # add the 'Security' tab
+    )
+
+    def __init__(self, id, title):
+        "Initialise a new instance of ZScoDoc"
+        self.id = id
+        self.title = title
+        self.manage_addProperty("admin_password_initialized", "0", "string")
+
+    security.declareProtected(ScoView, "ScoDocURL")
+
+    def ScoDocURL(self):
+        "base URL for this instance (top level for ScoDoc site)"
+        return self.absolute_url()
+
+    def _check_admin_perm(self, REQUEST):
+        """Check if user has permission to add/delete departements
+        """
+        authuser = REQUEST.AUTHENTICATED_USER
+        if authuser.has_role("manager") or authuser.has_permission(ScoSuperAdmin, self):
+            return ""
+        else:
+            return """<h2>Vous n'avez pas le droit d'accéder à cette page</h2>"""
+
+    def _check_users_folder(self, REQUEST=None):
+        """Vérifie UserFolder et le crée s'il le faut
+        """
+        try:
+            udb = self.UsersDB
+            return "<!-- uf ok -->"
+        except:
+            e = self._check_admin_perm(REQUEST)
+            if not e:  # admin permissions:
+                self.create_users_cnx(REQUEST)
+                self.create_users_folder(REQUEST)
+                return '<div class="head_message">Création du connecteur utilisateurs réussie</div>'
+            else:
+                return """<div class="head_message">Installation non terminée: connectez vous avec les droits d'administrateur</div>"""
+
+    security.declareProtected("View", "create_users_folder")
+
+    def create_users_folder(self, REQUEST=None):
+        """Create Zope user folder
+        """
+        e = self._check_admin_perm(REQUEST)
+        if e:
+            return e
+
+        if REQUEST is None:
+            REQUEST = {}
+
+        REQUEST.form["pgauth_connection"] = "UsersDB"
+        REQUEST.form["pgauth_table"] = "sco_users"
+        REQUEST.form["pgauth_usernameColumn"] = "user_name"
+        REQUEST.form["pgauth_passwordColumn"] = "passwd"
+        REQUEST.form["pgauth_rolesColumn"] = "roles"
+
+        add_method = self.manage_addProduct["OFSP"].manage_addexUserFolder
+        log("create_users_folder: in %s" % self.id)
+        return add_method(
+            authId="pgAuthSource",
+            propId="nullPropSource",
+            memberId="nullMemberSource",
+            groupId="nullGroupSource",
+            cryptoId="MD51",
+            # doAuth='1', doProp='1', doMember='1', doGroup='1', allDone='1',
+            cookie_mode=2,
+            session_length=500,
+            not_session_length=0,
+            REQUEST=REQUEST,
+        )
+
+    def _fix_users_folder(self):
+        """removes docLogin and docLogout dtml methods from exUserFolder, so that we use ours.
+        (called each time be index_html, to fix old ScoDoc installations.)
+        """
+        try:
+            self.acl_users.manage_delObjects(ids=["docLogin", "docLogout"])
+        except:
+            pass
+        # add missing getAuthFailedMessage (bug in exUserFolder ?)
+        try:
+            x = self.getAuthFailedMessage
+        except:
+            log("adding getAuthFailedMessage to Zope install")
+            parent = self.aq_parent
+            from OFS.DTMLMethod import addDTMLMethod
+
+            addDTMLMethod(parent, "getAuthFailedMessage", file="Identification")
+
+    security.declareProtected("View", "create_users_cnx")
+
+    def create_users_cnx(self, REQUEST=None):
+        """Create Zope connector to UsersDB
+
+        Note: la connexion est fixée (SCOUSERS) (base crée par l'installeur) !
+        Les utilisateurs avancés pourront la changer ensuite.
+        """
+        # ce connecteur zope - db est encore pour l'instant utilisé par exUserFolder.pgAuthSource
+        # (en lecture seule en principe)
+        oid = "UsersDB"
+        log("create_users_cnx: in %s" % self.id)
+        da = ZopeDA.Connection(
+            oid,
+            "Cnx bd utilisateurs",
+            SCO_DEFAULT_SQL_USERS_CNX,
+            False,
+            check=1,
+            tilevel=2,
+            encoding="LATIN1",
+        )
+        self._setObject(oid, da)
+
+    security.declareProtected("View", "change_admin_user")
+
+    def change_admin_user(self, password, REQUEST=None):
+        """Change password of admin user"""
+        # note: controle sur le role et non pas sur une permission
+        # (non definies au top level)
+        if not REQUEST.AUTHENTICATED_USER.has_role("Manager"):
+            log("user %s is not Manager" % REQUEST.AUTHENTICATED_USER)
+            log("roles=%s" % REQUEST.AUTHENTICATED_USER.getRolesInContext(self))
+            raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
+        log("trying to change admin password")
+        # 1-- check strong password
+        if pwdFascistCheck(password) != None:
+            log("refusing weak password")
+            return REQUEST.RESPONSE.redirect(
+                "change_admin_user_form?message=Mot%20de%20passe%20trop%20simple,%20recommencez"
+            )
+        # 2-- change password for admin user
+        username = "admin"
+        acl_users = self.aq_parent.acl_users
+        user = acl_users.getUser(username)
+        r = acl_users._changeUser(
+            username, password, password, user.roles, user.domains
+        )
+        if not r:
+            # OK, set property to indicate we changed the password
+            log("admin password changed successfully")
+            self.manage_changeProperties(admin_password_initialized="1")
+        return r or REQUEST.RESPONSE.redirect("index_html")
+
+    security.declareProtected("View", "change_admin_user_form")
+
+    def change_admin_user_form(self, message="", REQUEST=None):
+        """Form allowing to change the ScoDoc admin password"""
+        # note: controle sur le role et non pas sur une permission
+        # (non definies au top level)
+        if not REQUEST.AUTHENTICATED_USER.has_role("Manager"):
+            raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
+        H = [
+            self.scodoc_top_html_header(
+                REQUEST, page_title="ScoDoc: changement mot de passe"
+            )
+        ]
+        if message:
+            H.append('<div id="message">%s</div>' % message)
+        H.append(
+            """<h2>Changement du mot de passe administrateur (utilisateur admin)</h2>
+        <p>
+        <form action="change_admin_user" method="post"><table>
+        <tr><td>Nouveau mot de passe:</td><td><input type="password" size="14" name="password"/></td></tr>
+        <tr><td>Confirmation: </td><td><input type="password" size="14" name="password2" /></td></tr>
+        </table>
+        <input type="submit" value="Changer">
+"""
+        )
+        H.append("""</body></html>""")
+        return "\n".join(H)
+
+    security.declareProtected("View", "list_depts")
+
+    def list_depts(self, REQUEST=None):
+        """List departments folders
+        (returns a list of Zope folders containing a ZScolar instance)
+        """
+        folders = self.objectValues("Folder")
+        # select folders with Scolarite object:
+        r = []
+        for folder in folders:
+            try:
+                s = folder.Scolarite
+                r.append(folder)
+            except:
+                pass
+        return r
+
+    security.declareProtected("View", "create_dept")
+
+    def create_dept(self, REQUEST=None, DeptId="", pass2=False):
+        """Creation (ajout) d'un site departement
+        (instance ZScolar + dossier la contenant)
+        """
+        e = self._check_admin_perm(REQUEST)
+        if e:
+            return e
+
+        if not DeptId:
+            raise ValueError("nom de departement invalide")
+        if not pass2:
+            # 1- Creation de repertoire Dept
+            add_method = self.manage_addProduct["OFSP"].manage_addFolder
+            add_method(DeptId, title="Site dept. " + DeptId)
+
+        DeptFolder = self[DeptId]
+
+        if not pass2:
+            # 2- Creation du repertoire Fotos
+            add_method = DeptFolder.manage_addProduct["OFSP"].manage_addFolder
+            add_method("Fotos", title="Photos identites " + DeptId)
+
+        # 3- Creation instance ScoDoc
+        add_method = DeptFolder.manage_addProduct["ScoDoc"].manage_addZScolarForm
+        return add_method(DeptId, REQUEST=REQUEST)
+
+    security.declareProtected("View", "delete_dept")
+
+    def delete_dept(self, REQUEST=None, DeptId="", force=False):
+        """Supprime un departement (de Zope seulement, ne touche pas la BD)
+        """
+        e = self._check_admin_perm(REQUEST)
+        if e:
+            return e
+
+        if not force and DeptId not in [x.id for x in self.list_depts()]:
+            raise ValueError("nom de departement invalide")
+
+        self.manage_delObjects(ids=[DeptId])
+
+        return (
+            "<p>Département "
+            + DeptId
+            + """ supprimé du serveur web (la base de données n'est pas affectée)!</p><p><a href="%s">Continuer</a></p>"""
+            % REQUEST.URL1
+        )
+
+    _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">
+<head>
+<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" />
+
+<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
+    
+<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
+<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
+<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">
+ 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 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 language="javascript" type="text/javascript" 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>
+"""
+
+    def scodoc_top_html_header(self, REQUEST, page_title="ScoDoc"):
+        H = [
+            self._html_begin
+            % {"page_title": "ScoDoc: bienvenue", "encoding": SCO_ENCODING},
+            self._top_level_css,
+            """</head><body class="gtrcontent" id="gtrcontent">""",
+            CUSTOM_HTML_HEADER_CNX,
+        ]
+        return "\n".join(H)
+
+    security.declareProtected("View", "index_html")
+
+    def index_html(self, REQUEST=None, message=None):
+        """Top level page for ScoDoc
+        """
+        authuser = REQUEST.AUTHENTICATED_USER
+        deptList = self.list_depts()
+        self._fix_users_folder()  # fix our exUserFolder
+        isAdmin = not self._check_admin_perm(REQUEST)
+        try:
+            admin_password_initialized = self.admin_password_initialized
+        except:
+            admin_password_initialized = "0"
+        if isAdmin and admin_password_initialized != "1":
+            REQUEST.RESPONSE.redirect(
+                "ScoDoc/change_admin_user_form?message=Le%20mot%20de%20passe%20administrateur%20doit%20etre%20change%20!"
+            )
+
+        # Si l'URL indique que l'on est dans un folder, affiche page login du departement
+        try:
+            deptfoldername = REQUEST.URL0.split("ScoDoc")[1].split("/")[1]
+            if deptfoldername in [x.id for x in self.list_depts()]:
+                return self.index_dept(deptfoldername=deptfoldername, REQUEST=REQUEST)
+        except:
+            pass
+
+        H = [
+            self.scodoc_top_html_header(REQUEST, page_title="ScoDoc: bienvenue"),
+            self._check_users_folder(REQUEST=REQUEST),  # ensure setup is done
+        ]
+        if message:
+            H.append('<div id="message">%s</div>' % message)
+
+        if isAdmin and not message:
+            H.append('<div id="message">Attention: connecté comme administrateur</div>')
+
+        H.append(
+            """
+        <div class="maindiv">
+        <h2>ScoDoc: gestion scolarité</h2>
+        """
+        )
+        if authuser.has_role("Authenticated"):
+            H.append(
+                """<p>Bonjour <font color="red"><b>%s</b></font>.</p>""" % str(authuser)
+            )
+            H.append(
+                """<p>N'oubliez pas de vous <a href="acl_users/logout">déconnecter</a> après usage.</p>"""
+            )
+        else:
+            H.append(
+                """<p>Ce site est <font color="red"><b>réservé au personnel autorisé</b></font></p>"""
+            )
+            H.append(self.authentication_form(destination="."))
+
+        if not deptList:
+            H.append("<em>aucun département existant !</em>")
+            # si pas de dept et pas admin, propose lien pour loger admin
+            if not isAdmin:
+                H.append(
+                    """<p><a href="/force_admin_authentication">Identifiez vous comme administrateur</a> (au début: nom 'admin', mot de passe 'scodoc')</p>"""
+                )
+        else:
+            H.append('<ul class="main">')
+            if isAdmin:
+                dest_folder = "/Scolarite"
+            else:
+                dest_folder = ""
+            for deptFolder in self.list_depts():
+                if authuser.has_permission(ScoView, deptFolder.Scolarite):
+                    link_cls = "link_accessible"
+                else:
+                    link_cls = "link_unauthorized"
+                # Essai de recuperer le nom du departement dans ses preferences
+                try:
+                    DeptName = (
+                        deptFolder.Scolarite.get_preference("DeptName") or deptFolder.id
+                    )
+                except:
+                    DeptName = deptFolder.id
+                H.append(
+                    '<li><a class="stdlink %s" href="%s%s">Département %s</a>'
+                    % (link_cls, deptFolder.absolute_url(), dest_folder, DeptName)
+                )
+                # check if roles are initialized in this depts, and do it if necessary
+                if deptFolder.Scolarite.roles_initialized == "0":
+                    if isAdmin:
+                        deptFolder.Scolarite._setup_initial_roles_and_permissions()
+                    else:
+                        H.append(" (non initialisé, connectez vous comme admin)")
+                H.append("</li>")
+            H.append("</ul>")
+            # Recherche etudiant
+            H.append(sco_find_etud.form_search_etud_in_accessible_depts(self, REQUEST))
+
+        if isAdmin:
+            H.append('<p><a href="scodoc_admin">Administration de ScoDoc</a></p>')
+        else:
+            H.append(
+                '<p><a href="%s/force_admin_authentication">Se connecter comme administrateur</a></p>'
+                % REQUEST.BASE0
+            )
+
+        H.append(
+            """
+<div id="scodoc_attribution">
+<p><a href="%s">ScoDoc</a> est un logiciel libre de suivi de la scolarité des étudiants conçu par 
+E. Viennet (Université Paris 13).</p>
+</div>
+</div>"""
+            % (SCO_WEBSITE,)
+        )
+
+        H.append("""</body></html>""")
+        return "\n".join(H)
+
+    def authentication_form(self, destination=""):
+        """html snippet for authentication"""
+        return (
+            """<!-- authentication_form -->
+<form action="doLogin" method="post">
+   <input type="hidden" name="destination" value="%s"/>
+<p>
+ <table border="0" cellpadding="3">
+    <tr>
+      <td><b>Nom:</b></td>
+      <td><input id="name" type="text" name="__ac_name" size="20"/></td>
+    </tr><tr>
+      <td><b>Mot de passe:</b></td>
+      <td><input id="password" type="password" name="__ac_password" size="20"/></td>
+      <td><input id="submit" name="submit" type="submit" value="OK"/></td>
+    </tr>
+ </table>
+</p>
+</form>"""
+            % destination
+        )
+
+    security.declareProtected("View", "index_dept")
+
+    def index_dept(self, deptfoldername="", REQUEST=None):
+        """Page d'accueil departement"""
+        authuser = REQUEST.AUTHENTICATED_USER
+        try:
+            dept = getattr(self, deptfoldername)
+            if authuser.has_permission(ScoView, dept):
+                return REQUEST.RESPONSE.redirect("ScoDoc/%s/Scolarite" % deptfoldername)
+        except:
+            log(
+                "*** problem in index_dept (%s) user=%s"
+                % (deptfoldername, str(authuser))
+            )
+
+        H = [
+            self.standard_html_header(REQUEST),
+            """<div style="margin: 1em;">
+<h2>Scolarité du département %s</h2>"""
+            % deptfoldername,
+            """<p>Ce site est 
+<font color="#FF0000"><b>réservé au personnel du département</b></font>.
+</p>""",
+            self.authentication_form(destination="Scolarite"),
+            """
+<p>Pour quitter, <a href="acl_users/logout">logout</a></p>
+<p><a href="%s">Retour à l'accueil</a></p>
+</div>
+"""
+            % self.ScoDocURL(),
+            self.standard_html_footer(REQUEST),
+        ]
+        return "\n".join(H)
+
+    security.declareProtected("View", "doLogin")
+
+    def doLogin(self, REQUEST=None, destination=None):
+        "redirect to destination after login"
+        if destination:
+            return REQUEST.RESPONSE.redirect(destination)
+
+    security.declareProtected("View", "docLogin")
+    docLogin = DTMLFile("dtml/docLogin", globals())
+    security.declareProtected("View", "docLogout")
+    docLogout = DTMLFile("dtml/docLogout", globals())
+
+    security.declareProtected("View", "query_string_to_form_inputs")
+
+    def query_string_to_form_inputs(self, query_string=""):
+        """Return html snippet representing the query string as POST form hidden inputs.
+        This is useful in conjonction with exUserfolder to correctly redirect the response
+        after authentication.
+        """
+        H = []
+        for a in query_string.split("&"):
+            if a:
+                nv = a.split("=")
+                if len(nv) == 2:
+                    name, value = nv
+                    H.append(
+                        '<input type="hidden" name="'
+                        + name
+                        + '" value="'
+                        + value
+                        + '"/>'
+                    )
+
+        return "<!-- query string -->\n" + "\n".join(H)
+
+    security.declareProtected("View", "standard_html_header")
+
+    def standard_html_header(self, REQUEST=None):
+        """Standard HTML header for pages outside depts"""
+        # not used in ZScolar, see sco_header
+        return """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html><head>
+<title>ScoDoc: accueil</title>
+<META http-equiv="Content-Type" content="text/html; charset=%s">
+<META http-equiv="Content-Style-Type" content="text/css">
+<META name="LANG" content="fr">
+<META name="DESCRIPTION" content="ScoDoc: gestion scolarite">
+
+<link HREF="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css"/>
+
+</head><body>%s""" % (
+            SCO_ENCODING,
+            CUSTOM_HTML_HEADER_CNX,
+        )
+
+    security.declareProtected("View", "standard_html_footer")
+
+    def standard_html_footer(self, REQUEST=None):
+        return """<p class="footer">
+Problème de connexion (identifiant, mot de passe): <em>contacter votre responsable ou chef de département</em>.</p>
+<p>Probl&egrave;mes et suggestions sur le logiciel: <a href="mailto:%s">%s</a></p>
+<p><em>ScoDoc est un logiciel libre développé par Emmanuel Viennet.</em></p>
+</body></html>""" % (
+            SCO_USERS_LIST,
+            SCO_USERS_LIST,
+        )
+
+    # sendEmail is not used through the web
+    def sendEmail(self, msg):
+        # sends an email to the address using the mailhost, if there is one
+        try:
+            mail_host = self.MailHost
+        except:
+            log("warning: sendEmail: no MailHost found !")
+            return
+        # a failed notification shouldn't cause a Zope error on a site.
+        try:
+            mail_host.send(msg.as_string())
+            log("sendEmail: ok")
+        except Exception as e:
+            log("sendEmail: exception while sending message")
+            log(e)
+            pass
+
+    def sendEmailFromException(self, msg):
+        # Send email by hand, as it seems to be not possible to use Zope Mail Host
+        # from an exception handler (see https://bugs.launchpad.net/zope2/+bug/246748)
+        log("sendEmailFromException")
+        try:
+            p = os.popen("sendmail -t", "w")  # old brute force method
+            p.write(msg.as_string())
+            exitcode = p.close()
+            if exitcode:
+                log("sendmail exit code: %s" % exitcode)
+        except:
+            log("an exception occurred sending mail")
+
+    security.declareProtected("View", "standard_error_message")
+
+    def standard_error_message(
+        self,
+        error_value=None,
+        error_message=None,
+        error_type=None,
+        error_traceback=None,
+        error_tb=None,
+        **kv
+    ):
+        "Recuperation des exceptions Zope"
+        sco_exc_mail = SCO_EXC_MAIL
+        sco_dev_mail = SCO_DEV_MAIL
+        # neat (or should I say dirty ?) hack to get REQUEST
+        # in fact, our caller (probably SimpleItem.py) has the REQUEST variable
+        # that we'd like to use for our logs, but does not pass it as an argument.
+        try:
+            frame = inspect.currentframe()
+            REQUEST = frame.f_back.f_locals["REQUEST"]
+        except:
+            REQUEST = {}
+
+        # Authentication uses exceptions, pass them up
+        HTTP_X_FORWARDED_FOR = REQUEST.get("HTTP_X_FORWARDED_FOR", "")
+        if error_type == "LoginRequired":
+            #    raise 'LoginRequired', ''  # copied from exuserFolder (beurk, old style exception...)
+            #            if REQUEST:
+            #    REQUEST.response.setStatus( 401, "Unauthorized") # ??????
+            log("LoginRequired from %s" % HTTP_X_FORWARDED_FOR)
+            self.login_page = error_value
+            return error_value
+        elif error_type == "Unauthorized":
+            log("Unauthorized from %s" % HTTP_X_FORWARDED_FOR)
+            return self.acl_users.docLogin(self, REQUEST=REQUEST)
+
+        log("exception caught: %s" % error_type)
+        log(traceback.format_exc())
+
+        if error_type == "ScoGenError":
+            return "<p>" + str(error_value) + "</p>"
+        elif error_type in ("ScoValueError", "FormatError"):
+            # Not a bug, presents a gentle message to the user:
+            H = [
+                self.standard_html_header(REQUEST),
+                """<h2>Erreur !</h2><p>%s</p>""" % error_value,
+            ]
+            if error_value.dest_url:
+                H.append('<p><a href="%s">Continuer</a></p>' % error_value.dest_url)
+            H.append(self.standard_html_footer(REQUEST))
+            return "\n".join(H)
+        else:  # Other exceptions, try carefully to build an error page...
+            # log('exc A')
+            H = []
+            try:
+                H.append(self.standard_html_header(REQUEST))
+            except:
+                pass
+            H.append(
+                """<table border="0" width="100%%"><tr valign="top">
+<td width="10%%" align="center"></td>
+<td width="90%%"><h2>Erreur !</h2>
+  <p>Une erreur est survenue</p>
+  <p>
+  <strong>Error Type: %(error_type)s</strong><br>
+  <strong>Error Value: %(error_value)s</strong><br> 
+  </p>
+  <hr noshade>
+  <p>L'URL est peut-etre incorrecte ?</p>
+
+  <p>Si l'erreur persiste, contactez Emmanuel Viennet:
+   <a href="mailto:%(sco_dev_mail)s">%(sco_dev_mail)s</a>
+    en copiant ce message d'erreur et le contenu du cadre bleu ci-dessous si possible.
+  </p>
+</td></tr>
+</table>        """
+                % vars()
+            )
+            # display error traceback (? may open a security risk via xss attack ?)
+            # log('exc B')
+            txt_html = self._report_request(REQUEST, fmt="html")
+            H.append(
+                """<h4 class="scodoc">Zope Traceback (à envoyer par mail à <a href="mailto:%(sco_dev_mail)s">%(sco_dev_mail)s</a>)</h4><div style="background-color: rgb(153,153,204); border: 1px;">
+%(error_tb)s
+<p><b>Informations:</b><br/>
+%(txt_html)s
+</p>
+</div>
+
+<p>Merci de votre patience !</p>
+"""
+                % vars()
+            )
+            try:
+                H.append(self.standard_html_footer(REQUEST))
+            except:
+                log("no footer found for error page")
+                pass
+
+        # --- Mail:
+        error_traceback_txt = scodoc_html2txt(error_tb)
+        txt = (
+            """
+ErrorType: %(error_type)s
+
+%(error_traceback_txt)s
+"""
+            % vars()
+        )
+
+        self.send_debug_alert(txt, REQUEST=REQUEST)
+        # ---
+        log("done processing exception")
+        # log( '\n page=\n' + '\n'.join(H) )
+        return "\n".join(H)
+
+    def _report_request(self, REQUEST, fmt="txt"):
+        """string describing current request for bug reports"""
+        AUTHENTICATED_USER = REQUEST.get("AUTHENTICATED_USER", "")
+        dt = time.asctime()
+        URL = REQUEST.get("URL", "")
+        QUERY_STRING = REQUEST.get("QUERY_STRING", "")
+        if QUERY_STRING:
+            QUERY_STRING = "?" + QUERY_STRING
+        METHOD = REQUEST.get("REQUEST_METHOD", "")
+
+        if fmt == "txt":
+            REFERER = REQUEST.get("HTTP_REFERER", "")
+            HTTP_USER_AGENT = REQUEST.get("HTTP_USER_AGENT", "")
+        else:
+            REFERER = "na"
+            HTTP_USER_AGENT = "na"
+        form = REQUEST.get("form", "")
+        HTTP_X_FORWARDED_FOR = REQUEST.get("HTTP_X_FORWARDED_FOR", "")
+        svn_version = get_svn_version(self.file_path)
+        SCOVERSION = VERSION.SCOVERSION
+
+        txt = (
+            """
+Version: %(SCOVERSION)s
+User:    %(AUTHENTICATED_USER)s
+Date:    %(dt)s
+URL:     %(URL)s%(QUERY_STRING)s
+Method:  %(METHOD)s
+
+REFERER: %(REFERER)s
+Form: %(form)s
+Origin: %(HTTP_X_FORWARDED_FOR)s
+Agent: %(HTTP_USER_AGENT)s
+
+subversion: %(svn_version)s
+"""
+            % vars()
+        )
+        if fmt == "html":
+            txt = txt.replace("\n", "<br/>")
+        return txt
+
+    security.declareProtected(
+        ScoSuperAdmin, "send_debug_alert"
+    )  # not called through the web
+
+    def send_debug_alert(self, txt, REQUEST=None):
+        """Send an alert email (bug report) to ScoDoc developpers"""
+        if not SCO_EXC_MAIL:
+            log("send_debug_alert: email disabled")
+            return
+        if REQUEST:
+            txt = self._report_request(REQUEST) + txt
+            URL = REQUEST.get("URL", "")
+        else:
+            URL = "send_debug_alert"
+        msg = MIMEMultipart()
+        subj = Header("[scodoc] exc %s" % URL, SCO_ENCODING)
+        msg["Subject"] = subj
+        recipients = [SCO_EXC_MAIL]
+        msg["To"] = " ,".join(recipients)
+        msg["From"] = "scodoc-alert"
+        msg.epilogue = ""
+        msg.attach(MIMEText(txt, "plain", SCO_ENCODING))
+        self.sendEmailFromException(msg)
+        log("Sent mail alert:\n" + txt)
+
+    security.declareProtected("View", "scodoc_admin")
+
+    def scodoc_admin(self, REQUEST=None):
+        """Page Operations d'administration
+        """
+        e = self._check_admin_perm(REQUEST)
+        if e:
+            return e
+
+        H = [
+            self.scodoc_top_html_header(REQUEST, page_title="ScoDoc: bienvenue"),
+            """              
+<h3>Administration ScoDoc</h3>
+
+<p><a href="change_admin_user_form">changer le mot de passe super-administrateur</a></p>
+<p><a href="%s">retour à la page d'accueil</a></p>
+
+<h4 class="scodoc">Création d'un département</h4>
+<p class="help_important">Le département doit avoir été créé au préalable sur le serveur en utilisant le script
+<tt>create_dept.sh</tt> (à lancer comme <tt>root</tt> dans le répertoire <tt>config</tt> de ScoDoc).
+</p>"""
+            % self.absolute_url(),
+        ]
+
+        deptList = [x.id for x in self.list_depts()]  # definis dans Zope
+        deptIds = Set(self._list_depts_ids())  # definis sur le filesystem
+        existingDepts = Set(deptList)
+        addableDepts = deptIds - existingDepts
+
+        if not addableDepts:
+            # aucun departement defini: aide utilisateur
+            H.append("<p>Aucun département à ajouter !</p>")
+        else:
+            H.append("""<form action="create_dept"><select name="DeptId"/>""")
+            for deptId in addableDepts:
+                H.append("""<option value="%s">%s</option>""" % (deptId, deptId))
+            H.append(
+                """</select>
+            <input type="submit" value="Créer département">
+            </form>"""
+            )
+
+        if deptList:
+            H.append(
+                """
+<h4 class="scodoc">Suppression d'un département</h4>
+<p>Ceci permet de supprimer le site web associé à un département, mais n'affecte pas la base de données 
+(le site peut donc être recréé sans perte de données).
+</p>
+<form action="delete_dept">
+<select name="DeptId">
+              """
+            )
+            for deptFolder in self.list_depts():
+                H.append(
+                    '<option value="%s">%s</option>' % (deptFolder.id, deptFolder.id)
+                )
+            H.append(
+                """</select>
+<input type="submit" value="Supprimer département">
+
+</form>"""
+            )
+
+        H.append("""</body></html>""")
+        return "\n".join(H)
+
+    def _list_depts_ids(self):
+        """Liste de id de departements definis par create_dept.sh
+        (fichiers depts/*.cfg)
+        """
+        filenames = glob.glob(SCODOC_VAR_DIR + "/config/depts/*.cfg")
+        ids = [os.path.split(os.path.splitext(f)[0])[1] for f in filenames]
+        return ids
+
+    security.declareProtected("View", "http_expiration_date")
+
+    def http_expiration_date(self):
+        "http expiration date for cachable elements (css, ...)"
+        d = datetime.timedelta(minutes=10)
+        return (datetime.datetime.utcnow() + d).strftime("%a, %d %b %Y %H:%M:%S GMT")
+
+    security.declareProtected("View", "get_etud_dept")
+
+    def get_etud_dept(self, REQUEST=None):
+        """Returns the dept id (eg "GEII") of an etud (identified by etudid, INE or NIP in REQUEST).
+        Warning: This function is inefficient and its result should be cached.
+        """
+        depts = self.list_depts()
+        depts_etud = []  # liste des depts où l'etud est defini
+        for dept in depts:
+            etuds = dept.Scolarite.getEtudInfo(REQUEST=REQUEST)
+            if etuds:
+                depts_etud.append((dept, etuds))
+        if not depts_etud:
+            return ""  # not found
+        elif len(depts_etud) == 1:
+            return depts_etud[0][0].id
+        # inscriptions dans plusieurs departements: cherche la plus recente
+        last_dept = None
+        last_date = None
+        for (dept, etuds) in depts_etud:
+            dept.Scolarite.fillEtudsInfo(etuds)
+            etud = etuds[0]
+            if etud["sems"]:
+                if (not last_date) or (etud["sems"][0]["date_fin_iso"] > last_date):
+                    last_date = etud["sems"][0]["date_fin_iso"]
+                    last_dept = dept
+        if not last_dept:
+            # est present dans plusieurs semestres mais inscrit dans aucun
+            return depts_etud[0][0]
+        return last_dept.id
+
+    security.declareProtected("View", "table_etud_in_accessible_depts")
+    table_etud_in_accessible_depts = sco_find_etud.table_etud_in_accessible_depts
+
+    security.declareProtected("View", "search_inscr_etud_by_nip")
+    search_inscr_etud_by_nip = sco_find_etud.search_inscr_etud_by_nip
+
+
+def manage_addZScoDoc(self, id="ScoDoc", title="Site ScoDoc", REQUEST=None):
+    "Add a ZScoDoc instance to a folder."
+    log("==============   creating a new ScoDoc instance =============")
+    zscodoc = ZScoDoc(
+        id, title
+    )  # ne cree (presque rien), tout se passe lors du 1er accès
+    self._setObject(id, zscodoc)
+    if REQUEST is not None:
+        REQUEST.RESPONSE.redirect("%s/manage_workspace" % REQUEST.URL1)
+    return id
diff --git a/ZScoUsers.py b/ZScoUsers.py
new file mode 100644
index 0000000000000000000000000000000000000000..f270d74238ccc2fcbade58b8b2c73fc50c36d12f
--- /dev/null
+++ b/ZScoUsers.py
@@ -0,0 +1,1334 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+""" Gestion des utilisateurs (table SQL pour Zope User Folder)
+"""
+import string, re
+import time
+import md5, base64
+
+
+from sco_zope import *
+
+# ---------------
+
+import notesdb
+from notesdb import *
+from notes_log import log
+from scolog import logdb
+from sco_utils import *
+from scolars import format_prenom, format_nom
+import sco_import_users, sco_excel
+from TrivialFormulator import TrivialFormulator, TF
+from gen_tables import GenTable
+import scolars
+import sco_cache
+
+# ----------------- password checking
+import cracklib
+
+
+def pwdFascistCheck(cleartxt):
+    "returns None if OK"
+    if (
+        hasattr(CONFIG, "MIN_PASSWORD_LENGTH")
+        and CONFIG.MIN_PASSWORD_LENGTH > 0
+        and len(cleartxt) < CONFIG.MIN_PASSWORD_LENGTH
+    ):
+        return True  # invalid
+    try:
+        x = cracklib.FascistCheck(cleartxt)
+        return None
+    except ValueError as e:
+        return str(e)
+
+
+# ---------------
+# cache global: chaque instance,  repérée par son URL, a un cache
+# qui est recréé à la demande
+# On cache ici la liste des utilisateurs, pour une duree limitee
+# (une minute).
+
+CACHE_userlist = {}
+
+# ---------------
+
+
+class ZScoUsers(
+    ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit
+):
+
+    "ZScousers object"
+
+    meta_type = "ZScoUsers"
+    security = ClassSecurityInfo()
+
+    # This is the list of the methods associated to 'tabs' in the ZMI
+    # Be aware that The first in the list is the one shown by default, so if
+    # the 'View' tab is the first, you will never see your tabs by cliquing
+    # on the object.
+    manage_options = (
+        ({"label": "Contents", "action": "manage_main"},)
+        + PropertyManager.manage_options  # add the 'Properties' tab
+        + ({"label": "View", "action": "index_html"},)
+        + Item.manage_options  # add the 'Undo' & 'Owner' tab
+        + RoleManager.manage_options  # add the 'Security' tab
+    )
+
+    # no permissions, only called from python
+    def __init__(self, id, title):
+        "initialise a new instance"
+        self.id = id
+        self.title = title
+
+    # The form used to edit this object
+    def manage_editZScousers(self, title, RESPONSE=None):
+        "Changes the instance values"
+        self.title = title
+        self._p_changed = 1
+        RESPONSE.redirect("manage_editForm")
+
+    # Ajout (dans l'instance) d'un dtml modifiable par Zope
+    def defaultDocFile(self, id, title, file):
+        f = open(file_path + "/dtml-editable/" + file + ".dtml")
+        file = f.read()
+        f.close()
+        self.manage_addDTMLMethod(id, title, file)
+
+    # Connexion to SQL database of users:
+
+    # Ugly but necessary during transition out of Zope:
+    _db_cnx_string = "dbname=SCOUSERS port=5432"
+    security.declareProtected("Change DTML Documents", "GetUsersDBConnexion")
+    GetUsersDBConnexion = notesdb.GetUsersDBConnexion
+
+    # --------------------------------------------------------------------
+    #
+    #   Users (top level)
+    #
+    # --------------------------------------------------------------------
+    # used to view content of the object
+    security.declareProtected(ScoUsersView, "index_html")
+
+    def index_html(self, REQUEST, all=0, with_olds=0, format="html"):
+        "gestion utilisateurs..."
+        all = int(all)
+        with_olds = int(with_olds)
+        # Controle d'acces
+        authuser = REQUEST.AUTHENTICATED_USER
+        user_name = str(authuser)
+        # log('user: %s roles: %s'%(user_name,authuser.getRolesInContext(self)))
+        user = self._user_list(args={"user_name": user_name})
+        if not user:
+            zope_roles = authuser.getRolesInContext(self)
+            if ("Manager" in zope_roles) or ("manage" in zope_roles):
+                dept = ""  # special case for zope admin
+            else:
+                raise AccessDenied("Vous n'avez pas la permission de voir cette page")
+        else:
+            dept = user[0]["dept"]
+
+        H = [self.sco_header(REQUEST, page_title="Gestion des utilisateurs")]
+        H.append("<h2>Gestion des utilisateurs</h2>")
+
+        if authuser.has_permission(ScoUsersAdmin, self):
+            H.append(
+                '<p><a href="create_user_form" class="stdlink">Ajouter un utilisateur</a>'
+            )
+            H.append(
+                '&nbsp;&nbsp; <a href="import_users_form" class="stdlink">Importer des utilisateurs</a></p>'
+            )
+        if all:
+            checked = "checked"
+        else:
+            checked = ""
+        if with_olds:
+            olds_checked = "checked"
+        else:
+            olds_checked = ""
+        H.append(
+            """<p><form name="f" action="%s">
+        <input type="checkbox" name="all" value="1" onchange="document.f.submit();" %s>Tous les départements</input>
+        <input type="checkbox" name="with_olds" value="1" onchange="document.f.submit();" %s>Avec anciens utilisateurs</input>
+        </form></p>"""
+            % (REQUEST.URL0, checked, olds_checked)
+        )
+
+        L = self.list_users(
+            dept,
+            all=all,
+            with_olds=with_olds,
+            format=format,
+            REQUEST=REQUEST,
+            with_links=authuser.has_permission(ScoUsersAdmin, self),
+        )
+        if format != "html":
+            return L
+        H.append(L)
+
+        F = self.sco_footer(REQUEST)
+        return "\n".join(H) + F
+
+    _userEditor = EditableTable(
+        "sco_users",
+        "user_id",
+        (
+            "user_id",
+            "user_name",
+            "passwd",
+            "roles",
+            "date_modif_passwd",
+            "nom",
+            "prenom",
+            "email",
+            "dept",
+            "passwd_temp",
+            "status",
+            "date_expiration",
+        ),
+        output_formators={
+            "date_modif_passwd": DateISOtoDMY,
+            "date_expiration": DateISOtoDMY,
+        },
+        input_formators={
+            "date_modif_passwd": DateDMYtoISO,
+            "date_expiration": DateDMYtoISO,
+        },
+        sortkey="nom",
+        filter_nulls=False,
+    )
+
+    def _user_list(self, **kw):
+        # list info sur utilisateur(s)
+        cnx = self.GetUsersDBConnexion()
+        users = self._userEditor.list(cnx, **kw)
+        for u in users:
+            if u["status"] == "old":
+                u["status_txt"] = "(ancien)"
+            else:
+                u["status_txt"] = ""
+
+        return users
+
+    def _user_edit(self, user_name, vals):
+        # edit user
+        cnx = self.GetUsersDBConnexion()
+        vals["user_name"] = user_name
+        self._userEditor.edit(cnx, vals)
+        self.get_userlist_cache().inval_cache()  # >
+        self.acl_users.cache_removeUser(user_name)  # exUserFolder's caches
+        self.acl_users.xcache_removeUser(user_name)
+        # Ensure that if status is "old", login is disabled
+        # note that operation is reversible without having to re-enter a password
+        # We change the roles (to avoid dealing with passwd hash, controled by exUserFolder)
+        u = self._user_list(args={"user_name": user_name})[0]
+        if u["status"] == "old" and u["roles"] and u["roles"][0] != "-":
+            roles = ["-" + r for r in u["roles"].split(",")]
+            cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+            self.acl_users.scodoc_editUser(cursor, user_name, roles=roles)
+            self.get_userlist_cache().inval_cache()
+        elif not u["status"] and u["roles"] and u["roles"][0] == "-":
+            roles = [r[1:] for r in u["roles"].split(",") if (r and r[0] == "-")]
+            cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+            self.acl_users.scodoc_editUser(cursor, user_name, roles=roles)
+            self.get_userlist_cache().inval_cache()
+
+    def _user_delete(self, user_name):
+        # delete user
+        cnx = self.GetUsersDBConnexion()
+        user_id = self._user_list(args={"user_name": user_name})[0]["user_id"]
+        self._userEditor.delete(cnx, user_id)
+        self.get_userlist_cache().inval_cache()  # >
+
+    def _all_roles(self):
+        "ensemble de tous les roles attribués ou attribuables"
+        roles = set(self.DeptUsersRoles())
+        cnx = self.GetUsersDBConnexion()
+        L = self._userEditor.list(cnx, {})
+        for l in L:
+            roles.update([x.strip() for x in l["roles"].split(",")])
+        return [r for r in roles if r and r[0] != "-"]
+
+    security.declareProtected(ScoUsersAdmin, "user_info")
+
+    def user_info(self, user_name=None, user=None):
+        """Donne infos sur l'utilisateur (qui peut ne pas etre dans notre base).
+        Si user_name est specifie, interroge la BD. Sinon, user doit etre un dict.        
+        """
+        if user_name:
+            infos = self._user_list(args={"user_name": user_name})
+        else:
+            infos = [user.copy()]
+            user_name = user["user_name"]
+
+        if not infos:
+            # special case: user is not in our database
+            return {
+                "user_name": user_name,
+                "nom": user_name,
+                "prenom": "",
+                "email": "",
+                "dept": "",
+                "nomprenom": user_name,
+                "prenomnom": user_name,
+                "prenom_fmt": "",
+                "nom_fmt": user_name,
+                "nomcomplet": user_name,
+                "nomplogin": user_name,
+                "nomnoacc": suppress_accents(user_name),
+                "passwd_temp": 0,
+                "status": "",
+                "date_expiration": None,
+            }
+        else:
+            info = infos[0]
+            # always conceal password !
+            del info["passwd"]  # always conceal password !
+            #
+            if info["prenom"]:
+                p = format_prenom(info["prenom"])
+            else:
+                p = ""
+            if info["nom"]:
+                n = format_nom(
+                    info["nom"], uppercase=False
+                )  # strcapitalize(strlower(info['nom']))
+            else:
+                n = user_name
+
+            prenom_abbrv = abbrev_prenom(p)
+            # nomprenom est le nom capitalisé suivi de l'initiale du prénom
+            info["nomprenom"] = (n + " " + prenom_abbrv).strip()
+            # prenomnom est l'initiale du prénom suivie du nom
+            info["prenomnom"] = (prenom_abbrv + " " + n).strip()
+            # nom_fmt et prenom_fmt: minuscule capitalisé
+            info["nom_fmt"] = n
+            info["prenom_fmt"] = scolars.format_prenom(p)
+            # nomcomplet est le prenom et le nom complets
+            info["nomcomplet"] = info["prenom_fmt"] + " " + info["nom_fmt"]
+            # nomplogin est le nom en majuscules suivi du prénom et du login
+            # e.g. Dupont Pierre (dupont)
+            info["nomplogin"] = "%s %s (%s)" % (strupper(n), p, info["user_name"])
+            # nomnoacc est le nom en minuscules sans accents
+            info["nomnoacc"] = suppress_accents(strlower(info["nom"]))
+
+            return info
+
+    def _can_handle_passwd(self, authuser, user_name, allow_admindepts=False):
+        """true if authuser can see or change passwd of user_name.
+        If allow_admindepts, allow Admin from all depts (so they can view users from other depts
+        and add roles to them).
+        authuser is a Zope user object. user_name is a string.
+        """
+        # Is authuser a zope admin ?
+        zope_roles = authuser.getRolesInContext(self)
+        if ("Manager" in zope_roles) or ("manage" in zope_roles):
+            return True
+        # Anyone can change its own passwd (or see its informations)
+        if str(authuser) == user_name:
+            return True
+        # has permission ?
+        if not authuser.has_permission(ScoUsersAdmin, self):
+            return False
+        # Ok, now check that authuser can manage users from this departement
+        # Get user info
+        user = self._user_list(args={"user_name": user_name})
+        if not user:
+            return False  # we don't have infos on this user !
+        # Get authuser info
+        auth_name = str(authuser)
+        authuser_info = self._user_list(args={"user_name": auth_name})
+        if not authuser_info:
+            return False  # not admin, and not in out database
+        auth_dept = authuser_info[0]["dept"]
+        if not auth_dept:
+            return True  # if no dept, can access users from all depts !
+        if auth_dept == user[0]["dept"] or allow_admindepts:
+            return True
+        else:
+            return False
+
+    def _is_valid_passwd(self, passwd):
+        "check if passwd is secure enough"
+        return not pwdFascistCheck(passwd)
+
+    def do_change_password(self, user_name, password):
+        user = self._user_list(args={"user_name": user_name})
+        assert len(user) == 1, "database inconsistency: len(r)=%d" % len(r)
+        # should not occur, already tested in _can_handle_passwd
+        cnx = self.GetUsersDBConnexion()  # en mode autocommit
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "update sco_users set date_modif_passwd=now(), passwd_temp=0 where user_name=%(user_name)s",
+            {"user_name": user_name},
+        )
+
+        # Laisse le exUserFolder modifier les donnees:
+        self.acl_users.scodoc_editUser(
+            cursor, user_name, password=password, roles=[user[0]["roles"]]
+        )
+
+        log("change_password: change ok for %s" % user_name)
+        self.get_userlist_cache().inval_cache()  # >
+
+    security.declareProtected(ScoView, "change_password")
+
+    def change_password(self, user_name, password, password2, REQUEST):
+        "change a password"
+        # ScoUsersAdmin: modif tous les passwd de SON DEPARTEMENT
+        # sauf si pas de dept (admin global)
+        H = []
+        F = self.sco_footer(REQUEST)
+        # Check access permission
+        if not self._can_handle_passwd(REQUEST.AUTHENTICATED_USER, user_name):
+            # access denied
+            log(
+                "change_password: access denied (authuser=%s, user_name=%s, ip=%s)"
+                % (authuser, user_name, REQUEST.REMOTE_ADDR)
+            )
+            raise AccessDenied(
+                "vous n'avez pas la permission de changer ce mot de passe"
+            )
+        # check password
+        if password != password2:
+            H.append(
+                """<p>Les deux mots de passes saisis sont différents !</p>
+            <p><a href="form_change_password?user_name=%s" class="stdlink">Recommencer</a></p>"""
+                % user_name
+            )
+        else:
+            if not self._is_valid_passwd(password):
+                H.append(
+                    """<p><b>ce mot de passe n\'est pas assez compliqué !</b><br/>(oui, il faut un mot de passe vraiment compliqué !)</p>
+                <p><a href="form_change_password?user_name=%s" class="stdlink">Recommencer</a></p>
+                """
+                    % user_name
+                )
+            else:
+                # ok, strong password
+                # MD5 hash (now computed by exUserFolder)
+                # digest = md5.new()
+                # digest.update(password)
+                # digest = digest.digest()
+                # md5pwd = string.strip(base64.encodestring(digest))
+                #
+                self.do_change_password(user_name, password)
+                #
+                # ici page simplifiee car on peut ne plus avoir
+                # le droit d'acceder aux feuilles de style
+                H.append(
+                    "<h2>Changement effectué !</h2><p>Ne notez pas ce mot de passe, mais mémorisez le !</p><p>Rappel: il est <b>interdit</b> de communiquer son mot de passe à un tiers, même si c'est un collègue de confiance !</p><p><b>Si vous n'êtes pas administrateur, le système va vous redemander votre login et nouveau mot de passe au prochain accès.</b></p>"
+                )
+                return (
+                    """<?xml version="1.0" encoding="%s"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+<title>Mot de passe changé</title>
+<meta http-equiv="Content-Type" content="text/html; charset=%s" />
+<body><h1>Mot de passe changé !</h1>
+"""
+                    % (SCO_ENCODING, SCO_ENCODING)
+                    + "\n".join(H)
+                    + '<a href="%s"  class="stdlink">Continuer</a></body></html>'
+                    % self.ScoURL()
+                )
+        return self.sco_header(REQUEST) + "\n".join(H) + F
+
+    security.declareProtected(ScoView, "form_change_password")
+
+    def form_change_password(self, REQUEST, user_name=None):
+        """Formulaire changement mot de passe
+        Un utilisateur peut toujours changer son mot de passe"""
+        authuser = REQUEST.AUTHENTICATED_USER
+        if not user_name:
+            user_name = str(authuser)
+        H = [self.sco_header(REQUEST, user_check=False)]
+        F = self.sco_footer(REQUEST)
+        # check access
+        if not self._can_handle_passwd(authuser, user_name):
+            return (
+                "\n".join(H)
+                + "<p>Vous n'avez pas la permission de changer ce mot de passe</p>"
+                + F
+            )
+        #
+        H.append(
+            """<h2>Changement du mot de passe de <font color="red">%(user_name)s</font></h2>
+        <p>
+        <form action="change_password" method="post" action="%(url)s"><table>
+        <tr><td>Nouveau mot de passe:</td><td><input type="password" size="14" name="password"/></td></tr>
+        <tr><td>Confirmation: </td><td><input type="password" size="14" name="password2" /></td></tr>
+        </table>
+        <input type="hidden" value="%(user_name)s" name="user_name">
+        <input type="submit" value="Changer">
+        </p>
+        <p>Vous pouvez aussi: <a class="stdlink" href="reset_password_form?user_name=%(user_name)s">renvoyer un mot de passe aléatoire temporaire par mail à l'utilisateur</a>
+"""
+            % {"user_name": user_name, "url": REQUEST.URL0}
+        )
+        return "\n".join(H) + F
+
+    security.declareProtected(ScoView, "userinfo")
+
+    def userinfo(self, user_name=None, REQUEST=None):
+        "display page of info about connected user"
+        authuser = REQUEST.AUTHENTICATED_USER
+        if not user_name:
+            user_name = str(authuser)
+        # peut on divulguer ces infos ?
+        if not self._can_handle_passwd(
+            REQUEST.AUTHENTICATED_USER, user_name, allow_admindepts=True
+        ):
+            raise AccessDenied("Vous n'avez pas la permission de voir cette page")
+        H = [self.sco_header(REQUEST, page_title="Utilisateur %s" % user_name)]
+        F = self.sco_footer(REQUEST)
+        H.append("<h2>Utilisateur: %s" % user_name)
+        info = self._user_list(args={"user_name": user_name})
+        if info:
+            H.append("%(status_txt)s" % info[0])
+        H.append("</h2>")
+        if not info:
+            H.append(
+                "<p>L' utilisateur '%s' n'est pas défini dans ce module.</p>"
+                % user_name
+            )
+            if authuser.has_permission(ScoEditAllNotes, self):
+                H.append("<p>(il peut modifier toutes les notes)</p>")
+            if authuser.has_permission(ScoEditAllEvals, self):
+                H.append("<p>(il peut modifier toutes les évaluations)</p>")
+            if authuser.has_permission(ScoImplement, self):
+                H.append("<p>(il peut creer des formations)</p>")
+        else:
+            H.append(
+                """<p>
+            <b>Login :</b> %(user_name)s<br/>
+            <b>Nom :</b> %(nom)s<br/>
+            <b>Prénom :</b> %(prenom)s<br/>
+            <b>Mail :</b> %(email)s<br/>
+            <b>Roles :</b> %(roles)s<br/>
+            <b>Dept :</b> %(dept)s<br/>
+            <b>Dernière modif mot de passe:</b> %(date_modif_passwd)s<br/>
+            <b>Date d'expiration:</b> %(date_expiration)s
+            <p><ul>
+             <li><a class="stdlink" href="form_change_password?user_name=%(user_name)s">changer le mot de passe</a></li>"""
+                % info[0]
+            )
+            if authuser.has_permission(ScoUsersAdmin, self):
+                H.append(
+                    """
+             <li><a  class="stdlink" href="create_user_form?user_name=%(user_name)s&amp;edit=1">modifier/déactiver ce compte</a></li>
+             <li><a  class="stdlink" href="delete_user_form?user_name=%(user_name)s">supprimer cet utilisateur</a> <em>(à n'utiliser qu'en cas d'erreur !)</em></li>
+             """
+                    % info[0]
+                )
+
+            H.append("</ul>")
+
+            if str(authuser) == user_name:
+                H.append(
+                    '<p><b>Se déconnecter: <a class="stdlink" href="acl_users/logout">logout</a></b></p>'
+                )
+            # Liste des permissions
+            H.append(
+                '<div class="permissions"><p>Permission de cet utilisateur:</p><ul>'
+            )
+            permissions = self.ac_inherited_permissions(1)
+            scoperms = [p for p in permissions if p[0][:3] == "Sco"]
+            try:
+                thisuser = self.acl_users.getUser(user_name)
+            except:
+                # expired from cache ? retry...
+                thisuser = self.acl_users.getUser(user_name)
+            if not thisuser:
+                # Cas de figure incompris ? (bug IUT Amiens janvier 2014: login avec accent ?)
+                H.append(
+                    "<li><em>impossible de retrouver les permissions de l'utilisateur (contacter l'administrateur)</em></li>"
+                )
+            else:
+                for p in scoperms:
+                    permname, value = p[:2]
+                    if thisuser.has_permission(permname, self):
+                        b = "oui"
+                    else:
+                        b = "non"
+                    H.append("<li>%s : %s</li>" % (permname, b))
+            H.append("</ul></div>")
+        if authuser.has_permission(ScoUsersAdmin, self):
+            H.append(
+                '<p><a class="stdlink" href="%s/Users">Liste de tous les utilisateurs</a></p>'
+                % self.ScoURL()
+            )
+        return "\n".join(H) + F
+
+    security.declareProtected(ScoUsersAdmin, "create_user_form")
+
+    def create_user_form(self, REQUEST, user_name=None, edit=0):
+        "form. creation ou edit utilisateur"
+        # Get authuser info
+        authuser = REQUEST.AUTHENTICATED_USER
+        auth_name = str(authuser)
+        authuser_info = self._user_list(args={"user_name": auth_name})
+        initvalues = {}
+
+        # Access control
+        zope_roles = authuser.getRolesInContext(self)
+        if (
+            not authuser_info
+            and not ("Manager" in zope_roles)
+            and not ("manage" in zope_roles)
+        ):
+            # not admin, and not in database
+            raise AccessDenied("invalid user (%s)" % auth_name)
+        if authuser_info:
+            auth_dept = authuser_info[0]["dept"]
+        else:
+            auth_dept = ""
+        #
+        edit = int(edit)
+        H = [self.sco_header(REQUEST, bodyOnLoad="init_tf_form('')")]
+        F = self.sco_footer(REQUEST)
+        if edit:
+            if not user_name:
+                raise ValueError("missing argument: user_name")
+            initvalues = self._user_list(args={"user_name": user_name})[0]
+            H.append("<h2>Modification de l'utilisateur %s</h2>" % user_name)
+        else:
+            H.append("<h2>Création d'un utilisateur</h2>")
+
+        if authuser.has_permission(ScoSuperAdmin, self):
+            H.append("""<p class="warning">Vous êtes super administrateur !</p>""")
+
+        # Noms de roles pouvant etre attribues aux utilisateurs via ce dialogue
+        # si pas SuperAdmin, restreint aux rôles EnsX, SecrX, DeptX
+        #
+        if authuser.has_permission(ScoSuperAdmin, self):
+            log("create_user_form called by %s (super admin)" % (auth_name,))
+            editable_roles = set(self._all_roles())
+        else:
+            editable_roles = set(self.DeptUsersRoles())
+        # log('create_user_form: editable_roles=%s' % editable_roles)
+        #
+        if not edit:
+            submitlabel = "Créer utilisateur"
+            orig_roles = set()
+        else:
+            submitlabel = "Modifier utilisateur"
+            initvalues["roles"] = initvalues["roles"].split(",") or []
+            orig_roles = set(initvalues["roles"])
+            if initvalues["status"] == "old":
+                editable_roles = set()  # can't change roles of a disabled user
+        # add existing user roles
+        displayed_roles = list(editable_roles.union(orig_roles))
+        displayed_roles.sort()
+        disabled_roles = {}  # pour desactiver les roles que l'on ne peut pas editer
+        for i in range(len(displayed_roles)):
+            if displayed_roles[i] not in editable_roles:
+                disabled_roles[i] = True
+
+        # log('create_user_form: displayed_roles=%s' % displayed_roles)
+
+        descr = [
+            ("edit", {"input_type": "hidden", "default": edit}),
+            ("nom", {"title": "Nom", "size": 20, "allow_null": False}),
+            ("prenom", {"title": "Prénom", "size": 20, "allow_null": False}),
+        ]
+        if auth_name != user_name:  # no one can't change its own status
+            descr.append(
+                (
+                    "status",
+                    {
+                        "title": "Statut",
+                        "input_type": "radio",
+                        "labels": ("actif", "ancien"),
+                        "allowed_values": ("", "old"),
+                    },
+                )
+            )
+        if not edit:
+            descr += [
+                (
+                    "user_name",
+                    {
+                        "title": "Pseudo (login)",
+                        "size": 20,
+                        "allow_null": False,
+                        "explanation": "nom utilisé pour la connexion. Doit être unique parmi tous les utilisateurs.",
+                    },
+                ),
+                (
+                    "passwd",
+                    {
+                        "title": "Mot de passe",
+                        "input_type": "password",
+                        "size": 14,
+                        "allow_null": False,
+                    },
+                ),
+                (
+                    "passwd2",
+                    {
+                        "title": "Confirmer mot de passe",
+                        "input_type": "password",
+                        "size": 14,
+                        "allow_null": False,
+                    },
+                ),
+            ]
+        else:
+            descr += [
+                (
+                    "user_name",
+                    {"input_type": "hidden", "default": initvalues["user_name"]},
+                ),
+                ("user_id", {"input_type": "hidden", "default": initvalues["user_id"]}),
+            ]
+        descr += [
+            (
+                "email",
+                {
+                    "title": "e-mail",
+                    "input_type": "text",
+                    "explanation": "vivement recommandé: utilisé pour contacter l'utilisateur",
+                    "size": 20,
+                    "allow_null": True,
+                },
+            )
+        ]
+
+        if not auth_dept:
+            # si auth n'a pas de departement (admin global)
+            # propose de choisir le dept du nouvel utilisateur
+            # sinon, il sera créé dans le même département que auth
+            descr.append(
+                (
+                    "dept",
+                    {
+                        "title": "Département",
+                        "input_type": "text",
+                        "size": 12,
+                        "allow_null": True,
+                        "explanation": """département d\'appartenance de l\'utilisateur (s'il s'agit d'un administrateur, laisser vide si vous voulez qu'il puisse créer des utilisateurs dans d'autres départements)""",
+                    },
+                )
+            )
+            can_choose_dept = True
+        else:
+            can_choose_dept = False
+            if edit:
+                descr.append(
+                    (
+                        "d",
+                        {
+                            "input_type": "separator",
+                            "title": "L'utilisateur appartient au département %s"
+                            % auth_dept,
+                        },
+                    )
+                )
+            else:
+                descr.append(
+                    (
+                        "d",
+                        {
+                            "input_type": "separator",
+                            "title": "L'utilisateur  sera crée dans le département %s"
+                            % auth_dept,
+                        },
+                    )
+                )
+
+        descr += [
+            (
+                "date_expiration",
+                {
+                    "title": "Date d'expiration",  # j/m/a
+                    "input_type": "date",
+                    "explanation": "j/m/a, laisser vide si pas de limite",
+                    "size": 9,
+                    "allow_null": True,
+                },
+            ),
+            (
+                "roles",
+                {
+                    "title": "Rôles",
+                    "input_type": "checkbox",
+                    "vertical": True,
+                    "allowed_values": displayed_roles,
+                    "disabled_items": disabled_roles,
+                },
+            ),
+            (
+                "force",
+                {
+                    "title": "Ignorer les avertissements",
+                    "input_type": "checkbox",
+                    "explanation": "passer outre les avertissements (homonymes, etc)",
+                    "labels": ("",),
+                    "allowed_values": ("1",),
+                },
+            ),
+        ]
+
+        if "tf-submitted" in REQUEST.form and not "roles" in REQUEST.form:
+            REQUEST.form["roles"] = []
+        if "tf-submitted" in REQUEST.form:
+            # Ajoute roles existants mais non modifiables (disabled dans le form)
+            # orig_roles - editable_roles
+            REQUEST.form["roles"] = list(
+                set(REQUEST.form["roles"]).union(orig_roles - editable_roles)
+            )
+
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            descr,
+            initvalues=initvalues,
+            submitlabel=submitlabel,
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + "\n" + tf[1] + F
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+        else:
+            vals = tf[2]
+            roles = set(vals["roles"]).intersection(editable_roles)
+            if REQUEST.form.has_key("edit"):
+                edit = int(REQUEST.form["edit"])
+            else:
+                edit = 0
+            try:
+                force = int(vals["force"][0])
+            except:
+                force = 0
+
+            if edit:
+                user_name = initvalues["user_name"]
+            else:
+                user_name = vals["user_name"]
+            # ce login existe ?
+            err = None
+            users = self._user_list(args={"user_name": user_name})
+            if edit and not users:  # safety net, le user_name ne devrait pas changer
+                err = "identifiant %s inexistant" % user_name
+            if not edit and users:
+                err = "identifiant %s déjà utilisé" % user_name
+            if err:
+                H.append(tf_error_message("""Erreur: %s""" % err))
+                return "\n".join(H) + "\n" + tf[1] + F
+
+            if not force:
+                ok, msg = self._check_modif_user(
+                    edit,
+                    user_name=user_name,
+                    nom=vals["nom"],
+                    prenom=vals["prenom"],
+                    email=vals["email"],
+                    roles=vals["roles"],
+                )
+                if not ok:
+                    H.append(
+                        tf_error_message(
+                            """Attention: %s (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
+                            % msg
+                        )
+                    )
+
+                    return "\n".join(H) + "\n" + tf[1] + F
+
+            if edit:  # modif utilisateur (mais pas passwd)
+                if (not can_choose_dept) and vals.has_key("dept"):
+                    del vals["dept"]
+                if vals.has_key("passwd"):
+                    del vals["passwd"]
+                if vals.has_key("date_modif_passwd"):
+                    del vals["date_modif_passwd"]
+                if vals.has_key("user_name"):
+                    del vals["user_name"]
+                if (auth_name == user_name) and vals.has_key("status"):
+                    del vals["status"]  # no one can't change its own status
+
+                # traitement des roles: ne doit pas affecter les roles
+                # que l'on en controle pas:
+                for role in orig_roles:
+                    if role and not role in editable_roles:
+                        roles.add(role)
+
+                vals["roles"] = ",".join(roles)
+
+                # ok, edit
+                log("sco_users: editing %s by %s" % (user_name, auth_name))
+                # log('sco_users: previous_values=%s' % initvalues)
+                # log('sco_users: new_values=%s' % vals)
+                self._user_edit(user_name, vals)
+                return REQUEST.RESPONSE.redirect(
+                    "userinfo?user_name=%s&head_message=Utilisateur %s modifié"
+                    % (user_name, user_name)
+                )
+            else:  # creation utilisateur
+                vals["roles"] = ",".join(vals["roles"])
+                # check identifiant
+                if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]+$", vals["user_name"]):
+                    msg = tf_error_message(
+                        "identifiant invalide (pas d'accents ni de caractères spéciaux)"
+                    )
+                    return "\n".join(H) + msg + "\n" + tf[1] + F
+                # check passwords
+                if vals["passwd"] != vals["passwd2"]:
+                    msg = tf_error_message(
+                        """Les deux mots de passes ne correspondent pas !"""
+                    )
+                    return "\n".join(H) + msg + "\n" + tf[1] + F
+                if not self._is_valid_passwd(vals["passwd"]):
+                    msg = tf_error_message(
+                        """Mot de passe trop simple, recommencez !"""
+                    )
+                    return "\n".join(H) + msg + "\n" + tf[1] + F
+                if not can_choose_dept:
+                    vals["dept"] = auth_dept
+                # ok, go
+                log("sco_users: new_user %s by %s" % (vals["user_name"], auth_name))
+                self.create_user(vals, REQUEST=REQUEST)
+
+    def _check_modif_user(
+        self, edit, user_name="", nom="", prenom="", email="", roles=[]
+    ):
+        """Vérifie que et utilisateur peut etre crée (edit=0) ou modifié (edit=1)
+        Cherche homonymes.        
+        returns (ok, msg)
+          - ok : si vrai, peut continuer avec ces parametres
+              (si ok est faux, l'utilisateur peut quand même forcer la creation)
+          - msg: message warning a presenter l'utilisateur
+        """
+        if not user_name or not nom or not prenom:
+            return False, "champ requis vide"
+        if not email:
+            return False, "vous devriez indiquer le mail de l'utilisateur créé !"
+        # ce login existe ?
+        users = self._user_list(args={"user_name": user_name})
+        if edit and not users:  # safety net, le user_name ne devrait pas changer
+            return False, "identifiant %s inexistant" % user_name
+        if not edit and users:
+            return False, "identifiant %s déjà utilisé" % user_name
+
+        # Des noms/prénoms semblables existent ?
+        cnx = self.GetUsersDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "select * from sco_users where lower(nom) ~ %(nom)s and lower(prenom) ~ %(prenom)s;",
+            {"nom": nom.lower().strip(), "prenom": prenom.lower().strip()},
+        )
+        res = cursor.dictfetchall()
+        if edit:
+            minmatch = 1
+        else:
+            minmatch = 0
+        if len(res) > minmatch:
+            return (
+                False,
+                "des utilisateurs proches existent: "
+                + ", ".join(
+                    [
+                        "%s %s (pseudo=%s)" % (x["prenom"], x["nom"], x["user_name"])
+                        for x in res
+                    ]
+                ),
+            )
+        # Roles ?
+        if not roles and not (edit and users[0]["status"] == "old"):
+            # nb: si utilisateur desactivé (old), pas de role attribué
+            return False, "aucun rôle sélectionné, êtes vous sûr ?"
+        # ok
+        return True, ""
+
+    security.declareProtected(ScoUsersAdmin, "import_users_form")
+
+    def import_users_form(self, REQUEST, user_name=None, edit=0):
+        """Import utilisateurs depuis feuille Excel"""
+        head = self.sco_header(REQUEST, page_title="Import utilisateurs")
+        H = [
+            head,
+            """<h2>Téléchargement d'une nouvelle liste d'utilisateurs</h2>
+             <p style="color: red">A utiliser pour importer de <b>nouveaux</b> utilisateurs (enseignants ou secrétaires)
+             </p>
+             <p>
+             L'opération se déroule en deux étapes. Dans un premier temps,
+             vous téléchargez une feuille Excel type. Vous devez remplir
+             cette feuille, une ligne décrivant chaque utilisateur. Ensuite,
+             vous indiquez le nom de votre fichier dans la case "Fichier Excel"
+             ci-dessous, et cliquez sur "Télécharger" pour envoyer au serveur
+             votre liste.
+             </p>
+             """,
+        ]
+        help = """<p class="help">
+        Lors de la creation des utilisateurs, les opérations suivantes sont effectuées:
+        </p>
+        <ol class="help">
+        <li>vérification des données;</li>
+        <li>génération d'un mot de passe alétoire pour chaque utilisateur;</li>
+        <li>création de chaque utilisateur;</li>
+        <li>envoi à chaque utilisateur de son <b>mot de passe initial par mail</b>.</li>
+        </ol>"""
+        H.append(
+            """<ol><li><a class="stdlink" href="import_users_generate_excel_sample">
+        Obtenir la feuille excel à remplir</a></li><li>"""
+        )
+        F = self.sco_footer(REQUEST)
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (
+                (
+                    "xlsfile",
+                    {"title": "Fichier Excel:", "input_type": "file", "size": 40},
+                ),
+                ("formsemestre_id", {"input_type": "hidden"}),
+            ),
+            submitlabel="Télécharger",
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + "</li></ol>" + help + F
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+        else:
+            # IMPORT
+            diag = sco_import_users.import_excel_file(
+                tf[2]["xlsfile"], REQUEST=REQUEST, context=self
+            )
+            H = [head]
+            H.append("<p>Import excel: %s</p>" % diag)
+            H.append("<p>OK, import terminé !</p>")
+            H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % REQUEST.URL1)
+            return "\n".join(H) + help + F
+
+    security.declareProtected(ScoUsersAdmin, "import_users_generate_excel_sample")
+
+    def import_users_generate_excel_sample(self, REQUEST):
+        "une feuille excel pour importation utilisateurs"
+        data = sco_import_users.generate_excel_sample()
+        return sco_excel.sendExcelFile(REQUEST, data, "ImportUtilisateurs.xls")
+
+    security.declareProtected(ScoUsersAdmin, "create_user")
+
+    def create_user(self, args, REQUEST=None):
+        "creation utilisateur zope"
+        cnx = self.GetUsersDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        passwd = args["passwd"]
+        args["passwd"] = "undefined"
+        if "passwd2" in args:
+            del args["passwd2"]
+        log("create_user: args=%s" % args)  # log apres supr. du mot de passe !
+        r = self._userEditor.create(cnx, args)
+        self.get_userlist_cache().inval_cache()  # >
+
+        # call exUserFolder to set passwd
+        roles = args["roles"].split(",")
+        self.acl_users.scodoc_editUser(
+            cursor, args["user_name"], password=passwd, roles=roles
+        )
+
+        if REQUEST:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+
+    security.declareProtected(ScoUsersAdmin, "delete_user_form")
+
+    def delete_user_form(self, REQUEST, user_name, dialog_confirmed=False):
+        "delete user"
+        authuser = REQUEST.AUTHENTICATED_USER
+        if not self._can_handle_passwd(authuser, user_name):
+            return (
+                self.sco_header(REQUEST, user_check=False)
+                + "<p>Vous n'avez pas la permission de supprimer cet utilisateur</p>"
+                + self.sco_footer(REQUEST)
+            )
+
+        r = self._user_list(args={"user_name": user_name})
+        if len(r) != 1:
+            return ScoValueError("utilisateur %s inexistant" % user_name)
+        if not dialog_confirmed:
+            return self.confirmDialog(
+                """<h2>Confirmer la suppression de l\'utilisateur %s ?</h2>
+                <p class="warning">En général, il est déconseillé de supprimer un utilisateur, son
+                identité étant référencé dans les modules de formation. N'utilisez
+                cette fonction qu'en cas d'erreur (création de doublons, etc).
+                </p>
+                """
+                % user_name,
+                dest_url="",
+                REQUEST=REQUEST,
+                cancel_url=REQUEST.URL1,
+                parameters={"user_name": user_name},
+            )
+        self._user_delete(user_name)
+        REQUEST.RESPONSE.redirect(REQUEST.URL1)
+
+    def list_users(
+        self,
+        dept,
+        all=False,  # tous les departements
+        with_olds=False,  # inclue les anciens utilisateurs (status "old")
+        format="html",
+        with_links=True,
+        REQUEST=None,
+    ):
+        "List users"
+        authuser = REQUEST.AUTHENTICATED_USER
+        if dept and not all:
+            r = self.get_userlist(dept=dept, with_olds=with_olds)
+            comm = "dept. %s" % dept
+        else:
+            r = self.get_userlist(with_olds=with_olds)
+            comm = "tous"
+        if with_olds:
+            comm += ", avec anciens"
+        comm = "(" + comm + ")"
+        # -- Add some information and links:
+        for u in r:
+            # Can current user modify this user ?
+            can_modify = self._can_handle_passwd(
+                authuser, u["user_name"], allow_admindepts=True
+            )
+
+            # Add links
+            if with_links and can_modify:
+                target = "userinfo?user_name=%(user_name)s" % u
+                u["_user_name_target"] = target
+                u["_nom_target"] = target
+                u["_prenom_target"] = target
+
+            # Hide passwd modification date (depending on rights wrt user)
+            if not can_modify:
+                u["date_modif_passwd"] = "(non visible)"
+
+            # Add spaces between roles to ease line wrap
+            if u["roles"]:
+                u["roles"] = ", ".join(u["roles"].split(","))
+
+            # Convert dates to ISO if XML output
+            if format == "xml" and u["date_modif_passwd"] != "NA":
+                u["date_modif_passwd"] = DateDMYtoISO(u["date_modif_passwd"]) or ""
+
+            # Convert date_expiration and date_modif_passwd to ISO to ease sorting
+            if u["date_expiration"]:
+                u["date_expiration_iso"] = DateDMYtoISO(u["date_expiration"])
+            else:
+                u["date_expiration_iso"] = ""
+            if u["date_modif_passwd"]:
+                u["date_modif_passwd_iso"] = DateDMYtoISO(u["date_expiration"])
+            else:
+                u["date_modif_passwd_iso"] = ""
+
+        title = "Utilisateurs définis dans ScoDoc"
+        tab = GenTable(
+            rows=r,
+            columns_ids=(
+                "user_name",
+                "nom_fmt",
+                "prenom_fmt",
+                "email",
+                "dept",
+                "roles",
+                "date_expiration_iso",
+                "date_modif_passwd_iso",
+                "passwd_temp",
+                "status_txt",
+            ),
+            titles={
+                "user_name": "Login",
+                "nom_fmt": "Nom",
+                "prenom_fmt": "Prénom",
+                "email": "Mail",
+                "dept": "Dept.",
+                "roles": "Rôles",
+                "date_expiration_iso": "Expiration",
+                "date_modif_passwd_iso": "Modif. mot de passe",
+                "passwd_temp": "Temp.",
+                "status_txt": "Etat",
+            },
+            caption=title,
+            page_title="title",
+            html_title="""<h2>%d utilisateurs %s</h2>
+            <p class="help">Cliquer sur un nom pour changer son mot de passe</p>"""
+            % (len(r), comm),
+            html_class="table_leftalign list_users",
+            html_with_td_classes=True,
+            html_sortable=True,
+            base_url="%s?all=%s" % (REQUEST.URL0, all),
+            pdf_link=False,  # table is too wide to fit in a paper page => disable pdf
+            preferences=self.get_preferences(),
+        )
+
+        return tab.make_page(
+            self, format=format, with_html_headers=False, REQUEST=REQUEST
+        )
+
+    def get_userlist_cache(self):
+        url = self.ScoURL()
+        if CACHE_userlist.has_key(url):
+            return CACHE_userlist[url]
+        else:
+            log("get_userlist_cache: new simpleCache")
+            CACHE_userlist[url] = sco_cache.expiringCache(max_validity=60)
+            return CACHE_userlist[url]
+
+    security.declareProtected(ScoView, "get_userlist")
+
+    def get_userlist(self, dept=None, with_olds=False):
+        """Returns list of users.
+        If dept, select users from this dept,
+        else return all users.
+        """
+        # on ne cache que la liste sans les "olds"
+        if with_olds:
+            r = None
+        else:
+            cache = self.get_userlist_cache()
+            r = cache.get(dept)
+
+        if r != None:
+            return r
+        else:
+            args = {}
+            if not with_olds:
+                args["status"] = None
+            if dept != None:
+                args["dept"] = dept
+
+            r = self._user_list(args=args)
+
+            l = [self.user_info(user=user) for user in r]
+
+            if not with_olds:
+                cache.set(dept, l)
+            return l
+
+    security.declareProtected(ScoView, "get_userlist_xml")
+
+    def get_userlist_xml(self, dept=None, start="", limit=25, REQUEST=None):
+        """Returns XML list of users with name (nomplogin) starting with start.
+        Used for forms auto-completion."""
+        userlist = self.get_userlist(dept=dept)
+        start = suppression_diacritics(unicode(start, "utf-8"))
+        start = strlower(str(start))
+
+        userlist = [user for user in userlist if user["nomnoacc"].startswith(start)]
+        if REQUEST:
+            REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+        doc = jaxml.XML_document(encoding=SCO_ENCODING)
+        doc.results()
+        for user in userlist[:limit]:
+            doc._push()
+            doc.rs(user["nomplogin"], id=user["user_id"], info="")
+            doc._pop()
+        return repr(doc)
+
+    security.declareProtected(ScoView, "get_user_name_from_nomplogin")
+
+    def get_user_name_from_nomplogin(self, nomplogin):
+        """Returns user_name (login) from nomplogin
+        """
+        m = re.match(r".*\((.*)\)", nomplogin.strip())
+        if m:
+            return m.group(1)
+        else:
+            return None
+
+    security.declareProtected(ScoView, "reset_password_form")
+
+    def reset_password_form(self, user_name=None, dialog_confirmed=False, REQUEST=None):
+        """Form to reset a password"""
+        if not dialog_confirmed:
+            return self.confirmDialog(
+                """<h2>Ré-initialiser le mot de passe de %s ?</h2>
+<p>Le mot de passe de %s va être choisi au hasard et lui être envoyé par mail.
+Il devra ensuite se connecter et le changer.
+</p>
+                """
+                % (user_name, user_name),
+                parameters={"user_name": user_name},
+                REQUEST=REQUEST,
+            )
+        self.reset_password(user_name=user_name, REQUEST=REQUEST)
+        return REQUEST.RESPONSE.redirect(
+            REQUEST.URL1
+            + "?head_message=mot%20de%20passe%20de%20"
+            + user_name
+            + "%20reinitialise"
+        )
+
+    security.declareProtected(ScoView, "reset_password")
+
+    def reset_password(self, user_name=None, REQUEST=None):
+        """Reset a password:
+        - set user's passwd_temp to 1
+        - set roles to 'ScoReset'
+        - generate a random password and mail it
+        """
+        authuser = REQUEST.AUTHENTICATED_USER
+        auth_name = str(authuser)
+        if not user_name:
+            user_name = auth_name
+        # Access control
+        if not self._can_handle_passwd(authuser, user_name):
+            raise AccessDenied(
+                "vous n'avez pas la permission de changer ce mot de passe"
+            )
+        log("reset_password: %s" % user_name)
+        # Check that user has valid mail
+        info = self.user_info(user_name=user_name)
+        if not is_valid_mail(info["email"]):
+            raise Exception("pas de mail valide associé à l'utilisateur")
+        # Generate random password
+        password = sco_import_users.generate_password()
+        self.do_change_password(user_name, password)
+        # Flag it as temporary:
+        cnx = self.GetUsersDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        ui = {"user_name": user_name}
+        cursor.execute(
+            "update sco_users set passwd_temp=1 where user_name='%(user_name)s'" % ui
+        )
+        # Send email
+        info["passwd"] = password
+        sco_import_users.mail_password(info, context=self, reset=True)
+
+
+# --------------------------------------------------------------------
+#
+# Zope Product Administration
+#
+# --------------------------------------------------------------------
+def manage_addZScoUsers(
+    self, id="id_ZScousers", title="The Title for ZScoUsers Object", REQUEST=None
+):
+    "Add a ZScoUsers instance to a folder."
+    self._setObject(id, ZScoUsers(id, title))
+    if REQUEST is not None:
+        return self.manage_main(self, REQUEST)
diff --git a/ZScolar.py b/ZScolar.py
new file mode 100644
index 0000000000000000000000000000000000000000..209a60940b7a0daaefc9c728f2fa788119bd191f
--- /dev/null
+++ b/ZScolar.py
@@ -0,0 +1,2830 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Site Scolarite pour département IUT
+"""
+
+import sys
+import traceback
+import time, string, glob, re
+import urllib, urllib2, cgi, xml
+
+try:
+    from cStringIO import StringIO
+except:
+    from StringIO import StringIO
+from zipfile import ZipFile
+import thread
+import psycopg2
+
+from sco_zope import *
+
+
+# ---------------
+from notes_log import log
+
+log.set_log_directory(INSTANCE_HOME + "/log")
+log("restarting...")
+
+log("ZScolar home=%s" % file_path)
+
+
+from sco_utils import *
+import notesdb
+from notesdb import *
+from scolog import logdb
+
+import scolars
+import sco_preferences
+import sco_formations
+from scolars import (
+    format_nom,
+    format_prenom,
+    format_sexe,
+    format_lycee,
+    format_lycee_from_code,
+)
+from scolars import format_telephone, format_pays, make_etud_args
+import sco_find_etud
+import sco_photos
+import sco_formsemestre
+import sco_formsemestre_edit
+
+import sco_news
+from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC
+
+import html_sco_header, html_sidebar
+
+from gen_tables import GenTable
+import sco_excel
+import imageresize
+
+import ZNotes
+import ZAbsences
+import ZEntreprises
+import ZScoUsers
+import sco_modalites
+import ImportScolars
+import sco_portal_apogee
+import sco_synchro_etuds
+import sco_page_etud
+import sco_groups
+import sco_trombino
+import sco_groups_view
+import sco_trombino_tours
+import sco_parcours_dut
+import sco_report
+import sco_archives_etud
+import sco_debouche
+import sco_groups_edit
+import sco_up_to_date
+import sco_edt_cal
+import sco_dept
+import sco_dump_db
+
+from VERSION import SCOVERSION, SCONEWS
+
+if CONFIG.ABSOLUTE_URL:
+    log("ScoDoc: ABSOLUTE_URL='%s'" % CONFIG.ABSOLUTE_URL)
+log("ScoDoc: using encoding %s" % SCO_ENCODING)
+
+# import essai_cas
+
+# ---------------
+
+
+class ZScolar(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit):
+
+    "ZScolar object"
+
+    meta_type = "ZScolar"
+    security = ClassSecurityInfo()
+    file_path = Globals.package_home(globals())
+
+    # This is the list of the methods associated to 'tabs' in the ZMI
+    # Be aware that The first in the list is the one shown by default, so if
+    # the 'View' tab is the first, you will never see your tabs by cliquing
+    # on the object.
+    manage_options = (
+        ({"label": "Contents", "action": "manage_main"},)
+        + PropertyManager.manage_options  # add the 'Properties' tab
+        + (
+            # this line is kept as an example with the files :
+            #     dtml/manage_editZScolarForm.dtml
+            #     html/ZScolar-edit.stx
+            # {'label': 'Properties', 'action': 'manage_editForm',},
+            {"label": "View", "action": "index_html"},
+        )
+        + Item.manage_options  # add the 'Undo' & 'Owner' tab
+        + RoleManager.manage_options  # add the 'Security' tab
+    )
+
+    # no permissions, only called from python
+    def __init__(self, id, title, db_cnx_string=None):
+        "initialise a new instance of ZScolar"
+        log("*** creating ZScolar instance")
+        self.id = id
+        self.title = title
+        self._db_cnx_string = db_cnx_string
+        self._cnx = None
+        # --- add editable DTML documents:
+        # self.defaultDocFile('sidebar_dept',
+        #                    'barre gauche (partie haute)',
+        #                    'sidebar_dept')
+
+        # --- add DB connector
+        # id = 'DB'
+        # da = ZopeDA.Connection(
+        #    id, 'DB connector', db_cnx_string, False,
+        #    check=1, tilevel=2, encoding='utf-8')
+        # self._setObject(id, da)
+        # --- add Scousers instance
+        id = "Users"
+        obj = ZScoUsers.ZScoUsers(id, "Gestion utilisateurs zope")
+        self._setObject(id, obj)
+        # --- add Notes instance
+        id = "Notes"
+        obj = ZNotes.ZNotes(id, "Gestion Notes")
+        self._setObject(id, obj)
+        # --- add Absences instance
+        id = "Absences"
+        obj = ZAbsences.ZAbsences(id, "Gestion absences")
+        self._setObject(id, obj)
+        # --- add Entreprises instance
+        id = "Entreprises"
+        obj = ZEntreprises.ZEntreprises(id, "Suivi entreprises")
+        self._setObject(id, obj)
+
+        #
+        self.manage_addProperty("roles_initialized", "0", "string")
+
+    # The for used to edit this object
+    def manage_editZScolar(self, title, RESPONSE=None):
+        "Changes the instance values"
+        self.title = title
+        self._p_changed = 1
+        RESPONSE.redirect("manage_editForm")
+
+    def _setup_initial_roles_and_permissions(self):
+        """Initialize roles and permissions
+        create 3 roles: EnsXXX, SecrXXX, AdminXXX
+        and set default permissions for each one.
+        """
+        DeptId = self.DeptId()
+        log("initializing roles and permissions for %s" % DeptId)
+        H = []
+        ok = True
+        DeptRoles = self.DeptUsersRoles()
+
+        container = self.aq_parent  # creates roles and permissions in parent folder
+        valid_roles = set(container.valid_roles())
+        for role_name in DeptRoles:
+            if role_name not in valid_roles:
+                r = container._addRole(role_name)
+                if r:  # error
+                    H.append(r)
+                    ok = False
+
+        for permission in Sco_Default_Permissions.keys():
+            roles = [r + DeptId for r in Sco_Default_Permissions[permission]]
+            roles.append("Manager")
+            log("granting '%s' to %s" % (permission, roles))
+            try:
+                r = container.manage_permission(permission, roles=roles, acquire=0)
+                if r:
+                    H.append(r)
+                    ok = False
+            except ValueError:
+                log(traceback.format_exc())
+                log("failed, ignoring.")
+        # set property indicating that we did the job:
+        self.manage_changeProperties(roles_initialized="1")
+
+        return ok, "\n".join(H)
+
+    security.declareProtected(ScoView, "DeptId")
+
+    def DeptId(self):
+        """Returns Id for this department
+        (retreived as the name of the parent folder)
+        (c'est normalement l'id donne à create_dept.sh)
+        NB: la preference DeptName est au depart la même chose de cet id
+        mais elle peut être modifiée (préférences).
+        """
+        return self.aq_parent.id
+
+    def DeptUsersRoles(self):  # not published
+        # Donne les rôles utilisés dans ce departement.
+        DeptId = self.DeptId()
+        DeptRoles = []
+        for role_type in ("Ens", "Secr", "Admin", "RespPe"):
+            role_name = role_type + DeptId
+            DeptRoles.append(role_name)
+        return DeptRoles
+
+    security.declareProtected(ScoView, "essai")
+
+    def essai(self, x="", REQUEST=None):
+        """essai: header / body / footer"""
+        return """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Programme DUT R&amp;T</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<meta http-equiv="Content-Style-Type" content="text/css" />
+<meta name="LANG" content="fr" />
+<meta name="DESCRIPTION" content="ScoDoc" />
+
+<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script>
+
+
+<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.jeditable.mini.js"></script>
+<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/ue_list.js"></script>
+
+</head>
+<body>
+<p>
+UE11 Découverte métiers <span class="ue_code">(code UCOD46, 16 ECTS, Apo <span class="span_ue_apo" style="display: inline" id="toto">VRTU11</span>)</span>
+<span class="locked">[verrouillé]</span>
+</p>
+</body>
+        """
+        return (
+            self.sco_header(REQUEST)
+            + """<div class="xp">%s</div>""" % x
+            + self.sco_footer(REQUEST)
+        )
+        b = "<p>Hello, World !</p><br/>"
+        raise ValueError("essai exception")
+        # raise ScoValueError('essai exception !', dest_url='totoro', REQUEST=REQUEST)
+
+        # cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        # cursor.execute("select * from notes_formations")
+        # b += str(cursor.fetchall())
+        # b = self.Notes.gloups()
+        # raise NoteProcessError('test exception !')
+
+        # essai: liste des permissions
+        from AccessControl import getSecurityManager
+        from AccessControl.Permission import Permission
+
+        permissions = self.ac_inherited_permissions(1)
+        scoperms = [p for p in permissions if p[0][:3] == "Sco"]
+        # H.append( str(self.aq_parent.aq_parent.permission_settings()) )
+        # H.append('<p>perms: %s</p>'%str(scoperms))
+        # H.append('<p>valid_roles: %s</p>'%str(self.valid_roles()))
+        # H.append('<p>ac_inherited_permissions=%s</p>'%str(self.ac_inherited_permissions(1)))
+        def collect_roles(context, rd):
+            for p in scoperms:
+                name, value = p[:2]
+                P = Permission(name, value, context)
+                roles = list(P.getRoles())
+                if rd.has_key(name):
+                    rd[name] += roles
+                else:
+                    rd[name] = roles
+            if hasattr(context, "aq_parent"):
+                collect_roles(context.aq_parent, rd)
+
+        b = ""
+        rd = {}
+        collect_roles(self, rd)
+        b = "<p>" + str(rd) + "</p>"
+
+        authuser = REQUEST.AUTHENTICATED_USER
+        for p in scoperms:
+            permname, value = p[:2]
+            b += "<p>" + permname + " : "
+            if authuser.has_permission(permname, self):
+                b += "yes"
+            else:
+                b += "no"
+            b += "</p>"
+        b += "<p>xxx</p><hr><p>" + str(self.aq_parent.aq_parent)
+
+        return self.sco_header(REQUEST) + str(b) + self.sco_footer(REQUEST)
+
+    # essais calendriers:
+    security.declareProtected(ScoView, "experimental_calendar")
+    experimental_calendar = sco_edt_cal.experimental_calendar
+    security.declareProtected(ScoView, "group_edt_json")
+    group_edt_json = sco_edt_cal.group_edt_json
+
+    security.declareProtected(ScoView, "ScoURL")
+
+    def ScoURL(self):
+        "base URL for this sco instance"
+        # absolute_url is the classic Zope method
+        # The avoid the burden of configuring a proxy zope object, we offer
+        # a custom configuration via scodoc_config
+        return CONFIG.ABSOLUTE_URL or self.absolute_url()
+
+    security.declareProtected(ScoView, "sco_header")
+    sco_header = html_sco_header.sco_header
+    security.declareProtected(ScoView, "sco_footer")
+    sco_footer = html_sco_header.sco_footer
+
+    # --------------------------------------------------------------------
+    #
+    #    GESTION DE LA BD
+    #
+    # --------------------------------------------------------------------
+    security.declareProtected(ScoSuperAdmin, "GetDBConnexionString")
+
+    def GetDBConnexionString(self):
+        # should not be published (but used from contained classes via acquisition)
+        return self._db_cnx_string
+
+    security.declareProtected(ScoSuperAdmin, "GetDBConnexion")
+    GetDBConnexion = notesdb.GetDBConnexion
+
+    # A enlever après re-ecriture de ZEntreprises.py
+    security.declareProtected(ScoView, "TrivialFormulator")
+
+    def TrivialFormulator(
+        self,
+        form_url,
+        values,
+        formdescription=(),
+        initvalues={},
+        method="POST",
+        submitlabel="OK",
+        formid="tf",
+        cancelbutton=None,
+        readonly=False,
+    ):
+        "generator/validator of simple forms"
+        # obsolete, still used by dtml/entreprises old code...
+        return TrivialFormulator(
+            form_url,
+            values,
+            formdescription=formdescription,
+            initvalues=initvalues,
+            method=method,
+            submitlabel=submitlabel,
+            formid=formid,
+            cancelbutton=cancelbutton,
+            readonly=readonly,
+        )
+
+    # --------------------------------------------------------------------
+    #
+    #    SCOLARITE (top level)
+    #
+    # --------------------------------------------------------------------
+
+    security.declareProtected(ScoView, "about")
+
+    def about(self, REQUEST):
+        "version info"
+        H = [
+            """<h2>Système de gestion scolarité</h2>
+        <p>&copy; Emmanuel Viennet 1997-2020</p>
+        <p>Version %s (subversion %s)</p>
+        """
+            % (SCOVERSION, get_svn_version(file_path))
+        ]
+        H.append(
+            '<p>Logiciel libre écrit en <a href="http://www.python.org">Python</a>.</p><p>Utilise <a href="http://www.reportlab.org/">ReportLab</a> pour générer les documents PDF, et <a href="http://sourceforge.net/projects/pyexcelerator">pyExcelerator</a> pour le traitement des documents Excel.</p>'
+        )
+        H.append("<h2>Dernières évolutions</h2>" + SCONEWS)
+        H.append(
+            '<div class="about-logo">'
+            + icontag("borgne_img")
+            + " <em>Au pays des aveugles...</em></div>"
+        )
+        d = ""
+        return self.sco_header(REQUEST) + "\n".join(H) + d + self.sco_footer(REQUEST)
+
+    security.declareProtected(ScoView, "ScoErrorResponse")
+
+    def ScoErrorResponse(self, msg, format="html", REQUEST=None):
+        """Send an error message to the client, in html or xml format.
+        """
+        REQUEST.RESPONSE.setStatus(404, reason=msg)
+        if format == "html" or format == "pdf":
+            raise ScoValueError(msg)
+        elif format == "xml":
+            REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+            doc = jaxml.XML_document(encoding=SCO_ENCODING)
+            doc.error(msg=msg)
+            return repr(doc)
+        elif format == "json":
+            REQUEST.RESPONSE.setHeader("content-type", JSON_MIMETYPE)
+            return "undefined"  # XXX voir quoi faire en cas d'erreur json
+        else:
+            raise ValueError("ScoErrorResponse: invalid format")
+
+    # XXX essai XXX
+    # security.declareProtected(ScoView, 'essai_cas')
+    # essai_cas = essai_cas.essai_cas
+
+    # --------------------------------------------------------------------
+    #
+    #    PREFERENCES
+    #
+    # --------------------------------------------------------------------
+    security.declareProtected(ScoView, "get_preferences")
+
+    def get_preferences(self, formsemestre_id=None):
+        "Get preferences for this instance (a dict-like instance)"
+        return sco_preferences.sem_preferences(self, formsemestre_id)
+
+    security.declareProtected(ScoView, "get_preference")
+
+    def get_preference(self, name, formsemestre_id=None):
+        """Returns value of named preference.
+        All preferences have a sensible default value (see sco_preferences.py), 
+        this function always returns a usable value for all defined preferences names.
+        """
+        return sco_preferences.get_base_preferences(self).get(formsemestre_id, name)
+
+    security.declareProtected(ScoChangePreferences, "edit_preferences")
+
+    def edit_preferences(self, REQUEST):
+        """Edit global preferences"""
+        return sco_preferences.get_base_preferences(self).edit(REQUEST=REQUEST)
+
+    security.declareProtected(ScoView, "formsemestre_edit_preferences")
+
+    def formsemestre_edit_preferences(self, formsemestre_id, REQUEST):
+        """Edit preferences for a semestre"""
+        authuser = REQUEST.AUTHENTICATED_USER
+        sem = sco_formsemestre.get_formsemestre(self.Notes, formsemestre_id)
+        ok = (
+            authuser.has_permission(ScoImplement, self)
+            or ((str(authuser) in sem["responsables"]) and sem["resp_can_edit"])
+        ) and (sem["etat"] == "1")
+        if ok:
+            return self.get_preferences(formsemestre_id=formsemestre_id).edit(
+                REQUEST=REQUEST
+            )
+        else:
+            raise AccessDenied("Modification impossible pour %s" % authuser)
+
+    security.declareProtected(ScoView, "doc_preferences")
+
+    def doc_preferences(self, REQUEST):
+        """List preferences for wiki documentation"""
+        REQUEST.RESPONSE.setHeader("content-type", "text/plain" )
+        return sco_preferences.doc_preferences(self)
+    
+    # --------------------------------------------------------------------
+    #
+    #    ETUDIANTS
+    #
+    # --------------------------------------------------------------------
+
+    # -----------------  BANDEAUX -------------------
+    security.declareProtected(ScoView, "sidebar")
+    sidebar = html_sidebar.sidebar
+    security.declareProtected(ScoView, "sidebar_dept")
+    sidebar_dept = html_sidebar.sidebar_dept
+
+    security.declareProtected(ScoView, "showEtudLog")
+
+    def showEtudLog(self, etudid, format="html", REQUEST=None):
+        """Display log of operations on this student"""
+        etud = self.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+
+        ops = self.listScoLog(etudid)
+
+        tab = GenTable(
+            titles={
+                "date": "Date",
+                "authenticated_user": "Utilisateur",
+                "remote_addr": "IP",
+                "method": "Opération",
+                "msg": "Message",
+            },
+            columns_ids=("date", "authenticated_user", "remote_addr", "method", "msg"),
+            rows=ops,
+            html_sortable=True,
+            html_class="table_leftalign",
+            base_url="%s?etudid=%s" % (REQUEST.URL0, etudid),
+            page_title="Opérations sur %(nomprenom)s" % etud,
+            html_title="<h2>Opérations effectuées sur l'étudiant %(nomprenom)s</h2>"
+            % etud,
+            filename="log_" + make_filename(etud["nomprenom"]),
+            html_next_section='<ul><li><a href="ficheEtud?etudid=%(etudid)s">fiche de %(nomprenom)s</a></li></ul>'
+            % etud,
+            preferences=self.get_preferences(),
+        )
+
+        return tab.make_page(self, format=format, REQUEST=REQUEST)
+
+    def listScoLog(self, etudid):
+        "liste des operations effectuees sur cet etudiant"
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "select * from scolog where etudid=%(etudid)s ORDER BY DATE DESC",
+            {"etudid": etudid},
+        )
+        return cursor.dictfetchall()
+
+    # ----------  PAGE ACCUEIL (listes) --------------
+    security.declareProtected(ScoView, "index_html")
+    index_html = sco_dept.index_html
+
+    security.declareProtected(ScoView, "rssnews")
+
+    def rssnews(self, REQUEST=None):
+        "rss feed"
+        REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+        return sco_news.scolar_news_summary_rss(
+            self, "Nouvelles de " + self.get_preference("DeptName"), self.ScoURL()
+        )
+
+    # genere liste html pour accès aux groupes de ce semestre
+    def make_listes_sem(self, sem, REQUEST=None, with_absences=True):
+        context = self
+        authuser = REQUEST.AUTHENTICATED_USER
+        r = self.ScoURL()  # root url
+        # construit l'URL "destination"
+        # (a laquelle on revient apres saisie absences)
+        query_args = cgi.parse_qs(REQUEST.QUERY_STRING)
+        if "head_message" in query_args:
+            del query_args["head_message"]
+        destination = "%s?%s" % (REQUEST.URL, urllib.urlencode(query_args, True))
+        destination = destination.replace(
+            "%", "%%"
+        )  # car ici utilisee dans un format string !
+
+        #
+        H = []
+        # pas de menu absences si pas autorise:
+        if with_absences and not authuser.has_permission(ScoAbsChange, self):
+            with_absences = False
+
+        #
+        H.append(
+            '<h3>Listes de %(titre)s <span class="infostitresem">(%(mois_debut)s - %(mois_fin)s)</span></h3>'
+            % sem
+        )
+
+        formsemestre_id = sem["formsemestre_id"]
+
+        # calcule dates 1er jour semaine pour absences
+        try:
+            if with_absences:
+                first_monday = ZAbsences.ddmmyyyy(sem["date_debut"]).prev_monday()
+                FA = []  # formulaire avec menu saisi absences
+                FA.append(
+                    '<td><form action="Absences/SignaleAbsenceGrSemestre" method="get">'
+                )
+                FA.append(
+                    '<input type="hidden" name="datefin" value="%(date_fin)s"/>' % sem
+                )
+                FA.append(
+                    '<input type="hidden" name="group_ids" value="%(group_id)s"/>'
+                )
+
+                FA.append(
+                    '<input type="hidden" name="destination" value="%s"/>' % destination
+                )
+                FA.append('<input type="submit" value="Saisir absences du" />')
+                FA.append('<select name="datedebut" class="noprint">')
+                date = first_monday
+                for jour in self.Absences.day_names():
+                    FA.append('<option value="%s">%s</option>' % (date, jour))
+                    date = date.next()
+                FA.append("</select>")
+                FA.append(
+                    '<a href="Absences/EtatAbsencesGr?group_ids=%%(group_id)s&amp;debut=%(date_debut)s&amp;fin=%(date_fin)s">état</a>'
+                    % sem
+                )
+                FA.append("</form></td>")
+                FormAbs = "\n".join(FA)
+            else:
+                FormAbs = ""
+        except ScoInvalidDateError:  # dates incorrectes dans semestres ?
+            FormAbs = ""
+        #
+        H.append('<div id="grouplists">')
+        # Genere liste pour chaque partition (categorie de groupes)
+        for partition in sco_groups.get_partitions_list(
+            context, sem["formsemestre_id"]
+        ):
+            if not partition["partition_name"]:
+                H.append("<h4>Tous les étudiants</h4>" % partition)
+            else:
+                H.append("<h4>Groupes de %(partition_name)s</h4>" % partition)
+            groups = sco_groups.get_partition_groups(context, partition)
+            if groups:
+                H.append("<table>")
+                for group in groups:
+                    n_members = len(
+                        sco_groups.get_group_members(context, group["group_id"])
+                    )
+                    group["url"] = r
+                    if group["group_name"]:
+                        group["label"] = "groupe %(group_name)s" % group
+                    else:
+                        group["label"] = "liste"
+                    H.append('<tr class="listegroupelink">')
+                    H.append(
+                        """<td>
+                        <a href="%(url)s/groups_view?group_ids=%(group_id)s">%(label)s</a>
+                        </td><td>
+                        (<a href="%(url)s/groups_view?group_ids=%(group_id)s&amp;format=xls">format tableur</a>)
+                        <a href="%(url)s/groups_view?curtab=tab-photos&amp;group_ids=%(group_id)s&amp;etat=I">Photos</a>
+                        </td>"""
+                        % group
+                    )
+                    H.append("<td>(%d étudiants)</td>" % n_members)
+
+                    if with_absences:
+                        H.append(FormAbs % group)
+
+                    H.append("</tr>")
+                H.append("</table>")
+            else:
+                H.append('<p class="help indent">Aucun groupe dans cette partition')
+                if self.Notes.can_change_groups(REQUEST, formsemestre_id):
+                    H.append(
+                        ' (<a href="affectGroups?partition_id=%s" class="stdlink">créer</a>)'
+                        % partition["partition_id"]
+                    )
+                H.append("</p>")
+        if self.Notes.can_change_groups(REQUEST, formsemestre_id):
+            H.append(
+                '<h4><a href="editPartitionForm?formsemestre_id=%s">Ajouter une partition</a></h4>'
+                % formsemestre_id
+            )
+
+        H.append("</div>")
+        return "\n".join(H)
+
+    security.declareProtected(ScoView, "trombino")
+    trombino = sco_trombino.trombino
+
+    security.declareProtected(ScoView, "pdf_trombino_tours")
+    pdf_trombino_tours = sco_trombino_tours.pdf_trombino_tours
+
+    security.declareProtected(ScoView, "pdf_feuille_releve_absences")
+    pdf_feuille_releve_absences = sco_trombino_tours.pdf_feuille_releve_absences
+
+    security.declareProtected(ScoView, "trombino_copy_photos")
+    trombino_copy_photos = sco_trombino.trombino_copy_photos
+
+    security.declareProtected(ScoView, "groups_view")
+    groups_view = sco_groups_view.groups_view
+
+    security.declareProtected(ScoView, "export_groups_as_moodle_csv")
+    export_groups_as_moodle_csv = sco_groups_view.export_groups_as_moodle_csv
+
+    security.declareProtected(ScoView, "getEtudInfoGroupes")
+
+    def getEtudInfoGroupes(self, group_ids, etat=None):
+        """liste triée d'infos (dict) sur les etudiants du groupe indiqué.
+        Attention: lent, car plusieurs requetes SQL par etudiant !
+        """
+        etuds = []
+        for group_id in group_ids:
+            members = sco_groups.get_group_members(self, group_id, etat=etat)
+            for m in members:
+                etud = self.getEtudInfo(etudid=m["etudid"], filled=True)[0]
+                etuds.append(etud)
+
+        return etuds
+
+    # -------------------------- INFOS SUR ETUDIANTS --------------------------
+    security.declareProtected(ScoView, "getEtudInfo")
+
+    def getEtudInfo(self, etudid=False, code_nip=False, filled=False, REQUEST=None):
+        """infos sur un etudiant pour utilisation en Zope DTML
+        On peut specifier etudid
+        ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine
+        (dans cet ordre).
+        """
+        if etudid is None:
+            return []
+        cnx = self.GetDBConnexion()
+        args = make_etud_args(etudid=etudid, code_nip=code_nip, REQUEST=REQUEST)
+        etud = scolars.etudident_list(cnx, args=args)
+        if filled:
+            self.fillEtudsInfo(etud)
+        return etud
+
+    security.declareProtected(ScoView, "search_etud_in_dept")
+    search_etud_in_dept = sco_find_etud.search_etud_in_dept
+
+    security.declareProtected(ScoView, "search_etud_by_name")
+    search_etud_by_name = sco_find_etud.search_etud_by_name
+
+    security.declareProtected(ScoView, "fillEtudsInfo")
+
+    def fillEtudsInfo(self, etuds):
+        """etuds est une liste d'etudiants (mappings)
+        Pour chaque etudiant, ajoute ou formatte les champs
+        -> informations pour fiche etudiant ou listes diverses
+        """
+        cnx = self.GetDBConnexion()
+        # open('/tmp/t','w').write( str(etuds) )
+        for etud in etuds:
+            etudid = etud["etudid"]
+            etud["dept"] = self.DeptId()
+            adrs = scolars.adresse_list(cnx, {"etudid": etudid})
+            if not adrs:
+                # certains "vieux" etudiants n'ont pas d'adresse
+                adr = {}.fromkeys(scolars._adresseEditor.dbfields, "")
+                adr["etudid"] = etudid
+            else:
+                adr = adrs[0]
+                if len(adrs) > 1:
+                    log("fillEtudsInfo: etudid=%s a %d adresses" % (etudid, len(adrs)))
+            etud.update(adr)
+            scolars.format_etud_ident(etud)
+
+            # Semestres dans lesquel il est inscrit
+            ins = self.Notes.do_formsemestre_inscription_list({"etudid": etudid})
+            etud["ins"] = ins
+            now = time.strftime("%Y-%m-%d")
+            sems = []
+            cursem = None  # semestre "courant" ou il est inscrit
+            for i in ins:
+                sem = sco_formsemestre.get_formsemestre(self, i["formsemestre_id"])
+                debut = DateDMYtoISO(sem["date_debut"])
+                fin = DateDMYtoISO(sem["date_fin"])
+                if debut <= now and now <= fin:
+                    cursem = sem
+                    curi = i
+                sem["ins"] = i
+                sems.append(sem)
+            # trie les semestres par date de debut, le plus recent d'abord
+            # (important, ne pas changer (suivi cohortes))
+            sems.sort(lambda x, y: cmp(y["dateord"], x["dateord"]))
+            etud["sems"] = sems
+            etud["cursem"] = cursem
+            if cursem:
+                etud["inscription"] = cursem["titremois"]
+                etud["inscriptionstr"] = "Inscrit en " + cursem["titremois"]
+                etud["inscription_formsemestre_id"] = cursem["formsemestre_id"]
+                etud["etatincursem"] = curi["etat"]
+                etud["situation"] = self._descr_situation_etud(etudid, etud["ne"])
+                # XXX est-ce utile ? sco_groups.etud_add_group_infos(self, etud, cursem)
+            else:
+                if etud["sems"]:
+                    if etud["sems"][0]["dateord"] > time.strftime(
+                        "%Y-%m-%d", time.localtime()
+                    ):
+                        etud["inscription"] = "futur"
+                        etud["situation"] = "futur élève"
+                    else:
+                        etud["inscription"] = "ancien"
+                        etud["situation"] = "ancien élève"
+                else:
+                    etud["inscription"] = "non inscrit"
+                    etud["situation"] = etud["inscription"]
+                etud["inscriptionstr"] = etud["inscription"]
+                etud["inscription_formsemestre_id"] = None
+                # XXXetud['partitions'] = {} # ne va pas chercher les groupes des anciens semestres
+                etud["etatincursem"] = "?"
+
+            # nettoyage champs souvents vides
+            if etud["nomlycee"]:
+                etud["ilycee"] = "Lycée " + format_lycee(etud["nomlycee"])
+                if etud["villelycee"]:
+                    etud["ilycee"] += " (%s)" % etud["villelycee"]
+                etud["ilycee"] += "<br/>"
+            else:
+                if etud["codelycee"]:
+                    etud["ilycee"] = format_lycee_from_code(etud["codelycee"])
+                else:
+                    etud["ilycee"] = ""
+            rap = ""
+            if etud["rapporteur"] or etud["commentaire"]:
+                rap = "Note du rapporteur"
+                if etud["rapporteur"]:
+                    rap += " (%s)" % etud["rapporteur"]
+                rap += ": "
+                if etud["commentaire"]:
+                    rap += "<em>%s</em>" % etud["commentaire"]
+            etud["rap"] = rap
+
+            # if etud['boursier_prec']:
+            #    pass
+
+            if etud["telephone"]:
+                etud["telephonestr"] = "<b>Tél.:</b> " + format_telephone(
+                    etud["telephone"]
+                )
+            else:
+                etud["telephonestr"] = ""
+            if etud["telephonemobile"]:
+                etud["telephonemobilestr"] = "<b>Mobile:</b> " + format_telephone(
+                    etud["telephonemobile"]
+                )
+            else:
+                etud["telephonemobilestr"] = ""
+            etud["debouche"] = etud["debouche"] or ""
+
+    security.declareProtected(ScoView, "etud_info")
+
+    def etud_info(self, etudid=None, format="xml", REQUEST=None):
+        "Donne les informations sur un etudiant"
+        t0 = time.time()
+        args = make_etud_args(etudid=etudid, REQUEST=REQUEST)
+        cnx = self.GetDBConnexion()
+        etuds = scolars.etudident_list(cnx, args)
+        if not etuds:
+            # etudiant non trouvé: message d'erreur
+            d = {
+                "etudid": etudid,
+                "nom": "?",
+                "nom_usuel": "",
+                "prenom": "?",
+                "sexe": "?",
+                "email": "?",
+                "emailperso": "",
+                "error": "code etudiant inconnu",
+            }
+            return sendResult(
+                REQUEST, d, name="etudiant", format=format, force_outer_xml_tag=False
+            )
+        d = {}
+        etud = etuds[0]
+        self.fillEtudsInfo([etud])
+        etud["date_naissance_iso"] = DateDMYtoISO(etud["date_naissance"])
+        for a in (
+            "etudid",
+            "code_nip",
+            "code_ine",
+            "nom",
+            "nom_usuel",
+            "prenom",
+            "sexe",
+            "nomprenom",
+            "email",
+            "emailperso",
+            "domicile",
+            "codepostaldomicile",
+            "villedomicile",
+            "paysdomicile",
+            "telephone",
+            "telephonemobile",
+            "fax",
+            "bac",
+            "specialite",
+            "annee_bac",
+            "nomlycee",
+            "villelycee",
+            "codepostallycee",
+            "codelycee",
+            "date_naissance_iso",
+        ):
+            d[a] = quote_xml_attr(etud[a])
+        d["photo_url"] = quote_xml_attr(sco_photos.etud_photo_url(self, etud))
+
+        sem = etud["cursem"]
+        if sem:
+            sco_groups.etud_add_group_infos(self, etud, sem)
+            d["insemestre"] = [
+                {
+                    "current": "1",
+                    "formsemestre_id": sem["formsemestre_id"],
+                    "date_debut": DateDMYtoISO(sem["date_debut"]),
+                    "date_fin": DateDMYtoISO(sem["date_fin"]),
+                    "etat": quote_xml_attr(sem["ins"]["etat"]),
+                    "groupes": quote_xml_attr(
+                        etud["groupes"]
+                    ),  # slt pour semestre courant
+                }
+            ]
+        else:
+            d["insemestre"] = []
+        for sem in etud["sems"]:
+            if sem != etud["cursem"]:
+                d["insemestre"].append(
+                    {
+                        "formsemestre_id": sem["formsemestre_id"],
+                        "date_debut": DateDMYtoISO(sem["date_debut"]),
+                        "date_fin": DateDMYtoISO(sem["date_fin"]),
+                        "etat": quote_xml_attr(sem["ins"]["etat"]),
+                    }
+                )
+
+        log("etud_info (%gs)" % (time.time() - t0))
+        return sendResult(
+            REQUEST, d, name="etudiant", format=format, force_outer_xml_tag=False
+        )
+
+    security.declareProtected(ScoView, "XMLgetEtudInfos")
+    XMLgetEtudInfos = etud_info  # old name, deprecated
+
+    def isPrimoEtud(self, etud, sem):
+        """Determine si un (filled) etud a ete inscrit avant ce semestre.
+        Regarde la liste des semestres dans lesquels l'étudiant est inscrit
+        """
+        now = sem["dateord"]
+        for s in etud["sems"]:  # le + recent d'abord
+            if s["dateord"] < now:
+                return False
+        return True
+
+    # -------------------------- FICHE ETUDIANT --------------------------
+    security.declareProtected(ScoView, "ficheEtud")
+    ficheEtud = sco_page_etud.ficheEtud
+
+    security.declareProtected(ScoView, "etud_upload_file_form")
+    etud_upload_file_form = sco_archives_etud.etud_upload_file_form
+
+    security.declareProtected(ScoView, "etud_delete_archive")
+    etud_delete_archive = sco_archives_etud.etud_delete_archive
+
+    security.declareProtected(ScoView, "etud_get_archived_file")
+    etud_get_archived_file = sco_archives_etud.etud_get_archived_file
+
+    security.declareProtected(ScoView, "etudarchive_import_files_form")
+    etudarchive_import_files_form = sco_archives_etud.etudarchive_import_files_form
+
+    security.declareProtected(ScoView, "etudarchive_generate_excel_sample")
+    etudarchive_generate_excel_sample = (
+        sco_archives_etud.etudarchive_generate_excel_sample
+    )
+
+    def _descr_situation_etud(self, etudid, ne=""):
+        """chaine decrivant la situation actuelle de l'etudiant
+        """
+        cnx = self.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "select I.formsemestre_id, I.etat from notes_formsemestre_inscription I,  notes_formsemestre S where etudid=%(etudid)s and S.formsemestre_id = I.formsemestre_id and date_debut < now() and date_fin > now() order by S.date_debut desc;",
+            {"etudid": etudid},
+        )
+        r = cursor.dictfetchone()
+        if not r:
+            situation = "non inscrit"
+        else:
+            sem = sco_formsemestre.get_formsemestre(self, r["formsemestre_id"])
+            if r["etat"] == "I":
+                situation = "inscrit%s en %s" % (ne, sem["titremois"])
+                # Cherche la date d'inscription dans scolar_events:
+                events = scolars.scolar_events_list(
+                    cnx,
+                    args={
+                        "etudid": etudid,
+                        "formsemestre_id": sem["formsemestre_id"],
+                        "event_type": "INSCRIPTION",
+                    },
+                )
+                if not events:
+                    log(
+                        "*** situation inconsistante pour %s (inscrit mais pas d'event)"
+                        % etudid
+                    )
+                    date_ins = "???"  # ???
+                else:
+                    date_ins = events[0]["event_date"]
+                situation += " le " + str(date_ins)
+            else:
+                situation = "démission de %s" % sem["titremois"]
+                # Cherche la date de demission dans scolar_events:
+                events = scolars.scolar_events_list(
+                    cnx,
+                    args={
+                        "etudid": etudid,
+                        "formsemestre_id": sem["formsemestre_id"],
+                        "event_type": "DEMISSION",
+                    },
+                )
+                if not events:
+                    log(
+                        "*** situation inconsistante pour %s (demission mais pas d'event)"
+                        % etudid
+                    )
+                    date_dem = "???"  # ???
+                else:
+                    date_dem = events[0]["event_date"]
+                situation += " le " + str(date_dem)
+        return situation
+
+    # Debouche / devenir etudiant
+
+    # vrai si l'utilisateur peut modifier les informations de suivi sur la page etud"
+    def can_edit_suivi(self, REQUEST=None):
+        authuser = REQUEST.AUTHENTICATED_USER
+        return authuser.has_permission(ScoEtudChangeAdr, self)
+
+    security.declareProtected(ScoEtudChangeAdr, "itemsuivi_suppress")
+    itemsuivi_suppress = sco_debouche.itemsuivi_suppress
+    security.declareProtected(ScoEtudChangeAdr, "itemsuivi_create")
+    itemsuivi_create = sco_debouche.itemsuivi_create
+    security.declareProtected(ScoEtudChangeAdr, "itemsuivi_set_date")
+    itemsuivi_set_date = sco_debouche.itemsuivi_set_date
+    security.declareProtected(ScoEtudChangeAdr, "itemsuivi_set_situation")
+    itemsuivi_set_situation = sco_debouche.itemsuivi_set_situation
+    security.declareProtected(ScoView, "itemsuivi_list_etud")
+    itemsuivi_list_etud = sco_debouche.itemsuivi_list_etud
+    security.declareProtected(ScoView, "itemsuivi_tag_list")
+    itemsuivi_tag_list = sco_debouche.itemsuivi_tag_list
+    security.declareProtected(ScoView, "itemsuivi_tag_search")
+    itemsuivi_tag_search = sco_debouche.itemsuivi_tag_search
+    security.declareProtected(ScoEtudChangeAdr, "itemsuivi_tag_set")
+    itemsuivi_tag_set = sco_debouche.itemsuivi_tag_set
+
+    security.declareProtected(ScoEtudAddAnnotations, "doAddAnnotation")
+
+    def doAddAnnotation(self, etudid, comment, REQUEST):
+        "ajoute annotation sur etudiant"
+        authuser = REQUEST.AUTHENTICATED_USER
+        cnx = self.GetDBConnexion()
+        scolars.etud_annotations_create(
+            cnx,
+            args={
+                "etudid": etudid,
+                "comment": comment,
+                "zope_authenticated_user": str(authuser),
+                "zope_remote_addr": REQUEST.REMOTE_ADDR,
+            },
+        )
+        logdb(REQUEST, cnx, method="addAnnotation", etudid=etudid)
+        REQUEST.RESPONSE.redirect("ficheEtud?etudid=" + etudid)
+
+    security.declareProtected(ScoView, "canSuppressAnnotation")
+
+    def canSuppressAnnotation(self, annotation_id, REQUEST):
+        """True if current user can suppress this annotation
+        Seuls l'auteur de l'annotation et le chef de dept peuvent supprimer
+        une annotation.
+        """
+        cnx = self.GetDBConnexion()
+        annos = scolars.etud_annotations_list(cnx, args={"id": annotation_id})
+        if len(annos) != 1:
+            raise ScoValueError("annotation inexistante !")
+        anno = annos[0]
+        authuser = REQUEST.AUTHENTICATED_USER
+        # note: les anciennes installations n'ont pas le role ScoEtudSupprAnnotations
+        # c'est pourquoi on teste aussi ScoEtudInscrit (normalement détenue par le chef)
+        return (
+            (str(authuser) == anno["zope_authenticated_user"])
+            or authuser.has_permission(ScoEtudSupprAnnotations, self)
+            or authuser.has_permission(ScoEtudInscrit, self)
+        )
+
+    security.declareProtected(ScoView, "doSuppressAnnotation")
+
+    def doSuppressAnnotation(self, etudid, annotation_id, REQUEST):
+        """Suppression annotation.
+        """
+        if not self.canSuppressAnnotation(annotation_id, REQUEST):
+            raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+
+        cnx = self.GetDBConnexion()
+        annos = scolars.etud_annotations_list(cnx, args={"id": annotation_id})
+        if len(annos) != 1:
+            raise ScoValueError("annotation inexistante !")
+        anno = annos[0]
+        log("suppress annotation: %s" % str(anno))
+        logdb(REQUEST, cnx, method="SuppressAnnotation", etudid=etudid)
+        scolars.etud_annotations_delete(cnx, annotation_id)
+
+        REQUEST.RESPONSE.redirect(
+            "ficheEtud?etudid=%s&amp;head_message=Annotation%%20supprimée" % (etudid)
+        )
+
+    security.declareProtected(ScoEtudChangeAdr, "formChangeCoordonnees")
+
+    def formChangeCoordonnees(self, etudid, REQUEST):
+        "edit coordonnees etudiant"
+        cnx = self.GetDBConnexion()
+        etud = self.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+        adrs = scolars.adresse_list(cnx, {"etudid": etudid})
+        if adrs:
+            adr = adrs[0]
+        else:
+            adr = {}  # no data for this student
+        H = [
+            '<h2><font color="#FF0000">Changement des coordonnées de </font> %(nomprenom)s</h2><p>'
+            % etud
+        ]
+        header = self.sco_header(
+            REQUEST, page_title="Changement adresse de %(nomprenom)s" % etud
+        )
+
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (
+                ("adresse_id", {"input_type": "hidden"}),
+                ("etudid", {"input_type": "hidden"}),
+                (
+                    "email",
+                    {
+                        "size": 40,
+                        "title": "e-mail",
+                        "explanation": "adresse institutionnelle",
+                    },
+                ),
+                (
+                    "emailperso",
+                    {
+                        "size": 40,
+                        "title": "e-mail",
+                        "explanation": "adresse personnelle",
+                    },
+                ),
+                (
+                    "domicile",
+                    {"size": 65, "explanation": "numéro, rue", "title": "Adresse"},
+                ),
+                ("codepostaldomicile", {"size": 6, "title": "Code postal"}),
+                ("villedomicile", {"size": 20, "title": "Ville"}),
+                ("paysdomicile", {"size": 20, "title": "Pays"}),
+                ("", {"input_type": "separator", "default": "&nbsp;"}),
+                ("telephone", {"size": 13, "title": "Téléphone"}),
+                ("telephonemobile", {"size": 13, "title": "Mobile"}),
+            ),
+            initvalues=adr,
+            submitlabel="Valider le formulaire",
+        )
+        if tf[0] == 0:
+            return header + "\n".join(H) + tf[1] + self.sco_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+        else:
+            if adrs:
+                scolars.adresse_edit(cnx, args=tf[2], context=self)
+            else:
+                scolars.adresse_create(cnx, args=tf[2])
+            logdb(REQUEST, cnx, method="changeCoordonnees", etudid=etudid)
+            REQUEST.RESPONSE.redirect("ficheEtud?etudid=" + etudid)
+
+    security.declareProtected(ScoView, "formChangeGroup")
+
+    def formChangeGroup(self, formsemestre_id, etudid, REQUEST):
+        "changement groupe etudiant dans semestre"
+        if not self.Notes.can_change_groups(REQUEST, formsemestre_id):
+            raise ScoValueError(
+                "Vous n'avez pas le droit d'effectuer cette opération !"
+            )
+        cnx = self.GetDBConnexion()
+        etud = self.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+        sem = sco_formsemestre.do_formsemestre_list(self, formsemestre_id)
+        ins = self.Notes.do_formsemestre_inscription_list(
+            {"etudid": etudid, "formsemestre_id": formsemestre_id}
+        )[0]
+        #
+        # -- check lock
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        if sem["etat"] != "1":
+            raise ScoValueError("Modification impossible: semestre verrouille")
+        #
+        etud["semtitre"] = sem["titremois"]
+        H = [
+            '<h2><font color="#FF0000">Changement de groupe de</font> %(nomprenom)s (semestre %(semtitre)s)</h2><p>'
+            % etud
+        ]
+        header = self.sco_header(
+            REQUEST, page_title="Changement de groupe de %(nomprenom)s" % etud
+        )
+        # Liste des groupes existants
+        raise NotImplementedError  # XXX utiliser form_group_choice ou supprimer completement ?
+        #
+        H.append("""<form action="doChangeGroup" method="get" name="cg">""")
+
+        H.append(
+            """<input type="hidden" name="etudid" value="%s">
+<input type="hidden" name="formsemestre_id" value="%s">
+<p>
+(attention, vérifier que les groupes sont compatibles, selon votre organisation)
+</p>
+<script type="text/javascript">
+function tweakmenu( gname ) {
+   var gr = document.cg.newgroupname.value;
+   if (!gr) {
+      alert("nom de groupe vide !");
+      return false;
+   }
+   var menutd = document.getElementById(gname);
+   var newopt = document.createElement('option');
+   newopt.value = gr;
+   var textopt = document.createTextNode(gr);
+   newopt.appendChild(textopt);
+   menutd.appendChild(newopt);
+   var msg = document.getElementById("groupemsg");
+   msg.appendChild( document.createTextNode("groupe " + gr + " créé; ") );
+   document.cg.newgroupname.value = "";
+}
+</script>
+
+<p>Créer un nouveau groupe:
+<input type="text" id="newgroupname" size="8"/>
+<input type="button" onClick="tweakmenu( 'groupetd' );" value="créer groupe de %s"/>
+<input type="button" onClick="tweakmenu( 'groupeanglais' );" value="créer groupe de %s"/>
+<input type="button" onClick="tweakmenu( 'groupetp' );" value="créer groupe de %s"/>
+</p>
+<p id="groupemsg" style="font-style: italic;"></p>
+
+<input type="submit" value="Changer de groupe">
+<input type="button" value="Annuler" onClick="window.location='%s'">
+
+</form>"""
+            % (
+                etudid,
+                formsemestre_id,
+                sem["nomgroupetd"],
+                sem["nomgroupeta"],
+                sem["nomgroupetp"],
+                REQUEST.URL1,
+            )
+        )
+
+        return header + "\n".join(H) + self.sco_footer(REQUEST)
+
+    # --- Gestion des groupes:
+    security.declareProtected(ScoView, "affectGroups")
+    affectGroups = sco_groups_edit.affectGroups
+
+    security.declareProtected(ScoView, "XMLgetGroupsInPartition")
+    XMLgetGroupsInPartition = sco_groups.XMLgetGroupsInPartition
+
+    security.declareProtected(ScoView, "formsemestre_partition_list")
+    formsemestre_partition_list = sco_groups.formsemestre_partition_list
+
+    security.declareProtected(ScoView, "setGroups")
+    setGroups = sco_groups.setGroups
+
+    security.declareProtected(ScoView, "createGroup")
+    createGroup = sco_groups.createGroup
+
+    security.declareProtected(ScoView, "suppressGroup")
+    suppressGroup = sco_groups.suppressGroup
+
+    security.declareProtected(ScoView, "group_set_name")
+    group_set_name = sco_groups.group_set_name
+
+    security.declareProtected(ScoView, "group_rename")
+    group_rename = sco_groups.group_rename
+
+    security.declareProtected(ScoView, "groups_auto_repartition")
+    groups_auto_repartition = sco_groups.groups_auto_repartition
+
+    security.declareProtected(ScoView, "editPartitionForm")
+    editPartitionForm = sco_groups.editPartitionForm
+
+    security.declareProtected(ScoView, "partition_delete")
+    partition_delete = sco_groups.partition_delete
+
+    security.declareProtected(ScoView, "partition_set_attr")
+    partition_set_attr = sco_groups.partition_set_attr
+
+    security.declareProtected(ScoView, "partition_move")
+    partition_move = sco_groups.partition_move
+
+    security.declareProtected(ScoView, "partition_set_name")
+    partition_set_name = sco_groups.partition_set_name
+
+    security.declareProtected(ScoView, "partition_rename")
+    partition_rename = sco_groups.partition_rename
+
+    security.declareProtected(ScoView, "partition_create")
+    partition_create = sco_groups.partition_create
+
+    security.declareProtected(ScoView, "etud_info_html")
+    etud_info_html = sco_page_etud.etud_info_html
+
+    # --- Gestion des photos:
+    security.declareProtected(ScoView, "get_photo_image")
+
+    get_photo_image = sco_photos.get_photo_image
+
+    security.declareProtected(ScoView, "etud_photo_html")
+
+    etud_photo_html = sco_photos.etud_photo_html
+    
+    security.declareProtected(ScoView, "etud_photo_orig_page")
+
+    def etud_photo_orig_page(self, etudid=None, REQUEST=None):
+        "Page with photo in orig. size"
+        etud = self.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+        H = [
+            self.sco_header(REQUEST, page_title=etud["nomprenom"]),
+            "<h2>%s</h2>" % etud["nomprenom"],
+            '<div><a href="ficheEtud?etudid=%s">' % etudid,
+            sco_photos.etud_photo_orig_html(self, etud),
+            "</a></div>",
+            self.sco_footer(REQUEST),
+        ]
+        return "\n".join(H)
+
+    security.declareProtected(ScoEtudChangeAdr, "formChangePhoto")
+
+    def formChangePhoto(self, etudid=None, REQUEST=None):
+        """Formulaire changement photo étudiant
+        """
+        etud = self.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+        if sco_photos.etud_photo_is_local(self, etud):
+            etud["photoloc"] = "dans ScoDoc"
+        else:
+            etud["photoloc"] = "externe"
+        H = [
+            self.sco_header(REQUEST, page_title="Changement de photo"),
+            """<h2>Changement de la photo de %(nomprenom)s</h2>
+             <p>Photo actuelle (%(photoloc)s):             
+             """
+            % etud,
+            sco_photos.etud_photo_html(
+                self, etud, title="photo actuelle", REQUEST=REQUEST
+            ),
+            """</p><p>Le fichier ne doit pas dépasser 500Ko (recadrer l'image, format "portrait" de préférence).</p>
+             <p>L'image sera automagiquement réduite pour obtenir une hauteur de 90 pixels.</p>
+             """,
+        ]
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (
+                ("etudid", {"default": etudid, "input_type": "hidden"}),
+                (
+                    "photofile",
+                    {"input_type": "file", "title": "Fichier image", "size": 20},
+                ),
+            ),
+            submitlabel="Valider",
+            cancelbutton="Annuler",
+        )
+        if tf[0] == 0:
+            return (
+                "\n".join(H)
+                + tf[1]
+                + '<p><a class="stdlink" href="formSuppressPhoto?etudid=%s">Supprimer cette photo</a></p>'
+                % etudid
+                + self.sco_footer(REQUEST)
+            )
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(
+                REQUEST.URL1 + "/ficheEtud?etudid=" + etud["etudid"]
+            )
+        else:
+            data = tf[2]["photofile"].read()
+            status, diag = sco_photos.store_photo(self, etud, data, REQUEST=REQUEST)
+            if status != 0:
+                return REQUEST.RESPONSE.redirect(
+                    self.ScoURL() + "/ficheEtud?etudid=" + etud["etudid"]
+                )
+            else:
+                H.append('<p class="warning">Erreur:' + diag + "</p>")
+        return "\n".join(H) + self.sco_footer(REQUEST)
+
+    security.declareProtected(ScoEtudChangeAdr, "formSuppressPhoto")
+
+    def formSuppressPhoto(self, etudid=None, REQUEST=None, dialog_confirmed=False):
+        """Formulaire suppression photo étudiant
+        """
+        etud = self.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+        if not dialog_confirmed:
+            return self.confirmDialog(
+                "<p>Confirmer la suppression de la photo de %(nomprenom)s ?</p>" % etud,
+                dest_url="",
+                REQUEST=REQUEST,
+                cancel_url="ficheEtud?etudid=%s" % etudid,
+                parameters={"etudid": etudid},
+            )
+
+        sco_photos.suppress_photo(self, etud, REQUEST=REQUEST)
+
+        return REQUEST.RESPONSE.redirect(
+            REQUEST.URL1 + "/ficheEtud?etudid=" + etud["etudid"]
+        )
+
+    #
+    security.declareProtected(ScoEtudInscrit, "formDem")
+
+    def formDem(self, etudid, formsemestre_id, REQUEST):
+        "Formulaire Démission Etudiant"
+        return self._formDem_of_Def(
+            etudid,
+            formsemestre_id,
+            REQUEST=REQUEST,
+            operation_name="Démission",
+            operation_method="doDemEtudiant",
+        )
+
+    security.declareProtected(ScoEtudInscrit, "formDef")
+
+    def formDef(self, etudid, formsemestre_id, REQUEST):
+        "Formulaire Défaillance Etudiant"
+        return self._formDem_of_Def(
+            etudid,
+            formsemestre_id,
+            REQUEST=REQUEST,
+            operation_name="Défaillance",
+            operation_method="doDefEtudiant",
+        )
+
+    def _formDem_of_Def(
+        self,
+        etudid,
+        formsemestre_id,
+        REQUEST=None,
+        operation_name="",
+        operation_method="",
+    ):
+        "Formulaire démission ou défaillance Etudiant"
+        cnx = self.GetDBConnexion()
+        etud = self.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        if sem["etat"] != "1":
+            raise ScoValueError("Modification impossible: semestre verrouille")
+
+        etud["formsemestre_id"] = formsemestre_id
+        etud["semtitre"] = sem["titremois"]
+        etud["nowdmy"] = time.strftime("%d/%m/%Y")
+        etud["operation_name"] = operation_name
+        #
+        header = self.sco_header(
+            REQUEST,
+            page_title="%(operation_name)s de  %(nomprenom)s (du semestre %(semtitre)s)"
+            % etud,
+        )
+        H = [
+            '<h2><font color="#FF0000">%(operation_name)s de</font> %(nomprenom)s (semestre %(semtitre)s)</h2><p>'
+            % etud
+        ]
+        H.append(
+            """<form action="%s" method="get">
+        <b>Date de la %s (J/M/AAAA):&nbsp;</b>
+        """
+            % (operation_method, strlower(operation_name))
+        )
+        H.append(
+            """
+<input type="text" name="event_date" width=20 value="%(nowdmy)s">
+<input type="hidden" name="etudid" value="%(etudid)s">
+<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s">
+<p>
+<input type="submit" value="Confirmer">
+</form>"""
+            % etud
+        )
+        return header + "\n".join(H) + self.sco_footer(REQUEST)
+
+    security.declareProtected(ScoEtudInscrit, "doDemEtudiant")
+
+    def doDemEtudiant(self, etudid, formsemestre_id, event_date=None, REQUEST=None):
+        "Déclare la démission d'un etudiant dans le semestre"
+        return self._doDem_or_Def_Etudiant(
+            etudid,
+            formsemestre_id,
+            event_date=event_date,
+            etat_new="D",
+            operation_method="demEtudiant",
+            event_type="DEMISSION",
+            REQUEST=REQUEST,
+        )
+
+    security.declareProtected(ScoEtudInscrit, "doDefEtudiant")
+
+    def doDefEtudiant(self, etudid, formsemestre_id, event_date=None, REQUEST=None):
+        "Déclare la défaillance d'un etudiant dans le semestre"
+        return self._doDem_or_Def_Etudiant(
+            etudid,
+            formsemestre_id,
+            event_date=event_date,
+            etat_new=DEF,
+            operation_method="defailleEtudiant",
+            event_type="DEFAILLANCE",
+            REQUEST=REQUEST,
+        )
+
+    def _doDem_or_Def_Etudiant(
+        self,
+        etudid,
+        formsemestre_id,
+        event_date=None,
+        etat_new="D",  # 'D' or DEF
+        operation_method="demEtudiant",
+        event_type="DEMISSION",
+        REQUEST=None,
+    ):
+        "Démission ou défaillance d'un étudiant"
+        # marque 'D' ou DEF dans l'inscription au semestre et ajoute
+        # un "evenement" scolarite
+        cnx = self.GetDBConnexion()
+        # check lock
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        if sem["etat"] != "1":
+            raise ScoValueError("Modification impossible: semestre verrouille")
+        #
+        ins = self.Notes.do_formsemestre_inscription_list(
+            {"etudid": etudid, "formsemestre_id": formsemestre_id}
+        )[0]
+        if not ins:
+            raise ScoException("etudiant non inscrit ?!")
+        ins["etat"] = etat_new
+        self.Notes.do_formsemestre_inscription_edit(
+            args=ins, formsemestre_id=formsemestre_id
+        )
+        logdb(REQUEST, cnx, method=operation_method, etudid=etudid)
+        scolars.scolar_events_create(
+            cnx,
+            args={
+                "etudid": etudid,
+                "event_date": event_date,
+                "formsemestre_id": formsemestre_id,
+                "event_type": event_type,
+            },
+        )
+        if REQUEST:
+            return REQUEST.RESPONSE.redirect("ficheEtud?etudid=" + etudid)
+
+    security.declareProtected(ScoEtudInscrit, "doCancelDem")
+
+    def doCancelDem(
+        self, etudid, formsemestre_id, dialog_confirmed=False, args=None, REQUEST=None
+    ):
+        "Annule une démission"
+        return self._doCancelDem_or_Def(
+            etudid,
+            formsemestre_id,
+            dialog_confirmed=dialog_confirmed,
+            args=args,
+            operation_name="démission",
+            etat_current="D",
+            etat_new="I",
+            operation_method="cancelDem",
+            event_type="DEMISSION",
+            REQUEST=REQUEST,
+        )
+
+    security.declareProtected(ScoEtudInscrit, "doCancelDef")
+
+    def doCancelDef(
+        self, etudid, formsemestre_id, dialog_confirmed=False, args=None, REQUEST=None
+    ):
+        "Annule la défaillance de l'étudiant"
+        return self._doCancelDem_or_Def(
+            etudid,
+            formsemestre_id,
+            dialog_confirmed=dialog_confirmed,
+            args=args,
+            operation_name="défaillance",
+            etat_current=DEF,
+            etat_new="I",
+            operation_method="cancelDef",
+            event_type="DEFAILLANCE",
+            REQUEST=REQUEST,
+        )
+
+    def _doCancelDem_or_Def(
+        self,
+        etudid,
+        formsemestre_id,
+        dialog_confirmed=False,
+        args=None,
+        operation_name="",  # "démission" ou "défaillance"
+        etat_current="D",
+        etat_new="I",
+        operation_method="cancelDem",
+        event_type="DEMISSION",
+        REQUEST=None,
+    ):
+        "Annule une demission ou une défaillance"
+        # check lock
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        if sem["etat"] != "1":
+            raise ScoValueError("Modification impossible: semestre verrouille")
+        # verif
+        info = self.getEtudInfo(etudid, filled=True)[0]
+        ok = False
+        for i in info["ins"]:
+            if i["formsemestre_id"] == formsemestre_id:
+                if i["etat"] != etat_current:
+                    raise ScoValueError("etudiant non %s !" % operation_name)
+                ok = True
+                break
+        if not ok:
+            raise ScoValueError("etudiant non inscrit ???")
+        if not dialog_confirmed:
+            return self.confirmDialog(
+                "<p>Confirmer l'annulation de la %s ?</p>" % operation_name,
+                dest_url="",
+                REQUEST=REQUEST,
+                cancel_url="ficheEtud?etudid=%s" % etudid,
+                parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
+            )
+        #
+        ins = self.Notes.do_formsemestre_inscription_list(
+            {"etudid": etudid, "formsemestre_id": formsemestre_id}
+        )[0]
+        if ins["etat"] != etat_current:
+            raise ScoException("etudiant non %s !!!" % etat_current)  # obviously a bug
+        ins["etat"] = etat_new
+        cnx = self.GetDBConnexion()
+        self.Notes.do_formsemestre_inscription_edit(
+            args=ins, formsemestre_id=formsemestre_id
+        )
+        logdb(REQUEST, cnx, method=operation_method, etudid=etudid)
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "delete from scolar_events where etudid=%(etudid)s and formsemestre_id=%(formsemestre_id)s and event_type='"
+            + event_type
+            + "'",
+            {"etudid": etudid, "formsemestre_id": formsemestre_id},
+        )
+        cnx.commit()
+        return REQUEST.RESPONSE.redirect("ficheEtud?etudid=%s" % etudid)
+
+    security.declareProtected(ScoEtudInscrit, "etudident_create_form")
+
+    def etudident_create_form(self, REQUEST=None):
+        "formulaire creation individuelle etudiant"
+        return self.etudident_create_or_edit_form(REQUEST, edit=False)
+
+    security.declareProtected(ScoEtudInscrit, "etudident_edit_form")
+
+    def etudident_edit_form(self, REQUEST=None):
+        "formulaire edition individuelle etudiant"
+        return self.etudident_create_or_edit_form(REQUEST, edit=True)
+
+    security.declareProtected(ScoEtudInscrit, "etudident_create_or_edit_form")
+
+    def etudident_create_or_edit_form(self, REQUEST, edit):
+        "Le formulaire HTML"
+        H = [self.sco_header(REQUEST, init_jquery_ui=True)]
+        F = self.sco_footer(REQUEST)
+        AUTHENTICATED_USER = REQUEST.AUTHENTICATED_USER
+        etudid = REQUEST.form.get("etudid", None)
+        cnx = self.GetDBConnexion()
+        descr = []
+        if not edit:
+            # creation nouvel etudiant
+            initvalues = {}
+            submitlabel = "Ajouter cet étudiant"
+            H.append(
+                """<h2>Création d'un étudiant</h2>
+            <p>En général, il est <b>recommandé</b> d'importer les étudiants depuis Apogée.
+            N'utilisez ce formulaire que <b>pour les cas particuliers</b> ou si votre établissement
+            n'utilise pas d'autre logiciel de gestion des inscriptions.</p>
+            <p><em>L'étudiant créé ne sera pas inscrit.
+            Pensez à l'inscrire dans un semestre !</em></p>
+            """
+            )
+        else:
+            # edition donnees d'un etudiant existant
+            # setup form init values
+            if not etudid:
+                raise ValueError("missing etudid parameter")
+            descr.append(("etudid", {"default": etudid, "input_type": "hidden"}))
+            H.append(
+                '<h2>Modification d\'un étudiant (<a href="ficheEtud?etudid=%s">fiche</a>)</h2>'
+                % etudid
+            )
+            initvalues = scolars.etudident_list(cnx, {"etudid": etudid})
+            assert len(initvalues) == 1
+            initvalues = initvalues[0]
+            submitlabel = "Modifier les données"
+
+        # recuperation infos Apogee
+        # Si on a le code NIP, fait juste une requete, sinon tente de rechercher par nom
+        # (la recherche par nom ne fonctionne plus à Paris 13)
+        code_nip = initvalues.get("code_nip", "")
+        if code_nip:
+            try:
+                info = sco_portal_apogee.get_etud_apogee(self, code_nip)
+            except ValueError:
+                pass  # XXX a terminer
+        nom = REQUEST.form.get("nom", None)
+        if nom is None:
+            nom = initvalues.get("nom", None)
+        if nom is None:
+            infos = []
+        else:
+            prenom = REQUEST.form.get("prenom", "")
+            if REQUEST.form.get("tf-submitted", False) and not prenom:
+                prenom = initvalues.get("prenom", "")
+            infos = sco_portal_apogee.get_infos_apogee(self, nom, prenom)
+
+        if infos:
+            formatted_infos = [
+                """
+            <script type="text/javascript">
+            /* <![CDATA[ */
+            function copy_nip(nip) {
+            document.tf.code_nip.value = nip;
+            }
+            /* ]]> */
+            </script>
+            <ol>"""
+            ]
+            nanswers = len(infos)
+            nmax = 10  # nb max de reponse montrees
+            infos = infos[:nmax]
+            for i in infos:
+                formatted_infos.append("<li><ul>")
+                for k in i.keys():
+                    if k != "nip":
+                        item = "<li>%s : %s</li>" % (k, i[k])
+                    else:
+                        item = (
+                            '<li><form>%s : %s <input type="button" value="copier ce code" onmousedown="copy_nip(%s);"/></form></li>'
+                            % (k, i[k], i[k])
+                        )
+                    formatted_infos.append(item)
+
+                formatted_infos.append("</ul></li>")
+            formatted_infos.append("</ol>")
+            m = "%d étudiants trouvés" % nanswers
+            if len(infos) != nanswers:
+                m += " (%d montrés)" % len(infos)
+            A = """<div class="infoapogee">
+            <h5>Informations Apogée</h5>
+            <p>%s</p>
+            %s
+            </div>""" % (
+                m,
+                "\n".join(formatted_infos),
+            )
+        else:
+            A = """<div class="infoapogee"><p>Pas d'informations d'Apogée</p></div>"""
+
+        require_ine = self.get_preference("always_require_ine")
+
+        descr += [
+            ("adm_id", {"input_type": "hidden"}),
+            ("nom", {"size": 25, "title": "Nom", "allow_null": False}),
+            ("nom_usuel", {"size": 25, "title": "Nom usuel", "allow_null": True}),
+            (
+                "prenom",
+                {"size": 25, "title": "Prénom", "allow_null": CONFIG.ALLOW_NULL_PRENOM},
+            ),
+            (
+                "sexe",
+                {
+                    "input_type": "menu",
+                    "labels": ["H", "F"],
+                    "allowed_values": ["MR", "MME"],
+                    "title": "Genre",
+                },
+            ),
+            (
+                "date_naissance",
+                {
+                    "title": "Date de naissance",
+                    "input_type": "date",
+                    "explanation": "j/m/a",
+                },
+            ),
+            ("lieu_naissance", {"title": "Lieu de naissance", "size": 32}),
+            ("dept_naissance", {"title": "Département de naissance", "size": 5}),
+            ("nationalite", {"size": 25, "title": "Nationalité"}),
+            (
+                "statut",
+                {
+                    "size": 25,
+                    "title": "Statut",
+                    "explanation": '("salarie", ...) inutilisé par ScoDoc',
+                },
+            ),
+            (
+                "annee",
+                {
+                    "size": 5,
+                    "title": "Année admission IUT",
+                    "type": "int",
+                    "allow_null": False,
+                    "explanation": "année 1ere inscription (obligatoire)",
+                },
+            ),
+            #
+            ("sep", {"input_type": "separator", "title": "Scolarité antérieure:"}),
+            ("bac", {"size": 5, "explanation": "série du bac (S, STI, STT, ...)"}),
+            (
+                "specialite",
+                {
+                    "size": 25,
+                    "title": "Spécialité",
+                    "explanation": "spécialité bac: SVT M, GENIE ELECTRONIQUE, ...",
+                },
+            ),
+            (
+                "annee_bac",
+                {
+                    "size": 5,
+                    "title": "Année bac",
+                    "type": "int",
+                    "explanation": "année obtention du bac",
+                },
+            ),
+            (
+                "math",
+                {
+                    "size": 3,
+                    "type": "float",
+                    "title": "Note de mathématiques",
+                    "explanation": "note sur 20 en terminale",
+                },
+            ),
+            (
+                "physique",
+                {
+                    "size": 3,
+                    "type": "float",
+                    "title": "Note de physique",
+                    "explanation": "note sur 20 en terminale",
+                },
+            ),
+            (
+                "anglais",
+                {
+                    "size": 3,
+                    "type": "float",
+                    "title": "Note d'anglais",
+                    "explanation": "note sur 20 en terminale",
+                },
+            ),
+            (
+                "francais",
+                {
+                    "size": 3,
+                    "type": "float",
+                    "title": "Note de français",
+                    "explanation": "note sur 20 obtenue au bac",
+                },
+            ),
+            (
+                "type_admission",
+                {
+                    "input_type": "menu",
+                    "title": "Voie d'admission",
+                    "allowed_values": TYPES_ADMISSION,
+                },
+            ),
+            (
+                "boursier_prec",
+                {
+                    "input_type": "boolcheckbox",
+                    "labels": ["non", "oui"],
+                    "title": "Boursier ?",
+                    "explanation": "dans le cycle précédent (lycée)",
+                },
+            ),
+            (
+                "rang",
+                {
+                    "size": 1,
+                    "type": "int",
+                    "title": "Position établissement",
+                    "explanation": "rang de notre établissement dans les voeux du candidat (si connu)",
+                },
+            ),
+            (
+                "qualite",
+                {
+                    "size": 3,
+                    "type": "float",
+                    "title": "Qualité",
+                    "explanation": "Note de qualité attribuée au dossier (par le jury d'adm.)",
+                },
+            ),
+            (
+                "decision",
+                {
+                    "input_type": "menu",
+                    "title": "Décision",
+                    "allowed_values": [
+                        "ADMIS",
+                        "ATTENTE 1",
+                        "ATTENTE 2",
+                        "ATTENTE 3",
+                        "REFUS",
+                        "?",
+                    ],
+                },
+            ),
+            (
+                "score",
+                {
+                    "size": 3,
+                    "type": "float",
+                    "title": "Score",
+                    "explanation": "score calculé lors de l'admission",
+                },
+            ),
+            (
+                "classement",
+                {
+                    "size": 3,
+                    "type": "int",
+                    "title": "Classement",
+                    "explanation": "Classement par le jury d'admission (de 1 à N)",
+                },
+            ),
+            ("apb_groupe", {"size": 15, "title": "Groupe APB ou PS"}),
+            (
+                "apb_classement_gr",
+                {
+                    "size": 3,
+                    "type": "int",
+                    "title": "Classement",
+                    "explanation": "Classement par le jury dans le groupe ABP ou PS (de 1 à Ng)",
+                },
+            ),
+            ("rapporteur", {"size": 50, "title": "Enseignant rapporteur"}),
+            (
+                "commentaire",
+                {
+                    "input_type": "textarea",
+                    "rows": 4,
+                    "cols": 50,
+                    "title": "Note du rapporteur",
+                },
+            ),
+            ("nomlycee", {"size": 20, "title": "Lycée d'origine"}),
+            ("villelycee", {"size": 15, "title": "Commune du lycée"}),
+            ("codepostallycee", {"size": 15, "title": "Code Postal lycée"}),
+            (
+                "codelycee",
+                {
+                    "size": 15,
+                    "title": "Code Lycée",
+                    "explanation": "Code national établissement du lycée ou établissement d'origine",
+                },
+            ),
+            ("sep", {"input_type": "separator", "title": "Codes Apogée: (optionnels)"}),
+            (
+                "code_nip",
+                {
+                    "size": 25,
+                    "title": "Numéro NIP",
+                    "allow_null": True,
+                    "explanation": "numéro identité étudiant (Apogée)",
+                },
+            ),
+            (
+                "code_ine",
+                {
+                    "size": 25,
+                    "title": "Numéro INE",
+                    "allow_null": not require_ine,
+                    "explanation": "numéro INE",
+                },
+            ),
+            (
+                "dont_check_homonyms",
+                {
+                    "title": "Autoriser les homonymes",
+                    "input_type": "boolcheckbox",
+                    "explanation": "ne vérifie pas les noms et prénoms proches",
+                },
+            ),
+        ]
+        initvalues["dont_check_homonyms"] = False
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            descr,
+            submitlabel=submitlabel,
+            cancelbutton="Re-interroger Apogee",
+            initvalues=initvalues,
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + "<p>" + A + F
+        elif tf[0] == -1:
+            return "\n".join(H) + tf[1] + "<p>" + A + F
+            # return '\n'.join(H) + '<h4>annulation</h4>' + F
+        else:
+            # form submission
+            if edit:
+                etudid = tf[2]["etudid"]
+            else:
+                etudid = None
+            ok, NbHomonyms = scolars.check_nom_prenom(
+                cnx, nom=tf[2]["nom"], prenom=tf[2]["prenom"], etudid=etudid
+            )
+            if not ok:
+                return (
+                    "\n".join(H)
+                    + tf_error_message("Nom ou prénom invalide")
+                    + tf[1]
+                    + "<p>"
+                    + A
+                    + F
+                )
+            # log('NbHomonyms=%s' % NbHomonyms)
+            if not tf[2]["dont_check_homonyms"] and NbHomonyms > 0:
+                return (
+                    "\n".join(H)
+                    + tf_error_message(
+                        """Attention: il y a déjà un étudiant portant des noms et prénoms proches. Vous pouvez forcer la présence d'un homonyme en cochant "autoriser les homonymes" en bas du formulaire."""
+                    )
+                    + tf[1]
+                    + "<p>"
+                    + A
+                    + F
+                )
+
+            if not edit:
+                # creation d'un etudiant
+                etudid = scolars.etudident_create(
+                    cnx, tf[2], context=self, REQUEST=REQUEST
+                )
+                # crée une adresse vide (chaque etudiant doit etre dans la table "adresse" !)
+                adresse_id = scolars.adresse_create(
+                    cnx,
+                    {
+                        "etudid": etudid,
+                        "typeadresse": "domicile",
+                        "description": "(creation individuelle)",
+                    },
+                )
+
+                # event
+                scolars.scolar_events_create(
+                    cnx,
+                    args={
+                        "etudid": etudid,
+                        "event_date": time.strftime("%d/%m/%Y"),
+                        "formsemestre_id": None,
+                        "event_type": "CREATION",
+                    },
+                )
+                # log
+                logdb(
+                    REQUEST,
+                    cnx,
+                    method="etudident_edit_form",
+                    etudid=etudid,
+                    msg="creation initiale",
+                )
+                etud = scolars.etudident_list(cnx, {"etudid": etudid})[0]
+                self.fillEtudsInfo([etud])
+                etud["url"] = "ficheEtud?etudid=%(etudid)s" % etud
+                sco_news.add(
+                    self,
+                    REQUEST,
+                    typ=NEWS_INSCR,
+                    object=None,  # pas d'object pour ne montrer qu'un etudiant
+                    text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
+                    url=etud["url"],
+                )
+            else:
+                # modif d'un etudiant
+                scolars.etudident_edit(cnx, tf[2], context=self, REQUEST=REQUEST)
+                etud = scolars.etudident_list(cnx, {"etudid": etudid})[0]
+                self.fillEtudsInfo([etud])
+            # Inval semesters with this student:
+            to_inval = [s["formsemestre_id"] for s in etud["sems"]]
+            if to_inval:
+                self.Notes._inval_cache(
+                    formsemestre_id_list=to_inval
+                )  # > etudident_create_or_edit
+            #
+            return REQUEST.RESPONSE.redirect("ficheEtud?etudid=" + etudid)
+
+    security.declareProtected(ScoEtudInscrit, "etudident_delete")
+
+    def etudident_delete(self, etudid, dialog_confirmed=False, REQUEST=None):
+        "Delete a student"
+        cnx = self.GetDBConnexion()
+        etuds = scolars.etudident_list(cnx, {"etudid": etudid})
+        if not etuds:
+            raise ScoValueError("Etudiant inexistant !")
+        else:
+            etud = etuds[0]
+        self.fillEtudsInfo([etud])
+        if not dialog_confirmed:
+            return self.confirmDialog(
+                """<h2>Confirmer la suppression de l'étudiant <b>%(nomprenom)s</b> ?</h2>
+                </p>
+                <p style="top-margin: 2ex; bottom-margin: 2ex;">Prenez le temps de vérifier que vous devez vraiment supprimer cet étudiant !</p>
+                <p>Cette opération <font color="red"><b>irréversible</b></font> efface toute trace de l'étudiant: inscriptions, <b>notes</b>, absences... dans <b>tous les semestres</b> qu'il a fréquenté.</p>
+                <p>Dans la plupart des cas, vous avez seulement besoin de le <ul>désinscrire</ul> d'un semestre ? (dans ce cas passez par sa fiche, menu associé au semestre)</p>
+
+                <p><a href="ficheEtud?etudid=%(etudid)s">Vérifier la fiche de %(nomprenom)s</a>
+                </p>"""
+                % etud,
+                dest_url="",
+                REQUEST=REQUEST,
+                cancel_url="ficheEtud?etudid=%s" % etudid,
+                OK="Supprimer définitivement cet étudiant",
+                parameters={"etudid": etudid},
+            )
+        log("etudident_delete: etudid=%(etudid)s nomprenom=%(nomprenom)s" % etud)
+        # delete in all tables !
+        tables = [
+            "notes_appreciations",
+            "scolar_autorisation_inscription",
+            "scolar_formsemestre_validation",
+            "scolar_events",
+            "notes_notes_log",
+            "notes_notes",
+            "notes_moduleimpl_inscription",
+            "notes_formsemestre_inscription",
+            "group_membership",
+            "entreprise_contact",
+            "etud_annotations",
+            "scolog",
+            "admissions",
+            "adresse",
+            "absences",
+            "billet_absence",
+            "identite",
+        ]
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        for table in tables:
+            cursor.execute("delete from %s where etudid=%%(etudid)s" % table, etud)
+        cnx.commit()
+        # Inval semestres où il était inscrit:
+        to_inval = [s["formsemestre_id"] for s in etud["sems"]]
+        if to_inval:
+            self.Notes._inval_cache(formsemestre_id_list=to_inval)  # >
+        return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+
+    security.declareProtected(ScoEtudInscrit, "check_group_apogee")
+
+    def check_group_apogee(
+        self, group_id, REQUEST=None, etat=None, fix=False, fixmail=False
+    ):
+        """Verification des codes Apogee et mail de tout un groupe.
+        Si fix == True, change les codes avec Apogée.
+
+        XXX A re-écrire pour API 2: prendre liste dans l'étape et vérifier à partir de cela.
+        """
+        etat = etat or None
+        members, group, group_tit, sem, nbdem = sco_groups.get_group_infos(
+            self, group_id, etat=etat
+        )
+        formsemestre_id = group["formsemestre_id"]
+
+        cnx = self.GetDBConnexion()
+        H = [
+            self.Notes.html_sem_header(
+                REQUEST, "Etudiants du %s" % (group["group_name"] or "semestre"), sem
+            ),
+            '<table class="sortable" id="listegroupe">',
+            "<tr><th>Nom</th><th>Nom usuel</th><th>Prénom</th><th>Mail</th><th>NIP (ScoDoc)</th><th>Apogée</th></tr>",
+        ]
+        nerrs = 0  # nombre d'anomalies détectées
+        nfix = 0  # nb codes changes
+        nmailmissing = 0  # nb etuds sans mail
+        for t in members:
+            nom, nom_usuel, prenom, etudid, email, code_nip = (
+                t["nom"],
+                t["nom_usuel"],
+                t["prenom"],
+                t["etudid"],
+                t["email"],
+                t["code_nip"],
+            )
+            infos = sco_portal_apogee.get_infos_apogee(self, nom, prenom)
+            if not infos:
+                info_apogee = (
+                    '<b>Pas d\'information</b> (<a href="etudident_edit_form?etudid=%s">Modifier identité</a>)'
+                    % etudid
+                )
+                nerrs += 1
+            else:
+                if len(infos) == 1:
+                    nip_apogee = infos[0]["nip"]
+                    if code_nip != nip_apogee:
+                        if fix:
+                            # Update database
+                            scolars.identite_edit(
+                                cnx,
+                                args={"etudid": etudid, "code_nip": nip_apogee},
+                                context=self,
+                            )
+                            info_apogee = (
+                                '<span style="color:green">copié %s</span>' % nip_apogee
+                            )
+                            nfix += 1
+                        else:
+                            info_apogee = (
+                                '<span style="color:red">%s</span>' % nip_apogee
+                            )
+                            nerrs += 1
+                    else:
+                        info_apogee = "ok"
+                else:
+                    info_apogee = (
+                        '<b>%d correspondances</b> (<a href="etudident_edit_form?etudid=%s">Choisir</a>)'
+                        % (len(infos), etudid)
+                    )
+                    nerrs += 1
+            # check mail
+            if email:
+                mailstat = "ok"
+            else:
+                if fixmail and len(infos) == 1 and "mail" in infos[0]:
+                    mail_apogee = infos[0]["mail"]
+                    adrs = scolars.adresse_list(cnx, {"etudid": etudid})
+                    if adrs:
+                        adr = adrs[0]  # modif adr existante
+                        args = {"adresse_id": adr["adresse_id"], "email": mail_apogee}
+                        scolars.adresse_edit(cnx, args=args)
+                    else:
+                        # creation adresse
+                        args = {"etudid": etudid, "email": mail_apogee}
+                        scolars.adresse_create(cnx, args=args)
+                    mailstat = '<span style="color:green">copié</span>'
+                else:
+                    mailstat = "inconnu"
+                    nmailmissing += 1
+            H.append(
+                '<tr><td><a href="ficheEtud?etudid=%s">%s</a></td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'
+                % (etudid, nom, nom_usuel, prenom, mailstat, code_nip, info_apogee)
+            )
+        H.append("</table>")
+        H.append("<ul>")
+        if nfix:
+            H.append("<li><b>%d</b> codes modifiés</li>" % nfix)
+        H.append("<li>Codes NIP: <b>%d</b> anomalies détectées</li>" % nerrs)
+        H.append(
+            "<li>Adresse mail: <b>%d</b> étudiants sans adresse</li>" % nmailmissing
+        )
+        H.append("</ul>")
+        H.append(
+            """
+        <form method="get" action="%s">
+        <input type="hidden" name="formsemestre_id" value="%s"/>
+        <input type="hidden" name="group_id" value="%s"/>
+        <input type="hidden" name="etat" value="%s"/>
+        <input type="hidden" name="fix" value="1"/>
+        <input type="submit" value="Mettre à jour les codes NIP depuis Apogée"/>
+        </form>
+        <p><a href="Notes/formsemestre_status?formsemestre_id=%s"> Retour au semestre</a>
+        """
+            % (
+                REQUEST.URL0,
+                formsemestre_id,
+                strnone(group_id),
+                strnone(etat),
+                formsemestre_id,
+            )
+        )
+        H.append(
+            """
+        <form method="get" action="%s">
+        <input type="hidden" name="formsemestre_id" value="%s"/>
+        <input type="hidden" name="group_id" value="%s"/>
+        <input type="hidden" name="etat" value="%s"/>
+        <input type="hidden" name="fixmail" value="1"/>
+        <input type="submit" value="Renseigner les e-mail manquants (adresse institutionnelle)"/>
+        </form>
+        <p><a href="Notes/formsemestre_status?formsemestre_id=%s"> Retour au semestre</a>
+        """
+            % (
+                REQUEST.URL0,
+                formsemestre_id,
+                strnone(group_id),
+                strnone(etat),
+                formsemestre_id,
+            )
+        )
+
+        return "\n".join(H) + self.sco_footer(REQUEST)
+
+    security.declareProtected(ScoEtudInscrit, "form_students_import_excel")
+
+    def form_students_import_excel(self, REQUEST, formsemestre_id=None):
+        "formulaire import xls"
+        if formsemestre_id:
+            sem = sco_formsemestre.get_formsemestre(self.Notes, formsemestre_id)
+        else:
+            sem = None
+        if sem and sem["etat"] != "1":
+            raise ScoValueError("Modification impossible: semestre verrouille")
+        H = [
+            self.sco_header(REQUEST, page_title="Import etudiants"),
+            """<h2 class="formsemestre">Téléchargement d\'une nouvelle liste d\'etudiants</h2>
+             <div style="color: red">
+             <p>A utiliser pour importer de <b>nouveaux</b> étudiants (typiquement au
+             <b>premier semestre</b>).</p>
+             <p>Si les étudiants à inscrire sont déjà dans un autre
+             semestre, utiliser le menu "<em>Inscriptions (passage des étudiants)
+             depuis d'autres semestres</em> à partir du semestre destination.
+             </p>
+             <p>Si vous avez un portail Apogée, il est en général préférable d'importer les
+             étudiants depuis Apogée, via le menu "<em>Synchroniser avec étape Apogée</em>".
+             </p>
+             </div>
+             <p>
+             L'opération se déroule en deux étapes. Dans un premier temps,
+             vous téléchargez une feuille Excel type. Vous devez remplir
+             cette feuille, une ligne décrivant chaque étudiant. Ensuite,
+             vous indiquez le nom de votre fichier dans la case "Fichier Excel"
+             ci-dessous, et cliquez sur "Télécharger" pour envoyer au serveur
+             votre liste.
+             </p>
+             """,
+        ]  # '
+        if sem:
+            H.append(
+                """<p style="color: red">Les étudiants importés seront inscrits dans
+            le semestre <b>%s</b></p>"""
+                % sem["titremois"]
+            )
+        else:
+            H.append(
+                """
+             <p>Pour inscrire directement les étudiants dans un semestre de
+             formation, il suffit d'indiquer le code de ce semestre
+             (qui doit avoir été créé au préalable). <a class="stdlink" href="%s?showcodes=1">Cliquez ici pour afficher les codes</a>
+             </p>
+             """
+                % (self.ScoURL())
+            )
+
+        H.append("""<ol><li>""")
+        if formsemestre_id:
+            H.append(
+                """
+            <a class="stdlink" href="import_generate_excel_sample?with_codesemestre=0">
+            """
+            )
+        else:
+            H.append("""<a class="stdlink" href="import_generate_excel_sample">""")
+        H.append(
+            """Obtenir la feuille excel à remplir</a></li>
+        <li>"""
+        )
+
+        F = self.sco_footer(REQUEST)
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (
+                (
+                    "csvfile",
+                    {"title": "Fichier Excel:", "input_type": "file", "size": 40},
+                ),
+                (
+                    "check_homonyms",
+                    {
+                        "title": "Vérifier les homonymes",
+                        "input_type": "boolcheckbox",
+                        "explanation": "arrète l'importation si plus de 10% d'homonymes",
+                    },
+                ),
+                (
+                    "require_ine",
+                    {
+                        "title": "Importer INE",
+                        "input_type": "boolcheckbox",
+                        "explanation": "n'importe QUE les étudiants avec nouveau code INE",
+                    },
+                ),
+                ("formsemestre_id", {"input_type": "hidden"}),
+            ),
+            initvalues={"check_homonyms": True, "require_ine": False},
+            submitlabel="Télécharger",
+        )
+        S = [
+            """<hr/><p>Le fichier Excel décrivant les étudiants doit comporter les colonnes suivantes.
+<p>Les colonnes peuvent être placées dans n'importe quel ordre, mais
+le <b>titre</b> exact (tel que ci-dessous) doit être sur la première ligne.
+</p>
+<p>
+Les champs avec un astérisque (*) doivent être présents (nulls non autorisés).
+</p>
+
+
+<p>
+<table>
+<tr><td><b>Attribut</b></td><td><b>Type</b></td><td><b>Description</b></td></tr>"""
+        ]
+        for t in ImportScolars.sco_import_format(
+            with_codesemestre=(formsemestre_id == None)
+        ):
+            if int(t[3]):
+                ast = ""
+            else:
+                ast = "*"
+            S.append(
+                "<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>"
+                % (t[0], t[1], t[4], ast)
+            )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + "</li></ol>" + "\n".join(S) + F
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+        else:
+            return ImportScolars.students_import_excel(
+                self,
+                tf[2]["csvfile"],
+                REQUEST=REQUEST,
+                formsemestre_id=formsemestre_id,
+                check_homonyms=tf[2]["check_homonyms"],
+                require_ine=tf[2]["require_ine"],
+            )
+
+    security.declareProtected(ScoEtudInscrit, "import_generate_excel_sample")
+
+    def import_generate_excel_sample(self, REQUEST, with_codesemestre="1"):
+        "une feuille excel pour importation etudiants"
+        if with_codesemestre:
+            with_codesemestre = int(with_codesemestre)
+        else:
+            with_codesemestre = 0
+        format = ImportScolars.sco_import_format()
+        data = ImportScolars.sco_import_generate_excel_sample(
+            format, with_codesemestre, exclude_cols=["photo_filename"], REQUEST=REQUEST
+        )
+        return sco_excel.sendExcelFile(REQUEST, data, "ImportEtudiants.xls")
+
+    # --- Données admission
+    security.declareProtected(ScoView, "import_generate_admission_sample")
+
+    def import_generate_admission_sample(self, REQUEST, formsemestre_id):
+        "une feuille excel pour importation données admissions"
+        group = sco_groups.get_group(
+            self, sco_groups.get_default_group(self, formsemestre_id)
+        )
+        fmt = ImportScolars.sco_import_format()
+        data = ImportScolars.sco_import_generate_excel_sample(
+            fmt,
+            only_tables=["identite", "admissions", "adresse"],
+            exclude_cols=["nationalite", "foto", "photo_filename"],
+            group_ids=[group["group_id"]],
+            context=self.Notes,
+            REQUEST=REQUEST,
+        )
+        return sco_excel.sendExcelFile(REQUEST, data, "AdmissionEtudiants.xls")
+
+    # --- Données admission depuis fichier excel (version nov 2016)
+    security.declareProtected(ScoView, "form_students_import_infos_admissions")
+
+    def form_students_import_infos_admissions(self, REQUEST, formsemestre_id=None):
+        "formulaire import xls"
+        sem = sco_formsemestre.get_formsemestre(self.Notes, formsemestre_id)
+        authuser = REQUEST.AUTHENTICATED_USER
+        F = self.sco_footer(REQUEST)
+        if not authuser.has_permission(ScoEtudInscrit, self):
+            # autorise juste l'export
+            H = [
+                self.sco_header(
+                    REQUEST,
+                    page_title="Export données admissions (Parcoursup ou autre)",
+                ),
+                """<h2 class="formsemestre">Téléchargement des informations sur l'admission des étudiants</h2>
+             <p>
+             <a href="import_generate_admission_sample?formsemestre_id=%(formsemestre_id)s">Exporter les informations de ScoDoc (classeur Excel)</a> (ce fichier peut être ré-importé après d'éventuelles modifications)
+             </p>
+             <p class="warning">Vous n'avez pas le droit d'importer les données</p>
+             """
+                % {"formsemestre_id": formsemestre_id},
+            ]
+            return "\n".join(H) + F
+
+        # On a le droit d'importer:
+        H = [
+            self.sco_header(REQUEST, page_title="Import données admissions Parcoursup"),
+            """<h2 class="formsemestre">Téléchargement des informations sur l'admission des étudiants depuis feuilles import Parcoursup</h2>
+             <div style="color: red">
+             <p>A utiliser pour renseigner les informations sur l'origine des étudiants (lycées, bac, etc). Ces informations sont facultatives mais souvent utiles pour mieux connaitre les étudiants et aussi pour effectuer des statistiques (résultats suivant le type de bac...). Les données sont affichées sur les fiches individuelles des étudiants.</p>
+             </div>
+             <p>
+             Importer ici la feuille excel utilisée pour envoyer le classement Parcoursup. 
+             Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés, 
+             les autres lignes de la feuille seront ignorées. Et seules les colonnes intéressant ScoDoc 
+             seront importées: il est inutile d'éliminer les autres.
+             <br/>
+              <em>Seules les données "admission" seront modifiées (et pas l'identité de l'étudiant).</em>
+             <br/>
+              <em>Les colonnes "nom" et "prenom" sont requises, ou bien une colonne "etudid".</em>
+             </p>
+             <p>
+             Avant d'importer vos données, il est recommandé d'enregistrer les informations actuelles:
+             <a href="import_generate_admission_sample?formsemestre_id=%(formsemestre_id)s">exporter les données actuelles de ScoDoc</a> (ce fichier peut être ré-importé après d'éventuelles modifications)
+             </p>
+             """
+            % {"formsemestre_id": formsemestre_id},
+        ]  # '
+
+        type_admission_list = (
+            "Autre",
+            "Parcoursup",
+            "Parcoursup PC",
+            "APB",
+            "APB PC",
+            "CEF",
+            "Direct",
+        )
+
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            (
+                (
+                    "csvfile",
+                    {"title": "Fichier Excel:", "input_type": "file", "size": 40},
+                ),
+                (
+                    "type_admission",
+                    {
+                        "title": "Type d'admission",
+                        "explanation": "sera attribué aux étudiants modifiés par cet import n'ayant pas déjà un type",
+                        "input_type": "menu",
+                        "allowed_values": type_admission_list,
+                    },
+                ),
+                ("formsemestre_id", {"input_type": "hidden"}),
+            ),
+            submitlabel="Télécharger",
+        )
+
+        help_text = (
+            """<p>Les colonnes importables par cette fonction sont indiquées dans la table ci-dessous. 
+        Seule la première feuille du classeur sera utilisée.
+        <div id="adm_table_description_format">
+        """
+            + ImportScolars.adm_table_description_format(self).html()
+            + """</div>"""
+        )
+
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + help_text + F
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+        else:
+            return self._students_import_admission(
+                tf[2]["csvfile"],
+                type_admission=tf[2]["type_admission"],
+                REQUEST=REQUEST,
+                formsemestre_id=formsemestre_id,
+            )
+
+    # unpublished
+    def _students_import_admission(
+        self, csvfile, type_admission="", REQUEST=None, formsemestre_id=None
+    ):
+        "import donnees admission from Excel file (v2016)"
+        diag = ImportScolars.scolars_import_admission(
+            csvfile,
+            self.Notes,
+            REQUEST,
+            formsemestre_id=formsemestre_id,
+            type_admission=type_admission,
+        )
+        if REQUEST:
+            H = [self.sco_header(REQUEST, page_title="Import données admissions")]
+            H.append("<p>Import terminé !</p>")
+            H.append(
+                '<p><a class="stdlink" href="%s">Continuer</a></p>'
+                % "formsemestre_status?formsemestre_id=%s"
+                % formsemestre_id
+            )
+            if diag:
+                H.append(
+                    "<p>Diagnostic: <ul><li>%s</li></ul></p>" % "</li><li>".join(diag)
+                )
+
+            return "\n".join(H) + self.sco_footer(REQUEST)
+
+    security.declareProtected(ScoEtudInscrit, "formsemestre_import_etud_admission")
+
+    def formsemestre_import_etud_admission(
+        self, formsemestre_id, import_email=True, REQUEST=None
+    ):
+        """Reimporte donnees admissions par synchro Portail Apogée"""
+        (
+            no_nip,
+            unknowns,
+            changed_mails,
+        ) = sco_synchro_etuds.formsemestre_import_etud_admission(
+            self.Notes, formsemestre_id, import_identite=True, import_email=import_email
+        )
+        H = [
+            self.Notes.html_sem_header(REQUEST, "Reimport données admission"),
+            "<h3>Opération effectuée</h3>",
+        ]
+        if no_nip:
+            H.append("<p>Attention: étudiants sans NIP: " + str(no_nip) + "</p>")
+        if unknowns:
+            H.append(
+                "<p>Attention: étudiants inconnus du portail: codes NIP="
+                + str(unknowns)
+                + "</p>"
+            )
+        if changed_mails:
+            H.append("<h3>Adresses mails modifiées:</h3>")
+            for (info, new_mail) in changed_mails:
+                H.append(
+                    "%s: <tt>%s</tt> devient <tt>%s</tt><br/>"
+                    % (info["nom"], info["email"], new_mail)
+                )
+        return "\n".join(H) + self.sco_footer(REQUEST)
+
+    security.declareProtected(ScoEtudChangeAdr, "photos_import_files_form")
+    photos_import_files_form = sco_trombino.photos_import_files_form
+    security.declareProtected(ScoEtudChangeAdr, "photos_generate_excel_sample")
+    photos_generate_excel_sample = sco_trombino.photos_generate_excel_sample
+
+    # --- Statistiques
+    security.declareProtected(ScoView, "stat_bac")
+
+    def stat_bac(self, formsemestre_id):
+        "Renvoie statistisques sur nb d'etudiants par bac"
+        cnx = self.GetDBConnexion()
+        sem = sco_formsemestre.get_formsemestre(self, formsemestre_id)
+        ins = self.Notes.do_formsemestre_inscription_list(
+            args={"formsemestre_id": formsemestre_id}
+        )
+        Bacs = {}  # type bac : nb etud
+        for i in ins:
+            etud = scolars.etudident_list(cnx, {"etudid": i["etudid"]})[0]
+            typebac = "%(bac)s %(specialite)s" % etud
+            Bacs[typebac] = Bacs.get(typebac, 0) + 1
+        return Bacs
+
+    # --- Dump
+    security.declareProtected(ScoView, "sco_dump_and_send_db")
+    sco_dump_and_send_db = sco_dump_db.sco_dump_and_send_db
+
+    #
+    def confirmDialog(
+        self,
+        message="<p>Confirmer ?</p>",
+        OK="OK",
+        Cancel="Annuler",
+        dest_url="",
+        cancel_url="",
+        target_variable="dialog_confirmed",
+        parameters={},
+        add_headers=True,  # complete page
+        REQUEST=None,  # required
+        helpmsg=None,
+    ):
+        # dialog de confirmation simple
+        parameters[target_variable] = 1
+        # Attention: la page a pu etre servie en GET avec des parametres
+        # si on laisse l'url "action" vide, les parametres restent alors que l'on passe en POST...
+        if not dest_url:
+            dest_url = REQUEST.URL
+        # strip remaining parameters from destination url:
+        dest_url = urllib.splitquery(dest_url)[0]
+        H = [
+            """<form action="%s" method="post">""" % dest_url,
+            message,
+            """<input type="submit" value="%s"/>""" % OK,
+        ]
+        if cancel_url:
+            H.append(
+                """<input type ="button" value="%s"
+                onClick="document.location='%s';"/>"""
+                % (Cancel, cancel_url)
+            )
+        for param in parameters.keys():
+            if parameters[param] is None:
+                parameters[param] = ""
+            if type(parameters[param]) == type([]):
+                for e in parameters[param]:
+                    H.append('<input type="hidden" name="%s" value="%s"/>' % (param, e))
+            else:
+                H.append(
+                    '<input type="hidden" name="%s" value="%s"/>'
+                    % (param, parameters[param])
+                )
+        H.append("</form>")
+        if helpmsg:
+            H.append('<p class="help">' + helpmsg + "</p>")
+        if add_headers and REQUEST:
+            return self.sco_header(REQUEST) + "\n".join(H) + self.sco_footer(REQUEST)
+        else:
+            return "\n".join(H)
+
+    # --------------------------------------------------------------------
+    # Certaines methodes devant etre appeles en dehors de ZNotes:
+    security.declareProtected(ScoView, "formsemestre_edit_uecoefs")
+    formsemestre_edit_uecoefs = sco_formsemestre_edit.formsemestre_edit_uecoefs
+
+
+#
+# Product Administration
+#
+
+
+def manage_addZScolar(
+    self,
+    id="id_ZScolar",
+    title="The Title for ZScolar Object",
+    db_cnx_string="the db connexion string",
+    REQUEST=None,
+):
+    "Add a ZScolar instance to a folder."
+    zscolar = ZScolar(id, title, db_cnx_string=db_cnx_string)
+    self._setObject(id, zscolar)
+
+
+# The form used to get the instance id from the user.
+def manage_addZScolarForm(context, DeptId, REQUEST=None):
+    """Form used to create a new ZScolar instance"""
+
+    if not re.match("^[a-zA-Z0-9_]+$", DeptId):
+        raise ScoValueError("Invalid department id: %s" % DeptId)
+
+    H = [
+        context.standard_html_header(context),
+        "<h2>Ajout d'un département ScoDoc</h2>",
+        """<p>Cette page doit être utilisée pour ajouter un nouveau 
+          département au site.</p>
+
+          <p>Avant d'ajouter le département, il faut <b>impérativement</b> 
+          avoir préparé la base de données en lançant le script 
+          <tt>create_dept.sh nom_du_site</tt> en tant que
+          <em>root</em> sur le serveur.
+          </p>""",
+    ]
+
+    descr = [
+        (
+            "db_cnx_string",
+            {
+                "title": "DB connexion string",
+                "size": 32,
+                "explanation": "laisser vide si BD locale standard",
+            },
+        ),
+        ("pass2", {"input_type": "hidden", "default": "1"}),
+        ("DeptId", {"input_type": "hidden", "default": DeptId}),
+    ]
+
+    tf = TrivialFormulator(
+        REQUEST.URL0, REQUEST.form, descr, submitlabel="Créer le site ScoDoc"
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + context.standard_html_footer(context)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+    else:
+        DeptId = tf[2]["DeptId"].strip()
+        db_cnx_string = tf[2]["db_cnx_string"].strip()
+        # default connexion string
+        if not db_cnx_string:
+            db_name = "SCO" + DeptId.upper()
+            db_user = SCO_DEFAULT_SQL_USER
+            db_cnx_string = "user=%s dbname=%s port=%s" % (
+                db_user,
+                db_name,
+                SCO_DEFAULT_SQL_PORT,
+            )
+        # vérifie que la bd existe et possede le meme nom de dept.
+        try:
+            cnx = psycopg2.connect(db_cnx_string)
+            cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+            cursor.execute("select * from sco_prefs where name='DeptName'")
+        except:
+            return _simple_error_page(
+                context, "Echec de la connexion à la BD (%s)" % db_cnx_string, DeptId
+            )
+        r = cursor.dictfetchall()
+        if not r:
+            return _simple_error_page(
+                context, "Pas de departement défini dans la BD", DeptId
+            )
+        if r[0]["value"] != DeptId:
+            return _simple_error_page(
+                context,
+                "La BD ne correspond pas: nom departement='%s'" % r[0]["value"],
+                DeptId,
+            )
+        # ok, crée instance ScoDoc:
+        manage_addZScolar(
+            context,
+            id="Scolarite",
+            title="ScoDoc for %s" % DeptId,
+            db_cnx_string=db_cnx_string,
+        )
+
+        return REQUEST.RESPONSE.redirect("index_html")
+
+
+def _simple_error_page(context, msg, DeptId=None):
+    """Minimal error page (used by installer only).
+    """
+    H = [context.standard_html_header(context), "<h2>Erreur !</h2>", "<p>", msg, "</p>"]
+    if DeptId:
+        H.append(
+            '<p><a href="delete_dept?DeptId=%s&amp;force=1">Supprimer le dossier %s</a>(très recommandé !)</p>'
+            % (DeptId, DeptId)
+        )
+    H.append(context.standard_html_footer(context))
+    return "\n".join(H)
diff --git a/ZopeProducts/README b/ZopeProducts/README
new file mode 100644
index 0000000000000000000000000000000000000000..7422680302a476dc57d0c742b7b7082aec08ecd3
--- /dev/null
+++ b/ZopeProducts/README
@@ -0,0 +1,5 @@
+
+ Produits Zope2 anciens et adaptes pour ScoDoc
+
+E. Viennet 2013
+
diff --git a/ZopeProducts/ZPsycopgDA/DA.py b/ZopeProducts/ZPsycopgDA/DA.py
new file mode 100644
index 0000000000000000000000000000000000000000..723a68801e2a300f965d72df6c04be01c46dc6a9
--- /dev/null
+++ b/ZopeProducts/ZPsycopgDA/DA.py
@@ -0,0 +1,372 @@
+# ZPsycopgDA/DA.py - ZPsycopgDA Zope product: Database Connection
+#
+# Copyright (C) 2004-2010 Federico Di Gregorio  <fog@debian.org>
+#
+# psycopg2 is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# psycopg2 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 Lesser General Public
+# License for more details.
+
+# Import modules needed by _psycopg to allow tools like py2exe to do
+# their work without bothering about the module dependencies.
+
+
+import sys
+import time
+import db
+import re
+
+import Acquisition
+import Shared.DC.ZRDB.Connection
+
+from db import DB
+from Globals import HTMLFile
+from ExtensionClass import Base
+from App.Dialogs import MessageDialog
+from DateTime import DateTime
+
+# ImageFile is deprecated in Zope >= 2.9
+try:
+    from App.ImageFile import ImageFile
+except ImportError:
+    # Zope < 2.9.  If PIL's installed with a .pth file, we're probably
+    # hosed.
+    from ImageFile import ImageFile
+
+# import psycopg and functions/singletons needed for date/time conversions
+
+import psycopg2
+from psycopg2 import NUMBER, STRING, ROWID, DATETIME
+from psycopg2.extensions import INTEGER, LONGINTEGER, FLOAT, BOOLEAN, DATE
+from psycopg2.extensions import TIME, INTERVAL
+from psycopg2.extensions import new_type, register_type
+
+
+# add a new connection to a folder
+
+manage_addZPsycopgConnectionForm = HTMLFile('dtml/add',globals())
+
+def manage_addZPsycopgConnection(self, id, title, connection_string,
+                                 zdatetime=None, tilevel=2,
+                                 encoding='', check=None, REQUEST=None):
+    """Add a DB connection to a folder."""
+    self._setObject(id, Connection(id, title, connection_string,
+                                   zdatetime, check, tilevel, encoding))
+    if REQUEST is not None: return self.manage_main(self, REQUEST)
+
+
+# the connection object
+
+class Connection(Shared.DC.ZRDB.Connection.Connection):
+    """ZPsycopg Connection."""
+    _isAnSQLConnection = 1
+    
+    id                = 'Psycopg2_database_connection' 
+    database_type     = 'Psycopg2'
+    meta_type = title = 'Z Psycopg 2 Database Connection'
+    icon              = 'misc_/conn'
+
+    def __init__(self, id, title, connection_string,
+                 zdatetime, check=None, tilevel=2, encoding='UTF-8'):
+        self.zdatetime = zdatetime
+        self.id = str(id)
+        self.edit(title, connection_string, zdatetime,
+                  check=check, tilevel=tilevel, encoding=encoding)
+        
+    def factory(self):
+        return DB
+
+    ## connection parameters editing ##
+    
+    def edit(self, title, connection_string,
+             zdatetime, check=None, tilevel=2, encoding='UTF-8'):
+        self.title = title
+        self.connection_string = connection_string
+        self.zdatetime = zdatetime
+        self.tilevel = tilevel
+        self.encoding = encoding
+        
+        if check: self.connect(self.connection_string)
+
+    manage_properties = HTMLFile('dtml/edit', globals())
+
+    def manage_edit(self, title, connection_string,
+                    zdatetime=None, check=None, tilevel=2, encoding='UTF-8',
+                    REQUEST=None):
+        """Edit the DB connection."""
+        self.edit(title, connection_string, zdatetime,
+                  check=check, tilevel=tilevel, encoding=encoding)
+        if REQUEST is not None:
+            msg = "Connection edited."
+            return self.manage_main(self,REQUEST,manage_tabs_message=msg)
+
+    def connect(self, s):
+        try:
+            self._v_database_connection.close()
+        except:
+            pass
+
+        # check psycopg version and raise exception if does not match
+        check_psycopg_version(psycopg2.__version__)
+
+        self._v_connected = ''
+        dbf = self.factory()
+        
+        # TODO: let the psycopg exception propagate, or not?
+        self._v_database_connection = dbf(
+            self.connection_string, self.tilevel, self.get_type_casts(), self.encoding)
+        self._v_database_connection.open()
+        self._v_connected = DateTime()
+
+        return self
+
+    def get_type_casts(self):
+        # note that in both cases order *is* important
+        if self.zdatetime:
+            return ZDATETIME, ZDATE, ZTIME
+        else:
+            return DATETIME, DATE, TIME
+
+    ## browsing and table/column management ##
+
+    manage_options = Shared.DC.ZRDB.Connection.Connection.manage_options
+    # + (
+    #    {'label': 'Browse', 'action':'manage_browse'},)
+
+    #manage_tables = HTMLFile('dtml/tables', globals())
+    #manage_browse = HTMLFile('dtml/browse', globals())
+
+    info = None
+    
+    def table_info(self):
+        return self._v_database_connection.table_info()
+
+
+    def __getitem__(self, name):
+        if name == 'tableNamed':
+            if not hasattr(self, '_v_tables'): self.tpValues()
+            return self._v_tables.__of__(self)
+        raise KeyError, name
+
+    def tpValues(self):
+        res = []
+        conn = self._v_database_connection
+        for d in conn.tables(rdb=0):
+            try:
+                name = d['TABLE_NAME']
+                b = TableBrowser()
+                b.__name__ = name
+                b._d = d
+                b._c = c
+                try:
+                    b.icon = table_icons[d['TABLE_TYPE']]
+                except:
+                    pass
+                r.append(b)
+            except:
+                pass
+        return res
+
+def check_psycopg_version(version):
+    """
+    Check that the psycopg version used is compatible with the zope adpter.
+    """
+    try:
+        m = re.match(r'\d+\.\d+(\.\d+)?', version.split(' ')[0])
+        tver = tuple(map(int, m.group().split('.')))
+    except:
+        raise ImportError("failed to parse psycopg version %s" % version)
+
+    if tver < (2, 4):
+        raise ImportError("psycopg version %s is too old" % version)
+
+    if tver in ((2,4,2), (2,4,3)):
+        raise ImportError("psycopg version %s is known to be buggy" % version)
+
+
+## database connection registration data ##
+
+classes = (Connection,)
+
+meta_types = ({'name':'Z Psycopg 2 Database Connection',
+               'action':'manage_addZPsycopgConnectionForm'},)
+
+folder_methods = {
+    'manage_addZPsycopgConnection': manage_addZPsycopgConnection,
+    'manage_addZPsycopgConnectionForm': manage_addZPsycopgConnectionForm}
+
+__ac_permissions__ = (
+    ('Add Z Psycopg Database Connections',
+     ('manage_addZPsycopgConnectionForm', 'manage_addZPsycopgConnection')),)
+
+# add icons
+
+misc_={'conn': ImageFile('icons/DBAdapterFolder_icon.gif', globals())}
+
+for icon in ('table', 'view', 'stable', 'what', 'field', 'text', 'bin',
+             'int', 'float', 'date', 'time', 'datetime'):
+    misc_[icon] = ImageFile('icons/%s.gif' % icon, globals())
+
+
+## zope-specific psycopg typecasters ##
+
+# convert an ISO timestamp string from postgres to a Zope DateTime object
+def _cast_DateTime(iso, curs):
+    if iso:
+        if iso in ['-infinity', 'infinity']:
+            return iso
+        else:
+            return DateTime(iso)
+
+# convert an ISO date string from postgres to a Zope DateTime object
+def _cast_Date(iso, curs):
+    if iso:
+        if iso in ['-infinity', 'infinity']:
+            return iso
+        else:
+            return DateTime(iso)
+
+# Convert a time string from postgres to a Zope DateTime object.
+# NOTE: we set the day as today before feeding to DateTime so
+# that it has the same DST settings.
+def _cast_Time(iso, curs):
+    if iso:
+        if iso in ['-infinity', 'infinity']:
+            return iso
+        else:
+            return DateTime(time.strftime('%Y-%m-%d %H:%M:%S',
+                                      time.localtime(time.time())[:3]+
+                                      time.strptime(iso[:8], "%H:%M:%S")[3:]))
+
+# NOTE: we don't cast intervals anymore because they are passed
+# untouched to Zope.
+def _cast_Interval(iso, curs):
+    return iso
+
+ZDATETIME = new_type((1184, 1114), "ZDATETIME", _cast_DateTime)
+ZINTERVAL = new_type((1186,), "ZINTERVAL", _cast_Interval)
+ZDATE = new_type((1082,), "ZDATE", _cast_Date)
+ZTIME = new_type((1083,), "ZTIME", _cast_Time)
+
+
+## table browsing helpers ##
+
+class TableBrowserCollection(Acquisition.Implicit):
+    pass
+
+class Browser(Base):
+    def __getattr__(self, name):
+        try:
+            return self._d[name]
+        except KeyError:
+            raise AttributeError, name
+
+class values:
+    def len(self):
+        return 1
+
+    def __getitem__(self, i):
+        try:
+            return self._d[i]
+        except AttributeError:
+            pass
+        self._d = self._f()
+        return self._d[i]
+
+class TableBrowser(Browser, Acquisition.Implicit):
+    icon = 'what'
+    Description = check = ''
+    info = HTMLFile('table_info', globals())
+    menu = HTMLFile('table_menu', globals())
+
+    def tpValues(self):
+        v = values()
+        v._f = self.tpValues_
+        return v
+
+    def tpValues_(self):
+        r=[]
+        tname=self.__name__
+        for d in self._c.columns(tname):
+            b=ColumnBrowser()
+            b._d=d
+            try: b.icon=field_icons[d['Type']]
+            except: pass
+            b.TABLE_NAME=tname
+            r.append(b)
+        return r
+            
+    def tpId(self): return self._d['TABLE_NAME']
+    def tpURL(self): return "Table/%s" % self._d['TABLE_NAME']
+    def Name(self): return self._d['TABLE_NAME']
+    def Type(self): return self._d['TABLE_TYPE']
+
+    manage_designInput=HTMLFile('designInput',globals())
+    def manage_buildInput(self, id, source, default, REQUEST=None):
+        "Create a database method for an input form"
+        args=[]
+        values=[]
+        names=[]
+        columns=self._columns
+        for i in range(len(source)):
+            s=source[i]
+            if s=='Null': continue
+            c=columns[i]
+            d=default[i]
+            t=c['Type']
+            n=c['Name']
+            names.append(n)
+            if s=='Argument':
+                values.append("<dtml-sqlvar %s type=%s>'" %
+                              (n, vartype(t)))
+                a='%s%s' % (n, boboType(t))
+                if d: a="%s=%s" % (a,d)
+                args.append(a)
+            elif s=='Property':
+                values.append("<dtml-sqlvar %s type=%s>'" %
+                              (n, vartype(t)))
+            else:
+                if isStringType(t):
+                    if find(d,"\'") >= 0: d=join(split(d,"\'"),"''")
+                    values.append("'%s'" % d)
+                elif d:
+                    values.append(str(d))
+                else:
+                    raise ValueError, (
+                        'no default was given for <em>%s</em>' % n)
+
+class ColumnBrowser(Browser):
+    icon='field'
+
+    def check(self):
+        return ('\t<input type=checkbox name="%s.%s">' %
+                (self.TABLE_NAME, self._d['Name']))
+    def tpId(self): return self._d['Name']
+    def tpURL(self): return "Column/%s" % self._d['Name']
+    def Description(self):
+        d=self._d
+        if d['Scale']:
+            return " %(Type)s(%(Precision)s,%(Scale)s) %(Nullable)s" % d
+        else:
+            return " %(Type)s(%(Precision)s) %(Nullable)s" % d
+
+table_icons={
+    'TABLE': 'table',
+    'VIEW':'view',
+    'SYSTEM_TABLE': 'stable',
+    }
+
+field_icons={
+    NUMBER.name: 'i',
+    STRING.name: 'text',
+    DATETIME.name: 'date',
+    INTEGER.name: 'int',
+    FLOAT.name: 'float',
+    BOOLEAN.name: 'bin',
+    ROWID.name: 'int'
+    }
diff --git a/ZopeProducts/ZPsycopgDA/__init__.py b/ZopeProducts/ZPsycopgDA/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b501cbfdc5279f16de7a3b49f238dba2e4b57a91
--- /dev/null
+++ b/ZopeProducts/ZPsycopgDA/__init__.py
@@ -0,0 +1,29 @@
+# ZPsycopgDA/__init__.py - ZPsycopgDA Zope product
+#
+# Copyright (C) 2004-2010 Federico Di Gregorio  <fog@debian.org>
+#
+# psycopg2 is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# psycopg2 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 Lesser General Public
+# License for more details.
+
+# Import modules needed by _psycopg to allow tools like py2exe to do
+# their work without bothering about the module dependencies.
+
+__doc__ = "ZPsycopg Database Adapter Registration."
+__version__ = '2.4.6'
+
+import DA
+
+def initialize(context):
+    context.registerClass(
+        DA.Connection,
+        permission = 'Add Z Psycopg 2 Database Connections',
+        constructors = (DA.manage_addZPsycopgConnectionForm,
+                        DA.manage_addZPsycopgConnection),
+        icon = 'icons/DBAdapterFolder_icon.gif')
diff --git a/ZopeProducts/ZPsycopgDA/db.py b/ZopeProducts/ZPsycopgDA/db.py
new file mode 100644
index 0000000000000000000000000000000000000000..a272c02d41cbd386e11d6d3b642c9cad397c8418
--- /dev/null
+++ b/ZopeProducts/ZPsycopgDA/db.py
@@ -0,0 +1,209 @@
+# ZPsycopgDA/db.py - query execution
+#
+# Copyright (C) 2004-2010 Federico Di Gregorio  <fog@debian.org>
+#
+# psycopg2 is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# psycopg2 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 Lesser General Public
+# License for more details.
+
+# Import modules needed by _psycopg to allow tools like py2exe to do
+# their work without bothering about the module dependencies.
+
+from Shared.DC.ZRDB.TM import TM
+from Shared.DC.ZRDB import dbi_db
+
+from ZODB.POSException import ConflictError
+
+import site
+import pool
+
+import psycopg2
+from psycopg2.extensions import INTEGER, LONGINTEGER, FLOAT, BOOLEAN, DATE, TIME
+from psycopg2.extensions import TransactionRollbackError, register_type
+from psycopg2 import NUMBER, STRING, ROWID, DATETIME 
+
+
+# the DB object, managing all the real query work
+
+class DB(TM, dbi_db.DB):
+
+    _p_oid = _p_changed = _registered = None
+
+    def __init__(self, dsn, tilevel, typecasts, enc='utf-8'):
+        self.dsn = dsn
+        self.tilevel = tilevel
+        self.typecasts = typecasts
+        if enc is None or enc == "":
+            self.encoding = "utf-8"
+        else:
+            self.encoding = enc
+        self.failures = 0
+        self.calls = 0
+        self.make_mappings()
+
+    def getconn(self, init=True):
+        # if init is False we are trying to get hold on an already existing
+        # connection, so we avoid to (re)initialize it risking errors.
+        conn = pool.getconn(self.dsn)
+        if init:
+            # use set_session where available as in these versions
+            # set_isolation_level generates an extra query.
+            if psycopg2.__version__ >= '2.4.2':
+                conn.set_session(isolation_level=int(self.tilevel))
+            else:
+                conn.set_isolation_level(int(self.tilevel))
+            conn.set_client_encoding(self.encoding)
+            for tc in self.typecasts:
+                register_type(tc, conn)
+        return conn
+
+    def putconn(self, close=False):
+        try:
+            conn = pool.getconn(self.dsn, False)
+        except AttributeError:
+            pass
+        pool.putconn(self.dsn, conn, close)
+
+    def getcursor(self):
+        conn = self.getconn(False)
+        return conn.cursor()
+
+    def _finish(self, *ignored):
+        try:
+            conn = self.getconn(False)
+            conn.commit()
+            self.putconn()
+        except AttributeError:
+            pass
+            
+    def _abort(self, *ignored):
+        try:
+            conn = self.getconn(False)
+            conn.rollback()
+            self.putconn()
+        except AttributeError:
+            pass
+
+    def open(self):
+        # this will create a new pool for our DSN if not already existing,
+        # then get and immediately release a connection
+        self.getconn()
+        self.putconn()
+        
+    def close(self):
+        # FIXME: if this connection is closed we flush all the pool associated
+        # with the current DSN; does this makes sense?
+        pool.flushpool(self.dsn)
+
+    def sortKey(self):
+        return 1
+
+    def make_mappings(self):
+        """Generate the mappings used later by self.convert_description()."""
+        self.type_mappings = {}
+	for t, s in [(INTEGER,'i'), (LONGINTEGER, 'i'), (NUMBER, 'n'),  
+	             (BOOLEAN,'n'), (ROWID, 'i'),
+	             (DATETIME, 'd'), (DATE, 'd'), (TIME, 'd')]:
+            for v in t.values:
+	        self.type_mappings[v] = (t, s)
+
+    def convert_description(self, desc, use_psycopg_types=False):
+        """Convert DBAPI-2.0 description field to Zope format."""
+        items = []
+        for name, typ, width, ds, p, scale, null_ok in desc:
+	    m = self.type_mappings.get(typ, (STRING, 's'))
+            items.append({
+                'name': name,
+                'type': use_psycopg_types and m[0] or m[1],
+                'width': width,
+                'precision': p,
+                'scale': scale,
+                'null': null_ok,
+                })
+        return items
+
+    ## tables and rows ##
+
+    def tables(self, rdb=0, _care=('TABLE', 'VIEW')):
+        self._register()
+        c = self.getcursor()
+        c.execute(
+            "SELECT t.tablename AS NAME, 'TABLE' AS TYPE "
+            "  FROM pg_tables t WHERE tableowner <> 'postgres' "
+            "UNION SELECT v.viewname AS NAME, 'VIEW' AS TYPE "
+            "  FROM pg_views v WHERE viewowner <> 'postgres' "
+            "UNION SELECT t.tablename AS NAME, 'SYSTEM_TABLE\' AS TYPE "
+            "  FROM pg_tables t WHERE tableowner = 'postgres' "
+            "UNION SELECT v.viewname AS NAME, 'SYSTEM_TABLE' AS TYPE "
+            "FROM pg_views v WHERE viewowner = 'postgres'")
+        res = []
+        for name, typ in c.fetchall():
+            if typ in _care:
+                res.append({'TABLE_NAME': name, 'TABLE_TYPE': typ})
+        self.putconn()
+        return res
+
+    def columns(self, table_name):
+        self._register()
+        c = self.getcursor()
+        try:
+            r = c.execute('SELECT * FROM "%s" WHERE 1=0' % table_name)
+        except:
+            return ()
+        self.putconn()
+        return self.convert_description(c.description, True)
+    
+    ## query execution ##
+
+    def query(self, query_string, max_rows=None, query_data=None):
+        self._register()
+        self.calls = self.calls+1
+
+        desc = ()
+        res = []
+        nselects = 0
+
+        c = self.getcursor()
+
+        try:
+            for qs in [x for x in query_string.split('\0') if x]:
+                try:
+                    if query_data:
+                        c.execute(qs, query_data)
+                    else:
+                        c.execute(qs)
+                except TransactionRollbackError:
+                    # Ha, here we have to look like we are the ZODB raising conflict errrors, raising ZPublisher.Publish.Retry just doesn't work
+                    #logging.debug("Serialization Error, retrying transaction", exc_info=True)
+                    raise ConflictError("TransactionRollbackError from psycopg2")
+                except psycopg2.OperationalError:
+                    #logging.exception("Operational error on connection, closing it.")
+                    try:
+                        # Only close our connection
+                        self.putconn(True)
+                    except:
+                        #logging.debug("Something went wrong when we tried to close the pool", exc_info=True)
+                        pass
+                if c.description is not None:
+                    nselects += 1
+                    if c.description != desc and nselects > 1:
+                        raise psycopg2.ProgrammingError(
+                            'multiple selects in single query not allowed')
+                    if max_rows:
+                        res = c.fetchmany(max_rows)
+                    else:
+                        res = c.fetchall()
+                    desc = c.description
+            self.failures = 0
+
+        except StandardError, err:
+            self._abort()
+            raise err
+        
+        return self.convert_description(desc), res
diff --git a/ZopeProducts/ZPsycopgDA/dtml/add.dtml b/ZopeProducts/ZPsycopgDA/dtml/add.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..330a001b88f2a8def7d5c97ed75fee31195dff2d
--- /dev/null
+++ b/ZopeProducts/ZPsycopgDA/dtml/add.dtml
@@ -0,0 +1,108 @@
+<dtml-var manage_page_header>
+
+<dtml-var "manage_form_title(this(), _,
+           form_title='Add Z Psycopg 2 Database Connection',
+           help_product='ZPsycopgDA',
+           help_topic='ZPsycopgDA-Method-Add.stx'
+           )">
+
+<p class="form-help">
+A Zope Psycopg 2 Database Connection is used to connect and execute
+queries on a PostgreSQL database.
+</p>
+
+<p class="form-help"> 
+In the form below <em>Connection String</em> (also called the Data Source Name
+or DSN for short) is a string... (TODO: finish docs)
+</p>
+
+<form action="manage_addZPsycopgConnection" method="POST">
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Id
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="id" size="40"
+           value="Psycopg2_database_connection" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40"
+        value="Z Psycopg 2 Database Connection"/>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Connection string
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="connection_string" size="40" value="" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Connect immediately
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="checkbox" name="check" value="YES" checked="YES" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Use Zope's internal DateTime
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="checkbox" name="zdatetime" value="YES" checked="YES" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Transaction isolation level
+    </div>
+    </td>
+    <td align="left" valign="top">
+      <select name="tilevel:int">
+        <option value="4">Read uncommitted</option>
+        <option value="1">Read committed</option>
+        <option value="2" selected="YES">Repeatable read</option>
+        <option value="3">Serializable</option>
+      </select>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Encoding
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="encoding" size="40" value="" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top" colspan="2">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" value=" Add " />
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+<dtml-var manage_page_footer>
diff --git a/ZopeProducts/ZPsycopgDA/dtml/browse.dtml b/ZopeProducts/ZPsycopgDA/dtml/browse.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..deffd0abac831f47d0fac0462d0f6a665e7174c0
--- /dev/null
+++ b/ZopeProducts/ZPsycopgDA/dtml/browse.dtml
@@ -0,0 +1,11 @@
+<html>
+  <head><title><dtml-var title_or_id >tables</title></head>
+  <body bgcolor="#FFFFFF" link="#000099" vlink="#555555" alink="#77003B">
+    <dtml-var manage_tabs>
+    <dtml-tree header="info">
+      <IMG SRC="<dtml-var SCRIPT_NAME >/misc_/ZPsycopgDA/<dtml-var icon>"
+       ALT="<dtml-var Type>" BORDER="0">
+      <dtml-var Name><dtml-var Description>
+    </dtml-tree>
+  </body>
+</html>
diff --git a/ZopeProducts/ZPsycopgDA/dtml/edit.dtml b/ZopeProducts/ZPsycopgDA/dtml/edit.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..cffb43bfb4712c8c2e5640090d8b8aff117e7c36
--- /dev/null
+++ b/ZopeProducts/ZPsycopgDA/dtml/edit.dtml
@@ -0,0 +1,84 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<form action="manage_edit" method="POST">
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40"
+        value="&dtml-title;"/>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Connection string
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="connection_string" size="40"
+           value="&dtml-connection_string;" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Use Zope's internal DateTime
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="checkbox" name="zdatetime" value="YES"
+      <dtml-if expr="zdatetime">checked="YES"</dtml-if> />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Transaction isolation level
+    </div>
+    </td>
+    <td align="left" valign="top">
+      <select name="tilevel:int">
+        <option value="4"
+                <dtml-if expr="tilevel==4">selected="YES"</dtml-if>>
+        Read uncommitted</option>
+        <option value="1"
+                <dtml-if expr="tilevel==1">selected="YES"</dtml-if>>
+        Read committed</option>
+        <option value="2"
+                <dtml-if expr="tilevel==2">selected="YES"</dtml-if>>
+        Repeatable read</option>
+        <option value="3"
+                <dtml-if expr="tilevel==3">selected="YES"</dtml-if>>
+        Serializable</option>
+      </select>
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Encoding
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="encoding" size="40"
+           value="&dtml-encoding;" />
+    </td>
+  </tr>
+  <tr>
+    <td align="left" valign="top" colspan="2">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit"
+     value=" Save Changes " />
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+<dtml-var manage_page_footer>
diff --git a/ZopeProducts/ZPsycopgDA/dtml/table_info.dtml b/ZopeProducts/ZPsycopgDA/dtml/table_info.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..639c23fd14c5fcc2bea839bc5db3458cf90e2dcf
--- /dev/null
+++ b/ZopeProducts/ZPsycopgDA/dtml/table_info.dtml
@@ -0,0 +1,7 @@
+<dtml-var standard_html_header>
+
+<dtml-var TABLE_TYPE><dtml-if TABLE_OWNER>
+ owned by <dtml-var TABLE_OWNER></dtml-if>
+<dtml-if REMARKS><br><dtml-var REMARKS></dtml-if>
+
+<dtml-var standard_html_footer>
diff --git a/ZopeProducts/ZPsycopgDA/icons/DBAdapterFolder_icon.gif b/ZopeProducts/ZPsycopgDA/icons/DBAdapterFolder_icon.gif
new file mode 100755
index 0000000000000000000000000000000000000000..ced0ef26a54d7150fe6f0589e561f8854eb2960a
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/DBAdapterFolder_icon.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/bin.gif b/ZopeProducts/ZPsycopgDA/icons/bin.gif
new file mode 100644
index 0000000000000000000000000000000000000000..e4691265687b23c83b3032da963526c0b3a8e526
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/bin.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/date.gif b/ZopeProducts/ZPsycopgDA/icons/date.gif
new file mode 100644
index 0000000000000000000000000000000000000000..0d88a57343f8a727924fd2fc1bad32c4ecb5b24d
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/date.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/datetime.gif b/ZopeProducts/ZPsycopgDA/icons/datetime.gif
new file mode 100644
index 0000000000000000000000000000000000000000..faa540b11f97cfba8c689dcbf1162de4c51c4a63
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/datetime.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/field.gif b/ZopeProducts/ZPsycopgDA/icons/field.gif
new file mode 100644
index 0000000000000000000000000000000000000000..9bf8692be6c734783164f7c8df4a99c7696c6635
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/field.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/float.gif b/ZopeProducts/ZPsycopgDA/icons/float.gif
new file mode 100644
index 0000000000000000000000000000000000000000..dd427299369f706417724103e20b3b5c89480ffd
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/float.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/int.gif b/ZopeProducts/ZPsycopgDA/icons/int.gif
new file mode 100644
index 0000000000000000000000000000000000000000..ef2c5e3690d5b55e816b388c1112b58c9f55aee2
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/int.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/stable.gif b/ZopeProducts/ZPsycopgDA/icons/stable.gif
new file mode 100644
index 0000000000000000000000000000000000000000..acdd37df61c44de3b08fa95c8302e6702a423da5
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/stable.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/table.gif b/ZopeProducts/ZPsycopgDA/icons/table.gif
new file mode 100644
index 0000000000000000000000000000000000000000..cce83beaf96b7d4e6147e7c6a18c975c541df349
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/table.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/text.gif b/ZopeProducts/ZPsycopgDA/icons/text.gif
new file mode 100644
index 0000000000000000000000000000000000000000..a2e5aab6f29769c52dff27f822fc9da76f0be924
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/text.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/time.gif b/ZopeProducts/ZPsycopgDA/icons/time.gif
new file mode 100644
index 0000000000000000000000000000000000000000..6d089150008c5f5e5d35771e238266d2e0f37cc5
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/time.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/view.gif b/ZopeProducts/ZPsycopgDA/icons/view.gif
new file mode 100644
index 0000000000000000000000000000000000000000..71b30de161aa4c99d3f09e19aa7fcf9a7d1e1757
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/view.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/icons/what.gif b/ZopeProducts/ZPsycopgDA/icons/what.gif
new file mode 100644
index 0000000000000000000000000000000000000000..8b5516e397d2684c0ee4628ddd2730964e07bee0
Binary files /dev/null and b/ZopeProducts/ZPsycopgDA/icons/what.gif differ
diff --git a/ZopeProducts/ZPsycopgDA/pool.py b/ZopeProducts/ZPsycopgDA/pool.py
new file mode 100644
index 0000000000000000000000000000000000000000..b47f46ccad9c922b06ac0d47bb0c3a8f1fb9c4f1
--- /dev/null
+++ b/ZopeProducts/ZPsycopgDA/pool.py
@@ -0,0 +1,193 @@
+# ZPsycopgDA/pool.py - ZPsycopgDA Zope product: connection pooling
+#
+# Copyright (C) 2004-2010 Federico Di Gregorio  <fog@debian.org>
+#
+# psycopg2 is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# psycopg2 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 Lesser General Public
+# License for more details.
+
+# Import modules needed by _psycopg to allow tools like py2exe to do
+# their work without bothering about the module dependencies.
+
+# All the connections are held in a pool of pools, directly accessible by the
+# ZPsycopgDA code in db.py.
+
+import threading
+import psycopg2
+from psycopg2.pool import PoolError
+
+
+class AbstractConnectionPool(object):
+    """Generic key-based pooling code."""
+
+    def __init__(self, minconn, maxconn, *args, **kwargs):
+        """Initialize the connection pool.
+
+        New 'minconn' connections are created immediately calling 'connfunc'
+        with given parameters. The connection pool will support a maximum of
+        about 'maxconn' connections.
+        """
+        self.minconn = minconn
+        self.maxconn = maxconn
+        self.closed = False
+
+        self._args = args
+        self._kwargs = kwargs
+
+        self._pool = []
+        self._used = {}
+        self._rused = {} # id(conn) -> key map
+        self._keys = 0
+
+        for i in range(self.minconn):
+            self._connect()
+
+    def _connect(self, key=None):
+        """Create a new connection and assign it to 'key' if not None."""
+        conn = psycopg2.connect(*self._args, **self._kwargs)
+        if key is not None:
+            self._used[key] = conn
+            self._rused[id(conn)] = key
+        else:
+            self._pool.append(conn)
+        return conn
+
+    def _getkey(self):
+        """Return a new unique key."""
+        self._keys += 1
+        return self._keys
+
+    def _getconn(self, key=None):
+        """Get a free connection and assign it to 'key' if not None."""
+        if self.closed: raise PoolError("connection pool is closed")
+        if key is None: key = self._getkey()
+
+        if key in self._used:
+            return self._used[key]
+
+        if self._pool:
+            self._used[key] = conn = self._pool.pop()
+            self._rused[id(conn)] = key
+            return conn
+        else:
+            if len(self._used) == self.maxconn:
+                raise PoolError("connection pool exausted")
+            return self._connect(key)
+
+    def _putconn(self, conn, key=None, close=False):
+        """Put away a connection."""
+        if self.closed: raise PoolError("connection pool is closed")
+        if key is None: key = self._rused[id(conn)]
+
+        if not key:
+            raise PoolError("trying to put unkeyed connection")
+
+        if len(self._pool) < self.minconn and not close:
+            self._pool.append(conn)
+        else:
+            conn.close()
+
+        # here we check for the presence of key because it can happen that a
+        # thread tries to put back a connection after a call to close
+        if not self.closed or key in self._used:
+            del self._used[key]
+            del self._rused[id(conn)]
+
+    def _closeall(self):
+        """Close all connections.
+
+        Note that this can lead to some code fail badly when trying to use
+        an already closed connection. If you call .closeall() make sure
+        your code can deal with it.
+        """
+        if self.closed: raise PoolError("connection pool is closed")
+        for conn in self._pool + list(self._used.values()):
+            try:
+                conn.close()
+            except:
+                pass
+        self.closed = True
+
+
+class PersistentConnectionPool(AbstractConnectionPool):
+    """A pool that assigns persistent connections to different threads.
+
+    Note that this connection pool generates by itself the required keys
+    using the current thread id.  This means that until a thread puts away
+    a connection it will always get the same connection object by successive
+    `!getconn()` calls. This also means that a thread can't use more than one
+    single connection from the pool.
+    """
+
+    def __init__(self, minconn, maxconn, *args, **kwargs):
+        """Initialize the threading lock."""
+        import threading
+        AbstractConnectionPool.__init__(
+            self, minconn, maxconn, *args, **kwargs)
+        self._lock = threading.Lock()
+
+        # we we'll need the thread module, to determine thread ids, so we
+        # import it here and copy it in an instance variable
+        import thread
+        self.__thread = thread
+
+    def getconn(self):
+        """Generate thread id and return a connection."""
+        key = self.__thread.get_ident()
+        self._lock.acquire()
+        try:
+            return self._getconn(key)
+        finally:
+            self._lock.release()
+
+    def putconn(self, conn=None, close=False):
+        """Put away an unused connection."""
+        key = self.__thread.get_ident()
+        self._lock.acquire()
+        try:
+            if not conn: conn = self._used[key]
+            self._putconn(conn, key, close)
+        finally:
+            self._lock.release()
+
+    def closeall(self):
+        """Close all connections (even the one currently in use.)"""
+        self._lock.acquire()
+        try:
+            self._closeall()
+        finally:
+            self._lock.release()
+
+
+_connections_pool = {}
+_connections_lock = threading.Lock()
+
+def getpool(dsn, create=True):
+    _connections_lock.acquire()
+    try:
+        if not _connections_pool.has_key(dsn) and create:
+            _connections_pool[dsn] = \
+                PersistentConnectionPool(4, 200, dsn)
+    finally:
+        _connections_lock.release()
+    return _connections_pool[dsn]
+
+def flushpool(dsn):
+    _connections_lock.acquire()
+    try:
+        _connections_pool[dsn].closeall()
+        del _connections_pool[dsn]
+    finally:
+        _connections_lock.release()
+
+def getconn(dsn, create=True):
+    return getpool(dsn, create=create).getconn()
+
+def putconn(dsn, conn, close=False):
+    getpool(dsn).putconn(conn, close=close)
diff --git a/ZopeProducts/exUserFolder/AuthSources/__init__.py b/ZopeProducts/exUserFolder/AuthSources/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..dedaed0a952d41d3a32abea8cfde685a658bf713
--- /dev/null
+++ b/ZopeProducts/exUserFolder/AuthSources/__init__.py
@@ -0,0 +1,47 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: __init__.py,v 1.1 2004/11/10 14:15:34 akm Exp $
+
+#import etcAuthSource
+#import httpsAuthSource
+#import mysqlAuthSource
+import pgAuthSource
+#import pgAuthSourceAlt
+#import radiusAuthSource
+#import smbAuthSource
+#import usAuthSource
+#import zodbAuthSource
+#import zodbBTreeAuthSource
+
+#
+# These have special requirements for external libraries
+# that my not be present.
+#
+
+# try:
+# 	import nisAuthSource
+# except:
+# 	pass
+
+# try:
+# 	import LDAPAuthSource
+# except:
+# 	pass
diff --git a/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/.cvsignore b/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/__init__.py b/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a23a103c6710ae37ffae523fb3aed33d9989af16
--- /dev/null
+++ b/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/__init__.py
@@ -0,0 +1,2 @@
+# $Id: __init__.py,v 1.1 2004/11/10 14:15:36 akm Exp $
+import pgAuthSource
diff --git a/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/manage_addpgAuthSourceForm.dtml b/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/manage_addpgAuthSourceForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..ff0790c3827d9504b451d3270a2d85a273cfb54a
--- /dev/null
+++ b/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/manage_addpgAuthSourceForm.dtml
@@ -0,0 +1,40 @@
+<dtml-var "DialogHeader(_.None,_,DialogTitle='Add Postgresql Authentication Source')">
+<FORM ACTION="&dtml-URL;" METHOD="POST">
+<dtml-in "REQUEST.form.keys()">
+<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
+</dtml-in>
+<input type="HIDDEN" name="doProp" value="1">
+<TABLE CELLSPACING="2">
+<tr><th><dtml-babel src="'en'">Database Connection</dtml-babel>:</th>
+    <td>
+        <select name="pgauth_connection">
+        <dtml-in "SQLConnectionIDs()">
+            <option value="<dtml-var sequence-item>">
+            <dtml-var sequence-key></option>
+        </dtml-in>
+        </select>
+     </td>
+</tr>
+<tr>
+	<th><dtml-babel src="'en'">Table Name</dtml-babel>:</th>
+	<td><input type="text" name="pgauth_table" value="passwd"></td>
+</tr>
+<tr>
+	<th><dtml-babel src="'en'">Username Column</dtml-babel>:</th>
+	<td><input type="text" name="pgauth_usernameColumn" value="username"></td>
+</tr>
+<tr>
+	<th><dtml-babel src="'en'">Password Column</dtml-babel>:</th>
+	<td><input type="text" name="pgauth_passwordColumn" value="password"></td>
+</tr>
+<tr>
+	<th><dtml-babel src="'en'">Roles Column</dtml-babel>:</th>
+	<td><input type="text" name="pgauth_rolesColumn" value="roles"></td>
+</tr>
+<TR>
+<TD></TD>
+<TD><BR><INPUT TYPE="SUBMIT" VALUE="<dtml-babel src="'en'">Add</dtml-babel>"></TD>
+</TR>
+</TABLE>
+</FORM>
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/manage_editpgAuthSourceForm.dtml b/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/manage_editpgAuthSourceForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..7cd811cf4917fb137702119623c53aff41833515
--- /dev/null
+++ b/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/manage_editpgAuthSourceForm.dtml
@@ -0,0 +1,37 @@
+<dtml-var "DialogHeader(_.None,_,DialogTitle='Postgresql Authentication Source',dialog_width='100%')">
+<dtml-var manage_tabs>
+<FORM ACTION="manage_editAuthSource" METHOD="POST">
+<TABLE CELLSPACING="2">
+<tr><th><dtml-babel src="'en'">Database Connection</dtml-babel>:</th>
+    <td>
+        <select name="pgauth_connection">
+        <dtml-in "SQLConnectionIDs()">
+			<option value="<dtml-var sequence-item>"<dtml-if "currentAuthSource.connection==_['sequence-item']"> SELECTED</dtml-if>>
+            <dtml-var sequence-key></option>
+        </dtml-in>
+        </select>
+     </td>
+</tr>
+<tr>
+	<th><dtml-babel src="'en'">Table Name</dtml-babel>:</th>
+	<td><input type="text" name="pgauth_table" value="<dtml-var "currentAuthSource.table">"></td>
+</tr>
+<tr>
+	<th><dtml-babel src="'en'">Username Column</dtml-babel>:</th>
+	<td><input type="text" name="pgauth_usernameColumn" value="<dtml-var "currentAuthSource.usernameColumn">"></td>
+</tr>
+<tr>
+	<th><dtml-babel src="'en'">Password Column</dtml-babel>:</th>
+	<td><input type="text" name="pgauth_passwordColumn" value="<dtml-var "currentAuthSource.passwordColumn">"></td>
+</tr>
+<tr>
+	<th><dtml-babel src="'en'">Roles Column</dtml-babel>:</th>
+	<td><input type="text" name="pgauth_rolesColumn" value="<dtml-var "currentAuthSource.rolesColumn">"></td>
+</tr>
+<TR>
+<TD></TD>
+<TD><BR><INPUT TYPE="SUBMIT" VALUE=" <dtml-babel src="'en'">Edit</dtml-babel> "></TD>
+</TR>
+</TABLE>
+</FORM>
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/pgAuthSource.py b/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/pgAuthSource.py
new file mode 100644
index 0000000000000000000000000000000000000000..bffe3b7132aae0aaadda4b8e8e16ab9d8490a418
--- /dev/null
+++ b/ZopeProducts/exUserFolder/AuthSources/pgAuthSource/pgAuthSource.py
@@ -0,0 +1,333 @@
+#
+# Extensible User Folder
+# 
+# Postgres Authentication Source for exUserFolder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: pgAuthSource.py,v 1.1 2004/11/10 14:15:36 akm Exp $
+
+#
+# This class only authenticates users, it stores no properties.
+#
+
+import string,Acquisition
+
+from Globals import HTMLFile, MessageDialog, INSTANCE_HOME
+
+from OFS.Folder import Folder
+
+from Products.ZSQLMethods.SQL import SQL
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import PluginRegister
+
+try:
+	from crypt import crypt
+except:
+	from Products.exUserFolder.fcrypt.fcrypt import crypt
+
+# debug XXX
+# def xLOG(msg):
+#     f = open('/tmp/debug.log','a')
+#     f.write(msg+'\n')
+#     f.close()
+    
+def manage_addpgAuthSource(self, REQUEST):
+	""" Add a Postgres Auth Source """
+
+	connection=REQUEST['pgauth_connection']
+	table=REQUEST['pgauth_table']
+	usernameColumn=REQUEST['pgauth_usernameColumn']
+	passwordColumn=REQUEST['pgauth_passwordColumn']
+	rolesColumn=REQUEST['pgauth_rolesColumn']
+	o = pgAuthSource(connection, table, usernameColumn, passwordColumn,
+					 rolesColumn)
+	self._setObject('pgAuthSource', o, None, None, 0)
+	o=getattr(self,'pgAuthSource')
+	if hasattr(o, 'postInitialisation'):
+		o.postInitialisation(REQUEST)
+	
+	self.currentAuthSource=o
+	return ''
+
+manage_addpgAuthSourceForm=HTMLFile('manage_addpgAuthSourceForm', globals())
+manage_editpgAuthSourceForm=HTMLFile('manage_editpgAuthSourceForm', globals())
+
+class pgAuthSource(Folder):
+	""" Authenticate Users against a Postgres Database """
+
+	meta_type='Authentication Source'
+	title='Postgresql Authentication'
+
+	icon ='misc_/exUserFolder/exUserFolderPlugin.gif'
+
+	manage_tabs=Acquisition.Acquired
+
+	manage_editForm=manage_editpgAuthSourceForm
+
+	#
+	# You can define this to go off and do the authentication instead of
+	# using the basic one inside the User Object
+	#
+	remoteAuthMethod=None
+	
+	def __init__(self, connection, table, usernameColumn, passwordColumn,
+				 rolesColumn):
+		self.id='pgAuthSource'
+		self.connection=connection
+		self.table=table
+		self.usernameColumn=usernameColumn
+		self.passwordColumn=passwordColumn
+		self.rolesColumn=rolesColumn
+		self.addSQLQueries()
+
+	def manage_editAuthSource(self, REQUEST):
+		""" Edit a Postgres Auth Source """
+
+		self.connection=REQUEST['pgauth_connection']
+		self.table=REQUEST['pgauth_table']
+		self.usernameColumn=REQUEST['pgauth_usernameColumn']
+		self.passwordColumn=REQUEST['pgauth_passwordColumn']
+		self.rolesColumn=REQUEST['pgauth_rolesColumn']
+		self.delSQLQueries()
+		self.addSQLQueries() # Re-add queries with new parameters
+
+	def createUser(self, username, password, roles):
+		""" Add A Username """
+
+		if type(roles) != type([]):
+			if roles:
+				roles=list(roles)
+			else:
+				roles=[]
+
+		rolestring=''
+		for role in roles:
+			rolestring=rolestring+role+','
+
+		rolestring=rolestring[:-1]
+		secret=self.cryptPassword(username, password)
+		self.sqlInsertUser(username=username,
+						   password=secret,
+						   roles=rolestring)
+		self._v_lastUser={}
+
+	def updateUser(self, username, password, roles):
+		if type(roles) != type([]):
+			if roles:
+				roles=list(roles)
+			else:
+				roles=[]
+		
+		rolestring=''
+		for role in roles:
+			print role
+			rolestring=rolestring+role+','
+
+		rolestring=rolestring[:-1]
+
+		# Don't change passwords if it's null
+		if password:
+			secret=self.cryptPassword(username, password)
+			self.sqlUpdateUserPassword(username=username,
+									   password=secret)
+			
+		self.sqlUpdateUser(username=username,
+						   roles=rolestring)
+		self._v_lastUser={}
+		
+	def delSQLQueries(self):
+		sqllist=self.objectIds('Z SQL Method')
+		self.manage_delObjects(ids=sqllist)
+
+	def addSQLQueries(self):
+		sqlListUsers=SQL(
+			'sqlListUsers',
+			'List All Users',
+			self.connection,
+			'table=%s'%(self.table),
+			_sqlListUsers)
+
+		self._setObject('sqlListUsers', sqlListUsers)
+
+		sqlListOneUser=SQL(
+			'sqlListOneUser',
+			'List ONE User',
+			self.connection,
+			'table=%s usernameColumn=%s username:string'%(
+			self.table, self.usernameColumn),
+			_sqlListOneUser)
+
+		self._setObject('sqlListOneUser', sqlListOneUser)
+
+		sqlDeleteOneUser=SQL(
+			'sqlDeleteOneUser',
+			'Delete One User',
+			self.connection,
+			'table=%s usernameColumn=%s username:string'%(
+			self.table,self.usernameColumn),
+			_sqlDeleteOneUser)
+
+		self._setObject('sqlDeleteOneUser', sqlDeleteOneUser)
+
+		sqlInsertUser=SQL(
+			'sqlInsertUser',
+			'Insert One User',
+			self.connection,
+			'table=%s usernameColumn=%s passwordColumn=%s rolesColumn=%s username:string password:string roles:string'%(
+			self.table, self.usernameColumn, self.passwordColumn, self.rolesColumn),
+			_sqlInsertUser)
+
+		self._setObject('sqlInsertUser', sqlInsertUser)
+
+		sqlUpdateUser=SQL(
+			'sqlUpdateUser',
+			'Update User',
+			self.connection,
+			'table=%s rolesColumn=%s username:string roles:string'%(self.table, self.rolesColumn),
+			_sqlUpdateUser)
+
+		self._setObject('sqlUpdateUser', sqlUpdateUser)
+
+		sqlUpdateUserPassword=SQL(
+			'sqlUpdateUserPassword',
+			'Update just the password',
+			self.connection,
+			'table=%s usernameColumn=%s passwordColumn=%s username:string password:string'%(self.table, self.usernameColumn, self.passwordColumn),
+			_sqlUpdateUserPassword)
+
+		self._setObject('sqlUpdateUserPassword', sqlUpdateUserPassword)
+
+	def cryptPassword_old(self, username, password):
+			salt =username[:2]
+			secret = crypt(password, salt)
+			return secret
+
+	def deleteUsers(self, userids):
+		for uid in userids:
+			self.sqlDeleteOneUser(username=uid)
+		self._v_lastUser={}			
+
+	def listUserNames(self):
+		"""Returns a real list of user names """
+		users = []
+		result=self.sqlListUsers()
+		for n in result:
+			username=sqlattr(n,self.usernameColumn)
+			users.append(username)
+		return users
+		
+	def listUsers(self):
+		"""Returns a list of user names or [] if no users exist"""		
+		users = []
+		result=self.sqlListUsers()
+		for n in result:
+			roles=[]
+			username=sqlattr(n,self.usernameColumn)
+			if sqlattr(n, self.rolesColumn):
+				roles=string.split(sqlattr(n,self.rolesColumn),',')
+			password=sqlattr(n, self.passwordColumn)
+			N={'username':username, 'password':password, 'roles':roles}
+			users.append(N)
+		return users
+
+	def listOneUser(self,username):
+		#xLOG('pg.listOneUser(%s)' % username)
+		if getattr(self, '_v_lastUser', {}):
+			if self._v_lastUser['username']==username:
+				return self._v_lastUser['users']
+		#xLOG('pg.listOneUser continuing')
+		users = []
+		result=self.sqlListOneUser(username=username)
+		#xLOG('pg.listOneUser result=%s' % result)
+		for n in result:
+			roles=[]
+			username=sqlattr(n,self.usernameColumn)
+			password=sqlattr(n,self.passwordColumn)
+			if sqlattr(n, self.rolesColumn):
+				roles=string.split(sqlattr(n,self.rolesColumn),',')  #Andreas
+			N={'username':username, 'password':password, 'roles':roles}
+			users.append(N)
+		self._v_lastUser={}
+		self._v_lastUser['username']=username
+		self._v_lastUser['users']=users			
+		return users
+	
+	def postInitialisation(self, REQUEST):
+		self._v_lastUser={}
+
+pgAuthReg=PluginRegister('pgAuthSource', 'Postgresql Authentication Source',
+						 pgAuthSource, manage_addpgAuthSourceForm,
+						 manage_addpgAuthSource,
+						 manage_editpgAuthSourceForm)
+exUserFolder.authSources['pgAuthSource']=pgAuthReg
+
+from string import upper, lower
+import Missing
+mt=type(Missing.Value)
+
+def typeconv(val):
+    if type(val)==mt:
+        return ''
+    return val
+
+def sqlattr(ob, attr):
+    name=attr
+    if hasattr(ob, attr):
+        return typeconv(getattr(ob, attr))
+    attr=upper(attr)
+    if hasattr(ob, attr):
+        return typeconv(getattr(ob, attr))
+    attr=lower(attr)
+    if hasattr(ob, attr):
+        return typeconv(getattr(ob, attr))
+    raise NameError, name
+
+
+
+_sqlListUsers="""
+SELECT * FROM <dtml-var table>
+"""
+
+_sqlListOneUser="""
+SELECT * FROM <dtml-var table>
+where <dtml-var usernameColumn>=<dtml-sqlvar username type=string>
+"""
+
+_sqlDeleteOneUser="""
+DELETE FROM <dtml-var table>
+where <dtml-var usernameColumn>=<dtml-sqlvar username type=string>
+"""
+
+_sqlInsertUser="""
+INSERT INTO <dtml-var table> (<dtml-var usernameColumn>, <dtml-var passwordColumn>, <dtml-var rolesColumn>)
+VALUES (<dtml-sqlvar username type=string>,
+        <dtml-sqlvar password type=string>,
+		<dtml-sqlvar roles type=string>)
+"""
+
+_sqlUpdateUserPassword="""
+UPDATE <dtml-var table> set <dtml-var passwordColumn>=<dtml-sqlvar password type=string>
+WHERE <dtml-var usernameColumn>=<dtml-sqlvar username type=string>
+"""
+
+_sqlUpdateUser="""
+UPDATE <dtml-var table> set <dtml-var rolesColumn>=<dtml-sqlvar roles type=string>
+WHERE <dtml-var usernameColumn>=<dtml-sqlvar username type=string>
+"""
diff --git a/ZopeProducts/exUserFolder/CHANGES.txt b/ZopeProducts/exUserFolder/CHANGES.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6271ba5e350e527c450d5fccb69544b584edef3b
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CHANGES.txt
@@ -0,0 +1,865 @@
+Changes for 0.50.1
+
+Add a README.Upgrading file to explain the impact of the 0.50.0 source
+restructure, since people don't seem to be reading this file. --akm
+
+Fix the default docLogin to use &dtml-URL as the default destination.
+I porked the fcrypt import. It obviously doesn't get imported here since
+I have a crypt module installed. -- akm
+
+Fixed; https://sourceforge.net/tracker/?func=detail&aid=1084903&group_id=36318&atid=416446
+thanks to vigine -- akm
+
+Changes for 0.50.0
+
+Restructured Source Tree. This will make this version incompatible with
+previous versions, as the classes have moved. This breaks upgrading existing
+installs unless you keep the old classes around. If you only use external
+Auth/Prop/Group sources, you will probably be unaffected.
+
+o Auth Sources moved to single directory
+o Prop Sources moved to single directory
+o Group Sources moved to single directory
+o Docs moved to doc directory 
+--akm
+
+Added Pluggable Crypto methods. Any authSource that contains a
+cryptPassword method, will have it's method called, otherwise the
+method selected by the user is called. --akm
+
+Removed the cryptPassword method from existing Auth Sources. --akm
+
+docLoginRedirect is no longer used. --akm
+
+Changes for 0.20.2
+BLAH! I missed some LDAP changes! --akm
+
+Changes for 0.20.1
+
+Fix import problem for pgPropSource --akm
+Add performance boost to pgAuthSource and pgPropSource --akm
+Make zodbAuthSource.listUsernames return a list. --akm
+Update some LDAP Auth source bugs. --akm
+Change references to "Authorisation" to "Authentication" since XUF 
+auth sources authenticate, they don't authorise. --akm
+Changed the <h3> tags to <b> tags in the manage_adds.
+
+Changes for 0.20.0
+
+Fix:
+https://sourceforge.net/tracker/index.php?func=detail&aid=547327&group_id=36318&atid=416446
+https://sourceforge.net/tracker/index.php?func=detail&aid=616485&group_id=36318&atid=416448
+https://sourceforge.net/tracker/index.php?func=detail&aid=594081&group_id=36318&atid=416448
+https://sourceforge.net/tracker/index.php?func=detail&aid=594526&group_id=36318&atid=416448
+
+Added LDAPAuthSource, based on the auth_ldap module for Apache
+(http://www.rudedog.org/auth_ldap/) and the NDS Auth Source of
+Phil Harris (AKA ftmpsh). This is only lightly tested, I don't have
+the LDAP resources here to test all the features. Binding using uid/
+cn and using various filters works (if the userPassword item is
+present). This needs more testing by people with better LDAP setups
+that I do. --akm
+
+Padded docLoginRedirect to prevent IE from displaying "Friendly" error
+messages when -D flag not present when running Zope --akm.
+
+Update UZG to contain entry for LDAPAuthSource. Reformat text 
+slightly. --akm
+
+Propogate "unable to auth" here requests up. This means the Manager
+doesn't get locked out in cookie mode after adding an XUF instance.
+It also means that people using a non-existant username at this level
+get thrown up a level higher. This might not be what people want to
+happen. --akm
+
+Added method makeRedirectPath which is called from docLoginRedirect.
+This makes the destination include any querystring that was present
+when needing to redirect. -- akm.
+
+Removed some Class globals from exUseFolder.py. These are now set 
+in __set_state__ if not present in the class so that upgrading users
+don't get a crash (hopefully). -- akm.
+
+pgPropSource was losing track of properties under heavy load. 
+Only noticable if you were setting and deleting a lot of temporary
+properties. There is a global property timeout for pgPropSource. --akm
+
+Jason Gibson <jason.gibson@sbcglobal.net> provided a nisAuthSource,
+I've added it here --akm.
+
+Refactored validate method to behave a lot more like BasicUserFolder.
+Among other things, this fixes the issue where a local role could not
+be granted to a user and granted permissions on the same object.  --mb
+
+Add NuxUserGroups support (previously on NuxUserGroups_support_branch)
+and group sources.  --bmh, mb
+
+Now passes authFailedCode to Membership Login Page, The Default Login
+Page as defined in the README.Membership will correctly display reason
+for login being required --cab
+
+Fixed Edit management pages for user-supplied auth and property
+sources --bmh
+
+Removed overriding of __len__ to return the number of users.  This was
+causing performance problems during authentication.  See
+http://sourceforge.net/mailarchive/message.php?msg_id=2230743 for
+details.  WARNING: this means using len(acl_users) to get the number
+of users will no longer work!  If you were using this trick, please
+use len(acl_users.listUsers()) instead.  --bmh 
+
+Make title property editable --bmh
+
+Make Group Sources changeable dynamically after the acl_users folder has
+been created --bmh
+
+Inital import of https Auth source.  Also, added a listUsers method
+to the zodbBTreeProps source to support listUsers. -- jsb <jonah at cloud9.net>
+
+Changes for 0.10.10
+
+Added mysql Auth and mysql Prop source and mysql.sql schema. Just a
+copy of the appropriate pg source with sql that works with myqsl -cab
+
+Fixed negative user cache lookup in std_validade so that it actually
+works for users being authenticated thru basic auth, especially if
+they're authenticating in outer user folders -- rochael
+
+Made smbAuthSource catch NetBIOSTimeout errors during authentication -- rochael
+
+Fixed dtml/mainUser.dtml to be virtualhost-sensitive when displaying user
+icons -- rochael
+
+Updated UZG per user request. Fixed numbering, added information about
+addition parameters like Negative Caching.
+
+Changes for 0.10.9
+
+Made dummyZBabelTag compatible to replace the NoBabel in OrderedFolder
+while keeping its functionality in XUF -- cab
+
+Changed _doAddUser, _doChangeUser to work with the public interface for
+userfolders introduced in Zope2.5. Optional keyword arguments can now
+be passed to _doAddUser and _doChangeUser.
+
+PropertySource: Please note that createUser and updateUser, when called
+from _doAddUser and _doChangeUser, will no longer be passed a REQUEST,
+but a mapping with items from REQUEST updated with those from the
+optional keyword arguments.  -- pj
+
+Fixed the problem with upgrading from 0.10.7 and below that didn't
+account for existing XUF's not having a MessageDialog in their
+contents. Now unless specificy replace it will use the MessageDialog
+provided. Added how to do that to FAQ and README.Membership --cab
+
+Made docLoginRedirect provide an absolute URL --bmh
+
+MessageDialog in common no longer uses mangage_page_header and 
+mangage_page_footer v--cab
+
+Changes for 0.10.8
+
+Added the ability for members to change properties, and a default page
+in the README.Membership to show how to do it --cab
+
+MessageDialog is now an object in the ZODB that can be changed to fit
+the site --cab
+
+Now with 100% guaranteed race-condition-free UserCache goodness!  Those
+subclassing XUFUser, you will have to change your code.  See User.py
+for details.  --mb
+
+zodbBTreePropSource was returning None instead of the requested
+default value, when called with (e.g.) someuser.getProperty('shoesize',13).
+(Other property sources didn't have that bug.)
+--davidc@debian.org
+
+The tutorial loginform was wrong for Membership in README.Membership
+
+Seems delProperty has never worked.. fixed --akm
+Seems delProperty for pgPropSource has never worked.. fixed --akm
+
+Fixed Basic Auth not auth problem. --akm
+Fixed Basic Auth not cache problem. --akm
+Fixed Cached Users bypassing some auth checks. --akm
+
+Added usPropSource, which allows users to supply property methods TTW.
+--bmh
+
+Changes for 0.10.7
+
+PropertyEditor had a typo in dtml and was casting int to None. --zxc
+
+BasicAuth is now broken the other way, it'll allow any user to validate
+with any password. --akm
+
+Negative cache checking move was bogus. --akm
+
+redirectToLogin didn't have a security declaration so 2.5.0 refused to
+work in cookie mode *sigh* --akm
+
+Fixed the 'None' object has no attribute 'load' setstate errors that
+could crop up on propSources, and preemptively took care of the
+authSources as well.  Also fixed some of the weirder bugs relating to
+user object acquisition context. --mb
+
+Bug fixes from sf applied. --akm
+
+Changes for 0.10.6
+
+dummyZBabelTag used the python 2 re, which broke installations using
+python 1.5 which still used the now deprecated regex, changed it to
+catch the exception and use regex instead for python 1.5, else still
+use re --cab
+
+The redirectToLogin without Membership had a little logic problem where it
+would basically garantee the existence of a query string, with at least a
+lonely question mark even when there was no query string in the original
+URL --rochael
+
+smbAuthSource needed to cast NULL role properties to an empty list --akm
+
+smbAuthSource had some dodgey zLOGing in it. --akm
+
+smbAuthSource had some methods that should return [] instead of None. --akm
+
+s/postgres/RADIUS/ in the radiusAuthSource DTML --akm
+
+cookie_validate no longer pulls you from the cache if you're 
+logging in (which means your cookie wouldn't get set). --akm
+
+Cookies are no longer expired if you're successfully authenticated but
+merely unauthorized. --mb
+
+Basic auth resynched with standard user folder, trying to fix
+some basic auth issues. --akm.
+
+Negative cache checking now performed outside of the two specific
+validate methods. --akm.
+
+A fairly innocuous print debug statement turned into a zLOG at error
+level, removed --akm.
+
+Clean up smbAuthSource log messages, and quieten.  Only truly
+exceptional cases are now logged above BLATHER. --mb
+
+Changes for 0.10.5
+
+Membership redirecting to login was still broken. It should be better
+now (twice) --akm
+
+logout() wasn't clearing the advanced cookie. --akm
+
+Negative Cache Value wasn't being passed through to the XUF constructor. --akm
+Log Users Out DTML code was broken, should work now. --akm
+
+The User object now contains the authSource as well as the propSource,
+making access to roles for custom User-objects possible. --dlk
+
+Following akm's advice, fixed manage_beforeDelete to use two separate 
+try:except blocks to ensure that if cache-removal fails, deleting 
+the container.__allow_groups__  property is attempted. This should
+fix the problem where deleted xuf instances remain as "ghost" products
+causing interference with newer versions of xuf, and also fixes the 
+problem where deleting a xuf acl_users in a folder makes that folder
+inaccessible. --dlk
+
+Fixed cache_delete that was missing the "self" parameter in the method
+defintion. --dlk
+
+Fixed xcache_delete that was missing the "self" parameter in the method
+definition --akm d8)
+
+These previous two fix the problems with manage_beforeDelete, but, it
+will stay the same for now --akm.
+
+Fixed cache_deleteCookieCache that was missing the "self" parameter in
+the method defintion. --dlk ;)
+
+Changes for 0.10.4
+
+The instructions for File Based Auth were incorrect in the UZG --akm
+
+redirectToLogin was totally wrong for membership... --akm
+docLogin was fixed for VHM use. --akm
+
+Advanced Cookie Mode has changed so that it no longer sends the username
+and password. Instead a hash is used as a key into a module level cache.
+This should be 100% more secure than standard cookie mode, and removes
+the stupid back doors I enabled in the previous version. This work was
+based on conversations I had with Stuart Bishop (I basically lifted
+the hashing scheme from GUF). This makes use of the Module level cache
+code. --akm
+
+There was a code cleanup and a slight reorganisation of some files. --akm
+
+The main User Object has migrated to XUFUser and simarly with the
+AnonUser. There is now an empty [Anon]User class that has XUFUser as
+it's base. This allows people to create custom User Objects without
+jumping through hoops (and simplifies maintaining patches) --akm
+
+Cache Code has changed again. Now there is a module level cache, so
+that auth data is shared between threads for a single XUF (thanks to
+Stuart Bishop for an enlightening discussion on this and other issues,
+and thanks to Chris McDonough for talking me through setting up module 
+level globals [and sending me some code to work from]) --akm
+
+A Negative User Cache now exists. This is only generally useful for
+use with remote auth sources where repeatedly trying to auth non-existant
+users is very expensive (where they are authed at a higher level).
+You can enable this on creation or from the parameters screen (positive
+time in seconds enables). --akm
+
+Domain checking code finally removed. --akm
+
+zodbBTreePropSource changed to be friendlier about users that exist
+in remote locations (i.e. aren't create as such through the ZMI). -- akm
+
+Changed some 'print's in the code to use zLOG.LOG
+instead. Files affected so far (more to follow): -- rochael
+
+  * exUserFolder.py
+  * basicMemberSource/basicMemberSource.py
+  * zodbBTreePropSource/zodbBTreePropSource.py
+  * zodbPropSource/zodbPropSource.py
+
+Changed a couple things in smbAuthSource.py: -- rbanffy
+
+  * Method _authenticate_retry now logs several kinds of information
+    for debugging and diagnostics.
+
+  * Modified socket.error handling in _authenticate_retry: changed
+    "raise" to "return 0".
+
+  * Since this generated more problems (failed authentications) than
+    it solved (our impression it was not right not to return 0 in an
+    auth fail even due to a communications malfunction), we also
+    changed socket.error handling to retry no mather what errno tells
+    us (it said different things for the same problem under Windows
+    and Linux).
+
+  * In order to prevent infinite retries, changed retry handling a
+    bit. It now retries 3 times. Real-use data will tell us if we
+    should increase or not retries. To better convey the meaning of
+    the parameter, changed "retry_depth" to "retries". I strongly
+    advise the use of credential caching with smbAuthSource, tough, as
+    it reduces socket errors and load on the domain controllers.
+
+Changes for 0.10.3.1
+
+Readded support for I18N without ZBabel installation, somehow missed
+during the transition to SF CVS.
+
+Some text changes as well as an update to the dictionary while we're
+at it.  No functional changes for this release though.
+
+Changes for 0.10.3
+
+Missed a few LoginRequireds.
+
+Fixed a bug with __allow_groups__ not being set after paste
+(probably also not after import).
+
+The sources are now sorted by name in the drop down box..
+
+a BTree version of zodbAuthSource
+a BTree version of zodbPropSource
+
+These aren't really all that different to the originals that were
+provided by Alex, but, they use BTrees instead of PersistentMappings,
+and try to avoid various persistence problems associated with dicts.
+Both versions will continue to be supported.
+
+Patches from SF applied.
+
+Advanced Cookie Mode added.
+This mode adds a rotor cipher around the cookie. A secret is provided
+in order to encode the cookie. The username and password are placed
+within a small class which is pickled and then encrypted and then
+base64 encoded for transport. There is also a timestamp inside the cookie,
+so the ultra-paranoid of you can rotate the cookie based on the timestamp
+inside.
+
+Abstracted out the setting and decoding of cookies.
+
+Changes for 0.10.2
+
+all raise 'LoginRequired' <- raise 'Unauthorized'
+
+Raising unauthorizes breaks a million things. CMF people can just
+put up with configuring their portal properly.
+
+Radius resynced with version from sourceforge.
+manage_tabs redone to be ZBabel'd and to look like standard tabs.
+
+German Language added to the ZBabel dictionary.
+
+
+Changes for 0.10.1
+
+all raise 'LoginRequired' -> raise 'Unauthorized'
+
+Bug in etcAuthSource listUsers fixed, 
+and cryptPassword also fixed to get the actual salt.
+
+Zope 2.4.3 has dicked with security settings again.. I've had a round
+of permission whacking.
+
+Buggy handling of empty role lists was fixed.
+
+Change to smbAuthSource to use string.lower on usernames for
+python 1.5.2 compatibility?
+
+
+Changes for 0.10.0
+
+Added explicit roles for manage_editUser and friends, to allow
+the "Manage users" permission to be useful to non-Manager Users.
+Thanks to Heimo Laukkanen <huima@fountainpark.org> for reporting this
+one.
+
+zodbAuthSource made more persistent <alex@quad.com.ar>
+zodbPropSource was blowing when deleting temporary properties.
+
+XUF is now ZBabel'd which means you can view XUF in different languages
+for logging in and installation, if your browser locale is set up.
+You will need the latest ZBabel installed. The translation file is in the
+I18N directory. 
+
+Import this (using Import/Export in ZODB) at the same level as your 
+ZBabelTower, and then import it from ZBabel. If you have ZBabel installed, 
+but, your application can't find a ZBabelTower, because of a bug in the 
+current dtml-fish tag, you might experience some problems. This ZBabel 
+bug should be fixed sometime soon. 
+
+You do not need ZBabel installed to run XUF, XUF installs a dummy 
+interface for ZBabel so that XUF can continue to run (sorry folks it
+defaults to Australian English).
+
+getUserNames() was returning the wrong stuff (notably affected TheJester's
+WorkOrders Product)
+
+There is a now an 'Advanced Postgres' Auth Source that uses a seperate
+Roles table and a 'more relational' layout. The schema is with the
+auth source in pgAuthSourceAlt. Contributed by 
+Adam Manock <abmanock@earthlink.net>
+
+If you had a membership source and had specified a login page, XUF was
+still using the stock docLogin instead of the membership specified page
+(for redirectToLogin, exceptions still raise the docLogin).
+
+I changed the icon to something a *little* less hideous
+
+Leonardo Rochael Almeida <leo@hiper.com.br> made the following changes
+to smbAuthSource
+
+* Added a 'winsserver' constructor parameter and a '_winsserver'
+   instance variable to the 'smbAuthSource' class. This variable should
+   be the empty string, meaning that the authenticaton host will be
+   looked up by broadcast, or an IP address string pointing to a WINS
+   server.
+
+* Modified the dtml templates to ask for the above mentioned WINS
+   server (and also to replace 'Add' with 'Change' in
+   'manage_editsmbAuthSourceForm').
+
+* Refactored the smbAuthSource class to isolate all smb interaction
+   inside well defined methods.
+
+
+Changes for 0.9.0
+
+Messages are now sent back to the docLogin form. There's a file called
+LoginRequiredMessages.py where the messages are kept for now (it might
+end up a run-time configurable thing later).
+
+There's a new docLogin.dtml file on disk that shows how to use the new
+messages. Because docLogin is in the ZODB this won't be automatically
+upgraded.
+
+Idle Session Timeouts are in (this is the reason for the minor bump).
+If you flick the switch, then users are forced back to the login form
+(with a message saying their session timed out), when they're removed
+from the cache.
+
+I made some adjustments to the tabs on the management interface because
+they were too big, and I cleaned it up a bit for times when they run
+together.
+
+The internal API was inconsistent, so that's been updated.
+AuthSources no longer need to provide getUsers(), it was never
+being called anyway since exUserFolder built it's own.
+listUsers now returns the same data as listOneUser, this is used in
+other places as if it were a list of listOneUser calls.
+
+Fixed pgAuthSource to deal with NULL rather than empty roles
+columns (legacy columns).
+
+Changed Home Directory creation to use copy & paste functions to
+copy the skeleton data.
+
+Changes for 0.8.5
+
+I forgot to update the schema file for userproperties to reflect
+the temporary properties flag.
+
+Checks for existing cache weren't being performed before removing users
+from it, when their data was updated.
+
+Reversed the order for checking in cookie_validate, to allow logging
+in as a new user, when session tracking was on. Also now you can
+login as a different user, without logging out first, which might
+be useful to some people.
+
+etcAuthSource now looks for the correct salt from the file for
+encrypting the user supplied password
+
+Changes for 0.8.4
+
+Activating Session Tracking and then adding a new user when there
+were none in the XUF was broken.
+
+Changes for 0.8.3
+
+The idle users are flushed from the cache when you ask for the list
+of cache users (since it's iterating over the whole list anyway). So
+you can manually clear your cache by looking at the Cache Stats page.
+
+If you display the list of logged in users on your site, then your cache
+will be flushed for you automagically.
+
+Allowed a destination to be sent to redirectToLogin to allow you to
+manually override the destination after logging in.
+
+Added in a __setstate__ for pgPropSource to deal with new ZSQL Methods
+being added.
+
+Changes for 0.8.2
+A number of bugs related to temp properties fixed in pgPropSource
+
+FTP Access to folders protected with cookie_mode has been fixed, it
+now reverts to std_auth (which handles the FTP connection fine), since
+FTP auths are handled by getting a "Basic" auth tag coming through, which
+should never happen in cookie mode. 
+
+This has the knock-on effect of authenticating users that auth from a 
+higher acl_users that doesn't use cookies, 'more' correctly now. Which is
+if you have a user defined above, and in XUF and the XUF user has less
+permissions, it'll 401 you if you don't have permissions locally
+(which is the correct behaviour). This bit me in the arse when I changed it, 
+and I'm still leaving it this way. d8)
+
+Users are now flushed from the cache when you edit them (in case you changed
+roles), so that new roles should take effect immediately.
+
+The credential cache now uses the (Zope) builtin BTree Module for caching 
+rather than the AVL Tree implementation. There was a nasty issue with users 
+appearing multiple times in the AVL Tree which sucked.
+
+There is a report of the Radius Auth Source being broken (most likely
+by me), if your radius source stops working, you can try copying the
+py-radius.py file from sourceforge over the top of radius.py. If someone
+gives me a traceback, I can fix it. I don't seem to be having problems,
+but, I don't have a full time RADIUS source either.
+
+
+Changes for 0.8.1
+
+A bug in _doAddUser was fixed
+A bug in the User Object unconditionally calling the prop source was fixed.
+
+
+Changes for 0.8.0
+
+Experimental "Session Tracking" added (why is it called that? we don't really
+track anything, just associate arbitrary data with anonymous users).
+This relies on the credential cache being active. Your session will 
+automatically expire when the anonymous user is so idle that they are 
+expired from the cache. This is not currently acceptable (to me), but,
+it might be to other people, I await feedback on how sessions should expire
+gracefully.
+
+Updated the README.txt file to point at the UZG and to explain the
+version numbering system.
+
+All this time you couldn't delete properties from a user... who knew?
+It's fixed now.
+
+Temporary properties now available, you can setTempProperty() on a 
+user object, and also flushTempProperties() on a user object.
+Temporary properties are accessed like normal properties, and can be
+deleted in the same way. flushTempProperties is there to do a quick
+flush of all the crap you might have inserted (useful for sessions).
+If your user is flushed from the cache, then all temp properties will
+also be removed at that point.
+
+Propsource providers should look at the new temp properties stuff and
+update accordingly.
+
+Alex provided a whole heap of patches to make basicMembership more usable,
+well make it actually work.
+
+Matt Behrens supplied patches to prevent null logins and to allow case
+insensitive logins for smbAuthSource
+
+Added a basic FAQ.
+
+
+Changes for 0.7.10
+
+Active Users type functionality was added. The new function is called
+getUserCacheUsers(). It returns a list of dicts;
+
+{'username': theusername, 'lastAccessed': float_value} 
+
+lastAccessed represents the last time the user touched something.
+The Cache Stats page shows an example usage showing idle time (very cool
+I think :-)
+
+The logout method was not correctly removing users from the cache,
+although the cookie was removed, so logins were still enforced. I'm not
+sure of any side-effects related to it, but, 
+
+Some permissions were a little too liberal, including allowing arbitrary
+users to set and get Properties on the acl_users folder.
+
+Copy/Paste support for pasting exUserFolders into the root was added.
+I'm not sure I like the way this is done. I haven't found any side effects
+so far, but, just be wary. Adding an exUserFolder to the root becomes
+semi-trivial now. Create one in a sub-folder. Login as the emergency user.
+CUT the exUserFolder. Delete the standard acl_users folder. Paste exUserFolder.
+You should be away. At least it worked fine for me... YMMV
+
+_doChangeUser and _doDelUsers added so users can be altered and deleted 
+like for Standard UserFolder.
+
+_createInitialUser added so there should always be your initUser (hopefully) 
+when you create your exUserFolder.
+
+Emergency User checking brought into line with Standard Folder
+
+__creatable_by_emergency_user_ added and returns 1 to explicitly allow this.
+
+Unenlightened Zopistas Guide updated to have a 'Recipe' like section.
+Currently contains a section about adding exUserFolders from python.
+
+
+Changes for 0.7.9
+
+RADIUS authSource had a problem with non-integers being extracted from
+REQUEST (I wish someone at DC would fix this already). I worked around
+this problem
+
+Default port for RADIUS is now 1812 in line with the IANA sanctioned list.
+
+Unenlightened Zopistas Guide to exUserFolder version 0.0 included, 
+covers installation and authentication sources, and the most common 
+configuration mistake (or misunderstanding).
+
+I almost released with the daggy management screens all Purple or SkyBlue,
+so consider yoursevles lucky. This would have been the "Blue" release.
+
+Changes for 0.7.8
+
+zodbPropSource had a bug that must have been there since 0.0.0 where
+_p_changed wasn't being called on create, update, or delete user.
+Thanks to Bouke Scheurwater for spotting that one.
+
+Alex provided a number of patched to fix a whole bunch of goofy stuff
+with Basic Member Source that was stupidly wrong.
+
+Matt Behrens provided a patch to allow emergency user to own exUserFolders
+and some of the sources. I've grudgingly updated all the sources to allow
+this. It's just a hey nonny nonny to people using it as a root authenticator
+now.
+
+Matt Behrens also provided a patch to fix 'broken pipe' problems with
+smbAuthSource.
+
+pySMB is now at 0.2 for smbAuthSource WARNING: This will try to use DES 
+encrypted passwords. Apparently it should be ok if your server doesn't want
+them. However if it breaks, unpack the pySMB distribution in the 
+smbAuthSource directory, there are registry examples there to turn
+it off. It unfortunately needs the mxCrypto tools for encrypted passwords
+to work. When I've got a bit more time, I'll see if I can make it use
+crypt or fcrypt if available instead.
+
+Explicit checks for the emergency user were placed into the cookie_validate
+routines. I suspect this may have been the cause of some grief with people
+doing weird things like trying to make it the root auth folder.
+
+Changes for 0.7.7
+
+Some Auth sources had problems coping with no roles being selected when
+a user was created from the management interface, the stock ones were fixed.
+
+I screwed up some of the DTML, and forgot to change the loading of two of
+the methods from the dtml directory.
+
+NO MORE TRACEBACKS ON LOGIN FORMS, there is a little redirector dtml file
+dtml/docLoginRedirect that redirects to acl_users/docLogin with destination
+set to take them back to where they were going. If you have a custom loginPage
+change the redirector dtml to point to your new page.
+
+standard_html swapped for manage_page on Management Pages. Hopefully
+this doesn't break someone with an old copy of Zope.
+
+Credential Caching is now available by default for all Authentication Sources,
+upgrading installs will get this defaulted to 0 for no caching. You can alter
+the cache level from the Parameters Tab. Authors of external sources should
+remove any internal auth caching they're doing, and allow the user to decide
+how long to cache the credentials for.
+
+
+Changes for 0.7.6
+
+smbAuthSource included. Doesn't require any external libraries, or compiling.
+Uses pySMB from Micheal Teo <michaelteo@bigfoot.com>
+
+Changes for 0.7.5
+The Management Interface now batches the user list by 10. This isn't
+configurable at the moment (just change the dtml).
+
+The code was re-organised slightly, with all the DTML moving into its
+own directory for core.
+
+radiusAuthSource added, but, is so far untested. It is a direct port of
+ZRadius for GUF, but, I haven't had a chance to setup a RADIUS server to
+test it out.
+
+You can add properties to a user from the management interface.
+
+List Properties on users can be added and edited, if I can work out a decent 
+way to edit Dicts/Mappings, I'll add that feature in.
+
+This paves the way for defining a set of properties in the Membership 
+source, so it can create a Signup and Edit page for you automatically. 
+You will also be able to specify which properties the user can edit, or 
+roles required to edit a property, this will be in a later release though.
+
+pgPropSource was updated to take into account non-scalar types, and now
+pickles all data going into the database, this means ints will stay as ints,
+et al. 
+There is code in there to cope with older properties coming out as strings.
+The Schema remains the same.
+
+Changes for 0.7.2
+Changes to make it work with older version of python
+Some minor bug fixes for membership.
+
+Changes for 0.7.1
+DTML Change for cmfPropSource
+
+Changes for 0.7.0
+exUserFolder was a little too liberal in removing its cruft, this is now
+fixed.
+
+cmfPropSource was provided by Alan Runyan which is a layer around the CMF
+property stuff. It's conditionally imported, so if you don't have CMF
+installed you don't need to worry that'll it'll break.
+
+Property Sources are optional, and there is a NULL Property Source for this
+purpose.
+
+Membership hooks, and a rough start at membership (basicMemberSource),
+which has some usable functionality (you MUST read README.Membership before
+using this).
+
+Membership Sources are optional and there is a NULL Membership Source for
+this purpose.
+
+
+Changes for 0.6.2
+exUserFolder was leaving cruft around when it was being deleted from
+Folders. The cruft should now be obliterated if you delete an exUserFolder.
+
+Changes for 0.6.1
+Ownership tab enabled, for those sick monkeys that want to use it as a root
+Folder (there are some).
+
+fcrypt got the __init__.py that was missing from the 0.6.0 release
+zodbAuthSource updated to pull in fcrypt if crypt was missing.
+
+Changes for 0.6.0
+
+Updated for 2.4.1 / Python 2.1
+Bug in pgPropSource not deleting users from the property cache fixed.
+Bug with Local Roles not getting what it expected fixed.
+Alex Verstraeten provided zodbAuthSource, there's a README.zodbAuthSource,
+and the same README inside the zodbAuthSource directory.
+fcrypt is now included and used if crypt cannot be imported. More information 
+on fcrypt can be found at http://home.clear.net.nz/pages/c.evans/sw/. This
+should help particularly Windows users a lot.
+Rudimentary API doc included.
+
+Changes for 0.5.0
+
+A serious bug in zodbPropSource was fixed.
+
+There is now the option of providing a 'Remote Auth' function for
+validating. This allows things like IMAP/LDAP auth sources to do their
+authentication, since they don't return passwords you can use in general.
+
+There's already a 3rd Party solution that provides IMAP/POP3 authentication,
+using the new API.
+
+Changes for 0.4.6
+
+Minor dtml hacks
+
+Changes for 0.4.5
+
+Hooks for 'editing' Authentication and Property Sources were added, along
+with the relevant methods in each of the sources.
+
+The management interfaces got a little overhaul, just to make them 
+a little different (yes I know everything I do looks the same). The two
+I didn't want to mess with still have the acquired management interfaces.
+
+A fix for the ZODB Property Source which was missing a few methods.
+
+Changes for 0.4.0
+
+Based on an idea from Martin von Loewis, I added in support for defining
+roles for etcAuthSource. This basically uses the current Prop source to
+store a 'roles' property. The default role is still there as well for
+those of you who might be using it.
+
+Changes for 0.3.0
+
+Adrien Hernot noticed that properties for new users using zodbPropSource
+were causing havoc, and that the version.txt file was completely wrong.
+Andreas also noticed the version.txt was wrong. 
+
+I've been bugged enough by the pair of them to change the single += 
+into 1.5.2 compliant syntax. 
+
+I don't make any claims about it working under 1.5.2 though.
+
+Changes for 0.2.0
+
+Even more embarassment...
+
+Andreas Heckel provided fixes for some stupid things I left out including;
+
+o Fixing the way I was handling multiple roles coming out of the database
+o The wrong icon in the user display
+o Alerting me to the fact that pgPropSource didn't actually have a
+  deleteUsers hook
+o Providing a schema for automatically deleting properties in postgres
+  if you delete a user from the auth source (you have to be using both
+  pg sources for this to work, and they'd have to be in the same database)
+  I've put Andreas schema into the distribution, if you want to use 
+  exUserFolder as a straight pgUserFolder, you'll also need to edit
+  exUserFolder.py and comment out the line indicated in deleteUsers()
+
+Changes for 0.1.0
+
+Pretty embarassing really.
+
+M. Adam Kendall (DaJoker) found some stupid things in the 0.0.0 release 
+including the fact you couldn't edit user properties, or update them, 
+or actually change a user in anyway.
+
+I also discovered I was resetting the password to empty if you left it
+empty.. 
diff --git a/ZopeProducts/exUserFolder/CryptoSources/__init__.py b/ZopeProducts/exUserFolder/CryptoSources/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..49ebc6d4af60e4a63541330e46a6d91b75c6d2ba
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/__init__.py
@@ -0,0 +1,4 @@
+import pass_crypt
+import pass_md5
+import pass_sha
+import pass_plain
diff --git a/ZopeProducts/exUserFolder/CryptoSources/fcrypt/.cvsignore b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/CryptoSources/fcrypt/ChangeLog b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/ChangeLog
new file mode 100644
index 0000000000000000000000000000000000000000..70299bedcc3c92ce389e6203fc699c68cf6b9913
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/ChangeLog
@@ -0,0 +1,35 @@
+2001-05-05  Carey Evans  <careye@spamcop.net>
+
+	* fcrypt.py: Add module doc string for pydoc, and other globals
+	for pydoc as well.  Add __all__ for Python 2.1, and add
+	underscores to the front of private variables and functions.
+	(_set_key): Remove overly clever copying of globals into default
+	parameters, explicitly copying _shift2 and _skb before the loop.
+	(_body): Copy _SPtrans explicitly, as above.  Remove CR_ENCRYPT
+	inline function, and reroll unrolled loop using the contents of
+	this function.  Result: more readable code, and a 400% speedup!
+	(crypt): Add doc string for pydoc and doctest.
+	(_test): New function for doctest.
+
+	* setup.py: Add fields for PKG-INFO metadata.
+
+	* README: Add URL of distutils installation manual.
+
+	* LICENSE: Add note about license on fcrypt.py being the union of
+	my license on the Python code and Eric Young's on the original C.
+
+2001-03-24  Carey Evans  <careye@spamcop.net>
+
+	* setup.py: Move license to separate file.  Change email address
+	to SpamCop forwardder.  Update version to 1.1.
+
+	* fcrypt.py: Update license text and email address.
+	(crypt): Fix bug where passwords longer than eight characters were
+	not truncated.
+
+	* README: Update crypt module URL.  Remove license text, and add
+	pointer to LICENSE file.  Update email address.
+
+	* MANIFEST.in: Add LICENSE, ChangeLog and MANIFEST.in.
+
+	* LICENSE: New file.
diff --git a/ZopeProducts/exUserFolder/CryptoSources/fcrypt/LICENSE b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..c26e615deb42fa881a4d365bb33014c98c2158c8
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/LICENSE
@@ -0,0 +1,77 @@
+		   fcrypt.py copyrights and license
+		   --------------------------------
+
+
+The Python code by Carey Evans has the following license, which is the
+original Python license with the serial numbers filed off, and the
+restrictions on advertising removed.
+
+  Copyright (C) 2001, 2001  Carey Evans  <careye@spamcop.net>
+
+  Permission to use, copy, modify, and distribute this software and its
+  documentation for any purpose and without fee is hereby granted,
+  provided that the above copyright notice appear in all copies and that
+  both that copyright notice and this permission notice appear in
+  supporting documentation.
+
+  CAREY EVANS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+  INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
+  EVENT SHALL CAREY EVANS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+  CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
+  USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+  PERFORMANCE OF THIS SOFTWARE.
+
+
+The original C code on which this module was based has the following
+more restrictive license, so the source for fcrypt.py should be
+considered to be covered by the union of my license and Eric Young's.
+
+  This library is free for commercial and non-commercial use as long as
+  the following conditions are aheared to.  The following conditions
+  apply to all code found in this distribution, be it the RC4, RSA,
+  lhash, DES, etc., code; not just the SSL code.  The SSL documentation
+  included with this distribution is covered by the same copyright terms
+  except that the holder is Tim Hudson (tjh@mincom.oz.au).
+  
+  Copyright remains Eric Young's, and as such any Copyright notices in
+  the code are not to be removed.
+  If this package is used in a product, Eric Young should be given attribution
+  as the author of the parts of the library used.
+  This can be in the form of a textual message at program startup or
+  in documentation (online or textual) provided with the package.
+  
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions
+  are met:
+  1. Redistributions of source code must retain the copyright
+     notice, this list of conditions and the following disclaimer.
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+  3. All advertising materials mentioning features or use of this software
+     must display the following acknowledgement:
+     "This product includes cryptographic software written by
+      Eric Young (eay@mincom.oz.au)"
+     The word 'cryptographic' can be left out if the rouines from the library
+     being used are not cryptographic related :-).
+  4. If you include any Windows specific code (or a derivative thereof) from 
+     the apps directory (application code) you must include an acknowledgement:
+     "This product includes software written by Tim Hudson (tjh@mincom.oz.au)"
+  
+  THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
+  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+  SUCH DAMAGE.
+  
+  The licence and distribution terms for any publically available version or
+  derivative of this code cannot be changed.  i.e. this code cannot simply be
+  copied and put under another distribution licence
+  [including the GNU Public Licence.]
diff --git a/ZopeProducts/exUserFolder/CryptoSources/fcrypt/MANIFEST.in b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..a42d250c1a5a471f2e1bf672367fb9a31d15e6f0
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/MANIFEST.in
@@ -0,0 +1 @@
+include LICENSE ChangeLog MANIFEST.in
diff --git a/ZopeProducts/exUserFolder/CryptoSources/fcrypt/PKG-INFO b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/PKG-INFO
new file mode 100644
index 0000000000000000000000000000000000000000..c35737e03164955f4422303f9cb473137f2616f9
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/PKG-INFO
@@ -0,0 +1,13 @@
+Metadata-Version: 1.0
+Name: fcrypt
+Version: 1.2
+Summary: The Unix password crypt function.
+Home-page: http://home.clear.net.nz/pages/c.evans/sw/
+Author: Carey Evans
+Author-email: careye@spamcop.net
+License: BSD
+Description: A pure Python implementation of the Unix DES password crypt function,
+        based on Eric Young's fcrypt.c.  It works with any version of Python
+        from version 1.5 or higher, and because it's pure Python it doesn't
+        need a C compiler to install it.
+Platform: UNKNOWN
diff --git a/ZopeProducts/exUserFolder/CryptoSources/fcrypt/README b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/README
new file mode 100644
index 0000000000000000000000000000000000000000..168500ffc1f34afb04ac32c5315d5f50df104933
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/README
@@ -0,0 +1,33 @@
+			      fcrypt.py
+			      ---------
+
+This is a pure Python implementation of the Unix DES password crypt
+function.  It was ported from C code by Eric Young (eay@mincom.oz.au).
+See the file LICENSE for copyright and license details.
+
+This module is packaged with Distutils.  If you have this installed,
+or it came with your version of Python, you can install it by typing:
+
+    python setup.py install
+
+If not, you can just copy `fcrypt.py' into a directory on your Python
+library path, or into the same directory as the program that wants to
+use it.
+
+For more information, see the documentation for Python's built-in
+crypt module at:
+
+    http://www.python.org/doc/current/lib/module-crypt.html
+
+Eric Young's fcrypt.c is available from:
+
+    ftp://ftp.psy.uq.oz.au/pub/Crypto/DES/
+
+For more Distutils information, see:
+
+    http://www.python.org/doc/current/inst/inst.html
+    http://www.python.org/sigs/distutils-sig/
+
+-- 
+Carey Evans  <careye@spamcop.net>
+5 May 2001
diff --git a/ZopeProducts/exUserFolder/CryptoSources/fcrypt/__init__.py b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..97f6031cd7a2a9ac5e3e833186595c3e709c20d3
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/__init__.py
@@ -0,0 +1 @@
+import fcrypt
diff --git a/ZopeProducts/exUserFolder/CryptoSources/fcrypt/fcrypt.py b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/fcrypt.py
new file mode 100644
index 0000000000000000000000000000000000000000..760347cb8d25da08e2d4fbec9aeb749deae660ed
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/fcrypt.py
@@ -0,0 +1,602 @@
+# fcrypt.py
+
+"""Unix crypt(3) password hash algorithm.
+
+This is a port to Python of the standard Unix password crypt function.
+It's a single self-contained source file that works with any version
+of Python from version 1.5 or higher.  The code is based on Eric
+Young's optimised crypt in C.
+
+Python fcrypt is intended for users whose Python installation has not
+had the crypt module enabled, or whose C library doesn't include the
+crypt function.  See the documentation for the Python crypt module for
+more information:
+
+  http://www.python.org/doc/current/lib/module-crypt.html
+
+The crypt() function is a one-way hash function, intended to hide a
+password such that the only way to find out the original password is
+to guess values until you get a match.  If you need to encrypt and
+decrypt data, this is not the module for you.
+
+There are at least two packages providing Python cryptography support:
+M2Crypto at <http://www.pobox.org.sg/home/ngps/m2/>, and amkCrypto at
+<http://www.amk.ca/python/code/crypto.html>.
+
+Functions:
+
+  crypt() -- return hashed password
+"""
+
+__author__ = 'Carey Evans <careye@spamcop.net>'
+__version__ = '1.2'
+__date__ = '6 May 2001'
+__credits__ = '''michal j wallace for inspiring me to write this.
+Eric Young for the C code this module was copied from.'''
+
+__all__ = ['crypt']
+
+
+# Copyright (C) 2000, 2001  Carey Evans  <careye@spamcop.net>
+#
+# Permission to use, copy, modify, and distribute this software and
+# its documentation for any purpose and without fee is hereby granted,
+# provided that the above copyright notice appear in all copies and
+# that both that copyright notice and this permission notice appear in
+# supporting documentation.
+#
+# CAREY EVANS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
+# EVENT SHALL CAREY EVANS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
+# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# Based on C code by Eric Young (eay@mincom.oz.au), which has the
+# following copyright.  Especially note condition 3, which imposes
+# extra restrictions on top of the standard Python license used above.
+#
+# The fcrypt.c source is available from:
+#     ftp://ftp.psy.uq.oz.au/pub/Crypto/DES/
+
+# ----- BEGIN fcrypt.c LICENSE -----
+#
+# This library is free for commercial and non-commercial use as long as
+# the following conditions are aheared to.  The following conditions
+# apply to all code found in this distribution, be it the RC4, RSA,
+# lhash, DES, etc., code; not just the SSL code.  The SSL documentation
+# included with this distribution is covered by the same copyright terms
+# except that the holder is Tim Hudson (tjh@mincom.oz.au).
+# 
+# Copyright remains Eric Young's, and as such any Copyright notices in
+# the code are not to be removed.
+# If this package is used in a product, Eric Young should be given attribution
+# as the author of the parts of the library used.
+# This can be in the form of a textual message at program startup or
+# in documentation (online or textual) provided with the package.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+# 3. All advertising materials mentioning features or use of this software
+#    must display the following acknowledgement:
+#    "This product includes cryptographic software written by
+#     Eric Young (eay@mincom.oz.au)"
+#    The word 'cryptographic' can be left out if the rouines from the library
+#    being used are not cryptographic related :-).
+# 4. If you include any Windows specific code (or a derivative thereof) from 
+#    the apps directory (application code) you must include an acknowledgement:
+#    "This product includes software written by Tim Hudson (tjh@mincom.oz.au)"
+# 
+# THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+# 
+# The licence and distribution terms for any publically available version or
+# derivative of this code cannot be changed.  i.e. this code cannot simply be
+# copied and put under another distribution licence
+# [including the GNU Public Licence.]
+#
+# ----- END fcrypt.c LICENSE -----
+
+
+import string, struct
+
+
+_ITERATIONS = 16
+
+_SPtrans = (
+    # nibble 0
+    [ 0x00820200, 0x00020000, 0x80800000, 0x80820200,
+      0x00800000, 0x80020200, 0x80020000, 0x80800000,
+      0x80020200, 0x00820200, 0x00820000, 0x80000200,
+      0x80800200, 0x00800000, 0x00000000, 0x80020000,
+      0x00020000, 0x80000000, 0x00800200, 0x00020200,
+      0x80820200, 0x00820000, 0x80000200, 0x00800200,
+      0x80000000, 0x00000200, 0x00020200, 0x80820000,
+      0x00000200, 0x80800200, 0x80820000, 0x00000000,
+      0x00000000, 0x80820200, 0x00800200, 0x80020000,
+      0x00820200, 0x00020000, 0x80000200, 0x00800200,
+      0x80820000, 0x00000200, 0x00020200, 0x80800000,
+      0x80020200, 0x80000000, 0x80800000, 0x00820000,
+      0x80820200, 0x00020200, 0x00820000, 0x80800200,
+      0x00800000, 0x80000200, 0x80020000, 0x00000000,
+      0x00020000, 0x00800000, 0x80800200, 0x00820200,
+      0x80000000, 0x80820000, 0x00000200, 0x80020200 ],
+
+    # nibble 1
+    [ 0x10042004, 0x00000000, 0x00042000, 0x10040000,
+      0x10000004, 0x00002004, 0x10002000, 0x00042000,
+      0x00002000, 0x10040004, 0x00000004, 0x10002000,
+      0x00040004, 0x10042000, 0x10040000, 0x00000004,
+      0x00040000, 0x10002004, 0x10040004, 0x00002000,
+      0x00042004, 0x10000000, 0x00000000, 0x00040004,
+      0x10002004, 0x00042004, 0x10042000, 0x10000004,
+      0x10000000, 0x00040000, 0x00002004, 0x10042004,
+      0x00040004, 0x10042000, 0x10002000, 0x00042004,
+      0x10042004, 0x00040004, 0x10000004, 0x00000000,
+      0x10000000, 0x00002004, 0x00040000, 0x10040004,
+      0x00002000, 0x10000000, 0x00042004, 0x10002004,
+      0x10042000, 0x00002000, 0x00000000, 0x10000004,
+      0x00000004, 0x10042004, 0x00042000, 0x10040000,
+      0x10040004, 0x00040000, 0x00002004, 0x10002000,
+      0x10002004, 0x00000004, 0x10040000, 0x00042000 ],
+
+    # nibble 2
+    [ 0x41000000, 0x01010040, 0x00000040, 0x41000040,
+      0x40010000, 0x01000000, 0x41000040, 0x00010040,
+      0x01000040, 0x00010000, 0x01010000, 0x40000000,
+      0x41010040, 0x40000040, 0x40000000, 0x41010000,
+      0x00000000, 0x40010000, 0x01010040, 0x00000040,
+      0x40000040, 0x41010040, 0x00010000, 0x41000000,
+      0x41010000, 0x01000040, 0x40010040, 0x01010000,
+      0x00010040, 0x00000000, 0x01000000, 0x40010040,
+      0x01010040, 0x00000040, 0x40000000, 0x00010000,
+      0x40000040, 0x40010000, 0x01010000, 0x41000040,
+      0x00000000, 0x01010040, 0x00010040, 0x41010000,
+      0x40010000, 0x01000000, 0x41010040, 0x40000000,
+      0x40010040, 0x41000000, 0x01000000, 0x41010040,
+      0x00010000, 0x01000040, 0x41000040, 0x00010040,
+      0x01000040, 0x00000000, 0x41010000, 0x40000040,
+      0x41000000, 0x40010040, 0x00000040, 0x01010000 ],
+
+    # nibble 3
+    [ 0x00100402, 0x04000400, 0x00000002, 0x04100402,
+      0x00000000, 0x04100000, 0x04000402, 0x00100002,
+      0x04100400, 0x04000002, 0x04000000, 0x00000402,
+      0x04000002, 0x00100402, 0x00100000, 0x04000000,
+      0x04100002, 0x00100400, 0x00000400, 0x00000002,
+      0x00100400, 0x04000402, 0x04100000, 0x00000400,
+      0x00000402, 0x00000000, 0x00100002, 0x04100400,
+      0x04000400, 0x04100002, 0x04100402, 0x00100000,
+      0x04100002, 0x00000402, 0x00100000, 0x04000002,
+      0x00100400, 0x04000400, 0x00000002, 0x04100000,
+      0x04000402, 0x00000000, 0x00000400, 0x00100002,
+      0x00000000, 0x04100002, 0x04100400, 0x00000400,
+      0x04000000, 0x04100402, 0x00100402, 0x00100000,
+      0x04100402, 0x00000002, 0x04000400, 0x00100402,
+      0x00100002, 0x00100400, 0x04100000, 0x04000402,
+      0x00000402, 0x04000000, 0x04000002, 0x04100400 ],
+
+    # nibble 4
+    [ 0x02000000, 0x00004000, 0x00000100, 0x02004108,
+      0x02004008, 0x02000100, 0x00004108, 0x02004000,
+      0x00004000, 0x00000008, 0x02000008, 0x00004100,
+      0x02000108, 0x02004008, 0x02004100, 0x00000000,
+      0x00004100, 0x02000000, 0x00004008, 0x00000108,
+      0x02000100, 0x00004108, 0x00000000, 0x02000008,
+      0x00000008, 0x02000108, 0x02004108, 0x00004008,
+      0x02004000, 0x00000100, 0x00000108, 0x02004100,
+      0x02004100, 0x02000108, 0x00004008, 0x02004000,
+      0x00004000, 0x00000008, 0x02000008, 0x02000100,
+      0x02000000, 0x00004100, 0x02004108, 0x00000000,
+      0x00004108, 0x02000000, 0x00000100, 0x00004008,
+      0x02000108, 0x00000100, 0x00000000, 0x02004108,
+      0x02004008, 0x02004100, 0x00000108, 0x00004000,
+      0x00004100, 0x02004008, 0x02000100, 0x00000108,
+      0x00000008, 0x00004108, 0x02004000, 0x02000008 ],
+
+    # nibble 5
+    [ 0x20000010, 0x00080010, 0x00000000, 0x20080800,
+      0x00080010, 0x00000800, 0x20000810, 0x00080000,
+      0x00000810, 0x20080810, 0x00080800, 0x20000000,
+      0x20000800, 0x20000010, 0x20080000, 0x00080810,
+      0x00080000, 0x20000810, 0x20080010, 0x00000000,
+      0x00000800, 0x00000010, 0x20080800, 0x20080010,
+      0x20080810, 0x20080000, 0x20000000, 0x00000810,
+      0x00000010, 0x00080800, 0x00080810, 0x20000800,
+      0x00000810, 0x20000000, 0x20000800, 0x00080810,
+      0x20080800, 0x00080010, 0x00000000, 0x20000800,
+      0x20000000, 0x00000800, 0x20080010, 0x00080000,
+      0x00080010, 0x20080810, 0x00080800, 0x00000010,
+      0x20080810, 0x00080800, 0x00080000, 0x20000810,
+      0x20000010, 0x20080000, 0x00080810, 0x00000000,
+      0x00000800, 0x20000010, 0x20000810, 0x20080800,
+      0x20080000, 0x00000810, 0x00000010, 0x20080010 ],
+
+    # nibble 6
+    [ 0x00001000, 0x00000080, 0x00400080, 0x00400001,
+      0x00401081, 0x00001001, 0x00001080, 0x00000000,
+      0x00400000, 0x00400081, 0x00000081, 0x00401000,
+      0x00000001, 0x00401080, 0x00401000, 0x00000081,
+      0x00400081, 0x00001000, 0x00001001, 0x00401081,
+      0x00000000, 0x00400080, 0x00400001, 0x00001080,
+      0x00401001, 0x00001081, 0x00401080, 0x00000001,
+      0x00001081, 0x00401001, 0x00000080, 0x00400000,
+      0x00001081, 0x00401000, 0x00401001, 0x00000081,
+      0x00001000, 0x00000080, 0x00400000, 0x00401001,
+      0x00400081, 0x00001081, 0x00001080, 0x00000000,
+      0x00000080, 0x00400001, 0x00000001, 0x00400080,
+      0x00000000, 0x00400081, 0x00400080, 0x00001080,
+      0x00000081, 0x00001000, 0x00401081, 0x00400000,
+      0x00401080, 0x00000001, 0x00001001, 0x00401081,
+      0x00400001, 0x00401080, 0x00401000, 0x00001001 ],
+
+    # nibble 7
+    [ 0x08200020, 0x08208000, 0x00008020, 0x00000000,
+      0x08008000, 0x00200020, 0x08200000, 0x08208020,
+      0x00000020, 0x08000000, 0x00208000, 0x00008020,
+      0x00208020, 0x08008020, 0x08000020, 0x08200000,
+      0x00008000, 0x00208020, 0x00200020, 0x08008000,
+      0x08208020, 0x08000020, 0x00000000, 0x00208000,
+      0x08000000, 0x00200000, 0x08008020, 0x08200020,
+      0x00200000, 0x00008000, 0x08208000, 0x00000020,
+      0x00200000, 0x00008000, 0x08000020, 0x08208020,
+      0x00008020, 0x08000000, 0x00000000, 0x00208000,
+      0x08200020, 0x08008020, 0x08008000, 0x00200020,
+      0x08208000, 0x00000020, 0x00200020, 0x08008000,
+      0x08208020, 0x00200000, 0x08200000, 0x08000020,
+      0x00208000, 0x00008020, 0x08008020, 0x08200000,
+      0x00000020, 0x08208000, 0x00208020, 0x00000000,
+      0x08000000, 0x08200020, 0x00008000, 0x00208020 ] )
+
+_skb = (
+    # for C bits (numbered as per FIPS 46) 1 2 3 4 5 6
+    [ 0x00000000, 0x00000010, 0x20000000, 0x20000010,
+      0x00010000, 0x00010010, 0x20010000, 0x20010010,
+      0x00000800, 0x00000810, 0x20000800, 0x20000810,
+      0x00010800, 0x00010810, 0x20010800, 0x20010810,
+      0x00000020, 0x00000030, 0x20000020, 0x20000030,
+      0x00010020, 0x00010030, 0x20010020, 0x20010030,
+      0x00000820, 0x00000830, 0x20000820, 0x20000830,
+      0x00010820, 0x00010830, 0x20010820, 0x20010830,
+      0x00080000, 0x00080010, 0x20080000, 0x20080010,
+      0x00090000, 0x00090010, 0x20090000, 0x20090010,
+      0x00080800, 0x00080810, 0x20080800, 0x20080810,
+      0x00090800, 0x00090810, 0x20090800, 0x20090810,
+      0x00080020, 0x00080030, 0x20080020, 0x20080030,
+      0x00090020, 0x00090030, 0x20090020, 0x20090030,
+      0x00080820, 0x00080830, 0x20080820, 0x20080830,
+      0x00090820, 0x00090830, 0x20090820, 0x20090830 ],
+
+    # for C bits (numbered as per FIPS 46) 7 8 10 11 12 13
+    [ 0x00000000, 0x02000000, 0x00002000, 0x02002000,
+      0x00200000, 0x02200000, 0x00202000, 0x02202000,
+      0x00000004, 0x02000004, 0x00002004, 0x02002004,
+      0x00200004, 0x02200004, 0x00202004, 0x02202004,
+      0x00000400, 0x02000400, 0x00002400, 0x02002400,
+      0x00200400, 0x02200400, 0x00202400, 0x02202400,
+      0x00000404, 0x02000404, 0x00002404, 0x02002404,
+      0x00200404, 0x02200404, 0x00202404, 0x02202404,
+      0x10000000, 0x12000000, 0x10002000, 0x12002000,
+      0x10200000, 0x12200000, 0x10202000, 0x12202000,
+      0x10000004, 0x12000004, 0x10002004, 0x12002004,
+      0x10200004, 0x12200004, 0x10202004, 0x12202004,
+      0x10000400, 0x12000400, 0x10002400, 0x12002400,
+      0x10200400, 0x12200400, 0x10202400, 0x12202400,
+      0x10000404, 0x12000404, 0x10002404, 0x12002404,
+      0x10200404, 0x12200404, 0x10202404, 0x12202404 ],
+
+    # for C bits (numbered as per FIPS 46) 14 15 16 17 19 20
+    [ 0x00000000, 0x00000001, 0x00040000, 0x00040001,
+      0x01000000, 0x01000001, 0x01040000, 0x01040001,
+      0x00000002, 0x00000003, 0x00040002, 0x00040003,
+      0x01000002, 0x01000003, 0x01040002, 0x01040003,
+      0x00000200, 0x00000201, 0x00040200, 0x00040201,
+      0x01000200, 0x01000201, 0x01040200, 0x01040201,
+      0x00000202, 0x00000203, 0x00040202, 0x00040203,
+      0x01000202, 0x01000203, 0x01040202, 0x01040203,
+      0x08000000, 0x08000001, 0x08040000, 0x08040001,
+      0x09000000, 0x09000001, 0x09040000, 0x09040001,
+      0x08000002, 0x08000003, 0x08040002, 0x08040003,
+      0x09000002, 0x09000003, 0x09040002, 0x09040003,
+      0x08000200, 0x08000201, 0x08040200, 0x08040201,
+      0x09000200, 0x09000201, 0x09040200, 0x09040201,
+      0x08000202, 0x08000203, 0x08040202, 0x08040203,
+      0x09000202, 0x09000203, 0x09040202, 0x09040203 ],
+
+    # for C bits (numbered as per FIPS 46) 21 23 24 26 27 28
+    [ 0x00000000, 0x00100000, 0x00000100, 0x00100100,
+      0x00000008, 0x00100008, 0x00000108, 0x00100108,
+      0x00001000, 0x00101000, 0x00001100, 0x00101100,
+      0x00001008, 0x00101008, 0x00001108, 0x00101108,
+      0x04000000, 0x04100000, 0x04000100, 0x04100100,
+      0x04000008, 0x04100008, 0x04000108, 0x04100108,
+      0x04001000, 0x04101000, 0x04001100, 0x04101100,
+      0x04001008, 0x04101008, 0x04001108, 0x04101108,
+      0x00020000, 0x00120000, 0x00020100, 0x00120100,
+      0x00020008, 0x00120008, 0x00020108, 0x00120108,
+      0x00021000, 0x00121000, 0x00021100, 0x00121100,
+      0x00021008, 0x00121008, 0x00021108, 0x00121108,
+      0x04020000, 0x04120000, 0x04020100, 0x04120100,
+      0x04020008, 0x04120008, 0x04020108, 0x04120108,
+      0x04021000, 0x04121000, 0x04021100, 0x04121100,
+      0x04021008, 0x04121008, 0x04021108, 0x04121108 ],
+
+    # for D bits (numbered as per FIPS 46) 1 2 3 4 5 6
+    [ 0x00000000, 0x10000000, 0x00010000, 0x10010000,
+      0x00000004, 0x10000004, 0x00010004, 0x10010004,
+      0x20000000, 0x30000000, 0x20010000, 0x30010000,
+      0x20000004, 0x30000004, 0x20010004, 0x30010004,
+      0x00100000, 0x10100000, 0x00110000, 0x10110000,
+      0x00100004, 0x10100004, 0x00110004, 0x10110004,
+      0x20100000, 0x30100000, 0x20110000, 0x30110000,
+      0x20100004, 0x30100004, 0x20110004, 0x30110004,
+      0x00001000, 0x10001000, 0x00011000, 0x10011000,
+      0x00001004, 0x10001004, 0x00011004, 0x10011004,
+      0x20001000, 0x30001000, 0x20011000, 0x30011000,
+      0x20001004, 0x30001004, 0x20011004, 0x30011004,
+      0x00101000, 0x10101000, 0x00111000, 0x10111000,
+      0x00101004, 0x10101004, 0x00111004, 0x10111004,
+      0x20101000, 0x30101000, 0x20111000, 0x30111000,
+      0x20101004, 0x30101004, 0x20111004, 0x30111004 ],
+
+    # for D bits (numbered as per FIPS 46) 8 9 11 12 13 14
+    [ 0x00000000, 0x08000000, 0x00000008, 0x08000008,
+      0x00000400, 0x08000400, 0x00000408, 0x08000408,
+      0x00020000, 0x08020000, 0x00020008, 0x08020008,
+      0x00020400, 0x08020400, 0x00020408, 0x08020408,
+      0x00000001, 0x08000001, 0x00000009, 0x08000009,
+      0x00000401, 0x08000401, 0x00000409, 0x08000409,
+      0x00020001, 0x08020001, 0x00020009, 0x08020009,
+      0x00020401, 0x08020401, 0x00020409, 0x08020409,
+      0x02000000, 0x0A000000, 0x02000008, 0x0A000008,
+      0x02000400, 0x0A000400, 0x02000408, 0x0A000408,
+      0x02020000, 0x0A020000, 0x02020008, 0x0A020008,
+      0x02020400, 0x0A020400, 0x02020408, 0x0A020408,
+      0x02000001, 0x0A000001, 0x02000009, 0x0A000009,
+      0x02000401, 0x0A000401, 0x02000409, 0x0A000409,
+      0x02020001, 0x0A020001, 0x02020009, 0x0A020009,
+      0x02020401, 0x0A020401, 0x02020409, 0x0A020409 ],
+
+    # for D bits (numbered as per FIPS 46) 16 17 18 19 20 21
+    [ 0x00000000, 0x00000100, 0x00080000, 0x00080100,
+      0x01000000, 0x01000100, 0x01080000, 0x01080100,
+      0x00000010, 0x00000110, 0x00080010, 0x00080110,
+      0x01000010, 0x01000110, 0x01080010, 0x01080110,
+      0x00200000, 0x00200100, 0x00280000, 0x00280100,
+      0x01200000, 0x01200100, 0x01280000, 0x01280100,
+      0x00200010, 0x00200110, 0x00280010, 0x00280110,
+      0x01200010, 0x01200110, 0x01280010, 0x01280110,
+      0x00000200, 0x00000300, 0x00080200, 0x00080300,
+      0x01000200, 0x01000300, 0x01080200, 0x01080300,
+      0x00000210, 0x00000310, 0x00080210, 0x00080310,
+      0x01000210, 0x01000310, 0x01080210, 0x01080310,
+      0x00200200, 0x00200300, 0x00280200, 0x00280300,
+      0x01200200, 0x01200300, 0x01280200, 0x01280300,
+      0x00200210, 0x00200310, 0x00280210, 0x00280310,
+      0x01200210, 0x01200310, 0x01280210, 0x01280310 ],
+
+    # for D bits (numbered as per FIPS 46) 22 23 24 25 27 28
+    [ 0x00000000, 0x04000000, 0x00040000, 0x04040000,
+      0x00000002, 0x04000002, 0x00040002, 0x04040002,
+      0x00002000, 0x04002000, 0x00042000, 0x04042000,
+      0x00002002, 0x04002002, 0x00042002, 0x04042002,
+      0x00000020, 0x04000020, 0x00040020, 0x04040020,
+      0x00000022, 0x04000022, 0x00040022, 0x04040022,
+      0x00002020, 0x04002020, 0x00042020, 0x04042020,
+      0x00002022, 0x04002022, 0x00042022, 0x04042022,
+      0x00000800, 0x04000800, 0x00040800, 0x04040800,
+      0x00000802, 0x04000802, 0x00040802, 0x04040802,
+      0x00002800, 0x04002800, 0x00042800, 0x04042800,
+      0x00002802, 0x04002802, 0x00042802, 0x04042802,
+      0x00000820, 0x04000820, 0x00040820, 0x04040820,
+      0x00000822, 0x04000822, 0x00040822, 0x04040822,
+      0x00002820, 0x04002820, 0x00042820, 0x04042820,
+      0x00002822, 0x04002822, 0x00042822, 0x04042822 ] )
+
+_shifts2 = (0,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0)
+
+_con_salt = [
+    0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,
+    0xDA,0xDB,0xDC,0xDD,0xDE,0xDF,0xE0,0xE1,
+    0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,
+    0xEA,0xEB,0xEC,0xED,0xEE,0xEF,0xF0,0xF1,
+    0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,
+    0xFA,0xFB,0xFC,0xFD,0xFE,0xFF,0x00,0x01,
+    0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,
+    0x0A,0x0B,0x05,0x06,0x07,0x08,0x09,0x0A,
+    0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,
+    0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,
+    0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0x21,0x22,
+    0x23,0x24,0x25,0x20,0x21,0x22,0x23,0x24,
+    0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,
+    0x2D,0x2E,0x2F,0x30,0x31,0x32,0x33,0x34,
+    0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,
+    0x3D,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44 ]
+
+_cov_2char = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+
+
+def _HPERM_OP(a):
+    """Clever bit manipulation."""
+    t = ((a << 18) ^ a) & 0xcccc0000
+    return a ^ t ^ ((t >> 18) & 0x3fff)
+
+def _PERM_OP(a,b,n,m):
+    """Cleverer bit manipulation."""
+    t = ((a >> n) ^ b) & m
+    b = b ^ t
+    a = a ^ (t << n)
+    return a,b
+
+
+def _set_key(password):
+    """Generate DES key schedule from ASCII password."""
+
+    c,d = struct.unpack('<ii', password)
+    c = (c & 0x7f7f7f7f) << 1
+    d = (d & 0x7f7f7f7f) << 1
+
+    d,c = _PERM_OP(d,c,4,0x0f0f0f0f)
+    c = _HPERM_OP(c)
+    d = _HPERM_OP(d)
+    d,c = _PERM_OP(d,c,1,0x55555555)
+    c,d = _PERM_OP(c,d,8,0x00ff00ff)
+    d,c = _PERM_OP(d,c,1,0x55555555)
+
+    # Any sign-extended bits are masked off.
+    d = (((d & 0x000000ff) << 16) | (d & 0x0000ff00) |
+         ((d & 0x00ff0000) >> 16) | ((c >> 4) & 0x0f000000))
+    c = c & 0x0fffffff
+
+    # Copy globals into local variables for loop.
+    shifts2 = _shifts2
+    skbc0, skbc1, skbc2, skbc3, skbd0, skbd1, skbd2, skbd3 = _skb
+
+    k = [0] * (_ITERATIONS * 2)
+
+    for i in range(_ITERATIONS):
+        # Only operates on top 28 bits.
+        if shifts2[i]:
+            c = (c >> 2) | (c << 26)
+            d = (d >> 2) | (d << 26)
+        else:
+            c = (c >> 1) | (c << 27)
+            d = (d >> 1) | (d << 27)
+        c = c & 0x0fffffff
+        d = d & 0x0fffffff
+
+        s = ( skbc0[  c      & 0x3f                    ] |
+              skbc1[((c>> 6) & 0x03) | ((c>> 7) & 0x3c)] |
+              skbc2[((c>>13) & 0x0f) | ((c>>14) & 0x30)] |
+              skbc3[((c>>20) & 0x01) |
+                    ((c>>21) & 0x06) | ((c>>22) & 0x38)] )
+
+        t = ( skbd0[  d      & 0x3f                    ] |
+              skbd1[((d>> 7) & 0x03) | ((d>> 8) & 0x3c)] |
+              skbd2[((d>>15) & 0x3f)                   ] |
+              skbd3[((d>>21) & 0x0f) | ((d>>22) & 0x30)] )
+
+        k[2*i] = ((t << 16) | (s & 0x0000ffff)) & 0xffffffff
+        s = (s >> 16) | (t & 0xffff0000)
+
+        # Top bit of s may be 1.
+        s = (s << 4) | ((s >> 28) & 0x0f)
+        k[2*i + 1] = s & 0xffffffff
+
+    return k
+
+
+def _body(ks, E0, E1):
+    """Use the key schedule ks and salt E0, E1 to create the password hash."""
+
+    # Copy global variable into locals for loop.
+    SP0, SP1, SP2, SP3, SP4, SP5, SP6, SP7 = _SPtrans
+
+    inner = range(0, _ITERATIONS*2, 2)
+    l = r = 0
+    for j in range(25):
+        l,r = r,l
+        for i in inner:
+            t = r ^ ((r >> 16) & 0xffff)
+            u = t & E0
+            t = t & E1
+            u = u ^ (u << 16) ^ r ^ ks[i]
+            t = t ^ (t << 16) ^ r ^ ks[i+1]
+            t = ((t >> 4) & 0x0fffffff) | (t << 28)
+
+            l,r = r,(SP1[(t    ) & 0x3f] ^ SP3[(t>> 8) & 0x3f] ^
+                     SP5[(t>>16) & 0x3f] ^ SP7[(t>>24) & 0x3f] ^
+                     SP0[(u    ) & 0x3f] ^ SP2[(u>> 8) & 0x3f] ^
+                     SP4[(u>>16) & 0x3f] ^ SP6[(u>>24) & 0x3f] ^ l)
+
+    l = ((l >> 1) & 0x7fffffff) | ((l & 0x1) << 31)
+    r = ((r >> 1) & 0x7fffffff) | ((r & 0x1) << 31)
+
+    r,l = _PERM_OP(r, l,  1, 0x55555555)
+    l,r = _PERM_OP(l, r,  8, 0x00ff00ff)
+    r,l = _PERM_OP(r, l,  2, 0x33333333)
+    l,r = _PERM_OP(l, r, 16, 0x0000ffff)
+    r,l = _PERM_OP(r, l,  4, 0x0f0f0f0f)
+
+    return l,r
+
+
+def crypt(password, salt):
+    """Generate an encrypted hash from the passed password.  If the password
+is longer than eight characters, only the first eight will be used.
+
+The first two characters of the salt are used to modify the encryption
+algorithm used to generate in the hash in one of 4096 different ways.
+The characters for the salt must be alphanumeric, '.' or '/'.
+
+The returned hash begins with the two characters of the salt, and
+should be passed as the salt to verify the password.
+
+Example:
+
+  >>> from fcrypt import crypt
+  >>> password = 'AlOtBsOl'
+  >>> salt = 'cE'
+  >>> hash = crypt(password, salt)
+  >>> hash
+  'cEpWz5IUCShqM'
+  >>> crypt(password, hash) == hash
+  1
+  >>> crypt('IaLaIoK', hash) == hash
+  0
+
+In practice, you would read the password using something like the
+getpass module, and generate the salt randomly:
+
+  >>> import random, string
+  >>> saltchars = string.letters + string.digits + './'
+  >>> salt = random.choice(saltchars) + random.choice(saltchars)
+"""
+
+    if len(salt) < 2:
+        salt = salt + 'AA'
+
+    Eswap0 = _con_salt[ord(salt[0])]
+    Eswap1 = _con_salt[ord(salt[1])] << 4
+
+    ks = _set_key((password + '\0\0\0\0\0\0\0\0')[:8])
+    out1,out2 = _body(ks, Eswap0, Eswap1)
+
+    # Convert numbers to big-endian...
+    be1, be2 = struct.unpack('>ii', struct.pack('<ii', out1, out2))
+    # then extract 24-bit subsets.
+    b24 = [(be1 >> 8) & 0xffffff,
+           ((be1 << 16) & 0xff0000) | ((be2 >> 16) & 0xffff),
+           (be2 << 8) & 0xffff00]
+
+    # Convert to ASCII encoding, 4 characters for each 24 bits.
+    res = [salt[0], salt[1]]
+    for b in b24:
+        for i in range(18, -6, -6):
+            res.append(_cov_2char[(b >> i) & 0x3f])
+
+    return string.join(res[:13], '')
+
+def _test():
+    """Run doctest on fcrypt module."""
+    import doctest, fcrypt
+    return doctest.testmod(fcrypt)
+
+if __name__ == '__main__':
+    _test()
diff --git a/ZopeProducts/exUserFolder/CryptoSources/fcrypt/setup.py b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..33ccb6d33cbd5718150a07e75ca9a12e1c1a3724
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/fcrypt/setup.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+# distutils setup script for fcrypt.
+#
+# Copyright (C) 2000, 2001  Carey Evans  <careye@spamcop.net>
+
+from distutils.core import setup
+
+setup( name = 'fcrypt',
+       version = '1.2',
+       description = 'The Unix password crypt function.',
+       author = 'Carey Evans',
+       author_email = 'careye@spamcop.net',
+       url = 'http://home.clear.net.nz/pages/c.evans/sw/',
+       licence = 'BSD',
+       long_description = """\
+A pure Python implementation of the Unix DES password crypt function,
+based on Eric Young's fcrypt.c.  It works with any version of Python
+from version 1.5 or higher, and because it's pure Python it doesn't
+need a C compiler to install it.""",
+
+       py_modules = ['fcrypt'] )
diff --git a/ZopeProducts/exUserFolder/CryptoSources/pass_crypt.py b/ZopeProducts/exUserFolder/CryptoSources/pass_crypt.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef1d295470ddbce81867a11ec842ecd6c2e38da3
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/pass_crypt.py
@@ -0,0 +1,44 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: pass_crypt.py,v 1.3 2004/11/18 09:24:46 akm Exp $
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import CryptoPluginRegister
+
+try:
+	from crypt import crypt
+except:
+	from fcrypt.fcrypt import crypt
+
+
+def cryptPassword(authSource, username, password):
+	u = authSource.listOneUser(username)
+	if not u:
+		salt = username[:2]
+	else:
+		salt=u[0]['password'][:2]
+
+	secret = crypt(password, salt)
+	return secret
+	
+
+CryptPlugin=CryptoPluginRegister('Crypt', 'crypt', 'Crypt', cryptPassword)
+exUserFolder.cryptoSources['Crypt']=CryptPlugin
diff --git a/ZopeProducts/exUserFolder/CryptoSources/pass_md5.py b/ZopeProducts/exUserFolder/CryptoSources/pass_md5.py
new file mode 100644
index 0000000000000000000000000000000000000000..c67af1401e443ccbac7ee8e1f07d4d71570559d0
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/pass_md5.py
@@ -0,0 +1,47 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: pass_md5.py,v 1.1 2004/11/10 14:15:52 akm Exp $
+
+import md5, base64, string
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import CryptoPluginRegister
+
+# Simple digest
+def cryptPassword(authSource, username, password):
+	digest = md5.new()
+	digest.update(password)
+	digest = digest.digest()
+	secret = string.strip(base64.encodestring(digest))
+	return secret
+
+# Digest includes username
+# So two passwords for different users hash differently
+def cryptPassword2(authSource, username, password):
+	newPass = username+':'+password
+	return cryptPassword(authSource, username, newPass)
+
+
+MD5Plugin1=CryptoPluginRegister('MD51', 'MD5', 'MD5 Password Only', cryptPassword)
+exUserFolder.cryptoSources['MD51']=MD5Plugin1
+
+MD5Plugin2=CryptoPluginRegister('MD52', 'MD5', 'MD5 Username + Password', cryptPassword2)
+exUserFolder.cryptoSources['MD52']=MD5Plugin2
diff --git a/ZopeProducts/exUserFolder/CryptoSources/pass_plain.py b/ZopeProducts/exUserFolder/CryptoSources/pass_plain.py
new file mode 100644
index 0000000000000000000000000000000000000000..2d29c78d8611b926595ddbf063dffc064e24abe0
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/pass_plain.py
@@ -0,0 +1,31 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: pass_plain.py,v 1.1 2004/11/10 14:15:52 akm Exp $
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import CryptoPluginRegister
+
+# Simple digest
+def cryptPassword(authSource, username, password):
+	return password
+
+PlainPlugin=CryptoPluginRegister('Plaintext', 'Plaintext', 'No Encryption', cryptPassword)
+exUserFolder.cryptoSources['Plaintext']=PlainPlugin
diff --git a/ZopeProducts/exUserFolder/CryptoSources/pass_sha.py b/ZopeProducts/exUserFolder/CryptoSources/pass_sha.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4bc8fd3f3a151d12ba29647b9fc2095c7c479e5
--- /dev/null
+++ b/ZopeProducts/exUserFolder/CryptoSources/pass_sha.py
@@ -0,0 +1,41 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: pass_sha.py,v 1.1 2004/11/10 14:15:52 akm Exp $
+
+import sha
+from base64 import encodestring
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import CryptoPluginRegister
+
+
+def cryptPassword(authSource, username, password):
+	return encodestring(sha.new(password).digest())
+
+def cryptPassword2(authSource, username, password):
+	newPass = username+':'+password
+	return cryptPassword(authSource, username, newPass)
+
+SHAPlugin1=CryptoPluginRegister('SHA1', 'SHA', 'SHA Password Only', cryptPassword)
+exUserFolder.cryptoSources['SHA1']=SHAPlugin1
+
+SHAPlugin2=CryptoPluginRegister('SHA2', 'SHA', 'SHA Username + Password', cryptPassword2)
+exUserFolder.cryptoSources['SHA2']=SHAPlugin2
diff --git a/ZopeProducts/exUserFolder/Extensions/.cvsignore b/ZopeProducts/exUserFolder/Extensions/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/Extensions/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/Extensions/getOldGroups.py b/ZopeProducts/exUserFolder/Extensions/getOldGroups.py
new file mode 100644
index 0000000000000000000000000000000000000000..9fa8088ee18d1f239ebde5d3ec4996e0203c591a
--- /dev/null
+++ b/ZopeProducts/exUserFolder/Extensions/getOldGroups.py
@@ -0,0 +1,20 @@
+# This script interrogates the old-skool NuxUserGroups_support_branch
+# group structure and outputs a tab-delimited file you can send to
+# loadOldGroups.  Just in case anyone is using it. :-)
+#
+# Matt Behrens <matt.behrens@kohler.com>
+
+def getOldGroups(self):
+    "Reconstruct a group list from the old-style _groups property"
+    from string import join
+    props = self.currentPropSource.userProperties
+    groups = {}
+    for username in props.keys():
+	for groupname in props[username].getProperty('_groups', ()):
+	    if not groups.has_key(groupname):
+		groups[groupname] = []
+	    groups[groupname].append(username)
+    out = ''
+    for groupname in groups.keys():
+	out = out + '%s	%s\n' % (groupname, join(groups[groupname], '	'))
+    return out
diff --git a/ZopeProducts/exUserFolder/Extensions/loadOldGroups.py b/ZopeProducts/exUserFolder/Extensions/loadOldGroups.py
new file mode 100644
index 0000000000000000000000000000000000000000..b7f2f70295438c293ab03e0493d82f0d9d430434
--- /dev/null
+++ b/ZopeProducts/exUserFolder/Extensions/loadOldGroups.py
@@ -0,0 +1,26 @@
+# This takes 'old_groups.txt' from var (create it using getOldGroups)
+# and sets up all the groups therein using NuxUserGroups calls.  This
+# will load a group source if you need to do such a thing.
+#
+# Matt Behrens <matt.behrens@kohler.com>
+
+def loadOldGroups(self):
+    from os.path import join as pathJoin
+    from string import split, strip
+
+    groups_file = open(pathJoin(CLIENT_HOME, 'old_groups.txt'), 'r')
+    out = ''
+    for group_line in groups_file.readlines():
+	group_line_elements = split(strip(group_line), '	')
+	group_name = group_line_elements[0]
+	group_members = group_line_elements[1:]
+
+	if self.getGroupById(group_name, default=None) is None:
+	    out = out + 'adding group %s\n' % group_name
+	    self.userFolderAddGroup(group_name)
+
+	out = out + 'setting group %s membership to %s\n' % (group_name, group_members)
+	self.setUsersOfGroup(group_members, group_name)
+
+    return out
+
diff --git a/ZopeProducts/exUserFolder/Extensions/usAuthSourceMethods.py b/ZopeProducts/exUserFolder/Extensions/usAuthSourceMethods.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb43162df31ca38f4848ef2afdcfaf08e72db0de
--- /dev/null
+++ b/ZopeProducts/exUserFolder/Extensions/usAuthSourceMethods.py
@@ -0,0 +1,140 @@
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: usAuthSourceMethods.py,v 1.3 2001/12/01 08:40:04 akm Exp $
+#
+########################################################################
+#
+# This is an example of an Extension Module to provide User Supplied 
+# Authentication Methods.
+# 
+# It mimics the behaviour of the pgAuthSource Module, and the sql queries
+# Used here would be added as ZSQLMethods in the usAuthSource Folder.
+# (you can basically cut and paste them from the bottom of this .py file
+# into the ZSQL Method Template Area
+#
+# It's not complete, but, you do get the idea...
+#
+# Each function becomes usFunctionName
+#
+# e.g. listOneUser -> usListOneUser
+#
+import string
+from crypt import crypt
+
+def listOneUser(self,username):
+	users = []
+	result=self.sqlListOneUser(username=username)
+	for n in result:
+		username=sqlattr(n,'username')
+		password=sqlattr(n,'password')
+		roles=string.split(sqlattr(n,'roles'))
+		N={'username':username, 'password':password, 'roles':roles}
+		users.append(N)
+	return users
+
+def listUsers(self):
+	"""Returns a list of user names or [] if no users exist"""		
+	users = []
+	result=self.sqlListUsers()
+	for n in result:
+		username=sqlattr(n,'username')
+		N={'username':username}
+		users.append(N)
+	return users	
+
+def getUsers(self):
+	"""Return a list of user objects or [] if no users exist"""
+	data=[]
+	try:    items=self.listusers()
+	except: return data
+	for people in items:
+		roles=string.split(people['roles'],',')
+		user=User(people['username'], roles, '')
+		data.append(user)
+	return data
+
+def cryptPassword(self, username, password):
+		salt =username[:2]
+		secret = crypt(password, salt)
+		return secret
+
+def deleteUsers(self, userids):
+	for uid in userids:
+		self.sqlDeleteOneUser(userid=uid)
+
+
+# Helper Functions...
+from string import upper, lower
+import Missing
+mt=type(Missing.Value)
+
+def typeconv(val):
+    if type(val)==mt:
+        return ''
+    return val
+
+def sqlattr(ob, attr):
+    name=attr
+    if hasattr(ob, attr):
+        return typeconv(getattr(ob, attr))
+    attr=upper(attr)
+    if hasattr(ob, attr):
+        return typeconv(getattr(ob, attr))
+    attr=lower(attr)
+    if hasattr(ob, attr):
+        return typeconv(getattr(ob, attr))
+    raise NameError, name
+
+
+########################################################################
+# SQL METHODS USED ABOVE
+# PASTE INTO ZSQL METHODS
+# take note of what parameters are used in each query
+########################################################################
+
+_sqlListUsers="""
+SELECT * FROM passwd
+"""
+
+_sqlListOneUser="""
+SELECT * FROM passwd
+where username=<dtml-sqlvar username type=string>
+"""
+
+_sqlDeleteOneUser="""
+DELETE FROM passwd
+where uid=<dtml-sqlvar userid type=int>
+"""
+
+_sqlInsertUser="""
+INSERT INTO passwd (username, password, roles)
+VALUES (<dtml-sqlvar username type=string>,
+        <dtml-sqlvar password type=string>,
+		<dtml-sqlvar roles type=string>)
+"""
+
+_sqlUpdateUserPassword="""
+UPDATE passwd set password=<dtml-sqlvar password type=string>
+WHERE username=<dtml-sqlvar username type=string>
+"""
+
+_sqlUpdateUser="""
+UPDATE passwd set roles=<dtml-sqlvar roles type=string>
+WHERE username=<dtml-sqlvar username type=string>
+"""
+
diff --git a/ZopeProducts/exUserFolder/GroupSource/.cvsignore b/ZopeProducts/exUserFolder/GroupSource/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSource/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/GroupSource/GroupSource.py b/ZopeProducts/exUserFolder/GroupSource/GroupSource.py
new file mode 100644
index 0000000000000000000000000000000000000000..478ab0b3e9486cfe4678fe3bfe9c9be47899f348
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSource/GroupSource.py
@@ -0,0 +1,32 @@
+#
+# Extensible User Folder
+# 
+# Null Group Source for exUserFolder
+#
+# Author: Brent Hendricks <bmh@users.sourceforge.net>
+# $Id: GroupSource.py,v 1.1 2002/12/02 23:20:49 bmh Exp $
+from Globals import DTMLFile
+
+
+manage_addGroupSourceForm=DTMLFile('manage_addGroupSourceForm', globals(), __name__='manage_addGroupSourceForm')
+
+
+def manage_addGroupSource(dispatcher, REQUEST):
+	""" Add a Group Source """
+
+	# Get the XUF object we're being added to
+	xuf = dispatcher.Destination()
+	
+	groupId = REQUEST.get('groupId', None)
+	if groupId:
+		# Invoke the add method for this plugin
+		xuf.groupSources[groupId].manage_addMethod(xuf, REQUEST)
+	else:
+		raise "BadRequest", "Required parameter 'groupId' omitted"
+
+	dispatcher.manage_main(dispatcher, REQUEST)
+	
+
+class GroupSource:
+	pass
+
diff --git a/ZopeProducts/exUserFolder/GroupSource/__init__.py b/ZopeProducts/exUserFolder/GroupSource/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc5d3a81ae83df074a2e62579796a6ebc612c629
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSource/__init__.py
@@ -0,0 +1,2 @@
+# $Id: __init__.py,v 1.1 2002/12/02 23:20:49 bmh Exp $
+import GroupSource
diff --git a/ZopeProducts/exUserFolder/GroupSource/manage_addGroupSourceForm.dtml b/ZopeProducts/exUserFolder/GroupSource/manage_addGroupSourceForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..3e6605a0ab1c79857f63c384d472e86202741bec
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSource/manage_addGroupSourceForm.dtml
@@ -0,0 +1,33 @@
+<dtml-var manage_page_header>
+  <dtml-if currentGroupSource>
+    <dtml-var "MessageDialog(title='Group Source Exists', message='Error: There is already a group source here.  Please delete it first', action='manage_main')">
+  <dtml-elif allDone>
+    <dtml-var expr="manage_addGroupSource(REQUEST)">
+  <dtml-elif groupId>
+    <dtml-call "REQUEST.set('groupForm',doGroupSourceForm(groupId=groupId))">
+    <dtml-var "groupForm(mapping=_)">
+  <dtml-else>
+    <dtml-var "DialogHeader(_.None,_,DialogTitle='Add eXtensible User Folder Group Source')">
+    <form action="&dtml-URL;" method="post">
+      <table cellspacing="2">
+	<tr>
+	  <td align="left" valign="top">
+	    <b><dtml-babel src="'en'">Group Source</dtml-babel></b>
+	  </td>
+	  <td>
+	    <select name="groupId">
+	      <dtml-in getGroupSources sort="name">
+		<option value="<dtml-var "_['sequence-item'].name">"><dtml-var description></option>
+	      </dtml-in>
+	    </select>
+	  </td>
+	</tr>
+	<tr>
+	  <td></td>
+	  <td><br><input type="submit" value=" <dtml-babel src="'en'">Add</dtml-babel> "></td>
+	</tr>
+      </table>
+    </form>
+    <dtml-var DialogFooter>
+  </dtml-if>
+<dtml-var manage_page_footer>
\ No newline at end of file
diff --git a/ZopeProducts/exUserFolder/GroupSources/__init__.py b/ZopeProducts/exUserFolder/GroupSources/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f36cdc51e2d7f908a4f3c714668294bd162019e
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSources/__init__.py
@@ -0,0 +1,31 @@
+#
+# Extensible User Folder
+# 
+# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: __init__.py,v 1.1 2004/11/10 14:15:53 akm Exp $
+
+import nullGroupSource
+
+# If this fails due to NUG being absent, just skip it
+try:
+	import zodbGroupSource
+except ImportError:
+	pass
+
+
diff --git a/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/.cvsignore b/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/__init__.py b/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b894336907423a948496d1f44a8cf26aa535d083
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/__init__.py
@@ -0,0 +1,2 @@
+# $Id: __init__.py,v 1.1 2004/11/10 14:15:53 akm Exp $
+import nullGroupSource
diff --git a/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/manage_addNullPluginSourceForm.dtml b/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/manage_addNullPluginSourceForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..f57a2c3a37781057a50762e99ab1ff87eef4a6e8
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/manage_addNullPluginSourceForm.dtml
@@ -0,0 +1,21 @@
+<dtml-var "DialogHeader(_.None, _, DialogTitle='Add Basic Group Source', dialog_width='')">
+<FORM ACTION="&dtml-URL;" METHOD="POST">
+		<dtml-in "REQUEST.form.keys()">
+			<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
+				<dtml-let listVar=sequence-item>
+					<dtml-in "REQUEST[listVar]">
+						<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
+					</dtml-in>
+				</dtml-let>
+			<dtml-else>
+				<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
+			</dtml-if>
+
+		</dtml-in>
+
+<input type="HIDDEN" name="allDone" value="1">
+<b><dtml-babel src="'en'">This Group Source has no configuration Items</dtml-babel></b><br>
+<br>
+<input type="SUBMIT" value="<dtml-babel src="'en'">Add</dtml-babel>">
+</form>
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/nullGroupSource.py b/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/nullGroupSource.py
new file mode 100644
index 0000000000000000000000000000000000000000..cee50ff96e5309e6596609504a1b193b56739c88
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSources/nullGroupSource/nullGroupSource.py
@@ -0,0 +1,34 @@
+#
+# Extensible User Folder
+# 
+# Null Group Source for exUserFolder
+#
+# Author: Brent Hendricks <bmh@users.sourceforge.net>
+# $Id: nullGroupSource.py,v 1.1 2004/11/10 14:15:53 akm Exp $
+from Globals import HTMLFile, INSTANCE_HOME
+
+from OFS.Folder import Folder
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import PluginRegister
+from Products.exUserFolder.nullPlugin import nullPlugin
+
+def manage_addNullGroupSource(self, REQUEST):
+	""" Add a Group Source """
+	self.currentGroupSource=None
+	return ''
+
+
+manage_addNullGroupSourceForm=HTMLFile('manage_addNullPluginSourceForm',globals())
+manage_editNullGroupSourceForm=None
+
+		
+nullGroupReg=PluginRegister('nullGroupSource',
+						   'Null Group Source',
+						   nullPlugin,
+						   manage_addNullGroupSourceForm,
+						   manage_addNullGroupSource,
+						   manage_editNullGroupSourceForm)
+
+exUserFolder.groupSources['nullGroupSource']=nullGroupReg
+
diff --git a/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/.cvsignore b/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/__init__.py b/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c2e979ba1b765d278adc2e56bc484a7ccd9f768d
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/__init__.py
@@ -0,0 +1,2 @@
+# $Id: __init__.py,v 1.1 2004/11/10 14:15:54 akm Exp $
+import zodbGroupSource
diff --git a/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/manage_addzodbGroupSourceForm.dtml b/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/manage_addzodbGroupSourceForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..2d618cf5f955b3945ad728609ad21beb39ec9ae1
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/manage_addzodbGroupSourceForm.dtml
@@ -0,0 +1,21 @@
+<dtml-var "DialogHeader(_.None,_,DialogTitle='Add ZODB Group Source')">
+
+<FORM ACTION="&dtml-URL;" METHOD="POST">
+		<dtml-in "REQUEST.form.keys()">
+			<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
+				<dtml-let listVar=sequence-item>
+					<dtml-in "REQUEST[listVar]">
+						<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
+					</dtml-in>
+				</dtml-let>
+			<dtml-else>
+				<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
+			</dtml-if>
+
+		</dtml-in>
+
+<input type="HIDDEN" name="allDone" value="1">
+<b><dtml-babel src="'en'">This group source requires no user configuration items at this time.</dtml-babel></b><br>
+<INPUT TYPE="SUBMIT" VALUE=" <dtml-babel src="'en'">NEXT</dtml-babel> ">
+</FORM>
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/manage_editzodbGroupSourceForm.dtml b/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/manage_editzodbGroupSourceForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..3c04422e7e69a22b403edf0aa8df5ce06451f665
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/manage_editzodbGroupSourceForm.dtml
@@ -0,0 +1,7 @@
+<dtml-var "DialogHeader(_.None,_,DialogTitle='ZODB Group Source',dialog_width='100%')">
+<dtml-var manage_tabs>
+<FORM ACTION="manage_main" METHOD="POST">
+<b><dtml-babel src="'en'">This group source requires no user configuration items at this time.</dtml-babel></b><br>
+<INPUT TYPE="SUBMIT" VALUE=" <dtml-babel src="'en'">OK</dtml-babel> ">
+</FORM>
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/zodbGroupSource.py b/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/zodbGroupSource.py
new file mode 100644
index 0000000000000000000000000000000000000000..78b441b633e06d8a7a5decf774fd15fe45ade8ef
--- /dev/null
+++ b/ZopeProducts/exUserFolder/GroupSources/zodbGroupSource/zodbGroupSource.py
@@ -0,0 +1,177 @@
+#
+# Extensible User Folder
+# 
+# ZODB Group Source for exUserFolder
+#
+# Author: Brent Hendricks <mh@users.sourceforge.net>
+# $Id: zodbGroupSource.py,v 1.1 2004/11/10 14:15:54 akm Exp $
+from Globals import HTMLFile, MessageDialog, INSTANCE_HOME,Acquisition, PersistentMapping
+
+from OFS.Folder import Folder
+
+from Products.ZSQLMethods.SQL import SQL
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import PluginRegister
+from Products.NuxUserGroups.UserFolderWithGroups import Group, _marker
+
+import time
+import zLOG
+import sys
+
+manage_addGroupSourceForm=HTMLFile('manage_addzodbGroupSourceForm', globals())
+
+def manage_addzodbGroupSource(self, REQUEST):
+	""" Add a ZODB Group Source """
+
+	o = zodbGroupSource()
+	self._setObject('zodbGroupSource', o, None, None, 0)
+	o = getattr(self, 'zodbGroupSource')
+
+	# Allow Prop Source to setup default users...
+	if hasattr(o, 'postInitialisation'):
+		o.postInitialisation(REQUEST)
+	self.currentGroupSource=o
+
+manage_addzodbGroupSourceForm=HTMLFile('manage_addzodbGroupSourceForm', globals())
+manage_editzodbGroupSourceForm=HTMLFile('manage_editzodbGroupSourceForm', globals())
+
+#
+# Very very simple thing, used as an example of how to write a property source
+# Not recommended for large scale production sites...
+#
+
+class zodbGroupSource(Folder):
+	""" Store Group Data inside ZODB, the simplistic way """
+
+	meta_type='Group Source'
+	title='Simplistic ZODB Groups'
+	icon ='misc_/exUserFolder/exUserFolderPlugin.gif'	
+	manage_editForm=manage_editzodbGroupSourceForm
+	manage_tabs=Acquisition.Acquired
+	
+	def __init__(self):
+		self.id='zodbGroupSource'
+		self.groups=PersistentMapping()
+
+
+	def addGroup(self, groupname, title='', users=(), **kw):
+		"""Creates a group"""
+		if self.groups.has_key(groupname):
+			raise ValueError, 'Group "%s" already exists' % groupname
+		a = 'before: groupname %s groups %s' % (groupname, self.groups)
+		group = apply(Group, (groupname,), kw)
+		group.setTitle(title)
+		group._setUsers(users)
+		self.groups[groupname] = group
+
+
+	def getGroup(self, groupname, default=_marker):
+		"""Returns the given group"""
+		try:
+			group = self.groups[groupname]
+		except KeyError:
+			if default is _marker: raise
+			return default
+		return group
+
+	
+	def delGroup(self, groupname):
+		"""Deletes the given group"""
+		usernames = self.groups[groupname].getUsers()
+		#self.delUsersFromGroup(usernames, groupname)
+		del self.groups[groupname]
+
+	
+	def listGroups(self):
+		"""Returns a list of group names"""
+		return tuple(self.groups.keys())
+	
+
+	def getGroupsOfUser(self, username):
+		"Get a user's groups"
+		groupnames = []
+		allnames = self.listGroups()
+		groupnames = filter(lambda g, u=username, self=self: u in self.groups[g].getUsers(), allnames)
+		return tuple(groupnames)
+
+	
+	def setGroupsOfUser(self, groupnames, username):
+		"Set a user's groups"
+		oldGroups = self.getGroupsOfUser(username)
+		self.delGroupsFromUser(oldGroups, username)
+		self.addGroupsToUser(groupnames, username)
+
+
+	def addGroupsToUser(self, groupnames, username):
+		"Add groups to a user"
+		for name in groupnames:
+			group = self.groups[name]
+			if not username in group.getUsers():
+				group._addUsers([username])
+
+
+	def delGroupsFromUser(self, groupnames, username):
+		"Delete groups from a user"
+		for name in groupnames:
+			group = self.groups[name]
+			if username in group.getUsers():
+				group._delUsers([username])
+		
+	
+	def getUsersOfGroup(self, groupname):
+		"Get the users in a group"
+		return self.groups[groupname].getUsers()
+
+	
+	def setUsersOfGroup(self, usernames, groupname):
+		"Set the users in a group"
+		# uniquify
+		dict = {}
+		for u in usernames: dict[u] = None
+		usernames = dict.keys()
+
+		self.groups[groupname]._setUsers(usernames)
+
+
+	def addUsersToGroup(self, usernames, groupname):
+		"Add users to a group"
+		# uniquify
+		dict = {}
+		for u in usernames: dict[u] = None
+		usernames = dict.keys()
+
+		self.groups[groupname]._addUsers(usernames)
+		
+
+	def delUsersFromGroup(self, usernames, groupname):
+		"Delete users from a group"
+		# uniquify
+		dict = {}
+		for u in usernames: dict[u] = None
+		usernames = dict.keys()
+
+		self.groups[groupname]._delUsers(usernames)
+
+
+	def deleteUsers(self, usernames):
+		"Delete a list of users"
+		for user in usernames:
+			groups = self.getGroupsOfUser(user)
+			self.delGroupsFromUser(groups, user)
+
+
+	def postInitialisation(self, REQUEST):
+		pass
+
+
+        def manage_beforeDelete(self, item, container):
+                # Notify the exUserFolder that it doesn't have a group source anymore
+                container.currentGroupSource=None
+
+
+zodbGroupReg=PluginRegister('zodbGroupSource','Simplistic ZODB Group Source',
+						   zodbGroupSource, manage_addzodbGroupSourceForm,
+						   manage_addzodbGroupSource,
+						   manage_editzodbGroupSourceForm)
+exUserFolder.groupSources['zodbGroupSource']=zodbGroupReg
diff --git a/ZopeProducts/exUserFolder/I18N/ZBabelDictionary_txt.zexp b/ZopeProducts/exUserFolder/I18N/ZBabelDictionary_txt.zexp
new file mode 100644
index 0000000000000000000000000000000000000000..be6e17c2c0eea657d458e27f3931101ddc8200b0
Binary files /dev/null and b/ZopeProducts/exUserFolder/I18N/ZBabelDictionary_txt.zexp differ
diff --git a/ZopeProducts/exUserFolder/LICENSE b/ZopeProducts/exUserFolder/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..3a9694866797ad1ba19ab3d5168cbfdcc411bc1a
--- /dev/null
+++ b/ZopeProducts/exUserFolder/LICENSE
@@ -0,0 +1,91 @@
+XUF as a whole is covered by the BSD License, however it uses software 
+covered by other compatible licenses (see below)
+
+------------------------------------------------------------------------
+
+All of the documentation and software included in the exUserFolder
+Releases is copyrighted by The Internet (Aust) Pty Ltd and contributors
+ACN: 082 081 472  ABN: 83 082 081 472
+
+Copyright 2001, 2002 The Internet (Aust) Pty Ltd
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+
+------------------------------------------------------------------------
+
+This product includes software developed by Digital Creations for use in 
+the Z Object Publishing Environment (http://www.zope.org/) 
+
+Portions of smbAuthSource Copyright (C) 2001 Michael Teo 
+
+Portions of radiusAuthSource Copyright (C) 1999 Stuart Bishop 
+
+fcrypt is Copyright (C) 2001, 2001 Carey Evans 
+
+This product includes cryptographic software written by Eric Young 
+(eay@mincom.oz.au)
+
+------------------------------------------------------------------------
+
+Brief discussion of what the license means to you, not meant to be
+all encompassing, but, to give you the general idea. This editorial does
+not need to be distributed d8)
+
+If you want to incorporate this product (or parts of it) into a commercial 
+product that's fine.
+
+If you want to modify this product that's fine.
+
+If you want to modify and distribute this product that's fine (even in
+commercial products).
+
+If you want to incorporate this into a larger work that's fine (even
+if that work has a different license).
+
+None of the previous items place any obligation of notification, compensation,
+or return of code to us. In fact we don't care if you do these things. Go
+forth and prosper. Basically as long as you recognise that this doesn't
+belong to you, you can do what you want with it even charge money for it.
+
+Note: If you do distribute this as source, then the XUF components are
+removable and distributable independently of your license as a whole
+(although that's a lot of trouble to go to when they could just download it
+from the same place you did).
+
+What you can't do, is claim it's yours, and this one thing encompasses a lot
+of things, here's a few.
+
+If it's not yours you can't;
+
+Change the license even if you change the code since the copyright
+of the modified files remains with the original copyright holders.
+
+Use bits of it inside products that require the license to change, because
+only the copyright holders have the right to modify the license (not a
+concern for commercial projects, only some other Free/Open Source licenses).
+
+Assign the copyright or other IP to any other party of the whole or any
+part (even if you change the code), because it's not yours to give away or
+sell to a 3rd party.
+
+If the fact you can almost do whatever you want with this code isn't 
+liberal enough for you, contact us and we'll see what we can arrange.
diff --git a/ZopeProducts/exUserFolder/LoginRequiredMessages.py b/ZopeProducts/exUserFolder/LoginRequiredMessages.py
new file mode 100644
index 0000000000000000000000000000000000000000..1bee917315af9ea4abe41df1f918e72dc5175347
--- /dev/null
+++ b/ZopeProducts/exUserFolder/LoginRequiredMessages.py
@@ -0,0 +1,27 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: LoginRequiredMessages.py,v 1.2 2001/12/01 08:40:03 akm Exp $
+
+LoginRequiredMessages={
+	'session_expired':'Your Session has Expired',
+	'unauthorized':'Please Login',
+	'login_failed':'Login Failed',
+	}
diff --git a/ZopeProducts/exUserFolder/MembershipSources/__init__.py b/ZopeProducts/exUserFolder/MembershipSources/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..9dc1d5ff8aead49e04ca17a4ef76bf2c21b43491
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/__init__.py
@@ -0,0 +1,25 @@
+#
+# Extensible User Folder
+# 
+# (C) Copyright 2000-2005 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id:
+
+import basicMemberSource
+import nullMemberSource
+
diff --git a/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/.cvsignore b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/PasswordForm.dtml b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/PasswordForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..98bf3777a3b2958c441e2cde81c92dda6bff9931
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/PasswordForm.dtml
@@ -0,0 +1,22 @@
+<dtml-var "DialogHeader(DialogTitle='Change Password', dialog_width='')">
+<form action="acl_users/manage_changePassword" method="POST">
+<table>
+<tr>
+	<td align="right"><b><dtml-babel src="'en'">Old Password</dtml-babel></b></td>
+    <td><input type="password" name="current_password"></td>
+<tr>
+        <td align="right"><b><dtml-babel src="'en'">Password</dtml-babel></b></td>
+        <td><input type="password" name="password"></td>
+</tr>
+        <td align="right"><b><dtml-babel src="'en'">Confirm Password</dtml-babel></b></td>
+        <td><input type="password" name="password_confirm"></td>
+</tr>
+<dtml-if "forgottenPasswords=='hint'">
+<tr><td align="right"><b><dtml-babel src="'en'">Password Hint</dtml-babel></b></td>
+        <td><input type="text" name="user_hint" value="&dtml.missing-user_hint;"></td>
+</tr>
+</dtml-if>
+</table>
+<input type="submit" value=" <dtml-babel src="'en'">Change Password</dtml-babel> ">
+</form>
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/SignupForm.dtml b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/SignupForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..98ae04ef807eb7bf138196f58a591bf902091834
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/SignupForm.dtml
@@ -0,0 +1,31 @@
+<dtml-var "DialogHeader(DialogTitle='Signup', dialog_width='')">
+<form action="acl_users/manage_signupUser" method="POST">
+<table>
+<tr>
+	<td align="right"><b><dtml-babel src="'en'">Username</dtml-babel></td>
+	<td><input name="username" type="text" value="&dtml.missing-username;"></td>
+</tr>
+<dtml-if "passwordPolicy=='user'">
+<tr>
+        <td align="right"><b><dtml-babel src="'en'">Password</dtml-babel></b></td>
+        <td><input type="password" name="password" value="&dtml.missing-password;"></td>
+</tr>
+        <td align="right"><b><dtml-babel src="'en'">Confirm Password</dtml-babel></b></td>
+        <td><input type="password" name="password_confirm"></td>
+</tr>
+<dtml-if "forgottenPasswords=='hint'">
+<tr><td align="right"><b><dtml-babel src="'en'">Password Hint</dtml-babel></b></td>
+        <td><input type="text" name="user_hint" value="&dtml.missing-user_hint;"></td>
+</tr>
+</dtml-if>
+</dtml-if>
+<tr><td align="right"><b><dtml-babel src="'en'">Real Name</dtml-babel></b></td>
+        <td><input type="text" name="user_realname" value="&dtml.missing-user_realname;"></td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'"><dtml-babel src="'en'">Email</dtml-babel></dtml-babel></b></td>
+        <td><input type="text" name="user_email" value="&dtml.missing-user_email;"></td>
+</tr>
+</table>
+<input type="submit" value=" <dtml-babel src="'en'">Signup</dtml-babel> ">
+</form>
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/__init__.py b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a774af80b728ee327c524b6a02d58a2955c80da4
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/__init__.py
@@ -0,0 +1,2 @@
+# $Id: __init__.py,v 1.1 2004/11/10 14:15:55 akm Exp $
+import basicMemberSource
diff --git a/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/basicMemberSource.py b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/basicMemberSource.py
new file mode 100644
index 0000000000000000000000000000000000000000..7be098a5982d50b2ad547efacdfdba1393aa3e80
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/basicMemberSource.py
@@ -0,0 +1,629 @@
+#
+# Extensible User Folder
+# 
+# Basic Membership Source for exUserFolder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: basicMemberSource.py,v 1.1 2004/11/10 14:15:55 akm Exp $
+
+#
+# Basically membership is a layer between the signup/login form, and
+# the authentication layer, it uses the prop source of the users to
+# store additional information about a user i.e. doesn't impact on the
+# authentication source.
+#
+# Some membership features imply some extra properties for the user will
+# be available; specifically at this time an email property.
+#
+# You also need a MailHost setup and ready to go for emailing stuff to users
+#
+
+import string,Acquisition
+from random import choice
+	
+
+from Globals import HTMLFile, INSTANCE_HOME
+
+from OFS.Folder import Folder
+from OFS.DTMLMethod import DTMLMethod
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import PluginRegister
+
+from base64 import encodestring
+from urllib import quote
+
+import zLOG
+
+"""
+Password Policy enforcement (min/max length, caps etc)
+Create Password, or User Chooses.
+Timing out of passwords...
+Empty Password force change on login...
+Create Home Directory
+Copy files from Skelton Directory
+EMail password hint to user (forgot my password)
+Reset password and email user (needs plugin?)
+Redirect on login to fixed or varying per username location.
+Automatically add users, or manually approve of users.
+"""
+
+# Stupid little things for making a password
+# Don't hassle me, it's supposed to be basic.
+
+nouns=['ace', 'ant', 'arc', 'arm', 'axe',
+	   'bar', 'bat', 'bee', 'bib', 'bin',
+	   'can', 'cap', 'car', 'cat', 'cob',
+	   'day', 'den', 'dog', 'dot', 'dux',
+	   'ear', 'eel', 'egg', 'elf', 'elk',
+	   'fad', 'fan', 'fat', 'fig', 'fez',
+	   'gag', 'gas', 'gin', 'git', 'gum',
+	   'hag', 'hat', 'hay', 'hex', 'hub']
+
+pastConjs = [ 'did', 'has', 'was' ]
+suffixes  = [ 'ing', 'es', 'ed', 'ious', 'ily']
+
+def manage_addBasicMemberSource(self, REQUEST):
+	""" Add a Membership Source """
+
+	pvfeatures=[]
+	minLength=0
+	passwordPolicy=''
+	createHomedir=0
+	homeRoot=''
+	copyFilesFrom=''
+	postLogin=''
+	postSignup=''
+	forgottenPasswords=''
+	defaultRoles=[]
+	usersCanChangePasswords=0
+	baseURL=''
+	loginPage=''
+	signupPage=''
+	passwordPage=''
+	mailHost=''
+	fixedDest=''
+	
+	if REQUEST.has_key('basicmember_pvfeatures'):
+		pvfeatures=REQUEST['basicmember_pvfeatures']
+
+	if REQUEST.has_key('basicmember_roles'):
+		defaultRoles=REQUEST['basicmember_roles']
+
+	if not defaultRoles:
+		defaultRoles=['Member']
+
+	if 'minlength' in pvfeatures:
+		minLength=REQUEST['basicmember_minpasslen']
+
+	if REQUEST.has_key('basicmember_passwordpolicy'):
+		passwordPolicy=REQUEST['basicmember_passwordpolicy']
+
+	if REQUEST.has_key('basicmember_createhomedir'):
+		homeRoot=REQUEST['basicmember_homeroot']
+		createHomedir=1
+
+	if REQUEST.has_key('basicmember_copyfiles'):
+		copyFilesFrom=REQUEST['basicmember_copyfiles']
+
+	if REQUEST.has_key('basicmember_changepasswords'):
+		usersCanChangePasswords=1
+
+	if REQUEST.has_key('basicmember_fixeddest'):
+		fixedDest=''
+
+	forgottenPasswords=REQUEST['basicmember_forgottenpasswords']
+	postLogin=REQUEST['basicmember_postlogin']
+
+	baseURL=REQUEST['basicmember_baseurl']
+	loginPage=REQUEST['basicmember_loginpage']
+	signupPage=REQUEST['basicmember_signuppage']
+	passwordPage=REQUEST['basicmember_passwordpage']
+	siteEmail=REQUEST['basicmember_siteemail']
+	siteName=REQUEST['basicmember_sitename']
+
+	mailHost=REQUEST['basicmember_mailhost']
+	
+	# postSignup=REQUEST['basicmember_postsignup']
+
+	#
+	# Yep this is obscene
+	#
+	o = BasicMemberSource(pvfeatures, minLength, passwordPolicy,
+						  createHomedir, copyFilesFrom, postLogin,
+						  homeRoot, forgottenPasswords, defaultRoles,
+						  usersCanChangePasswords, baseURL, loginPage,
+						  signupPage, passwordPage, mailHost,
+						  siteName, siteEmail, fixedDest)
+
+	self._setObject('basicMemberSource', o, None, None, 0)
+	o = getattr(self, 'basicMemberSource')
+
+	if hasattr(o, 'postInitialisation'):
+		o.postInitialisation(REQUEST)
+
+	self.currentMembershipSource=o
+	return ''
+
+
+manage_addBasicMemberSourceForm=HTMLFile('manage_addBasicMemberSourceForm',
+										 globals())
+manage_editBasicMemberSourceForm=HTMLFile('manage_editBasicMemberSourceForm',
+										 globals())
+
+#
+# Crap, I don't know why I called this basic, I'd hate to see a
+# complicated one.
+#
+class BasicMemberSource(Folder):
+	""" Provide High Level User Management """
+	meta_type="Membership Source"
+	title="Basic Membership Source"
+	icon ='misc_/exUserFolder/exUserFolderPlugin.gif'
+	manage_tabs=Acquisition.Acquired
+	manage_editForm=manage_editBasicMemberSourceForm
+
+	# Ugh...
+	def __init__(self, pvFeatures=[], minLength=0, passwordPolicy='',
+				 createHomeDir=0, copyFilesFrom='', postLogin='', homeRoot='',
+				 forgottenPasswords='', defaultRoles=[], usersCanChangePasswords=0,
+				 baseURL='', loginPage='', signupPage='', passwordPage='',
+				 mailHost='', siteName='', siteEmail='', fixedDest=''):
+		
+		self.id='basicMemberSource'
+		self.pvFeatures=pvFeatures
+		self.minLength=int(minLength)
+		self.passwordPolicy=passwordPolicy
+		self.createHomeDir=createHomeDir
+		self.copyFilesFrom=copyFilesFrom
+		self.postLogin=postLogin
+		self.homeRoot=homeRoot
+		self.forgottenPasswords=forgottenPasswords
+		self.defaultRoles=defaultRoles
+		self.usersCanChangePasswords=usersCanChangePasswords
+		self.baseURL=baseURL
+		self.loginPage=loginPage
+		self.signupPage=signupPage
+		self.passwordPage=passwordPage
+		self.siteName=siteName
+		self.siteEmail=siteEmail
+		self.fixedDest=fixedDest
+		
+		_SignupForm=HTMLFile('SignupForm', globals())
+		SignupForm=DTMLMethod()
+		SignupForm.manage_edit(data=_SignupForm, title='Signup Form')
+		self._setObject('SignupForm', SignupForm)
+
+		_PasswordForm=HTMLFile('PasswordForm', globals())
+		PasswordForm=DTMLMethod()
+		PasswordForm.manage_edit(data=_PasswordForm,
+								 title='Change Password')
+		self._setObject('PasswordForm', PasswordForm)
+
+		self.mailHost=mailHost
+
+		_newPasswordEmail=HTMLFile('newPasswordEmail', globals())
+		newPasswordEmail=DTMLMethod()
+		newPasswordEmail.manage_edit(data=_newPasswordEmail,
+									 title='Send New Password')
+		self._setObject('newPasswordEmail', newPasswordEmail)
+
+		_forgotPasswordEmail=HTMLFile('forgotPasswordEmail', globals())
+		forgotPasswordEmail=DTMLMethod()
+		forgotPasswordEmail.manage_edit(data=_forgotPasswordEmail,
+										title='Send Forgotten Password')
+		self._setObject('forgotPasswordEmail', forgotPasswordEmail)
+
+		_passwordHintEmail=HTMLFile('passwordHintEmail', globals())
+		passwordHintEmail=DTMLMethod()
+		passwordHintEmail.manage_edit(data=_passwordHintEmail,
+										title='Send Forgotten Password Hint')
+		self._setObject('passwordHintEmail', passwordHintEmail)
+
+	def postInitialisation(self, REQUEST):
+		if self.createHomeDir and self.homeRoot:
+			self.findHomeRootObject()
+		else:
+			self.homeRootObj=None
+			
+		if self.copyFilesFrom:
+			self.findSkelRootObject()
+		else:
+			self.homeSkelObj=None
+
+		# The nice sendmail tag doesn't allow expressions for
+		# the mailhost
+		self.mailHostObject=getattr(self, self.mailHost)
+
+	def manage_editMembershipSource(self, REQUEST):
+		""" Edit a basic Membership Source """
+		if REQUEST.has_key('pvfeatures'):
+			self.pvFeatures=REQUEST['pvfeatures']
+		else:
+			self.pvFeatures=[]
+			
+		if REQUEST.has_key('minpasslength'):
+			self.minLength=REQUEST['minpasslength']
+
+		if REQUEST.has_key('createhomedir'):
+			createHomeDir=1
+		else:
+			createHomeDir=0
+
+		if createHomeDir:
+			self.copyFilesFrom=REQUEST['copyfiles']
+			if self.copyFilesFrom:
+				self.findSkelRootObject()
+			else:
+				self.homeRoot=REQUEST['homeroot']
+			self.findHomeRootObject()
+		
+		if REQUEST.has_key('memberroles'):
+			self.defaultRoles=REQUEST['memberroles']
+		if REQUEST.has_key('changepasswords'):
+			self.usersCanChangePasswords=1
+		else:
+			self.usersCanChangePasswords=0
+
+		self.postLogin=REQUEST['postlogin']
+		if REQUEST.has_key('fixeddest'):
+			self.fixedDest=REQUEST['fixeddest']
+
+		self.baseURL=REQUEST['baseurl']
+		self.loginPage=REQUEST['loginpage']
+		self.signupPage=REQUEST['signuppage']
+		self.passwordPage=REQUEST['passwordpage']
+		self.siteName=REQUEST['sitename']
+		self.siteEmail=REQUEST['siteemail']
+		return self.MessageDialog(self,
+				title  ='Updated!', 
+				message="Membership was Updated",
+				action ='manage_editMembershipSourceForm',
+				REQUEST=REQUEST)
+
+		
+
+	def forgotPassword(self, REQUEST):
+		username=REQUEST['username']
+		curUser=self.getUser(username)
+		if not curUser:
+			return self.MessageDialog(self,
+				title  ='No such user', 
+				message="No users matching that username were found.",
+				action ='%s/%s'%(self.baseURL, self.loginPage),
+				REQUEST=REQUEST)			
+
+			
+		userEmail=curUser.getProperty('email')
+		userName=curUser.getProperty('realname')
+		if self.forgottenPasswords == "hint":
+			passwordHint=curUser.getProperty('passwordhint')
+			self.passwordHintEmail(self,
+								   REQUEST=REQUEST,
+								   username=username,
+								   hint=passwordHint,
+								   realname=userName,
+								   email=userEmail)
+		else:
+			# make a new password, and mail it to the user
+			password = self.generatePassword()
+			curCrypt=self.currentAuthSource.cryptPassword(username,password)
+
+			# Update the user
+			bogusREQUEST={}
+			#bogusREQUEST['username']=username
+			bogusREQUEST['password']=password
+			bogusREQUEST['password_confirm']=password
+			bogusREQUEST['roles']=curUser.roles
+			self.manage_editUser(username, bogusREQUEST)
+			
+			self.forgotPasswordEmail(self,
+									REQUEST=REQUEST,
+									username=username,
+									password=password,
+									realname=userName,
+									email=userEmail)
+		return self.MessageDialog(self,
+				title  ='Sent!', 
+				message="Password details have been emailed to you",
+				action ='%s/%s'%(self.baseURL, self.loginPage),
+				REQUEST=REQUEST)			
+
+
+	def changeProperties(self, REQUEST):
+ 
+		curUser=self.listOneUser(REQUEST['AUTHENTICATED_USER'].getUserName())
+		curUser=curUser[0]
+		if not curUser:
+			return self.MessageDialog(self,
+				title  ='Erm!', 
+				message="You don't seem to be logged in",
+				action ='%s/%s'%(self.baseURL, self.passwordPage),
+				REQUEST=REQUEST)
+			
+
+		
+		self.currentPropSource.updateUser(curUser['username'],REQUEST)
+		
+		return self.MessageDialog(self,
+				   title  ='Properties updated',
+				   message="Your properties have been updated",
+				   action =self.baseURL,
+				   REQUEST=REQUEST,
+				   )
+
+	
+	def changePassword(self, REQUEST):
+		if not self.usersCanChangePasswords:
+			return ''
+
+		curUser=self.listOneUser(REQUEST['AUTHENTICATED_USER'].getUserName())
+		curUser=curUser[0]
+		if not curUser:
+			return self.MessageDialog(
+				title  ='Erm!', 
+				message="You don't seem to be logged in",
+				action ='%s/%s'%(self.baseURL, self.passwordPage),
+				REQUEST=REQUEST)
+			
+		curCrypt=self.currentAuthSource.cryptPassword(curUser['username'],REQUEST['current_password'])
+		if curCrypt != curUser['password']:
+			return self.MessageDialog(self,
+				title  ='Password Mismatch', 
+				message="Password is incorrect",
+				action ='%s/%s'%(self.baseURL, self.passwordPage),
+				REQUEST=REQUEST)
+
+		if REQUEST['password'] != REQUEST['password_confirm']:
+			return self.MessageDialog(self,
+				title  ='Password Mismatch', 
+				message="Passwords do not match",
+				action ='%s/%s'%(self.baseURL, self.passwordPage),
+				REQUEST=REQUEST)
+
+		# OK the old password matches the one the user provided
+		# Both new passwords match...
+		# Time to validate against our normal set of rules...
+		#
+		if not self.validatePassword(REQUEST['password'], curUser['username']):
+			return self.MessageDialog(self,
+				title  ='Password problem', 
+				message="Your password is invalid, please choose another",
+				action ='%s/%s'%(self.baseURL, self.passwordPage),
+				REQUEST=REQUEST)
+
+		if self.passwordPolicy=='hint':
+			if not hasattr(REQUEST,'user_passwordhint'):
+				return self.MessageDialog(self,
+					title  ='Password requires hint', 
+					message='You must choose a password hint',
+					action ='%s/%s'%(self.baseURL, self.passwordPage),
+					REQUEST=REQUEST)		
+
+		bogusREQUEST={}
+
+		bogusREQUEST['password']=REQUEST['password']
+		bogusREQUEST['password_confirm']=REQUEST['password']
+		bogusREQUEST['roles']=curUser['roles']
+		self.manage_editUser(curUser['username'],bogusREQUEST)
+		# update the cookie so he doesnt have to re-login:
+		if self.cookie_mode:
+			token='%s:%s' %(curUser['username'], REQUEST['password'])
+			token=encodestring(token)
+			token=quote(token)
+			REQUEST.response.setCookie('__ac', token, path='/')
+			REQUEST['__ac']=token
+
+		return self.MessageDialog(self,
+			title  ='Password updated', 
+			message="Your password has been updated",
+			action =self.baseURL,
+			REQUEST=REQUEST)
+		
+
+	def goHome(self, REQUEST, RESPONSE):
+		redirectstring="%s/%s/%s/manage_main"%(self.baseURL, self.homeRoot, REQUEST.AUTHENTICATED_USER.getUserName())
+		RESPONSE.redirect(redirectstring)
+		return ''
+	
+	# Tell exUserFolder where we want to go...
+	def getLoginDestination(self, REQUEST):
+		script=''
+		pathinfo=''
+		querystring=''
+		redirectstring=''
+		if self.postLogin=="destination":
+			script=REQUEST['SCRIPT_NAME']
+			pathinfo=REQUEST['PATH_INFO']
+		elif self.postLogin=="varied":
+			script=self.baseURL
+			pathinfo="/acl_users/goHome"
+			
+		elif self.postLogin=="fixed":
+			pathinfo="%s"%(self.fixedDest)
+
+		if REQUEST.has_key('QUERY_STRING'):
+			querystring='?'+REQUEST['QUERY_STRING']
+
+		redirectstring=script+pathinfo
+		if querystring:
+			redirectstring=redirectstring+querystring		
+
+		return redirectstring
+	
+	def validatePassword(self, password, username):
+		if 'minlength' in self.pvFeatures:
+			if len(password) < self.minLength:
+				return 0
+
+		if 'mixedcase' in self.pvFeatures:
+			lower = 0
+			upper = 0
+			for c in password:
+				if c in string.lowercase:
+					lower = 1
+				if c in string.uppercase:
+					upper = 1
+			if not upper and lower:
+				return 0
+			
+		if 'specialchar' in self.pvFeatures:
+			special = 0
+			for c in password:
+				if c in string.punctuation:
+					special = 1
+					break
+				elif c in string.digits:
+					special = 1
+					break
+			if not special:
+				return 0
+
+		#
+		# XXX Move this somewhere else
+		#
+			
+		if 'notstupid' in self.pvFeatures:
+			email=''
+			# We try some permutations here...
+			curUser=self.getUser(username)
+			if curUser:
+				email = curUser.getProperty('email')
+			elif hasattr(self, 'REQUEST'):
+				if self.REQUEST.has_key('user_email'): # new signup
+					email=self.REQUEST['user_email']
+				elif self.REQUEST.has_key('email'):
+					email=self.REQUEST['email']
+
+			if ((string.find(password, username)>=0) or
+				( email and
+				  (string.find(password,
+							   string.split(email,'@')[0]) >=0))):
+				return 0
+		return 1
+
+	# These next two look the same (and they are for now), but, the reason I
+	# Don't use one single method, is I think that SkelObj might migrate to
+	# using full paths, not relative paths.
+	
+	def findSkelRootObject(self):
+		# Parent should be acl_users
+		parent = getattr(self, 'aq_parent')
+
+		# This should be the root...
+		root = getattr(parent, 'aq_parent')
+		searchPaths = string.split(self.copyFilesFrom, '/')
+		for o in searchPaths:
+			if not getattr(root, o):
+				break
+			root = getattr(root, o)
+
+		self.homeSkelObj=root		
+	
+	def findHomeRootObject(self):
+		# Parent should be acl_users
+		parent = getattr(self, 'aq_parent')
+
+		# This should be the root...
+		root = getattr(parent, 'aq_parent')
+
+		searchPaths = string.split(self.homeRoot, '/')
+		for o in searchPaths:
+			if o not in root.objectIds():
+				root.manage_addFolder(id=o, title=o, createPublic=0, createUserF=0)
+			root = getattr(root, o)
+
+		self.homeRootObj=root
+		
+	def makeHomeDir(self, username):
+		if not self.homeRootObj:
+			return
+		
+		self.homeRootObj.manage_addFolder(id=username, title=username, createPublic=0, createUserF=0)
+		home = getattr(self.homeRootObj, username)
+		
+
+		# Allow user to be in charge of their own destiny
+		# XXXX WARNING THIS IS A NORMAL FOLDER *SO USERS CAN ADD ANYTHING*
+		# YOU NEED TO CHANGE THE TYPE OF OBJECT ADDED FOR A USER UNLESS
+		# THIS IS WHAT YOU WANT TO HAPPEN
+		home.manage_addLocalRoles(userid=username, roles=['Manager'])
+
+		if self.copyFilesFrom and self.homeSkelObj and self.homeSkelObj.objectIds():
+			cp=self.homeSkelObj.manage_copyObjects(
+				self.homeSkelObj.objectIds())
+			home.manage_pasteObjects(cp)
+
+		# Fix it so the user owns their stuff
+		curUser=self.getUser(username).__of__(self.aq_parent)
+		home.changeOwnership(curUser, recursive=1)
+		
+	def generatePassword(self):
+		password = (choice(nouns) + choice(pastConjs) +
+					choice(nouns) + choice(suffixes))
+		return password
+			
+	def createUser(self, REQUEST):
+		if self.passwordPolicy == 'user':
+			if not self.validatePassword(REQUEST['password'], REQUEST['username']):
+				return self.MessageDialog(self,
+					title  ='Password problem', 
+					message='Your password is invalid, please choose another',
+					action ='%s/%s'%(self.baseURL, self.signupPage),
+					REQUEST=REQUEST)
+
+			if self.passwordPolicy=='hint':
+				if not hasattr(REQUEST,'user_passwordhint'):
+					return self.MessageDialog(self,
+						title  ='Password requires hint', 
+						message='You must choose a password hint',
+						action ='%s/%s'%(self.baseURL, self.signupPage),
+						REQUEST=REQUEST)
+
+		elif self.passwordPolicy == 'system':
+			REQUEST['password']=self.generatePassword()
+			REQUEST['password_confirm']=REQUEST['password']
+
+			# Email the password.
+			self.newPasswordEmail(self, REQUEST)
+
+		zLOG.LOG("exUserFolder.basicMemberSource", zLOG.BLATHER,
+                         "Creating user",
+                         "Passed all tests -- creating [%s]" % REQUEST['username'])
+		REQUEST['roles']=self.defaultRoles
+		self.manage_addUser(REQUEST) # Create the User...
+		if self.createHomeDir:
+			self.makeHomeDir(REQUEST['username'])
+
+		return self.MessageDialog(self,
+			title  ='You have signed up', 
+			message='You have been signed up succesfully',
+			action ='%s'%(self.baseURL),
+			REQUEST=REQUEST)
+		
+
+		
+basicMemberReg=PluginRegister('basicMemberSource',
+							  'Basic Membership Source',
+							  BasicMemberSource,
+							  manage_addBasicMemberSourceForm,
+							  manage_addBasicMemberSource,
+							  manage_editBasicMemberSourceForm)
+exUserFolder.membershipSources['basicMemberSource']=basicMemberReg
+
diff --git a/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/forgotPasswordEmail.dtml b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/forgotPasswordEmail.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..1dc67b2b9f29a8b9fbd5b043b62d8ea5eae99650
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/forgotPasswordEmail.dtml
@@ -0,0 +1,15 @@
+<dtml-sendmail mailhost=mailHostObject>
+To: <dtml-var realname> <<dtml-var email>>
+From: <dtml-var siteName> <<dtml-var siteEmail>>
+Subject: You forgot your password for <dtml-var siteName>
+
+Dear <dtml-var realname>,
+
+Your username is <dtml-var username> and your password is now
+<dtml-var password>.
+
+You should have tested this first, and now that you've tested it, you'll
+see you need to customise this method.
+
+</dtml-sendmail>
+
diff --git a/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/manage_addBasicMemberSourceForm.dtml b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/manage_addBasicMemberSourceForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..434e1f0925e7bd3a264f63cbdaec4560726e3f22
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/manage_addBasicMemberSourceForm.dtml
@@ -0,0 +1,143 @@
+<dtml-var "DialogHeader(_.None, _, DialogTitle='Add Basic Membership Source', dialog_width='')">
+<b><dtml-babel src="'en'">Membership requires a valid property source, you cannot use this with NULL Property Source</dtml-babel></b>
+<FORM ACTION="&dtml-URL;" METHOD="POST">
+		<dtml-in "REQUEST.form.keys()">
+			<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
+				<dtml-let listVar=sequence-item>
+					<dtml-in "REQUEST[listVar]">
+						<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
+					</dtml-in>
+				</dtml-let>
+			<dtml-else>
+				<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
+			</dtml-if>
+
+		</dtml-in>
+
+<input type="HIDDEN" name="doGroup" value="1">
+<table cellspacing="2">
+<tr><td align="right"><b><dtml-babel src="'en'">Site Name (used in emails)</dtml-babel></b></td>
+	<td><input type="text" name="basicmember_sitename">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel src="'en'">Site Email (used for emails)</dtml-babel></b></td>
+	<td><input type="text" name="basicmember_siteemail">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel src="'en'">Mail Host</dtml-babel></b></td>
+	<td>
+        <select name="basicmember_mailhost">
+        <dtml-in "MailHostIDs()">
+            <option value="<dtml-var sequence-item>">
+            <dtml-var sequence-key></option>
+        </dtml-in>
+        </select>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Site Base</dtml-babel></b></td>
+	<td><input type="text" name="basicmember_baseurl"
+			value="<dtml-var "absolute_url()">">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel src="'en'">Relative Path (from base) of Login Page</dtml-babel></b></td>
+	<td><input type="text" name="basicmember_loginpage"	value="LoginForm">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel src="'en'">Relative Path (from base) of Signup Page</dtml-babel></b></td>
+	<td><input type="text" name="basicmember_signuppage" value="SignupForm">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel src="'en'">Relative Path (from base) of Change Password Page</dtml-babel></b></td>
+	<td><input type="text" name="basicmember_passwordpage" value="ChangePasswordForm">
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Password Validation Features</dtml-babel></b></td>
+	<td>
+		<select name="basicmember_pvfeatures:list" multiple>
+		<option value="minlength">Minimum Length</option>
+		<option value="mixedcase">Must have Mixed Case</option>
+		<option value="specichar">Must have Special Chars</option>
+		<option value="notstupid">Not Stupid (username/email/part of name)</option>
+		</select>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Minimum Length (0 if not required)</dtml-babel></b></td>
+	<td>
+		<input type="text" name="basicmember_minpasslen:int" value="0">
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Password Policy</dtml-babel></b></td>
+	<td>
+		<select name="basicmember_passwordpolicy">
+		<option value="user">User Chooses</option>
+		<option value="system">System Chooses and emails User</option>
+		</select>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Forgotten Passwords</dtml-babel></b></td>
+	<td>
+		<select name="basicmember_forgottenpasswords">
+		<option value="hint"><dtml-babel src="'en'">Email a Hint</dtml-babel></option>
+		<option value="reset"><dtml-babel src="'en'">Reset and Email New password</dtml-babel></option>
+		</select>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Allow users to change passwords</dtml-babel></b></td>
+	<td>
+		<input type="checkbox" name="basicmember_changepasswords" checked><dtml-babel src="'en'">Yes</dtml-babel>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Create 'Home Directory'</dtml-babel></b></td>
+	<td>
+		<input type="checkbox" name="basicmember_createhomedir"><dtml-babel src="'en'">Yes</dtml-babel>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Relative Path to 'Home Directory' Root</dtml-babel></b></td>
+	<td>
+		<input type="text" name="basicmember_homeroot" value="Members">
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Copy initial 'Home Directory' files from...(empty=No Copy)</dtml-babel></b></td>
+	<td>
+		<input type="text" name="basicmember_copyfiles", value="<dtml-var "_.string.join(getPhysicalPath()[1:], '/')">">
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">After login....</dtml-babel></b></td>
+	<td>
+	<select name="basicmember_postlogin">
+	<option value="destination"><dtml-babel src="'en'">Go to intended destination</dtml-babel></option>
+	<option value="fixed"><dtml-babel src="'en'">Go to fixed destination</dtml-babel></option>
+	<option value="varied"><dtml-babel src="'en'">Go to Home Directory</dtml-babel></option>
+	</select>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Fixed Destination</dtml-babel></b></td>
+	<td>
+		<input type="text" name="basicmember_fixeddest">
+	</td>
+</tr>
+<tr>
+  <td valign="top" align="right"><b><dtml-babel src="'en'">Default Roles</dtml-babel></b></td>
+  <td align="left" valign="top">
+  <select name="basicmember_roles:list" size="5" multiple>
+  <dtml-in valid_roles>
+  <dtml-if expr="_vars['sequence-item'] != 'Anonymous'">
+  <dtml-if expr="_vars['sequence-item'] != 'Authenticated'">
+  <dtml-if expr="_vars['sequence-item'] != 'Shared'">
+  <option value="<dtml-var sequence-item html_quote>"><dtml-var sequence-item>
+  </dtml-if>
+  </dtml-if>
+  </dtml-if>
+  </dtml-in valid_roles>
+  </select>
+</td>
+</tr>
+</table>
+<input type="SUBMIT" value="<dtml-babel src="'en'">Add</dtml-babel>">
+</form>
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/manage_editBasicMemberSourceForm.dtml b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/manage_editBasicMemberSourceForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..c7092b323acc0b9dacd8984cd7371ed6f0c76624
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/manage_editBasicMemberSourceForm.dtml
@@ -0,0 +1,116 @@
+<dtml-var "DialogHeader(_.None, _, DialogTitle='Edit Basic Membership Source', dialog_width='')">
+<dtml-var manage_tabs>
+<FORM ACTION="manage_editMembershipSource" METHOD="POST">
+<dtml-with currentMembershipSource>
+<table cellspacing="2">
+<tr><td align="right"><b><dtml-babel src="'en'">Site Name (used in emails)</dtml-babel></b></td>
+	<td><input type="text" name="sitename" value="&dtml.missing-siteName;">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel src="'en'">Site Email (used for emails)</dtml-babel></b></td>
+	<td><input type="text" name="siteemail" value="&dtml.missing-siteEmail;">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel src="'en'">Mail Host</dtml-babel></b></td>
+	<td>
+        <select name="mailhost">
+        <dtml-in "MailHostIDs()">
+            <option value="<dtml-var sequence-item>"<dtml-if "mailHost==_.getitem('sequence-item',0)"> selected</dtml-if>><dtml-var sequence-key></option>
+        </dtml-in>
+        </select>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Site Base</dtml-babel></b></td>
+	<td><input type="text" name="baseurl"
+			value="&dtml.missing-baseURL;">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel src="'en'">Relative Path (from base) of Login Page</dtml-babel></b></td>
+	<td><input type="text" name="loginpage"	value="&dtml.missing-loginPage;">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel>Relative Path (from base) of Signup Page</dtml-babel></b></td>
+	<td><input type="text" name="signuppage" value="&dtml.missing-signupPage;">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel src="'en'">Relative Path (from base) of Change Password Page</dtml-babel></b></td>
+	<td><input type="text" name="passwordpage" value="&dtml.missing-passwordPage;">
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Password Validation Features</dtml-babel></b></td>
+	<td>
+		<select name="pvfeatures:list" multiple>
+		<option value="minlength" <dtml-if "'minlength' in pvFeatures"> selected</dtml-if>><dtml-babel src="'en'">Minimum Length</dtml-babel></option>
+		<option value="mixedcase" <dtml-if "'mixedcase' in pvFeatures"> selected</dtml-if>><dtml-babel src="'en'">Must have Mixed Case</dtml-babel></option>
+		<option value="specichar" <dtml-if "'specichar' in pvFeatures"> selected</dtml-if>><dtml-babel src="'en'">Must have Special Chars</dtml-babel></option>
+		<option value="notstupid" <dtml-if "'notstupid' in pvFeatures"> selected</dtml-if>><dtml-babel src="'en'">Not Stupid (username/email/part of name)</dtml-babel></option>
+		</select>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Minimum Length (if required)</dtml-babel></b></td>
+	<td>
+		<input type="text" name="minpasslen:int" value="&dtml.missing-minLength;">
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Allow users to change passwords</dtml-babel></b></td>
+	<td>
+		<input type="checkbox" name="changepasswords"<dtml-if usersCanChangePasswords> checked</dtml-if>><dtml-babel src="'en'">Yes</dtml-babel>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Create 'Home Directory'</dtml-babel></b></td>
+	<td>
+		<input type="checkbox" name="createhomedir"<dtml-if createHomeDir> checked</dtml-if>><dtml-babel src="'en'">Yes</dtml-babel>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Path to 'Home Directory' Root</dtml-babel></b></td>
+	<td>
+		<input type="text" name="homeroot" value="&dtml.missing-homeRoot;">
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Copy initial 'Home Directory' files from...(empty=No Copy)</dtml-babel></b></td>
+	<td>
+		<input type="text" name="copyfiles" value="&dtml.missing-copyFilesFrom;">
+	</td>
+</tr>
+
+<tr><td align="right"><b><dtml-babel src="'en'">After login....</dtml-babel></b></td>
+	<td>
+	<select name="postlogin">
+	<option value="destination"<dtml-if "postLogin=='destination'"> selected</dtml-if>><dtml-babel src="'en'">Go to intended destination</dtml-babel></option>
+	<option value="fixed"<dtml-if "postLogin=='fixed'"> selected</dtml-if>><dtml-babel src="'en'">Go to fixed destination</dtml-babel></option>
+	<option value="varied"<dtml-if "postLogin=='varied'"> selected</dtml-if>><dtml-babel src="'en'">Go to Home Directory</dtml-babel></option>
+	</select>
+	</td>
+</tr>
+<tr><td align="right"><b><dtml-babel src="'en'">Fixed Destination</dtml-babel></b></td>
+	<td>
+		<input type="text" name="fixeddest" value="&dtml.missing-fixedDest;">
+	</td>
+</tr>
+<tr>
+  <td valign="top" align="right"><b><dtml-babel src="'en'">Default Roles</dtml-babel></b></td>
+  <td align="left" valign="top">
+  <select name="memberroles:list" size="5" multiple>
+  <dtml-in valid_roles>
+  <dtml-if expr="_vars['sequence-item'] != 'Anonymous'">
+  <dtml-if expr="_vars['sequence-item'] != 'Authenticated'">
+  <dtml-if expr="_vars['sequence-item'] != 'Shared'">
+  <option value="<dtml-var sequence-item html_quote>"<dtml-if "_['sequence-item'] in defaultRoles"> selected</dtml-if>><dtml-var sequence-item>
+  </dtml-if>
+  </dtml-if>
+  </dtml-if>
+  </dtml-in valid_roles>
+  </select>
+</td>
+</tr>
+</table>
+<input type="SUBMIT" value=" <dtml-babel src="'en'">Update</dtml-babel> ">
+</dtml-with>
+</form>
+
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/newPasswordEmail.dtml b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/newPasswordEmail.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..4de7dc6b52cbdc5f1e3946dddd5ff14552dfa501
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/newPasswordEmail.dtml
@@ -0,0 +1,16 @@
+<dtml-sendmail mailhost=mailHostObject>
+To: <dtml-var user_realname> <<dtml-var user_email>>
+From: <dtml-var siteName> <<dtml-var siteEmail>>
+Subject: Welcome to <dtml-var siteName>
+
+Dear <dtml-var user_realname>,
+
+Welcome to <dtml-var siteName>.
+
+Your username is <dtml-var username> and your password is <dtml-var password>.
+
+You should have tested this first, and now that you've tested it, you'll
+see you need to customise this method.
+
+</dtml-sendmail>
+
diff --git a/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/passwordHintEmail.dtml b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/passwordHintEmail.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..75c7cca9dd6ae31774dc47ab71c6e405edfb0b33
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/basicMemberSource/passwordHintEmail.dtml
@@ -0,0 +1,15 @@
+<dtml-sendmail mailhost=mailHostObject>
+To: <dtml-var realname> <<dtml-var email>>
+From: <dtml-var siteName> <<dtml-var siteEmail>>
+Subject: Hint for <dtml-var siteName>
+
+Dear <dtml-var realname>,
+
+Your username is <dtml-var username> and your password hint was;
+<dtml-var hint>.
+
+You should have tested this first, and now that you've tested it, you'll
+see you need to customise this method.
+
+</dtml-sendmail>
+
diff --git a/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/.cvsignore b/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/__init__.py b/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..9eb5162288db1b6cfd8f8675f52b7cf75019b446
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/__init__.py
@@ -0,0 +1,2 @@
+# $Id: __init__.py,v 1.1 2004/11/10 14:15:55 akm Exp $
+import nullMemberSource
diff --git a/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/manage_addNullPluginSourceForm.dtml b/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/manage_addNullPluginSourceForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..4ae809bf7d96ef60e98f2f87aa3a134e0edfb3f9
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/manage_addNullPluginSourceForm.dtml
@@ -0,0 +1,21 @@
+<dtml-var "DialogHeader(_.None, _, DialogTitle='Add Basic Membership Source', dialog_width='')">
+<FORM ACTION="&dtml-URL;" METHOD="POST">
+		<dtml-in "REQUEST.form.keys()">
+			<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
+				<dtml-let listVar=sequence-item>
+					<dtml-in "REQUEST[listVar]">
+						<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
+					</dtml-in>
+				</dtml-let>
+			<dtml-else>
+				<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
+			</dtml-if>
+
+		</dtml-in>
+
+<input type="HIDDEN" name="doGroup" value="1">
+<b><dtml-babel src="'en'">This Membership Source has no configuration Items</dtml-babel></b><br>
+<br>
+<input type="SUBMIT" value="<dtml-babel src="'en'">Add</dtml-babel>">
+</form>
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/nullMemberSource.py b/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/nullMemberSource.py
new file mode 100644
index 0000000000000000000000000000000000000000..49f26a5019fe29e9c60e1e6d6731055d33f1a4d9
--- /dev/null
+++ b/ZopeProducts/exUserFolder/MembershipSources/nullMemberSource/nullMemberSource.py
@@ -0,0 +1,49 @@
+#
+# Extensible User Folder
+# 
+# Null Membership Source for exUserFolder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: nullMemberSource.py,v 1.1 2004/11/10 14:15:55 akm Exp $
+from Globals import HTMLFile, INSTANCE_HOME
+
+from OFS.Folder import Folder
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import PluginRegister
+from Products.exUserFolder.nullPlugin import nullPlugin
+
+def manage_addNullMemberSource(self, REQUEST):
+	""" Add a Membership Source """
+	self.currentMembershipSource=None
+	return ''
+
+
+manage_addNullMemberSourceForm=HTMLFile('manage_addNullPluginSourceForm',globals())
+manage_editNullMemberSourceForm=None
+
+		
+nullMemberReg=PluginRegister('nullMemberSource',
+							  'Null Membership Source',
+							 nullPlugin,
+							 manage_addNullMemberSourceForm,
+							 manage_addNullMemberSource,
+							 manage_editNullMemberSourceForm)
+exUserFolder.membershipSources['nullMemberSource']=nullMemberReg
+
diff --git a/ZopeProducts/exUserFolder/Plugins.py b/ZopeProducts/exUserFolder/Plugins.py
new file mode 100644
index 0000000000000000000000000000000000000000..58cdc2ef2422827f6b001f2e46be5fea795a003d
--- /dev/null
+++ b/ZopeProducts/exUserFolder/Plugins.py
@@ -0,0 +1,46 @@
+#
+#
+# (C) Copyright 2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: Plugins.py,v 1.5 2004/11/10 14:15:33 akm Exp $
+
+import App, Globals, OFS
+import string
+import time
+
+from Globals import ImageFile, HTMLFile, HTML, MessageDialog, package_home
+from OFS.Folder import Folder
+
+class PluginRegister:
+	def __init__(self, name, description, pluginClass,
+				 pluginStartForm, pluginStartMethod,
+				 pluginEditForm=None, pluginEditMethod=None):
+		self.name=name #No Spaces please...
+		self.description=description
+		self.plugin=pluginClass
+		self.manage_addForm=pluginStartForm
+		self.manage_addMethod=pluginStartMethod
+		self.manage_editForm=pluginEditForm
+		self.manage_editMethod=pluginEditMethod
+
+class CryptoPluginRegister:
+	def __init__(self, name, crypto, description, pluginMethod):
+		self.name = name #No Spaces please...
+		self.cryptoMethod = crypto 
+		self.description = description
+		self.plugin = pluginMethod
diff --git a/ZopeProducts/exUserFolder/PropSources/__init__.py b/ZopeProducts/exUserFolder/PropSources/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e11388e46bebcdc73706be5c74fc73918b5a34cf
--- /dev/null
+++ b/ZopeProducts/exUserFolder/PropSources/__init__.py
@@ -0,0 +1,28 @@
+#
+# Extensible User Folder
+# 
+# (C) Copyright 2000-2004 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: __init__.py,v 1.1 2004/11/10 14:15:55 akm Exp $
+
+
+import nullPropSource
+# aucune autre prop source pour ScoDoc
+
+
+
diff --git a/ZopeProducts/exUserFolder/PropSources/nullPropSource/.cvsignore b/ZopeProducts/exUserFolder/PropSources/nullPropSource/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/PropSources/nullPropSource/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/PropSources/nullPropSource/__init__.py b/ZopeProducts/exUserFolder/PropSources/nullPropSource/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..14ecc0148d3249be8a1f3a703654b6742997aa55
--- /dev/null
+++ b/ZopeProducts/exUserFolder/PropSources/nullPropSource/__init__.py
@@ -0,0 +1,2 @@
+# $Id: __init__.py,v 1.1 2004/11/10 14:15:56 akm Exp $
+import nullPropSource
diff --git a/ZopeProducts/exUserFolder/PropSources/nullPropSource/manage_addNullPluginSourceForm.dtml b/ZopeProducts/exUserFolder/PropSources/nullPropSource/manage_addNullPluginSourceForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..a1979b0e3c1d761848d61b4a2442d2cb75dbacc7
--- /dev/null
+++ b/ZopeProducts/exUserFolder/PropSources/nullPropSource/manage_addNullPluginSourceForm.dtml
@@ -0,0 +1,21 @@
+<dtml-var "DialogHeader(_.None, _, DialogTitle='Add Basic Property Source', dialog_width='')">
+<FORM ACTION="&dtml-URL;" METHOD="POST">
+		<dtml-in "REQUEST.form.keys()">
+			<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
+				<dtml-let listVar=sequence-item>
+					<dtml-in "REQUEST[listVar]">
+						<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
+					</dtml-in>
+				</dtml-let>
+			<dtml-else>
+				<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
+			</dtml-if>
+
+		</dtml-in>
+
+<input type="HIDDEN" name="doMember" value="1">
+<b><dtml-babel src="'en'">This Property Source has no configuration Items</dtml-babel></b><br>
+<br>
+<input type="SUBMIT" value="<dtml-babel src="'en'">Add</dtml-babel>">
+</form>
+<dtml-var DialogFooter>
diff --git a/ZopeProducts/exUserFolder/PropSources/nullPropSource/nullPropSource.py b/ZopeProducts/exUserFolder/PropSources/nullPropSource/nullPropSource.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab269b03dd19fdf27297650efa47d3a51577d516
--- /dev/null
+++ b/ZopeProducts/exUserFolder/PropSources/nullPropSource/nullPropSource.py
@@ -0,0 +1,50 @@
+#
+# Extensible User Folder
+# 
+# Null Membership Source for exUserFolder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: nullPropSource.py,v 1.1 2004/11/10 14:15:56 akm Exp $
+from Globals import HTMLFile, INSTANCE_HOME
+
+from OFS.Folder import Folder
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import PluginRegister
+from Products.exUserFolder.nullPlugin import nullPlugin
+
+def manage_addNullPropSource(self, REQUEST):
+	""" Add a Property Source """
+	self.currentPropSource=None
+	return ''
+
+
+manage_addNullPropSourceForm=HTMLFile('manage_addNullPluginSourceForm',globals())
+manage_editNullPropSourceForm=None
+
+		
+nullPropReg=PluginRegister('nullPropSource',
+						   'Null Property Source',
+						   nullPlugin,
+						   manage_addNullPropSourceForm,
+						   manage_addNullPropSource,
+						   manage_editNullPropSourceForm)
+
+exUserFolder.propSources['nullPropSource']=nullPropReg
+
diff --git a/ZopeProducts/exUserFolder/PropertyEditor/.cvsignore b/ZopeProducts/exUserFolder/PropertyEditor/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/PropertyEditor/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/PropertyEditor/PropertyEditor.py b/ZopeProducts/exUserFolder/PropertyEditor/PropertyEditor.py
new file mode 100644
index 0000000000000000000000000000000000000000..711d4a0fe11bead1bd087aea2273c049c4f86cd7
--- /dev/null
+++ b/ZopeProducts/exUserFolder/PropertyEditor/PropertyEditor.py
@@ -0,0 +1,122 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: PropertyEditor.py,v 1.3 2002/01/29 17:42:02 alex_zxc Exp $
+
+from Globals import DTMLFile, MessageDialog, INSTANCE_HOME
+from string import join,strip,split,lower,upper,find
+from urllib import quote, unquote
+
+
+def editStringProperty( name, value):
+	""" """
+	return('<input type="TEXT" name="%s:string" value="%s">\n'%(name, value))
+
+def viewStringProperty( name, value):
+	""" """
+	return('<input type="HIDDEN" name="propValue:string" value="%s"><br>%s<br>\n'%(value, value))
+
+
+def editIntegerProperty( name, value):
+	""" """
+	return('<input type="TEXT" name="%s:int" value="%d">\n'%(name, value or 0))
+
+def viewIntegerProperty( name, value):
+	""" """
+	return('<input type="HIDDEN" name="propValue:int" value="%d"><br>%d<br>\n'%(value or 0 , value or 0))
+
+
+def editLongProperty( name, value):
+	""" """
+	return('<input type="TEXT" name="%s:int" value="%d">\n'%(name, value or 0))	
+
+def viewLongProperty( name, value):
+	""" """
+	return('<input type="HIDDEN" name="propValue:long" value="%d"><br>%d<br>\n'%(value or 0, value or 0))
+
+
+def editFloatProperty( name, value):
+	""" """
+	return('<input type="TEXT" name="%s:float" value="%d">\n'%(name, value))
+
+def viewFloatProperty( name, value):
+	""" """
+	return('<input type="HIDDEN" name="propValue:float" value="%f"><br>%f<br>\n'%(value, value))
+
+
+def editListProperty( name, value):
+	a=''
+	if value:
+		a = a + 'Select Items to keep<br>\n'
+		a = a + '<select name="%s:list" multiple>\n'%(name)
+		for i in value:
+			a = a + (
+				'<option value="%s" SELECTED>%s\n'%(i, i))
+		a = a + '</select>\n<br>'
+	a = a + 'Add an item\n<br>'
+	a = a + '<input type="TEXT" name="%s:list">'%(name)
+	return(a)
+
+def viewListProperty( name, value):
+	a=''
+	if value:
+		for i in value:
+			a = a + (
+				'<input type="HIDDEN" name="propValue:list" value="%s">\n'%(i))
+			a = a + '%s\n<br>'%(i)
+	return(a)
+
+
+def editDictProperty( name, value):
+	""" """
+	a=''
+	if value and value.keys():
+		for i in value.keys():
+			a = a + '%s : <input type="TEXT" name="%s.%s" value="%s">\n<br>'%(i, name, i, value[i])
+	return a
+
+
+def viewDictProperty( name, value):
+	""" """
+	a=''
+	if value and value.keys():
+		for i in value.keys():
+			a = a + '%s : <input type="HIDDEN" name="propValue.%s" value="%s">\n<br>'%(i, name, i, value[i])
+			a = a + '%s\n<br>'%(value[i])
+	return a
+
+EditMethods={'String':editStringProperty,
+			 'Integer':editIntegerProperty,
+			 'Long':editLongProperty,
+			 'Float':editFloatProperty,
+			 'List':editListProperty,
+			 'Dict':editDictProperty}
+			 
+
+ViewMethods={'String':viewStringProperty,
+			 'Integer':viewIntegerProperty,
+			 'Long':viewLongProperty,
+			 'Float':viewFloatProperty,
+			 'List':viewListProperty,
+			 'Dict':viewDictProperty}
+			 
+
+
+
diff --git a/ZopeProducts/exUserFolder/PropertyEditor/__init__.py b/ZopeProducts/exUserFolder/PropertyEditor/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4189a84d1e5baede66f9b7fb4795b60110ddd7f5
--- /dev/null
+++ b/ZopeProducts/exUserFolder/PropertyEditor/__init__.py
@@ -0,0 +1,22 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: __init__.py,v 1.2 2001/12/01 08:40:04 akm Exp $
+import PropertyEditor
diff --git a/ZopeProducts/exUserFolder/README.Upgrading b/ZopeProducts/exUserFolder/README.Upgrading
new file mode 100644
index 0000000000000000000000000000000000000000..384001a40a7ee0dc8d2b4ce7d993043f27016646
--- /dev/null
+++ b/ZopeProducts/exUserFolder/README.Upgrading
@@ -0,0 +1,14 @@
+If you are upgrading an existing site from < 0.50
+
+I have restructured the source tree. This will make this version
+incompatible with previous versions, as the classes have moved. This
+breaks upgrading existing installs unless you keep the old classes
+around. If you only use external Auth/Prop/Group sources, you will
+probably be unaffected.
+
+This means for those of you using SQL or LDAP or any non-ZODB sources,
+you can remove and then re-add your XUF acl_users to get going again.
+
+If you are using a ZODB source, then you need to keep the old classes
+and the old paths around (e.g. symlink zodbAuthSource to
+AuthSources/zodbAuthSource).
diff --git a/ZopeProducts/exUserFolder/User.py b/ZopeProducts/exUserFolder/User.py
new file mode 100644
index 0000000000000000000000000000000000000000..7b55e0f9a756c7f4eb82fde03fbfe594a6f92303
--- /dev/null
+++ b/ZopeProducts/exUserFolder/User.py
@@ -0,0 +1,243 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: User.py,v 1.10 2004/12/14 05:30:29 akm Exp $
+
+##############################################################################
+#
+# Zope Public License (ZPL) Version 0.9.4
+# ---------------------------------------
+# 
+# Copyright (c) Digital Creations.  All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+# 
+# 1. Redistributions in source code must retain the above
+#    copyright notice, this list of conditions, and the following
+#    disclaimer.
+# 
+# 6. Redistributions of any form whatsoever must retain the
+#    following acknowledgment:
+# 
+#      "This product includes software developed by Digital
+#      Creations for use in the Z Object Publishing Environment
+#      (http://www.zope.org/)."
+# 
+# Disclaimer
+# 
+#   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND
+#   ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+#   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+#   FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT
+#   SHALL DIGITAL CREATIONS OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+#   DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+#   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+#   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+#   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+#   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+#   LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+#   IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+#   THE POSSIBILITY OF SUCH DAMAGE.
+#
+##############################################################################
+from AccessControl.User import BasicUser
+from string import join,strip,split,lower,upper,find
+
+class XUFUser(BasicUser):
+
+	icon='misc_/exUserFolder/exUser.gif'
+
+	# cacheable is a dict that must contain at least name, password,
+	# roles, and domains -- unless you're working with your own User class,
+	# in which case you need to override __init__ and define it yourself.
+	def __init__(self, cacheable, propSource, cryptPassword, authSource,
+				 groupSource=None):
+		self.name   =cacheable['name']
+		self.__     =cacheable['password']
+		if cacheable['roles']:
+			self.roles = filter(None, cacheable['roles'])
+		else:
+			self.roles = []
+		# domains may be passed as a string or a list
+		if type(cacheable['domains']) == type(''):
+			self.domains=filter(None, map(strip,
+										  split(cacheable['domains'], ',')))
+		else:
+			self.domains=cacheable['domains']
+		self._authSource=authSource
+		self._propSource=propSource
+		self._groupSource=groupSource		
+		self.cryptPassword=cryptPassword
+
+	def getUserName(self):
+		return self.name
+
+	def _getPassword(self):
+		return self.__
+
+	def getRoles(self):
+		return tuple(self.roles) + ('Authenticated',)
+    
+	def getDomains(self):
+		return self.domains
+
+	# Ultra generic way of getting, checking and setting properties
+	def getProperty(self, property, default=None):
+		if self._propSource:
+			return self._propSource.getUserProperty(property, self.name, default)
+
+	def hasProperty(self, property):
+		if self._propSource:
+			return self._propSource.hasProperty(property)
+
+	def setProperty(self, property, value):
+		if property[0]=='_':
+			return
+		if self._propSource:
+			return self._propSource.setUserProperty(property, self.name, value)
+
+	def setTempProperty(self, property, value):
+		if property[0]=='_':
+			return
+		if self._propSource:
+			return self._propSource.setTempProperty(property, value)
+
+	def flushTempProperties(self):
+		if self._propSource:
+			return self._propSource.flushTempProperties()
+
+	def delProperty(self, property):
+		if property[0]=='_':
+			return
+		if self._propSource:
+			return self._propSource.delUserProperty(property, self.name)
+		
+	def listProperties(self):
+		if self._propSource:
+			return self._propSource.listUserProperties(self.name)
+
+	# Try to allow User['property'] -- won't work for password d;)
+	def __getitem__(self, key):
+		# Don't return 'private' keys
+		if key[0] != '_':
+			if hasattr(self, key):
+				return getattr(self, key)
+			if self._propSource and self._propSource.hasProperty(key):
+
+				return self._propSource.getUserProperty(key, self.name)
+		raise KeyError, key
+
+	def __setitem__(self, key, value):
+		if key[0]=='_':
+			return
+		if self._propSource:
+			self._propSource.setUserProperty(key, self.name, value)
+		
+	# List one user is supplied by the Auth Source...
+	
+	def authenticate(self, listOneUser, password, request, remoteAuth=None):
+		result=listOneUser(username=self.name)
+		for people in result:
+			if remoteAuth:
+				return remoteAuth(self.name, password)
+			else:
+				secret=self.cryptPassword(self.name, password)
+				return secret==people['password']
+		return None
+
+	# You can set logout times or whatever here if you want to, the
+	# property source is still active.
+	def notifyCacheRemoval(self):
+		if self._propSource:
+			self._propSource.flushTempProperties()
+
+	# You must override this and __init__ if you are subclassing
+	# the user object, or your user object may not be reconstructed
+	# properly!  All values in this dict must be non-Persistent objects
+	# or types, and may not hold any references to Persistent objects,
+	# or the cache will break.
+	def _getCacheableDict(self):
+		return {'name':		self.name,
+				'password':	self.__,
+				'roles':	self.roles,
+				'domains':	self.domains}
+
+	def getGroups(self):
+		if self._groupSource:
+			return self._groupSource.getGroupsOfUser(self.name)
+		else:
+			return ()
+
+
+	def _setGroups(self, groupnames):
+		if self._groupSource:
+			return self._groupSource.setGroupsOfUser(groupnames, self.name)
+
+
+	def _addGroups(self, groupnames):
+		if self._groupSource:
+			return self._groupSource.addGroupsToUser(groupnames, self.name)
+
+	def _delGroups(self, groupnames):
+		if self._groupSource:
+			return self._groupSource.delGroupsFromUser(groupnames, self.name)
+
+	def getId(self):
+		if self._propSource and self._propSource.getUserProperty('userid', self.name):
+			return self._propSource.getUserProperty('userid', self.name)
+		return self.name
+
+
+#
+# An Anonymous User for session tracking...
+# Can set and get properties just like a normal user.
+#
+# These objects live in the cache, so, we have a __del__ method to
+# clean ourselves up.
+#
+
+class XUFAnonUser(XUFUser):
+	def __init__(self, name, roles, propSource):
+		self.name   =name
+		self.__     =''
+		self.roles  =filter(None, roles)
+		self._propSource=propSource
+
+	def getRoles(self):
+		return tuple(self.roles) + ('Anonymous',)
+
+	def authenticate(self, listOneUser, password, request, remoteAuth=None):
+		return 1
+	
+	def notifyCacheRemoval(self):
+		if self._propSource:
+			self._propSource.deleteUsers([self.name,])
+
+# We now set up a dummy classes so that people can extend the User objects
+# or override stuff with much less pain --akm
+
+class User(XUFUser):
+	pass
+
+class AnonUser(XUFAnonUser):
+	pass
+
diff --git a/ZopeProducts/exUserFolder/UserCache/.cvsignore b/ZopeProducts/exUserFolder/UserCache/.cvsignore
new file mode 100644
index 0000000000000000000000000000000000000000..57c4bccb72c2d947e23fba28042f1b81ad5b5612
--- /dev/null
+++ b/ZopeProducts/exUserFolder/UserCache/.cvsignore
@@ -0,0 +1,4 @@
+*.pyc
+*.pyo
+*~
+.*.swp
diff --git a/ZopeProducts/exUserFolder/UserCache/UserCache.py b/ZopeProducts/exUserFolder/UserCache/UserCache.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3b6a94233fadcf78bdc453cf17970df5bc348c1
--- /dev/null
+++ b/ZopeProducts/exUserFolder/UserCache/UserCache.py
@@ -0,0 +1,409 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: UserCache.py,v 1.16 2003/07/10 21:11:26 akm Exp $
+
+# Module Level Caches for Various User Operations
+
+from time import time
+from BTrees.OOBTree import OOBTree
+import threading
+from Acquisition import aq_inner
+from Products.exUserFolder.User import User
+
+class UserCacheItem:
+	lastAccessed=0
+	def __init__(self, username, password, cacheable):
+		self.username=username
+		self.password=password
+		self.cacheable=cacheable
+		self.lastAccessed=time()
+
+	def touch(self):
+		self.lastAccessed=time()
+		
+	def __repr__(self):
+		return self.username
+
+class NegativeUserCacheItem(UserCacheItem):
+	def __init__(self, username):
+		self.username=username
+		self.lastAccessed=time()
+
+class AdvancedCookieCacheItem(UserCacheItem):
+	lastAccessed=0
+	def __init__(self, username, password):
+		self.username=username
+		self.password=password
+		self.lastAccessed=time()
+
+class SessionExpiredException(Exception):
+    'User Session Expired'
+
+
+class UserCache:
+	def __init__(self, sessionLength):
+		self.sessionLength=sessionLength
+		self.cache=OOBTree()
+		self.hits=0
+		self.fail=0
+		self.nouser=0
+		self.attempts=0
+		self.timeouts=0
+		self.cacheStarted=time()
+		self.lock=threading.Lock()
+		
+	def addToCache(self, username, password, User):
+		self.lock.acquire()		
+		try:
+			if not self.sessionLength:
+				return
+
+			try:
+				u = self.cache.items(username)
+				if u:
+					for x in self.cache[username]:
+						self.cache.remove(x)
+			except:
+				pass
+
+			u = UserCacheItem(username, password, User._getCacheableDict())
+			self.cache[username]=u
+		finally:
+			self.lock.release()
+
+	def getUser(self, caller, username, password, checkpassword=1):
+		self.lock.acquire()
+		try:
+			if not self.sessionLength:
+				return None
+
+			self.attempts=self.attempts+1
+
+			u = None
+			try:
+				u = self.cache[username]
+			except KeyError:
+				self.nouser=self.nouser+1
+				return None
+
+			now = time()
+			if u:
+				if checkpassword and (u.password != password):
+					self.fail=self.fail+1
+					del self.cache[u.username]
+				elif self.sessionLength and (
+					(now - u.lastAccessed) > self.sessionLength):
+					del self.cache[u.username]
+					self.timeouts=self.timeouts+1
+					user_object=User(u.cacheable,
+									 caller.currentPropSource,
+									 caller.cryptPassword,
+									 caller.currentAuthSource,
+									 caller.currentGroupSource)
+					user_object.notifyCacheRemoval()
+					del u
+					raise SessionExpiredException
+				else:
+					u.touch()
+					self.hits=self.hits+1
+					return User(u.cacheable,
+								caller.currentPropSource,
+								caller.cryptPassword,
+								caller.currentAuthSource,
+								caller.currentGroupSource)
+
+			self.nouser=self.nouser+1
+			return None
+		finally:
+			self.lock.release()
+
+	def removeUser(self, username):
+		self.lock.acquire()
+		try:
+			if not self.sessionLength:
+				return
+			try:
+				if self.cache[username]:
+					del self.cache[username]
+			except:
+				pass
+		finally:
+			self.lock.release()
+
+	def getCacheStats(self):
+		self.lock.acquire()
+		try:
+			return (
+				{'attempts':self.attempts,
+				 'hits':self.hits,
+				 'fail':self.fail,
+				 'misses':self.nouser,
+				 'cachesize':len(self.cache),
+				 'time':self.cacheStarted,
+				 'timeouts':self.timeouts,
+				 'length':self.sessionLength})
+		finally:
+			self.lock.release()
+		
+	def getCurrentUsers(self, caller):
+		self.lock.acquire()
+		try:
+			x=[]
+			now = time()		
+			for z in self.cache.keys():
+				u = self.cache[z]
+				if self.sessionLength and (
+					(now - u.lastAccessed) > self.sessionLength):
+					del self.cache[u.username]
+					self.timeouts=self.timeouts+1
+					user_object=User(u.cacheable,
+									 caller.currentPropSource,
+									 caller.cryptPassword,
+									 caller.currentAuthSource,
+									 caller.currentGroupSource)
+					user_object.notifyCacheRemoval()
+					del u
+				else:
+					x.append({'username':u.username,
+							  'lastAccessed':u.lastAccessed})
+			return x
+		finally:
+			self.lock.release()
+
+class NegativeUserCache:
+	def __init__(self, sessionLength):
+		self.sessionLength=sessionLength
+		self.cache=OOBTree()
+		self.hits=0
+		self.cacheStarted=time()
+		self.lock=threading.Lock()
+		
+	def addToCache(self, username):
+		self.lock.acquire()		
+		try:
+			if not self.sessionLength:
+				return
+
+			try:
+				u = self.cache.items(username)
+				if u:
+					for x in self.cache[username]:
+						self.cache.remove(x)
+			except:
+				pass
+
+			u = NegativeUserCacheItem(username)
+			self.cache[username]=u
+		finally:
+			self.lock.release()
+
+	def getUser(self, username):
+		self.lock.acquire()
+		try:
+			if not self.sessionLength:
+				return 0
+
+			u = None
+			try:
+				u = self.cache[username]
+			except KeyError:
+				return 0
+
+			now = time()
+			if u:
+				if self.sessionLength and (
+					(now - u.lastAccessed) > self.sessionLength):
+					del self.cache[u.username]
+				else:
+					# We don't touch negative user caches
+					# u.touch()
+					self.hits=self.hits+1
+					return 1
+			return 0
+		finally:
+			self.lock.release()
+
+	def removeUser(self, username):
+		self.lock.acquire()
+		try:
+			if not self.sessionLength:
+				return
+			try:
+				del self.cache[username]
+			except:
+				pass
+		finally:
+			self.lock.release()					
+
+class CookieCache:
+	def __init__(self, sessionLength):
+		self.sessionLength=sessionLength
+		self.cache=OOBTree()
+		self.hits=0
+		self.cacheStarted=time()
+		self.lock=threading.Lock()
+		
+	def addToCache(self, username, password, key):
+		self.lock.acquire()		
+		try:
+			if not self.sessionLength:
+				return
+
+			try:
+				u = self.cache.items(key)
+				if u:
+					for x in self.cache[key]:
+						self.cache.remove(x)
+			except:
+				pass
+			u = AdvancedCookieCacheItem(username, password)
+			self.cache[key]=u
+		finally:
+			self.lock.release()
+
+	def getUser(self, key):
+		self.lock.acquire()
+		try:
+			if not self.sessionLength:
+				return None
+
+			u = None
+			try:
+				u = self.cache[key]
+			except KeyError:
+				return None
+
+			now = time()
+			if u:
+				if self.sessionLength and (
+					(now - u.lastAccessed) > self.sessionLength):
+					del self.cache[key]
+				else:
+					# We don't touch negative user caches
+					# u.touch()
+					self.hits=self.hits+1
+					return u.username, u.password
+			return None
+		finally:
+			self.lock.release()
+
+	def removeUser(self, key):
+		self.lock.acquire()
+		try:
+			if not self.sessionLength:
+				return
+			try:
+				del self.cache[key]
+			except:
+				pass
+		finally:
+			self.lock.release()
+
+class GlobalUserCache:
+	caches={}
+	def __init__(self):
+		self.lock = threading.Lock()
+
+	def createCache(self, who, sessionLength):
+		self.lock.acquire()
+		try:
+			self.caches[who]=UserCache(sessionLength)
+			return self.caches[who]
+		finally:
+			self.lock.release()
+
+	def getCache(self, who):
+		self.lock.acquire()
+		try:
+			if self.caches.has_key(who):
+				return self.caches[who]
+			else:
+				return None
+		finally:
+			self.lock.release()
+			
+	def deleteCache(self, who):
+		self.lock.acquire()
+		try:
+			del self.caches[who]
+		finally:
+			self.lock.release()
+
+class GlobalNegativeUserCache:
+	caches={}
+	def __init__(self):
+		self.lock = threading.Lock()
+
+	def createCache(self, who, sessionLength):
+		self.lock.acquire()
+		try:
+			self.caches[who]=NegativeUserCache(sessionLength)
+			return self.caches[who]
+		finally:
+			self.lock.release()
+
+	def getCache(self, who):
+		self.lock.acquire()
+		try:
+			if self.caches.has_key(who):
+				return self.caches[who]
+			else:
+				return None
+		finally:
+			self.lock.release()
+			
+	def deleteCache(self, who):
+		self.lock.acquire()
+		try:
+			del self.caches[who]
+		finally:
+			self.lock.release()
+
+class GlobalAdvancedCookieCache:
+	caches={}
+	def __init__(self):
+		self.lock = threading.Lock()
+
+	def createCache(self, who, sessionLength):
+		self.lock.acquire()
+		try:
+			self.caches[who]=CookieCache(sessionLength)
+			return self.caches[who]
+		finally:
+			self.lock.release()
+
+	def getCache(self, who):
+		self.lock.acquire()
+		try:
+			if self.caches.has_key(who):
+				return self.caches[who]
+			else:
+				return None
+		finally:
+			self.lock.release()
+			
+	def deleteCache(self, who):
+		self.lock.acquire()
+		try:
+			del self.caches[who]
+		finally:
+			self.lock.release()
+	
diff --git a/ZopeProducts/exUserFolder/UserCache/__init__.py b/ZopeProducts/exUserFolder/UserCache/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a484ebdb6d6259ad4fa24fb6c6e872b067a677d
--- /dev/null
+++ b/ZopeProducts/exUserFolder/UserCache/__init__.py
@@ -0,0 +1,22 @@
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: __init__.py,v 1.2 2001/12/01 08:40:04 akm Exp $
+import UserCache
diff --git a/ZopeProducts/exUserFolder/__init__.py b/ZopeProducts/exUserFolder/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..49f0cbcc1b24c52d64cb8f0c906c7c7a4562b1ae
--- /dev/null
+++ b/ZopeProducts/exUserFolder/__init__.py
@@ -0,0 +1,85 @@
+#
+# Extensible User Folder
+# 
+# (C) Copyright 2000-2005 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: __init__.py,v 1.18 2004/11/10 14:15:33 akm Exp $
+
+import exUserFolder
+
+import CryptoSources
+import AuthSources
+import PropSources
+import MembershipSources
+import GroupSources
+
+from GroupSource import GroupSource
+
+from App.ImageFile import ImageFile
+import OFS
+
+#
+# Install a dummy ZBabel setup if we don't have ZBabel installed.
+#
+import dummyZBabelTag
+
+# Methods we need access to from any ObjectManager context
+legacy_methods = (
+	    ('manage_addexUserFolderForm', exUserFolder.manage_addexUserFolderForm),
+	    ('manage_addexUserFolder',     exUserFolder.manage_addexUserFolder),
+	    ('getAuthSources',             exUserFolder.getAuthSources),
+	    #('getPropSources',             exUserFolder.getPropSources),
+		('getCryptoSources',           exUserFolder.getCryptoSources),
+	    ('getMembershipSources',       exUserFolder.getMembershipSources),
+	    ('getGroupSources',            exUserFolder.getGroupSources),
+	    ('doAuthSourceForm',           exUserFolder.doAuthSourceForm),
+	    #('doPropSourceForm',           exUserFolder.doPropSourceForm),
+	    ('doMembershipSourceForm',     exUserFolder.doMembershipSourceForm),
+        #	    ('doGroupSourceForm',          exUserFolder.doGroupSourceForm),
+	    ('getVariableType',            exUserFolder.getVariableType),
+	    ('DialogHeader',               exUserFolder.exUserFolder.DialogHeader),
+	    ('DialogFooter',               exUserFolder.exUserFolder.DialogFooter),
+	    #('MailHostIDs',                exUserFolder.MailHostIDs),
+	    )
+
+# Image files to place in the misc_ object so they are accesible from misc_/exUserFolder
+misc_={'exUserFolder.gif': ImageFile('exUserFolder.gif', globals()),
+       'exUserFolderPlugin.gif': ImageFile('exUserFolderPlugin.gif', globals()),
+       'exUser.gif': ImageFile('exUser.gif', globals()),
+       }
+
+
+def initialize(context):
+    """
+    Register base classes
+    """
+    context.registerClass(exUserFolder.exUserFolder,
+			  meta_type="ex User Folder",
+			  permission="Add exUser Folder",
+			  constructors=(exUserFolder.manage_addexUserFolderForm,
+					exUserFolder.manage_addexUserFolder,),
+			  legacy=legacy_methods,
+			  icon="exUserFolder.gif")
+
+    context.registerClass(GroupSource.GroupSource,
+			  meta_type="ex User Folder Group Source",
+			  permission="Add exUser Folder",
+			  constructors=(GroupSource.manage_addGroupSourceForm,
+					GroupSource.manage_addGroupSource,),
+			  icon="exUserFolderPlugin.gif")
+
diff --git a/ZopeProducts/exUserFolder/common/DialogFooter.dtml b/ZopeProducts/exUserFolder/common/DialogFooter.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..00add5fe28949365624085747575f291f461d1bf
--- /dev/null
+++ b/ZopeProducts/exUserFolder/common/DialogFooter.dtml
@@ -0,0 +1,4 @@
+</td>
+</tr>
+</table>
+</td></tr></table>
\ No newline at end of file
diff --git a/ZopeProducts/exUserFolder/common/DialogHeader.dtml b/ZopeProducts/exUserFolder/common/DialogHeader.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..fad5379425b1bfce8b62fdd4c4948800f1d397b9
--- /dev/null
+++ b/ZopeProducts/exUserFolder/common/DialogHeader.dtml
@@ -0,0 +1,7 @@
+<table bgcolor="#808080" width="&dtml.missing-dialog_width;"><tr><td align="center" valign="middle">
+<table width="100%" border="1" bgcolor="White" cellpadding="0" cellspacing="0">
+<tr>
+    <td align="center"><table width="100%" bgcolor="#dddddd" border="0" cellspacing="0"><tr><td align="center"><font color="Black" size=+1 face="Helvetica"><b><dtml-babel src="'en'" literal="1"><dtml-var DialogTitle></dtml-babel></b></font></td></tr></table></td>
+</tr>
+<tr>
+   <td>
diff --git a/ZopeProducts/exUserFolder/common/MessageDialog.dtml b/ZopeProducts/exUserFolder/common/MessageDialog.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..0d0bc5557fa4e84f84666fd013211f4c332d5a4d
--- /dev/null
+++ b/ZopeProducts/exUserFolder/common/MessageDialog.dtml
@@ -0,0 +1,41 @@
+<head><body>
+<FORM ACTION="<dtml-var action>" METHOD="POST" <dtml-if target>TARGET="<dtml-var target>"</dtml-if>>
+<dtml-in "REQUEST.form.keys()">
+	<dtml-if "getVariableType(REQUEST[_['sequence-item']]) == 'List'">
+		<dtml-let listVar=sequence-item>
+			<dtml-in "REQUEST[listVar]">
+				<input type="HIDDEN" name="<dtml-var listVar>:list" value="<dtml-var sequence-item>">
+			</dtml-in>
+		</dtml-let>
+	<dtml-else>
+		<input type="HIDDEN" name="<dtml-var sequence-item>" value="<dtml-var "REQUEST[_.getitem('sequence-item',0)]">">
+	</dtml-if>
+</dtml-in>
+<dtml-var "DialogHeader(DialogTitle=title, dialog_width='')">
+<TABLE BORDER="0" WIDTH="100%" CELLPADDING="10">
+<TR>
+  <TD VALIGN="TOP">
+  <BR>
+  <CENTER><B><FONT SIZE="+6" COLOR="#77003B">!</FONT></B></CENTER>
+  </TD>
+  <TD VALIGN="TOP">
+  <BR><BR>
+  <CENTER>
+  <dtml-babel src="'en'" literal="1"><dtml-var message></dtml-babel>
+  </CENTER>
+  </TD>
+</TR>
+<TR>
+  <TD VALIGN="TOP">
+  </TD>
+  <TD VALIGN="TOP">
+  <CENTER>
+  <INPUT TYPE="SUBMIT" VALUE="   <dtml-babel src="'en'">Ok</dtml-babel>   ">
+  </CENTER>
+  </TD>
+</TR>
+</TABLE>
+<dtml-var DialogFooter>
+</FORM>
+
+</head></body>
diff --git a/ZopeProducts/exUserFolder/common/manage_tabs.dtml b/ZopeProducts/exUserFolder/common/manage_tabs.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..c6c66c2e4792856453aa26e4fc2454b468ddeb65
--- /dev/null
+++ b/ZopeProducts/exUserFolder/common/manage_tabs.dtml
@@ -0,0 +1,176 @@
+<dtml-with "_(manage_options=filtered_manage_options())">
+<dtml-if manage_options>
+<dtml-call "REQUEST.set('n_', _.len(manage_options)-1)">
+<dtml-call "REQUEST.set('a_', 0)">
+<dtml-in manage_options mapping>
+<dtml-if expr="URL[-(_.len(action)):]==action or
+                URL[-17:]=='/manage_workspace' and _['sequence-start']">
+<dtml-call "REQUEST.set('a_', _['sequence-index'])">
+</dtml-if>
+<dtml-if "_.has_key('management_view') and management_view==label">
+<dtml-call "REQUEST.set('a_', _['sequence-index'])">
+</dtml-if>
+</dtml-in>
+
+
+<table cellpadding="0" cellspacing="0" width="100%" border="0">
+
+<tr>
+  <td bgcolor="#000000" rowspan="5" width="10%" valign="bottom" 
+   align="left">&nbsp;&nbsp;<img src="&dtml-BASEPATH1;/p_/sp" 
+   width="2" height="1" alt="" />
+  </td>
+  <td bgcolor="#000000" colspan="<dtml-var "4 * (n_ + 1)">"><img 
+   src="&dtml-BASEPATH1;/p_/sp" width="1" height="5" alt="" /></td>
+</tr>
+
+<tr>
+<dtml-in manage_options>
+<dtml-if "_['sequence-index']==a_">
+  <td bgcolor="#ffffff" rowspan="2" valign="top" 
+   align="left"><img src="&dtml-BASEPATH1;/p_/ltab" width="5" 
+   height="5" alt="" /></td>
+  <td bgcolor="#ffffff"><img src="&dtml-BASEPATH1;/p_/sp"
+   width="1" height="2" alt="" /></td>
+  <td bgcolor="#ffffff" rowspan="2" valign="top" 
+   align="right"><img src="&dtml-BASEPATH1;/p_/rtab" width="5" 
+   height="5" alt="" /></td>
+  <td bgcolor="#000000" rowspan="4"><img src="&dtml-BASEPATH1;/p_/sp"
+ width="2" height="1" alt="" /></td>
+<dtml-else>
+  <td bgcolor="#efefef" rowspan="2" valign="top" 
+   align="left"><img src="&dtml-BASEPATH1;/p_/ltab" width="5" 
+   height="5" alt="" /></td>
+  <td bgcolor="#efefef"><img src="&dtml-BASEPATH1;/p_/sp"
+ width="1" height="2" alt="" /></td>
+  <td bgcolor="#efefef" rowspan="2" valign="top" 
+   align="right"><img src="&dtml-BASEPATH1;/p_/rtab" width="5" 
+   height="5" alt="" /></td>
+  <td bgcolor="#000000" rowspan="4"><img src="&dtml-BASEPATH1;/p_/sp"
+ width="2" height="1" alt="" /></td>
+</dtml-if>
+</dtml-in>
+</tr>
+<tr>
+<dtml-in manage_options mapping>
+<dtml-if "_['sequence-index']==a_">
+  <td bgcolor="#ffffff" valign="bottom" class="tab-small" 
+   align="center"><font face="Verdana, Arial, Helvetica" 
+   size="1" color="#000000">&nbsp;<a <dtml-if 
+   action>href="&dtml-action;"<dtml-else>href="&dtml-URL1;"</dtml-if
+   ><dtml-if target> target="&dtml-target;"</dtml-if
+   >><span style="color: #000000;"><strong><dtml-babel src="'en'" literal="1">
+	<dtml-var label></dtml-babel></strong></span></a>&nbsp;</font></td>
+<dtml-else>
+  <td bgcolor="#efefef" valign="bottom" class="tab-small" 
+   align="center"><font face="Verdana, Arial, Helvetica" 
+   size="1" color="#000000">&nbsp;<a <dtml-if 
+   action>href="&dtml-action;"<dtml-else>href="&dtml-URL1;"</dtml-if
+   ><dtml-if target> target="&dtml-target;"</dtml-if
+   >><span style="color: #000000;"><strong>
+	<dtml-babel src="'en'" literal="1"><dtml-var label></dtml-babel></strong></span></a>&nbsp;</font></td>
+</dtml-if>
+</dtml-in>
+</tr>
+<tr>
+<dtml-in manage_options>
+<dtml-if "_['sequence-index']==a_">
+  <td colspan="3" bgcolor="#ffffff"><img src="&dtml-BASEPATH1;/p_/sp"
+ width="2" height="1" alt="" /></td>
+<dtml-else>
+  <td colspan="3" bgcolor="#efefef"><img src="&dtml-BASEPATH1;/p_/sp"
+ width="2" height="1" alt="" /></td>
+</dtml-if>
+</dtml-in>
+</tr>
+<tr>
+<dtml-in manage_options>
+<dtml-if "_['sequence-index']==a_">
+  <td colspan="3" bgcolor="#ffffff"><img src="&dtml-BASEPATH1;/p_/sp"
+ width="2" height="1" alt="" /></td>
+<dtml-else>
+  <td colspan="3" bgcolor="#c0c0c0"><img src="&dtml-BASEPATH1;/p_/sp"
+ width="2" height="1" alt="" /></td>
+</dtml-if>
+</dtml-in>
+</tr>
+</table>
+</dtml-if>
+
+<dtml-unless MANAGE_TABS_NO_BANNER>
+  <br />
+  <table width="100%" cellspacing="0" cellpadding="2" border="0">
+  <tr class="location-bar">
+    <td align="left" valign="top">
+    <div class="std-text">
+    <dtml-if icon>
+    <img src="&dtml-BASEPATH1;/&dtml-icon;" 
+         alt="&dtml-meta_type;" border="0" />
+    </dtml-if>
+    <strong>
+    <dtml-if meta_type>
+      <dtml-if class_manage_path>
+    <a href="&dtml-BASEPATH1;&dtml-class_manage_path;"
+       title="Manage the ZClass of this object">&dtml-meta_type;</a>
+      <dtml-else>
+    &dtml-meta_type;
+      </dtml-if>
+    <dtml-else>
+    Object
+    </dtml-if> 
+    at <dtml-var expr="tabs_path_default(REQUEST)">
+    </strong>
+    <dtml-if locked_in_version>
+      <dtml-if modified_in_version>
+        <img src="&dtml-BASEPATH1;/p_/locked"
+         alt="This item has been modified in this version" />
+      <dtml-else>
+        <img src="&dtml-BASEPATH1;/p_/lockedo"
+         alt="This item has been modified in another version" />
+              (<em><dtml-var locked_in_version html_quote></em>)
+      </dtml-if>
+    </dtml-if>
+    <dtml-if wl_isLocked>
+     <img src="&dtml-BASEPATH1;/p_/davlocked"
+      alt="This item has been locked by WebDAV"
+      title="This item has been locked by WebDAV" />
+    </dtml-if wl_isLocked>
+    </div>
+    </td>
+  <dtml-if "_.has_key('help_topic') and _.has_key('help_product')">
+  <td align="right" valign="top">
+  <div class="std-text">
+  <dtml-var "HelpSys.helpLink(help_product, help_topic)">
+  </div>
+  </td>
+  <dtml-else>
+  <dtml-if manage_options>
+  <dtml-with "_(option=manage_options[a_])">
+  <dtml-if "option.has_key('help')">
+  <td align="right" valign="top">
+  <div class="std-text">
+  <dtml-var "HelpSys.helpLink(option['help'][0], option['help'][1])">
+  </div>
+  </td>
+  </dtml-if>
+  </dtml-with>
+  </dtml-if>
+  </dtml-if>
+  </tr>
+  </table>
+
+<dtml-if Zope-Version>
+<div class="system-msg">
+<em>You are currently working in version <a href="&dtml-SERVER_URL;&dtml-Zope-Version;/manage_main"><dtml-var Zope-Version html_quote></a></em>
+</div>
+</dtml-if>
+</dtml-unless>
+
+<dtml-if manage_tabs_message>
+<div class="system-msg">
+<dtml-var manage_tabs_message> 
+(<dtml-var ZopeTime fmt="%Y-%m-%d %H:%M">)
+</div>
+</dtml-if>
+
+</dtml-with>
diff --git a/ZopeProducts/exUserFolder/common/text_manage_tabs.dtml b/ZopeProducts/exUserFolder/common/text_manage_tabs.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..62922a0ed128f1aa71fed13fb1acc4690ffb6a8f
--- /dev/null
+++ b/ZopeProducts/exUserFolder/common/text_manage_tabs.dtml
@@ -0,0 +1,19 @@
+<dtml-with "_(manage_options=filtered_manage_options())">
+    <dtml-if manage_options>
+        <table cellpadding="0" cellspacing="0" width="100%" border="2">
+	<tr><td>
+        <table cellpadding="0" cellspacing="5" width="100%" border="0">
+        <tr>
+   	    <td valign="bottom" align="left" class="tab-small">
+            <dtml-in manage_options mapping>
+                    <b><a <dtml-if action>href="<dtml-var action>" 
+                       <dtml-else>href="<dtml-var URL1>" </dtml-if> 
+                       <dtml-if target> target="<dtml-var target>"</dtml-if>>
+                    [<dtml-babel src="'en'" literal="1"><dtml-var label></dtml-babel>]</a></b>&nbsp;
+            </dtml-in>
+            </td>    
+        </tr>
+        </table>
+	</td></tr></table>
+    </dtml-if>
+</dtml-with>
diff --git a/ZopeProducts/exUserFolder/doc/FAQ.txt b/ZopeProducts/exUserFolder/doc/FAQ.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d2a0be5d33c45e0290aa2a69819cca621b2af435
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/FAQ.txt
@@ -0,0 +1,119 @@
+Frequently Asked Questions
+
+1.  Why shouldn't I use Core Session Tracking + Login Manager?
+XUF serves a different set of users to the combination above. XUF
+aims to be a simple out of the box solution. Login Manager allows
+for very complex authorisation schemes that can query multiple user
+sources. We don't do that.
+
+2.  Why use XUF at all?
+In its simplest configuration, XUF provides the same functionality
+as the standard User Folder, but, is more secure. Passwords are
+stored encrypted, which is not the case for the standard User Folder.
+So even if you don't want to set properties on users, or any
+membership facilities, there is a benefit to running XUF.
+
+3.  Do I have to have all this other stuff?
+No. The only thing you need to enable is authentication. There is
+a null property source, and a null membership source. Everything
+other than authentication is optional.
+
+4.  Can I use it as a root folder?
+Some people have reported success in doing so. We don't recommend
+it for various reasons. The main one is that the internal Zope API
+can change without warning, which could break XUF and lock you out
+of your Zope. This can happen with any User Folder product. We
+recommend you look at VHM and other Site Access methods to allow
+you to store your main site in a sub-folder.
+
+5.  When will XUF support authentication against XYZ system?
+That depends. First the active developers need to have an interest
+in it, and more importantly they need to be able to test it. Writing
+your authentication method is very simple, so if you understand
+what you want to authenticate against, and know some python you
+could write one in an hour.  You can also use the usAuthSource to
+write one using PythonScripts, ExternalMethods, DTML, or any other
+callable method that Zope supports.
+
+6.  I wrote this cool authentication source can I get it into the main
+    distribution?
+Yes and No. If your authentication is Open Source, and has a
+compatible license with XUF, and doesn't require any external
+libraries, odds are it'll go into the main distribution. If it
+depends on external libraries, it's possible it can conditionally
+go into the main distribution. The nice thing about XUF is that
+Authentication, Property, and Membership sources are all packagable
+as independent products, so you can distribute it as a standalone
+product, and it'll work (without having to have the code drop into
+the XUF directory either).
+
+7.  Is XUF going to be part of the Core Zope?
+No idea. At the moment (0.10.5) XUF is probably not at a level that
+Zope Corporation would consider mature enough for core inclusion
+anyway.
+
+Actually the answer now, is probably not. At a minimum smbAuthSource,
+and radiusAuthSource would have to be stripped and distributed
+seperately.  Over and above that, I would have to assign Zope Corp
+co-ownership rights on the IP, which amongst other things gives
+them or anyone that buys them unlimited access to future derived
+works. I refuse to do this on principle, the liberal licensing of
+the product should be more than adequate for any (especially open
+source) endeavour.
+
+8.  What's with the Management Screens?
+It's a joke on the Zope World.
+
+9.  But they're really ugly I want to change them.
+That's fine, you do that, that's the point.
+
+10. Can I send you patches to put them back to standard Management
+    Screens?
+You can put patches into the tracker at Source Forge if you want to.
+
+11. HELP!!!! I tried to install XUF as my root folder, without
+    reading the FAQ, or really knowing what I'm doing, and now I'm
+    hosed!!!
+That's a shame.
+
+12. Will XUF work with ZEO?
+Unknown. However, it's almost certain that in its current form
+credential caching will not work across ZEO -- you will get a
+seperate User Cache for each Zope Client (which isn't really all
+that bad). However, it means that if you want to use Session Tracking,
+you need to lock users to one particular server. Most commercial
+Load Balancers do this for you anyhow. A persistent cache will form
+part of an upcoming release which will allow all of this to work
+transparently across ZEO farms.
+
+13. Shouldn't it be EUF?
+No, it should be XUF :-P
+
+14. How can I log in a user via a form submission?
+Yes, the key is sending the __ac_name and __ac_password (yes that's
+two underscores in front) as form variables to any object covered
+by the XUF.
+
+This form will take your users to the /index_html and log them in.
+You can place this anywhere in your site including /index_html.
+
+<form action="/index_html">
+Name: <input type="text" size="20" name="__ac_name"><br>
+Password: <input type="password" size="20" name="__ac_password">
+<input type="submit" value="Log in">
+</form>
+
+15. That Dialog box sure is ugly! How can I customize it so it looks
+    right in my site?
+Under the contents tab add an object called MessageDialog , it can
+be a dtml document, method, or even a page template.  Make it look
+how you want, it will acquire all objects from the folder the XUF
+is in so you can use the standard_html_header, call scripts, etc.
+
+16. Why can't I change the default crypto method?
+Because someone will change it, and all the existing passwords will cease
+to work. Then I'll get lots of hate-mail, or get slagged off on other mailing
+lists :-)
+
+17. Where is the Zen Master's Guide to exUserFolder?
+Everywhere and nowhere.
diff --git a/ZopeProducts/exUserFolder/doc/README.API b/ZopeProducts/exUserFolder/doc/README.API
new file mode 100644
index 0000000000000000000000000000000000000000..ab6e8ebd05a47d29e99be884104fdea3d05fc83d
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/README.API
@@ -0,0 +1,171 @@
+User Sources
+------------
+
+This is the list of functions your auth source should provide.
+
+createUser(self, username, password, roles)
+Create a User to authenticate against username, password and roles,
+should all be obvious.
+
+
+updateUser(self, username, password, roles)
+Update a user's roles and password.
+An empty password means do not change passwords, so at least at this time
+it's not possible to have passwordless accounts.
+
+
+cryptPassword(self, username, password)
+Encrypt a password
+If no 'crypt' method is supplied return the Password -- 
+i.e. plaintext password.
+
+
+deleteUsers(self, userids)
+Delete a set of users, userids is a list of usernames to delete.
+
+
+listUserNames(self)
+returns a list of usernames.
+
+listOneUser(self,username)
+Return one user matching the username
+Should be a dictionary; 
+	{'username':username, 'password':cryptedPassword, 'roles':list_of_roles}
+Once again, you can provide more information than this if you're customising
+the forms.
+
+listUsers(self)
+Return the list of users in the same format as listOneUser
+
+
+remoteAuthMethod(self, username, password)
+You can define this to go off and do the authentication instead of
+using the basic one inside the User Object. Useful for IMAP/LDAP auth
+type methods where the authentication is handled elsewhere and we just want
+to know success or failure. If you don't want to do this you should have;
+
+remoteAuthMethod=None
+
+in your AuthSource (to explicitly tell the exUserFolder that you don't).
+
+------------------------------------------------------------------------
+This is a skeleton class;
+
+manage_addfooAuthSourceForm=HTMLFile('manage_addfooAuthSourceForm', globals())
+manage_editfooAuthSourceForm=HTMLFile('manage_editfooAuthSourceForm', globals())
+
+class fooAuthSource(Folder):
+
+	meta_type='Authentication Source'
+	title='User Supplied Authentication'
+	icon ='misc_/exUserFolder/exUserFolderPlugin.gif'
+	manage_editForm=manage_editfooAuthSourceForm
+		
+	def __init__(self):
+		self.id='fooAuthSource'
+
+	# Create a User to authenticate against
+	# username, password and roles
+	def createUser(self, username, password, roles):
+		""" Add A Username """
+		pass
+
+	# Update a user's roles and password
+	# An empty password means do not change passwords...
+	def updateUser(self, username, password, roles):
+		pass
+
+	# Encrypt a password
+	# If no 'crypt' method is supplied return the
+	# Password -- i.e. plaintext password
+	def cryptPassword(self, username, password):
+		pass
+
+	# Delete a set of users
+	def deleteUsers(self, userids):
+		pass
+
+	# Return a list of usernames
+	def listUserNames(self):
+		pass
+		
+	# Return a list of user dictionaries with
+	# {'username':username} can be extended to pass back other
+	# information, but, we don't do that just now
+	def listUsers(self):
+		pass
+
+	# Return one user matching the username
+	# Should be a dictionary;
+	# {'username':username, 'password':cryptedPassword, 'roles':list_of_roles}
+	def listOneUser(self,username):
+		pass
+
+	#
+	# Return a list of users, dictionary format as for listOneUser
+	#
+	def getUsers(self):
+		pass
+
+	#
+	# You can define this to go off and do the authentication instead of
+	# using the basic one inside the User Object
+	#
+	remoteAuthMethod=None
+
+##	def remoteAuthMethod(self, username, password):
+##		pass
+
+	def postInitialisation(self, REQUEST):
+		pass
+
+fooAuthReg=PluginRegister('fooAuthSource', 'User Supplied Authentication Source',
+						 fooAuthSource, manage_addfooAuthSourceForm,
+						 manage_addfooAuthSource,
+						 manage_editfooAuthSourceForm)
+exUserFolder.authSources['fooAuthSource']=fooAuthReg
+
+------------------------------------------------------------------------
+
+Property Sources
+----------------
+
+Property Sources have only a few things they need to provide;
+
+hasProperty(self, key)
+Returns true if the current user has that property
+
+setProperty(self, key, value)
+Sets a property for the current user
+
+
+setUserProperty(self, key, username, value)
+Sets a property for the given user.
+
+
+getProperty(self, key, default=None)
+Returns the requested property or the default for the current user.
+
+
+getUserProperty(self, key, username, default=None)
+Returns the requested property or the default for the named user.
+
+
+listProperties(self)
+Returns a list of properties (just the properties not their values).
+
+
+listUserProperties(self, username)
+Returns a list of properties for the named user.
+
+createUser(self, username, REQUEST)
+Creates a new user, and adds in the properties in the REQUEST.
+New properties are preceded with "user_KEYNAME", so strip user_ to
+set the property.
+
+deleteUsers(self, userids)
+Delete the list of users (and their properties) contained within userids.
+
+updateUser(self, username, REQUEST)
+Change the list of properties for a user, the variables are formatted as for
+createUser.
diff --git a/ZopeProducts/exUserFolder/doc/README.LDAPAuthSource b/ZopeProducts/exUserFolder/doc/README.LDAPAuthSource
new file mode 100644
index 0000000000000000000000000000000000000000..fa3bc00410a509ca07b473dd5ffa8a1965e88a5d
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/README.LDAPAuthSource
@@ -0,0 +1,20 @@
+This is a reimplementation of the auth_ldap auth source for apache, written by
+Dave Carrigan and others.
+
+You can find the original auth_ldap Apache code at
+http://www.rudedog.org/auth_ldap/
+
+This auth source is covered by the Apache license;
+
+Copyright (C) 1998, 1999 Enbridge Pipelines Inc. 
+Copyright (C) 1999-2001 Dave Carrigan
+Copyright (C) 2003 The Internet (Aust) Pty Ltd.
+All rights reserved.
+
+This module is free software; you can redistribute it and/or modify
+it under the same terms as Apache itself. This module 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. The copyright holder of this
+module can not be held liable for any general, special, incidental
+or consequential damages arising out of the use of the module.
diff --git a/ZopeProducts/exUserFolder/doc/README.Membership b/ZopeProducts/exUserFolder/doc/README.Membership
new file mode 100644
index 0000000000000000000000000000000000000000..17937f39ec06e826a43bbde955c4bff277791e40
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/README.Membership
@@ -0,0 +1,144 @@
+There are now membership hooks defined, and a basic membership
+source defined for exUserFolder. This is a first stab at this, so
+please be careful :-)
+
+Membership adds a level of complexity to everything, and basically is a
+controlled way of breaching your security.
+
+You will need to prepare a few things before trying to add an exUserFolder
+with Membership support;
+
+a) You will need a MailHost object available
+b) You will need to define some methods for Membership to use
+	  i) a Login Page
+	 ii) a Change Password Page
+	iii) a Signup Page
+	 iv) a Forgot My Password Page (optional)
+	  v) a change Properties Page (optional)
+   These should live at the same level as your acl_user (i.e. not inside).
+   These should be fairly simple, I've included some examples below.
+   These should just wrap the ones below acl_users. There will be methods
+   you can edit to get all your fields and layout done.
+c) If you want the results pages from Signup, Change Password, etc to fit 
+   your site, you'll need to add a MessageDialog document in the contents 
+   of the XUF. See FAQ 15 for more
+
+
+When you see the creation form, obviously some of the options are
+mutually exclusive.
+
+e.g. You can't choose system defined passwords, and have the system
+     email a hint, if they forgot their password. So try to pick sane
+     combinations of options.
+
+
+
+
+
+If you choose to have Home Directories, basicMemberSource will create
+the path you provide, so you don't need to do that in advance.
+
+If you want to have skeleton files copied to their homedir you'll need
+to have that directory (it can be empty) setup and ready to go, before
+the first user signs up.
+
+WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING 
+WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING 
+WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING 
+If you get basicMembershipSource to create Home Directories for your 
+users, it will create a 'Folder' and it will give the user management
+permissions on that Folder. This means that they will be able to add
+any object you can, just at a level below this. You should create/have
+a proper HomeFolder object that is restricted in what is available
+for adding, and change makeHomeDir() in basicMemberSource.py to create
+one of those.
+
+I will look at creating a restricted HomeDirectory Object in a later
+release, and allow you to add and remove meta_types from it.
+WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING 
+WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING 
+WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING 
+
+========================================================================
+
+
+
+------------------------------------------------------------------------
+LoginForm
+------------------------------------------------------------------------
+<dtml-with acl_users>
+<dtml-var docLogin>
+</dtml-with>
+------------------------------------------------------------------------
+
+
+
+------------------------------------------------------------------------
+ChangePasswordForm
+------------------------------------------------------------------------
+<dtml-var standard_html_header>
+<dtml-with acl_users>
+<dtml-with currentMembershipSource>
+<dtml-var PasswordForm>
+</dtml-with>
+</dtml-with>
+<dtml-var standard_html_footer>
+------------------------------------------------------------------------
+
+
+
+------------------------------------------------------------------------
+SignupForm
+------------------------------------------------------------------------
+<dtml-var standard_html_header>
+<dtml-with acl_users>
+<dtml-with currentMembershipSource>
+<dtml-var SignupForm>
+</dtml-with>
+</dtml-with>
+<dtml-var standard_html_footer>
+
+
+------------------------------------------------------------------------
+ForgotMyPassword
+------------------------------------------------------------------------
+<dtml-var standard_html_header>
+<form action="acl_users/manage_forgotPassword" method="POST">
+Username: <input type="text" name="username">
+</form>
+<dtml-var standard_html_footer>
+
+
+
+------------------------------------------------------------------------
+ChangePropertiesForm
+------------------------------------------------------------------------
+<dtml-var standard_html_header>
+
+<h2> Changing Properties for <dtml-var AUTHENTICATED_USER></h2>
+<dtml-with acl_users>
+
+<form action="acl_users/manage_changeProps" method="POST">
+
+
+                <hr>
+                <h2>Properties</h2>
+                <table border>
+                <tr>
+                        <th>Property Name</th><th>Value</th>
+                </tr>
+        <dtml-in "AUTHENTICATED_USER.listProperties()"> 
+        <tr>                    <td><dtml-var sequence-item></td>
+                <td><input name="user_<dtml-var sequence-item>"
+value="<dtml-var
+"AUTHENTICATED_USER.getProperty(_['sequence-item'])">"></td>
+                </tr>
+                </dtml-in>
+                </table>
+
+        <input type="submit" value=" Change ">
+        </form>
+
+</dtml-with>
+<dtml-var standard_html_footer>
+------------------------------------------------------------------------
diff --git a/ZopeProducts/exUserFolder/doc/README.httpsAuthSource b/ZopeProducts/exUserFolder/doc/README.httpsAuthSource
new file mode 100644
index 0000000000000000000000000000000000000000..21697df3fb576fe5cfdc8cd1b3d07132341bcba2
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/README.httpsAuthSource
@@ -0,0 +1,28 @@
+This plugin implements authentication from an https service.  
+
+Upon installation, the mangament forms allow you to configure:
+* the url to the service, 
+* the parameter that will contain the username 
+* the parameter that will contain the password
+* The expected authorization response regex (returned from the authorization service).
+* The default role that authorized users will be assinged upon their first login
+
+The https auth source posts a request over https to the named service with the username and 
+passowrd passed according to the parameters defined in the configuration.  It will attempt 
+to match the authorization pattern specified, and if the pattern is found, the user will be
+authenticated.
+
+Once a user has logged in, they will appear in xuf user list, and their roles can be updated.
+
+This auth source has been developed using the zodbBTreeProps plugin, and stores the user's
+roles in this property tool.
+
+A typical use case for this authorization service might be to authenticate against
+a legacy user directory for which no xuf auth plugin currently exists.  Hopefully, the 
+development of a the auth service on the other end will be trivial, or better yet,
+already exist.
+
+IMPORTANT NOTE: In order to use this plugin you must compile your python to include 
+ssl support.  The python that ships with zope 2.X does not have this enabled by default.
+
+Thanks to akm, bcsaller, runyaga for all their help.
diff --git a/ZopeProducts/exUserFolder/doc/README.pgAuthSource-Alternate b/ZopeProducts/exUserFolder/doc/README.pgAuthSource-Alternate
new file mode 100644
index 0000000000000000000000000000000000000000..8652263c1c9b02408c7d0bfeec935e3ee23e6966
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/README.pgAuthSource-Alternate
@@ -0,0 +1,20 @@
+This alternate pgAuthSource was developed to allow Zope and jakarta-tomcat to 
+share common PostGreSQL auth tables. It's really just a mod of the original 
+pgAuthSource, with changes to the original kept to a minimum. This should help
+when it comes to cross porting improvements / maintenence changes between the 
+two versions.
+
+The only thing that's new is the table schema. This auth source uses:
+A user table 
+	Username, password
+
+A role table:
+	rolename
+
+and a associative userrole table for relating the two:
+	username, rolename
+
+ps. Use the Source, Luke!
+If you dig a little you will find a couple of different ways of crypting 
+passwords commented out (plain and MD5).
+
diff --git a/ZopeProducts/exUserFolder/doc/README.radiusAuthSource b/ZopeProducts/exUserFolder/doc/README.radiusAuthSource
new file mode 100644
index 0000000000000000000000000000000000000000..60b940a5d12af06d71afafc1245bba3437efed4c
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/README.radiusAuthSource
@@ -0,0 +1,12 @@
+I have converted ZRadius to work with exUserFolder as an AuthSource
+it should function at least as well as it functioned with GUF d8)
+-- akm
+
+Radius
+
+    This product implements simple Radius authentication. If you need
+    to authenticate Zope users from a Radius server, this product will
+    plug into the GenericUserFolder product.
+
+    &copy; Copywrite 1999 Stuart Bishop &LT;zen@cs.rmit.edu.au&GT;
+
diff --git a/ZopeProducts/exUserFolder/doc/README.smbAuthSource b/ZopeProducts/exUserFolder/doc/README.smbAuthSource
new file mode 100644
index 0000000000000000000000000000000000000000..ba4e52abc177d6aa24ed6e31909d7de6b51e1481
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/README.smbAuthSource
@@ -0,0 +1,41 @@
+ 
+SMB Authentication Source for exUserFolder
+
+(C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+ACN: 082 081 472	ABN: 83 082 081 472
+All Rights Reserved
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+
+This smbAuthSource uses pySMB by Michael Teo whose copyright is as follows.
+
+Copyright (C) 2001 Michael Teo <michaelteo@bigfoot.com>
+
+This software is provided 'as-is', without any express or implied warranty. 
+In no event will the author be held liable for any damages arising from the 
+use of this software.
+
+Permission is granted to anyone to use this software for any purpose, 
+including commercial applications, and to alter it and redistribute it 
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not 
+   claim that you wrote the original software. If you use this software 
+   in a product, an acknowledgment in the product documentation would be
+   appreciated but is not required.
+
+2. Altered source versions must be plainly marked as such, and must not be 
+   misrepresented as being the original software.
+
+3. This notice cannot be removed or altered from any source distribution.
+
diff --git a/ZopeProducts/exUserFolder/doc/README.txt b/ZopeProducts/exUserFolder/doc/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3152aa79a8d964a3c16e355435846c04f4ab75a6
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/README.txt
@@ -0,0 +1,127 @@
+A lot of this stuff is now covered in the Unenlightened Zopistas Guide to
+exUserFolder, so this document is slightly redundant (but not completely).
+It's also shockingly out of date...
+
+A note on the version numbering... there's none of that odd/even 
+numbering nonsense you find in Lin*x land. The numbers are based on
+
+Major.Minor.Micro
+
+A bump in Major means a milestone or set of milestones has been reached.
+A bump in Minor means some piece of functionality has been added, or
+a major bug was fixed.
+A bump in Micro usually means bug fixes.
+
+These numbers go from 0-99, and are not necessarily continuous or
+monotonically increasing (but they are increasing).
+
+What you consider major and what I consider major are probably two
+different things.
+
+Release candidates before a Major bump start at 99.. so;
+
+0.99.0  is the first Release Candidate prior to 1.0.0
+0.99.1  is the next Release Candidate
+1.0.0 is the 'final' release.
+
+It's possible that there will be no changes between final release candidate 
+and release.
+
+Sometimes due to the nature of the changes a release will be marked 
+development. This usually means some core functionality was changed.
+
+Extensible User Folder
+
+Extensible User Folder is a user folder that requires the authentication 
+of users to be removed from the storage of properties for users.
+
+Writing new authentication or property sources is (almost) a trivial operation
+and require no authentication knowledge to write, most simply return lists
+of attributes, or dictionaries. You don't need to incorporate them into
+the base exUserFolder code, they can be written as normal Zope Products,
+(i.e. you can distribute new sources independently of exUserFolder).
+
+There are three authentication sources provided OOTB;
+
+o pgAuthSource -- Postgresql Authentication Source
+Actually this is pretty generic, and could be used for most SQL databases,
+the schema isn't all that advanced either.
+
+This source allows you to specify the table, and the name of each of the
+columns (username, password, roles), so you can use an existing database.
+
+
+All ZSQL Methods are available inside the pgAuthSource folder for editing.
+You need to have a DB Connection already in place for use.
+
+o usAuthSource -- User Supplied Authentication
+This is similar to Generic User Folder, or Generic User Sources for
+Login Manager. You provide a set of methods;
+
+createUser    -- if you want to create users.
+cryptPassword -- if you want to encrypt passwords.
+listUsers     -- return a list of userIds.
+listOneUser   -- return a dictionary containing the username, password, and 
+                 a list of roles
+getUsers      -- return a list of users ala listOneUser but lots of them.
+
+that's it. listOneUser is mandatory.
+
+There is an example of ExternalMethods you could use to create an 'sql'
+user source in the Extensions directory.
+
+o etcAuthSource -- File Based Authentication
+This is etcUserFolder reworked to be a plugin for this. Since I used
+etcUserFolder as a base for this product, I might as well add the functionality
+in.
+
+Each of the plugins has a 'manage_addForm' that is called when the User Folder
+is added, so that parameters can be garnered from the user (pg*Source e.g.
+get the dbconnection)
+
+etcAuthSource doesn't allow roles to be set, although it does ask for
+a default role to be assigned to a user (which it dutifully does).
+
+
+There are two property sources provided:
+
+o pgPropertySource -- Postgresql Property Source
+A very naive sql implementation for properties, works fine, I wouldn't want
+to load it up too high though.
+
+o zodbProperySource -- ZODB Property Source
+This is a very simple property keeper, and is more available as an example
+of what functionality needs to be provided.
+
+There is a postUserCreate method which you can replace with anything really,
+if you want to do something once a user is created (send an email to someone,
+page someone...)
+
+You can mix-n-match authentication methods and property methods.
+
+You can have cookie or standard (Basic) authentication.
+
+docLogin and docLogout methods are present in the ZODB for editing, because
+you will hate the way the default login and logout pages look d8)
+
+The various plugins need some more configurable options atm, but, it 
+shouldn't be that much of a drama to add them in soon.
+
+Arbitrary properties can be set on a user (at least if the PropertySource
+is written correctly they can).
+
+<dtml-call "AUTHENTICATED_USER.setProperty(key, value)">
+<dtml-if "AUTHENTICATED_USER.hasProperty(key)">
+<dtml-var "AUTHENTICATED_USER.getProperty(key, defaultValue)">
+<dtml-var "AUTHENTICATED_USER['key']">
+
+Will all work (assuming the user is logged in).
+
+When creating a new user any fields with user_ will be set as properties.
+So 'user_email' field will create an 'email' property. You just have to
+provide the input form, and exUserFolder will do the rest.
+
+This has only been lightly tested, but, it seems to work just fine.
+
+
+
diff --git a/ZopeProducts/exUserFolder/doc/README.zodbAuthSource b/ZopeProducts/exUserFolder/doc/README.zodbAuthSource
new file mode 100644
index 0000000000000000000000000000000000000000..492e7f3ff11f405fe29c2d73eaa1d53ddfc5fdc8
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/README.zodbAuthSource
@@ -0,0 +1,13 @@
+ZODB Authentication Source for exUserFolder
+
+This is an auth source that works pretty much like the standard user folder provided by zope.
+It stores the usernames, roles and passwords on a ZODB persistent dictionary. 
+
+It doesn't require any configuration at all, just select it as your auth source and you're
+ready to add user accounts.
+
+
+Author: Alex Verstraeten (aka: zxc)
+Email: alex@quad.com.ar
+
+
diff --git a/ZopeProducts/exUserFolder/doc/UnenlightenedZopistasGuide.txt b/ZopeProducts/exUserFolder/doc/UnenlightenedZopistasGuide.txt
new file mode 100644
index 0000000000000000000000000000000000000000..547bdc207de2e985a7a5f39b172158bfd9bd32c8
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/UnenlightenedZopistasGuide.txt
@@ -0,0 +1,671 @@
+The Unenlightened Zopistas Guide to exUserFolder.
+(C) 2001-2003 Andrew Milton <akm@theinternet.com.au>
+
+0. INTRODUCTION.
+
+exUserFolder is an extensible authentication product for the Zope
+Application Server. It allows a user to choose from a number of
+methods of authenticating their users, and allows the setting and
+fetching of arbitrary properties on a User object.
+
+Authentication methods, and Property methods do not have to use the
+same backing store, so it is possible to use legacy user sources, and
+still have configuration information stored about a user.
+
+exUserFolder supports HTTP Basic Authentication, and the so called
+Cookie Authentication schemes popular with most webservers.
+
+0.1 Audience.
+
+Everybody, and Nobody. If we build our product correctly, we shouldn't
+need user documentation, that's why Nobody. For some reason, normal
+sane people seem to lose all of their common sense when sitting in
+front of a computer. To the point where plain messages e.g. "Please
+Insert a Floppy", evoke a response of "What does that mean?" So that's
+why this document is for Everybody.
+
+This is not a guide for writing your own authentication, property, or
+membership sources. You need the Zen Masters Guide to exUserFolder
+document for that.
+
+
+1. GETTING STARTED
+
+exUserFolder requires Python 2.1 or above (may work with older
+pythons, but, this is not supported). And has been tested on Zope
+2.3.0 and above (including 2.4.3).
+
+exUserFolder comes as a source tarball, and it does not rely on any
+outside dependencies to be work. Some items may require additional
+products to be installed in your Zope tree, but, simply will not
+appear if these are not available. Some items may also require
+external products to add functionality to them, but, at no time should
+the Product not install because of these dependencies.
+
+
+1.1 Installing exUserFolder.
+
+Unpack exUserFolder in your Products directory. This can be achieved by
+executing the following on most UNIX systems.
+
+gzip -d -c exUserFolder-Ma_Mi_u.tgz | tar xvf -
+
+where exUserFolder-Ma_Mi_u.tgz is the version of exUserFolder you have
+downloaded.
+
+On systems that have the GNU tar this can be shortened to;
+
+tar zxvf exUserFolder-Ma_Mi_u.tgz
+
+You should restart Zope after unpacking. Installing the product will not
+affect your Zope installation in anyway.
+
+If you go to Folder inside your Zope Management Interface, you should see
+exUserFolder as a dropdown item near the bottom somewhere.
+
+Congratulations, it's installed.
+
+
+2. AUTHENTICATION SOURCES AND YOU.
+
+The only mandatory component of an exUserFolder installation, is
+choosing an Authentication Source. There are six Authentication
+Sources to choose from in the default install. There are other add-on
+Sources available from other parties.
+
+Each Authentication Source is different, and assumes at least some
+knowledge of that type of authentication scheme.
+
+Most if not all sources store the password encrypted in some
+manner. This means that discovering other people's passwords is going
+to be more difficult, than with the standard user folder that comes
+with Zope.
+
+By default crypt or fcrypt are used, which is are DES encryption
+methods.  While this is not the strongest, fastest, choose another
+superlative, of encryption techniques, it is certainly adequate for
+protecting webpages.
+
+In a later release exUserFolder will allow you to choose what method
+is used for password hashing.
+
+Some Authentication Sources can list the users that are available,
+some cannot (or will not). Some allow you to add users, and others do
+not.  What features are availble depend on the individual
+Authentication Source.
+
+
+2.1 ZODB Authentication Source
+
+The ZODB Authentication Source operates just like a normal User
+Folder. It stores its authentication items in the ZODB as the name
+suggests. This is the simplest folder to setup, requiring no
+parameters.
+
+Choosing ZODB Authentication Source is recommended for testing your install.
+
+
+2.2 File Based Authentication.
+
+File Based Authentication allows you to have a fixed set of users in a
+file with their encrypted passwords. The prerequisites for this are
+somewhat convoluted.
+
+In the root of your Zope Installation, on the actual file system (not
+in the ZODB), create a directory called exUsers. Make sure that Zope
+has access to that directory.
+
+This is the directory where you will create your files.
+
+This is a read only Authentication Source. You will not be able to
+create users, or modify their passwords. You can change their roles if you
+choose a Property Source.
+
+There are two parameters asked for;
+
+2.2.1 Password File
+
+This is the name of the file that contains your users and passwords.
+It should be of the format;
+
+username:cryptedPassword
+user2:otherCryptedPasswd
+
+I can contain other fields after the password also delimited by : but these
+will not be ussed.
+
+This file should exist inside the exUsers directory.
+
+2.2.2 Default Role
+
+This is the role that all users should be given when the log in. Because this
+is a Read Only authentication source, you may not be able to add Roles at a
+later date.
+
+
+2.3 Postgresql Authentication Source
+
+Postgresql Authentication source is an RDBMS backed user store. You
+can add, change, and list users. It requires a Postgresql Database
+Connection to be created before creating the User Folder.
+
+You should be familiar with databases, and with your schema before
+using this Authentication Source. If you don't already have a table
+structure in place, a default schema is provided called 'pgScheme.sql'
+in the exUserFolder distribution.
+
+The required schema is very simple. You need to store usernames,
+passwords, and roles. If your existing schema doesn't support a roles
+column you will have to add one.
+
+The configuration scheme looks daunting, but, it is setup to use the
+defaults for 'pgScheme.sql' so if you're using this you can safely
+continue.
+
+We will run through the items.
+
+2.3.1 Database Connection
+
+If you have any database connections, they will be listed in the drop
+down box. Choose the one that represents your connection to your users
+table.
+
+2.3.2 Table Name
+
+This is the name of the table containing your users. If you have a
+different table to the default, you should change it here.
+
+2.3.3 Username Column
+
+This is the name of the column inside your table that contains the
+usernames or logins of your users. This should contain exactly what
+the user needs to type in as their username.
+
+
+2.3.4 Password Column
+
+This is the name of the column inside your table that contains the
+encrypted passwords of your users.
+
+
+2.3.5 Roles Column
+
+This is where the roles are stored. These are used to provide access
+to items in Zope.
+
+
+2.4 User Supplied Authentication Source
+
+This allows you to create your methods in DTML, PythonScripts,
+External Methods, or any other callable Zope item for listing,
+authenticating, adding and changing your users.
+
+It is beyond the scope of this guide to describe how to do this, but,
+the API is quite well defined inside the source, and also in the
+README.API document.
+
+This Authentication Source has no configuration parameters.
+
+
+2.5 RADIUS Authentication Source
+
+This allows you to authenticate your users against a RADIUS server. If
+you don't know what this means, then this User Source is not for you
+:-)
+
+You will require a RADIUS server to be operating, and for the server
+that Zope is running on to have access to it. You will also need to
+know the secret key to access the RADIUS server.
+
+2.5.1 Host
+
+This is the host your RADIUS server is running on.
+
+2.5.2 Port
+
+This is the port your RADIUS server is running on. Older installs may
+require this to be 1645. The new 'blessed' port by IANA is 1812, and
+this is now the default port.
+
+2.5.3 Secret
+
+Every remote host has a secret key it has to share with the server in
+order to gain access to the authentication server. You need to know
+this.
+
+2.5.4 Retries
+
+Because this is a networked authentication service, errors can
+occur. This sets the number of times it will try to authenticate
+before giving up.
+
+2.5.5 Timeout
+
+This is how long the RADIUS authenticator will wait for a
+response. Because RADIUS operates over UDP, which is a connectionless
+protocol, answers may never come back, or never reach their
+destination in the first place.
+
+The default is 5 seconds which is actually quite a long time.
+
+
+2.6 SMB Authentication Source
+
+This source allows you to authenticate your users in a Microsoft
+Environment, using the SMB protocols. This is not the same as
+authenticating via Directory Services.
+
+If your SMB server requires passwords to be encrypted in transit, you'll 
+need to install mxCrypto.
+
+2.6.1 Host
+
+This is the host that your Authentication service is on, this is
+normally an NT or Win2K server, but, it can also be a UNIX box running
+Samba. This should be the NetBIOS name of the server.
+
+2.6.2 Domain
+
+This is the NT/Windows DOMAIN that the user is to authenticate
+against.
+
+2.6.3 WINS Server IP Address (optional)
+
+If provided, this should be the IP address of the WINS server to be
+queried to locate your auth host (see 2.5.1 above).
+
+If you leave this field empty, the location of the authentication host
+will be queried by broadcast, which works just fine if the Zope
+machine is on the same subnet as your auth host but not if the auth
+host is across a subnet link or if it's in the same machine as Zope
+(don't ask. Apparently, some braindmamaged creature at M$ decided that
+a machine shouldn't answer to its own broadcasts no matter what)
+
+Fill in this field if you are getting "NetBIOSTimeout" errors but you
+are sure that your auth host was specified correctly, or if Windows
+machines in your subnet also use a WINS server.
+
+
+2.7 LDAP Authentication
+
+This source allows you to authenticate your users against an LDAP
+server.  This code is based on the auth_ldap module for Apache. The
+documentation for these parameters is unashamedly lifted directly from
+the documentation of the Apache directives for auth_ldap.
+
+See: http://www.rudedog.org/auth_ldap/
+
+You must choose a property source when using LDAP Authentication, all
+of the properties associated with the LDAP user entry are stored as
+properties when they authenticate. Items with multiple entries are
+stored as a list of items.
+
+You will need the pyLDAP module installed to use this authsource.
+If you don't have it installed, you will not see an LDAP Auth Source available
+for use.
+
+2.7.1 URL
+
+An RFC 2255 URL which specifies the LDAP search parameters to use. The syntax
+of the URL is
+ldap://host:port/basedn?attribute?scope?filter
+
+ldap      For regular ldap, use the string ldap. For secure LDAP, use ldaps
+          instead. Secure LDAP is only available if auth_ldap was compiled with
+          SSL support.                                                         
+host:port The name/port of the ldap server (defaults to localhost:389 for ldap,
+          and localhost:636 for ldaps). 
+                        
+          Once a connection has been made to a server, that connection remains
+          active for the life of the Zope process, or until the LDAP server
+          goes down.
+                                                                          
+          If the LDAP server goes down and breaks an existing connection,
+          the Auth Source will attempt to re-connect
+                       
+basedn    The DN of the branch of the directory where all searches should start
+          from. At the very least, this must be the top of your directory tree,
+          but could also specify a subtree in the directory.
+
+attribute The attribute to search for. Although RFC 2255 allows a
+          comma-separated list of attributes, only the first attribute will be
+          used, no matter how many are provided. If no attributes are provided,
+          the default is to use uid. It's a good idea to choose an attribute
+          that will be unique across all entries in the subtree you will be
+          using.                                                              
+
+scope     The scope of the search. Can be either one or sub. Note that a scope
+          of base is also supported by RFC 2255, but is not supported by this
+          module. If the scope is not provided, or if base scope is specified,
+          the default is to use a scope of sub.
+
+filter    A valid LDAP search filter. If not provided, defaults to (objectClass
+          =*), which will search for all objects in the tree.
+
+When doing searches, the attribute, filter and username passed by the HTTP
+client are combined to create a search filter that looks like (&(filter)
+(attribute=username)).
+
+For example, consider an URL of ldap://ldap.xuf.com/o=XUF?cn?sub?(posixid
+=*). When a client attempts to connect using a username of The Jester, the
+resulting search filter will be (&(posixid=*)(cn=The Jester)).
+
+
+2.7.2 Bind DN
+
+An optional Distinguished Name user to bind to the server when searching
+for entries. If not provided an Anonymous bind will be used.
+
+2.7.3 Bind Password.
+A bind password to use in conjunction with the bind DN. Note that the bind
+password is probably sensitive data, and should be properly protected. You
+should only use the Bind DN and Bind Password if you absolutely
+need them to search the directory.
+
+2.7.4 Cert DB Path
+
+Specifies in which directory LDAP Auth Source should look for the certificate
+authorities database. There should be a file named cert7.db in that directory.
+
+2.7.5 Compare DN On Server
+
+When set, LDAP Auth Source will use the LDAP server to compare the
+DNs. This is the only foolproof way to compare DNs. LDAP Auth Source
+will search the directory for the DN specified with the require dn
+directive, then, retrieve the DN and compare it with the DN retrieved
+from the user entry. If this directive is not set, LDAP Auth Source
+simply does a string comparison. It is possible to get false negatives
+with this approach, but it is much faster. Note the LDAP Auth Source cache
+can speed up DN comparison in most situations.
+
+2.7.6 Dereference Aliases
+
+This directive specifies when LDAP Auth Source will de-reference
+aliases during LDAP operations. The default is always.
+
+2.7.7 Group Attribute is DN
+
+When set, this directive says to use the distinguished name of the
+client username when checking for group membership. Otherwise, the
+username will be used. For example, assume that the client sent the
+username tjester, which corresponds to the LDAP DN cn=The Jester,
+o=XUF. If this directive is set, LDAP Auth Source will check if the
+group has cn=The Jester, o=XUF as a member. If this directive is not
+set, then LDAP Auth Source will check if the group has tjester as a
+member.
+
+2.7.8 Compare Cache Size
+
+This specifies the size of the cache used to cache LDAP compare
+operations. The default is 1024 entries. Setting it to 0 disables
+operation caching.
+
+2.7.9 Compare Cache TTL
+
+Specifies the time (in seconds) that entries in the operation cache
+remain valid. The default is 600 seconds.
+
+2.7.10 Start TLS
+
+If this is set to Yes, LDAP Auth Source will start a secure TLS
+session after connecting to the LDAP server. This requires your LDAP
+server to support TLS.
+
+2.7.11 Require User (one per line)
+
+The require user directive specifies what usernames can access the
+resource.  Once LDAP Auth Source has retrieved a unique DN from the
+directory, it does an LDAP compare operation using the username
+specified in the require user to see if that username is part of the
+just-fetched LDAP entry. Multiple users can be granted access by
+putting multiple usernames in the box, separated with newlines. For
+example, with a AuthLDAPURL of ldap://ldap/o=XUF?cn (i.e., cn is used
+for searches), the following require entries could be used to restrict
+access: The Jester Fred User Joe Manager
+
+Because of the way that LDAP Auth Source handles this directive, The
+Jester could sign on as The Jester, Zen Jester or any other cn that he
+has in his LDAP entry. Only the single require user line is needed to
+support all values of the attribute in the user's entry.
+
+If the uid attribute was used instead of the cn attribute in the URL
+above, the above three lines could be;
+
+tj
+fred_u
+jmanager
+
+2.7.12 Require Group (one per line)
+
+This directive specifies an LDAP group whose members are allowed
+access. It takes the distinguished name of the LDAP group. For
+example, assume that the following entry existed in the LDAP
+directory:
+
+dn: cn=Administrators, o=XUF
+objectClass: groupOfUniqueNames
+uniqueMember: cn=The Jester, o=XUF
+uniqueMember: cn=Fred User, o=XUF
+
+The following directive would grant access to both Fred and Jester:
+
+require group cn=Administrators, o=XUF
+
+Behavior of this directive is modified by the Group Attribute and 
+Group Attribute Is DN options.
+
+2.7.13 Require DN
+
+The require dn option allows the administrator to grant access based
+on distinguished names. It specifies a DN that must match for access
+to be granted. If the distinguished name that was retrieved from the
+directory server matches the distinguished name in the require dn,
+then authorization is granted.
+
+The following directive would grant access to a specific DN:
+require dn cn=The Jester, o=XUF
+
+Behavior of this directive is modified by the Compare DN On Server option.
+
+2.7.14 Default Manager
+
+This allows you to specify the username of the Manager for this area.
+The manager will still need to meet auth requirements above, but, if
+they do they will get the 'Manager' role added to their list of roles.
+
+2.7.15 Default Role
+
+This is a role to be assigned to users when they auth correctly. This
+is to differentiate them from merely being 'authenticated'.
+
+2.7.16 Examples
+
+  * Grant access to anyone who exists in the LDAP directory, using their UID
+    for searches.
+    URL ldap://ldap1.zope.com:389/ou=People, o=XUF?uid?sub?(objectClass=*)
+
+  * The next example is similar to the previous one, but is uses the common
+    name instead of the UID. Note that this could be problematical if multiple
+    people in the directory share the same cn, because a search on cn must
+    return exactly one entry. That's why this approach is not recommended: it's
+    a better idea to choose an attribute that is guaranteed unique in your
+    directory, such as uid.
+    URL ldap://ldap.zope.com/ou=People, o=XUF?cn
+
+  * Grant access to anybody in the Administrators group. The users must
+    authenticate using their UID.
+    URL ldap://ldap.zope.com/o=XUF?uid
+    require group: 
+    cn=Administrators, o=XUF
+
+  * The next example assumes that everyone at XUF who carries an
+    alphanumeric pager will have an LDAP attribute of qpagePagerID. The example
+    will grant access only to people (authenticated via their UID) who have
+    alphanumeric pagers:
+    URL: ldap://ldap.zope.com/o=XUF?uid??(qpagePagerID=*)
+
+  * The next example demonstrates the power of using filters to accomplish
+    complicated administrative requirements. Without filters, it would have
+    been necessary to create a new LDAP group and ensure that the group's
+    members remain synchronized with the pager users. This becomes trivial with
+    filters. The goal is to grant access to anyone who has a filter, plus grant
+    access to Joe Manager, who doesn't have a pager, but does need to access
+    the same resource:
+    URL ldap://ldap.zope.com/o=XUF?uid??(|(qpagePagerID=*)(uid=jmanager))
+
+    This last may look confusing at first, so it helps to evaluate what the
+    search filter will look like based on who connects, as shown below.
+    If Fred User connects as fuser, the filter would look like
+   
+    (&(|(qpagePagerID=*)(uid=jmanager))(uid=fuser))
+   
+    The above search will only succeed if fuser has a pager. When Joe Manager
+    connects as jmanager, the filter looks like
+   
+    (&(|(qpagePagerID=*)(uid=jmanager))(uid=jmanager))
+
+    The above search will succeed whether jmanager has a pager or not.
+
+
+2.8 General Items.
+
+You can choose to use standard auth, or cookie auth, and you can
+decide how long you want to cache the users credentials before
+retrying.
+
+2.8.1 Authentication Type
+
+2.8.1.1 Standard Authentication
+
+This method causes the browser to pop up a dialog box to ask for the
+username and password.
+
+2.8.1.2 Cookie Authentication
+
+This method allows you to use a normal HTML form to get the username
+and password from the user. It also will present the default form to
+the user if they try to access an unauthorised area.
+
+
+2.8.1.3 Secure Cookie based Authentication
+
+This method, like Cookie Authentication allows you to use a HTML form
+to get the user details. However, the cookie it uses does not contain
+any login information. It is internally checked against a cache of
+hashes and the information is derived from that. This cache disappears
+if you restart Zope, so this is not a good option for people who want
+to persistently cache logins across sessions.
+
+
+2.8.2 Credential Cache Timeout in Seconds
+
+exUserFolder by default caches credential information, so that the
+authorisation source isn't hit *for every object and page* that has to
+be fetched. For remote authentication services this can slow things
+down quite considerably. Even setting this to a modest setting will
+quicken response times.
+
+Setting this too long could cause problems if you want to lock out a
+troublesome user. The credential cache is flushed if someone provides
+a password that doesn't match the one in the cache.
+
+
+2.8.3 Negative Credential Cache Timeout in Seconds
+
+exUserFolder allows you to cache login failures for users that do not
+exist. This means you don't have to go out to your auth source when
+you know for certain this user is never going to be able to
+authenticate.
+
+Due to the way some auth sources are designed, this doesn't work for
+auth sources like SMB Auth Source that lie initially about the user
+existing (because you can't verify the existence of a user without
+authenticating them), and then check the credentials later.
+
+It's possible to create a hybrid auth source that lets this work
+correctly for auth sources that can't list the users.
+
+
+2.8.4 Log out users who expire from cache?
+
+If you've got caching turned on, then this will force any user who has
+their session expire to login again. Some people like to do this.
+
+
+2.8.5 Activate Session Tracking for anoymous users?
+
+For any anonymous user, a new temporary user is created. This allows
+you to set/get properties for anonymous users too. Currently
+experimental.
+
+
+3.0 PROPERTY SOURCES
+
+4.0 MEMBERSHIP SOURCES
+
+5.0 TIPS FOR THE UNWARY
+
+Generally these things apply to Cookie Authentication models, since
+there is additional access required to present the login form.
+
+5.1 Too much protection.
+
+A lot of people try to protect a folder by placing an exUserFolder
+inside.  They then change the permissions on this folder to only allow
+Authenticated or some Local Role to have permission.
+
+5.1.1 The problem
+
+When you try to access the folder, instead of getting the form, you
+get a popup box, even though you chose Cookie Authentication. Even
+when you enter a username and password it doesn't work.
+
+
+5.1.2 What happened
+
+You tried to access an area you don't have access to. Zope found the
+closest user folder to the object you were trying to access. The user
+folder decided you were not authorised and tried to display the login
+form. You don't have access to view the login form, so Zope finds the
+nearest user folder to the login form, which is the user folder above
+the protected directory. It pops up the authentication dialog. If you
+put in a valid username and password for this top level, then lower
+level then displays the login form.
+
+5.1.3 Solution 1 (preferred).
+
+Place the user folder one level *above* the folder you want to protect,
+that is in the unprotected area. Everything should work fine.
+
+5.1.4. Solution 2 (not so preferred).
+
+Set the 'View' permission on the docLogin form inside the acl_users
+folder.  You can get there by Choosing 'Contents' on docLogin and
+scrolling down to the bottom.
+
+6.0 MISCELLANY
+
+6.1 Adding an exUserFolder from a product.
+
+You can add an exUserFolder from a Python product fairly easily, if
+not a tad messily.
+
+
+from Products.exUserFolder.exUserFolder import manage_addexUserFolder, eUserFolder
+
+manage_addexUserFolder(authId='zodbAuthSource', propId='zodbPropSource',
+                       memberId='basicMemberSource',
+                       cookie_mode=1, session_length=600, REQUEST)
+
+Obviously change authId, propId, and memberId to what you want.
+However, you'll need to ram in the appropriate form fields for the various
+source constructors into your REQUEST.
+
+6.2 Session Tracking.
+
+Session tracking (currently) relies on having the credential cache
+active, and a property source active. Your trackable user will only
+last as long as they are not expired from the cache. You should set
+the cache expiry length to be somewhat longer than normal if you plan
+to use Session Tracking, and you should also be prepared to check that
+the current session is valid.
diff --git a/ZopeProducts/exUserFolder/doc/mysql.sql b/ZopeProducts/exUserFolder/doc/mysql.sql
new file mode 100644
index 0000000000000000000000000000000000000000..b2bf5f822818f43c32c59a77f261683c0a47f1fa
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/mysql.sql
@@ -0,0 +1,18 @@
+DROP TABLE IF EXISTS passwd;
+CREATE TABLE passwd (
+	username varchar(64) NOT NULL PRIMARY KEY,
+	password varchar(64) NOT NULL,
+	roles varchar(255)
+);
+
+DROP TABLE IF EXISTS UserProperties;
+CREATE TABLE UserProperties (
+	username varchar(64) NOT NULL,
+	prop_key varchar(128) NOT NULL,
+	value text NOT NULL,
+	istemporary int
+);
+
+CREATE UNIQUE INDEX username_prop_idx on UserProperties(username,prop_key );
+CREATE INDEX username_idx on UserProperties(username);
+
diff --git a/ZopeProducts/exUserFolder/doc/pgAndreasScheme.sql b/ZopeProducts/exUserFolder/doc/pgAndreasScheme.sql
new file mode 100644
index 0000000000000000000000000000000000000000..57fd21a4372122bb2f045c69f5b2671c34e9d411
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/pgAndreasScheme.sql
@@ -0,0 +1,14 @@
+CREATE TABLE "passwd" (
+	"username" character varying(64) UNIQUE NOT NULL,
+	"password" character varying(64) NOT NULL,
+	"roles" character varying(255),
+	Constraint "passwd_pkey" Primary Key ("username")
+);
+
+CREATE TABLE "userproperties" (
+	"username" character varying(64) NOT NULL REFERENCES passwd (username) ON DELETE CASCADE ON UPDATE CASCADE,
+	"key" character varying(128) NOT NULL,
+	"value" text NOT NULL	
+);
+
+CREATE  INDEX "username_idx" on "userproperties" using btree ( "username" "varchar_ops" );
diff --git a/ZopeProducts/exUserFolder/doc/pgScheme.sql b/ZopeProducts/exUserFolder/doc/pgScheme.sql
new file mode 100644
index 0000000000000000000000000000000000000000..45e5e1a0975ac4d350e135c9ca9e7625ca64d630
--- /dev/null
+++ b/ZopeProducts/exUserFolder/doc/pgScheme.sql
@@ -0,0 +1,46 @@
+CREATE SEQUENCE "passwd_userid_seq" start 1 increment 1 maxvalue 2147483647 minvalue 1  cache 1 ;
+
+--
+-- TOC Entry ID 6 (OID 24949)
+--
+-- Name: passwd Type: TABLE Owner: akm
+--
+
+CREATE TABLE "passwd" (
+	"userid" integer DEFAULT nextval('"passwd_userid_seq"'::text) NOT NULL,
+	"username" character varying(64) NOT NULL,
+	"password" character varying(64) NOT NULL,
+	"roles" character varying(255),
+	Constraint "passwd_pkey" Primary Key ("userid")
+);
+
+--
+-- TOC Entry ID 4 (OID 24965)
+--
+-- Name: userproperties_propertyid_seq Type: SEQUENCE Owner: akm
+--
+
+CREATE SEQUENCE "userproperties_propertyid_seq" start 1 increment 1 maxvalue 2147483647 minvalue 1  cache 1 ;
+
+--
+-- TOC Entry ID 7 (OID 24984)
+--
+-- Name: userproperties Type: TABLE Owner: akm
+--
+
+CREATE TABLE "userproperties" (
+	"propertyid" integer DEFAULT nextval('"userproperties_propertyid_seq"'::text) NOT NULL,
+	"username" character varying(64) NOT NULL,
+	"key" character varying(128) NOT NULL,
+	"value" text NOT NULL,
+	"istemporary" integer,
+	Constraint "userproperties_pkey" Primary Key ("propertyid")
+);
+
+--
+-- TOC Entry ID 8 (OID 24984)
+--
+-- Name: "username_idx" Type: INDEX Owner: akm
+--
+
+CREATE  INDEX "username_idx" on "userproperties" using btree ( "username" "varchar_ops" );
diff --git a/ZopeProducts/exUserFolder/dtml/docLogin.dtml b/ZopeProducts/exUserFolder/dtml/docLogin.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..87e301948e27d72fd0cd2156ce124dd3c92bfd01
--- /dev/null
+++ b/ZopeProducts/exUserFolder/dtml/docLogin.dtml
@@ -0,0 +1,49 @@
+<dtml-var standard_html_header>
+<center>
+<dtml-if authFailedCode>
+<dtml-call "REQUEST.set('loginTitle', getAuthFailedMessage(authFailedCode))">
+<dtml-else>
+<dtml-call "REQUEST.set('loginTitle', 'Login Required')">
+</dtml-if>
+<dtml-var "DialogHeader(_.None,_,DialogTitle=loginTitle)">
+<P>
+<dtml-if destination>
+<FORM ACTION="&dtml-destination;" METHOD="POST">
+<dtml-else>
+<FORM ACTION="&dtml-URL;" METHOD="POST">
+</dtml-if>
+
+<dtml-var "query_string_to_form_inputs(QUERY_STRING)"> <dtml-comment> Added by Emmanuel for ScoDoc</dtml-comment>
+
+
+<TABLE>
+<TR>
+  <TD ALIGN="LEFT" VALIGN="TOP">
+  <STRONG><dtml-babel src="'en'">Name</dtml-babel></STRONG>
+  </TD>
+  <TD ALIGN="LEFT" VALIGN="TOP">
+  <INPUT TYPE="TEXT" NAME="__ac_name" SIZE="20">
+  </TD>
+</TR>
+
+<TR>
+  <TD ALIGN="LEFT" VALIGN="TOP">
+  <STRONG><dtml-babel src="'en'">Password</dtml-babel></STRONG>
+  </TD>
+  <TD ALIGN="LEFT" VALIGN="TOP">
+  <INPUT TYPE="PASSWORD" NAME="__ac_password" SIZE="20">
+  </TD>
+</TR>
+<TR>
+  <TD ALIGN="LEFT" VALIGN="TOP">
+  </TD>
+</TR>
+</TABLE>
+<center>
+<INPUT TYPE="SUBMIT" NAME="submit" VALUE=" <dtml-babel src="'en'">Ok</dtml-babel> ">
+</center>
+</FORM>
+<br>
+<dtml-var DialogFooter>
+</center>
+<dtml-var standard_html_footer>
diff --git a/ZopeProducts/exUserFolder/dtml/docLoginRedirect.dtml b/ZopeProducts/exUserFolder/dtml/docLoginRedirect.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..30ed97b4a80950e24ab76076bb928b2fb5ae36c6
--- /dev/null
+++ b/ZopeProducts/exUserFolder/dtml/docLoginRedirect.dtml
@@ -0,0 +1,26 @@
+<HTML>
+<HEAD>
+<TITLE>Logging In</TITLE>
+<dtml-call makeRedirectPath>
+<META HTTP-EQUIV=Expires CONTENT="<dtml-var "ZopeTime()-1" fmt="%Y-%m-%d">">
+<META HTTP-EQUIV=Refresh CONTENT="0; URL=&dtml-URL1;/acl_users/redirectToLogin?destination=&dtml-URL;<dtml-if "REQUEST.has_key('authFailedCode')">&authFailedCode=&dtml-authFailedCode;</dtml-if>">
+</HEAD>
+<BODY>
+<!-- This is here to stop IE's default 512 byte limit from kicking in and
+     showing us "Friendly" error pages.
+
+The Unenlightened Zopistas Guide to exUserFolder.
+(C) 2001-2003 Andrew Milton <akm@theinternet.com.au>
+
+0. INTRODUCTION.
+
+exUserFolder is an extensible authentication product for the Zope
+Application Server. It allows a user to choose from a number of
+methods of authenticating their users, and allows the setting and
+fetching of arbitrary properties on a User object.
+
+Authentication methods, and Property methods do not have to use the
+same backing store, so it is possible to use legacy user sources, and
+-->
+</BODY></HTML>
+
diff --git a/ZopeProducts/exUserFolder/dtml/docLogout.dtml b/ZopeProducts/exUserFolder/dtml/docLogout.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..5210fc745ded506c448389a3b7e1dfc4a4703ff3
--- /dev/null
+++ b/ZopeProducts/exUserFolder/dtml/docLogout.dtml
@@ -0,0 +1,16 @@
+<dtml-var standard_html_header>
+<P>
+<CENTER>
+<p>Vous êtes déconnecté de ScoDoc.
+</p>
+
+<p><a href="<dtml-var "BASE0">">revenir à l'accueil</a></p>
+
+<br/>
+<p><em style="color: red;">(Attention: si vous êtes administrateur, vous ne pouvez vous déconnecter complètement qu'en relançant votre navigateur)
+</em></p>
+</CENTER>
+
+
+
+<dtml-var standard_html_footer>
diff --git a/ZopeProducts/exUserFolder/dtml/mainUser.dtml b/ZopeProducts/exUserFolder/dtml/mainUser.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..e118816557721e96b4bccd0c3827d7c814e380bb
--- /dev/null
+++ b/ZopeProducts/exUserFolder/dtml/mainUser.dtml
@@ -0,0 +1,55 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<form action="manage_users" method="post">
+<dtml-if user_names>
+<p class="form-help">
+The following users have been defined. Click on the name of a 
+user to edit that user.
+</p>
+
+<table cellspacing="0" cellpadding="2" border="0">
+<dtml-in user_names>
+<dtml-if sequence-odd>
+<tr class="row-normal">
+<dtml-else>
+<tr class="row-hilite">
+</dtml-if>
+  <td align="left" valign="top">
+  <input type="checkbox" name="names:list" value="&dtml-sequence-item;" />
+  </td>
+  <td align="left" valign="top">
+  <div class="list-item">
+  <a href="manage_users?name=&dtml.url_quote-sequence-item;&submit=Edit"><img src="&dtml-BASEPATH1;/p_/User_icon"
+   alt="" border="0" /></a>
+  <a href="manage_users?name=&dtml.url_quote-sequence-item;&submit=Edit">&dtml-sequence-item;</a>
+  </div>
+  </td>
+</tr>
+</dtml-in user_names>
+<tr>
+  <td align="left" valign="top">&nbsp;
+  </td>
+  <td align="left" valign="top">
+  <div class="form-element">
+  <input class="form-element" type="submit" name="submit" value="Add..." />
+  <input class="form-element" type="submit" name="submit" value="Delete" />
+  </div>
+  </td>
+</tr>
+</table>
+<dtml-else user_names>
+<p class="std-text">
+There are no users defined.
+</p>
+
+<p>
+<div class="form-element">
+<input class="form-element" type="submit" name="submit" value="Add..." />
+</div>
+</p>
+</dtml-if user_names>
+</form>
+
+<dtml-var manage_page_footer>
+
diff --git a/ZopeProducts/exUserFolder/dummyZBabelTag.py b/ZopeProducts/exUserFolder/dummyZBabelTag.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc45c244e2ac7071be65685b4fb82fb3ecae2694
--- /dev/null
+++ b/ZopeProducts/exUserFolder/dummyZBabelTag.py
@@ -0,0 +1,164 @@
+try:
+	from Products.ZBabel import ZBabelTag
+except:
+
+	from DocumentTemplate.DT_String import String
+	from DocumentTemplate.DT_Util   import render_blocks, Eval, ParseError
+	import string, zLOG
+
+
+	# fake Babel/Fish Tags
+
+	class ZBabelTag:
+		'''ZBabel Tag class - The cool translation tag'''
+
+		# define the name of the tag; also tell the system it is a doublet
+		name = 'babel'
+		blockContinuations = ()
+
+
+		def __init__(self, blocks):
+			'''__init__(self, blocks) --> Initialize tag object; return None'''
+			(tname, args, section,) = blocks[0]
+
+			self.section = section
+
+
+		def render(self, md):
+			'''render(self, md) --> Do the Translation; return string'''
+			return render_blocks(self.section.blocks, md)
+		__call__=render
+	# register the DTML-BABEL tag
+	String.commands['babel'] = ZBabelTag
+
+	class FishTag:
+		'''Fish Tag class - Short-Cut version of the cool translation tag (babel)
+
+		   This tag is used to quickly translate menu-like text snippets, similar to
+		   the KDE translation.'''
+
+		# define the tag name
+		name = 'fish'
+
+		# define additional variables
+		literal = 1
+
+		src = 'label'
+		attrs = {'dst': None, 'label': '', 'data': None, 'towerId': None}
+
+		def __init__(self, args):
+			'''__init__(self, blocks) --> Initialize tag object; return None'''
+			self.section = None
+			args = parseTagParameters(args, tag=self.name)
+			self.args = self.validateArguments(args)
+			
+			for attr in self.attrs.keys(): 
+				setattr(self, attr, self.attrs[attr])
+				
+		def validateArguments(self, args):
+			'''validateArguments(self, args) --> try to evaluate the passed expression or try to get an object from the passed id; if all this fails, leave the string, it is probably cool!; return tuple of (name, value)'''
+			# I stole this from dtml-let...
+			# SR: Like he said: Always copy existing code to make you life easier (evben though
+			#	  I changed some variables around
+			for count in range(len(args)):
+				(name, expr,) = args[count]
+				if ((expr[:1] == '"') and ((expr[-1:] == '"') and (len(expr) > 1))):
+					expr = expr[1:-1]
+					try:
+
+						args[count] = (name, Eval(expr).eval)
+
+					except SyntaxError, v:
+						(m, (huh, l, c, src,),) = v
+						raise ParseError, (('<strong>Expression (Python) Syntax error</strong>:' +
+											'<pre>\012%s\012</pre>\012' % v[0]), 'babel')
+
+				elif ((expr[:1] == "'") and ((expr[-1:] == "'") and (len(expr) > 1))):
+					expr = expr[1:-1]
+					args[count] = (name, expr)
+
+			return args
+			
+		def render(self, md):
+			'''render(self, md) --> Do the Translation; return string'''
+			data = None
+			for name, expr in self.args:
+				if type(expr) is type(''):
+					try:
+						data = md[expr]
+					except:
+						data = expr
+				else:
+					data = expr(md)
+					
+				#zLOG.LOG("exUserFolder", zLOG.INFO, "rendering name=%s expr=%s data=%s"%(name,expr,data))
+				
+			print data
+			return str(data)
+					  	 
+		__call__=render
+
+
+	# register the DTML-FISH tag
+	String.commands['fish'] = FishTag
+
+
+
+try:
+	import re
+	parmre=re.compile('''([\000- ]*([^\000- ="']+)=([^\000- ="']+))''');#"))
+	dqparmre=re.compile('([\000- ]*([^\000- ="]+)="([^"]*)")')
+	sqparmre=re.compile('''([\000- ]*([^\000- =']+)='([^']*)')''')
+except:
+	import regex
+	parmre=regex.compile('''([\000- ]*([^\000- ="']+)=([^\000- ="']+))''');#"))
+ 	dqparmre=regex.compile('([\000- ]*([^\000- ="]+)="([^"]*)")')
+ 	sqparmre=regex.compile('''([\000- ]*([^\000- =']+)='([^']*)')''')
+	
+
+
+def parseTagParameters(paramText, result = None, tag = 'babel',
+					   parmre=parmre,
+					   dqparmre=dqparmre,
+					   sqparmre=sqparmre,
+					   **parms):
+	result = (result or [])
+
+	parsedParam	  = parmre.match(paramText)
+	dqParsedParam = dqparmre.match(paramText)
+	sqParsedParam = sqparmre.match(paramText)
+
+	# Parse parameters of the form: name=value
+	if parsedParam is not None:
+		name   = parsedParam.group(2)
+		value  = parsedParam.group(3)
+		length = len(parsedParam.group(1))
+
+	# Parse parameters of the form: name="value"
+	elif dqParsedParam is not None:
+		name = dqParsedParam.group(2)
+		value = ('"%s"' % dqParsedParam.group(3))
+		length = len(dqParsedParam.group(1))
+
+	# Parse parameters of the form: name='value'
+	elif sqParsedParam is not None:
+		name = sqParsedParam.group(2)
+		value = ('''"'%s'"''' % sqParsedParam.group(3))
+		length = len(sqParsedParam.group(1))
+
+	else:
+		# There are no more parameters to parse
+		if ((not paramText) or (not string.strip(paramText))):
+			return result
+		raise ParseError, (('invalid parameter: "%s"' % paramText), tag)
+
+	# add the parameter/value pait to the results
+	result.append((name, value))
+
+	# remove the found parameter from the paramText
+	paramText = string.strip(paramText[length:])
+
+	if paramText:
+		return apply(parseTagParameters, (paramText, result, tag), parms)
+	else:
+		return result
diff --git a/ZopeProducts/exUserFolder/exUser.gif b/ZopeProducts/exUserFolder/exUser.gif
new file mode 100644
index 0000000000000000000000000000000000000000..c7f852b0ef2cac8ade0d72cbcdb558d3ba5facbf
Binary files /dev/null and b/ZopeProducts/exUserFolder/exUser.gif differ
diff --git a/ZopeProducts/exUserFolder/exUserFolder.gif b/ZopeProducts/exUserFolder/exUserFolder.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fea892ae56789c2cd308879ee498ff26aa7d89e3
Binary files /dev/null and b/ZopeProducts/exUserFolder/exUserFolder.gif differ
diff --git a/ZopeProducts/exUserFolder/exUserFolder.py b/ZopeProducts/exUserFolder/exUserFolder.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c330c96efbf3c810f7c6f57fb306da72d7615cd
--- /dev/null
+++ b/ZopeProducts/exUserFolder/exUserFolder.py
@@ -0,0 +1,1380 @@
+# Zope User Folder for ScoDoc
+# Adapte de l'Extensible User Folder
+# simplifie pour les besoins de ScoDoc.
+# Emmanuel Viennet 2013
+
+#
+# Extensible User Folder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: exUserFolder.py,v 1.93 2004/11/10 14:15:33 akm Exp $
+
+##############################################################################
+#
+# Zope Public License (ZPL) Version 0.9.4
+# ---------------------------------------
+# 
+# Copyright (c) Digital Creations.  All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+# 
+# 1. Redistributions in source code must retain the above
+#    copyright notice, this list of conditions, and the following
+#    disclaimer.
+# 
+# 6. Redistributions of any form whatsoever must retain the
+#    following acknowledgment:
+# 
+#      "This product includes software developed by Digital
+#      Creations for use in the Z Object Publishing Environment
+#      (http://www.zope.org/)."
+# 
+# Disclaimer
+# 
+#   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND
+#   ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+#   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+#   FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT
+#   SHALL DIGITAL CREATIONS OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+#   DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+#   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+#   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+#   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+#   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+#   LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+#   IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+#   THE POSSIBILITY OF SUCH DAMAGE.
+#
+##############################################################################
+
+# Portions Copyright (c) 2002 Nuxeo SARL <http://nuxeo.com>,
+#          Copyright (c) 2002 Florent Guillaume <mailto:fg@nuxeo.com>.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in
+#    the documentation and/or other materials provided with the
+#    distribution.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import Globals, App.Undo, socket, os, string, sha, random, sys, zLOG
+
+from Globals import DTMLFile, PersistentMapping
+from string import join,strip,split,lower,upper,find
+
+from OFS.Folder import Folder
+from OFS.CopySupport import CopyContainer
+
+from base64 import decodestring, encodestring
+from urllib import quote, unquote
+
+from Acquisition import aq_base
+from AccessControl import ClassSecurityInfo
+from AccessControl.Role import RoleManager
+from AccessControl.User import BasicUser, BasicUserFolder, readUserAccessFile
+from AccessControl.PermissionRole import PermissionRole
+from AccessControl.ZopeSecurityPolicy import _noroles
+from OFS.DTMLMethod import DTMLMethod
+from time import time
+from OFS.ObjectManager import REPLACEABLE
+from Persistence import Persistent
+
+from PropertyEditor import *
+
+from User import User, AnonUser
+from UserCache.UserCache import GlobalUserCache, GlobalNegativeUserCache, GlobalAdvancedCookieCache, SessionExpiredException
+
+from LoginRequiredMessages import LoginRequiredMessages
+
+from AccessControl import Unauthorized
+
+class LoginRequired(Exception):
+    """Login required"""
+    pass
+
+
+# If there is no NUG Product just define a dummy class
+try:
+	from Products.NuxUserGroups.UserFolderWithGroups import BasicGroupFolderMixin, _marker
+except ImportError:
+	class BasicGroupFolderMixin:
+		pass
+	_marker = None
+
+# Little function to create temp usernames
+def createTempName():
+	t=time()
+	t1=time()
+	t2=time()
+	t3 = 0.0
+	t3 = (t + t1 + t2) / 3
+	un = "Anonymous %.0f"%(t3)
+	return(un)
+
+
+manage_addexUserFolderForm=DTMLFile('dtml/manage_addexUserFolder', globals(), __name__='manage_addexUserFolderForm')
+
+
+
+def manage_addexUserFolder(self, authId, propId, memberId,
+						   cookie_mode=0, session_length=0,
+						   not_session_length=0,
+						   sessionTracking=None, idleTimeout=None,
+						   REQUEST={}, groupId=None, cryptoId=None):
+	""" """
+	if hasattr(self.aq_base, 'acl_users'):
+		return Globals.MessageDialog(self,REQUEST,
+			title  ='Item Exists',
+			message='This object already contains a User Folder',
+			action ='%s/manage_main' % REQUEST['URL1'])
+	ob=exUserFolder(authId, propId, memberId, groupId, cryptoId, cookie_mode,
+					session_length, sessionTracking, idleTimeout,
+					not_session_length)
+			
+	self._setObject('acl_users', ob, None, None, 0)
+	self.__allow_groups__=self.acl_users
+	ob=getattr(self, 'acl_users')
+	ob.postInitialisation(REQUEST)
+
+	if REQUEST:
+		return self.manage_main(self, REQUEST)
+	return ''
+
+#
+# Module level caches
+#
+XUFUserCache=GlobalUserCache()
+XUFNotUserCache=GlobalNegativeUserCache()
+XUFCookieCache=GlobalAdvancedCookieCache()
+
+class exUserFolder(Folder,BasicUserFolder,BasicGroupFolderMixin,
+				   CopyContainer):
+	""" """
+
+	# HACK! We use this meta_type internally so we can be pasted into
+	# the root. We registered with 'exUserFolder' meta_type however, so
+	# our constructors work.
+	meta_type='User Folder'
+	id       ='acl_users'
+	title    ='Extensible User Folder'
+	icon     ='misc_/exUserFolder/exUserFolder.gif'
+
+	isPrincipiaFolderish=1
+	isAUserFolder=1
+	__allow_access_to_unprotected_subobjects__=1
+	authSources={}
+	propSources={}
+	cryptoSources={}
+	membershipSources={}
+	groupSources={} # UNUSED by ScoDoc
+
+	manage_options=(
+		{'label':'Users',      'action':'manage_main'},
+		{'label':'Groups',	   'action':'manage_userGroups'},
+		{'label':'Parameters', 'action':'manage_editexUserFolderForm'},
+		{'label':'Authentication Source','action':'manage_editAuthSourceForm'},
+		{'label':'Properties Source','action':'manage_editPropSourceForm'},
+		{'label':'Membership Source', 'action':'manage_editMembershipSourceForm'},
+		{'label':'Cache Data', 'action':'manage_showCacheData'},
+		{'label':'Security',   'action':'manage_access'},
+		{'label':'Contents',   'action':'manage_contents'},
+		{'label':'Ownership',  'action':'manage_owner'},
+		{'label':'Undo',       'action':'manage_UndoForm'},
+		)
+
+	__ac_permissions__=(
+		('View management screens', ('manage','manage_menu','manage_main',
+									 'manage_copyright', 'manage_tabs',
+									 'manage_properties', 'manage_UndoForm',
+									 'manage_edit', 'manage_contents',
+									 'manage_cutObjects','manage_copyObjects',
+									 'manage_pasteObjects',
+									 'manage_renameForm',
+									 'manage_renameObject',
+									 'manage_renameObjects', ),
+		 ('Manager',)),
+		
+		('Undo changes',            ('manage_undo_transactions',),
+		 ('Manager',)),
+		
+		('Change permissions',      ('manage_access',),
+		 ('Manager',)),
+		
+		('Manage users',            ('manage_users', 'manage_editUserForm',
+									 'manage_editUser', 'manage_addUserForm',
+									 'manage_addUser', 'manage_userActions',
+									 'userFolderAddGroup',
+									 'userFolderDelGroups',
+									 'getGroupNames',
+					                                 'getGroupById',
+								         'manage_userGroups',
+									 'manage_addGroup',
+									 'manage_showGroup',),
+		 ('Manager',)),
+		
+		('Change exUser Folders',   ('manage_edit',),
+		 ('Manager',)),
+		
+		('View',                    ('manage_changePassword',
+									 'manage_forgotPassword', 'docLogin','docLoginRedirect',
+									 'docLogout', 'logout', 'DialogHeader',
+									 'DialogFooter', 'manage_signupUser',
+									 'MessageDialog', 'redirectToLogin','manage_changeProps'),
+		 ('Anonymous', 'Authenticated', 'Manager')),
+		
+		('Manage properties',       ('manage_addProperty',
+									 'manage_editProperties',
+									 'manage_delProperties',
+									 'manage_changeProperties',
+									 'manage_propertiesForm',
+									 'manage_propertyTypeForm',
+									 'manage_changePropertyTypes',
+									 ),
+		 ('Manager',)),
+		('Access contents information', ('hasProperty', 'propertyIds',
+										 'propertyValues','propertyItems',
+										 'getProperty', 'getPropertyType',
+										 'propertyMap', 'docLogin','docLoginRedirect',
+										 'DialogHeader', 'DialogFooter',
+										 'MessageDialog', 'redirectToLogin',),
+		 ('Anonymous', 'Authenticated', 'Manager')),
+		)
+	manage_access=DTMLFile('dtml/access',globals())
+	manage_tabs=DTMLFile('common/manage_tabs',globals())
+	manage_properties=DTMLFile('dtml/properties', globals())
+	manage_main=DTMLFile('dtml/mainUser', globals())
+	manage_contents=Folder.manage_main
+	manage_showCacheData=DTMLFile('dtml/manage_showCacheData', globals())
+
+	# This is going away soon...
+	docLoginRedirect=DTMLFile('dtml/docLoginRedirect', globals())	
+
+	# Stupid crap
+	try:
+		manage_contents._setName('manage_contents')
+	except AttributeError:
+		pass
+
+
+	MessageDialog=DTMLFile('common/MessageDialog', globals())
+	MessageDialog.__replaceable__ = REPLACEABLE
+	
+	manage_addUserForm=DTMLFile('dtml/manage_addUserForm',globals())
+	manage_editUserForm=DTMLFile('dtml/manage_editUserForm',globals())
+
+	DialogHeader__roles__=()
+	DialogHeader=DTMLFile('common/DialogHeader',globals())
+	DialogFooter__roles__=()
+	DialogFooter=DTMLFile('common/DialogFooter',globals())
+
+	manage_editAuthSourceForm=DTMLFile('dtml/manage_editAuthSourceForm',globals())
+	manage_editPropSourceForm=DTMLFile('dtml/manage_editPropSourceForm',globals())
+	manage_editMembershipSourceForm=DTMLFile('dtml/manage_editMembershipSourceForm', globals())
+
+	manage_addPropertyForm=DTMLFile('dtml/manage_addPropertyForm', globals())
+	manage_createPropertyForm=DTMLFile('dtml/manage_createPropertyForm', globals())
+	manage_editUserPropertyForm=DTMLFile('dtml/manage_editUserPropertyForm', globals())
+
+	manage_editexUserFolderForm=DTMLFile('dtml/manage_editexUserFolderForm', globals())
+
+	manage_userGroups=DTMLFile('dtml/mainGroup',globals())
+
+
+	# Use pages from NUG if it's there, otherwise no group support
+	try:
+		manage_addGroup = BasicGroupFolderMixin.manage_addGroup
+		manage_showGroup = BasicGroupFolderMixin.manage_showGroup
+	except:
+		manage_addGroup = None
+		manage_showGroup = None
+
+	# No more class globals
+	
+	# sessionLength=0 # Upgrading users should get no caching.
+	# notSessionLength=0 # bad cache limit
+	# cookie_mode=0
+	# sessionTracking=None # Or session tracking.
+	# idleTimeout=0
+	
+	def __init__(self, authId, propId, memberId, groupId, cryptoId,
+				 cookie_mode=0, session_length=0, sessionTracking=None,
+				 idleTimeout=0, not_session_length=0):
+		self.cookie_mode=cookie_mode
+		self.sessionLength=session_length
+		self.notSessionLength=not_session_length
+		self.sessionTracking=sessionTracking
+		self.idleTimeout=idleTimeout
+		
+		_docLogin=DTMLFile('dtml/docLogin',globals())
+		_docLogout=DTMLFile('dtml/docLogout',globals())
+
+		docLogin=DTMLMethod(__name__='docLogin')
+		docLogin.manage_edit(data=_docLogin, title='Login Page')
+		self._setObject('docLogin', docLogin, None, None, 0)
+
+		docLogout=DTMLMethod(__name__='docLogout')
+		docLogout.manage_edit(data=_docLogout, title='Logout Page')
+		self._setObject('docLogout', docLogout, None, None, 0)
+
+		postUserCreate=DTMLMethod(__name__='postUserCreate')
+		postUserCreate.manage_edit(data=_postUserCreate, title='Post User Creation methods')
+		self._setObject('postUserCreate', postUserCreate, None, None, 0)
+
+		self.manage_addAuthSource=self.authSources[authId].manage_addMethod
+		self.manage_addPropSource=self.propSources[propId].manage_addMethod
+		self.manage_addMembershipSource=self.membershipSources[memberId].manage_addMethod
+
+		self.manage_addGroupSource=None # UNUSED by ScoDoc
+		self.currentGroupsSource=None
+
+		if cryptoId:
+			self.cryptoId = cryptoId
+		else:
+			self.cryptoId = 'Crypt'
+			
+	def __setstate__(self, state):
+		Persistent.__setstate__(self, state)
+		if not hasattr(self, 'currentGroupSource'):
+			self.currentGroupSource = None
+		if not hasattr(self, 'sessionLength'):
+			self.sessionLength = 0
+		if not hasattr(self, 'notSessionLength'):
+			self.notSessionLength = 0
+		if not hasattr(self, 'cookie_mode'):
+			self.cookie_mode = 0
+		if not hasattr(self, 'sessionTraining'):
+			self.sessionTracking = None
+		if not hasattr(self, 'idleTimeout'):
+			self.idleTimeout=0
+
+	def manage_beforeDelete(self, item, container):
+		zLOG.LOG("exUserFolder", zLOG.BLATHER, "Attempting to delete an exUserFolder instance")
+		if item is self:
+			try:
+				self.cache_deleteCache()
+				self.xcache_deleteCache()
+				zLOG.LOG("exUserFolder", zLOG.BLATHER, "-- Caches deleted")
+			except:
+				#pass
+				zLOG.LOG("exUserFolder", zLOG.BLATHER, "-- Cache deletion failed")
+
+			try:
+				del container.__allow_groups__
+				zLOG.LOG("exUserFolder", zLOG.BLATHER, "-- container.__allow_groups_ deleted")
+			except:
+				#pass
+				zLOG.LOG("exUserFolder", zLOG.BLATHER, "-- container.__allow_groups_ deletion failed")
+
+
+	def manage_afterAdd(self, item, container):
+		zLOG.LOG("exUserFolder", zLOG.BLATHER, "Adding an exUserFolder")
+                         
+		if item is self:
+			if hasattr(self, 'aq_base'): self=self.aq_base
+			container.__allow_groups__=self
+
+	def manage_editPropSource(self, REQUEST):
+		""" Edit Prop Source """
+		if self.currentPropSource:
+			self.currentPropSource.manage_editPropSource(REQUEST)
+		return self.manage_main(self, REQUEST)
+
+	def manage_editAuthSource(self, REQUEST):
+		""" Edit Auth Source """
+		self.currentAuthSource.manage_editAuthSource(REQUEST)
+		return self.manage_main(self, REQUEST)
+
+	def manage_editMembershipSource(self, REQUEST):
+		""" Edit Membership Source """
+		if self.currentMembershipSource:
+			return self.currentMembershipSource.manage_editMembershipSource(REQUEST)
+
+	def postInitialisation(self, REQUEST):
+		self.manage_addAuthSource(self=self,REQUEST=REQUEST)
+		self.manage_addPropSource(self=self,REQUEST=REQUEST)
+		self.manage_addMembershipSource(self=self,REQUEST=REQUEST)
+		self.currentGroupSource = None
+	
+	def addAuthSource(self, REQUEST={}):
+		return self.manage_addAuthSourceForm(self, REQUEST)
+
+	def addPropSource(self, REQUEST={}):
+		return self.manage_addPropSourceForm(self, REQUEST)
+
+	def addMembershipSource(self, REQUEST={}):
+		return self.manage_editMembershipSourceForm(self, REQUEST)
+
+	def listUserProperties(self, username):
+		if self.currentPropSource:
+			return self.currentPropSource.listUserProperties(username=username)
+
+	def getUserProperty(self, username, key):
+		if self.currentPropSource:
+			return self.currentPropSource.getUserProperty(key=key, username=username)
+	
+	def reqattr(self, request, attr, default=None):
+		try:    return request[attr]
+		except: return default
+
+	def getAuthFailedMessage(self, code):
+		""" Return a code """
+		if LoginRequiredMessages.has_key(code):
+			return LoginRequiredMessages[code]
+		return 'Login Required'
+
+	# Called when we are deleted
+	def cache_deleteCache(self):
+		pp = string.join(self.getPhysicalPath(), '/')
+		XUFUserCache.deleteCache(pp)
+		
+	def cache_addToCache(self, username, password, user):
+		if not self.sessionLength:
+			return
+		# fix by emmanuel
+		if username == self._emergency_user.getUserName():
+			return
+		# /fix
+		pp = string.join(self.getPhysicalPath(), '/')
+		x = XUFUserCache.getCache(pp)
+		if not x:
+			x = XUFUserCache.createCache(pp, self.sessionLength)
+		x.addToCache(username, password, user)
+
+	def cache_getUser(self, username, password, checkpassword=1):
+		if not self.sessionLength:
+			return None
+		pp = string.join(self.getPhysicalPath(), '/')
+		x = XUFUserCache.getCache(pp)
+		if not x:
+			return None
+		u = x.getUser(self, username, password, checkpassword)
+		if u is not None:
+			u = u.__of__(self)
+		return u
+
+	def cache_removeUser(self, username):
+		if not self.sessionLength:
+			return
+		pp = string.join(self.getPhysicalPath(), '/')
+		x = XUFUserCache.getCache(pp)
+		if x:
+			x.removeUser(username)
+
+	def cache_getCacheStats(self):
+		pp = string.join(self.getPhysicalPath(), '/')
+		x = XUFUserCache.getCache(pp)
+		if not x:
+			x = XUFUserCache.createCache(pp, self.sessionLength)			
+		if x:
+			return x.getCacheStats()
+
+	def cache_getCurrentUsers(self):
+		pp = string.join(self.getPhysicalPath(), '/')
+		x = XUFUserCache.getCache(pp)
+		if x:
+			return x.getCurrentUsers(self)
+
+	# negative cache functions
+	def xcache_deleteCache(self):
+		pp = string.join(self.getPhysicalPath(), '/')
+		XUFNotUserCache.deleteCache(pp)
+		
+	def xcache_addToCache(self, username):
+		if not self.notSessionLength:
+			return
+		pp = string.join(self.getPhysicalPath(), '/')
+		x = XUFNotUserCache.getCache(pp)
+		if not x:
+			x = XUFNotUserCache.createCache(pp, self.notSessionLength)
+		x.addToCache(username)
+
+	def xcache_getUser(self, username):
+		if not self.notSessionLength:
+			return None
+		pp = string.join(self.getPhysicalPath(), '/')
+		x = XUFNotUserCache.getCache(pp)
+		if not x:
+			return None
+		return x.getUser(username)
+
+	def xcache_removeUser(self, username):
+		#zLOG.LOG('exUserFolder', zLOG.PANIC, 'xcache_removeUser(%s)' % username)
+		if not self.notSessionLength:
+			return
+		pp = string.join(self.getPhysicalPath(), '/')
+		x = XUFNotUserCache.getCache(pp)
+		if x:
+			#zLOG.LOG('exUserFolder', zLOG.PANIC, 'xcache_removeUser removing')
+			x.removeUser(username)
+
+	# Cookie Cache Functions
+	def cache_deleteCookieCache(self):
+		pp = string.join(self.getPhysicalPath(), '/')
+		XUFCookieCache.deleteCache(pp)
+
+	def cache_addToCookieCache(self, username, password, key):
+		pp = string.join(self.getPhysicalPath(), '/')
+		c = XUFCookieCache.getCache(pp)
+		if not c:
+			c = XUFCookieCache.createCache(pp, 86400)
+		c.addToCache(username, password, key)
+
+	def cache_getCookieCacheUser(self, key):
+		pp = string.join(self.getPhysicalPath(), '/')
+		c = XUFCookieCache.getCache(pp)
+		if not c:
+			return None
+		return c.getUser(key)
+
+	def cache_removeCookieCacheUser(self, key):
+		pp = string.join(self.getPhysicalPath(), '/')
+		c = XUFCookieCache.getCache(pp)
+		if c:
+			c.removeUser(key)
+    
+	def manage_editUser(self, username, REQUEST={}): # UNUSED by ScoDoc
+ 		""" Edit a User """
+		# username=self.reqattr(REQUEST,'username')
+		password=self.reqattr(REQUEST,'password')
+		password_confirm=self.reqattr(REQUEST,'password_confirm')
+		roles=self.reqattr(REQUEST,'roles', [])
+		groups=self.reqattr(REQUEST, 'groupnames', [])
+        
+		if not username:
+			return self.MessageDialog(self,REQUEST=REQUEST,
+				title  ='Illegal value', 
+				message='A username must be specified',
+				action ='manage_main')
+
+		if (password or password_confirm) and (password != password_confirm):
+			return self.MessageDialog(self,REQUEST=REQUEST,
+				title  ='Illegal value', 
+				message='Password and confirmation do not match',
+				action ='manage_main')
+		
+		self._doChangeUser(username, password, roles, domains='', groups=groups, REQUEST=REQUEST)
+		
+		return self.MessageDialog(self,REQUEST=REQUEST,
+			title = 'User Updated',
+			message= 'User %s was updated.'%(username),
+			action = 'manage_main')
+    
+    
+    # Methode special pour ScoDoc: evite le code inutile dans notre contexte
+    # et accede a la BD via le curseur psycopg2 fourni
+    # (facilitera la separation de Zope)
+	def scodoc_editUser(self, cursor, username, password=None, roles=[]):
+		"""Edit a ScoDoc user"""
+		roles = list(roles)
+		rolestring= ','.join(roles)
+		# Don't change passwords if it's null
+		if password:
+			secret=self.cryptPassword(username, password)
+			# Update just the password:			   
+			# self.sqlUpdateUserPassword(username=username, password=secret)
+			cursor.execute("UPDATE sco_users SET passwd=%(secret)s WHERE user_name=%(username)s",
+						   { 'secret':secret, 'username': username } )
+		
+		#self.sqlUpdateUser(username=username, roles=rolestring)
+		cursor.execute("UPDATE sco_users SET roles=%(rolestring)s WHERE user_name=%(username)s",
+					   { 'rolestring':rolestring, 'username': username } )
+
+		if hasattr(self.currentAuthSource, '_v_lastUser'):
+			# Specific for pgAuthSource:
+			self.currentAuthSource._v_lastUser={} # clear pg user cache
+		
+		# We may have updated roles or passwords... flush the user...
+		self.cache_removeUser(username)
+		self.xcache_removeUser(username)
+    
+	#
+	# Membership helper
+	#
+	def goHome(self, REQUEST, RESPONSE):
+		""" Go to home directory """
+		if self.currentMembershipSource:
+			self.currentMembershipSource.goHome(REQUEST, RESPONSE)
+
+
+	# 
+	# Membership method of changing user properties
+	# 
+
+	def manage_changeProps(self, REQUEST):
+		""" Change Properties """
+		if self.currentMembershipSource:
+			return self.currentMembershipSource.changeProperties(REQUEST)
+		else:
+			
+			return self.MessageDialog(self,REQUEST,
+				title = 'This is a test',
+				message= 'This was a test',
+				action = '..')
+ 
+
+	#
+	# Membership method of adding a new user.
+	# If everything goes well the membership plugin calls manage_addUser()
+	#
+	
+	def manage_signupUser(self, REQUEST):
+		""" Signup a new user """
+		""" This is seperate so you can add users using the normal """
+		""" interface w/o going through membership policy """
+
+		username=self.reqattr(REQUEST,'username')
+		roles=self.reqattr(REQUEST,'roles')
+
+		if not username:
+			return self.MessageDialog(self,REQUEST=REQUEST,
+				title  ='Illegal value', 
+				message='A username must be specified',
+				action ='manage_main')
+
+		if (self.getUser(username) or
+			(self._emergency_user and
+			 username == self._emergency_user.getUserName())):
+			return self.MessageDialog(self,REQUEST=REQUEST,
+				title  ='Illegal value', 
+				message='A user with the specified name already exists',
+				action ='manage_main')
+
+		if self.currentMembershipSource:
+			return self.currentMembershipSource.createUser(REQUEST)
+
+	#
+	# Membership method of changing passwords
+	#
+	def manage_changePassword(self, REQUEST):
+		""" Change a password """
+		if self.currentMembershipSource:
+			return self.currentMembershipSource.changePassword(REQUEST)
+		
+	#
+	# User says they can't remember their password
+	#
+	def manage_forgotPassword(self, REQUEST):
+		""" So something about forgetting your password """
+		if self.currentMembershipSource:
+			return self.currentMembershipSource.forgotPassword(REQUEST)
+		
+	def __creatable_by_emergency_user__(self): return 1
+
+	def manage_addUser(self, REQUEST):
+		""" Add a New User """
+		username=self.reqattr(REQUEST,'username')
+		password=self.reqattr(REQUEST,'password')
+		password_confirm=self.reqattr(REQUEST,'password_confirm')
+		roles=self.reqattr(REQUEST,'roles')
+		groups=self.reqattr(REQUEST, 'groupnames', [])
+
+		if not username:
+			return self.MessageDialog(self,REQUEST=REQUEST,
+				title  ='Illegal value', 
+				message='A username must be specified',
+				action ='manage_main')
+
+		if not password or not password_confirm:
+			return self.MessageDialog(self,REQUEST=REQUEST,
+				title  ='Illegal value', 
+				message='Password and confirmation must be specified',
+				action ='manage_main')
+
+		if (self.getUser(username) or
+			(self._emergency_user and
+			 username == self._emergency_user.getUserName())):
+			return self.MessageDialog(self,REQUEST=REQUEST,
+				title  ='Illegal value', 
+				message='A user with the specified name already exists',
+				action ='manage_main')
+
+		if (password or password_confirm) and (password != password_confirm):
+			return self.MessageDialog(self,REQUEST=REQUEST,
+				title  ='Illegal value', 
+				message='Password and confirmation do not match',
+				action ='manage_main')
+
+		self._doAddUser(username, password, roles, domains='', groups=groups, REQUEST=REQUEST)
+		#
+		# Explicitly check our contents, do not just acquire postUserCreate
+		#
+		if 'postUserCreate' in self.objectIds():
+			self.postUserCreate(self, REQUEST)
+		
+		return self.MessageDialog(self,REQUEST=REQUEST,
+			title = 'User Created',
+			message= 'User %s was created.'%(username),
+			action = 'manage_main')
+
+	def _doAddUser(self, name, password, roles, domains='', groups=(), **kw):
+		""" For programatically adding simple users """
+		self.currentAuthSource.createUser(name, password, roles)
+		if self.currentPropSource:
+			# copy items not in kw from REQUEST
+			REQUEST = kw.get('REQUEST', self.REQUEST)
+			map(kw.setdefault, REQUEST.keys(), REQUEST.values())
+			self.currentPropSource.createUser(name, kw)
+
+	def _doChangeUser(self, name, password, roles, domains='', groups=(), **kw):
+		self.currentAuthSource.updateUser(name, password, roles)
+		if self.currentPropSource:
+			# copy items not in kw from REQUEST
+			REQUEST = kw.get('REQUEST', self.REQUEST)
+			map(kw.setdefault, REQUEST.keys(), REQUEST.values())
+			self.currentPropSource.updateUser(name, kw)
+		# We may have updated roles or passwords... flush the user...
+		self.cache_removeUser(name)
+		self.xcache_removeUser(name)
+		
+	def _doDelUsers(self, names):
+		self.deleteUsers(names)
+
+	def _createInitialUser(self):
+		if len(self.getUserNames()) <= 1:
+			info = readUserAccessFile('inituser')
+			if info:
+				name, password, domains, remote_user_mode = info
+				self._doAddUser(name, password, ('Manager',), domains)
+
+
+	def getUsers(self):
+		"""Return a list of user objects or [] if no users exist"""
+		data=[]
+		try:
+			items=self.listUsers()
+			for people in items:
+				user=User({'name':		people['username'],
+						   'password':	people['password'],
+						   'roles':		people['roles'], 
+						   'domains':	''},
+						  self.currentPropSource,
+						  self.cryptPassword,
+						  self.currentAuthSource,
+						  self.currentGroupSource)
+				data.append(user)
+		except:
+			import traceback
+			traceback.print_exc()
+			pass
+			
+		return data
+
+	getUsers__roles__=('Anonymous','Authenticated')
+	
+	def getUser(self, name):
+		"""Return the named user object or None if no such user exists"""
+		user = self.cache_getUser(name, '', 0)
+		#zLOG.LOG('exUserFolder.getUser', zLOG.PANIC, 'cache_getUser(%s)=%s' % (name,user))
+		if user:
+			return user
+		try:
+			items=self.listOneUser(name)
+			#zLOG.LOG('exUserFolder.getUser', zLOG.PANIC, 'listOneUser=%s' % items) 
+		except:
+			zLOG.LOG("exUserFolder", zLOG.ERROR,
+                                 "error trying to list user %s" % name,
+                                 '',
+                                 sys.exc_info())
+			return None
+
+		if not items:
+			return None
+		
+		for people in items:
+			user =  User({'name':    people['username'],
+						  'password':people['password'],
+						  'roles':   people['roles'],
+						  'domains':	''},
+						 self.currentPropSource,
+						 self.cryptPassword,
+						 self.currentAuthSource,
+						 self.currentGroupSource)
+			return user
+		return None
+		
+	def manage_userActions(self, submit=None, userids=None, REQUEST={}):
+		""" Do things to users """
+		if submit==' Add ':
+			if hasattr(self.currentAuthSource,'manage_addUserForm'):
+				return self.currentAuthSource.manage_addUserForm(self, REQUEST)
+			else:
+				return self.manage_addUserForm(self, REQUEST)
+		if submit==' Delete ':
+			self.deleteUsers(userids)
+			return self.MessageDialog(self,REQUEST=REQUEST,
+				title  ='Users Deleted',
+				message='Selected Users have been deleted',
+				action =REQUEST['URL1']+'/manage_main',
+				target ='manage_main')
+
+		if REQUEST:
+			return self.manage_main(self,REQUEST)
+		return ''
+
+	def identify(self, auth):
+		# Identify the username and password.  This is where new modes should
+		# be called from, and if pluggable modes ever take shape, here ya go!
+
+		if self.cookie_mode and not auth:
+			# The identify signature does not include the request, sadly.
+			# I think that's dumb.
+			request = self.REQUEST
+			response = request.RESPONSE
+	
+			if request.has_key('__ac_name') and request.has_key('__ac_password'):
+				return request['__ac_name'], request['__ac_password']
+			elif request.has_key('__ac') and self.cookie_mode == 1:
+				return self.decodeBasicCookie(request, response)
+			elif request.has_key('__aca') and self.cookie_mode == 2:
+				return self.decodeAdvancedCookie(request, response)
+
+		if auth and lower(auth[:6]) == 'basic ':
+				return tuple(split(decodestring(split(auth)[-1]), ':', 1))
+
+		return None, None
+
+	def decodeUserCookie(self, request, response):
+		return self.identify('')
+
+	def validate(self, request, auth='', roles=_noroles):
+		"""
+		Perform identification, authentication, and authorization.
+		"""
+		# Called at each web request
+		#zLOG.LOG('exUserFolder', zLOG.PANIC, 'validate')
+		v = request['PUBLISHED']
+		a, c, n, v = self._getobcontext(v, request)
+
+		name, password = self.identify(auth) # decode cookie, and raises LoginRequired if no ident info
+        # password is the cleartext passwd
+		# zLOG.LOG('exUserFolder', zLOG.DEBUG, 'identify returned %s, %s' % (name, password))
+
+		response = request.RESPONSE
+		if name is not None:
+			try:
+				xcached_user = self.xcache_getUser(name)
+				#zLOG.LOG('exUserFolder.validate', zLOG.PANIC, 'xcached_user=%s' % xcached_user)
+				if xcached_user:
+					#zLOG.LOG('exUserFolder.validate', zLOG.PANIC, 'returning None')
+					return None
+			except:
+				zLOG.LOG('exUserFolder', zLOG.ERROR,
+						 "error while looking up '%s' on the xcache" % name,
+						 '',
+						 sys.exc_info())
+
+			user = self.authenticate(name, password, request)
+			#zLOG.LOG('exUserFolder.validate', zLOG.PANIC, 'user=%s' % user) 
+			if user is None:
+				# If it's none, because there's no user by that name,
+				# don't raise a login, allow it to go higher...
+				# This kinda breaks for people putting in the wrong username
+				# when the Folder above uses a different auth method.
+				# But it doesn't lock Manager users out inside Zope.
+				# Perhaps this should be a tunable.
+
+				# modified by Emmanuel
+				try:
+					lou = self.listOneUser(name) 
+				except:
+					lou = None
+				if lou:
+					self.challenge(request, response, 'login_failed', auth)
+				return None
+			self.remember(name, password, request)
+			self.cache_addToCache(name, password, user)
+			emergency = self._emergency_user
+			if emergency and user is emergency:
+				if self._isTop():
+					return emergency.__of__(self)
+				else:
+					return None
+			if self.authorize(user, a, c, n, v, roles):
+				return user.__of__(self)
+			if self._isTop() and self.authorize(self._nobody, a, c, n, v, roles):
+				return self._nobody.__of__(self)
+			self.challenge(request, response, 'unauthorized')
+			return None
+		else:
+			if self.sessionTracking and self.currentPropSource:
+				user = self.createAnonymousUser(request, response)
+				if self.authorize(user, a, c, n, v, roles):
+					return user.__of__(self)
+			if self.authorize(self._nobody, a, c, n, v, roles):
+				if self._isTop():
+					return self._nobody.__of__(self)
+				else:
+					return None
+			else:
+				self.challenge(request, response, None, auth)
+				return None
+	
+	def authenticate(self, name, password, request):
+		#zLOG.LOG('exUserFolder.authenticate', zLOG.PANIC, '%s %s' % (name, password)) 
+		emergency = self._emergency_user
+		if emergency and name == emergency.getUserName():
+			return emergency
+		try:
+			user = self.cache_getUser(name, password)
+			#zLOG.LOG('exUserFolder.authenticate', zLOG.PANIC, 'cache_getUser=%s' % user) 
+			if user:
+				return user
+		except SessionExpiredException:
+			if self.idleTimeout:
+				self.logout(request)
+				self.challenge(request, request.RESPONSE, 'session_expired')
+				return None
+		user = self.getUser(name)
+		#zLOG.LOG('exUserFolder.authenticate', zLOG.PANIC, 'getUser=%s' % user) 
+		if user is not None:
+			if user.authenticate(self.currentAuthSource.listOneUser,
+								 password,
+								 request,
+								 self.currentAuthSource.remoteAuthMethod):
+				return user
+		return None
+
+	def challenge(self, request, response, reason_code='unauthorized',
+				  auth=''):
+		# Give whatever mode we're in a chance to challenge the validation
+		# failure.  We do this to preserve LoginRequired behavior.  The
+		# other thing we could do is let the None propagate on up and patch
+		# the request's unauthorized method to 
+
+		if self.cookie_mode and not auth:
+			zLOG.LOG('exUserFolder', zLOG.DEBUG, 'raising LoginRequired for %s' % reason_code)
+			if reason_code == 'login_failed':
+				response.expireCookie('__ac', path='/')
+				response.expireCookie('__aca', path='/')
+			if reason_code:
+				request.set('authFailedCode', reason_code)
+			raise LoginRequired(self.docLogin(self, request))
+		else:
+			zLOG.LOG('exUserFolder', zLOG.DEBUG, 'not raising LoginRequired for %s' % reason_code)
+
+	def remember(self, name, password, request):
+		response = request.RESPONSE
+		if self.cookie_mode == 1:
+			self.setBasicCookie(name, password, request, response)
+		elif self.cookie_mode == 2:
+			self.setAdvancedCookie(name, password, request, response)
+
+		if self.cookie_mode:
+			try:
+				del request.form['__ac_name']
+				del request.form['__ac_password']
+			except KeyError:
+				pass
+
+	def makeRedirectPath(self):
+		REQUEST=self.REQUEST
+		if not REQUEST.has_key('destination'):
+			script=REQUEST['SCRIPT_NAME']
+			pathinfo=REQUEST['PATH_INFO']
+			redirectstring=script+pathinfo
+			if REQUEST.has_key('QUERY_STRING'):
+				querystring='?'+quote(REQUEST['QUERY_STRING'])
+				redirectstring=redirectstring+querystring
+
+			REQUEST['destination']=redirectstring
+		
+	def redirectToLogin(self, REQUEST):
+		""" Allow methods to call from Web """
+		script=''
+		pathinfo=''
+		querystring=''
+		redirectstring=''
+		authFailedCode=''
+		
+		if not REQUEST.has_key('destination'):
+			if self.currentMembershipSource:
+				redirectstring = self.currentMembershipSource.getLoginDestination(REQUEST)
+			else:
+				script=REQUEST['SCRIPT_NAME']
+				pathinfo=REQUEST['PATH_INFO']
+				redirectstring=script+pathinfo
+				if REQUEST.has_key('QUERY_STRING'):
+					querystring='?'+REQUEST['QUERY_STRING']
+					redirectstring=redirectstring+querystring
+
+			REQUEST['destination']=redirectstring
+
+		
+		if REQUEST.has_key('authFailedCode'):
+			authFailedCode='&authFailedCode='+REQUEST['authFailedCode']
+		
+			
+			
+		if self.currentMembershipSource and self.currentMembershipSource.loginPage:
+			try:
+				REQUEST.RESPONSE.redirect('%s/%s?destination=%s%s'%(self.currentMembershipSource.baseURL, self.currentMembershipSource.loginPage,REQUEST['destination'],authFailedCode))				
+				return
+			except:
+				pass
+		return self.docLogin(self,REQUEST)
+
+	def decodeBasicCookie(self, request, response):
+		c=request['__ac']
+		c=unquote(c)
+		try:
+			c=decodestring(c)
+		except:
+			response.expireCookie('__ac', path='/')
+			raise LoginRequired(self.docLogin(self, request))
+		
+		name,password=tuple(split(c, ':', 1))
+		return name, password
+		
+	def decodeAdvancedCookie(self, request, response):
+		c = ''
+		try:
+			c = request['__aca']
+			c = unquote(c)
+		except:
+			response.expireCookie('__aca', path='/')
+			response.expireCookie('__ac', path='/')	# Precaution
+			response.flush()
+			raise LoginRequired(self.docLogin(self, request))
+
+		u = self.cache_getCookieCacheUser(c)
+		if u:
+			return u
+
+		response.expireCookie('__aca', path='/')
+		response.expireCookie('__ac', path='/')	# Precaution
+		response.flush()
+		raise LoginRequired(self.docLogin(self, request))
+
+	def setBasicCookie(self, name, password, request, response):
+		token='%s:%s' % (name, password)
+		token=encodestring(token)
+		token=quote(token)
+		response.setCookie('__ac', token, path='/')
+		request['__ac']=token
+		
+	def setAdvancedCookie(self, name, password, request, response):
+		xufid = self._p_oid
+		hash = encodestring(sha.new('%s%s%f%f%s'%(
+			name, password, time(), random.random(), str(request))).digest())
+		token=quote(hash)
+		response.setCookie('__aca', token, path='/')
+		response.flush()
+		request['__aca']=token
+		self.cache_addToCookieCache(name, password, hash)
+		
+	def setAnonCookie(self, name, request, resp):
+		token='%s:%s' % (name, '')
+		token=encodestring(token)
+		token=quote(token)
+		resp.setCookie('__ac', token, path='/')
+		request['__ac']=token
+
+	def createAnonymousUser(self, request, resp):
+		aName=createTempName()
+		bogusREQUEST={}
+		bogusREQUEST['user_realname']='Guest User'
+		self.currentPropSource.createUser(aName, bogusREQUEST)
+		ob = AnonUser(aName, [], self.currentPropSource)
+		ob = ob.__of__(self)
+		self.cache_addToCache(aName, '', ob)			
+		self.setAnonCookie(aName, request, resp)
+		return ob
+		
+	def manage_edit(self, cookie_mode, session_length, sessionTracking=None,
+					idleTimeout=0, not_session_length=0,
+			                title=None,
+					REQUEST=None):
+		"""Change properties"""
+
+		self.cookie_mode=cookie_mode
+		self.sessionLength=session_length
+		self.notSessionLength=not_session_length
+		self.sessionTracking=sessionTracking
+		self.idleTimeout=idleTimeout
+		if title:
+			self.title = title
+		
+		if REQUEST:
+			return self.MessageDialog(self,REQUEST=REQUEST,
+				title  ='exUserFolder Changed',
+				message='exUserFolder properties have been updated',
+				action =REQUEST['URL1']+'/manage_main',
+				target ='manage_main')
+
+	def logout(self, REQUEST):
+		"""Logout"""
+		try:
+			self.cache_removeUser(REQUEST['AUTHENTICATED_USER'].getUserName())
+		except:
+			pass
+		
+		REQUEST['RESPONSE'].expireCookie('__ac', path='/')
+		REQUEST.cookies['__ac']=''
+		try:
+			acc = REQUEST['__aca']
+			self.cache_removeCookieCacheUser(acc)
+			REQUEST.cookies['__aca']=''
+		except:
+			pass
+		REQUEST['RESPONSE'].expireCookie('__aca', path='/')
+
+		
+		
+		return self.docLogout(self, REQUEST)
+
+	#
+	# Methods to be supplied by Auth Source
+	#
+	def deleteUsers(self, userids):
+		self.currentAuthSource.deleteUsers(userids)
+
+		# Comment out to use Andreas' pgSchema
+		if self.currentPropSource:
+			self.currentPropSource.deleteUsers(userids)
+
+		if self.currentGroupSource:
+			self.currentGroupSource.deleteUsers(userids)
+			
+
+	def listUsers(self):
+		return self.currentAuthSource.listUsers()
+
+	def user_names(self):
+		return self.currentAuthSource.listUserNames()
+	
+	def getUserNames(self):
+		return self.currentAuthSource.listUserNames()
+
+	def listOneUser(self,username):
+		return self.currentAuthSource.listOneUser(username)
+
+	def cryptPassword(self, username, password):
+		if hasattr(aq_base(self.currentAuthSource), 'cryptPassword'):
+			return self.currentAuthSource.cryptPassword(username, password)
+
+		if hasattr(self, 'cryptoId'):
+			return self.cryptoSources[self.cryptoId].plugin(self, username, password)
+		return self.cryptoSources['Crypt'].plugin(self, username, password)
+
+	def PropertyEditor(self):
+		""" """
+		if self.REQUEST.has_key(self.REQUEST['propName']):
+			return PropertyEditor.EditMethods[self.REQUEST['propType']](self.REQUEST['propName'], self.REQUEST[self.REQUEST['propName']])
+		return PropertyEditor.EditMethods[self.REQUEST['propType']](self.REQUEST['propName'], None)
+
+	def PropertyView(self):
+		""" """
+		if self.REQUEST.has_key(self.REQUEST['propName']):
+			return PropertyEditor.ViewMethods[self.REQUEST['propType']](self.REQUEST['propName'], self.REQUEST[self.REQUEST['propName']])
+		return PropertyEditor.ViewMethods[self.REQUEST['propType']](self.REQUEST['propName'], None)
+
+	def manage_addUserProperty(self, username, propName, propValue, REQUEST):
+		""" add a new property """
+		self.currentPropSource.setUserProperty(propName, username, propValue)
+		if hasattr(self.currentAuthSource,'manage_editUserForm'):
+			return self.currentAuthSource.manage_editUserForm(self, REQUEST)
+		else:
+			return self.manage_editUserForm(self,REQUEST)
+
+	def getUserCacheStats(self):
+		""" Stats """
+		if self.sessionLength:
+			if self.cache_getCacheStats()['attempts']:
+				return self.cache_getCacheStats()
+		return None
+
+	def getUserCacheUsers(self):
+		""" Current Users """
+		if self.sessionLength:
+			return self.cache_getCurrentUsers()
+		return None
+
+	def userFolderAddGroup(self, groupname, title='', **kw):
+		"""Creates a group"""
+		if self.currentGroupSource:
+			apply(self.currentGroupSource.addGroup, (groupname, title), kw)
+	
+	def userFolderDelGroups(self, groupnames):
+		"""Deletes groups"""
+		if self.currentGroupSource:
+			for groupname in groupnames:
+				self.currentGroupSource.delGroup(groupname)
+
+	def getGroupNames(self):
+		"""Returns a list of group names"""
+		if self.currentGroupSource:
+			return self.currentGroupSource.listGroups()
+		else:
+			return []
+
+
+	def getGroupById(self, groupname, default=_marker):
+		"""Returns the given group"""
+		if self.currentGroupSource:
+			group = self.currentGroupSource.getGroup(groupname, default)
+			if group:
+				return group.__of__(self)
+			else:
+				return None
+
+	def setUsersOfGroup(self, usernames, groupname):
+		"""Sets the users of the group"""
+		if self.currentGroupSource:
+			return self.currentGroupSource.setUsersOfGroup(usernames, groupname)
+
+	def addUsersToGroup(self, usernames, groupname):
+		"""Adds users to a group"""
+		if self.currentGroupSource:
+			return self.currentGroupSource.addUsersToGroup(usernames, groupname)
+
+	def delUsersFromGroup(self, usernames, groupname):
+		"""Deletes users from a group"""
+		if self.currentGroupSource:
+			return self.currentGroupSource.delUsersFromGroup(usernames, groupname)
+
+	def setGroupsOfUser(self, groupnames, username):
+		"""Sets the groups of a user"""
+		if self.currentGroupSource:
+			return self.currentGroupSource.setGroupsOfUser(groupnames, username)
+
+	def addGroupsOfUser(self, groupnames, username):
+		"""Add groups to a user"""
+		if self.currentGroupSource:
+			return self.currentGroupSource.addGroupsToUser(groupnames, username)
+
+	def delGroupsOfUser(self, groupnames, username):
+		"""Deletes groups from a user"""
+		if self.currentGroupSource:
+			return self.currentGroupSource.delGroupsFromUser(groupnames, username)
+
+	# We lie.
+	def hasUsers(self):
+		return 1
+
+
+def doAuthSourceForm(self,authId):
+	""" la de da """
+	return exUserFolder.authSources[authId].manage_addForm
+
+def doPropSourceForm(self,propId):
+	""" la de da """
+	return exUserFolder.propSources[propId].manage_addForm
+
+def doMembershipSourceForm(self, memberId):
+	""" doot de doo """
+	return exUserFolder.membershipSources[memberId].manage_addForm
+
+#def doGroupSourceForm(self,groupId):
+#	""" la de da """
+#	return exUserFolder.groupSources[groupId].manage_addForm
+
+def getAuthSources(self):
+	""" Hrm I need a docstring """
+	l=[]
+	for o in exUserFolder.authSources.keys():
+		l.append(
+			exUserFolder.authSources[o]
+			)
+	return l
+
+def getPropSources(self):
+	""" Hrm I need a docstring """
+	l=[]
+	for o in exUserFolder.propSources.keys():
+		l.append(
+			exUserFolder.propSources[o]
+			)
+	return l
+
+def getMembershipSources(self):
+	""" Hrm I need a docstring """
+	l=[]
+	for o in exUserFolder.membershipSources.keys():
+		l.append(
+			exUserFolder.membershipSources[o]
+			)
+	return l
+
+def getGroupSources(self):
+	""" Hrm I need a docstring """
+	return [] # UNUSED by ScoDoc: empty
+
+def getCryptoSources(self):
+	""" Doc String """
+	l = []
+	for o in exUserFolder.cryptoSources.keys():
+		l.append(
+			exUserFolder.cryptoSources[o]
+			)
+	return l
+
+def MailHostIDs(self):
+    """Find SQL database connections in the current folder and above
+
+    This function return a list of ids.
+    """
+    return [] # UNUSED BY SCODOC
+
+from types import ListType, IntType, LongType, FloatType, NoneType, DictType, StringType
+
+def getVariableType(self, o):
+
+	if type(o) == ListType:
+		return 'List'
+	if type(o) == IntType:
+		return 'Int'
+	if type(o) == LongType:
+		return 'Long'
+	if type(o) == FloatType:
+		return 'Float'
+	if type(o) == NoneType:
+		return 'None'
+	if type(o) == DictType:
+		return 'Dict'
+	if type(o) == StringType:
+		return 'String'
+	return 'Unknown or Restricted'
+
+_postUserCreate='''
+<dtml-comment>
+Replace this method with whatever you want to do
+when a user is created, you can use a Python Script,
+or External Method, or keep it as a DTML Method if you
+want to
+</dtml-comment>
+'''
diff --git a/ZopeProducts/exUserFolder/exUserFolderPlugin.gif b/ZopeProducts/exUserFolder/exUserFolderPlugin.gif
new file mode 100644
index 0000000000000000000000000000000000000000..dd8e2c9f736567a421b9e96dae3313469c071b5c
Binary files /dev/null and b/ZopeProducts/exUserFolder/exUserFolderPlugin.gif differ
diff --git a/ZopeProducts/exUserFolder/nullPlugin/__init__.py b/ZopeProducts/exUserFolder/nullPlugin/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0d9e576611653b30e534e402fd228eb86ebda95
--- /dev/null
+++ b/ZopeProducts/exUserFolder/nullPlugin/__init__.py
@@ -0,0 +1,2 @@
+# $Id: __init__.py,v 1.4 2004/11/10 14:15:57 akm Exp $
+import nullPlugin
diff --git a/ZopeProducts/exUserFolder/nullPlugin/nullPlugin.py b/ZopeProducts/exUserFolder/nullPlugin/nullPlugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd9cd526d7f308c6e58f52a58db25be4a8bff53a
--- /dev/null
+++ b/ZopeProducts/exUserFolder/nullPlugin/nullPlugin.py
@@ -0,0 +1,39 @@
+#
+# Extensible User Folder
+# 
+# Null Plugin for exUserFolder
+#
+# (C) Copyright 2000,2001 The Internet (Aust) Pty Ltd
+# ACN: 082 081 472  ABN: 83 082 081 472
+# All Rights Reserved
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# Author: Andrew Milton <akm@theinternet.com.au>
+# $Id: nullPlugin.py,v 1.5 2004/11/10 14:15:57 akm Exp $
+import string,Acquisition
+
+from Globals import HTMLFile, INSTANCE_HOME
+
+from OFS.Folder import Folder
+
+from Products.exUserFolder.exUserFolder import exUserFolder
+from Products.exUserFolder.Plugins import PluginRegister
+
+class NullPlugin(Folder):
+
+	def __init__(self):
+		pass
+
+	def postInitialisation(self, REQUEST):
+		pass
diff --git a/ZopeProducts/exUserFolder/version.txt b/ZopeProducts/exUserFolder/version.txt
new file mode 100644
index 0000000000000000000000000000000000000000..af0accc33c7f6eb0baa02b3a5f455a5a1f6ec163
--- /dev/null
+++ b/ZopeProducts/exUserFolder/version.txt
@@ -0,0 +1 @@
+exUserFolder-0-50-1
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..97c50056b498c39bdce1f781d45f7bdfa52a6e93
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,75 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+from ZScolar import ZScolar, manage_addZScolarForm, manage_addZScolar
+
+# from ZNotes  import ZNotes, manage_addZNotesForm, manage_addZNotes
+
+from ZScoDoc import ZScoDoc, manage_addZScoDoc
+
+# from sco_zope import *
+# from notes_log import log
+# log.set_log_directory( INSTANCE_HOME + '/log' )
+
+
+__version__ = "1.0.0"
+
+
+def initialize(context):
+    """initialize the Scolar products"""
+    # called at each startup (context is a ProductContext instance, basically useless)
+
+    # --- ZScolars
+    context.registerClass(
+        ZScolar,
+        constructors=(
+            manage_addZScolarForm,  # this is called when someone adds the product
+            manage_addZScolar,
+        ),
+        icon="static/icons/sco_icon.png",
+    )
+
+    # context.registerHelp()
+    # context.registerHelpTitle("ZScolar")
+
+    # --- ZScoDoc
+    context.registerClass(
+        ZScoDoc, constructors=(manage_addZScoDoc,), icon="static/icons/sco_icon.png"
+    )
+
+    # --- ZNotes
+    # context.registerClass(
+    #   ZNotes,
+    #   constructors = (
+    #       manage_addZNotesForm,
+    #       manage_addZNotes
+    #   ),
+    #  icon = 'static/icons/notes_icon.png'
+    # )
+
+    # context.registerHelp()
+    # context.registerHelpTitle("ZNotes")
diff --git a/bonus_sport.py b/bonus_sport.py
new file mode 100644
index 0000000000000000000000000000000000000000..c002ff599299bf57ba84244da89fa80aaf9984c8
--- /dev/null
+++ b/bonus_sport.py
@@ -0,0 +1,422 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+from operator import mul
+import pprint
+
+
+def bonus_iutv(notes_sport, coefs, infos=None):
+    """Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse
+
+    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
+    de l'Université Paris 13 (sports, musique, deuxième langue,
+    culture, etc) non rattachés à une unité d'enseignement. Les points
+    au-dessus de 10 sur 20 obtenus dans chacune des matières
+    optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à
+    la moyenne générale du semestre déjà obtenue par l'étudiant.
+    """
+    sumc = sum(coefs)  # assumes sum. coefs > 0
+    note_sport = sum(map(mul, notes_sport, coefs)) / sumc  # moyenne pondérée
+    bonus = sum([(x - 10) / 20.0 for x in notes_sport if x > 10])
+    return bonus
+
+
+def bonus_iut_stdenis(notes_sport, coefs, infos=None):
+    """Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points.
+    """
+    points = sum([x - 10 for x in notes_sport if x > 10])  # points au dessus de 10
+    bonus = points * 0.05  # ou / 20
+    return min(bonus, 0.5)  # bonus limité à 1/2 point
+
+
+def bonus_colmar(notes_sport, coefs, infos=None):
+    """Calcul bonus modules optionels (sport, culture), règle IUT Colmar.
+
+    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
+    de l'U.H.A.  (sports, musique, deuxième langue, culture, etc) non
+    rattachés à une unité d'enseignement. Les points au-dessus de 10
+    sur 20 obtenus dans chacune des matières optionnelles sont cumulés
+    dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à
+    la moyenne générale du semestre déjà obtenue par l'étudiant.
+    
+    """
+    # les coefs sont ignorés
+    points = sum([x - 10 for x in notes_sport if x > 10])
+    points = min(10, points)  # limite total à 10
+    bonus = points / 20.0  # 5%
+    return bonus
+
+
+def bonus_iutva(notes_sport, coefs, infos=None):
+    """Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray
+    
+    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
+    de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
+    Si la note est >= 10 et < 12, bonus de 0.1 point
+    Si la note est >= 12 et < 16, bonus de 0.2 point
+    Si la note est >= 16, bonus de 0.3 point
+    Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
+    l'étudiant.
+    """
+    sumc = sum(coefs)  # assumes sum. coefs > 0
+    note_sport = sum(map(mul, notes_sport, coefs)) / sumc  # moyenne pondérée
+    if note_sport >= 16.0:
+        return 0.3
+    if note_sport >= 12.0:
+        return 0.2
+    if note_sport >= 10.0:
+        return 0.1
+    return 0
+
+
+# XXX Inutilisé (mai 2020) ? à confirmer avant suppression XXX
+# def bonus_iut1grenoble_v0(notes_sport, coefs, infos=None):
+#     """Calcul bonus sport IUT Grenoble sur la moyenne générale
+#
+#     La note de sport de nos étudiants va de 0 à 5 points.
+#     Chaque point correspond à un % qui augmente la moyenne de chaque UE et la moyenne générale.
+#     Par exemple : note de sport 2/5 : chaque UE sera augmentée de 2%, ainsi que la moyenne générale.
+#
+#     Calcul ici du bonus sur moyenne générale et moyennes d'UE non capitalisées.
+#     """
+#     # les coefs sont ignorés
+#     # notes de 0 à 5
+#     points = sum([x for x in notes_sport])
+#     factor = (points / 4.0) / 100.0
+#     bonus = infos["moy"] * factor
+#     # Modifie les moyennes de toutes les UE:
+#     for ue_id in infos["moy_ues"]:
+#         ue_status = infos["moy_ues"][ue_id]
+#         if ue_status["sum_coefs"] > 0:
+#             # modifie moyenne UE ds semestre courant
+#             ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] * (1.0 + factor)
+#             if not ue_status["is_capitalized"]:
+#                 # si non capitalisee, modifie moyenne prise en compte
+#                 ue_status["moy"] = ue_status["cur_moy_ue"]
+#
+#         # open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' )
+#     return bonus
+
+
+def bonus_iut1grenoble_2017(notes_sport, coefs, infos=None):
+    """Calcul bonus sport IUT Grenoble sur la moyenne générale (version 2017)
+    
+    La note de sport de nos étudiants va de 0 à 5 points. 
+    Chaque point correspond à un % qui augmente la moyenne de chaque UE et la moyenne générale.
+    Par exemple : note de sport 2/5 : la moyenne générale sera augmentée de 2%.
+    
+    Calcul ici du bonus sur moyenne générale
+    """
+    # les coefs sont ignorés
+    # notes de 0 à 5
+    points = sum([x for x in notes_sport])
+    factor = (points / 4.0) / 100.0
+    bonus = infos["moy"] * factor
+
+    return bonus
+
+
+def bonus_lille(notes_sport, coefs, infos=None):
+    """calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq
+
+    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
+    de l'Université Lille 1 (sports,etc) non rattachés à une unité d'enseignement. Les points
+    au-dessus de 10 sur 20 obtenus dans chacune des matières
+    optionnelles sont cumulés et 4% (2% avant aout 2010) de ces points cumulés s'ajoutent à
+    la moyenne générale du semestre déjà obtenue par l'étudiant.
+    """
+    sumc = sum(coefs)  # assumes sum. coefs > 0
+    note_sport = sum(map(mul, notes_sport, coefs)) / sumc  # moyenne pondérée
+    if (
+        infos["sem"]["date_debut_iso"] > "2010-08-01"
+    ):  # changement de regle en aout 2010.
+        return sum([(x - 10) / 25.0 for x in notes_sport if x > 10])
+    return sum([(x - 10) / 50.0 for x in notes_sport if x > 10])
+
+
+# Fonction Le Havre, par Dom. Soud.
+def bonus_iutlh(notes_sport, coefs, infos=None):
+    """Calcul bonus sport IUT du Havre sur moyenne générale et UE
+
+    La note de sport de nos étudiants va de 0 à 20 points. 
+      m2=m1*(1+0.005*((10-N1)+(10-N2))
+   m2 : Nouvelle moyenne de l'unité d'enseignement si note de sport et/ou de langue supérieure à 10
+   m1 : moyenne de l'unité d'enseignement avant bonification
+   N1 : note de sport si supérieure à 10
+   N2 : note de seconde langue si supérieure à 10
+    Par exemple : sport 15/20 et langue 12/20 : chaque UE sera multipliée par 1+0.005*7, ainsi que la moyenne générale.
+    Calcul ici de la moyenne générale et moyennes d'UE non capitalisées.
+    """
+    # les coefs sont ignorés
+    points = sum([x - 10 for x in notes_sport if x > 10])
+    points = min(10, points)  # limite total à 10
+    factor = 1.0 + (0.005 * points)
+    # bonus nul puisque les moyennes sont directement modifiées par factor
+    bonus = 0
+    # Modifie la moyenne générale
+    infos["moy"] = infos["moy"] * factor
+    # Modifie les moyennes de toutes les UE:
+    for ue_id in infos["moy_ues"]:
+        ue_status = infos["moy_ues"][ue_id]
+        if ue_status["sum_coefs"] > 0:
+            # modifie moyenne UE ds semestre courant
+            ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] * factor
+            if not ue_status["is_capitalized"]:
+                # si non capitalisee, modifie moyenne prise en compte
+                ue_status["moy"] = ue_status["cur_moy_ue"]
+
+        # open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' )
+    return bonus
+
+
+# Bonus sport IUT Tours
+def bonus_tours(notes_sport, coefs, infos=None):
+    """Calcul bonus sport & culture IUT Tours sur moyenne generale
+
+    La note de sport & culture de nos etudiants est applique sur la moyenne generale.
+    """
+    return min(1.0, sum(notes_sport))  # bonus maximum de 1 point
+
+
+def bonus_iutr(notes_sport, coefs, infos=None):
+    """Calcul du bonus , regle de l'IUT de Roanne (contribuée par Raphael C., nov 2012)
+
+       Le bonus est compris entre 0 et 0.35 point.
+       cette procédure modifie la moyenne de chaque UE capitalisable.
+
+    """
+    # modifie les moyennes de toutes les UE:
+    # le bonus est le minimum entre 0.35 et la somme de toutes les bonifs
+    bonus = min(0.35, sum([x for x in notes_sport]))
+    for ue_id in infos["moy_ues"]:
+        # open('/tmp/log','a').write( ue_id +  infos['moy_ues'] + '\n\n' )
+        ue_status = infos["moy_ues"][ue_id]
+        if ue_status["sum_coefs"] > 0:
+            # modifie moyenne UE dans semestre courant
+            ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] + bonus
+            if not ue_status["is_capitalized"]:
+                ue_status["moy"] = ue_status["cur_moy_ue"]
+    return bonus
+
+
+def bonus_iutam(notes_sport, coefs, infos=None):
+    """Calcul bonus modules optionels (sport), regle IUT d'Amiens.
+    Les etudiants de l'IUT peuvent suivre des enseignements optionnels.
+    Si la note est de 10.00 a 10.49 -> 0.50% de la moyenne
+    Si la note est de 10.50 a 10.99 -> 0.75%
+    Si la note est de 11.00 a 11.49 -> 1.00%
+    Si la note est de 11.50 a 11.99 -> 1.25%
+    Si la note est de 12.00 a 12.49 -> 1.50%
+    Si la note est de 12.50 a 12.99 -> 1.75%
+    Si la note est de 13.00 a 13.49 -> 2.00%
+    Si la note est de 13.50 a 13.99 -> 2.25%
+    Si la note est de 14.00 a 14.49 -> 2.50%
+    Si la note est de 14.50 a 14.99 -> 2.75%
+    Si la note est de 15.00 a 15.49 -> 3.00%
+    Si la note est de 15.50 a 15.99 -> 3.25%
+    Si la note est de 16.00 a 16.49 -> 3.50%
+    Si la note est de 16.50 a 16.99 -> 3.75%
+    Si la note est de 17.00 a 17.49 -> 4.00%
+    Si la note est de 17.50 a 17.99 -> 4.25%
+    Si la note est de 18.00 a 18.49 -> 4.50%
+    Si la note est de 18.50 a 18.99 -> 4.75%
+    Si la note est de 19.00 a 20.00 -> 5.00%
+    Ce bonus s'ajoute a la moyenne generale du semestre de l'etudiant.
+    """
+    # une seule note
+    note_sport = notes_sport[0]
+    if note_sport < 10.0:
+        return 0
+    prc = min((int(2 * note_sport - 20.0) + 2) * 0.25, 5)
+    bonus = infos["moy"] * prc / 100
+    return bonus
+
+
+def bonus_saint_etienne(notes_sport, coefs, infos=None):
+    """IUT de Saint-Etienne (jan 2014)
+    Nous avons différents types de bonification
+    bonfication Sport / Associations
+    coopératives de département / Bureau Des Étudiants 
+    / engagement citoyen / Langues optionnelles
+    Nous ajoutons sur le bulletin une bonification qui varie entre 0,1 et 0,3 ou 0,35 pour chaque item
+    la bonification totale ne doit pas excéder les 0,6 point.
+    Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
+
+
+    Dans ScoDoc: on a déclarer une UE "sport&culture" dans laquelle on aura des modules
+    pour chaque activité (Sport, Associations, ...)
+    avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la
+    valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale)
+    """
+    bonus = min(0.6, sum([x for x in notes_sport]))  # plafonnement à 0.6 points
+
+    return bonus
+
+
+def bonus_iutTarbes(notes_sport, coefs, infos=None):
+    """Calcul bonus modules optionnels 
+    (sport, Langues, action sociale, Théâtre), règle IUT Tarbes
+    Les coefficients ne sont pas pris en compte, 
+     seule la meilleure note est prise en compte
+    le 1/30ème des points au-dessus de 10 sur 20  est retenu et s'ajoute à
+    la moyenne générale du semestre déjà obtenue par l'étudiant.
+    """
+    bonus = max([(x - 10) / 30.0 for x in notes_sport if x > 10] or [0.0])
+    return bonus
+
+
+def bonus_iutSN(notes_sport, coefs, infos=None):
+    """Calcul bonus sport IUT Saint-Nazaire sur moyenne générale
+
+    La note de sport de nos étudiants va de 0 à 5 points.
+    La note de culture idem,
+    Elles sont cumulables,
+    Chaque point correspond à un % qui augmente la moyenne générale.
+    Par exemple : note de sport 2/5 : la moyenne générale sera augmentée de 2%.
+
+    Calcul ici du bonus sur moyenne générale et moyennes d'UE non capitalisées.
+    """
+    # les coefs sont ignorés
+    # notes de 0 à 5
+    points = sum([x for x in notes_sport])
+    factor = points / 100.0
+    bonus = infos["moy"] * factor
+    return bonus
+
+
+def bonus_iutBordeaux1(notes_sport, coefs, infos=None):
+    """Calcul bonus modules optionels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale et UE
+
+    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
+    de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement.
+    En cas de double activité, c'est la meilleure des 2 notes qui compte.
+    Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
+    qui augmente la moyenne de chaque UE et la moyenne générale.
+    Formule : le % = points>moyenne / 2
+    Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
+
+    Calcul ici du bonus sur moyenne générale et moyennes d'UE non capitalisées.
+    """
+    # open('/tmp/log','a').write( '\n---------------\n' + pprint.pformat(infos) + '\n' )
+    # les coefs sont ignorés
+    # on récupère la note maximum et les points au-dessus de la moyenne
+    sport = max(notes_sport)
+    points = max(0, sport - 10)
+    # on calcule le bonus
+    factor = (points / 2.0) / 100.0
+    bonus = infos["moy"] * factor
+    # Modifie les moyennes de toutes les UE:
+    for ue_id in infos["moy_ues"]:
+        ue_status = infos["moy_ues"][ue_id]
+        if ue_status["sum_coefs"] > 0:
+            # modifie moyenne UE ds semestre courant
+            ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] * (1.0 + factor)
+            if not ue_status["is_capitalized"]:
+                # si non capitalisee, modifie moyenne prise en compte
+                ue_status["moy"] = ue_status["cur_moy_ue"]
+
+        # open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' )
+    return bonus
+
+
+def bonus_iuto(notes_sport, coefs, infos=None):
+    """Calcul bonus modules optionels (sport, culture), règle IUT Orleans
+    * Avant aout 2013
+    Un bonus de 2,5% de la note de sport est accordé à chaque UE sauf
+    les UE de Projet et Stages
+    * Après aout 2013
+    Un bonus de 2,5% de la note de sport est accordé à la moyenne générale
+    """
+    sumc = sum(coefs)  # assumes sum. coefs > 0
+    note_sport = sum(map(mul, notes_sport, coefs)) / sumc  # moyenne pondérée
+    bonus = note_sport * 2.5 / 100
+    if (
+        infos["sem"]["date_debut_iso"] > "2013-08-01"
+    ):  # changement de regle en aout 2013.
+        return bonus
+    coefs = 0
+    coefs_total = 0
+    for ue_id in infos["moy_ues"]:
+        ue_status = infos["moy_ues"][ue_id]
+        coefs_total = coefs_total + ue_status["sum_coefs"]
+        #  Extremement spécifique (et n'est plus utilisé)
+        if ue_status["ue"]["ue_code"] not in {
+            "ORA14",
+            "ORA24",
+            "ORA34",
+            "ORA44",
+            "ORB34",
+            "ORB44",
+            "ORD42",
+            "ORE14",
+            "ORE25",
+            "ORN44",
+            "ORO44",
+            "ORP44",
+            "ORV34",
+            "ORV42",
+            "ORV43",
+        }:
+            if ue_status["sum_coefs"] > 0:
+                coefs = coefs + ue_status["sum_coefs"]
+                # modifie moyenne UE ds semestre courant
+                ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] + bonus
+                if not ue_status["is_capitalized"]:
+                    # si non capitalisee, modifie moyenne prise en compte
+                    ue_status["moy"] = ue_status["cur_moy_ue"]
+    return bonus * coefs / coefs_total
+
+
+def bonus_iutbethune(notes_sport, coefs, infos=None):
+    """Calcul bonus modules optionels (sport), règle IUT Bethune
+
+    Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre.
+    Ce bonus est égal au nombre de points divisé par 200 et multiplié par la
+    moyenne générale du semestre de l'étudiant.
+    """
+    # les coefs sont ignorés
+    points = sum([x - 10 for x in notes_sport if x > 10])
+    points = min(10, points)  # limite total à 10
+    bonus = int(infos["moy"] * points / 2) / 100.0  # moyenne-semestre x points x 0,5%
+    return bonus
+
+
+def bonus_demo(notes_sport, coefs, infos=None):
+    """Fausse fonction "bonus" pour afficher les informations disponibles
+    et aider les développeurs.
+    Les informations sont placées dans le fichier /tmp/scodoc_bonus.log    
+    qui est ECRASE à chaque appel.
+    *** Ne pas utiliser en production !!! ***
+    """
+    f = open("/tmp/scodoc_bonus.log", "w")  # mettre 'a' pour ajouter en fin
+    f.write("\n---------------\n" + pprint.pformat(infos) + "\n")
+    # Statut de chaque UE
+    # for ue_id in infos['moy_ues']:
+    #    ue_status = infos['moy_ues'][ue_id]
+    #   #open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' )
+
+    return 0.0
diff --git a/config/README b/config/README
new file mode 100644
index 0000000000000000000000000000000000000000..7d51341f9dcaa154d2447272c30ca6244314fd06
--- /dev/null
+++ b/config/README
@@ -0,0 +1,56 @@
+
+       CONFIGURATION DE SCODOC
+       -----------------------
+
+Emmanuel Viennet, juin 2008, mar 2017
+
+
+
+0) INSTALL de base:
+- prerequis: apache2, utilisateur www-data
+- detarer ScoDoc.tgz  (== Zope + produits + scodoc)
+
+
+1) Creation de la base utilisateurs (initialement vide)
+
+-------
+
+Sept 2013: ScoDoc 7 : python2.7, Debian 7, Zope 2.13.21
+
+
+Pour la construction de Zope: 
+attention: on veut bénéficier des paquets python Debian !
+donc
+
+apt-get install python-dev 
+apt-get install python-virtualenv
+apt-get install gcc
+virtualenv --system-site-packages /opt/zope213
+cd zope213/
+bin/easy_install -i http://download.zope.org/Zope2/index/2.13.21 Zope2
+# ...long...
+
+bin/easy_install Products.ZSQLMethods
+bin/easy_install ZPsycopgDA
+# Avec Debian 8: probleme install de ZPsycopgDA
+# essai en le copiant de la version Debian 7. Semble ok.
+
+# Problemes persistant avec pydot (voir commentaire dans script install)
+
+# Si besoin, création de l'instance Zope:
+bin/mkzopeinstance -d /opt/inst
+
+(XXX admin / admin)
+
+
+2) Migration bases de données
+Sur ScoDoc6:
+  pg_dumpall > scodoc.dump.txt
+
+
+
+passage en UTF-8
+- sources Python
+- locale systeme (scrip install)
+- creation des bases (users et depts)
+- recodage du dump sql
diff --git a/config/anonymize_db.py b/config/anonymize_db.py
new file mode 100755
index 0000000000000000000000000000000000000000..dd3911ff5403afc48765d0959a341a0c7dd9d245
--- /dev/null
+++ b/config/anonymize_db.py
@@ -0,0 +1,151 @@
+#!/opt/zope213/bin/python
+# -*- coding: utf-8 -*-
+# -*- mode: python -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2019 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
+#
+##############################################################################
+
+
+"""Anonymize une base de données ScoDoc
+
+Runned as "www-data" with scodoc and postgresql up.
+
+E. Viennet, Jan 2019
+"""
+
+import sys, os, traceback, psycopg2
+
+def log(msg):
+    sys.stdout.flush()
+    sys.stderr.write(msg+'\n')
+    sys.stderr.flush()
+
+
+# --- Fonctions d'Anonymisation, en SQL
+
+anonymize_name = "random_text_md5(8)"
+anonymize_date = "'1970-01-01'"
+anonymize_question_str = "'?'"
+anonymize_null = "NULL"
+
+# aggregate_length = lambda column, _: 'length({})'.format(column)
+
+
+# --- Champs à anonymiser (cette configuration pourrait être placé dans un fichier séparé
+#     et le code serait alors générique pour tout base posgresql, aux données de connection
+#     près)
+# On essaie de retirer les données personnelles des étudiants et des entreprises
+# L'identité (login) des enseignants n'est pas modifiée
+#  (on ne sait rien d'autre sur eux dans cette base, et changer le login ets compliqué
+#   car c'est la clé avec SCOUSERS)
+#
+ANONYMIZED_FIELDS = {
+    'identite.nom' : anonymize_name,
+    'identite.prenom' : anonymize_name,
+    'identite.date_naissance' : anonymize_date,
+    'identite.lieu_naissance' : anonymize_question_str,
+    'identite.nationalite' : anonymize_question_str,
+    'identite.foto' : anonymize_null,
+    'identite.code_nip' : anonymize_null,
+    'identite.code_ine' : anonymize_null,
+    'identite.nom_usuel' : anonymize_null,
+
+    'adresse.email' : "'ano@nyme.fr'",
+    'adresse.emailperso' : anonymize_null,
+    'adresse.domicile' : anonymize_null,
+    'adresse.telephone' : anonymize_null,
+    'adresse.telephonemobile' : anonymize_null,
+    'adresse.fax' : anonymize_null,
+
+    'billet_absence.description' : anonymize_null,
+    'etud_annotations.comment' : anonymize_name,
+    
+    'entreprises.nom' : anonymize_name,
+    'entreprises.adresse' : anonymize_null,
+    'entreprises.ville' : anonymize_null,
+    'entreprises.codepostal' : anonymize_null,
+    'entreprises.pays' : anonymize_null,
+    'entreprises.contact_origine' : anonymize_null,
+    'entreprises.secteur' : anonymize_null,
+    'entreprises.note' : anonymize_null,
+    'entreprises.privee' : anonymize_null,
+    'entreprises.localisation' : anonymize_null,
+
+    'entreprise_correspondant.nom' : anonymize_name,
+    'entreprise_correspondant.prenom' : anonymize_name,
+    'entreprise_correspondant.phone1' : anonymize_null,
+    'entreprise_correspondant.phone2' : anonymize_null,
+    'entreprise_correspondant.mobile' : anonymize_null,
+    'entreprise_correspondant.mail1' : anonymize_null,
+    'entreprise_correspondant.mail2' : anonymize_null,
+    'entreprise_correspondant.note' : anonymize_null,
+    'entreprise_correspondant.fax' : anonymize_null,
+
+    'entreprise_contact.description' : anonymize_null,
+    'entreprise_contact.enseignant' : anonymize_null,
+    
+    'notes_appreciations.comment' : anonymize_name,
+    }
+
+def anonymize_column(cursor, tablecolumn):
+    """Anonymise une colonne
+    tablecolumn est de la forme nom_de_table.nom_de_colonne, par exemple "identite.nom"
+    key_name est le nom de la colonne (clé) à utiliser pour certains remplacements
+    (cette clé doit être anonyme et unique). Par exemple, un nom propre pourrait être 
+    remplacé par nom_valeur_de_la_clé.
+    """    
+    table, column = tablecolumn.split('.')
+    anonymization = ANONYMIZED_FIELDS[ tablecolumn ]
+    log('processing {}'.format(tablecolumn))
+    cursor.execute("UPDATE {table} SET {column} = {value};".format(
+        table=table,
+        column=column,
+        value=anonymization(column, key_name) if callable(anonymization) else anonymization
+        ))
+
+def anonymize_db(cursor):
+    """Traite, une à une, les colonnes indiquées dans ANONYMIZED_FIELDS
+    """
+    for tablecolumn in ANONYMIZED_FIELDS:
+        anonymize_column(cursor, tablecolumn)
+
+
+dbname = sys.argv[1]
+
+log('\nAnonymizing database %s' % dbname)
+cnx_string = 'dbname=' + dbname
+try:
+    cnx = psycopg2.connect( cnx_string )
+except:
+    log("\n*** Error: can't connect to database %s ***\n" % dbname)
+    log('connexion string was "%s"' % cnx_string) 
+    traceback.print_exc()
+
+cnx.set_session(autocommit=False)
+cursor = cnx.cursor()
+
+anonymize_db(cursor)
+
+cnx.commit()
+cnx.close()
diff --git a/config/config.sh b/config/config.sh
new file mode 100644
index 0000000000000000000000000000000000000000..4b8368b883c94d7b2bc074badfe51f845f547a01
--- /dev/null
+++ b/config/config.sh
@@ -0,0 +1,61 @@
+
+# Version majeure de Debian (..., 9, 10)
+debian_version=$(cat /etc/debian_version)
+debian_version=${debian_version%%.*}
+
+
+# Fix path
+export PATH="${PATH}":/usr/sbin:/sbin
+
+# ScoDoc: environment variables
+umask 0022
+
+export SCODOC_DIR=${PWD%/*}
+# normalement: /opt/scodoc/Products/ScoDoc
+export SCODOC_VAR_DIR=$(realpath "$SCODOC_DIR/../../var/scodoc")
+#  = /opt/scodoc/var/scodoc
+export SCODOC_VERSION_DIR="${SCODOC_VAR_DIR}/config/version"
+export SCODOC_LOGOS_DIR="${SCODOC_VAR_DIR}/config/logos"
+# Postgresql superuser:
+export POSTGRES_SUPERUSER=postgres
+
+# Postgresql normal user: (by default, same a zope==www-data)
+# IMPORTANT: must match SCO_DEFAULT_SQL_USER defined in sco_utils.py
+export POSTGRES_USER=www-data
+
+# psql command: if various versions installed, force the one we want:
+if [ ${debian_version} = "10" ]
+then
+ PSQL=/usr/lib/postgresql/11/bin/psql
+elif [ ${debian_version} = "9" ]
+then
+ PSQL=/usr/lib/postgresql/9.6/bin/psql
+elif [ ${debian_version} = "8" ] 
+then
+ PSQL=/usr/lib/postgresql/9.4/bin/psql
+elif [ ${debian_version} = "7" ] 
+then
+ PSQL=/usr/lib/postgresql/9.1/bin/psql
+elif [ ${debian_version} = "5" ] 
+ then
+   PSQL=/usr/lib/postgresql/8.3/bin/psql
+ elif [ ${debian_version} = "6" ] 
+ then
+   PSQL=/usr/lib/postgresql/8.4/bin/psql
+ else
+   PSQL=/usr/lib/postgresql/8.1/bin/psql
+fi
+
+
+# tcp port for SQL server (under Debian 4, 5432 or 5433 for 8.1 if 7.4 also installed !)
+# Important note: if changed, you should probably also change it in
+#      sco_utils.py (SCO_DEFAULT_SQL_PORT).
+export POSTGRES_PORT=5432
+
+# Utilise par le script de reset du mot de passe: 
+if [ ${debian_version} -ge "7" ]
+then
+  export ZOPE_VERSION=2.13
+else
+  export ZOPE_VERSION=2.11.0
+fi
diff --git a/config/create_database.sh b/config/create_database.sh
new file mode 100755
index 0000000000000000000000000000000000000000..3485b2164ba412197aa719a81a54cebbbc01608c
--- /dev/null
+++ b/config/create_database.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# Create database for a ScoDoc instance
+# This script must be executed as postgres user
+#
+# $db_name is passed ias an environment variable
+
+source config.sh
+source utils.sh
+
+echo 'Creating postgresql database'
+
+# ---
+echo 'Creating postgresql database ' $db_name
+createdb -E UTF-8  -p $POSTGRES_PORT -O $POSTGRES_USER $db_name
+
diff --git a/config/create_dept.sh b/config/create_dept.sh
new file mode 100755
index 0000000000000000000000000000000000000000..4b39b6283fdc4a7df982b388a6fa5dcfdb98701e
--- /dev/null
+++ b/config/create_dept.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+
+#
+# ScoDoc: creation initiale d'un departement
+#
+# Ce script prend en charge la creation de la base de donnees
+# et doit être lancé par l'utilisateur unix root dans le repertoire .../config
+#                          ^^^^^^^^^^^^^^^^^^^^^
+# E. Viennet, Juin 2008
+#
+
+
+source config.sh
+source utils.sh
+
+check_uid_root $0
+
+
+echo -n "Nom du departement (un mot sans ponctuation, exemple \"Info\"): "
+read DEPT
+
+if [[ ! "$DEPT" =~ ^[A-Za-z0-9]+$ ]]
+then
+ echo 'Nom de departement invalide !'
+ exit 1
+fi
+
+export DEPT
+
+export db_name=SCO$(to_upper "$DEPT")
+
+cfg_pathname="${SCODOC_VAR_DIR}/config/depts/$DEPT".cfg
+
+if [ -e $cfg_pathname ]
+then
+  echo 'Erreur: Il existe deja une configuration pour "'$DEPT'"'
+  exit 1
+fi
+
+# --- Ensure postgres user www-data exists
+init_postgres_user
+
+# -----------------------  Create database
+su -c ./create_database.sh $POSTGRES_SUPERUSER 
+
+# ----------------------- Create tables
+# POSTGRES_USER == regular unix user (www-data)
+su -c ./initialize_database.sh $POSTGRES_USER
+
+# ----------------------- Enregistre fichier config
+echo "dbname="$db_name > $cfg_pathname
+
+# ----------------------- Force mise à jour
+echo -n "Voulez vous mettre a jour ScoDoc (tres recommande) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    (cd "$SCODOC_DIR/config"; ./upgrade.sh)
+fi
+
+# ----------------------- 
+echo
+echo " Departement $DEPT cree"
+echo
+echo " Attention: la base de donnees n'a pas de copies de sauvegarde"
+echo 
+echo " Maintenant, vous pouvez ajouter le departement via l'application web"
+echo " en suivant le lien \"Administration de ScoDoc\" sur la page d'accueil."
+echo
diff --git a/config/create_user_db.sh b/config/create_user_db.sh
new file mode 100755
index 0000000000000000000000000000000000000000..8790cd5d6ccebd524c2224715d8fa3d4eee72897
--- /dev/null
+++ b/config/create_user_db.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+#
+# ScoDoc: creation de la base de donnees d'utilisateurs
+#
+# Ce script prend en charge la creation de la base de donnees
+# et doit �tre lanc� par l'utilisateur unix root dans le repertoire .../config
+#                          ^^^^^^^^^^^^^^^^^^^^^
+# E. Viennet, Juin 2008
+#
+
+source config.sh
+source utils.sh
+
+check_uid_root $0
+
+# --- Ensure postgres user www-data exists
+init_postgres_user
+
+db_name=SCOUSERS
+
+echo 'Creating postgresql database ' $db_name
+
+su -c "createdb -E UTF-8 -O $POSTGRES_USER  -p $POSTGRES_PORT $db_name" $POSTGRES_SUPERUSER 
+
+echo 'Initializing tables in database ' $db_name
+echo su -c "$PSQL -U $POSTGRES_USER -p $POSTGRES_PORT $db_name < $SCODOC_DIR/misc/create_user_table.sql" $POSTGRES_USER
+su -c "$PSQL -U $POSTGRES_USER -p $POSTGRES_PORT  $db_name < $SCODOC_DIR/misc/create_user_table.sql" $POSTGRES_USER
diff --git a/config/default-etapes.txt b/config/default-etapes.txt
new file mode 100644
index 0000000000000000000000000000000000000000..80e037d1a14d665af0bdbd57772b8f140fc0b5f2
--- /dev/null
+++ b/config/default-etapes.txt
@@ -0,0 +1,20 @@
+# Etapes Apogee par defaut: a adapter a votre cas
+# Les etapes sont d'abord demandees au portail 
+# et en cas d'echec lues dans ce fichier
+#
+# E. Viennet, 2007
+
+# Format: Departement : Code_etape_apogee : nom de la formation
+# Le code departement est celui indiqué par la propriété
+# "portal_dept_name" du département.
+
+# Attention: le codage de ce fichier doit etre le même que 
+# celui de ScoDoc (utf-8)
+
+rt : V1RT  : DUT Réseaux et Télécommunications 1
+rt : V2RT2 : DUT Réseaux et Télécommunications 2
+rt : V3ASR3 : Licence Pro R&T ASUR (FC)
+rt : V3ASR2 : Licence Pro R&T ASUR (Apprentissage)
+rt : V3ON  : Licence pro. Electronique, Optique et Nanotechnologies
+gea: V1GE  : DUT Gestion des Entreprises et Administration 1
+gea: V2GE  : DUT Gestion des Entreprises et Administration 2
diff --git a/config/delete_dept.sh b/config/delete_dept.sh
new file mode 100755
index 0000000000000000000000000000000000000000..9a88a70cefc21db25e8c8b4fb644ad532dc23bdb
--- /dev/null
+++ b/config/delete_dept.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+#
+# ScoDoc: suppression d'un departement
+#
+# Ce script supprime la base de donnees ScoDoc d'un departement
+# *** le departement doit au prealable avoir �t� supprime via l'interface web ! ***
+#
+# Ne fonctionne que pour les configurations "standards" (dbname=xxx)
+#
+# Il doit �tre lanc� par l'utilisateur unix root dans le repertoire .../config
+#                          ^^^^^^^^^^^^^^^^^^^^^
+# E. Viennet, Sept 2008
+#
+
+
+source config.sh
+source utils.sh
+
+check_uid_root $0
+
+echo
+echo "Ce script supprime la base de donnees ScoDoc d'un departement"
+echo
+echo "Attention: le departement doit au prealable avoir ete supprime via l'interface web !"
+echo "faites le AVANT d'executer ce script !!!"
+echo
+echo -n "Nom du departement a supprimer (un mot sans ponctuation, exemple \"Info\"): "
+read DEPT
+
+if [[ ! "$DEPT" =~ ^[A-Za-z0-9]+$ ]]
+then
+ echo "Nom de departement invalide !"
+ exit 1
+fi
+
+export DEPT
+
+cfg_pathname="${SCODOC_VAR_DIR}/config/depts/$DEPT".cfg
+
+if [ -e $cfg_pathname ]
+then
+  # arret de ScoDoc
+  /etc/init.d/scodoc stop
+  # suppression de la base postgres
+  db_name=$(cat $cfg_pathname | sed '/^dbname=*/!d; s///;q')
+  echo "suppression de la base postgres $db_name"
+  su -c "dropdb $db_name" $POSTGRES_SUPERUSER || terminate "ne peux supprimer base de donnees $db_name"
+  # suppression du fichier de config
+  /bin/rm -f $cfg_pathname || terminate "ne peux supprimer $cfg_pathname"
+  # relance ScoDoc
+  echo -n "Demarrer le serveur ScoDoc ? (y/n) [n]"
+  read ans
+  if [ "$(norm_ans "$ans")" = 'Y' ]
+  then
+     /etc/init.d/scodoc start
+  fi
+  exit 0
+else
+  echo 'Erreur: pas de configuration trouvee pour "'$DEPT'"'
+  exit 1
+fi
diff --git a/config/diagnostic.sh b/config/diagnostic.sh
new file mode 100755
index 0000000000000000000000000000000000000000..fc0fb959dd0870971675cbf89258a8caa32f0ffd
--- /dev/null
+++ b/config/diagnostic.sh
@@ -0,0 +1,256 @@
+#!/bin/bash
+
+# Rassemble informations sur le systeme et l'installation ScoDoc pour 
+# faciliter le support a distance.
+#
+# Avec option:
+#    -a : sauve aussi les bases de données
+#
+DEST_ADDRESS=emmanuel.viennet@univ-paris13.fr 
+
+INSTANCE_DIR=/opt/scodoc
+
+TMP=/tmp/scodoc-$(date +%F-%s)
+
+DEPTS_TO_SAVE=""
+SAVE_USERS=0
+SEND_BY_MAIL=1
+
+# -------------------------------------
+# Arguments
+# -------------------------------------
+
+function join_by { local IFS="$1"; shift; echo "$*"; }
+
+while getopts ":d:aunh" opt; do
+  case $opt in
+      a)
+	  # Liste des noms des departements, a partir des bases SQL SCO*
+	  DEPTS_TO_SAVE=$( (su postgres -c "psql -l") | grep SCO | grep -v SCOUSERS | awk '{ gsub("SCO", "", $1); print $1}' )
+	  SAVE_USERS=1
+	  ;;
+      u)
+	  SAVE_USERS=1
+	  ;;
+      n)
+	  SEND_BY_MAIL=0
+	  ;;
+      d)
+	  DEPTS_TO_SAVE=$( join_by ' ' $DEPTS_TO_SAVE $OPTARG )
+	  ;;
+      h)
+	  echo "Diagnostic installation ScoDoc"
+	  echo "Rassemble informations sur le systeme et l'installation ScoDoc"
+	  echo "Usage: $0 [-h] [-n] [-a] [-u] [-d dept]"
+	  echo "  -h  cette aide"
+	  echo "  -n  pas d'envoi par mail"
+	  echo "  -a  enregistre aussi toutes les bases de donnees"
+	  echo "  -u  enregistre la base utilisateurs"
+	  echo "  -d dept   enregistre la base du departement dept"
+	  exit 0
+	  ;;
+      \?)
+	  echo "Invalid option: -$OPTARG" >&2
+	  exit 1
+	  ;;
+      :)
+	  echo "Option -$OPTARG requires an argument." >&2
+	  exit 1
+	  ;;
+  esac
+done
+
+
+# -------------------------------------
+# Configuration
+# -------------------------------------
+
+# needed for uuencode
+if [ ! -e /usr/bin/uuencode ]
+then
+   apt-get install sharutils
+fi
+
+mkdir $TMP
+
+# Files to copy:
+FILES="/etc/hosts /etc/debian_version /etc/apt /etc/apache2"
+
+
+echo "ScoDoc diagnostic: informations about your system will be "
+if [ "${SEND_BY_MAIL}" = "1" ]
+then
+    echo "sent to ${DEST_ADDRESS}"
+    echo -n "and "
+fi
+echo "left in ${TMP}"
+
+
+# -------------------------------------
+# Logs
+# -------------------------------------
+
+copy_log() {
+ if [ -e $1 ]
+ then
+   cp $1 $TMP/scodoc_logs/
+ fi
+}
+mkdir $TMP/scodoc_logs/
+copy_log /opt/scodoc/instance/log/event.log
+copy_log /opt/scodoc/instance/log/event.log.1
+copy_log /opt/scodoc/instance/log/notes.log
+copy_log /opt/scodoc/instance/log/notes.log.1
+
+
+# -------------------------------------
+# Linux System Configuration
+# -------------------------------------
+
+iptables -L > $TMP/iptables.out
+ip a > $TMP/ifconfig.out
+ps auxww > $TMP/ps.out
+df -h > $TMP/df.out
+dpkg -l > $TMP/dpkg.lst
+
+(cd /opt/scodoc/instance/Products/ScoDoc; svn status > $TMP/svn.status)
+(cd /opt/scodoc/instance/Products/ScoDoc; svn diff > $TMP/svn.diff)
+
+(cd /opt/scodoc/instance/Products/ScoDoc; svnversion >  $TMP/svn.version)
+ls -laR /opt/scodoc/instance/Products/ScoDoc > $TMP/ls-laR
+
+
+# -------------------------------------
+# Databases configurations
+# -------------------------------------
+(su postgres -c "psql -l") > "${TMP}/psql-l.out"
+for dept in "${INSTANCE_DIR}"/var/scodoc/config/depts/*.cfg
+do
+  cnx=$(cat $dept)
+  (su postgres -c "echo '\dt' | psql -d $cnx") > "${TMP}/psql-$(basename ${dept%%.*}).out"
+done
+
+
+# -------------------------------------
+# Other system configuration files
+# -------------------------------------
+# copy files:
+for f in $FILES 
+do 
+   cp -R $f $TMP
+done
+
+
+# -------------------------------------
+# Optionally save dept(s) database(s)
+# -------------------------------------
+DEPTS_TO_SAVE=$(echo ${DEPTS_TO_SAVE} | tr ' ' '\n' | sort | uniq)
+
+# Dump database of a dept (eg "RT")
+function dump_dept_db {
+    dept=$1
+    DB=$2
+    echo "Dumping database ${DB}..."
+    mkdir -p "${TMP}/depts/${dept}"
+    chmod -R a+wr "${TMP}/depts/"
+    (su postgres -c "pg_dump --create ${DB}") | gzip > "${TMP}/depts/${dept}/${DB}.dump.gz"
+    # may add archives ? (but probably too big)
+}
+
+for dept in ${DEPTS_TO_SAVE}
+do
+    dump_dept_db "${dept}" "SCO${dept}"
+done
+
+
+# -------------------------------------
+# Optionally saveUSERS db
+# -------------------------------------
+if [ "${SAVE_USERS}" = "1" ]
+then
+    dump_dept_db "USERS" "SCOUSERS"
+fi
+
+
+# -------------------------------------
+# Archive all stuff to /tmp
+# -------------------------------------
+
+tar cfz $TMP.tgz $TMP
+
+echo
+echo "Fichier de diagnostic:  $TMP.tgz"
+echo
+
+# If no mail, stop here
+if [ "${SEND_BY_MAIL}" = "0" ]
+then
+    exit 0
+fi
+
+# -------------------------------------
+# Send by e-mail
+# -------------------------------------
+
+
+# Code below found on http://www.zedwood.com/article/103/bash-send-mail-with-an-attachment
+
+#requires: basename,date,md5sum,sed,sendmail,uuencode
+function fappend {
+    echo "$2">>$1;
+}
+YYYYMMDD=`date +%Y%m%d`
+
+# CHANGE THESE
+TOEMAIL=$DEST_ADDRESS
+FREMAIL="scodoc-diagnostic@none.org";
+SUBJECT="ScoDoc diagnostic - $YYYYMMDD";
+MSGBODY="ScoDoc diagnostic sent by diagnostic.sh";
+ATTACHMENT="$TMP.tgz"
+MIMETYPE="application/gnutar" #if not sure, use http://www.webmaster-toolkit.com/mime-types.shtml
+
+
+# DON'T CHANGE ANYTHING BELOW
+TMP="/tmp/tmpfil_123"$RANDOM;
+BOUNDARY=`date +%s|md5sum`
+BOUNDARY=${BOUNDARY:0:32}
+FILENAME=`basename $ATTACHMENT`
+
+rm -rf $TMP;
+cat $ATTACHMENT|uuencode --base64 $FILENAME>$TMP;
+sed -i -e '1,1d' -e '$d' $TMP;#removes first & last lines from $TMP
+DATA=`cat $TMP`
+
+rm -rf $TMP;
+fappend $TMP "From: $FREMAIL";
+fappend $TMP "To: $TOEMAIL";
+fappend $TMP "Reply-To: $FREMAIL";
+fappend $TMP "Subject: $SUBJECT";
+fappend $TMP "Content-Type: multipart/mixed; boundary=\""$BOUNDARY"\"";
+fappend $TMP "";
+fappend $TMP "This is a MIME formatted message.  If you see this text it means that your";
+fappend $TMP "email software does not support MIME formatted messages.";
+fappend $TMP "";
+fappend $TMP "--$BOUNDARY";
+fappend $TMP "Content-Type: text/plain; charset=ISO-8859-1; format=flowed";
+fappend $TMP "Content-Transfer-Encoding: 7bit";
+fappend $TMP "Content-Disposition: inline";
+fappend $TMP "";
+fappend $TMP "$MSGBODY";
+fappend $TMP "";
+fappend $TMP "";
+fappend $TMP "--$BOUNDARY";
+fappend $TMP "Content-Type: $MIMETYPE; name=\"$FILENAME\"";
+fappend $TMP "Content-Transfer-Encoding: base64";
+fappend $TMP "Content-Disposition: attachment; filename=\"$FILENAME\";";
+fappend $TMP "";
+fappend $TMP "$DATA";
+fappend $TMP "";
+fappend $TMP "";
+fappend $TMP "--$BOUNDARY--";
+fappend $TMP "";
+fappend $TMP "";
+#cat $TMP>out.txt
+cat $TMP|sendmail -t -f none@example.com;
+rm $TMP;
+
diff --git a/config/distrib.sh b/config/distrib.sh
new file mode 100644
index 0000000000000000000000000000000000000000..0958fb107d214cf315c7f3696425abb854ff61f3
--- /dev/null
+++ b/config/distrib.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+# Pense bete pour tout nettoyer avant de faire une distribution...
+#
+#
+# E. Viennet, jul 2008
+
+source config.sh
+source utils.sh
+
+if [ "$UID" != "0" ] 
+then
+  echo "Erreur: le script $0 doit etre lance par root"
+  exit 1
+fi
+
+
+echo "Changing to directory " $SCODOC_DIR/config
+cd  $SCODOC_DIR/config
+
+echo "Stopping ScoDoc..."
+/etc/init.d/scodoc stop
+
+# DROITS
+echo -n "Verification des droits: proprietaire www-data ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+  echo 'changing owner to www-data'
+  chown -R www-data.www-data ..
+fi
+
+echo -n 'Suppression des backups des sources (*~) ? (y/n) [y] '
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+   /bin/rm -f ../*~ ../*/*~
+fi
+
+
+# SVN
+echo -n "svn update ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+  echo 'Updating from SVN...'
+  (cd ..; svn update)
+fi
+
+
+# DEPARTEMENTS (maintenant inutile car dans /var)
+echo -n "Supprimer les (anciennes) configs de departements ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+   echo "moving " depts/*.cfg "to /tmp"
+   mv depts/*.cfg /tmp
+fi
+
+# .../var/
+echo -n "Supprimer et recréer .../var (archives, photos, configs, ...) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+  echo "moving ../../../var/scodoc to /tmp"
+  mv ../../../var/scodoc /tmp
+  mkdir -p ../../../var/scodoc/config/depts
+  mkdir ../../../var/scodoc/photos
+  mkdir ../../../var/scodoc/tmp
+  mkdir ../../../var/scodoc/archives
+  chown -R www-data.www-data ../../../var/scodoc/
+fi
+
+# LOGS ZOPE
+echo -n "Effacer les logs de Zope et ScoDoc  ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    (cd ../../../log/; ./purge)
+fi
+
+# IMAGE Data.fs
+echo -n "Recopier le Data.fs original  ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+   echo "moving Data.fs to /tmp"
+   mv ../../../var/Data.fs ../../../var/Data.fs.index /tmp
+   DATAFS=../../../var/Data.fs.ok-to-distrib-545
+   echo "copying $DATAFS to Data.fs"
+   cp -p $DATAFS ../../../var/Data.fs
+fi
+
+#
+echo
+echo "OK, vous pouvez archiver la distribution !"
+echo
diff --git a/config/doc_poursuites_etudes/README.txt b/config/doc_poursuites_etudes/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6e2449c69e80c74b46741ba95345fdafb49dc4e9
--- /dev/null
+++ b/config/doc_poursuites_etudes/README.txt
@@ -0,0 +1,16 @@
+Documents pour l'édition des avis de poursuite d'étude
+
+Ces documents sont joints au zip renvoyé par la fonction de synthèse des avis de poursuite d'étude:
+
+- images (logos)
+- classe LaTeX
+- modele pour frise chronologique en LaTeX
+
+
+Tous les documents et répertoires placés dans le répertoire distrib/ ou local/ seront ajoutés.
+Si un document du même nom est présent dans le répertoire local/, c'est lui qui sera
+utilisé à la place.
+Cela permet des adaptations locales, sans générer de conflits lors des mises à jour de ScoDoc
+(pensez à sauvegarder votre répertoire local/)
+
+
diff --git a/config/doc_poursuites_etudes/distrib/README.txt b/config/doc_poursuites_etudes/distrib/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ea19fd3d9bfe23af27617c0a2a08f447f916f5e6
--- /dev/null
+++ b/config/doc_poursuites_etudes/distrib/README.txt
@@ -0,0 +1,11 @@
+
+        Avis de poursuites d'étude générés par ScoDoc
+
+
+Pour compiler et générer le document pdf (avec les avis de tous les étudiants):
+
+   pdflatex avis.txt
+
+
+
+(si vous souhaitez modifier les fichiers sur le serveur, faites le dans le répertoire ../local/)
diff --git a/config/doc_poursuites_etudes/distrib/avis.tex b/config/doc_poursuites_etudes/distrib/avis.tex
new file mode 100755
index 0000000000000000000000000000000000000000..524c590bd2cd21e0a7f25c8f1662e16335164bc5
--- /dev/null
+++ b/config/doc_poursuites_etudes/distrib/avis.tex
@@ -0,0 +1,29 @@
+%% Original : Cléo BARAS
+%%
+%% Version 1.0
+%%
+%% Ce fichier est distribué par ScoDoc
+%%
+%% Si vous voulez le modifier sur le serveur, copiez le dans
+%% ../local/
+%% et éditez votre copie dans local.
+%%
+
+
+\documentclass[12pt]{avisPE}
+
+\usepackage[T1]{fontenc}
+\usepackage[utf8x]{inputenc}
+\usepackage{frcursive}
+\usepackage{graphics,graphicx}
+\usepackage{tikz}
+\usepackage{colortbl}
+\usepackage[francais]{babel} % Date francisée
+
+
+\begin{document}
+\input{modeles/parcourstimeline.tex}
+
+\input{avis_poursuite}
+
+\end{document}
diff --git a/config/doc_poursuites_etudes/distrib/avisPE.cls b/config/doc_poursuites_etudes/distrib/avisPE.cls
new file mode 100755
index 0000000000000000000000000000000000000000..eacc1b8d6ae4d60987a6234dfbdaa9db540a4cd7
--- /dev/null
+++ b/config/doc_poursuites_etudes/distrib/avisPE.cls
@@ -0,0 +1,126 @@
+%% Original : Cléo BARAS
+%%
+%% Version 1.0
+%%
+%% Ce fichier est distribué par ScoDoc
+%%
+%% Si vous voulez le modifier sur le serveur, copiez le dans
+%% ../local/
+%% et éditez votre copie dans local.
+%%
+
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesClass{avisPE}
+  [2017/05/06 v0.1 Modele avis PE]
+
+\LoadClass[12pt]{article}
+\RequirePackage{ifthen}
+\RequirePackage{array}
+\RequirePackage[paper=a4paper,textwidth=190mm]{geometry}
+\RequirePackage{bookman}
+\RequirePackage{xcolor}
+\RequirePackage{fontawesome}
+
+   %%%%%%%%%%%%%%%%%%%%%%%%%%%
+   % Mise en page par défaut %
+   %%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+\setlength{\textwidth}{190mm}    %% Largeur de la zone texte
+\setlength{\textheight}{300mm}    %% Hauteur de la zone texte
+\setlength{\topmargin}{-25mm}
+\setlength{\evensidemargin}{-15mm}
+\setlength{\oddsidemargin}{-15mm}  
+
+\definecolor{ugared}{rgb}{0.94,0.02,0.02}
+\definecolor{ugadarkgray}{rgb}{0.72,0.72,0.72}
+\definecolor{ugagray}{rgb}{0.42,0.42,0.42}
+\definecolor{ugablack}{rgb}{0,0,0}
+
+\pagestyle{empty} % pour ne pas indiquer de numéro de page...
+
+% On définit les pieds de page comme des \stretch de force 1,
+% soit 4 plus fort que celui qui est en bas du chapeau
+% (Cf. ci-dessous). De cette façon, un avis qui ne remplit pas toute la
+% page a un bel espacement.
+\makeatletter
+\def\@textbottom{\vspace*{\stretch{1}}}
+\makeatother
+
+  
+   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+   %      Paramètres réglables          %
+   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% L'utilisateur peut les redéfinir ceux-ci «\renewcommand» si besoin :
+\newcommand{\espaceInterRubrique}{\medskip\vspace{\stretch{0.1}}}
+
+\newlength{\rubriquedureeparindent} 
+\setlength{\rubriquedureeparindent}{-16pt}
+
+   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+   %     Macros pratiques       %
+   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+\def\er{$^{\hbox{er}}$}
+\def\ere{$^{\hbox{ère}}$}
+\def\eme{$^{\hbox{ème}}$}
+
+
+   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+   % L'environnement "rubrique" %
+   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Usage : \begin{rubrique}[Indentation]{Titre} [...] \end{rubrique}
+% On met le texte qu'on veut à l'intérieur.
+
+\newenvironment{nom}   % "rubrique" prend un seul argument, le titre
+{
+\relax%
+\noindent
+\medskip\bfseries	 \LARGE}{%
+\normalsize\normalfont
+\espaceInterRubrique
+}
+
+\newenvironment{rubrique}[1]   % "rubrique" prend un seul argument, le titre
+{
+\relax%
+\noindent
+\tikz{\fill[ugagray]  (0,0) rectangle (0.5cm,0.5cm); }
+%\faArrowCircleRight
+\medskip\hspace{0.2cm}\bfseries		\color{ugagray} \Large{#1}\par
+\noindent\tikz{\fill[white]  (0,0) rectangle (0.5cm,0.5cm); 
+\draw[ugagray] (0.8cm,1cm)--(19cm,1cm); }\par
+		\color{ugablack}\normalsize\normalfont}{% le contenu
+\espaceInterRubrique
+}
+
+\newenvironment{sousrubrique}[1]   % "rubrique" prend un seul argument, le titre
+{\espaceInterRubrique\relax%
+		\color{ugagray}{\faCaretSquareORight~\textbf{\large{#1}}}
+		\color{ugablack}\normalsize\normalfont\par%
+		\espaceInterRubrique
+		}{}
+
+
+   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+   %    L'en-tête de l'avis     %
+   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% en paramètre: le logo
+\newenvironment{entete}[1]{% 
+	\def\ligne{\rule[2pt]{24pt}{1pt}} % Ligne séparatrice
+	\noindent
+	\begin{minipage}{\linewidth}%\hspace{-2cm}%
+		\begin{minipage}{5.5cm}
+			\includegraphics[height=3.5cm,keepaspectratio=true]{#1}
+		\end{minipage}
+		\hfill
+		\begin{minipage}{13cm}
+		\color{ugagray}
+}
+{		\color{ugablack}
+\relax\vspace*{\fill}
+\end{minipage}
+\end{minipage}
+\vspace{\stretch{0.25}}}
diff --git a/config/doc_poursuites_etudes/distrib/make_avis.bat b/config/doc_poursuites_etudes/distrib/make_avis.bat
new file mode 100755
index 0000000000000000000000000000000000000000..ceb8ac31745a6fbf8d095ae5f12f6057ed221d25
--- /dev/null
+++ b/config/doc_poursuites_etudes/distrib/make_avis.bat
@@ -0,0 +1,3 @@
+
+echo "Compilation des avis latex"
+pdflatex avis.tex
diff --git a/config/doc_poursuites_etudes/distrib/modeles/parcourstimeline.tex b/config/doc_poursuites_etudes/distrib/modeles/parcourstimeline.tex
new file mode 100755
index 0000000000000000000000000000000000000000..fec03f654066731e08c77c9b602c16a791be9e52
--- /dev/null
+++ b/config/doc_poursuites_etudes/distrib/modeles/parcourstimeline.tex
@@ -0,0 +1,67 @@
+%% Original : Cléo BARAS
+%%
+%% Version 1.0
+%%
+%% Ce fichier est distribué par ScoDoc
+%%
+%% Si vous voulez le modifier sur le serveur, copiez le dans
+%% ../local/
+%% et éditez votre copie dans local.
+%%
+
+\definecolor{ugagray}{rgb}{0.42,0.42,0.42}
+
+% define global counters
+\newcounter{yearcount}
+
+
+\newcounter{leftcount}
+
+% env parcourstimeline (d'après copyright (c) 2016 Jan Küster)
+% param 1: année de début 
+% param 2: année de fin
+% param 3: nombre de semestre
+% param 4: largeur totale
+\newenvironment{parcourstimeline}[4]{
+
+	% param 1:	no
+	% param 2:	Nom
+	\newcommand{\parcoursevent}[3] {
+		\pgfmathparse{\largeur*(##1-1)+\largeur/2+ \marge}\let\startexp\pgfmathresult
+
+	\node[draw,color=white,] (Ev##1) at (\startexp,0.7){
+			\textcolor{black}{##2}
+		};
+		\node[draw,color=white,] (Ev##1) at (\startexp,-0.6){
+			\textcolor{black}{##3}
+		};
+}
+
+	%------------------------------------------------------------
+	%	BEGIN
+	%------------------------------------------------------------
+
+	\begin{tikzpicture}
+
+	%calc pas = number of years
+ 	\pgfmathparse{(#4/#3)}\let\pas\pgfmathresult
+			\pgfmathparse{\pas*0.9}\let\largeur\pgfmathresult
+	\pgfmathparse{(#4-\largeur*#3)/2}\let\marge\pgfmathresult
+	\draw[draw=ugagray,line width=2pt] (0,0) -- (#4,0) ;	%the timeline
+
+	%for each year put a horizontal line in place
+	\setcounter{yearcount}{1}
+	\whiledo{\value{yearcount} < #3}{ % \fullrange}{
+		\draw[fill=white,draw=ugagray, line width=2pt]  (\largeur*\value{yearcount}+\marge+0.,0) circle (0.1);
+		\stepcounter{yearcount}
+	}
+
+	%start year
+	\node[draw,color=ugagray,rectangle,line width=2pt,fill=ugagray!20,rounded corners=3pt] (Yd) at(0,0){\textcolor{black}{\textbf{\small#1}}};
+
+	%end year
+	\node[draw,color=ugagray,rectangle,line width=2pt,fill=ugagray!20,rounded corners=3pt] (Yd) at(#4,0){\textcolor{black}{\textbf{\small#2}}};
+	
+
+}%end begin part of newenv
+{\end{tikzpicture}}
\ No newline at end of file
diff --git a/config/doc_poursuites_etudes/distrib/modeles/un_avis.tex b/config/doc_poursuites_etudes/distrib/modeles/un_avis.tex
new file mode 100644
index 0000000000000000000000000000000000000000..05a6320f1f98c48f0a69390cbe8a048c8577559d
--- /dev/null
+++ b/config/doc_poursuites_etudes/distrib/modeles/un_avis.tex
@@ -0,0 +1,164 @@
+%% Original : Cléo BARAS
+%%
+%%   Template LaTeX des avis
+%%
+%%   Version 1.0
+%%
+%% Ce fichier est distribué par ScoDoc
+%%
+%% Si vous voulez le modifier sur le serveur, copiez le dans
+%% ../local/
+%% et éditez votre copie dans local.
+%%
+
+
+% Ce document est n'est pas utilisé si le paramètre eTemplate LaTeX des avis" est rempli.
+
+% ************************************************************
+% En-tête de l'avis 
+% ************************************************************
+\begin{entete}{logos/logo_header}
+		\textbf{\Huge{Avis de Poursuites d'Etudes}} \\
+		\ligne \\
+		\normalsize{Département **DeptFullName**} \\
+		\normalsize{**InstituteName**} \\
+\end{entete}
+
+\begin{nom}
+**sexe** **prenom** **nom** (**age** ans)
+\end{nom}
+
+% ************************************************************
+% Parcours scolaire de l'étudiant
+% ************************************************************
+
+\begin{rubrique}{Parcours scolaire}
+
+% TIMELINE
+**parcourstimeline**
+
+{
+\scriptsize
+\textbf{Guide de lecture} : 
+$\bullet$ Les options de la formation: FI = initiale, UFA = alternance (contrat pro), ENEPS = École nationale de l'enseignement professionnel supérieur
+}
+\end{rubrique}
+
+% ************************************************************
+% Bilan scolaire
+% ************************************************************
+
+\begin{rubrique}{Bilan scolaire synthétique}
+
+
+\begin{tabular}{|p{0.08\textwidth}|p{0.11\textwidth}|*{8}{p{0.07\textwidth}|}} 
+% Titres des colonnes
+\hline     & \textbf{Resultats} & \multicolumn{4}{c|}{\textbf{Dans le groupe}\textdagger} 
+																& \multicolumn{4}{c|}{\textbf{Dans la promo}\textdagger}  \\
+					 &                    & Class.  & Min    & Moy    & Max 
+							                  & Class. & Min & Moy & Max \\
+\hline
+\hline  S1 & **S1:groupe:dut:note** & **S1:groupe:dut:rang** 
+																		& {\scriptsize **S1:groupe:dut:min**}
+																		& {\scriptsize **S1:groupe:dut:moy**}
+																		& {\scriptsize **S1:groupe:dut:max**}
+																		& **S1:promo:dut:rang** 
+																		& {\scriptsize **S1:promo:dut:min**}
+																		& {\scriptsize **S1:promo:dut:moy**}
+																		& {\scriptsize **S1:promo:dut:max**}  \\
+\hline  S2 & **S2:groupe:dut:note** & **S2:groupe:dut:rang** 
+																		& {\scriptsize **S2:groupe:dut:min**}
+																		& {\scriptsize **S2:groupe:dut:moy**}
+																		& {\scriptsize **S2:groupe:dut:max**}
+																		& **S2:promo:dut:rang** 
+																		& {\scriptsize **S2:promo:dut:min**}
+																		& {\scriptsize **S2:promo:dut:moy**}
+																		& {\scriptsize **S2:promo:dut:max**}  \\
+\hline
+\hline  \cellcolor{black!20} 1A & **1A:groupe:dut:note** & **1A:groupe:dut:rang** 
+																		& {\scriptsize **1A:groupe:dut:min**}
+																		& {\scriptsize **1A:groupe:dut:moy**}
+																		& {\scriptsize **1A:groupe:dut:max**}
+																		& **1A:promo:dut:rang** 
+																		& {\scriptsize **1A:promo:dut:min**}
+																		& {\scriptsize **1A:promo:dut:moy**}
+																		& {\scriptsize **1A:promo:dut:max**}  \\
+\hline
+\hline  S3 & **S3:groupe:dut:note** & **S3:groupe:dut:rang** 
+																		& {\scriptsize **S3:groupe:dut:min**}
+																		& {\scriptsize **S3:groupe:dut:moy**}
+																		& {\scriptsize **S3:groupe:dut:max**}
+																		& **S3:promo:dut:rang** 
+																		& {\scriptsize **S3:promo:dut:min**}
+																		& {\scriptsize **S3:promo:dut:moy**}
+																		& {\scriptsize **S3:promo:dut:max**}  \\
+\hline  S4* & **S4:groupe:dut:note** & **S4:groupe:dut:rang** 
+																		& {\scriptsize **S4:groupe:dut:min**}
+																		& {\scriptsize **S4:groupe:dut:moy**}
+																		& {\scriptsize **S4:groupe:dut:max**}
+																		& **S4:promo:dut:rang** 
+																		& {\scriptsize **S4:promo:dut:min**}
+																		& {\scriptsize **S4:promo:dut:moy**}
+																		& {\scriptsize **S4:promo:dut:max**}  \\
+\hline
+\hline  \cellcolor{black!20} 2A* & **2A:groupe:dut:note** & **2A:groupe:dut:rang** 
+																		& {\scriptsize **2A:groupe:dut:min**}
+																		& {\scriptsize **2A:groupe:dut:moy**}
+																		& {\scriptsize **2A:groupe:dut:max**}
+																		& **2A:promo:dut:rang** 
+																		& {\scriptsize **2A:promo:dut:min**}
+																		& {\scriptsize **2A:promo:dut:moy**}
+																		& {\scriptsize **2A:promo:dut:max**}  \\
+\hline
+\hline  DUT* & **4S:groupe:dut:note** & **4S:groupe:dut:rang** 
+																		& {\scriptsize **4S:groupe:dut:min**}
+																		& {\scriptsize **4S:groupe:dut:moy**}
+																		& {\scriptsize **4S:groupe:dut:max**}
+																		& **4S:promo:dut:rang** 
+																		& {\scriptsize **4S:promo:dut:min**}
+																		& {\scriptsize **4S:promo:dut:moy**}
+																		& {\scriptsize **4S:promo:dut:max**}  \\
+\hline
+\end{tabular}
+
+\begin{minipage}{\textwidth}
+\scriptsize 
+\textbf{Guide de lecture} : * Sur notes disponibles ; \textdagger~Le groupe est l'ensemble des étudiants ayant suivi le même semestre que **prenom** **nom** alors que la promo est l'ensemble des étudiants recevant le diplôme de DUT à la même date que **prenom** **nom** (ayant donc des parcours potentiellement différents)
+\end{minipage}
+
+\end{rubrique}
+
+% ************************************************************
+% Avis de poursuite d'études manuscrit ou saisi dans les 
+% annotations de l'étudiant 
+% ************************************************************
+
+\begin{rubrique}{Avis du jury de Poursuites d'Etudes}
+**annotation**
+\end{rubrique}
+
+\newpage % Saut de page
+% ------------------------------------------------------------
+
+% ************************************************************
+% Bilan automatisé par matières
+% ************************************************************
+
+\begin{rubrique}{Bilan par matières}
+**bilanParTag**
+\end{rubrique}
+
+% ************************************************************
+% Signature 
+% ************************************************************
+
+\begin{rubrique}{Cachet de l'établissement}
+\begin{flushright}
+Le~\today,\\
+**NomResponsablePE**~\\
+\textit{Responsable des Poursuites d'Études du département **DeptName**}
+\end{flushright}
+\vspace{0.5cm}
+\end{rubrique}
+
+% Le pied de page (indiqué dans les paramètres ScoDoc) vient après cette ligne
diff --git a/config/doc_poursuites_etudes/distrib/un_footer.tex b/config/doc_poursuites_etudes/distrib/un_footer.tex
new file mode 100644
index 0000000000000000000000000000000000000000..ba4ff1626b6f07187bbcf03e2126bc6d91e59e37
--- /dev/null
+++ b/config/doc_poursuites_etudes/distrib/un_footer.tex
@@ -0,0 +1,7 @@
+\begin{minipage}{\textwidth} \scriptsize 
+%IUT exemple - Dpt RT, 3 rue xxx, France
+
+% \textbf{Secrétariat Poursuite d'Etudes}: Xxx Yyy, Tel 01.02.03.04.05, xxx.yyy@example.com
+
+% \textbf{Responsables Poursuite d'Etudes RT}: Zzz Wwww, Tel 01.02.03.04.05, zzz.www@example.com
+\end{minipage}
\ No newline at end of file
diff --git a/config/etablissements.csv b/config/etablissements.csv
new file mode 100644
index 0000000000000000000000000000000000000000..8cd2ceaf4a08fd6ccb6f419e4061259e5a66b0c0
--- /dev/null
+++ b/config/etablissements.csv
@@ -0,0 +1,5317 @@
+COD_ETB;LIB_OFF_ETB;LIB_AD1_ETB;COD_POS_ADR_ETB;LIB_COM;LAT;LNG
+0400786M;Afasec Ecole Des Courses Ctre Des Lads Et Jockeys;Hippodrome 164 Rue Geroges Pela;40000;Mont De Marsan;43.893485;-0.499782
+0753239N;Agence Nat Valorisation Recher;43 Rue De Caumartin;75009;Paris 09;48.8727925;2.3281692
+0290324V;Agrotech Formation;Le Cleusmeur;29260;Lesneven;48.5782643;-4.3214093
+0410629L;Annexe Du Legta De Vendome Lycée Horticole De Blois;5 7 R Des Grands Champs;41029;Blois;47.5660138;1.3272299
+0910831J;Annexe Ecole Agricole Privée St Nicolas;10 Avenue Division Leclerc;91430;Igny;48.7405146;2.229604
+0602005D;Annexe Lycée Agricole;40 Rue Villebois Mareuil;60000;Beauvais;49.4368684;2.0892144
+0593462T;Annexe Lycée G. Et T. Agricole Legta Du Nord;52 Rue Jean-baptiste Lebas;59177;Sains Du Nord;50.0905687;4.0044665
+0595540B;Annexe Lycée G. Et T. Agricole Legta Du Nord;17 Rue Des Tilleuls;59530;Le Quesnoy;50.245023;3.6509335
+0422266K;Ant Du Lap E Gautier De Nandax;219 Route De Fleury;42190;Charlieu;46.1618988;4.1710338
+0421168S;Ant Du Legta De Precieux;Le Creux Du Balay;42660;St Genest Malifaux;45.3482709;4.431769
+0421858S;Antenne Du Legta Roanne Cherve;37 Rue De La Republique;42440;Noiretable;45.8193741;3.764883
+0011056T;Antenne Du Lppr De L'ain;160 Rue De La Poype;01330;Villars Les Dombes;46.0035471;5.0313797
+0772151V;Ass Les Sinoplies Ctre De Formation;12 Rue Joseph Bodin Boismortier;77680;Roissy En Brie;48.8016537;2.6384353
+0410980T;C F P A;Domaine Des Frileuses;41150;Chaumont Sur Loire;47.480012;1.182876
+0133440U;C.n.r.s.;31 Chemin Joseph Aiguier;13009;Marseille 09;43.25633;5.4049604
+0130231F;Centre D Enseignement Et De Recherche De L Ensam;2 Cours Des Arts Et Metiers;13617;Aix En Provence;43.5302287;5.454456
+0490067G;Centre D Enseignement Et De Recherche De L Ensam D Angers;2 Bd Du Ronceray;49035;Angers;47.4751627;-0.5593541
+0761366X;Centre De Formation Le Havre Formation Entreprise;10 R Mal Delattre De Tassigny;76600;Le Havre;49.4914767;0.1139103
+0831023L;Centre De Formation Technique Drh De La Deleg.gener.armement;Rte Tour Royale Le Mourillon;83000;Toulon;43.1200697;5.9383291
+0831024M;Centre De Formation Technique Drh De La Deleg.gener.armement;Rte Tour Royale Le Mourillon;83000;Toulon;43.1200697;5.9383291
+0312020C;Centre D'etudes Superieures Industrielles Midi-pyrenees;Rue Magellan Innopole;31315;Labege;43.530582;1.529754
+0291758D;Centre D'instruction Naval Ecole De Maistrance;;29240;Brest;48.4472219;-4.421667
+0332373A;Centre Enseignement Medoc Hauteville;1 Impasse Lamire Hauteville;33250;Pauillac;45.1985079;-0.753281
+0831521C;Centre Etud Sup Techn Indust;Pl G.pompidou Qua Mayol;83000;Toulon;43.1200697;5.9383291
+0451467A;Centre Nat Rech Scientifique;3e Av Recherche Scientifique;45071;Orleans;47.8396941;1.9431002
+0451502N;Centre Nat Rech Scientifique;3e Av Recherche Scientifique;45071;Orleans;47.8396941;1.9431002
+0341860M;Centre National De La Recherche Scientif;1919 Route De Mende;34093;Montpellier;43.6388093;3.8653614
+0383049H;Centre National De La Recherche Scientif;;38042;Grenoble;45.195057;5.732322
+0753639Y;Centre National De La Recherche Scientif;15 Quai Anatole France;75007;Paris 07;48.86134;2.3230543
+0251189G;Centre National De Recherche;32 Avenue De L Observatoire;25030;Besancon;47.2478005;5.9896566
+0802092P;Centre Regional De Formation Aux Metiers Du Football;Rue Du Chapitre;80016;Amiens;49.8922653;2.2592521
+0171423E;Centre Sup Technique Privé Dietetique Esthetique Optique;5 Ter Avenue Kennedy;17044;La Rochelle;46.1680031;-1.1851484
+0332984P;Cesi Aquitaine;60 Rue De Maurian;33290;Blanquefort;44.9197812;-0.630999
+0542260N;Cesi Nancy;2 Bis Rue De La Credence;54600;Villers Les Nancy;48.6552886;6.1174951
+0622384E;Cesi Nord;7 Rue Diderot;62000;Arras;50.2971954;2.7335494
+0922455U;Cesi Paris;116 Avenue Aristide Briand;92220;Bagneux;48.7985634;2.322656
+0691696U;Cesi Rhone Alpes Auvergne;19 Avenue Guy De Collongue;69131;Dardilly;45.7832503;4.7658188
+0762969P;Cesi Rouen;Parc Vatine 1 Rue Marconi;76130;Mont St Aignan;49.459778;1.079798
+0442292C;Cesi St Nazaire;Boulevard De L Universite;44603;St Nazaire;47.253238;-2.264146
+0341564R;Cnearc- Eitarc Montpellier;1101 Avenue Agropolis;34033;Montpellier;43.6477471;3.868611
+0352342F;Cnrs;74e Rue De Paris Ctre Oberthur;35000;Rennes;48.1173421;-1.7075198
+0332865K;Cnrs Et Organisme Associe;Esplanade Des Arts Metiers;33405;Talence;44.8052072;-0.6012446
+0753480A;College De France;11 Pl Marcelin Berthelot;75231;Paris 05;48.8493963;2.3447743
+9840018X;College Privé College Pomare Iv;Papeete;98714;Papeete;-17.535022;-149.569594
+0672866B;Compagnie De Formation Pigier;15 Rue Des Magasins;67000;Strasbourg;48.5900534;7.7400921
+0753471R;Conservatoire National Des Arts Et Métiers (paris);292,rue Saint Martin;75141;Paris 03;48.8667317;2.3542533
+0312733C;Cours Assistance Toulouse;269 Route De Narbonne;31400;Toulouse;43.5536414;1.4680912
+0922297X;Cours Complementaire Privé Assurmath;44 Avenue Maurice Labrousse;92160;Antony;48.7555554;2.2959254
+0831447X;Cours Privé Cours Sigma;Chemin De La Buge;83110;Sanary Sur Mer;43.1207576;5.8079614
+0211551X;Cours Privé Saint Dominique;Rue De Velard;21320;Pouilly En Auxois;47.266633;4.5539379
+0312150U;Cours Privé Voltaire;57 Bd Armand Duportal;31000;Toulouse;43.6095732;1.4347543
+0411021M;Cours Promotion Sociale Coiffure;5 Rue De La Picardiere;41000;Blois;47.5884358;1.2950781
+0541559B;Ctr.form.prof.agricole Jeunes;3 Rue Drouas;54200;Toul;48.6725691;5.8924462
+0291759E;Ctre De Formation De Brest;Porte De Carpon;29240;Brest;48.390394;-4.486076
+0311230U;Ctre D'ecologie;29 Rue J Marvig;31055;Toulouse;43.5779576;1.4639799
+0930604B;Ctre Etud Sup Tech Industriel;3 Rue Fernand Hainaut;93407;St Ouen;48.9192718;2.3327857
+0751700R;Ctre Nat Exploitation Oceans;39 Avenue D Iena;75116;Paris 16;48.8687074;2.2955003
+0312339Z;Ctre Nat Rech Scientifique;16 Avenue Edouard Belin;31054;Toulouse;43.5631811;1.480326
+0631814E;Ctre Nat.rech.scientifique;34 Avenue Carnot;63000;Clermont Ferrand;45.7762666;3.0916043
+0110942P;Ctre Polyvalent Formation Prof;La Rouatiere;11400;Souilhanels;43.355982;1.902977
+0941958T;Deleg Regionale;27 Rue Paul Bert;94200;Ivry Sur Seine;48.8181225;2.3709464
+0941957S;Deleg Regionale Cnrs;Tour Europa 126 Belle Epine;94320;Thiais;48.760344;2.387405
+0672674T;Delegation Regionale Cnrs;23 Rue Du Loess;67037;Strasbourg;48.6042226;7.7115393
+0542276F;Delegation Regionale Du Cnrs;39 Rue Gambetta;54000;Nancy;48.691842;6.1794993
+0595987M;Delegation Regionale Du Cnrs;2 Rue Des Cannonniers;59046;Lille;50.6401336;3.0691392
+0411013D;E T I C Ecole Des Tech Communication;2 Rue Jehan De Saveuse;41000;Blois;47.5882653;1.3381421
+0611020C;E.s.a.t.;;61210;Giel Courteilles;48.7645229;-0.196656
+0951623Y;Ec Inter Sc Trait Information;Avenue Du Parc;95011;Cergy;49.0337852;2.072182
+0180910S;Ec Nat Sup Ingenieurs Bourges;10 Boulevard Lahitolle;18020;Bourges;47.0830802;2.417787
+0330203S;Ec Nle Ing Des Travaux Agricol;1 Cours Du Gl De Gaulle;33175;Gradignan;44.774267;-0.618945
+0641923T;Ec Sup Des Techno Indust Avanc;;64102;Bayonne;43.493181;-1.4749344
+0801911T;Ec Sup Ing Electr Et Electrote;14 Quai De La Somme;80083;Amiens;49.8996519;2.2914433
+0142182W;Ec Sup Ing Travaux Constr Caen;1 Rue Pierre Et Marie Curie;14610;Epron;49.2154076;-0.3662197
+0590353N;Ec Sup Tech Indust Textiles;52 Allee Lakanal;59654;Villeneuve D Ascq;50.6409021;3.1377931
+0332880B;Ec Tech Pr Esthetique Cosmet. Cecile Briat;21 Cours Marechal Foch;33000;Bordeaux;44.8470956;-0.575386
+0060693W;Ec.secondaire Et Techn.privee Cours Cannois-guy Furet;134 Boulevard De La Republique;06400;Cannes;43.5631735;7.0210192
+0782128P;Ec.secondaire Et Techn.privee Cours Du Prieure;27 Rue Diderot;78100;St Germain En Laye;48.8923567;2.0910716
+0730768N;Ec.secondaire Et Techn.privee D'altitude Le Val D'arly;Le Val D'arly;73590;St Nicolas La Chapelle;45.808921;6.501631
+0781947T;Ec.secondaire Et Techn.privee Foot;Domaine De Montjoye;78120;Clairefontaine En Yvelines;48.61237;1.908489
+0920925F;Ec.secondaire Et Techn.privee Marymount School;72 Boulevard De La Saussaye;92200;Neuilly Sur Seine;48.8928396;2.2700496
+0941954N;Ec.sup.ing.trav.const.;28 Ave Du President Wilson;94234;Cachan;48.7934331;2.3305073
+0332911K;Ec.tech.pr.coiffure Ec.tech.pr.a.berge;8 Place Tartas;33000;Bordeaux;44.8408689;-0.590832
+0952044F;Ecole 2d Degre General Privée;9 Bis Rue Saint Flaive;95120;Ermont;48.9881778;2.2598042
+0061991G;Ecole 2d Degre General Privée Alliance/apeda;22 Rue Michelet;06100;Nice;43.714606;7.2590664
+0752180M;Ecole 2d Degre General Privée Beausejour;23 Boulevard Beausejour;75016;Paris 16;48.8574466;2.2702555
+0952134D;Ecole 2d Degre General Privée Beith Yaacov;14 Avenue Charles Peguy;95200;Sarcelles;48.9811624;2.3754349
+0694066V;Ecole 2d Degre General Privée Beth Hanna;293 Rue Francis De Pressense;69100;Villeurbanne;45.7681543;4.8969586
+0952126V;Ecole 2d Degre General Privée Beth Israel;9 Ruelle Des Jardins;95360;Montmagny;48.9769243;2.3448886
+0132965C;Ecole 2d Degre General Privée Beth-myriam;60 Che Vallon De Toulouse;13010;Marseille 10;43.2664233;5.4263163
+0881749Y;Ecole 2d Degre General Privée Bienheureux Frassati;50 Rue Monseigneur Rodhain;88800;Mandres Sur Vair;48.2234792;5.891083
+0754083F;Ecole 2d Degre General Privée Carnot;98 Rue Pierre Demours;75017;Paris 17;48.8835651;2.2995698
+9720871P;Ecole 2d Degre General Privée Centre Saint Raphael;Quartier Bel Air;97233;Schoelcher;0.0;0.0
+0753829E;Ecole 2d Degre General Privée Claude Bernard;34 Rue De La Clef;75005;Paris 05;48.8416993;2.3526449
+0754672W;Ecole 2d Degre General Privée Colbert;5 Rue De La Sante;75013;Paris 13;48.8378098;2.342051
+0940872M;Ecole 2d Degre General Privée Cours Henri Iv;6 Avenue Du Reservoir;94100;St Maur Des Fosses;48.8081416;2.4967768
+0391047B;Ecole 2d Degre General Privée Cours N.d. De L'annonciation;Chateau;39270;Cressia;46.5223129;5.483452
+0690557F;Ecole 2d Degre General Privée Cours Pascal;21 Rue Longue;69001;Lyon 01;45.7654048;4.8337143
+0754971W;Ecole 2d Degre General Privée Cours Privé Progress;41 Rue De Turenne;75003;Paris 03;48.8571717;2.364311
+0342019K;Ecole 2d Degre General Privée Ec Pr Bilingue Internationale;Le Chateau Domaine De Massane;34670;Baillargues;43.650414;4.003782
+0753857K;Ecole 2d Degre General Privée Ecole Du Champ De Mars;87 Avenue De La Bourdonnais;75007;Paris 07;48.8559885;2.3026302
+0133785U;Ecole 2d Degre General Privée Fides;6 Cours Sexctius;13100;Aix En Provence;43.5275763;5.4433404
+0753859M;Ecole 2d Degre General Privée Fides;10 Avenue De La Bourdonnais;75007;Paris 07;48.8600406;2.2962319
+0492344G;Ecole 2d Degre General Privée Foyer Saint Thomas;Couvent Haye Aux Bonshommes;49240;Avrille;47.505335;-0.590629
+0651036Y;Ecole 2d Degre General Privée Gambetta;2 Rue De L'harmonie;65000;Tarbes;43.232008;0.0723895
+0755193M;Ecole 2d Degre General Privée Gaston Tenoudji;2 Rue Emile Borel;75017;Paris 17;48.8988905;2.3223466
+0752456M;Ecole 2d Degre General Privée Geoffroy Saint-hilaire;24 Rue Charcot;75013;Paris 13;48.8308116;2.3712295
+0753870Z;Ecole 2d Degre General Privée Hattemer;52 Rue De Londres;75008;Paris 08;48.8784036;2.3254588
+0430967T;Ecole 2d Degre General Privée Institut Alcuin;;43270;Ceaux D Allegre;45.181719;3.746936
+0611249B;Ecole 2d Degre General Privée Institut Croix Des Vents;55 Rue D Argentre;61500;Sees;48.6058338;0.1799387
+0940873N;Ecole 2d Degre General Privée Institut Francais De Grignon;19 Rue Jean-francois Marmontel;94320;Thiais;48.7530756;2.3962989
+0490950S;Ecole 2d Degre General Privée Institution Bois Robert;;49370;Becon Les Granits;47.503413;-0.80139
+0752803P;Ecole 2d Degre General Privée International School Of Paris;6 Rue Beethoven;75016;Paris 16;48.8584557;2.2873279
+0062013F;Ecole 2d Degre General Privée International School Of Sophia;60 Rue Du Vallon;06560;Valbonne;43.6263899;7.0532296
+0753377N;Ecole 2d Degre General Privée Ipecom Prepa Sciences;8 Rue Benjamin Godard;75016;Paris 16;48.8664881;2.275755
+0755278E;Ecole 2d Degre General Privée Jcours Cours Privés;6 Rue Mayran;75009;Paris 09;48.8772948;2.3454524
+0312170R;Ecole 2d Degre General Privée Jean-jacques Rousseau;22 Rue Ingres;31000;Toulouse;43.614223;1.4426803
+0940890G;Ecole 2d Degre General Privée Jeanne D'albret;3 Place Moreau David;94120;Fontenay Sous Bois;48.8431774;2.4652823
+0312527D;Ecole 2d Degre General Privée La Clairiere;8 Impasse Marestan;31100;Toulouse;43.5649875;1.4068322
+0320682T;Ecole 2d Degre General Privée Le Carouet;;32230;Monlezun;43.500014;0.213362
+0753911U;Ecole 2d Degre General Privée L'ecole;24 R Ingenieur Robert Keller;75015;Paris 15;48.8472783;2.2838185
+0750026W;Ecole 2d Degre General Privée L'enseign Par Petits Groupes;17 Avenue Felix Faure;75015;Paris 15;48.8421981;2.2908869
+0672863Y;Ecole 2d Degre General Privée Michael;2c Rue Du Schnokeloch;67200;Strasbourg;48.5784653;7.7138219
+0753895B;Ecole 2d Degre General Privée Moliere;2 Boulevard Soult;75012;Paris 12;48.8360654;2.4076243
+0753845X;Ecole 2d Degre General Privée Montaigne;1 Rue Dupin;75006;Paris 06;48.8502773;2.324821
+0693316E;Ecole 2d Degre General Privée Multilingue Ombrosa;95 Quai Clemenceau;69300;Caluire Et Cuire;45.807247;4.8438576
+0753060U;Ecole 2d Degre General Privée Ohr Joseph;29 Rue De Thionville;75019;Paris 19;48.8904226;2.3857306
+0753146M;Ecole 2d Degre General Privée Prepasup;38 Rue Des Blancs Manteaux;75004;Paris 04;48.8597021;2.3551971
+0753818T;Ecole 2d Degre General Privée Rene Reaumur;81 Rue Reaumur;75002;Paris 02;48.866918;2.3486024
+0030906U;Ecole 2d Degre General Privée Rudolf Steiner;Chateau De La Mhotte;03210;St Menoux;46.572769;3.115683
+0595489W;Ecole 2d Degre General Privée Saint Benoit;116 Boulevard D'armentieres;59100;Roubaix;50.7010617;3.1628397
+0142219L;Ecole 2d Degre General Privée Sainte Catherine De Sienne;;14740;St Manvieu Norrey;49.180678;-0.501961
+0753109X;Ecole 2d Degre General Privée Saint-john Perse;14 Rue Cavallotti;75018;Paris 18;48.8866068;2.327952
+0753376M;Ecole 2d Degre General Privée Saint-john Perse;3 Rue De L'eure;75014;Paris 14;48.8324435;2.3221673
+0942025R;Ecole 2d Degre General Privée Sophia;5 Rue Colonel P.brossolette;94480;Ablon Sur Seine;48.7249353;2.4195636
+0753894A;Ecole 2d Degre General Privée Spinoza;22 Rue Du Sergent Bauchat;75012;Paris 12;48.845549;2.3930299
+0861216E;Ecole 2d Degre General Privée St Thomas D Aquin;Le Parc;86700;Romagne;46.290382;0.3073
+0441850X;Ecole 2d Degre General Privée Ste Catherine De Sienne;32 Rue De La Bastille;44000;Nantes;47.2185622;-1.5664552
+0753878H;Ecole 2d Degre General Privée Verlaine;37 Rue Des Martyrs;75009;Paris 09;48.879285;2.3398549
+0754676A;Ecole 2d Degre General Privée Yechiva Chaare Thora;3 5 Rue Henri Turot;75019;Paris 19;48.876668;2.3725952
+0851557F;Ecole 2d Degre International Chavagnes;96 Rue Du Calvaire;85250;Chavagnes En Paillers;46.8935629;-1.2523775
+0261430N;Ecole 2d Degre Polyval Privée Formacom;3 Place Marx Dormoy;26200;Montelimar;44.5549839;4.7495125
+0450771U;Ecole 2d Degre Polyval Privée Formation Entreprise - Cecam;28 Rue Du Fbg De Bourgogne;45000;Orleans;47.9011332;1.9212467
+0731504N;Ecole 2d Degre Polyval Privée Univeria;725 Faubourg Montmelian;73000;Chambery;45.5665104;5.9347242
+0312579K;Ecole 2d Degre Prof Privée Dynameca;75 Avenue De Grande Bretagne;31300;Toulouse;43.5999437;1.4151779
+0171382K;Ecole 2d Degre Prof Privée Esthetique Coiffure De Sainton;4 Rue De La Comedie;17100;Saintes;45.7454397;-0.6320982
+0171434S;Ecole 2d Degre Prof Privée Inst Reg Techn Image Et Son;25 Avenue Dassault;17304;Rochefort;45.945147;-0.9578502
+0171416X;Ecole 2d Degre Prof Privée J. Eudes;8 Boul. Du Commandant Charcot;17443;Aytre;46.1351019;-1.1261536
+0861245L;Ecole 2d Degre Prof Privée Skhole D'art;2 Et 4 Place Jean De Berry;86000;Poitiers;46.5903977;0.3396993
+0141992P;Ecole 2d Degre Prof. Privée Arcade Caen;9 Rue Du Colonel Remy;14000;Caen;49.202484;-0.3878073
+0593022P;Ecole 2d Degre Prof. Privée Coupe Couture;38 Rue De Roubaix;59800;Lille;50.6388879;3.0697772
+0251070C;Ecole 2d Degre Prof. Privée Cours Hotelier De Besancon;98 Grande Rue;25000;Besancon;47.2357282;6.0272858
+0593076Y;Ecole 2d Degre Prof. Privée Cte Formation Perfectionnement;Rue Des Cligneux;59330;Hautmont;50.24159;3.895865
+0251674J;Ecole 2d Degre Prof. Privée Ecole De Coiffure Cordier;11 Bis Rue Nicolas Bruand;25000;Besancon;47.2493423;6.0227103
+0900039D;Ecole 2d Degre Prof. Privée Ecole De Commerce Kisel;7 Faubourg De Lyon;90000;Belfort;47.6343043;6.850814
+0251523V;Ecole 2d Degre Prof. Privée Ecole Du Fc Sochaux;20 Rue De La Cote;25230;Seloncourt;47.4588815;6.8458273
+0900035Z;Ecole 2d Degre Prof. Privée Ecole Ema / Imt;11-13 Rue Aristide Briand;90000;Belfort;47.6327477;6.8553195
+0622169W;Ecole 2d Degre Prof. Privée Ecole Europeenne D'esthetique;114 Rue Saint Aubert;62000;Arras;50.2924651;2.7692463
+0596339V;Ecole 2d Degre Prof. Privée Ecole Privée De Coiffure;74 Boulevard De La Liberte;59800;Lille;50.633826;3.0576975
+0596351H;Ecole 2d Degre Prof. Privée Efficom-nord;144/146 Rue Nationale;59000;Lille;50.6335348;3.0541112
+0595721Y;Ecole 2d Degre Prof. Privée Lp Privé Cours Auber-icam;6 Rue Auber;59046;Lille;50.6302567;3.0417529
+0595828P;Ecole 2d Degre Prof. Privée Pigier I.t.m.-i.c.a.;14 Rue Des Fusiliers Marins;59140;Dunkerque;51.0329454;2.3721355
+0593019L;Ecole 2d Degre Prof. Privée Schanfelaer-normand;20 Avenue Charles Saint Venant;59800;Lille;50.634035;3.0703394
+0594621C;Ecole 2d Degre Prof. Privée Soins Esthetiques;9-11 Rue Leon Trulin;59800;Lille;50.6384147;3.0663107
+0593098X;Ecole 2d Degre Prof. Privée Sueur Delabre Ctr Format Privé;Parks Service Unite1;59406;Cambrai;50.173538;3.236633
+0251908N;Ecole 2d Degre Prof.priv. Afco Ass. Franc-comt. Form.coiffur;16 Rue De Belfort;25000;Besancon;47.2469149;6.026832
+0360846Z;Ecole 2d Degre Prof.privee;8-10 Rue Guimon Latouche;36000;Chateauroux;46.8126291;1.6942297
+0580977X;Ecole 2d Degre Prof.privee;5 Avenue Claude Delisse;58360;St Honore Les Bains;46.8962966;3.8561571
+0711892W;Ecole 2d Degre Prof.privee;Charly;71250;Mazille;46.396423;4.610709
+0610799M;Ecole 2d Degre Prof.privee A.d.f.i.;40 Rue Du Puit Au Verrier;61001;Alencon;48.4363865;0.0876791
+0142359N;Ecole 2d Degre Prof.privee Acad Coif Esthet Morriss Partn;177 Rue D Auge;14000;Caen;49.1747803;-0.3407445
+0596780Z;Ecole 2d Degre Prof.privee Academie Privée Michel Dervyn;344 Avenue De La Marne;59700;Marcq En Baroeul;50.6798793;3.1150119
+0672904T;Ecole 2d Degre Prof.privee Academy Serge Comtesse;11 Rue Boston;67000;Strasbourg;48.5766769;7.7707505
+0333122P;Ecole 2d Degre Prof.privee Alienor D Esthetique;55 57 Rue Jules Ferry;33200;Bordeaux;44.842255;-0.6034116
+0851559H;Ecole 2d Degre Prof.privee Animation Et Petite Enfance;Zi Montifaut;85700;Pouzauges;46.782648;-0.837243
+0440432F;Ecole 2d Degre Prof.privee Anjorrant;80 Rue Du General Buat;44009;Nantes;47.2305106;-1.5393662
+0422052C;Ecole 2d Degre Prof.privee Ass Forezienne Ec Production;10 Rue Scheurer Kestner;42000;St Etienne;45.453516;4.3906874
+0721300X;Ecole 2d Degre Prof.privee Assoc Formation Personnel;47 Avenue Olivier Messiaen;72018;Le Mans;48.0146373;0.1594294
+0383299E;Ecole 2d Degre Prof.privee A-tel Communication;21 Avenue De La Plaine Fleurie;38240;Meylan;45.20604;5.7576666
+0693134G;Ecole 2d Degre Prof.privee Atelier Apprent Gorge De Loup;105 Avenue Sidoine Apollinaire;69009;Lyon 09;45.7672746;4.7977054
+0691474C;Ecole 2d Degre Prof.privee Ateliers D'apprentissage;La Giraudiere;69690;Brussieu;45.752063;4.547178
+0133909D;Ecole 2d Degre Prof.privee Ateliers De L'image Et Du Son;40 Rue Borde;13008;Marseille 08;43.2788168;5.3910828
+0693444U;Ecole 2d Degre Prof.privee Athena;22 Rue Pizay;69202;Lyon 01;45.7671314;4.836327
+0422109P;Ecole 2d Degre Prof.privee Avenir Formation;10 Rue Emile Noirot;42302;Roanne;46.0386546;4.0683222
+0611282M;Ecole 2d Degre Prof.privee Beaute Coiffure Et Formation;93 Avenue Rhin Et Danube;61000;Alencon;48.4183713;0.0973169
+0492317C;Ecole 2d Degre Prof.privee Beaute Coiffure Formation;51 Place Allain Targe;49400;Saumur;47.2570769;-0.0691588
+0171586G;Ecole 2d Degre Prof.privee Campus Des Metiers;Rue Du Chateau Site Du Prieure;17000;La Rochelle;48.7991114;-1.3941003
+0761772N;Ecole 2d Degre Prof.privee Catherine Lorene;52 Rue Jean Lecanuet;76000;Rouen;49.4454094;1.0882638
+0754129F;Ecole 2d Degre Prof.privee Ccip Ecoles Gregoire-ferrandi;28 Rue De L'abbe Gregoire;75006;Paris 06;48.8474723;2.3254989
+0660876U;Ecole 2d Degre Prof.privee Centre Enseignement Formation;Route De Thuir;66011;Perpignan;42.6820032;2.8646257
+0755207C;Ecole 2d Degre Prof.privee Chapellerie Modiste;8 Passage Abel Leblanc;75012;Paris 12;48.8459871;2.3814297
+0501212W;Ecole 2d Degre Prof.privee Coiff'ecole;26 Rue De La Paix;50100;Cherbourg Octeville;49.643053;-1.6270152
+0195038Y;Ecole 2d Degre Prof.privee Coiffure;4 Place Saint Pierre;19100;Brive La Gaillarde;45.1594566;1.5344809
+0721515F;Ecole 2d Degre Prof.privee Coiffure Esthet Espace Bel Air;200 Avenue Georges Durand;72100;Le Mans;47.9754992;0.2147089
+0442345K;Ecole 2d Degre Prof.privee Coiffure Et Esthetique;44 Quai Malakoff;44000;Nantes;47.2147689;-1.5414584
+0011255J;Ecole 2d Degre Prof.privee Coiffure Europeenne De L'ain;30 Avenue Alphonse Baudin;01000;Bourg En Bresse;46.200325;5.216353
+0111011P;Ecole 2d Degre Prof.privee Coiffure K L;3 Square Gambetta;11000;Carcassonne;43.2116828;2.357763
+0133683H;Ecole 2d Degre Prof.privee Coiffure Leader;441 Avenue Du Prado;13008;Marseille 08;43.266863;5.3830066
+0851437A;Ecole 2d Degre Prof.privee Coiffure Morriss;12 Rue Jean Jaures;85000;La Roche Sur Yon;46.6696044;-1.4289695
+0421913B;Ecole 2d Degre Prof.privee Createc;12 Rue Michel Rondet;42000;St Etienne;45.4380867;4.3854206
+0754168Y;Ecole 2d Degre Prof.privee Ctre Marcel Lamy Coiffure-esth;15-17 Rue Des Fillettes;75018;Paris 18;48.8946669;2.3633961
+0754338H;Ecole 2d Degre Prof.privee Ctre Pri Ens Soins Esthetiques;9 Rue Volney;75002;Paris 02;48.8696803;2.3301585
+0311202N;Ecole 2d Degre Prof.privee D'esthetique Cosmetique;23 Rue Gabriel Peri;31000;Toulouse;43.6060897;1.4534456
+0752123A;Ecole 2d Degre Prof.privee Ec Francaise D'ens Technique;110 Rue De Picpus;75012;Paris 12;48.8385284;2.4013752
+0754872N;Ecole 2d Degre Prof.privee Ec Internat Esthetiq Francaise;5 Avenue Bertie Albrecht;75008;Paris 08;48.8756935;2.3011723
+0754108H;Ecole 2d Degre Prof.privee Ec Internationale De Coiffure;76 Rue Reaumur;75002;Paris 02;48.8664449;2.3517469
+0342216Z;Ecole 2d Degre Prof.privee Ec Pr Esthetique Millenaire;1350 Av Albert Einstein;34000;Montpellier;43.6101676;3.9133879
+0341752V;Ecole 2d Degre Prof.privee Ec Sup Metiers Aern Esma;Aerp Montpellier Mediterranee;34137;Mauguio;43.5788389;3.959328
+0922623B;Ecole 2d Degre Prof.privee Ecofac;72 Avenue De La Republique;92320;Chatillon;48.8019714;2.2959133
+0754169Z;Ecole 2d Degre Prof.privee Ecole Active De Coiffure;203 205 Rue Lafayette;75010;Paris 10;48.8810547;2.3636236
+0754112M;Ecole 2d Degre Prof.privee Ecole De Bijouterie-joaillerie;58 Rue Du Louvre;75002;Paris 02;48.8658847;2.3434157
+0750115T;Ecole 2d Degre Prof.privee Ecole De Coiffure;60 Rue Saint Lazare;75009;Paris 09;48.8768576;2.3333014
+0754114P;Ecole 2d Degre Prof.privee Ecole Dentaire Francaise;3 Rue De L'est;75020;Paris 20;48.8708623;2.3944941
+0180939Y;Ecole 2d Degre Prof.privee Ecole Des Metiers;145-151 Rue De Turly;18000;Bourges;47.1015295;2.4242279
+0681962N;Ecole 2d Degre Prof.privee Ecole Des Metiers Artistiques;2 Rue Sainte-catherine;68100;Mulhouse;47.7426617;7.3390234
+0251890U;Ecole 2d Degre Prof.privee Ecole Ema 25 Besancon;13 Rue Du Balcon;25000;Besancon;47.2476688;6.0251837
+0624303R;Ecole 2d Degre Prof.privee Ecole Europeenne Esthetique Ii;112 Rue Saint Aubert;62000;Arras;50.2924167;2.7693128
+0442412H;Ecole 2d Degre Prof.privee Ecole Nantaise De Coiffure;6 Place Montaigne;44000;Nantes;47.2201576;-1.5615212
+0011294B;Ecole 2d Degre Prof.privee Ecole Technique Du Bois;560 B Rue Du Marais;01110;Cormaranche En Bugey;45.957123;5.602139
+0212188P;Ecole 2d Degre Prof.privee Eisec;35 Rue Charles Dumont;21000;Dijon;47.3123064;5.0399242
+0624372R;Ecole 2d Degre Prof.privee Elit Academy;50 60 Rue De Folkestone;62200;Boulogne Sur Mer;50.7279326;1.5994104
+0062052Y;Ecole 2d Degre Prof.privee Elysees Cannes;22 Bd De La Republique;06400;Cannes;43.5545307;7.0250715
+0672997U;Ecole 2d Degre Prof.privee Elysees Marbeuf;26 Rue Des Magasins;67000;Strasbourg;48.591175;7.7408775
+0212187N;Ecole 2d Degre Prof.privee Ema;15 Rue De L'arquebuse;21000;Dijon;47.3190199;5.031125
+0831653W;Ecole 2d Degre Prof.privee Ema;1349 Boulevard Jean Moulin;83700;St Raphael;43.4279147;6.7900075
+0292256V;Ecole 2d Degre Prof.privee Emb Pigier;31 Rue De Douarnenez;29000;Quimper;47.9980478;-4.1098548
+0121234B;Ecole 2d Degre Prof.privee Emilie De Rodat;11 Rue Seguy;12000;Rodez;44.3519011;2.5755888
+0460637T;Ecole 2d Degre Prof.privee Enseignement Commercial;17 Rue De La Prefecture;46000;Cahors;44.4475229;1.441989
+0421947N;Ecole 2d Degre Prof.privee Epec-darfeuille;5 Place Jean Jaures;42000;St Etienne;45.4405716;4.3855444
+0580978Y;Ecole 2d Degre Prof.privee Estec;2/4 Rue Saint-benin;58000;Nevers;46.9862229;3.1492371
+0875054W;Ecole 2d Degre Prof.privee Esthetiq Cosmetiq Coiffure;3 Allee St Alexis;87000;Limoges;45.8253606;1.2591981
+0573543S;Ecole 2d Degre Prof.privee Esthetique Coiffure;9 Rue Ausone;57000;Metz;49.1087379;6.1725896
+0820777Z;Ecole 2d Degre Prof.privee Esthetique Cosmet Skhole D'art;3 R Bataille De Dunkerque 1940;82000;Montauban;44.0068309;1.3461514
+0011256K;Ecole 2d Degre Prof.privee Esthetique M C Dumonceau;30 Avenue Alphonse Baudin;01000;Bourg En Bresse;46.200325;5.216353
+0753082T;Ecole 2d Degre Prof.privee Ets - Iris;63 Rue Ampere;75017;Paris 17;48.885201;2.3009995
+0752124B;Ecole 2d Degre Prof.privee F.morice Esthetique-cosmetique;27 Rue Vernet;75008;Paris 08;48.8720338;2.2978549
+0241248B;Ecole 2d Degre Prof.privee Formafoot;87 Route De Bordeaux;24430;Marsac Sur L Isle;45.1841986;0.6576263
+0442617F;Ecole 2d Degre Prof.privee Formation Corps Et Beaute;24 Rue De L'ile De France;44600;St Nazaire;47.2845926;-2.209649
+0754465W;Ecole 2d Degre Prof.privee Formul?abc;105 Boulevard De Magenta;75010;Paris 10;48.878965;2.3535647
+0492028N;Ecole 2d Degre Prof.privee Francaise Coiffure Esthetique;Place Du Chapeau De Gendarme;49000;Angers;47.4511367;-0.5553704
+0492343F;Ecole 2d Degre Prof.privee Francaise Coiffure Esthetique;192 Rue Nationale;49300;Cholet;47.0618334;-0.8842425
+0133825M;Ecole 2d Degre Prof.privee Giorgifont;2a Rue De Rome;13001;Marseille 01;43.2957629;5.3782139
+0133567G;Ecole 2d Degre Prof.privee Groupe Pluralis;16 Bd Theodore Thurner;13006;Marseille 06;43.2918566;5.383141
+0422027A;Ecole 2d Degre Prof.privee Hair School;35 Rue D'arcole;42000;St Etienne;45.43977;4.3804548
+0912185F;Ecole 2d Degre Prof.privee I P F C;93 Bd Decauville;91024;Evry;48.6332379;2.4421065
+0301757X;Ecole 2d Degre Prof.privee Ifc Gard;125 Rue De L Hostellerie;30000;Nimes;43.8139025;4.3459059
+0693368L;Ecole 2d Degre Prof.privee Ifosupd;181 Avenue Jean Jaures;69348;Lyon 07;45.7368155;4.836897
+0142172K;Ecole 2d Degre Prof.privee Inst Form Esth Et Coiff Ifec;1 Et 3 Rue De L'arquette;14000;Caen;49.1748513;-0.3556685
+0341674K;Ecole 2d Degre Prof.privee Inst Sup Privé Sup Exup;Aerp Montpellier Mediterranee;34137;Mauguio;43.5788389;3.959328
+0851584K;Ecole 2d Degre Prof.privee Inst Sup Prothese Assist Dent;Route De La Goriandiere;85504;Les Herbiers;46.872157;-0.999918
+0841042B;Ecole 2d Degre Prof.privee Inst. Privé Format.commerciale;250 Rue Du 12e Reg. De Zouaves;84094;Avignon;43.949317;4.805528
+0721610J;Ecole 2d Degre Prof.privee Instit K Form Esthetique;200 Avenue Georges Durand;72100;Le Mans;47.9754992;0.2147089
+0492342E;Ecole 2d Degre Prof.privee Institut De Bijouterie;Square Balzac;49412;Saumur;47.2519481;-0.0800867
+0754163T;Ecole 2d Degre Prof.privee Institut Europeen De Coiffure;28 Rue De Trevise;75009;Paris 09;48.8746401;2.3454931
+0851599B;Ecole 2d Degre Prof.privee Iseca;Village D'entreprises;85700;Pouzauges;46.782648;-0.837243
+0840077C;Ecole 2d Degre Prof.privee Isfap;11 Avenue Des Sources;84000;Avignon;43.9413462;4.8139846
+0573734Z;Ecole 2d Degre Prof.privee Isfec;15 Rue Des Charpentiers;57070;Metz;49.1114889;6.2334259
+0061888V;Ecole 2d Degre Prof.privee Jeune Aiglon;177 Route De Grenoble;06200;Nice;43.6810228;7.1990378
+0624366J;Ecole 2d Degre Prof.privee Joe's Art Concept;3 Rue Bayard;62300;Lens;50.4298166;2.8333087
+0312693J;Ecole 2d Degre Prof.privee La Pradette;12 Chemin De La Pradette;31600;Muret;43.4624447;1.3224734
+0690636S;Ecole 2d Degre Prof.privee Le C.e.p.a.j;Chemin De Bernicot;69564;St Genis Laval;45.6933714;4.771675
+0673004B;Ecole 2d Degre Prof.privee L'inst. Coiffure Et Esthetique;10 Rue Paul Eluard;67200;Strasbourg;48.5882779;7.6973786
+0301777U;Ecole 2d Degre Prof.privee L'institut;4 Bis Rue Julien Trelis;30100;Ales;44.127204;4.083352
+0631974D;Ecole 2d Degre Prof.privee Maestris;9/12 Rue Gilbert Romme;63000;Clermont Ferrand;45.7675847;3.101037
+0596791L;Ecole 2d Degre Prof.privee Maestris Beaute;679 Avenue De La Republique;59000;Lille;50.6562547;3.0875691
+0596794P;Ecole 2d Degre Prof.privee Maestris Beaute;9 Bis Rue De Roubaix;59400;Cambrai;50.1811372;3.2380149
+0133780N;Ecole 2d Degre Prof.privee Maestris Marseille;54 Traverse Maridet;13012;Marseille 12;43.3096761;5.4199628
+0133787W;Ecole 2d Degre Prof.privee Maestris Pigier;2 Rue Le Corbusier;13090;Aix En Provence;43.5233286;5.4325772
+0422190C;Ecole 2d Degre Prof.privee Maison De La Coiffure;38 Rue Des Passementiers;42100;St Etienne;45.4198431;4.396064
+0754242D;Ecole 2d Degre Prof.privee Marbeuf Esthetique Elysees;27 Rue Marbeuf;75008;Paris 08;48.8691291;2.3042693
+0312251D;Ecole 2d Degre Prof.privee Marge Verlair;5 Impasse Saint Aubin;31000;Toulouse;43.6037233;1.4532092
+0440425Y;Ecole 2d Degre Prof.privee Matile Esthetique Manucurie;24 Rue Crebillon;44000;Nantes;47.2132721;-1.5619479
+0721546P;Ecole 2d Degre Prof.privee Metiers De La Musique;71 Avenue Olivier Messiaen;72000;Le Mans;48.0154161;0.1544436
+0492333V;Ecole 2d Degre Prof.privee Morriss Partner;70 Rue Du Grand Launay;49000;Angers;47.4678294;-0.5932746
+0442196Y;Ecole 2d Degre Prof.privee Morriss-partner;5 Allee Baco;44002;Nantes;47.2146182;-1.5479843
+0061926L;Ecole 2d Degre Prof.privee N?1 Ctre Perf Maitres Coiffeur;1 Place Massena;06000;Nice;43.6974679;7.2702352
+0596406T;Ecole 2d Degre Prof.privee Patricia Houzet;12 Rue Sadi Carnot;59290;Wasquehal;50.668925;3.1303161
+0660070T;Ecole 2d Degre Prof.privee Pigier;49 Bd Clemenceau 7 Rue P Courty;66000;Perpignan;42.698684;2.8958719
+0694088U;Ecole 2d Degre Prof.privee Pigier;110 Place De La Gare;69400;Villefranche Sur Saone;45.9845849;4.7202289
+0841119K;Ecole 2d Degre Prof.privee Pigier;40 Bis Chemin Du Lavarin;84000;Avignon;43.9295739;4.8047154
+0442547E;Ecole 2d Degre Prof.privee Pigier Coiffure;16 Rue De Vauzelles;44110;Chateaubriant;47.712001;-1.3750486
+0492230H;Ecole 2d Degre Prof.privee Pigier Coiffure Esthetique;30 Rue Bodinier;49000;Angers;47.4726409;-0.5544881
+0720829K;Ecole 2d Degre Prof.privee Pigier Coiffure Esthetique;41 Rue Du Docteur Leroy;72005;Le Mans;48.0016328;0.1969314
+0301749N;Ecole 2d Degre Prof.privee Pigier Formanimes;105 Rue Claude Nicolas Ledoux;30900;Nimes;43.8150783;4.3471676
+0754686L;Ecole 2d Degre Prof.privee R.ferrere Esthetiq-cosmetique;14 Rue Du Faubourg St Honore;75008;Paris 08;48.8687211;2.3223202
+0751593Z;Ecole 2d Degre Prof.privee Saint-louis Union Academie;113 115 Rue Du Fg Poissonniere;75009;Paris 09;48.8779515;2.3490733
+0341831F;Ecole 2d Degre Prof.privee Sarl Ecole Sonia Delaunay;Route De Boujan;34500;Beziers;43.3663036;3.2315349
+0721645X;Ecole 2d Degre Prof.privee Sarth'esthetique Formations;185 Rue Henri Champion;72100;Le Mans;47.9827235;0.232009
+0690696G;Ecole 2d Degre Prof.privee Silvya Terrade;53 Place De La Republique;69002;Lyon 02;45.7606843;4.8360085
+0693974V;Ecole 2d Degre Prof.privee Studio M;565 Rue Du Sans Souci;69760;Limonest;45.805516;4.7733391
+0333187K;Ecole 2d Degre Prof.privee Visagisme Coiffure Developpeme;81 Avenue De L'epinette;33500;Libourne;44.9106614;-0.2289695
+0752435P;Ecole 2d Degre Prof.privee Y.rocher Esthetique-cosmetique;62 Avenue D'iena;75016;Paris 16;48.8687256;2.2959626
+0751698N;Ecole 2d Degree Prof. Privée Pigier;67 69 Rue De Douai;75009;Paris 09;48.883762;2.3287504
+0132665B;Ecole 2nd Degre General Privée Cours Du Parc Breteuil;7 Rue Lacedemone;13006;Marseille 06;43.2814125;5.3771657
+0133446A;Ecole 2nd Degre General Privée Hamaskaine;60 Boulevard Pinatel;13012;Marseille 12;43.3148118;5.4413806
+0133148B;Ecole 2nd Degre General Privée I.b.s. Of Provence;500 Petite Rte De Bouc Bel Air;13080;Aix En Provence;43.4513324;5.429245
+0131938L;Ecole 2nd Degre General Privée Institut Leschi;58 Cours Julien;13006;Marseille 06;43.2943303;5.383053
+0133242D;Ecole 2nd Degre General Privée St Pierre;24 Av St Eloi;13790;Peynier;43.4467555;5.6397325
+0131669U;Ecole 2nd Degre General Privée Val Saint Andre;19 Avenue Malacrida;13100;Aix En Provence;43.5165876;5.4668556
+0430842G;Ecole Agricole Privée;36 Rue Du Colonel De Turenne;43200;Yssingeaux;45.1426639;4.1220723
+0601404A;Ecole Agricole Privée;;60120;Rouvroy Les Merles;49.647906;2.358818
+0601575L;Ecole Agricole Privée;Place De L'hotel De Ville;60190;Estrees St Denis;49.426115;2.639508
+0611018A;Ecole Agricole Privée;Saint Gervais;61220;Briouze;48.704021;-0.369682
+0611021D;Ecole Agricole Privée;;61240;Nonant Le Pin;48.7069199;0.217466
+0631560D;Ecole Agricole Privée;;63580;Vernet La Varenne;45.472891;3.451612
+0631561E;Ecole Agricole Privée;1 Route De Riom;63720;Ennezat;45.8977695;3.2206849
+0601576M;Ecole Agricole Privée (filles);39 Route Des Flandres;60490;Orvillers Sorel;49.5786617;2.7084991
+0332376D;Ecole Agricole Privée Centre D Enseignement Agricole;44 Crs Gambetta;33210;Langon;44.5532316;-0.2524309
+0840746E;Ecole Agricole Privée Centre De Formation;19 Quai Pasteur;84110;Vaison La Romaine;44.2401186;5.0726233
+0595121W;Ecole Agricole Privée Centre D'enseignement Agricole;110 Route De Gravelines;59630;Bourbourg;50.9522873;2.1920151
+0501576S;Ecole Agricole Privée Centre D'etudes Rurales;Rue De Monteglise;50720;Barenton;48.6026445;-0.8315884
+0595122X;Ecole Agricole Privée Centre Education Et Promotion;15 Rue Du College;59940;Estaires;50.6462778;2.7208128
+0141724Y;Ecole Agricole Privée Centre Horticole;Rue Du Prieure;14480;St Gabriel Brecy;49.2780979;-0.5664422
+0840747F;Ecole Agricole Privée Centre Saint Dominique;Rue Des Ursulines;84601;Valreas;44.385702;4.989613
+0030888Z;Ecole Agricole Privée Claude Mercier;Route De Lapalisse;03250;Le Mayet De Montagne;46.0750346;3.6671551
+0261265J;Ecole Agricole Privée Ctr D'etudes Forestieres Agr.;103 Av De Rochemaure;26200;Montelimar;44.5657532;4.735804
+0611175W;Ecole Agricole Privée Ctre Form Technicien Agricole;25 Rue Pierre Neveu;61600;La Ferte Mace;48.5935429;-0.3524704
+0771738W;Ecole Agricole Privée Ctre Formation Prof Agr Et Hor;Domaine De Morfonde;77270;Villeparisis;48.947136;2.638554
+0511646M;Ecole Agricole Privée Ctre Hort Foyer De L'enfance;33 Rue Du Clair Logis;51000;Chalons En Champagne;48.9740922;4.3620971
+0501793C;Ecole Agricole Privée Ctre Nat.form.lads Et Jockeys;Graignes;50620;Graignes Mesnil Angot;49.2381699;-1.19717
+0931654T;Ecole Agricole Privée Ec Horticulture Fenelon;1 Rue De Montauban;93410;Vaujours;48.9301569;2.5695529
+0881452A;Ecole Agricole Privée Ecole D Horticulture Et De Pay;6 Rue Du College;88700;Roville Aux Chenes;48.3861293;6.6027303
+0840788A;Ecole Agricole Privée Et Reg Form Prof Agric Forest;;84240;La Bastide Des Jourdans;43.784867;5.634354
+0781068M;Ecole Agricole Privée Horticole;43 Rue Du General De Gaulle;78490;Le Tremblay Sur Mauldre;48.7703215;1.8781482
+0133500J;Ecole Agricole Privée I.c.o.p.;1 Allee Des Pinsons;13012;Marseille 12;43.3136607;5.4380651
+0595012C;Ecole Agricole Privée Inst Enseignement Technologiqu;Route De Warhem;59492;Hoymille;50.972972;2.461305
+0641545G;Ecole Agricole Privée Inst.secondaire Jean Errecart;Route Aicirits;64120;St Palais;43.327408;-1.032999
+0595120V;Ecole Agricole Privée Institut Agricole;19 Rue De L'epee;59400;Cambrai;50.1735274;3.2293037
+0595124Z;Ecole Agricole Privée Institut Agricole;69 Rue Du Violon D'or;59190;Hazebrouck;50.7219064;2.5298525
+0623280D;Ecole Agricole Privée Institut Agricole;22 Ancienne Route Nationale;62690;Savy Berlette;50.3497901;2.557783
+0623279C;Ecole Agricole Privée Institut Agricole Saint Joseph;Route Nationale 43;62137;Coulogne;50.905437;1.9459807
+0593257V;Ecole Agricole Privée Institut De Genech;Rue De La Liberation;59242;Genech;50.5300906;3.2117313
+0501578U;Ecole Agricole Privée Institut L'abbaye;Route De Quineville;50310;Montebourg;49.4967815;-1.3685693
+0141723X;Ecole Agricole Privée Institut Lemonier;Rue D Herouville;14013;Caen;49.1935596;-0.3500571
+0030925P;Ecole Agricole Privée Institut Rural;;03110;Escurolles;46.143095;3.26744
+0400753B;Ecole Agricole Privée Ireo;Cassen;40360;Castelnau Chalosse;43.679287;-0.846823
+0382374Z;Ecole Agricole Privée La Maison;La Bonte;38620;St Geoire En Valdaine;45.4581366;5.6351566
+0133005W;Ecole Agricole Privée Lads Et Jockeys;Les Plaines De L Arbois;13480;Cabries;43.44489;5.320507
+0601613C;Ecole Agricole Privée Lads-jockeys;5 Rue Du Chauffour;60270;Gouvieux;49.1894825;2.4274189
+0641542D;Ecole Agricole Privée Lap Frantses Enia;Av Du Jai Alai;64220;St Jean Pied De Port;43.1663684;-1.2335137
+0781066K;Ecole Agricole Privée Le Logis;5 Rue De Port Royal;78470;St Lambert;48.7332837;2.0194432
+0150661M;Ecole Agricole Privée Letrt St Vincent;2 Rue Marcellin Boudet;15100;St Flour;45.0348129;3.0871023
+0623276Z;Ecole Agricole Privée Lycée Agricole Sainte Marie;52 Rue D'isbergues;62120;Aire Sur La Lys;50.6386462;2.4046454
+0593256U;Ecole Agricole Privée Lycée Privé D'anchin;Abbaye D'anchin;59146;Pecquencourt;50.375524;3.212056
+0595119U;Ecole Agricole Privée Lycée Privé Rural;5 Rue De La Chaussee Brunehaut;59570;Bavay;50.2871242;3.7776055
+0831109E;Ecole Agricole Privée Lycée Professionnel Agricole;Route De Barjols;83470;St Maximin La Ste Baume;43.472847;5.869149
+0623278B;Ecole Agricole Privée Lycée Rural Privé Catholique;Rue Druet;62116;Bucquoy;50.137547;2.70619
+0781022M;Ecole Agricole Privée Notre-dame De La Roche;Boite Postale 10;78320;Levis St Nom;48.722222;1.949444
+0150658J;Ecole Agricole Privée Saint Joseph;Avenue De La Gare;15600;Maurs;44.7084459;2.1994338
+0911551S;Ecole Agricole Privée St Antoine;Chateau;91460;Marcoussis;48.645942;2.220813
+0762284V;Ecole Agricole Privée St Joseph;Chateau De Mesnieres;76270;Mesnieres En Bray;49.7649649;1.380615
+0781069N;Ecole Agricole Privée Sully;22 Avenue De L'europe;78200;Magnanville;48.9665663;1.6800698
+0781023N;Ecole Agricole Privée Tecomah;Parc De Jouy;78350;Jouy En Josas;48.764757;2.163188
+0690194L;Ecole Catholique D'arts Et Metiers;40 Montee Saint Barthelemy;69321;Lyon 05;45.7624909;4.8246771
+0590349J;Ecole Centrale De Lille;Cite Scientifique;59651;Villeneuve D Ascq;50.623334;3.145
+0690187D;Ecole Centrale De Lyon;36 Avenue Guy De Collongue;69131;Dardilly;45.782627;4.7662635
+0440100V;Ecole Centrale De Nantes;1 Rue De La Noe;44321;Nantes;47.2482417;-1.5499091
+0754431J;Ecole Centrale D'electronique;53 Rue De Grenelle;75007;Paris 07;48.8545505;2.3256016
+0921225G;Ecole Centrale Des Arts Et Manufactures, Paris;Grande Voie Des Vignes;92295;Chatenay Malabry;48.7660651;2.2865429
+0951807Y;Ecole Commerciale Privée Fayol;4 Rue Revert;95300;Pontoise;49.052526;2.0952513
+0251480Y;Ecole D'agriculture Privé Saint Joseph;Place Cretin;25270;Levier;46.954174;6.116989
+0951820M;"ecole De Biologie Industrielle ""ebi""";32 Boulevard Du Port;95094;Cergy;49.0322975;2.0640348
+0501669T;Ecole De Formation Technique Ecole Des Fourriers;;50460;Querqueville;49.664767;-1.695441
+0595781N;Ecole De Formation Technique Ecole Reedu Prof Andre Maginot;35 Rue Du General Sarail;59056;Roubaix;50.6915213;3.1716011
+0130230E;Ecole De L'air;Base Aerienne 701;13300;Salon De Provence;43.603333;5.106389
+0440429C;Ecole De Metiers Ctre Formation Gaz De France;La Croix Gaudin;44360;St Etienne De Montluc;47.2834179;-1.7417426
+0741433F;Ecole De Production Privée E.c.a.u.t;301 Route De Bregny;74250;Viuz En Sallaz;46.147207;6.393383
+0470064P;Ecole De Tersac Ac;Tersac;47180;Meilhan Sur Garonne;44.495752;0.06094
+0312188K;Ecole Dentaire Francaise;20 Rue St Antoine Du T;31000;Toulouse;43.603794;1.4473808
+0251950J;Ecole Des Arts Et Techniques;11 Rue Nicolas Bruand;25000;Besancon;47.2493423;6.0227103
+0753742K;Ecole Des Hautes Etudes En Sciences Sociales;54 Boulevard Raspail;75006;Paris 06;48.8501959;2.3269388
+EHESS;Ecole Des Hautes Etudes En Sciences Sociales;54, 96,105 Boulevard Raspail;75005;Paris 05;48.8461933;2.3281331
+0750043P;Ecole D'ingenieurs De La Ville De Paris;57 Boulevard Saint Germain;75240;Paris 05;48.8504483;2.3463367
+0623921A;Ecole D'ingenieurs Du Pas De Calais;Campus De La Malassise;62967;Longuenesse;50.736744;2.243632
+0171435T;Ecole D'ingenieurs En Genie Des Systemes Industriels;26 Rue Vaux De Foletier;17011;La Rochelle;46.1398628;-1.1537103
+0171550T;Ecole Francaise Coiffure Et Esthetique;17-19 Avenue De Mulhouse;17000;La Rochelle;0.0;0.0
+0791162J;Ecole Francaise Coiffure Et Esthetique;7-27 Rue Marcel Paul;79000;Niort;0.0;0.0
+0753592X;Ecole Francaise D'electronique Et D'informatique;10 Rue Amyot;75005;Paris 05;48.8434614;2.3468998
+0941934S;Ecole Francaise D'electronique Et D'informatique;3 Rue Victor Hugo;94800;Villejuif;48.78874;2.3650286
+0951678H;Ecole Gen Et Technol Privée Compta-sup Marketing;1 Av De La Div Leclerc;95350;St Brice Sous Foret;48.9922397;2.3583183
+0830177S;Ecole Gen. Et Technol.privee Cours Renaissance;Le Mayol Rue Leon Reboul;83000;Toulon;43.1185381;5.937864
+0831372R;Ecole Gen. Et Technol.privee Guy Furet;702 Avenue Du 15e Corps;83600;Frejus;43.4394351;6.7435339
+0752593L;Ecole Gen.et Technol. Privé Nation Bauchat Ecole D La Lune;22 Rue Du Sergent Bauchat;75012;Paris 12;48.845549;2.3930299
+0753372H;Ecole Gen.et Technol. Privée Beth Sefer Yad Mordekhai;145 Rue Saint Maur;75011;Paris 11;48.8679559;2.375428
+0753819U;Ecole Gen.et Technol. Privée Inst Marais-charlemagne-polles;9 Rue Dieu;75010;Paris 10;48.8703187;2.3644784
+0061990F;Ecole Gen.et Technol.privee As Cannes Football;Bp 179;06152;Cannes;43.552847;7.017369
+0133566F;Ecole Gen.et Technol.privee Bac 2000;13 Rue De La Poudriere;13090;Aix En Provence;43.52038;5.4449743
+0752382G;Ecole Gen.et Technol.privee Clapeyron Europe Math;23 Rue Clapeyron;75008;Paris 08;48.8823236;2.3234815
+0755261L;Ecole Gen.et Technol.privee Cours Legendre;25 Rue Du Petit Musc;75004;Paris 04;48.8528745;2.3643336
+0693366J;Ecole Gen.et Technol.privee Cours Montesquieu;11 13 Rue Auguste Lacroix;69003;Lyon 03;45.7549248;4.8451646
+0781946S;Ecole Gen.et Technol.privee Cours Versaillais;28 Rue Albert Joly;78000;Versailles;48.8102821;2.139174
+9830416K;Ecole Gen.et Technol.privee Da Costa;Imm.galaxie 71 Av Benebig;98800;Noumea;0.0;0.0
+0311758T;Ecole Gen.et Technol.privee De Photo Et D'audiovisuel;7 Rue Eugene Labiche;31200;Toulouse;43.6283648;1.4348721
+0596808E;Ecole Gen.et Technol.privee Des Met Du Sport Professionnel;Grand Rue;59780;Camphin En Pevele;50.5934675;3.2630624
+9711174Z;Ecole Gen.et Technol.privee Ecole Jean Roumain;15 Rue De La Liberte;97150;St Martin;43.1913245;0.12305
+9830634X;Ecole Gen.et Technol.privee Epsilon Etudes;74 Bis Rue Rene Milliards;98800;Noumea;-22.2758;166.458
+0101094K;Ecole Gen.et Technol.privee Estac;11 Rue Marie Curie;10000;Troyes;48.2716992;4.0753397
+0442618G;Ecole Gen.et Technol.privee Etu.di.a;Plaine De Jeux De La Joneliere;44240;La Chapelle Sur Erdre;47.299576;-1.550638
+0752904Z;Ecole Gen.et Technol.privee Lafayette;16 Rue Du Mail;75002;Paris 02;48.8668132;2.3427
+0530936E;Ecole Gen.et Technol.privee Moliere;6 Bis Rue Ambroise Pare;53000;Laval;48.0703895;-0.7677541
+0783355Y;Ecole Gen.et Technol.privee Perceval;5 Av D Epremesnil;78400;Chatou;48.8872505;2.1584939
+0301767H;Ecole Gen.et Technol.privee Sports Etudes Concept;Chemin Des Hauts De Nimes;30900;Nimes;43.8632132;4.2957487
+0352337A;Ecole Louis De Broglie;Campus De Ker Lann;35170;Bruz;48.0477814;-1.7425205
+0171157R;Ecole Militaire Enseignement Technique;Base Aerienne 722 Saintes Air;17136;Saintes;45.744175;-0.633389
+0161158X;Ecole Militaire Preparatoire;Base Aerienne 709;16109;Cognac;45.657776;-0.3136109
+0171333G;Ecole Militaire Preparatoire;Base Aerienne 721;17133;Rochefort;44.5163889;4.8611111
+0131596P;Ecole Militaire Preparatoire Poilus (bd Des);13 Boulevard Des Poilus;13617;Aix En Provence;43.5267509;5.4595152
+0610055D;Ecole Nat Prof Des Haras Le Pin Au Haras;Le Pin Au Haras;61310;Le Pin Au Haras;48.738069;0.149685
+0755204Z;Ecole Nationale De Commerce;70 Bd Bessières;75017;Paris 17;0.0;0.0
+0312069F;Ecole Nationale De La Meteorologie;42 Avenue Gaspard Coriolis;31057;Toulouse;43.5775472;1.376741
+0350095N;Ecole Nationale De La Sante Publique;Av Du Professeur Leon Bernard;35043;Rennes;48.117507;-1.7000516
+0311256X;Ecole Nationale De L'aviation Civile De Toulouse;7 Avenue Edouard Belin;31055;Toulouse;43.565156;1.479281
+0753478Y;Ecole Nationale Des Chartes;19 Rue De La Sorbonne;75005;Paris 05;48.8486428;2.3430165
+0753493P;Ecole Nationale Des Mines De Paris;60 Bld Saint-michel;75006;Paris 06;48.8455131;2.339794
+0753501Y;Ecole Nationale Des Ponts Et Chaussees;28 Rue Des Saints Peres;75007;Paris 07;48.8556079;2.3310429
+0772517T;Ecole Nationale Des Ponts Et Chaussees;6 Av B Pascal Cite Descartes;77420;Champs Sur Marne;48.852775;2.602651
+0772496V;Ecole Nationale Des Sciences Geographiques;6-8 Av B Pascal Cite Descartes;77455;Champs Sur Marne;48.852775;2.602651
+0692587M;Ecole Nationale Des Travaux Maritimes;Rue Maurice Audin;69518;Vaulx En Velin;45.7790055;4.9221344
+0692566P;Ecole Nationale Des Travaux Publics De L'etat;Rue Maurice Audin;69518;Vaulx En Velin;45.7790055;4.9221344
+0290119X;Ecole Nationale D'ingenieurs De Brest;Technopole Brest Iroise;29608;Brest;48.3651848;-4.5489314
+0420093Y;Ecole Nationale D'ingenieurs De Saint-etienne;58 Rue Jean Parot;42023;St Etienne;45.430467;4.4237117
+0650048Z;Ecole Nationale D'ingenieurs De Tarbes;47 Avenue D'azereix;65016;Tarbes;43.224813;0.0526893
+0341652L;Ecole Nationale Du Genie Rural Des Eaux Et Des Forets;648 Rue Jean-francois Breton;34093;Montpellier;43.6462081;3.8767159
+0540137F;Ecole Nationale Du Genie Rural Des Eaux Et Des Forets;14 Rue Girardet;54042;Nancy;48.6937927;6.1878434
+0631920V;Ecole Nationale Du Genie Rural Des Eaux Et Des Forets;24 Avenue Des Landais;63172;Aubiere;45.7640856;3.1123259
+0753503A;Ecole Nationale Du Genie Rural Des Eaux Et Des Forets;19 Avenue Du Maine;75732;Paris 15;48.843847;2.320594
+0910684Z;Ecole Nationale Sup Des Industries Agricoles Et Alimentaires;1 Rue Des Olympiades;91300;Massy;48.732702;2.2953659
+0340131H;Ecole Nationale Superieure Agronomique De Montpellier;2 Place Pierre Viala;34060;Montpellier;43.6170101;3.8548718
+0350087E;Ecole Nationale Superieure Agronomique De Rennes;65 Rue De Saint Brieuc;35042;Rennes;48.1136527;-1.7053288
+0753472S;Ecole Nationale Supérieure D'art Et Métiers, Paris;151, Bld De L'hopital;75013;Paris 13;48.8339145;2.3573451
+0753237L;École Nationale Supérieure D'arts Et Métiers Paris;151, Bd De L'hopital;75013;Paris 13;48.8339145;2.3573451
+0410981U;Ecole Nationale Superieure De La Nature Et Du Paysage;5-7 Rue Des Grands Champs;41029;Blois;47.5660138;1.3272299
+0590339Y;Ecole Nationale Superieure Des Arts Et Metiers De Lille;8 Boulevard Louis Xiv;59046;Lille;50.6282777;3.0711917
+0420094Z;Ecole Nationale Superieure Des Mines De Saint Etienne;158 Cours Fauriel;42023;St Etienne;45.422755;4.408854
+0751878J;Ecole Nationale Supérieure Des Techniques Avancées Paris;32 Bld Victor;75015;Paris 15;48.8342821;2.2835648
+0753510H;Ecole Nationale Supérieure Des Télécommunications Paris;46 Rue Barrault;75634;Paris 13;48.8262704;2.3464541
+0290124C;Ecole Navale;Lanveoc Poulmic;29160;Lanveoc;48.279167;-4.448333
+0753455Y;Ecole Normale Superieure;45, Rue D'ulm;75005;Paris 05;48.8418453;2.3440345
+0940607Z;Ecole Normale Supérieure De Cachan;61, Avenue Du Président Wilson;94230;Cachan;0.0;0.0
+0920812H;Ecole Normale Superieure De Fontenay Saint-cloud;31, Avenue Lombard;92266;Fontenay Aux Roses;48.789776;2.287181
+0693259T;Ecole Normale Superieure De Sciences Lyon;47 Allee D'italie;69364;Lyon 07;45.7298048;4.8280041
+0951819L;Ecole Phys Math Industrielles;32 Boulevard Du Port;95092;Cergy;49.0322975;2.0640348
+0911568K;Ecole Polytechnique;;91128;Palaiseau;48.7057547;2.219477
+0920674H;Ecole Polytechnique Feminine;3 Bis Rue Lakanal;92330;Sceaux;48.7786376;2.3035209
+0260111E;Ecole Pr D'esthestique/coiffur Epec Beatrice Auger;13 Rue Mirabel Chambaud;26000;Valence;44.9310141;4.8878778
+0333124S;Ecole Pr Fc Girondins Bx Fc Girondins Bordeaux;Rue Joliot Curie;33187;Le Haillan;44.8776541;-0.672552
+0133147A;Ecole Pr Internat. Esthetique Francoise Morice;28 Rue Pierre Et Marie Curie;13100;Aix En Provence;43.5324645;5.4480192
+0331574G;Ecole Pr Pigier;23 Quai De Paludate;33800;Bordeaux;44.8292492;-0.5550825
+0753486G;Ecole Pratique Des Hautes Etudes;45 Rue Des Ecoles;75005;Paris 05;48.8497216;2.3442935
+0352267Z;Ecole Privée Coif Esthetique Academy Jacques Bedfert;24 Boulevard Solferino;35000;Rennes;48.1042709;-1.6691883
+0861398C;Ecole Privée D Esthetique Agnes Briat;12 Passage Boncenne;86000;Poitiers;46.5835089;0.3405425
+0371561W;Ecole Prof. De Coiffure;213 Rue Febvotte;37000;Tours;47.3754237;0.6818723
+0511678X;Ecole Prof. Ouvriere Agricole;1 Rue Du Levant;51600;Somme Suippe;49.1144052;4.5829985
+0693500E;Ecole Reconversion Prof. Georges Guynemer;37 Rue Challemel Lacour;69364;Lyon 07;45.7299676;4.836691
+0875057Z;Ecole Reg. Metiers De Coiffure;26 Bis Rue Ferdinand Buisson;87000;Limoges;45.8251395;1.2561492
+0022101Y;Ecole Secondaire Haute Picardi Haute Picardie;9 Rue Dachery;02100;St Quentin;49.842455;3.2941865
+0672656Y;Ecole Secondaire Privée Beth Hannah;59 Rue Du Faubourg De Pierre;67000;Strasbourg;48.5896482;7.7450122
+0780812J;Ecole Secondaire Privée British School Of Paris;38 Quai De L Ecluse;78290;Croissy Sur Seine;48.8737663;2.126996
+0671616T;Ecole Secondaire Privée Centre Eshel;19 Rue Schweighaeuser;67000;Strasbourg;48.5862803;7.7635035
+0061636W;Ecole Secondaire Privée Cours Albert Camus;3 Avenue Paderi;06200;Nice;43.6894342;7.2347465
+0061541T;Ecole Secondaire Privée Cours Du Prado;4 Avenue Prince De Galles;06400;Cannes;43.5583829;7.022587
+0061243U;Ecole Secondaire Privée Cours Frederic Mistral Ii;2 Boulevard Marechal Juin;06800;Cagnes Sur Mer;43.6624639;7.1505143
+0060687P;Ecole Secondaire Privée Cours Henri Iv;5 Rue Gustave Deloye;06000;Nice;43.6993942;7.2696539
+0332756S;Ecole Secondaire Privée Cours Polles;47 Rue Nicot;33300;Bordeaux;44.8337782;-0.5865275
+0831317F;Ecole Secondaire Privée Cours Privé Mirabeau;25 Rue Mirabeau;83000;Toulon;43.1274443;5.9298803
+0830102K;Ecole Secondaire Privée Cours Saint Dominique;Quartier Pre Tuillieres;83170;La Celle;43.393994;6.041015
+0672582T;Ecole Secondaire Privée Cours Saint-thomas D'aquin;Le Mullerhof Muhlbach/bruche;67280;Urmatt;48.52264;7.312944
+0332759V;Ecole Secondaire Privée Crs Secondaire Privé Peret;43 Bis Rue Du Perigord;33160;St Medard En Jalles;44.8992335;-0.6985045
+0920901E;Ecole Secondaire Privée D'aguesseau;16 Rue D Aguesseau;92100;Boulogne Billancourt;48.8443346;2.2363708
+0781901T;Ecole Secondaire Privée Dali;45 Rue De Montval;78160;Marly Le Roi;48.8738951;2.0902186
+0371442S;Ecole Secondaire Privée Du Perreux;Chateau Du Perreux;37530;Nazelles Negron;47.4127;0.950414
+0332808Y;Ecole Secondaire Privée Dwight Moody;247 Boulevard Marechal Leclerc;33000;Bordeaux;44.8279564;-0.5935838
+0911496G;Ecole Secondaire Privée E.s.e.d.;88 Rue De La Division Leclerc;91160;Saulx Les Chartreux;48.6897758;2.2623625
+0681792D;Ecole Secondaire Privée Ecole Mathias Grunewald;4 Rue Herzog;68124;Wintzenheim;48.0854575;7.3187032
+0061117G;Ecole Secondaire Privée Institut Mediterraneen D'etude;21 Rue Meyerbeer;06000;Nice;43.6967782;7.2605294
+0881710F;Ecole Secondaire Privée Institution St Dominique;184 Rue Du Clos Mariotte;88460;La Baffe;48.1596058;6.576138
+0831326R;Ecole Secondaire Privée Institution St Joseph;269 Avenue Alphonse Daudet;83300;Draguignan;43.5415395;6.4538543
+0061393G;Ecole Secondaire Privée International School Of Nice;15 Avenue Claude Debussy;06200;Nice;43.6854715;7.1997893
+0332919U;Ecole Secondaire Privée Jacques Prevert;173 Rue Du Jardin Public;33300;Bordeaux;44.8562027;-0.5732713
+0781902U;Ecole Secondaire Privée Jeanne D'arc;35 Rue Remilly;78000;Versailles;48.8115151;2.1400778
+0110840D;Ecole Secondaire Privée La Clarte-dieu;Le Cammazou;11270;Fanjeaux;43.195433;2.049444
+0573122J;Ecole Secondaire Privée L'etoile Du Matin;112 Route De Waldeck;57230;Eguelshardt;49.0251882;7.5131059
+0060692V;Ecole Secondaire Privée Lycée Privé Michelet;48 Rue Gioffredo;06000;Nice;43.6992502;7.272996
+0950757G;Ecole Secondaire Privée Montaigne;2-4 Rue Etienne Fourmont;95220;Herblay;48.9916461;2.1607171
+0061392F;Ecole Secondaire Privée Mougins School;615 Avenue Docteur Donat;06250;Mougins;43.6112691;7.0172605
+0022006V;Ecole Secondaire Privée Notre Dame Des Victoires;Rue Du Chateau;02120;Le Herie La Vieville;49.824039;3.647624
+0332601Y;Ecole Secondaire Privée Notre Dame Du Rosaire;Les Cordeliers;33490;St Macaire;44.561713;-0.231533
+0061229D;Ecole Secondaire Privée Or Torah;2 Av Villebois Mareuil;06000;Nice;43.7096465;7.272545
+0572029W;Ecole Secondaire Privée Pilatre De Rozier;6 Rue De L'ermitage;57160;Lessy;49.1192846;6.0963774
+0110912G;Ecole Secondaire Privée Saint Joseph Des Carmes;Les Carmes;11290;Montreal;43.2145699;2.1848932
+0922221P;Ecole Secondaire Privée St Bernard;1 Pl Des Trois Freres Rocquigny;92400;Courbevoie;48.8965949;2.2595784
+0782131T;Ecole Secondaire Privée St Dominique;18 20 Avenue Charles De Gaulle;78230;Le Pecq;48.8874253;2.1020683
+0360671J;Ecole Secondaire Privée St Michel;5 Rue Du Chateau;36250;Niherne;46.8363903;1.5639221
+0920930L;Ecole Secondaire Privée St Pie X;19 Rue Des Ecoles;92210;St Cloud;48.8417127;2.2178743
+0561404X;Ecole Secondaire Privée St Thomas D'aquin;Pontcalec;56240;Berne;47.99546;-3.393066
+0352236R;Ecole Secondaire Privée Ste Marie;Le Bois Martin;35430;St Pere;48.5885182;-1.9175387
+0910844Y;Ecole Secondaire Prof Privée Mg Formation - Escade;98 Allee Des Champs Elysees;91042;Evry;48.63019;2.4243841
+0021486E;Ecole Secondaire Prof.privee;3 Rue De La Mairie;02850;Courtemont Varennes;49.054245;3.5755585
+0910847B;Ecole Secondaire Prof.privee Air-france;Centre D Instruction Vilgenis;91300;Massy;48.730756;2.27137
+0060810Y;Ecole Secondaire Prof.privee Balzac;53 Boulevard Carnot;06400;Cannes;43.5586532;7.0166352
+0110063J;Ecole Secondaire Prof.privee Beaute Et Coiffure;10 Rue Du Luxembourg;11100;Narbonne;43.1809088;3.0021156
+0133404E;Ecole Secondaire Prof.privee C.n.a.t. (coiffure Esth.);Rue Paul Marcel Les Rayettes;13500;Martigues;43.404811;5.053728
+0512015N;Ecole Secondaire Prof.privee Capucine;28 Bis Rue De Courcelles;51100;Reims;49.2603658;4.0195699
+0780026E;Ecole Secondaire Prof.privee Ccip;25 Avenue Des Robaresses;78570;Andresy;48.9871366;2.0579322
+0711717F;Ecole Secondaire Prof.privee Chatelet Coiffure;6 Et 15 Rue St Georges;71100;Chalon Sur Saone;46.7818229;4.8552658
+0061637X;Ecole Secondaire Prof.privee Coiffure Ecole Balzac;28 Boulevard Carabacel;06000;Nice;43.7044827;7.2738384
+0133514Z;Ecole Secondaire Prof.privee Coiffure Esthetique A.f.q.a.;1 Rue Oswald Ortis;13500;Martigues;43.4018692;5.0578334
+0022026S;Ecole Secondaire Prof.privee Coiffure Et Esthetique;19 Bis Boulevard Leon Blum;02100;St Quentin;49.8412959;3.292847
+0133229P;Ecole Secondaire Prof.privee Coiffure Et Esthetique;3 Impasse De L Escouniere;13127;Vitrolles;43.450964;5.2466691
+0332588J;Ecole Secondaire Prof.privee Coiffure Et Esthetique;161 Rue Guillaume Leblanc;33000;Bordeaux;44.8312081;-0.5964205
+0831412J;Ecole Secondaire Prof.privee Coiffure Et Esthetique;1 Rue D Entraigues;83170;Brignoles;43.4059946;6.0612509
+0831393N;Ecole Secondaire Prof.privee Coiffure Performances;80 Avenue Marechal Foch;83000;Toulon;43.1265235;5.9242973
+0060701E;Ecole Secondaire Prof.privee Cours Azur;56 Av Marechal Gallieni;06400;Cannes;43.5586412;7.0176078
+0142159W;Ecole Secondaire Prof.privee Ctre Formation Stade Malherbe;23 Boulevard Georges Pompidou;14064;Caen;49.1808568;-0.3974758
+0371413K;Ecole Secondaire Prof.privee Ctre Promotion De La Coiffure;4 Rue Rene Besnard;37000;Tours;47.3891021;0.699764
+0731207R;Ecole Secondaire Prof.privee Ctre Tech Hotel L'arlequin;Route Nationale 6;73802;Montmelian;45.5068086;6.0799376
+0021592V;Ecole Secondaire Prof.privee D Hotellerie-dosnon;2 Rue De L Eglise;02220;Couvrelles;49.3389541;3.4900193
+0781692R;Ecole Secondaire Prof.privee De Coiffure Des Yvelines;Rue A Honegger Rce Des Forets;78100;St Germain En Laye;48.898908;2.093761
+0110958G;Ecole Secondaire Prof.privee De Prothese Dentaire;2 Rue Simon Castan;11100;Narbonne;43.1849396;2.9975438
+0110981G;Ecole Secondaire Prof.privee D'esthetique;10 Rue Du Luxembourg;11100;Narbonne;43.1809088;3.0021156
+0340927Y;Ecole Secondaire Prof.privee Duclaux;4 Avenue Georges Clemenceau;34000;Montpellier;43.6049943;3.8743094
+0190070Y;Ecole Secondaire Prof.privee Ec Application Travaux Pub;Av Des Papes Limousins;19300;Egletons;45.40101;2.0529799
+0331557N;Ecole Secondaire Prof.privee Ec Tech Esthetique;1 Allee De Chartres;33000;Bordeaux;44.8474789;-0.5712593
+0672682B;Ecole Secondaire Prof.privee Ec.privee Carrieres De La Mode;1 Bis Rue De La Course;67000;Strasbourg;48.5832604;7.7377959
+0332720C;Ecole Secondaire Prof.privee Ec.tech.privee Coiffure;71 Crs Anatole France;33000;Bordeaux;44.8350088;-0.587269
+0641807S;Ecole Secondaire Prof.privee Ecole Coiffure Centre Jorlis;Centre Jorlis;64600;Anglet;43.481402;-1.514699
+0840087N;Ecole Secondaire Prof.privee Ecole Des Arts De La Coiffure;6 Boulevard Saint Michel;84000;Avignon;43.942479;4.8093831
+0061766M;Ecole Secondaire Prof.privee Ecole Europeenne Esthetique;23 Boulevard Dubouchage;06000;Nice;43.7017687;7.2716067
+0681616M;Ecole Secondaire Prof.privee Ecole Vendome;5 Avenue De Colmar;68200;Mulhouse;47.7504281;7.3386179
+0381725U;Ecole Secondaire Prof.privee Elag;31 Rue De La Bajatiere;38100;Grenoble;45.1792916;5.7385752
+0301527X;Ecole Secondaire Prof.privee Espace Robert Bourgier;65 Avenue Jean Jaures;30900;Nimes;43.8276135;4.3533655
+0660756N;Ecole Secondaire Prof.privee Esthet.coiffure Giorgifont Ii;24 24bis Avenue Louis Torcatis;66000;Perpignan;42.7040138;2.890867
+0190834D;Ecole Secondaire Prof.privee Esthetique;22 Rue Andre Devaux;19100;Brive La Gaillarde;45.159555;1.533937
+0341651K;Ecole Secondaire Prof.privee Esthetique Coiffure Giorgifont;16 18 Rue Durand;34000;Montpellier;43.6048143;3.8783242
+0641900T;Ecole Secondaire Prof.privee Esthetique Cosmetique;5bis Et 45 Rue Marechal Joffre;64000;Pau;43.2952632;-0.3731234
+0870913V;Ecole Secondaire Prof.privee Esthetique Cosmetique;2 Bis Rue Gustave Nadaud;87000;Limoges;45.8351859;1.2538931
+0110820G;Ecole Secondaire Prof.privee Esthetique Cosmetique Cybele;8 Boulevard Commandant Roumens;11000;Carcassonne;43.2104305;2.3544704
+0371411H;Ecole Secondaire Prof.privee Etc Esthetique;49 Et 51 Rue Laponneraye;37000;Tours;47.3819579;0.6901009
+0340922T;Ecole Secondaire Prof.privee Etec Formatep;24 26 Avenue Du Pont Juvenal;34000;Montpellier;43.6062402;3.8851375
+0671634M;Ecole Secondaire Prof.privee Europeenne De Beaute;3 Rue De Turenne;67300;Schiltigheim;48.6126471;7.7301404
+0133218C;Ecole Secondaire Prof.privee Football Club;31 Chemin De Paradis;13500;Martigues;43.4077601;5.0489976
+0641911E;Ecole Secondaire Prof.privee Formation Coiffure;Rue Louis Barthou;64000;Pau;43.2944096;-0.3675756
+0211836G;Ecole Secondaire Prof.privee Futura;6 Rue Du Cap Vert;21800;Quetigny;47.3105834;5.0934395
+0382818G;Ecole Secondaire Prof.privee Grenoble Football 38;18 Chemin De La Poterne;38100;Grenoble;45.1726906;5.7475301
+0332868N;Ecole Secondaire Prof.privee I.p.s.o.;77 Rue Lecocq;33000;Bordeaux;44.834317;-0.5862338
+0370759Z;Ecole Secondaire Prof.privee I.s.t.;8 Rue Du Cygne;37000;Tours;47.3955648;0.6912651
+0951707P;Ecole Secondaire Prof.privee Igesa;23 Rue Du General Leclerc;95780;La Roche Guyon;49.0823097;1.6318458
+0131518E;Ecole Secondaire Prof.privee Igesec Ipec;64 Rue Seneque;13300;Salon De Provence;43.6422595;5.0935196
+0190071Z;Ecole Secondaire Prof.privee Inst Brivis Sup Adm Et Commerc;1 Rue Ernest Rupin;19100;Brive La Gaillarde;45.161126;1.5284506
+0831554N;Ecole Secondaire Prof.privee Inst Var Form Metiers Football;Avenue Aristide Briand;83200;Toulon;43.1263887;5.8994285
+0040496T;Ecole Secondaire Prof.privee Institut Avenir Provence;456 Bd St Joseph Z.i.st Joseph;04100;Manosque;43.835744;5.790916
+0542235L;Ecole Secondaire Prof.privee Institut Superieur Decoration;12 Rue De Tivoli;54400;Longwy;49.5293194;5.7649789
+0541921V;Ecole Secondaire Prof.privee Int Esthetique Haute Coiffure;19 Rue Saint Lambert;54000;Nancy;48.687104;6.1665792
+0131426E;Ecole Secondaire Prof.privee Jeanne Michaud;5 Rue Pisancon;13001;Marseille 01;43.2940239;5.3780345
+0370755V;Ecole Secondaire Prof.privee La Chaumette;39 Rue De La Chaumette;37304;Joue Les Tours;47.3579712;0.6500568
+0840090S;Ecole Secondaire Prof.privee Les Cigales;670 Rue Meyne Claire;84100;Orange;44.127041;4.8278237
+0741410F;Ecole Secondaire Prof.privee Maestris Beaute;2 Bis Avenue Zanaroli;74600;Seynod;45.8947154;6.1133921
+0830120E;Ecole Secondaire Prof.privee Objectif 3;154 Avenue Philippe Lebon;83000;Toulon;43.1238546;5.9401956
+0930960N;Ecole Secondaire Prof.privee Ort;39-45 Rue Raspail;93107;Montreuil;48.8528323;2.424645
+0100072Z;Ecole Secondaire Prof.privee Pariset;1 Rue Victorien Sardou;10000;Troyes;48.3001866;4.0506345
+0133211V;Ecole Secondaire Prof.privee Prothesistes Dentaires;11 Av Du General Brosset;13009;Marseille 09;43.2660279;5.4062018
+0341882L;Ecole Secondaire Prof.privee Ruffel;5 Rue Gabriel Peri;34200;Sete;43.4052616;3.6958817
+0341123L;Ecole Secondaire Prof.privee Sarl La Coquille;Route De Boujan;34500;Beziers;43.3663036;3.2315349
+0030867B;Ecole Secondaire Prof.privee Soins Esthetiques;3 Rue President Roosevelt;03200;Vichy;46.1245309;3.4216715
+0332935L;Ecole Secondaire Prof.privee Solange Lourie;8 Cours 30 Juillet;33000;Bordeaux;44.8432096;-0.5743866
+0331597G;Ecole Secondaire Prof.privee St Francois Xavier;181 Rue St F Xavier;33173;Gradignan;44.7753179;-0.5896844
+0341691D;Ecole Secondaire Prof.privee Sup Odontologique;13 Rue Claude Chappe;34000;Montpellier;43.6108708;3.9010094
+0780745L;Ecole Secondaire Tech.privee La Maison;1 Rue Louis Massotte;78530;Buc;48.7737404;2.1264434
+0753574C;Ecole Speciale De Mecanique Et D'electricite;4 Rue Blaise Desgoffe;75006;Paris 06;48.8459266;2.3242783
+0560068V;Ecole Speciale Militaire De Saint Cyr;St Cyr Coetquidan;56381;Guer;47.9394226;-2.1330897
+0772219U;Ecole Sup Infor Et Genie Telec;1 Rue Du Port De Valvins;77215;Avon;48.4247422;2.7446464
+0171523N;Ecole Sup. Innov. Concep. Simu Esics Formation;3 R Alfred Kastler Les Minimes;17000;La Rochelle;46.160329;-1.151139
+0180888T;Ecole Sup.esthetique Du Centre Estec;8d Cours Des Jacobins;18000;Bourges;47.0825143;2.3953601
+0492246A;Ecole Superieure Angevine D'informatique Et De Productique;18 Rue Du 8 Mai 1945;49180;St Barthelemy D Anjou;47.4636468;-0.4970921
+0753560M;Ecole Superieure D Informatique Electronique Automatique;9 Rue Vesale;75005;Paris 05;48.8379228;2.352546
+0490072M;Ecole Superieure D'agriculture D'angers;55 Rue Rabelais;49007;Angers;47.4586985;-0.5450618
+0310154Z;Ecole Superieure D'agriculture De Purpan Toulouse;75 Voie Du Toec;31076;Toulouse;43.6019615;1.4036581
+0951803U;Ecole Superieure De Chimie Organique Et Minerale Escom;13 Boulevard De L'hautil;95092;Cergy;49.03288;2.0788717
+0693623N;Ecole Superieure De Chimie Physique Electronique De Lyon;43 Bd Du 11 Novembre 1918;69616;Villeurbanne;45.7791704;4.8728736
+0920672F;Ecole Superieure De Fonderie;12 Rue Berthelot La Defense;92400;Courbevoie;48.8978987;2.2180596
+0595714R;Ecole Superieure De Metrologie;941 Rue Charles Bourseul;59508;Douai;50.3757844;3.0671778
+0753429V;Ecole Superieure De Physique Et De Chimie Industrielles;10 Rue Vauquelin;75005;Paris 05;48.8413208;2.3477955
+0011293A;Ecole Superieure De Plasturgie D'oyonnax;85 Rue Henri Becquerel;01100;Bellignat;46.242107;5.629371
+0351781W;Ecole Superieure D'electricite;Av De La Boulaie;35511;Cesson Sevigne;48.1230581;-1.6253834
+0911494E;Ecole Superieure D'electricite Privée Supelec;Plateau Du Moulon;91192;Gif Sur Yvette;48.7082355;2.1628765
+0573492L;Ecole Superieure D'electricite Supelec De Metz;2 Rue Edouard Belin;57078;Metz;49.1046825;6.2201426
+0490075R;Ecole Superieure D'electronique De L'ouest;4 Rue Merlet De La Boulaye;49009;Angers;47.478419;-0.563166
+0642012P;Ecole Superieure Des Affaires Groupe Esa Ipa;Rue Jean Zay;64000;Pau;43.3185461;-0.3226804
+0721575W;Ecole Superieure Des Geometres Topographes;1 Rue Pythagore;72000;Le Mans;48.0181117;0.1551443
+0941875C;Ecole Superieure Des Industries Du Caoutchouc;60 Rue Auber;94408;Vitry Sur Seine;48.8026632;2.3746261
+0880077F;Ecole Superieure Des Industries Textiles D'epinal;85 Rue D'alsace;88025;Epinal;48.166412;6.4466764
+0762378X;Ecole Superieure D'ingenieurs En Genie Electrique;1 Rue Du Marechal Juin;76131;Mont St Aignan;49.4630688;1.0638285
+0910725U;Ecole Superieure D'optique;Ctre Scientifique Bat 503;91403;Orsay;48.7068033;2.1741725
+0672881T;Ecole Superieure D'optique;26 Rue Des Magasins;67000;Strasbourg;48.591175;7.7408775
+0442278M;Ecole Superieure Du Bois;Rue Christian Pauc;44306;Nantes;47.2828202;-1.5157572
+0932078D;Ecole Superieure Du Soudage Et De Ses Applications;90 Rue Des Vanesses;95942;Roissy En France;48.9736945;2.5064275
+0492202C;Ecole Superieure Et D Application Du Genie;106 Rue Elbe;49041;Angers;47.4558692;-0.5656686
+0351842M;Ecole Superieure Et D'application Des Transmissions;Avenue De La Touraudais;35510;Cesson Sevigne;48.1212814;-1.6302067
+0921929X;Ecole Superieure Privée;3 Rue Jules Verne;92300;Levallois Perret;48.894939;2.2989649
+0133885C;Ecole Superieure Privée Studio M;29 Boulevard Charles Nedelec;13003;Marseille 03;43.3031966;5.3785285
+0312745R;Ecole Tech Priv D'esthetique Esther Mario;28 Rue De Metz;31000;Toulouse;43.6002386;1.4434844
+0561654U;Ecole Tech Privée Coif Estheti Scotto Di Cesare;2 Allee De Laroiseau;56000;Vannes;47.668463;-2.7838366
+0332918T;Ecole Tech Privée Creasud;8 Rue Du General Cheyron;33100;Bordeaux;44.8398802;-0.5563023
+0292186U;Ecole Techn Privée De Coiffure Chantal Conan;59 Rue De Brest;29800;Landerneau;48.4509184;-4.2574215
+0731479L;Ecole Technique De Coiffure E.c.m. Chambery;357 Faubourg Montmelian;73000;Chambery;45.5663336;5.9304253
+0501515A;Ecole Technique Preparatoire;Ec Tech Armt De La Marine;50100;Cherbourg Octeville;49.633998;-1.613426
+0781858W;Ecole Technique Privé Iscg De Commerce Et De Gestion;76 Rue Du Marechal Lyautey;78100;St Germain En Laye;48.8949513;2.0959937
+0290204P;Ecole Technique Privée Academie Bretagne Coiffure;52 Ter Rue De Douarnenez;29000;Quimper;47.9990273;-4.110778
+0383053M;Ecole Technique Privée Academy;21 Rue Boucher De Perthes;38000;Grenoble;45.1842117;5.7060419
+0271601U;Ecole Technique Privée Alm Evreux Basket;1 Avenue Aristide Briard;27040;Evreux;49.0270129;1.151361
+0292261A;Ecole Technique Privée Anaho Pigier;51 Rue Traverse;29200;Brest;48.3868325;-4.4923533
+0561862V;Ecole Technique Privée Anaho Pigier;87 Rue De Lanveur;56100;Lorient;47.7439993;-3.3914865
+0541973B;Ecole Technique Privée Asnl;Parc De Haye;54840;Velaine En Haye;48.704136;6.069881
+0352178C;Ecole Technique Privée Association Odorico;6 Rue Du Moulin Du Comte;35000;Rennes;48.1084383;-1.7099738
+0530074T;Ecole Technique Privée Beaute Coiffure Formation;38-40 Rue De Paris;53000;Laval;48.0712391;-0.7628592
+0261436V;Ecole Technique Privée Centre De Formation De La Coif;25 Rue Frederic Chopin;26000;Valence;44.9258887;4.9229792
+0791083Y;Ecole Technique Privée Chamois Niortais Football Club;66 Rue H Sellier;79001;Niort;46.3166545;-0.4930179
+0350805K;Ecole Technique Privée Chantal Le Cozic;52 Bd Villebois Mareuil;35000;Rennes;48.105558;-1.6507469
+0430988R;Ecole Technique Privée Coiffure Du Velay;99 Avenue Charles Dupuy;43700;Brives Charensac;45.0487048;3.9209091
+0541361L;Ecole Technique Privée Compagnie De Formation-pigier;43 Cours Leopold;54000;Nancy;48.6972591;6.1737517
+0281111F;Ecole Technique Privée De La Coiffure;15 Rue D'etampes;28000;Chartres;48.4471028;1.5129974
+0602084P;Ecole Technique Privée Des Jeunes Sportifs De L'oise;171 Avenue Marcel Dassault;60000;Beauvais;49.4489162;2.0963607
+0811312L;Ecole Technique Privée Des Lgt Et Lp Privés Notre Dam;Avenue D'hauterive;81101;Castres;43.599188;2.244541
+0312711D;Ecole Technique Privée Du Stade Toulousain;114 Rue Des Troenes;31022;Toulouse;43.620776;1.416174
+0672119P;Ecole Technique Privée Ec Internationle Tunon;15 Rue Du Fosse Des Treize;67000;Strasbourg;48.5879967;7.7473844
+0060907D;Ecole Technique Privée Ec Tech D'esthetique Gontard;47 Rue Hotel Des Postes;06000;Nice;43.6991792;7.2705291
+0331579M;Ecole Technique Privée Ec Tech Jamet Buffereau;67 Cours Pasteur;33800;Bordeaux;44.8326055;-0.5738464
+0641386J;Ecole Technique Privée Ec.d Esthetique;80 Rue De Madrid;64201;Biarritz;43.4673441;-1.5713779
+0261286G;Ecole Technique Privée Epseco;Le Forum 7 Avenue De Verdun;26000;Valence;44.9382727;4.8994976
+0131515B;Ecole Technique Privée Esthetique Parfumerie;149 Rue De Rome;13006;Marseille 06;43.2889789;5.3821494
+0382050X;Ecole Technique Privée Faugier Hays;38 Rue D'alembert;38000;Grenoble;45.1880988;5.7110881
+0881383A;Ecole Technique Privée Formapole-epinal;19 Rue Paul Oulmont;88005;Epinal;48.163398;6.4506308
+0710134K;Ecole Technique Privée Fpt-powertrain Technologies Fr;79, Avenue Puzenat;71140;Bourbon Lancy;46.6261932;3.7495169
+0161183Z;Ecole Technique Privée Hair Styl'mak'up Academy;8 Route De Bordeaux;16000;Angouleme;45.6522967;0.1584877
+0352550G;Ecole Technique Privée Iffdec;8 Quai Robinot De St Cyr;35000;Rennes;48.1055424;-1.717577
+0060777M;Ecole Technique Privée Ilec;12 Boulevard Dubouchage;06000;Nice;43.702245;7.2722329
+0060818G;Ecole Technique Privée Ilec;10 Rue Mozart;06400;Cannes;43.5581488;7.0181123
+0580083A;Ecole Technique Privée Institut Superieur Techniciens;5 Rue Des Francs Bourgeois;58000;Nevers;46.9912616;3.1626878
+0571998M;Ecole Technique Privée Isct;2 Rue Des Parmentiers;57000;Metz;49.1159983;6.177431
+0451468B;Ecole Technique Privée Loiret Orleans Judo;Rue Fernand Pelloutier;45016;Orleans;47.920072;1.888645
+0261392X;Ecole Technique Privée Maestris;19 Avenue Victor Hugo;26000;Valence;44.9294864;4.890829
+0830662U;Ecole Technique Privée Maestris;10 Rue Truguet;83000;Toulon;43.1250211;5.9349305
+0631039M;Ecole Technique Privée Michelin;15 Place Des Carmes;63040;Clermont Ferrand;45.7767324;3.0881041
+0561854L;Ecole Technique Privée Morriss 56;1 Bis Rue Francois D'argouges;56000;Vannes;47.6620426;-2.7567142
+0891118G;Ecole Technique Privée Moulin De Preuilly;Route De Vaux;89000;Auxerre;47.7833102;3.593685
+0301500T;Ecole Technique Privée Nimes Olympique Association;123 Avenue De La Bouvine;30023;Nimes;43.8173886;4.358485
+0292208T;Ecole Technique Privée Oscelorn;59 Rue De Brest;29800;Landerneau;48.4509184;-4.2574215
+0382170C;Ecole Technique Privée Paul Louis Merlin;41 Rue Henri Wallon;38400;St Martin D Heres;45.1738193;5.7608852
+0383347G;Ecole Technique Privée Pigier;5 Avenue Marcellin Berthelot;38000;Grenoble;45.1799156;5.7316907
+0381730Z;Ecole Technique Privée Pro' Style Formation;2 Rue Henri Chatelier;38000;Grenoble;45.1872052;5.7117155
+0783465T;Ecole Technique Privée Psa Peugeot Citroen Et3pc;1 A 3 Bd De L Europe;78300;Poissy;48.9347739;2.0451355
+0660655D;Ecole Technique Privée Rive Gauche;11 Rue Du Docteur Pous;66000;Perpignan;42.6992586;2.889932
+0340937J;Ecole Technique Privée Ruffel;Route Nationale 113;34290;Servian;43.397764;3.3692659
+0133369S;Ecole Technique Privée Saint Francois De Sales;20 Bd Madeleine Remuzat;13384;Marseille 13;43.296482;5.36978
+0131499J;Ecole Technique Privée Soins Esthet Et Coiffure;1 Boulevard Dugommier;13001;Marseille 01;43.2976149;5.3808487
+0341793P;Ecole Technique Privée Studio M;3320 Boulevard Paul Valery;34070;Montpellier;43.5907556;3.8548311
+0132309P;Ecole Technique Privée Susini Esthetique;Le California Bat D;13090;Aix En Provence;43.5388336;5.4044268
+0382896S;Ecole Technique Privée Univeria;27 Rue De Turenne;38100;Grenoble;45.1838167;5.720223
+0711856G;Ecole Technique Privée Univeria;71 Rue Jean Mace;71000;Macon;46.3163972;4.8245756
+0383383W;Ecole Technologique Privée;13 Place Du Triforium;38080;L Isle D Abeau;45.6208808;5.2221078
+0741692M;Ecole Technologique Privée;166 Route De Livron;74100;Vetraz Monthoux;46.1744216;6.2574083
+0731532U;Ecole Technologique Privée Agfp;25 Rue Croix D'or;73000;Chambery;45.5647917;5.9239496
+0693401X;Ecole Technologique Privée Arts Appliques Bellecour;3 Place Bellecour;69002;Lyon 02;45.7575832;4.8320746
+0750106H;Ecole Technologique Privée Ccip Ctre Format Industrielles;247 Avenue Gambetta;75020;Paris 20;48.8755533;2.4057617
+0383349J;Ecole Technologique Privée Charmilles Isf;8 Rue Du Tour De L Eau;38400;St Martin D Heres;45.1865579;5.7726517
+0651026M;Ecole Technologique Privée Concept Formation;54 Avenue Bertrand Barere;65000;Tarbes;43.2373153;0.0712894
+0693451B;Ecole Technologique Privée De Conde;5 Place Gensoul;69002;Lyon 02;45.7517842;4.8243867
+0740283F;Ecole Technologique Privée De Poisy;;74330;Poisy;45.921423;6.063557
+0061542U;Ecole Technologique Privée E.s.c.c.o.m.;22 Rue El Nouzah;06000;Nice;43.7077627;7.2773541
+0754101A;Ecole Technologique Privée Ec Dessin Techn Artist Sornas;108 Rue Saint Honore;75001;Paris 01;48.8615803;2.3418634
+0931906S;Ecole Technologique Privée Ecofih;79 Rue De Paris;93000;Bobigny;48.898946;2.4435126
+0840083J;Ecole Technologique Privée Ecole Hoteliere D Avignon;Allee Des Fenaisons;84032;Avignon;43.9272966;4.8473351
+0596455W;Ecole Technologique Privée Ecole Superieure De Tourisme;70 Rue De Bouvines;59000;Lille;50.6356752;3.0878986
+0160080A;Ecole Technologique Privée Estac;29 Rue Montalembert;16000;Angouleme;45.6465899;0.1677237
+0831631X;Ecole Technologique Privée Esthetique Veronique Harlaut;146 Boulevard Marechal Foch;83000;Toulon;43.1268203;5.9234125
+0763365V;Ecole Technologique Privée Fc Rouen;48 Avenue Des Canadiens;76140;Le Petit Quevilly;49.4115983;1.072111
+0312346G;Ecole Technologique Privée Gamma;1 Rue Felix Debax;31700;Blagnac;43.6323187;1.3975413
+0596770N;Ecole Technologique Privée I.s.c.o.m;7 Av De L'architecte Cordonnier;59000;Lille;50.6355544;3.0440254
+0595877T;Ecole Technologique Privée Institut De Gestion Hoteliere;17 Place Charles Roussel;59200;Tourcoing;50.7215161;3.1568082
+0062035E;Ecole Technologique Privée International Business School;1240 Route Des Dolines;06560;Valbonne;43.6252096;7.0403607
+0061992H;Ecole Technologique Privée Ispecc;21 Boulevard Grosso;06000;Nice;43.6958857;7.2516559
+0731527N;Ecole Technologique Privée Itcc;Place Maurice Mollard;73100;Aix Les Bains;45.688374;5.915667
+0755057P;Ecole Technologique Privée Itecom Inst Techn Communic;12 Rue Du 4 Septembre;75002;Paris 02;48.8695094;2.3374631
+0593035D;Ecole Technologique Privée Kienz;39 Avenue De Flandre;59700;Marcq En Baroeul;50.6653766;3.1073796
+0133283Y;Ecole Technologique Privée Leschi;34 Avenue Sainte Victoire;13100;Aix En Provence;43.5320321;5.4547984
+0383350K;Ecole Technologique Privée Lodima Rhone Alpes;25 Rue De Sassenage;38600;Fontaine;45.1974483;5.6830524
+0752877V;Ecole Technologique Privée Met De La Creation-la Ruche;14 Rue Lally Tollendal;75019;Paris 19;48.8830729;2.3742869
+0062012E;Ecole Technologique Privée Orbicom;2780 Route Nationale 7;06270;Villeneuve Loubet;43.6253366;7.131598
+0311189Z;Ecole Technologique Privée Ort Maurice Grynfogel;14 Rue Etienne Collongues;31770;Colomiers;43.601951;1.333155
+0383393G;Ecole Technologique Privée Performances Concept;;38590;St Etienne De St Geoirs;45.339389;5.34384
+0711884M;Ecole Technologique Privée Pigier;33 Rue De Lyon;71000;Macon;46.3002379;4.8268272
+0741690K;Ecole Technologique Privée Pigier;2 Rue De La Cesiere;74600;Seynod;45.8868104;6.1117354
+0783504K;Ecole Technologique Privée Sportifs De Haut Niveau;4 Bis Avenue Kennedy;78100;St Germain En Laye;48.9146897;2.0854407
+0383369F;Ecole Technologique Privée Sup Formation;22 Cours Senozan;38500;Voiron;45.3654833;5.5909482
+0690693D;Ecole Technologique Privée Supdemod;10 Rue Des Marronniers;69002;Lyon 02;45.7565575;4.8344883
+0312303K;Ecole Technologique Privée Toulouse Arts Appliques;24 Rue Ingres;31000;Toulouse;43.6142705;1.4428243
+0311768D;Ecole Technologique Privée Tunon;3 Rue Albert Lautmann;31000;Toulouse;43.6065141;1.4390729
+0590342B;Emd Douai;941 Rue Charles Bourseul;59508;Douai;50.3757844;3.0671778
+0670189S;Engees Strasbourg;1 Quai Koch;67070;Strasbourg;48.584972;7.7580286
+0492248C;Enihp Angers;2 Rue Le Notre;49045;Angers;47.4791574;-0.6058396
+0631786Z;Enita Clermont-ferrand;Rn 89 Marmilhat;63370;Lempdes;45.7796779;3.17838
+0441679L;Enitiaa Nantes;Rue De La Geraudiere;44322;Nantes;47.264745;-1.56533
+0590338X;Ens Arts Indust Textil Roubaix;2 Pl Martyrs De La Resistance;59070;Roubaix;50.6750202;3.1456381
+0670190T;Ens Arts Industries Strasbourg;24 Boulevard De La Victoire;67084;Strasbourg;48.5822995;7.7649338
+0870862P;Ens Ceramique Iindust Limoges;47 A 73 Av Albert Thomas;87065;Limoges;45.8365048;1.2425704
+0951376E;Ens Electroniq Applicat Cergy;6 Av Du Ponceau;95014;Cergy;49.039012;2.0713216
+0693817Z;Ens Lettres Lyon;15 Parvis Rene Descartes;69007;Lyon 07;0.0;0.0
+0311587G;Ensae Toulouse;10 Avenue Edouard Belin;31055;Toulouse;43.5665729;1.4751039
+0330201P;Ensam Centre D Enseignement Et De Recherche De Bordeaux;Esplanade Des Arts & Metiers;33405;Talence;44.802614;-0.588054
+0710091N;Ensam Centre D Enseignement Et De Recherche De Cluny;Rue Porte De Paris;71250;Cluny;46.434167;4.662638
+0510083N;Ensam Cer Chalons;Rue Saint Dominique;51006;Chalons En Champagne;48.9564418;4.3572928
+0573513J;Ensam Cer Metz;4 Rue Augustin Fresnel;57078;Metz;49.0963218;6.2255916
+9711101V;Enseignement Sup. Technique Formates;4, Rue Achille Rene Boisneuf;97110;Pointe A Pitre;0.0;0.0
+0492247B;Enshap Angers;2 Rue Le Notre;49045;Angers;47.4791574;-0.6058396
+0590341A;Ensiaa Cycle D'etudes En Industries Alimentaires;41 Rue Du Port;59046;Lille;50.6319132;3.0461578
+0310146R;Ensica Toulouse;1 Place Emile Blouin;31056;Toulouse;43.604652;1.444209
+0290125D;Ensieta Brest;2 Rue Francois Verny;29806;Guipavas;48.4198889;-4.4721985
+0920815L;Enspmr Rueil Malmaison;228 232 Av Napoleon Bonaparte;92506;Rueil Malmaison;48.8771709;2.1729069
+0692459Y;Enssib;17 21 Bd Du 11 Novembre 1918;69623;Villeurbanne;45.7795294;4.8611273
+0291811L;Enst Bretagne;Technopole Brest Iroise;29285;Brest;48.3651848;-4.5489314
+0352402W;Enst Bretagne;Rue De La Chataigneraie;35512;Cesson Sevigne;48.1209253;-1.6286523
+0300063F;Enstima Ales;6 Avenue De Clavieres;30319;Ales;44.1331218;4.0883578
+0811200P;Enstimac Albi-carmaux;Campus Jarlart Rte De Teillet;81013;Albi;43.9124307;2.1939809
+0442205H;Enstimn Nantes;4 Rue A Kastler La Chantrerie;44307;Nantes;47.218371;-1.553621
+0100735V;Eplefpa Ets Pub Local Enseig Form Prof Agric;Route De Vielaines;10120;St Pouange;48.221263;4.039121
+0641772D;Epseco;15 Rue Vauban;64100;Bayonne;43.4956281;-1.4801189
+0530939H;Esiea Laval;38 Rue Drs Calmette Et Guerin;53000;Laval;48.077863;-0.770138
+0932019P;Esiee Noisy Le Grand;2 Bd Blaise Pascal Cite Descart;93162;Noisy Le Grand;48.848579;2.55261
+0132396J;Esim Marseille;Technopole De Chateau Gombert;13451;Marseille 13;43.3417;5.43649
+0573389Z;Esitc Metz;6 Rue Marconi Technopole 2000;57070;Metz;49.1005629;6.2173198
+0271338H;Esitpa Rouen;Rue Grande;27106;Val De Reuil;49.2721551;1.2138012
+0753607N;Estp Paris;57 Boulevard Saint Germain;75240;Paris 05;48.8504483;2.3463367
+0783529M;Etab Ens Second Privé Sfef-completude;22 Rue Henri De Regnier;78000;Versailles;48.7934699;2.125999
+0212024L;Etab.national Enseignement Sup;26 Bd Dr Petitjean;21036;Dijon;47.3111605;5.0659413
+0755182A;Etablissement Experimental;48 Avenue Des Gobelins;75013;Paris 13;48.8342909;2.3533682
+0932377D;Etablissement Experimental Auto Ecole;12 Rue De La Liberte;93200;St Denis;48.9438959;2.3653605
+0754401B;Etablissement Experimental Lycée Autogere;393 Rue De Vaugirard;75015;Paris 15;48.8348375;2.2918472
+0772586T;Etablissement Experimental Micro Lycée De Senart;Rue Du Lycée;77552;Moissy Cramayel;0.0;0.0
+0932375B;Etablissement Experimental Nouvelles Chances;25 Rue Des Trois Noyers;93220;Gagny;48.8874111;2.5425454
+0932376C;Etablissement Experimental Nouvelles Chances;146 Av Henri Barbusse;93000;Bobigny;48.9083628;2.42744
+0932396Z;Etablissement Experimental Nouvelles Chances;10 Ave Charles De Gaulle;93152;Le Blanc Mesnil;48.9438125;2.4679399
+0870581J;Ets Public Local Enseignement Agricole Vaseix;Les Vaseix;87430;Verneuil Sur Vienne;45.836771;1.172854
+0762917H;Formavenir Formavenir;28 Place Saint Marc;76000;Rouen;49.4385493;1.1018603
+0341704T;Gpt Interet Public Reclus;17 Rue Abbe De L Epee;34000;Montpellier;43.621583;3.8741726
+0590348H;Hautes Etudes Industrielles;13 Rue De Toul;59046;Lille;50.6339335;3.0449176
+0352422T;I E S I E L;65 Rue De St Brieuc;35042;Rennes;48.1136527;-1.7053288
+0753488J;I.n.a.l.c.o. De Paris;2 Rue De Lille;75343;Paris 07;48.8577011;2.3325963
+0753483D;Inst Fr Froid Ind Genie Climat;292 Rue Saint Martin;75141;Paris 03;48.8667317;2.3542533
+0341013S;Inst Nat Recherche Agronomique;2 Place Pierre Viala;34060;Montpellier;43.6170101;3.8548718
+0332282B;Inst Sces Nature Agroalimentai De Bordeaux;Rue St Jean;33140;Villenave D Ornon;44.795055;-0.5725998
+0753558K;Inst Scien Math Et Econ Appli;14 Rue Corvisart;75013;Paris 13;48.833382;2.346573
+0161165E;Inst Sup Format Alternance Con Isfac;Z I 3 Espace Victor Hugo;16340;L Isle D Espagnac;45.663854;0.199192
+0171501P;Inst Sup Format Alternance Con Isfac;17 Rue Jean Perrin;17000;La Rochelle;46.1405865;-1.1584497
+0520939N;Inst. Rural D'educ. Et D'orien;Rue Du Baron De Beine;52000;Buxieres Les Villiers;48.106388;5.0360569
+0942034A;Inst.nat.rech.transp.securite;2 Av Du Gal Malleret Joinville;94114;Arcueil;48.805173;2.3442021
+0332473J;Instit Rurale Educ Orientation;Domaine De Lagron;33870;Vayres;44.8964799;-0.319015
+0110678C;Institut Agricole Privé Saint Joseph;Av Andre Chenier La Raque;11303;Limoux;43.053289;2.218142
+0691678Z;Institut Agricole Privé Sandar;392 Chemin De La Sabliere;69579;Limonest;45.8348979;4.7678854
+0590345E;Institut Catholique D'arts Et Metiers;6 Rue Auber;59046;Lille;50.6302567;3.0417529
+0442185L;Institut Catholique D'arts Et Metiers De Nantes;35 Av Champ De Manoeuvre;44470;Carquefou;47.2748684;-1.5086799
+0312421N;Institut Catholique Des Arts Et Metiers De Toulouse;75 Avenue De Grande Bretagne;31300;Toulouse;43.5999437;1.4151779
+0753431X;Institut D'etudes Politiques De Paris;27 Rue Saint Guillaume;75341;Paris 07;48.854072;2.3283597
+0912141H;Institut D'informatique D'entreprise;18 Allee Jean Rostand;91025;Evry;48.6269332;2.4313463
+0870997L;Institut D'ingenierie Informatique De Limoges;43 Rue Ste Anne;87015;Limoges;45.8189643;1.2712663
+0631833A;Institut FranÇais De Mecanique Avancee De Clermont-ferrand;Campus Des Cezeaux;63175;Aubiere;45.7690647;3.1153321
+0753465J;Institut National Agronomique Paris Grignon;16 Rue Claude Bernard;75005;Paris 05;48.839766;2.347641
+0690192J;Institut National Des Sciences Appliquees De Lyon;20 Avenue Albert Einstein;69621;Villeurbanne;45.7824151;4.8777701
+0350097R;Institut National Des Sciences Appliquees De Rennes;20 Avenue Des Buttes De Coesmes;35043;Rennes;48.1225975;-1.6377774
+0760165S;Institut National Des Sciences Appliquees De Rouen;Place Emile Blondel;76131;Mont St Aignan;49.458089;1.069146
+0310152X;Institut National Des Sciences Appliquees De Toulouse;135 Avenue De Rangueil;31077;Toulouse;43.5703224;1.4679205
+0910685A;Institut National Des Sciences Et Techniques Nucleaires;Centre D Etudes De Saclay;91191;Gif Sur Yvette;48.7099264;2.164881
+0911781S;Institut National Des Telecommunications;9 Rue Fourier;91000;Evry;48.6254354;2.4432663
+0381912X;Institut National Polytechnique De Grenoble;46 Avenue Felix Viallet;38031;Grenoble;45.1907112;5.7174034
+0541564G;Institut National Polytechnique De Nancy;2 Avenue De La Foret De Haye;54501;Vandoeuvre Les Nancy;48.6543335;6.1342988
+0311381H;Institut National Polytechnique De Toulouse;Place Des Hauts Murats;31006;Toulouse;43.5945193;1.4471792
+0352347L;Institut National Superieur De Formation Agro-alimentaire;65 Rue De St Brieuc;35042;Rennes;48.1136527;-1.7053288
+0900384D;Institut Polytechnique De Sevenans;Rue Du Chateau;90010;Belfort;47.6410762;6.8386557
+0763212D;Institut Rural De Hte Normandi Totes;11 Rue Du General Leclerc;76890;Totes;49.6775732;1.050514
+0611136D;Institut Sup De Plasturgie;Montfoulon;61250;Damigny;48.4450915;0.0590041
+0600071B;Institut Superieur Agricole De Beauvais;19 Rue Pierre Waguet;60026;Beauvais;49.4611762;2.0685274
+0753559L;Institut Superieur D Electronique De Paris;28 Rue Notre Dame Des Champs;75006;Paris 06;48.845356;2.328294
+0590343C;Institut Superieur D'agriculture;41 Rue Du Port;59046;Lille;50.6319132;3.0461578
+0692353H;Institut Superieur D'agriculture Rhone Alpes;31 Place Bellecour;69288;Lyon 02;45.7575832;4.8320746
+0292125C;Institut Superieur D'electronique De Bretagne;20 Rue Cuirasse Bretagne;29604;Brest;48.4066763;-4.4959757
+0831458J;Institut Superieur D'electronique De La Mediterranee;Place Georges Pompidou;83000;Toulon;43.120804;5.937864
+0590347G;Institut Superieur D'electronique Du Nord;41 Boulevard Vauban;59046;Lille;50.6342668;3.0487049
+0721484X;Institut Superieur Des Materiaux Du Mans;44 Av Bartholdi;72000;Le Mans;48.0199119;0.1574886
+0930603A;Institut Superieur Des Materiaux Et Construction Mecanique;3, Rue Fernand Hainaut;93430;Villetaneuse;48.9192718;2.3327857
+0061874E;Institut Superieur D'informatique Et D'automatique;Rue Claude Daunesse;06904;Valbonne;43.6156514;7.051952
+0130238N;Institut Superieur Du Beton Arme-ccimp Groupe Esim Imt;Imt Technopole Chateau Gombert;13451;Marseille 13;43.296482;5.36978
+0693364G;Institut Textile Et Chimique De Lyon;87 Chemin Des Mouilles;69134;Dardilly;45.7863735;4.7690268
+0171552V;Int Rur Educat Et Orientation De La Saintonge Et De L Aunis;15 Avenue De Saintes;17240;St Genis De Saintonge;45.4831161;-0.5674628
+0351780V;Ipssa;Les Hairies;35370;Etrelles;48.055495;-1.1761569
+0351949D;Ipssa;12 Avenue Du Mail;35130;La Guerche De Bretagne;47.9402114;-1.2314923
+0351963U;Ipssa;2 Allee De La Hodeyere;35504;Vitre;48.119326;-1.2031957
+0133347T;Ismea Marseille;Technopole De Chateau Gombert;13451;Marseille 13;43.3417;5.43649
+9830289X;Itfm Nouvelle Caledonie;Rue P Sauvan Anse Vata;98807;Noumea;-22.2976224;166.4471083
+0801885P;Iufm;49 Boulevard De Chateaudun;80044;Amiens;49.8826199;2.2835252
+0133393T;Iufm Aix Marseille;32 Rue Eugene Cas;13004;Marseille 04;43.3129449;5.3982588
+9710939U;Iufm Antilles Guyane;Morne Ferret;97159;Pointe A Pitre;16.241111;-61.533056
+0332826T;Iufm Aquitaine;160 Av De Verdun;33705;Merignac;44.8393097;-0.6294005
+0251762E;Iufm Besancon;Fort Griffon;25042;Besancon;47.242863;6.019874
+0142158V;Iufm Caen;186 Rue De La Delivrande;14053;Caen;49.1997374;-0.3587701
+0631821M;Iufm Clermont Ferrand;20 Avenue Raymond Bergougnan;63039;Clermont Ferrand;45.7833384;3.0703968
+7200164R;Iufm Corse;2 Rue De L'eglise;20250;Corte;42.3048203;9.1497992
+0941936U;Iufm Creteil;Route De Brevannes;94388;Bonneuil Sur Marne;48.7656805;2.5144979
+0211960S;Iufm Dijon;Maison Univ.esplanade Erasme;21009;Dijon;47.3141252;5.0663861
+9830491S;Iufm Du Pacifique;15 Rue De Verdun,bp Mga1;98802;Noumea;-22.2758;166.458
+0382955F;Iufm Grenoble;30 Avenue Marcelin Berthelot;38000;Grenoble;45.1763086;5.7317449
+9741061K;Iufm La Reunion;Allee Des Aigues Marines;97487;St Denis;-20.8939781;55.4425462
+0595851P;Iufm Lille;2 Bis Rue Parmentier;59650;Villeneuve D Ascq;50.6399807;3.1349929
+0871012C;Iufm Limoges;209 Bd De Vanteaux;87036;Limoges;45.823034;1.2270492
+0693480H;Iufm Lyon;5 Rue Anselme;69317;Lyon 04;45.77362;4.8210967
+0341818S;Iufm Montpellier;2 Place Marcel Godechot;34092;Montpellier;43.6198978;3.8692638
+0542255H;Iufm Nancy Metz;5 Rue Paul Richard;54320;Maxeville;48.7104439;6.1644578
+0442199B;Iufm Nantes;4 Chemin Launay Violette;44322;Nantes;47.2497938;-1.5579682
+0061758D;Iufm Nice;89 Avenue Georges V;06046;Nice;43.7135641;7.2699222
+0451482S;Iufm Orleans Tours;72 R Du Faubourg De Bourgogne;45044;Orleans;47.9022511;1.9264231
+0754445Z;Iufm Paris;10 Rue Molitor;75016;Paris 16;48.8452554;2.2652208
+0861249R;Iufm Poitiers;22 Rue De La Tranchee;86000;Poitiers;46.5764592;0.3333732
+0511935B;Iufm Reims;23 Rue Clement Ader;51685;Reims;49.2325574;4.0695783
+0352291A;Iufm Rennes;153 Rue De St Malo;35043;Rennes;48.1215596;-1.6831814
+0762952W;Iufm Rouen;2 Rue Du Tronquet;76131;Mont St Aignan;49.4703815;1.0767393
+0672635A;Iufm Strasbourg;200 Avenue De Colmar;67100;Strasbourg;48.558093;7.7482963
+0312299F;Iufm Toulouse;56 Avenue De L'urss;31078;Toulouse;43.5806199;1.4494941
+0781938H;Iufm Versailles;45 Avenue Des Etats Unis;78000;Versailles;48.8106931;2.1485567
+0470817H;L Professionnel Ste Genevieve Ctr.etu.fem.rur.ste Genevieve;19 Rue Felix;47220;Astaffort;44.0641604;0.6531217
+0441783Z;L.e.p.agricole Privé Lp Agric Pr La Marchanderie;;44150;Ancenis;47.365464;-1.177491
+0180098J;La Compagnie De Formation Pigier;29 Boulevard Gambetta;18000;Bourges;47.0880089;2.3899259
+0311429K;Laboratoire Automatique;7 Avenue Du Colonel Roche;31400;Toulouse;43.5637248;1.4765815
+0311430L;Laboratoire Ctre D'hematologie;Place Du Docteur Baylac;31300;Toulouse;43.6078649;1.3973901
+0311473H;Laboratoire De Biochimie Et Genetique Ce;118 Route De Narbonne;31062;Toulouse;43.5631577;1.4628392
+0311431M;Laboratoire De Pharmacologie Toxicologie;205 Route De Narbonne;31078;Toulouse;43.5564738;1.4656272
+0311274S;Laboratoire D'optique;29 Rue Jeanne Marvig;31055;Toulouse;43.5779576;1.4639799
+0090467J;Laboratoire Souterrain;;09200;Moulis;42.961685;1.091185
+0580055V;Legta De Nevers;243 Route De Lyon;58000;Challuy;46.9475361;3.1579337
+0331493U;Lg Pr Lyc Des Metiers St Genes;160 Rue De Saint Genes;33081;Bordeaux;44.8249282;-0.5813679
+0120024L;Lgt Lycée Des Metiers Alexis Monteil;14 Rue Carnus;12034;Rodez;44.3559366;2.5738937
+0671509B;Lgt Lycée Des Metiers Alphonse Heinrich;123 Route De Strasbourg;67504;Haguenau;48.8001884;7.7656679
+0730029K;Lgt Lycée Des Metiers Ambroise Croizat;244 Avenue De La Liberation;73604;Moutiers;45.4828426;6.5291499
+0601864A;Lgt Lycée Des Metiers Andre Malraux;1 Place Nelson Mandela;60160;Montataire;49.2597525;2.425907
+0060037H;Lgt Lycée Des Metiers Beau Site;38 Avenue D'estienne D'orves;06050;Nice;43.701575;7.245204
+0800001S;Lgt Lycée Des Metiers Boucher De Perthes;1 Rue Paul Delique;80142;Abbeville;50.1023384;1.8422737
+0460010L;Lgt Lycée Des Metiers Champollion;13 15 Av Fernand Pezet;46106;Figeac;44.6090478;2.0281224
+0460007H;Lgt Lycée Des Metiers Clement Marot;59 Rue Des Augustins;46005;Cahors;44.4499879;1.4392957
+0020050U;Lgt Lycée Des Metiers Condorcet;Rond Point Joliot Curie;02100;St Quentin;49.861713;3.293589
+0640044A;Lgt Lycée Des Metiers De La Chimie Albert Camus;Avenue Pierre Angot;64150;Mourenx;43.3703832;-0.6251249
+0570319M;Lgt Lycée Des Metiers De L'industrie;Les Grands Bois;57703;Hayange;49.3168954;6.0671076
+0641779L;Lgt Lycée Des Metiers Du Pays De Soule;Avenue Jean Monnet;64130;Cheraute;43.2265756;-0.8742488
+0570087K;Lgt Lycée Des Metiers Et Des Techno Innov. C. Jully;59 Rue Marechal Foch;57501;St Avold;49.1057612;6.6927566
+0880153N;Lgt Lycée Des Metiers Filiere Bois Andre Malraux;Rue De L'epinette;88204;Remiremont;48.021875;6.5831674
+0020031Y;Lgt Lycée Des Metiers Frederic Et Irene Joliot Curie;1 Place Du Pigeon Blanc;02500;Hirson;49.9200919;4.084192
+0570099Y;Lgt Lycée Des Metiers Henri Nomine;60 R Du Marechal Foch;57215;Sarreguemines;49.1165272;7.0802942
+0470003Y;Lgt Lycée Des Metiers Jean Baptiste De Baudre;5 Allee Pierre Pomarede;47916;Agen;44.203142;0.616363
+0600040T;Lgt Lycée Des Metiers Jean Calvin;Mont Saint Simeon;60400;Noyon;49.580638;3.019981
+0030038A;Lgt Lycée Des Metiers Jean Monnet;39 Place Jules Ferry;03401;Yzeure;46.5660374;3.3552285
+0801853E;Lgt Lycée Des Metiers Jean Racine;541 Rue Pasteur;80500;Montdidier;49.6533011;2.5807174
+0110007Y;Lgt Lycée Des Metiers Jules Fil;Boulevard Joliot Curie;11021;Carcassonne;43.2148527;2.3675002
+0630021F;Lgt Lycée Des Metiers La Fayette;21 Boulevard Robert Schuman;63002;Clermont Ferrand;45.7626229;3.1315173
+0320067Z;Lgt Lycée Des Metiers Le Garros;1 Bis Rue Darwin;32021;Auch;43.6317386;0.5885775
+0260113G;Lgt Lycée Des Metiers Les Catalins;24 Avenue Des Catalins;26216;Montelimar;44.5696163;4.7576998
+0060075Z;Lgt Lycée Des Metiers Les Eucalyptus;7 Avenue Des Eucalyptus;06200;Nice;43.6762904;7.2225405
+0810004P;Lgt Lycée Des Metiers Louis Rascol;10 Rue De La Republique;81012;Albi;43.9318293;2.1542966
+0600020W;Lgt Lycée Des Metiers Marie Curie;Boulevard Pierre De Coubertin;60180;Nogent Sur Oise;49.266627;2.464235
+0730016W;Lgt Lycée Des Metiers Monge;1 Avenue Marius Berroir;73000;Chambery;45.5660699;5.9341975
+0382203N;Lgt Lycée Des Metiers Pablo Neruda;35 Rue Henri Wallon;38400;St Martin D Heres;45.1728602;5.7587385
+0310017A;Lgt Lycée Des Metiers Paul Mathou;Avenue De Luchon;31210;Gourdan Polignan;43.0674443;0.5872643
+0880021V;Lgt Lycée Des Metiers Pierre Mendes France;2 Rue Du Haut Des Etages;88000;Epinal;48.186392;6.455177
+0230025C;Lgt Lycée Des Metiers Raymond Loewy;Place Filderstadt;23300;La Souterraine;46.239087;1.482986
+0630020E;Lgt Lycée Des Metiers Sidoine Apollinaire;20 Rue Jean Richepin;63037;Clermont Ferrand;45.7818105;3.0868666
+0310028M;Lgt Lycée Des Metiers Vincent Auriol;36 Route De Soreze;31250;Revel;43.4563296;2.0116845
+9830537S;Lp Agricole Do Neva (asee-agri);Ecole De Do Neva;98816;Houailou;0.0;0.0
+0672118N;Lp Agricole Privé Schattenmann;88 Grand'rue;67330;Bouxwiller;48.8272341;7.4782172
+0470823P;Lp Des Metiers Vie Rurale Ctr.etu.fem.rur.l Oustal;Rue Paul Sabatier;47300;Villeneuve Sur Lot;44.393685;0.733397
+0741689J;Lp Leap;Site De Chavanod;74650;Chavanod;45.890053;6.03928
+0330018R;Lp Lycée Des Metiers;24 Rue Du Clg Technique;33294;Blanquefort;44.9122244;-0.6202443
+0410718H;Lp Lycée Des Metiers;2 Avenue Jean Magnon;41110;St Aignan;47.266268;1.3718823
+0801534H;Lp Lycée Des Metiers;541 Rue Pasteur;80500;Montdidier;49.6533011;2.5807174
+0950688G;Lp Lycée Des Metiers;71 Avenue De Ceinture;95880;Enghien Les Bains;48.97192;2.2940159
+0421606T;Lp Lycée Des Metiers Adrien Testud;33 Boulevard D'auvergne;42502;Le Chambon Feugerolles;45.3930502;4.3204203
+0370040T;Lp Lycée Des Metiers Albert Bayet;50 Boulevard Preuilly;37058;Tours;47.3929843;0.6707489
+0010001W;Lp Lycée Des Metiers Alexandre Berard;223 Rue Alexandre Berard;01505;Amberieu En Bugey;45.972793;5.3518732
+0910630R;Lp Lycée Des Metiers Alexandre Denis;Ch Montmirault Av Carnot;91590;Cerny;48.483799;2.3326528
+0120037A;Lp Lycée Des Metiers Alexis Monteil;14 Rue Carnus;12034;Rodez;44.3559366;2.5738937
+0660026V;Lp Lycée Des Metiers Alfred Sauvy;Chateau Lagrange;66740;Villelongue Dels Monts;42.526599;2.9025059
+0050005D;Lp Lycée Des Metiers Alpes Et Durance;Quartier De La Robeyere;05200;Embrun;44.5604975;6.4881374
+0332445D;Lp Lycée Des Metiers Alphonse Beau De Rochas;Rue Jean Hameau;33300;Bordeaux;44.8667377;-0.5677874
+0730030L;Lp Lycée Des Metiers Ambroise Croizat;244 Avenue De La Liberation;73604;Moutiers;45.4828426;6.5291499
+0630024J;Lp Lycée Des Metiers Amedee Gasquet;12 Rue Jean Baptiste Torrilhon;63037;Clermont Ferrand;45.7782828;3.0776281
+0410031L;Lp Lycée Des Metiers Andre Ampere;2 Rue Ampere;41107;Vendome;47.8033433;1.082092
+0573211F;Lp Lycée Des Metiers Andre Citroen;5 Rue De 11eme D'aviation;57155;Marly;49.059828;6.154795
+0601870G;Lp Lycée Des Metiers Andre Malraux;1 Place Nelson Mandela;60160;Montataire;49.2597525;2.425907
+0670024M;Lp Lycée Des Metiers Andre Siegfried;12 Rue Des Dominicains;67504;Haguenau;48.8156319;7.7946128
+0870058R;Lp Lycée Des Metiers Antone De Saint Exupery;Route Du Palais;87000;Limoges;45.8499103;1.2977815
+0772244W;Lp Lycée Des Metiers Antonin Careme;1 Place Gustave Courbet;77176;Savigny Le Temple;48.5955639;2.578176
+0011120M;Lp Lycée Des Metiers Arbez Carme;1 Rue Pierre Et Marie Curie;01100;Bellignat;46.2508923;5.6331189
+0940138P;Lp Lycée Des Metiers Armand Guillaumin;Rue Pierre Corneille;94310;Orly;48.749033;2.404065
+0340069R;Lp Lycée Des Metiers Auto Cycles Moto Jacques Brel;15 Avenue De La Gare;34220;St Pons De Thomieres;43.4879072;2.7645737
+0560001X;Lp Lycée Des Metiers B. Du Guesclin;50 Rue Pierre Allio;56406;Auray;47.6840168;-2.9983391
+0820001F;Lp Lycée Des Metiers Batiment Et Topographie;578 Avenue De Gascogne;82500;Beaumont De Lomagne;43.8767017;0.9780313
+0310053P;Lp Lycée Des Metiers Bayard;150 Route De Launaguet;31021;Toulouse;43.6387232;1.4392188
+0060908E;Lp Lycée Des Metiers Beau Site;38 Avenue Estienne D Orves;06050;Nice;43.7002115;7.2489081
+0350102W;Lp Lycée Des Metiers Beaumont;10 Rue Du Lycée;35605;Redon;0.0;0.0
+0770943G;Lp Lycée Des Metiers Benjamin Franklin;Rue De La Foret La Rochette;77012;Melun;48.508725;2.663036
+0800063J;Lp Lycée Des Metiers Boucher De Perthes;1 Rue Paul Delique;80142;Abbeville;50.1023384;1.8422737
+0460032K;Lp Lycée Des Metiers Champollion;13 Avenue Fernand Pezet;46106;Figeac;44.6090478;2.0281224
+0600041U;Lp Lycée Des Metiers Charles De Bovelles;Mont Saint Simeon;60402;Noyon;49.580638;3.019981
+0312217S;Lp Lycée Des Metiers Charles De Gaulle;24 Avenue Charles De Gaulle;31604;Muret;43.4689165;1.3215619
+0340078A;Lp Lycée Des Metiers Charles De Gaulle;38 Rue Robespierre;34200;Sete;43.4110658;3.6790691
+0830661T;Lp Lycée Des Metiers Claret;202 Boulevard Trucy;83000;Toulon;43.1340433;5.9256355
+0930136T;Lp Lycée Des Metiers Claude-nicolas Ledoux;Avenue Du Quatorze Juillet;93320;Les Pavillons Sous Bois;48.9117268;2.4912496
+0460051F;Lp Lycée Des Metiers Clement Marot;59 Rue Des Augustins;46005;Cahors;44.4499879;1.4392957
+0020079A;Lp Lycée Des Metiers Condorcet;Rond Point Joliot Curie;02100;St Quentin;49.861713;3.293589
+0930130L;Lp Lycée Des Metiers Condorcet;31 Rue Desire Chevalier;93105;Montreuil;48.857903;2.446193
+0510069Y;Lp Lycée Des Metiers Croix Cordier;40 Rue Croix Cordier;51434;Tinqueux;49.2460676;3.985723
+0300047N;Lp Lycée Des Metiers D'art Georges Guynemer;Place De Verdun;30703;Uzes;44.0145529;4.4206567
+0240006B;Lp Lycée Des Metiers De L Alba;4 Rue Ch Gonthier;24100;Bergerac;44.8430899;0.493039
+0400019D;Lp Lycée Des Metiers De L Auto Frederic Esteve;Quart. St Medard Rue F.esteve;40010;Mont De Marsan;43.893485;-0.499782
+0640026F;Lp Lycée Des Metiers De L Habitat;25 Rue Louis Barthou;64110;Gelos;43.2838439;-0.3738321
+0470040N;Lp Lycée Des Metiers De L Habitat Louis Couffignal;Rue Du Rooy;47300;Villeneuve Sur Lot;44.396361;0.712915
+0310088C;Lp Lycée Des Metiers De L'ameublement;Plaine Du Laudot;31250;Revel;43.458611;2.004573
+0171571R;Lp Lycée Des Metiers De L'atlantique;2 Rue De Montreal;17200;Royan;45.6336048;-1.0180228
+0220059V;Lp Lycée Des Metiers Des Metiers Jean Moulin;3 Rue Du Vau Gicquel;22022;St Brieuc;48.5255564;-2.7908495
+0690048C;Lp Lycée Des Metiers Diderot;41 Cours General Giraud;69283;Lyon 01;45.7710133;4.8167669
+0120096P;Lp Lycée Des Metiers Du Batiment;2 Avenue Du Lycée;12110;Aubin;0.0;0.0
+0230019W;Lp Lycée Des Metiers Du Batiment;Les Granges Route D'aubussson;23500;Felletin;45.884543;2.173878
+0290130J;Lp Lycée Des Metiers Du Batiment;Rue De Kervern;29190;Pleyben;48.2217709;-3.970686
+0101022G;Lp Lycée Des Metiers Edouard Herriot;Rue De La Maladiere;10302;Ste Savine;48.2935455;4.0271421
+0280864M;Lp Lycée Des Metiers Elsa Triolet;2 Rue Des Ecoles;28110;Luce;48.439743;1.4632135
+0690105P;Lp Lycée Des Metiers Emile Bejuit (automobile);282 Route De Genas;69675;Bron;45.7514458;4.9054389
+0560008E;Lp Lycée Des Metiers Emile James;56 Rue Emile James;56410;Etel;47.6631024;-3.2049371
+0560070X;Lp Lycée Des Metiers Emile Zola;30 Rue Emile Zola;56704;Hennebont;47.8259374;-3.2514182
+0421489R;Lp Lycée Des Metiers Etienne Legrand;8 Boulevard Charles Gallet;42124;Le Coteau;46.0226751;4.0940381
+0080010T;Lp Lycée Des Metiers Etion;Rue Jean De La Fontaine;08000;Charleville Mezieres;49.7789551;4.701555
+0220071H;Lp Lycée Des Metiers Eugene Freyssinet;32 Rue Mansart;22023;St Brieuc;48.5195608;-2.777683
+0510038P;Lp Lycée Des Metiers Europe;71 Avenue De L'europe;51673;Reims;49.2495206;4.0658188
+0110013E;Lp Lycée Des Metiers Francois Andreossy;1 Rue Saint Francois;11494;Castelnaudary;43.3190962;1.9585015
+0370032J;Lp Lycée Des Metiers Francois Clouet;8 Rue Lepage;37081;Tours;47.4285682;0.6913718
+0630012W;Lp Lycée Des Metiers Francois Rabelais;13 Avenue De Charbonnier;63570;Brassac Les Mines;45.4134991;3.3218257
+0451304Y;Lp Lycée Des Metiers Francoise Dolto;125 Rue Claude Debussy;45160;Olivet;47.865765;1.8794481
+0020089L;Lp Lycée Des Metiers Frederic Et Irene Joliot Curie;1 Place Du Pigeon Blanc;02500;Hirson;49.9200919;4.084192
+0300058A;Lp Lycée Des Metiers Frederic Mistral;457 Ancienne Route De Generac;30913;Nimes;43.823195;4.3589827
+0640098J;Lp Lycée Des Metiers Gabriel Haure-place;6 Av Carmel Lasportes;64800;Coarraze;43.177514;-0.2400054
+0830960T;Lp Lycée Des Metiers Gallieni;Rue Marechal Lyautey;83600;Frejus;43.4454017;6.7551626
+0450064A;Lp Lycée Des Metiers Gaudier-brzeska;40 Avenue Denis Papin;45800;St Jean De Braye;47.9115438;1.9462399
+0730039W;Lp Lycée Des Metiers General Ferrie;39 Avenue De La Republique;73140;St Michel De Maurienne;45.2190734;6.4710134
+0010020S;Lp Lycée Des Metiers Georges Charpak;Avenue Charles De Gaulle;01400;Chatillon Sur Chalaronne;46.1254895;4.9607151
+0830058M;Lp Lycée Des Metiers Georges Cisson;272 Rue Andre Chenier;83100;Toulon;43.1292577;5.9614162
+0691626T;Lp Lycée Des Metiers Georges Lamarque;10 Route De Geneve;69140;Rillieux La Pape;45.8037404;4.8823896
+0510036M;Lp Lycée Des Metiers Gustave Eiffel;34 Rue De Neufchatel;51066;Reims;49.275193;4.0269257
+0380036H;Lp Lycée Des Metiers Guynemer;56 Avenue Marcelin Berthelot;38037;Grenoble;45.1705531;5.731918
+0310057U;Lp Lycée Des Metiers Helene Boucher;1 Rue Lucien Lafforgue;31901;Toulouse;43.6132367;1.4345703
+0931193S;Lp Lycée Des Metiers Helene Boucher;70 Avenue Gilbert Berger;93290;Tremblay En France;48.9502883;2.5784449
+0380104G;Lp Lycée Des Metiers Henri Fabre;35 Rue Henri Wallon;38400;St Martin D Heres;45.1728602;5.7587385
+0570123Z;Lp Lycée Des Metiers Henri Nomine;60 R Marechal Foch;57215;Sarreguemines;49.1165272;7.0802942
+0630041C;Lp Lycée Des Metiers Henri Sainte-claire Deville;Chemin Des Croizettes;63504;Issoire;45.5401316;3.2576825
+0421736J;Lp Lycée Des Metiers Hotelier;18 Rue Francois Gillet;42405;St Chamond;45.4546377;4.5045484
+0811144D;Lp Lycée Des Metiers Hotelier;45 Rue Lapeyrouse;81207;Mazamet;43.5031335;2.3830315
+0261148G;Lp Lycée Des Metiers Hotelier De L'hermitage;Rue Jean Monnet;26600;Tain L Hermitage;45.06968;4.860383
+0460529A;Lp Lycée Des Metiers Hoteliers Quercy-perigord;Avenue Roger Couderc;46200;Souillac;44.8950338;1.4700737
+0332194F;Lp Lycée Des Metiers Hotellerie Tourisme E Services;1 Av Roland Dorgeles;33120;Arcachon;44.6510118;-1.1604617
+0301270T;Lp Lycée Des Metiers Industriel Jules Raimu;12 Rue Jules Raimu;30908;Nimes;43.8195221;4.3290667
+0600003C;Lp Lycée Des Metiers J.b. Corot - Batiment;4 Et 6 Rue Henri Lebesgue;60000;Beauvais;49.42964;2.081875
+0332441Z;Lp Lycée Des Metiers Jacques Brel;Rue Jean Lurcat;33305;Lormont;44.8758255;-0.5124773
+0940141T;Lp Lycée Des Metiers Jacques Brel;90-100 Avenue D'alfortville;94600;Choisy Le Roi;48.7737868;2.4151804
+0690043X;Lp Lycée Des Metiers Jacques De Flesselles;15 Rue De Flesselles;69283;Lyon 01;45.7710983;4.8266437
+0771997C;Lp Lycée Des Metiers Jacques Prevert;7 Avenue Jean Jaures;77385;Combs La Ville;48.6273907;2.5860068
+0382271M;Lp Lycée Des Metiers Jean Claude Aubry;Chemin De Rosiere;38303;Bourgoin Jallieu;45.5958819;5.2972999
+0360026H;Lp Lycée Des Metiers Jean D Alembert;8 Rue De La Limoise;36105;Issoudun;46.9567081;2.0000772
+0180009M;Lp Lycée Des Metiers Jean De Berry;;18026;Bourges;47.060833;2.372222
+0650041S;Lp Lycée Des Metiers Jean Dupuy;1 Rue Aristide Berges;65016;Tarbes;43.229533;0.064581
+0400097N;Lp Lycée Des Metiers Jean Garnier;24 Rue Henri Barbusse;40110;Morcenx;44.0401193;-0.9135882
+0180025E;Lp Lycée Des Metiers Jean Guehenno;Rue Des Sables;18200;St Amand Montrond;46.733637;2.5082521
+0430024T;Lp Lycée Des Metiers Jean Monnet;45 Boulevard Bertrand;43000;Le Puy En Velay;45.0368885;3.8870656
+0290001U;Lp Lycée Des Metiers Jean Moulin;27 Rue De La Republique;29780;Plouhinec;48.0182717;-4.5302967
+0400027M;Lp Lycée Des Metiers Jean Taris;Avenue J Dupaya;40300;Peyrehorade;43.5493036;-1.0898504
+0080047H;Lp Lycée Des Metiers Jean-baptiste Clement;11 Rue J Jaures;08200;Sedan;49.6998064;4.9329191
+0931233K;Lp Lycée Des Metiers Jean-baptiste Clement;25 Rue Des Trois Noyers;93220;Gagny;48.8874111;2.5425454
+0880072A;Lp Lycée Des Metiers Jean-charles Pellerin;44 Rue Abel Ferry;88021;Epinal;48.1665516;6.4548634
+0511430C;Lp Lycée Des Metiers Joliot-curie;4 Rue Joliot-curie;51096;Reims;49.2313467;4.0037269
+0420045W;Lp Lycée Des Metiers Joseph Haubtmann;20 Rue Burdeau;42000;St Etienne;45.4457783;4.4108791
+0090006H;Lp Lycée Des Metiers Joseph Marie Jacquard;Rue Jacquard;09300;Lavelanet;42.923053;1.840868
+0670058Z;Lp Lycée Des Metiers Jules Verne;31 Rue Saint Nicolas;67703;Saverne;48.7345469;7.3714457
+0221595P;Lp Lycée Des Metiers La Fontaine Des Eaux;La Fontaine Des Eaux;22102;Dinan;48.4614665;-2.0384413
+0320068A;Lp Lycée Des Metiers Le Garros;1 Bis Rue Darwin;32021;Auch;43.6317386;0.5885775
+0870748R;Lp Lycée Des Metiers Le Mas Jambost;7 Allee Du Marechal Fayolle;87065;Limoges;45.833096;1.222058
+0021477V;Lp Lycée Des Metiers Leonard De Vinci;1 Espace Jean Guerland;02331;Soissons;49.3726602;3.3073808
+0341385W;Lp Lycée Des Metiers Leonard De Vinci;Rue Professeur Blayac;34085;Montpellier;43.6246951;3.8272737
+0693045K;Lp Lycée Des Metiers Les Canuts;2 Rue Ho Chi Minh;69120;Vaulx En Velin;45.7797002;4.9205183
+0260114H;Lp Lycée Des Metiers Les Catalins;24 Avenue Des Catalins;26216;Montelimar;44.5696163;4.7576998
+0060015J;Lp Lycée Des Metiers Les Coteaux;4/6 Che Morgon Av Des Coteaux;06400;Cannes;43.5662949;7.022452
+0060082G;Lp Lycée Des Metiers Les Eucalyptus;7 Avenue Des Eucalyptus;06200;Nice;43.6762904;7.2225405
+0080953T;Lp Lycée Des Metiers Louis Armand;;08440;Vivier Au Court;49.733206;4.830856
+0561507J;Lp Lycée Des Metiers Louis Armand;3 Rue Jean Moulin;56500;Locmine;47.8792681;-2.8294521
+0920171L;Lp Lycée Des Metiers Louis Bleriot;67 Rue De Verdun;92150;Suresnes;48.8742683;2.2303113
+0771171E;Lp Lycée Des Metiers Louis Lumiere;32 Avenue De L'europe;77500;Chelles;48.882983;2.6085512
+0810046K;Lp Lycée Des Metiers Louis Rascol;10 Rue De La Republique;81012;Albi;43.9318293;2.1542966
+0931735F;Lp Lycée Des Metiers Louise Michel;77 Rue De Saint Gratien;93800;Epinay Sur Seine;48.9620907;2.2997924
+0450066C;Lp Lycée Des Metiers Mal Leclerc De Hauteclocque;85 Avenue Georges Clemenceau;45140;St Jean De La Ruelle;47.8960395;1.8605698
+0190027B;Lp Lycée Des Metiers Marcel Barbanceys;Rue De L'artisanat;19160;Neuvic;45.382449;2.272544
+0630054S;Lp Lycée Des Metiers Marie Laurencin;1 Avenue Jean Monnet;63201;Riom;45.9006673;3.1039393
+0560027A;Lp Lycée Des Metiers Marie Le Franc;128 Boulevard Leon Blum;56321;Lorient;47.7456044;-3.3806542
+0370054H;Lp Lycée Des Metiers Martin Nadaud;67 Rue Jeanne Labourbe;37700;St Pierre Des Corps;47.394938;0.7418599
+0870004G;Lp Lycée Des Metiers Martin Nadaud;30 Avenue De La Liberation;87300;Bellac;46.1224081;1.0612588
+0280022X;Lp Lycée Des Metiers Maurice Viollette;Rue Pablo Neruda;28101;Dreux;48.736652;1.345303
+0340043M;Lp Lycée Des Metiers Mendes France;49 Bis Av Georges Clemenceau;34007;Montpellier;43.602191;3.8701314
+0020051V;Lp Lycée Des Metiers Metiers De L Ameublement;Rue Fleming;02100;St Quentin;49.8638229;3.303156
+0751710B;Lp Lycée Des Metiers Nicolas Louis Vauquelin;13-21 Avenue Boutroux;75013;Paris 13;48.8220969;2.3749517
+0060040L;Lp Lycée Des Metiers Pasteur;25 Rue Du Professeur Delvalle;06000;Nice;43.722971;7.2839949
+0670050R;Lp Lycée Des Metiers Paul Emile Victor;1a Avenue De Gail;67210;Obernai;48.4632396;7.4912864
+0310089D;Lp Lycée Des Metiers Paul Mathou;Avenue De Luchon;31210;Gourdan Polignan;43.0674443;0.5872643
+0670006T;Lp Lycée Des Metiers Philippe-charles Goulden;2 Rue De La Piscine;67241;Bischwiller;48.7749993;7.8502476
+0631480S;Lp Lycée Des Metiers Pierre Boulanger;85 Chemin Des Palisses;63430;Pont Du Chateau;45.792027;3.2478132
+0470015L;Lp Lycée Des Metiers Porte Du Lot;Porte Du Lot;47320;Clairac;44.358916;0.375807
+0180049F;Lp Lycée Des Metiers Rene Cassin;41 Bis Rue Charles Hurvoy;18108;Vierzon;47.2279543;2.0673471
+0420029D;Lp Lycée Des Metiers Rene Cassin;Le Marthoret;42800;Rive De Gier;45.522087;4.605514
+0400020E;Lp Lycée Des Metiers Robert Wlerick;6 Rue Jean Mace -;40005;Mont De Marsan;43.8969788;-0.5144499
+0630022G;Lp Lycée Des Metiers Roger Claustres;127 Rue Docteur Hospital;63039;Clermont Ferrand;45.795454;3.0877122
+0310052N;Lp Lycée Des Metiers Roland Garros;32 Rue Mathaly;31200;Toulouse;43.6290045;1.4298117
+0941604H;Lp Lycée Des Metiers Samuel De Champlain;61-91 Rue Des Bordes;94430;Chennevieres Sur Marne;48.7987853;2.5577377
+0281021H;Lp Lycée Des Metiers Sully;8 Rue Des Viennes;28404;Nogent Le Rotrou;48.3245558;0.8218301
+0690010L;Lp Lycée Des Metiers Tony Garnier;235 Boulevard Pinel;69676;Bron;45.7261136;4.8871026
+0770919F;Lp Lycée Des Metiers Uruguay France;1 Avenue Des Marronniers;77211;Avon;48.4103009;2.7355078
+0100004A;Lp Lycée Des Metiers Val More;Avenue Bernard Pieds;10110;Bar Sur Seine;48.1121812;4.3867353
+0631668W;Lp Lycée Des Metiers Vercingetorix;4 Rue De Laubize;63540;Romagnat;45.737373;3.106057
+0391131T;Lp Privé Institut Europeen De Form Compagnons Tour De France;2 Rue Leopold Alixant;39330;Mouchard;46.976214;5.797567
+9830401U;Lp Privé St Pierre Chanel La Conception (ddec);Rue Beautemps Beaupre;98874;Le Mont Dore;0.0;0.0
+0331740M;Lpa Viticulture Oenologie La Tour Blanche;Le Bourg;33210;Bommes;44.5613178;-0.3551972
+0731198F;Lpa Costa De Beauregard Site De Chambery Le Bocage;340 Rue Costa De Beauregard;73000;Chambery;45.5633945;5.9311119
+0731199G;Lpa Costa De Beauregard Site De La Ravoire Charmilles;148 Rue L Costa De Beauregard;73490;La Ravoire;45.5639798;5.9610297
+0400141L;Lpa De Sabres;Route De Luglon;40630;Sabres;44.1457175;-0.7411813
+0631669X;Lpo Lycée Des Metiers;Voie Romaine;63400;Chamalieres;45.7782726;3.0496304
+0240026Y;Lpo Lycée Des Metiers Albert Claveille;80 Rue V Hugo;24001;Perigueux;45.1894956;0.7130103
+0031043T;Lpo Lycée Des Metiers Albert Einstein;Rue Albert Einstein;03105;Montlucon;46.3508329;2.604608
+0670087F;Lpo Lycée Des Metiers Alexandre Dumas (hotelier);75 Rte Du Rhin;67404;Illkirch Graffenstaden;48.5734116;7.7971354
+0770938B;Lpo Lycée Des Metiers Andre Malraux;Avenue Du Lycée;77130;Montereau Fault Yonne;0.0;0.0
+0932129J;Lpo Lycée Des Metiers Application De L'enna;Place Du 8 Mai 1945;93203;St Denis;48.939438;2.355926
+0410001D;Lpo Lycée Des Metiers Augustin Thierry;13 Avenue De Chateaudun;41018;Blois;47.5966852;1.3290092
+0750681H;Lpo Lycée Des Metiers Boulle Esaa;9 Rue Pierre Bourdan;75012;Paris 12;48.8468966;2.3914855
+0560016N;Lpo Lycée Des Metiers Broceliande;Bellevue;56383;Guer;47.9401893;-2.1337777
+0640001D;Lpo Lycée Des Metiers Cantau;1 Allee De Cantau;64600;Anglet;43.4758828;-1.5121686
+0521032P;Lpo Lycée Des Metiers Charles De Gaulle;Avenue Christian Pineau;52903;Chaumont;48.0738585;5.1462257
+0681801N;Lpo Lycée Des Metiers Charles De Gaulle;14 Rte De Ruelisheim;68840;Pulversheim;47.8328666;7.3074998
+0772228D;Lpo Lycée Des Metiers Charles De Gaulle;6 Place Jean Mermoz;77230;Longperrier;49.0483392;2.6684576
+0430020N;Lpo Lycée Des Metiers Charles Et Adrien Dupuy;3 Avenue Docteur Durand;43003;Le Puy En Velay;45.0371683;3.8938789
+0560026Z;Lpo Lycée Des Metiers Colbert;117 Boulevard Leon Blum;56101;Lorient;47.7456671;-3.3807475
+0781578S;Lpo Lycée Des Metiers De Hotellerie Et De Tourisme;Place Francois Rabelais;78042;Guyancourt;48.762895;2.075323
+0332870R;Lpo Lycée Des Metiers De La Mer;Port De La Barbotiere;33470;Gujan Mestras;44.635731;-1.071189
+0470020S;Lpo Lycée Des Metiers De La Plasturgie V De Garonne;22 Rue Ejea De Los Caballeros;47207;Marmande;44.504404;0.1562185
+0290044R;Lpo Lycée Des Metiers De L'elorn;10 Place St Houardon;29207;Landerneau;48.4489257;-4.2496666
+0542262R;Lpo Lycée Des Metiers Du Batiment Et Tp E. Here;86 Blv Marechal Foch;54525;Laxou;48.6827791;6.1514542
+0260023J;Lpo Lycée Des Metiers Du Dauphine;Boulevard Remy Roure;26103;Romans Sur Isere;45.0500065;5.0436672
+0771658J;Lpo Lycée Des Metiers Du Gue A Tresmes;Domaine Du Gue A Tresmes;77440;Congis Sur Therouanne;49.007185;2.97453
+0951728M;Lpo Lycée Des Metiers Edmond Rostand;75 Rue De Paris St Ouen L'aumon;95312;St Ouen L Aumone;48.9004041;2.3043039
+0690128P;Lpo Lycée Des Metiers Edouard Branly;25 Rue De Tourvielle;69322;Lyon 05;45.7527217;4.7911202
+0681809X;Lpo Lycée Des Metiers Ettore Bugatti;8 Rue Des Jonquilles;68110;Illzach;47.7622552;7.3652658
+0932119Y;Lpo Lycée Des Metiers Eugene Henaff;55 Avenue Raspail;93170;Bagnolet;48.8746864;2.4310918
+0772312V;Lpo Lycée Des Metiers Flora Tristan;12 Avenue Du 8 Mai 1945;77130;Montereau Fault Yonne;48.3765466;2.9451169
+0511565Z;Lpo Lycée Des Metiers Francois Arago;1 Rue Francois Arago;51095;Reims;49.2288823;4.0051108
+0080008R;Lpo Lycée Des Metiers Francois Bazin;145 Avenue Charles De Gaulle;08013;Charleville Mezieres;49.7734776;4.7050401
+0940585A;Lpo Lycée Des Metiers Francois Mansart;25 Avenue De La Banque;94211;St Maur Des Fosses;48.7903039;2.4971131
+0932126F;Lpo Lycée Des Metiers Francois Rabelais;Rue Francois Rabelais;93440;Dugny;48.949219;2.417634
+0750695Y;Lpo Lycée Des Metiers Fresnel;31 Boulevard Pasteur;75015;Paris 15;48.8431925;2.3126765
+0921156G;Lpo Lycée Des Metiers Galilee;79 Avenue Chandon;92230;Gennevilliers;48.9207477;2.3027836
+0912142J;Lpo Lycée Des Metiers Gaspard Monge;1 Place Gaspard Monge;91600;Savigny Sur Orge;48.6851259;2.349282
+0460493L;Lpo Lycée Des Metiers Gaston Monnerville;Rue George Sand;46005;Cahors;44.463535;1.455082
+0772311U;Lpo Lycée Des Metiers Georges Cormier;6 Rue Des Templiers;77527;Coulommiers;48.824111;3.093208
+0470038L;Lpo Lycée Des Metiers Georges Leygues;Avenue D Agen;47307;Villeneuve Sur Lot;44.3939869;0.7092418
+0572590F;Lpo Lycée Des Metiers Gustave Eiffel;Cite Technique;57525;Talange;49.235;6.168168
+0681810Y;Lpo Lycée Des Metiers Gustave Eiffel;Rue Gustave Eiffel;68704;Cernay;47.805144;7.18261
+0672616E;Lpo Lycée Des Metiers Gutenberg;22 Rue Lixenbuhl;67404;Illkirch Graffenstaden;48.5294488;7.7224172
+0401002X;Lpo Lycée Des Metiers Haroun Tazieff;Bld St Vincent De Paul;40993;St Paul Les Dax;43.7296115;-1.0383382
+0672534R;Lpo Lycée Des Metiers Haut-barr;4 Rue Jean De Manderscheid;67703;Saverne;48.7317063;7.3513877
+0690040U;Lpo Lycée Des Metiers Hector Guimard;23 Rue Claude Veyron;69361;Lyon 07;45.7456711;4.8579449
+0220196U;Lpo Lycée Des Metiers Henri Avril;7 Et 9 Rue De Dinard;22402;Lamballe;48.4765006;-2.5018486
+0180036S;Lpo Lycée Des Metiers Henri Brisson;25 Avenue Henri Brisson;18100;Vierzon;47.2279808;2.0612978
+0410899E;Lpo Lycée Des Metiers Hotel.et Tourisme Val De Loire;174 Rue Albert 1er;41033;Blois;47.5720598;1.3057101
+0332192D;Lpo Lycée Des Metiers Hotel.tourisme Gascogne;15 Rue Francois Rabelais;33405;Talence;44.7970282;-0.5983577
+0171405K;Lpo Lycée Des Metiers Hotelier;Avenue Des Minimes;17030;La Rochelle;46.143788;-1.162168
+0871030X;Lpo Lycée Des Metiers Hotelier Jean Monnet;12 Rue Louis Armstrong;87065;Limoges;45.8526566;1.2911805
+0573320Z;Lpo Lycée Des Metiers Hotelier Raymond Mondon;2 Boulevard De La Defense;57070;Metz;49.1054834;6.2095119
+0132974M;Lpo Lycée Des Metiers Hotelier Regional;114 Avenue Zenatti;13266;Marseille 08;43.2473062;5.3818559
+0060034E;Lpo Lycée Des Metiers Hotelier Tourisme-paul Augier;163 Boulevard Rene Cassin;06203;Nice;43.6688186;7.212126
+0310046G;Lpo Lycée Des Metiers Hotellerie Et Tourisme;1 Rue De L'abbe Jules Lemire;31026;Toulouse;43.596917;1.4068498
+0831453D;Lpo Lycée Des Metiers Hotellerie Et Tourisme Tpm;Place Vatel;83098;Toulon;43.124228;5.928
+0020012C;Lpo Lycée Des Metiers Jean De La Fontaine;2 Av De Champagne;02401;Chateau Thierry;49.0525511;3.3881731
+0022008X;Lpo Lycée Des Metiers Jean Mace;7 Rue Andre Ternynck;02300;Chauny;49.6216079;3.2145425
+0940129E;Lpo Lycée Des Metiers Jean Mace;Rue Jules Ferry;94407;Vitry Sur Seine;48.8016614;2.4003992
+0783431F;Lpo Lycée Des Metiers Jules Verne;2 Rue De La Constituante;78500;Sartrouville;48.9427537;2.160041
+0221571N;Lpo Lycée Des Metiers Kerraoul;Kerraoul;22502;Paimpol;48.779782;-3.048828
+0770920G;Lpo Lycée Des Metiers Lafayette;Place Des Celestins;77811;Moret Sur Loing;48.4041358;2.7973142
+0681817F;Lpo Lycée Des Metiers Lazare De Schwendi;19 Rue De Turckheim;68040;Ingersheim;48.0776787;7.3464456
+0672198A;Lpo Lycée Des Metiers Le Corbusier;15 Rue Lixenbuhl;67404;Illkirch Graffenstaden;48.5303263;7.7218261
+0382440W;Lpo Lycée Des Metiers Leonard De Vinci;Boulevard De Villefontaine;38091;Villefontaine;45.615892;5.1484454
+0770934X;Lpo Lycée Des Metiers Leonard De Vinci;2 Bis Rue Edouard Branly;77011;Melun;48.5510247;2.6601886
+0932046U;Lpo Lycée Des Metiers Leonard De Vinci;115 Route Des Petits Ponts;93290;Tremblay En France;48.9668896;2.5735809
+0771336J;Lpo Lycée Des Metiers Les Pannevelles;Route De Chalautre;77487;Provins;48.5559405;3.3040064
+0380035G;Lpo Lycée Des Metiers Lesdiguieres;15 Avenue Beaumarchais;38034;Grenoble;45.1679451;5.708607
+0931779D;Lpo Lycée Des Metiers L'horticulture;16 Rue Paul Doumer;93512;Montreuil;48.8591808;2.4567758
+0940118T;Lpo Lycée Des Metiers Louis Armand;173 Boulevard De Strasbourg;94130;Nogent Sur Marne;48.8465135;2.4912988
+0670085D;Lpo Lycée Des Metiers Louis Couffignal;11 Route De La Federation;67025;Strasbourg;48.5621702;7.7496898
+0460028F;Lpo Lycée Des Metiers Louis Vicat;1 Rue Pierre Bourthoumieux;46200;Souillac;44.8945267;1.4803757
+0940112L;Lpo Lycée Des Metiers Louise Michel;7 Rue Pierre Marie Derrien;94507;Champigny Sur Marne;48.8170061;2.4949002
+0932074Z;Lpo Lycée Des Metiers Marcel Cachin;11-13 Rue Marcel Cachin;93400;St Ouen;48.9174351;2.3385731
+0171455P;Lpo Lycée Des Metiers Marcel Dassault;40 Avenue Marcel Dassault;17300;Rochefort;45.9469735;-0.9578103
+0561641E;Lpo Lycée Des Metiers Marcelin Berthelot;Avenue Roland Garros;56230;Questembert;47.6628885;-2.4619313
+0501850P;Lpo Lycée Des Metiers Maurice Marland;159 Rue Des Lycées;50400;Granville;0.0;0.0
+0940126B;Lpo Lycée Des Metiers Maximilien Perret;Place San Benedetto Del Tronto;94142;Alfortville;48.7871379;2.4256714
+0942130E;Lpo Lycée Des Metiers Montaleau;2 Bis Rue Pierre Semard;94372;Sucy En Brie;48.7698796;2.5168592
+0932291K;Lpo Lycée Des Metiers Nicolas-joseph Cugnot;55 Boulevard Louis Armand;93330;Neuilly Sur Marne;48.8620632;2.5203576
+0830923C;Lpo Lycée Des Metiers Paul Langevin;Boulevard De L'europe;83514;La Seyne Sur Mer;43.1141507;5.8566865
+0190018S;Lpo Lycée Des Metiers Pierre Caraminot;28 Avenue De Ventadour;19300;Egletons;45.4072622;2.0561062
+0350030T;Lpo Lycée Des Metiers Pierre Mendes France;34 Rue Bahon Rault;35069;Rennes;48.1336823;-1.6840215
+0180008L;Lpo Lycée Des Metiers Pierre-emile Martin;1 Avenue De Gionne;18026;Bourges;47.0736177;2.4059603
+0750691U;Lpo Lycée Des Metiers Raspail;5 Bis Avenue Maurice D Ocagne;75014;Paris 14;48.8246549;2.3135463
+0910620E;Lpo Lycée Des Metiers Robert Doisneau;95 Bd Jean Jaures;91107;Corbeil Essonnes;48.612295;2.4605384
+0382099A;Lpo Lycée Des Metiers Roger Deschaux;5 Rue Des Pies;38360;Sassenage;45.1992827;5.6681035
+0410030K;Lpo Lycée Des Metiers Ronsard;Rue Joliot Curie;41100;Vendome;47.807054;1.057515
+0922276Z;Lpo Lycée Des Metiers Santos Dumont;39 Rue Pasteur;92210;St Cloud;48.8414385;2.1996884
+0740047Z;Lpo Lycée Des Metiers Savoie Leman;40 Boulevard Carnot;74203;Thonon Les Bains;46.3726701;6.4752202
+0782587N;Lpo Lycée Des Metiers Viollet-le-duc;1 Route De Septeuil;78640;Villiers St Frederic;48.8143515;1.875185
+0290071V;Lpo Lycée Des Metiers Yves Thepot;28 Avenue Yves Thepot;29104;Quimper;47.9882623;-4.0884842
+0352072M;Lpo Privé Des Metiers Institution St Malo-providence;2 Rue Du College;35409;St Malo;48.6503421;-2.0254142
+0290340M;Lpo Privé Lycée Des Metiers Estran Fenelon;Ker Stears;29287;Brest;48.390394;-4.486076
+0220117H;Lpo Privé Sacre Coeur;2 Bd St Jean-bapt De La Salle;22003;St Brieuc;0.0;0.0
+0230018V;Lt Lycée Des Metiers Des Metiers Du Batiment;Les Granges Route D'aubusson;23500;Felletin;45.889542;2.177806
+0800011C;Lt Lycée Des Metiers Edouard Gand;70 Bd De St Quentin;80098;Amiens;49.8791607;2.2965051
+0350005R;Lt Lycée Des Metiers Hotellerie Et Restauration;33 Rue Des Ecoles;35803;Dinard;48.6254023;-2.0585228
+0650027B;Lt Lycée Des Metiers Jean Dupuy;1 Rue Aristide Berges;65016;Tarbes;43.229533;0.064581
+0021476U;Lt Lycée Des Metiers Leonard De Vinci;1 Espace Jean Guerland;02331;Soissons;49.3726602;3.3073808
+0641548K;Lyc Agricole Privé N Dame;Route D Oraas;64390;Sauveterre De Bearn;43.399781;-0.942935
+0211173L;Lyc Enst Gen Et Techno Agricol Felix Kir;85 Rue De Velars;21370;Plombieres Les Dijon;47.3416891;4.9660479
+0340128E;Lyc Enst Gen.techno Agricole Frederic Bazille - Agropolis;3224 Route De Mende;34093;Montpellier;43.6481537;3.864392
+0110857X;Lyc Enst Privé Agric Form Prof;8 Boulevard Des Pins;11170;Pezens;43.255745;2.2655503
+0660632D;Lyc Enst Prof Agricole Privé Beausoleil;Rue Beau Soleil;66403;Ceret;42.4881192;2.7500287
+0383208F;Lyc Gen Tech Pr Itec Boisfleur Itec Boisfleury Europe;118 Avenue De L'eygala;38700;Corenc;45.2122675;5.7577651
+0671639T;Lyc Gen Tech Privé Des Metiers Institution Sainte Clotilde;19 Rue De Verdun;67083;Strasbourg;48.5874674;7.7724234
+0801742J;Lyc Gen Tech Privé Des Metiers La Providence;146 Bd De Saint Quentin;80094;Amiens;49.8789518;2.3007807
+0021869W;Lyc Gen Tech Privé Des Metiers Saint Joseph;4 Place Thiers;02402;Chateau Thierry;49.0428865;3.4009902
+0801888T;Lyc Gen Tech Privé Des Metiers Saint Martin;68 Rue Delpech;80043;Amiens;49.8842672;2.29491
+0801479Y;Lyc Gen Tech Privé Des Metiers Saint Remi;4 Rue Des Sergents;80006;Amiens;49.8942788;2.2987524
+0740092Y;Lyc Gen Tech Privé Des Metiers Sainte Famille;Avenue Des Voirons;74805;La Roche Sur Foron;46.0773767;6.2971227
+0290168A;Lyc Gen Tech Privé Des Metiers St Gabriel;Rue Jean Lautredou;29125;Pont L Abbe;47.8619569;-4.216529
+0022108F;Lyc Gen Tech Privé Des Metiers St Vincent De Paul;13 Avenue De Reims;02205;Soissons;49.3754463;3.3353105
+0430113P;Lyc Gen Techno Prof Agricole;Domaine De Choumouroux;43200;Yssingeaux;45.142667;4.123734
+0430112N;Lyc Gen Techno Prof Agricole Bonnefont;;43100;Fontannes;45.288694;3.422626
+0110669T;Lyc Gen Techno Prof Agricole Charlemagne;Route De Saint Hilaire;11000;Carcassonne;43.1941938;2.3440721
+0780004F;Lyc Gen Techno Prof Agricole De Chambourcy;Rte Forestiere Des Princesses;78100;St Germain En Laye;48.898908;2.093761
+0150037J;Lyc Gen Techno Prof Agricole Enil - Georges Pompidou;21 Rue De Salers;15005;Aurillac;44.9356765;2.4448977
+0630984C;Lyc Gen Techno Prof Agricole Louis Pasteur;Marmilhat;63370;Lempdes;45.7796779;3.17838
+0300139N;Lyc Gen Techno Prof Agricole Marie Durand;Domaine De Donadille;30230;Rodilhan;43.8269904;4.4476196
+0110677B;Lyc Gen Techno Prof Agricole Pierre Paul Riquet;935 Avenue Du Docteur Laennec;11491;Castelnaudary;43.3077647;1.9414051
+0210036A;Lyc Gen Techno Prof Agricole Viticole;16 Av Charles Jaffelin;21206;Beaune;47.0243711;4.8287285
+0420976H;Lyc Gen.et Technol.prive Sainte Barbe;10 Rue Franklin;42028;St Etienne;45.4285207;4.3845892
+0300125Y;Lyc Polyval Privé Des Metiers De La Salle;17 Place Henri Barbusse;30106;Ales;44.1232736;4.0805697
+0560180S;Lyc Polyval Privé Des Metiers Des Metiers N.dame De La Paix;Beg Er Lann;56275;Ploemeur;47.7506666;-3.3973939
+0371532P;Lyc Polyval Privé Des Metiers Esthetique De Touraine;49 Et 51 Rue Laponneraye;37000;Tours;47.3819579;0.6901009
+0480680D;Lyc Polyval Privé Des Metiers Hotellerie Sacre-coeur;43 Avenue De La Gare;48200;St Chely D Apcher;44.8001253;3.273392
+0350795Z;Lyc Polyval Privé Des Metiers Jean-baptiste De La Salle;5 Rue De La Motte Brulon;35702;Rennes;48.125487;-1.6682656
+0931797Y;Lyc Polyval Privé Des Metiers Jean-baptiste De La Salle;6-8 Place De La Resistance;93200;St Denis;48.932793;2.3549513
+0350797B;Lyc Polyval Privé Des Metiers Jeanne D'arc;61 Rue La Fontaine;35708;Rennes;48.1186302;-1.6535782
+0560182U;Lyc Polyval Privé Des Metiers Jeanne D'arc-st Ivy;29 Rue Abbe Martin;56306;Pontivy;48.0716012;-2.9614094
+0693786R;Lyc Polyval Privé Des Metiers Jehanne De France;6 Rue De La Fraternelle;69009;Lyon 09;45.7693693;4.8036475
+0560105K;Lyc Polyval Privé Des Metiers La Mennais;2 Rue General Dubreton;56801;Ploermel;47.9316915;-2.3986823
+0422158T;Lyc Polyval Privé Des Metiers La Salesienne;35 Rue De La Richelandiere;42100;St Etienne;45.4387009;4.4019391
+0290186V;Lyc Polyval Privé Des Metiers Le Paraclet;35 Avenue Des Glenan;29018;Quimper;47.9955627;-4.1146056
+0860789R;Lyc Polyval Privé Des Metiers Le Porteau;62 Rue Du Porteau;86001;Poitiers;46.601507;0.3392301
+0350791V;Lyc Polyval Privé Des Metiers Marcel Callo;21 Avenue Etienne Gascon;35603;Redon;47.6544207;-2.0810479
+0772275E;Lyc Polyval Privé Des Metiers Maurice Rondeau;1 Place Du Clos Saint Georges;77600;Bussy St Georges;48.8420023;2.7042405
+0560200N;Lyc Polyval Privé Des Metiers Notre Dame Le Menimur;71 Rue De Metz;56000;Vannes;47.6669014;-2.7622223
+0370749N;Lyc Polyval Privé Des Metiers Sainte Marguerite;86 Avenue De Grammont;37041;Tours;47.3825627;0.6921553
+0860774Z;Lyc Polyval Privé Des Metiers St Jacques De Compostelle;2 Avenue De La Revolution;86036;Poitiers;46.5861427;0.3817243
+0511146U;Lyc Polyval Privé Des Metiers St J-b De La Salle;20 Rue De Contrai;51066;Reims;49.2510721;4.0364069
+0560181T;Lyc Polyval Privé Des Metiers St Joseph;42 Rue De Kerguestenen;56109;Lorient;47.751421;-3.3928125
+0560198L;Lyc Polyval Privé Des Metiers St Joseph;39 Boulevard Des Iles;56010;Vannes;47.642256;-2.7821005
+0341470N;Lyc Pr Tech Agric Hortic Paysa;Route De Pezenas;34150;Gignac;43.6459898;3.5428841
+0332380H;Lyc Prof Horticole Ville De Bordeaux;Domaine De Bel Air Cedex 1052;33185;Le Haillan;44.873167;-0.677914
+0440980B;Lyc Techn Et General Agric Pr;Briace;44430;Le Landreau;47.202598;-1.317968
+0260074P;Lyc Technol Privé Des Metiers Montplaisir;75 Rue Montplaisir;26000;Valence;44.9312156;4.9083899
+0731405F;Lyc Technol Privé Des Metiers Sainte Anne;39 Montee St Jean;73292;La Motte Servolex;45.5973189;5.8780976
+0022022M;Lyc Technol Privé Des Metiers Sainte Sophie;22 Rue Curie;02110;Bohain En Vermandois;49.9830328;3.4565714
+0331590Z;Lyc Technol Privé Des Metiers St Vincent De Paul;47 A 51 Rue Des Sablieres;33074;Bordeaux;44.825364;-0.5753274
+0331556M;Lyc Technol Privé Des Metiers Ste Famille Saintonge;12 Rue De Saintonge;33023;Bordeaux;44.8321381;-0.5798944
+0260791U;Lyc.gen.tech.agricole De Romans Sur Isere;Route De Tain;26105;Romans Sur Isere;45.0451381;5.0138158
+0561454B;Lycée Agric Et Horticole Privé De Kerplouz;Kerplouz;56404;Auray;47.657435;-2.971875
+0351047Y;Lycée Agric Et Horticole Privé La Lande Du Breil;Rue Fernand Robert;35042;Rennes;48.1322242;-1.7079189
+0801825Z;Lycée Agricole De La Haute Som;Rue De La Poste;80800;Ribemont Sur Ancre;49.9585849;2.566318
+0641547J;Lycée Agricole Pr St Christoph;Le Bourg;64310;St Pee Sur Nivelle;43.31283;-1.58032
+0120936C;Lycée Agricole Privé;Boulevard Penevayre;12200;Villefranche De Rouergue;44.3552133;2.0415109
+0421676U;Lycée Agricole Privé;1 Rue Du Petit Lavoir;42450;Sury Le Comtal;45.5340512;4.1820966
+0100734U;Lycée Agricole Privé;105 Route De Mery-sur-seine;10150;Ste Maure;48.349943;4.0570357
+0360686A;Lycée Agricole Privé;St Cyran Du Jambot;36700;St Cyran Du Jambot;47.017235;1.139101
+0280944Z;Lycée Agricole Privé De Nermont;2 Rue De Nermont;28202;Chateaudun;48.0643465;1.3301401
+0100891P;Lycée Agricole Privé Des Cordeliers;29 Rue Des Cordeliers;10700;Arcis Sur Aube;48.5349144;4.1402841
+0421090G;Lycée Agricole Privé Etienne Gautier;Ressins;42720;Nandax;46.096594;4.189123
+0080943G;Lycée Agricole Privé L.e.t.p.a.p;18 Rue Du Chateau;08260;Maubert Fontaine;49.8696137;4.4309129
+0560858D;Lycée Agricole Privé La Touche;La Touche;56801;Ploermel;47.9491412;-2.4040591
+0241033T;Lycée Agricole Privé Le Cluzeau;Le Cluzeau;24240;Sigoules;44.764981;0.419171
+0281101V;Lycée Agricole Privé Notre Dame (orph.app.auteuil);Chateau De Vaux;28240;La Loupe;47.786628;0.936139
+0511645L;Lycée Agricole Privé Reims-thillois;4 Rue Des Ecoles;51370;Thillois;49.2565382;3.9547396
+9741060J;Lycée Agricole Privé Sainte Suzanne;Chemin Des Trois Freres;97441;Ste Suzanne;-20.90902;55.6055231
+0271382F;Lycée Agricole Privé Tourville Sur Pont-audemer;Route De Lisieux;27504;Pont Audemer;49.3501675;0.5151331
+0721009F;Lycée Agricole Privé Val De Sarthe;Route Du Mans;72303;Sable Sur Sarthe;47.838713;-0.304241
+0641538Z;Lycée Agricole Rural Pr Ois;;64130;Mauleon Licharre;43.220131;-0.8895
+0131690S;Lycée Agricole Tech. Privé Institut De Fontlongue;Bd Theodore Aubanel;13148;Miramas;43.5881724;4.9977599
+0631162W;Lycée Agro-environ Privé Saint Joseph;Chateau De Saint Quentin;63340;Le Breuil Sur Couze;45.460253;3.273918
+0251477V;Lycée Agrotechnologique Privé Francois Xavier;5 Rue Du Chapitre;25000;Besancon;47.2333772;6.0305139
+0380097Z;Lycée Climatique Et Sportif Jean Prevost;470 Rue De Tintaine;38250;Villard De Lans;45.0619796;5.5561287
+0660057D;Lycée Climatique Et Sportif Pierre De Coubertin;5 Avenue Pierre De Coubertin;66120;Font Romeu Odeillo Via;42.5131055;2.0447502
+0050004C;Lycée Climatique Honore Romane;Route De Caleyere;05202;Embrun;44.5702302;6.4862717
+0650001Y;Lycée Climatique Rene Billeres;6 Avenue Marcel Lemettre;65402;Argeles Gazost;43.0017111;-0.0948255
+0390042J;Lycée Climatique Victor Considerant;Route D'ornans;39110;Salins Les Bains;46.9525457;5.8851415
+0671636P;Lycée D'ens. General Et Techn. Ort;14 Rue Sellenick;67083;Strasbourg;48.5906831;7.7508928
+0671685T;Lycée D'ens. General Et Technologique Agricole;44 Bld D'europe;67212;Obernai;48.46226;7.48168
+0431000D;Lycée D'enseig. Agricole Privé Site De Vals De L'isvt;72 Avenue De Vals;43750;Vals Pres Le Puy;45.0335241;3.8797025
+0520893N;Lycée D'enseignement Agricole Privé;Rue De La Motte;52220;Droyes;48.5130399;4.703696
+0021818R;Lycée D'enseignement Technolog Robert Schuman;10 Route D'ugny;02300;Chauny;49.6265626;3.1986091
+0280947C;Lycée Ens Prof Agri Privé Du Perche;35 Rue De La Touche;28400;Nogent Le Rotrou;48.3111788;0.8100851
+0180758B;Lycée Ens Prof Privé Agricole Bengy/lignieres;2 Rue Du Chanoine Volton;18520;Bengy Sur Craon;47.0017201;2.7487466
+0180757A;Lycée Ens Prof Privé Agricole Site De Lignieres;10 Rue Raymond Chassagne;18160;Lignieres;46.7505044;2.1756904
+0410855G;Lycée Ens Prof Rural Privé Boissay;;41120;Fougeres Sur Bievre;47.4478099;1.3411169
+0360710B;Lycée Ens Prof Rural Privé De Lignac;Rue Alienor D Aquitaine;36370;Lignac;46.4667777;1.2176906
+0410854F;Lycée Ens Prof Rural Privé Ste Cecile;12 Rue Saint Laurent;41800;Montoire Sur Le Loir;47.7521229;0.8632596
+0190056H;Lycée Ens. General Privé Inst Jeanne D Arc;Rue Du Jardin Public;19400;Argentat;45.093215;1.940286
+0230040U;Lycée Ens. General Privé Notre Dame;5 Av Du Docteur Manouvrier;23008;Gueret;46.167861;1.8743524
+0441787D;Lycée Ensei Agricole Privé Saint-exupery;9 Rue Pierre Landais;44430;Le Loroux Bottereau;47.2379078;-1.3457308
+0280967Z;Lycée Enseig Agricole Privé Efagrir;2 Rue Des Fleurs;28630;Mignieres;48.3595403;1.4288729
+0561463L;Lycée Enseignement Agric Privé Sts Anges Kerlebost;St Thuriau;56306;Pontivy;48.0565733;-2.9246647
+0870864S;Lycée Enseignement General Pri St Jean;26 Rue Eugene Varlin;87016;Limoges;45.8263601;1.2749309
+0400080V;Lycée Enseignement Prof. Jean Cassaigne;Avenue Kennedy;40280;St Pierre Du Mont;43.869494;-0.489719
+0781070P;Lycée Enseigt Privé Agricole Lepa Le Buat;21 Rue Du Buat;78580;Maule;48.906077;1.8409898
+0110984K;Lycée Enst Agricole Privé;8 Avenue Georges Clemenceau;11200;Lezignan Corbieres;43.200028;2.7645461
+0341477W;Lycée Enst Agricole Privé Bonne Terre;Route De Beziers;34120;Pezenas;43.448806;3.4123228
+0341474T;Lycée Enst Agricole Privé La Gardiole;2 Rue Du Couvent;34770;Gigean;43.4990181;3.7125009
+0660631C;Lycée Enst Agricole Privé Le Mas Blanc;Av Emmanuel Brousse;66760;Bourg Madame;42.4391763;1.9445521
+0341471P;Lycée Enst Agricole Privé Les Buissonnets;1 Avenue De Nissan;34310;Capestang;43.3272313;3.0496255
+0480502K;Lycée Enst Agricole Privé Terre Nouvelle;2 Av Martyrs De La Resistance;48100;Marvejols;44.554723;3.2925132
+0301324B;Lycée Enst Prof Agric Privé;9 Route De Bezouce;30840;Meynes;43.8791144;4.5539775
+0221676C;Lycée Enst Prof Agricole Privé Xavier Grall;8 Rue Lavergne;22605;Loudeac;48.1797422;-2.7566822
+0221680G;Lycée Enst Prof Privé Rural;La Guerche;22100;St Helen;48.468966;-1.986689
+0758513W;Lycée Etranger Privé Leonard De Vinci;12 Rue Sedillot;75007;Paris 07;48.8586491;2.3017117
+0592922F;Lycée Europeen Privé Therese D'avila;254 Rue Nationale;59003;Lille;50.6306982;3.0475752
+0791005N;Lycée Gen Et Tech Agricole;22 Rue De La Baritauderie;79302;Bressuire;46.838952;-0.4751457
+0160006V;Lycée Gen Et Tech Agricole Angouleme-l Oisellerie;Logis De L'oisellerie;16400;La Couronne;45.625025;0.103934
+0170087C;Lycée Gen Et Tech Agricole Georges Desclaude;Rue Georges Desclaude;17119;Saintes;45.7559394;-0.6430047
+0170393K;Lycée Gen Et Tech Agricole Ind.laitiere Agro-alimentaire;Avenue Francois Miterrand;17700;Surgeres;46.104913;-0.751411
+0791057V;Lycée Gen Et Tech Agricole Inst.rur.educ.orient.;47 Avenue De Poitiers;79170;Brioux Sur Boutonne;46.1527195;-0.2107398
+0790768F;Lycée Gen Et Tech Agricole Jacques Bujault;Route De La Roche;79500;Melle;46.2272777;-0.1514867
+0790706N;Lycée Gen Et Tech Agricole Les Sicaudieres;Boulevard De Nantes;79308;Bressuire;46.843654;-0.519819
+0491807Y;Lycée Gen Et Tech Agricole Pr La Verzee;1 Rue Du Chevalier D'avoynes;49520;Combree;47.705533;-1.025685
+0160980D;Lycée Gen Et Tech Agricole Roc Fleuri;6 Bd Des Grands Rocs;16700;Ruffec;46.0309545;0.2026405
+0171265H;Lycée Gen Et Tech Agricole Saint Antoine;2 Allee Saint Antoine;17240;Bois;45.476325;-0.5915032
+0860718N;Lycée Gen Et Tech Agricole Xavier Bernard;Venours;86480;Rouille;46.400326;0.072145
+0331683A;Lycée Gen Et Techno Agricole;Avenue De La Republique;33430;Bazas;44.4336384;-0.2244781
+0470662P;Lycée Gen Et Techno Agricole;Route De Francescas;47600;Nerac;44.1092632;0.3702156
+0780235G;Lycée Gen Et Technol Agricole Centre D Enseign Zootechnique;Parc Du Chateau;78120;Rambouillet;48.6748927;1.807541
+0290018M;Lycée Gen Et Technol Agricole De Brehoulou;Brehoulou;29170;Fouesnant;47.888817;-4.012919
+0290341N;Lycée Gen Et Technol Agricole De L'aulne;Rocade Du Parc De Bihan;29150;Chateaulin;48.195389;-4.094207
+0290126E;Lycée Gen Et Technol Agricole De Suscinio;Ploujean;29600;Morlaix;48.605845;-3.833374
+0120937D;Lycée Gen Et Technol Agricole Epla De Rodez;La Roque;12033;Rodez;44.3824285;2.5858486
+0220470S;Lycée Gen Et Technol Agricole Kernilien;Plouisy;22204;Guingamp;48.5763113;-3.1825986
+0560013K;Lycée Gen Et Technol Agricole Le Gros Chene;;56308;Pontivy;48.0565733;-2.9246647
+0221031B;Lycée Gen Et Technol Agricole Public;6 Rue Du Porhoet;22230;Merdrignac;48.190409;-2.4262933
+0350700W;Lycée Gen Et Technol Agricole Theodore Monod;55 Avenue De La Bouvardiere;35651;Le Rheu;48.0998905;-1.8040923
+0290181P;Lycée Gen Et Technol Privé Brest - Rive Droite - Javouhey;4 Rue Des Remparts;29238;Brest;48.3832192;-4.5023223
+0220094H;Lycée Gen Et Technol Privé Cordeliers-n-d De La Victoire;1 Place Des Cordeliers;22102;Dinan;48.4538527;-2.0443642
+0220102S;Lycée Gen Et Technol Privé Jacques Bossuet;4 Rue De La Bienfaisance;22304;Lannion;48.7287135;-3.4562299
+0220106W;Lycée Gen Et Technol Privé Jean Xxiii;1 Rue Du Seminaire;22800;Quintin;48.3991817;-2.9154743
+0290335G;Lycée Gen Et Technol Privé La Croix Rouge;2 Rue Mirabeau;29229;Brest;48.4075125;-4.4894481
+0290170C;Lycée Gen Et Technol Privé Le Likes;20 Place De La Tourbie;29196;Quimper;47.9996048;-4.1024423
+0290177K;Lycée Gen Et Technol Privé Nd Du Kreisker;2 Rue Cadiou;29250;St Pol De Leon;48.6825255;-3.9870981
+0220097L;Lycée Gen Et Technol Privé Notre Dame;21 Rue Des Capucins;22205;Guingamp;48.5613277;-3.1433799
+0220107X;Lycée Gen Et Technol Privé Notre Dame De Campostal;5 Place Du Bourg Coz;22110;Rostrenen;48.235163;-3.3159261
+0350769W;Lycée Gen Et Technol Privé Notre Dame Des Marais;9 Bis Rue Eugene Pacory;35304;Fougeres;48.3569757;-1.2046288
+0290164W;Lycée Gen Et Technol Privé Notre Dame Du Mur;10 Rue Guy Le Normand;29671;Morlaix;48.5731563;-3.8200636
+0560098C;Lycée Gen Et Technol Privé Notre Dame Du Voeu;Rue Du Champ De Foire;56704;Hennebont;47.8067327;-3.2789444
+0950759J;Lycée Gen Et Technol Privé Notre-dame De Bury;1 Av Georges Pompidou;95580;Margency;49.0026782;2.2838011
+0920928J;Lycée Gen Et Technol Privé Passy Buzenval;50 Avenue Otis Mygatt;92508;Rueil Malmaison;48.8591268;2.1832515
+0910808J;Lycée Gen Et Technol Privé St Charles;2 Rue G.anthonioz De Gaulle;91200;Athis Mons;48.7091074;2.3900057
+0781974X;Lycée Gen Et Technol Privé St Exupery;11 Rue Michael Faraday;78180;Montigny Le Bretonneux;48.7952039;2.0497303
+0221589H;Lycée Gen Et Technol Privé St Joseph;42 Rue De La Cheze;22600;Loudeac;48.174469;-2.7502399
+0291654R;Lycée Gen Et Technol Privé St Joseph;61 Rue Bayard;29187;Concarneau;47.8740713;-3.9214211
+0290156M;Lycée Gen Et Technol Privé St Louis;63 Grand'rue;29150;Chateaulin;48.1965682;-4.0868452
+0560101F;Lycée Gen Et Technol Privé St Louis;5 Rue Perault;56102;Lorient;47.7448536;-3.3636004
+0350777E;Lycée Gen Et Technol Privé St Martin;31 Rue D'antrain;35706;Rennes;48.1166798;-1.6785836
+0950762M;Lycée Gen Et Technol Privé St Martin De France;1 Avenue De Verdun;95300;Pontoise;49.042589;2.0905789
+0560117Y;Lycée Gen Et Technol Privé St Paul;12 Allee Gabriel Deshayes;56017;Vannes;47.6502167;-2.7624641
+0220111B;Lycée Gen Et Technol Privé St Pierre;16 Rue St Pierre;22015;St Brieuc;48.5128487;-2.7660968
+0350774B;Lycée Gen Et Technol Privé St Sauveur;16 Place St Sauveur;35603;Redon;47.6505318;-2.0844461
+0290159R;Lycée Gen Et Technol Privé St Sebastien;4 Rue H De Guebriant;29208;Landerneau;48.4510307;-4.2578666
+0350783L;Lycée Gen Et Technol Privé Ste Genevieve;14 Rue Ginguene;35006;Rennes;48.0990499;-1.6853943
+0560096A;Lycée Gen Et Technol Privé Ste Jeanne D'arc;2 Rue Penantraon;56110;Gourin;48.1392054;-3.6092159
+0290171D;Lycée Gen Et Technol Privé Ste Therese;56 Avenue De Remscheid;29337;Quimper;47.9811832;-4.0962165
+0920937U;Lycée Gen Et Technol Privé Suger;11 Sente De L Abbe Suger;92420;Vaucresson;48.84389;2.1624469
+0470019R;Lycée Gen Et Technol.agricole Etienne Restat;Route De Casseneuil;47110;Ste Livrade Sur Lot;44.4222789;0.6061423
+0594278E;Lycée Gen. Et Tech. Privé Lycée E.p.i.d.;20 Rue De Lille;59140;Dunkerque;51.0277456;2.3724543
+0381817U;Lycée Gen. Et Tech.agricole Grenoble St-ismier;1 Chemin Charvinieres;38332;St Ismier;45.2413573;5.8229581
+0271107G;Lycée Gen. Et Techn. Agricole;6 Rue Georges Politzer;27032;Evreux;49.0072785;1.150413
+0271016H;Lycée Gen. Et Techn. Agricole De Chambray;Gouville;27240;Gouville;48.8505833;0.9909485
+0500116E;Lycée Gen. Et Technol. Privé Germain;73 Rue D'ilkley;50208;Coutances;49.0608464;-1.4492908
+0520679F;Lycée Gen. Et Technol. Privé Immaculee Conception;1 Bis Rue Du Mal De Lattre;52115;St Dizier;48.6385221;4.9486945
+0500112A;Lycée Gen. Et Technol. Privé Notre Dame De La Providence;9 Rue Du Chanoine Beranger;50303;Avranches;48.686819;-1.3690481
+0070069M;Lycée Gen. Et Technol. Privé Sacre Coeur - Notre Dame;6 Boulevard De Vernon;07000;Privas;44.7359923;4.6003035
+0730763H;Lycée Gen. Et Technol. Privé Saint Ambroise;2 Rue Burdin;73025;Chambery;45.5703402;5.9246533
+0370737A;Lycée Gen. Et Technol. Privé Saint Denis;19 Av Du Gal De Gaulle;37601;Loches;47.127138;0.9929978
+0141164P;Lycée Gen. Et Technol. Privé Sainte Marie;8 Avenue De La Croix Guerin;14000;Caen;49.190183;-0.3565417
+0070066J;Lycée Gen. Et Technol. Privé Vernet;21 Avenue Notre Dame;07700;Bourg St Andeol;44.3730123;4.6450101
+0212082Z;Lycée Gen.et Tech. Agricole;7 Rue Du Champ De Foire;21140;Semur En Auxois;47.4953913;4.3360813
+0460727R;Lycée Gen.et Tech. Agricole;;46120;Lacapelle Marival;44.72784;1.925505
+0861392W;Lycée Gen.et Tech. Agricole Annexe Du Lycée De Venours;45 Avenue Maurice Bailly;86400;Civray;46.1358095;0.287391
+0480514Y;Lycée Gen.et Tech. Agricole Francois Rabelais;Civergols;48200;St Chely D Apcher;44.807858;3.302736
+0050012L;Lycée Gen.et Tech. Agricole Hautes-alpes (des);Les Emeyeres 127 Rte Valserres;05000;Gap;44.5416045;6.0907911
+0480657D;Lycée Gen.et Tech. Agricole Louis Pasteur;Chemin Du Fraissinet;48500;La Canourgue;44.4348979;3.242143
+0900246D;Lycée Gen.et Tech. Agricole Lucien Quelet;95 Rue De Turenne;90300;Valdoie;47.6785817;6.8404106
+0030094L;Lycée Gen.et Tech. Agricole Tourret;Neuville;03000;Neuvy;46.546982;3.287709
+0530081A;Lycée Gen.et Tech.agricole;321 Route De St Nazaire;53013;Laval;48.0476546;-0.7976282
+0850151C;Lycée Gen.et Tech.agricole Bel Air;1 Boulevard Hoche;85205;Fontenay Le Comte;46.4612234;-0.7962385
+0771436T;Lycée Gen.et Tech.agricole Bougainville;Domaine De Sansalle Rn 19;77257;Brie Comte Robert;48.6912267;2.6103697
+0442061B;Lycée Gen.et Tech.agricole Jules Rieffel;5 Rue De La Syonniere;44817;St Herblain;47.2468499;-1.6477727
+0720010V;Lycée Gen.et Tech.agricole La Germiniere;;72700;Rouillon;48.00385;0.135487
+0850152D;Lycée Gen.et Tech.agricole Lucon-petre;;85400;Ste Gemme La Plaine;46.483266;-1.113742
+0850144V;Lycée Gen.et Tech.prof.agricol Lycée Nature;Allee Des Druides;85035;La Roche Sur Yon;46.6641852;-1.3811329
+0890851S;Lycée Gen.et Techn.agricole Annexe;3 Rte De Champcevrais;89350;Champignelles;47.7807971;3.0679892
+0741219Y;Lycée Gen.et Techn.agricole H.b.de Saussure;;74920;Combloux;45.896937;6.641306
+0601700X;Lycée Gen.et Technol Privé Sevigne;20 Rue De La Sous Prefecture;60200;Compiegne;49.415931;2.8299869
+0382373Y;Lycée Gen.et Technol. Agricole Du Guiers Val D'ainan;6 Place Du Marche;38480;Le Pont De Beauvoisin;45.5367798;5.6698057
+0740082M;Lycée Gen.et Technol. Privé Saint Michel;27 Faubourg Des Balmettes;74007;Annecy;45.8918267;6.1237631
+0230030H;Lycée Gen.et Technol.agricole;Le Chaussadis;23150;Ahun;46.093558;2.030514
+0421088E;Lycée Gen.et Technol.agricole;Le Bourg;42600;Precieux;45.568298;4.081896
+0640220S;Lycée Gen.et Technol.agricole;Route De Pau;64121;Montardon;43.3616096;-0.3613466
+0690279D;Lycée Gen.et Technol.agricole;234 Route De Charly;69230;St Genis Laval;45.6727335;4.7911863
+0711067Z;Lycée Gen.et Technol.agricole;Poncetys;71960;Davaye;46.3067223;4.7375335
+0080863V;Lycée Gen.et Technol.agricole;27 Rue Du Muguet;08090;St Laurent;49.7642188;4.7790123
+0180636U;Lycée Gen.et Technol.agricole;Route De Barlieu;18260;Vailly Sur Sauldre;47.4622514;2.6472006
+0331424U;Lycée Gen.et Technol.agricole;84 Av Du General De Gaulle;33294;Blanquefort;44.9172959;-0.6343214
+0381819W;Lycée Gen.et Technol.agricole;57 Avenue Charles De Gaulle;38260;La Cote St Andre;45.3874636;5.2629127
+0400750Y;Lycée Gen.et Technol.agricole;Heugas;40180;Heugas;43.6302977;-1.0735488
+0501213X;Lycée Gen.et Technol.agricole;Route De Montmartin;50200;Coutances;49.047808;-1.4452609
+0601782L;Lycée Gen.et Technol.agricole;;60600;Airion;49.425303;2.416204
+0610790C;Lycée Gen.et Technol.agricole;Rue Du 11 Novembre 1918;61500;Sees;48.5959227;0.1730679
+0680003J;Lycée Gen.et Technol.agricole;8 Aux Remparts;68250;Rouffach;47.9555409;7.3007998
+0711068A;Lycée Gen.et Technol.agricole;10 Lieu Dit La Platiere;71150;Fontaines;46.859713;4.771907
+0820559M;Lycée Gen.et Technol.agricole;Dom Capou 1915 Rte De Bordeaux;82000;Montauban;44.0324358;1.3154167
+0881070K;Lycée Gen.et Technol.agricole;Avenue De Lattre De Tassigny;88500;Mirecourt;48.2920391;6.1305618
+0060793E;Lycée Gen.et Technol.agricole Agricole Et Horticole;1285 Avenue Jules Grec;06600;Antibes;43.5991017;7.1129973
+0830077H;Lycée Gen.et Technol.agricole Agricole Et Horticole;Carrefour Du Lycée;83408;Hyeres;0.0;0.0
+0381886U;Lycée Gen.et Technol.agricole Agrotec;Montee Bon Accueil;38206;Vienne;45.5465829;4.8648632
+0890045R;Lycée Gen.et Technol.agricole Auxerre-la-brosse;La Brosse;89290;Venoy;47.7928286;3.6535285
+0320039U;Lycée Gen.et Technol.agricole Beaulieu-lavacant;Route De Tarbes;32020;Auch;43.6233119;0.5770361
+0251263M;Lycée Gen.et Technol.agricole Besancon Granvelle;2 Rue Des Chanets;25410;Dannemarie Sur Crete;47.2020212;5.873984
+0190244M;Lycée Gen.et Technol.agricole Brive-objat;23 Rue Murat;19130;Voutezac;45.2831337;1.4250998
+0593395V;Lycée Gen.et Technol.agricole Communaute Urbaine De Lille;Rue De La Mitterie;59463;Lille;50.6487941;2.9947205
+9710804X;Lycée Gen.et Technol.agricole Convenance;Convenance;97122;Baie Mahault;16.2370417;-61.5986037
+9720616M;Lycée Gen.et Technol.agricole Croix Rivail;Croix Rivail;97232;Le Lamentin;14.6131612;-60.9523142
+0511196Y;Lycée Gen.et Technol.agricole De Chalons En Champagne;;51460;Somme Vesle;48.985722;4.590599
+0020060E;Lycée Gen.et Technol.agricole De Crezancy;1 Rue De Paris;02650;Crezancy;49.0498047;3.5155052
+0730813M;Lycée Gen.et Technol.agricole De La Motte Servolex;Domaine Reinach;73290;La Motte Servolex;45.596488;5.874112
+0080804F;Lycée Gen.et Technol.agricole De Rethel;Route De Novion;08300;Rethel;49.5285468;4.3743447
+0211135V;Lycée Gen.et Technol.agricole Dijon-quetigny;21 Bd Olivier De Serres;21800;Quetigny;47.3091823;5.1138121
+0593255T;Lycée Gen.et Technol.agricole Du Nord;458 Rue De La Motte Julien;59507;Douai;50.3892001;3.0651718
+0622257S;Lycée Gen.et Technol.agricole Du Pas-de-calais;Route De Cambrai;62217;Tilloy Les Mofflaines;50.274208;2.83736
+0681222J;Lycée Gen.et Technol.agricole Du Pflixbourg;2 Lieu Dit Saint Gilles;68920;Wintzenheim;48.073122;7.289782
+0251202W;Lycée Gen.et Technol.agricole Ecole D'industrie Laitiere;Grande Rue;25620;Mamirolle;47.196804;6.158412
+0390812W;Lycée Gen.et Technol.agricole Ecole D'industrie Laitiere;Rue De Versailles;39801;Poligny;46.83417;5.705002
+0390810U;Lycée Gen.et Technol.agricole Edgar Faure;614 Avenue Edgar Faure;39570;Montmorot;46.689511;5.5099358
+0010059J;Lycée Gen.et Technol.agricole Edouard Herriot;Domaine De Cibeins;01600;Miserieux;45.9973531;4.8236968
+9740078S;Lycée Gen.et Technol.agricole Emile Boyer De La Giroday;165 Route De Mafate;97460;St Paul;0.0;0.0
+0810121S;Lycée Gen.et Technol.agricole Epla D Albi;Fonlabour Route De Toulouse;81000;Albi;43.9185017;2.1151666
+0312354R;Lycée Gen.et Technol.agricole Epla D Ondes;Rue Castelnau;31330;Ondes;43.782461;1.316476
+0460490H;Lycée Gen.et Technol.agricole Epla De Figeac;La Vinadie;46100;Figeac;44.573023;2.028841
+0090022A;Lycée Gen.et Technol.agricole Epla De Pamiers;Route De Belpech;09100;Pamiers;43.12866;1.6549179
+0311262D;Lycée Gen.et Technol.agricole Epla De Toulouse Auzeville;2 Rte De Narbonne;31326;Castanet Tolosan;43.5348091;1.4844659
+0120621K;Lycée Gen.et Technol.agricole Epla Villefranche De Rouergue;Beauregard Cote Mas De Bonnet;12204;Villefranche De Rouergue;44.349746;2.035381
+0240023V;Lycée Gen.et Technol.agricole Et Agro-alimentaire;Avenue Winston Churchill;24660;Coulounieix Chamiers;45.1872222;0.6922222
+0700827U;Lycée Gen.et Technol.agricole Etienne Munier;Le Grand Montmarin;70014;Vesoul;47.619788;6.15428
+0660699B;Lycée Gen.et Technol.agricole Federico Garcia Lorca;Route Nationale 114;66200;Theza;42.6020137;2.9695758
+0100687T;Lycée Gen.et Technol.agricole Forestier De Crogny;Rue Des Etangs;10210;Les Loges Margueron;48.0863362;4.0991274
+0740927F;Lycée Gen.et Technol.agricole Industrie Laitiere;;74805;La Roche Sur Foron;46.0666904;6.3112656
+0623277A;Lycée Gen.et Technol.agricole Institut Agricole Saint Eloi;36 Rue Marcellin Gaudefroy;62452;Bapaume;50.1027923;2.8464795
+0650139Y;Lycée Gen.et Technol.agricole Jean Monnet Epla Vic-en-big;11 Bis Promenade Des Acacias;65500;Vic En Bigorre;43.3823572;0.0457979
+0211217J;Lycée Gen.et Technol.agricole La Barotte;La Barotte Rte De Langres;21403;Chatillon Sur Seine;47.85948;4.614945
+0690250X;Lycée Gen.et Technol.agricole La Bruyere;26 Chemin De La Bruyere;69570;Dardilly;45.813486;4.769397
+0450094H;Lycée Gen.et Technol.agricole Le Chesnoy;Le Chesnoy;45200;Amilly;47.971965;2.7392439
+0801272Y;Lycée Gen.et Technol.agricole Le Paraclet;Le Paraclet Route De Cottenchy;80440;Boves;49.8249135;2.3942367
+0260765R;Lycée Gen.et Technol.agricole Le Valentin;Route Nationale 7;26500;Bourg Les Valence;44.9965985;4.8429604
+0180585N;Lycée Gen.et Technol.agricole Legta;Rue Theophile Gauthier;18028;Bourges;47.1059678;2.412406
+0280706R;Lycée Gen.et Technol.agricole Legta;La Saussaye;28630;Sours;48.406038;1.556544
+0360017Y;Lycée Gen.et Technol.agricole Legta;Route De Velles;36018;Chateauroux;46.7822046;1.6898082
+0360600G;Lycée Gen.et Technol.agricole Legta;16 Rue H De Latouche;36400;La Chatre;46.5790648;1.9898776
+0370781Y;Lycée Gen.et Technol.agricole Legta;La Petite Plaine;37230;Fondettes;47.399712;0.6381559
+0410018X;Lycée Gen.et Technol.agricole Legta;Rue De La Valle Du Loir;41106;Vendome;47.8004267;1.0920294
+0451535Z;Lycée Gen.et Technol.agricole Les Barres;Les Barres;45290;Nogent Sur Vernisson;47.838247;2.756901
+0010819K;Lycée Gen.et Technol.agricole Les Sardieres;79 Avenue De Jasseron;01000;Bourg En Bresse;46.2116792;5.2565142
+0570112M;Lycée Gen.et Technol.agricole Lycée Agricole Departemental;40 Route De Strasbourg;57170;Chateau Salins;48.8187729;6.515628
+0570086J;Lycée Gen.et Technol.agricole Metz-courcelles-chaussy;1 3 Av D Urville;57530;Courcelles Chaussy;49.1086404;6.38191
+0331863W;Lycée Gen.et Technol.agricole Montagne;Le Bourg;33570;Montagne;44.971429;-0.141059
+0541207U;Lycée Gen.et Technol.agricole Nancy-pixerecourt;Pixerecourt;54220;Malzeville;48.7304099;6.1804006
+0071125K;Lycée Gen.et Technol.agricole Olivier De Serres;;07205;Aubenas;44.2136489;3.999401
+0550752U;Lycée Gen.et Technol.agricole Philippe De Vilmorin;Chemin De Popey;55000;Bar Le Duc;48.7618197;5.1849786
+0623723K;Lycée Gen.et Technol.agricole Radinghem;Rue Neuve;62310;Radinghem;50.55078;2.117642
+0421078U;Lycée Gen.et Technol.agricole Roanne-cherve;;42120;Perreux;46.0376199;4.122751
+6200190M;Lycée Gen.et Technol.agricole Sartene;Route De Levie;20100;Sartene;41.621822;8.97472
+0500060U;Lycée Gen.et Technol.agricole St Lo There;;50620;Le Hommet D Arthenay;49.189776;-1.188463
+0950786N;Lycée Gen.et Technol.prive St Rosaire;53 Rue Pierre Brossolette;95200;Sarcelles;48.9919938;2.3747125
+9830504F;Lycée Gen.et Technol.prive Apollinaire Anova (ddec);24 C Rte Bretelle Savexpress;98890;Paita;0.0;0.0
+0690539L;Lycée Gen.et Technol.prive Assomption Bellevue;39 Quai Jean Jacques Rousseau;69350;La Mulatiere;45.7310895;4.8137961
+0690522T;Lycée Gen.et Technol.prive Aux Lazaristes;24 Montee Saint Barthelemy;69321;Lyon 05;45.7638385;4.826226
+0940822H;Lycée Gen.et Technol.prive Batiment Et Travaux Publics;18 Rue De Belfort;94307;Vincennes;48.8501369;2.4311329
+0880099E;Lycée Gen.et Technol.prive Beau Jardin;9 Rue Du Beau Jardin;88104;St Die Des Vosges;48.2892768;6.9523158
+0870081R;Lycée Gen.et Technol.prive Beaupeyrat;9 Ter Rue Petiniaud Beaupeyrat;87036;Limoges;45.8280651;1.2550534
+0300077W;Lycée Gen.et Technol.prive Bellevue;14 Rue Alfred De Musset;30100;Ales;44.1324998;4.0852321
+0690536H;Lycée Gen.et Technol.prive Belmont-capdepon;43 Rue Pasteur;69007;Lyon 07;45.7526828;4.8397656
+0641907A;Lycée Gen.et Technol.prive Bernat Etxepare;12 Bis Av Mounede;64100;Bayonne;43.4986058;-1.4580377
+9830261S;Lycée Gen.et Technol.prive Blaise Pascal (ddec);22 Rue B. Pascal A. Vata;98807;Noumea;-22.2980465;166.4424651
+0783282U;Lycée Gen.et Technol.prive Blanche De Castille;1 Avenue De La Breteche;78150;Le Chesnay;48.8295148;2.1317755
+0772324H;Lycée Gen.et Technol.prive Campus Sainte Therese;Av Erasme - Rond Pt De L'europe;77330;Ozoir La Ferriere;48.763827;2.671739
+9741256X;Lycée Gen.et Technol.prive Catholique Levavasseur;4 Rue Fenelon;97400;St Denis;48.7876977;2.5414736
+0442083A;Lycée Gen.et Technol.prive Cens;La Joneliere;44244;La Chapelle Sur Erdre;47.2794482;-1.5401117
+0430056C;Lycée Gen.et Technol.prive Cevenol;;43400;Le Chambon Sur Lignon;45.061316;4.3021269
+0490952U;Lycée Gen.et Technol.prive Champ Blanc;Champ Blanc;49710;Le Longeron;47.016606;-1.067741
+0691654Y;Lycée Gen.et Technol.prive Champagnat;250 Rue Chanoine Pavailler;69590;St Symphorien Sur Coise;45.6291908;4.4521899
+0542408Z;Lycée Gen.et Technol.prive Charles De Foucauld;1 Rue Jeannot;54000;Nancy;48.6894923;6.1881706
+0690514J;Lycée Gen.et Technol.prive Charles De Foucauld;6 Rue Bara;69003;Lyon 03;45.7514974;4.871623
+0754086J;Lycée Gen.et Technol.prive Charles De Foucauld;5 Rue De La Madone;75018;Paris 18;48.8927022;2.3612027
+0753890W;Lycée Gen.et Technol.prive Charles Peguy;54 Avenue De La Republique;75011;Paris 11;48.8649983;2.375791
+0530049R;Lycée Gen.et Technol.prive D'avesnieres;51 Rue D Avesnieres;53012;Laval;48.0625624;-0.7676675
+0441982R;Lycée Gen.et Technol.prive De Bretagne;15 Rue Du Landreau;44300;Nantes;47.2324995;-1.5206435
+0570311D;Lycée Gen.et Technol.prive De La Salle;2 Rue St Maximin;57070;Metz;49.1048183;6.1935928
+0690542P;Lycée Gen.et Technol.prive Des Chassagnes;13 Chemin Des Chassagnes;69600;Oullins;45.7227891;4.8097245
+0541309E;Lycée Gen.et Technol.prive Des Recollets;44 Rue Du General Pershing;54400;Longwy;49.5170129;5.7657012
+0060773H;Lycée Gen.et Technol.prive Don Bosco;40 Place Don Bosco;06046;Nice;43.7086006;7.2813297
+0211068X;Lycée Gen.et Technol.prive Du Saint Coeur;3 Rue Du Faubourg St Nicolas;21200;Beaune;47.0279594;4.8395949
+0672050P;Lycée Gen.et Technol.prive Ecole Commerciale Privée;19 Rue De Wissembourg;67000;Strasbourg;48.591122;7.741682
+0300080Z;Lycée Gen.et Technol.prive Emmanuel D Alzon;11 Rue Sainte Perpetue;30020;Nimes;43.834299;4.3718
+0010069V;Lycée Gen.et Technol.prive Ensemble Scolaire Lamartine;41 Rue Georges Girerd;01302;Belley;45.7612095;5.6848602
+0941719H;Lycée Gen.et Technol.prive Epin;19 Avenue Eugene Pelletan;94400;Vitry Sur Seine;48.7942096;2.3847676
+9730010Y;Lycée Gen.et Technol.prive Externat Saint Joseph;15 Rue Lallouette;97326;Cayenne;4.9393005;-52.3308137
+0690521S;Lycée Gen.et Technol.prive Externat Sainte Marie;4 Montee Saint Barthelemy;69321;Lyon 05;45.7655825;4.8268167
+0060674A;Lycée Gen.et Technol.prive Fenelon;7 Avenue Yves Emmanuel Baudouin;06130;Grasse;43.6589192;6.9208425
+0761708U;Lycée Gen.et Technol.prive Fenelon;2 Rue D'alsace;76501;Elbeuf;49.29007;1.0141247
+0932036H;Lycée Gen.et Technol.prive Francoise Cabrini;20 Rue Du Docteur Sureau;93167;Noisy Le Grand;48.8461618;2.5491228
+0593007Y;Lycée Gen.et Technol.prive Frederic Ozanam;50 Rue Saint Gabriel;59045;Lille;50.6393334;3.0867728
+0440175B;Lycée Gen.et Technol.prive Gabriel Deshayes;Route De Redon;44530;St Gildas Des Bois;47.5223759;-2.044929
+0631068U;Lycée Gen.et Technol.prive Godefroy De Bouillon;14 Rue Godefroy De Bouillon;63037;Clermont Ferrand;45.781122;3.0929055
+0370727P;Lycée Gen.et Technol.prive Grand St Gregoire;3 Quai Paul Bert;37081;Tours;47.4014833;0.685655
+0940821G;Lycée Gen.et Technol.prive Gregor Mendel;205 Rue De Fontenay;94300;Vincennes;48.8488616;2.4272778
+0601702Z;Lycée Gen.et Technol.prive Guynemer;8 Bis Av De La Foret;60205;Compiegne;49.4087315;2.840105
+0910823A;Lycée Gen.et Technol.prive Ile De France;7 Rue Du Baron De Niviere;91140;Villebon Sur Yvette;48.7000351;2.2570051
+0640130U;Lycée Gen.et Technol.prive Immac.concept.beau Frene;Bd Edouard Herriot;64051;Pau;43.3050247;-0.3782249
+0530048P;Lycée Gen.et Technol.prive Immaculee Conception;15 Rue Crossardiere;53000;Laval;48.0734452;-0.7680567
+0592965C;Lycée Gen.et Technol.prive Industriel Et Commercial Privé;27 Rue Du Dragon;59202;Tourcoing;50.7191345;3.1488557
+9720070U;Lycée Gen.et Technol.prive Institut Martiniquais D'etudes;74 Rue Du Pr Raymond Garcin;97200;Fort De France;14.609371;-61.07256
+0595657D;Lycée Gen.et Technol.prive Institut Saint Vincent De Paul;6 Rue Du Marechal Joffre;59120;Loos;50.6162933;3.0052777
+0500120J;Lycée Gen.et Technol.prive Institut Saint-lo;18 Rue De L'oratoire;50180;Agneaux;49.1184709;-1.1060995
+0681917P;Lycée Gen.et Technol.prive Institution Don Bosco;60 Rue D'ensisheim;68272;Wittenheim;47.8136036;7.3394336
+0671610L;Lycée Gen.et Technol.prive Institution Notre Dame;6 Rue Des Bonnes Gens;67082;Strasbourg;48.5880447;7.7433479
+0671602C;Lycée Gen.et Technol.prive Institution Ste Philomene;19a Boulevard Hanauer;67504;Haguenau;48.8113152;7.7888563
+0622150A;Lycée Gen.et Technol.prive Jean Bosco;19 Place Du Marechal Foch;62340;Guines;50.869395;1.8697706
+0761721H;Lycée Gen.et Technol.prive Jean Xxiii;16 Rue De La Gare;76191;Yvetot;49.6223882;0.7592408
+0850076W;Lycée Gen.et Technol.prive Jean Xxiii;Avenue Des Sables;85505;Les Herbiers;46.8650254;-1.0292923
+0690671E;Lycée Gen.et Technol.prive Jean-baptiste De La Salle;1 Rue Neyret;69283;Lyon 01;45.7715543;4.8280213
+0761715B;Lycée Gen.et Technol.prive Jean-baptiste De La Salle;84 Rue Saint-gervais;76042;Rouen;49.4481293;1.08442
+0541320S;Lycée Gen.et Technol.prive Jean-baptiste Vatelot;6 Rue De La Republique;54203;Toul;48.6729066;5.8919085
+0132300E;Lycée Gen.et Technol.prive Jeanne D Arc;5 Chemin Saint Roch;13632;Arles;43.6743556;4.6432771
+0610694Y;Lycée Gen.et Technol.prive Jeanne D Arc;10 Rue Du College;61203;Argentan;48.7461287;-0.0164951
+0680151V;Lycée Gen.et Technol.prive Jeanne D Arc;15 Rue Du Chanoine Brun;68090;Mulhouse;47.7434603;7.3370328
+0910815S;Lycée Gen.et Technol.prive Jeanne D Arc;11 Bd Henri Iv;91150;Etampes;48.4358438;2.1576582
+0920918Y;Lycée Gen.et Technol.prive Jeanne D Arc;44 Rue Gabriel Peri;92120;Montrouge;48.8187323;2.3185306
+0921663H;Lycée Gen.et Technol.prive Jeanne D Arc;9 Boulevard De Valmy;92700;Colombes;48.9248684;2.2526402
+0390091M;Lycée Gen.et Technol.prive Jeanne D'arc;10 Rue Du Sauget;39303;Champagnole;46.7456228;5.9058307
+0710112L;Lycée Gen.et Technol.prive Jeanne D'arc;17 Rue Pasteur;71600;Paray Le Monial;46.4526677;4.1219752
+0880097C;Lycée Gen.et Technol.prive Jeanne D'arc;6 Rue Du Canton;88202;Remiremont;48.0192476;6.5872824
+0690535G;Lycée Gen.et Technol.prive Jeanne De Lestonnac;132 Rue Vendome;69006;Lyon 06;45.7648477;4.8456527
+0761716C;Lycée Gen.et Technol.prive Join-lambert;39 Rue De L'avalasse;76044;Rouen;49.4489928;1.0980641
+0070064G;Lycée Gen.et Technol.prive Jules Froment;3 Rue Albert Seibel;07201;Aubenas;44.6172525;4.3886254
+0331495W;Lycée Gen.et Technol.prive L Assomption;370 Bd President Wilson;33000;Bordeaux;44.8520362;-0.5944037
+0783283V;Lycée Gen.et Technol.prive L Ermitage;18 Rue Des Cotes;78600;Maisons Laffitte;48.9430565;2.1455384
+0430063K;Lycée Gen.et Technol.prive La Bruyere;1 Boulevard Des Passementiers;43140;St Didier En Velay;45.2992188;4.2760259
+0132828D;Lycée Gen.et Technol.prive La Cadenelle;134 Bd Des Liberateurs;13012;Marseille 12;43.3020176;5.4572235
+0430055B;Lycée Gen.et Technol.prive La Chartreuse;Rue Du Pont De La Chartreuse;43700;Brives Charensac;45.0529219;3.9138113
+0690520R;Lycée Gen.et Technol.prive La Favorite Sainte Therese;62 Rue De La Favorite;69322;Lyon 05;45.7578457;4.8120645
+0601703A;Lycée Gen.et Technol.prive La Maison Francaise;La Chesnoye;60350;Cuise La Motte;49.372412;2.996137
+9710062R;Lycée Gen.et Technol.prive La Perseverance Mixte;Boissard;97182;Pointe A Pitre;16.2386048;-61.5386617
+0021872Z;Lycée Gen.et Technol.prive La Providence;2 Rue Clerjot;02001;Laon;49.5636589;3.6226864
+0761707T;Lycée Gen.et Technol.prive La Providence;Chemin Des Vertus;76371;Dieppe;49.905225;1.074298
+0761713Z;Lycée Gen.et Technol.prive La Providence;6 Rue De Neuvillette;76240;Le Mesnil Esnard;49.4064428;1.1403376
+0763112V;Lycée Gen.et Technol.prive La Providence;35 Rue Queue De Renard;76400;Fecamp;49.7565044;0.3888415
+0920916W;Lycée Gen.et Technol.prive La Source;11 Rue Ernest Renan;92190;Meudon;48.8188276;2.2268149
+0340871M;Lycée Gen.et Technol.prive La Trinite;6 Avenue Jean Moulin;34503;Beziers;43.3467234;3.219544
+0640111Y;Lycée Gen.et Technol.prive Largente;42 Avenue Jacques Loeb;64115;Bayonne;43.4776655;-1.4841692
+0541305A;Lycée Gen.et Technol.prive L'assomption;1 Rue Foch;54150;Briey;49.2499091;5.9385613
+0783297K;Lycée Gen.et Technol.prive Le Bon Sauveur;6 Rue Henri Cloppet;78110;Le Vesinet;48.8873186;2.1463532
+0040034R;Lycée Gen.et Technol.prive Le Sacre Coeur;2 Avenue Des Thermes;04000;Digne Les Bains;44.0917238;6.2407469
+0251024C;Lycée Gen.et Technol.prive Les Augustins;1 Rue Du Fbg St Etienne;25300;Pontarlier;46.9020595;6.3592476
+0783293F;Lycée Gen.et Technol.prive Les Chataigniers;11 Bis Avenue Jean Jaures;78000;Versailles;48.8174674;2.1442539
+0860755D;Lycée Gen.et Technol.prive Les Feuillants;15 Rue Des Feuillants;86035;Poitiers;46.5844278;0.3491711
+0753820V;Lycée Gen.et Technol.prive Les Francs Bourgeois;21 Rue Saint Antoine;75004;Paris 04;48.8535159;2.3657551
+9710991A;Lycée Gen.et Technol.prive Les Perseverants;28 Rue Lardenoy;97100;Basse Terre;15.9932345;-61.7242003
+0753815P;Lycée Gen.et Technol.prive Les Petits Champs;19 Rue Voltaire;75011;Paris 11;48.8525604;2.3915183
+0840059H;Lycée Gen.et Technol.prive Louis Pasteur;13 Rue Du Pont Trouca;84071;Avignon;43.9478109;4.8132766
+0754965P;Lycée Gen.et Technol.prive Lucien De Hirsch;70 Avenue Secretan;75019;Paris 19;48.8797122;2.3764183
+9840017W;Lycée Gen.et Technol.prive Lycée-college La Mennais;Papeete;98714;Papeete;-17.535022;-149.569594
+0921365J;Lycée Gen.et Technol.prive Madeleine Danielou;61 63 Rue Du General Miribel;92500;Rueil Malmaison;48.8614432;2.1711774
+9710054G;Lycée Gen.et Technol.prive Maitrise De Massabielle;29 Faubourg Victor Hugo;97157;Pointe A Pitre;16.241111;-61.533056
+0592980U;Lycée Gen.et Technol.prive Marie Noel;31 Rue De Renaix;59200;Tourcoing;50.7228956;3.1689614
+0801743K;Lycée Gen.et Technol.prive Montalembert;3 Route D Amiens;80600;Doullens;50.153554;2.3391028
+0331506H;Lycée Gen.et Technol.prive Montesquieu;31 Rue Jules Simon;33500;Libourne;44.9133394;-0.2462284
+0490828J;Lycée Gen.et Technol.prive N-d De Bonnes Nouvelles;3 Rue Mongazon;49600;Beaupreau;47.1982814;-0.9966793
+0850079Z;Lycée Gen.et Technol.prive Nd De La Tourteliere;Rue Buffon;85700;Pouzauges;46.773501;-0.835248
+0720834R;Lycée Gen.et Technol.prive Nd De Ste Croix;25 Rue Antoine De St Exupery;72000;Le Mans;48.0000944;0.2133997
+0950761L;Lycée Gen.et Technol.prive N-dame De La Compassion;8 Place Nicolas Flamel;95300;Pontoise;49.0520594;2.0991487
+0920919Z;Lycée Gen.et Technol.prive N-dame De Ste Croix;30 Avenue Du Roule;92200;Neuilly Sur Seine;48.8819566;2.2806369
+0783351U;Lycée Gen.et Technol.prive N-dame Du Grandchamp;97 Rue Royale;78000;Versailles;48.7933549;2.1239896
+0280665W;Lycée Gen.et Technol.prive Notre Dame;2 Avenue Bethouart;28000;Chartres;48.4511272;1.4921613
+0720837U;Lycée Gen.et Technol.prive Notre Dame;23 Av Francois Mitterrand;72000;Le Mans;48.0038785;0.1997462
+0783288A;Lycée Gen.et Technol.prive Notre Dame;3 Rue De Temara;78100;St Germain En Laye;48.8940298;2.0754316
+0783289B;Lycée Gen.et Technol.prive Notre Dame;5 Rue De La Sangle;78200;Mantes La Jolie;48.9886407;1.7215203
+0783344L;Lycée Gen.et Technol.prive Notre Dame;106 Grande Rue;78480;Verneuil Sur Seine;48.9826866;1.9733268
+0830100H;Lycée Gen.et Technol.prive Notre Dame;29 Boulevard Abbe Duploye;83100;Toulon;43.1279722;5.956097
+0920897A;Lycée Gen.et Technol.prive Notre Dame;1 Avenue Charles De Gaulle;92100;Boulogne Billancourt;48.8467426;2.2368542
+0880101G;Lycée Gen.et Technol.prive Notre Dame - Saint Joseph;23 Rue Thiers;88012;Epinal;48.1701687;6.4513835
+0693267B;Lycée Gen.et Technol.prive Notre Dame De Bel Air;5 Avenue Des Belges;69170;Tarare;45.8904997;4.4380599
+0660059F;Lycée Gen.et Technol.prive Notre Dame De Bon Secours;39 Avenue Julien Panchot;66028;Perpignan;42.6915814;2.8827875
+0340881Y;Lycée Gen.et Technol.prive Notre Dame De La Merci;62 Cours Gambetta;34965;Montpellier;43.6082615;3.8686748
+0570313F;Lycée Gen.et Technol.prive Notre Dame De La Providence;22 Place Notre Dame;57126;Thionville;49.3576246;6.1602375
+0950753C;Lycée Gen.et Technol.prive Notre Dame De La Providence;7 Boulevard Sadi Carnot;95880;Enghien Les Bains;48.967214;2.3122446
+0601701Y;Lycée Gen.et Technol.prive Notre Dame De La Tilloye;1 Avenue De La Liberation;60204;Compiegne;49.4062037;2.8305249
+0170103V;Lycée Gen.et Technol.prive Notre Dame De Recouvrance;88 Cours Genet;17100;Saintes;45.7486442;-0.65005
+0910816T;Lycée Gen.et Technol.prive Notre Dame De Sion;1 Avenue De Ratisbonne;91000;Evry;48.6437303;2.4357094
+0900029T;Lycée Gen.et Technol.prive Notre Dame Des Anges;46 Bis Faubourg De Montbeliard;90000;Belfort;47.6334827;6.8570422
+0410675L;Lycée Gen.et Technol.prive Notre Dame Des Aydes;7 Rue Franciade;41000;Blois;47.5897279;1.3293085
+0381683Y;Lycée Gen.et Technol.prive Notre Dame Des Victoires;1 Rue De La Terrasse;38506;Voiron;45.367053;5.59632
+0430058E;Lycée Gen.et Technol.prive Notre Dame Du Chateau;Montee Du Prince;43120;Monistrol Sur Loire;45.292432;4.172183
+0470063N;Lycée Gen.et Technol.prive Notre Dame La Compassion;1 Ter Rue De Langeot;47200;Marmande;44.4978288;0.1723321
+0480025S;Lycée Gen.et Technol.prive Notre-dame;Quartier Fontanilles;48000;Mende;44.517611;3.501873
+0920906K;Lycée Gen.et Technol.prive Notre-dame;65 Avenue Du General Leclerc;92340;Bourg La Reine;48.7817469;2.3162608
+0920917X;Lycée Gen.et Technol.prive Notre-dame;24 Rue Alexandre Guilmant;92190;Meudon;48.8157744;2.2403627
+0950785M;Lycée Gen.et Technol.prive Notre-dame;106 Bd Charles De Gaulle;95110;Sannois;48.9730488;2.2543252
+0592916Z;Lycée Gen.et Technol.prive Notre-dame De Grace;Quai Des Nerviens;59602;Maubeuge;50.2764329;3.9688679
+0753842U;Lycée Gen.et Technol.prive Notre-dame De Sion;61 R Notre Dame Des Champs;75006;Paris 06;48.84375;2.3310368
+0593036E;Lycée Gen.et Technol.prive Notre-dame Du Tilleul;48 Place De L'industrie;59600;Maubeuge;50.2706855;3.9481957
+0951940T;Lycée Gen.et Technol.prive Ozar Hatorah;1 Rue Jean Lurcat;95200;Sarcelles;48.9827985;2.3890294
+0754666P;Lycée Gen.et Technol.prive Passy-saint-honore;117 Avenue Victor Hugo;75016;Paris 16;48.8682833;2.282365
+9710055H;Lycée Gen.et Technol.prive Pensionnat De Versailles;8 Rue Victor Hugues;97100;Basse Terre;16.1985639;-61.5901551
+0341521U;Lycée Gen.et Technol.prive Pierre Rouge;85 Rue Lunaret;34090;Montpellier;43.6207134;3.8837999
+0381666E;Lycée Gen.et Technol.prive Pierre Termier;5 Bis Rue Joseph Fourier;38028;Grenoble;45.1889828;5.7349988
+0341765J;Lycée Gen.et Technol.prive Rabelais;36 Rue Des Aiguerelles;34000;Montpellier;43.6026039;3.883383
+0920898B;Lycée Gen.et Technol.prive Rambam;11 Rue Des Abondances;92100;Boulogne Billancourt;48.8431743;2.2294952
+0761718E;Lycée Gen.et Technol.prive Rey;15 Rue Verte;76000;Rouen;49.4479795;1.0934318
+0941720J;Lycée Gen.et Technol.prive Robert Schuman;5-6 Rue De L'eglise;94340;Joinville Le Pont;48.8192804;2.4664616
+0381678T;Lycée Gen.et Technol.prive Robin;Place Saint Pierre;38204;Vienne;45.5234014;4.8708709
+0430065M;Lycée Gen.et Technol.prive Sacre Coeur;11 Place Charles De Gaulle;43200;Yssingeaux;45.1433694;4.1235441
+0492015Z;Lycée Gen.et Technol.prive Sacre Coeur;2 Rue Millet;49101;Angers;47.4727929;-0.556212
+0801207C;Lycée Gen.et Technol.prive Sacre Coeur;1 Rue De L Oratoire;80007;Amiens;49.8933658;2.3044821
+0801226Y;Lycée Gen.et Technol.prive Sacre Coeur;36 Boulevard Des Anglais;80200;Peronne;49.9264177;2.9342576
+0910824B;Lycée Gen.et Technol.prive Sacre Coeur;Passage De Graville;91620;La Ville Du Bois;48.6609531;2.2674756
+0131862D;Lycée Gen.et Technol.prive Sacre Coeur (le);29 Rue Manuel;13100;Aix En Provence;43.5282729;5.453657
+0761719F;Lycée Gen.et Technol.prive Sacre-coeur;31 32 Rue Blaise Pascal;76176;Rouen;49.4297216;1.0808737
+0631075B;Lycée Gen.et Technol.prive Saint Alyre;20 Rue Sainte George;63037;Clermont Ferrand;45.7834986;3.0818986
+0030084A;Lycée Gen.et Technol.prive Saint Benoit;4 Rue Achille Roche;03008;Moulins;46.5632283;3.3286114
+0690563M;Lycée Gen.et Technol.prive Saint Bruno-saint Louis;16 Rue Des Chartreux;69283;Lyon 01;45.7719779;4.8230673
+0382854W;Lycée Gen.et Technol.prive Saint Charles;1 Place Des Capucins;38217;Vienne;45.5275795;4.8766132
+0450110A;Lycée Gen.et Technol.prive Saint Charles;24 Rue Des Grands Champs;45058;Orleans;47.9055842;1.8998948
+0790058J;Lycée Gen.et Technol.prive Saint Charles;1 Rue Jules Michelet;79101;Thouars;46.9785341;-0.2100378
+0580062C;Lycée Gen.et Technol.prive Saint Cyr;22 Rue Jeanne D Arc;58000;Nevers;46.991543;3.1530512
+0071126L;Lycée Gen.et Technol.prive Saint Denis;1 Chemin De La Muette;07104;Annonay;45.2411669;4.6779477
+0601699W;Lycée Gen.et Technol.prive Saint Esprit;68 Rue De Pontoise;60026;Beauvais;49.4192451;2.086629
+0593136N;Lycée Gen.et Technol.prive Saint Jacques;58 Rue De La Sous Prefecture;59190;Hazebrouck;50.7232207;2.5301757
+0251028G;Lycée Gen.et Technol.prive Saint Jean;1 Rue De L'esperance;25000;Besancon;47.2589558;6.0204846
+0133822J;Lycée Gen.et Technol.prive Saint Jean De Garguier;Chemin Du Puits;13420;Gemenos;43.2845374;5.6190148
+0141173Z;Lycée Gen.et Technol.prive Saint Jean Eudes;2 Avenue De La Gare;14503;Vire;48.8457455;-0.8844612
+0010075B;Lycée Gen.et Technol.prive Saint Joseph;101 Rue Henri Grobon;01705;Miribel;45.825157;4.9512247
+0021871Y;Lycée Gen.et Technol.prive Saint Joseph;2 Chaussee De Fontaine;02140;Fontaine Les Vervins;49.8365787;3.906547
+0030445T;Lycée Gen.et Technol.prive Saint Joseph;11 Rue Du Fg De La Gironde;03100;Montlucon;46.3411588;2.6080734
+0371306U;Lycée Gen.et Technol.prive Saint Joseph;1 Place Saint Mexme;37502;Chinon;47.167917;0.24509
+0740100G;Lycée Gen.et Technol.prive Saint Joseph;5 Avenue Du Leman;74200;Thonon Les Bains;46.3774405;6.4835126
+0830098F;Lycée Gen.et Technol.prive Saint Joseph;2229 Route De Faveyrolles;83190;Ollioules;43.139436;5.8845344
+0572340J;Lycée Gen.et Technol.prive Saint Joseph-la Providence;2 Avenue Du General Passaga;57600;Forbach;49.1853591;6.9006303
+0592926K;Lycée Gen.et Technol.prive Saint Jude;18 Rue Lamartine;59280;Armentieres;50.6867725;2.8756446
+0430053Z;Lycée Gen.et Technol.prive Saint Julien;7 Place Du Valla;43101;Brioude;45.2911231;3.3878825
+0170098P;Lycée Gen.et Technol.prive Saint Louis;73 Avenue Andre Malraux;17250;Pont L Abbe D Arnoult;45.829429;-0.8705604
+0550047C;Lycée Gen.et Technol.prive Saint Louis;28 Rue Voltaire;55001;Bar Le Duc;48.7719202;5.1608526
+0160062F;Lycée Gen.et Technol.prive Saint Paul;101 Rue De Beaulieu;16007;Angouleme;45.65054;0.1489427
+0251021Z;Lycée Gen.et Technol.prive Saint Paul;8 Boulevard Diderot;25000;Besancon;47.2453023;6.0351931
+0622114L;Lycée Gen.et Technol.prive Saint Paul;38 Route De La Bassee;62301;Lens;50.4365139;2.8216873
+0420973E;Lycée Gen.et Technol.prive Saint Paul Forez;13 Rue Du College;42603;Montbrison;45.6084481;4.0659129
+0010070W;Lycée Gen.et Technol.prive Saint Pierre;7 Rue Villeneuve;01001;Bourg En Bresse;46.1996249;5.2230636
+0030072M;Lycée Gen.et Technol.prive Saint Pierre;26 Allee Pierre Berthomier;03304;Cusset;46.1342557;3.4580004
+0622110G;Lycée Gen.et Technol.prive Saint Pierre;72 Rue Arago;62100;Calais;50.9413829;1.8641701
+0801206B;Lycée Gen.et Technol.prive Saint Pierre;24 Place Clemenceau;80103;Abbeville;50.1087199;1.8347016
+0021874B;Lycée Gen.et Technol.prive Saint Remy;8 Rue St Jean;02204;Soissons;49.3763018;3.3235831
+0801874C;Lycée Gen.et Technol.prive Saint Riquier;50 Chaussee Jules Ferry;80094;Amiens;49.8829134;2.3258793
+0690543R;Lycée Gen.et Technol.prive Saint Thomas D'aquin Veritas;56 Rue Du Perron;69600;Oullins;45.711866;4.8086923
+0601149Y;Lycée Gen.et Technol.prive Saint Vincent;30 Rue De Meaux;60304;Senlis;49.203401;2.5889031
+0271053Y;Lycée Gen.et Technol.prive Saint-adjutor;54 Rue De Marzelles;27200;Vernon;49.0859189;1.4575149
+0680149T;Lycée Gen.et Technol.prive Saint-andre;19 Rue Rapp;68025;Colmar;48.0805738;7.3597557
+0271282X;Lycée Gen.et Technol.prive Saint-anselme;11/13 R Leprevost De Beaumont;27300;Bernay;49.0942864;0.6045856
+0420974F;Lycée Gen.et Technol.prive Sainte Anne;4 Rue Saint Alban;42300;Roanne;46.0363817;4.0633804
+0550049E;Lycée Gen.et Technol.prive Sainte Anne;14 Rue Mautrote;55104;Verdun;49.1604635;5.3812032
+0801209E;Lycée Gen.et Technol.prive Sainte Famille;13 Rue De Castille;80017;Amiens;49.8882835;2.3096003
+0550048D;Lycée Gen.et Technol.prive Sainte Jeanne D'arc;23 Rue Poincare;55200;Commercy;48.7606796;5.5902284
+0592928M;Lycée Gen.et Technol.prive Sainte Jeanne D'arc;157 Rue De L'hotel De Ville;59620;Aulnoye Aymeries;50.2001783;3.8323903
+0060670W;Lycée Gen.et Technol.prive Sainte Marie;4 Avenue Windsor;06400;Cannes;43.5514185;7.0307566
+0390070P;Lycée Gen.et Technol.prive Sainte Marie Fenelon;84 Rue St Desire;39000;Lons Le Saunier;46.6706079;5.5489595
+0030073N;Lycée Gen.et Technol.prive Sainte Procule;22 Rue Des Augustins;03800;Gannat;46.0996289;3.1938761
+0631070W;Lycée Gen.et Technol.prive Sainte Thecle;7 Rue Amelie Murat;63402;Chamalieres;45.7771931;3.0670184
+0592929N;Lycée Gen.et Technol.prive Sainte Therese;4 Place Guillemin;59362;Avesnes Sur Helpe;50.1225956;3.9322081
+0753844W;Lycée Gen.et Technol.prive Sainte-genevieve;64 Rue D'assas;75006;Paris 06;48.8458567;2.331337
+0753852E;Lycée Gen.et Technol.prive Sainte-jeanne Elisabeth;8 Rue Maurice De La Sizeranne;75007;Paris 07;48.8473086;2.3147319
+0900030U;Lycée Gen.et Technol.prive Sainte-marie;40 Fbg Des Ancetres;90006;Belfort;47.6405457;6.8573468
+0271045P;Lycée Gen.et Technol.prive Saint-francois De Sales;8 Rue Portevin;27025;Evreux;49.0165487;1.1504096
+0761710W;Lycée Gen.et Technol.prive Saint-joseph;207 Rue Felix Faure;76072;Le Havre;49.5005512;0.1056686
+0681793E;Lycée Gen.et Technol.prive Saint-joseph De Cluny;53 Avenue Roger Salengro;68100;Mulhouse;47.7509394;7.347119
+0271049U;Lycée Gen.et Technol.prive Saint-ouen;30 Rue Sadi Carnot;27500;Pont Audemer;49.3571648;0.5145026
+0060677D;Lycée Gen.et Technol.prive Sasserno;1 Place Sasserno;06000;Nice;43.7026106;7.2711176
+9720829U;Lycée Gen.et Technol.prive Seminaire College Ste Marie;27 Rue Martin Luther King;97247;Fort De France;14.604913;-61.075087
+0911935J;Lycée Gen.et Technol.prive Senart-enseignement;9 Rue De L Industrie;91210;Draveil;48.6837846;2.4333977
+0421034W;Lycée Gen.et Technol.prive Sevigne;29 Rue Michelet;42000;St Etienne;45.434245;4.3898936
+0631033F;Lycée Gen.et Technol.prive Sevigne Saint Louis;2 Avenue De La Liberation;63500;Issoire;45.5411893;3.2487329
+0920907L;Lycée Gen.et Technol.prive Sophie Barat;50 Rue Des Grillons;92290;Chatenay Malabry;48.7579109;2.2704913
+0470060K;Lycée Gen.et Technol.prive St Caprais;8 Rue Raspail;47000;Agen;44.2073871;0.6188656
+0640131V;Lycée Gen.et Technol.prive St Dominique;30 Av Fouchet;64000;Pau;43.3139559;-0.3757428
+0920921B;Lycée Gen.et Technol.prive St Dominique;23 Quater Bd D Argenson;92200;Neuilly Sur Seine;48.8881037;2.2648464
+0131391S;Lycée Gen.et Technol.prive St Eloi;9 Avenue Jules Isaac;13626;Aix En Provence;43.5348838;5.4513323
+0783286Y;Lycée Gen.et Technol.prive St Erembert;5 Rue Salomon Reinach;78100;St Germain En Laye;48.8958802;2.0996683
+0781899R;Lycée Gen.et Technol.prive St Francois D Assise;6 Place Paul Claudel;78180;Montigny Le Bretonneux;48.7790268;2.0410595
+0920894X;Lycée Gen.et Technol.prive St Gabriel;21 Rue De La Lisette;92220;Bagneux;48.7925201;2.3011991
+0401016M;Lycée Gen.et Technol.prive St Jacques De Compostelle;32 Rue Lahargou;40100;Dax;43.6951039;-1.057711
+0783350T;Lycée Gen.et Technol.prive St Jean Hulst;26 R Mal De Lattre De Tassigny;78000;Versailles;48.8179183;2.1368041
+0021873A;Lycée Gen.et Technol.prive St Jean La Croix;25 Rue Antoine Lecuyer;02105;St Quentin;49.85011;3.2855838
+0240079F;Lycée Gen.et Technol.prive St Joseph;23 Avenue Georges Pompidou;24000;Perigueux;45.1889317;0.7230677
+0440149Y;Lycée Gen.et Technol.prive St Joseph;66 Rue Du College;44153;Ancenis;47.3665584;-1.1794334
+0440151A;Lycée Gen.et Technol.prive St Joseph;40 Rue Michel Grimault;44141;Chateaubriant;47.7156138;-1.3765267
+0440201E;Lycée Gen.et Technol.prive St Joseph;14 Rue Des Capucins;44270;Machecoul;46.9932154;-1.8232442
+0541308D;Lycée Gen.et Technol.prive St Joseph;413 Avenue De Boufflers;54524;Laxou;48.6934935;6.1381862
+0640137B;Lycée Gen.et Technol.prive St Joseph;650 Rue Hiribehere;64480;Ustaritz;43.4045104;-1.462666
+0840072X;Lycée Gen.et Technol.prive St Joseph;62 Rue Des Lices;84000;Avignon;43.9455461;4.8107194
+9720063L;Lycée Gen.et Technol.prive St Joseph De Cluny;22 Route De Cluny;97200;Fort De France;14.627187;-61.0738508
+0131339K;Lycée Gen.et Technol.prive St Joseph De La Madeleine;172 B Bd De La Liberation;13248;Marseille 04;43.3020176;5.3944529
+0331502D;Lycée Gen.et Technol.prive St Joseph De Tivoli;40 Av D Eysines;33073;Bordeaux;44.8538365;-0.5966724
+0601831P;Lycée Gen.et Technol.prive St Joseph Du Moncel;8 Place De L Eglise;60722;Pont Ste Maxence;49.3158812;2.6215379
+0131331B;Lycée Gen.et Technol.prive St Joseph Les Maristes;24 Rue Sainte Victoire;13006;Marseille 06;43.2862953;5.382018
+0771237B;Lycée Gen.et Technol.prive St Laurent-la Paix Notre Dame;43 Rue Alfred Brebion;77400;Lagny Sur Marne;48.8734226;2.7086692
+0490838V;Lycée Gen.et Technol.prive St Louis;47 Rue D Alsace;49400;Saumur;47.2588834;-0.0854513
+0133314G;Lycée Gen.et Technol.prive St Louis - Ste Marie;Allee Saint Louis - Rn 568;13180;Gignac La Nerthe;43.390916;5.233463
+0430059F;Lycée Gen.et Technol.prive St Louis Notre Dame De France;5 Rue Latour Maubourg;43010;Le Puy En Velay;45.042305;3.8789494
+0910826D;Lycée Gen.et Technol.prive St Louis St Clement;1 Rue Margot;91170;Viry Chatillon;48.6718555;2.372288
+0530046M;Lycée Gen.et Technol.prive St Michel;5 Rue Henri Dunant;53204;Chateau Gontier;47.8312371;-0.7100198
+0922353H;Lycée Gen.et Technol.prive St Nicolas;19 Rue Victor Hugo;92130;Issy Les Moulineaux;48.8264404;2.2758007
+0280667Y;Lycée Gen.et Technol.prive St Pierre St Paul;16 Boulevard Dubois;28109;Dreux;48.7375877;1.3745466
+0640134Y;Lycée Gen.et Technol.prive St Thomas D Aquin;Rue Biscarbidea;64500;St Jean De Luz;43.388051;-1.663055
+0920908M;Lycée Gen.et Technol.prive St Thomas De Villeneuve;1646 Avenue Roger Salengro;92370;Chaville;48.8075482;2.1862517
+0131403E;Lycée Gen.et Technol.prive St Vincent De Paul;30 Rue Stanislas Torrents;13006;Marseille 06;43.287572;5.3782713
+0060673Z;Lycée Gen.et Technol.prive Stanislas;1 Place Stanislas;06403;Cannes;43.5468601;6.9368506
+0490819Z;Lycée Gen.et Technol.prive Ste Agnes;7 Rue Volney;49000;Angers;47.464216;-0.5472721
+0920889S;Lycée Gen.et Technol.prive Ste Genevieve;19 Rue De La Station;92600;Asnieres Sur Seine;48.9068047;2.2853198
+0132810J;Lycée Gen.et Technol.prive Ste Marie;13 Rue Jeu De Ballon;13400;Aubagne;43.2916499;5.5711486
+0920875B;Lycée Gen.et Technol.prive Ste Marie;24 Boulevard Victor Hugo;92200;Neuilly Sur Seine;48.8850578;2.2811906
+0331501C;Lycée Gen.et Technol.prive Ste Marie De La Bastide;45 Rue De Dijon;33100;Bordeaux;44.8425946;-0.5526472
+0920904H;Lycée Gen.et Technol.prive Ste Marie La Croix;2 Rue De L Abbaye;92160;Antony;48.7539117;2.2999629
+0180561M;Lycée Gen.et Technol.prive Ste Marie St Dominique;38 Rue Jean Baffier;18020;Bourges;47.077094;2.4032838
+0240076C;Lycée Gen.et Technol.prive Ste Marthe - St Front;74 Av Pasteur;24100;Bergerac;44.8601889;0.4951121
+0781664K;Lycée Gen.et Technol.prive Ste Therese;7 Rue Beziel;78120;Rambouillet;48.6454729;1.8336261
+0370731U;Lycée Gen.et Technol.prive Ste Ursule;26 Rue Emile Zola;37009;Tours;47.3938697;0.6906468
+0940878U;Lycée Gen.et Technol.prive Teilhard De Chardin;2 Place D'armes;94100;St Maur Des Fosses;48.8125041;2.4720666
+0420979L;Lycée Gen.et Technol.prive Tezenas Du Montcel;14 Place Girodet;42000;St Etienne;45.4480193;4.3809644
+0501299R;Lycée Gen.et Technol.prive Thomas Helye;37 Rue Emile Zola;50100;Cherbourg Octeville;49.6370758;-1.6291084
+0951048Y;Lycée Gen.et Technol.prive Torat Emet;14 Av Charles Peguy;95200;Sarcelles;48.9811624;2.3754349
+0860903P;Lycée Gen.et Technol.prive Union Chretienne;2 Plan Sainte Croix;86011;Poitiers;46.5792977;0.3487512
+0951221L;Lycée Gen.et Technol.prive Vauban;23 Place Du Petit Martroy;95300;Pontoise;49.0513181;2.0969748
+0671611M;Lycée Gen.prive Instit.la Doctrine Chretienne;14 Rue Brulee;67000;Strasbourg;48.584854;7.752502
+9830484J;Lycée Gen.tech.prof.agricole De Pouembout (agri);Route Munipale 2;98825;Pouembout;0.0;0.0
+0580584V;Lycée Gen.techn.agricole Les Cottereaux;66 Rue Jean Monnet;58200;Cosne Cours Sur Loire;47.3910652;2.9324174
+0190609J;Lycée Gen.technolo Agricole Ecole Forestiere;Meymac;19250;Meymac;45.5362265;2.14547
+0190087S;Lycée Gen.technolo. Agricole Henri Queuille;;19160;Neuvic;45.382449;2.272544
+0422132P;Lycée General;16 Rue Arquilliere;42130;Boen;45.745278;4.004247
+0694069Y;Lycée General;Les Grands Champs;69210;Sain Bel;45.822538;4.599975
+0922615T;Lycée General;13 17 Avenue D Alsace;92400;Courbevoie;48.8908791;2.2503094
+0590212K;Lycée General;80 Boulevard Gambetta;59208;Tourcoing;50.7133268;3.1588623
+0140004D;Lycée General Alain Chartier;Place De La Lombarderie;14402;Bayeux;49.278971;-0.712644
+0180005H;Lycée General Alain Fournier;50 Rue Stephane Mallarme;18016;Bourges;47.1000872;2.4108852
+0590063Y;Lycée General Albert Chatelet;357 Rue Marceline;59508;Douai;50.3738413;3.0855523
+0260022H;Lycée General Albert Triboulet;55 Avenue Gambetta;26102;Romans Sur Isere;45.0453925;5.0568298
+0140043W;Lycée General Andre Maurois;10 Boulevard Cornuche;14800;Deauville;49.3628042;0.0747539
+0290069T;Lycée General Auguste Brizeux;6 Rue Bourg Les Bourgs;29191;Quimper;47.9930436;-4.1116997
+0590182C;Lycée General Baudelaire;23 Avenue Le Notre;59100;Roubaix;50.6804598;3.1673908
+0720030S;Lycée General Bellevue;2 Rue Abbaye St Vincent;72001;Le Mans;48.0121822;0.2021824
+0810005R;Lycée General Bellevue;98 Rue Du Roc;81011;Albi;43.9221442;2.1574435
+0870045B;Lycée General Bernard Palissy;11 R Leon Jouhaux;87400;St Leonard De Noblat;45.8344936;1.4931518
+0240024W;Lycée General Bertran De Born;1 Rue Ch Mangold;24001;Perigueux;45.1802367;0.7199966
+0030044G;Lycée General Blaise De Vigenere;51 Avenue Pasteur;03500;St Pourcain Sur Sioule;46.3112153;3.2927033
+0630018C;Lycée General Blaise Pascal;36 Avenue Carnot;63037;Clermont Ferrand;45.7762478;3.0934525
+0750693W;Lycée General Buffon;16 Boulevard Pasteur;75015;Paris 15;48.8434549;2.3117609
+0101028N;Lycée General Camille Claudel;28 Rue Des Terrasses;10026;Troyes;48.2918918;4.0787778
+0860035W;Lycée General Camille Guerin;33 Rue De La Gibauderie;86022;Poitiers;46.5687695;0.3635822
+0760093N;Lycée General Camille Saint-saens;22 Rue Saint-lo;76005;Rouen;49.4422597;1.0937285
+0750694X;Lycée General Camille See;11 Rue Leon Lhermitte;75015;Paris 15;48.8430219;2.2972982
+0260035X;Lycée General Camille Vernet;160 Rue Faventines;26021;Valence;44.9243431;4.9036578
+0060011E;Lycée General Carnot;90 Boulevard Carnot;06408;Cannes;43.5636838;7.0164947
+0750704H;Lycée General Carnot;145 Boulevard Malesherbes;75017;Paris 17;48.8845308;2.3078305
+0380027Y;Lycée General Champollion;Cours La Fontaine;38026;Grenoble;45.1878714;5.7248743
+0080006N;Lycée General Chanzy;13 Rue Delvincourt;08000;Charleville Mezieres;49.7725131;4.730864
+0480007X;Lycée General Chaptal;Avenue Paulin Daude;48001;Mende;44.520214;3.508123
+0570106F;Lycée General Charlemagne;17 Avenue Clemenceau;57100;Thionville;49.3603724;6.1679017
+0750652B;Lycée General Charlemagne;14 Rue Charlemagne;75004;Paris 04;48.8541627;2.3609691
+0142107P;Lycée General Charles De Gaulle;39 Rue D'hastings;14000;Caen;49.1844909;-0.3794536
+0211928G;Lycée General Charles De Gaulle;25 Av General Touzet Du Vigier;21000;Dijon;47.3495084;5.0418001
+0390012B;Lycée General Charles Nodier;6 Grande Rue;39107;Dole;47.0907375;5.4933764
+0693446W;Lycée General Cite Scolaire Internationale;2 Place De Montreal;69361;Lyon 07;0.0;0.0
+0420041S;Lycée General Claude Fauriel;28 Avenue De La Liberation;42007;St Etienne;45.4373521;4.3929933
+0880020U;Lycée General Claude Gellee;44 Rue Abel Ferry;88021;Epinal;48.1665516;6.4548634
+0740003B;Lycée General Claude Louis Berthollet;9 Boulevard Du Lycée;74008;Annecy;0.0;0.0
+0750683K;Lycée General Claude Monet;1 Rue Du Docteur Magnan;75013;Paris 13;48.8274487;2.3622395
+0511901P;Lycée General Colbert;56 Rue Du Dr Schweitzer;51100;Reims;49.2803364;4.0213284
+0750673Z;Lycée General Colbert;27 Rue De Chateau Landon;75010;Paris 10;48.88268;2.3634216
+0750667T;Lycée General Condorcet;8 Rue Du Havre;75009;Paris 09;48.8747904;2.3269255
+0190011J;Lycée General D Arsonval;Place Du 15 Aout 1944;19100;Brive La Gaillarde;45.1605553;1.530861
+0320025D;Lycée General D'artagnan;Avenue Des Pyrenees;32110;Nogaro;43.754576;-0.029898
+0490001K;Lycée General David D Angers;1 Rue Paul Langevin;49035;Angers;47.4653426;-0.5453201
+0290007A;Lycée General De Kerichen;Rue Prince De Joinville;29801;Brest;48.4043354;-4.4826021
+0290010D;Lycée General De L'harteloire;1 Rue Du Guesclin;29213;Brest;48.4284698;-4.5611577
+0290009C;Lycée General De L'iroise;7 Place De Strasbourg;29223;Brest;48.4018349;-4.4663852
+0332846P;Lycée General Des Graves;238 Cours Du Gl De Gaulle;33173;Gradignan;44.774267;-0.618945
+0590024F;Lycée General Des Nerviens;Place Charles De Gaulle;59570;Bavay;50.2991014;3.7947585
+0370035M;Lycée General Descartes;10 Rue Des Minimes;37010;Tours;47.3914217;0.6900424
+0090018W;Lycée General Du Couserans;Esplanade Mendes France;09201;St Girons;42.9780405;1.1514912
+0260008T;Lycée General Du Diois;Rue Maurice Faure;26150;Die;44.757877;5.367689
+0690026D;Lycée General Du Parc;1 Boulevard Anatole France;69458;Lyon 06;45.7714439;4.8568367
+0490040C;Lycée General Duplessis Mornay;1 Rue Duruy;49408;Saumur;47.255425;-0.077033
+0940117S;Lycée General Edouard Branly;8 Rue Bauyn De Perreuse;94130;Nogent Sur Marne;48.8349733;2.4828212
+0150646W;Lycée General Emile Duclaux;16 Avenue Henri Mondor;15005;Aurillac;44.9269037;2.439407
+0260034W;Lycée General Emile Loubet;2 Rue Du Lycée;26021;Valence;0.0;0.0
+0141112H;Lycée General Et Tech Agricole Le Robillard;Le Robillard;14170;St Pierre Sur Dives;48.98834;-0.005216
+0810068J;Lycée General Et Techno. Privé D'amboise;13 Boulevard Carnot;81000;Albi;43.9227213;2.1450617
+0741469V;Lycée General Et Techno. Privé Demotz De La Salle;2 Rue Du College;74150;Rumilly;45.867922;5.9444106
+0121438Y;Lycée General Et Techno. Privé Francois D'estaing;22 Boulevard Denys Puech;12056;Rodez;44.3491284;2.5776399
+0820044C;Lycée General Et Techno. Privé Institut Familial;1 Allee De Mortarieu;82005;Montauban;44.0151243;1.356479
+0120051R;Lycée General Et Techno. Privé Jeanne D'arc;3 Place Du Mandarous;12104;Millau;44.0996885;3.0784006
+0810079W;Lycée General Et Techno. Privé Jeanne D'arc;23 Rue De La Vanne;81207;Mazamet;43.4847739;2.3737673
+0320051G;Lycée General Et Techno. Privé Oratoire Sainte-marie;50 Bis Rue Victor Hugo;32002;Auch;43.6505336;0.5776322
+0312355S;Lycée General Et Techno. Privé Ort Maurice Grynfogel;14 Rue Etienne Collongues;31770;Colomiers;43.601951;1.333155
+0311147D;Lycée General Et Techno. Privé Sainte-marie De Nevers;10 Rue Du Perigord;31070;Toulouse;43.6072684;1.4432532
+0311146C;Lycée General Et Techno. Privé Sainte-marie Des Champs;169 Avenue Jean Rieux;31506;Toulouse;43.5912753;1.4691653
+0460038S;Lycée General Et Techno. Privé Saint-etienne;49 Rue Des Soubirous;46000;Cahors;44.4504769;1.4412933
+0120059Z;Lycée General Et Techno. Privé Saint-gabriel;23 Rue Lamartine;12402;St Affrique;43.956919;2.890886
+0120061B;Lycée General Et Techno. Privé Saint-joseph;Avenue Etienne Soulie;12200;Villefranche De Rouergue;44.357108;2.0390698
+0120104Y;Lycée General Et Techno. Privé Saint-joseph;1 Rue Sarrus;12000;Rodez;44.3451427;2.5715885
+0311145B;Lycée General Et Techno. Privé Saint-joseph;85 Rue De Limayrac;31079;Toulouse;43.593452;1.4734636
+0311133N;Lycée General Et Technol Privé Le Caousou;42 Avenue Camille Pujol;31079;Toulouse;43.6010623;1.4610488
+0810103X;Lycée General Et Technol Privé Notre-dame;Avenue D'hauterive;81101;Castres;43.599188;2.244541
+0312744P;Lycée General Et Technologique;Chemin De La Cepette;31860;Pins Justaret;43.4754398;1.3911496
+9750001C;Lycée General Et Technologique;Rm Bonin;97500;St Pierre;0.0;0.0
+0090013R;Lycée General Et Technologique;Route De Limoux;09500;Mirepoix;43.0849235;1.8953166
+0120006S;Lycée General Et Technologique;Avenue Leo Lagrange;12300;Decazeville;44.5676272;2.2523499
+0670007U;Lycée General Et Technologique;4 Place Du Chateau;67330;Bouxwiller;48.8259746;7.4840954
+0910625K;Lycée General Et Technologique;2 Place De L Europe;91230;Montgeron;48.7050057;2.4537264
+9720694X;Lycée General Et Technologique Acajou 1;Quartier Acajou;97285;Le Lamentin;0.0;0.0
+0610001V;Lycée General Et Technologique Alain;27 Boulevard Mezeray;61014;Alencon;48.438637;0.0813898
+0782568T;Lycée General Et Technologique Alain;25 Route De La Cascade;78110;Le Vesinet;48.8913223;2.1225849
+0260015A;Lycée General Et Technologique Alain Borne;10 Place Du Theatre;26216;Montelimar;44.5872248;4.7021409
+0580753D;Lycée General Et Technologique Alain Colas;Rue D'estutt De Tracy;58000;Nevers;46.98518;3.1285249
+0560051B;Lycée General Et Technologique Alain Rene Lesage;20 Rue Winston Churchill;56017;Vannes;47.6477281;-2.7703646
+0060031B;Lycée General Et Technologique Albert Calmette;5 Avenue Marechal Foch;06050;Nice;43.7040719;7.2711532
+0300023M;Lycée General Et Technologique Albert Camus;51 Avenue Georges Pompidou;30911;Nimes;43.8363398;4.3463446
+0420013L;Lycée General Et Technologique Albert Camus;32 Bis Rue De La Loire;42704;Firminy;45.3883922;4.2830196
+0440288Z;Lycée General Et Technologique Albert Camus;11 Rue Etienne Coutan;44100;Nantes;47.2051598;-1.6079868
+0692517L;Lycée General Et Technologique Albert Camus;Avenue Des Nations;69140;Rillieux La Pape;45.8212079;4.9071706
+0920132U;Lycée General Et Technologique Albert Camus;131 Rue Pierre Joigneaux;92270;Bois Colombes;48.9106181;2.2595527
+0620166U;Lycée General Et Technologique Albert Chatelet;Rue Cassin;62165;St Pol Sur Ternoise;50.38609;2.3445988
+0911346U;Lycée General Et Technologique Albert Einstein;Av De La Liberte;91706;Ste Genevieve Des Bois;48.6400356;2.3486662
+0680031P;Lycée General Et Technologique Albert Schweitzer;8 Bld De La Marne;68068;Mulhouse;47.7459702;7.3241297
+0930830X;Lycée General Et Technologique Albert Schweitzer;11 Allee Valere Lefebvre;93340;Le Raincy;48.898921;2.5108023
+0420033H;Lycée General Et Technologique Albert Thomas;20 Rue Albert Thomas;42328;Roanne;46.0440474;4.0740983
+0442309W;Lycée General Et Technologique Alcide D'orbigny;Place De L Edit De Nantes;44830;Bouaye;47.2154657;-1.5678387
+0920801W;Lycée General Et Technologique Alexandre Dumas;112 Bd De La Republique;92210;St Cloud;48.8493852;2.2138325
+0620161N;Lycée General Et Technologique Alexandre Ribot;42 Rue Gambetta;62505;St Omer;50.7478277;2.2586747
+0061760F;Lycée General Et Technologique Alexis De Tocqueville;22 Che De L Orme;06131;Grasse;43.6545971;6.9414071
+0500017X;Lycée General Et Technologique Alexis De Tocqueville;34 Avenue Henri Poincare;50100;Cherbourg Octeville;49.6304753;-1.6077873
+0370016S;Lycée General Et Technologique Alfred De Vigny;Rue Paul Delvaux;37600;Loches;47.1286731;0.976984
+0590060V;Lycée General Et Technologique Alfred Kastler;123 Rue Paul Elie Casanova;59723;Denain;50.3293842;3.4072591
+0680015X;Lycée General Et Technologique Alfred Kastler;5 Rue Du Luspel;68502;Guebwiller;47.9070545;7.2048104
+0850027T;Lycée General Et Technologique Alfred Kastler;Boulevard Guitton;85020;La Roche Sur Yon;46.659238;-1.4360472
+0951399E;Lycée General Et Technologique Alfred Kastler;26 Avenue De La Palette;95011;Cergy;49.0319888;2.0841331
+0540030P;Lycée General Et Technologique Alfred Mezieres;3 Avenue Andre Malraux;54401;Longwy;49.5216573;5.7601943
+0860038Z;Lycée General Et Technologique Alienor D Aquitaine;41 Rue Pierre De Coubertin;86034;Poitiers;46.5883777;0.3652348
+0130164H;Lycée General Et Technologique Alphonse Daudet;1 Boulevard Jules Ferry;13150;Tarascon;43.8021567;4.6635779
+0300021K;Lycée General Et Technologique Alphonse Daudet;3 Boulevard Victor Hugo;30039;Nimes;43.8358168;4.3578013
+0630077S;Lycée General Et Technologique Ambroise Brugiere;44 Rue Des Planchettes;63039;Clermont Ferrand;45.7997921;3.1075266
+0530010Y;Lycée General Et Technologique Ambroise Pare;17 Rue Du Lycée;53013;Laval;0.0;0.0
+9740019C;Lycée General Et Technologique Ambroise Vollard;3 Avenue De Soweto;97448;St Pierre;-21.3417856;55.4892561
+0060020P;Lycée General Et Technologique Amiral De Grasse;20 Avenue Sainte Lorette;06130;Grasse;43.6523288;6.9209759
+9740471U;Lycée General Et Technologique Amiral Pierre Bouvet;76 Rue Joseph Hubert;97470;St Benoit;0.0;0.0
+0290008B;Lycée General Et Technologique Amiral Ronarc'h;3 Rue Mozart;29231;Brest;48.3959427;-4.5224841
+0690023A;Lycée General Et Technologique Ampere;31 Rue De La Bourse;69289;Lyon 02;45.7650005;4.8368975
+0330010G;Lycée General Et Technologique Anatole De Monzie;12 Cours Gambetta;33430;Bazas;44.4332382;-0.213939
+0620120U;Lycée General Et Technologique Anatole France;64 Boulevard De Paris;62190;Lillers;50.5559013;2.4778684
+0381603L;Lycée General Et Technologique Andre Argouges;61 Rue Leon Jouhaux;38029;Grenoble;45.1753906;5.7445124
+0931585T;Lycée General Et Technologique Andre Boulloche;18 Boulevard Gutenberg;93190;Livry Gargan;48.9125431;2.5194358
+0271580W;Lycée General Et Technologique Andre Malraux;59 Avenue Francois Mitterand;27607;Gaillon;49.160734;1.33341
+0620042J;Lycée General Et Technologique Andre Malraux;314 Rue Jules Massenet;62408;Bethune;50.5200303;2.6516077
+0721548S;Lycée General Et Technologique Andre Malraux;3 Rue De Beau Soleil;72700;Allonnes;47.9559769;0.1669132
+0670005S;Lycée General Et Technologique Andre Maurois;1 Rue Du Lycée;67242;Bischwiller;0.0;0.0
+0760029U;Lycée General Et Technologique Andre Maurois;1 Rue De Lorraine;76503;Elbeuf;49.2906662;1.0159498
+0860009T;Lycée General Et Technologique Andre Theuriet;;86400;Civray;46.148901;0.295258
+0352686E;Lycée General Et Technologique Anita Conti;Esplanade Du Lycée;35174;Bruz;0.0;0.0
+0690031J;Lycée General Et Technologique Antoine De Saint Exupery;82 Rue Henon;69316;Lyon 04;45.7797287;4.8219853
+0570023R;Lycée General Et Technologique Antoine De Saint-exupery;11 Avenue Saint Exupery;57290;Fameck;49.2470951;6.1087402
+9830507J;Lycée General Et Technologique Antoine Kela;Rte Provinciale 3;98822;Poindimie;0.0;0.0
+9740787M;Lycée General Et Technologique Antoine Roussin;25 Rue Leconte De Lisle;97450;St Louis;0.0;0.0
+0132733A;Lycée General Et Technologique Antonin Artaud;25 Ch N D De La Consolation;13013;Marseille 13;43.3402529;5.4230519
+0750680G;Lycée General Et Technologique Arago;4 Place De La Nation;75012;Paris 12;48.847931;2.3943586
+0011119L;Lycée General Et Technologique Arbez Carme;1 Rue Pierre Et Marie Curie;01100;Bellignat;46.2508923;5.6331189
+0840026X;Lycée General Et Technologique Arc (de L');346 Av Des Etudiants;84106;Orange;44.1415649;4.8019433
+0141274J;Lycée General Et Technologique Arcisse De Caumont;3 Rue Baron Gerard;14402;Bayeux;49.2843359;-0.7097585
+0050007F;Lycée General Et Technologique Aristide Briand;20 Avenue Commandant Dumont;05007;Gap;44.5662436;6.0836082
+0270016W;Lycée General Et Technologique Aristide Briand;2 Rue Pierre Semard;27031;Evreux;49.0168441;1.1602102
+0440069L;Lycée General Et Technologique Aristide Briand;10 Bd Pierre De Coubertin;44606;St Nazaire;47.2693774;-2.2334295
+0382780R;Lycée General Et Technologique Aristides Berges;10 Avenue Aime Bouchayer;38171;Seyssinet Pariset;45.1846564;5.6945758
+0250058C;Lycée General Et Technologique Armand Peugeot;30 Rue Des Carrieres;25702;Valentigney;47.4583749;6.8706804
+0240032E;Lycée General Et Technologique Arnaud Daniel;Rue Couleau;24600;Riberac;45.245019;0.336231
+0132495S;Lycée General Et Technologique Arthur Rimbaud;Quartier Des Salles;13808;Istres;43.513006;4.987968
+0595885B;Lycée General Et Technologique Arthur Rimbaud;1075 Rue Paul Foucaut;59450;Sin Le Noble;50.3496401;3.1081053
+0540044E;Lycée General Et Technologique Arthur Varoquaux;Rue Jean Moulin;54510;Tomblaine;48.6955865;6.2138163
+0590073J;Lycée General Et Technologique Auguste Angellier;Bd Republique -fr. Mitterrand;59942;Dunkerque;51.0465854;2.4108807
+0620109G;Lycée General Et Technologique Auguste Behal;6 Rue Paul Eluard;62300;Lens;50.4456289;2.8230467
+0610014J;Lycée General Et Technologique Auguste Chevalier;7 Place Du Champ De Foire;61700;Domfront;48.5921466;-0.6422106
+0492061Z;Lycée General Et Technologique Auguste Et Jean Renoir;15 Impasse Ampere;49035;Angers;47.4751203;-0.5682534
+0690035N;Lycée General Et Technologique Auguste Et Louis Lumiere;50 Boulevard Des Etats Unis;69372;Lyon 08;45.7361542;4.8620526
+0220018A;Lycée General Et Technologique Auguste Pavie;13 Rue Anatole Le Braz;22205;Guingamp;48.5585676;-3.146937
+0060009C;Lycée General Et Technologique Auguste Renoir;18 Avenue Marcel Pagnol;06802;Cagnes Sur Mer;43.718958;7.1656502
+0870017W;Lycée General Et Technologique Auguste Renoir;119 Rue Sainte-claire;87036;Limoges;45.8235722;1.2409963
+0920131T;Lycée General Et Technologique Auguste Renoir;137 Rue Du Menil;92600;Asnieres Sur Seine;48.9221719;2.2821664
+0140014P;Lycée General Et Technologique Augustin Fresnel;77 Rue Eustache Restout;14020;Caen;49.1638565;-0.3552215
+0270003G;Lycée General Et Technologique Augustin Fresnel;14 Rue Kleber Mercier;27301;Bernay;49.0846704;0.5939843
+9710003B;Lycée General Et Technologique Baimbridge;Boulevard Des Heros;97139;Les Abymes;16.2498118;-61.5230947
+0370036N;Lycée General Et Technologique Balzac;36 Rue D Entraigues;37013;Tours;47.3878353;0.6872794
+0261099D;Lycée General Et Technologique Barthelemy De Laffemas;Rue Barthelemy De Laffemas;26901;Valence;44.917059;4.915496
+0680007N;Lycée General Et Technologique Bartholdi;9 Rue Du Lycée;68025;Colmar;0.0;0.0
+0350022J;Lycée General Et Technologique Beaumont;10 Rue Du Lycée;35605;Redon;0.0;0.0
+0590093F;Lycée General Et Technologique Beaupre;Avenue De Beaupre;59481;Haubourdin;50.6018;2.9906864
+0420018S;Lycée General Et Technologique Beauregard;4 Avenue Paul Cezanne;42605;Montbrison;45.6137248;4.0570332
+0830050D;Lycée General Et Technologique Beaussier;Qua Beaussier Place Galilee;83512;La Seyne Sur Mer;43.1000479;5.8780319
+9741046U;Lycée General Et Technologique Bellepierre;Avenue Gaston Monnerville;97475;St Denis;48.830502;2.597217
+0170058W;Lycée General Et Technologique Bellevue;1 Chemin Des Cotieres;17100;Saintes;45.7370327;-0.6435025
+9720003W;Lycée General Et Technologique Bellevue;Rue Marie Therese Gertrude;97262;Fort De France;14.6040646;-61.084565
+0561534N;Lycée General Et Technologique Benjamin Franklin;1 Rue De La Foret;56408;Auray;47.6712271;-2.9796708
+0170060Y;Lycée General Et Technologique Bernard Palissy;1 Rue De Gascogne;17107;Saintes;45.7360174;-0.6152519
+0450029M;Lycée General Et Technologique Bernard Palissy;9 Rue Du 32e R I;45502;Gien;47.699878;2.6321739
+0470001W;Lycée General Et Technologique Bernard Palissy;164 Boulevard De La Liberte;47000;Agen;44.1992878;0.6215667
+0190038N;Lycée General Et Technologique Bernart De Ventadour;Boulevard De La Jaloustre;19200;Ussel;45.541611;2.315461
+0350053T;Lycée General Et Technologique Bertrand D'argentre;15 Rue Du College;35506;Vitre;48.1200837;-1.211593
+0490782J;Lycée General Et Technologique Blaise Pascal;2 Rue Du Lycée;49502;Segre;0.0;0.0
+0570030Y;Lycée General Et Technologique Blaise Pascal;5 Rue Paul Ney;57608;Forbach;49.1806835;6.8958864
+0622803K;Lycée General Et Technologique Blaise Pascal;Rue Roger Salengro;62967;Longuenesse;50.7396525;2.2532221
+0693518Z;Lycée General Et Technologique Blaise Pascal;2 Avenue Bergeron;69751;Charbonnieres Les Bains;45.7751178;4.7452169
+0910626L;Lycée General Et Technologique Blaise Pascal;18 A 20 Rue Alexandre Fleming;91406;Orsay;48.6959013;2.1868463
+0831243A;Lycée General Et Technologique Bonaparte;Avenue W. Churchill;83097;Toulon;43.1249494;5.9263291
+0810959C;Lycée General Et Technologique Borde Basse;Cite Scolaire;81108;Castres;43.606214;2.241295
+0320009L;Lycée General Et Technologique Bossuet;42 Rue Jules Ferry;32100;Condom;43.9617419;0.3733974
+0820021C;Lycée General Et Technologique Bourdelle;3 Boulevard Edouard Herriot;82003;Montauban;44.0128398;1.3704149
+0350028R;Lycée General Et Technologique Brequigny;7 Avenue Georges Graff;35205;Rennes;48.0836213;-1.6920985
+0060013G;Lycée General Et Technologique Bristol;10 Avenue St Nicolas;06405;Cannes;43.5554966;7.0188911
+0630052P;Lycée General Et Technologique C. Et P. Virlogeux;1 Rue Du General Chapsal;63201;Riom;45.8868933;3.1143602
+0410959V;Lycée General Et Technologique Camille Claudel;10 Rue Albert Camus;41018;Blois;47.5764443;1.3027224
+0442207K;Lycée General Et Technologique Camille Claudel;14 Boulevard Jules Verne;44130;Blain;47.4812962;-1.7638862
+0590083V;Lycée General Et Technologique Camille Claudel;Cite Scolaire Rue Paul Lafargue;59613;Fourmies;50.0115008;4.0428865
+0711137A;Lycée General Et Technologique Camille Claudel;Route De Roanne;71160;Digoin;46.4693907;4.002237
+0772243V;Lycée General Et Technologique Camille Claudel;Place Anyama;77347;Pontault Combault;48.801255;2.607598
+0911938M;Lycée General Et Technologique Camille Claudel;17 Rue Robespierre;91120;Palaiseau;48.7189784;2.2340194
+0383069E;Lycée General Et Technologique Camille Corot;454 Rue Paul Claudel;38510;Morestel;45.674976;5.474023
+0330023W;Lycée General Et Technologique Camille Jullian;29 Rue De La Croix Blanche;33074;Bordeaux;44.8446226;-0.5901585
+0951922Y;Lycée General Et Technologique Camille Saint-saens;18 Au 22 Rue Guynemer;95170;Deuil La Barre;48.9734465;2.3360722
+0680008P;Lycée General Et Technologique Camille See;42 Avenue De L'europe;68025;Colmar;48.0804554;7.325755
+0441993C;Lycée General Et Technologique Carcouet;115 Bd Du Massacre;44184;Nantes;47.2316043;-1.5925015
+0210015C;Lycée General Et Technologique Carnot;16 Boulevard Thiers;21000;Dijon;47.3236983;5.0492348
+0420034J;Lycée General Et Technologique Carnot;35 Avenue Carnot;42300;Roanne;46.0419603;4.0699904
+0600013N;Lycée General Et Technologique Cassini;11 Rue Henri Breuil;60607;Clermont;49.3845904;2.4076904
+0891200W;Lycée General Et Technologique Catherine Et Raymond Janot;1 Place Lech Walesa;89094;Sens;48.20065;3.28268
+0590121L;Lycée General Et Technologique Cesar Baggio;Boulevard D'alsace;59000;Lille;50.6177376;3.0676499
+0220058U;Lycée General Et Technologique Chaptal;6 Allee Chaptal;22015;St Brieuc;48.5125105;-2.7377027
+0750663N;Lycée General Et Technologique Chaptal;45 Boulevard Des Batignolles;75008;Paris 08;48.8817015;2.3196312
+0160004T;Lycée General Et Technologique Charles A Coulomb;Avenue Joachim Du Bellay;16016;Angouleme;45.6303964;0.1570913
+0561627P;Lycée General Et Technologique Charles De Gaulle;23 Avenue Paul Cezanne;56017;Vannes;47.6764559;-2.772802
+0601863Z;Lycée General Et Technologique Charles De Gaulle;Rue Jacques Daguerre;60321;Compiegne;49.387482;2.7840919
+0781898P;Lycée General Et Technologique Charles De Gaulle;10 Rue Gustave Eiffel;78306;Poissy;48.9483064;2.0613496
+0932031C;Lycée General Et Technologique Charles De Gaulle;102 Rue Lavoisier;93110;Rosny Sous Bois;48.8615917;2.4910337
+0400018C;Lycée General Et Technologique Charles Despiau;637 Route Du Houga;40010;Mont De Marsan;43.8821356;-0.4870724
+0590049H;Lycée General Et Technologique Charles Deulin;89 Rue De La Chaussiette;59163;Conde Sur L Escaut;50.4598956;3.5860573
+0500026G;Lycée General Et Technologique Charles Francois Lebrun;2 Place Georges Davy;50207;Coutances;49.047808;-1.4452609
+0300046M;Lycée General Et Technologique Charles Gide;Place Adolphe Bosc;30700;Uzes;44.016096;4.416498
+0771763Y;Lycée General Et Technologique Charles Le Chauve;4 Rue J. Bodin De Boismortier;77680;Roissy En Brie;48.8016537;2.6384353
+0451526P;Lycée General Et Technologique Charles Peguy;1 Cours Victor Hugo;45074;Orleans;47.8891246;1.897411
+0740017S;Lycée General Et Technologique Charles Poncet;1 Avenue Charles Poncet;74302;Cluses;46.0625378;6.579908
+0692800U;Lycée General Et Technologique Charlie Chaplin;13 Rue Francisco Ferrer;69152;Decines Charpieu;45.7737096;4.9761696
+0490003M;Lycée General Et Technologique Chevrollier;2 Rue Adrien Recouvreur;49035;Angers;47.4543325;-0.5571808
+0370037P;Lycée General Et Technologique Choiseul;78 Rue Des Douets;37095;Tours;47.435967;0.6806159
+0100022V;Lycée General Et Technologique Chrestien De Troyes;3 Rue De Quebec;10009;Troyes;48.2688104;4.0777951
+0790036K;Lycée General Et Technologique Cite Scolaire Jean Moulin;Rue Albert Buisson;79101;Thouars;46.9937436;-0.2131568
+0590149S;Lycée General Et Technologique Cite Scolaire Pierre Forest;Boulevard Charles De Gaulle;59605;Maubeuge;50.2784762;3.9816647
+0860005N;Lycée General Et Technologique Cite Technique Edouard Branly;2 Rue Edouard Branly;86106;Chatellerault;46.8021551;0.5431246
+0750698B;Lycée General Et Technologique Claude Bernard;1 Avenue Du Parc Des Princes;75016;Paris 16;48.842485;2.254794
+0410017W;Lycée General Et Technologique Claude De France;9 11 Avenue De Paris;41206;Romorantin Lanthenay;47.3693756;1.738879
+0420040R;Lycée General Et Technologique Claude Lebois;8 Boulevard Alamagny;42403;St Chamond;45.4685576;4.5111391
+0760174B;Lycée General Et Technologique Claude Monet;267 Rue Felix Faure;76085;Le Havre;49.5009436;0.110217
+0251711Z;Lycée General Et Technologique Claude Nicolas Ledoux;14 Rue Alain Savary;25006;Besancon;47.2507592;5.9961549
+0820883P;Lycée General Et Technologique Claude Nougaro;;82300;Monteils;44.1741667;1.5661111
+0440021J;Lycée General Et Technologique Clemenceau;1 Rue Georges Clemenceau;44042;Nantes;47.2196647;-1.5456315
+0590214M;Lycée General Et Technologique Colbert;2 Parvis Jean Baptiste Colbert;59208;Tourcoing;50.724993;3.16207
+0690042W;Lycée General Et Technologique Colbert;20 Rue Louis Jouvet;69372;Lyon 08;45.7460272;4.8608427
+0720048L;Lycée General Et Technologique Colbert De Torcy;Rue St Denis;72305;Sable Sur Sarthe;47.838785;-0.342691
+0601865B;Lycée General Et Technologique Condorcet;Chemin Du Tour De Ville;60111;Meru;49.232327;2.140134
+0620108F;Lycée General Et Technologique Condorcet;25 Rue Etienne Dolet;62303;Lens;50.4322132;2.836371
+0693478F;Lycée General Et Technologique Condorcet;29 Rue Edmond Rostand;69802;St Priest;45.6943551;4.9411916
+0900002N;Lycée General Et Technologique Condorcet;13 Avenue Roosevelt;90016;Belfort;47.6333787;6.8506415
+0930122C;Lycée General Et Technologique Condorcet;31 Rue Desire Chevalier;93105;Montreuil;48.857903;2.446193
+0170042D;Lycée General Et Technologique Cordouan;28 Rue Henri Dunant;17200;Royan;45.6214498;-1.0029599
+0940121W;Lycée General Et Technologique D'arsonval;65 Rue Du Pont De Creteil;94107;St Maur Des Fosses;48.8032591;2.4718505
+0620143U;Lycée General Et Technologique D'artois;Rue Jules Mousseron;62290;Noeux Les Mines;50.4729178;2.6598973
+0310032S;Lycée General Et Technologique De Bagatelle;114 Avenue Francois Mitterrand;31806;St Gaudens;43.1122133;0.7456576
+0290098Z;Lycée General Et Technologique De Cornouaille;8 Avenue Des Oiseaux;29191;Quimper;47.9922221;-4.1232942
+0290076A;Lycée General Et Technologique De Kerneuzec;Kerneuzec;29391;Quimperle;47.88185;-3.552623
+0801900F;Lycée General Et Technologique De L Authie;20 Rue De Routequeue;80600;Doullens;50.149672;2.3555172
+0573281G;Lycée General Et Technologique De La Communication;3 Boulevard Arago;57070;Metz;49.1006107;6.2227012
+0762953X;Lycée General Et Technologique De La Cote D'albatre;24 Bis Rue Du Noroit;76460;St Valery En Caux;49.8670293;0.6986804
+0011326L;Lycée General Et Technologique De La Cotiere;270 Chemin Du Grand Casset;01120;La Boisse;45.8460955;5.0355377
+0011194T;Lycée General Et Technologique De La Plaine De L'ain;Rue Leon Blum;01500;Amberieu En Bugey;45.9629402;5.3435213
+0741532N;Lycée General Et Technologique De L'albanais;Rue Du Lycée;74152;Rumilly;0.0;0.0
+0595809U;Lycée General Et Technologique De L'escaut;1 Avenue De Saint Amand;59305;Valenciennes;50.3654932;3.5145784
+0590072H;Lycée General Et Technologique De L'europe;809 Rue Du Banc Vert;59640;Dunkerque;51.0203846;2.354849
+0781949V;Lycée General Et Technologique De Villaroy;2 Rue E Viollet Le Duc;78041;Guyancourt;48.7647055;2.0683153
+9741230U;Lycée General Et Technologique De Vincendo;10 Route De La Marine;97480;St Joseph;-21.3766787;55.6686797
+9870026P;Lycée General Et Technologique De Wallis;Mata Utu;98600;Uvea;-13.2825091;-176.1764475
+0110023R;Lycée General Et Technologique Denis Diderot;2 Rue Jean Moulin;11100;Narbonne;43.1899207;3.01902
+0310044E;Lycée General Et Technologique Deodat De Severac;26 Boulevard Deodat De Severac;31076;Toulouse;43.5869625;1.4260334
+0610018N;Lycée General Et Technologique Des Andaines;3 Place Du General De Gaulle;61600;La Ferte Mace;48.5869648;-0.3589215
+0312267W;Lycée General Et Technologique Des Arenes;Place Emile Male;31024;Toulouse;43.5912216;1.4186141
+9710882G;Lycée General Et Technologique Des Droits De L'homme;Pointe A Bacchus;97170;Petit Bourg;16.2002975;-61.5883791
+0590101P;Lycée General Et Technologique Des Flandres;2 Avenue Des Flandres;59522;Hazebrouck;50.7282931;2.5320417
+0781512V;Lycée General Et Technologique Descartes;6 Boulevard Descartes;78180;Montigny Le Bretonneux;48.766296;2.0387934
+0920130S;Lycée General Et Technologique Descartes;1 Avenue Lavoisier;92761;Antony;48.7484691;2.3136729
+0300026R;Lycée General Et Technologique Dhuoda;17 Rue Dhuoda;30913;Nimes;43.8268494;4.356595
+0520021R;Lycée General Et Technologique Diderot;21 Av Du Gal De Gaulle;52206;Langres;47.847086;5.3305042
+0620070P;Lycée General Et Technologique Diderot;Avenue Montaigne;62220;Carvin;50.4909624;2.9457485
+0670071N;Lycée General Et Technologique Docteur Koeberle;Bld Charlemagne;67604;Selestat;48.2633909;7.4554325
+0110022P;Lycée General Et Technologique Docteur Lacroix;Rue Gay Lussac;11100;Narbonne;43.1783759;3.0009809
+0050006E;Lycée General Et Technologique Dominique Villars;Place De Verdun;05010;Gap;-27.4251257;152.9935547
+0530011Z;Lycée General Et Technologique Douanier Rousseau;7 Rue Des Archives;53013;Laval;48.0717168;-0.7606616
+0260019E;Lycée General Et Technologique Dr. Gustave Jaume;Avenue Henri Becquerel;26702;Pierrelatte;44.380434;4.698
+0861228T;Lycée General Et Technologique Du Bois D'amour;9 Rue De La Garenne;86034;Poitiers;46.5506752;0.3075189
+0010010F;Lycée General Et Technologique Du Bugey;113 Rue Du 5eme Rtm;01306;Belley;45.7606158;5.68952
+0090015T;Lycée General Et Technologique Du Castella;Place Du Mercadal;09104;Pamiers;43.114347;1.6083829
+0831407D;Lycée General Et Technologique Du Coudon;Avenue Toulouse-lautrec;83957;La Garde;43.1328032;6.0180676
+9830557N;Lycée General Et Technologique Du Grand Noumea;Avenue Victor Hugo;98830;Dumbea;-22.2131693;166.467175
+0731392S;Lycée General Et Technologique Du Granier;185 Avenue Joseph Fontanet;73492;La Ravoire;45.5509773;5.9675821
+0292047T;Lycée General Et Technologique Du Leon;25 Bd De La Republique;29406;Landivisiau;48.5083658;-4.0595902
+0595616J;Lycée General Et Technologique Du Noordover;26 Avenue De Suwalki;59792;Grande Synthe;51.0111625;2.3014666
+0011276G;Lycée General Et Technologique Du Val De Saone;220 Chemin D'arras;01606;Trevoux;45.9455462;4.7619654
+0801864S;Lycée General Et Technologique Du Vimeu;3 Rue Denis Papin;80534;Friville Escarbotin;50.0832324;1.5461642
+0450062Y;Lycée General Et Technologique Duhamel Du Monceau;16 Avenue De France;45300;Pithiviers;48.1809395;2.2549277
+0830053G;Lycée General Et Technologique Dumont D Urville;212 Avenue Amiral Jaujard;83056;Toulon;43.1179515;5.9386824
+0590112B;Lycée General Et Technologique Dupleix;10 Boulevard Des Resistants;59550;Landrecies;50.1229644;3.6895214
+0560025Y;Lycée General Et Technologique Dupuy De Lome;Rue Le Coutaller;56100;Lorient;47.7478486;-3.3697288
+0450042B;Lycée General Et Technologique Durzy;23 Rue Leonard De Vinci;45702;Villemandeur;48.0003639;2.7159501
+0380029A;Lycée General Et Technologique Eaux Claires;7 Rue De Dunkerque;38030;Grenoble;45.1790269;5.7127172
+0010014K;Lycée General Et Technologique Edgar Quinet;5 Avenue Jean Marie Verne;01011;Bourg En Bresse;46.1953623;5.225314
+0520844K;Lycée General Et Technologique Edme Bouchardon;16 Rue Youri Gagarine;52903;Chaumont;48.104773;5.1478457
+0590065A;Lycée General Et Technologique Edmond Labbe;817 Rue Charles Bourseul;59508;Douai;50.3764002;3.0685187
+0911961M;Lycée General Et Technologique Edmond Michelet;2 Bd Abel Cornaton;91290;Arpajon;48.5870056;2.2468739
+0190032G;Lycée General Et Technologique Edmond Perrier;6 Avenue Henri De Bournazel;19012;Tulle;45.2680719;1.768389
+0311334G;Lycée General Et Technologique Edmond Rostand;2 Boulevard Charles De Gaulle;31110;Bagneres De Luchon;42.7929599;0.5975252
+0620052V;Lycée General Et Technologique Edouard Branly;2 Rue De La Porte Gayole;62321;Boulogne Sur Mer;50.7198852;1.6152414
+0101016A;Lycée General Et Technologique Edouard Herriot;Rue De La Maladiere;10302;Ste Savine;48.2935455;4.0271421
+0690027E;Lycée General Et Technologique Edouard Herriot;6 Place Edgar Quinet;69455;Lyon 06;45.7657284;4.8446272
+0180035R;Lycée General Et Technologique Edouard Vaillant;41 Bis Rue Charles Hurvoy;18100;Vierzon;47.2279543;2.0673471
+0332744D;Lycée General Et Technologique Elie Faure;Rue Jules Ferry;33305;Lormont;44.8701779;-0.519413
+0160010Z;Lycée General Et Technologique Elie Vinet;7 Avenue Pierre Mendes France;16300;Barbezieux St Hilaire;45.4741718;-0.1501908
+0750692V;Lycée General Et Technologique Emile Dubois;14 Rue Emile Dubois;75014;Paris 14;48.8317647;2.3369673
+0130001F;Lycée General Et Technologique Emile Zola;Av Arc De Meyran;13181;Aix En Provence;43.5118833;5.4584873
+0280015P;Lycée General Et Technologique Emile Zola;26 Rue De Civry;28200;Chateaudun;48.0722541;1.3347452
+0350024L;Lycée General Et Technologique Emile Zola;2 Avenue Janvier;35044;Rennes;48.1073742;-1.6736042
+0772294A;Lycée General Et Technologique Emily Bronte;10 Bis Mail Le Corbusier;77185;Lognes;48.8392621;2.6392575
+0380032D;Lycée General Et Technologique Emmanuel Mounier;6 Avenue Marcellin Berthelot;38029;Grenoble;45.1788938;5.7314999
+0492089E;Lycée General Et Technologique Emmanuel Mounier;1 Boulevard Robert Schuman;49017;Angers;47.4895805;-0.5269842
+0920135X;Lycée General Et Technologique Emmanuel Mounier;35 Rue Des Pres Hauts;92290;Chatenay Malabry;48.768766;2.2759475
+0130160D;Lycée General Et Technologique Emperi (l');21 Montee Du Puech;13657;Salon De Provence;43.6383013;5.0983648
+0450040Z;Lycée General Et Technologique En Foret;Route De Paucourt;45207;Montargis;48.0084469;2.750206
+0570081D;Lycée General Et Technologique Erckmann Chatrian;13 Rue De L'arsenal;57372;Phalsbourg;48.7667374;7.260468
+0540034U;Lycée General Et Technologique Ernest Bichat;4 Avenue Docteur Paul Kahn;54300;Luneville;48.5974249;6.5061312
+0590192N;Lycée General Et Technologique Ernest Couteaux;37 Avenue Du College;59734;St Amand Les Eaux;50.4477034;3.4431281
+0790029C;Lycée General Et Technologique Ernest Perochon;40 Rue Taillepied;79204;Parthenay;46.6423084;-0.2526283
+0220057T;Lycée General Et Technologique Ernest Renan;2 Et 4 Bd Herault;22021;St Brieuc;48.517094;-2.75443
+0720021G;Lycée General Et Technologique Estournelles De Constant;Domaine Bouchevereau;72205;La Fleche;47.69589;-0.074646
+0420046X;Lycée General Et Technologique Etienne Mimard;32 Rue Etienne Mimard;42021;St Etienne;45.438102;4.3960568
+0510007F;Lycée General Et Technologique Etienne Oehmichen;8 Avenue Du Mont Hery;51037;Chalons En Champagne;48.9761202;4.3588161
+0940116R;Lycée General Et Technologique Eugene Delacroix;5 Rue Pierre Curie;94704;Maisons Alfort;48.8107477;2.426031
+0220060W;Lycée General Et Technologique Eugene Freyssinet;32 Rue Mansart;22023;St Brieuc;48.5195608;-2.777683
+0230002C;Lycée General Et Technologique Eugene Jamot;1 Rue Williams Dumazet;23200;Aubusson;45.9530125;2.1677732
+0620140R;Lycée General Et Technologique Eugene Woillez;1 Rue Porte Becquerelle;62170;Montreuil;50.4644173;1.7663501
+0595867G;Lycée General Et Technologique Europeen Montebello;196 Boulevard Montebello;59006;Lille;50.6206484;3.0469456
+9740597F;Lycée General Et Technologique Evariste De Parny;Plateau Caillou 85 Rue A.vinson;97867;St Paul;0.0;0.0
+0782924E;Lycée General Et Technologique Evariste Galois;87 Avenue De Tobrouk;78500;Sartrouville;48.9285342;2.1613742
+0100015M;Lycée General Et Technologique F. Et I. Joliot Curie;1 Rue Guy Moquet;10100;Romilly Sur Seine;48.5155735;3.716231
+0570054Z;Lycée General Et Technologique Fabert;12 Rue Saint Vincent;57045;Metz;49.1226525;6.1717052
+0590119J;Lycée General Et Technologique Faidherbe;9 Rue Armand Carrel;59034;Lille;50.6165715;3.0739224
+9710774P;Lycée General Et Technologique Faustin Fleret;Section Esperance;97111;Morne A L Eau;0.0;0.0
+9730001N;Lycée General Et Technologique Felix Eboue;Rocade Sud;97306;Cayenne;4.9227;-52.3269
+0040010P;Lycée General Et Technologique Felix Esclangon;Boulevard Martin Bret;04103;Manosque;43.836809;5.781223
+0600001A;Lycée General Et Technologique Felix Faure;31 Boulevard De L Assaut;60000;Beauvais;49.4347804;2.0895545
+0220023F;Lycée General Et Technologique Felix Le Dantec;Rue Des Cordiers;22303;Lannion;48.7310544;-3.446948
+0572022N;Lycée General Et Technologique Felix Mayer;2 Square Georges Bastide;57150;Creutzwald;49.204987;6.6962469
+0590035T;Lycée General Et Technologique Fenelon;Place Fenelon;59407;Cambrai;50.1764669;3.2299983
+0331760J;Lycée General Et Technologique Fernand Daguin;15 Rue Gustave Flaubert;33698;Merignac;44.8366276;-0.6500899
+0620093P;Lycée General Et Technologique Fernand Darchicourt;211 Rue Rene Cassin;62251;Henin Beaumont;50.410645;2.960295
+0490054T;Lycée General Et Technologique Fernand Renaudeau;Rue La Tuilerie;49321;Cholet;47.0709727;-0.8903426
+6200001G;Lycée General Et Technologique Fesch;5 Cours Grandval;20176;Ajaccio;41.9176744;8.7311055
+0931565W;Lycée General Et Technologique Flora Tristan;27 Rue Des Hauts Roseaux;93166;Noisy Le Grand;48.8478226;2.5765564
+0951147F;Lycée General Et Technologique Fragonard;Allee Verte;95290;L Isle Adam;49.1193445;2.2241912
+0510062R;Lycée General Et Technologique Francois 1er;Fg De Vitry Le Brule;51308;Vitry Le Francois;48.729036;4.59224
+0660010C;Lycée General Et Technologique Francois Arago;22 Avenue Paul Doumer;66028;Perpignan;42.698684;2.8958719
+0770926N;Lycée General Et Technologique Francois Couperin;Route Hurtault;77305;Fontainebleau;48.3979083;2.6875719
+0330027A;Lycée General Et Technologique Francois Mauriac;1 Rue Henri Dunant;33015;Bordeaux;44.8394554;-0.5560782
+0421976V;Lycée General Et Technologique Francois Mauriac-forez;32 Rue Des Bullieux;42166;Andrezieux Boutheon;45.5374319;4.2959439
+0410002E;Lycée General Et Technologique Francois Philibert Dessaigne;;41007;Blois;47.588402;1.335616
+0220056S;Lycée General Et Technologique Francois Rabelais;8 Rue Rabelais;22022;St Brieuc;48.504309;-2.7467497
+0601823F;Lycée General Et Technologique Francois Truffaut;4 Rue De Pontoise;60007;Beauvais;49.4236814;2.0846913
+0851346B;Lycée General Et Technologique Francois Truffaut;8 Rue De La Cailletiere;85300;Challans;46.8538105;-1.8685351
+0451484U;Lycée General Et Technologique Francois Villon;Avenue Pierre De Felice;45190;Beaugency;47.7856371;1.6185055
+0750690T;Lycée General Et Technologique Francois Villon;10 16 Avenue Marc Sangnier;75014;Paris 14;48.826174;2.306449
+0780422K;Lycée General Et Technologique Francois Villon;Rue Salvador Allende;78133;Les Mureaux;48.97137;1.922145
+0911021R;Lycée General Et Technologique Francois-joseph Talma;1 Rue Des Cerfs;91805;Brunoy;48.6928052;2.5025791
+0510034K;Lycée General Et Technologique Franklin Roosevelt;10 Rue Roosevelt;51096;Reims;49.2620465;4.0259878
+9720350Y;Lycée General Et Technologique Frantz Fanon;Cite Scolaire Frantz Fanon;97220;La Trinite;0.0;0.0
+0540040A;Lycée General Et Technologique Frederic Chopin;39 Rue Du Sergent Blandan;54042;Nancy;48.6804667;6.1664682
+0690103M;Lycée General Et Technologique Frederic Fays;46 Rue Frederic Fays;69615;Villeurbanne;45.7625759;4.9047165
+0131549N;Lycée General Et Technologique Frederic Joliot-curie;4 Avenue Des Goums;13400;Aubagne;43.2916597;5.5612548
+0680051L;Lycée General Et Technologique Frederic Kirschleger;8 Rue Du Docteur Heid;68140;Munster;48.0385537;7.1326565
+0281047L;Lycée General Et Technologique Fulbert;Rue Saint Cheron;28011;Chartres;48.4464583;1.5061027
+0220027K;Lycée General Et Technologique Fulgence Bienvenue;Rue Eon De L'etoile;22606;Loudeac;48.1851796;-2.7600028
+0910687C;Lycée General Et Technologique Fustel De Coulanges;11 Rue Des Migneaux;91300;Massy;48.7330082;2.2656033
+0090002D;Lycée General Et Technologique Gabriel Faure;5 Rue Lt Paul Delpech;09008;Foix;42.964127;1.605232
+0740005D;Lycée General Et Technologique Gabriel Faure;2 Avenue Du Rhone;74008;Annecy;45.8989205;6.1181581
+0720033V;Lycée General Et Technologique Gabriel Touchard;8 Place Washington;72002;Le Mans;47.9956706;0.2030578
+0710071S;Lycée General Et Technologique Gabriel Voisin;Rue Saint Jean;71700;Tournus;46.574065;4.907238
+0380083J;Lycée General Et Technologique Galilee;124 Avenue General Leclerc;38209;Vienne;45.5052095;4.8559693
+0772127U;Lycée General Et Technologique Galilee;Avenue Andre Malraux;77385;Combs La Ville;48.6574407;2.5584154
+0951637N;Lycée General Et Technologique Galilee;11 Av Du Jour;95801;Cergy;49.0459761;2.0299018
+0620007W;Lycée General Et Technologique Gambetta;25 Boulevard Carnot;62022;Arras;50.2874799;2.7750733
+0440030U;Lycée General Et Technologique Gaspard Monge;2 Rue De La Fantaisie;44322;Nantes;47.2535027;-1.5719339
+0100003Z;Lycée General Et Technologique Gaston Bachelard;5 Bis Rue Gaston Bachelard;10200;Bar Sur Aube;48.2298972;4.7045231
+0770922J;Lycée General Et Technologique Gaston Bachelard;32 Avenue De L'europe;77505;Chelles;48.9146526;2.5976487
+0590258K;Lycée General Et Technologique Gaston Berger;Avenue Gaston Berger Prolongee;59016;Lille;50.6143623;3.0807063
+0640052J;Lycée General Et Technologique Gaston Febus;20 Av Georges Moutet;64301;Orthez;43.4914882;-0.7661496
+9730108E;Lycée General Et Technologique Gaston Monnerville;Rue Du Lycée;97387;Kourou;0.0;0.0
+0020014E;Lycée General Et Technologique Gay Lussac;Boulevard Gambetta;02302;Chauny;49.613832;3.2214
+0670057Y;Lycée General Et Technologique General Leclerc;8 Rue Poincare;67703;Saverne;48.7409019;7.360907
+0771663P;Lycée General Et Technologique George Sand;Rue De La Mare Au Diable;77350;Le Mee Sur Seine;48.549902;2.636207
+0880152M;Lycée General Et Technologique Georges Baumont;Rue De L'orme;88102;St Die Des Vosges;48.29796;6.946644
+0300950V;Lycée General Et Technologique Georges Brassens;Avenue Vigan Braquet;30205;Bagnols Sur Ceze;44.1607643;4.6116424
+0594413B;Lycée General Et Technologique Georges Bustin;13 Rue Du 8 Mai 1945;59690;Vieux Conde;50.4642251;3.569136
+0190010H;Lycée General Et Technologique Georges Cabanis;6 Bd Henri De Jouvenel;19311;Brive La Gaillarde;45.1600815;1.5232644
+0340039H;Lycée General Et Technologique Georges Clemenceau;31 Avenue Georges Clemenceau;34060;Montpellier;43.6031139;3.8721038
+0850006V;Lycée General Et Technologique Georges Clemenceau;Place De La Republique;85111;Chantonnay;46.6849967;-1.0493349
+0930127H;Lycée General Et Technologique Georges Clemenceau;130 Rue De Neuilly;93250;Villemomble;48.8772594;2.5239123
+0250030X;Lycée General Et Technologique Georges Cuvier;1 Place Jean Monnet;25207;Montbeliard;47.510356;6.798466
+0540041B;Lycée General Et Technologique Georges De La Tour;5 Rue De La Croix St Claude;54052;Nancy;48.6928494;6.1381574
+0572757M;Lycée General Et Technologique Georges De La Tour;Place Maud'huy;57045;Metz;49.2036097;6.1574195
+0133525L;Lycée General Et Technologique Georges Duby;200 Rue Georges Duby;13080;Aix En Provence;43.4805284;5.4133099
+0270044B;Lycée General Et Technologique Georges Dumezil;Route D'ivry;27207;Vernon;49.0818358;1.4769566
+0921594H;Lycée General Et Technologique Georges Pompidou;1 Avenue Georges Pompidou;92390;Villeneuve La Garenne;48.9419734;2.3246588
+0020059D;Lycée General Et Technologique Gerard De Nerval;14 Rue Paul Deviolaine;02209;Soissons;49.3860844;3.3238462
+0950647M;Lycée General Et Technologique Gerard De Nerval;Place De L Europe;95270;Luzarches;49.113738;2.441136
+9710002A;Lycée General Et Technologique Gerville Reache;37 Rue Amedee Fengarol;97100;Basse Terre;16.2431386;-61.5372836
+7200009X;Lycée General Et Technologique Giocante De Casabianca;Av.jean Zuccarelli;20293;Bastia;42.7033101;9.4445024
+0510068X;Lycée General Et Technologique Godart Roger;8 Rue Godart Roger;51331;Epernay;49.0398693;3.9654465
+0330003Z;Lycée General Et Technologique Grand Air;Avenue Dr Lorentz Monod;33311;Arcachon;44.6531506;-1.1636262
+0440012Z;Lycée General Et Technologique Grand Air;77 Avenue Du Bois D Amour;44503;La Baule Escoublac;47.2848421;-2.3741177
+0370038R;Lycée General Et Technologique Grandmont;Avenue De Sevigne;37204;Tours;47.3603558;0.7008859
+0160002R;Lycée General Et Technologique Guez De Balzac;Place Beaulieu;16016;Angouleme;45.65111;0.146709
+0061763J;Lycée General Et Technologique Guillaume Apollinaire;29 Bd Jean Baptiste Verany;06300;Nice;43.7155171;7.2826203
+0940123Y;Lycée General Et Technologique Guillaume Apollinaire;42 Rue Du Pave De Grignon;94320;Thiais;48.7571462;2.4006742
+0760072R;Lycée General Et Technologique Guillaume Le Conquerant;Allee De La Cote Blanche;76170;Lillebonne;49.521553;0.545717
+0900003P;Lycée General Et Technologique Gustave Courbet;Av Gambiez;90020;Belfort;47.652328;6.856124
+0931272C;Lycée General Et Technologique Gustave Eiffel;16 Chemin De La Renardiere;93220;Gagny;48.8887212;2.5413326
+0760096S;Lycée General Et Technologique Gustave Flaubert;1 Rue Albert Dupuis;76044;Rouen;49.4508628;1.1226207
+0950644J;Lycée General Et Technologique Gustave Monod;71 Avenue De Ceinture;95880;Enghien Les Bains;48.97192;2.2940159
+0860021F;Lycée General Et Technologique Guy Chauvet;Rue De L Eperon;86206;Loudun;47.0060213;0.0850569
+0760035A;Lycée General Et Technologique Guy De Maupassant;1575 Boulevard Nelson Mandela;76401;Fecamp;49.7442498;0.3892768
+0920137Z;Lycée General Et Technologique Guy De Maupassant;52 Rue Robert Schuman;92701;Colombes;48.9273923;2.240501
+0440005S;Lycée General Et Technologique Guy Moquet;1 Rue De L Europe;44146;Chateaubriant;47.729359;-1.3637892
+0940124Z;Lycée General Et Technologique Hector Berlioz;106 Avenue De Paris;94307;Vincennes;48.8456885;2.4269693
+0491966W;Lycée General Et Technologique Henri Bergson;85 Rue De La Barre;49036;Angers;47.4721055;-0.5929403
+0750711R;Lycée General Et Technologique Henri Bergson;27 Rue Edouard Pailleron;75019;Paris 19;48.8810293;2.377875
+0500082T;Lycée General Et Technologique Henri Cornat;36 Rue Henri Cornat;50700;Valognes;49.5071261;-1.4752266
+0620113L;Lycée General Et Technologique Henri Darras;Chemin Des Manufactures;62803;Lievin;50.412424;2.766446
+0340009A;Lycée General Et Technologique Henri Iv;1 Rue Ignace Brunel;34543;Beziers;43.3436459;3.2107736
+0540042C;Lycée General Et Technologique Henri Loritz;29 Rue Des Jardiniers;54042;Nancy;48.6903847;6.1913797
+0061884R;Lycée General Et Technologique Henri Matisse;101 Avenue Foch;06140;Vence;43.72337;7.1054579
+0312290W;Lycée General Et Technologique Henri Matisse;Avenue Du Comminges;31270;Cugnaux;43.5335144;1.3592883
+0670041F;Lycée General Et Technologique Henri Meck;10 Rue Henri Meck;67125;Molsheim;48.5361513;7.4912458
+0770930T;Lycée General Et Technologique Henri Moissan;20 Cours De Verdun;77100;Meaux;48.9584066;2.8748634
+0710054Y;Lycée General Et Technologique Henri Parriat;49 Rue De Gourdon;71307;Montceau Les Mines;46.6749138;4.3728594
+0540038Y;Lycée General Et Technologique Henri Poincare;2 Rue De La Visitation;54042;Nancy;48.6918437;6.179348
+0590221V;Lycée General Et Technologique Henri Wallon;16 Place De La Republique;59322;Valenciennes;50.3630096;3.5208558
+0930116W;Lycée General Et Technologique Henri Wallon;146 Rue Des Cites;93300;Aubervilliers;48.9123093;2.3885117
+0130175V;Lycée General Et Technologique Honore Daumier;46 Avenue Clot Bey;13008;Marseille 08;43.2585306;5.3838879
+0360024F;Lycée General Et Technologique Honore De Balzac;8 Rue De La Limoise;36105;Issoudun;46.9567081;2.0000772
+0750705J;Lycée General Et Technologique Honore De Balzac;118 Boulevard Bessieres;75849;Paris 17;48.8954075;2.316289
+0060033D;Lycée General Et Technologique Honore D'estienne D'orves;13 Avenue D Estienne D Orves;06050;Nice;43.7036523;7.2446985
+0420042T;Lycée General Et Technologique Honore D'urfe;1 Impasse Le Chatelier;42014;St Etienne;45.4143637;4.3895743
+0601826J;Lycée General Et Technologique Hugues Capet;Avenue De Reims;60309;Senlis;49.1915037;2.5690088
+0510035L;Lycée General Et Technologique Hugues Libergier;55 Rue Libergier;51095;Reims;49.2514521;4.0278114
+0161095D;Lycée General Et Technologique Image Et Son;303 Avenue De Navarre;16022;Angouleme;45.6332536;0.1538142
+0570108H;Lycée General Et Technologique Industriel;15 Route De La Briquerie;57100;Thionville;49.363583;6.1609848
+0010072Y;Lycée General Et Technologique International;Avenue Des Sports;01216;Ferney Voltaire;46.2620007;6.1160646
+0061642C;Lycée General Et Technologique International;190 Rue F. Mistral;06902;Valbonne;43.6211121;7.0416901
+0040543U;Lycée General Et Technologique International Iter;116 Bd Regis Ryckebush;04100;Manosque;43.831469;5.7990491
+0312093G;Lycée General Et Technologique International Victor Hugo;Boulevard Victor Hugo;31773;Colomiers;43.6169776;1.3132042
+0340076Y;Lycée General Et Technologique Irene Et Frederic Joliot Curi;19 Boulevard Joliot Curie;34200;Sete;43.3972261;3.6659039
+0840017M;Lycée General Et Technologique Ismael Dauphin;Rue Pierre Fabre;84300;Cavaillon;43.8245562;5.0387837
+0851401L;Lycée General Et Technologique J.de Lattre De Tassigny;165 Rue Hubert Cailler;85021;La Roche Sur Yon;46.6754534;-1.4044832
+0420014M;Lycée General Et Technologique Jacob Holtzer;5 Rue Michelet;42704;Firminy;45.3882874;4.2914303
+0750713T;Lycée General Et Technologique Jacquard;2 Et 2 Bis Rue Bouret;75019;Paris 19;48.8805143;2.3765501
+0770933W;Lycée General Et Technologique Jacques Amyot;6 Bis Rue Michelet;77000;Melun;48.5454598;2.6590212
+0060001U;Lycée General Et Technologique Jacques Audiberti;Boulevard Wilson;06631;Antibes;43.578323;7.11987
+0692717D;Lycée General Et Technologique Jacques Brel;7 Avenue D'oschatz;69200;Venissieux;45.6952773;4.8802134
+0931430Z;Lycée General Et Technologique Jacques Brel;4-6 Rue Dulcie September;93120;La Courneuve;48.9284905;2.4004255
+0540070H;Lycée General Et Technologique Jacques Callot;12 Rue Jacques Callot;54500;Vandoeuvre Les Nancy;48.6663588;6.1630763
+0180007K;Lycée General Et Technologique Jacques Coeur;108 Rue Jean Baffier;18026;Bourges;47.0738989;2.409201
+0371418R;Lycée General Et Technologique Jacques De Vaucanson;1 Rue Vedrines;37081;Tours;47.4229477;0.7057627
+0930120A;Lycée General Et Technologique Jacques Feyder;10 Rue Henri Wallon;93806;Epinay Sur Seine;48.9551431;2.3208206
+0540058V;Lycée General Et Technologique Jacques Marquette;Place Foch;54701;Pont A Mousson;48.90329;6.054868
+0451462V;Lycée General Et Technologique Jacques Monod;7 Rue Leon Blum;45803;St Jean De Braye;47.9106684;1.9688004
+0641839B;Lycée General Et Technologique Jacques Monod;10 Rue Du Parvis;64234;Lescar;43.332829;-0.4354594
+0921555R;Lycée General Et Technologique Jacques Monod;46 Rue Du Fort;92140;Clamart;48.7956785;2.2724849
+0271431J;Lycée General Et Technologique Jacques Prevert;30 Route De Saint-paul;27500;Pont Audemer;49.3509874;0.5239679
+0440077V;Lycée General Et Technologique Jacques Prevert;17 Rue Joseph Malegue;44260;Savenay;47.364721;-1.9397342
+0911577V;Lycée General Et Technologique Jacques Prevert;23 Rue Jules Ferry;91162;Longjumeau;48.6865888;2.2969624
+0920134W;Lycée General Et Technologique Jacques Prevert;163 Rue De Billancourt;92100;Boulogne Billancourt;48.8333966;2.2376962
+0950651S;Lycée General Et Technologique Jacques Prevert;23 Chemin Vert De Boissy;95150;Taverny;49.0137009;2.2123305
+0622276M;Lycée General Et Technologique Jan Lavezzari;Avenue Du Phare;62602;Berck;50.3988519;1.5605537
+9710923B;Lycée General Et Technologique Jardin D'essai;Route Des Abymes;97139;Les Abymes;16.2417208;-61.4735645
+0330020T;Lycée General Et Technologique Jaufre Rudel;2 Rue Urbain Chasseloup;33394;Blaye;45.1239711;-0.660485
+0241137F;Lycée General Et Technologique Jay De Beaufort;9 Rue Turenne;24007;Perigueux;45.1825357;0.7117974
+0830025B;Lycée General Et Technologique Jean Aicard;Avenue Gallieni;83412;Hyeres;43.1188945;6.1262759
+0910627M;Lycée General Et Technologique Jean Baptiste Corot;Pl Davout Le Chateau;91605;Savigny Sur Orge;48.6851259;2.349282
+0870050G;Lycée General Et Technologique Jean Baptiste Darnet;28 Avenue De Perigueux;87500;St Yrieix La Perche;45.5048579;1.2020927
+0801700N;Lycée General Et Technologique Jean Baptiste Delambre;Rue Montaigne;80084;Amiens;49.9121084;2.3220718
+0590071G;Lycée General Et Technologique Jean Bart;1 Rue Du Nouvel Arsenal;59383;Dunkerque;51.0363738;2.3838308
+0492148U;Lycée General Et Technologique Jean Bodin;Av De L Europe;49137;Les Ponts De Ce;47.4409485;-0.5286577
+0352318E;Lycée General Et Technologique Jean Brito;Avenue Du Bois Greffier;35470;Bain De Bretagne;47.8470053;-1.6939432
+0400067F;Lycée General Et Technologique Jean Cassaigne;Avenue Kennedy;40280;St Pierre Du Mont;43.869494;-0.489719
+0133195C;Lycée General Et Technologique Jean Cocteau;Avenue Jean Cocteau;13141;Miramas;43.5872654;5.0088295
+0332747G;Lycée General Et Technologique Jean Condorcet;89 Rue Condorcet;33030;Bordeaux;44.8590368;-0.5776445
+0170028N;Lycée General Et Technologique Jean Dautet;18 Rue Delayant;17022;La Rochelle;46.1645196;-1.1540598
+0570098X;Lycée General Et Technologique Jean De Pange;16 Rue Du Lycée;57216;Sarreguemines;0.0;0.0
+0820004J;Lycée General Et Technologique Jean De Prades;Route De Toulouse;82102;Castelsarrasin;44.0334192;1.1211267
+0500016W;Lycée General Et Technologique Jean Francois Millet;1 Rue De Bougainville;50130;Cherbourg Octeville;49.6407942;-1.6467298
+0870003F;Lycée General Et Technologique Jean Giraudoux;Avenue Charles De Gaulle;87300;Bellac;46.1255359;1.061553
+0350011X;Lycée General Et Technologique Jean Guehenno;11 Rue Du Champ Rossignol;35305;Fougeres;48.3484995;-1.2000706
+0610021S;Lycée General Et Technologique Jean Guehenno;16 Rue Pierre Huet;61105;Flers;48.744858;-0.552585
+0541270M;Lycée General Et Technologique Jean Hanzelet;Place De Trey;54701;Pont A Mousson;48.9095605;6.0576284
+0510032H;Lycée General Et Technologique Jean Jaures;17 Rue Ruinart De Brimont;51062;Reims;49.258353;4.0443359
+0810012Y;Lycée General Et Technologique Jean Jaures;Route De Blaye;81400;Carmaux;44.0511877;2.1480562
+9741182S;Lycée General Et Technologique Jean Joly;2 Chemin La Ouete;97899;St Louis;-21.2709804;55.4310343
+0132210G;Lycée General Et Technologique Jean Lurcat;Chemin De St Macaire;13693;Martigues;43.4217515;5.0344096
+0880004B;Lycée General Et Technologique Jean Lurcat;22 Av De Lattre De Tassigny;88600;Bruyeres;48.205641;6.7148648
+0350026N;Lycée General Et Technologique Jean Mace;10 Rue Jean Mace;35704;Rennes;48.1173362;-1.6712538
+0790023W;Lycée General Et Technologique Jean Mace;18-20 Rue Gustave Eiffel;79004;Niort;46.3310176;-0.4685901
+0390019J;Lycée General Et Technologique Jean Michel;400 Rue Du Dr Michel;39015;Lons Le Saunier;46.6791715;5.5512839
+0150006A;Lycée General Et Technologique Jean Monnet;10 Rue Du Docteur Chibret;15005;Aurillac;44.9354809;2.4517539
+0341736C;Lycée General Et Technologique Jean Monnet;Rue De Malbosc;34088;Montpellier;43.6319486;3.8311411
+0420043U;Lycée General Et Technologique Jean Monnet;16 Rue Portail Rouge;42014;St Etienne;45.4168613;4.4105286
+0601832R;Lycée General Et Technologique Jean Monnet;10 Rue Des Cedres;60803;Crepy En Valois;49.2239895;2.8929225
+0670078W;Lycée General Et Technologique Jean Monnet;2 Place A Schweitzer;67028;Strasbourg;48.6419332;7.7584307
+0741476C;Lycée General Et Technologique Jean Monnet;1 Place De Lattre De Tassigny;74106;Annemasse;46.1828008;6.2460882
+0080040A;Lycée General Et Technologique Jean Moulin;996 Avenue De La Cite Scolaire;08500;Revin;49.9269895;4.6510517
+0180024D;Lycée General Et Technologique Jean Moulin;45 Rue Jean Moulin;18206;St Amand Montrond;46.7345343;2.5142771
+0290023T;Lycée General Et Technologique Jean Moulin;10 Rue Du Lycée;29150;Chateaulin;0.0;0.0
+0340011C;Lycée General Et Technologique Jean Moulin;Av Des Martyrs De La Resistance;34521;Beziers;43.3429502;3.233546
+0340059E;Lycée General Et Technologique Jean Moulin;1 Avenue Vidal De La Blache;34120;Pezenas;43.4590752;3.4162673
+0570029X;Lycée General Et Technologique Jean Moulin;7 Rue Maurice Barres;57608;Forbach;49.183268;6.9061491
+0590185F;Lycée General Et Technologique Jean Moulin;49 Bd Du General De Gaulle;59051;Roubaix;50.68501;3.17217
+0730005J;Lycée General Et Technologique Jean Moulin;12 Rue Felix Chautemps;73202;Albertville;45.6734316;6.3903457
+0772120L;Lycée General Et Technologique Jean Moulin;6-8 Av Jean Moulin - Torcy;77206;Torcy;48.8349124;2.6594968
+0830015R;Lycée General Et Technologique Jean Moulin;Place De La Paix;83300;Draguignan;43.535376;6.469779
+0860028N;Lycée General Et Technologique Jean Moulin;Avenue Jean Moulin;86501;Montmorillon;46.428122;0.876037
+0440062D;Lycée General Et Technologique Jean Perrin;20 Rue Du Chateau De Reze;44400;Reze;47.1806714;-1.5600159
+0590110Z;Lycée General Et Technologique Jean Perrin;2 Avenue Sakharov;59832;Lambersart;50.6476975;3.038367
+0690082P;Lycée General Et Technologique Jean Perrin;48 Rue Pierre Baizet;69338;Lyon 09;45.7936129;4.8146401
+0760076V;Lycée General Et Technologique Jean Prevost;Avenue Jean Prevost;76290;Montivilliers;49.5450214;0.177044
+0595786U;Lycée General Et Technologique Jean Prouve;2 Rue De Lompret;59463;Lille;50.6533164;2.9946618
+0930118Y;Lycée General Et Technologique Jean Renoir;11 Rue Fremin;93141;Bondy;48.9027563;2.473295
+0140017T;Lycée General Et Technologique Jean Rostand;98 Route D'ifs;14054;Caen;49.1629104;-0.3522713
+0590184E;Lycée General Et Technologique Jean Rostand;361 Grande Rue;59057;Roubaix;50.6954992;3.1964449
+0600009J;Lycée General Et Technologique Jean Rostand;Place Georges Paquier;60501;Chantilly;49.181337;2.462574
+0120012Y;Lycée General Et Technologique Jean Vigo;Le Puits De Cales;12100;Millau;44.090247;3.059129
+0301722J;Lycée General Et Technologique Jean Vilar;616 Av Du Dr Paul Gache;30409;Villeneuve Les Avignon;43.9814603;4.788257
+0772229E;Lycée General Et Technologique Jean Vilar;83 Avenue Salvador Allende;77100;Meaux;48.9547859;2.893935
+0780582J;Lycée General Et Technologique Jean Vilar;1 Rue Jacques Prevert;78375;Plaisir;48.8240075;1.9601951
+0540076P;Lycée General Et Technologique Jean Zay;2 Rue De La Tuilerie;54801;Jarny;49.1581264;5.8767019
+0630069H;Lycée General Et Technologique Jean Zay;21 Rue Jean Zay;63300;Thiers;45.864148;3.5305398
+0572027U;Lycée General Et Technologique Jean-baptiste Colbert;7 Impasse Colbert;57129;Thionville;49.366287;6.1478214
+0590064Z;Lycée General Et Technologique Jean-baptiste Corot;133 Rue Saint Vaast;59508;Douai;50.3751343;3.0784266
+0601906W;Lycée General Et Technologique Jean-baptiste Corot;Rue Henri Lebesgue;60000;Beauvais;49.42964;2.081875
+0300002P;Lycée General Et Technologique Jean-baptiste Dumas;Place De La Belgique;30104;Ales;44.128435;4.0760861
+0750700D;Lycée General Et Technologique Jean-baptiste Say;11 Bis Rue D'auteuil;75016;Paris 16;48.8474879;2.2677956
+0950648N;Lycée General Et Technologique Jean-jacques Rousseau;20 Rue De Jaigny;95160;Montmorency;48.9902224;2.3179386
+0211909L;Lycée General Et Technologique Jean-marc Boivin;4 Bis Route De Dijon;21800;Chevigny St Sauveur;47.3024948;5.127503
+0290034E;Lycée General Et Technologique Jean-marie Le Bris;2 Place Du Lycée;29172;Douarnenez;0.0;0.0
+0782132U;Lycée General Et Technologique Jeanne D Albret;6 Rue Giraud Teulon;78101;St Germain En Laye;48.8936305;2.0989384
+0540039Z;Lycée General Et Technologique Jeanne D Arc;16 Rue Pierre Fourier;54000;Nancy;48.6932069;6.1859085
+0760091L;Lycée General Et Technologique Jeanne D'arc;Rue Sainte-genevieve Du Mont;76044;Rouen;49.4441008;1.1081523
+0601824G;Lycée General Et Technologique Jeanne Hachette;31 Boulevard Amyot D Inville;60009;Beauvais;49.4354984;2.081916
+0693044J;Lycée General Et Technologique Jean-paul Sartre;93 Avenue Francois Mitterrand;69675;Bron;45.7372573;4.9196386
+0920802X;Lycée General Et Technologique Jean-pierre Vernant;21 Rue Du Dr Ledermann;92310;Sevres;48.8255916;2.211662
+0570085H;Lycée General Et Technologique Jean-victor Poncelet;Rue Des Anglais;57501;St Avold;49.1018211;6.7061996
+0760023M;Lycée General Et Technologique Jehan Ango;25 Rue Roger Lecoffre;76203;Dieppe;49.9217708;1.0652099
+0490002L;Lycée General Et Technologique Joachim Du Bellay;1 Avenue Marie Talet;49105;Angers;47.4762607;-0.5475231
+0340038G;Lycée General Et Technologique Joffre;150 Allee De La Citadelle;34060;Montpellier;43.6135477;3.8827869
+0311902Z;Lycée General Et Technologique Jolimont;44 Chemin Cassaing;31079;Toulouse;43.6168618;1.4763001
+0350029S;Lycée General Et Technologique Joliot-curie;144 Bd De Vitre;35703;Rennes;48.1220757;-1.6510693
+0920141D;Lycée General Et Technologique Joliot-curie;92 Avenue Joliot Curie;92014;Nanterre;48.890764;2.2068759
+0790019S;Lycée General Et Technologique Joseph Desfontaines;2 Rue Guillotiere;79500;Melle;46.2238133;-0.144721
+9720004X;Lycée General Et Technologique Joseph Gaillard;Rue Marie Therese Gertrude;97261;Fort De France;14.6040646;-61.084565
+0560038M;Lycée General Et Technologique Joseph Loth;4 Rue J M De Lamennais;56306;Pontivy;48.0662246;-2.9630909
+0220065B;Lycée General Et Technologique Joseph Savina;5 Place De La Republique;22220;Treguier;48.7859048;-3.2335656
+0010016M;Lycée General Et Technologique Joseph-marie Carriat;1 Rue De Crouy;01011;Bourg En Bresse;46.1981597;5.2225406
+0440001M;Lycée General Et Technologique Joubert;160 Rue Du Pressoir Rouge;44154;Ancenis;47.3752742;-1.1822546
+0260036Y;Lycée General Et Technologique Jules Algoud;Rue Barthelemy De Laffemas;26901;Valence;44.917059;4.915496
+0060014H;Lycée General Et Technologique Jules Ferry;82 Boulevard De La Republique;06402;Cannes;43.5586684;7.0219303
+0770924L;Lycée General Et Technologique Jules Ferry;4 Rue Henri Dunant;77527;Coulommiers;48.8240411;3.0905384
+0781845G;Lycée General Et Technologique Jules Ferry;7 Rue Bouyssel;78700;Conflans Ste Honorine;48.9989596;2.0960496
+0880055G;Lycée General Et Technologique Jules Ferry;48 Rue Saint Charles;88109;St Die Des Vosges;48.2877577;6.9551559
+0290013G;Lycée General Et Technologique Jules Lesven;34 Rue Jules Lesven;29801;Brest;48.4056622;-4.4787824
+0590282L;Lycée General Et Technologique Jules Mousseron;Boulevard Du 8 Mai 1945;59723;Denain;50.32402;3.396566
+0580031U;Lycée General Et Technologique Jules Renard;11 Boulevard Saint Exupery;58002;Nevers;47.0034899;3.1609303
+0600021X;Lycée General Et Technologique Jules Uhry;10 Rue Aristide Briand;60107;Creil;49.2580804;2.4755361
+0021939X;Lycée General Et Technologique Jules Verne;;02401;Chateau Thierry;49.064633;3.479051
+0911983L;Lycée General Et Technologique Jules Verne;49 Rue D'arpajon;91470;Limours;48.6420954;2.0892316
+0570146Z;Lycée General Et Technologique Julie Daubie;Rue Joffre;57120;Rombas;49.2534261;6.0999961
+0690032K;Lycée General Et Technologique Juliette Recamier;57 Rue De La Charite;69287;Lyon 02;45.7563614;4.8333688
+0500036T;Lycée General Et Technologique Julliot De La Morandiere;Rue De La Crete;50406;Granville;48.8264841;-1.5782999
+0920144G;Lycée General Et Technologique L Agora;120 Rue De Verdun;92816;Puteaux;48.8780243;2.2345512
+0782563M;Lycée General Et Technologique La Bruyere;31 Avenue De Paris;78000;Versailles;48.8005385;2.1377792
+0440086E;Lycée General Et Technologique La Coliniere;129 Rue Du Landreau;44319;Nantes;47.2345916;-1.5099978
+0801882L;Lycée General Et Technologique La Hotoie;Rue Du Batonnier Mahiu;80016;Amiens;49.8964029;2.2741636
+0690038S;Lycée General Et Technologique La Martiniere Duchere;300 Avenue Andrei Sakharov;69338;Lyon 09;45.7867171;4.7986751
+0690037R;Lycée General Et Technologique La Martiniere Diderot;18 Place Gabriel Rambaud;69283;Lyon 01;45.767838;4.8294264
+0692866R;Lycée General Et Technologique La Martiniere Monplaisir;41 Rue Antoine Lumiere;69372;Lyon 08;45.7399085;4.8673503
+9830002K;Lycée General Et Technologique La Perouse;5 Rue G. Baudoux;98849;Noumea;-22.2811991;166.4425546
+0710023P;Lycée General Et Technologique La Prat's;Rue Du 19 Mars 1962;71250;Cluny;46.4311567;4.6548977
+0650040R;Lycée General Et Technologique La Serre De Sarsan;Rue St Exupery;65107;Lourdes;43.1082589;-0.0241297
+0021522U;Lycée General Et Technologique La Thierache;Le Pont De Pierre;02140;Fontaine Les Vervins;49.832387;3.886854
+0740046Y;Lycée General Et Technologique La Versoie;12 Avenue Du Forchat;74203;Thonon Les Bains;46.3652365;6.4698132
+0690029G;Lycée General Et Technologique Lacassagne;93 Rue Antoine Charial;69425;Lyon 03;45.7577924;4.8703604
+6200002H;Lycée General Et Technologique Laetitia Bonaparte;3, Ave Napoleon Iii;20192;Ajaccio;41.9266076;8.7346838
+0920145H;Lycée General Et Technologique Lakanal;3 Av Pdt Franklin Roosevelt;92331;Sceaux;48.7772605;2.3077341
+0800007Y;Lycée General Et Technologique Lamarck;Avenue Robert Solente;80301;Albert;49.9999141;2.6573268
+0710045N;Lycée General Et Technologique Lamartine;Avenue Des Gaises;71018;Macon;46.317061;4.82604
+0240025X;Lycée General Et Technologique Laure Gatet;25 Av G Pompidou;24003;Perigueux;45.189843;0.7240838
+0530016E;Lycée General Et Technologique Lavoisier;281 Rue Du Pommier;53103;Mayenne;48.3058312;-0.628314
+0782546U;Lycée General Et Technologique Le Corbusier;88 Rue De Villiers;78300;Poissy;48.9233505;2.0271586
+0930117X;Lycée General Et Technologique Le Corbusier;44 Rue Leopold Rechossiere;93533;Aubervilliers;48.9145674;2.3910044
+0250033A;Lycée General Et Technologique Le Grand Chenois;Rue Pierre Donzelot;25204;Montbeliard;47.496347;6.808773
+9741185V;Lycée General Et Technologique Le Verger;1 Avenue Des Corossols;97438;Ste Marie;-20.9285865;55.4894457
+9740001H;Lycée General Et Technologique Leconte De Lisle;Cite Scolaire Du Butor;97491;St Denis;48.936181;2.357443
+0370001A;Lycée General Et Technologique Leonard De Vinci;Rue Du Clos Des Gardes;37402;Amboise;47.3993791;0.9837666
+0430947W;Lycée General Et Technologique Leonard De Vinci;Le Mazel;43120;Monistrol Sur Loire;45.292432;4.172183
+0951753P;Lycée General Et Technologique Leonard De Vinci;2 Rue Robquin;95470;St Witz;49.0876254;2.55935
+0170029P;Lycée General Et Technologique Leonce Vieljeux;118 Rue Des Gonthieres;17028;La Rochelle;46.1774956;-1.1513475
+0271579V;Lycée General Et Technologique Leopold Sedar Senghor;Rue Du Canada;27035;Evreux;49.0100928;1.1380869
+0760109F;Lycée General Et Technologique Les Bruyeres;67 Avenue Des Canadiens;76300;Sotteville Les Rouen;49.4127725;1.0733177
+0701052N;Lycée General Et Technologique Les Haberges;1 Rue Du Dr Jean Georges Girard;70014;Vesoul;47.630099;6.140727
+0721329D;Lycée General Et Technologique Les Horizons;Rue Victor Duruy;72650;St Saturnin;48.056355;0.165149
+0100025Y;Lycée General Et Technologique Les Lombards;12 Avenue Des Lombards;10000;Troyes;48.2747672;4.0725464
+0780515L;Lycée General Et Technologique Les Sept Mares;13 Rue De La Beauce;78310;Maurepas;48.766684;1.9452384
+0261277X;Lycée General Et Technologique Les Trois Sources;110 Chemin Du Valentin;26501;Bourg Les Valence;44.9493203;4.8967491
+0440029T;Lycée General Et Technologique Livet;16 Rue Dufour;44042;Nantes;47.2252268;-1.5446675
+0830032J;Lycée General Et Technologique Lorgues;1 Rue Emile Heraud;83510;Lorgues;43.4925907;6.3552396
+0693330V;Lycée General Et Technologique Louis Aragon;12 Che De La Cote A Cailloux;69700;Givors;45.6012034;4.7609173
+0680034T;Lycée General Et Technologique Louis Armand;3 Boulevard Des Nations;68058;Mulhouse;47.7321514;7.305391
+0731248K;Lycée General Et Technologique Louis Armand;321 Rue Du Grand Champ;73020;Chambery;45.5947932;5.9203139
+0860037Y;Lycée General Et Technologique Louis Armand;63 Rue De La Bugellerie;86022;Poitiers;46.5974421;0.3312595
+0541286E;Lycée General Et Technologique Louis Bertrand;27 Av Albert De Briey;54154;Briey;49.2572689;5.9349594
+0620040G;Lycée General Et Technologique Louis Blaringhem;Boulevard Victor Hugo;62408;Bethune;50.5308387;2.6424351
+0781861Z;Lycée General Et Technologique Louis De Broglie;1 Avenue Jean Beranger;78160;Marly Le Roi;48.8668593;2.1005876
+0640011P;Lycée General Et Technologique Louis De Foix;4 Av Jean Rostand;64103;Bayonne;43.4849353;-1.4754015
+0880019T;Lycée General Et Technologique Louis Lapicque;5 Rue Nicolas Bellot;88021;Epinal;48.1749063;6.4408913
+0540066D;Lycée General Et Technologique Louis Majorelle;16 Rue Porte De Metz;54200;Toul;48.6783902;5.8933465
+0250008Y;Lycée General Et Technologique Louis Pasteur;4 Rue Du Lycée;25043;Besancon;0.0;0.0
+0590117G;Lycée General Et Technologique Louis Pasteur;1 Rue Des Urbanistes;59000;Lille;50.6417246;3.0706587
+0590207E;Lycée General Et Technologique Louis Pasteur;151 Boulevard Louise Michel;59490;Somain;50.3493932;3.2826358
+0620095S;Lycée General Et Technologique Louis Pasteur;800 Rue Leon Blum;62251;Henin Beaumont;50.4306013;2.9485013
+0670082A;Lycée General Et Technologique Louis Pasteur;24 Rue Humann;67085;Strasbourg;48.5756269;7.7381955
+9741050Y;Lycée General Et Technologique Louis Payen;329 Rue Saint Louis;97863;St Paul;-21.000214;55.2816619
+0250010A;Lycée General Et Technologique Louis Pergaud;91 93 Boulevard Blum;25022;Besancon;47.263734;6.0453694
+0570058D;Lycée General Et Technologique Louis Vincent;Rue De Verdun;57000;Metz;49.1064034;6.1678408
+0270026G;Lycée General Et Technologique Louise Michel;47 Route De Dieppe;27140;Gisors;49.2850688;1.7823312
+0931613Y;Lycée General Et Technologique Louise Michel;70 Avenue Jean Jaures;93000;Bobigny;48.9025957;2.440509
+0841093G;Lycée General Et Technologique Lucie Aubrac;224 Rue Ernest Laffont;84500;Bollene;44.2939051;4.7496556
+0021946E;Lycée General Et Technologique Lycée Europeen;Avenue De Noue;02600;Villers Cotterets;49.2513352;3.0801771
+9741324W;Lycée General Et Technologique Mahatma Gandhi;Zac Porte Des Salazes;97440;St Andre;0.0;0.0
+0240005A;Lycée General Et Technologique Maine De Biran;108 Rue Valette;24108;Bergerac;44.8571395;0.4744008
+0140013N;Lycée General Et Technologique Malherbe;14 Avenue Albert Sorel;14052;Caen;49.1758952;-0.3712852
+0783140P;Lycée General Et Technologique Mansart;26 Rue Victorien Sardou;78210;St Cyr L Ecole;48.8044358;2.0638552
+0271582Y;Lycée General Et Technologique Marc Bloch;1 Voie Bacheliere;27106;Val De Reuil;49.2680332;1.2021861
+0672604S;Lycée General Et Technologique Marc Bloch;Allee Blaise Pascal;67803;Bischheim;48.6165342;7.7652418
+0511926S;Lycée General Et Technologique Marc Chagall;60 Chaussee Saint Martin;51726;Reims;49.2416308;4.0282581
+0130037V;Lycée General Et Technologique Marcel Pagnol;128 Boulevard De Saint Loup;13395;Marseille 10;43.2831428;5.4344926
+0910623H;Lycée General Et Technologique Marcel Pagnol;Avenue De La Terrasse;91205;Athis Mons;48.6946547;2.3768362
+0690104N;Lycée General Et Technologique Marcel Sembat;20 Boulevard Marcel Sembat;69694;Venissieux;45.7159355;4.8819736
+0760110G;Lycée General Et Technologique Marcel Sembat;128 Rue Leon Salva;76300;Sotteville Les Rouen;49.4152133;1.0855593
+0310039Z;Lycée General Et Technologique Marcelin Berthelot;59 Rue Achille Viadieu;31078;Toulouse;43.5883613;1.4444026
+0860003L;Lycée General Et Technologique Marcelin Berthelot;1 Avenue Du President Wilson;86106;Chatellerault;46.8148527;0.5406476
+0930124E;Lycée General Et Technologique Marcelin Berthelot;110 Avenue Jean Jaures;93500;Pantin;48.9063319;2.3950721
+0940120V;Lycée General Et Technologique Marcelin Berthelot;6 Boulevard Maurice Berteaux;94100;St Maur Des Fosses;48.8100943;2.470758
+0810033W;Lycée General Et Technologique Marechal Soult;Rue Du Lycée;81207;Mazamet;0.0;0.0
+0590086Y;Lycée General Et Technologique Marguerite De Flandre;15 Rue Pasteur;59147;Gondecourt;50.5443046;2.9799955
+0180006J;Lycée General Et Technologique Marguerite De Navarre;50 Rue De Vauvert;18016;Bourges;47.0813154;2.3815361
+0610002W;Lycée General Et Technologique Marguerite De Navarre;Avenue Du General Leclerc;61014;Alencon;48.4166835;0.0894658
+0160003S;Lycée General Et Technologique Marguerite De Valois;Rue Louise Leriget;16017;Angouleme;45.6532678;0.1797211
+0470018P;Lycée General Et Technologique Marguerite Filhol;Avenue Charles De Gaulle;47501;Fumel;44.498344;0.958251
+0721493G;Lycée General Et Technologique Marguerite Yourcenar;2 Rue Du Miroir;72007;Le Mans;47.9895703;0.2007066
+0141555P;Lycée General Et Technologique Marie Curie;16 Rue Octave Greard;14504;Vire;48.8401949;-0.8975237
+0382920T;Lycée General Et Technologique Marie Curie;Avenue Du 8 Mai 1945;38435;Echirolles;45.1701009;5.7563365
+0650026A;Lycée General Et Technologique Marie Curie;Rue Georges Ledormeur;65930;Tarbes;43.223345;0.07366
+0782567S;Lycée General Et Technologique Marie Curie;70 Av De Paris;78002;Versailles;48.7987327;2.1414585
+0100023W;Lycée General Et Technologique Marie De Champagne;2 Avenue Marie De Champagne;10026;Troyes;48.3040593;4.0617662
+0133244F;Lycée General Et Technologique Marie Madeleine Fourcade;Avenue Du Groupe Manouchian;13120;Gardanne;43.455669;5.470649
+0383263R;Lycée General Et Technologique Marie Reynoard;20 Rue Louis Neel;38190;Villard Bonnot;45.2511485;5.8974898
+0622949U;Lycée General Et Technologique Mariette;69 Rue De Beaurepaire;62321;Boulogne Sur Mer;50.7321268;1.6119389
+0130038W;Lycée General Et Technologique Marseilleveyre;83 Traverse Parangon;13285;Marseille 08;43.2463268;5.3798313
+0772292Y;Lycée General Et Technologique Martin Luther King;21 Av Du General De Gaulle;77600;Bussy St Georges;48.8390621;2.7187791
+0350042F;Lycée General Et Technologique Maupertuis;Rue Pierre De Coubertin;35407;St Malo;48.6400752;-2.0058975
+0911927A;Lycée General Et Technologique Maurice Eliot;Rue De Provence;91860;Epinay Sous Senart;48.6992445;2.5232739
+0132410Z;Lycée General Et Technologique Maurice Genevoix;Avenue General De Gaulle;13700;Marignane;43.4128456;5.2048951
+0790007D;Lycée General Et Technologique Maurice Genevoix;36 Rue De Malabry;79301;Bressuire;46.8407623;-0.4847921
+0921399W;Lycée General Et Technologique Maurice Genevoix;29 Avenue Du Fort;92120;Montrouge;48.810644;2.323172
+0170022G;Lycée General Et Technologique Maurice Merleau-ponty;3 Rue Raymonde Maous;17304;Rochefort;45.9505449;-0.9713481
+0750715V;Lycée General Et Technologique Maurice Ravel;89 Cours De Vincennes;75020;Paris 20;48.8476082;2.4082145
+0330088S;Lycée General Et Technologique Max Linder;43 All Robert Boulin;33505;Libourne;44.9171154;-0.2403445
+0940580V;Lycée General Et Technologique Maximilien Sorre;61 Avenue Du President Wilson;94230;Cachan;48.789111;2.325304
+0680032R;Lycée General Et Technologique Michel De Montaigne;5 Rue De Metz;68090;Mulhouse;47.7489658;7.3421381
+0920149M;Lycée General Et Technologique Michelet;5 Rue Jullien;92174;Vanves;48.8250185;2.287109
+0600015R;Lycée General Et Technologique Mireille Grenet;13 Avenue De Huy;60321;Compiegne;49.4025055;2.8188505
+0740037N;Lycée General Et Technologique Mme De Stael;16 Route De Thairy;74163;St Julien En Genevois;46.1409969;6.0704696
+0270017X;Lycée General Et Technologique Modeste Leroy;32 Rue Pierre Brossolette;27016;Evreux;49.0216265;1.1618627
+0080027L;Lycée General Et Technologique Monge;2 Avenue De St Julien;08000;Charleville Mezieres;49.7613023;4.7131743
+0210017E;Lycée General Et Technologique Montchapet;36 Boulevard Francois Pompon;21000;Dijon;47.3343589;5.0174446
+0630068G;Lycée General Et Technologique Montdory;Cite Pontel 7 Ter Av J.jaures;63304;Thiers;45.857907;3.545177
+0951723G;Lycée General Et Technologique Montesquieu;Rue Emile Zola;95220;Herblay;48.9963505;2.152554
+9720727H;Lycée General Et Technologique Montgerald;Quartier Montgerald;97290;Le Marin;0.0;0.0
+0130042A;Lycée General Et Technologique Montgrand;13 Rue Montgrand;13291;Marseille 06;43.2915831;5.3783833
+0630034V;Lycée General Et Technologique Murat;27 Boulevard Pasteur;63500;Issoire;45.5506873;3.2484695
+0610026X;Lycée General Et Technologique Napoleon;15 Rue Des Sports;61306;L Aigle;48.7582148;0.6363548
+9710940V;Lycée General Et Technologique Nord Basse Terre;La Ramee;97115;Ste Rose;45.9474919;4.838131
+0332081H;Lycée General Et Technologique Odilon Redon;R Maquis Des Vignes Oudides;33250;Pauillac;45.1907714;-0.7495194
+0930123D;Lycée General Et Technologique Olympe De Gouges;205 Rue De Brement;93130;Noisy Le Sec;48.8872318;2.4667557
+0310047H;Lycée General Et Technologique Ozenne;9 Rue Merly;31070;Toulouse;43.610058;1.4428833
+0762169V;Lycée General Et Technologique Pablo Neruda;Chemin Des Bruyeres;76204;Dieppe;49.900744;1.06796
+0620027T;Lycée General Et Technologique Pablo Picasso;2 Boulevard Anatole France;62210;Avion;50.4005382;2.8354531
+0660014G;Lycée General Et Technologique Pablo Picasso;120 Avenue General Jean Gilles;66028;Perpignan;42.6975235;2.9260966
+0941347D;Lycée General Et Technologique Pablo Picasso;2 Avenue Pablo Picasso;94120;Fontenay Sous Bois;48.855411;2.4862776
+0332722E;Lycée General Et Technologique Pape Clement;1 Rue Leo Lagrange;33605;Pessac;44.8086751;-0.6500731
+0690074F;Lycée General Et Technologique Parc Chabrieres;9 Chemin Des Chassagnes;69600;Oullins;45.7224869;4.8100176
+0911251R;Lycée General Et Technologique Parc Des Loges;Boulevard Des Champs Elysees;91012;Evry;48.6410335;2.4334713
+0060029Z;Lycée General Et Technologique Parc Imperial;2 Avenue Paul Arene;06050;Nice;43.7038318;7.2505348
+0320002D;Lycée General Et Technologique Pardailhan;Chemin De Baron;32008;Auch;43.6546824;0.5749589
+7200021K;Lycée General Et Technologique Pascal Paoli;Av President Pierucci;20250;Corte;42.3041836;9.1512357
+0130002G;Lycée General Et Technologique Paul Cezanne;Av J Et M Fontenaille;13100;Aix En Provence;43.5346979;5.467873
+0020032Z;Lycée General Et Technologique Paul Claudel;Place Foch;02001;Laon;49.5655582;3.6097485
+0141275K;Lycée General Et Technologique Paul Cornu;9 Rue Paul Cornu;14107;Lisieux;49.15711;0.2224269
+0590034S;Lycée General Et Technologique Paul Duez;1 Boulevard Paul Bezin;59407;Cambrai;50.174142;3.2420519
+0790024X;Lycée General Et Technologique Paul Guerin;19 Rue Des Fiefs;79004;Niort;46.3145227;-0.443402
+0590010R;Lycée General Et Technologique Paul Hazard;1 Rue Paul Hazard;59426;Armentieres;50.6883173;2.8763804
+0730037U;Lycée General Et Technologique Paul Heroult;307 Avenue Du Mont Cenis;73302;St Jean De Maurienne;45.2725564;6.3502605
+0600002B;Lycée General Et Technologique Paul Langevin;3 Avenue Montaigne;60009;Beauvais;49.414897;2.1063143
+0920147K;Lycée General Et Technologique Paul Langevin;2 Rue Payret Dortail;92150;Suresnes;48.877042;2.2221624
+0920138A;Lycée General Et Technologique Paul Lapie;5 Bd Aristide Briand;92401;Courbevoie;48.899279;2.257718
+0110004V;Lycée General Et Technologique Paul Sabatier;36 Rue Alfred De Musset;11802;Carcassonne;43.2222389;2.3451227
+0720055U;Lycée General Et Technologique Paul Scarron;La Brunetiere;72140;Sille Le Guillaume;48.1924459;-0.118455
+0080039Z;Lycée General Et Technologique Paul Verlaine;Rue Du Docteur Gobinet;08305;Rethel;49.5167514;4.3704596
+0370039S;Lycée General Et Technologique Paul-louis Courier;2 Pl Gregoire De Tours;37012;Tours;47.3959502;0.6953666
+0441992B;Lycée General Et Technologique Pays De Retz;Place Joseph Girard;44211;Pornic;47.1175499;-2.096457
+0130036U;Lycée General Et Technologique Perier;270 Rue Paradis;13295;Marseille 08;43.2942989;5.3763908
+0720027N;Lycée General Et Technologique Perseigne;Rue Jean Jaures;72600;Mamers;48.349964;0.363104
+0332835C;Lycée General Et Technologique Philippe Cousteau;13 Rue Arnaudin;33240;St Andre De Cubzac;44.9914275;-0.4497814
+0840005Z;Lycée General Et Technologique Philippe De Girard;138 Av De Tarascon;84082;Avignon;43.9250989;4.8091279
+0301552Z;Lycée General Et Technologique Philippe Lamour;Rue De L Occitanie;30001;Nimes;43.8322264;4.3863175
+0080045F;Lycée General Et Technologique Pierre Bayle;Rue Rogissart;08200;Sedan;49.6930223;4.9478574
+0383119J;Lycée General Et Technologique Pierre Beghin;Rue De La Roche Brune;38430;Moirans;45.3211861;5.5864031
+0230020X;Lycée General Et Technologique Pierre Bourdan;Place Moliere;23011;Gueret;46.171715;1.871472
+0690132U;Lycée General Et Technologique Pierre Brossolette;161 Cours Emile Zola;69628;Villeurbanne;45.7698199;4.8784389
+0782822U;Lycée General Et Technologique Pierre Corneille;Avenue Corneille;78170;La Celle St Cloud;48.8450625;2.1372043
+0600014P;Lycée General Et Technologique Pierre D Ailly;136 Bd Des Etats Unis;60321;Compiegne;49.406211;2.8338602
+0310024H;Lycée General Et Technologique Pierre D'aragon;14 Avenue Henri Peyrusse;31605;Muret;43.4478905;1.3276737
+0620062F;Lycée General Et Technologique Pierre De Coubertin;320 Boulevard Du 8 Mai;62225;Calais;50.958627;1.837843
+0770931U;Lycée General Et Technologique Pierre De Coubertin;Chaussee De Paris;77100;Meaux;48.9559126;2.869815
+0020049T;Lycée General Et Technologique Pierre De La Ramee;1 Rue Jules Siegfried;02100;St Quentin;49.8511637;3.3005423
+0382270L;Lycée General Et Technologique Pierre Du Terrail;Avenue Du Lycée;38530;Pontcharra;0.0;0.0
+0060026W;Lycée General Et Technologique Pierre Et Marie Curie;Avenue Du Doyen Jean Lepine;06500;Menton;43.777619;7.485669
+0360009P;Lycée General Et Technologique Pierre Et Marie Curie;31 Rue P Et M Curie;36018;Chateauroux;46.809443;1.7081627
+0501219D;Lycée General Et Technologique Pierre Et Marie Curie;377 Rue De L'exode;50010;St Lo;49.107496;-1.0893717
+0880040R;Lycée General Et Technologique Pierre Et Marie Curie;Rue Victor Martin;88307;Neufchateau;48.358763;5.6969839
+0891168L;Lycée General Et Technologique Pierre Larousse;6 Rue Des Montagnes;89130;Toucy;47.739374;3.2968476
+0800046R;Lycée General Et Technologique Pierre Mendes France;Route De Saint Denis;80201;Peronne;49.9361685;2.9435387
+0772188K;Lycée General Et Technologique Pierre Mendes-france;11 Avenue De L'europe;77176;Savigny Le Temple;48.5970024;2.576709
+0850025R;Lycée General Et Technologique Pierre Mendes-france;Boulevard Arago;85021;La Roche Sur Yon;46.6704474;-1.4451322
+9740952S;Lycée General Et Technologique Pierre Poivre;Rue Hippolyte Foucque;97480;St Joseph;-21.3746802;55.6251873
+0040490L;Lycée General Et Technologique Pierre-gilles De Gennes;Quartier St Christophe;04000;Digne Les Bains;44.077491;6.187476
+0861223M;Lycée General Et Technologique Pilote Innovant International;Teleport 5;86130;Jaunay Clan;46.6634435;0.3613592
+0781297L;Lycée General Et Technologique Plaine De Neauphle;Avenue Salvador Allende;78190;Trappes;48.783188;1.98746
+0270042Z;Lycée General Et Technologique Porte De Normandie;830 Chemin Des Poissonniers;27130;Verneuil Sur Avre;48.7405634;0.9156189
+0380089R;Lycée General Et Technologique Portes De L'oisans;Avenue Aristide Briand;38220;Vizille;45.067413;5.769686
+0450049J;Lycée General Et Technologique Pothier;2 Bis Rue Marcel Proust;45044;Orleans;47.908273;1.9068319
+0240035H;Lycée General Et Technologique Pre De Cordy;5 Avenue Josephine Baker;24200;Sarlat La Caneda;44.8703019;1.2059934
+0390786T;Lycée General Et Technologique Pre Saint Sauveur;Cite Scolaire Pre St Sauveur;39201;St Claude;46.387405;5.8677659
+0030051P;Lycée General Et Technologique Presles;Boulevard Du 8 Mai 1945;03306;Cusset;46.131694;3.442323
+9710921Z;Lycée General Et Technologique Providence;Morne L'epingle Providence;97139;Les Abymes;16.27395;-61.502615
+0920798T;Lycée General Et Technologique Rabelais;6 Rue Georges Langrognet;92190;Meudon;48.8019343;2.2402667
+0750688R;Lycée General Et Technologique Rabelais-paramedical Et Socia;9 Rue Francis De Croisset;75018;Paris 18;48.8994731;2.3455576
+0720012X;Lycée General Et Technologique Racan;9 Avenue Du Mans;72500;Chateau Du Loir;47.6987575;0.4132499
+0750664P;Lycée General Et Technologique Racine;20 Rue Du Rocher;75008;Paris 08;48.8765125;2.3224703
+0870118F;Lycée General Et Technologique Raoul Dautry;14 Rue Du Puy Imbert;87036;Limoges;45.8387379;1.2785581
+0580032V;Lycée General Et Technologique Raoul Follereau;9 Boulevard Saint Exupery;58000;Nevers;47.0040888;3.1563606
+0900004R;Lycée General Et Technologique Raoul Follereau;3 Rue Louis Marchal;90016;Belfort;47.6383708;6.8462164
+0550002D;Lycée General Et Technologique Raymond Poincare;1 Place Paul Lemagny;55012;Bar Le Duc;48.7772859;5.1572672
+0594424N;Lycée General Et Technologique Raymond Queneau;Place Leon Blum;59650;Villeneuve D Ascq;50.6501468;3.1057879
+0120031U;Lycée General Et Technologique Raymond Savignac;Rue Agnes Savignac;12203;Villefranche De Rouergue;44.3563661;2.0381734
+0530012A;Lycée General Et Technologique Reaumur;39 Avenue Chanzy;53013;Laval;48.0669908;-0.7596169
+0280036M;Lycée General Et Technologique Remi Belleau;33 Rue Bretonnerie;28400;Nogent Le Rotrou;48.3189144;0.8109948
+0352235P;Lycée General Et Technologique Rene Cassin;Route De St Meen Le Grand;35162;Montfort Sur Meu;48.138879;-1.957605
+0690085T;Lycée General Et Technologique Rene Cassin;75 Route De Saint Clement;69173;Tarare;45.8973407;4.4437914
+0911632E;Lycée General Et Technologique Rene Cassin;17 Rue Jean Moulin;91294;Arpajon;48.580006;2.2446388
+0950646L;Lycée General Et Technologique Rene Cassin;7 Avenue Francois Mitterrand;95500;Gonesse;48.985717;2.4325823
+0840935K;Lycée General Et Technologique Rene Char;2 Rue Pierre A Renoir;84033;Avignon;43.9460637;4.8531827
+0351907H;Lycée General Et Technologique Rene Descartes;Chemin De Ronde;35207;Rennes;48.0893228;-1.6435067
+0631861F;Lycée General Et Technologique Rene Descartes;Avenue Jules Ferry;63801;Cournon D Auvergne;45.7436203;3.2103095
+0693654X;Lycée General Et Technologique Rene Descartes;Avenue De Gadagne;69230;St Genis Laval;45.6945428;4.7859462
+0340023R;Lycée General Et Technologique Rene Gosse;2 Rue Victor Hugo;34800;Clermont L Herault;43.6264775;3.4318207
+0170027M;Lycée General Et Technologique Rene Josue Valin;Rue Henri Barbusse;17023;La Rochelle;46.155842;-1.137402
+0290062K;Lycée General Et Technologique Rene Laennec;61 Rue Du Lycée;29120;Pont L Abbe;0.0;0.0
+0730043A;Lycée General Et Technologique Rene Perrin;41 Rue Rene Perrin;73400;Ugine;45.7514631;6.4161324
+0680060W;Lycée General Et Technologique Ribeaupierre;12 Rue Du Chateau;68150;Ribeauville;48.1969038;7.3163765
+0920799U;Lycée General Et Technologique Richelieu;64 Rue George Sand;92501;Rueil Malmaison;48.8669727;2.1806262
+0311323V;Lycée General Et Technologique Rive Gauche;Avenue Jean Baylet;31081;Toulouse;43.5846573;1.3942102
+9710884J;Lycée General Et Technologique Riviere Des Peres;Riviere Des Peres;97100;Basse Terre;16.008758;-61.7407286
+0801841S;Lycée General Et Technologique Robert De Luzarches;4 Rue Le Mongnier;80097;Amiens;49.8856067;2.3204605
+0693619J;Lycée General Et Technologique Robert Doisneau;5 Rue Du Lycée;69511;Vaulx En Velin;0.0;0.0
+0570057C;Lycée General Et Technologique Robert Schuman;4 Rue Monseigneur Pelt;57074;Metz;49.1122787;6.2034203
+0670020H;Lycée General Et Technologique Robert Schuman;2 Quai Des Pecheurs;67504;Haguenau;48.8145686;7.7953074
+0760058A;Lycée General Et Technologique Robert Schuman;51 Avenue Du 8 Mai;76610;Le Havre;49.5161566;0.1664377
+0620006V;Lycée General Et Technologique Robespierre;Avenue Des Fusilles;62022;Arras;50.2867806;2.7650924
+0750696Z;Lycée General Et Technologique Roger Verlomme;24 Rue Fondary;75015;Paris 15;48.8480367;2.2932272
+0950640E;Lycée General Et Technologique Romain Rolland;1 Place Romain Rolland;95100;Argenteuil;48.9516193;2.2306041
+0280019U;Lycée General Et Technologique Rotrou;Les Marchebeaux;28104;Dreux;48.734267;1.362417
+0260017C;Lycée General Et Technologique Roumanille;Draye De Meyne;26111;Nyons;44.361204;5.139882
+0490055U;Lycée General Et Technologique Sadi Carnot;25 Rue Marceau;49401;Saumur;47.250839;-0.0746699
+0130039X;Lycée General Et Technologique Saint Charles;5 Rue Guy Fabre;13232;Marseille 01;43.3058152;5.3920359
+0130048G;Lycée General Et Technologique Saint Exupery;529 Che De La Madrague Ville;13326;Marseille 15;43.3463387;5.3547981
+0690028F;Lycée General Et Technologique Saint Just;21 Rue Des Farges;69321;Lyon 05;45.7575431;4.8217746
+0171418Z;Lycée General Et Technologique Saint-exupery;Allee De Lattre De Tassigny;17028;La Rochelle;46.1569483;-1.1815663
+0641732K;Lycée General Et Technologique Saint-john Perse;2 Rue J Ferry;64012;Pau;43.319586;-0.3639694
+0141796B;Lycée General Et Technologique Salvador Allende;15 Rue Guyon De Guercheville;14200;Herouville St Clair;49.2033372;-0.3448826
+0941470M;Lycée General Et Technologique Samuel De Champlain;61 Rue Des Bordes;94430;Chennevieres Sur Marne;48.7987853;2.5577377
+9740043D;Lycée General Et Technologique Sarda Garriga;755 Rue De La Communaute;97440;St Andre;-20.9643338;55.6558272
+0850032Y;Lycée General Et Technologique Savary De Mauleon;78 Avenue De Bretagne;85108;Les Sables D Olonne;46.5047869;-1.7776341
+0680073K;Lycée General Et Technologique Scheurer Kestner;1 Rue Moschenross;68802;Thann;47.8011401;7.1139147
+0670002N;Lycée General Et Technologique Schure;2 Rue Du Lycée;67142;Barr;0.0;0.0
+0080007P;Lycée General Et Technologique Sevigne;14 Rue Mme De Sevigne;08013;Charleville Mezieres;49.7710181;4.7172012
+0352304P;Lycée General Et Technologique Sevigne;2 Rue De La Chalotais;35513;Cesson Sevigne;48.1647545;-1.5796421
+0590215N;Lycée General Et Technologique Sevigne;151 Rue De La Malcense;59208;Tourcoing;50.730799;3.1650448
+0951766D;Lycée General Et Technologique Simone De Beauvoir;171 Av De Stalingrad;95141;Garges Les Gonesse;48.9677071;2.4101572
+0420044V;Lycée General Et Technologique Simone Weil;63 Avenue Albert Raimond;42272;St Priest En Jarez;45.4752;4.3691484
+0430021P;Lycée General Et Technologique Simone Weil;22 Boulevard Marechal Joffre;43003;Le Puy En Velay;45.048158;3.8937244
+0750651A;Lycée General Et Technologique Simone Weil;7 Rue De Poitou;75003;Paris 03;48.8613887;2.3639619
+0501839C;Lycée General Et Technologique Sivard De Beaulieu;7 Bis Rue Sivard De Beaulieu;50500;Carentan;49.304491;-1.2397239
+0620063G;Lycée General Et Technologique Sophie Berthelot;224 Boulevard Gambetta;62104;Calais;50.9466002;1.8418599
+0750653C;Lycée General Et Technologique Sophie Germain;9 Rue De Jouy;75004;Paris 04;48.8548803;2.3581925
+0520027X;Lycée General Et Technologique St Exupery;82 Rue Anatole France;52105;St Dizier;48.6430006;4.9626225
+0782539L;Lycée General Et Technologique St Exupery;8 Rue Marcel Fouque;78201;Mantes La Jolie;48.9959726;1.6947006
+9710922A;Lycée General Et Technologique Ste Anne;Poirier De Gissac;97180;Ste Anne;0.0;0.0
+0210012Z;Lycée General Et Technologique Stephen Liegeard;1 Rue Stephen Liegeard;21220;Brochon;47.2390412;4.9716086
+0573326F;Lycée General Et Technologique Teyssier;23 Rue Du Schellenthal;57230;Bitche;49.0565652;7.4161082
+0840004Y;Lycée General Et Technologique Theodore Aubanel;14 Rue De La Palapharnerie;84025;Avignon;43.952567;4.8119249
+0061691F;Lycée General Et Technologique Thierry Maulnier;2 Avenue Claude Debussy;06200;Nice;43.6854715;7.1997893
+0130040Y;Lycée General Et Technologique Thiers;5 Place Du Lycée;13232;Marseille 01;0.0;0.0
+0760005T;Lycée General Et Technologique Thomas Corneille;Avenue Andre Maurois;76360;Barentin;49.5566128;0.9530339
+0311586F;Lycée General Et Technologique Toulouse-lautrec;64 Bd Pierre Curie;31020;Toulouse;43.6251328;1.4415296
+0290051Y;Lycée General Et Technologique Tristan Corbiere;16 Rue De Kerveguen;29671;Morlaix;48.5705251;-3.8221176
+0750647W;Lycée General Et Technologique Turgot;69 Rue De Turbigo;75003;Paris 03;48.8660831;2.3583935
+0870056N;Lycée General Et Technologique Turgot;6 Rue Paul Derignac;87031;Limoges;45.8284168;1.2486231
+0770918E;Lycée General Et Technologique Uruguay France;1 Avenue Des Marronniers;77211;Avon;48.4103009;2.7355078
+0841117H;Lycée General Et Technologique Vaison La Romaine;Vaison La Romaine;84110;Vaison La Romaine;44.2414859;5.0777653
+0761742F;Lycée General Et Technologique Val De Seine;Avenue Georges Braque;76124;Le Grand Quevilly;49.402714;1.04095
+0062015H;Lycée General Et Technologique Valbonne;1265 Route De Biot;06560;Valbonne;43.6435755;7.0381165
+0590122M;Lycée General Et Technologique Valentine Labbe;41 Rue Paul Doumer;59110;La Madeleine;50.6473736;3.0721697
+0911913K;Lycée General Et Technologique Vallee De Chevreuse;8 Rue De Madrid;91192;Gif Sur Yvette;48.7000948;2.0991226
+0762879S;Lycée General Et Technologique Vallee Du Cailly;Rue Du Petit Aulnay;76250;Deville Les Rouen;49.462879;1.045217
+0771512A;Lycée General Et Technologique Van Dongen;45 Rue Jean Mermoz;77400;Lagny Sur Marne;48.8677351;2.7191655
+0950645K;Lycée General Et Technologique Van Gogh;Rue Du General Decaen;95123;Ermont;48.9828272;2.2525097
+0080018B;Lycée General Et Technologique Vauban;15 Rue Andre Bouzy;08600;Givet;50.1333015;4.8290573
+0380033E;Lycée General Et Technologique Vaucanson;27 Rue Anatole France;38030;Grenoble;45.1722442;5.7083809
+0130003H;Lycée General Et Technologique Vauvenargues;60 Boulevard Carnot;13625;Aix En Provence;43.5284055;5.4549853
+0791062A;Lycée General Et Technologique Venise Verte;71 Rue Laurent Bonnevay;79012;Niort;46.3171043;-0.4858123
+0440031V;Lycée General Et Technologique Vial;12 Rue Du 14 Juillet;44042;Nantes;47.2182601;-1.5678263
+0400017B;Lycée General Et Technologique Victor Duruy;Rue Noneres;40002;Mont De Marsan;43.898948;-0.499903
+0352009U;Lycée General Et Technologique Victor Et Helene Basch;15 Avenue Charles Tillon;35083;Rennes;48.1268772;-1.6876718
+0501828R;Lycée General Et Technologique Victor Grignard;12 R Guillaume Fouace;50100;Cherbourg Octeville;49.6385355;-1.6291646
+0130043B;Lycée General Et Technologique Victor Hugo;3 Boulevard Gustave Desplaces;13003;Marseille 03;43.3051852;5.3805831
+0250007X;Lycée General Et Technologique Victor Hugo;1 Rue Rembrandt;25052;Besancon;47.2183047;5.9777021
+0530004S;Lycée General Et Technologique Victor Hugo;4 Rue Du Gal Lemonnier;53200;Chateau Gontier;47.828315;-0.6982177
+0561607T;Lycée General Et Technologique Victor Hugo;26 Avenue Francois Mitterrand;56704;Hennebont;47.8091467;-3.2627756
+0840016L;Lycée General Et Technologique Victor Hugo;139 Av Victor Hugo;84208;Carpentras;44.0515235;5.0474084
+0860034V;Lycée General Et Technologique Victor Hugo;10 Rue Victor Hugo;86034;Poitiers;46.5805351;0.337763
+9720002V;Lycée General Et Technologique Victor Schoelcher;Boulevard Robert Attuly;97262;Fort De France;14.6029213;-61.0763884
+0250032Z;Lycée General Et Technologique Viette;1b Rue P Donzelot;25206;Montbeliard;47.4951765;6.8092234
+0930834B;Lycée General Et Technologique Voillaume;136 Rue De Mitry;93604;Aulnay Sous Bois;48.9457777;2.5099926
+0450782F;Lycée General Et Technologique Voltaire;3 Avenue Voltaire;45072;Orleans;47.8350087;1.937364
+0623915U;Lycée General Et Technologique Voltaire;Place Bertin Ledoux;62410;Wingles;50.494578;2.852404
+0750675B;Lycée General Et Technologique Voltaire;101 Avenue De La Republique;75011;Paris 11;48.8634943;2.3846316
+0590222W;Lycée General Et Technologique Watteau;20 Boulevard Pater;59307;Valenciennes;50.3582668;3.5317714
+0932034F;Lycée General Et Technologique Wolfgang Amadeus Mozart;10 Ave Charles De Gaulle;93152;Le Blanc Mesnil;48.9438125;2.4679399
+0640017W;Lycée General Experimental A.malraux;Rue Du 8 Mai 1945;64203;Biarritz;43.485562;-1.545229
+0590116F;Lycée General Fenelon;27 Rue Alexandre Leleux;59013;Lille;50.6315402;3.0572537
+0750660K;Lycée General Fenelon;2 Rue De L'eperon;75006;Paris 06;48.8533839;2.3409528
+0340005W;Lycée General Ferdinand Fabre;Boulevard Jean Moulin;34600;Bedarieux;43.613182;3.155169
+0120022J;Lycée General Ferdinand Foch;1 Rue Vieussens;12000;Rodez;44.350556;2.5620421
+0312754A;Lycée General Fonsorbes;Avenue Lucie Aubrac Fonsorbes;31470;Fonsorbes;43.535456;1.230645
+0783548H;Lycée General Franco Allemand;5 Rue Collin Mamet;78530;Buc;48.7712546;2.1214749
+0760052U;Lycée General Francois 1er;2 Rue Jean-paul Sartre;76066;Le Havre;49.4954384;0.1137507
+0770927P;Lycée General Francois 1er;11 Rue Victor Hugo;77300;Fontainebleau;48.4091563;2.6957656
+0260006R;Lycée General Francois Jean Armorin;35 Avenue H.grand;26402;Crest;44.731422;5.0164927
+0330026Z;Lycée General Francois Magendie;10 Rue Des Treuils;33023;Bordeaux;44.8270751;-0.5854467
+0820016X;Lycée General Francois Mitterrand;Boulevard Du Quercy;82201;Moissac;44.1012226;1.0931804
+0350710G;Lycée General Francois Rene De Chateaubriand;136 Bd De Vitre;35703;Rennes;48.1220757;-1.6510693
+0840003X;Lycée General Frederic Mistral;37 Rue D Annanelle;84023;Avignon;43.9467812;4.7999217
+0670049P;Lycée General Freppel;25 Rue Du Gen Gouraud;67212;Obernai;48.4616378;7.4873303
+0670079X;Lycée General Fustel De Coulanges;1 Place Du Chateau;67061;Strasbourg;48.5816654;7.7520269
+0750684L;Lycée General Gabriel Faure;81 Avenue De Choisy;75013;Paris 13;48.823831;2.3615927
+0440024M;Lycée General Gabriel Guisthau;3 Rue Marie Anne Du Boccage;44042;Nantes;47.2163672;-1.5683753
+0442112G;Lycée General Galilee;16 Avenue Gustave Flaubert;44353;Guerande;47.3241762;-2.4171927
+0870015U;Lycée General Gay Lussac;12 Boulevard Georges Perin;87031;Limoges;45.8308335;1.2627435
+0470028A;Lycée General George Sand;Bd Pierre De Coubertin;47600;Nerac;44.137773;0.334861
+0754684J;Lycée General Georges Brassens;40 Rue Manin;75019;Paris 19;48.8840069;2.3904607
+0510031G;Lycée General Georges Clemenceau;46 Avenue Georges Clemenceau;51096;Reims;49.2516684;4.0457383
+0300008W;Lycée General Gerard Philipe;17 Avenue Leon Blum;30205;Bagnols Sur Ceze;44.1625881;4.616978
+0240013J;Lycée General Giraut De Borneil;10 Boulevard A Dupuy;24160;Excideuil;45.3397691;1.0460386
+0570107G;Lycée General Helene Boucher;55 Boulevard Foch;57100;Thionville;49.358428;6.1612623
+0750714U;Lycée General Helene Boucher;75 Cours De Vincennes;75020;Paris 20;48.8477131;2.4070344
+0750654D;Lycée General Henri Iv;23 Rue Clovis;75005;Paris 05;48.8463061;2.347663
+0020048S;Lycée General Henri Martin;1 Rue Gabriel Girodon;02100;St Quentin;49.8694377;3.2486887
+0782562L;Lycée General Hoche;73 Avenue De St Cloud;78000;Versailles;48.806093;2.136819
+0783549J;Lycée General International;Rue Du Fer A Cheval;78104;St Germain En Laye;48.8978284;2.0627911
+0383242T;Lycée General International Europole;4 Place De Sfax;38012;Grenoble;45.1946007;5.7111866
+0890003V;Lycée General Jacques Amyot;3 Rue De L'etang Saint Vigile;89015;Auxerre;47.799159;3.571808
+0750668U;Lycée General Jacques Decour;12 Avenue Trudaine;75009;Paris 09;48.881468;2.3434282
+0750699C;Lycée General Janson De Sailly;106 Rue De La Pompe;75016;Paris 16;48.8655035;2.2794818
+0750702F;Lycée General Jean De La Fontaine;1 Place De La Porte Molitor;75016;Paris 16;48.8451877;2.2565988
+0360008N;Lycée General Jean Giraudoux;31 Avenue Marcel Lemoine;36000;Chateauroux;46.8162727;1.696676
+0460026D;Lycée General Jean Lurcat;Quai Jules Ferry;46400;St Cere;44.8580683;1.8929695
+0332745E;Lycée General Jean Monnet;70 Av Gl De Gaulle;33294;Blanquefort;44.910084;-0.635244
+0371417P;Lycée General Jean Monnet;45 Rue De La Gitonniere;37306;Joue Les Tours;47.3382097;0.6558827
+0331636Z;Lycée General Jean Moulin;Bd Francois Mauriac;33210;Langon;44.5478647;-0.2565149
+0690030H;Lycée General Jean Moulin;1 Place Des Minimes;69321;Lyon 05;45.757748;4.8209387
+0420031F;Lycée General Jean Puy;Rue Jean Puy;42328;Roanne;46.0381422;4.0698154
+0630019D;Lycée General Jeanne D'arc;40 Avenue De Grande Bretagne;63037;Clermont Ferrand;45.777262;3.0955677
+0320036R;Lycée General Joseph Saverne;5 Avenue Claude Auge;32600;L Isle Jourdain;43.6128214;1.0853875
+0750669V;Lycée General Jules Ferry;77 Boulevard De Clichy;75009;Paris 09;48.883866;2.3288165
+0640047D;Lycée General Jules Supervielle;Boulevard F.mitterrand;64404;Oloron Ste Marie;43.184366;-0.621821
+0440022K;Lycée General Jules Verne;1 Rue General Meusnier;44042;Nantes;47.2173915;-1.5617182
+0670080Y;Lycée General Kleber;25 Place De Bordeaux;67082;Strasbourg;48.5950246;7.7579489
+0920143F;Lycée General La Folie Saint James;41 Rue De Longchamp;92200;Neuilly Sur Seine;48.8825303;2.2557562
+0510053F;Lycée General La Fontaine Du Ve;Avenue De La Fontaine Du Ve;51122;Sezanne;48.726156;3.7170255
+0880030E;Lycée General La Haie Griselle;Boite Postale 109;88407;Gerardmer;48.070081;6.877292
+0010013J;Lycée General Lalande;16 Rue Du Lycée;01011;Bourg En Bresse;0.0;0.0
+0750670W;Lycée General Lamartine;121 Rue Du Fg Poissonniere;75009;Paris 09;48.8784359;2.3490775
+0681761V;Lycée General Lambert;73 Rue Josue Heilmann;68069;Mulhouse;47.7546303;7.3302166
+0810006S;Lycée General Laperouse;2 Lices Georges Pompidou;81030;Albi;43.9302181;2.1472285
+0810030T;Lycée General Las Cases;Place De La Resistance;81500;Lavaur;43.698135;1.8209846
+0620017G;Lycée General Lavoisier;99 Rue Jean Jaures;62260;Auchel;50.5075071;2.4754481
+0750656F;Lycée General Lavoisier;17 Rue Henri Barbusse;75005;Paris 05;48.8423289;2.3392448
+0500065Z;Lycée General Le Verrier;7 Rue Le Verrier;50002;St Lo;49.114593;-1.085313
+0460013P;Lycée General Leo Ferre;75 Avenue Cavaignac;46300;Gourdon;44.7341503;1.3771929
+0510015P;Lycée General Leon Bourgeois;29 Avenue De Champagne;51331;Epernay;49.043149;3.9594547
+0870016V;Lycée General Leonard Limosin;13 Rue Des Clairettes;87036;Limoges;45.8296501;1.2542492
+0640055M;Lycée General Louis Barthou;2 Rue Louis Barthou;64015;Pau;43.2946775;-0.3652763
+0750655E;Lycée General Louis Le Grand;123 Rue Saint Jacques;75005;Paris 05;48.8479761;2.3441552
+0920142E;Lycée General Louis Pasteur;17 A 21 Bd D Inkermann;92200;Neuilly Sur Seine;48.8858222;2.2741732
+0800009A;Lycée General Louis Thuillier;70 Boulevard De Saint Quentin;80098;Amiens;49.8791607;2.2965051
+9840001D;Lycée General Lycée D'uturoa;Uturoa;98735;Uturoa;-16.7331706;-151.4403326
+9840002E;Lycée General Lycée Paul Gauguin;Papeete;98714;Papeete;-17.535022;-149.569594
+0030025L;Lycée General Madame De Stael;1 Rue Madame De Stael;03100;Montlucon;46.3388313;2.6097306
+0800010B;Lycée General Madeleine Michelis;43 Rue Des Otages;80037;Amiens;49.8890443;2.3029357
+0280007F;Lycée General Marceau;2 Rue Pierre Mendes France;28000;Chartres;48.4414461;1.4951576
+0140061R;Lycée General Marcel Gambier;3 Rue General Leclerc;14107;Lisieux;49.1496609;0.2243572
+0670083B;Lycée General Marie Curie;7 Rue De Leicester;67084;Strasbourg;48.5777746;7.7720751
+0920146J;Lycée General Marie Curie;1 Rue Constant Pilate;92331;Sceaux;48.7778987;2.2870922
+0060030A;Lycée General Massena;2 Avenue Felix Faure;06050;Nice;43.6991436;7.2762535
+0640065Y;Lycée General Maurice Ravel;Av Du Pr Maranon;64502;St Jean De Luz;43.388051;-1.663055
+0590181B;Lycée General Maxence Van Der Meersch;1 Av Maxence Van Der Meersch;59052;Roubaix;50.7014845;3.238885
+0330021U;Lycée General Michel Montaigne;118 Cours Victor Hugo;33075;Bordeaux;44.834861;-0.5718881
+0130045D;Lycée General Michelet;21 Avenue Marechal Foch;13248;Marseille 04;43.3009814;5.3993878
+0650012K;Lycée General Michelet;148 Rue Michelet;65303;Lannemezan;43.1267858;0.3861872
+0820020B;Lycée General Michelet;22 Faubourg Lacapelle;82004;Montauban;44.0133311;1.3611955
+0750703G;Lycée General Moliere;71 Rue Du Ranelagh;75016;Paris 16;48.8550931;2.272105
+0750657G;Lycée General Montaigne;17 Rue Auguste Comte;75006;Paris 06;48.8442327;2.3349031
+0330022V;Lycée General Montesquieu;4 Et 5 Place De Longchamps;33081;Bordeaux;48.7535502;0.0030397
+0720029R;Lycée General Montesquieu;1 Rue Montesquieu;72008;Le Mans;48.0099285;0.2002142
+0130010R;Lycée General Montmajour;Che Des Moines Qua Du Trebon;13200;Arles;43.6017739;4.6243751
+0332724G;Lycée General Nord Bassin;128 Avenue De Bordeaux;33510;Andernos Les Bains;44.7518058;-1.0797311
+0750689S;Lycée General Paul Bert;7 Rue Huyghens;75014;Paris 14;48.8408655;2.328794
+0640046C;Lycée General Paul Rey;6 Avenue Jean Seigneres;64800;Nay;43.1810505;-0.2540108
+0340075X;Lycée General Paul Valery;55 Rue Paul Valery;34200;Sete;43.4013903;3.6930027
+0750679F;Lycée General Paul Valery;38 Boulevard Soult;75012;Paris 12;48.8391381;2.4089398
+0442334Y;Lycée General Pierre Abelard;Chemin Du Rouaud;44330;Vallet;47.1685954;-1.2643431
+0510006E;Lycée General Pierre Bayen;22 Rue Du Lycée;51037;Chalons En Champagne;0.0;0.0
+0312696M;Lycée General Pierre Bourdieu;Avenue De Villaudric;31620;Fronton;43.834408;1.401836
+0760090K;Lycée General Pierre Corneille;4 Rue Du Maulevrier;76044;Rouen;49.4453249;1.1001636
+0310036W;Lycée General Pierre De Fermat;Parvis Des Jacobins;31068;Toulouse;43.604652;1.444209
+0650038N;Lycée General Pierre Mendes France;Rue Du College;65501;Vic En Bigorre;43.388411;0.056435
+0710011B;Lycée General Pontus De Tyard;13 Rue Des Gaillardons;71321;Chalon Sur Saone;46.7792939;4.8365859
+0596122J;Lycée General Privé Active Bilingue Jeannine Manue;418 Bis Rue Albert Bailly;59700;Marcq En Baroeul;50.6905276;3.1198309
+0940880W;Lycée General Privé Albert De Mun;12-14 Av Des Marronniers;94736;Nogent Sur Marne;48.8371754;2.470904
+0331491S;Lycée General Privé Albert Le Grand;189 Rue De Saint Genes;33000;Bordeaux;44.8239861;-0.5819519
+0931813R;Lycée General Privé Alliance;35 Allee Robert Estienne;93320;Les Pavillons Sous Bois;48.9066801;2.5114479
+0133555U;Lycée General Privé Ami;47 Rue St Suffren;13006;Marseille 06;43.2871912;5.3813577
+0311132M;Lycée General Privé Annonciation;7 Chemin De Percin;31840;Seilh;43.678854;1.3673649
+0350778F;Lycée General Privé Assomption;18 Boulevard Painleve;35702;Rennes;48.1217772;-1.6560228
+0772648K;Lycée General Privé Assomption;2 Rue De Salins;77130;Forges;48.418206;2.960877
+0810071M;Lycée General Privé Barral;113 Rue Marcel Briguiboul;81100;Castres;43.593354;2.2327115
+0941909P;Lycée General Privé Bernard Palissy;2 Rue Merciere;94470;Boissy St Leger;48.7508613;2.5140211
+0754325U;Lycée General Privé Beth Hanna;49 Rue Petit;75019;Paris 19;48.8855041;2.3853435
+0910838S;Lycée General Privé Beth Rivkah;43 49 Rue Raymond Poincare;91330;Yerres;48.711413;2.5006397
+0753384W;Lycée General Privé Beth Yacov;50 Bis Rue Des Prairies;75020;Paris 20;48.8634435;2.401922
+0610700E;Lycée General Privé Bignon;3 Rue De La Comedie;61400;Mortagne Au Perche;48.5222373;0.5449647
+0440154D;Lycée General Privé Blanche De Castille;43-45 Boulevard Jules Verne;44319;Nantes;47.237147;-1.5333106
+0930974D;Lycée General Privé Blanche De Castille;1 Place Charles De Gaulle;93250;Villemomble;48.9012637;2.4777022
+0753915Y;Lycée General Privé Blomet;5 Rue Blomet;75015;Paris 15;48.8440336;2.3089779
+0753887T;Lycée General Privé Bossuet-notre-dame;17 Rue Yves Toudic;75010;Paris 10;48.8704201;2.3632866
+0622107D;Lycée General Privé Catho Haffreingue Chanlaire;67 Avenue Charles De Gaulle;62200;Boulogne Sur Mer;50.7302448;1.6189027
+0260064D;Lycée General Privé Chabrillan;Route De Dieulefit;26207;Montelimar;44.552049;4.6923919
+0511131C;Lycée General Privé Charles Peguy;2 Bis Allees Paul Doumer;51037;Chalons En Champagne;48.9525137;4.3710585
+0930962R;Lycée General Privé Charles Peguy;216 Avenue Henri Barbusse;93000;Bobigny;48.9116357;2.434482
+0690574Z;Lycée General Privé Chevreul;21 Rue Sala;69287;Lyon 02;45.7560734;4.8289023
+0131335F;Lycée General Privé Chevreul Blancarde;1 Rue Saint Francois De Sales;13248;Marseille 04;43.3017559;5.4016838
+0932110N;Lycée General Privé Chne Or;150 Rue Andre Karman;93300;Aubervilliers;48.9112912;2.3852031
+0680141J;Lycée General Privé College Episcopal;5 Rue Du Seminaire;68720;Zillisheim;47.7002658;7.3057379
+0671609K;Lycée General Privé College Episcopal St Etienne;2 Rue De La Pierre Large;67084;Strasbourg;48.583169;7.75531
+0131344R;Lycée General Privé Cours Bastide;50 Rue De Lodi;13006;Marseille 06;43.2897281;5.386952
+0141165R;Lycée General Privé Cours Notre-dame;Rue De L'arbalete;14440;Douvres La Delivrande;49.2954884;-0.3728552
+0440158H;Lycée General Privé D'amboise-chavagnes;11 Rue Mondesir;44015;Nantes;47.2177089;-1.5694414
+0160065J;Lycée General Privé De Beaulieu;23 Place Beaulieu;16105;Cognac;45.6944011;-0.3306255
+0753943D;Lycée General Privé De La Tour;86 Rue De La Tour;75016;Paris 16;48.8615462;2.2788627
+0690529A;Lycée General Privé Deborde;72 Rue Ney;69006;Lyon 06;45.7666327;4.8556717
+0271051W;Lycée General Privé Des Roches;Avenue Edmond Demolins;27137;Verneuil Sur Avre;48.7386465;0.911939
+0753848A;Lycée General Privé D'hulst;21 Rue De Varenne;75007;Paris 07;48.8536447;2.3250815
+0292137R;Lycée General Privé Diwan;Kerampuil;29270;Carhaix Plouguer;48.273696;-3.552728
+0530052U;Lycée General Privé Don Bosco;18 Bd Anatole France;53102;Mayenne;48.295993;-0.6158636
+0680134B;Lycée General Privé Don Bosco;1 Rue Don Bosco;68440;Landser;47.6849042;7.3871869
+0331498Z;Lycée General Privé Du Mirail;36 Rue Du Mirail;33000;Bordeaux;44.8333866;-0.5710599
+0070071P;Lycée General Privé Du Sacre Coeur;7 Avenue De La Gare;07301;Tournon Sur Rhone;45.0637796;4.834587
+0752937K;Lycée General Privé Ecole Active Bilingue Etoile;24 Bis Rue De Berri;75008;Paris 08;48.8732062;2.3056124
+0753874D;Lycée General Privé Ecole Active Bilingue J.manuel;70 Rue Du Theatre;75015;Paris 15;48.847462;2.291572
+0753647G;Lycée General Privé Ecole Alsacienne;109 R Notre Dame Des Champs;75006;Paris 06;48.8409608;2.3336956
+0671615S;Lycée General Privé Ecole Aquiba;4 Rue Baldung Grien;67000;Strasbourg;48.5899787;7.7529847
+0753883N;Lycée General Privé Edgar Poe;2 R Du Faubourg Poissonniere;75010;Paris 10;48.8709741;2.3480187
+0930961P;Lycée General Privé Esperance;35 Rue Anatole France;93600;Aulnay Sous Bois;48.935783;2.4938629
+0290336H;Lycée General Privé Estran Charles De Foucauld;32 Rue De Quimper;29287;Brest;48.4021619;-4.4602772
+0690533E;Lycée General Privé Externat De La Trinite;31 Rue De Seze;69006;Lyon 06;45.7682665;4.8465112
+0440160K;Lycée General Privé Externat Enfants Nantais;31 Avenue Camus;44042;Nantes;47.2189478;-1.5726768
+0381803D;Lycée General Privé Externat Notre Dame;43 Avenue Marcellin Berthelot;38100;Grenoble;45.1737452;5.731904
+0421021G;Lycée General Privé Externat Saint Michel;4 Rue Jules Valles;42030;St Etienne;45.4315189;4.3977734
+0631074A;Lycée General Privé Fenelon;1 Cours Raymond Poincare;63037;Clermont Ferrand;45.7709705;3.0900322
+0830099G;Lycée General Privé Fenelon;251 Rue Pourquoi Pas;83000;Toulon;43.1097419;5.9519764
+0931799A;Lycée General Privé Fenelon;1 Rue De Montauban;93410;Vaujours;48.9301569;2.5695529
+0753873C;Lycée General Privé Fenelon Sainte-marie;24 Rue Du General Foy;75008;Paris 08;48.8784763;2.3179146
+0941693E;Lycée General Privé Foyer Des Ptt;36 Avenue Du President Wilson;94234;Cachan;48.7922449;2.3291571
+0753935V;Lycée General Privé Georges Leven;30 Boulevard Carnot;75012;Paris 12;48.8424354;2.4129184
+0753941B;Lycée General Privé Gerson;31 Rue De La Pompe;75116;Paris 16;48.8608035;2.2751876
+0771720B;Lycée General Privé Guy Gasnier-sainte Bathilde;28 Rue Du Tir;77500;Chelles;48.8893418;2.5961092
+0755025E;Lycée General Privé Heikhal Menahem Sinai;110 Boulevard De Menilmontant;75020;Paris 20;48.8655642;2.384824
+0930965U;Lycée General Privé Henri Matisse;88 Bis Rue Jules Guesde;93100;Montreuil;48.8627932;2.4792419
+0120052S;Lycée General Privé Immaculee Conception;Rue Auzuech;12500;Espalion;44.525222;2.769631
+0690554C;Lycée General Privé Immaculee Conception;74 Place Grandclement;69613;Villeurbanne;45.7591491;4.8897359
+0840074Z;Lycée General Privé Immaculee Conception;18 Rue Des Marins;84208;Carpentras;44.0539842;5.0490871
+0596335R;Lycée General Privé Institut D Anchin;Abbaye D'anchin;59146;Pecquencourt;50.375524;3.212056
+0690564N;Lycée General Privé Institution Des Chartreux;58 Rue Pierre Dupont;69283;Lyon 01;45.7718191;4.8226139
+0141169V;Lycée General Privé Institution Fremont;12 Rue Paul Banaston;14100;Lisieux;49.1476683;0.2237108
+0141157G;Lycée General Privé Institution Jeanne D'arc;10 Rue D'eterville;14403;Bayeux;49.2809816;-0.7065406
+6200650M;Lycée General Privé Institution Saint-paul;Avenue Marechal Lyautey;20186;Ajaccio;41.9379483;8.7447941
+0671552Y;Lycée General Privé Jan Amos Comenius;8 Place Des Etudiants;67060;Strasbourg;48.5842395;7.7489578
+0511130B;Lycée General Privé Jean Xxiii;18 Rue Andrieux;51064;Reims;49.2592782;4.0360628
+0572341K;Lycée General Privé Jean Xxiii;10 R Monseigneur Heintz;57958;Montigny Les Metz;49.0988857;6.1588903
+0110047S;Lycée General Privé Jeanne D Arc;47 Rue De La Baffe;11400;Castelnaudary;43.3159746;1.957673
+0640127R;Lycée General Privé Jeanne D Arc;47 Rue Moncade;64300;Orthez;43.4903576;-0.7717527
+0141161L;Lycée General Privé Jeanne D'arc;27 Rue Claude Chappe;14000;Caen;49.1815244;-0.395588
+0381670J;Lycée General Privé Jeanne D'arc;2 Rue Poincare;38550;Le Peage De Roussillon;45.3678002;4.7966512
+0490822C;Lycée General Privé Jeanne D'arc;3 Rue Joubert;49103;Angers;47.4725034;-0.5449917
+0650062P;Lycée General Privé Jeanne D'arc;17 Rue Massey;65000;Tarbes;43.2347802;0.0738099
+0730760E;Lycée General Privé Jeanne D'arc;3 Place De L'eglise;73203;Albertville;45.6780169;6.3908431
+0771232W;Lycée General Privé Jeanne D'arc Saint Aspais;18 Boulevard Andre Maginot;77300;Fontainebleau;48.4057991;2.6893858
+0260062B;Lycée General Privé Jeunes Filles;77 Rue Geoffroy De Moirans;26330;Chateauneuf De Galaure;45.2345255;4.9553014
+0693769X;Lycée General Privé Juif De Lyon;40 Rue Alexandre Boutin;69100;Villeurbanne;45.768352;4.868835
+0592935V;Lycée General Privé La Croix Blanche;2 Rue De L'abbe Six;59588;Bondues;50.7198225;3.1094008
+0622116N;Lycée General Privé La Malassise;Pension Saint Joseph;62968;Longuenesse;50.736744;2.243632
+0541986R;Lycée General Privé La Malgrange;Avenue De La Malgrange;54140;Jarville La Malgrange;48.669525;6.199726
+0442226F;Lycée General Privé La Mennais;24 Rue Jean Baptiste Legeay;44350;Guerande;47.3216018;-2.4317213
+0570201J;Lycée General Privé La Misericorde;11 Rue Des Recollets;57000;Metz;49.1206035;6.1806771
+0440172Y;Lycée General Privé La Perverie Sacre Coeur;63 Rue De La Perverie;44322;Nantes;47.2408522;-1.5621761
+0753851D;Lycée General Privé La Rochefoucauld;90 Bis Rue Saint Dominique;75007;Paris 07;48.8594019;2.3057007
+0331504F;Lycée General Privé La Sauque;Le Bourg;33650;La Brede;44.6707635;-0.4790839
+0693587Z;Lycée General Privé La Xaviere;252 Route De Vienne;69008;Lyon 08;45.7282439;4.8537692
+0131324U;Lycée General Privé Lacordaire;7 Boulevard Lacordaire;13013;Marseille 13;43.3197549;5.4057658
+0753849B;Lycée General Privé L'alma;12 Avenue Bosquet;75007;Paris 07;48.8602799;2.3023934
+0753948J;Lycée General Privé L'assomption;6 Rue De Lubeck;75116;Paris 16;48.8666108;2.2940415
+0311126F;Lycée General Privé Le Ferradou;Route De Grenade;31700;Blagnac;43.6401295;1.3886897
+0850118S;Lycée General Privé L'esperance;Bourdevaire;85110;Ste Cecile;46.711412;-1.113992
+0771228S;Lycée General Privé Libre De Juilly;7 Rue Barre;77230;Juilly;49.0111848;2.7045603
+0131345S;Lycée General Privé L'olivier - Robert Coffy;29 Avenue Des Caillols;13012;Marseille 12;43.3039572;5.4277833
+0753827C;Lycée General Privé Louise De Marillac;32 R Geoffroy Saint Hilaire;75005;Paris 05;48.8408725;2.3561278
+0080081V;Lycée General Privé Mabillon;23 Av Martyrs De La Resistance;08201;Sedan;49.7000092;4.930206
+0610702G;Lycée General Privé Marie Immaculee;6 Rue Charles Forget;61500;Sees;48.6067391;0.1716333
+0141172Y;Lycée General Privé Marie-joseph;Avenue De La Marniere;14360;Trouville Sur Mer;49.3710312;0.1146815
+0631847R;Lycée General Privé Massillon;5 Rue Bansac;63037;Clermont Ferrand;45.7786092;3.091566
+0753824Z;Lycée General Privé Massillon;2 Bis Quai Des Celestins;75004;Paris 04;48.851529;2.3607945
+0131328Y;Lycée General Privé Melizan;55 Route Des Camoins;13011;Marseille 11;43.2971335;5.4945421
+0932303Y;Lycée General Privé Merkaz Hatorah Filles;67 Boulevard Du Midi;93340;Le Raincy;48.8945151;2.5251787
+0931464L;Lycée General Privé Merkaz Hatorah Garcons;92-94 Chemin Des Bourdons;93220;Gagny;48.8909288;2.5225507
+0631076C;Lycée General Privé Monanges;7 Rue De Metz;63000;Clermont Ferrand;45.7807747;3.0980538
+0060668U;Lycée General Privé Mont Saint Jean;Avenue Du Chataignier;06600;Antibes;43.5809119;7.1129044
+0940881X;Lycée General Privé Montalembert;28 Boulevard Gambetta;94736;Nogent Sur Marne;48.838178;2.4713696
+0753823Y;Lycée General Privé Moria - Diane Benvenuti;9 11 Rue Lekain;75116;Paris 16;48.8571388;2.277618
+0131319N;Lycée General Privé Nativite (la);8 Rue Jean Andreani La Beauvale;13097;Aix En Provence;43.529742;5.447427
+0622118R;Lycée General Privé Nazareth;84 Rue De Maquetra;62280;St Martin Boulogne;50.7287576;1.6255495
+0440171X;Lycée General Privé Nd De L'abbaye;38 Bis Rue De L Abbaye;44100;Nantes;47.1998104;-1.5962954
+0440166S;Lycée General Privé Nd De Toutes Aides;33 Boulevard Louis Millet;44319;Nantes;47.22695;-1.519285
+0421660B;Lycée General Privé Nd De Valbenoite Le Rond Point;10 Place De L'abbaye;42030;St Etienne;45.4216919;4.3986281
+0440178E;Lycée General Privé Nd D'esperance;15 Rue Du Bois Savary;44615;St Nazaire;47.2747567;-2.2080565
+0490834R;Lycée General Privé Nd D'orveau;;49500;Nyoiseau;47.716305;-0.914938
+0211074D;Lycée General Privé Notre Dame;97 Rue De Talant;21000;Dijon;47.3310729;5.0153828
+0260069J;Lycée General Privé Notre Dame;91 Rue Montplaisir;26000;Valence;44.930458;4.9086288
+0331499A;Lycée General Privé Notre Dame;45 Rue Du Palais Gallien;33000;Bordeaux;44.843183;-0.5815713
+0421025L;Lycée General Privé Notre Dame;9 Rue Cacherat;42190;Charlieu;46.1614459;4.1732205
+0490837U;Lycée General Privé Notre Dame;;49310;La Salle De Vihiers;47.15538;-0.63641
+0720833P;Lycée General Privé Notre Dame;46 Rue De La Magdeleine;72205;La Fleche;47.7006579;-0.0802937
+0690540M;Lycée General Privé Notre Dame De Bellegarde;22 Avenue Gambetta;69250;Neuville Sur Saone;45.870459;4.8419155
+0640122K;Lycée General Privé Notre Dame De Betharram;1 Place Saint Michel;64800;Lestelle Betharram;43.125087;-0.212631
+0131333D;Lycée General Privé Notre Dame De France;132 Rue Breteuil;13253;Marseille 06;43.2846524;5.378104
+0290174G;Lycée General Privé Notre Dame De Kerbertrand;154 Rue De Pont Aven;29391;Quimperle;47.8710014;-3.5663631
+0940877T;Lycée General Privé Notre Dame De La Providence;7 Avenue Gabriel Peri;94307;Vincennes;48.8472148;2.4409415
+0690552A;Lycée General Privé Notre Dame De Mongre;276 Avenue Saint Exupery;69652;Villefranche Sur Saone;45.9909387;4.7116054
+0131341M;Lycée General Privé Notre Dame De Sion;231 Rue Paradis;13006;Marseille 06;43.2823556;5.3815876
+0690519P;Lycée General Privé Notre Dame Des Minimes;65 Rue Des Aqueducs;69322;Lyon 05;45.7594493;4.789366
+0941877E;Lycée General Privé Notre Dame Des Missions;4 Rue Du President Kennedy;94220;Charenton Le Pont;48.8223455;2.404544
+0541318P;Lycée General Privé Notre Dame Saint Sigisbert;19 21 Cours Leopold;54042;Nancy;48.695498;6.1750984
+0090036R;Lycée General Privé Notre-dame;3 Place Des Cordeliers;09100;Pamiers;43.1175009;1.6124603
+0593141U;Lycée General Privé Notre-dame;15 Rue Des Capucins;59308;Valenciennes;50.358955;3.5292575
+0753902J;Lycée General Privé Notre-dame De France;63 Rue De La Sante;75013;Paris 13;48.833057;2.3413569
+0592938Y;Lycée General Privé Notre-dame De Grace;31 Boulevard De La Liberte;59402;Cambrai;50.1705607;3.2302062
+0593109J;Lycée General Privé Notre-dame De La Paix;14 Place Du Concert;59009;Lille;50.6427151;3.0616162
+0622120T;Lycée General Privé Notre-dame De Sion;52 Rue Courteville;62501;St Omer;50.7540117;2.2558843
+0592913W;Lycée General Privé Notre-dame Des Anges;4 Rue Du Bruille;59733;St Amand Les Eaux;50.4487482;3.4257749
+0593102B;Lycée General Privé Notre-dame Des Dunes;60 Rue Du Sud;59140;Dunkerque;51.0341311;2.3836906
+0753946G;Lycée General Privé Notre-dame Des Oiseaux;12 Rue Michel Ange;75016;Paris 16;48.8468059;2.2630177
+0511135G;Lycée General Privé Notre-dame St Victor;13 Rue Jean Chandon Moet;51200;Epernay;49.0412476;3.9571882
+0931970L;Lycée General Privé Ohr Sarah;6 Rue Saint Louis;93500;Pantin;48.8914438;2.418068
+0312356T;Lycée General Privé Ozar Hatorah;33 Rue Jules Dalou;31500;Toulouse;43.6217711;1.4620342
+0754479L;Lycée General Privé Ozar Hatorah;34 Rue Du Moulin Joly;75011;Paris 11;48.8701778;2.3779337
+0941711Z;Lycée General Privé Ozar Hatorah Filles;65 Rue Saint Simon;94000;Creteil;48.7947133;2.4451292
+0941943B;Lycée General Privé Ozar Hatorah Garcons;2 Voie Felix Eboue;94000;Creteil;48.798567;2.4507301
+0753919C;Lycée General Privé Pascal;33 Boulevard Lannes;75116;Paris 16;48.8670712;2.2721096
+0753809H;Lycée General Privé Paul Claudel;118 120 Rue De Grenelle;75007;Paris 07;48.8570302;2.319517
+7200073S;Lycée General Privé Pensionnat Jeanne D Arc;15 Boulevard Benoite Danesi;20297;Bastia;42.6969548;9.4436937
+0940891H;Lycée General Privé Petit Val;12 Avenue Albert Pleuvry;94370;Sucy En Brie;48.7688364;2.5165907
+0650058K;Lycée General Privé Peyramale Saint-joseph;13 Avenue Du Marechal Joffre;65100;Lourdes;43.0948515;-0.0445345
+0690538K;Lycée General Privé Pierre Termier;23 Rue Des Alouettes;69008;Lyon 08;45.7414256;4.8733393
+0740097D;Lycée General Privé Presentation De Marie;10 Rue Monseigneur Paget;74163;St Julien En Genevois;46.1432999;6.0801747
+0596281G;Lycée General Privé Privé De Genech;Rue De La Liberation;59242;Genech;50.5300906;3.2117313
+0261395A;Lycée General Privé Protestant Du Cedre;La Cloitre;26750;Montmiral;45.1702732;5.1414735
+0131323T;Lycée General Privé Provence (de);42 Boulevard Emile Sicard;13272;Marseille 08;43.2702033;5.3844699
+0753885R;Lycée General Privé Rocroy Saint-leon;106 R Du Faubourg Poissonniere;75010;Paris 10;48.8786454;2.3492599
+0911844K;Lycée General Privé Rudolf Steiner;62 Rue De Paris;91370;Verrieres Le Buisson;48.7391608;2.2441374
+0593139S;Lycée General Privé Sacre Coeur;111 Rue De Lille;59334;Tourcoing;50.7229;3.149171
+0511142P;Lycée General Privé Sacre-coeur;86 Rue De Courlancy;51096;Reims;49.2407341;4.0270779
+0592944E;Lycée General Privé Saint Adrien;15 Rue St Jb De La Salle;59653;Villeneuve D Ascq;50.6288481;3.1487012
+0940885B;Lycée General Privé Saint Andre;12 Avenue Leon Gourdault;94600;Choisy Le Roi;48.7634434;2.4057917
+0570211V;Lycée General Privé Saint Antoine;Chemin Des Dames;57370;Phalsbourg;48.7699551;7.2698303
+0570194B;Lycée General Privé Saint Augustin;56 Rue Jean Jacques Kieffer;57231;Bitche;49.044565;7.4272673
+0260063C;Lycée General Privé Saint Bonnet;Saint Bonnet;26330;Chateauneuf De Galaure;45.218168;4.940543
+0021870X;Lycée General Privé Saint Charles;1 Rue Du Brouage;02300;Chauny;49.6147218;3.2121038
+0541319R;Lycée General Privé Saint Dominique;11 Rue Du Manege;54004;Nancy;48.690925;6.1880469
+0602070Z;Lycée General Privé Saint Dominique;5,rue Gerard De Nerval;60128;Mortefontaine;49.1109074;2.6017471
+0301499S;Lycée General Privé Saint Felix;90 Chemin Des Marguilliers;30300;Beaucaire;43.818271;4.636691
+0740104L;Lycée General Privé Saint Francois;19 Rue Fernand David;74100;Ville La Grand;46.2049328;6.2495591
+0592940A;Lycée General Privé Saint Jean;246 Rue Saint Jean;59506;Douai;50.3694281;3.0865161
+0690550Y;Lycée General Privé Saint Joseph;7 Rue Lieutenant Audras;69160;Tassin La Demi Lune;45.7606452;4.7579506
+0740098E;Lycée General Privé Saint Joseph;St Martin Sur Arve;74703;Sallanches;45.912929;6.689997
+0740099F;Lycée General Privé Saint Joseph;Rue Bienheureux Pierre Favre;74230;Thones;45.8834922;6.3272261
+0541313J;Lycée General Privé Saint Leon Ix;20 Rue St Leon;54000;Nancy;48.6891381;6.172546
+0421020F;Lycée General Privé Saint Louis;22 Rue Desire Claude;42030;St Etienne;45.4290047;4.3889574
+0931812P;Lycée General Privé Saint Louis-sainte Clotilde;37 Allee De La Fontaine;93340;Le Raincy;48.8928994;2.5177604
+0690571W;Lycée General Privé Saint Marc;10 Rue Sainte Helene;69287;Lyon 02;45.7550807;4.8288541
+0260066F;Lycée General Privé Saint Maurice;Rue Eugene Blain;26106;Romans Sur Isere;45.046612;5.042497
+0370728R;Lycée General Privé Saint Medard Inst N-d La Riche;30 Rue Delperier;37058;Tours;47.3925342;0.6769264
+0593138R;Lycée General Privé Saint Michel;13 Rue Emile Zola;59730;Solesmes;50.1838289;3.5030972
+0940882Y;Lycée General Privé Saint Michel De Picpus;10 Ter Rue Jeanne D'arc;94165;St Mande;48.8403691;2.418619
+0421023J;Lycée General Privé Saint Paul;15 Rue Bourgneuf;42308;Roanne;46.038829;4.0705744
+0421035X;Lycée General Privé Saint Paul;11 Rue De La Paix;42016;St Etienne;45.4392601;4.385133
+0593114P;Lycée General Privé Saint Paul;25 Bis Rue Colbert;59000;Lille;50.6278376;3.0477018
+0450108Y;Lycée General Privé Saint Paul-bourdon Blanc;4 Rue Neuve St Aignan;45057;Orleans;47.8984756;1.9160429
+0593117T;Lycée General Privé Saint Pierre;18 Rue Saint J-b De La Salle;59000;Lille;50.6284889;3.0392123
+0631032E;Lycée General Privé Saint Pierre;;63120;Courpiere;45.754178;3.538632
+0570216A;Lycée General Privé Saint Pierre Chanel;33 Rue Du Chardon;57109;Thionville;49.3635917;6.1516523
+0541312H;Lycée General Privé Saint Pierre Fourier;14 Rue Des Benedictins;54300;Luneville;48.5874967;6.4975357
+0300083C;Lycée General Privé Saint Stanislas;16 Rue Des Chassaintes;30900;Nimes;43.8371085;4.3525051
+0622106C;Lycée General Privé Saint Vaast-saint Dominique;92 Rue De L'universite;62401;Bethune;50.5243179;2.6481843
+0260071L;Lycée General Privé Saint Victor;3 Rue De La Cecile;26000;Valence;44.9228247;4.8883094
+0381660Y;Lycée General Privé Sainte Cecile;Avenue Marechal Foch;38260;La Cote St Andre;45.3900283;5.2511309
+0772602K;Lycée General Privé Sainte Celine;29 Rue Pierre Marx;77262;La Ferte Sous Jouarre;48.9494327;3.1265198
+0570214Y;Lycée General Privé Sainte Chretienne;20 Rue Ste Croix;57204;Sarreguemines;49.111329;7.0675702
+0593113N;Lycée General Privé Sainte Claire;8 Rue Des Augustins;59800;Lille;50.6343712;3.0691758
+0772290W;Lycée General Privé Sainte Croix;1 Rue Des Jacobins;77160;Provins;48.5629412;3.2939093
+0741443S;Lycée General Privé Sainte Croix Des Neiges;Chef Lieu;74360;Abondance;46.298615;6.789512
+0410676M;Lycée General Privé Sainte Marie;33 Rue Du Bourgneuf;41013;Blois;47.5902427;1.3309065
+0592932S;Lycée General Privé Sainte Marie;31 Rue De L'eglise;59134;Beaucamps Ligny;50.6028831;2.9202882
+0631034G;Lycée General Privé Sainte Marie;3 Place Marinette Menut;63203;Riom;45.8932067;3.1086693
+0771247M;Lycée General Privé Sainte Marie;41 Rue De Chaage;77109;Meaux;48.9658178;2.8806747
+0830095C;Lycée General Privé Sainte Marie;1 Place Germain Loro;83501;La Seyne Sur Mer;43.1015838;5.8770157
+0421022H;Lycée General Privé Sainte Marie La Grand'grange;15 Route Du Coin;42401;St Chamond;45.4714856;4.5209864
+0593106F;Lycée General Privé Sainte Odile;244 Avenue De Dunkerque;59130;Lambersart;50.6368821;3.0253351
+0753916Z;Lycée General Privé Sainte-elisabeth;112 Rue De Lourmel;75015;Paris 15;48.8428294;2.2860109
+0753947H;Lycée General Privé Saint-jean De Passy;72 Rue Raynouard;75016;Paris 16;48.8544406;2.2785455
+0320054K;Lycée General Privé Saint-joseph;1 Rue De L'abbe Tournier;32700;Lectoure;43.933604;0.623621
+0810075S;Lycée General Privé Saint-joseph De L'apparition;10 Boulevard Gambetta;81600;Gaillac;43.8992922;1.8986894
+0753880K;Lycée General Privé Saint-louis;50 Rue De Clichy;75009;Paris 09;48.8802553;2.3290393
+0753933T;Lycée General Privé Saint-louis De Gonzague;12 Rue Benjamin Franklin;75116;Paris 16;48.8595496;2.2854729
+0660768B;Lycée General Privé Saint-louis-de-gonzague;71 Av Du Docteur Schweitzer;66043;Perpignan;42.7182794;2.8790947
+0753897D;Lycée General Privé Saint-michel De Picpus;53 Rue De La Gare De Reuilly;75012;Paris 12;48.8422799;2.3964787
+0753959W;Lycée General Privé Saint-michel Des Batignolles;35 Avenue De Saint Ouen;75017;Paris 17;48.8897216;2.3263756
+0753898E;Lycée General Privé Saint-pierre Fourier;13 Rue De Prague;75012;Paris 12;48.84972;2.3757534
+0753838P;Lycée General Privé Saint-sulpice;68 Rue D'assas;75006;Paris 06;48.8455323;2.3316045
+0820046E;Lycée General Privé Saint-theodard;12 Quai De Verdun;82008;Montauban;44.0217919;1.3480475
+0753850C;Lycée General Privé Saint-thomas D'aquin;44 Rue De Grenelle;75007;Paris 07;48.8541469;2.3267057
+0671618V;Lycée General Privé Seminaire De Jeunes;60 Grand'rue;67360;Walbourg;48.8873256;7.7852027
+0131348V;Lycée General Privé Sevigne;1 Avenue Saint Jerome;13388;Marseille 13;43.3289588;5.4188772
+0500119H;Lycée General Privé Sevigne;15 Bd Girard Desprairies;50400;Granville;48.840647;-1.5908235
+0753598D;Lycée General Privé Sevigne;28 Rue Pierre Nicole;75005;Paris 05;48.8408072;2.3394515
+0753231E;Lycée General Privé Sinai;2-6 Rue Tristan Tzara;75018;Paris 18;48.8946459;2.3657946
+0100047X;Lycée General Privé St Bernard;8 Rue Du Palais De Justice;10041;Troyes;48.296588;4.0706044
+0220110A;Lycée General Privé St Charles;2 Rue Cordiere;22021;St Brieuc;48.5101209;-2.7676223
+0131342N;Lycée General Privé St Charles Camas;21 Rue Du Camas;13392;Marseille 05;43.2984501;5.3926775
+0440176C;Lycée General Privé St Dominique;103 Avenue Cheverny;44807;St Herblain;47.2439329;-1.6042189
+0331488N;Lycée General Privé St Elme;50 Boulevard Deganne;33120;Arcachon;44.6554765;-1.1525843
+0290198H;Lycée General Privé St Esprit;3 Rue Emile Souvestre;29403;Landivisiau;48.5121112;-4.0797023
+0440167T;Lycée General Privé St Felix;27 Rue Du Ballet;44001;Nantes;47.2316606;-1.560045
+0640124M;Lycée General Privé St Francois;1 Rue De Belzunce;64130;Mauleon Licharre;43.2233966;-0.8850869
+0100046W;Lycée General Privé St Francois De Sales;11 Rue General Saussier;10000;Troyes;48.2955822;4.0742386
+0290160S;Lycée General Privé St Francois-notre Dame;1 Rue Des Recollets;29260;Lesneven;48.5708202;-4.3245065
+0560114V;Lycée General Privé St Francois-xavier;3 Rue Thiers;56000;Vannes;47.6538433;-2.7595614
+0050035L;Lycée General Privé St Joseph;2 Rue Des Pins;05000;Gap;44.5629426;6.0769717
+0352341E;Lycée General Privé St Joseph;3 Av. Alphonse Legault;35171;Bruz;48.0262162;-1.7463371
+0490835S;Lycée General Privé St Joseph;50 Rue De La Loire;49620;La Pommeraye;47.3601893;-0.8600084
+0511140M;Lycée General Privé St Joseph;177 Rue Des Capucins;51095;Reims;49.2475072;4.0338348
+0640125N;Lycée General Privé St Joseph;Avenue Des Abbes Dupont;64800;Nay;43.1720298;-0.2670033
+0640126P;Lycée General Privé St Joseph;1 Rue Palou;64400;Oloron Ste Marie;43.188782;-0.6093897
+0840078D;Lycée General Privé St Joseph;105 Rue Duplessis;84205;Carpentras;44.0524315;5.0457201
+0850083D;Lycée General Privé St Joseph;40 Rue Victor Hugo;85035;La Roche Sur Yon;46.6732361;-1.426608
+0440161L;Lycée General Privé St Joseph Du Loquidy;73 Boulevard Michelet;44322;Nantes;47.2388267;-1.5566766
+0440177D;Lycée General Privé St Louis;10 Boulevard Albert 1er;44616;St Nazaire;47.2681941;-2.217917
+0840075A;Lycée General Privé St Louis;Colline St Eutrope;84100;Orange;44.132831;4.808196
+0640115C;Lycée General Privé St Louis Villa Pia;Avenue Marechal Soult;64100;Bayonne;43.4856105;-1.4920018
+0352471W;Lycée General Privé St Magloire;2 Rue Du Chanoine Boursier;35120;Dol De Bretagne;48.5518844;-1.7542833
+0490824E;Lycée General Privé St Martin;5 Cloitre Saint Martin;49100;Angers;47.491878;-0.5478767
+0720842Z;Lycée General Privé St Michel Des Perrais;;72330;Parigne Le Polin;47.8496499;0.10935
+0721549T;Lycée General Privé St Paul-notre Dame;54 Av De La Republique;72406;La Ferte Bernard;48.1880106;0.646915
+0910812N;Lycée General Privé St Pierre;70 Rue De Montgeron;91800;Brunoy;48.6945366;2.4899797
+0080082W;Lycée General Privé St Remi;6 Place Winston Churchill;08000;Charleville Mezieres;49.7718959;4.7217776
+0440163N;Lycée General Privé St Stanislas;2 Rue Saint Stanislas;44009;Nantes;47.2222759;-1.5552563
+0131343P;Lycée General Privé St Thomas D Aquin;23 Rue Dieude;13006;Marseille 06;43.2923588;5.3816291
+0350776D;Lycée General Privé St Vincent-providence;57 Rue De Paris;35064;Rennes;48.1126265;-1.665047
+0060729K;Lycée General Privé Stanislas;25 Av Bieckert;06008;Nice;43.7082584;7.2717928
+0753840S;Lycée General Privé Stanislas;22 R Notre Dame Des Champs;75006;Paris 06;48.8457593;2.3277363
+0831551K;Lycée General Privé Stanislas;2431 Bd Pierre Delli Zotti;83700;St Raphael;43.4356915;6.7948685
+0290338K;Lycée General Privé Ste Anne;20 Rue Lamotte-picquet;29220;Brest;48.390394;-4.486076
+0470065R;Lycée General Privé Ste Catherine;2 Chemin De Velours;47300;Villeneuve Sur Lot;44.4033113;0.7143613
+0131320P;Lycée General Privé Ste Catherine De Sienne;20 Rue Mignet;13100;Aix En Provence;43.5304783;5.4508604
+0450757D;Lycée General Privé Ste Croix St Euverte;28 Rue De L Etelon;45043;Orleans;47.9025554;1.9171035
+0290194D;Lycée General Privé Ste Elisabeth;16 Rue Lamennais;29171;Douarnenez;48.0904856;-4.3274811
+0490831M;Lycée General Privé Ste Marie;43 Rue St Bonaventure;49307;Cholet;47.0594586;-0.8778263
+0331503E;Lycée General Privé Ste Marie Grand Lebrun;164 Av Charles De Gaulle;33021;Bordeaux;44.8502305;-0.6056323
+0131347U;Lycée General Privé Ste Trinite;55 Av Lattre Tassigny Mazargue;13009;Marseille 09;43.2465459;5.4082558
+0754081D;Lycée General Privé Ste Ursule-louise Bettignies;25 Rue Daubigny;75017;Paris 17;48.8858511;2.3075599
+0730759D;Lycée General Privé Talmudique;50 Montee De La Reine Victoria;73105;Aix Les Bains;45.6922535;5.8982884
+0754860A;Lycée General Privé Therese Chappuis;52 Rue Vaneau;75007;Paris 07;48.8510132;2.3191724
+0731040J;Lycée General Privé Tomer Debora;9 Chemin De Saint Pol;73100;Aix Les Bains;45.6854748;5.9200229
+0131327X;Lycée General Privé Tour Sainte (de);Av De Tour Sainte Ste Marthe;13014;Marseille 14;43.3450548;5.3873213
+0490823D;Lycée General Privé Urbain Mongazon;1 Rue Du Colombier;49036;Angers;47.4592971;-0.5294038
+0131360H;Lycée General Privé Viala Lacoste;16 Avenue Gaston Cabrier;13300;Salon De Provence;43.6407537;5.1006531
+0753834K;Lycée General Privé Yabne;29 Avenue Leon Bollee;75013;Paris 13;48.8179276;2.3624561
+0132472S;Lycée General Privé Yavne;44 Bd Barry St Just;13013;Marseille 13;43.3178816;5.4096639
+0330115W;Lycée General Reclus;7 Avenue De Verdun;33220;Ste Foy La Grande;44.8382054;0.2152357
+0640010N;Lycée General Rene Cassin;2 Rue Lasseguette;64100;Bayonne;43.4809394;-1.4805897
+0750682J;Lycée General Rodin;19 Rue Corvisart;75013;Paris 13;48.8325019;2.3470622
+0360002G;Lycée General Rollinat;Rue Du Lycée;36200;Argenton Sur Creuse;0.0;0.0
+0940115P;Lycée General Romain Rolland;17 Rue Lucien Nadaire;94200;Ivry Sur Seine;48.8015275;2.3935972
+0694026B;Lycée General Rosa Parks;13 Rue Pollet;69582;Neuville Sur Saone;45.8763307;4.8483431
+0731507S;Lycée General Saint Exupery;107 Rue Du Pre De Foire;73704;Bourg St Maurice;45.6188058;6.7651855
+0400046H;Lycée General Saint-exupery;Av Du Lycée Cite Scolaire;40160;Parentis En Born;0.0;0.0
+0750658H;Lycée General Saint-louis;44 Boulevard Saint Michel;75006;Paris 06;48.848959;2.341577
+0310041B;Lycée General Saint-sernin;3 Place St Sernin;31070;Toulouse;43.608488;1.440625
+0670081Z;Lycée General Sections Internationales;1 Rue Des Pontonniers;67081;Strasbourg;48.5840973;7.7564613
+0380028Z;Lycée General Stendhal;1 Bis Place Jean Achard;38816;Grenoble;45.189156;5.729899
+0470009E;Lycée General Stendhal;Allees Charles De Gaulle;47190;Aiguillon;44.300538;0.3372336
+0400933X;Lycée General Sud Des Landes;11 Voie Romaine;40231;St Vincent De Tyrosse;43.6667923;-1.2957191
+0332831Y;Lycée General Sud Medoc La Boetie;2 Rue Du Lycée;33320;Le Taillan Medoc;0.0;0.0
+0220099N;Lycée General Technologi Privé St Joseph;Rue Mouexigne;22404;Lamballe;48.4631626;-2.5132448
+0030036Y;Lycée General Theodore De Banville;12 Cours Vincent D'indy;03017;Moulins;46.5683421;3.3269656
+0650025Z;Lycée General Theophile Gautier;15 Rue Abbe Torne;65016;Tarbes;43.2337724;0.0718967
+0080053P;Lycée General Thomas Masaryk;35 Rue Bournizet;08400;Vouziers;49.3974951;4.6979019
+0730013T;Lycée General Vaugelas;;73006;Chambery;46.874236;1.586684
+0750662M;Lycée General Victor Duruy;33 Boulevard Des Invalides;75007;Paris 07;48.852408;2.3147672
+0142059M;Lycée General Victor Hugo;16 Rue Defense Passive;14070;Caen;49.1989716;-0.3668609
+0750648X;Lycée General Victor Hugo;27 Rue De Sevigne;75003;Paris 03;48.8572227;2.3629231
+0590143K;Lycée General Yves Kernanec;91 Avenue Du Docteur Calmette;59700;Marcq En Baroeul;50.6619541;3.0871451
+0580661D;Lycée Horticole Et Rural Privé Du Haut Nivernais;Chemin Des Plantes Froides;58210;Varzy;47.358735;3.388036
+0692681P;Lycée Horticole Privé De Lyon Pressin;81 Chemin De Beaunant;69230;St Genis Laval;45.7087787;4.7789349
+0593040J;Lycée Hotelier Notre-dame De La Providence;Rue Des Glycines;59358;Orchies;50.4806719;3.2420076
+0783441S;Lycée International Montessori;5 Rue De Chaponval;78870;Bailly;48.8359161;2.0710087
+0780015T;Lycée Militaire Militaire;2 Avenue Jean Jaures;78211;St Cyr L Ecole;48.7993005;2.0427068
+0711064W;Lycée Militaire National;3 Rue Gaston Joliet;71404;Autun;46.9497566;4.3057409
+0720896H;Lycée Militaire National Prytanee National Militaire;22 Rue Du College;72208;La Fleche;47.7003277;-0.0743896
+0930863H;Lycée Ministere Justice Maison De La Legion D'honneur;5 Rue De La Legion D'honneur;93206;St Denis;48.9344018;2.3581156
+0290063L;Lycée Naval;Avenue De L'ecole Navale;29240;Brest;48.3741391;-4.5258197
+0150747F;Lycée Polyvalent;Avenue Raymond Cortat;15200;Mauriac;45.2120206;2.3397724
+0772685A;Lycée Polyvalent;19 Rue Du Lion;77260;La Ferte Sous Jouarre;48.9433531;3.1318635
+0810023K;Lycée Polyvalent;41 Rue Victor Hugo;81604;Gaillac;43.9032896;1.9009418
+9720695Y;Lycée Polyvalent Acajou 2;Quartier Acajou;97232;Le Lamentin;0.0;0.0
+0941294W;Lycée Polyvalent Adolphe Cherioux;195 Rue Julian Grimau;94408;Vitry Sur Seine;48.7804644;2.3734255
+0781983G;Lycée Polyvalent Adrienne Bolland;62 64 Bd Devaux;78300;Poissy;48.9327605;2.0499354
+0693734J;Lycée Polyvalent Aiguerande;Chemin Des Sablons;69823;Belleville;46.0990944;4.7468894
+0320023B;Lycée Polyvalent Alain-fournier;Avenue Laplagne;32300;Mirande;43.517913;0.404015
+0831440P;Lycée Polyvalent Albert Camus;560 Rue Henri Giraud;83600;Frejus;43.443681;6.7538626
+0140056K;Lycée Polyvalent Albert Sorel;Avenue Du Labrador;14600;Honfleur;49.398803;0.236287
+0240021T;Lycée Polyvalent Alcide Dusolier;Avenue Jules Ferry;24300;Nontron;45.534132;0.666243
+0040027H;Lycée Polyvalent Alexandra David Neel;17 Avenue General Leclerc;04000;Digne Les Bains;43.0535129;2.2158732
+0330135T;Lycée Polyvalent Alfred Kastler;2 Avenue De L Universite;33402;Talence;44.802672;-0.599039
+0550072E;Lycée Polyvalent Alfred Kastler;1 Rue De Munnerstadt;55700;Stenay;49.487591;5.201371
+0911985N;Lycée Polyvalent Alfred Kastler;Chemin Du Champ De Course;91410;Dourdan;48.5346237;1.9966259
+0932026X;Lycée Polyvalent Alfred Nobel;130 Allee De Gagny;93390;Clichy Sous Bois;48.9014549;2.5470145
+0840021S;Lycée Polyvalent Alphonse Benoit;Cours Victor Hugo;84803;L Isle Sur La Sorgue;43.92001;5.0478131
+0681888H;Lycée Polyvalent Amelie Zurcher;30 Rue Jean Mermoz;68310;Wittelsheim;47.7978887;7.2389802
+0300052U;Lycée Polyvalent Andre Chamson;Boulevard Pasteur;30123;Le Vigan;43.9901973;3.6016316
+0040003G;Lycée Polyvalent Andre Honnorat;1 Rue Honnorat;04400;Barcelonnette;44.3881411;6.6525371
+0595884A;Lycée Polyvalent Andre Lurcat;113 Rue D'haumont;59602;Maubeuge;50.267331;3.944058
+0932123C;Lycée Polyvalent Andre Sabatier;140 Rue De La Republique;93000;Bobigny;48.9072448;2.4295277
+0740051D;Lycée Polyvalent Anna De Noailles;2 Avenue Anna De Noailles;74500;Evian Les Bains;46.3992246;6.5798893
+0210047M;Lycée Polyvalent Anna Judic;3 Rue Du Champ De Foire;21140;Semur En Auxois;47.4953913;4.3360813
+0830042V;Lycée Polyvalent Antoine De Saint Exupery;270 Avenue De Valescure;83700;St Raphael;43.4500551;6.8063128
+0940114N;Lycée Polyvalent Antoine De Saint Exupery;2-4 Rue Henri Matisse;94000;Creteil;48.7825757;2.460862
+0241125T;Lycée Polyvalent Antoine De St Exupery;Les Plantes;24122;Terrasson Lavilledieu;45.131328;1.281654
+0781948U;Lycée Polyvalent Antoine Lavoisier;40 A 60 Bd De La Republique;78440;Porcheville;48.9729155;1.7749472
+0660809W;Lycée Polyvalent Aristide Maillol;73 Avenue Pau Casals;66044;Perpignan;42.7173923;2.884448
+0951787B;Lycée Polyvalent Arthur Rimbaud;99 Av De La Div Leclerc;95140;Garges Les Gonesse;48.9697855;2.3963489
+0930933J;Lycée Polyvalent Assomption;12 Avenue De Verdun;93140;Bondy;48.9059128;2.4892334
+0070004S;Lycée Polyvalent Astier;Quartier Roqua;07205;Aubenas;44.620909;4.389863
+0850016F;Lycée Polyvalent Atlantique;5 Rue Jean Jaures;85402;Lucon;46.4541495;-1.1606375
+0930126G;Lycée Polyvalent Auguste Blanqui;54 Rue Charles Schmidt;93404;St Ouen;48.903085;2.3338269
+0131747D;Lycée Polyvalent Auguste Et Louis Lumiere;Avenue Jules Ferry;13600;La Ciotat;43.1835597;5.6019241
+0340002T;Lycée Polyvalent Auguste Loubatieres;Boulevard Des Hellenes;34304;Agde;43.299603;3.4826301
+0762975W;Lycée Polyvalent Auguste Perret;Place Robert Schuman;76600;Le Havre;49.5138762;0.1616656
+0700009E;Lycée Polyvalent Augustin Cournot;73 Grande Rue;70104;Gray;47.4462233;5.5896275
+9711046K;Lycée Polyvalent Baimbridge 2 (ex Caraibes);Boulevard Des Heros;97001;Les Abymes;16.2498118;-61.5230947
+9741270M;Lycée Polyvalent Bel Air;2 Rue Du Lycée;97441;Ste Suzanne;0.0;0.0
+0310038Y;Lycée Polyvalent Bellevue;135 Route De Narbonne;31031;Toulouse;43.567539;1.4578558
+0450051L;Lycée Polyvalent Benjamin Franklin;21 Bis Rue Eugene Vignat;45010;Orleans;47.9107742;1.9118459
+9730235T;Lycée Polyvalent Bertene Juminer;Route De Saint Maurice;97320;St Laurent Du Maroni;0.0;0.0
+0932048W;Lycée Polyvalent Blaise Cendrars;12 Avenue Leon Jouhaux;93270;Sevran;48.9395961;2.538669
+0360043B;Lycée Polyvalent Blaise Pascal;27 Bd Blaise Pascal;36019;Chateauroux;46.7980558;1.7115349
+0630001J;Lycée Polyvalent Blaise Pascal;23 Rue Blaise Pascal;63600;Ambert;45.5458577;3.7456095
+0680010S;Lycée Polyvalent Blaise Pascal;74 Rue Du Logelbach;68025;Colmar;48.0828085;7.345401
+0760095R;Lycée Polyvalent Blaise Pascal;5 Rue Des Emmurees;76174;Rouen;49.4329729;1.08562
+0772230F;Lycée Polyvalent Blaise Pascal;15 Allee Du Commandant Guesnet;77253;Brie Comte Robert;48.6991093;2.5959103
+0932221J;Lycée Polyvalent Blaise Pascal;18 Rue Marc Vieville;93250;Villemomble;48.8819491;2.5110143
+0771200L;Lycée Polyvalent Blanche De Castille;42 Bis Rue Du Chateau;77300;Fontainebleau;48.4065006;2.703165
+9741087N;Lycée Polyvalent Boisjoly Potier;Rue Ignaz Pleyel;97839;Le Tampon;-21.249148;55.525948
+0070001N;Lycée Polyvalent Boissy D'anglas;50 Avenue Jean Jaures;07104;Annonay;45.2470817;4.67011
+0710001R;Lycée Polyvalent Bonaparte;Place Du Champ De Mars;71403;Autun;46.9501701;4.2987595
+9741051Z;Lycée Polyvalent Bras Panon;51 Chemin Bras Panon;97412;Bras Panon;-21.0023692;55.6829973
+0783533S;Lycée Polyvalent Camille Claudel;21 Rue De La Lyre;78711;Mantes La Ville;48.9641158;1.7162431
+0951710T;Lycée Polyvalent Camille Claudel;Boulevard De L'oise;95490;Vaureal;49.030652;2.0219014
+0590042A;Lycée Polyvalent Camille Desmoulins;47 Rue De La Republique;59360;Le Cateau Cambresis;50.1021507;3.541294
+0950649P;Lycée Polyvalent Camille Pissarro;1 Rue Henri Matisse;95300;Pontoise;49.0560649;2.0891196
+0620056Z;Lycée Polyvalent Carnot;148 Rue Alfred Leroy;62701;Bruay La Buissiere;50.4792475;2.5445456
+9720825P;Lycée Polyvalent Centre Sud;Qua Vaudrancourt;97224;Ducos;0.0;0.0
+0951727L;Lycée Polyvalent Charles Baudelaire;13 Rue Du Grand Tremblay;95470;Fosses;49.101103;2.504328
+9711032V;Lycée Polyvalent Charles Coeffin;Trioncelle;97122;Baie Mahault;0.0;0.0
+0840001V;Lycée Polyvalent Charles De Gaulle (place);104 Place Charles De Gaulle;84405;Apt;47.3408348;-1.5269078
+0380053B;Lycée Polyvalent Charles Gabriel Pravaz;257 Rue Du Pre Saint Martin;38480;Le Pont De Beauvoisin;45.5398188;5.6687632
+0570021N;Lycée Polyvalent Charles Hermite;6 Rue Du Calvaire;57260;Dieuze;48.8088865;6.7245785
+0922277A;Lycée Polyvalent Charles Petiet;65 Bd Gallieni;92391;Villeneuve La Garenne;48.9267832;2.3291268
+0660021P;Lycée Polyvalent Charles Renouvier;Route De Catllar;66500;Prades;42.6265495;2.4199729
+0142120D;Lycée Polyvalent Charles Tellier;Route De Vire;14110;Conde Sur Noireau;48.8514625;-0.5649747
+0890032B;Lycée Polyvalent Chevalier D'eon;2 Place Edmond Jacob;89700;Tonnerre;47.8571627;3.9726577
+0941918Z;Lycée Polyvalent Christophe Colomb;154 Rue De Boissy;94370;Sucy En Brie;48.760231;2.5268783
+0690097F;Lycée Polyvalent Claude Bernard;234 Rue Philippe Heron;69665;Villefranche Sur Saone;45.9892277;4.7122182
+0922427N;Lycée Polyvalent Claude Garamont;69 Rue De L'industrie;92701;Colombes;48.9143256;2.2460678
+0271581X;Lycée Polyvalent Clement Ader;Rue De Rouen;27305;Bernay;49.0936362;0.6014418
+0772342C;Lycée Polyvalent Clement Ader;74-76 Rue Georges Clemenceau;77220;Tournan En Brie;48.7426674;2.7497947
+0910676R;Lycée Polyvalent Clement Ader;37 Bis Rue G.anthonioz De Gaull;91200;Athis Mons;48.7107797;2.3924387
+0050003B;Lycée Polyvalent Climatique D Altitude;2 Rue Marius Chancel;05105;Briancon;44.8987602;6.6403997
+0381630R;Lycée Polyvalent Clinique Du Gresivaudan;10 Avenue Maquis Gresivaudan;38702;La Tronche;45.2048437;5.7474496
+0210006T;Lycée Polyvalent Clos Maire;4 Rue Des Roles;21206;Beaune;47.033372;4.8358441
+0781884Z;Lycée Polyvalent Condorcet;Allee Condorcet;78520;Limay;48.986312;1.751793
+0940122X;Lycée Polyvalent Condorcet;1 Avenue Condorcet;94214;St Maur Des Fosses;48.7954086;2.5120495
+0831563Y;Lycée Polyvalent Costebelle;150 Bd Felix Descroix;83408;Hyeres;43.0980537;6.1232362
+0762920L;Lycée Polyvalent Coubertin;130 Rue Georges Clemenceau;76210;Bolbec;49.5651014;0.4607496
+0750650Z;Lycée Polyvalent D'alembert;22 Sente Des Dorees;75019;Paris 19;48.886887;2.393574
+0932122B;Lycée Polyvalent D'alembert;7 Rue Du Commandant L'herminier;93300;Aubervilliers;48.9173889;2.3867244
+0941474S;Lycée Polyvalent Darius Milhaud;80 Rue Du Professeur Bergonie;94276;Le Kremlin Bicetre;48.8022789;2.3507331
+7200123W;Lycée Polyvalent De Balagne;Av.paul Bisgambiglia;20220;L Ile Rousse;42.63651;8.937046
+9741206T;Lycée Polyvalent De Bois D'olive;;97432;St Pierre;49.1838819;-0.360612
+0400007R;Lycée Polyvalent De Borda;7 Avenue Paul Doumer;40107;Dax;43.7031026;-1.0566084
+9741233X;Lycée Polyvalent De Bras Fusil;Zac De Bras Fusil;97470;St Benoit;0.0;0.0
+9760316P;Lycée Polyvalent De Chirongui;;97620;Chirongui;0.0;0.0
+0150030B;Lycée Polyvalent De Haute Auvergne;20 Rue Marcellin Boudet;15101;St Flour;45.0349214;3.0838309
+0951824S;Lycée Polyvalent De L Hautil;1 Rue Gabriel Faure;95280;Jouy Le Moutier;49.010143;2.025968
+0220013V;Lycée Polyvalent De La Fontaine Des Eaux;La Fontaine Des Eaux;22102;Dinan;48.4614665;-2.0384413
+0772296C;Lycée Polyvalent De La Mare Carree;Rue Du Lycée;77552;Moissy Cramayel;0.0;0.0
+0380049X;Lycée Polyvalent De La Matheysine;3 Rue Lesdiguieres;38350;La Mure;44.9032559;5.785122
+0061987C;Lycée Polyvalent De La Montagne;Quartier Du Clot;06420;Valdeblore;44.0699954;7.1679872
+0950947N;Lycée Polyvalent De La Tourelle;8 Rue Fernand Leger;95200;Sarcelles;48.9928689;2.3828852
+0492224B;Lycée Polyvalent De L'hyrome;71 Rue Nationale;49120;Chemille;47.2166775;-0.7288251
+9760127J;Lycée Polyvalent De Mamoudzou;;97600;Mamoudzou;-12.780556;45.227778
+0641844G;Lycée Polyvalent De Navarre;Rue Jai Alai;64220;St Jean Pied De Port;43.1673642;-1.2319105
+9760229V;Lycée Polyvalent De Petite-terre;;97615;Pamandzi;-12.8004511;45.2829634
+9760182U;Lycée Polyvalent De Sada;;97640;Sada;45.0526421;7.3985249
+9741231V;Lycée Polyvalent De Sainte Anne;;97437;St Benoit;43.6491386;3.8061648
+9741380G;Lycée Polyvalent De St Paul 4;363 Route De Savannah;97411;St Paul;0.0;0.0
+9741186W;Lycée Polyvalent De Trois Bassins;81 Rue Georges Brassens;97426;Les Trois Bassins;-21.1040298;55.3016406
+0130050J;Lycée Polyvalent Denis Diderot;23 Bd Laveran;13388;Marseille 13;43.3251745;5.4146234
+0660004W;Lycée Polyvalent Deodat De Severac;Avenue Des Tilleuls;66403;Ceret;42.4852289;2.7526652
+0890008A;Lycée Polyvalent Des Chaumes;16 Avenue Du Parc Des Chaumes;89206;Avallon;47.4878384;3.9149854
+0740009H;Lycée Polyvalent Des Glieres;2 A Avenue De Verdun;74107;Annemasse;46.1882191;6.2434341
+0210013A;Lycée Polyvalent Desire Nisard;19 Rue De Seine;21402;Chatillon Sur Seine;47.8624852;4.5786136
+0750712S;Lycée Polyvalent Diderot;61 Rue David D'angers;75019;Paris 19;48.8813652;2.3961279
+0750676C;Lycée Polyvalent Dorian;74 Avenue Philippe Auguste;75011;Paris 11;48.8543909;2.3924246
+0390029V;Lycée Polyvalent Du Bois;Rue De Strasbourg;39330;Mouchard;46.974334;5.797075
+7200719U;Lycée Polyvalent Du Fium'orbu;Zi Miggliacciaro;20243;Prunelli Di Fiumorbo;42.010314;9.325429
+0421788R;Lycée Polyvalent Du Forez;10 Route De Civens;42110;Feurs;45.7508946;4.2267226
+0763002A;Lycée Polyvalent Du Golf;Chemin Du Golf;76372;Dieppe;49.918011;1.059239
+0831242Z;Lycée Polyvalent Du Golfe De Saint Tropez;Quartier Saint Martin;83580;Gassin;43.248582;6.569659
+0382863F;Lycée Polyvalent Du Gresivaudan;1 Avenue Du Taillefer;38240;Meylan;45.2098436;5.7821035
+0790031E;Lycée Polyvalent Du Haut Val De Sevre;22 Rue Du Panier Fleuri;79403;St Maixent L Ecole;46.4112715;-0.2201006
+9760270P;Lycée Polyvalent Du Nord;;97630;Acoua;41.8291937;9.4039267
+0781883Y;Lycée Polyvalent Dumont D Urville;2 Avenue De Franche Comte;78310;Maurepas;48.764406;1.9522346
+0762600N;Lycée Polyvalent E. Delamare Deboutteville;5 Rue Andre Bertrand;76440;Forges Les Eaux;49.6182713;1.5372705
+0212045J;Lycée Polyvalent E.j. Marey;5 Rue Du 16e Chasseurs;21206;Beaune;47.0286521;4.8505659
+0251671F;Lycée Polyvalent Edgar Faure;2 Rue Du Dr Sauze;25503;Morteau;47.0622542;6.6074931
+0750671X;Lycée Polyvalent Edgar Quinet;63 Rue Des Martyrs;75009;Paris 09;48.8814303;2.3398192
+0700905D;Lycée Polyvalent Edouard Belin;18 Rue Edouard Belin;70006;Vesoul;47.6337311;6.1648428
+0280021W;Lycée Polyvalent Edouard Branly;29 Avenue Kennedy;28100;Dreux;48.7305183;1.3791302
+0941018W;Lycée Polyvalent Edouard Branly;33 Rue Du Petit Bois;94000;Creteil;48.7840361;2.4667703
+0380091T;Lycée Polyvalent Edouard Herriot;Avenue Edouard Herriot;38506;Voiron;45.3698096;5.5884471
+0380073Y;Lycée Polyvalent Elie Cartan;2 Rue Justin Vernet;38351;La Tour Du Pin;45.5642227;5.4436956
+0750677D;Lycée Polyvalent Elisa Lemonnier;20 Avenue Armand Rousseau;75012;Paris 12;48.8360288;2.4091258
+0711729U;Lycée Polyvalent Emiland Gauthey;20 Rue De L'ancien Collège;71321;Chalon Sur Saone;0.0;0.0
+0170020E;Lycée Polyvalent Emile Combes;Rue Des Cordeliers;17800;Pons;45.578308;-0.550741
+0500002F;Lycée Polyvalent Emile Littre;28 Rue De Verdun;50303;Avranches;48.679946;-1.3603845
+0670089H;Lycée Polyvalent Emile Mathis;1 Rue Du Dauphine;67311;Schiltigheim;48.6337017;7.7455694
+0160022M;Lycée Polyvalent Emile Roux;Avenue Du General De Gaulle;16500;Confolens;46.0082;0.6721341
+0590233H;Lycée Polyvalent Emile Zola;174 Rue De La Baillerie;59391;Wattrelos;50.7073916;3.2319465
+0781819D;Lycée Polyvalent Emilie De Breteuil;3 Rue Du Canal;78053;Montigny Le Bretonneux;48.7819147;2.0421257
+0772688D;Lycée Polyvalent Emilie Du Chatelet;110 Bd Du Champ Du Moulin;77700;Serris;48.8486962;2.7884286
+0430953C;Lycée Polyvalent Emmanuel Chabrier;Le Piny-haut;43201;Yssingeaux;45.128135;4.1201489
+0573491K;Lycée Polyvalent Ernest Cuvelette;Rue De Grenoble;57800;Freyming Merlebach;49.146598;6.787573
+0300027S;Lycée Polyvalent Ernest Hemingway;98 Avenue Jean Jaures;30910;Nimes;43.8260467;4.3533949
+0770940D;Lycée Polyvalent Etienne Bezout;31 Avenue Etienne Dailly;77796;Nemours;48.2606523;2.7152517
+0922443F;Lycée Polyvalent Etienne-jules Marey;154 Rue De Silly;92100;Boulogne Billancourt;48.8320565;2.2340158
+0930119Z;Lycée Polyvalent Eugene Delacroix;4 Rue Du Docteur Schweitzer;93700;Drancy;48.9239128;2.4582228
+0922397F;Lycée Polyvalent Eugene Ionesco;152 Avenue De Verdun;92130;Issy Les Moulineaux;48.8189753;2.2539959
+0951810B;Lycée Polyvalent Eugene Ronceray;5 7 Rue Marcel Langlois;95875;Bezons;48.9218246;2.2168644
+0590168M;Lycée Polyvalent Eugene Thomas;100 Avenue Leo Lagrange;59530;Le Quesnoy;50.2480377;3.6440963
+0932116V;Lycée Polyvalent Eugenie Cotton;58 Avenue Faidherbe;93100;Montreuil;48.866896;2.4372326
+0490018D;Lycée Polyvalent Europe Robert Schuman;39 Avenue De L Europe;49311;Cholet;47.0461537;-0.88536
+0932047V;Lycée Polyvalent Evariste Galois;32 Avenue Montaigne;93165;Noisy Le Grand;48.8444723;2.5393145
+0951748J;Lycée Polyvalent Evariste Galois;14 Bd Leon Blum;95260;Beaumont Sur Oise;49.1454738;2.2950659
+0951811C;Lycée Polyvalent F. Et N. Leger;7 Allee F Et N Leger;95104;Argenteuil;48.947411;2.2489499
+0380092U;Lycée Polyvalent Ferdinand Buisson;21 Boulevard Edgar Kolfer;38506;Voiron;45.3607983;5.5995988
+0760030V;Lycée Polyvalent Ferdinand Buisson;6 Rue Auguste Houzeau;76504;Elbeuf;49.2867649;1.0043623
+0941972H;Lycée Polyvalent Fernand Leger;15 Avenue Henri Barbusse;94200;Ivry Sur Seine;48.8092163;2.3777145
+0910621F;Lycée Polyvalent Francisque Sarcey;Chemin Du Champ De Courses;91410;Dourdan;48.5346237;1.9966259
+0941952L;Lycée Polyvalent Francois Arago;36 Avenue De L'europe;94190;Villeneuve St Georges;48.7311569;2.4586032
+0693566B;Lycée Polyvalent Francois Mansart;Rue Jacquard;69240;Thizy;46.0249019;4.3125338
+0142132S;Lycée Polyvalent Francois Rabelais;Esplanade Rabelais;14200;Herouville St Clair;49.2082446;-0.290863
+0370009J;Lycée Polyvalent Francois Rabelais;28 Quai Danton;37501;Chinon;47.16364;0.2344406
+0693504J;Lycée Polyvalent Francois Rabelais;Chemin Du Dodin;69571;Dardilly;45.8161875;4.7629028
+0850068M;Lycée Polyvalent Francois Rabelais;45 Rue Rabelais;85205;Fontenay Le Comte;46.4679784;-0.8171308
+0752701D;Lycée Polyvalent Francois Truffaut;28 Rue Debelleyme;75003;Paris 03;48.861928;2.3634247
+0911937L;Lycée Polyvalent Francois Truffaut;Rue Georges Pompidou;91072;Bondoufle;48.6186709;2.3897445
+0762976X;Lycée Polyvalent Francoise De Grace;16 Rue De La Vallee;76071;Le Havre;49.4930608;0.1501086
+0771027Y;Lycée Polyvalent Frederic Joliot Curie;168 Rue F Joliot Curie;77196;Dammarie Les Lys;48.518903;2.6489522
+0941301D;Lycée Polyvalent Frederic Mistral;7-9 Rue Frederic Mistral;94260;Fresnes;48.7609403;2.3299967
+0611182D;Lycée Polyvalent Gabriel;7 Rue Saint Exupery;61204;Argentan;48.7533879;-0.0075234
+0070029U;Lycée Polyvalent Gabriel Faure;Place Mallarme;07301;Tournon Sur Rhone;45.0672005;4.8344977
+0762911B;Lycée Polyvalent Galilee;461 Route De Belbeuf;76520;Franqueville St Pierre;49.3983018;1.1607919
+0400002K;Lycée Polyvalent Gaston Crampe;Av Droits De L Homme Et Citoyen;40801;Aire Sur L Adour;43.700062;-0.262294
+0301654K;Lycée Polyvalent Genevieve De Gaulle-anthonioz;43 Rue Du Moulin;30540;Milhaud;43.7864243;4.309575
+0031044U;Lycée Polyvalent Genevieve Vincent;15 Bd Du General De Gaulle;03600;Commentry;46.2911311;2.7336414
+0910622G;Lycée Polyvalent Geoffroy-st-hilaire;2-6 Av Geoffroy St Hilaire;91153;Etampes;48.4351205;2.1463921
+0360019A;Lycée Polyvalent George Sand;25 Avenue George Sand;36400;La Chatre;46.5824064;1.9815737
+0951788C;Lycée Polyvalent George Sand;Avenue Du Lycée;95331;Domont;0.0;0.0
+0763237F;Lycée Polyvalent Georges Baptiste;41 Rte De Duclair;76380;Canteleu;49.4431486;1.0186583
+0950666H;Lycée Polyvalent Georges Braque;21 Rue Victor Puiseux;95100;Argenteuil;48.950485;2.2505933
+0420027B;Lycée Polyvalent Georges Brassens;8 Rue Grange Burlat;42800;Rive De Gier;45.5325721;4.60488
+0762601P;Lycée Polyvalent Georges Brassens;27 Boulevard Gustave Eiffel;76270;Neufchatel En Bray;49.7379225;1.4389657
+0911828T;Lycée Polyvalent Georges Brassens;8 R Georges Brassens;91080;Courcouronnes;48.626643;2.4205622
+0932260B;Lycée Polyvalent Georges Brassens;Rue Des Bancs Publics;93420;Villepinte;48.9554743;2.5328795
+0940743X;Lycée Polyvalent Georges Brassens;12 Avenue Le Foll;94290;Villeneuve Le Roi;48.7301773;2.4276725
+9740053P;Lycée Polyvalent Georges Brassens;16 Avenue Georges Brassens;97493;St Denis;44.9366126;-0.4864077
+0772339Z;Lycée Polyvalent Georges Clemenceau;5 Rue Georges Clemenceau;77430;Champagne Sur Seine;48.3990891;2.7972818
+6200043C;Lycée Polyvalent Georges Clemenceau;Bd Jean Nicolai;20100;Sartene;41.621822;8.97472
+0700018P;Lycée Polyvalent Georges Colomb;1 Rue G Colomb;70204;Lure;47.6879696;6.4987354
+0672614C;Lycée Polyvalent Georges Imbert;2 Rue Vincent D'indy;67261;Sarre Union;48.9359751;7.0886788
+0341921D;Lycée Polyvalent Georges Pompidou;351 Av Mal De Lattre Tassigny;34172;Castelnau Le Lez;43.6469391;3.9199801
+0771940R;Lycée Polyvalent Gerard De Nerval;89 Cours Des Roches Noisiel;77441;Champs Sur Marne;48.8427362;2.6154529
+9711012Y;Lycée Polyvalent Grand Bourg;Rue De La Savane;97112;Grand Bourg;0.0;0.0
+0940742W;Lycée Polyvalent Guillaume Bude;2 Voie Georges Pompidou;94456;Limeil Brevannes;48.7441012;2.5110985
+0740013M;Lycée Polyvalent Guillaume Fichet;219 Rue De Pressy;74136;Bonneville;46.0793702;6.411561
+0754476H;Lycée Polyvalent Guillaume Tirel;237 Boulevard Raspail;75014;Paris 14;48.8387393;2.3308641
+0330028B;Lycée Polyvalent Gustave Eiffel;143 Crs Marne;33074;Bordeaux;44.837789;-0.57918
+0922398G;Lycée Polyvalent Gustave Eiffel;78 Avenue Du Pdt Pompidou;92500;Rueil Malmaison;48.8715605;2.1940934
+0940111K;Lycée Polyvalent Gustave Eiffel;61 Avenue Du President Wilson;94235;Cachan;48.789111;2.325304
+0620256S;Lycée Polyvalent Guy Mollet;57 Rue Bocquet Flochel;62022;Arras;50.2800007;2.7946232
+0380014J;Lycée Polyvalent Hector Berlioz;Place De L Europe;38260;La Cote St Andre;45.2002269;6.6710747
+0772277G;Lycée Polyvalent Henri Becquerel;1 Bd Henri Rousselle;77370;Nangis;48.5518365;3.0076186
+0261397C;Lycée Polyvalent Henri Laurens;Quartier Des Rioux;26241;St Vallier;0.0;0.0
+0912251C;Lycée Polyvalent Henri Poincaré;36 Rue Leon Bourgeois;91122;Palaiseau;48.702946;2.2370181
+0932120Z;Lycée Polyvalent Henri Sellier;73 Avenue Du Colonel Fabien;93190;Livry Gargan;48.9186434;2.5162221
+0710042K;Lycée Polyvalent Henri Vincenot;Montee Saint Claude;71500;Louhans;46.623382;5.214177
+0550008K;Lycée Polyvalent Henri Vogt;12 Rue Andre Malraux;55205;Commercy;48.7548233;5.5858673
+0711422K;Lycée Polyvalent Hilaire De Chardonnet;1 Rue Henri Dunant;71321;Chalon Sur Saone;46.8024283;4.8663853
+0210018F;Lycée Polyvalent Hippolyte Fontaine;20 Boulevard Voltaire;21033;Dijon;47.3181031;5.0511309
+0771996B;Lycée Polyvalent Honore De Balzac;Avenue Paul Langevin;77290;Mitry Mory;48.9724391;2.6505336
+0022044L;Lycée Polyvalent Hotelier;Passage Le Corbusier;02200;Soissons;49.376636;3.32342
+0622807P;Lycée Polyvalent Hotelier;Avenue Du Chateau;62520;Le Touquet Paris Plage;50.516361;1.602514
+9711066G;Lycée Polyvalent Hotelier Du Gosier;Saint-felix;97190;Le Gosier;0.0;0.0
+0881664F;Lycée Polyvalent Hotelier J-b Simeon Chardin;32 Boulevard D'alsace;88407;Gerardmer;48.0732813;6.8764418
+9720823M;Lycée Polyvalent Hotelier Nord Caraibes;Bellefontaine;97222;Bellefontaine;46.558079;6.068529
+0641823J;Lycée Polyvalent Hotelier Tourisme;2 Rue F Jammes;64204;Biarritz;43.4623738;-1.5522272
+0390033Z;Lycée Polyvalent Hyacinthe Friant;2 Rue Hyacinthe Friant;39801;Poligny;0.0;0.0
+9710981P;Lycée Polyvalent Iles Du Nord;Marigot;97052;St Martin;18.0664178;-63.0872351
+9830003L;Lycée Polyvalent J.garnier;65 Avenue James Cook;98849;Noumea;-22.2686289;166.4242812
+0350048M;Lycée Polyvalent Jacques Cartier;31 Rue De La Balue;35403;St Malo;48.6280158;-2.0040075
+0390013C;Lycée Polyvalent Jacques Duhamel;Rue Charles Laurent Thouverey;39107;Dole;47.09534;5.49081
+0750428H;Lycée Polyvalent Jacques Monod;12 Rue Victor Cousin;75005;Paris 05;48.8476683;2.3423041
+0301778V;Lycée Polyvalent Jacques Prevert;;30380;St Christol Les Ales;44.083566;4.077927
+0110019L;Lycée Polyvalent Jacques Ruffie;Esplanade Francois Mitterand;11303;Limoux;43.0506812;2.2194271
+0542293Z;Lycée Polyvalent Jacques-marie Boutet De Monvel;4 Rue Boutet De Monvel;54300;Luneville;48.5986914;6.4925295
+0022042J;Lycée Polyvalent Jean Bouin;Rue Gaston Bachelard;02100;St Quentin;49.854359;3.30866
+0750708M;Lycée Polyvalent Jean Drouant;20 Rue Mederic;75017;Paris 17;48.880936;2.3040609
+0110012D;Lycée Polyvalent Jean Durand;Avenue Docteur Laennec;11493;Castelnaudary;43.3068767;1.9402732
+0230051F;Lycée Polyvalent Jean Favard;27 Rte De Courtille;23003;Gueret;46.168088;1.8569052
+0840015K;Lycée Polyvalent Jean Henri Fabre;Av Du Mont Ventoux;84208;Carpentras;44.0569097;5.0619963
+9740979W;Lycée Polyvalent Jean Hinglo;2 Rue Des Sans Soucis;97825;Le Port;43.0154964;3.0609934
+0170135E;Lycée Polyvalent Jean Hyppolite;Place Saint Exupery;17502;Jonzac;45.4414119;-0.4322947
+0680001G;Lycée Polyvalent Jean Jacques Henner;20 Rue De Hirtzbach;68130;Altkirch;47.6153682;7.234954
+0120025M;Lycée Polyvalent Jean Jaures;Avenue Jean Jaures;12401;St Affrique;43.9510528;2.8953583
+0342066L;Lycée Polyvalent Jean Jaures;Avenue Saint Sauveur;34980;St Clement De Riviere;43.7098148;3.8323688
+0921166T;Lycée Polyvalent Jean Jaures;280 Avenue Jean Jaures;92291;Chatenay Malabry;48.7677839;2.254895
+0930121B;Lycée Polyvalent Jean Jaures;1 Rue Dombasle;93105;Montreuil;48.8636396;2.4491594
+0941974K;Lycée Polyvalent Jean Jaures;9 Avenue Jean Jaures;94220;Charenton Le Pont;48.8247353;2.4126023
+0950641F;Lycée Polyvalent Jean Jaures;25 Rue Charles Lecoq;95100;Argenteuil;48.9403527;2.225817
+0660011D;Lycée Polyvalent Jean Lurcat;25 Avenue Albert Camus;66000;Perpignan;42.6918634;2.9063835
+0753268V;Lycée Polyvalent Jean Lurcat;48 Avenue Des Gobelins;75013;Paris 13;48.8342909;2.3533682
+0561698S;Lycée Polyvalent Jean Mace;Rue Jean-paul Sartre;56601;Lanester;47.765248;-3.344219
+0340042L;Lycée Polyvalent Jean Mermoz;717 Avenue Jean Mermoz;34060;Montpellier;43.6109837;3.8901026
+0680066C;Lycée Polyvalent Jean Mermoz;53 Rue Du Docteur Hurst;68300;St Louis;47.5793744;7.558521
+0133288D;Lycée Polyvalent Jean Monnet;Bd Rhin Et Danube;13127;Vitrolles;43.424732;5.2717125
+0160020K;Lycée Polyvalent Jean Monnet;66 Boulevard De Chatenay;16100;Cognac;45.701557;-0.3180834
+0611148S;Lycée Polyvalent Jean Monnet;2 Rue Jean Monnet;61400;Mortagne Au Perche;48.5124382;0.5531263
+0781839A;Lycée Polyvalent Jean Monnet;Place De L Europe;78940;La Queue Les Yvelines;48.8022064;1.7827586
+0851400K;Lycée Polyvalent Jean Monnet;57 Rue De La Demoiselle;85500;Les Herbiers;46.8663273;-1.0202618
+0951722F;Lycée Polyvalent Jean Monnet;Rue Jean Monnet;95131;Franconville;48.9919298;2.2151614
+0271585B;Lycée Polyvalent Jean Moulin;Rue Du Marechal Foch;27700;Les Andelys;49.2492308;1.4406574
+0492123S;Lycée Polyvalent Jean Moulin;1 Place Jean Moulin;49017;Angers;47.491678;-0.551624
+0932118X;Lycée Polyvalent Jean Moulin;2 Avenue Charles De Gaulle;93150;Le Blanc Mesnil;48.9433914;2.4685539
+0130053M;Lycée Polyvalent Jean Perrin;74 Rue Verdillon;13395;Marseille 10;43.2746696;5.4264973
+0951104J;Lycée Polyvalent Jean Perrin;2 Rue Des Egalisses;95310;St Ouen L Aumone;49.0572645;2.1326835
+0910975R;Lycée Polyvalent Jean Pierre Timbaud;4 Rue Henri Douard;91220;Bretigny Sur Orge;48.6109653;2.3129067
+0330109P;Lycée Polyvalent Jean Renou;2 Rue Jean Renou;33192;La Reole;44.5815968;-0.0413168
+0670084C;Lycée Polyvalent Jean Rostand;18 Boulevard De La Victoire;67084;Strasbourg;48.5829461;7.7626845
+0782540M;Lycée Polyvalent Jean Rostand;66 Rue Fernand Bodet;78200;Mantes La Jolie;48.9979639;1.696681
+0931584S;Lycée Polyvalent Jean Rostand;8 Rue Pierre Audat;93420;Villepinte;48.959629;2.545226
+0511951U;Lycée Polyvalent Jean Talon;105 Avenue Daniel Simonnot;51037;Chalons En Champagne;48.9551578;4.3400748
+0450050K;Lycée Polyvalent Jean Zay;2 Rue Ferdinand Buisson;45044;Orleans;47.9067083;1.9127555
+0930833A;Lycée Polyvalent Jean Zay;Avenue Du Marechal Juin;93604;Aulnay Sous Bois;48.93782;2.5057694
+0550025D;Lycée Polyvalent Jean-auguste Margueritte;13 Place Commandant Galland;55107;Verdun;49.1563087;5.3904397
+0271634E;Lycée Polyvalent Jean-baptiste Decretot;7 Rue De La Gare;27400;Louviers;49.2162353;1.1756759
+0782557F;Lycée Polyvalent Jean-baptiste Poquelin;72 Rue Leon Desoyer;78101;St Germain En Laye;48.8995389;2.0856482
+0671832C;Lycée Polyvalent Jean-baptiste Schwilgue;1 Rue Du Stade;67604;Selestat;48.2504987;7.3971004
+0880036L;Lycée Polyvalent Jean-baptiste Vuillaume;Avenue Graillet;88503;Mirecourt;48.298504;6.132288
+0341794R;Lycée Polyvalent Jean-francois Champollion;Avenue De Figuieres;34973;Lattes;43.5876661;3.9306222
+0942125Z;Lycée Polyvalent Jean-jacques Rousseau;15 Rue Lebrun;94400;Vitry Sur Seine;48.7828517;2.3819482
+0950650R;Lycée Polyvalent Jean-jacques Rousseau;2 Rue J J Rousseau;95200;Sarcelles;48.9938677;2.3821641
+0280044W;Lycée Polyvalent Jehan De Beauce;20 Rue Du Commandant Chesne;28000;Chartres;48.4586432;1.4735645
+0772276F;Lycée Polyvalent Jehan De Chelles;47 Rue Des Cites;77649;Chelles;48.888756;2.6115193
+0420008F;Lycée Polyvalent Jeremie De La Rue;Route De Saint Bonnet;42190;Charlieu;46.167128;4.1718563
+0590018Z;Lycée Polyvalent Jesse De Forest;15 Avenue Du Pont Rouge;59363;Avesnes Sur Helpe;50.121543;3.9255446
+0941930M;Lycée Polyvalent Johannes Gutenberg;16-18 Rue De Saussure;94000;Creteil;48.7697246;2.4709831
+0890005X;Lycée Polyvalent Joseph Fourier;Rue R Poincare;89010;Auxerre;47.79215;3.552682
+0312759F;Lycée Polyvalent Joseph Gallieni;79 Route D'espagne;31100;Toulouse;43.573033;1.4243835
+0590044C;Lycée Polyvalent Joseph Marie Jacquard;4 Avenue Jean Moulin;59544;Caudry;50.1263865;3.4040061
+9720726G;Lycée Polyvalent Joseph Pernock;Le Lorrain;97214;Le Lorrain;0.0;0.0
+0681839E;Lycée Polyvalent Joseph Storck (hotelier);Rue Jules Ferry;68504;Guebwiller;47.901203;7.228875
+0340028W;Lycée Polyvalent Joseph Vallot;5 Boulevard Gambetta;34702;Lodeve;43.7313632;3.3171287
+9720725F;Lycée Polyvalent Joseph Zobel;Quartier Thoraille;97215;Riviere Salee;0.0;0.0
+0142131R;Lycée Polyvalent Jules Dumont D'urville;73 Rue De Lebisey;14070;Caen;49.1965539;-0.3548514
+0782565P;Lycée Polyvalent Jules Ferry;29 Rue Du Marechal Joffre;78000;Versailles;48.7942841;2.1208585
+0340040J;Lycée Polyvalent Jules Guesde;110 Avenue De Lodeve;34060;Montpellier;43.6106065;3.8486836
+0250011B;Lycée Polyvalent Jules Haag;1 Rue Labbe;25041;Besancon;47.239422;6.013522
+0951756T;Lycée Polyvalent Jules Verne;1 Rue Michel Strogoff;95800;Cergy;49.0528165;2.009558
+0710018J;Lycée Polyvalent Julien Wittmer;13 Rue De Champagny;71120;Charolles;46.4353344;4.2731882
+0911492C;Lycée Polyvalent L Essouriau;Avenue De Dordogne;91940;Les Ulis;48.6767778;2.1657625
+0710026T;Lycée Polyvalent L?on Blum;72 Rue Jean Jaures;71200;Le Creusot;46.8049544;4.4140633
+0430003V;Lycée Polyvalent La Fayette;Plateau St Laurent;43103;Brioude;45.295564;3.386482
+9720771F;Lycée Polyvalent La Jetee;Route De La Jetee;97240;Le Francois;46.21037;6.15433
+0382838D;Lycée Polyvalent La Pleiade;Rue Du Repos;38232;Pont De Cheruy;45.7529419;5.1711019
+9741173G;Lycée Polyvalent La Possession;;97419;La Possession;44.6915692;-0.8600616
+0380063M;Lycée Polyvalent La Saulaie;La Saulaie;38162;St Marcellin;45.1599563;5.3196875
+0772295B;Lycée Polyvalent La Tour Des Dames;Rue De Vilpres;77540;Rozay En Brie;48.6791913;2.9646581
+0940113M;Lycée Polyvalent Langevin-wallon;126 Avenue Roger Salengro;94507;Champigny Sur Marne;48.8171426;2.4974266
+0772340A;Lycée Polyvalent L'arche Guedon;2 Passage De L'arche Guedon;77200;Torcy;48.8499971;2.6359863
+0681768C;Lycée Polyvalent Lavoisier;42 Rue Lavoisier;68200;Mulhouse;47.7536602;7.3250939
+0620008X;Lycée Polyvalent Lazare Carnot;21 Boulevard Carnot;62022;Arras;50.2875817;2.77396
+0210019G;Lycée Polyvalent Le Castel;22 Rue Daubenton;21033;Dijon;47.3131033;5.0362136
+0071397F;Lycée Polyvalent Le Cheylard;Quartier Plaisance;07160;Le Cheylard;44.9113929;4.42675
+0762964J;Lycée Polyvalent Le Corbusier;Rue De L'universite;76800;St Etienne Du Rouvray;49.3870486;1.0650878
+0721094Y;Lycée Polyvalent Le Mans Sud;128 Rue Henri Champion;72058;Le Mans;47.9831673;0.2336784
+0831646N;Lycée Polyvalent Le Muy;Avenue De Vaugrenier;83490;Le Muy;43.4761296;6.5373255
+0932282A;Lycée Polyvalent Leo Lagrange;2 Rue Compagnon;93140;Bondy;48.9064253;2.4914471
+0941413A;Lycée Polyvalent Leon Blum;5 Rue Jean Gabin;94000;Creteil;48.779746;2.4450351
+0061478Z;Lycée Polyvalent Leonard De Vinci;214 Rue Jean Joannon;06633;Antibes;43.6049397;7.0729317
+0624141P;Lycée Polyvalent Leonard De Vinci;Rue Pasteur Martin Luther King;62228;Calais;50.9537082;1.8876944
+0754475G;Lycée Polyvalent Leonard De Vinci;20 Rue Bourseul;75015;Paris 15;48.8382608;2.3020551
+0782556E;Lycée Polyvalent Leonard De Vinci;2 Bd Hector Berlioz;78100;St Germain En Laye;48.8914922;2.0638407
+0851390Z;Lycée Polyvalent Leonard De Vinci;Rue Du Fromenteau;85603;Montaigu;46.971961;-1.303299
+0911946W;Lycée Polyvalent Leonard De Vinci;1 Place Leonard De Vinci;91240;St Michel Sur Orge;48.6177611;2.3315865
+0921230M;Lycée Polyvalent Leonard De Vinci;4 Av Georges Pompidou;92304;Levallois Perret;48.8973095;2.2778034
+0781951X;Lycée Polyvalent Leopold Sedar Senghor;Place Pierre Beregovoy;78200;Magnanville;48.96488;1.683672
+9740045F;Lycée Polyvalent Les Avirons;;97425;Les Avirons;-21.240637;55.334205
+0441552Y;Lycée Polyvalent Les Bourdonnieres;Rue De La Perriere;44265;Nantes;47.186446;-1.517926
+0270029K;Lycée Polyvalent Les Fontenelles;Chemin Les Fontenelles;27406;Louviers;49.209294;1.156647
+0332832Z;Lycée Polyvalent Les Iris;13 Rue Saint Cricq;33305;Lormont;44.8700168;-0.5322274
+0040533H;Lycée Polyvalent Les Iscles;116 Bd Regis Ryckebush;04100;Manosque;43.831469;5.7990491
+0211986V;Lycée Polyvalent Les Marcs D'or;24 R Du Fort De La Motte Giron;21006;Dijon;47.3158772;4.9969497
+0781860Y;Lycée Polyvalent Les Pierres Vives;1 Rue Des Alouettes;78420;Carrieres Sur Seine;48.913654;2.1725297
+0932267J;Lycée Polyvalent Liberte;27 A 39 Rue De La Liberte;93230;Romainville;48.8829412;2.4294153
+9740054R;Lycée Polyvalent Lislet Geoffroy;Cite Scolaire Du Butor;97491;St Denis;48.936181;2.357443
+0380008C;Lycée Polyvalent L'oiselet;Rue Marion Prolongee;38317;Bourgoin Jallieu;45.597108;5.27212
+0701035V;Lycée Polyvalent Louis Aragon;Rue Pierre Mendes France;70400;Hericourt;47.574053;6.761598
+0691644M;Lycée Polyvalent Louis Armand;Avenue Du Beaujolais;69651;Villefranche Sur Saone;45.9990616;4.7063263
+0751708Z;Lycée Polyvalent Louis Armand;319 321 Rue Lecourbe;75015;Paris 15;48.837404;2.2850709
+0951974E;Lycée Polyvalent Louis Armand;32 Rue Stephane Proust;95600;Eaubonne;48.9970012;2.288332
+0170051N;Lycée Polyvalent Louis Audouin Dubreuil;Rue Philippe Jeannet;17415;St Jean D Angely;45.951385;-0.526721
+0782549X;Lycée Polyvalent Louis Bascan;5 Av Du Gal Leclerc;78513;Rambouillet;48.6410608;1.8245937
+0891199V;Lycée Polyvalent Louis Davier;Avenue Moliere;89306;Joigny;47.982086;3.414577
+0573227Y;Lycée Polyvalent Louis De Cormontaigne;12 Place Cormontaigne;57010;Metz;49.1243217;6.1633963
+0340030Y;Lycée Polyvalent Louis Feuillade;49 Rue Romain Rolland;34402;Lunel;43.6697437;4.1223872
+0951763A;Lycée Polyvalent Louis Jouvet;26 Rue De St Prix;95152;Taverny;49.0058062;2.2732907
+0740006E;Lycée Polyvalent Louis Lachenal-argonay;335 Route Champ Farcon;74372;Argonay;45.938068;6.132849
+0140052F;Lycée Polyvalent Louis Liard;15 Rue Saint Jean;14700;Falaise;48.8949844;-0.1949088
+0672615D;Lycée Polyvalent Louis Marchal;2 Route De La Hardt;67125;Molsheim;48.5369628;7.5012312
+0922464D;Lycée Polyvalent Louise Michel;11 Boulevard Du Midi;92000;Nanterre;48.8897402;2.193836
+0680068E;Lycée Polyvalent Louise Weiss;Route Du Stade;68160;Ste Marie Aux Mines;48.236375;7.1681767
+0781950W;Lycée Polyvalent Louise Weiss;201 Avenue Du Gal De Gaulle;78260;Acheres;48.9707469;2.0741265
+0750463W;Lycée Polyvalent Lucas De Nehou;4 Rue Des Feuillantines;75005;Paris 05;48.8412988;2.3435246
+0932117W;Lycée Polyvalent Lucie Aubrac;51 Rue Victor Hugo;93500;Pantin;48.8937132;2.4096485
+0701078S;Lycée Polyvalent Lumiere;33 Ter Rue Grammont;70306;Luxeuil Les Bains;47.8248597;6.377976
+9840023C;Lycée Polyvalent Lycée Polyvalent De Taaone;Pirae;98716;Pirae;-17.5353835;-149.5478833
+9840407V;Lycée Polyvalent Lycée Tertiaire De Pirae;Pirae;98716;Pirae;-17.5353835;-149.5478833
+0570094T;Lycée Polyvalent Mangin;34 Rue Gambetta;57401;Sarrebourg;48.7391327;7.0583227
+0342091N;Lycée Polyvalent Marc Bloch;;34500;Beziers;43.344233;3.215795
+0070003R;Lycée Polyvalent Marcel Gimond;Boulevard De L'europe;07205;Aubenas;44.619386;4.3808809
+0672806L;Lycée Polyvalent Marcel Rudloff;Avenue Francois Mitterrand;67200;Strasbourg;48.5862403;7.6958226
+0320015T;Lycée Polyvalent Marechal Lannes;Cite Scolaire 1 Pl Brossolette;32700;Lectoure;43.933604;0.623621
+0623902E;Lycée Polyvalent Marguerite Yourcenar;68 Rue Antoine De Saint Exupery;62660;Beuvry;50.51858;2.6641857
+0672677W;Lycée Polyvalent Marguerite Yourcenar;Rue Victor Schoelcher;67152;Erstein;48.4227904;7.6499811
+0911945V;Lycée Polyvalent Marguerite Yourcenar;62 Rue Des Edouets;91423;Morangis;48.7081491;2.3259431
+0342225J;Lycée Polyvalent Marianne;;34000;Montpellier;43.6047275;3.9011747
+0911962N;Lycée Polyvalent Marie Laurencin;51 Rue Paul Cezanne;91542;Mennecy;48.5616978;2.4440346
+0312746S;Lycée Polyvalent Marie Louise Dissard Francoise;5 Boulevard Alain Savary;31170;Tournefeuille;43.5423773;1.3405506
+0730003G;Lycée Polyvalent Marlioz;Chemin Du Lycée;73102;Aix Les Bains;0.0;0.0
+0754530S;Lycée Polyvalent Martin Nadaud;23 Rue De La Bidassoa;75020;Paris 20;48.8662003;2.393588
+0681882B;Lycée Polyvalent Martin Schongauer;25 Rue Voltaire;68000;Colmar;48.0696475;7.3511399
+0941951K;Lycée Polyvalent Marx Dormoy;500 Rue Du Professeur Milliez;94507;Champigny Sur Marne;48.8154449;2.5349215
+0875023M;Lycée Polyvalent Maryse Bastie;Rue Louis Amstrong;87000;Limoges;45.8162774;1.2623048
+0710010A;Lycée Polyvalent Mathias;3 Place Mathias;71321;Chalon Sur Saone;46.7865371;4.8603337
+0580761M;Lycée Polyvalent Maurice Genevoix;51 Route D'avril Sur Loire;58300;Decize;46.8284621;3.441725
+0831559U;Lycée Polyvalent Maurice Janetti;Quartier Mirade;83470;St Maximin La Ste Baume;43.452497;5.864004
+0932030B;Lycée Polyvalent Maurice Utrillo;152 Rue Jean Durand;93240;Stains;48.9524279;2.3691348
+0750502N;Lycée Polyvalent Maximilien Vox-art-dessin;5 Rue Madame;75006;Paris 06;48.8520258;2.3319993
+0133406G;Lycée Polyvalent Mediterranee (de La );Qua La Plaine Av Mediterranee;13600;La Ciotat;43.190758;5.611854
+9730309Y;Lycée Polyvalent Melkior Garre;Route De Montabo;97305;Cayenne;4.9347066;-52.2954502
+0610006A;Lycée Polyvalent Mezeray;6 Place Robert Dugue;61200;Argentan;48.7448047;-0.0151903
+0760032X;Lycée Polyvalent Michel Anguier;41 Bis Rue De La Republique;76260;Eu;50.0482297;1.4220286
+0740027C;Lycée Polyvalent Mont Blanc Rene Dayve;Rue Rene Dayve;74190;Passy;45.907806;6.708722
+0922249V;Lycée Polyvalent Montesquieu;21 Rue Du Capitaine Facq;92350;Le Plessis Robinson;48.7776941;2.2637716
+0920136Y;Lycée Polyvalent Newton-enrea;1 Place Jules Verne;92110;Clichy;48.9087541;2.3073035
+0442094M;Lycée Polyvalent Nicolas Appert;24 Avenue De La Choliere;44702;Orvault;47.251063;-1.6067232
+9720692V;Lycée Polyvalent Nord Atlantique;Boulevard De La Voie Lactee;97230;Ste Marie;14.7787858;-60.9900416
+9711082Z;Lycée Polyvalent Nord Grande Terre;Port Louis;97117;Port Louis;47.7107928;-3.3642883
+0142133T;Lycée Polyvalent P. S. De Laplace;130 Rue De La Delivrande;14075;Caen;49.1961605;-0.3600384
+0910727W;Lycée Polyvalent Parc De Vilgenis;80 Rue De Versailles;91305;Massy;48.7325208;2.257078
+0360005K;Lycée Polyvalent Pasteur;12 Boulevard Fr Mitterrand;36300;Le Blanc;48.9090111;2.4475893
+0040023D;Lycée Polyvalent Paul Arene;13 Avenue Du Stade;04203;Sisteron;44.1830487;5.9474019
+0030026M;Lycée Polyvalent Paul Constans;Rue Christophe Thivrier;03107;Montlucon;46.3359499;2.577509
+0940119U;Lycée Polyvalent Paul Doumer;2 Rue Paul Doumer;94170;Le Perreux Sur Marne;48.8336998;2.5088853
+0870040W;Lycée Polyvalent Paul Eluard;Bellvue De Glane;87205;St Junien;45.8951323;0.8918402
+0930125F;Lycée Polyvalent Paul Eluard;15 Et 17 Avenue Jean Moulin;93206;St Denis;48.941642;2.364800
+0391092A;Lycée Polyvalent Paul Emile Victor;625 Avenue De Gottmadingen;39303;Champagnole;46.7427137;5.922959
+0130143K;Lycée Polyvalent Paul Langevin;131 Avenue Dr Alexander Fleming;13691;Martigues;43.404811;5.053728
+0912163G;Lycée Polyvalent Paul Langevin;Rue Paul Langevin;91706;Ste Genevieve Des Bois;48.6281114;2.3335769
+0932229T;Lycée Polyvalent Paul Le Rolland;136 Avenue De Castelnau;93700;Drancy;48.9245764;2.461786
+0010034G;Lycée Polyvalent Paul Painleve;13 Place Des Deportes;01108;Oyonnax;46.256308;5.6597797
+0750558Z;Lycée Polyvalent Paul Poiret;19 Rue Des Taillandiers;75011;Paris 11;48.8546499;2.3750458
+0932073Y;Lycée Polyvalent Paul Robert;2-4 Rue Du Chateau;93260;Les Lilas;48.8834638;2.4213298
+0290022S;Lycée Polyvalent Paul Serusier;Av De Waldkappel;29837;Carhaix Plouguer;48.2788842;-3.5441266
+0951937P;Lycée Polyvalent Paul-emile Victor;116 Rue De Livilliers;95520;Osny;49.0723746;2.0700655
+0382895R;Lycée Polyvalent Philibert Delorme;68 Boulevard Saint Hubert;38081;L Isle D Abeau;45.6224977;5.2225335
+0520019N;Lycée Polyvalent Philippe Lebon;11 Rue De Sprendlingen;52301;Joinville;48.4440729;5.1395217
+0941975L;Lycée Polyvalent Pierre Brossolette;5 Rue Pierre Brossolette;94270;Le Kremlin Bicetre;48.8150108;2.3595076
+0290030A;Lycée Polyvalent Pierre Gueguin;Le Porzou;29182;Concarneau;47.8582636;-3.8778006
+9741263E;Lycée Polyvalent Pierre Lagourgue;30 Chemin Mazeau Trois Mares;97430;Le Tampon;-21.25766;55.50818
+0020034B;Lycée Polyvalent Pierre Mechain;19 Rue Leo Lagrange;02011;Laon;49.5587989;3.6319641
+0133015G;Lycée Polyvalent Pierre Mendes France;Avenue Yitzhak Rabin;13741;Vitrolles;43.448548;5.246601
+0580014A;Lycée Polyvalent Pierre-gilles De Gennes;Rue Du Colonel Rabier;58206;Cosne Cours Sur Loire;47.400273;2.92962
+9711033W;Lycée Polyvalent Pointe Noire;Grande Plaine;97116;Pointe Noire;-4.1428413;11.8891721
+0760054W;Lycée Polyvalent Porte Oceane;44 Rue Emile Zola;76090;Le Havre;49.4885624;0.1055085
+6200063Z;Lycée Polyvalent Porto Vecchio;Rte De L'ospedale;20137;Porto Vecchio;41.652522;9.193939
+0210003P;Lycée Polyvalent Prieur De La Cote D'or;6 Rue Vauban;21130;Auxonne;47.1947391;5.3866255
+9730328U;Lycée Polyvalent Privé A.m.javouhey Cayenne;Cite Des Manguiers;97328;Cayenne;4.9332324;-52.3234507
+9720479N;Lycée Polyvalent Privé Adventiste Rama;Cite Scolaire Adventiste Rama;97228;Ste Luce;14.4904216;-60.9530711
+0754030Y;Lycée Polyvalent Privé Albert De Mun;2 Rue D'olivet;75007;Paris 07;48.8497255;2.3195956
+9720615L;Lycée Polyvalent Privé Amep;183 Route De Redoute;97204;Fort De France;14.6282525;-61.0579894
+0430098Y;Lycée Polyvalent Privé Anne Marie Martel;2 Et 9 Rue De Vienne;43002;Le Puy En Velay;45.046138;3.888936
+0110670U;Lycée Polyvalent Privé Beausejour;16 Rue Michelet;11108;Narbonne;43.1855901;3.0063747
+0490840X;Lycée Polyvalent Privé Bourg Chevreau Ste Anne;7 Rue Du 8 Mai 1945;49504;Segre;47.6814632;-0.8721799
+0754029X;Lycée Polyvalent Privé Carcado Saisseval;121 Boulevard Raspail;75006;Paris 06;48.8453132;2.3288349
+0754045P;Lycée Polyvalent Privé Catherine Laboure;29 Rue Gassendi;75014;Paris 14;48.8335807;2.3258612
+0711896A;Lycée Polyvalent Privé Catholique Chalonnais;3 Rue General Giraud;71100;Chalon Sur Saone;46.7911722;4.8574959
+9741255W;Lycée Polyvalent Privé Catholique St Charles;Rue Auguste Babet;97458;St Pierre;-21.33756;55.47994
+0762442S;Lycée Polyvalent Privé Daniel Brottier;11 Impasse Du Chateau;76660;Smermesnil;49.8063368;1.4206513
+0300112J;Lycée Polyvalent Privé De La Cci;1 Ter Avenue General Leclerc;30020;Nimes;43.832345;4.3683606
+0150047V;Lycée Polyvalent Privé De La Communication St Geraud;23 Rue Du College;15013;Aurillac;44.9314468;2.4464301
+9830377T;Lycée Polyvalent Privé Do Kamo (asee);15 Bis Rue Taragnat;98800;Noumea;-22.2742559;166.4556316
+0711755X;Lycée Polyvalent Privé Du Sacre Coeur;22 Avenue De Charolles;71604;Paray Le Monial;46.4510984;4.124986
+0311134P;Lycée Polyvalent Privé Emilie De Rodat;25 Avenue De Lombez;31027;Toulouse;43.5959637;1.4207585
+0170100S;Lycée Polyvalent Privé Fenelon Notre-dame;36 Rue Massiou;17005;La Rochelle;46.1652912;-1.1504523
+0371184L;Lycée Polyvalent Privé Fontiville;77 Rue De Fontiville;37250;Veigne;47.2973253;0.7387831
+0711249X;Lycée Polyvalent Privé Frederic Ozanam;45 Rue De L'heritan;71031;Macon;46.3115653;4.8261909
+0951998F;Lycée Polyvalent Privé G A R A C;3 Boulevard Gallieni;95102;Argenteuil;48.9438075;2.2446175
+0150760V;Lycée Polyvalent Privé Gerbert;47 Avenue Des Prades;15000;Aurillac;44.9163841;2.4366131
+0761359P;Lycée Polyvalent Privé Germaine Coty;86 Rue De Chateaudun;76620;Le Havre;49.5152892;0.1198095
+0530068L;Lycée Polyvalent Privé Haute Follis;91 Rue Haute Follis;53000;Laval;48.0740234;-0.7854105
+0440119R;Lycée Polyvalent Privé Hotelier Ste Anne;250 Boulevard Laennec;44615;St Nazaire;47.2752568;-2.2350718
+0060750H;Lycée Polyvalent Privé Institution St Joseph Carnoles;191 Avenue Aristide Briand;06190;Roquebrune Cap Martin;43.7649927;7.4842023
+0350801F;Lycée Polyvalent Privé Jean Paul Ii;2 Rue Antoine De Saint Exupery;35762;St Gregoire;45.5157091;2.3704006
+0440257R;Lycée Polyvalent Privé Jeanne Bernard-bel Air;48 Avenue De La Baraudiere;44800;St Herblain;47.2355437;-1.5912533
+0010878Z;Lycée Polyvalent Privé Jeanne D'arc;95 Rue Du Lycée;01170;Cessy;0.0;0.0
+0350808N;Lycée Polyvalent Privé Jeanne D'arc;13 Place De La Republique;35503;Vitre;48.124893;-1.2086706
+0460039T;Lycée Polyvalent Privé Jeanne D'arc;51 Bd Colonel Teulie;46100;Figeac;44.6122989;2.0341249
+0761735Y;Lycée Polyvalent Privé Jeanne D'arc;22 Rue Du General De Gaulle;76310;Ste Adresse;49.501971;0.0892924
+0850136L;Lycée Polyvalent Privé Jeanne D'arc;3 Bis Bd Raymond Parpaillon;85603;Montaigu;46.9798514;-1.3120818
+0890064L;Lycée Polyvalent Privé Jeanne D'arc;69 Grande Rue Aristide Briand;89200;Avallon;47.4869016;3.9071141
+0490904S;Lycée Polyvalent Privé Jeanne Delanoue;11 Boulevard Jeanne D Arc;49304;Cholet;47.0640598;-0.8673109
+0480706G;Lycée Polyvalent Privé Joseph Gibelin;43 Avenue De La Gare;48200;St Chely D Apcher;44.8001253;3.273392
+0754015G;Lycée Polyvalent Privé Jules Richard Microtechniques;21 Rue Carducci;75019;Paris 19;48.8774956;2.3855772
+0611147R;Lycée Polyvalent Privé L Esperance;Boulevard Du Nord;61800;Tinchebray;48.764231;-0.736472
+0440279P;Lycée Polyvalent Privé La Baugerie;38 Bd Des Pas Enchantes;44232;St Sebastien Sur Loire;47.2100641;-1.5019567
+0761343X;Lycée Polyvalent Privé La Chataigneraie;2 Rue Charles Scherer;76240;Le Mesnil Esnard;49.4132672;1.1461844
+0342175E;Lycée Polyvalent Privé La Merci Littoral;603 Av De La Petite Motte;34280;La Grande Motte;43.5656148;4.0735973
+0150051Z;Lycée Polyvalent Privé La Presentation Notre Dame;1 Cours Spy Des Ternes;15100;St Flour;45.0344847;3.0899936
+0351930H;Lycée Polyvalent Privé La Providence;21 Rue De Rennes;35360;Montauban De Bretagne;48.1983239;-2.0463959
+0410693F;Lycée Polyvalent Privé La Providence;23 Rue Des Saintes Maries;41034;Blois;47.5930538;1.3304792
+0754042L;Lycée Polyvalent Privé Le Rebours;44 Boulevard Auguste Blanqui;75013;Paris 13;48.8301339;2.3505104
+0422160V;Lycée Polyvalent Privé Le Renouveau;All Des Bois Chateau Colcombet;42530;St Genest Lerpt;45.452188;4.341445
+0211091X;Lycée Polyvalent Privé Les Arcades;13 Rue Du Vieux College;21000;Dijon;47.3200096;5.0457552
+0761341V;Lycée Polyvalent Privé Les Tourelles;53 Rue Verte;76000;Rouen;49.4502554;1.0928484
+0580071M;Lycée Polyvalent Privé L'esperance;10 Cloitre St Cyr;58000;Nevers;46.9862254;3.1562197
+0830101J;Lycée Polyvalent Privé Maintenon;10 Boulevard Pasteur;83409;Hyeres;43.1200536;6.1340928
+0190083M;Lycée Polyvalent Privé Marguerite Bahuet;41bis Av Michelet St Antoine;19316;Brive La Gaillarde;45.14663;1.530476
+0370881G;Lycée Polyvalent Privé Marmoutier;17 Quai Marmoutier;37100;Tours;47.4018774;0.7172022
+0660552S;Lycée Polyvalent Privé Maso;7 Avenue Des Palmiers;66000;Perpignan;42.6997916;2.8874756
+0921484N;Lycée Polyvalent Privé Montalembert;238 Boulevard Saint Denis;92400;Courbevoie;48.903176;2.2760713
+0311138U;Lycée Polyvalent Privé Montalembert Ntre-dame;152 Avenue De Lespinet;31400;Toulouse;43.5761758;1.4709404
+0850130E;Lycée Polyvalent Privé Nd Du Roc;Rue Charlemagne;85035;La Roche Sur Yon;46.6839572;-1.4199783
+0340991T;Lycée Polyvalent Privé Nevers;18 Rue De La Garenne;34090;Montpellier;43.6204369;3.8692299
+0440274J;Lycée Polyvalent Privé Notre Dame;50 Rue Jean Jaures;44401;Reze;47.1877269;-1.5471117
+0850142T;Lycée Polyvalent Privé Notre Dame;Rue De Bois Fosse;85304;Challans;46.832239;-1.881754
+0851344Z;Lycée Polyvalent Privé Notre Dame;29 Rue Rabelais;85205;Fontenay Le Comte;46.467908;-0.8142398
+0290206S;Lycée Polyvalent Privé Notre Dame De Kerbertrand;154 Rue De Pont-aven;29391;Quimperle;47.8710014;-3.5663631
+0190062P;Lycée Polyvalent Privé Notre Dame De La Providence;19 Rue General De Gaulle;19200;Ussel;45.5495205;2.3075733
+0080091F;Lycée Polyvalent Privé Notre-dame;1 Place De La Basilique;08000;Charleville Mezieres;49.7607798;4.7162174
+0271070S;Lycée Polyvalent Privé Notre-dame;7 Rue Du Chantier;27000;Evreux;49.0203676;1.1525311
+0650885J;Lycée Polyvalent Privé Notre-dame De Garaison;2 Route Cier Garaison;65670;Monleon Magnoac;43.2066692;0.5032436
+0951994B;Lycée Polyvalent Privé O R T;32 Avenue De Choiseul;95400;Villiers Le Bel;48.9940718;2.4104291
+0520685M;Lycée Polyvalent Privé Oudinot;Route De Neuilly;52000;Chaumont;48.0843712;5.1460026
+0391146J;Lycée Polyvalent Privé Pasteur Mont Roland;;39107;Dole;46.9583818;5.5032587
+0751712D;Lycée Polyvalent Privé Petrelle;8 Rue Petrelle;75009;Paris 09;48.8806327;2.3486726
+0761356L;Lycée Polyvalent Privé Providence Misericorde;42 Rue De Le Nostre;76020;Rouen;49.4441716;1.0794378
+0440246D;Lycée Polyvalent Privé Sacre Coeur;3 Rue Francis Portais;44100;Nantes;47.2080132;-1.6088332
+0480022N;Lycée Polyvalent Privé Sacre Coeur;1 Rue Du College;48300;Langogne;44.7256821;3.8539001
+0790078F;Lycée Polyvalent Privé Saint Andre;14 Rue De Souche;79007;Niort;46.3274523;-0.4445119
+0211090W;Lycée Polyvalent Privé Saint Benigne;99 Rue De Talant;21000;Dijon;47.3310729;5.0153828
+0360505D;Lycée Polyvalent Privé Saint Cyr;1 Rue Lecherbonnier;36101;Issoudun;46.951712;1.995903
+0890067P;Lycée Polyvalent Privé Saint Etienne;2 Rue Louise Et Leon Vernis;89100;Sens;48.204667;3.294032
+0450106W;Lycée Polyvalent Privé Saint Francois De Sales;66 Rue Paul Bert;45504;Gien;47.6882519;2.6252631
+0370748M;Lycée Polyvalent Privé Saint Gatien;107 Rue De La Douzillere;37300;Joue Les Tours;47.3353239;0.6524983
+0060774J;Lycée Polyvalent Privé Saint Joseph;14 Rue Barla;06300;Nice;43.7018025;7.2828387
+0211089V;Lycée Polyvalent Privé Saint Joseph;39 Rue Du Transvaal;21010;Dijon;47.3153813;5.0377019
+0410954P;Lycée Polyvalent Privé Saint Joseph;18 Rue Lemyre De Villers;41100;Vendome;47.792551;1.065343
+0580778F;Lycée Polyvalent Privé Saint Joseph;118 Bis Rue Des Montapins;58000;Nevers;46.977082;3.1350203
+0790080H;Lycée Polyvalent Privé Saint Joseph;4 Rue Du Docteur Brillaud;79302;Bressuire;46.8380755;-0.4907658
+0160083D;Lycée Polyvalent Privé Saint Joseph L'amandier;Allee Jean Pierre Gault;16710;St Yrieix Sur Charente;45.6931026;0.1181619
+0711494N;Lycée Polyvalent Privé Saint Lazare;7 Rue St Germain;71404;Autun;46.9493625;4.2955232
+0912161E;Lycée Polyvalent Privé Saint Leon;8 Quai De L Essonne;91100;Corbeil Essonnes;48.6088484;2.4825139
+0450107X;Lycée Polyvalent Privé Saint Louis;Au Chateau;45203;Montargis;48.0391073;2.7103188
+0450758E;Lycée Polyvalent Privé Saint Paul-bourdon Blanc;4 Rue Neuve Saint Aignan;45057;Orleans;47.8984756;1.9160429
+0610698C;Lycée Polyvalent Privé Saint Thomas D'aquin;1 Place C Duperron;61105;Flers;48.7491921;-0.5655076
+0212023K;Lycée Polyvalent Privé Saint Vincent De Paul;9 Rue E Humblot;21401;Chatillon Sur Seine;47.859253;4.5757386
+0831444U;Lycée Polyvalent Privé Sainte Jeanne D Arc;Avenue Dreo;83170;Brignoles;43.4059478;6.0668721
+0160079Z;Lycée Polyvalent Privé Sainte Marthe Chavagnes;51 Rue Du Minage;16000;Angouleme;45.6497236;0.15127
+0360693H;Lycée Polyvalent Privé Sainte Solange;1 Place De La Gare;36028;Chateauroux;46.810104;1.698855
+0660077A;Lycée Polyvalent Privé Sainte-louise-de-marillac;68 Avenue Victor Dalbiez;66000;Perpignan;42.6895911;2.8844606
+0311131L;Lycée Polyvalent Privé Sainte-therese;16 Rue Du Bugatet;31804;St Gaudens;43.1061649;0.7217848
+0753217P;Lycée Polyvalent Privé Sainte-therese;40 Rue La Fontaine;75016;Paris 16;48.8509879;2.2719157
+0341523W;Lycée Polyvalent Privé Saint-joseph;11 Avenue Marx Dormoy;34202;Sete;43.4038819;3.6931944
+0480023P;Lycée Polyvalent Privé Saint-joseph;2 Rue Des Penitents;48100;Marvejols;44.5542067;3.2915228
+0500126R;Lycée Polyvalent Privé Saint-joseph;25 Rue Des Ecoles;50800;Villedieu Les Poeles;48.8370642;-1.2167469
+0754025T;Lycée Polyvalent Privé Saint-nicolas;92 Rue De Vaugirard;75006;Paris 06;48.8468268;2.3260737
+0110045P;Lycée Polyvalent Privé Saint-stanislas;77 Rue Aime Ramond;11000;Carcassonne;43.2118475;2.3495443
+0580076T;Lycée Polyvalent Privé Simone Dounon;5 Rue Marcelin Berthelot;58200;Cosne Cours Sur Loire;47.4125079;2.9241087
+0720818Y;Lycée Polyvalent Privé St Charles;75 Avenue Bollee;72000;Le Mans;48.0020737;0.2075092
+0350793X;Lycée Polyvalent Privé St Etienne;99 Rue De La Chalotais;35512;Cesson Sevigne;48.1295633;-1.597344
+0440247E;Lycée Polyvalent Privé St Felix;27 Rue Du Ballet;44001;Nantes;47.2316606;-1.560045
+0610692W;Lycée Polyvalent Privé St Francois De Sales;100 Rue Labillardiere;61007;Alencon;48.430496;0.0965975
+0440253L;Lycée Polyvalent Privé St Jb De La Salle;14 Rue Du Ballet;44010;Nantes;47.2317713;-1.5611167
+0180571Y;Lycée Polyvalent Privé St Jean Baptiste De La Salle;52 Avenue De La Liberation;18020;Bourges;47.1051882;2.4041241
+0912117G;Lycée Polyvalent Privé St Jean St Paul;1 Av De Ratisbonne;91000;Evry;48.6437303;2.4357094
+0240081H;Lycée Polyvalent Privé St Joseph;Boulevard Eugene Leroy;24205;Sarlat La Caneda;44.8902364;1.2144075
+0290184T;Lycée Polyvalent Privé St Joseph;Route De Pencran;29413;Landerneau;48.4479875;-4.2370715
+0441653H;Lycée Polyvalent Privé St Joseph La Joliverie;141 Route De Clisson;44232;St Sebastien Sur Loire;47.1897277;-1.5177365
+0721478R;Lycée Polyvalent Privé St Joseph Lorraine;Le Pizieux;72700;Pruille Le Chetif;47.984176;0.1387529
+0490888Z;Lycée Polyvalent Privé St Julien La Baronnerie;Rue Helene Boucher;49481;St Sylvain D Anjou;47.487831;-0.496899
+0850137M;Lycée Polyvalent Privé St Louis;104 Rue Pierre Brossolette;85007;La Roche Sur Yon;46.6861278;-1.4404186
+0511145T;Lycée Polyvalent Privé St Michel;39 Rue Martin Peller;51100;Reims;49.2466602;4.0133052
+0910843X;Lycée Polyvalent Privé St Pierre;70 Rue De Montgeron;91800;Brunoy;48.6945366;2.4899797
+0440256P;Lycée Polyvalent Privé St Pierre La Joliverie;141 Route De Clisson;44232;St Sebastien Sur Loire;47.1897277;-1.5177365
+0300127A;Lycée Polyvalent Privé St Vincent De Paul;3 Bd De Bruxelles;30020;Nimes;43.834096;4.362298
+0511147V;Lycée Polyvalent Privé St Vincent De Paul;1 Rue De La Fraternite;51001;Chalons En Champagne;48.9779739;4.3614798
+0720843A;Lycée Polyvalent Privé Ste Anne;5 Rue Alain De Rouge;72305;Sable Sur Sarthe;47.8412381;-0.3363364
+0560119A;Lycée Polyvalent Privé Ste Anne-st Louis;2 Route De Locmaria;56400;Ste Anne D Auray;47.7047103;-2.9507241
+0720822C;Lycée Polyvalent Privé Ste Catherine;202 Rue De St Aubin;72000;Le Mans;48.0222385;0.1782785
+0451335G;Lycée Polyvalent Privé Ste Croix St Euverte;28 Rue De L Etelon;45043;Orleans;47.9025554;1.9171035
+0850135K;Lycée Polyvalent Privé Ste Marie;12 Place Jeanne D Arc;85111;Chantonnay;46.6862118;-1.0522409
+0850133H;Lycée Polyvalent Privé Ste Marie Du Port;La Meriniere;85108;Les Sables D Olonne;46.509488;-1.765298
+0350840Y;Lycée Polyvalent Privé Ste Therese;27 Rue Sully Prudhomme;35000;Rennes;48.0948205;-1.6669327
+0850077X;Lycée Polyvalent Privé Ste Ursule;56 Rue Georges Clemenceau;85403;Lucon;46.4577364;-1.1672514
+0440252K;Lycée Polyvalent Privé Talensac;18 Rue De Talensac;44002;Nantes;47.2218408;-1.5585936
+9720782T;Lycée Polyvalent Privé Techno-compta;188 Avenue Maurice Bishop;97200;Fort De France;14.6087039;-61.0560875
+0931026K;Lycée Polyvalent Protectorat Saint Joseph;36 Rue Jacques Duclos;93600;Aulnay Sous Bois;48.9456045;2.4946283
+0530949U;Lycée Polyvalent Raoul Vadepied;Boulevard Rossignol;53602;Evron;48.1517259;-0.40075
+0310040A;Lycée Polyvalent Raymond Naves;139 Route D'albi;31018;Toulouse;43.6371858;1.4645232
+0762880T;Lycée Polyvalent Raymond Queneau;Rue Du Docteur Zamenhof;76194;Yvetot;49.613437;0.769068
+0830007G;Lycée Polyvalent Raynouard;Rue G. Pelissier;83170;Brignoles;43.40655;6.061187
+0922149L;Lycée Polyvalent Rene Auffray;23 Rue Fernand Pelloutier;92582;Clichy;48.9033466;2.3000039
+0710048S;Lycée Polyvalent Rene Cassin;49 Boulevard Des Neuf Cles;71018;Macon;46.3147768;4.8211064
+0771941S;Lycée Polyvalent Rene Cassin;1 Av P Mendes Frances Noisiel;77426;Champs Sur Marne;48.8425281;2.6013372
+0932222K;Lycée Polyvalent Rene Cassin;16 Allee Des Bosquets;93340;Le Raincy;48.9008371;2.5202268
+0772223Y;Lycée Polyvalent Rene Descartes;4 Boulevard Copernic;77420;Champs Sur Marne;48.844743;2.5858742
+0500049G;Lycée Polyvalent Robert De Mortain;30 Rue De La 30e Div Americaine;50140;Mortain;48.649222;-0.941546
+0720017C;Lycée Polyvalent Robert Garnier;Avenue Du General De Gaulle;72405;La Ferte Bernard;48.184306;0.648803
+0741669M;Lycée Polyvalent Roger Frison Roche;Promenade Du Fori;74401;Chamonix Mont Blanc;45.928101;6.872169
+9740002J;Lycée Polyvalent Roland Garros;Rue Roland Garros;97839;Le Tampon;-21.2735691;55.5219446
+0580008U;Lycée Polyvalent Romain Rolland;5 Rue Pablo Neruda;58503;Clamecy;47.4566989;3.5317777
+0950667J;Lycée Polyvalent Romain Rolland;21 Av De Montmorency;95190;Goussainville;49.0299597;2.453191
+0660856X;Lycée Polyvalent Rosa Luxemburg;2 Avenue Jean Moulin;66141;Canet En Roussillon;42.7039851;3.021471
+0831616F;Lycée Polyvalent Rouviere;Quartier Sainte Musse;83070;Toulon;43.124228;5.928
+0771238C;Lycée Polyvalent Saint Aspais;36 Rue Saint Barthelemy;77007;Melun;48.542074;2.6558301
+0930936M;Lycée Polyvalent Saint Benoist De L'europe;82 Avenue Gambetta;93172;Bagnolet;48.870888;2.4176316
+0640057P;Lycée Polyvalent Saint Cricq;4 B Av Des Etats Unis;64015;Pau;43.2982785;-0.3595802
+0890070T;Lycée Polyvalent Saint Joseph;1 Bd De La Marne;89015;Auxerre;47.811752;3.56117
+0771246L;Lycée Polyvalent Sainte Genevieve;12 Rue De La Visitation;77109;Meaux;48.9653532;2.8843027
+0772153X;Lycée Polyvalent Sainte Marie;41 Rue De Chaage;77109;Meaux;48.9658178;2.8806747
+0010006B;Lycée Polyvalent Saint-exupery;15 Avenue Saint-exupery;01206;Bellegarde Sur Valserine;46.1142691;5.8180076
+0312686B;Lycée Polyvalent Saint-exupery;1 Place Alain Savary;31703;Blagnac;43.6816094;1.4178833
+9730371R;Lycée Polyvalent Saint-laurent Ii;3 All?e Des Buissons Ardents;97393;St Laurent Du Maroni;0.0;0.0
+0281077U;Lycée Polyvalent Silvia Monfort;6 Rue De L Orme De Sours;28600;Luisant;48.4192711;1.4555464
+0772310T;Lycée Polyvalent Simone Signoret;Place Du 14 Juillet;77000;Vaux Le Penil;48.5310815;2.6726108
+0212015B;Lycée Polyvalent Simone Weil;1 Rue Pelletier De Chambure;21000;Dijon;47.3210003;5.0500088
+0783447Y;Lycée Polyvalent Simone Weil;Rue Du Val D Oise;78700;Conflans Ste Honorine;49.0066604;2.0870198
+0772332S;Lycée Polyvalent Sonia Delaunay;1 Rue Du Lycée;77240;Cesson;0.0;0.0
+0781952Y;Lycée Polyvalent Sonia Delaunay;Avenue De St Germain;78450;Villepreux;48.8301026;1.9931895
+0542208G;Lycée Polyvalent Stanislas;468 Rue De Vandoeuvre;54600;Villers Les Nancy;48.6636628;6.1530599
+0670114K;Lycée Polyvalent Stanislas;7 Rue Du Lycée;67163;Wissembourg;0.0;0.0
+9741052A;Lycée Polyvalent Stella;;97424;St Leu;48.8626934;2.3496713
+0932121A;Lycée Polyvalent Suger;6 Avenue Le Roy Des Barres;93200;St Denis;48.936181;2.357443
+0870019Y;Lycée Polyvalent Suzanne Valadon;39 Rue Francois Perrin;87032;Limoges;45.8293539;1.2447165
+0680016Y;Lycée Polyvalent Theodore Deck;5 Rue Des Chanoines;68504;Guebwiller;47.9061156;7.2133487
+0480688M;Lycée Polyvalent Theophile Roussel;15 Rue Du Docteur Yves Dalle;48200;St Chely D Apcher;44.8039562;3.2735377
+0770942F;Lycée Polyvalent Thibaut De Champagne;3 Rue Du College;77160;Provins;48.5612146;3.2921093
+0840918S;Lycée Polyvalent Val De Durance;Rte Etang De La Bonde;84123;Pertuis;43.694275;5.501843
+0511884W;Lycée Polyvalent Val De Murigny;2 Rue Vauban;51097;Reims;49.2237219;4.0277678
+0031082K;Lycée Polyvalent Valery Larbaud;8 Boulevard Gabriel Peronnet;03306;Cusset;46.126235;3.445922
+0290012F;Lycée Polyvalent Vauban;Rue De Kerichen;29801;Brest;48.4284436;-4.4688478
+0623981R;Lycée Polyvalent Vauban;1 Rue De Bretagne;62120;Aire Sur La Lys;50.6333856;2.4090237
+0781984H;Lycée Polyvalent Vaucanson;14 Rue Albert Thomas;78132;Les Mureaux;48.982369;1.923312
+0390027T;Lycée Polyvalent Victor Berard;35 Quai Aime Lamy;39403;Morez;46.5171316;6.0256123
+0650005C;Lycée Polyvalent Victor Duruy;Allees Jean Jaures;65201;Bagneres De Bigorre;43.0637029;0.153508
+0342090M;Lycée Polyvalent Victor Hugo;;34400;Lunel;43.67445;4.135366
+0330126H;Lycée Polyvalent Victor Louis;2 Avenue De Thouars;33405;Talence;44.8017092;-0.593576
+0070021K;Lycée Polyvalent Vincent D'indy;9 Boulevard Du Lycée;07006;Privas;0.0;0.0
+0781859X;Lycée Polyvalent Vincent Van Gogh;Rue Jules Ferry;78410;Aubergenville;48.954462;1.855006
+9830483H;Lycée Polyvalent Williama Haudra;;98820;Lifou;-22.2518502;166.4523382
+0010032E;Lycée Polyvalent Xavier Bichat;Avenue Du Lac;01130;Nantua;46.1557254;5.601148
+0071351F;Lycée Polyvalent Xavier Mallet;Rue Frederic Mistral;07400;Le Teil;44.5379585;4.6891329
+0250043L;Lycée Polyvalent Xavier Marmier;53 Rue De Doubs;25304;Pontarlier;46.9158344;6.3514785
+0622196A;Lycée Privé Baudimont Saint-charles;17 Rue Saint Maurice;62008;Arras;50.2937988;2.770153
+0910819W;Lycée Privé Cours Secondaire;11 Rue De Courtaboeuf;91400;Orsay;48.6946177;2.1908145
+0451565G;Lycée Privé De L Horticulture Et Du Paysage D Orleans;66 Avenue De La Mouillere;45072;Orleans;47.881944;1.9102444
+0430968U;Lycée Privé Europeen St-jacques De Compostelle;4-10 Boulevard Montferrand;43000;Le Puy En Velay;45.0473977;3.8821493
+0291574D;Lycée Privé Hortic Et Paysage De Kerbernez;Kerbernez;29700;Plomelin;47.943747;-4.129242
+0593225K;Lycée Privé Lycée Privé De Marcq;170 Rue Du College;59700;Marcq En Baroeul;50.6829913;3.1036007
+0753825A;Lycée Privé Pour Handicapes Morvan;68 Rue De La Chaussee D'antin;75009;Paris 09;48.8758516;2.3320853
+0754089M;Lycée Privé Pour Handicapes Votre Ecole Chez Vous;29 Rue Merlin;75011;Paris 11;48.8613416;2.3848162
+0011058V;Lycée Privé Prof Rural Le Molard;10 Place De La Halle;01150;St Sorlin En Bugey;45.9040178;5.3485298
+0592921E;Lycée Privé Saint Remi;10 Rue Notre Dame Des Victoires;59100;Roubaix;50.6950525;3.1805112
+0641535W;Lycée Prof Agric Horticol Pr Armand David;1 Route De Mssionnaires;64240;Hasparren;43.3811075;-1.3049408
+0601768W;Lycée Prof Agric Privé Du Valois;15 Rue D'ognes;60440;Nanteuil Le Haudouin;49.136273;2.813378
+0400784K;Lycée Prof Agricole Pr;Au Rayon;40230;Saubrigues;43.610771;-1.314267
+0351958N;Lycée Prof Agricole Privé;26 Rue Du Chatelet;35605;Redon;47.6473408;-2.0925548
+0351960R;Lycée Prof Agricole Privé;15 Rue Du Bourg Au Loup;35140;St Aubin Du Cormier;48.2568795;-1.396259
+0561460H;Lycée Prof Agricole Privé;20 Place Du Docteur Queinnec;56140;Malestroit;47.8099502;-2.3846483
+0561464M;Lycée Prof Agricole Privé;Rue Du Chanoine Niol;56230;Questembert;47.663927;-2.454126
+0351947B;Lycée Prof Agricole Privé Edmond Michelet;Parc De Montaubert;35300;Fougeres;48.3572455;-1.2009328
+0280946B;Lycée Prof Agricole Privé Efagrir;2 Rue Des Fleurs;28630;Mignieres;48.3595403;1.4288729
+0280942X;Lycée Prof Agricole Privé Gabriel Bridet;40 Rte Hubert Baraine;28260;Anet;48.8526049;1.435093
+0351943X;Lycée Prof Agricole Privé La Noe St Yves;Route De Chateaubriant;35470;Bain De Bretagne;47.8025633;-1.7386898
+0221677D;Lycée Prof Agricole Privé Le Restmeur;Pabu;22200;Pabu;48.7057532;-3.252573
+0221675B;Lycée Prof Agricole Privé Notre Dame;Rue St Vincent;22290;Lanvollon;48.630202;-2.9889739
+0291573C;Lycée Prof Agricole Privé Pierre Tremintin;2 Rue St Pol;29430;Plouescat;48.6583593;-4.1696711
+0561456D;Lycée Prof Agricole Privé St Yves;2 Rue De La Liberation;56110;Gourin;48.1407693;-3.6033035
+0221671X;Lycée Prof Agricole Privé Ste Marie;29 Rue De La Barriere;22250;Broons;48.3221871;-2.2642991
+0291575E;Lycée Prof Agricole Privé Ste Marie;Rue Du 11 Novembre;29610;Plouigneau;48.5651547;-3.6991958
+0690275Z;Lycée Prof Agricole Viticole;37 Route De Beaujeu;69220;St Jean D Ardieres;46.1136651;4.7273837
+0221670W;Lycée Prof Horticole Privé;Coat An Doc'h;22170;Lanrodec;48.532569;-3.033864
+0941724N;Lycée Prof Privé Des Metiers Batiment Et Travaux Publics;18 Rue De Belfort;94307;Vincennes;48.8501369;2.4311329
+0332495H;Lycée Prof Privé Des Metiers Bel Orme;67 Rue Bel Orme;33000;Bordeaux;44.8497536;-0.5941378
+0693373S;Lycée Prof Privé Des Metiers Carrel;7 Rue Pierre Robin;69362;Lyon 07;45.7493853;4.8581918
+0290200K;Lycée Prof Privé Des Metiers Des Metiers Le Porsmeur;4 Rue Du Docteur Prouff;29678;Morlaix;48.5769099;-3.8335892
+0420998G;Lycée Prof Privé Des Metiers Des Monts Du Lyonnais;9 Rue De Montbrison;42140;Chazelles Sur Lyon;45.6376681;4.3846408
+0693371P;Lycée Prof Privé Des Metiers Don Bosco;12 Et 24 Montee Saint Laurent;69322;Lyon 05;45.7510345;4.8167844
+0931369H;Lycée Prof Privé Des Metiers Francoise Cabrini;20 Rue Du Docteur Sureau;93167;Noisy Le Grand;48.8461618;2.5491228
+0312062Y;Lycée Prof Privé Des Metiers Issec Pigier;36 Rue Alsace Lorraine;31000;Toulouse;43.605598;1.445689
+0690712Z;Lycée Prof Privé Des Metiers Jamet Buffereau;17 Rue Des Chartreux;69283;Lyon 01;45.7720374;4.8233234
+0292140U;Lycée Prof Privé Des Metiers La Croix Rouge;2 Rue Mirabeau;29229;Brest;48.4075125;-4.4894481
+0693374T;Lycée Prof Privé Des Metiers La Mache;75 Boulevard Jean Xxiii;69373;Lyon 08;45.7364603;4.8695511
+0801946F;Lycée Prof Privé Des Metiers La Providence;146 Bd De Saint Quentin;80094;Amiens;49.8789518;2.3007807
+0292144Y;Lycée Prof Privé Des Metiers Le Likes;20 Place De La Tourbie;29196;Quimper;47.9996048;-4.1024423
+0420991Z;Lycée Prof Privé Des Metiers Le Marais Sainte Therese;48 Boulevard Thiers;42000;St Etienne;45.4507808;4.3896969
+0121429N;Lycée Prof Privé Des Metiers Louis Querbes;29 Rue Maurice Bompard;12000;Rodez;44.3485775;2.5792732
+0220122N;Lycée Prof Privé Des Metiers Montbareil;2 Rue Marechal Joffre;22200;Guingamp;48.5632365;-3.1498718
+0070114L;Lycée Prof Privé Des Metiers Notre Dame;26 Avenue Du Vanel;07000;Privas;44.7358608;4.6056151
+0693375U;Lycée Prof Privé Des Metiers Notre Dame;72 Rue Des Jardiniers;69657;Villefranche Sur Saone;45.9885367;4.7206723
+0810113H;Lycée Prof Privé Des Metiers Notre-dame;Avenue D'hauterive;81101;Castres;43.599188;2.244541
+0690653K;Lycée Prof Privé Des Metiers Orsel;30 Rue Orsel;69600;Oullins;45.7164792;4.8123384
+0691680B;Lycée Prof Privé Des Metiers Saint Charles;2831 Route De Strasbourg;69140;Rillieux La Pape;45.820291;4.8954392
+0021999M;Lycée Prof Privé Des Metiers Saint Joseph;4 Place Thiers;02402;Chateau Thierry;49.0428865;3.4009902
+0690641X;Lycée Prof Privé Des Metiers Saint Marc;4 Rue Sainte Helene;69287;Lyon 02;45.7554117;4.828113
+0801948H;Lycée Prof Privé Des Metiers Saint Martin;68 Rue Delpech;80043;Amiens;49.8842672;2.29491
+0801950K;Lycée Prof Privé Des Metiers Saint Remi;4 Rue Des Sergents;80006;Amiens;49.8942788;2.2987524
+0421006R;Lycée Prof Privé Des Metiers Saint Vincent;Place De L'egalite;42400;St Chamond;45.4798433;4.5132717
+0421980Z;Lycée Prof Privé Des Metiers Sainte Barbe;10 Rue Franklin;42028;St Etienne;45.4285207;4.3845892
+0420985T;Lycée Prof Privé Des Metiers Sainte Claire;8 Rue Des Parottes;42450;Sury Le Comtal;45.5360878;4.187921
+0420984S;Lycée Prof Privé Des Metiers Sainte Marie;39 Rue Des Freres Chappe;42007;St Etienne;45.4399091;4.395858
+0020498F;Lycée Prof Privé Des Metiers Sainte Sophie;22 Rue Curie;02110;Bohain En Vermandois;49.9830328;3.4565714
+0650886K;Lycée Prof Privé Des Metiers Saint-pierre;24 Avenue D'azereix;65000;Tarbes;43.2289169;0.0578127
+0290202M;Lycée Prof Privé Des Metiers St Gabriel;Rue Jean Lautredou;29125;Pont L Abbe;47.8619569;-4.216529
+0020492Z;Lycée Prof Privé Des Metiers St Vincent De Paul;13 Avenue De Reims;02205;Soissons;49.3754463;3.3353105
+0421742R;Lycée Prof Privé Des Metiers Tezenas Du Montcel;14 Place Girodet;42000;St Etienne;45.4480193;4.3809644
+0352446U;Lycée Prof Privé Hotelier Ste Therese;18 Rue Du Four;35130;La Guerche De Bretagne;47.9429138;-1.2313142
+0011057U;Lycée Prof Privé Rural De L'ain;5 Rue Docteur Levrat;01130;Nantua;46.1537153;5.6035237
+0351962T;Lycée Prof Rural Privé;50 Rue Nationale;35190;Tinteniac;48.3282833;-1.8332082
+0561458F;Lycée Prof Rural Privé Ker Anna;3 Rue Ker Anna;56700;Kervignac;47.7614575;-3.238536
+0761744H;Lycée Prof. Agricole Annexe Du Pays De Bray;4 Ave Des Canadiens;76270;Neufchatel En Bray;49.7304107;1.4327269
+0430114R;Lycée Prof. Agricole Privé Isvt St Dominique Vals;La Roche Arnaud;43001;Le Puy En Velay;45.038381;3.891388
+0121293R;Lycée Prof.agricole Privé;14 Rue Frayssinous;12130;St Geniez D Olt;44.466538;2.9759538
+0211504W;Lycée Prof.agricole Privé;20 Rue Danton;21210;Saulieu;47.2773555;4.2315727
+0820724S;Lycée Prof.agricole Privé;30 Rue De La Republique;82500;Beaumont De Lomagne;43.8823822;0.9864743
+0771740Y;Lycée Prof.agricole Privé Assomption;2 Rue De Salins;77130;Forges;48.418206;2.960877
+0820725T;Lycée Prof.agricole Privé Clair Foyer Caussadais;Route De Negrepelisse;82300;Caussade;44.14681;1.5243137
+0711441F;Lycée Prof.agricole Privé De La Bresse;600 Avenue Fernand Point;71500;Louhans;46.6235063;5.2333128
+0441782Y;Lycée Prof.agricole Privé Grand Blottereau;34 Chemin Du Ponceau;44300;Nantes;47.224905;-1.51142
+0121291N;Lycée Prof.agricole Privé Horticole;Route De Villefranche;12390;Rignac;44.4130599;2.2824371
+0460504Y;Lycée Prof.agricole Privé Institut Alain De Solminihac;Rue Du 8 Mai;46220;Prayssac;44.5050085;1.1855864
+0580662E;Lycée Prof.agricole Privé Iperma;Domaine Laveyne;58330;St Saulge;47.104764;3.514389
+0441784A;Lycée Prof.agricole Privé Jean-baptiste Eriau;Espace Rohan;44150;Ancenis;47.365464;-1.177491
+0441794L;Lycée Prof.agricole Privé Kerguenec;;44350;St Molf;47.390932;-2.424955
+0341472R;Lycée Prof.agricole Privé Le Cep D Or;Avenue De La Piscine;34800;Clermont L Herault;43.6314047;3.4337484
+0341473S;Lycée Prof.agricole Privé Le Roc Blanc;1 Rue De L Albarede;34190;Ganges;43.9336912;3.7098028
+0491801S;Lycée Prof.agricole Privé Les Buissonnets;7 Boulevard Daviers;49100;Angers;47.4791568;-0.5584661
+0721328C;Lycée Prof.agricole Privé Les Horizons;15 Rue De Touraine;72220;St Gervais En Belin;47.8742353;0.2176217
+0441785B;Lycée Prof.agricole Privé Les Prateaux;28 Bis Rue Bizeul;44130;Blain;47.4756295;-1.769521
+0530815Y;Lycée Prof.agricole Privé Lp Rural Privé;Pannard;53500;Ernee;48.3039668;-0.9412869
+0881690J;Lycée Prof.agricole Privé Lycée Prof.rural;684 Rue De La Mairie;88270;Harol;48.1542309;6.247106
+0912126S;Lycée Prof.agricole Privé M.f.h.essonne Verte;Avenue Du 8 Mai 1945;91154;Etampes;48.4402827;2.1552271
+0341606L;Lycée Prof.agricole Privé Maurice Clavel;Rue De La Raffinerie;34110;Frontignan;43.4434006;3.7583253
+0721337M;Lycée Prof.agricole Privé Nazareth;;72340;Ruille Sur Loir;47.7497789;0.620518
+0721336L;Lycée Prof.agricole Privé Notre Dame;10 Rue Du Tertre;72400;La Ferte Bernard;48.1886105;0.6387134
+0781071R;Lycée Prof.agricole Privé Privé Le Notre;Domaine De Pinceloup;78120;Sonchamp;48.5868348;1.8825612
+0711439D;Lycée Prof.agricole Privé Reine Antier;Au Bourg;71620;St Martin En Bresse;46.340815;5.0723608
+0530813W;Lycée Prof.agricole Privé Robert Schuman;62 Rue De La Division Leclerc;53200;Chateau Gontier;47.8339799;-0.7164153
+0311843K;Lycée Prof.agricole Privé Rural;7 Place Des Marchands;31370;Rieumes;43.4127035;1.12037
+0311842J;Lycée Prof.agricole Privé Rural L'oustal;15 Place D'orleans;31380;Montastruc La Conseillere;43.7168431;1.5901694
+0441790G;Lycée Prof.agricole Privé Saint Joseph;40 Rue Michel Grimault;44141;Chateaubriant;47.7156138;-1.3765267
+0441789F;Lycée Prof.agricole Privé Saint Martin;Rte De La Foret;44270;Machecoul;46.9980392;-1.8028421
+0371259T;Lycée Prof.agricole Privé Sainte Jeanne D Arc;;37600;Verneuil Sur Indre;47.055817;1.042124
+0121292P;Lycée Prof.agricole Privé Vaxergues Charles Palies;198 Avenue Du Dr Galtier;12400;St Affrique;43.95819;2.887126
+0891001E;Lycée Prof.rural Privé Sainte Colombe;Rue Henri Cavallier;89100;St Denis;48.214857;3.273402
+0332382K;Lycée Profes Agricole Privé St Clement;Domaine Labeyrie;33430;Cudos;44.3933095;-0.2146137
+0120867C;Lycée Profess Agricole Privé Francois Marty;;12200;Monteils;44.26547;1.998179
+0673005C;Lycée Professessionnel I.e.s.c.;14 Rue Du Zornhoff;67700;Saverne;48.7492289;7.3773102
+0730012S;Lycée Professionnel;Rue Du Grand Barberaz;73190;Challes Les Eaux;45.5498015;5.9820346
+0750787Y;Lycée Professionnel;61 Rue Corvisart;75013;Paris 13;48.8305676;2.3487691
+0811324Z;Lycée Professionnel;Rue Du Lycée;81207;Mazamet;0.0;0.0
+0020025S;Lycée Professionnel;Chateau Potel;02460;La Ferte Milon;49.177915;3.124745
+0110027V;Lycée Professionnel;1 Av Edouard Herriot;11500;Quillan;42.875129;2.18546
+0120036Z;Lycée Professionnel;Avenue Leo Lagrange;12300;Decazeville;44.5676272;2.2523499
+0490020F;Lycée Professionnel;Rue De La Tuilerie;49321;Cholet;47.0709727;-0.8903426
+0500090B;Lycée Professionnel;Rue Dauphine;50600;St Hilaire Du Harcouet;48.5715264;-1.0976365
+0542306N;Lycée Professionnel;4 Rue De La Tuilerie;54803;Jarny;49.1581264;5.8767019
+0590123N;Lycée Professionnel;115 Rue Francisco Ferrer;59007;Lille;50.63052;3.0934521
+0590125R;Lycée Professionnel;1 Rue Michel Servet;59003;Lille;50.628714;3.0361729
+0593496E;Lycée Professionnel;89 Rue De La Chaussiette;59163;Conde Sur L Escaut;50.4598956;3.5860573
+0620030W;Lycée Professionnel;60 Route D'albert;62452;Bapaume;50.1059853;2.8437041
+0620191W;Lycée Professionnel;800 Rue Leon Blum;62251;Henin Beaumont;50.4306013;2.9485013
+0622240Y;Lycée Professionnel;1 Rue Porte Becquerelle;62170;Montreuil;50.4644173;1.7663501
+0720028P;Lycée Professionnel;Rue Jean Jaures;72600;Mamers;48.349964;0.363104
+0750786X;Lycée Professionnel;92 96 Rue Barrault;75013;Paris 13;48.8230263;2.3467166
+0752608C;Lycée Professionnel;135 Rue Belliard;75018;Paris 18;48.8962641;2.3344205
+0800060F;Lycée Professionnel;Avenue Robert Solente;80301;Albert;49.9999141;2.6573268
+0850029V;Lycée Professionnel;29 Boulevard Guitton;85020;La Roche Sur Yon;46.658489;-1.4330727
+0880031F;Lycée Professionnel;Route De La Rochotte;88407;Gerardmer;48.064932;6.8739463
+0950709E;Lycée Professionnel;100 Av Charles Vaillant;95400;Arnouville Les Gonesse;48.9854345;2.4073595
+0750770E;Lycée Professionnel Abbe Gregoire;70 Bis Rue De Turbigo;75003;Paris 03;48.8661184;2.3593985
+0131709M;Lycée Professionnel Adam De Craponne;218 Rue Chateau Redon;13665;Salon De Provence;43.6355195;5.0986535
+0031047X;Lycée Professionnel Agricole;;03310;Durdat Larequille;46.251014;2.69973
+0071231A;Lycée Professionnel Agricole;Montee Du Savel;07100;Annonay;45.2407678;4.675018
+0071234D;Lycée Professionnel Agricole;19 Av Victor Descours;07270;Lamastre;44.9871441;4.5842853
+0350940G;Lycée Professionnel Agricole;La Lande De La Rencontre;35140;St Aubin Du Cormier;48.2645709;-1.428232
+0382375A;Lycée Professionnel Agricole;5 Rue De La Republique;38440;St Jean De Bournay;45.5023241;5.1423932
+0740276Y;Lycée Professionnel Agricole;;74130;Contamine Sur Arve;46.128077;6.34251
+0150599V;Lycée Professionnel Agricole;Domaine De Volzac;15100;St Flour;45.024512;3.06915
+0190624A;Lycée Professionnel Agricole;;19460;Naves;45.312913;1.767888
+0370794M;Lycée Professionnel Agricole;104 Avenue De La Republique;37170;Chambray Les Tours;47.3405814;0.7240973
+0381818V;Lycée Professionnel Agricole;La Marteliere;38500;Voiron;45.377585;5.592192
+0381888W;Lycée Professionnel Agricole;164 Allee Louis Clerget;38110;La Tour Du Pin;45.565881;5.4382031
+0410626H;Lycée Professionnel Agricole;6 Rue De L Agriculture;41800;Montoire Sur Le Loir;47.7495268;0.8763012
+0430111M;Lycée Professionnel Agricole;Avenue De L'hermitage;43000;Espaly St Marcel;45.055765;3.86089
+0450027K;Lycée Professionnel Agricole;7 Rue Des Deportes;45340;Beaune La Rolande;48.0709911;2.433444
+0450836P;Lycée Professionnel Agricole;11 Rue Des Pervenches;45270;Bellegarde;47.9881528;2.438748
+0470107L;Lycée Professionnel Agricole;Fazanis;47400;Tonneins;44.37731;0.348298
+0501234V;Lycée Professionnel Agricole;Route De Fougeres;50600;St Hilaire Du Harcouet;48.5683384;-1.095299
+0520741Y;Lycée Professionnel Agricole;5 Ruelle Aux Loups;52500;Fayl Billot;47.7791736;5.6033372
+0530520C;Lycée Professionnel Agricole;4o Route De Sable;53200;Chateau Gontier;47.8306463;-0.6922463
+0601265Z;Lycée Professionnel Agricole;91 Rue Andre Regnier;60170;Ribecourt Dreslincourt;49.5135257;2.9208686
+0610746E;Lycée Professionnel Agricole;Avenue Du General Leclerc;61000;Alencon;48.4172116;0.089494
+0631225P;Lycée Professionnel Agricole;Le Marchedial;63210;Rochefort Montagne;45.6864149;2.800831
+0640253C;Lycée Professionnel Agricole;Route De Bordeaux;64300;Orthez;43.4943844;-0.7597037
+0640254D;Lycée Professionnel Agricole;Soeix;64404;Oloron Ste Marie;43.1629834;-0.5907772
+0650138X;Lycée Professionnel Agricole;59 Route De Pau;65000;Tarbes;43.2381467;0.0400529
+0671779V;Lycée Professionnel Agricole;33 Avenue De La Gare;67152;Erstein;48.419731;7.6515589
+0711073F;Lycée Professionnel Agricole;Les Perrieres;71700;Tournus;46.5636067;4.892747
+0711120G;Lycée Professionnel Agricole;Chemin D'ouze;71120;Charolles;46.4307098;4.2649363
+0790767E;Lycée Professionnel Agricole;130 Route De Coulonges;79011;Niort;46.3410959;-0.4743859
+0870590U;Lycée Professionnel Agricole;Route De La Souterraine;87190;Magnac Laval;46.214577;1.166048
+0870671G;Lycée Professionnel Agricole;Domaine De La Faye;87500;St Yrieix La Perche;45.511306;1.249044
+0890849P;Lycée Professionnel Agricole;1 Avenue Du Docteur Schweitzer;89290;Champs Sur Yonne;47.7347694;3.603143
+0771357G;Lycée Professionnel Agricole Agricole;La Bretonniere;77120;Chailly En Brie;48.813781;3.152278
+0131848N;Lycée Professionnel Agricole Amenagement Paysager (de L');89 Traverse Parangon;13008;Marseille 08;43.2463268;5.3798313
+0720907V;Lycée Professionnel Agricole Andre Provots;Le Haut Bois;72250;Brette Les Pins;47.917679;0.332145
+0561758G;Lycée Professionnel Agricole Antenne Lpa St Jean Brevelay;76 Rue Du Talhouet;56701;Hennebont;47.8168284;-3.2640287
+0030118M;Lycée Professionnel Agricole Antoine Brun;44 Avenue Charles De Gaulle;03120;Lapalisse;46.2443117;3.6338821
+0021523V;Lycée Professionnel Agricole Aumont;Place Foch;02380;Coucy La Ville;49.533112;3.328146
+0382376B;Lycée Professionnel Agricole Bellevue;4 Rue Des Recollets;38160;St Marcellin;45.1563352;5.3213845
+0170394L;Lycée Professionnel Agricole Chadignac;Le Petit Chadignac;17119;Saintes;45.721281;-0.663772
+0341059S;Lycée Professionnel Agricole Charles Marie De La Condamine;4 Allee General Montagne;34120;Pezenas;43.4618994;3.4197081
+0660039J;Lycée Professionnel Agricole Claude Simon;4 Rue Pasteur;66602;Rivesaltes;42.7661082;2.8699034
+0595770B;Lycée Professionnel Agricole Communaute Urbaine Dunkerque;1972 Rue De Leffrinckoucke;59240;Dunkerque;51.0462169;2.4456828
+0860818X;Lycée Professionnel Agricole Danielle Mathiron;Domaine Des Chevaliers;86540;Thure;46.831987;0.459635
+0400139J;Lycée Professionnel Agricole De Chalosse;Route De Pomarez;40250;Mugron;43.751052;-0.748762
+0730812L;Lycée Professionnel Agricole De Cognin;13 Avenue Henry Bordeaux;73160;Cognin;45.5643012;5.8933314
+0801335S;Lycée Professionnel Agricole De La Baie De Somme;21 Rue Du Lieutenant Caron;80100;Abbeville;50.1108191;1.8427116
+0801328J;Lycée Professionnel Agricole De La Haute Somme;10 Rue Du Quinconce;80200;Peronne;49.9358497;2.9280029
+0421210M;Lycée Professionnel Agricole De Montravel-villars;Montravel;42390;Villars;45.4751884;4.3506677
+9840277D;Lycée Professionnel Agricole De Opunohu;A;98728;Moorea Maiao;-17.6555246;-150.6343154
+0580054U;Lycée Professionnel Agricole De Plagny;128 Rte De Lyon;58000;Sermoise Sur Loire;46.9632774;3.1571413
+9740097M;Lycée Professionnel Agricole De Saint Joseph;24 Rue Raphael Babet;97480;St Joseph;-21.3766259;55.6161038
+0631223M;Lycée Professionnel Agricole Des Combrailles;Avenue De La Gare;63390;St Gervais D Auvergne;46.030667;2.814032
+0040056P;Lycée Professionnel Agricole Digne-carmejane;Route D Espinouse;04510;Le Chaffaut St Jurson;44.047146;6.158502
+0763004C;Lycée Professionnel Agricole Du Bois;Rue Du General De Gaulle;76630;Envermeu;49.8930353;1.2701613
+0752426E;Lycée Professionnel Agricole Du Breuil-ec.hort.et Arbor.;Route De La Ferme;75012;Paris 12;48.8235759;2.4572633
+0580582T;Lycée Professionnel Agricole Du Morvan;Rue Mendes France;58120;Chateau Chinon Ville;47.059639;3.938102
+0761767H;Lycée Professionnel Agricole Du Pays De Bray;Le Chateau;76220;Bremontier Merval;49.494288;1.652919
+0261069W;Lycée Professionnel Agricole Du Tricastin-baronnies;25 Le Courreau;26130;St Paul Trois Chateaux;44.3472476;4.7671547
+0171590L;Lycée Professionnel Agricole Ensmic;Avenue Francois Mitterrand;17700;Surgeres;46.1087429;-0.7680787
+0460661U;Lycée Professionnel Agricole Epla De Cahors;Lacoste;46090;Le Montat;44.377312;1.441901
+0810578N;Lycée Professionnel Agricole Epla De Lavaur;Domaine De Flamarens;81500;Lavaur;43.694745;1.864111
+0320557G;Lycée Professionnel Agricole Epla De Mirande;Domaine De Valentees;32300;Mirande;43.516823;0.403639
+0320646D;Lycée Professionnel Agricole Epla De Mirande;Route De Pau;32400;Riscle;43.658594;-0.0868189
+0820531G;Lycée Professionnel Agricole Epla De Moissac;Avenue Du Sarlac;82201;Moissac;44.101327;1.09785
+0120938E;Lycée Professionnel Agricole Epla De Saint-affrique;Route De Bournac;12400;St Affrique;43.9578528;2.8419831
+0311268K;Lycée Professionnel Agricole Epla Saint-gaudens;16 Rue Olivier De Serres;31806;St Gaudens;43.1174843;0.730278
+0160108F;Lycée Professionnel Agricole Felix Gaillard;Chez Fouquet;16300;Salles De Barbezieux;45.4515173;-0.1120687
+9720779P;Lycée Professionnel Agricole Four A Chaux;Four A Chaux;97231;Le Robert;14.6647441;-60.936892
+0271108H;Lycée Professionnel Agricole Gilbert Martin;Rue Pierre Corneille;27110;Le Neubourg;49.146564;0.907873
+0341001D;Lycée Professionnel Agricole Honore De Balzac;Avenue De La Galine;34172;Castelnau Le Lez;43.630843;3.902609
+0861145C;Lycée Professionnel Agricole Inst Maison Famil Vienne;47 Route De Montmorillon;86300;Chauvigny;46.5635216;0.646916
+0161002C;Lycée Professionnel Agricole Inst Rur Education Orientation;;16370;Cherves Richemont;45.744433;-0.356736
+0860820Z;Lycée Professionnel Agricole Jean Marie Bouloux;Chateau Ringuet;86501;Montmorillon;46.4163527;0.8441393
+0741556P;Lycée Professionnel Agricole Jeanne Antide;55 Impasse Du Brevent;74930;Reignier;46.1310261;6.2712676
+0370878D;Lycée Professionnel Agricole La Gabilliere;46 Av Emile Gounin;37403;Amboise;47.3929481;0.9793789
+0071239J;Lycée Professionnel Agricole La Pelissiere Vallee Du Rhone;4 Rue Du Repos;07300;Tournon Sur Rhone;45.0652226;4.8340225
+0861122C;Lycée Professionnel Agricole La Perriere;La Perriere;86200;La Roche Rigault;46.974342;0.186944
+0840606C;Lycée Professionnel Agricole La Ricarde;Avnue Jean Boin;84800;L Isle Sur La Sorgue;43.9219696;5.0448297
+0560274U;Lycée Professionnel Agricole Le Suillo;;56660;St Jean Brevelay;47.845229;-2.720734
+0790977H;Lycée Professionnel Agricole Le Val De L Ouin;3 Au 9 Rue De La Sagesse;79700;Mauleon;46.9223173;-0.7528173
+0382369U;Lycée Professionnel Agricole Le Vallon;13 Rue De L Eglise;38690;Chabons;45.4443564;5.4306894
+0595771C;Lycée Professionnel Agricole Legta Du Nord;Avenue Du Chateau;59590;Raismes;50.3959279;3.4830104
+0382370V;Lycée Professionnel Agricole Les Alpes;42 Rue Des Alpes;38350;La Mure;44.9054002;5.7895942
+0131715U;Lycée Professionnel Agricole Les Alpilles;Rue Edouard Heriot;13210;St Remy De Provence;43.790754;4.835772
+0830184Z;Lycée Professionnel Agricole Les Arcs;Route De Draguignan D555;83460;Les Arcs;43.4855012;6.4849386
+0741225E;Lycée Professionnel Agricole Les Roselieres;3260 Route D Albertville;74320;Sevrier;45.8541292;6.1438701
+0741224D;Lycée Professionnel Agricole Les Trois Vallees;2 Avenue De L'ermitage;74200;Thonon Les Bains;46.3679703;6.48484
+0160981E;Lycée Professionnel Agricole Ltpr Claire Champagne;1 Rue Malestiers;16130;Segonzac;45.61808;-0.2231605
+0261315N;Lycée Professionnel Agricole Lycée De Val De Drome;Route De Valence N;26760;Monteleger;44.868636;4.9412779
+0390809T;Lycée Professionnel Agricole Mancy;410 Montee Gauthier Villars;39015;Lons Le Saunier;46.6644134;5.5482578
+0550754W;Lycée Professionnel Agricole Martial Brousse;Place Saint Paul;55100;Verdun;49.163828;5.3843585
+0110710M;Lycée Professionnel Agricole Martin Luther King;Centre Pierre Reverdy;11782;Narbonne;43.184277;3.003078
+0110712P;Lycée Professionnel Agricole Martin Luther King;36 Avenue De Narbonne;11220;St Laurent De La Cabrerisse;43.0856549;2.7015968
+0490963F;Lycée Professionnel Agricole Montreuil-bellay;Rte De Meron;49260;Montreuil Bellay;47.1312384;-0.1332827
+0861123D;Lycée Professionnel Agricole Odile Pasquier;5 Place De La Marne;86700;Couhe;46.3002383;0.1827319
+0382371W;Lycée Professionnel Agricole Paul Claudel;16 Rue Du Marche Vieux;38460;Cremieu;45.7264346;5.253525
+0320530C;Lycée Professionnel Agricole Pavie;;32550;Pavie;43.609862;0.591676
+9840356P;Lycée Professionnel Agricole Privé Anne Marie Javourey;Place Notre Dame;98714;Papeete;-17.535022;-149.569594
+9760181T;Lycée Professionnel Agricole Privé De Coconi;;97670;Chiconi;43.4865563;-0.7687041
+0221032C;Lycée Professionnel Agricole Public;Route De Dinan;22350;Caulnes;48.2968278;-2.1519311
+9760206V;Lycée Professionnel Agricole Public De Coconi;;97670;Chiconi;43.4865563;-0.7687041
+0382378D;Lycée Professionnel Agricole Saint Exupery;264 Rue Des Frenes;38870;St Simeon De Bressieux;45.3301563;5.2612136
+9730364H;Lycée Professionnel Agricole Savane Matiti;Savane Matiti Macouria;97355;Macouria;4.9155544;-52.3693218
+0261065S;Lycée Professionnel Agricole Val De Drome;Route De Valence;26760;Monteleger;44.868636;4.9412779
+0711815M;Lycée Professionnel Agricole Velet;Route De St Didier-sur-arroux;71190;Etang Sur Arroux;46.8627761;4.1780787
+0141421U;Lycée Professionnel Agricole Vire;Route De Caen;14500;Vire;48.8541038;-0.8794987
+0840218F;Lycée Professionnel Agricole Viticole;Mas Mongin Quartier Du Gres;84100;Orange;44.1380989;4.807511
+0640028H;Lycée Professionnel Aizpurdi;1 Les Allees;64702;Hendaye;43.3570403;-1.7742956
+0570061G;Lycée Professionnel Alain Fournier;Rue Emile Boilvin;57000;Metz;49.10305;6.1761419
+0595787V;Lycée Professionnel Alain Savary;Rue Alain Savary;59391;Wattrelos;50.7099702;3.2012067
+0620257T;Lycée Professionnel Alain Savary;14 Avenue Jean Zay;62022;Arras;50.2838793;2.7487122
+0550026E;Lycée Professionnel Alain-fournier;12 Av President Kennedy;55107;Verdun;49.1447039;5.3973437
+0251557G;Lycée Professionnel Albert Camus;Impasse Camus;25200;Bethoncourt;47.537051;6.8015439
+0420074C;Lycée Professionnel Albert Camus;32 Bis Rue De La Loire;42704;Firminy;45.3883922;4.2830196
+0440056X;Lycée Professionnel Albert Chassagne;5 Rue Alexis Maneyrol;44560;Paimboeuf;47.2866375;-2.0259524
+0030061A;Lycée Professionnel Albert Londres;Avenue De La Liberation;03306;Cusset;46.1306952;3.4360709
+0572756L;Lycée Professionnel Albert Schweitzer;44 Rue Schellenthal;57230;Bitche;49.0567339;7.4180043
+0420077F;Lycée Professionnel Albert Thomas;20 Rue Albert Thomas;42328;Roanne;46.0440474;4.0740983
+0710080B;Lycée Professionnel Alexandre Dumaine;95 Espace Alexandre Dumaine;71018;Macon;46.30415;4.833486
+0840113S;Lycée Professionnel Alexandre Dumas;Rue Alphonse Jauffret;84300;Cavaillon;43.8326963;5.0287304
+0500089A;Lycée Professionnel Alexis De Tocqueville;34 Avenue Henri Poincare;50100;Cherbourg Octeville;49.6304753;-1.6077873
+0931198X;Lycée Professionnel Alfred Costes;146 Avenue Henri Barbusse;93000;Bobigny;48.9083628;2.42744
+0690107S;Lycée Professionnel Alfred De Musset;128 Rue De La Poudrette;69100;Villeurbanne;45.75696;4.9211312
+0061561P;Lycée Professionnel Alfred Hutinel;21 Rue De Cannes;06150;Cannes;43.5533656;6.970213
+0590264S;Lycée Professionnel Alfred Kastler;123 Rue Paul Elie Casanova;59723;Denain;50.3293842;3.4072591
+0801704T;Lycée Professionnel Alfred Manessier;33 Rue De La Resistance;80420;Flixecourt;50.0091454;2.0757544
+0540077R;Lycée Professionnel Alfred Mezieres;3 Avenue Andre Malraux;54401;Longwy;49.5216573;5.7601943
+0040007L;Lycée Professionnel Alphonse Beau De Rochas;10 Avenue General Leclerc;04000;Digne Les Bains;43.0535129;2.2158732
+0622174B;Lycée Professionnel Alphonse Daudet;Rue Alphonse Daudet;62620;Barlin;50.456584;2.619812
+0350009V;Lycée Professionnel Alphonse Pelle;30 Rue Montplaisir;35120;Dol De Bretagne;48.5435906;-1.7620966
+0130146N;Lycée Professionnel Alpilles (les);Quartier Les Molieres;13140;Miramas;43.588896;5.002136
+0261100E;Lycée Professionnel Amblard;43 Rue Amblard;26000;Valence;44.9278503;4.9011313
+0400049L;Lycée Professionnel Ambroise Croizat;92 Av Marcel Paul;40220;Tarnos;43.5355002;-1.4901891
+0594532F;Lycée Professionnel Ambroise Croizat;Rue Marcel Paul;59950;Auby;50.4150948;3.0507573
+0740062R;Lycée Professionnel Amedee Gordini;31 Route De Sacconges;74602;Seynod;45.8851811;6.109274
+9740479C;Lycée Professionnel Amiral Lacaze;Rue Stanislas Gimart;97493;St Denis;48.936181;2.357443
+0130072H;Lycée Professionnel Ampere;56 Bd Romain Rolland;13395;Marseille 10;43.2796927;5.4237647
+0560019S;Lycée Professionnel Ampere;Rue Guethennoc;56120;Josselin;47.9598749;-2.547896
+0720064D;Lycée Professionnel Ampere;Boulevard Du Quebec;72206;La Fleche;47.7087173;-0.0792501
+0600049C;Lycée Professionnel Amyot D Inville;Chemin De Reims;60309;Senlis;49.192707;2.5701139
+0330011H;Lycée Professionnel Anatole De Monzie;12 Cours Gambetta;33430;Bazas;44.4332382;-0.213939
+9720515C;Lycée Professionnel Andre Aliker;34 Boulevard Amilcar Cabral;97200;Fort De France;14.609371;-61.07256
+0381605N;Lycée Professionnel Andre Argouges;61 Rue Leon Jouhaux;38029;Grenoble;45.1753906;5.7445124
+0440315D;Lycée Professionnel Andre Boulloche;32 Rue Du Plessis;44606;St Nazaire;47.2873869;-2.2243657
+0640031L;Lycée Professionnel Andre Campa;29 Avenue Joliot Curie;64110;Jurancon;43.2913485;-0.4011011
+0692968B;Lycée Professionnel Andre Cuzin;42 Chemin De Crepieux;69300;Caluire Et Cuire;45.8010809;4.8573366
+0594653M;Lycée Professionnel Andre Jurenil;Boulevard Du 8 Mai 1945;59723;Denain;50.32402;3.396566
+0622089J;Lycée Professionnel Andre Malraux;314 Rue Jules Massenet;62408;Bethune;50.5200303;2.6516077
+0911037H;Lycée Professionnel Andre-marie Ampere;12b A 26 Rte Du Bois Pommier;91390;Morsang Sur Orge;48.6571262;2.3453375
+0810018E;Lycée Professionnel Anne Veaute;46 Boulevard Magenta;81104;Castres;43.6008543;2.2348464
+0480505N;Lycée Professionnel Annexe Lp E Peytavin Mende;Quartier Des Chauvets;48300;Langogne;44.727236;3.855416
+0211356K;Lycée Professionnel Antoine Antoine;5 Rue De Longvic;21303;Chenove;47.2897502;5.00852
+0592850C;Lycée Professionnel Antoine De Saint Exupery;1 Rue Louis Pergaud;59432;Halluin;50.7783564;3.1134241
+0470004Z;Lycée Professionnel Antoine Lomet;221 Av D Italie;47000;Agen;44.1865224;0.6245447
+0762765T;Lycée Professionnel Antoine-laurent Lavoisier;Rue Des Moteaux;76620;Le Havre;49.5263165;0.1002857
+0140005E;Lycée Professionnel Arcisse De Caumont;3 Rue Baron Gerard;14402;Bayeux;49.2843359;-0.7097585
+0840763Y;Lycée Professionnel Argensol (quartier De L');Rue Henri Dunant;84106;Orange;44.1317994;4.8348234
+0090019X;Lycée Professionnel Aristide Berges;Avenue De La Gare;09201;St Girons;43.6583119;-0.5866688
+0271268G;Lycée Professionnel Aristide Briand;2 Rue Pierre Semard;27031;Evreux;49.0168441;1.1602102
+0670062D;Lycée Professionnel Aristide Briand;12 Rue Du Barrage;67301;Schiltigheim;48.6038727;7.7515437
+0930831Y;Lycée Professionnel Aristide Briand;120 Avenue Aristide Briand;93155;Le Blanc Mesnil;48.94482;2.4493596
+0840046U;Lycée Professionnel Aristide Briand (cours);7 Cours Aristide Briand;84100;Orange;44.138288;4.8045546
+0752700C;Lycée Professionnel Armand Carrel;45 Rue Armand Carrel;75019;Paris 19;48.8827398;2.3759898
+0080028M;Lycée Professionnel Armand Malaise;84 Rue Du Bois Fortant;08003;Charleville Mezieres;49.7465138;4.7139474
+0240050Z;Lycée Professionnel Arnaud Daniel;Rue Couleau;24600;Riberac;45.245019;0.336231
+0601822E;Lycée Professionnel Arthur Rimbaud;319 Rue Aristide Briand;60170;Ribecourt Dreslincourt;49.5074328;2.9250178
+0931738J;Lycée Professionnel Arthur Rimbaud;112-114 Avenue Jean Jaures;93120;La Courneuve;48.9219026;2.4080165
+0520008B;Lycée Professionnel Ashton;47 Avenue D'ashton Under Lyne;52000;Chaumont;48.0919255;5.143953
+0710087J;Lycée Professionnel Astier;Rue De Bourgogne;71600;Paray Le Monial;46.4628376;4.1041988
+0810047L;Lycée Professionnel Aucouturier;Route De Blaye;81400;Carmaux;44.0511877;2.1480562
+0430023S;Lycée Professionnel Auguste Aymard;2 Rue Saint Marcel;43000;Espaly St Marcel;45.0490932;3.8675833
+0620110H;Lycée Professionnel Auguste Behal;6 Rue Paul Eluard;62300;Lens;50.4456289;2.8230467
+0260044G;Lycée Professionnel Auguste Bouvet;10 Rue Bouvet;26102;Romans Sur Isere;45.0481031;5.0466555
+0061635V;Lycée Professionnel Auguste Escoffier;Chemin Du Brecq;06801;Cagnes Sur Mer;43.6713865;7.1486137
+0951618T;Lycée Professionnel Auguste Escoffier;77 Rue De Pierrelaye;95610;Eragny;49.0215662;2.1093175
+0770944H;Lycée Professionnel Auguste Perdonnet;1 Allee Du Chateau;77407;Lagny Sur Marne;48.8541166;2.6465725
+0911343R;Lycée Professionnel Auguste Perret;Avenue De La Liberte;91024;Evry;48.6405766;2.4346873
+9710746J;Lycée Professionnel Augustin Arron;Rue De La Republique;97122;Baie Mahault;16.2655166;-61.5871614
+0271606Z;Lycée Professionnel Augustin Boismard;Rue Emile Neuville;27800;Brionne;49.197356;0.713757
+0270018Y;Lycée Professionnel Augustin Hebert;20 Rue Dugesclin;27025;Evreux;49.0193767;1.1716123
+9830460H;Lycée Professionnel Augustin Ty;Touho;98831;Touho;-20.7797486;165.2385163
+0590144L;Lycée Professionnel Automobile Alfred Mongy;129 Rue De La Briqueterie;59700;Marcq En Baroeul;50.6678924;3.0819707
+0594400M;Lycée Professionnel Automobile Et Transports;Rue Alexis Carrel;59791;Grande Synthe;51.0042875;2.3198217
+0630073M;Lycée Professionnel B.t.p;29 Route De Marsat;63530;Volvic;45.8754617;3.0454878
+0690003D;Lycée Professionnel Barthelemy Thimonnier;160 Avenue Andre Lassagne;69592;L Arbresle;45.8343637;4.6130454
+0700019R;Lycée Professionnel Bartholdi;31 Rue Du Dr Deubel;70202;Lure;47.6844457;6.4874168
+9720424D;Lycée Professionnel Bateliere;1 Rue Mon Ideal;97233;Schoelcher;0.0;0.0
+0060038J;Lycée Professionnel Batiment;17 Boulevard Pierre Sola;06300;Nice;43.7074475;7.2861065
+0750793E;Lycée Professionnel Beaugrenelle;62 Rue Saint Charles;75015;Paris 15;48.8482389;2.2868428
+0593187U;Lycée Professionnel Beaupre;Avenue De Beaupre;59481;Haubourdin;50.6018;2.9906864
+0371211R;Lycée Professionnel Beauregard;15 Rue Andre Bauchant;37110;Chateau Renault;47.585999;0.906274
+0420075D;Lycée Professionnel Beauregard;4 Avenue Paul Cezanne;42605;Montbrison;45.6137248;4.0570332
+0700024W;Lycée Professionnel Beauregard;32 Rue P Rimey;70306;Luxeuil Les Bains;47.8220301;6.3674511
+0350050P;Lycée Professionnel Bel Air;7 Rue Ernest Renan;35190;Tinteniac;48.3324662;-1.8241016
+0420049A;Lycée Professionnel Benoit Charvet;30 Avenue Benoit Charvet;42021;St Etienne;45.4455705;4.3772934
+0470641S;Lycée Professionnel Benoit D Azy;Av Charles De Gaulle;47501;Fumel;44.498344;0.958251
+0420958N;Lycée Professionnel Benoit Fourneyron;24 Rue Virgile;42014;St Etienne;45.4176774;4.4197149
+0620124Y;Lycée Professionnel Bernard Chochoy;Rue Marx Dormoy;62380;Lumbres;50.709505;2.129646
+0170078T;Lycée Professionnel Bernard Palissy;1 Rue De Gascogne;17107;Saintes;45.7360174;-0.6152519
+0760022L;Lycée Professionnel Bernard Palissy;5 Sente Aux Loups;76152;Maromme;49.481859;1.044099
+0190674E;Lycée Professionnel Bernart De Ventadour;Boulevard De La Jaloustre;19200;Ussel;45.541611;2.315461
+0540085Z;Lycée Professionnel Bertrand Schwartz;5 Rue Ste Anne;54340;Pompey;48.7738149;6.107286
+9710090W;Lycée Professionnel Blachon Lamentin;Blachon;97129;Lamentin;0.0;0.0
+0130059U;Lycée Professionnel Blaise Pascal;49 Traverse Capron;13012;Marseille 12;43.313125;5.4208382
+0170052P;Lycée Professionnel Blaise Pascal;11 Rue De Dampierre;17415;St Jean D Angely;45.9498953;-0.5125652
+0490785M;Lycée Professionnel Blaise Pascal;2 Rue Du Lycée;49502;Segre;0.0;0.0
+0520029Z;Lycée Professionnel Blaise Pascal;1 Avenue Marcel Paul;52100;St Dizier;48.6456201;4.9737363
+0570031Z;Lycée Professionnel Blaise Pascal;5 Rue Paul Ney;57608;Forbach;49.1806835;6.8958864
+0890057D;Lycée Professionnel Blaise Pascal;20 Ter Avenue Des Cosmonautes;89400;Migennes;47.9674694;3.5100005
+0810962F;Lycée Professionnel Borde Basse;Cite Scolaire;81108;Castres;43.606214;2.241295
+0190008F;Lycée Professionnel Bort-artense;259 Rue Du Lycée;19110;Bort Les Orgues;0.0;0.0
+0820032P;Lycée Professionnel Bourdelle;3 Boulevard Edouard Herriot;82003;Montauban;44.0128398;1.3704149
+0750794F;Lycée Professionnel Brassai;8 Rue Quinault;75015;Paris 15;48.844121;2.2984015
+0350059Z;Lycée Professionnel Brequigny;7 Avenue Georges Graff;35205;Rennes;48.0836213;-1.6920985
+0440074S;Lycée Professionnel Brossaud-blancho;10 Bd Pierre De Coubertin;44616;St Nazaire;47.2693774;-2.2334295
+0131606A;Lycée Professionnel Calade (la);430 Che De La Madrague Ville;13015;Marseille 15;43.3408984;5.3530112
+0020088K;Lycée Professionnel Camille Claudel;9 Rue De Panleu;02209;Soissons;49.3786895;3.3245369
+0140019V;Lycée Professionnel Camille Claudel;57 Av Mal De Lattre De Tassigny;14070;Caen;49.2034569;-0.3849055
+0590260M;Lycée Professionnel Camille Claudel;1 Rue Paul Lafargue;59613;Fourmies;50.0122895;4.0404278
+0631409P;Lycée Professionnel Camille Claudel;4 Rue De La Charme;63039;Clermont Ferrand;45.7981945;3.1198096
+0690125L;Lycée Professionnel Camille Claudel;15 Rue De Cuire;69004;Lyon 04;45.7765856;4.8303022
+0881140L;Lycée Professionnel Camille Claudel;2 Rue Du Parmont;88202;Remiremont;48.014224;6.5852025
+0940145X;Lycée Professionnel Camille Claudel;4 Rue Des Carrieres;94400;Vitry Sur Seine;48.7998475;2.3944324
+0501860A;Lycée Professionnel Camille Corot;242 Rue De L'exode;50015;St Lo;49.1092676;-1.0899166
+0750419Y;Lycée Professionnel Camille Jenatzy;6 Rue Charles Hermite;75018;Paris 18;48.8998372;2.3697896
+0130068D;Lycée Professionnel Camille Jullian;50 Boulevard De La Barasse;13396;Marseille 11;43.2859618;5.4774775
+0670043H;Lycée Professionnel Camille Schneider;13 Avenue De La Gare;67120;Molsheim;48.5387452;7.4970582
+0420076E;Lycée Professionnel Carnot;35 Avenue Carnot;42300;Roanne;46.0419603;4.0699904
+9710722H;Lycée Professionnel Carnot;28 Rue Jean Jaures;97110;Pointe A Pitre;0.0;0.0
+0790928E;Lycée Professionnel Carrosserie G Barre;Rue Jean Perrin;79010;Niort;46.315039;-0.439958
+0590266U;Lycée Professionnel Cesar Baggio;Boulevard D'alsace;59000;Lille;50.6177376;3.0676499
+0220070G;Lycée Professionnel Chaptal;6 Allee Chaptal;22015;St Brieuc;48.5125105;-2.7377027
+0240012H;Lycée Professionnel Chardeuil Metiers Du Batiment;Batiment Chardeuil;24420;Coulaures;45.304424;0.955207
+0160047P;Lycée Professionnel Charles A Coulomb;Avenue Joachim Du Bellay;16016;Angouleme;45.6303964;0.1570913
+0340061G;Lycée Professionnel Charles Allies;24 Boulevard Joliot Curie;34120;Pezenas;43.4561047;3.417854
+0771880A;Lycée Professionnel Charles Baudelaire;Bd Du Chevalier Bayard;77333;Meaux;48.954379;2.914558
+0911254U;Lycée Professionnel Charles Baudelaire;Avenue De La Liberte;91000;Evry;48.6405766;2.4346873
+0660015H;Lycée Professionnel Charles Blanc;Rue Charles Blanc;66000;Perpignan;42.694663;2.9296329
+0110823K;Lycée Professionnel Charles Cros;1 Rue Michel Verges;11870;Carcassonne;43.2278833;2.3385008
+0720049M;Lycée Professionnel Charles Cros;Rue St Denis;72305;Sable Sur Sarthe;47.838785;-0.342691
+0601871H;Lycée Professionnel Charles De Gaulle;Rue Jacques Daguerre;60321;Compiegne;49.387482;2.7840919
+0750508V;Lycée Professionnel Charles De Gaulle;17 Rue Ligner;75020;Paris 20;48.8580201;2.3973207
+0570088L;Lycée Professionnel Charles Jully;59 Rue Marechal Foch;57501;St Avold;49.1057612;6.6927566
+0130151U;Lycée Professionnel Charles Mongrand;10 Bd Cristofol;13110;Port De Bouc;43.405449;4.985931
+0330076D;Lycée Professionnel Charles Peguy;2 Rue Du College Technique;33326;Eysines;44.8886389;-0.626501
+0680074L;Lycée Professionnel Charles Pointet;5 Rue Des Tirailleurs Marocains;68802;Thann;47.810702;7.102382
+0130171R;Lycée Professionnel Charles Privat;10 Rue Lucien Guintoli;13632;Arles;43.6705402;4.6268451
+0680037W;Lycée Professionnel Charles Stoessel;1 Rue Du Fil;68068;Mulhouse;47.7478368;7.3198372
+0351054F;Lycée Professionnel Charles Tillon;9 Allee Georges Palante;35009;Rennes;48.1069004;-1.6607643
+0590085X;Lycée Professionnel Charlotte Perriand;Chateau Du Bois;59242;Genech;50.523207;3.218428
+0451442Y;Lycée Professionnel Chateau Blanc;1 Rue Saint Just;45120;Chalette Sur Loing;48.0043061;2.7179901
+9720501M;Lycée Professionnel Chateauboeuf;Z A C De Chateauboeuf;97255;Fort De France;14.62199;-61.0428795
+0360003H;Lycée Professionnel Chateauneuf;83 Avenue Rollinat;36200;Argenton Sur Creuse;46.5824816;1.514488
+0130055P;Lycée Professionnel Chatelier (le);108 Avenue Roger Salengro;13003;Marseille 03;43.3145473;5.3705429
+0750783U;Lycée Professionnel Chenneviere Malezieux;31 35 Avenue Ledru Rollin;75012;Paris 12;48.8474871;2.37054
+0490059Y;Lycée Professionnel Chevrollier;2 Rue Adrien Recouvreur;49001;Angers;47.4543325;-0.5571808
+0790038M;Lycée Professionnel Cite Scolaire Jean Moulin;Rue Albert Buisson;79101;Thouars;46.9937436;-0.2131568
+0590268W;Lycée Professionnel Cite Scolaire Pierre Forest;Boulevard Charles De Gaulle;59605;Maubeuge;50.2784762;3.9816647
+0750436S;Lycée Professionnel Claude Anthime Corbon;5 Rue Corbon;75015;Paris 15;48.8363768;2.303238
+0720003M;Lycée Professionnel Claude Chappe;Rue Des Colleges;72231;Arnage;47.935313;0.185062
+0921626T;Lycée Professionnel Claude Chappe;54 A 80 Rue Des Alouettes;92000;Nanterre;48.8830353;2.2112838
+0430028X;Lycée Professionnel Claude Favard;6 Rue Jean Catinot;43250;Ste Florine;45.4024955;3.3243487
+0420078G;Lycée Professionnel Claude Lebois;8 Boulevard Alamagny;42403;St Chamond;45.4685576;4.5111391
+0761322Z;Lycée Professionnel Claude Monet;267 Rue Felix Faure;76085;Le Havre;49.5009436;0.110217
+0320030J;Lycée Professionnel Clement Ader;10 Rue Du Chemin Neuf;32130;Samatan;43.4952631;0.9297273
+9740679V;Lycée Professionnel Cluny Catholique Ste Suzanne;4 Chemin Des Trois Freres;97441;Ste Suzanne;-20.90902;55.6055231
+0351878B;Lycée Professionnel Coetlogon;53 Rue Antoine Joly;35083;Rennes;48.1229261;-1.6921595
+0020052W;Lycée Professionnel Colard Noel;132 Avenue De La Republique;02100;St Quentin;49.8628358;3.3085027
+0130071G;Lycée Professionnel Colbert;13 Rue Capitaine Dessemond;13284;Marseille 07;43.2882341;5.3567872
+0590269X;Lycée Professionnel Colbert;2 Parvis Jean Baptiste Colbert;59208;Tourcoing;50.724993;3.16207
+0760087G;Lycée Professionnel Colbert;197 Avenue Des Allies;76141;Le Petit Quevilly;49.4211557;1.0520198
+0783249H;Lycée Professionnel Colbert;52 Avenue Pasteur;78170;La Celle St Cloud;48.8397868;2.1336782
+9830006P;Lycée Professionnel Commercial Et Hotelier;2 Ë 4 Rue G. Baudoux;98845;Noumea;0.0;0.0
+0250014E;Lycée Professionnel Conde;5 Place Marulaz;25030;Besancon;47.2394964;6.0179211
+0370888P;Lycée Professionnel D Arsonval;Place De La Marne;37305;Joue Les Tours;47.347121;0.6668929
+9830517V;Lycée Professionnel D Ouvea (alp);Lekine;98814;Ouvea;0.0;0.0
+0921595J;Lycée Professionnel Daniel Balavoine;7 Rue Marceau Delorme;92270;Bois Colombes;48.9199025;2.2723388
+0690018V;Lycée Professionnel Danielle Casanova;7 Avenue Danielle Casanova;69700;Givors;45.5945754;4.7674675
+0190013L;Lycée Professionnel Danton;Rue Danton;19311;Brive La Gaillarde;45.1590287;1.5216108
+0540032S;Lycée Professionnel Darche;2 Rue Vauban;54405;Longwy;49.523518;5.764968
+0320026E;Lycée Professionnel D'artagnan;Avenue Des Pyrenees;32110;Nogaro;43.754576;-0.029898
+0622271G;Lycée Professionnel D'artois;Rue Jules Mousseron;62290;Noeux Les Mines;50.4729178;2.6598973
+0440036A;Lycée Professionnel De Bougainville;2 Rue Eugene Leroux;44100;Nantes;47.1980237;-1.5930076
+9760296T;Lycée Professionnel De Chirongui;Chirongui;97620;Chirongui;0.0;0.0
+0441550W;Lycée Professionnel De Guerande Olivier Guichard;3 Rue Des Colleges;44350;Guerande;47.338907;-2.424615
+9830511N;Lycée Professionnel De Houailou (alp);Wani Route Provinciale 3;98816;Houailou;0.0;0.0
+9760125G;Lycée Professionnel De Kahani;;97600;Ouangani;0.0;0.0
+9760163Y;Lycée Professionnel De Kaweni;Kaweni;97600;Mamoudzou;-12.780556;45.227778
+9830514S;Lycée Professionnel De Kone (alp);Lotissement Internat;98860;Kone;0.0;0.0
+9830515T;Lycée Professionnel De Koumac (alp);Rue Willy Boarat;98850;Koumac;0.0;0.0
+0861408N;Lycée Professionnel De Kyoto;35 Rue Pierre De Coubertin;86034;Poitiers;46.589285;0.3654183
+0800013E;Lycée Professionnel De L Acheuleen;21 Bis Rue Du 31 Aout 1944;80097;Amiens;49.8809507;2.3249847
+0800065L;Lycée Professionnel De L Authie;20 Rue De Routequeue;80600;Doullens;50.149672;2.3555172
+0332781U;Lycée Professionnel De L Estuaire;41 Rue Jauffre Rudel;33390;Blaye;45.126841;-0.662941
+0210032W;Lycée Professionnel De La Ceramique Henry Moisand;9 Rue Du Lycée;21110;Longchamp;0.0;0.0
+9830509L;Lycée Professionnel De La Foa (alp);;98880;La Foa;45.980309;4.177918
+0601845E;Lycée Professionnel De La Foret;Place Georges Paquier;60635;Chantilly;49.181337;2.462574
+0881411F;Lycée Professionnel De La Haute Moselotte;125 Rue Du Bois Des Dames;88290;Saulxures Sur Moselotte;47.9503028;6.7725191
+9830516U;Lycée Professionnel De La Roche (alp);Route Municipale 1;98828;Mare;49.0742798;-0.0712908
+0740056J;Lycée Professionnel De La Vallee De L'arve P.beche;165 Avenue P. Bechet;74304;Cluses;46.0649788;6.5745449
+9830508K;Lycée Professionnel De La Vallee Du Tir (alp);30 Rue Calmette;98801;Noumea;-22.2620695;166.447292
+0620163R;Lycée Professionnel De L'aa;1 Avenue De Rome;62505;St Omer;50.748789;2.278218
+0510050C;Lycée Professionnel De L'argonne;Avenue Pertison;51801;Ste Menehould;49.096369;4.916346
+0650014M;Lycée Professionnel De L'arrouza;28 Boulevard Roger Cazenave;65107;Lourdes;43.0895992;-0.0486093
+9740737H;Lycée Professionnel De L'horizon;25 Avenue Georges Brassens;97408;St Denis;44.9368252;-0.4863571
+0490013Y;Lycée Professionnel De Narce;Route De Narce;49800;Brain Sur L Authion;47.445012;-0.4313428
+9830512P;Lycée Professionnel De Poindimie (alp);30 Bis Route Provinciale 3;98822;Poindimie;0.0;0.0
+0920150N;Lycée Professionnel De Prony;4 Rue De Bretagne;92600;Asnieres Sur Seine;48.9041761;2.2846969
+9740575G;Lycée Professionnel De Saint Pierre;180 Rue Marius Et Ary Leblond;97410;St Pierre;0.0;0.0
+0230008J;Lycée Professionnel Delphine Gay;Avenue Joliot Curie;23400;Bourganeuf;45.954755;1.749135
+0100016N;Lycée Professionnel Denis Diderot;102 Avenue Jean Jaures;10100;Romilly Sur Seine;48.5166632;3.7160661
+0900355X;Lycée Professionnel Denis Diderot;Rue D'alembert;90800;Bavilliers;47.627813;6.837506
+0410036S;Lycée Professionnel Denis Papin;46 Rue De La Deniserie;41206;Romorantin Lanthenay;47.3673731;1.7432173
+0930128J;Lycée Professionnel Denis Papin;34 Avenue Michelet;93120;La Courneuve;48.928846;2.3863127
+0310090E;Lycée Professionnel Deodat De Severac;26 Boulevard Deodat De Severac;31076;Toulouse;43.5869625;1.4260334
+0762602R;Lycée Professionnel Des 4 Cantons - Grieu;1 Avenue Des Quatre Cantons;76044;Rouen;49.4538868;1.1333653
+0610050Y;Lycée Professionnel Des Andaines;3 Place Du General De Gaulle;61600;La Ferte Mace;48.5869648;-0.3589215
+0620131F;Lycée Professionnel Des Deux Caps;56 Rue Pasteur;62250;Marquise;50.8170749;1.7088829
+0330033G;Lycée Professionnel Des Menuts;36 Rue Des Douves;33800;Bordeaux;44.829973;-0.5631822
+0290092T;Lycée Professionnel Des Metiers;Rue De Logodec;29590;Pont De Buis Les Quimerch;48.251652;-4.091863
+9730372S;Lycée Professionnel Des Metiers;Quartier Balata Rn1;97351;Matoury;4.847214;-52.33112
+0291633T;Lycée Professionnel Des Metiers Rene Laennec;61 Rue Du Lycée;29120;Pont L Abbe;0.0;0.0
+0590102R;Lycée Professionnel Des Monts De Flandre;4 Avenue Des Flandres;59522;Hazebrouck;50.6933308;2.5173338
+0594652L;Lycée Professionnel Des Plaines Du Nord;74 Avenue De Petite Synthe;59792;Grande Synthe;51.0148286;2.3096488
+0622801H;Lycée Professionnel Des Travaux Publics;Le Bois Des Dames;62702;Bruay La Buissiere;50.4999418;2.5616801
+0440541Z;Lycée Professionnel Des Trois Rivieres;1 Rue Des Cormiers;44160;Pontchateau;47.4435735;-2.0964974
+0630061Z;Lycée Professionnel Desaix;Le Mas Boutin;63700;St Eloy Les Mines;46.161333;2.8192499
+0760036B;Lycée Professionnel Descartes;1575 Boulevard Nelson Mandela;76401;Fecamp;49.7442498;0.3892768
+9720429J;Lycée Professionnel Dillon;Cite Dillon;97200;Fort De France;14.6035016;-61.0460438
+0594375K;Lycée Professionnel Dinah Derycke;365 Rue Jules Guesde;59658;Villeneuve D Ascq;50.6395081;3.1380213
+0810995S;Lycée Professionnel Docteur Clement De Pemille;17 Bis Avenue De L'europe;81302;Graulhet;43.7650158;1.9948594
+0570095U;Lycée Professionnel Dominique Labroise;22 Rue De La Mesange;57403;Sarrebourg;48.736734;7.0686686
+0600048B;Lycée Professionnel Donation De Rothschild;Chateau De Laversine;60740;St Maximin;49.235208;2.451886
+0090024C;Lycée Professionnel Dr Philippe Tissie;17 Rue Du Capus;09700;Saverdun;43.2330329;1.5797801
+0690092A;Lycée Professionnel Du 1er Film;14 Rue Du Premier Film;69008;Lyon 08;45.745231;4.8688581
+0640050G;Lycée Professionnel Du 4 Septembre 1870;22 Av Du Quatre Septembre;64400;Oloron Ste Marie;43.1977225;-0.6090365
+0160792Z;Lycée Professionnel Du Batiment;360 Route De Bordeaux Sillac;16022;Angouleme;45.648377;0.1562369
+0860048K;Lycée Professionnel Du Batiment - Auguste Perret;46 Rue De La Bugellerie;86036;Poitiers;46.5973386;0.3314259
+0280009H;Lycée Professionnel Du Batiment Ph De L Orme;7 Rue Jules Ferry;28114;Luce;48.4374249;1.4602902
+0560039N;Lycée Professionnel Du Blavet;43 Rue Charles Gounod;56306;Pontivy;48.0568748;-2.9592048
+0310006N;Lycée Professionnel Du Bois;Route De Luchon;31110;Montauban De Luchon;42.80681;0.60472
+0011010T;Lycée Professionnel Du Bugey;113 Rue Du 5eme Rtm;01306;Belley;45.7606158;5.68952
+0740059M;Lycée Professionnel Du Chablais;Chemin De Morcy;74207;Thonon Les Bains;46.359481;6.458842
+0620188T;Lycée Professionnel Du Detroit;200 Rue Guillaume Apollinaire;62107;Calais;50.939417;1.8738335
+0451037H;Lycée Professionnel Du Giennois;20 Rue Du 32 Eme R I;45500;Gien;47.698059;2.624758
+0951572T;Lycée Professionnel Du Grand Cerf;1 Et 3 Rue Karl Marx;95870;Bezons;48.923505;2.2077554
+0590270Y;Lycée Professionnel Du Hainaut;1 Avenue Villars;59322;Valenciennes;50.3582813;3.5107769
+0420065T;Lycée Professionnel Du Haut Forez;Le Bourg;42600;Verrieres En Forez;45.570879;3.996448
+0801739F;Lycée Professionnel Du Marquenterre;2 Rue Du Marais;80120;Rue;50.2688787;1.6760326
+0311324W;Lycée Professionnel Du Mirail;Avenue Jean Baylet;31081;Toulouse;43.5846573;1.3942102
+0730032N;Lycée Professionnel Du Nivolet;Route De Barby;73491;La Ravoire;45.5713919;5.9668199
+0170070J;Lycée Professionnel Du Pays D'aunis;Rue Du Stade;17700;Surgeres;46.108536;-0.747223
+0680041A;Lycée Professionnel Du Rebberg;1 Rue De Verdun;68100;Mulhouse;47.7403099;7.3460648
+0951282C;Lycée Professionnel Du Vexin;Route De Marines;95750;Chars;49.1580627;1.9424217
+0800061G;Lycée Professionnel Du Vimeu;3 Rue Denis Papin;80534;Friville Escarbotin;50.0832324;1.5461642
+9710690Y;Lycée Professionnel Ducharmoy;Cite Huyghes Despointes;97120;St Claude;46.387405;5.8677659
+0290108K;Lycée Professionnel Dupuy De Lome;34 Rue Dupuy De Lome;29287;Brest;48.3909239;-4.509126
+0671696E;Lycée Professionnel Economique;Place Du Dr Fr. Kretz;67604;Selestat;48.259386;7.454241
+0671737Z;Lycée Professionnel Economique;3 Quai Charles Frey;67000;Strasbourg;48.5784816;7.7465186
+0520795G;Lycée Professionnel Edme Bouchardon;16 Rue Youri Gagarine;52903;Chaumont;48.104773;5.1478457
+0500032N;Lycée Professionnel Edmond Doucet;Rue Paul Doumer;50120;Equeurdreville Hainneville;49.646515;-1.6551279
+0590265T;Lycée Professionnel Edmond Labbe;817 Rue Charles Bourseul;59508;Douai;50.3764002;3.0685187
+0690129R;Lycée Professionnel Edmond Labbe;9 Chemin Des Chassagnes;69600;Oullins;45.7224869;4.8100176
+0760007V;Lycée Professionnel Edmond Labbe;52 Avenue Aristide Briand;76360;Barentin;49.5422723;0.956811
+0750800M;Lycée Professionnel Edmond Rostand;15 Rue De L'evangile;75018;Paris 18;48.8927119;2.3626153
+0620193Y;Lycée Professionnel Edouard Branly;2 Rue De La Porte Gayole;62321;Boulogne Sur Mer;50.7198852;1.6152414
+0801336T;Lycée Professionnel Edouard Branly;70 Boulevard De Saint Quentin;80098;Amiens;49.8791607;2.2965051
+0850028U;Lycée Professionnel Edouard Branly;5 Boulevard Edouard Branly;85006;La Roche Sur Yon;46.6746179;-1.4417319
+0860006P;Lycée Professionnel Edouard Branly;2 Rue Edouard Branly;86106;Chatellerault;46.8021551;0.5431246
+0800062H;Lycée Professionnel Edouard Gand;70 Bd De St Quentin;80098;Amiens;49.8791607;2.2965051
+0593661J;Lycée Professionnel Edouard Lalo;11 Rue De Thionville;59009;Lille;50.6434451;3.065631
+0870041X;Lycée Professionnel Edouard Vaillant;38 Route Du Derot;87200;St Junien;45.8992579;0.911986
+9730308X;Lycée Professionnel Elie Castor;Rue Edjide Duchesne;97388;Kourou;5.1518573;-52.644595
+0760088H;Lycée Professionnel Elisa Lemonnier;59 Boulevard Charles De Gaulle;76141;Le Petit Quevilly;49.4193664;1.0601232
+0310033T;Lycée Professionnel Elisabeth Et Norbert Casteret;27 Avenue De L'isle;31806;St Gaudens;43.1126792;0.7258415
+0520032C;Lycée Professionnel Emile Baudot;Route De Saint Dizier;52130;Wassy;48.499508;4.948753
+0331882S;Lycée Professionnel Emile Combes;23 Rue Emile Combes;33321;Begles;44.8064674;-0.5473837
+0371258S;Lycée Professionnel Emile Delataille;Place Des Prebendes;37602;Loches;47.1263983;0.98353
+0880064S;Lycée Professionnel Emile Galle;5 Rue Auguste Dedecker;88151;Thaon Les Vosges;48.2491595;6.4083611
+0540015Y;Lycée Professionnel Emile Levassor;2 Rue Levassor;54110;Dombasle Sur Meurthe;48.6079509;6.359411
+0480019K;Lycée Professionnel Emile Peytavin;Avenue Du 11 Novembre;48001;Mende;44.5233613;3.4833187
+0132569X;Lycée Professionnel Emile Zola;Av De L Arc De Meyran;13181;Aix En Provence;43.5118833;5.4584873
+0550003E;Lycée Professionnel Emile Zola;5 Rue D'anjou;55012;Bar Le Duc;48.7816739;5.1710159
+0440141P;Lycée Professionnel Emilien Maillard;230 Rue Du Pressoir Rouge;44154;Ancenis;47.3752742;-1.1822546
+0760024N;Lycée Professionnel Emulation Dieppoise;2 Rue De Stalingrad;76201;Dieppe;49.917085;1.082993
+0850033Z;Lycée Professionnel Eric Tabarly;48 Rue Du Moulin;85108;Les Sables D Olonne;46.4964057;-1.8007527
+0752846L;Lycée Professionnel Erik Satie;2-4 Rue Durouchoux;75014;Paris 14;48.8331604;2.3263037
+0590193P;Lycée Professionnel Ernest Couteaux;37 Avenue Du College;59734;St Amand Les Eaux;50.4477034;3.4431281
+0130058T;Lycée Professionnel Estaque (l');310 Rue Rabelais;13016;Marseille 16;43.3620918;5.3247729
+0750798K;Lycée Professionnel Esthetique;9 Rue Fortuny;75017;Paris 17;48.8820156;2.3062483
+0750808W;Lycée Professionnel Etienne Dolet;7 9 Rue D'eupatoria;75020;Paris 20;48.8687503;2.3868082
+0440294F;Lycée Professionnel Etienne Lenoir;9 Rue De L Europe;44146;Chateaubriant;47.729359;-1.3637892
+0420079H;Lycée Professionnel Etienne Mimard;32 Rue Etienne Mimard;42021;St Etienne;45.438102;4.3960568
+0510008G;Lycée Professionnel Etienne Oehmichen;8 Avenue Du Mont Hery;51037;Chalons En Champagne;48.9761202;4.3588161
+0130025G;Lycée Professionnel Etoile (de L');14 Rue Jules Ferry;13541;Gardanne;43.4529944;5.469251
+0550891V;Lycée Professionnel Eugene Freyssinet;6 Av President Kennedy;55107;Verdun;49.1456674;5.3935978
+0210056X;Lycée Professionnel Eugene Guillaume;12 Av Mal De Lattre De Tassigny;21506;Montbard;47.6226812;4.3398528
+0311092U;Lycée Professionnel Eugene Montel;2 Boulevard Marcel Dassault;31770;Colomiers;43.6101188;1.3508466
+0680039Y;Lycée Professionnel F.d. Roosevelt;17 Bd Presid Roosevelt;68060;Mulhouse;47.7486201;7.3305514
+0220083W;Lycée Professionnel Felix Le Dantec;Rue Des Cordiers;22303;Lannion;48.7310544;-3.446948
+0950657Y;Lycée Professionnel Ferdinand Buisson;245 Rue Ferdinand Buisson;95120;Ermont;48.9779421;2.2726886
+0390914G;Lycée Professionnel Ferdinand Fillod;1 Rue Lamartine;39160;St Amour;46.4378786;5.3440389
+0120038B;Lycée Professionnel Ferdinand Foch;1 Rue Vieussens;12000;Rodez;44.350556;2.5620421
+0840700E;Lycée Professionnel Ferdinand Revoul;Route De Nyons;84601;Valreas;44.3870032;5.0112201
+0620018H;Lycée Professionnel Fernand Degrugillier;1 Boulevard Emile Basly;62260;Auchel;50.5069023;2.4663519
+0693094N;Lycée Professionnel Fernand Forest;Rue De L'egalite;69800;St Priest;45.691598;4.9463832
+0250001R;Lycée Professionnel Fernand Leger;6 Rue Rene Girardot;25404;Audincourt;47.4837032;6.8498439
+0340006X;Lycée Professionnel Fernand Leger;63 Rte De Clermont L Herault;34600;Bedarieux;43.6120939;3.1676971
+0590263R;Lycée Professionnel Fernand Leger;Route De Steendam;59411;Coudekerque Branche;51.0187231;2.3991535
+0610022T;Lycée Professionnel Fernand Leger;Allee Eugene Cabrol;61104;Flers;48.743349;-0.555477
+0762836V;Lycée Professionnel Fernand Leger;Chemin Des Coquereaux;76530;Grand Couronne;49.362074;1.0172754
+0130157A;Lycée Professionnel Ferrages (quartier Les);Quartier Les Ferrages;13250;St Chamas;43.550669;5.034508
+6200004K;Lycée Professionnel Finosello;Av Mal Lyautey;20189;Ajaccio;41.9379483;8.7447941
+0611287T;Lycée Professionnel Flers;;61100;Flers;48.750059;-0.575794
+0330060L;Lycée Professionnel Flora Tristan;Domaine De La Chausse;33360;Camblanes Et Meynac;44.7653476;-0.4941232
+0610019P;Lycée Professionnel Flora Tristan;7 9 Av Meunier De La Raillere;61600;La Ferte Mace;48.589377;-0.356027
+0620121V;Lycée Professionnel Flora Tristan;9 Ave Du General De Gaulle;62192;Lillers;50.5309405;2.5081599
+0920170K;Lycée Professionnel Florian;9 Bis Rue De La Marne;92330;Sceaux;48.7815135;2.3034754
+0130056R;Lycée Professionnel Floride (la);54 Bd Gay Lussac Zin 903;13014;Marseille 14;0.0;0.0
+0060023T;Lycée Professionnel Francis De Croisset;34 Chemin De La Cavalerie;06130;Grasse;43.6526492;6.9315142
+0640053K;Lycée Professionnel Francis Jammes;Route De Mont De Marsan;64301;Orthez;43.48756;-0.7739279
+0511748Y;Lycée Professionnel Francois 1er;Faubourg De Vitry Le Brule;51308;Vitry Le Francois;48.729036;4.59224
+0620144V;Lycée Professionnel Francois Albert;Rue Jules Mousseron;62290;Noeux Les Mines;50.4729178;2.6598973
+0440033X;Lycée Professionnel Francois Arago;23 Rue Du Recteur Schmitt;44322;Nantes;47.2520908;-1.5519956
+0090020Y;Lycée Professionnel Francois Camel;28 Rue Jules Ferry;09201;St Girons;42.9738873;1.1537953
+0693095P;Lycée Professionnel Francois Cevert;104 Chemin De La Sauvegarde;69132;Ecully;45.7836623;4.7878684
+0623377J;Lycée Professionnel Francois Hennebique;Rue Colbert;62804;Lievin;50.4257392;2.7540206
+0260041D;Lycée Professionnel Francois Jean Armorin;35 Avenue H.grand;26402;Crest;44.731422;5.0164927
+0594302F;Lycée Professionnel Francois Mansart;270 Avenue Henri Barbusse;59582;Marly;50.3445555;3.5571247
+0580552K;Lycée Professionnel Francois Mitterrand;Rue Pierre Mendes France;58120;Chateau Chinon Ville;47.059639;3.938102
+0595894L;Lycée Professionnel Francois Rabelais;817 Rue Charles Bourseul;59507;Douai;50.3764002;3.0685187
+0021479X;Lycée Professionnel Francoise Dolto;4 Avenue Des Lilas;02120;Guise;49.9027382;3.639282
+0382274R;Lycée Professionnel Francoise Dolto;4 Rue Piardiere;38120;Fontanil Cornillon;45.2512325;5.666418
+0710079A;Lycée Professionnel Francoise Dolto;1 Rue Capitaine Priet;71307;Montceau Les Mines;46.681852;4.3871729
+0930138V;Lycée Professionnel Frederic Bartholdi;12 Rue De La Liberte;93200;St Denis;48.9438959;2.3653605
+0690106R;Lycée Professionnel Frederic Fays;46 Rue Frederic Fays;69615;Villeurbanne;45.7625759;4.9047165
+0130062X;Lycée Professionnel Frederic Mistral;46 Boulevard Sainte Anne;13417;Marseille 08;43.2580155;5.392781
+0220187J;Lycée Professionnel Fulgence Bienvenue;Rue Eon De L'etoile;22606;Loudeac;48.1851796;-2.7600028
+0540084Y;Lycée Professionnel Fulgence Bienvenue;10 Rue Prosper Merimee;54580;Auboue;49.2107677;5.9836034
+0720034W;Lycée Professionnel Funay-helene Boucher;22 Rue Rodolphe Diesel;72003;Le Mans;47.984752;0.2253982
+0310056T;Lycée Professionnel Gabriel Peri;30 Rue Gabriel Peri;31013;Toulouse;43.6061733;1.454834
+0940132H;Lycée Professionnel Gabriel Peri;41 Avenue Boileau;94500;Champigny Sur Marne;48.8067115;2.5458851
+0011118K;Lycée Professionnel Gabriel Voisin;21 Avenue De Jasseron;01000;Bourg En Bresse;46.2113123;5.2450739
+0100945Y;Lycée Professionnel Gabriel Voisin;Chemin Des Champs De La Loge;10012;Troyes;48.31499;4.069673
+0380100C;Lycée Professionnel Galilee;124 Avenue General Leclerc;38209;Vienne;45.5052095;4.8559693
+0750785W;Lycée Professionnel Galilee;28 Rue De Patay;75013;Paris 13;48.8252444;2.3751304
+0380010E;Lycée Professionnel Gambetta;14 Avenue Gambetta;38309;Bourgoin Jallieu;45.5880165;5.273173
+0130006L;Lycée Professionnel Gambetta (cours);100 Bis Cours Gambetta;13100;Aix En Provence;43.5203551;5.4599761
+0100906F;Lycée Professionnel Gaston Bachelard;5 Bis Rue Gaston Bachelard;10200;Bar Sur Aube;48.2298972;4.7045231
+0750553U;Lycée Professionnel Gaston Bachelard;2 Rue Tagore;75013;Paris 13;48.8206816;2.3602263
+0300057Z;Lycée Professionnel Gaston Darboux;7 Rue Jules Raimu;30908;Nimes;43.8184647;4.3295053
+0530778H;Lycée Professionnel Gaston Lesnard;84 Boulevard Volney;53015;Laval;48.0565772;-0.7961695
+0020077Y;Lycée Professionnel Gay Lussac;Boulevard Gambetta;02302;Chauny;49.613832;3.2214
+0870013S;Lycée Professionnel George Sand;9 Rue De L'hozanne;87210;Le Dorat;46.2145054;1.0820723
+0720065E;Lycée Professionnel George Washington;8 Place Washington;72002;Le Mans;47.9956706;0.2030578
+0572175E;Lycée Professionnel Georges Bastide;Square Georges Bastide;57150;Creutzwald;49.204987;6.6962469
+0880056H;Lycée Professionnel Georges Baumont;Rue De L'orme;88102;St Die Des Vosges;48.29796;6.946644
+0300951W;Lycée Professionnel Georges Brassens;Avenue Vigan Braquet;30205;Bagnols Sur Ceze;44.1607643;4.6116424
+0590250B;Lycée Professionnel Georges Bustin;13 Rue Du 8 Mai 1945;59690;Vieux Conde;50.4642251;3.569136
+0190045W;Lycée Professionnel Georges Cabanis;6 Boulevard Henry De Jouvenel;19311;Brive La Gaillarde;45.1600815;1.5232644
+0270052K;Lycée Professionnel Georges Dumezil;Route D'ivry;27207;Vernon;49.0818358;1.4769566
+0590198V;Lycée Professionnel Georges Guynemer;99 Rue De La Republique;59430;St Pol Sur Mer;51.0287687;2.3564086
+0740054G;Lycée Professionnel Germain Sommeiller;2 Boulevard Taine;74008;Annecy;45.906769;6.1291068
+0280925D;Lycée Professionnel Gilbert Courtois;2 Rue Salvador Allende;28105;Dreux;48.7366183;1.3444523
+0630053R;Lycée Professionnel Gilbert Romme;75 Rue Du Creux;63201;Riom;45.8859718;3.131179
+0170025K;Lycée Professionnel Gilles Jamain;2a Boulevard Pouzet;17312;Rochefort;45.9309537;-0.9657396
+0510017S;Lycée Professionnel Godart Roger;8 Rue Godart Roger;51331;Epernay;49.0398693;3.9654465
+0831014B;Lycée Professionnel Golf-hotel;Allee Georges Dussauge;83400;Hyeres;43.1306904;6.1572977
+0940140S;Lycée Professionnel Gourdou-leseurre;50-56 Boulevard De Champigny;94210;St Maur Des Fosses;48.8024866;2.5069878
+0141640G;Lycée Professionnel Guibray;8 Rue Des Champs St Georges;14700;Falaise;48.8900926;-0.1882493
+0030905T;Lycée Professionnel Gustave Eiffel;Rue Jules Bertin;03800;Gannat;46.088356;3.197488
+0110034C;Lycée Professionnel Gustave Eiffel;2 Rue Jean Moulin;11108;Narbonne;43.1899207;3.01902
+0130013U;Lycée Professionnel Gustave Eiffel;Avenue Manoukian;13682;Aubagne;43.2867325;5.5484578
+0370053G;Lycée Professionnel Gustave Eiffel;1 Rue Marechal Augereau;37073;Tours;47.4209434;0.67437
+0590262P;Lycée Professionnel Gustave Eiffel;96 Rue Jules Lebleu;59427;Armentieres;50.6880881;2.8733271
+0690008J;Lycée Professionnel Gustave Eiffel;6 A 12 Av Ferdinand Gaillard;69530;Brignais;45.6766545;4.7549711
+0752961L;Lycée Professionnel Gustave Eiffel;1 Rue Du General Camou;75007;Paris 07;48.858841;2.2994949
+0770945J;Lycée Professionnel Gustave Eiffel;4 Av. D'ormes;77130;Varennes Sur Seine;48.3741592;2.9264466
+0910632T;Lycée Professionnel Gustave Eiffel;9 Avenue De La Republique;91300;Massy;48.7351223;2.2952471
+0951673C;Lycée Professionnel Gustave Eiffel;9 Allee Jean De Florette;95120;Ermont;48.9837039;2.2450203
+0750775K;Lycée Professionnel Gustave Ferrie;7 Rue Des Ecluses St Martin;75010;Paris 10;48.8765153;2.3673877
+0760142S;Lycée Professionnel Gustave Flaubert;1 Rue Albert Dupuis;76044;Rouen;49.4508628;1.1226207
+0590255G;Lycée Professionnel Guy Debeyre;R Ctre Torpilleur Le Triomphant;59383;Dunkerque;51.03456;2.3752019
+0310051M;Lycée Professionnel Guynemer;43 Rue Leo Lagrange;31400;Toulouse;43.5874782;1.4525238
+0640049F;Lycée Professionnel Guynemer;1 Av Du 19 Mars 1962;64404;Oloron Ste Marie;43.1835704;-0.6322436
+0670067J;Lycée Professionnel Haute-bruche;18 Rue Des Grives;67131;Schirmeck;48.4859231;7.2352351
+0750802P;Lycée Professionnel Hector Guimard;19 Rue Curial;75019;Paris 19;48.8918906;2.3723661
+0441823T;Lycée Professionnel Heinlex;1 Rue Albert Einstein;44600;St Nazaire;47.2646046;-2.2632765
+0540059W;Lycée Professionnel Helene Bardot;12 Place Saint Antoine;54701;Pont A Mousson;48.9037504;6.0558391
+0690093B;Lycée Professionnel Helene Boucher;18 Rue Rosenberg;69631;Venissieux;45.6981022;4.8907074
+0371099U;Lycée Professionnel Henri Becquerel;1 Rue Jules Ladoumegue;37000;Tours;47.377011;0.731656
+0332344U;Lycée Professionnel Henri Brulle;Route De St Emilion;33503;Libourne;44.9122971;-0.2202256
+0620190V;Lycée Professionnel Henri Darras;Chemin Des Manufactures;62803;Lievin;50.412424;2.766446
+0490784L;Lycée Professionnel Henri Dunant;15 Rue De Haarlem;49017;Angers;47.4842499;-0.5241609
+0700011G;Lycée Professionnel Henri Fertet;Place Du General Boichut;70104;Gray;47.444006;5.59541
+0780584L;Lycée Professionnel Henri Matisse;55 Rue De Montfort;78196;Trappes;48.7814528;1.9945968
+0623328F;Lycée Professionnel Henri Senez;Bd F Darchicourt;62252;Henin Beaumont;50.405948;2.962536
+0541290J;Lycée Professionnel Henri Wallon;1 Rue Henry Wallon;54190;Villerupt;49.4639405;5.9266187
+0640058R;Lycée Professionnel Honore Baradat;Avenue Du Loup;64050;Pau;43.3132924;-0.3555257
+0660520G;Lycée Professionnel Hotel Resto Leon Blum;15 Avenue Paul Alduy;66103;Perpignan;42.6741796;2.9046494
+0070016E;Lycée Professionnel Hotelier;Route De Tauriers;07110;Largentiere;44.55113;4.279915
+0910629P;Lycée Professionnel Hotelier Chateau Des Coudraies;2 Bd Charles De Gaulle;91450;Etiolles;48.6349849;2.466748
+0650874X;Lycée Professionnel Hotelier Et Econom Lautreamont;Avenue D Azereix;65016;Tarbes;43.227193;0.055477
+0740014N;Lycée Professionnel Hotelier Francois Bise;86 Rue D'asniere;74136;Bonneville;46.0780249;6.409053
+0640042Y;Lycée Professionnel Hotelier Haute Vue;Avenue Des Cimes;64160;Morlaas;43.3406345;-0.2758834
+9740738J;Lycée Professionnel Hotelier La Renaissance;63 Rue Auguste Vinson;97867;St Paul;-21.0284314;55.2652076
+0573080N;Lycée Professionnel Hurlevent;Rue Du Petit Bois;57460;Behren Les Forbach;49.172573;6.926953
+0590257J;Lycée Professionnel Ile De Flandre;246 Bis Quai De La Derivation;59427;Armentieres;50.6911473;2.8904884
+0595480L;Lycée Professionnel Ile Jeanty;Rue Auguste Waeteraere;59140;Dunkerque;51.030326;2.361877
+0330089T;Lycée Professionnel Indust.et Hotel.j.monnet;40 Avenue Henri Brulle;33506;Libourne;44.9053141;-0.2418679
+0950658Z;Lycée Professionnel Industriel Chateau D Epluches;45 Avenue Du Chateau;95310;St Ouen L Aumone;49.0549212;2.1309912
+0340088L;Lycée Professionnel Irene Et Frederic Joliot Curie;19 Boulevard Joliot Curie;34200;Sete;43.3972261;3.6659039
+0090543S;Lycée Professionnel Irenee Cros;2 Avenue De L'ariege;09103;Pamiers;43.1098821;1.6140065
+0880023X;Lycée Professionnel Isabelle Viviani;75 Rue De Remiremont;88020;Epinal;48.1629574;6.4496322
+9740921H;Lycée Professionnel Isnelle Amelin;Rue Marcel Goulette Duparc;97438;Ste Marie;0.0;0.0
+0801252B;Lycée Professionnel J C Athanase Peltier;8 Avenue Jean Moulin;80400;Ham;49.7510579;3.077901
+0070002P;Lycée Professionnel J Et E Montgolfier;17 Rue Du Capitaine De Canson;07100;Annonay;45.2442892;4.6718918
+0420073B;Lycée Professionnel Jacob Holtzer;5 Rue Michelet;42704;Firminy;45.3882874;4.2914303
+0760006U;Lycée Professionnel Jacquard;2 Rue Antoine Bourdelle;76360;Barentin;49.5417967;0.9497241
+0880057J;Lycée Professionnel Jacques Augustin;20 Rue Du Nouvel Hopital;88100;St Die Des Vosges;48.2885775;6.9603839
+0692718E;Lycée Professionnel Jacques Brel;7 Avenue D'oschatz;69200;Venissieux;45.6952773;4.8802134
+0180042Y;Lycée Professionnel Jacques Coeur;108 Rue Jean Baffier;18026;Bourges;47.0738989;2.409201
+0470029B;Lycée Professionnel Jacques De Romas;Route De Mezin;47600;Nerac;44.1274306;0.311799
+0060002V;Lycée Professionnel Jacques Dolle;120 Chemin De Saint Claude;06600;Antibes;43.5929211;7.1137122
+0620162P;Lycée Professionnel Jacques Durand;9 Rue Monsigny;62505;St Omer;50.7501372;2.2593951
+0620011A;Lycée Professionnel Jacques Le Caron;61 Avenue De L'hippodrome;62000;Arras;50.2926345;2.7440119
+0380023U;Lycée Professionnel Jacques Prevert;9 Rue La Republique;38601;Fontaine;45.1951041;5.6674681
+0390015E;Lycée Professionnel Jacques Prevert;31 Place Barberousse;39107;Dole;47.0886756;5.488823
+0782603F;Lycée Professionnel Jacques Prevert;88 Avenue Des Etats Unis;78000;Versailles;48.81003;2.1492612
+0590249A;Lycée Professionnel Jacques-yves Cousteau;27 Rue Pasteur;59290;Wasquehal;50.6723546;3.1272292
+0620037D;Lycée Professionnel Jan Lavezzari;15 Avenue Du Phare;62602;Berck;50.3988519;1.5605537
+0380037J;Lycée Professionnel Jean Jaures;5 Rue Ancien Champ De Mars;38000;Grenoble;45.1887412;5.7201699
+0160862A;Lycée Professionnel Jean Albert Gregoire;Rue Jean Albert Gregoire;16800;Soyaux;45.629823;0.217669
+0130064Z;Lycée Professionnel Jean Baptiste Brochier;9 Bd Mireille Lauze;13010;Marseille 10;43.282372;5.4039153
+0870051H;Lycée Professionnel Jean Baptiste Darnet;28 Avenue De Perigueux;87500;St Yrieix La Perche;45.5048579;1.2020927
+0820039X;Lycée Professionnel Jean Baylet;20 Avenue Du Marechal Leclerc;82403;Valence;44.1144715;0.8939859
+0490058X;Lycée Professionnel Jean Bertin;25 Rue Marceau;49401;Saumur;47.250839;-0.0746699
+0160036C;Lycée Professionnel Jean Caillaud;Cite Scolaire De Puyguillen;16600;Ruelle Sur Touvre;45.6767619;0.219218
+0240007C;Lycée Professionnel Jean Capelle;Av Marechal Leclerc;24108;Bergerac;44.8433876;0.477406
+0290072W;Lycée Professionnel Jean Chaptal;35 Chemin Des Justices;29191;Quimper;47.9918404;-4.1193383
+0371123V;Lycée Professionnel Jean Chaptal;Rue Du Clos Des Gardes;37400;Amboise;47.3993791;0.9837666
+0400047J;Lycée Professionnel Jean D Arcet;1 Place Ste Quitterie;40801;Aire Sur L Adour;43.6966232;-0.2690223
+0450750W;Lycée Professionnel Jean De La Taille;Allee De Burglengenfeld;45300;Pithiviers;48.1679602;2.2424772
+0820700R;Lycée Professionnel Jean De Prades;Route De Toulouse;82102;Castelsarrasin;44.0334192;1.1211267
+0090003E;Lycée Professionnel Jean Durroux;1 Et 3 Avenue Jean Durroux;09000;Ferrieres Sur Ariege;42.942515;1.61601
+0280700J;Lycée Professionnel Jean Felix Paulsen;1 Boulevard Du 8 Mai 1945;28205;Chateaudun;48.0626165;1.3363333
+0670129B;Lycée Professionnel Jean Geiler;14 Rue Des Bateliers;67064;Strasbourg;48.5802264;7.755564
+0350761M;Lycée Professionnel Jean Guehenno;3 Promenade Du Gue Maheu;35305;Fougeres;48.3418263;-1.2027846
+0560053D;Lycée Professionnel Jean Guehenno;79 Avenue De La Marne;56017;Vannes;47.6597846;-2.7844548
+0610049X;Lycée Professionnel Jean Guehenno;16 Rue Pierre Huet;61105;Flers;48.744858;-0.552585
+0541283B;Lycée Professionnel Jean Hanzelet;Place De Trey;54701;Pont A Mousson;48.9095605;6.0576284
+0440310Y;Lycée Professionnel Jean Jacques Audubon;10 Boulevard Paul Langevin;44220;Coueron;47.2137802;-1.7226389
+0230003D;Lycée Professionnel Jean Jaures;38 Rue Jean Jaures;23200;Aubusson;45.7620497;3.541304
+0350031U;Lycée Professionnel Jean Jaures;24 Rue Victor Rault;35205;Rennes;48.0924623;-1.6893605
+0141599M;Lycée Professionnel Jean Jooris;1 Rue Salvador Allende;14162;Dives Sur Mer;49.2802055;-0.0990374
+0132211H;Lycée Professionnel Jean Lurcat;Chemin De St Macaire;13693;Martigues;43.4217515;5.0344096
+0451067R;Lycée Professionnel Jean Lurcat;4 Rue Du Perron;45401;Fleury Les Aubrais;47.9295823;1.9245113
+0690045Z;Lycée Professionnel Jean Lurcat;4 Rue Ludovic Arrachart;69008;Lyon 08;45.7323666;4.8628141
+0572075W;Lycée Professionnel Jean Mace;58 Avenue Jean Mermoz;57290;Fameck;49.3016789;6.1145402
+0941232D;Lycée Professionnel Jean Mace;103 Rue Mirabeau;94600;Choisy Le Roi;48.7637484;2.4216827
+0541605B;Lycée Professionnel Jean Marc Reiser;1 Rue Du Stade;54810;Longlaville;49.5258418;5.7963167
+9730094P;Lycée Professionnel Jean Marie Michotte;Boulevard De La Republique;97305;Cayenne;4.9367721;-52.323572
+0142178S;Lycée Professionnel Jean Mermoz;1 Rue Georges Fauvel;14504;Vire;48.8384706;-0.8776385
+0150036H;Lycée Professionnel Jean Mermoz;10 Rue Du Docteur Chibret;15005;Aurillac;44.9354809;2.4517539
+0180010N;Lycée Professionnel Jean Mermoz;Allee Des Colleges;18028;Bourges;47.0995745;2.4231125
+0340012D;Lycée Professionnel Jean Mermoz;25 Rue Ferdinand De Lesseps;34500;Beziers;43.3492469;3.2328792
+0950949R;Lycée Professionnel Jean Mermoz;21 Rue Emile Combres;95560;Montsoult;49.0673876;2.3237953
+0020022N;Lycée Professionnel Jean Monnet;22 Rue D Aboville;02800;La Fere;49.6600295;3.3666683
+0030060Z;Lycée Professionnel Jean Monnet;39 Place Jules Ferry;03401;Yzeure;46.5660374;3.3552285
+0220075M;Lycée Professionnel Jean Monnet;9 Rue Des Ursulines;22800;Quintin;48.4057441;-2.9105637
+0470782V;Lycée Professionnel Jean Monnet;Rue Marcel Pagnol;47510;Foulayronnes;44.231803;0.6430793
+0541816F;Lycée Professionnel Jean Monnet;8 Rue St Don;54110;Dombasle Sur Meurthe;48.6255302;6.3542519
+0590124P;Lycée Professionnel Jean Monnet;111 Avenue De Dunkerque;59042;Lille;50.6334462;3.0310112
+0910631S;Lycée Professionnel Jean Monnet;51 Ave Du Gal De Gaulle;91260;Juvisy Sur Orge;48.6952319;2.3801097
+0920164D;Lycée Professionnel Jean Monnet;128 Avenue Jean Jaures;92120;Montrouge;48.813054;2.3056284
+0540086A;Lycée Professionnel Jean Morette;1 Rue Du College;54970;Landres;49.3163495;5.8025968
+0080066D;Lycée Professionnel Jean Moulin;996 Av De La Cite Scolaire;08500;Revin;49.9269895;4.6510517
+0130150T;Lycée Professionnel Jean Moulin;1 Boulevard Marcel Cachin;13110;Port De Bouc;43.4076675;4.9949774
+0180026F;Lycée Professionnel Jean Moulin;45 Rue Jean Moulin;18206;St Amand Montrond;46.7345343;2.5142771
+0340013E;Lycée Professionnel Jean Moulin;Avenue Martyrs De La Resistance;34521;Beziers;43.3429502;3.233546
+0590188J;Lycée Professionnel Jean Moulin;49 Bd Du General De Gaulle;59051;Roubaix;50.68501;3.17217
+0623327E;Lycée Professionnel Jean Moulin;3 Rue Jean Moulin;62117;Brebieres;50.3312733;3.0211252
+0782602E;Lycée Professionnel Jean Moulin;18 Rue Du Docteur Audigier;78150;Le Chesnay;48.8263325;2.1329428
+0931739K;Lycée Professionnel Jean Moulin;9 Rue Jean Moulin;93110;Rosny Sous Bois;48.8656577;2.4930275
+0940143V;Lycée Professionnel Jean Moulin;8 Rue Du Docteur Lebel;94307;Vincennes;48.8465644;2.4312472
+7200093N;Lycée Professionnel Jean Nicoli;Cours Pierangeli;20296;Bastia;42.6985128;9.4509783
+0782593V;Lycée Professionnel Jean Perrin;1 Rue Lucien Sampaix;78210;St Cyr L Ecole;48.8028083;2.0597426
+0910715H;Lycée Professionnel Jean Perrin;26 Rue Leontine Sohier;91163;Longjumeau;48.6909037;2.3033805
+9740910W;Lycée Professionnel Jean Perrin;Rue Du Lycée;97440;St Andre;0.0;0.0
+0540081V;Lycée Professionnel Jean Prouve;53 Rue De Bonsecours;54052;Nancy;48.6757518;6.1959881
+0721301Y;Lycée Professionnel Jean Rondeau;Route De Rahay;72120;St Calais;47.932495;0.7654461
+0160049S;Lycée Professionnel Jean Rostand;Rue Louise Leriget;16017;Angouleme;45.6532678;0.1797211
+0580050P;Lycée Professionnel Jean Rostand;9 Boulevard Saint Exupery;58019;Nevers;47.0040888;3.1563606
+0760082B;Lycée Professionnel Jean Rostand;Hameau De Neufmesnil;76550;Offranville;49.857248;1.019358
+0572012C;Lycée Professionnel Jean Victor Poncelet;7 Rue Paul Valery;57070;Metz;49.1090537;6.2192344
+0120014A;Lycée Professionnel Jean Vigo;Le Puits De Cales;12100;Millau;44.090247;3.059129
+0590256H;Lycée Professionnel Jean-baptiste Carpeaux;Rue Pelabon;59154;Crespin;50.4079774;3.6689185
+0300056Y;Lycée Professionnel Jean-baptiste Dumas;Place De La Belgique;30104;Ales;44.128435;4.0760861
+0622275L;Lycée Professionnel Jean-charles Cazin;42 Rue Cazin;62321;Boulogne Sur Mer;50.7206515;1.609213
+0790015M;Lycée Professionnel Jean-francois Cail;1 Rue Maurice Gadioux;79110;Chef Boutonne;46.1042153;-0.0694183
+0670127Z;Lycée Professionnel Jean-frederic Oberlin;4 Rue De L'academie;67000;Strasbourg;48.5816645;7.7601602
+0820006L;Lycée Professionnel Jean-louis Etienne;4 Rue Lavoisier;82303;Caussade;44.1612238;1.5403785
+0450043C;Lycée Professionnel Jeannette Verdier;35 Avenue Gaillardin;45207;Montargis;47.9940559;2.7288555
+0640040W;Lycée Professionnel Jean-pierre Champo;7 Rue Des Evades De France;64130;Mauleon Licharre;43.2241138;-0.8777013
+0931024H;Lycée Professionnel Jean-pierre Timbaud;103 Avenue De La Republique;93300;Aubervilliers;48.9044948;2.3912217
+0330119A;Lycée Professionnel Jehan Duperier;Chemin Tiran;33166;St Medard En Jalles;44.8960834;-0.705443
+0310091F;Lycée Professionnel Jolimont;44 Chemin Cassaing;31079;Toulouse;43.6168618;1.4763001
+0620150B;Lycée Professionnel Joliot Curie;9 Rue Leo Lagrange;62590;Oignies;50.4673489;2.9974803
+0150022T;Lycée Professionnel Joseph Constant;3 Rue De La Coste;15300;Murat;45.1109178;2.860881
+0573283J;Lycée Professionnel Joseph Cressot;6 Rue Mozart;57310;Guenange;49.2998824;6.2004744
+0371100V;Lycée Professionnel Joseph Cugnot;Les Hucherolles;37500;Chinon;47.170435;0.2522948
+0310050L;Lycée Professionnel Joseph Gallieni;39 Rue Adonis;31086;Toulouse;43.6313119;1.4271439
+0220072J;Lycée Professionnel Joseph Savina;5 Place De La Republique;22220;Treguier;48.7859048;-3.2335656
+0680027K;Lycée Professionnel Joseph Vogt;5 Rue Paul Burgi;68290;Masevaux;47.7681754;6.9983689
+0010017N;Lycée Professionnel Joseph-marie Carriat;1 Rue De Crouy;01011;Bourg En Bresse;46.1981597;5.2225406
+0690281F;Lycée Professionnel Joseph-marie Jacquard;20 Rue Louis Auguste Blanqui;69921;Oullins;45.7118138;4.8102856
+0250063H;Lycée Professionnel Jouffroy D'abbans;16 Rue De L'helvetie;25112;Baume Les Dames;47.3504244;6.3520577
+0570237Y;Lycée Professionnel Jouffroy D'abbans;39 Chemin D'avril;57250;Moyeuvre Grande;49.264711;6.0130248
+0260042E;Lycée Professionnel Jules Algoud;Rue Barthelemy De Laffemas;26901;Valence;44.917059;4.915496
+6200003J;Lycée Professionnel Jules Antonini;Av Noel Franchini;20186;Ajaccio;41.919229;8.738635
+0340045P;Lycée Professionnel Jules Ferry;270 Avenue De La Colline;34077;Montpellier;43.607123;3.8419213
+0620142T;Lycée Professionnel Jules Ferry;61 Rue Bocquet Flochel;62022;Arras;50.2791979;2.7961293
+0900019G;Lycée Professionnel Jules Ferry;18 Rue De Verdun;90101;Delle;47.5088411;6.99076
+0110008Z;Lycée Professionnel Jules Fil;Boulevard Joliot Curie;11021;Carcassonne;43.2148527;2.3675002
+0760062E;Lycée Professionnel Jules Lecesne;99 Rue Jules Lecesne;76090;Le Havre;49.4931496;0.1201446
+0290102D;Lycée Professionnel Jules Lesven;34 Rue Jules Lesven;29801;Brest;48.4056622;-4.4787824
+0760132F;Lycée Professionnel Jules Siegfried;1 Rue Dume D'aplemont;76054;Le Havre;49.4974627;0.1241609
+0600063T;Lycée Professionnel Jules Uhry;10 Rue Aristide Briand;60107;Creil;49.2580804;2.4755361
+0020013D;Lycée Professionnel Jules Verne;;02401;Chateau Thierry;49.064633;3.479051
+0140020W;Lycée Professionnel Jules Verne;12 Rue Lucien Bossoutrot;14126;Mondeville;49.176963;-0.319769
+0220019B;Lycée Professionnel Jules Verne;Route De Corlay;22205;Guingamp;48.5538053;-3.1497201
+0601897L;Lycée Professionnel Jules Verne;Rue Caradane;60210;Grandvilliers;49.665342;1.939705
+0620187S;Lycée Professionnel Jules Verne;54 Avenue Du Mont Levin;62630;Etaples;50.5205723;1.6454476
+0690130S;Lycée Professionnel Jules Verne;75 Route De Saint Clement;69173;Tarare;45.8973407;4.4437914
+0020078Z;Lycée Professionnel Julie Daubie;3 Place Robert Aumont;02011;Laon;49.5611106;3.6109299
+0570145Y;Lycée Professionnel Julie Daubie;Rue Joffre;57120;Rombas;49.2534261;6.0999961
+0560042S;Lycée Professionnel Julien Crozet;4 Rue Des Recollets;56290;Port Louis;47.706917;-3.3562259
+0710077Y;Lycée Professionnel Julien De Balleure;141 Avenue Boucicaut;71321;Chalon Sur Saone;46.7888739;4.8359262
+9740082W;Lycée Professionnel Julien De Rontaunay;Cite Scolaire Du Butor;97492;St Denis;48.936181;2.357443
+0500037U;Lycée Professionnel Julliot De La Morandiere;Rue De La Crete;50406;Granville;48.8264841;-1.5782999
+0570109J;Lycée Professionnel La Briquerie;15 Route De La Briquerie;57100;Thionville;49.363583;6.1609848
+0731043M;Lycée Professionnel La Cardiniere;191 Chemin De La Cardiniere;73017;Chambery;45.5625736;5.9310737
+0350709F;Lycée Professionnel La Champagne;2 Rue Du Sergent Harris;35506;Vitre;48.1210809;-1.229255
+0440032W;Lycée Professionnel La Chauviniere;2 Rue De La Fantaisie;44322;Nantes;47.2535027;-1.5719339
+0220064A;Lycée Professionnel La Closerie;10 Rue Pierre Loti;22410;St Quay Portrieux;48.6576705;-2.8477013
+0831354W;Lycée Professionnel La Coudouliere;Chemin De La Coudouliere;83140;Six Fours Les Plages;43.0975054;5.8170999
+0511190S;Lycée Professionnel La Fontaine Du Ve;Avenue De La Fontaine Du Ve;51122;Sezanne;48.726156;3.7170255
+0572755K;Lycée Professionnel La Malgrange;Chaussee D'amerique;57129;Thionville;49.3717995;6.16505
+0330069W;Lycée Professionnel La Morlette;62 Rue Camille Pelletan;33151;Cenon;44.8564425;-0.5190718
+0620220C;Lycée Professionnel La Peupleraie;Rue Jules Mattez;62430;Sallaumines;50.4131724;2.8628092
+0500027H;Lycée Professionnel La Roquelle;5 Rue Des Courtilles;50207;Coutances;49.0412575;-1.4410444
+0490005P;Lycée Professionnel La Roseraie;200 Rue Du Docteur Guichard;49015;Angers;47.4491825;-0.5502084
+0691676X;Lycée Professionnel La Sauvagere;5 Rue Communieu;69256;Lyon 09;45.7963868;4.8256612
+0940137N;Lycée Professionnel La Source;54 Avenue De La Source;94130;Nogent Sur Marne;48.8298866;2.4656826
+0540060X;Lycée Professionnel La Tournelle;2 Rue De Lorraine;54550;Pont St Vincent;48.6020214;6.1028929
+0920158X;Lycée Professionnel La Tournelle;87 Boulevard National;92250;La Garenne Colombes;48.9076332;2.2381311
+9720430K;Lycée Professionnel La Trinite;Cite Scolaire Frantz Fanon;97220;La Trinite;0.0;0.0
+0190701J;Lycée Professionnel Lavoisier;Rue Lavoisier;19311;Brive La Gaillarde;45.1527958;1.5133529
+0590189K;Lycée Professionnel Lavoisier;31 Rue Lavoisier;59059;Roubaix;50.676634;3.1298011
+0601470X;Lycée Professionnel Lavoisier;8 Rue Jules Ferry;60110;Meru;49.2297048;2.125951
+0771995A;Lycée Professionnel Le Champ De Claye;71 Rue Pasteur;77414;Claye Souilly;48.9380503;2.6749314
+0080048J;Lycée Professionnel Le Chateau;1 Place Du Chateau;08208;Sedan;49.6930187;4.9075711
+0880001Y;Lycée Professionnel Le Chesnois;44 Rue Du Chesnois;88240;Bains Les Bains;48.0003643;6.2481647
+0020061F;Lycée Professionnel Le Corbusier;Passage Le Corbusier;02200;Soissons;49.376636;3.32342
+0590216P;Lycée Professionnel Le Corbusier;74 Rue De Lille;59208;Tourcoing;50.7224263;3.1544077
+0950656X;Lycée Professionnel Le Corbusier;30 Rue Jean Jaures;95240;Cormeilles En Parisis;48.9784415;2.2015132
+0390020K;Lycée Professionnel Le Corbusier - Batiment;255 Rue Charles Ragmey;39015;Lons Le Saunier;46.6790667;5.5648843
+0860039A;Lycée Professionnel Le Dolmen;71 Rue Du Dolmen;86036;Poitiers;46.5781164;0.3674768
+0730006K;Lycée Professionnel Le Grand Arc;265 Chemin De La Charrette;73200;Albertville;45.661974;6.3661905
+0881339C;Lycée Professionnel Le Haut De Bellieu;Route Du Stand;88306;Neufchateau;48.360448;5.706625
+0520940P;Lycée Professionnel Le Haut Du Val;9 Rue St Exupery;52008;Chaumont;48.0985269;5.1329186
+0760114L;Lycée Professionnel Le Hurle-vent;1 Avenue Jean Moulin;76470;Le Treport;50.0566018;1.3684502
+0580042F;Lycée Professionnel Le Mont-chatelet;Boulevard Saint Saturnin;58210;Varzy;47.359394;3.383169
+0740010J;Lycée Professionnel Le Saleve;59 Route D'etrembieres;74107;Annemasse;46.1860606;6.2311348
+0810016C;Lycée Professionnel Le Sidobre;80 Avenue Rene Cassin;81103;Castres;43.5958949;2.2370177
+0861113T;Lycée Professionnel Le Verger;14 Rue Jean Pidoux;86100;Chatellerault;46.8190339;0.5624545
+0130063Y;Lycée Professionnel Leau (bd);63 Boulevard Leau;13008;Marseille 08;43.2534617;5.3862316
+0440116M;Lycée Professionnel Leloup Bouhier;11 Boulevard De Launay;44100;Nantes;47.2102412;-1.5728065
+0460012N;Lycée Professionnel Leo Ferre;75 Avenue Cavaignac;46300;Gourdon;44.7341503;1.3771929
+0620059C;Lycée Professionnel Leo Lagrange;Rue Victor Schoelcher;62160;Bully Les Mines;50.4289606;2.764828
+0830016S;Lycée Professionnel Leon Blum;1111 Boulevard Leon Blum;83011;Draguignan;43.5271939;6.4854513
+0060022S;Lycée Professionnel Leon Chiris;51 Chemin Des Capucins;06130;Grasse;43.6507373;6.9307514
+9740552G;Lycée Professionnel Leon De Lepervanche;Av Raymond Mondon;97828;Le Port;-20.9343006;55.3032171
+0070009X;Lycée Professionnel Leon Pavin;Route De La Gare;07210;Chomerac;44.7068354;4.6613722
+0130172S;Lycée Professionnel Leonard De Vinci;8 Rue Du Rempart;13007;Marseille 07;43.2888013;5.3649989
+0240984P;Lycée Professionnel Leonard De Vinci;Chemin De Saltgourde;24000;Perigueux;45.1949847;0.6770043
+0260050N;Lycée Professionnel Leonard De Vinci;Avenue Henri Becquerel;26702;Pierrelatte;44.380434;4.698
+0440035Z;Lycée Professionnel Leonard De Vinci;31 Rue De La Bottiere;44319;Nantes;47.2381866;-1.5215084
+0530079Y;Lycée Professionnel Leonard De Vinci;129 Boulevard De L Europe;53103;Mayenne;48.3010639;-0.623681
+0595678B;Lycée Professionnel Leonard De Vinci;Rue De La Renaissance;59125;Trith St Leger;50.330411;3.482308
+0791029P;Lycée Professionnel Leonard De Vinci;37 Boulevard Lescure;79300;Bressuire;46.8321172;-0.4870723
+0920680P;Lycée Professionnel Leonard De Vinci;5 Avenue Henri Barbusse;92220;Bagneux;48.7979653;2.3079713
+0170030R;Lycée Professionnel Leonce Vieljeux;118 Rue Des Gonthieres;17001;La Rochelle;46.1774956;-1.1513475
+0741164N;Lycée Professionnel Les Carillons;3 Avenue De Prelevet;74962;Cran Gevrier;45.8949329;6.1014022
+0360011S;Lycée Professionnel Les Charmilles;;36019;Chateauroux;46.859935;1.7200194
+0331460H;Lycée Professionnel Les Chartrons;130 Rue Du Jardin Public;33026;Bordeaux;44.8558775;-0.573314
+0921592F;Lycée Professionnel Les Cotes De Villebon;3 Rue Henri Etlin;92360;Meudon;48.794339;2.2211379
+0520041M;Lycée Professionnel Les Franchises;65 Rue Eugene Gallion;52200;Langres;47.8670425;5.3428612
+0911493D;Lycée Professionnel Les Freres Moreau;Rte De Brunoy;91480;Quincy Sous Senart;48.6769958;2.5295462
+0630256L;Lycée Professionnel Les Gravouses;4 Rue De Barante;63000;Clermont Ferrand;45.7867625;3.0677884
+0790090U;Lycée Professionnel Les Grippeaux;1 Rue Edouard Herriot;79204;Parthenay;46.638001;-0.2595192
+0592833J;Lycée Professionnel Les Hauts De Flandre;Rue Jude Blanckaert;59474;Seclin;50.5438352;3.0261402
+0250067M;Lycée Professionnel Les Huisselets;8 Avenue De Lattre De Tassigny;25206;Montbeliard;47.5117132;6.7966991
+0600004D;Lycée Professionnel Les Jacobins;2 Rue Vincent De Beauvais;60000;Beauvais;49.4306564;2.086988
+0100039N;Lycée Professionnel Les Lombards;12 Avenue Des Lombards;10025;Troyes;48.2747672;4.0725464
+0060042N;Lycée Professionnel Les Palmiers;15 Avenue Banco;06300;Nice;43.6985849;7.2919792
+0500028J;Lycée Professionnel Les Sapins;7 Rue Des Courtilles;50207;Coutances;49.0412575;-1.4410444
+0440537V;Lycée Professionnel Les Savarieres;5 Avenue De Glinde;44230;St Sebastien Sur Loire;47.211077;-1.4905961
+0860010U;Lycée Professionnel Les Terres Rouges;Rue Jean Moulin;86400;Civray;46.134811;0.284259
+0550004F;Lycée Professionnel Ligier Richier;3 R Du Stade;55013;Bar Le Duc;48.7757163;5.1677519
+0772225A;Lycée Professionnel Lino Ventura;Avenue Marcel Pagnol;77834;Ozoir La Ferriere;48.7607611;2.6924768
+0381602K;Lycée Professionnel L'odyssee;5 Boulevard Des Colleges;38232;Pont De Cheruy;45.7571727;5.1765032
+0270027H;Lycée Professionnel Louis Aragon;23 Rue Vieille D'eragny;27140;Gisors;49.279774;1.776579
+0440352U;Lycée Professionnel Louis Armand;Bd Jean De Grandmaison;44270;Machecoul;46.9910112;-1.8193584
+0592712C;Lycée Professionnel Louis Armand;Rue Mermoz;59571;Jeumont;50.2945453;4.1134706
+0731249L;Lycée Professionnel Louis Armand;321 Rue Du Grand Champ;73020;Chambery;45.5947932;5.9203139
+0860052P;Lycée Professionnel Louis Armand;63 Rue De La Bugellerie;86022;Poitiers;46.5974421;0.3312595
+0910756C;Lycée Professionnel Louis Armand;9 Rue Pierre De Coubertin;91330;Yerres;48.7130134;2.4793774
+0130033R;Lycée Professionnel Louis Bleriot;8 Bd De La Liberation;13721;Marignane;43.4192168;5.251698
+0592611T;Lycée Professionnel Louis Bleriot;Rue Gauthier;59407;Cambrai;50.162795;3.254045
+0780273Y;Lycée Professionnel Louis Bleriot;Rue Leo Lagrange;78197;Trappes;48.777248;1.981663
+0911401D;Lycée Professionnel Louis Bleriot;2 Avenue Des Meuniers;91154;Etampes;48.4369719;2.1466572
+0921505L;Lycée Professionnel Louis Dardenne;25 Rue Louis Dardenne;92170;Vanves;48.8200923;2.287475
+0400004M;Lycée Professionnel Louis Darmante;23 Rue Jean Baptiste Gabarra;40130;Capbreton;43.6409194;-1.4311885
+0640012R;Lycée Professionnel Louis De Foix;4 Av Jean Rostand;64103;Bayonne;43.4849353;-1.4754015
+0160119T;Lycée Professionnel Louis Delage;245 Avenue Victor Hugo;16100;Cognac;45.6873156;-0.310592
+9710052E;Lycée Professionnel Louis Delgres;Rue Amedee Fengarol;97160;Le Moule;0.0;0.0
+0250002S;Lycée Professionnel Louis Garnier;29 Rue Des Cantons;25404;Audincourt;47.4827916;6.8496192
+0230027E;Lycée Professionnel Louis Gaston Roussillat;La Valette;23320;St Vaury;46.220712;1.76194
+0881370L;Lycée Professionnel Louis Geisler;3 Rue De La Belle Orge;88110;Raon L Etape;48.4037543;6.8345632
+0920163C;Lycée Professionnel Louis Girard;85 Rue Louis Girard;92240;Malakoff;48.8121182;2.2822567
+0350032V;Lycée Professionnel Louis Guilloux;76 Av Des Buttes De Coesmes;35703;Rennes;48.1249592;-1.6332938
+0590187H;Lycée Professionnel Louis Loucheur;8 Boulevard De Lyon;59059;Roubaix;50.6776818;3.1752082
+0040011R;Lycée Professionnel Louis Martin Bret;Allee Du Parc;04100;Manosque;43.827348;5.7848689
+0251002D;Lycée Professionnel Louis Pergaud;91 93 Boulevard Blum;25022;Besancon;47.263734;6.0453694
+0590037V;Lycée Professionnel Louise De Bettignies;Boulevard Paul Bezin;59407;Cambrai;50.1724242;3.2399915
+0690046A;Lycée Professionnel Louise Labe;65 Boulevard Yves Farge;69007;Lyon 07;45.7396294;4.8310855
+0161003D;Lycée Professionnel Louise Michel;Rue Villebois Mareuil;16700;Ruffec;46.0266253;0.1927592
+0380099B;Lycée Professionnel Louise Michel;30 Rue Louise Michel;38037;Grenoble;45.1687882;5.7081825
+0440063E;Lycée Professionnel Louis-jacques Goussier;20 Rue Du Chateau De Reze;44400;Reze;47.1806714;-1.5600159
+0783214V;Lycée Professionnel Lucien Rene Duchesne;49 Av Maurice De Hirsch;78170;La Celle St Cloud;48.8413881;2.1185136
+0491646Y;Lycée Professionnel Ludovic Menard;24 Place Des Tilleuls;49800;Trelaze;47.4464427;-0.504172
+0700038L;Lycée Professionnel Luxembourg;16 Pl Du 11e Chasseurs;70014;Vesoul;47.6206224;6.1507974
+9840267T;Lycée Professionnel Lycée Professionnel De Faaa;Faaa;98704;Faaa;0.0;0.0
+0595856V;Lycée Professionnel Lycée Professionnel De L'yser;18 Bis Route D'esquelbecq;59726;Wormhout;50.8568096;2.4376592
+9840341Y;Lycée Professionnel Lycée Professionnel De Mahina;Mahina;98709;Mahina;-41.2713267;174.9088245
+9840166H;Lycée Professionnel Lycée Professionnel D'uturoa;Uturoa;98735;Uturoa;-16.7331706;-151.4403326
+9840160B;Lycée Professionnel Lycée Professionnel St Joseph;Papeete;98718;Punaauia;-17.5753232;-149.6059799
+9840019Y;Lycée Professionnel Lycée Protestant Tuteao A Vaih;Uturoa;98735;Uturoa;-16.7331706;-151.4403326
+0600016S;Lycée Professionnel M.grenet (industriel);13 Avenue De Huy;60321;Compiegne;49.4025055;2.8188505
+0600017T;Lycée Professionnel M.grenet (mixte);13 Avenue De Huy;60321;Compiegne;49.4025055;2.8188505
+0690047B;Lycée Professionnel Magenta;64 66 Rue Magenta;69100;Villeurbanne;45.7660861;4.8714756
+0060043P;Lycée Professionnel Magnan;34 Rue Auguste Renoir;06000;Nice;43.693971;7.2481912
+0720013Y;Lycée Professionnel Mal Leclerc Hauteclocque;Rue Du Grand Douai;72500;Chateau Du Loir;47.6925943;0.4146083
+0860022G;Lycée Professionnel Marc Godrie;19 Rue Des Meures;86206;Loudun;47.0112905;0.0893771
+0692418D;Lycée Professionnel Marc Seguin;20 Boulevard Marcel Sembat;69694;Venissieux;45.7159355;4.8819736
+0331668J;Lycée Professionnel Marcel Dassault;3 Rue Chateaubriand;33695;Merignac;44.8051719;-0.6258497
+0750788Z;Lycée Professionnel Marcel Deprez;39 Rue De La Roquette;75011;Paris 11;48.8552481;2.3736701
+0880007E;Lycée Professionnel Marcel Goulette;3 Rue Etienne Simard;88132;Charmes;48.3785984;6.2980364
+0610004Y;Lycée Professionnel Marcel Mezen;25 Rue Marcel Mezen;61041;Alencon;48.4296677;0.1044785
+0870730W;Lycée Professionnel Marcel Pagnol;103 Rue De Feytiat;87039;Limoges;45.8201238;1.2850476
+0930129K;Lycée Professionnel Marcel Pagnol;1 Rue Jules Guesde;93140;Bondy;48.9041305;2.4741275
+0760146W;Lycée Professionnel Marcel Sembat;128 Rue Leon Salva;76300;Sotteville Les Rouen;49.4152133;1.0855593
+0010021T;Lycée Professionnel Marcelle Parde;47 Avenue Alsace Lorraine;01011;Bourg En Bresse;46.2019083;5.2208402
+0611157B;Lycée Professionnel Marechal Leclerc;30 Rue Jean Henri Fabre;61014;Alencon;48.4113332;0.0934634
+0840041N;Lycée Professionnel Maria Casares;1 Rue Des Bavardages;84082;Avignon;43.9356519;4.8292506
+0753350J;Lycée Professionnel Maria Deraismes;19 Rue Maria Deraismes;75017;Paris 17;48.8947945;2.3274455
+0300041G;Lycée Professionnel Marie Curie;Route De Florac;30270;St Jean Du Gard;44.1140867;3.8718827
+0600062S;Lycée Professionnel Marie Curie;Boulevard Pierre De Coubertin;60180;Nogent Sur Oise;49.266627;2.464235
+0630023H;Lycée Professionnel Marie Curie;19 Boulevard Ambroise Brugiere;63039;Clermont Ferrand;45.7894827;3.1182095
+0690109U;Lycée Professionnel Marie Curie;64 Boulevard Eugene Reguillon;69100;Villeurbanne;45.7614463;4.8962541
+0100027A;Lycée Professionnel Marie De Champagne;2 Avenue Marie De Champagne;10026;Troyes;48.3040593;4.0617662
+0750776L;Lycée Professionnel Marie Laurencin;114 Quai De Jemmapes;75010;Paris 10;48.8746642;2.3635341
+0540061Y;Lycée Professionnel Marie Marvingt;8 Rue Jean Moulin;54510;Tomblaine;48.6960232;6.2152112
+0171432P;Lycée Professionnel Maritime;Avenue Du Marechal Juin;17022;La Rochelle;46.1575289;-1.180007
+0561486L;Lycée Professionnel Maritime;38 Avenue Louis Bougo;56410;Etel;47.6612096;-3.2015002
+9720787Y;Lycée Professionnel Maritime;Quartier Beausejour;97220;La Trinite;0.0;0.0
+0641770B;Lycée Professionnel Maritime;Av Eugene Corre Qu De L Untxin;64500;Ciboure;43.387537;-1.675432
+0762735K;Lycée Professionnel Maritime Anita Conti;84 Quai Guy De Maupassant;76402;Fecamp;49.763033;0.3711007
+7200118R;Lycée Professionnel Maritime Batterie Les Turquines;Batterie Les Turquines;20200;Bastia;42.697283;9.450881
+0561861U;Lycée Professionnel Maritime Centre Francois Toullec;10 Rue Francois Toullec;56100;Lorient;47.7323195;-3.3792775
+0623903F;Lycée Professionnel Maritime De Boulogne Le Portel;6 Rue Georges Honore;62480;Le Portel;50.7190753;1.5860061
+0442092K;Lycée Professionnel Maritime Des Pays De La Loire;Rue Du Port Boyer;44315;Nantes;47.2410135;-1.5374604
+0291107W;Lycée Professionnel Maritime Du Guilvinec;Zone D'activites De Kervarc'h;29730;Treffiagat;47.807191;-4.261599
+9741098A;Lycée Professionnel Maritime E.a.m Le Port;1 Rue De La Poste;97821;Le Port;-20.9341273;55.291486
+0501599S;Lycée Professionnel Maritime Et Aquacole;Rue Matignon;50103;Cherbourg Octeville;49.63774;-1.618745
+0341699M;Lycée Professionnel Maritime Paul Bousquet;Rue Des Cormorans;34207;Sete;43.4192489;3.6729485
+0221624W;Lycée Professionnel Maritime Pierre Loti;1 Rue Pierre Loti;22501;Paimpol;48.8017255;-3.0192327
+0351914R;Lycée Professionnel Maritime Public;116 Bd Des Tallards;35403;St Malo;48.64063;-2.0087017
+0070031W;Lycée Professionnel Marius Bouvier;Avenue Lamastre;07301;Tournon Sur Rhone;45.068112;4.799918
+0570077Z;Lycée Professionnel Maryse Bastie;Rue Du Tivoli;57700;Hayange;49.3347331;6.0763366
+0350062C;Lycée Professionnel Maupertuis;Rue Pierre De Coubertin;35407;St Malo;48.6400752;-2.0058975
+0590133Z;Lycée Professionnel Maurice Duhamel;1079 Rue Guy Mocquet;59373;Loos;50.6025679;3.0118909
+0132319A;Lycée Professionnel Maurice Genevoix;Av Du General De Gaulle;13700;Marignane;43.4128456;5.2048951
+0030059Y;Lycée Professionnel Maurice Guyot;Rue Ernest Montuses;03105;Montlucon;46.3228476;2.6201222
+0623864N;Lycée Professionnel Maximilien De Robespierre;89 Rue Leon Blum;62307;Lens;50.4452423;2.7877082
+0711322B;Lycée Professionnel Metiers De L Automobile;1 Rue De La Manutention;71321;Chalon Sur Saone;46.7792864;4.8435194
+0750784V;Lycée Professionnel Metiers De L'ameublement;9 Rue Pierre Bourdan;75012;Paris 12;48.8468966;2.3914855
+0570144X;Lycée Professionnel Metiers Du Batiment Et T.p;154 Chemin De Blory;57950;Montigny Les Metz;49.0873538;6.1678431
+0440034Y;Lycée Professionnel Michelet;41 Boulevard Michelet;44322;Nantes;47.2343621;-1.556604
+0941298A;Lycée Professionnel Michelet;1 Rue Michelet;94120;Fontenay Sous Bois;48.8518993;2.4732442
+0270051J;Lycée Professionnel Modeste Leroy;32 Rue Pierre Brossolette;27016;Evreux;49.0216265;1.1618627
+0640080P;Lycée Professionnel Moliere;11 Rue Moliere;64301;Orthez;43.4800842;-0.7688565
+0730050H;Lycée Professionnel Monge;1 Avenue Marius Berroir;73000;Chambery;45.5660699;5.9341975
+0801194N;Lycée Professionnel Montaigne;3 Rue Montaigne;80084;Amiens;49.9115347;2.3217034
+0390021L;Lycée Professionnel Montciel;1 Avenue De Montciel;39016;Lons Le Saunier;46.6724435;5.5427757
+0260116K;Lycée Professionnel Montesquieu;2 Rue De Montesquieu;26000;Valence;44.9384823;4.9256564
+0250064J;Lycée Professionnel Montjoux;25 Av Du Cdt Marceau;25010;Besancon;47.2490074;6.016123
+0910755B;Lycée Professionnel Nadar;42bis Rue Charles Mory;91210;Draveil;48.6831045;2.4326334
+0610027Y;Lycée Professionnel Napoleon;15 Rue Des Sports;61306;L Aigle;48.7582148;0.6363548
+0330032F;Lycée Professionnel Nicolas Bremontier;152 Cours De L Yser;33077;Bordeaux;44.823338;-0.5706489
+0623463C;Lycée Professionnel Normandie-niemen;Avenue Normandie-niemen;62228;Calais;50.9523426;1.9037152
+0750796H;Lycée Professionnel Octave Feuillet;9 Rue Octave Feuillet;75016;Paris 16;48.8613442;2.2723331
+0330102G;Lycée Professionnel Odilon Redon;R Maquis Vignes Oudides;33250;Pauillac;45.1907714;-0.7495194
+0441656L;Lycée Professionnel Pablo Neruda;Rue Pablo Neruda;44340;Bouguenais;47.16973;-1.623866
+0240028A;Lycée Professionnel Pablo Picasso;64 Av Georges Pompidou;24004;Perigueux;45.1924021;0.7267429
+0620255R;Lycée Professionnel Pablo Picasso;2 Boulevard Anatole France;62210;Avion;50.4005382;2.8354531
+0693200D;Lycée Professionnel Pablo Picasso;12 Che De La Cote A Cailloux;69700;Givors;45.6012034;4.7609173
+0830059N;Lycée Professionnel Parc St Jean;Place Du 4 Septembre;83059;Toulon;43.1230487;5.9530768
+0320040V;Lycée Professionnel Pardailhan;Chemin De Baron;32008;Auch;43.6546824;0.5749589
+9740472V;Lycée Professionnel Patu De Rosemont;72 Rue Joseph Hubert;97470;St Benoit;0.0;0.0
+0910628N;Lycée Professionnel Paul Belmondo;23 Av De La Division Leclerc;91290;Arpajon;48.5966104;2.2461236
+0640013S;Lycée Professionnel Paul Bert;73 Rue Bourgneuf;64115;Bayonne;43.4908724;-1.471528
+0941355M;Lycée Professionnel Paul Bert;1 Rue Du Gue Aux Aurochs;94700;Maisons Alfort;48.8155654;2.4242767
+0330114V;Lycée Professionnel Paul Broca;7 Avenue De Verdun;33220;Ste Foy La Grande;44.8382054;0.2152357
+0141276L;Lycée Professionnel Paul Cornu;9 Rue Paul Cornu;14107;Lisieux;49.15711;0.2224269
+0570072U;Lycée Professionnel Paul Dassenoy;Route De Conthil;57340;Morhange;48.9118442;6.6439595
+0490801E;Lycée Professionnel Paul Emile Victor;101 Rue Des Roses;49243;Avrille;47.5010511;-0.5887594
+0450786K;Lycée Professionnel Paul Gauguin;10 Av De La Rech Scientifique;45071;Orleans;47.8395647;1.9428858
+0790043T;Lycée Professionnel Paul Guerin;19 Rue Des Fiefs;79004;Niort;46.3145227;-0.443402
+0050008G;Lycée Professionnel Paul Heraud;25 Chemin De Bonne;05000;Gap;44.5671853;6.0801577
+0730900G;Lycée Professionnel Paul Heroult;307 Avenue Du Mont Cenis;73302;St Jean De Maurienne;45.2725564;6.3502605
+9710418C;Lycée Professionnel Paul Lacave;Avenue Germain St-ruff;97130;Capesterre Belle Eau;16.05;-61.57
+0300011Z;Lycée Professionnel Paul Langevin;21 Rue De La Redoute;30301;Beaucaire;43.8118169;4.6414128
+0593495D;Lycée Professionnel Paul Langevin;Rue Paul Langevin;59119;Waziers;50.387032;3.102779
+0600061R;Lycée Professionnel Paul Langevin;3 Avenue Montaigne;60009;Beauvais;49.414897;2.1063143
+0921677Y;Lycée Professionnel Paul Langevin;9 Rue Paul Langevin;92000;Nanterre;48.890582;2.184257
+9740934X;Lycée Professionnel Paul Langevin;6 Allee Des Hibiscus;97480;St Joseph;-21.3813367;55.6096619
+0540037X;Lycée Professionnel Paul Lapie;6 Avenue Du Docteur Kahn;54301;Luneville;48.5978471;6.5103348
+0540082W;Lycée Professionnel Paul Louis Cyffle;1 Rue Cyffle;54000;Nancy;48.6867685;6.1812641
+0921625S;Lycée Professionnel Paul Painleve;5 Rue De La Montagne;92400;Courbevoie;48.8955744;2.2588753
+0060028Y;Lycée Professionnel Paul Valery;1 Avenue Saint Jacques;06500;Menton;43.7834263;7.511873
+0080838T;Lycée Professionnel Paul Verlaine;Rue Du Docteur Gobinet;08305;Rethel;49.5167514;4.3704596
+0130012T;Lycée Professionnel Perdiguier;Che Des Moines Qua Du Trebon;13200;Arles;43.6017739;4.6243751
+9720091S;Lycée Professionnel Petit Manoir;Quartier Petit Manoir;97286;Le Lamentin;0.0;0.0
+9830306R;Lycée Professionnel Petro Attiti;1 Rue Teillard De Chardin;98849;Noumea;-22.2758;166.458
+0332345V;Lycée Professionnel Philadelphe De Gerde;3, Allee Philadelphe De Gerde;33600;Pessac;44.7870737;-0.6520039
+0332346W;Lycée Professionnel Philippe Cousteau;Rue Arnaudin;33240;St Andre De Cubzac;44.9912798;-0.4501201
+0580020G;Lycée Professionnel Pierre Beregovoy;56 Rue Gambetta;58600;Fourchambault;47.019317;3.0831499
+0420021V;Lycée Professionnel Pierre Coton;Le Bourg;42510;Neronde;45.838012;4.238368
+0620189U;Lycée Professionnel Pierre De Coubertin;320 Boulevard Du 8 Mai;62225;Calais;50.958627;1.837843
+0770932V;Lycée Professionnel Pierre De Coubertin;Chaussee De Paris;77100;Meaux;48.9559126;2.869815
+0421691K;Lycée Professionnel Pierre Desgranges;32 Rue Des Bullieux;42166;Andrezieux Boutheon;45.5374319;4.2959439
+0170031S;Lycée Professionnel Pierre Doriole;221 Avenue De Perigny;17012;La Rochelle;46.1616962;-1.1243029
+0060027X;Lycée Professionnel Pierre Et Marie Curie;353 Av Du Doyen Jean Lepine;06500;Menton;43.7776606;7.4846068
+0500066A;Lycée Professionnel Pierre Et Marie Curie;377 Rue De L Exode;50010;St Lo;49.107496;-1.0893717
+0530040F;Lycée Professionnel Pierre Et Marie Curie;1 Rue Branly;53204;Chateau Gontier;47.8341556;-0.6994932
+0570051W;Lycée Professionnel Pierre Et Marie Curie;21 Rue Des Vosges;57804;Freyming Merlebach;49.153717;6.8188013
+0590015W;Lycée Professionnel Pierre Et Marie Curie;1 Rue Du Foyer;59620;Aulnoye Aymeries;50.2026966;3.835739
+0640079N;Lycée Professionnel Pierre Et Marie Curie;Avenue Pierre Angot;64150;Mourenx;43.3703832;-0.6251249
+0760013B;Lycée Professionnel Pierre Et Marie Curie;33 Rue Du Calvaire;76210;Bolbec;49.5698937;0.4817532
+0890053Z;Lycée Professionnel Pierre Et Marie Curie;1 Place Walesa;89094;Sens;48.20065;3.28268
+0132276D;Lycée Professionnel Pierre Latecoere;Avenue Des Bolles;13800;Istres;43.5039779;4.994962
+0752388N;Lycée Professionnel Pierre Lescot;35 Rue Des Bourdonnais;75001;Paris 01;48.8602998;2.3453363
+0050027C;Lycée Professionnel Pierre Mendes France;Place Des Aires;05400;Veynes;44.5309937;5.7990979
+0650035K;Lycée Professionnel Pierre Mendes France;Rue Du College;65501;Vic En Bigorre;43.388411;0.056435
+0801514L;Lycée Professionnel Pierre Mendes France;Route De Saint Denis;80201;Peronne;49.9361685;2.9435387
+0880013L;Lycée Professionnel Pierre Mendes France;100 Rue Du Shah De Perse;88141;Contrexeville;48.1816011;5.8950657
+0911578W;Lycée Professionnel Pierre Mendes France;Av De L Aunette;91133;Ris Orangis;48.6393295;2.4105804
+0620167V;Lycée Professionnel Pierre Mendes-france;Rue Antoine De Saint Exupery;62166;St Pol Sur Ternoise;51.024809;2.343421
+0620192X;Lycée Professionnel Pierre Mendes-france;Rue De Saint Omer;62701;Bruay La Buissiere;50.4874136;2.5382001
+0951090U;Lycée Professionnel Pierre Mendes-france;1 Rue De Goussainville;95400;Villiers Le Bel;49.0047221;2.4173569
+0390024P;Lycée Professionnel Pierre Vernotte;6 Route De St Laurent;39261;Moirans En Montagne;46.4348496;5.7248544
+0250013D;Lycée Professionnel Pierre-adrien Paris;8 Rue Mercator;25000;Besancon;47.2604676;5.9960551
+0160048R;Lycée Professionnel Pierre-andre Chabanne;28 Rue Bir Hakeim;16260;Chasseneuil Sur Bonnieure;45.8235254;0.453489
+0590005K;Lycée Professionnel Pierre-joseph Fontaine;2 Av Leo Lagrange;59416;Anzin;50.3696507;3.4916736
+0592610S;Lycée Professionnel Pierre-joseph Laurent;128 Rue Edmond Laudeau;59580;Aniche;50.3265557;3.2625615
+0101122R;Lycée Professionnel Pigier;40 Place Jean Jaures;10000;Troyes;48.2947188;4.0701812
+9720844K;Lycée Professionnel Place D Armes;Place D Armes;97232;Le Lamentin;14.6166147;-60.9887419
+0590098L;Lycée Professionnel Placide Courtoy;Rue Pierre Curie;59330;Hautmont;50.255989;3.905332
+0130054N;Lycée Professionnel Poinso-chapuis;49 Traverse Parangon;13272;Marseille 08;43.2427647;5.3806897
+9720005Y;Lycée Professionnel Pointe Des Negres;Rue Marie Therese Gertrude;97261;Fort De France;14.6040646;-61.084565
+0700882D;Lycée Professionnel Pontarcher;Place Jacques Brel;70014;Vesoul;47.6354117;6.1686975
+0240039M;Lycée Professionnel Porte D Aquitaine;15 Rue A Bonneau;24800;Thiviers;45.4243846;0.9267852
+0271319M;Lycée Professionnel Porte De Normandie;830 Chemin Des Poissonniers;27130;Verneuil Sur Avre;48.7405634;0.9156189
+0740031G;Lycée Professionnel Porte Des Alpes;26 Rue De La Curdy;74151;Rumilly;45.8644993;5.9406325
+0380101D;Lycée Professionnel Portes De L'oisans;Avenue Aristide Briand;38220;Vizille;45.067413;5.769686
+0240048X;Lycée Professionnel Pre De Cordy;Av. De La Dordogne;24200;Sarlat La Caneda;44.8754955;1.2126435
+0390056Z;Lycée Professionnel Pre Saint Sauveur;Cite Scolaire Pre St Sauveur;39201;St Claude;46.387405;5.8677659
+0430103D;Lycée Professionnel Privé;1 Place Neron;43120;Monistrol Sur Loire;45.2946516;4.1708988
+9711139L;Lycée Professionnel Privé 3ec;1108 Imm.capt Grammont;97142;Les Abymes;16.27395;-61.502615
+0700947Z;Lycée Professionnel Privé Agricole Sainte Marie;13 Rue Victor Hugo;70106;Gray;47.4436381;5.5914217
+0311200L;Lycée Professionnel Privé Airbus - Toulouse;57 Chemin Du Sang De Serp;31060;Toulouse;43.6158658;1.4282595
+0382093U;Lycée Professionnel Privé Alpes Sud-isere;Rond Point Du Villaret-susville;38350;Susville;44.9166667;5.7811111
+0030112F;Lycée Professionnel Privé Anna Rodier;39 Cours Jean Jaures;03008;Moulins;46.5669568;3.3349455
+0631050Z;Lycée Professionnel Privé Anna Rodier;54 Boulevard Jean Jaures;63000;Clermont Ferrand;45.7679299;3.0798386
+0570283Y;Lycée Professionnel Privé Anne De Mejanes;3 Rue Goussaud;57000;Metz;49.1235079;6.173823
+0420987V;Lycée Professionnel Privé Arago;28 Rue Arago;42300;Roanne;46.0292922;4.0690676
+0920979P;Lycée Professionnel Privé Atelier App.pte Mecan.;19 Rue Victor Hugo;92130;Issy Les Moulineaux;48.8264404;2.2758007
+0690652J;Lycée Professionnel Privé Ateliers Appren De L'industrie;148 Avenue Franklin Roosevelt;69120;Vaulx En Velin;45.7546702;4.9351651
+9710879D;Lycée Professionnel Privé Bel Air;Moudong Nord;97122;Baie Mahault;0.0;0.0
+0312059V;Lycée Professionnel Privé Billieres;18 Rue Du 14 Juillet;31100;Toulouse;43.5910132;1.414594
+0450764L;Lycée Professionnel Privé Blanche De Castille;;45303;Pithiviers;48.156984;2.192561
+9710083N;Lycée Professionnel Privé Boc-calmet;Rue Paul Lacave- Assainissement;97110;Pointe A Pitre;16.241111;-61.533056
+0381806G;Lycée Professionnel Privé Bordier;26 Rue Prosper Merimee;38100;Grenoble;45.1737355;5.7260681
+0292139T;Lycée Professionnel Privé Brest - Rive Droite - Javouhey;4 Rue Des Remparts;29238;Brest;48.3832192;-4.5023223
+0131484T;Lycée Professionnel Privé Brise Lames;Route De La Vierge;13500;Martigues;43.4170127;5.0437853
+0131463V;Lycée Professionnel Privé Cabucelle (la);16 Boulevard Denis Papin;13015;Marseille 15;43.3342857;5.3626747
+0312063Z;Lycée Professionnel Privé Castelnouvel;Castelnouvel;31490;Leguevin;43.594447;1.254584
+9741308D;Lycée Professionnel Privé Catholique De La Montagne;Chemin Piton Tresor Pk7;97417;St Denis;48.936181;2.357443
+0131474G;Lycée Professionnel Privé Caucadis;Boulevard Alfred Casile;13127;Vitrolles;43.4478245;5.2426452
+0941407U;Lycée Professionnel Privé Ce 3p;5 Rue Rene Robin;94200;Ivry Sur Seine;48.8137588;2.3781028
+0740152N;Lycée Professionnel Privé Cecam;Chemin De Beauregard;74490;St Jeoire;46.1429375;6.4595274
+0131415T;Lycée Professionnel Privé Celony;4 Bis Av Delattre De Tassigny;13090;Aix En Provence;43.5303056;5.4383758
+0241129X;Lycée Professionnel Privé Cent.tech.coif.esthetique;49 Rue Candillac;24100;Bergerac;44.8507187;0.4855329
+0300028T;Lycée Professionnel Privé Centre Cevenol;3 Quai Boissier De Sauvages;30100;Ales;44.12714;4.0767327
+0240092V;Lycée Professionnel Privé Chambre Com Et Industrie;Zone Industrielle Av H Deluc;24750;Boulazac;45.180548;0.768949
+0541998D;Lycée Professionnel Privé Charles De Foucauld;1 Rue Jeannot;54000;Nancy;48.6894923;6.1881706
+0672602P;Lycée Professionnel Privé Charles De Foucauld;Allee D'athenes;67306;Schiltigheim;48.6075759;7.7099709
+0133276R;Lycée Professionnel Privé Charles Peguy;102 Rue Sylvabelle;13006;Marseille 06;43.2882408;5.3748305
+0131445A;Lycée Professionnel Privé Charlotte Grawitz;20 Chemin Chateau Gombert;13013;Marseille 13;43.3350871;5.4206869
+0381758E;Lycée Professionnel Privé Charmilles;15 Rue Montesquieu;38100;Grenoble;45.1762181;5.7280364
+0932424E;Lycée Professionnel Privé Chne Or;17 Rue Du Clos Benard;93300;Aubervilliers;48.9114654;2.3861326
+0541370W;Lycée Professionnel Privé Claude Daunot;10 Boulevard Clemenceau;54052;Nancy;48.6779501;6.1805671
+0133305X;Lycée Professionnel Privé Clovis Hugues;1 Rue Fernand Dol;13100;Aix En Provence;43.5260677;5.452697
+0754165V;Lycée Professionnel Privé Coiffure;15-17 Rue Des Fillettes;75018;Paris 18;48.8946669;2.3633961
+0771190A;Lycée Professionnel Privé Coiffure Et Esthetique;6 Rue Jean Jaures;77670;St Mammes;48.3866661;2.8150085
+0440267B;Lycée Professionnel Privé Coiffure P.masson;12 Rue D Alger;44100;Nantes;47.210504;-1.5676183
+0601164P;Lycée Professionnel Privé Croiset;7 Avenue De Joinville;60500;Chantilly;49.1761667;2.4601513
+0470821M;Lycée Professionnel Privé Ctr.etu.fem.rur.l Ermitage;304 Avenue Amouroux;47000;Agen;44.2118115;0.6187838
+0442071M;Lycée Professionnel Privé Daniel Brottier;Chemin Du Couvent;44340;Bouguenais;47.187338;-1.5866796
+0762285W;Lycée Professionnel Privé Daniel Brottier;11 Impasse Du Chateau;76660;Smermesnil;49.8063368;1.4206513
+0691724Z;Lycée Professionnel Privé De Coiffure;22 Rue D'algerie;69001;Lyon 01;45.767637;4.8327698
+0691723Y;Lycée Professionnel Privé De Coiffure De Lyon;28 Rue Valentin Couturier;69004;Lyon 04;45.7763552;4.8274152
+9711080X;Lycée Professionnel Privé De Coiffure Et D'esthetique;Lot 76 Centre Commercial;97139;Les Abymes;16.27395;-61.502615
+0311190A;Lycée Professionnel Privé De Coiffure Skhole D'art;55 Avenue Louis Breguet;31400;Toulouse;43.5772349;1.4794166
+0820885S;Lycée Professionnel Privé De Coiffure Skhole D'art;3 Rue Bataille De Dunkerque;82000;Montauban;44.0068309;1.3461514
+0280687V;Lycée Professionnel Privé De Couasnon;37 Rue De Moronval;28100;Dreux;48.7329442;1.3867752
+0441791H;Lycée Professionnel Privé De L Erdre;13 Rue Du Gal Leclerc;44390;Nort Sur Erdre;47.4389938;-1.4992748
+0593020M;Lycée Professionnel Privé De La Coiffure;68 Rue Saint Etienne;59800;Lille;50.6361624;3.0595327
+0761353H;Lycée Professionnel Privé De La Coiffure A. Pourriere;24 Rue D'herbouville;76000;Rouen;49.4489961;1.0861707
+0331547C;Lycée Professionnel Privé De La Cote D Argent;46 Bis Avenue De La Liberation;33380;Biganos;44.6407598;-0.9743709
+0573270V;Lycée Professionnel Privé De La Salle;2 Rue St Maximin;57070;Metz;49.1048183;6.1935928
+0420982P;Lycée Professionnel Privé Des Collines;4 Rue Ferdinand Buisson;42800;Rive De Gier;45.5261265;4.6165582
+0561606S;Lycée Professionnel Privé Des Metiers N.dame De La Paix;Beg Er Lann;56275;Ploemeur;47.7506666;-3.3973939
+0061462G;Lycée Professionnel Privé Don Bosco;40 Place Don Bosco;06046;Nice;43.7086006;7.2813297
+0131466Y;Lycée Professionnel Privé Don Bosco;78 Rue Stanislas Torrents;13006;Marseille 06;43.2855609;5.3795529
+0530073S;Lycée Professionnel Privé Don Bosco;18 Boulevard Anatole France;53102;Mayenne;48.295993;-0.6158636
+0622211S;Lycée Professionnel Privé Du Calaisis;71 73 Rue Chantilly;62100;Calais;50.9435057;1.8553408
+0070113K;Lycée Professionnel Privé Du Sacre Coeur;7 Avenue De La Gare;07301;Tournon Sur Rhone;45.0637796;4.834587
+0720823D;Lycée Professionnel Privé Du Sacre Coeur De Pontlieu;30 Rue Des Sapins;72100;Le Mans;47.9798872;0.2105683
+0022109G;Lycée Professionnel Privé Du Val De Serre;10 Rue De La Halle;02270;Pouilly Sur Serre;49.6813163;3.587762
+0741285V;Lycée Professionnel Privé E.c.a.;2 Rue Des Carillons;74942;Annecy Le Vieux;45.914268;6.1419587
+0760143T;Lycée Professionnel Privé Ecole Industrielle;50 Rue Meridienne;76100;Rouen;49.4256007;1.0837566
+0131434N;Lycée Professionnel Privé Ecole Libre De Metiers;24 Rue Des Bons Enfants;13006;Marseille 06;43.2908998;5.388285
+0132193N;Lycée Professionnel Privé Edmond Rostand;114 Rue Edmond Rostand;13006;Marseille 06;43.2821817;5.3828274
+0440255N;Lycée Professionnel Privé Encia;6 Rue Crebillon;44003;Nantes;47.214125;-1.5596911
+0011258M;Lycée Professionnel Privé Ensemble Scolaire Lamartine;41 Rue Georges Girerd;01302;Belley;45.7612095;5.6848602
+0941915W;Lycée Professionnel Privé Espace Beaute Thalgo;16 Rue D'alsace Lorraine;94100;St Maur Des Fosses;48.8054972;2.4744603
+0754184R;Lycée Professionnel Privé Eugene Napoleon;254 Rue Du Fg Saint Antoine;75012;Paris 12;48.8489028;2.3916976
+9710827X;Lycée Professionnel Privé Faeec;Lot 76 Centre Commercial;97139;Les Abymes;16.27395;-61.502615
+0940823J;Lycée Professionnel Privé Foyer Des Ptt;36 Avenue Du President Wilson;94234;Cachan;48.7922449;2.3291571
+9830272D;Lycée Professionnel Privé Francois D Assise (ddec);Lot 25 Pie Era;98870;Bourail;44.5379358;1.6760691
+0381732B;Lycée Professionnel Privé Francois Verguin;Rte De Sablons;38550;Le Peage De Roussillon;45.3619259;4.7957204
+0280689X;Lycée Professionnel Privé Francoise D Aubigne;23 Rue Du Marechal Maunoury;28130;Maintenon;48.587838;1.5762147
+9830273E;Lycée Professionnel Privé Gabriel Rivat (ddec);Village;98824;Pouebo;0.0;0.0
+0920985W;Lycée Professionnel Privé Georges Guerin;5 Rue Deves;92200;Neuilly Sur Seine;48.8831271;2.2707066
+0610724F;Lycée Professionnel Privé Giel Don Bosco;Les Cours;61210;Giel Courteilles;48.7569657;-0.1951044
+0592973L;Lycée Professionnel Privé Gilbert Cesbron;34 Rue Richard Lenoir;59060;Roubaix;50.6978383;3.171796
+0631736V;Lycée Professionnel Privé Godefroy De Bouillon;14 Rue Godefroy De Bouillon;63037;Clermont Ferrand;45.781122;3.0929055
+0700077D;Lycée Professionnel Privé Gpe Scolaire De La Compassion;Route De Saint Sulpice;70110;Villersexel;47.551021;6.432211
+0941722L;Lycée Professionnel Privé Gregor Mendel;205 Rue De Fontenay;94300;Vincennes;48.8488616;2.4272778
+0280691Z;Lycée Professionnel Privé Guery;5 Rue Des Marais;28000;Chartres;48.4414704;1.4978421
+0592976P;Lycée Professionnel Privé Helene Boucher;Rue Roger Salengro;59490;Somain;50.3734891;3.2807582
+0133282X;Lycée Professionnel Privé Henri Leroy;1 Rue Des Ecoles;13230;Port St Louis Du Rhone;43.3867633;4.8031175
+0881623L;Lycée Professionnel Privé Horticulture Et Paysage;Roville Aux Chenes;88700;Roville Aux Chenes;48.358452;6.630084
+9710878C;Lycée Professionnel Privé I S F C A;9 Centre Commercial Le Galion;97139;Les Abymes;16.27395;-61.502615
+0442227G;Lycée Professionnel Privé Ifom;17 Bd Des Martyrs Nantais;44200;Nantes;47.2030171;-1.5454647
+0641664L;Lycée Professionnel Privé Immac.concept.beau Frene;Bd Edouard Herriot;64051;Pau;43.3050247;-0.3782249
+0520692V;Lycée Professionnel Privé Immaculee Conception;1 Bis Rue Du Mal De Lattre;52115;St Dizier;48.6385221;4.9486945
+0530904V;Lycée Professionnel Privé Immaculee Conception;15 Rue Crossardiere;53000;Laval;48.0734452;-0.7680567
+0593027V;Lycée Professionnel Privé Industries Lilloises;82 Rue Des Meuniers;59000;Lille;50.6221365;3.0581427
+0500132X;Lycée Professionnel Privé Ingenieur Cachin;4 Rue Ingenieur Cachin;50100;Cherbourg Octeville;49.6396131;-1.617479
+0691875N;Lycée Professionnel Privé Inst Techn Carrieres Carole;21 Quai Tilsitt;69002;Lyon 02;45.7558718;4.8263561
+0593094T;Lycée Professionnel Privé Institut Familial;16 Ter Rue Lamartine;59426;Armentieres;50.6866796;2.8758479
+0141866C;Lycée Professionnel Privé Institut Lemonnier;60 Rue D Herouville;14013;Caen;49.1935816;-0.3487181
+0501825M;Lycée Professionnel Privé Institut Saint Lo;18 Rue De L'oratoire;50180;Agneaux;49.1184709;-1.1060995
+0680154Y;Lycée Professionnel Privé Institut Sonnenberg;1 Rue Du Moulin;68130;Carspach;47.6651522;7.2235055
+0680161F;Lycée Professionnel Privé Institution Don Bosco;60 Rue D'ensisheim;68272;Wittenheim;47.8136036;7.3394336
+0681656F;Lycée Professionnel Privé Institution Saint Jean;3 Route De Bale;68025;Colmar;48.0119328;7.3843166
+0672305S;Lycée Professionnel Privé Institution Sainte Clotilde;19 Rue De Verdun;67083;Strasbourg;48.5874674;7.7724234
+0672299K;Lycée Professionnel Privé Institution Ste Philomene;19a Boulevard Hanauer;67504;Haguenau;48.8113152;7.7888563
+0570225K;Lycée Professionnel Privé Interentreprise;Rue Robert Schuman;57220;Boulay Moselle;49.185664;6.491576
+0572500H;Lycée Professionnel Privé Interentreprise;Rue Jacques Touba;57430;Sarralbe;48.991472;7.024928
+0690654L;Lycée Professionnel Privé Interfora;6 Rue Jean Mace;69190;St Fons;45.7059602;4.8532599
+0492082X;Lycée Professionnel Privé Itec;2 Rue Pilastre;49100;Angers;47.479365;-0.5445983
+0381899H;Lycée Professionnel Privé Itec Boisfleury;76 Grande Rue;38701;La Tronche;45.2047507;5.7377606
+0930934K;Lycée Professionnel Privé Itmc;Cent Aff Paris Nord Imm Ampere;93153;Le Blanc Mesnil;48.941345;2.46436
+0132790M;Lycée Professionnel Privé Jacques Raynaud;59 Traverse Susini;13013;Marseille 13;43.3355765;5.4182619
+0690660T;Lycée Professionnel Privé Japy;1 Place Louis Pradel;69203;Lyon 01;45.7680387;4.8375832
+0311883D;Lycée Professionnel Privé Jasmin Coiffure;4 Rue Des Teinturiers;31300;Toulouse;43.5969809;1.4357278
+0382893N;Lycée Professionnel Privé Jean Marie Vianney;22 Avenue Hector Berlioz;38260;La Cote St Andre;45.386288;5.26266
+9830270B;Lycée Professionnel Privé Jean Xxiii (ddec);Route Du Mont Mou;98890;Paita;0.0;0.0
+0421002L;Lycée Professionnel Privé Jean-baptiste D'allard;7 Rue Du Bief;42601;Montbrison;45.602668;4.0626362
+0693370N;Lycée Professionnel Privé Jean-baptiste De La Salle;1 Rue Neyret;69283;Lyon 01;45.7715543;4.8280213
+0541377D;Lycée Professionnel Privé Jean-baptiste Vatelot;6 Rue De La Republique;54203;Toul;48.6729066;5.8919085
+0133274N;Lycée Professionnel Privé Jeanne D Arc;5 Rue Saint Roch;13632;Arles;43.6743556;4.6432771
+0331569B;Lycée Professionnel Privé Jeanne D Arc;3 Place De La Fraternite;33230;St Medard De Guizieres;45.0154483;-0.0603156
+0610721C;Lycée Professionnel Privé Jeanne D Arc;16 Rue De La Vicomte;61203;Argentan;48.7455115;-0.0208491
+0740156T;Lycée Professionnel Privé Jeanne D Arc;18 Bis Avenue Jules Ferry;74200;Thonon Les Bains;46.3724399;6.4832781
+0880133S;Lycée Professionnel Privé Jeanne D Arc;1 Avenue Chanzy;88600;Bruyeres;48.2086223;6.7188568
+0950800D;Lycée Professionnel Privé Jeanne D Arc;20 Rue De La Liberte;95100;Argenteuil;48.9433576;2.2505002
+0950812S;Lycée Professionnel Privé Jeanne D Arc;2 Bis Bd Toussaint Lucas;95130;Franconville;48.9875913;2.2306862
+0080093H;Lycée Professionnel Privé Jeanne D'arc;27 Rue Bournizet;08400;Vouziers;49.3973761;4.6979472
+0120105Z;Lycée Professionnel Privé Jeanne D'arc;3 Place Du Mandarous;12104;Millau;44.0996885;3.0784006
+0121310J;Lycée Professionnel Privé Jeanne D'arc;23 Rue Lamartine;12402;St Affrique;43.956919;2.890886
+0220123P;Lycée Professionnel Privé Jeanne D'arc;4 Rue De La Bienfaisance;22300;Lannion;48.7287135;-3.4562299
+0381733C;Lycée Professionnel Privé Jeanne D'arc;2 Rue Jacques Prevert;38550;Le Peage De Roussillon;45.3712936;4.8011019
+0391001B;Lycée Professionnel Privé Jeanne D'arc;10 Rue Du Sauget;39303;Champagnole;46.7456228;5.9058307
+0511153B;Lycée Professionnel Privé Jeanne D'arc;94 Avenue De Laon;51100;Reims;49.2662257;4.0257119
+0730773U;Lycée Professionnel Privé Jeanne D'arc;3 Place De L'eglise;73203;Albertville;45.6780169;6.3908431
+0810108C;Lycée Professionnel Privé Jeanne D'arc;23 Rue De La Vanne;81200;Mazamet;43.4847739;2.3737673
+0881498A;Lycée Professionnel Privé Jeanne D'arc;6 Rue Du Canton;88202;Remiremont;48.0192476;6.5872824
+0881651S;Lycée Professionnel Privé Jeanne D'arc;1 Avenue De Herringen;88300;Neufchateau;48.3590156;5.6926631
+0271074W;Lycée Professionnel Privé Jeanne D'arc - Saint-anselme;11/13 R Leprevost De Beaumont;27300;Bernay;49.0942864;0.6045856
+0693372R;Lycée Professionnel Privé Jeanne De Lestonnac;132 Rue Vendome;69006;Lyon 06;45.7648477;4.8456527
+0930929E;Lycée Professionnel Privé Jeanne La Lorraine;3 Boulevard Du Nord;93340;Le Raincy;48.9017008;2.513264
+0100078F;Lycée Professionnel Privé Jeanne Mance;Rue Du Paradis;10000;Troyes;48.2903865;4.0658966
+9830299H;Lycée Professionnel Privé Joahana Vakie (ddec);Ecole De Nindiah;98816;Houailou;0.0;0.0
+0720825F;Lycée Professionnel Privé Joseph Roussel;50 Avenue Bollee;72000;Le Mans;48.0020204;0.2071975
+0450759F;Lycée Professionnel Privé L Abbaye;2 Rue De L Abbaye;45190;Beaugency;47.7765588;1.6341757
+0133275P;Lycée Professionnel Privé La Cadenelle;134 Bd Des Liberateurs;13012;Marseille 12;43.3020176;5.4572235
+0830128N;Lycée Professionnel Privé La Colette;Residence Ste Catherine Tour A;83000;Toulon;43.1200697;5.9383291
+0690697H;Lycée Professionnel Privé La Favorite Sainte Therese;107 Rue Cdt Charcot;69110;Ste Foy Les Lyon;45.7464605;4.7883369
+0740148J;Lycée Professionnel Privé La Fontaine;116 Rue De La Fontaine;74210;Faverges;45.7445312;6.2942685
+0060715V;Lycée Professionnel Privé La Providence;236 Route De Grenoble;06200;Nice;43.6819305;7.1990341
+0260082Y;Lycée Professionnel Privé La Providence;14 18 Rue Henri Chalamet;26000;Valence;44.9294183;4.8938111
+0311209W;Lycée Professionnel Privé La Providence;30 Rue Jean Moulin;31250;Revel;43.4582667;2.0065414
+0490886X;Lycée Professionnel Privé La Providence;33 Avenue Gustave Ferrie;49306;Cholet;47.0623002;-0.8582673
+0570248K;Lycée Professionnel Privé La Providence;4 Rue De Sarrelouis;57320;Bouzonville;49.2902615;6.5356148
+0570279U;Lycée Professionnel Privé La Providence;1 Rue Des Freres Mesguin;57260;Dieuze;48.8116359;6.7201268
+0331561T;Lycée Professionnel Privé La Ruche;1 Rue Poquelin Moliere;33000;Bordeaux;44.8401086;-0.576113
+0693332X;Lycée Professionnel Privé La Vidaude;Chemin De La Vidaude;69563;St Genis Laval;45.6925289;4.7800249
+0420992A;Lycée Professionnel Privé Lachaux;Rue Edouard Michot;42501;Le Chambon Feugerolles;45.3979878;4.3243091
+0020493A;Lycée Professionnel Privé Lacordaire;Route De Laon;02800;Charmes;49.655364;3.385869
+0501789Y;Lycée Professionnel Privé Le Bon Sauveur;Rue E De Surville;50001;St Lo;49.112089;-1.094787
+0381741L;Lycée Professionnel Privé Le Breda;L'epinette;38580;Allevard;45.404864;6.084012
+0640179X;Lycée Professionnel Privé Le Guichot;42 Rue D Espagne;64100;Bayonne;43.489412;-1.4771746
+0730772T;Lycée Professionnel Privé Le Margeriaz;28 Rue De Buisson Rond;73000;Barberaz;45.5665442;5.9414436
+0701016Z;Lycée Professionnel Privé Le Marteroy;7 Avenue Aristide Briand;70000;Vesoul;47.6288058;6.1668986
+0440282T;Lycée Professionnel Privé Le Masle;9 Rue Jean Pierre Dufrexou;44612;St Nazaire;47.2722287;-2.2058751
+0340923U;Lycée Professionnel Privé Le Parterre;15 Rue Courbezou;34600;Bedarieux;43.6146339;3.1582161
+0490903R;Lycée Professionnel Privé Le Pinier Neuf;;49601;Beaupreau;48.384469;0.1593171
+0131485U;Lycée Professionnel Privé Le Rocher;Montee De La Transhumance;13300;Salon De Provence;43.6449432;5.0949437
+0340992U;Lycée Professionnel Privé Le Sacre Coeur;46 Boulevard D Angleterre;34500;Beziers;43.3470472;3.2120912
+0592964B;Lycée Professionnel Privé Leonard De Vinci;10 Rue Notre Dame Des Victoires;59100;Roubaix;50.6950525;3.1805112
+0100063P;Lycée Professionnel Privé Leonie Aviat;3 Rue Etienne Pedron;10000;Troyes;48.3043057;4.0791779
+0490910Y;Lycée Professionnel Privé Les Ardilliers;1 Quai Du Jagueneau;49421;Saumur;47.2516631;-0.0584264
+0741287X;Lycée Professionnel Privé Les Bressis;85 Route Des Creuses;74600;Seynod;45.8932394;6.1001202
+0782100J;Lycée Professionnel Privé Les Chataigniers;11 Bis Avenue Jean Jaures;78000;Versailles;48.8174674;2.1442539
+0631408N;Lycée Professionnel Privé Les Cordeliers;Place Des Cordeliers;63100;Clermont Ferrand;45.7942923;3.1131005
+0740143D;Lycée Professionnel Privé Les Cordeliers;11 Rue M. Berthelot;74301;Cluses;46.0593129;6.5787129
+0061394H;Lycée Professionnel Privé Les Fauvettes;44 Avenue J De Noailles;06400;Cannes;43.5541346;6.9996242
+0381809K;Lycée Professionnel Privé Les Gorges;22 Rue Des Orphelines;38500;Voiron;45.3692772;5.5910909
+0090045A;Lycée Professionnel Privé Les Jacobins;Rue Du Rempart Du Touroncq;09100;Pamiers;43.114753;1.607953
+9710775R;Lycée Professionnel Privé Les Perseverants;28 Rue Lardenoy;97100;Basse Terre;15.9932345;-61.7242003
+0381805F;Lycée Professionnel Privé Les Portes De Chartreuse;387 Avenue De Stalingrad;38343;Voreppe;45.2947821;5.6336357
+0311971Z;Lycée Professionnel Privé Les Potiers;4 Rue Du Sachet;31400;Toulouse;43.5931107;1.4478264
+0382169B;Lycée Professionnel Privé Les Prairies;31 Rue Mainssieux;38500;Voiron;45.3630158;5.5928307
+0141868E;Lycée Professionnel Privé Les Rosiers;17 Chemin De Rocques;14100;Lisieux;49.154762;0.2306014
+9711064E;Lycée Professionnel Privé L'institut Des Ameriques;Forum De Grand-camp;97142;Les Abymes;16.27395;-61.502615
+0595723A;Lycée Professionnel Privé Louise De Marillac;2 Rue D'antin;59003;Lille;50.6268165;3.0474897
+0593066M;Lycée Professionnel Privé Lp Des Forges;13 Rue Du Couvent;59220;Denain;50.32239;3.382042
+0595918M;Lycée Professionnel Privé Lp E.p.i.d.;20 Rue De Lille;59140;Dunkerque;51.0277456;2.3724543
+0070100W;Lycée Professionnel Privé Marc Seguin-saint Charles;Route De Californie;07100;Annonay;45.2440877;4.6758762
+9830271C;Lycée Professionnel Privé Marcellin Champagnat (ddec);Rt Terr 1;98890;Paita;0.0;0.0
+0593015G;Lycée Professionnel Privé Maria Goretti;1/e Rue De Verlinghem;59832;Lambersart;50.653684;3.0247746
+0221867K;Lycée Professionnel Privé Marie Balavenne;47 Boulevard Laennec;22005;St Brieuc;48.5111408;-2.7744799
+0830116A;Lycée Professionnel Privé Marie France;220 Avenue Marcel Castie;83000;Toulon;43.1234854;5.941595
+0131449E;Lycée Professionnel Privé Marie Gasquet;38 R Electriciens St Barnabe;13012;Marseille 12;43.3041972;5.4172944
+0541364P;Lycée Professionnel Privé Marie Immaculee;33 Av Du General Leclerc;54000;Nancy;48.6821438;6.1840703
+0133277S;Lycée Professionnel Privé Modele Electronique;233 Bd St Marcel;13011;Marseille 11;43.2865274;5.4686969
+0740155S;Lycée Professionnel Privé Mont Blanc;148 Route De Doran;74700;Sallanches;45.9375916;6.6254618
+0820061W;Lycée Professionnel Privé Montauriol;1 Boulevard Montauriol;82000;Montauban;44.0100506;1.3594123
+0640184C;Lycée Professionnel Privé Montpensier;34 Rue Montpensier;64000;Pau;43.3010083;-0.3729012
+0261139X;Lycée Professionnel Privé Montplaisir;75 Rue Montplaisir;26000;Valence;44.9312156;4.9083899
+0940851P;Lycée Professionnel Privé Morin;22 Avenue De Verdun;94000;Creteil;48.7948841;2.462591
+0312064A;Lycée Professionnel Privé Myriam;9 Et 20 Rue Mage;31000;Toulouse;43.5977887;1.4472602
+0440262W;Lycée Professionnel Privé Nazareth;40 Rue Michel Grimault;44141;Chateaubriant;47.7156138;-1.3765267
+0290207T;Lycée Professionnel Privé Nd Du Kreisker;2 Rue Cadiou;29250;St Pol De Leon;48.6825255;-3.9870981
+0781582W;Lycée Professionnel Privé N-dame Du Grandchamp;97 Rue Royale;78000;Versailles;48.7933549;2.1239896
+0280684S;Lycée Professionnel Privé Notre Dame;Chateau Des Vaux;28240;St Maurice St Germain;48.494383;1.089488
+0350790U;Lycée Professionnel Privé Notre Dame;6 Rue De Vannes;35601;Redon;47.6473747;-2.0900067
+0350807M;Lycée Professionnel Privé Notre Dame;Rue De Dinan;35290;St Meen Le Grand;48.1926274;-2.188228
+0501941N;Lycée Professionnel Privé Notre Dame;43 Rue Sebline;50500;Carentan;49.3051863;-1.2471225
+0541357G;Lycée Professionnel Privé Notre Dame;1 Rue Du Bois Prieur;54350;Mont St Martin;49.5335424;5.764854
+0691465T;Lycée Professionnel Privé Notre Dame;1 Rue Honore Petetin;69700;Givors;45.5910944;4.7716109
+0783537W;Lycée Professionnel Privé Notre Dame;15 Rue De Strasbourg;78200;Mantes La Jolie;48.991379;1.711402
+0881530K;Lycée Professionnel Privé Notre Dame - Saint Joseph;23 Rue Thiers;88012;Epinal;48.1701687;6.4513835
+0141191U;Lycée Professionnel Privé Notre Dame De Fidelite;8 Rue Du Petit Clos Saint Marc;14074;Caen;49.2111144;-0.3733099
+0501787W;Lycée Professionnel Privé Notre Dame De La Providence;9 Rue Du Chanoine Beranger;50303;Avranches;48.686819;-1.3690481
+0572951Y;Lycée Professionnel Privé Notre Dame De La Providence;22 Place Notre Dame;57126;Thionville;49.3576246;6.1602375
+0880120C;Lycée Professionnel Privé Notre Dame De La Providence;14 Rue Pasteur;88109;St Die Des Vosges;48.2825237;6.9504488
+0601946P;Lycée Professionnel Privé Notre Dame De La Tilloye;1 Av De La Liberation;60204;Compiegne;49.4062037;2.8305249
+0141193W;Lycée Professionnel Privé Notre Dame De Nazareth;Avenue De La Basilique;14440;Douvres La Delivrande;49.2970159;-0.3732011
+0260078U;Lycée Professionnel Privé Notre Dame Des Champs;Rue Eugene Blain;26106;Romans Sur Isere;45.046612;5.042497
+0550700M;Lycée Professionnel Privé Notre Dame Des Vertus;30 Rue Du General De Gaulle;55500;Ligny En Barrois;48.6873861;5.3236757
+0470730N;Lycée Professionnel Privé Notre Dame La Compassion;1ter Rue De Langeot;47200;Marmande;44.4978288;0.1723321
+0480041J;Lycée Professionnel Privé Notre-dame;Quartier Fontanilles;48000;Mende;44.517611;3.501873
+0761347B;Lycée Professionnel Privé Notre-dame;5 Rue Hervieux;76500;Elbeuf;49.2889278;1.0017247
+0593030Y;Lycée Professionnel Privé Notre-dame Du Sacre Coeur;158 Avenue Saint Marcel;59120;Loos;50.6097613;3.0255216
+0754191Y;Lycée Professionnel Privé Notre-dame -st Vincent De Paul;49 Rue Bobillot;75013;Paris 13;48.828049;2.3529993
+0950804H;Lycée Professionnel Privé Notre-famille;2 Rue Des Patis;95520;Osny;49.0501663;2.086339
+0693406C;Lycée Professionnel Privé O R T;133 Rue Marius Berliet;69008;Lyon 08;45.7380862;4.8677943
+0141186N;Lycée Professionnel Privé Oasis;18 Rue De L Oratoire;14000;Caen;49.18111;-0.3599751
+0320652K;Lycée Professionnel Privé Oratoire Sainte-marie;50 Bis Rue Victor Hugo;32002;Auch;43.6505336;0.5776322
+0131424C;Lycée Professionnel Privé Ort Leon Bramson;9 Rue Des Forges;13010;Marseille 10;43.2791281;5.4104058
+0312191N;Lycée Professionnel Privé Ort Maurice Grynfogel;14 Rue Etienne Collongues;31770;Colomiers;43.601951;1.333155
+0430101B;Lycée Professionnel Privé Paradis;Rue Pont De La Chartreuse;43700;Brives Charensac;45.0529219;3.9138113
+0921932A;Lycée Professionnel Privé Passy Buzenval;50 Avenue Otis Mygatt;92508;Rueil Malmaison;48.8591268;2.1832515
+0300109F;Lycée Professionnel Privé Pasteur;3 Rue Pasteur;30110;La Grand Combe;44.2107935;4.0283226
+9720073X;Lycée Professionnel Privé Patronage Saint-louis;Route Du Lamentin Km 4;97200;Fort De France;14.609371;-61.07256
+9710066V;Lycée Professionnel Privé Pensionnat De Versailles;8 Rue Victor Hugues;97100;Basse Terre;16.1985639;-61.5901551
+9830294C;Lycée Professionnel Privé Pere Gueneau (ddec);24 Rte Terr 1 Village;98870;Bourail;0.0;0.0
+0131437S;Lycée Professionnel Privé Phocea R Attoyan (coiffure);1 Rue D Arcole;13006;Marseille 06;43.2894588;5.3774154
+0541363N;Lycée Professionnel Privé Pierre De Coubertin;5 R Du President Robert Schuman;54000;Nancy;48.6825501;6.1799118
+0050039R;Lycée Professionnel Privé Pierre Et Louis Poutrain;;05260;St Jean St Nicolas;44.6683333;6.2294444
+0771204R;Lycée Professionnel Privé Pigier Melun;30 Boulevard Victor Hugo;77000;Melun;48.5394984;2.6580575
+0070107D;Lycée Professionnel Privé Presentation De Marie;21 Avenue Notre Dame;07700;Bourg St Andeol;44.3730123;4.6450101
+0631048X;Lycée Professionnel Privé Rene Rambaud - Coiffure;27 Avenue Du Marechal Leclerc;63000;Clermont Ferrand;45.7843743;3.0868642
+0931573E;Lycée Professionnel Privé Robert Schuman;5 Avenue Du General De Gaulle;93440;Dugny;48.9494067;2.4150708
+0940820F;Lycée Professionnel Privé Robert Schuman;5-6 Rue De L'eglise;94340;Joinville Le Pont;48.8192804;2.4664616
+0381738H;Lycée Professionnel Privé Robin St Vincent De Paul;;38204;Vienne;46.3491859;-1.0850299
+0783330W;Lycée Professionnel Privé Roulleau;42 Rue De Tessancourt;78250;Meulan;49.0086789;1.9050102
+0801947G;Lycée Professionnel Privé Sacre Coeur;1 Rue De L Oratoire;80026;Amiens;49.8933658;2.3044821
+0801951L;Lycée Professionnel Privé Sacre Coeur;36 Boulevard Des Anglais;80200;Peronne;49.9264177;2.9342576
+0040502Z;Lycée Professionnel Privé Sacre Coeur (du);2 Avenue Des Thermes;04000;Digne Les Bains;44.0917238;6.2407469
+0940853S;Lycée Professionnel Privé Sacre-coeur;3 Boulevard De Stalingrad;94320;Thiais;48.7672928;2.4035625
+0631737W;Lycée Professionnel Privé Saint Alyre;20 Rue Sainte George;63037;Clermont Ferrand;45.7834986;3.0818986
+0070112J;Lycée Professionnel Privé Saint Andre;18 Rue Emile Combes;07400;Le Teil;44.5388909;4.6878848
+0572101Z;Lycée Professionnel Privé Saint Andre;1 Rue De L'eglise;57840;Ottange;49.4410443;6.0178869
+0251112Y;Lycée Professionnel Privé Saint Benigne;1 Rue Du Chanoine Prenel;25300;Pontarlier;46.9020101;6.3561888
+0021906L;Lycée Professionnel Privé Saint Charles;1 Rue Du Brouage;02300;Chauny;49.6147218;3.2121038
+0860752A;Lycée Professionnel Privé Saint Gabriel Notre Dame;27 Rue Du Vieux Palais;86108;Chatellerault;46.8156207;0.542157
+0891223W;Lycée Professionnel Privé Saint Jacques;6 Rue Du Faubourg St Jacques;89300;Joigny;47.9834057;3.3881557
+0950805J;Lycée Professionnel Privé Saint Jean;Rond Point De La Tour Du Mail;95117;Sannois;48.970782;2.256869
+0010099C;Lycée Professionnel Privé Saint Joseph;3 Rue Du Lycée;01000;Bourg En Bresse;0.0;0.0
+0010814E;Lycée Professionnel Privé Saint Joseph;101 Rue Henri Grobon;01705;Miribel;45.825157;4.9512247
+0022002R;Lycée Professionnel Privé Saint Joseph;2 Chaussee De Fontaine;02140;Fontaine Les Vervins;49.8365787;3.906547
+0180572Z;Lycée Professionnel Privé Saint Joseph;11 Rue Gourdon;18100;Vierzon;47.2244551;2.0669876
+0251592V;Lycée Professionnel Privé Saint Joseph;28 Av Fontaine Argent;25000;Besancon;47.2462201;6.0340665
+0382600V;Lycée Professionnel Privé Saint Joseph;1 Rue Lakanal;38506;Voiron;45.3678543;5.5870773
+0622192W;Lycée Professionnel Privé Saint Joseph;30 Rue Des Berceaux;62630;Etaples;50.5129606;1.6437015
+0690651H;Lycée Professionnel Privé Saint Joseph;327 Rue Garibaldi;69363;Lyon 07;45.7475187;4.8515373
+0741355W;Lycée Professionnel Privé Saint Joseph;St Martin Sur Arve;74703;Sallanches;45.912929;6.689997
+0831187P;Lycée Professionnel Privé Saint Joseph;2229 Route De Faveyrolles;83190;Ollioules;43.139436;5.8845344
+0900425Y;Lycée Professionnel Privé Saint Joseph;14 Rue De Badonvillers;90000;Belfort;47.6461321;6.8486574
+0680159D;Lycée Professionnel Privé Saint Joseph De Cluny;53 Avenue Roger Salengro;68100;Mulhouse;47.7509394;7.347119
+0570264C;Lycée Professionnel Privé Saint Joseph-la Providence;10 Rue De L Abbe Heydel;57801;Freyming Merlebach;49.1466767;6.8148536
+0572953A;Lycée Professionnel Privé Saint Joseph-la Providence;2 Avenue Passaga;57600;Forbach;49.1853591;6.9006303
+0261138W;Lycée Professionnel Privé Saint Louis;Clos Soubeyran;26402;Crest;0.0;0.0
+0593060F;Lycée Professionnel Privé Saint Louis;145 Avenue Marc Sangnier;59427;Armentieres;50.7001104;2.8845967
+0371328T;Lycée Professionnel Privé Saint Martin;47 Rue Nericault Destouches;37000;Tours;47.3920294;0.6835155
+0541344T;Lycée Professionnel Privé Saint Michel;Chartreuse De Bosserville;54510;Art Sur Meurthe;48.664136;6.243642
+0754175F;Lycée Professionnel Privé Saint Paul;81 Ter Rue Jean Pierre Timbaud;75011;Paris 11;48.867957;2.3777608
+0421978X;Lycée Professionnel Privé Saint Paul Forez;13 Rue Du College;42603;Montbrison;45.6084481;4.0659129
+0801945E;Lycée Professionnel Privé Saint Pierre;24 Place Clemenceau;80103;Abbeville;50.1087199;1.8347016
+0701001H;Lycée Professionnel Privé Saint Pierre Fourier;10 Q Rue Des Casernes;70103;Gray;47.4448206;5.5926946
+0801949J;Lycée Professionnel Privé Saint Riquier;50 Chaussee Jules Ferry;80094;Amiens;49.8829134;2.3258793
+0030105Y;Lycée Professionnel Privé Saint Vincent;150 Boulevard De Courtais;03100;Montlucon;46.3419497;2.6072664
+0740144E;Lycée Professionnel Privé Saint Vincent;55 Route De Bossey;74160;Collonges Sous Saleve;46.1413463;6.1456029
+0060776L;Lycée Professionnel Privé Saint Vincent De Paul;17 Rue Fodere;06300;Nice;43.6991063;7.2864757
+0370757X;Lycée Professionnel Privé Saint Vincent De Paul;30 Rue Delperier;37058;Tours;47.3925342;0.6769264
+0570241C;Lycée Professionnel Privé Saint Vincent De Paul;4 Rue Marie Douchet;57440;Algrange;49.3580552;6.0494087
+0930946Y;Lycée Professionnel Privé Saint Vincent De Paul;25 Rue Albert Walter;93200;St Denis;48.9380004;2.3591377
+0810100U;Lycée Professionnel Privé Saint-dominique;17 Lices Georges Pompidou;81000;Albi;43.9298245;2.1477169
+0420980M;Lycée Professionnel Privé Sainte Anne;24 Rue Du Pilat;42400;St Chamond;45.4720855;4.514178
+0421979Y;Lycée Professionnel Privé Sainte Anne;4 Rue Saint Alban;42300;Roanne;46.0363817;4.0633804
+0550983V;Lycée Professionnel Privé Sainte Anne;14 Rue Mautrote;55104;Verdun;49.1604635;5.3812032
+0730774V;Lycée Professionnel Privé Sainte Anne;39 Montee Saint Jean;73292;La Motte Servolex;45.5973189;5.8780976
+0700076C;Lycée Professionnel Privé Sainte Anne Saint Joseph;1 Rue De La Tannerie;70204;Lure;47.6877792;6.4943028
+0593079B;Lycée Professionnel Privé Sainte Bernadette;225 Rue Des Anciens D'afn;59572;Jeumont;50.296322;4.100791
+0570268G;Lycée Professionnel Privé Sainte Chretienne;1 Passage Du Pensionnat;57506;St Avold;49.1049716;6.7083946
+0131675A;Lycée Professionnel Privé Sainte Elisabeth;Lot N?35;13240;Septemes Les Vallons;43.3973237;5.3694594
+0251035P;Lycée Professionnel Privé Sainte Famille;33 Rue Du General Brulard;25000;Besancon;47.2275972;6.0026427
+0741286W;Lycée Professionnel Privé Sainte Famille;Avenue Des Voirons;74805;La Roche Sur Foron;46.0773767;6.2971227
+0730776X;Lycée Professionnel Privé Sainte Genevieve;2 Boulevard Du Theatre;73000;Chambery;45.5659213;5.9237499
+0551032Y;Lycée Professionnel Privé Sainte Jeanne D'arc;23 Rue Poincare;55200;Commercy;48.7606796;5.5902284
+0570294K;Lycée Professionnel Privé Sainte Marie;21 Avenue Du General De Gaulle;57400;Sarrebourg;48.7310531;7.05137
+0593063J;Lycée Professionnel Privé Sainte Marie;2 Rue Emile Hie;59270;Bailleul;50.7375223;2.73195
+0622195Z;Lycée Professionnel Privé Sainte Marie;31 Rue Du Paradis;62310;Fruges;50.5156254;2.1365448
+0390094R;Lycée Professionnel Privé Sainte Marie - Fenelon;84 Rue Saint Desire;39000;Lons Le Saunier;46.6706079;5.5489595
+0420996E;Lycée Professionnel Privé Sainte Marie La Grand'grange;Rue Mondragon;42400;St Chamond;45.483205;4.5102909
+0830114Y;Lycée Professionnel Privé Sainte Marthe;8 Rue Des 4 Freres Bernard;83390;Cuers;43.237875;6.0721999
+0671679L;Lycée Professionnel Privé Sainte Therese;4 Rue Des Allies;67970;Oermingen;49.0008662;7.128229
+0740151M;Lycée Professionnel Privé Sainte Therese;8 Avenue De L'aumone;74150;Rumilly;45.8644371;5.950467
+0141867D;Lycée Professionnel Privé Sainte Ursule;30 R De La Misericorde;14019;Caen;49.1819847;-0.3555065
+0271073V;Lycée Professionnel Privé Sainte-agnes;126 Rue D'albufera;27200;Vernon;49.090865;1.4801589
+0811070Y;Lycée Professionnel Privé Sainte-cecile;1 Avenue Du Breuil;81000;Albi;43.9432297;2.1483149
+0300106C;Lycée Professionnel Privé Sainte-marie;Impasse Des Recollets;30200;Bagnols Sur Ceze;44.1612433;4.6238415
+0311970Y;Lycée Professionnel Privé Sainte-marie De Nevers;10 Rue Du Perigord;31070;Toulouse;43.6072684;1.4432532
+0311219G;Lycée Professionnel Privé Sainte-marie Saint-sernin;19 Boulevard Armand Duportal;31000;Toulouse;43.6068673;1.4345863
+0761344Y;Lycée Professionnel Privé Sainte-therese;34 Rue Ferdinand Cartier;76150;Maromme;49.4855079;1.0441959
+0460050E;Lycée Professionnel Privé Saint-etienne;49 Rue Des Soubirous;46000;Cahors;44.4504769;1.4412933
+0110053Y;Lycée Professionnel Privé Saint-francois;2 Rue Des Amidonniers;11000;Carcassonne;43.2096831;2.3495761
+0761348C;Lycée Professionnel Privé Saint-hildevert;43 Bis Rue De Ferrieres;76220;Gournay En Bray;49.4821029;1.7281726
+0754239A;Lycée Professionnel Privé Saint-jean De Montmartre;31 Rue Caulaincourt;75018;Paris 18;48.8881336;2.3336903
+0121260E;Lycée Professionnel Privé Saint-joseph;Avenue Etienne Soulie;12200;Villefranche De Rouergue;44.357108;2.0390698
+0121309H;Lycée Professionnel Privé Saint-joseph;1 Rue Sarrus;12000;Rodez;44.3451427;2.5715885
+0141192V;Lycée Professionnel Privé Saint-joseph;145 Avenue De La Republique;14800;Deauville;49.3547504;0.0669966
+0312065B;Lycée Professionnel Privé Saint-joseph;85 Rue De Limayrac;31079;Toulouse;43.593452;1.4734636
+0810105Z;Lycée Professionnel Privé Saint-joseph;38 Avenue De Lavaur;81100;Castres;43.6075533;2.2350212
+0271081D;Lycée Professionnel Privé Saint-ouen;30 Rue Sadi Carnot;27500;Pont Audemer;49.3571648;0.5145026
+0141184L;Lycée Professionnel Privé Saint-pierre;50 Rue Canchy;14000;Caen;49.1741178;-0.3513773
+0820551D;Lycée Professionnel Privé Saint-roch;Malepeyre;82390;Durfort Lacapelette;44.19504;1.1837629
+0761349D;Lycée Professionnel Privé Saint-vincent De Paul;26 Boulevard Amiral Mouchez;76600;Le Havre;49.4849886;0.1351204
+0730779A;Lycée Professionnel Privé Savoisienne;85 Chemin Des Nuettes;73420;Drumettaz Clarafond;45.6649369;5.911833
+0601947R;Lycée Professionnel Privé Sevigne;20 Rue De La Sous Prefecture;60200;Compiegne;49.415931;2.8299869
+0771211Y;Lycée Professionnel Privé Sncf;35 Rue Du Chateau D'arcy;77390;Chaumes En Brie;48.666504;2.880979
+0690698J;Lycée Professionnel Privé Societe Enseig. Prof. Du Rhone;46 Rue Professeur Rochaix;69003;Lyon 03;45.7487694;4.8778351
+0131432L;Lycée Professionnel Privé St Andre (les Routiers);368 Boulevard Barnier;13016;Marseille 16;43.3606468;5.3425878
+0331585U;Lycée Professionnel Privé St Augustin;19 Rue Paul Courteault;33000;Bordeaux;44.8346037;-0.607579
+0440302P;Lycée Professionnel Privé St Donatien-immaculee;6 Rue Guillet De La Brosse;44013;Nantes;47.2296841;-1.5440911
+0133280V;Lycée Professionnel Privé St Eloi;9 Avenue Jules Isaac;13626;Aix En Provence;43.5348838;5.4513323
+0781856U;Lycée Professionnel Privé St Erembert;5 Rue Salomon Reinach;78100;St Germain En Laye;48.8958802;2.0996683
+0292158N;Lycée Professionnel Privé St Esprit;3 Rue Emile Souvestre;29403;Landivisiau;48.5121112;-4.0797023
+0911964R;Lycée Professionnel Privé St Eugene;10 Rue De La Pie Voleuse;91120;Palaiseau;48.7131857;2.2405189
+0781855T;Lycée Professionnel Privé St Francois D Assise;45 Avenue Du Manet;78180;Montigny Le Bretonneux;48.7664455;2.0213888
+0920963X;Lycée Professionnel Privé St Francois D Assise;39 Rue Boris Vilde;92260;Fontenay Aux Roses;48.7884925;2.2801094
+0332550T;Lycée Professionnel Privé St Genes;160 Rue St Genes;33081;Bordeaux;44.8249282;-0.5813679
+0560199M;Lycée Professionnel Privé St Georges;16 Rue Marechal Foch;56000;Vannes;47.6524299;-2.7669182
+0131433M;Lycée Professionnel Privé St Henri;37 Chemin De Bernex;13016;Marseille 16;43.366048;5.3394241
+0870919B;Lycée Professionnel Privé St Jean;26 Rue Eugene Varlin;87016;Limoges;45.8263601;1.2749309
+0840941S;Lycée Professionnel Privé St Jean Baptiste De La Salle;9 R Notre Dame Sept Douleurs;84008;Avignon;43.9489266;4.815644
+0101015Z;Lycée Professionnel Privé St Joseph;21 Rue Du Cloitre St Etienne;10042;Troyes;48.2970565;4.0816607
+0221865H;Lycée Professionnel Privé St Joseph;Rue Mouexigne;22404;Lamballe;48.4631626;-2.5132448
+0332535B;Lycée Professionnel Privé St Joseph;12 Avenue Du 8 Mai;33291;Blanquefort;44.9087878;-0.6456849
+0350788S;Lycée Professionnel Privé St Joseph;42 Route De St James;35300;Fougeres;48.3659336;-1.2085963
+0400883T;Lycée Professionnel Privé St Joseph;Le Bourg;40310;Gabarret;43.984986;0.011111
+0400916D;Lycée Professionnel Privé St Joseph;Le Berceau;40990;St Vincent De Paul;43.7469013;-1.0103303
+0511151Z;Lycée Professionnel Privé St Joseph;41 Rue Du General Fery;51037;Chalons En Champagne;48.9614846;4.3716507
+0541988T;Lycée Professionnel Privé St Joseph;413 Avenue De Boufflers;54524;Laxou;48.6934935;6.1381862
+0640181Z;Lycée Professionnel Privé St Joseph;1 Rte Des Missionnaires;64240;Hasparren;43.3811075;-1.3049408
+9710593T;Lycée Professionnel Privé St Joseph De Cluny;La Jaille;97122;Baie Mahault;16.2547641;-61.56198
+9720074Y;Lycée Professionnel Privé St Joseph De Cluny;22 Route De Cluny;97200;Fort De France;14.627187;-61.0738508
+9830269A;Lycée Professionnel Privé St Joseph De Cluny (ddec);45 Rue Alma Ctre Ville;98845;Noumea;-22.2697334;166.4407828
+0601896K;Lycée Professionnel Privé St Joseph Du Moncel;8 Place De L Eglise;60722;Pont Ste Maxence;49.3158812;2.6215379
+0131468A;Lycée Professionnel Privé St Louis;538 Chemin De La Madrague-ville;13015;Marseille 15;43.347103;5.3554272
+0291808H;Lycée Professionnel Privé St Marc;12 Rue De Kerfeunteun;29910;Tregunc;47.8537038;-3.8548436
+0721298V;Lycée Professionnel Privé St Martin;Chateau De Maison Rouge;72500;La Bruere Sur Loir;48.537019;-0.503046
+0331548D;Lycée Professionnel Privé St Michel;20 Av Gl De Gaulle;33290;Blanquefort;44.910084;-0.635244
+0560197K;Lycée Professionnel Privé St Michel;Fondation D'auteuil;56320;Priziac;48.1024272;-3.4316011
+0851516L;Lycée Professionnel Privé St Michel;32 Rue Du Calvaire;85290;St Laurent Sur Sevre;46.9593823;-0.8910565
+0131441W;Lycée Professionnel Privé St Michel (ann. St Ch. Camas);75 Rue Saint Savournin;13005;Marseille 05;43.2967646;5.3871316
+0920981S;Lycée Professionnel Privé St Philippe;1 Rue Du Pere Brottier;92190;Meudon;48.8066002;2.2467973
+0490897J;Lycée Professionnel Privé St Serge;13 Rue Des Fours A Chaux;49103;Angers;47.4840708;-0.5409198
+0440261V;Lycée Professionnel Privé St Thomas D'aquin;83 Rue Clemenceau;44153;Ancenis;47.3668587;-1.1786221
+0781581V;Lycée Professionnel Privé St Thomas De Villeneuve;15 Rue Des Louviers;78100;St Germain En Laye;48.8983954;2.0921371
+0290192B;Lycée Professionnel Privé St Tremeur;1 Rue Cazuguel;29833;Carhaix Plouguer;48.275584;-3.574188
+0132835L;Lycée Professionnel Privé St Vincent De Paul;30 Rue Stanislas Torrents;13006;Marseille 06;43.287572;5.3782713
+0240089S;Lycée Professionnel Privé St Vincent De Paul;23 Av G Pompidou;24000;Perigueux;45.1889317;0.7230677
+0332497K;Lycée Professionnel Privé St Vincent De Paul;47 A 51 Rue Des Sablieres;33074;Bordeaux;44.825364;-0.5753274
+0601162M;Lycée Professionnel Privé St Vincent De Paul;8 Bd Du General De Gaulle;60000;Beauvais;49.4291558;2.0897125
+0640189H;Lycée Professionnel Privé St Vincent De Paul;25 Rue Rene Cassin;64000;Pau;43.301636;-0.3672163
+0783325R;Lycée Professionnel Privé St Vincent De Paul;44 Avenue De Saint Cloud;78000;Versailles;48.8049728;2.1359398
+0641663K;Lycée Professionnel Privé Ste Anne;Notre Dame Du Refuge;64604;Anglet;43.496758;-1.52561
+0641408H;Lycée Professionnel Privé Ste Bernadette;1 Avenue De Gassion;64190;Audaux;43.3580963;-0.7937267
+0371326R;Lycée Professionnel Privé Ste Clotilde St Joseph;9 Rue Henri Dunan;37400;Amboise;47.4085229;0.9738971
+0292157M;Lycée Professionnel Privé Ste Elisabeth;9 Rue Lamennais;29171;Douarnenez;48.0905065;-4.3275018
+0641665M;Lycée Professionnel Privé Ste Elisabeth;45 Avenue Du Pic Du Midi;64800;Igon;43.1518007;-0.2248725
+0221866J;Lycée Professionnel Privé Ste Elisabeth-kersa;Chateau De Kersa;22620;Ploubazlanec;48.794307;-3.046996
+0332496J;Lycée Professionnel Privé Ste Famille -saintonge;12 Rue De Saintonge;33023;Bordeaux;44.8321381;-0.5798944
+0131459R;Lycée Professionnel Privé Ste Marie;2 Rue De Jouques;13100;Aix En Provence;43.5318267;5.4462333
+0511149X;Lycée Professionnel Privé Ste Marie;26 Rue Du Docteur Verron;51200;Epernay;49.0449891;3.9564484
+0492003L;Lycée Professionnel Privé Ste Marie-groupe Maine;3 Rue Du Margat;49105;Angers;47.4855856;-0.5607137
+0240094X;Lycée Professionnel Privé Ste Marthe - St Front;74 Avenue Pasteur;24100;Bergerac;44.8601889;0.4951121
+0440307V;Lycée Professionnel Privé Ste Therese;80 Rue D Anjou;44600;St Nazaire;47.2796821;-2.2092813
+0641705F;Lycée Professionnel Privé St-joseph;650 Rue Hiribehere;64480;Ustaritz;43.4045104;-1.462666
+0580075S;Lycée Professionnel Privé Therese Desreumaux;4 Rte De La Machine;58300;St Leger Des Vignes;46.838903;3.456643
+0501788X;Lycée Professionnel Privé Thomas Helye;37 Rue Emile Zola;50100;Cherbourg Octeville;49.6370758;-1.6291084
+0340920R;Lycée Professionnel Privé Turgot;94 Rue Pierre Flourens;34098;Montpellier;43.6377935;3.8324336
+0141197A;Lycée Professionnel Privé Victorine Magne;39 Avenue Du 6 Juin;14103;Lisieux;49.1409396;0.2117813
+0131677C;Lycée Professionnel Privé Vigie (la);Pl La Bastide La Maurelette;13015;Marseille 15;0.0;0.0
+0650067V;Lycée Professionnel Privé Vincent De Paul;16 Rue St Vincent De Paul;65000;Tarbes;43.2303093;0.079062
+0840082H;Lycée Professionnel Privé Vincent De Paul;1 Rue Chiron;84000;Avignon;43.9511699;4.8046831
+0620221D;Lycée Professionnel Professeur Clerc;34 Rue Andre Pantigny;62230;Outreau;50.703243;1.5894109
+0220186H;Lycée Professionnel Public;47 Rue Rene Le Magorec;22110;Rostrenen;48.2332958;-3.3089866
+0640066Z;Lycée Professionnel Ramiro Arrue;Quart Chantaco Rte D Ascain;64500;St Jean De Luz;43.3691803;-1.6437374
+0870119G;Lycée Professionnel Raoul Dautry;14 Rue Du Puy Imbert;87036;Limoges;45.8387379;1.2785581
+0900236T;Lycée Professionnel Raoul Follereau;3 Rue Louis Marchal;90016;Belfort;47.6383708;6.8462164
+0860029P;Lycée Professionnel Raoul Mortier;72 Avenue De L Europe;86501;Montmorillon;46.4257827;0.8591632
+0150008C;Lycée Professionnel Raymond Cortat;55 Avenue Jean Chanal;15005;Aurillac;44.947086;2.4625249
+9720468B;Lycée Professionnel Raymond Neris;Cite Scolaire Marin;97290;Le Marin;14.4904216;-60.9530711
+0121157T;Lycée Professionnel Raymond Savignac;Rue Agnes Savignac;12203;Villefranche De Rouergue;44.3563661;2.0381734
+0860823C;Lycée Professionnel Reaumur;4 Rue Salvador Allende;86036;Poitiers;46.5817037;0.370543
+0650029D;Lycée Professionnel Reffye;76 Avenue Joffre;65016;Tarbes;43.2394768;0.0631445
+0540067E;Lycée Professionnel Regional Du Toulois;440 Avenue Du General Bigeard;54201;Toul;48.6667052;5.8891311
+0280043V;Lycée Professionnel Remi Belleau;33 Rue Bretonnerie;28400;Nogent Le Rotrou;48.3189144;0.8109948
+0130057S;Lycée Professionnel Rene Caillie;173 Boulevard De St-loup;13011;Marseille 11;43.2817581;5.4283584
+0190034J;Lycée Professionnel Rene Cassin;Bd Du Marquisat;19012;Tulle;45.2632089;1.7723838
+0570124A;Lycée Professionnel Rene Cassin;2 Rue Rene Cassin;57052;Metz;49.1358349;6.1674007
+0594404S;Lycée Professionnel Rene Cassin;Rue Des Ecoles;59182;Montigny En Ostrevent;50.3695619;3.1875453
+0750588G;Lycée Professionnel Rene Cassin;185 Avenue De Versailles;75016;Paris 16;48.840068;2.2637559
+0900294F;Lycée Professionnel Rene Cassin;3 Rue Louis Marchal;90016;Belfort;47.6383708;6.8462164
+0840939P;Lycée Professionnel Rene Char;2 Rue Pierre A Renoir;84033;Avignon;43.9460637;4.8531827
+0850146X;Lycée Professionnel Rene Couzinet;Boulevard Jean Yole;85302;Challans;46.840774;-1.87671
+0730048F;Lycée Professionnel Rene Perrin;41 Rue Rene Perrin;73400;Ugine;45.7514631;6.4161324
+0310054R;Lycée Professionnel Renee Bonnet;1 Allee Du Lieutenant Lafay;31432;Toulouse;43.5550127;1.4681713
+9710709U;Lycée Professionnel Richeval;Quartier Richeval;97111;Morne A L Eau;0.0;0.0
+0271097W;Lycée Professionnel Risle-seine;Rue Des Deportes;27500;Pont Audemer;49.353527;0.520603
+0530013B;Lycée Professionnel Robert Buron;68 Rue Bellessort;53013;Laval;48.0694098;-0.7598676
+0601363F;Lycée Professionnel Robert Desnos;2 Rue Jules Michelet;60803;Crepy En Valois;49.2284134;2.8941975
+0760144U;Lycée Professionnel Robert Schuman;51 Avenue Du 8 Mai;76610;Le Havre;49.5161566;0.1664377
+0840042P;Lycée Professionnel Robert Schuman;138 Av De Tarascon;84084;Avignon;43.9250989;4.8091279
+0601787S;Lycée Professionnel Roberval;10 Rue Des Grez;60600;Breuil Le Vert;49.3641021;2.4171306
+9740004L;Lycée Professionnel Roches Maigres;25 Rue Leconte De Lisle;97899;St Louis;0.0;0.0
+0570016H;Lycée Professionnel Romain Rolland;30 Rue Des Peupliers;57150;Creutzwald;49.1995571;6.6988264
+0590078P;Lycée Professionnel Romain Rolland;11 Rue Du Pont De Pierre;59750;Feignies;50.2915826;3.9206938
+0801628K;Lycée Professionnel Romain Rolland;Rue Romain Rolland;80080;Amiens;49.9159787;2.2980057
+0171238D;Lycée Professionnel Rompsay;Avenue De Perigny;17025;La Rochelle;46.15927;-1.132608
+0290078C;Lycée Professionnel Roz Glas;1 Place Jean Zay;29391;Quimperle;47.8846911;-3.5532464
+0400783J;Lycée Professionnel Rural Ste Elisabeth Et;137 Route De La Vieille Cote;40180;St Pandelon;43.6721063;-1.0435113
+0891159B;Lycée Professionnel Saint Germain;2 Place St Germain;89000;Auxerre;47.8002699;3.5720023
+9720516D;Lycée Professionnel Saint James;Route De Fonds St Denis;97250;St Pierre;14.7419691;-61.133656
+9750003E;Lycée Professionnel Saint Pierre;Rue Marcel Bonin;97500;St Pierre;0.0;0.0
+0400057V;Lycée Professionnel Saint-exupery;Avenue Du Lycée;40161;Parentis En Born;0.0;0.0
+0620043K;Lycée Professionnel Salvador Allende;Rue De L'universite;62408;Bethune;50.5213285;2.6532171
+0501677B;Lycée Professionnel Sauxmarais;444 Rue De La Chasse Aux Loups;50110;Tourlaville;49.6380184;-1.591671
+7200011Z;Lycée Professionnel Scamaroni;Rue Du 4eme D M N;20600;Bastia;42.697283;9.450881
+0692518M;Lycée Professionnel Sermenaz;Rue De Bonn;69140;Rillieux La Pape;45.819655;4.897126
+0050009H;Lycée Professionnel Sevigne;6 Rue Jean Mace;05003;Gap;44.5614745;6.0739966
+0590217R;Lycée Professionnel Sevigne;151 Rue De La Malcense;59208;Tourcoing;50.730799;3.1650448
+0570100Z;Lycée Professionnel Simon Lazard;25 Rue J J Kieffer;57215;Sarreguemines;49.1209348;7.074776
+0790702J;Lycée Professionnel Simone Signoret;36 Rue De Malabry;79301;Bressuire;46.8407623;-0.4847921
+0930135S;Lycée Professionnel Simone Weil;121 Avenue Jean Lolive;93500;Pantin;48.8927394;2.4107582
+0650028C;Lycée Professionnel Sixte Vignon;12 Rue Du Taillade;65801;Aureilhan;43.2462704;0.0848529
+0410832G;Lycée Professionnel Sonia Delaunay;21 Bis Rue D Auvergne;41016;Blois;47.5948627;1.3360367
+0590111A;Lycée Professionnel Sonia Delaunay;121 Rue De La Mitterie;59461;Lille;50.6491583;2.9967355
+0630070J;Lycée Professionnel Sonia Delaunay;Cite Pontel 7 Ter Av J.jaures;63304;Thiers;45.857907;3.545177
+0572028V;Lycée Professionnel Sophie Germain;9 Impasse Colbert;57129;Thionville;49.366287;6.1478214
+0841078R;Lycée Professionnel Sorgues (de);Chemin De La Lucette;84700;Sorgues;43.9976096;4.8794552
+0520923W;Lycée Professionnel St Exupery;82 Rue Anatole France;52105;St Dizier;48.6430006;4.9626225
+0330082K;Lycée Professionnel Sud Gironde;1 Av Des Resistants;33212;Langon;44.54777;-0.252276
+0752109K;Lycée Professionnel Suzanne Valadon;7 Rue Ferdinand Flocon;75018;Paris 18;48.8916988;2.345863
+0711384U;Lycée Professionnel Theodore Monod;Rue De La Loge;71450;Blanzy;46.680842;4.3819194
+0921676X;Lycée Professionnel Theodore Monod;26 Avenue Leon Jouhaux;92160;Antony;48.735355;2.3064839
+0930133P;Lycée Professionnel Theodore Monod;187 Rue De Brement;93130;Noisy Le Sec;48.8872329;2.4666533
+0752845K;Lycée Professionnel Theophile Gautier;49 Rue De Charenton;75012;Paris 12;48.8511953;2.3727407
+0710014E;Lycée Professionnel Thomas Dumorey Metiers Batimen;3 Rue Jean Rostand;71321;Chalon Sur Saone;46.7776154;4.8396918
+0382031B;Lycée Professionnel Thomas Edison;Rue Normandie Niemen;38130;Echirolles;45.1467389;5.719765
+0790964U;Lycée Professionnel Thomas Jean Main;39 R Coteau St Hubert;79011;Niort;46.3419255;-0.4775602
+0330031E;Lycée Professionnel Toulouse Lautrec;115 Rue Joseph Abria;33000;Bordeaux;44.8339958;-0.5933109
+0810003N;Lycée Professionnel Toulouse-lautrec;15 Rue Charles Portal;81027;Albi;43.925348;2.1395873
+0251349F;Lycée Professionnel Toussaint Louverture;81 Route De Besancon;25304;Pontarlier;46.9141729;6.3458015
+0330142A;Lycée Professionnel Tregey Rive De Garonne;24 Rue De Tregey;33015;Bordeaux;44.8391974;-0.5478589
+0251079M;Lycée Professionnel Tristan Bernard;13 Rue Goya;25000;Besancon;47.2176939;5.9813344
+0290105G;Lycée Professionnel Tristan Corbiere;16 Rue De Kerveguen;29671;Morlaix;48.5705251;-3.8221176
+0590186G;Lycée Professionnel Turgot;2 Rue De La Paix;59053;Roubaix;50.6962493;3.1783973
+0951281B;Lycée Professionnel Turgot;3 Place Au Pain;95162;Montmorency;48.9853003;2.3212499
+0750778N;Lycée Professionnel Turquetil;18 Passage Turquetil;75011;Paris 11;48.8521798;2.3928741
+0030924N;Lycée Professionnel Val D'allier;15 Rue Du Beaupuy;03150;Varennes Sur Allier;46.3144612;3.4066414
+0940134K;Lycée Professionnel Val De Bievre;15 17 Rue D'arcueil;94257;Gentilly;48.8129955;2.3447508
+0630078T;Lycée Professionnel Val De Dore;68 Avenue Leo Lagrange;63300;Thiers;45.8468155;3.5278217
+0570234V;Lycée Professionnel Val De Fensch;204 Rue Victor Rimmel;57240;Knutange;49.3429114;6.0222329
+0590152V;Lycée Professionnel Val De Lys;Rue Jacqueminemars;59940;Estaires;50.6466217;2.7048506
+0760145V;Lycée Professionnel Val De Seine;Avenue Georges Braque;76124;Le Grand Quevilly;49.402714;1.04095
+0570324T;Lycée Professionnel Valentin Metzinger;Rue De Montreal;57500;St Avold;49.1002999;6.7197012
+0590126S;Lycée Professionnel Valentine Labbe;41 Rue Paul Doumer;59110;La Madeleine;50.6473736;3.0721697
+0921229L;Lycée Professionnel Valmy;130 Boulevard De Valmy;92700;Colombes;48.9306689;2.2551693
+0920166F;Lycée Professionnel Vassily Kandinsky;96 Boulevard Bineau;92200;Neuilly Sur Seine;48.888011;2.2757687
+0890819G;Lycée Professionnel Vauban;22 Rue Faidherbe;89010;Auxerre;47.8000287;3.565054
+0380098A;Lycée Professionnel Vaucanson;27 Rue Anatole France;38030;Grenoble;45.1722442;5.7083809
+0130170P;Lycée Professionnel Vauvenargues;60 Boulevard Carnot;13100;Aix En Provence;43.5284055;5.4549853
+0180823X;Lycée Professionnel Vauvert;Rue Marguerite Audoux;18000;Bourges;47.0776989;2.3787816
+0592832H;Lycée Professionnel Vertes Feuilles;95 Rue Georges Maertens;59871;St Andre Lez Lille;50.6327986;3.0613862
+0260037Z;Lycée Professionnel Victor Hugo;442 Avenue Victor Hugo;26021;Valence;44.9076529;4.8831446
+0840044S;Lycée Professionnel Victor Hugo;139 Av Victor Hugo;84208;Carpentras;44.0515235;5.0474084
+0370771M;Lycée Professionnel Victor Laloux;2 Rue Marcel Proust;37200;Tours;47.358625;0.7029341
+0140018U;Lycée Professionnel Victor Lepine;40 Rue Victor Lepine;14000;Caen;49.1711498;-0.3420176
+9740020D;Lycée Professionnel Victor Schoelcher;14 Rue Saint Philippe;97899;St Louis;45.7504748;4.8660377
+0250066L;Lycée Professionnel Viette;1 B Rue P Donzelot;25206;Montbeliard;47.4951765;6.8092234
+0130065A;Lycée Professionnel Viste (la);Traverse Bonnet;13015;Marseille 15;43.35932;5.3571954
+0921500F;Lycée Professionnel Voilin;26 Rue Lucien Voilin;92800;Puteaux;48.8828072;2.2370604
+0930846P;Lycée Professionnel Voillaume;136 Rue De Mitry;93604;Aulnay Sous Bois;48.9457777;2.5099926
+0301210C;Lycée Professionnel Voltaire;399 Rue Bellini;30903;Nimes;43.8266006;4.3381288
+0620258U;Lycée Professionnel Voltaire;Place Bertin Ledoux;62410;Wingles;50.494578;2.852404
+9740015Y;Lycée Professionnel Vue Belle;;97422;St Paul;49.164628;5.38442
+0670023L;Lycée Professionnel Xavier Nessel;123 Route De Strasbourg;67504;Haguenau;48.8001884;7.7656679
+0510037N;Lycée Professionnel Yser;Avenue De L'yser;51053;Reims;49.2484301;4.0585216
+0692682R;Lycée Rural Privé Jean Monnet;304 Boulevard De La Bardiere;69590;St Symphorien Sur Coise;45.6295165;4.4610934
+0441788E;Lycée Techn Et Professionnel Le Bois Tillac;Allee Du Bois Tillac;44640;Le Pellerin;47.2018844;-1.7712182
+0390970T;Lycée Technique Rural Privé;1 Allee De La Savine;39400;Morbier;46.5571966;6.0150673
+0291208F;Lycée Technol Agricole Privé;Le Nivot;29590;Loperec;48.312256;-4.011024
+0351954J;Lycée Technol Agricole Privé;L'abbaye;35160;Montfort Sur Meu;48.130223;-1.943634
+0560228U;Lycée Technol Agricole Privé Anne De Bretagne;5 Place Anne De Bretagne;56500;Locmine;47.8833082;-2.8324355
+0221593M;Lycée Technol Agricole Privé Centre De Formation D'armor;Le Chef Du Bois;22450;Pommerit Jaudy;48.742229;-3.244975
+0561408B;Lycée Technol Agricole Privé Des Pays De Vilaine;Route De Peillac;56220;St Jacut Les Pins;47.7154571;-2.1948712
+0221033D;Lycée Technol Agricole Privé Espa;La Ville Davy;22120;Quessoy;48.425678;-2.667812
+0311270M;Lycée Technol Agricole Privé La Cadene;Rue Buissonniere;31670;Labege;43.535875;1.521728
+0350918H;Lycée Technol Agricole Privé Les Vergers;4 Rue Des Murets;35120;Dol De Bretagne;48.5521611;-1.7541892
+0292000S;Lycée Technol Agricole Privé St Antoine;11 Rue St J-b De La Salle;29870;Lannilis;48.5722565;-4.5194361
+0221673Z;Lycée Technol Agricole Privé St Ilan;52 Rue De St Ilan;22360;Langueux;48.5048429;-2.7062202
+0291751W;Lycée Technol Agricole Privé St Joseph;12 Allee De Kerustum;29101;Quimper;47.9804929;-4.0927405
+0351953H;Lycée Technol Agricole Privé St Nicolas-la Providence;28 Rue De Rennes;35360;Montauban De Bretagne;48.1977763;-2.0426486
+0211503V;Lycée Technol.agricole Privé Anne-marie Javouhey;Rue Anne-marie Javouhey;21250;Chamblanc;47.0220768;5.1517811
+0601194X;Lycée Technol.agricole Privé Charles Quentin;1 Rue Sabatier;60350;Pierrefonds;49.3433941;2.9760698
+0530816Z;Lycée Technol.agricole Privé D Orion;2 Rue De La Liberation;53600;Evron;48.1565742;-0.4088119
+0441032H;Lycée Technol.agricole Privé Ecole D'agriculture;29 Rue De Rennes;44590;Derval;47.6713835;-1.6741115
+0491804V;Lycée Technol.agricole Privé Edmond Michelet;Le Pre Neuf;49330;Etriche;47.6505269;-0.444111
+0440981C;Lycée Technol.agricole Privé Gabriel Deshayes;4, Route De Redon;44530;St Gildas Des Bois;47.5189743;-2.0396361
+0320633P;Lycée Technol.agricole Privé Institut Saint-christophe;Aux Stournes;32140;Masseube;43.429726;0.579788
+0441795M;Lycée Technol.agricole Privé La Motte;17 Rue Abbe Perrin;44320;St Pere En Retz;47.2054129;-2.0427713
+0491809A;Lycée Technol.agricole Privé Les 3 Provinces;11 Boulevard Jeanne D Arc;49304;Cholet;47.0640598;-0.8673109
+0840797K;Lycée Technol.agricole Privé Les Chenes;524 Av Du Pont Des Fontaines;84200;Carpentras;44.0610607;5.0551521
+0850609A;Lycée Technol.agricole Privé Les Etablieres;Rte De Nantes;85015;La Roche Sur Yon;46.7055761;-1.4296139
+0261067U;Lycée Technol.agricole Privé Les Mandailles;;26330;Chateauneuf De Galaure;45.234516;4.960742
+0491027A;Lycée Technol.agricole Privé Pouille;;49136;Les Ponts De Ce;47.4268572;-0.5516965
+0491802T;Lycée Technol.agricole Privé Robert D Arbrissel;Place Urbain Ii;49120;Chemille;47.210608;-0.7294054
+0530818B;Lycée Technol.agricole Privé Rochefeuille;Route De Caen;53100;Mayenne;48.543782;-0.207489
+0801613U;Lycée Technol.agricole Privé Sainte Colette;Rue De L Enclos;80800;Corbie;49.9096772;2.510355
+0711440E;Lycée Technol.agricole Privé Ste Marguerite Marie;Jannots;71220;Verosvres;46.401991;4.453972
+0810833R;Lycée Technol.agricole Privé Touscayrats;Touscayrats;81110;Verdalle;43.5060659;2.180924
+0421674S;Lycée Technol.prive Agricole Le Puits De L'aune;Rue Louis Blanc;42110;Feurs;45.7383928;4.2364878
+0130161E;Lycée Technologique Adam De Craponne;218 Rue Chateau Redon;13651;Salon De Provence;43.6355195;5.0986535
+0131656E;Lycée Technologique Agricole Aix-valabre;Chemin Moulin Fort;13548;Gardanne;43.464305;5.453012
+0040512K;Lycée Technologique Agricole Digne-carmejane;Route D Espinouse;04510;Le Chaffaut St Jurson;44.047146;6.158502
+0840110N;Lycée Technologique Agricole Francois Petrarque;Site Agroparc;84911;Avignon;43.949317;4.805528
+0251481Z;Lycée Technologique Agricole Jeanne D'arc;22 Rue Jeanne D'arc;25300;Pontarlier;46.9039198;6.3575183
+0251479X;Lycée Technologique Agricole Les Fontenelles;15 Rue Du Couvent;25210;Les Fontenelles;47.1959275;6.7471417
+0840607D;Lycée Technologique Agricole Louis Giraud;Chemin De L Hermitage;84208;Carpentras;44.0836262;5.059517
+0750710P;Lycée Technologique Auguste Renoir;24 Rue Ganneron;75018;Paris 18;48.8870203;2.3283565
+0750697A;Lycée Technologique Batiment-saint-lambert;15 Rue Saint Lambert;75015;Paris 15;48.8377738;2.2915594
+0420937R;Lycée Technologique Benoit Fourneyron;24 Rue Virgile;42013;St Etienne;45.4176774;4.4197149
+0520028Y;Lycée Technologique Blaise Pascal;1 Avenue Marcel Paul;52100;St Dizier;48.6456201;4.9737363
+0500087Y;Lycée Technologique Claude Lehec;Rue Dauphine;50600;St Hilaire Du Harcouet;48.5715264;-1.0976365
+0190012K;Lycée Technologique Danton;Rue Danton;19311;Brive La Gaillarde;45.1590287;1.5216108
+9760220K;Lycée Technologique De Dzoumogne;;97650;Bandraboua;0.0;0.0
+0590223X;Lycée Technologique Du Hainaut;1 Avenue Villars;59322;Valenciennes;50.3582813;3.5107769
+0750672Y;Lycée Technologique Duperre Esaa;11 Rue Dupetit Thouars;75003;Paris 03;48.8652834;2.3620241
+0750685M;Lycée Technologique Ec Nat Chimie Physiq Biologie;11 Rue Pirandello;75013;Paris 13;48.8365925;2.3562968
+0750612H;Lycée Technologique Ec Nat Sup Des Arts Appliques;63 65 Rue Olivier De Serres;75015;Paris 15;48.833647;2.2948847
+0594391C;Lycée Technologique Ec Sup Arts Appliques Textile;539 Avenue Des Nations Unies;59056;Roubaix;50.6936638;3.1752265
+0750707L;Lycée Technologique École Nationale De Commerce;70 Boulevard Bessieres;75017;Paris 17;48.8971407;2.3208753
+0801327H;Lycée Technologique Edouard Branly;70 Boulevard De Saint Quentin;80098;Amiens;49.8791607;2.2965051
+0480009Z;Lycée Technologique Emile Peytavin;Avenue Du 11 Novembre;48001;Mende;44.5233613;3.4833187
+0750686N;Lycée Technologique Estienne Esaig;18 Boulevard Auguste Blanqui;75013;Paris 13;48.8308013;2.3524558
+0211033J;Lycée Technologique Gustave Eiffel;15 Avenue Champollion;21074;Dijon;47.3338611;5.0676398
+0590011S;Lycée Technologique Gustave Eiffel;96 Rue Jules Lebleu;59427;Armentieres;50.6880881;2.8733271
+0290070U;Lycée Technologique Jean Chaptal;35 Chemin Des Justices;29191;Quimper;47.9918404;-4.1193383
+0750674A;Lycée Technologique Jules Siegfried;12 Rue D'abbeville;75010;Paris 10;48.8788604;2.3501271
+0760056Y;Lycée Technologique Jules Siegfried;1 Rue Dume D'aplemont;76054;Le Havre;49.4974627;0.1241609
+0141687H;Lycée Technologique Jules Verne;12 Rue Lucien Bossoutrot;14126;Mondeville;49.176963;-0.319769
+0332836D;Lycée Technologique Latresne Constructions Aeronautiques;Chateau De Latresne;33360;Latresne;44.779974;-0.491589
+0380034F;Lycée Technologique Louise Michel;30 Rue Louise Michel;38037;Grenoble;45.1687882;5.7081825
+0171428K;Lycée Technologique Lycée De La Mer Et Du Littoral;Rue William Bertrand;17560;Bourcefranc Le Chapus;45.846735;-1.15707
+9840268U;Lycée Technologique Lycée Hotelier De Tahiti;Punaauia;98718;Punaauia;-17.5943083;-149.6116857
+0130051K;Lycée Technologique Marie Curie;16 Boulevard Jeanne D Arc;13392;Marseille 05;43.2937083;5.4004604
+0710012C;Lycée Technologique Nicephore Niepce;141 Avenue Boucicaut;71321;Chalon Sur Saone;46.7888739;4.8359262
+0330029C;Lycée Technologique Nicolas Bremontier;152 Cours De L Yser;33077;Bordeaux;44.823338;-0.5706489
+0130011S;Lycée Technologique Pasquet;54 Bd Marcellin Berthelot;13200;Arles;43.6724263;4.6297406
+7200583W;Lycée Technologique Paul Vincensini;Rue Du 4eme D M M;20600;Bastia;42.697283;9.450881
+0031041R;Lycée Technologique Privé Anna Rodier;39 Cours Jean Jaures;03008;Moulins;46.5669568;3.3349455
+0573683U;Lycée Technologique Privé Anne De Mejanes;3 Rue Goussaud;57000;Metz;49.1235079;6.173823
+0170124T;Lycée Technologique Privé Aris Formation Pigier;16 Rue Leonce Vieljeux;17000;La Rochelle;46.1578836;-1.1553283
+9710993C;Lycée Technologique Privé Bel Air;Moudong Nord;97122;Baie Mahault;0.0;0.0
+0331591A;Lycée Technologique Privé Bel Orme;67 Rue De Bel Orme;33000;Bordeaux;44.8497536;-0.5941378
+0133286B;Lycée Technologique Privé Belsunce;13 Rue Fauchier;13002;Marseille 02;43.3044451;5.3717817
+0690646C;Lycée Technologique Privé Carrel;7 Rue Pierre Robin;69362;Lyon 07;45.7493853;4.8581918
+0133424B;Lycée Technologique Privé Caucadis;Bd Alfred Casile;13127;Vitrolles;43.4478245;5.2426452
+0133425C;Lycée Technologique Privé Celony;4 Bis Av De Lattre De Tassigny;13090;Aix En Provence;43.5303056;5.4383758
+0131402D;Lycée Technologique Privé Charles Peguy;102 Rue Sylvabelle;13006;Marseille 06;43.2882408;5.3748305
+0131681G;Lycée Technologique Privé Chimie Et Biologie La Forbine;4 Boulevard De La Forbine;13011;Marseille 11;43.2868081;5.4665119
+0542448T;Lycée Technologique Privé Claude Daunot;10 Boulevard Clemenceau;54052;Nancy;48.6779501;6.1805671
+0592969G;Lycée Technologique Privé Dampierre;85 Avenue De Denain;59326;Valenciennes;50.3543272;3.4992095
+0521045D;Lycée Technologique Privé De L'assomption;50 Rue Du Mal De Lattre;52108;St Dizier;48.6381589;4.9511085
+0593071T;Lycée Technologique Privé Deforest De Lewarde;151 Rue Jean De Gouy;59504;Douai;50.3715086;3.0819657
+0133396W;Lycée Technologique Privé Don Bosco;78 Rue Stanislas Torrents;13006;Marseille 06;43.2855609;5.3795529
+0691659D;Lycée Technologique Privé Don Bosco;12 Et 24 Montee Saint Laurent;69322;Lyon 05;45.7510345;4.8167844
+0070098U;Lycée Technologique Privé Du Sacre Coeur;7 Avenue De La Gare;07301;Tournon Sur Rhone;45.0637796;4.834587
+0753537M;Lycée Technologique Privé Ec Sup Tech Biologie Appliquee;56 Rue Planchat;75020;Paris 20;48.8560952;2.3964535
+0750330B;Lycée Technologique Privé Ec Technique Sup Laboratoire;93 95 R Du Dessous Des Berges;75013;Paris 13;48.8281956;2.3731118
+0593078A;Lycée Technologique Privé Fondation Depoorter;7-9 Rue Depoorter;59524;Hazebrouck;50.7201268;2.5342537
+0132280H;Lycée Technologique Privé Henri Leroy;1 Rue Des Ecoles;13230;Port St Louis Du Rhone;43.3867633;4.8031175
+0690631L;Lycée Technologique Privé Icof;8 Avenue Debrousse;69005;Lyon 05;45.7505097;4.8141393
+0870097H;Lycée Technologique Privé Inst D Econ Sociale Familiale;5 Rue De La Cite;87000;Limoges;45.8290178;1.2642372
+0141175B;Lycée Technologique Privé Institut Lemonnier;60 Rue D Herouville;14013;Caen;49.1935816;-0.3487181
+0751883P;Lycée Technologique Privé Institut Privé;119 Boulevard Diderot;75012;Paris 12;48.8479674;2.3912553
+0681885E;Lycée Technologique Privé Institut Sonnenberg;1 Rue Du Moulin;68130;Carspach;47.6651522;7.2235055
+0380018N;Lycée Technologique Privé Iser - Bordier;26 Rue Prosper Merimee;38100;Grenoble;45.1737355;5.7260681
+0381900J;Lycée Technologique Privé Itec Boisfleury Europe;76 Grande Rue;38701;La Tronche;45.2047507;5.7377606
+0741307U;Lycée Technologique Privé Jeanne D Arc;18 Bis Avenue Jules Ferry;74200;Thonon Les Bains;46.3724399;6.4832781
+0592966D;Lycée Technologique Privé Jehanne D'arc;106 Rue De Lille;59202;Tourcoing;50.7228019;3.1512774
+0690634P;Lycée Technologique Privé La Mache;75 Boulevard Jean Xxiii;69373;Lyon 08;45.7364603;4.8695511
+0754037F;Lycée Technologique Privé La Plaine Monceau;9 Boulevard De Courcelles;75008;Paris 08;48.881052;2.3158423
+0061852F;Lycée Technologique Privé La Providence;236 Route De Grenoble;06200;Nice;43.6819305;7.1990341
+0592967E;Lycée Technologique Privé La Sagesse;40 Rue De Mons;59306;Valenciennes;50.3598706;3.5288319
+0593061G;Lycée Technologique Privé La Sagesse;7 Rue Du Temple;59405;Cambrai;50.1755939;3.2277333
+0500127S;Lycée Technologique Privé Le Bon Sauveur;Rue E De Surville;50001;St Lo;49.112089;-1.094787
+0133554T;Lycée Technologique Privé Le Rocher;42 Boulevard Ledru Rollin;13300;Salon De Provence;43.6441863;5.0948018
+0741101V;Lycée Technologique Privé Les Bressis;85 Route Des Creuses;74600;Seynod;45.8932394;6.1001202
+0060735S;Lycée Technologique Privé Les Fauvettes;44 Avenue Jean De Noailles;06400;Cannes;43.5541346;6.9996242
+0382943T;Lycée Technologique Privé Les Portes De Chartreuse;387 Avenue De Stalingrad;38343;Voreppe;45.2947821;5.6336357
+0350787R;Lycée Technologique Privé Les Rimains;Esplanade De La Gare;35409;St Malo;48.5706771;-1.9687989
+0141418R;Lycée Technologique Privé Les Rosiers;17 Chemin De Rocques;14100;Lisieux;49.154762;0.2306014
+0754016H;Lycée Technologique Privé L'initiative;24 Rue Bouret;75019;Paris 19;48.8814352;2.3745457
+0121428M;Lycée Technologique Privé Louis Querbes;29 Rue Maurice Bompard;12000;Rodez;44.3485775;2.5792732
+0641534V;Lycée Technologique Privé Ltp Nay Baudreix;Route De Lys;64800;Baudreix;43.20577;-0.2589659
+0071359P;Lycée Technologique Privé Marc Seguin-saint Charles;Route De Californie;07100;Annonay;45.2440877;4.6758762
+0220116G;Lycée Technologique Privé Marie Balavenne;47 Boulevard Laennec;22005;St Brieuc;48.5111408;-2.7744799
+0831207L;Lycée Technologique Privé Marie France;220 Avenue Marcel Castie;83000;Toulon;43.1234854;5.941595
+0133474F;Lycée Technologique Privé Marie Gasquet;38 R Electriciens St Barnabe;13012;Marseille 12;43.3041972;5.4172944
+0131398Z;Lycée Technologique Privé Maximilien De Sully;52 Rue Auguste Blanqui;13006;Marseille 06;43.2922507;5.3874655
+0131436R;Lycée Technologique Privé Modele Electronique;233 Bd St Marcel;13396;Marseille 11;43.2865274;5.4686969
+0754048T;Lycée Technologique Privé Morin;182 Rue De Vaugirard;75015;Paris 15;48.8421148;2.3096576
+0311201M;Lycée Technologique Privé Myriam;9 Et 20 Rue Mage;31000;Toulouse;43.5977887;1.4472602
+0440259T;Lycée Technologique Privé Nd D'esperance;15 Rue Du Bois Savary;44615;St Nazaire;47.2747567;-2.2080565
+0570210U;Lycée Technologique Privé Notre Dame;2 Rue De Metz;57245;Peltre;49.0684672;6.2497106
+0690553B;Lycée Technologique Privé Notre Dame;72 Rue Des Jardiniers;69657;Villefranche Sur Saone;45.9885367;4.7206723
+0142084P;Lycée Technologique Privé Notre Dame De Fidelite;8 Rue Du Petit Clos Saint Marc;14074;Caen;49.2111144;-0.3733099
+0142108R;Lycée Technologique Privé Notre Dame De Nazareth;Avenue De La Basilique;14440;Douvres La Delivrande;49.2970159;-0.3732011
+0593006X;Lycée Technologique Privé Notre-dame D'annay;15 Place Du Concert;59000;Lille;50.6422985;3.0607077
+0690642Y;Lycée Technologique Privé O R T;133 Rue Marius Berliet;69008;Lyon 08;45.7380862;4.8677943
+0931796X;Lycée Technologique Privé Ort;39-45 Rue Raspail;93107;Montreuil;48.8528323;2.424645
+0133334D;Lycée Technologique Privé Ort Leon Bramson;9 Rue Des Forges;13010;Marseille 10;43.2791281;5.4104058
+9711022J;Lycée Technologique Privé Pensionnat De Versailles;8 Rue Victor Hugues;97100;Basse Terre;16.1985639;-61.5901551
+0542161F;Lycée Technologique Privé Pierre De Coubertin;5 R Du President Robert Schuman;54000;Nancy;48.6825501;6.1799118
+0650065T;Lycée Technologique Privé Pradeau-la Sede;14 Rue Mesclin;65912;Tarbes;43.2366268;0.0682376
+0070099V;Lycée Technologique Privé Saint Andre;18 Rue Emile Combes;07400;Le Teil;44.5388909;4.6878848
+0573519R;Lycée Technologique Privé Saint Andre;1 Rue De L'eglise;57840;Ottange;49.4410443;6.0178869
+0622199D;Lycée Technologique Privé Saint Denis;8 Place Saint Jean;62504;St Omer;50.750051;2.2606387
+0593077Z;Lycée Technologique Privé Saint Joseph;10 Rue De La Paix;59529;Hazebrouck;50.718103;2.5331551
+0622198C;Lycée Technologique Privé Saint Joseph;26 Route De Calais;62280;St Martin Boulogne;50.7367968;1.6223612
+0741414K;Lycée Technologique Privé Saint Joseph;St Martin Sur Arve;74703;Sallanches;45.912929;6.689997
+0573073F;Lycée Technologique Privé Saint Joseph-la Providence;10 Rue De L'abbe Heydel;57801;Freyming Merlebach;49.146198;6.811347
+0260072M;Lycée Technologique Privé Saint Louis;Clos Soubeyran;26402;Crest;0.0;0.0
+0592963A;Lycée Technologique Privé Saint Martin;54 Rue De Lille;59062;Roubaix;50.6867645;3.1712288
+0542295B;Lycée Technologique Privé Saint Michel;Charteuse De Bosserville;54510;Art Sur Meurthe;48.664136;6.243642
+0593104D;Lycée Technologique Privé Saint Pierre;10 Rue Du General Goutierre;59612;Fourmies;50.0092398;4.0503645
+0573366Z;Lycée Technologique Privé Saint Vincent De Paul;4 Rue Marie Douchet;57440;Algrange;49.3580552;6.0494087
+0573172N;Lycée Technologique Privé Sainte Chrétienne;1 Passage Du Pensionnat;57506;St Avold;49.1049716;6.7083946
+0141178E;Lycée Technologique Privé Sainte Ursule;1 Place Wurzburg;14067;Caen;49.2015078;-0.3799957
+0810095N;Lycée Technologique Privé Sainte-Cécile;1 Avenue Du Breuil;81000;Albi;43.9432297;2.1483149
+0811175M;Lycée Technologique Privé Saint-Joseph;38 Avenue De Lavaur;81100;Castres;43.6075533;2.2350212
+0650921Y;Lycée Technologique Privé Saint-Pierre;24 Avenue D'azereix;65000;Tarbes;43.2289169;0.0578127
+0755121J;Lycée Technologique Privé Sinai;9 Rue Jacques Ibert;75017;Paris 17;48.8890075;2.2909761
+0371439N;Lycée Technologique Privé St Francois Inst N-d La Riche;30 Rue Delperier;37058;Tours;47.3925342;0.6769264
+0132922F;Lycée Technologique Privé St Jean;76 Av Georges Borel;13300;Salon De Provence;43.6406902;5.0821442
+0840940R;Lycée Technologique Privé St Jean Baptiste De La Salle;9 Rue Notre Dame Sept Douleurs;84008;Avignon;43.9489266;4.815644
+0593005W;Lycée Technologique Privé St Jean-baptiste De La Salle;2 Rue Jean Le Vasseur;59000;Lille;50.6138111;3.0423599
+0100059K;Lycée Technologique Privé St Joseph;21 Rue Cloitre St Etienne;10042;Troyes;48.2970565;4.0816607
+0783323N;Lycée Technologique Privé St Thomas De Villeneuve;15 Rue Des Louviers;78100;St Germain En Laye;48.8983954;2.0921371
+0601950U;Lycée Technologique Privé St Vincent De Paul;8 Bd Du General De Gaulle;60000;Beauvais;49.4291558;2.0897125
+0641441U;Lycée Technologique Privé Ste Anne;Notre Dame Du Refuge;64604;Anglet;43.496758;-1.52561
+0911981J;Lycée Technologique Privé Ste Jeanne D Arc;36 Av Du 8 Mai 1945;91120;Palaiseau;48.7096979;2.2428188
+0133395V;Lycée Technologique Privé Ste Marie;2 Rue De Jouques;13100;Aix En Provence;43.5318267;5.4462333
+0490887Y;Lycée Technologique Privé Ste Marie;43 Rue St Bonaventure;49307;Cholet;47.0594586;-0.8778263
+0593034C;Lycée Technologique Privé Theophile Legrand;16 Rue Bertrand;59720;Louvroil;50.2659008;3.9615428
+0492019D;Lycée Technologique Privé Urbain Mongazon;1 Rue Du Colombier;49036;Angers;47.4592971;-0.5294038
+0593058D;Lycée Technologique Privé Vauban;1 Place Vauban;59140;Dunkerque;51.0294231;2.3781519
+0130049H;Lycée Technologique Rempart (rue Du);1 Rue Du Rempart;13007;Marseille 07;43.2889391;5.3652071
+0670086E;Lycée Technologique Rene Cassin;4 Rue Schoch;67046;Strasbourg;48.5829505;7.7702809
+0332468D;Lycée Technologique St Louis;Rue Capitaine Kleber Dupuy;33028;Bordeaux;44.837789;-0.57918
+0511219Y;Lycée Viticole De La Champagne;Route D'oger;51190;Avize;48.9669337;4.0084888
+0241111C;Lycéee Prof Agricole;Domaine De La Brie;24240;Monbazillac;44.793529;0.492161
+0301433V;Maison Famil Rurale Horticole Le Grand Mas;Route De Nimes;30700;Uzes;43.9965584;4.4132526
+0520934H;Maison Familiale Rurale Des Recollets;Avenue Du 21eme R.i.;52202;Langres;47.849961;5.3290179
+0753494R;Museum D'histoire Naturelle;57 Rue Cuvier;75231;Paris 05;48.8441222;2.3556153
+0753496T;Observatoire De Paris;61 Avenue De L'observatoire;75014;Paris 14;48.8369989;2.3365396
+0190058K;Ogec Notre Dame Jeanne D'arc Lycée Bossuet;11 Rue Bossuet;19107;Brive La Gaillarde;45.1582613;1.52277
+0750030A;Palais De La Decouverte;Avenue Franklin D Roosevelt;75008;Paris 08;48.8705294;2.3101262
+0450762J;Sarl Inst. Formation Prof.;31 Av Louis Maurice Chautemps;45200;Montargis;48.0069208;2.7464731
+0382974B;Ste Grenobloise De Formation Sup2i;9 Rue General Ferrie;38100;Grenoble;45.1796694;5.723777
+9710585J;Université Antilles Guyane;Bd Legitimus Bp 771;97159;Pointe A Pitre;16.241111;-61.533056
+0131842G;Université Aix-marseille 1;3, Place Victor-hugo;13331;Marseille 03;43.3042684;5.3782868
+0131843H;Université Aix-marseille 2;58 Bd Charles Livon;13284;Marseille 07;43.2920635;5.3590116
+0132364Z;Université Aix-marseille 3;3, Avenue Robert Schuman;13628;Aix En Provence;43.5192855;5.4406011
+0331764N;Université Bordeaux 1;351, Cours De La Liberation;33405;Talence;44.808849;-0.5914683
+0331765P;Université Bordeaux 2;146, Rue Leo Saignat;33076;Bordeaux;44.8264547;-0.6017476
+0331766R;Université Bordeaux 3;Esplanade Montaigne Dom Univ;33405;Talence;44.802614;-0.588054
+0332929E;Universite Bordeaux 4;Avenue Leon Duguit;33608;Pessac;44.7988471;-0.6154882
+0631262E;Université Clermont-ferrand 1;49 Boulevard Gergovia Bp 32;63001;Clermont Ferrand;45.777222;3.087025
+0631525R;Université Clermont-ferrand 2;34 Avenue Carnot Bp 185;63006;Clermont Ferrand;45.777222;3.087025
+0801344B;Université D'amiens;Chemin Du Thil;80025;Amiens;49.8762421;2.2668361
+0490970N;Université D'angers;Rue Des Arenes Bp 3532;49035;Angers;47.478419;-0.563166
+0623957P;Université D'arras;9 Rue Du Temple Bp 665;62030;Arras;50.291002;2.777535
+0840685N;Université D'avignon;35 Rue Joseph Vernet Bp 218;84010;Avignon;43.949317;4.805528
+0251215K;Université De Besancon;3 Rue Goudimel;25030;Besancon;47.2402992;6.0225572
+0290346U;Université De Brest;Rue Des Archives Bp 137;29269;Brest;48.390394;-4.486076
+0561718N;Universite De Bretagne Sud;12 Avenue Saint Symphorien;56000;Vannes;47.6623582;-2.7552643
+0141408E;Université De Caen;Esplanade De La Paix;14032;Caen;49.1881893;-0.3638111
+0951793H;Université De Cergy-pontoise;33 Bd Du Port Bp 220;95011;Cergy;49.035617;2.060325
+0730858L;Université De Chambery;27 Rue Marcoz Bp 1104;73011;Chambery;45.564601;5.917781
+0601223D;Université De Compiègne;Rue Roger Couttolenc Bp 649;60206;Compiegne;49.417816;2.826145
+7200664J;Université De Corse;7 Av J Nicoli Bp 52;20250;Corte;42.309409;9.149022
+0211237F;Université De Dijon;Campus Univ Montmuzard Bp 138;21004;Dijon;47.322047;5.04148
+0595964M;Université De Dunkerque;9 Quai De La Citadelle Bp 1022;59375;Dunkerque;51.03456;2.3752019
+9830445S;Universite De La Nouvelle-caledonie;145 Av James Cook;98847;Noumea;-22.2686289;166.4242812
+9840349G;Universite De La Polynesie Francaise;Punaauia;98714;Papeete;-17.5753232;-149.6059799
+9740478B;Université De La Réunion;15 Avenue Rene Cassin;97489;St Denis;47.827397;1.9235839
+0171463Y;Université De La Rochelle;23 Avenue Albert Einstein;17071;La Rochelle;46.148256;-1.154995
+0870669E;Université De Limoges;Hotel Burgy 13 Rue De Geneve;87065;Limoges;45.8340567;1.2442706
+0772502B;Universite De Marne La Vallee;5 Bld Descartes A Champs/marne;77454;Champs Sur Marne;48.8400977;2.5862493
+0932056E;Université De Marne La Vallée;2 Rue De La Butte Verte;93160;Noisy Le Grand;48.8423725;2.5796219
+0572081C;Université De Metz;Ile Du Saulcy Bp 794;57012;Metz;49.119666;6.176905
+0681166Y;Université De Mulhouse;2, Rue Des Freres Lumiere;68093;Mulhouse;47.73208;7.315734
+0440984F;Université De Nantes;Quai Tourville Bp 1026;44035;Nantes;47.218371;-1.553621
+0060931E;Université De Nice;28, Avenue Valrose;06034;Nice;43.7158479;7.2657113
+0640251A;Université De Pau Et Pays De L'adour;Av De L Universite Bp 576;64012;Pau;43.2951;-0.370797
+0660437S;Université De Perpignan;52 Avenue De Villeneuve;66860;Perpignan;42.6809965;2.9025928
+0860856N;Université De Poitiers;15 Rue De L Hotel Dieu;86034;Poitiers;46.5857078;0.3458139
+0511296G;Université De Reims;Villa Douce 9 Bd De La Paix;51097;Reims;49.2543882;4.0406026
+0761904G;Université De Rouen;1, Rue Thomas Becket;76821;Mont St Aignan;49.4603349;1.0673087
+0421095M;Université De Saint-etienne;34 Rue Francis Baulier;42023;St Etienne;45.4294967;4.391713
+0830766G;Université De Toulon;Avenue De L Universite Bp 132;83957;La Garde;43.128777;6.017199
+0370800U;Université De Tours;3, Rue Des Tanneurs;37041;Tours;47.3960763;0.6815944
+0101060Y;Université De Troyes;13 Bd Henri Barbusse Bp 2060;10010;Troyes;48.2965919;4.074735
+0593279U;Université De Valenciennes;Le Mont Houy Bp 311;59304;Valenciennes;50.357113;3.518332
+0781944P;Université De Versailles;23, Rue Du Refuge;78035;Versailles;48.8040427;2.1440967
+0450855K;Université D'orléans;Chateau De La Source;45067;Orleans;47.8507118;1.9337794
+0762762P;Université Du Havre;25 Rue Philippe Lebon Bp 1123;76063;Le Havre;49.49437;0.107929
+0720916E;Université Du Mans;Av Messiaen Bp 535;72017;Le Mans;48.00611;0.199556
+0911975C;Université Evry;23 Boulevard Des Coquibus;91025;Evry;48.6226142;2.4426967
+9840303G;Université Francaise Du Pacifique;B P 4635 Papeete;98800;Noumea;-22.2758;166.458
+0381838S;Université Grenoble 1;621 Av Centrale Bp53x;38041;Grenoble;45.188529;5.724524
+0381839T;Université Grenoble 2;Domaine Universitaire Bp 47x;38402;St Martin D Heres;45.165133;5.767761
+0381840U;Université Grenoble 3;Dom Univ Ave Centrale Bp 25;38040;Grenoble;45.188529;5.724524
+0593559Y;Université Lille 1;Dom Universitaire Scientifique;59655;Villeneuve D Ascq;50.623334;3.145
+0593560Z;Université Lille 2;42, Rue Paul Duez;59800;Lille;50.6312819;3.0756487
+0593561A;Université Lille Iii;Dom Univ Lit Et Jur Bp 149;59653;Villeneuve D Ascq;50.623334;3.145
+0691774D;Université Lyon 1;43 Bd Du 11 Nov 1918 Bp 761;69622;Villeurbanne;45.771944;4.8901709
+0691775E;Université Lyon 2;86, Rue Pasteur;69365;Lyon 07;45.7495518;4.8374252
+0692437Z;Université Lyon 3;1 Rue Universite Bp 0638;69239;Lyon 02;0.0;0.0
+0341087X;Université Montpellier 1;5,bd Henri Iv;34006;Montpellier;43.6149374;3.8736413
+0341088Y;Université Montpellier 2;Place Eugene Bataillon;34095;Montpellier;43.631505;3.860602
+0341089Z;Université Montpellier 3;B P 5043;34032;Montpellier;43.6340946;3.8704922
+0541507V;Université Nancy 1;24 30 R Lionnois Bp 3069;54013;Nancy;48.692054;6.184417
+0541508W;Université Nancy 2;25, Rue Baron Louis Bp 454;54001;Nancy;48.692054;6.184417
+0751717J;Université Paris 1;12 Place Du Pantheon;75231;Paris 05;48.8468209;2.3448981
+0921204J;Université Paris 10;200 Avenue De La Republique;92001;Nanterre;48.9047283;2.2088302
+0911101C;Université Paris 11;15, Rue Georges Clemenceau;91405;Orsay;48.6981864;2.181768
+0941111X;Université Paris 12;61, Avenue Du General De Gaulle;94010;Creteil;48.788165;2.445979
+0931238R;Université Paris 13;Avenue Jean-baptiste Clement;93430;Villetaneuse;48.955902;2.3388238
+0751718K;Université Paris 2;12, Place Du Pantheon;75231;Paris 05;48.8468209;2.3448981
+0751719L;Université Paris 3;17, Place De La Sorbonne;75230;Paris 05;48.8485744;2.3421878
+0751720M;Université Paris 4;1, Rue Victor Cousin;75230;Paris 05;48.8478098;2.3424414
+0751721N;Université Paris 5;12 Rue De L Ecole De Medecine;75270;Paris 06;48.8512077;2.3407847
+0751722P;Université Paris 6;4, Place Jussieu;75252;Paris 05;48.8465589;2.3544484
+0751723R;Université Paris 7;2,place Jussieu;75251;Paris 05;48.8460464;2.355228
+0931827F;Université Paris 8;2 Rue De La Liberte;93526;St Denis;48.9452421;2.3639438
+0750736T;Université Paris 9;Place Du Marechal De Tassigny;75775;Paris 16;48.8712128;2.2744029
+0350936C;Université Rennes 1;2 Rue Du Thabor Bp 1134;35014;Rennes;48.113475;-1.675708
+0350937D;Université Rennes 2;6, Avenue Gaston Berger;35043;Rennes;48.1187652;-1.7048071
+0671712X;Université Strasbourg 1;4 Rue Blaise Pascal Bp1032 F;67070;Strasbourg;48.583148;7.747882
+0671713Y;Université Strasbourg 2;22, Rue Descartes;67084;Strasbourg;48.579007;7.7651262
+0671778U;Université Strasbourg 3;1 Place D Athenes Bp 66;67045;Strasbourg;48.583148;7.747882
+0470940S;Universite Thematique Agen;Agropole Route De Condom;47931;Agen;44.203142;0.616363
+0311382J;Université Toulouse 1;Place Anatole France;31042;Toulouse;43.6060249;1.4384679
+0311383K;Université Toulouse 2;5, Allee Antonio Machado;31058;Toulouse;43.5776992;1.4040191
+0311384L;Université Toulouse 3;118, Route De Narbonne;31062;Toulouse;43.5631577;1.4628392
diff --git a/config/etc/README b/config/etc/README
new file mode 100644
index 0000000000000000000000000000000000000000..26aea491bb98f78cd5cfd2cfe45790b10265d0b9
--- /dev/null
+++ b/config/etc/README
@@ -0,0 +1,9 @@
+
+* Scripts utiles pour l'installation de ScoDoc
+
+- scodoc :  script de demarrage a placer dans /etc/rc.*
+
+- scodoc-site-ssl: configuration de site (https) pour Apache, a placer dans 
+                   /etc/apache2/sites-available/
+                   puis editer pour remplacer le nom du site et indiquer vos certificats crypto
+                   puis activer par a2ensite scodoc-site-ssl
diff --git a/config/etc/firehol.conf b/config/etc/firehol.conf
new file mode 100644
index 0000000000000000000000000000000000000000..2f5d964a91e898989953cf79a23ed47574b52408
--- /dev/null
+++ b/config/etc/firehol.conf
@@ -0,0 +1,19 @@
+#
+# Exemple de configuration pare-feu firehol minimal pour un serveur ScoDoc
+#
+# Suppose que l'interface reseau Internet est XXX_INTERFACE_XXX
+#
+# E. Viennet, juin 2008, jul 2017
+#
+
+version 5
+
+FIREHOL_LOG_MODE="ULOG"
+
+interface XXX_INTERFACE_XXX internet
+	protection strong
+	client all accept  # plutot gentil, a adapter a vos besoins (attention a ntp et aux mises a jour)
+	server ssh accept
+	server https accept
+	# pas de log des paquets venant d'Internet...
+        server any nolog drop
diff --git a/config/etc/scodoc b/config/etc/scodoc
new file mode 100755
index 0000000000000000000000000000000000000000..cf7a10ffff4a144e0bea68e6e09d3e82711ea5df
--- /dev/null
+++ b/config/etc/scodoc
@@ -0,0 +1,75 @@
+#!/bin/sh
+#
+# zope		This shell script takes care of starting and stopping
+#		zope under apache (proxy)
+#               Emmanuel Viennet @ LIPN, June 2002, Sept 2011 (LSB-compliance for Debian 6)
+#
+# chkconfig: - 90 10
+# description: zope is a web server
+
+
+### BEGIN INIT INFO
+# Provides: ScoDoc
+# Required-Start: $local_fs $remote_fs $network $syslog
+# Required-Stop: $local_fs $remote_fs $network $syslog
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: Start/stop ScoDoc server
+### END INIT INFO
+
+# Source function library.
+#. /etc/init.d/functions
+
+
+ZOPE_DIR=/opt/scodoc/instance
+
+# [ -x  $ZOPE_DIR/bin/python ] || exit 0
+
+RETVAL=0
+prog="Zope"
+
+start() {
+	# Start Zope persistent process
+        # Start daemons.
+        echo -n $"Starting $prog: "
+	echo $ZOPE_DIR/bin/zopectl start 
+        $ZOPE_DIR/bin/zopectl start 2> /dev/null
+	RETVAL=$?
+        echo
+#        [ $RETVAL -eq 0 ] && touch /var/lock/subsys/zope
+	return $RETVAL
+}
+
+stop() {
+        # Stop daemons.
+	echo -n 'Stopping zope daemon: '
+        $ZOPE_DIR/bin/zopectl stop
+	RETVAL=$?
+        echo
+#        [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/zope
+	return $RETVAL
+}
+
+# See how we were called.
+case "$1" in
+  start)
+	start
+        ;;
+  stop)
+	stop
+        ;;
+#  status)
+#	status $ZOPE_DIR/bin/python
+#	RETVAL=$?
+#	;;
+  restart|reload)
+	stop
+	start
+	RETVAL=$?
+	;;
+  *)
+        echo $"Usage: $0 {start|stop|restart}"
+        exit 1
+esac
+
+exit $RETVAL
diff --git a/config/etc/scodoc-site-ssl-apache2.4.orig b/config/etc/scodoc-site-ssl-apache2.4.orig
new file mode 100644
index 0000000000000000000000000000000000000000..ba1a612ca00ef8fa8e209b93843daf23165cb575
--- /dev/null
+++ b/config/etc/scodoc-site-ssl-apache2.4.orig
@@ -0,0 +1,56 @@
+
+# Fichier config Apache basique pour ScoDoc
+# E. Viennet, juin 2008, sept 2009, mar 2017 (Apache 2.4)
+
+# Apache fonctionne ici en proxy https devant zope (lui meme sur 8080).
+
+# A EDITER !!!
+#  1- REMPLACER YOUR.FULL.HOST.NAME par votre nom de site (www.monscodoc.com)
+#  2- indiquer vos certificats ssl (.key, .pem)
+#  3- eventuellement, le certificat de votre authorite de certif. (CA).
+#  4- eventuellement, revoir les fichiers de logs (penser au logrotate !)
+
+
+NameVirtualHost *:443
+<VirtualHost *:443>
+  ServerAdmin webmaster
+  ServerName  YOUR.FULL.HOST.NAME
+  SSLEngine on
+
+  SSLCertificateFile /etc/apache2/scodoc-ssl/apache.pem
+  # SSLCertificateKeyFile /etc/apache2/ssl/iutv.univ-paris13.fr.key
+
+  # Votre authorite de certification:
+  #  SSLCACertificateFile /etc/apache2/scodoc-ssl/ct_root.pem
+  #  SSLCACertificateFile /etc/apache2/scodoc-ssl/sureserverEDU.pem
+
+  DocumentRoot /var/www/
+
+  ErrorLog /var/log/apache2/ssl-error.log
+
+  # Possible values include: debug, info, notice, warn, error, crit,
+  # alert, emerg.
+  LogLevel warn
+
+  CustomLog /var/log/apache2/ssl-access.log combined
+
+  RewriteEngine on
+  # "RewriteLogLevel" n'est plus disponible dans apache2.4, il faut utiliser l'option "rewrite" dans l'instruction "LogLevel"
+  #LogLevel warn rewrite:trace3
+
+  # ScoDoc static content, served directly:
+  # RewriteCond %{HTTP:Authorization}  ^(.*)
+  RewriteRule ^/ScoDoc/static/(.*) /opt/scodoc/Products/ScoDoc/static/$1 [L]
+
+  # Le reste est pour Zope:
+  RewriteRule ^/(.*) http://localhost:8080/VirtualHostBase/https/YOUR.FULL.HOST.NAME:443/$1 [L,P]
+
+  # Log avec utilisateur authentifie par ScoDoc
+  LogFormat "%h %l %{X-ScoDoc-User}o %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" scodoc
+  CustomLog "/var/log/apache2/scodoc_access.log" scodoc
+
+</VirtualHost>
+
+<Directory /opt/scodoc/Products/ScoDoc/static/>
+Require all granted
+</Directory>
diff --git a/config/etc/scodoc-site-ssl.orig b/config/etc/scodoc-site-ssl.orig
new file mode 100644
index 0000000000000000000000000000000000000000..0f628f231bf7229af0a742939c3845f64d9ac1c4
--- /dev/null
+++ b/config/etc/scodoc-site-ssl.orig
@@ -0,0 +1,52 @@
+
+# Fichier config Apache basique pour ScoDoc
+# E. Viennet, juin 2008, sept 2009
+
+# Apache fonctionne ici en proxy https devant zope (lui meme sur 8080).
+
+# A EDITER !!!
+#  1- REMPLACER YOUR.FULL.HOST.NAME par votre nom de site (www.monscodoc.com)
+#  2- indiquer vos certificats ssl (.key, .pem)
+#  3- eventuellement, le certificat de votre authorite de certif. (CA).
+#  4- eventuellement, revoir les fichiers de logs (penser au logrotate !)
+
+
+NameVirtualHost *:443
+<VirtualHost *:443>
+  ServerAdmin webmaster
+  ServerName  YOUR.FULL.HOST.NAME
+  SSLEngine on
+
+  SSLCertificateFile /etc/apache2/scodoc-ssl/apache.pem
+  # SSLCertificateKeyFile /etc/apache2/ssl/iutv.univ-paris13.fr.key
+
+  # Votre authorite de certification:
+  #  SSLCACertificateFile /etc/apache2/scodoc-ssl/ct_root.pem
+  #  SSLCACertificateFile /etc/apache2/scodoc-ssl/sureserverEDU.pem
+
+  DocumentRoot /var/www/
+
+  ErrorLog /var/log/apache2/ssl-error.log
+
+  # Possible values include: debug, info, notice, warn, error, crit,
+  # alert, emerg.
+  LogLevel warn
+
+  CustomLog /var/log/apache2/ssl-access.log combined
+
+  RewriteEngine on
+  RewriteLog   /var/log/rewrite.ssl
+  RewriteLogLevel 0
+
+  # ScoDoc static content, served directly:
+  RewriteCond %{HTTP:Authorization}  ^(.*)
+  RewriteRule ^/ScoDoc/static/(.*) /opt/scodoc/instance/Products/ScoDoc/static/$1 [L]
+
+  # Le reste est pour Zope:
+  RewriteRule ^/(.*) http://localhost:8080/VirtualHostBase/https/YOUR.FULL.HOST.NAME:443/$1 [L,P]
+
+  # Log avec utilisateur authentifie par ScoDoc
+  LogFormat "%h %l %{X-ScoDoc-User}o %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" scodoc
+  CustomLog "/var/log/apache2/scodoc_access.log" scodoc
+
+</VirtualHost>
diff --git a/config/etc/scodoc-site.orig b/config/etc/scodoc-site.orig
new file mode 100644
index 0000000000000000000000000000000000000000..6829d876890f4f48312e848ae85cdee91cbc30f7
--- /dev/null
+++ b/config/etc/scodoc-site.orig
@@ -0,0 +1,22 @@
+#
+# Config Apache minimale http: redirige tout vers https
+# (voir scodoc-site-ssl)
+#
+NameVirtualHost *
+<VirtualHost *>
+	ServerAdmin webmaster@localhost
+	DocumentRoot /var/www/
+	<Directory />
+		AllowOverride None
+	</Directory>
+
+	RewriteEngine on
+	
+	ReWriteCond %{SERVER_PORT} !^443$
+	RewriteRule ^/(.*) https://%{HTTP_HOST}/$1 [NC,R,L]
+
+	ErrorLog /var/log/apache2/error.log
+	LogLevel warn
+	CustomLog /var/log/apache2/access.log combined
+	ServerSignature On
+</VirtualHost>
diff --git a/config/etc/scodoc-updater.service b/config/etc/scodoc-updater.service
new file mode 100644
index 0000000000000000000000000000000000000000..dd0ca9b9f18513491248a93efd627fa77c3a8645
--- /dev/null
+++ b/config/etc/scodoc-updater.service
@@ -0,0 +1,18 @@
+#
+# Systemd service to upgrade ScoDoc
+#
+# Put this file in /etc/systemd/system, along with scodoc-updater.timer
+# and then run
+#    systemctl enable scodoc-updater.timer
+#    systemctl start scodoc-updater.timer
+# 
+# E. Viennet jun 2019
+#
+
+[Unit]
+Description=Update ScoDoc Software
+
+[Service]
+Type=oneshot
+ExecStart=/opt/scodoc/Products/ScoDoc/config/upgrade.sh
+
diff --git a/config/etc/scodoc-updater.timer b/config/etc/scodoc-updater.timer
new file mode 100644
index 0000000000000000000000000000000000000000..3a7cddff662ff37e6ffaface634d6d93ef8d56f9
--- /dev/null
+++ b/config/etc/scodoc-updater.timer
@@ -0,0 +1,18 @@
+#
+# Systemd service to upgrade ScoDoc
+#
+# Put this file in /etc/systemd/system, along with scodoc-updater.service
+#
+# E. Viennet jun 2019
+#
+
+[Unit]
+Description=Upgrade ScoDoc and Linux each week on Saturday night (sunday, 2:30 am)
+
+[Timer]
+OnCalendar=Sun *-*-* 02:30:00
+# Add random delay, to avoid overloading ScoDoc upgrade server
+RandomizedDelaySec=30min
+
+[Install]
+WantedBy=timers.target
diff --git a/config/initialize_database.sh b/config/initialize_database.sh
new file mode 100755
index 0000000000000000000000000000000000000000..d57849a01b68cde7a66dbc5b816dfb44474590c1
--- /dev/null
+++ b/config/initialize_database.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# Initialize database (create tables) for a ScoDoc instance
+# This script must be executed as www-data user
+#
+# $db_name and $DEPT passed as environment variables
+
+source config.sh
+source utils.sh
+
+if [ $(id -nu) != $POSTGRES_USER ]
+then
+ echo "$0: script must be runned as user $POSTGRES_USER"
+ exit 1
+fi
+
+echo 'Initializing tables in database ' $db_name
+$PSQL -U $POSTGRES_USER -p $POSTGRES_PORT $db_name -f $SCODOC_DIR/misc/createtables.sql
+
+
+# Set DeptName in preferences:
+echo "insert into sco_prefs (name, value) values ('DeptName', '"${DEPT}\'\) | $PSQL -U $POSTGRES_USER  -p $POSTGRES_PORT $db_name
\ No newline at end of file
diff --git a/config/install_cal_modules.sh b/config/install_cal_modules.sh
new file mode 100755
index 0000000000000000000000000000000000000000..1e5f489871ae48338803cdb35488dbb2cab63cc0
--- /dev/null
+++ b/config/install_cal_modules.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Install module(s) for calendars
+# (if already installed, do nothing)
+
+
+# Test if installed
+# (NB: don't launch python, to be faster and also to avoid import bug zope vs pytz)
+
+if [ -e /opt/zope213/lib/python2.7/site-packages/icalendar ] || [ /usr/lib/python2.7/dist-packages/icalendar ]
+then
+    exit 0 # already installed
+else
+    echo "Installing icalendar"
+    /opt/zope213/bin/pip install icalendar
+fi
+
+
+
diff --git a/config/install_debian10.sh b/config/install_debian10.sh
new file mode 100755
index 0000000000000000000000000000000000000000..7e9579ee2964c58ed3042f332f3f4d41436e80da
--- /dev/null
+++ b/config/install_debian10.sh
@@ -0,0 +1,259 @@
+#!/bin/bash
+
+#
+# ScoDoc: install third-party software necessary for our installation
+# starting for a minimal Debian (Stretch, 9.0) install.
+#
+# E. Viennet, Jun 2008, Apr 2009, Sept 2011, Sept 2013, Nov 2013, Mar 2017, Jul 2017, Jun 2019, Oct 2019
+#
+
+source config.sh
+source utils.sh
+
+check_uid_root $0
+
+PYTHON=/opt/zope213/bin/python
+
+# ------------ Safety checks
+if [ ${debian_version} != "10" ]
+then
+   echo "Version du systeme Linux Debian incompatible"
+   exit 1
+fi
+
+if [ $(arch) != "x86_64" ]
+then
+   echo "Version du systeme Linux Debian incompatible (pas X86 64 bits)"
+   exit 1
+fi
+
+# ------------ Permissions & directories
+# source dir should be writable by scodoc to write bytecode files
+chgrp www-data $SCODOC_DIR $SCODOC_DIR/ZopeProducts/*
+chmod g+w $SCODOC_DIR $SCODOC_DIR/ZopeProducts/*
+
+chgrp -R www-data "${SCODOC_DIR}"/static/photos
+chmod -R g+w "${SCODOC_DIR}"/static/photos
+
+if [ ! -e "${SCODOC_VERSION_DIR}" ]; then
+  mkdir "${SCODOC_VERSION_DIR}"
+  chown www-data.www-data "${SCODOC_VERSION_DIR}"
+fi
+
+# ------------ LOCALES
+echo 
+echo '---- Configuration des locales...'
+echo
+
+if [ ! -e /etc/locale.gen ]
+then
+    touch /etc/locale.gen
+fi
+
+
+for locname in en_US.UTF-8 en_US.ISO-8859-15 en_US.ISO-8859-1
+do
+  outname=$(echo ${locname//-/} | tr '[A-Z]' '[a-z]')
+  if [ $(locale -a | egrep -i ^${outname}$ | wc -l) -lt 1 ]
+  then
+    echo adding $locname
+    echo "$locname ${locname##*.}" >> /etc/locale.gen
+  fi
+done
+
+/usr/sbin/locale-gen --keep-existing 
+
+
+if [ "$LANG" != "en_US.UTF-8" ]
+then
+   # ceci est necessaire a cause de postgresql 8.3 qui 
+   # cree son cluster lors de l'install avec la locale par defaut !
+   echo "Attention: changement de la locale par defaut"
+   mv /etc/default/locale /etc/default/locale.orig
+   echo "LANG=\"en_US.UTF-8\"" > /etc/default/locale
+   export LANG=en_US.UTF-8
+fi
+echo 'Done.'
+
+# ------------ FIX pour passage Debian 7 -> Debian >= 8
+chsh -s /bin/sh www-data
+
+# ------------ AJOUT DES PAQUETS NECESSAIRES
+apt-get update
+apt-get -y install subversion curl cracklib-runtime
+apt-get -y install apache2 ssl-cert 
+apt-get -y install postgresql
+apt-get -y install graphviz
+
+# ------------ INSTALL DES EXTENSIONS PYTHON (2.7)
+
+apt-get -y install python-docutils
+apt-get -y install python-jaxml 
+apt-get -y install python-psycopg2 
+apt-get -y install python-pyrss2gen 
+apt-get -y install python-pil python-reportlab 
+apt-get -y install python-cracklib # was python-crack
+apt-get -y install python-icalendar
+apt-get -y install python-requests
+
+apt-get -y install python-egenix-mxtools python-egenix-mxdatetime
+
+
+# ------------
+SVNVERSION=$(cd ..; svnversion)
+SVERSION=$(curl --silent http://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/version?mode=install\&svn=$SVNVERSION)
+echo $SVERSION > "${SCODOC_VERSION_DIR}/scodoc.sn"
+
+
+# ------------ POSTFIX
+echo 
+echo "ScoDoc a besoin de pouvoir envoyer des messages par mail."
+echo -n "Voulez vous configurer la messagerie (tres recommande) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    apt-get -y install postfix
+fi
+
+# ------------ CONFIG FIREWALL (non teste en Debian 10)
+echo 
+echo "Le firewall aide a proteger votre serveur d'intrusions indesirables."
+echo -n "Voulez vous configurer un firewall minimal (ufw) ? (y/n) [n] "
+read ans
+if [ "$(norm_ans "$ans")" = 'Y' ]
+then
+    echo 'Installation du firewall IP ufw (voir documentation Debian)'
+    echo '   on autorise les connexions ssh et https'
+    apt-get -y install ufw
+    ufw default deny incoming
+    ufw default allow outgoing
+    ufw allow ssh
+    ufw allow https
+    yes | ufw enable
+fi
+
+# Nota: after this point, the network _may_ be unreachable 
+# (if firewall config is wrong)
+
+# ------------ CONFIG APACHE
+a2enmod ssl
+a2enmod proxy
+a2enmod proxy_http
+a2enmod rewrite
+
+echo 
+echo "La configuration du serveur web va modifier votre installation Apache pour supporter ScoDoc."
+echo -n "Voulez vous configurer le serveur web Apache maintenant (tres conseille) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    echo "Configuration d'Apache"
+    server_name=""
+    while [ -z $server_name ]
+    do
+        echo "Le nom de votre serveur doit normalement etre connu dans le DNS."
+	echo -n "Nom complet de votre serveur (exemple: notes.univ.fr): "
+	read server_name
+    done
+    # --- CERTIFICATS AUTO-SIGNES
+    echo 
+    echo "Il est possible d'utiliser des certificats cryptographiques"
+    echo "auto-signes, qui ne seront pas reconnus comme de confiance"
+    echo "par les navigateurs, mais offrent une certaine securite."
+    echo -n 'Voulez vous generer des certificats ssl auto-signes ? (y/n) [y] '
+    read ans
+    if [ "$(norm_ans "$ans")" != 'N' ]
+    then
+        # attention: utilise dans scodoc-site-ssl.orig
+	    ssl_dir=/etc/apache2/scodoc-ssl 
+    	if [ ! -e $ssl_dir ]
+	    then
+            mkdir $ssl_dir
+	    fi
+	    /usr/sbin/make-ssl-cert /usr/share/ssl-cert/ssleay.cnf $ssl_dir/apache.pem
+        cert_status=$?
+    else
+        cert_status=-1
+    fi
+    # ---
+    echo 'generation de /etc/apache2/sites-available/scodoc-site-ssl'
+    cat $SCODOC_DIR/config/etc/scodoc-site-ssl-apache2.4.orig | sed -e "s:YOUR\.FULL\.HOST\.NAME:$server_name:g" > /etc/apache2/sites-available/scodoc-site-ssl.conf
+    echo 'activation du site...'
+    a2ensite scodoc-site-ssl
+
+    echo 'Remplacement du site Apache par defaut (sic ! old saved as .bak)'
+    fn=/etc/apache2/sites-available/000-default.conf
+    if [ -e $fn ]
+    then
+       mv $fn $fn.bak
+    fi
+    cp $SCODOC_DIR/config/etc/scodoc-site.orig $fn
+
+    if [ -z "$(grep Listen /etc/apache2/ports.conf | grep 443)" ]
+    then
+      echo 'adding port 443'
+      echo 'Listen 443' >> /etc/apache2/ports.conf
+    fi
+
+    echo 'configuring Apache proxy'
+    mv /etc/apache2/mods-available/proxy.conf /etc/apache2/mods-available/proxy.conf.bak
+    cat > /etc/apache2/mods-available/proxy.conf <<EOF
+<IfModule mod_proxy.c>
+# Proxy config for ScoDoc default installation
+ProxyRequests Off
+  <ProxyMatch http://localhost:8080>
+          Order deny,allow
+          Allow from all
+  </ProxyMatch>
+</IfModule>
+EOF
+
+fi
+
+systemctl restart apache2
+
+# ------------ CONFIG SERVICE SCODOC
+echo 
+echo "Installer le service scodoc permet de lancer automatiquement le serveur au demarrage."
+echo -n "Voulez vous installer le service scodoc ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    echo 'Installation du demarrage automatique de ScoDoc'
+    cp $SCODOC_DIR/config/etc/scodoc /etc/init.d/
+    update-rc.d scodoc defaults
+fi
+
+
+# ------------ CONFIG MISE A JOUR HEBDOMADAIRE
+echo
+echo -n "Mises a jour hebdomadaires (tres recommande) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    cp $SCODOC_DIR/config/etc/scodoc-updater.service /etc/systemd/system
+    cp $SCODOC_DIR/config/etc/scodoc-updater.timer /etc/systemd/system
+    systemctl enable scodoc-updater.timer
+    systemctl start scodoc-updater.timer
+fi
+
+# ------------ THE END
+echo
+echo "Installation terminee."
+echo
+echo "Vous pouvez maintenant creer la base d'utilisateurs avec ./create_user_db.sh"
+echo "puis creer un departement avec  ./create_dept.sh"
+echo "Ou bien restaurer vos donnees a partir d'une ancienne installation a l'aide du script restore_scodoc_data.sh"
+echo "(voir https://trac.lipn.univ-paris13.fr/projects/scodoc/wiki/MigrationDonneesScoDoc)"
+echo
+
+
+if [ "${cert_status}" != 0 ]
+then
+    echo "Attention: le serveur Web Apache n'a pas de certificat."
+    echo "Il est probable qu'il ne fonctionne pas."
+    echo "Installez vos certificats ou generez provisoirement des certificats autosignes"
+    echo "avec la commande: /usr/sbin/make-ssl-cert /usr/share/ssl-cert/ssleay.cnf $ssl_dir/apache.pem"
+    echo
+fi
+
diff --git a/config/install_debian7.sh b/config/install_debian7.sh
new file mode 100755
index 0000000000000000000000000000000000000000..aed314e34e2d955f1891ec25a066b0561b9efd17
--- /dev/null
+++ b/config/install_debian7.sh
@@ -0,0 +1,256 @@
+#!/bin/bash
+
+#
+# ScoDoc: install third-party software necessary for our installation
+# starting for a minimal Debian (Wheezy, 7.0) install.
+#
+# E. Viennet, Jun 2008, Apr 2009, Sept 2011, Sept 2013, Nov 2013
+#
+
+source config.sh
+source utils.sh
+
+check_uid_root $0
+
+PYTHON=/opt/zope213/bin/python
+
+# ------------ Safety checks
+if [ ${debian_version} != "7" ]
+then
+   echo "Version du systeme Linux Debian incompatible"
+   exit 1
+fi
+
+if [ $(arch) != "x86_64" ]
+then
+   echo "Version du systeme Linux Debian incompatible (pas X86 64 bits)"
+   exit 1
+fi
+
+# ------------ Permissions
+# source dir should be writable by scodoc to write bytecode files
+chgrp www-data $SCODOC_DIR $SCODOC_DIR/ZopeProducts/*
+chmod g+w $SCODOC_DIR $SCODOC_DIR/ZopeProducts/*
+
+chgrp -R www-data "${SCODOC_DIR}"/static/photos
+chmod -R g+w "${SCODOC_DIR}"/static/photos
+
+# ------------ LOCALES
+echo 
+echo '---- Configuration des locales...'
+echo
+
+if [ ! -e /etc/locale.gen ]
+then
+touch /etc/locale.gen
+fi
+
+
+for locname in en_US.UTF-8 en_US.ISO-8859-15 en_US.ISO-8859-1
+do
+  outname=$(echo ${locname//-/} | tr '[A-Z]' '[a-z]')
+  if [ $(locale -a | egrep -i ^${outname}$ | wc -l) -lt 1 ]
+  then
+    echo adding $locname
+    echo "$locname ${locname##*.}" >> /etc/locale.gen
+  fi
+done
+
+/usr/sbin/locale-gen --keep-existing 
+
+
+if [ "$LANG" != "en_US.UTF-8" ]
+then
+   # ceci est necessaire a cause de postgresql 8.3 qui 
+   # cree son cluster lors de l'install avec la locale par defaut !
+   echo "Attention: changement de la locale par defaut"
+   mv /etc/default/locale /etc/default/locale.orig
+   echo "LANG=\"en_US.UTF-8\"" > /etc/default/locale
+   export LANG=en_US.UTF-8
+fi
+echo 'Done.'
+
+
+# ------------ AJOUT DES PAQUETS NECESSAIRES
+apt-get update
+apt-get -y dist-upgrade
+apt-get -y install subversion curl cracklib-runtime firehol
+apt-get -y install apache2 ssl-cert 
+apt-get -y install postgresql-9.1 postgresql-client-9.1
+apt-get -y install graphviz
+
+# ------------ INSTALL DES EXTENSIONS PYTHON (2.7)
+
+apt-get -y install python-jaxml 
+apt-get -y install python-psycopg2 
+apt-get -y install python-pyrss2gen 
+apt-get -y install python-imaging python-reportlab 
+apt-get -y install python-cracklib # was python-crack
+apt-get -y install python-pyparsing
+apt-get -y install python-beautifulsoup
+
+apt-get -y install python-egenix-mxtools python-egenix-mxdatetime
+
+# Installe la version standard de pydot (XXX A TESTER)
+# apt-get -y install python-pydot
+# python-pydot is currently bugged in Debian 5: install our 0.9.10
+# Le probleme: pydot v > 1 a change l'API : resultat de get_node est une liste. Resolu par sco_utils.pydot_get_node
+# pydot 1.0.25 bug avec python 2.4 (get_node_list() renvoie toujours [])
+#       1.0.3 idem (voir misc/testpydot.py)
+echo '\nInstallation de pydot\n'
+apt-get -y remove python-pydot
+(cd /tmp; tar xfz $SCODOC_DIR/config/softs/pydot-0.9.10.tar.gz)
+(cd /tmp/pydot-0.9.10;  $PYTHON setup.py install)
+
+
+# UNUSED BY ScoDoc 7:
+# SOFTS="$SCODOC_DIR/config/softs"
+
+# ------------ Upgrade from svn
+(cd "${SCODOC_DIR}"; svn update)
+
+# ------------
+SVNVERSION=$(cd ..; svnversion)
+SVERSION=$(curl --silent http://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/version?mode=install\&svn=$SVNVERSION)
+echo $SVERSION > "${SCODOC_VERSION_DIR}/scodoc.sn"
+
+
+# ------------ PYEXCELERATOR
+echo
+echo 'Installation de pyExcelerator'
+echo
+
+(cd /tmp; tar xfz $SCODOC_DIR/config/softs/pyExcelerator-0.6.3a.patched.tgz)
+(cd /tmp/pyExcelerator-0.6.3a.patched; $PYTHON setup.py install)
+
+echo 'Done.'
+
+# ------------ POSTFIX
+echo 
+echo "ScoDoc a besoin de pouvoir envoyer des messages par mail."
+echo -n "Voulez vous configurer la messagerie (tres recommande) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    apt-get -y install postfix
+fi
+
+# ------------ CONFIG FIREWALL
+echo 
+echo "Le firewall aide a proteger votre serveur d'intrusions indesirables."
+echo -n "Voulez vous installer un firewall minimal ? (y/n) [n] "
+read ans
+if [ "$(norm_ans "$ans")" = 'Y' ]
+then
+    echo 'Installation du firewall IP (voir /etc/firehol/firehol.conf)'
+    echo "Attention: suppose que l'interface reseau vers Internet est eth0"
+    echo "  si ce n'est pas le cas, editer /etc/firehol/firehol.conf"
+    echo "  et relancer: /etc/init.d/firehol restart"
+    echo
+    cp $SCODOC_DIR/config/etc/firehol.conf /etc/firehol/
+    mv /etc/default/firehol /etc/default/firehol.orig
+    cat /etc/default/firehol.orig | sed 's/START_FIREHOL=NO/START_FIREHOL=YES/' > /tmp/firehol && mv /tmp/firehol /etc/default/firehol
+    # mise a jour RESERVED_IPS (le script fournit en Deb 7 get-iana est buggy)
+    echo yes |  $SCODOC_DIR/config/softs/get-iana.sh
+fi
+
+# Nota: after this point, the network may be unreachable 
+# (if firewall config is wrong)
+
+# ------------ CONFIG APACHE
+a2enmod ssl
+a2enmod proxy
+a2enmod proxy_http
+a2enmod rewrite
+
+echo 
+echo "La configuration du serveur web va modifier votre installation Apache pour supporter ScoDoc."
+echo -n "Voulez vous configurer le serveur web Apache maintenant ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    echo "Configuration d'Apache"
+    server_name=""
+    while [ -z $server_name ]
+    do
+        echo "Le nom de votre serveur doit normalement etre connu dans le DNS."
+	echo -n "Nom complet de votre serveur (exemple: notes.univ.fr): "
+	read server_name
+    done
+    # --- CERTIFICATS AUTO-SIGNES
+    echo 
+    echo "Il est possible d'utiliser des certificats cryptographiques"
+    echo "auto-signes, qui ne seront pas reconnus comme de confiance"
+    echo "par les navigateurs, mais offrent une certaine securite."
+    echo -n 'Voulez vous generer des certificats ssl auto-signes ? (y/n) [y] '
+    read ans
+    if [ "$(norm_ans "$ans")" != 'N' ]
+    then
+        # attention: utilise dans scodoc-site-ssl.orig
+	    ssl_dir=/etc/apache2/scodoc-ssl 
+    	if [ ! -e $ssl_dir ]
+	    then
+          mkdir $ssl_dir
+	    fi
+	    /usr/sbin/make-ssl-cert /usr/share/ssl-cert/ssleay.cnf $ssl_dir/apache.pem
+    fi
+    # ---
+    echo 'generation de /etc/apache2/sites-available/scodoc-site-ssl'
+    cat $SCODOC_DIR/config/etc/scodoc-site-ssl.orig | sed -e "s:YOUR\.FULL\.HOST\.NAME:$server_name:g" > /etc/apache2/sites-available/scodoc-site-ssl
+    echo 'activation du site...'
+    a2ensite scodoc-site-ssl
+
+    echo 'Remplacement du site Apache par defaut (sic ! old saved as .bak)'
+    fn=/etc/apache2/sites-available/default
+    if [ -e $fn ]
+    then
+       mv $fn $fn.bak
+    fi
+    cp $SCODOC_DIR/config/etc/scodoc-site.orig $fn
+
+    if [ -z "$(grep Listen /etc/apache2/ports.conf | grep 443)" ]
+    then
+      echo 'adding port 443'
+      echo 'Listen 443' >> /etc/apache2/ports.conf
+    fi
+
+    echo 'configuring Apache proxy'
+    mv /etc/apache2/mods-available/proxy.conf /etc/apache2/mods-available/proxy.conf.bak
+    cat > /etc/apache2/mods-available/proxy.conf <<EOF
+<IfModule mod_proxy.c>
+# Proxy config for ScoDoc default installation
+ProxyRequests Off
+  <ProxyMatch http://localhost:8080>
+          Order deny,allow
+          Allow from all
+  </ProxyMatch>
+</IfModule>
+EOF
+
+    /etc/init.d/apache2 restart
+fi
+
+
+# ------------ CONFIG SERVICE SCODOC
+echo 
+echo "Installer le service scodoc permet de lancer automatiquement le serveur au demarrage."
+echo -n "Voulez vous installer le service scodoc ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    echo 'Installation du demarrage automatique de ScoDoc'
+    cp $SCODOC_DIR/config/etc/scodoc /etc/init.d/
+    insserv scodoc
+fi
+
+# ------------ THE END
+echo
+echo "Installation terminee."
+echo
+echo "Vous pouvez maintenant creer la base d'utilisateurs avec ./create_user_db.sh"
+echo "puis creer un departement avec  ./create_dept.sh"
+echo "Ou bien restaurer vos donnees a partir d'une ancienne installation a l'aide du script restore_scodoc_data.sh"
+echo "(voir https://trac.lipn.univ-paris13.fr/projects/scodoc/wiki/MigrationVersScoDocSept)"
+echo
+
+
diff --git a/config/install_debian8.sh b/config/install_debian8.sh
new file mode 100755
index 0000000000000000000000000000000000000000..2741ceed7f82a1cb35bf891801abdcaae4ae3a81
--- /dev/null
+++ b/config/install_debian8.sh
@@ -0,0 +1,275 @@
+#!/bin/bash
+
+#
+# ScoDoc: install third-party software necessary for our installation
+# starting for a minimal Debian (Jessie, 8.0) install.
+#
+# E. Viennet, Jun 2008, Apr 2009, Sept 2011, Sept 2013, Nov 2013, Mar 2017
+#
+
+source config.sh
+source utils.sh
+
+check_uid_root $0
+
+PYTHON=/opt/zope213/bin/python
+
+# ------------ Safety checks
+if [ ${debian_version} != "8" ]
+then
+   echo "Version du systeme Linux Debian incompatible"
+   exit 1
+fi
+
+if [ $(arch) != "x86_64" ]
+then
+   echo "Version du systeme Linux Debian incompatible (pas X86 64 bits)"
+   exit 1
+fi
+
+# ------------ Permissions
+# source dir should be writable by scodoc to write bytecode files
+chgrp www-data $SCODOC_DIR $SCODOC_DIR/ZopeProducts/*
+chmod g+w $SCODOC_DIR $SCODOC_DIR/ZopeProducts/*
+
+chgrp -R www-data "${SCODOC_DIR}"/static/photos
+chmod -R g+w "${SCODOC_DIR}"/static/photos
+
+# ------------ LOCALES
+echo 
+echo '---- Configuration des locales...'
+echo
+
+if [ ! -e /etc/locale.gen ]
+then
+touch /etc/locale.gen
+fi
+
+
+for locname in en_US.UTF-8 en_US.ISO-8859-15 en_US.ISO-8859-1
+do
+  outname=$(echo ${locname//-/} | tr '[A-Z]' '[a-z]')
+  if [ $(locale -a | egrep -i ^${outname}$ | wc -l) -lt 1 ]
+  then
+    echo adding $locname
+    echo "$locname ${locname##*.}" >> /etc/locale.gen
+  fi
+done
+
+/usr/sbin/locale-gen --keep-existing 
+
+
+if [ "$LANG" != "en_US.UTF-8" ]
+then
+   # ceci est necessaire a cause de postgresql 8.3 qui 
+   # cree son cluster lors de l'install avec la locale par defaut !
+   echo "Attention: changement de la locale par defaut"
+   mv /etc/default/locale /etc/default/locale.orig
+   echo "LANG=\"en_US.UTF-8\"" > /etc/default/locale
+   export LANG=en_US.UTF-8
+fi
+echo 'Done.'
+
+# ------------ FIX pour passage Debien 7 -> Debian 8
+chsh -s /bin/sh www-data
+
+# ------------ AJOUT DES PAQUETS NECESSAIRES
+apt-get update
+apt-get -y install subversion curl cracklib-runtime firehol
+apt-get -y install apache2 ssl-cert 
+apt-get -y install postgresql-9.4 postgresql-client-9.4
+apt-get -y install graphviz
+
+# ------------ INSTALL DES EXTENSIONS PYTHON (2.7)
+
+apt-get -y install python-jaxml 
+apt-get -y install python-psycopg2 
+apt-get -y install python-pyrss2gen 
+apt-get -y install python-imaging python-reportlab 
+apt-get -y install python-cracklib # was python-crack
+#apt-get -y install python-pyparsing
+apt-get -y install python-beautifulsoup
+
+apt-get -y install python-egenix-mxtools python-egenix-mxdatetime
+
+# pydot est un probleme: todo: rececrire l'affichae du graphe sans pydot
+# ou contribuer une versions ans bug de pydot
+# ou encore simplement eviter d'utiliser graph_from_edges ?
+#
+# python-pydot is currently bugged in Debian 5, 6, 7, 8: install our 0.9.10
+# Le probleme: pydot v > 1 a change l'API : resultat de get_node est une liste. Resolu par sco_utils.pydot_get_node
+# pydot 1.0.25 bug avec python 2.4 (get_node_list() renvoie toujours [])
+#       1.0.3 idem (voir misc/testpydot.py)
+#       1.2.4 / python2.7: bug graph_from_edges toujours là 
+#       voir https://bugs.launchpad.net/ubuntu/+source/pydot/+bug/490015
+echo '\nInstallation de pydot\n'
+
+apt-get -y remove python-pydot
+# Version ancienne de pyparsing requise par pydot
+apt-get -y remove python-pyparsing
+/opt/zope213/bin/pip install pyparsing==1.5.7
+(cd /tmp; tar xfz $SCODOC_DIR/config/softs/pydot-0.9.10.tar.gz)
+(cd /tmp/pydot-0.9.10;  $PYTHON setup.py install)
+
+
+# UNUSED BY ScoDoc 7:
+# SOFTS="$SCODOC_DIR/config/softs"
+
+# ------------
+SVNVERSION=$(cd ..; svnversion)
+SVERSION=$(curl --silent http://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/version?mode=install\&svn=$SVNVERSION)
+echo $SVERSION > "${SCODOC_VERSION_DIR}/scodoc.sn"
+
+
+# ------------ PYEXCELERATOR
+# inutile en Debian 8 car dans l'image ScoDoc
+#echo
+#echo 'Installation de pyExcelerator'
+#echo
+#(cd /tmp; tar xfz $SCODOC_DIR/config/softs/pyExcelerator-0.6.3a.patched.tgz)
+#(cd /tmp/pyExcelerator-0.6.3a.patched; $PYTHON setup.py install)
+#echo 'Done.'
+
+# ------------ POSTFIX
+echo 
+echo "ScoDoc a besoin de pouvoir envoyer des messages par mail."
+echo -n "Voulez vous configurer la messagerie (tres recommande) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    apt-get -y install postfix
+fi
+
+# ------------ CONFIG FIREWALL
+echo 
+echo "Le firewall aide a proteger votre serveur d'intrusions indesirables."
+echo -n "Voulez vous installer un firewall minimal ? (y/n) [n] "
+read ans
+if [ "$(norm_ans "$ans")" = 'Y' ]
+then
+    network_interface="eth0"
+    echo 'Installation du firewall IP (voir /etc/firehol/firehol.conf)'
+    echo "Attention: suppose que l'interface reseau vers Internet est eth0"
+    echo "  si ce n'est pas le cas, editer /etc/firehol/firehol.conf"
+    echo "  et relancer: /etc/init.d/firehol restart"
+    echo
+    cat $SCODOC_DIR/config/etc/firehol.conf | sed "s/XXX_INTERFACE_XXX/${network_interface}/" > /etc/firehol/firehol.conf
+    mv /etc/default/firehol /etc/default/firehol.orig
+    cat /etc/default/firehol.orig | sed 's/START_FIREHOL=NO/START_FIREHOL=YES/' > /tmp/firehol && mv /tmp/firehol /etc/default/firehol
+    # mise a jour RESERVED_IPS (le script fournit en Deb 7 get-iana est buggy)
+    echo yes |  $SCODOC_DIR/config/softs/get-iana.sh
+fi
+
+# Nota: after this point, the network may be unreachable 
+# (if firewall config is wrong)
+
+# ------------ CONFIG APACHE
+a2enmod ssl
+a2enmod proxy
+a2enmod proxy_http
+a2enmod rewrite
+
+echo 
+echo "La configuration du serveur web va modifier votre installation Apache pour supporter ScoDoc."
+echo -n "Voulez vous configurer le serveur web Apache maintenant (tres conseille) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    echo "Configuration d'Apache"
+    server_name=""
+    while [ -z $server_name ]
+    do
+        echo "Le nom de votre serveur doit normalement etre connu dans le DNS."
+	echo -n "Nom complet de votre serveur (exemple: notes.univ.fr): "
+	read server_name
+    done
+    # --- CERTIFICATS AUTO-SIGNES
+    echo 
+    echo "Il est possible d'utiliser des certificats cryptographiques"
+    echo "auto-signes, qui ne seront pas reconnus comme de confiance"
+    echo "par les navigateurs, mais offrent une certaine securite."
+    echo -n 'Voulez vous generer des certificats ssl auto-signes ? (y/n) [y] '
+    read ans
+    if [ "$(norm_ans "$ans")" != 'N' ]
+    then
+        # attention: utilise dans scodoc-site-ssl.orig
+	    ssl_dir=/etc/apache2/scodoc-ssl 
+    	if [ ! -e $ssl_dir ]
+	    then
+          mkdir $ssl_dir
+	    fi
+	    /usr/sbin/make-ssl-cert /usr/share/ssl-cert/ssleay.cnf $ssl_dir/apache.pem
+        cert_status=$?
+    else
+        cert_status=-1
+    fi
+    # ---
+    echo 'generation de /etc/apache2/sites-available/scodoc-site-ssl'
+    cat $SCODOC_DIR/config/etc/scodoc-site-ssl-apache2.4.orig | sed -e "s:YOUR\.FULL\.HOST\.NAME:$server_name:g" > /etc/apache2/sites-available/scodoc-site-ssl.conf
+    echo 'activation du site...'
+    a2ensite scodoc-site-ssl
+
+    echo 'Remplacement du site Apache par defaut (sic ! old saved as .bak)'
+    fn=/etc/apache2/sites-available/000-default.conf
+    if [ -e $fn ]
+    then
+       mv $fn $fn.bak
+    fi
+    cp $SCODOC_DIR/config/etc/scodoc-site.orig $fn
+
+    if [ -z "$(grep Listen /etc/apache2/ports.conf | grep 443)" ]
+    then
+      echo 'adding port 443'
+      echo 'Listen 443' >> /etc/apache2/ports.conf
+    fi
+
+    echo 'configuring Apache proxy'
+    mv /etc/apache2/mods-available/proxy.conf /etc/apache2/mods-available/proxy.conf.bak
+    cat > /etc/apache2/mods-available/proxy.conf <<EOF
+<IfModule mod_proxy.c>
+# Proxy config for ScoDoc default installation
+ProxyRequests Off
+  <ProxyMatch http://localhost:8080>
+          Order deny,allow
+          Allow from all
+  </ProxyMatch>
+</IfModule>
+EOF
+
+fi
+
+service apache2 restart
+
+
+# ------------ CONFIG SERVICE SCODOC
+echo 
+echo "Installer le service scodoc permet de lancer automatiquement le serveur au demarrage."
+echo -n "Voulez vous installer le service scodoc ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    echo 'Installation du demarrage automatique de ScoDoc'
+    cp $SCODOC_DIR/config/etc/scodoc /etc/init.d/
+    insserv scodoc
+fi
+
+# ------------ THE END
+echo
+echo "Installation terminee."
+echo
+echo "Vous pouvez maintenant creer la base d'utilisateurs avec ./create_user_db.sh"
+echo "puis creer un departement avec  ./create_dept.sh"
+echo "Ou bien restaurer vos donnees a partir d'une ancienne installation a l'aide du script restore_scodoc_data.sh"
+echo "(voir https://trac.lipn.univ-paris13.fr/projects/scodoc/wiki/MigrationDonneesScoDoc)"
+echo
+
+if [ "${cert_status}" != 0 ]
+then
+    echo "Attention: le serveur Web Apache n'a pas de certificat."
+    echo "Il est probable qu'il ne fonctionne pas."
+    echo "Installez vos certificats ou generez provisoirement des certificats autosignes"
+    echo "avec la commande: /usr/sbin/make-ssl-cert /usr/share/ssl-cert/ssleay.cnf $ssl_dir/apache.pem"
+    echo
+fi
+
+
diff --git a/config/install_debian9.sh b/config/install_debian9.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0e54bd14efc684bf25e9dac7ecb5598875548c19
--- /dev/null
+++ b/config/install_debian9.sh
@@ -0,0 +1,302 @@
+#!/bin/bash
+
+#
+# ScoDoc: install third-party software necessary for our installation
+# starting for a minimal Debian (Stretch, 9.0) install.
+#
+# E. Viennet, Jun 2008, Apr 2009, Sept 2011, Sept 2013, Nov 2013, Mar 2017, Jul 2017, Jun 2019
+#
+
+source config.sh
+source utils.sh
+
+check_uid_root $0
+
+PYTHON=/opt/zope213/bin/python
+
+# ------------ Safety checks
+if [ ${debian_version} != "9" ]
+then
+   echo "Version du systeme Linux Debian incompatible"
+   exit 1
+fi
+
+if [ $(arch) != "x86_64" ]
+then
+   echo "Version du systeme Linux Debian incompatible (pas X86 64 bits)"
+   exit 1
+fi
+
+# ------------ Permissions & directories
+# source dir should be writable by scodoc to write bytecode files
+chgrp www-data $SCODOC_DIR $SCODOC_DIR/ZopeProducts/*
+chmod g+w $SCODOC_DIR $SCODOC_DIR/ZopeProducts/*
+
+chgrp -R www-data "${SCODOC_DIR}"/static/photos
+chmod -R g+w "${SCODOC_DIR}"/static/photos
+
+if [ ! -e "${SCODOC_VERSION_DIR}" ]; then
+  mkdir "${SCODOC_VERSION_DIR}"
+  chown www-data.www-data "${SCODOC_VERSION_DIR}"
+fi
+
+# ------------ LOCALES
+echo 
+echo '---- Configuration des locales...'
+echo
+
+if [ ! -e /etc/locale.gen ]
+then
+touch /etc/locale.gen
+fi
+
+
+for locname in en_US.UTF-8 en_US.ISO-8859-15 en_US.ISO-8859-1
+do
+  outname=$(echo ${locname//-/} | tr '[A-Z]' '[a-z]')
+  if [ $(locale -a | egrep -i ^${outname}$ | wc -l) -lt 1 ]
+  then
+    echo adding $locname
+    echo "$locname ${locname##*.}" >> /etc/locale.gen
+  fi
+done
+
+/usr/sbin/locale-gen --keep-existing 
+
+
+if [ "$LANG" != "en_US.UTF-8" ]
+then
+   # ceci est necessaire a cause de postgresql 8.3 qui 
+   # cree son cluster lors de l'install avec la locale par defaut !
+   echo "Attention: changement de la locale par defaut"
+   mv /etc/default/locale /etc/default/locale.orig
+   echo "LANG=\"en_US.UTF-8\"" > /etc/default/locale
+   export LANG=en_US.UTF-8
+fi
+echo 'Done.'
+
+# ------------ FIX pour passage Debian 7 -> Debian 8
+chsh -s /bin/sh www-data
+
+# ------------ AJOUT DES PAQUETS NECESSAIRES
+apt-get update
+apt-get -y install subversion curl cracklib-runtime
+apt-get -y install apache2 ssl-cert 
+apt-get -y install firehol
+apt-get -y install insserv
+apt-get -y install postgresql-9.6 postgresql-client-9.6
+apt-get -y install graphviz
+
+# ------------ INSTALL DES EXTENSIONS PYTHON (2.7)
+
+apt-get -y install python-docutils
+apt-get -y install python-jaxml 
+apt-get -y install python-psycopg2 
+apt-get -y install python-pyrss2gen 
+apt-get -y install python-imaging python-reportlab 
+apt-get -y install python-cracklib # was python-crack
+#apt-get -y install python-pyparsing
+apt-get -y install python-beautifulsoup
+
+apt-get -y install python-egenix-mxtools python-egenix-mxdatetime
+
+# pydot est un probleme: todo: rececrire l'affichage du graphe sans pydot
+# ou contribuer une versions sans bug de pydot
+# ou encore simplement eviter d'utiliser graph_from_edges ?
+#
+# python-pydot is currently bugged in Debian 5, 6, 7, 8: install our 0.9.10
+# Le probleme: pydot v > 1 a change l'API : resultat de get_node est une liste. Resolu par sco_utils.pydot_get_node
+# pydot 1.0.25 bug avec python 2.4 (get_node_list() renvoie toujours [])
+#       1.0.3 idem (voir misc/testpydot.py)
+#       1.2.4 / python2.7: bug graph_from_edges toujours là 
+#       voir https://bugs.launchpad.net/ubuntu/+source/pydot/+bug/490015
+echo '\nInstallation de pydot\n'
+
+apt-get -y remove python-pydot
+# Version ancienne de pyparsing requise par pydot
+apt-get -y remove python-pyparsing
+/opt/zope213/bin/pip install pyparsing==1.5.7
+(cd /tmp; tar xfz $SCODOC_DIR/config/softs/pydot-0.9.10.tar.gz)
+(cd /tmp/pydot-0.9.10;  $PYTHON setup.py install)
+
+# Fix dateutil:
+/opt/zope213/bin/pip install python-dateutil --upgrade
+
+# UNUSED BY ScoDoc 7:
+# SOFTS="$SCODOC_DIR/config/softs"
+
+# ------------
+SVNVERSION=$(cd ..; svnversion)
+SVERSION=$(curl --silent http://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/version?mode=install\&svn=$SVNVERSION)
+echo $SVERSION > "${SCODOC_VERSION_DIR}/scodoc.sn"
+
+
+# ------------ PYEXCELERATOR
+# inutile en Debian 8 car dans l'image ScoDoc
+#echo
+#echo 'Installation de pyExcelerator'
+#echo
+#(cd /tmp; tar xfz $SCODOC_DIR/config/softs/pyExcelerator-0.6.3a.patched.tgz)
+#(cd /tmp/pyExcelerator-0.6.3a.patched; $PYTHON setup.py install)
+#echo 'Done.'
+
+# ------------ POSTFIX
+echo 
+echo "ScoDoc a besoin de pouvoir envoyer des messages par mail."
+echo -n "Voulez vous configurer la messagerie (tres recommande) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    apt-get -y install postfix
+fi
+
+# ------------ CONFIG FIREWALL
+echo 
+echo "Le firewall aide a proteger votre serveur d'intrusions indesirables."
+echo -n "Voulez vous installer un firewall minimal (firehol) ? (y/n) [n] "
+read ans
+if [ "$(norm_ans "$ans")" = 'Y' ]
+then
+    echo 'Installation du firewall IP (voir /etc/firehol/firehol.conf)'
+    echo "Debian 9 n'utilise plus eth0 mais des noms fixes, comme enp0s3"
+    echo "Vos interfaces reseau actuelles sont:"
+    ip a
+    echo -n "Nom de votre interface reseau vers Internet ? "
+    read network_interface    
+    echo "Attention: configure l'interface reseau vers Internet  ${network_interface}"
+    echo "  si ce n'est pas le cas, editer /etc/firehol/firehol.conf"
+    echo "  et relancer: /etc/init.d/firehol restart"
+    echo
+    cat $SCODOC_DIR/config/etc/firehol.conf | sed "s/XXX_INTERFACE_XXX/${network_interface}/" > /etc/firehol/firehol.conf
+    mv /etc/default/firehol /etc/default/firehol.orig
+    cat /etc/default/firehol.orig | sed 's/START_FIREHOL=NO/START_FIREHOL=YES/' > /tmp/firehol && mv /tmp/firehol /etc/default/firehol
+    # mise a jour RESERVED_IPS (le script fournit en Deb 7 get-iana est buggy)
+    echo yes |  $SCODOC_DIR/config/softs/get-iana.sh
+fi
+
+# Nota: after this point, the network may be unreachable 
+# (if firewall config is wrong)
+
+# ------------ CONFIG APACHE
+a2enmod ssl
+a2enmod proxy
+a2enmod proxy_http
+a2enmod rewrite
+
+echo 
+echo "La configuration du serveur web va modifier votre installation Apache pour supporter ScoDoc."
+echo -n "Voulez vous configurer le serveur web Apache maintenant (tres conseille) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    echo "Configuration d'Apache"
+    server_name=""
+    while [ -z $server_name ]
+    do
+        echo "Le nom de votre serveur doit normalement etre connu dans le DNS."
+	echo -n "Nom complet de votre serveur (exemple: notes.univ.fr): "
+	read server_name
+    done
+    # --- CERTIFICATS AUTO-SIGNES
+    echo 
+    echo "Il est possible d'utiliser des certificats cryptographiques"
+    echo "auto-signes, qui ne seront pas reconnus comme de confiance"
+    echo "par les navigateurs, mais offrent une certaine securite."
+    echo -n 'Voulez vous generer des certificats ssl auto-signes ? (y/n) [y] '
+    read ans
+    if [ "$(norm_ans "$ans")" != 'N' ]
+    then
+        # attention: utilise dans scodoc-site-ssl.orig
+	    ssl_dir=/etc/apache2/scodoc-ssl 
+    	if [ ! -e $ssl_dir ]
+	    then
+          mkdir $ssl_dir
+	    fi
+	    /usr/sbin/make-ssl-cert /usr/share/ssl-cert/ssleay.cnf $ssl_dir/apache.pem
+        cert_status=$?
+    else
+        cert_status=-1
+    fi
+    # ---
+    echo 'generation de /etc/apache2/sites-available/scodoc-site-ssl'
+    cat $SCODOC_DIR/config/etc/scodoc-site-ssl-apache2.4.orig | sed -e "s:YOUR\.FULL\.HOST\.NAME:$server_name:g" > /etc/apache2/sites-available/scodoc-site-ssl.conf
+    echo 'activation du site...'
+    a2ensite scodoc-site-ssl
+
+    echo 'Remplacement du site Apache par defaut (sic ! old saved as .bak)'
+    fn=/etc/apache2/sites-available/000-default.conf
+    if [ -e $fn ]
+    then
+       mv $fn $fn.bak
+    fi
+    cp $SCODOC_DIR/config/etc/scodoc-site.orig $fn
+
+    if [ -z "$(grep Listen /etc/apache2/ports.conf | grep 443)" ]
+    then
+      echo 'adding port 443'
+      echo 'Listen 443' >> /etc/apache2/ports.conf
+    fi
+
+    echo 'configuring Apache proxy'
+    mv /etc/apache2/mods-available/proxy.conf /etc/apache2/mods-available/proxy.conf.bak
+    cat > /etc/apache2/mods-available/proxy.conf <<EOF
+<IfModule mod_proxy.c>
+# Proxy config for ScoDoc default installation
+ProxyRequests Off
+  <ProxyMatch http://localhost:8080>
+          Order deny,allow
+          Allow from all
+  </ProxyMatch>
+</IfModule>
+EOF
+
+fi
+
+service apache2 restart
+
+
+# ------------ CONFIG SERVICE SCODOC
+echo 
+echo "Installer le service scodoc permet de lancer automatiquement le serveur au demarrage."
+echo -n "Voulez vous installer le service scodoc ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    echo 'Installation du demarrage automatique de ScoDoc'
+    cp $SCODOC_DIR/config/etc/scodoc /etc/init.d/
+    insserv scodoc
+fi
+
+
+# ------------ CONFIG MISE A JOUR HEBDOMADAIRE
+echo
+echo -n "Mises a jour hebdomadaires (tres recommande) ? (y/n) [y] "
+read ans
+if [ "$(norm_ans "$ans")" != 'N' ]
+then
+    cp $SCODOC_DIR/config/etc/scodoc-updater.service /etc/systemd/system
+    cp $SCODOC_DIR/config/etc/scodoc-updater.timer /etc/systemd/system
+    systemctl enable scodoc-updater.timer
+    systemctl start scodoc-updater.timer
+fi
+
+# ------------ THE END
+echo
+echo "Installation terminee."
+echo
+echo "Vous pouvez maintenant creer la base d'utilisateurs avec ./create_user_db.sh"
+echo "puis creer un departement avec  ./create_dept.sh"
+echo "Ou bien restaurer vos donnees a partir d'une ancienne installation a l'aide du script restore_scodoc_data.sh"
+echo "(voir https://trac.lipn.univ-paris13.fr/projects/scodoc/wiki/MigrationDonneesScoDoc)"
+echo
+
+
+if [ "${cert_status}" != 0 ]
+then
+    echo "Attention: le serveur Web Apache n'a pas de certificat."
+    echo "Il est probable qu'il ne fonctionne pas."
+    echo "Installez vos certificats ou generez provisoirement des certificats autosignes"
+    echo "avec la commande: /usr/sbin/make-ssl-cert /usr/share/ssl-cert/ssleay.cnf $ssl_dir/apache.pem"
+    echo
+fi
+
diff --git a/config/postupgrade-db.py b/config/postupgrade-db.py
new file mode 100755
index 0000000000000000000000000000000000000000..b6d930d732857983426099facbecd3196458531a
--- /dev/null
+++ b/config/postupgrade-db.py
@@ -0,0 +1,650 @@
+#!/opt/zope213/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+ScoDoc post-upgrade script: databases housekeeping
+
+This script is runned by upgrade.sh after each SVN update.
+
+Runned as "www-data" with Zope shutted down and postgresql up.
+
+
+Useful to update database schema (eg add new tables or columns to 
+existing scodoc instances).
+
+E. Viennet, june 2008, sept 2013
+"""
+
+from scodocutils import *
+
+for dept in get_depts():
+    log('\nChecking database for dept %s' % dept)
+    cnx_string = None
+    try:
+        cnx_string = get_dept_cnx_str(dept)
+        cnx = psycopg2.connect( cnx_string )
+    except:
+        log('\n*** Error: departement %s not upgraded ! ***\n' % dept)
+        log('connexion string was "%s"' % cnx_string) 
+        traceback.print_exc()
+        continue
+    cnx.set_session(autocommit=False)
+    cursor = cnx.cursor()
+    # Apply upgrades:
+    
+    # SVN 564 -> 565
+    # add resp_can_edit to notes_formsemestre:
+    check_field(cnx, 'notes_formsemestre', 'resp_can_edit',
+                ['alter table notes_formsemestre add column resp_can_edit int default 0',
+                 'update notes_formsemestre set resp_can_edit=0'])
+
+    # SVN 580 -> 581
+    # add resp_can_change_ens to notes_formsemestre:
+    check_field(cnx, 'notes_formsemestre', 'resp_can_change_ens',
+                ['alter table notes_formsemestre add column resp_can_change_ens int default 1',
+                 'update notes_formsemestre set resp_can_change_ens=1'])
+
+    # Fix bug ayant empeche la creation de la table
+    check_table( cnx, 'admissions', [ 
+    """CREATE TABLE admissions (
+    adm_id text DEFAULT notes_newid_etud('ADM'::text) NOT NULL,
+    etudid text NOT NULL,
+    annee integer,
+    bac text,
+    specialite text,
+    annee_bac integer,
+    math real,
+    physique real,
+    anglais real,
+    francais real,
+    rang integer, -- dans les voeux du candidat (inconnu avec APB)
+    qualite real,
+    rapporteur text,
+    decision text,
+    score real,
+    commentaire text,
+    nomlycee text,
+    villelycee text,
+    codepostallycee text,
+    codelycee text,
+    debouche text, -- situation APRES etre passe par chez nous (texte libre)
+    type_admission text, -- 'APB', 'APC-PC', 'CEF', 'Direct', '?' (autre)
+    boursier_prec integer default NULL, -- etait boursier dans le cycle precedent (lycee) ?
+    classement integer default NULL, -- classement par le jury d'admission (1 à N), global (pas celui d'APB si il y a des groupes)
+    apb_groupe text, -- code du groupe APB
+    apb_classement_gr integer default NULL -- classement (1..Ngr) par le jury dans le groupe APB
+) WITH OIDS;
+"""] )
+    
+    # SVN 651
+    # Nouvelles donnees d'admission
+    check_field(cnx, 'admissions', 'codelycee',
+                ['alter table admissions add column codelycee text',
+                 ])
+    check_field(cnx, 'admissions', 'codepostallycee',
+                ['alter table admissions add column codepostallycee text',
+                 ])
+    
+    # New preferences system
+    check_field(cnx, 'sco_prefs', 'formsemestre_id',
+                ["alter table sco_prefs add column pref_id text DEFAULT notes_newid('PREF'::text) UNIQUE NOT NULL",
+                 "update sco_prefs set pref_id=oid",
+                 "alter table sco_prefs add column formsemestre_id text default NULL",
+                 "alter table sco_prefs drop CONSTRAINT sco_prefs_pkey",
+                 "alter table sco_prefs add unique( name, formsemestre_id)",
+                 # copie anciennes prefs:
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'left_margin', left_margin, formsemestre_id from notes_formsemestre_pagebulletin",
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'top_margin', top_margin, formsemestre_id from notes_formsemestre_pagebulletin",
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'right_margin', right_margin, formsemestre_id from notes_formsemestre_pagebulletin",
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'bottom_margin', bottom_margin, formsemestre_id from notes_formsemestre_pagebulletin",
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'bul_title', title, formsemestre_id from notes_formsemestre_pagebulletin",
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'bul_intro_mail', intro_mail, formsemestre_id from notes_formsemestre_pagebulletin",
+                 "drop table notes_formsemestre_pagebulletin",
+                 # anciens champs de formsemestre:
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'bul_show_abs', gestion_absence, formsemestre_id from notes_formsemestre",
+                 "alter table notes_formsemestre drop column gestion_absence",
+                 
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'bul_show_decision', bul_show_decision, formsemestre_id from notes_formsemestre",
+                 "alter table notes_formsemestre drop column bul_show_decision",
+                 
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'bul_show_uevalid', bul_show_uevalid, formsemestre_id from notes_formsemestre",
+                 "alter table notes_formsemestre drop column bul_show_uevalid",
+                 
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'bul_show_codemodules', bul_show_codemodules, formsemestre_id from notes_formsemestre",
+                 "alter table notes_formsemestre drop column bul_show_codemodules",
+
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'bul_show_rangs', bul_show_rangs, formsemestre_id from notes_formsemestre",
+                 "alter table notes_formsemestre drop column bul_show_rangs",
+                 
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'bul_show_ue_rangs', bul_show_ue_rangs, formsemestre_id from notes_formsemestre",
+                 "alter table notes_formsemestre drop column bul_show_ue_rangs",
+                 
+                 "insert into sco_prefs (name, value, formsemestre_id) select 'bul_show_mod_rangs', bul_show_mod_rangs, formsemestre_id from notes_formsemestre",
+                 "alter table notes_formsemestre drop column bul_show_mod_rangs",
+                 ])
+    # fixed previous bug (misspelled pref)
+    cursor.execute("update sco_prefs set name = 'bul_show_codemodules' where name = 'bul_showcodemodules'")
+
+    # billets d'absences
+    if not sequence_exists(cnx, 'notes_idgen_billets'):
+        log('creating sequence notes_idgen_billets')
+        cursor.execute('CREATE SEQUENCE notes_idgen_billets;')
+    
+    if not function_exists(cnx, 'notes_newid_billet'):
+        log('creating function notes_newid_billet')
+        cursor.execute("""CREATE FUNCTION notes_newid_billet( text ) returns text as '
+	select $1 || to_char(  nextval(''notes_idgen_billets''), ''FM999999999'' ) 
+	as result;
+	' language SQL;""")
+    
+    check_table( cnx, 'billet_absence', [            
+            """CREATE TABLE billet_absence (
+    billet_id text DEFAULT notes_newid_billet('B'::text) NOT NULL,
+    etudid text NOT NULL,
+    abs_begin timestamp with time zone,
+    abs_end  timestamp with time zone,
+    description text, -- "raison" de l'absence
+    etat integer default 0 -- 0 new, 1 processed    
+) WITH OIDS;
+"""] )
+    
+    # description absence
+    check_field(cnx, 'absences', 'description',
+                ['alter table absences add column description text'
+                 ])
+    check_field(cnx, 'absences', 'entry_date',
+                ['alter table absences add column entry_date timestamp with time zone DEFAULT now()'
+                 ])
+    check_field(cnx, 'billet_absence', 'entry_date',
+                ['alter table billet_absence add column entry_date timestamp with time zone DEFAULT now()'
+                 ])
+    # Nouvelles preferences pour bulletins PDF: migre bul_show_chiefDept
+    cursor.execute("update sco_prefs set name = 'bul_show_sig_right' where name = 'bul_show_chiefDept'")
+    # cursor.execute("insert into sco_prefs (name, value, formsemestre_id) select 'bul_show_sig_left', value, formsemestre_id from sco_prefs where name = 'bul_show_sig_right'")
+    # date et lieu naissance (pour IFAG Sofia)
+    check_field(cnx, 'identite', 'date_naissance',
+                ['alter table identite add column date_naissance date',
+                 "update identite set date_naissance=to_date(to_char( annee_naissance, 'FM9999') || '-01-01', 'YYYY-MM-DD')",
+                 'alter table identite drop column annee_naissance'
+                 ])
+    check_field(cnx, 'identite', 'lieu_naissance',
+                ['alter table identite add column lieu_naissance text'
+                 ])
+    # justification billets:
+    check_field(cnx, 'billet_absence', 'justified', 
+                [ 'alter table billet_absence add column justified integer default 0',
+                  'update billet_absence set justified=0'
+                  ])
+    
+    # ----------------------- New groups
+    # 1- Create new tables
+    check_table( cnx, 'partition', [
+            """CREATE TABLE partition(
+       partition_id text default notes_newid2('P') PRIMARY KEY,
+       formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id),
+       partition_name text, -- "TD", "TP", ...
+       compute_ranks integer default 1, -- calcul rang etudiants dans les groupes
+       numero SERIAL, -- ordre de presentation
+       UNIQUE(formsemestre_id,partition_name)
+) WITH OIDS;
+"""] )
+    check_table( cnx, 'group_descr', [
+            """CREATE TABLE group_descr (
+       group_id text default notes_newid2('G') PRIMARY KEY,
+       partition_id text REFERENCES partition(partition_id),
+       group_name text, -- "A", "C2", ...
+       UNIQUE(partition_id, group_name)     
+) WITH OIDS;
+"""] )
+    check_table( cnx, 'group_membership', [
+            """CREATE TABLE group_membership(
+       group_membership_id text default notes_newid2('GM') PRIMARY KEY,
+       etudid text REFERENCES identite(etudid),       
+       group_id text REFERENCES group_descr(group_id),
+       UNIQUE(etudid, group_id)
+) WITH OIDS;
+"""] )
+
+    # 2- For each sem, create 1 to 4 partitions: all, TD (if any), TP, TA
+    # Here we have to deal with plain SQL, nasty...
+    if field_exists(cnx, 'notes_formsemestre_inscription', 'groupetd'):
+        # Some very old stduents didn't have addresses: it's now mandatory
+        cursor.execute("insert into adresse (etudid) select etudid from identite i except select etudid from adresse")
+        #
+        cursor.execute("SELECT formsemestre_id from notes_formsemestre")
+        formsemestre_ids = [ x[0] for x in cursor.fetchall() ]
+        for formsemestre_id in formsemestre_ids:
+            # create "all" partition (with empty name)
+            cursor.execute("INSERT into partition (formsemestre_id, compute_ranks) VALUES (%(formsemestre_id)s, 1)", {'formsemestre_id' : formsemestre_id } )
+            cursor.execute("select partition_id from partition where oid=%(oid)s", { 'oid' : cursor.lastoid() })
+            partition_id = cursor.fetchone()[0]
+            # create group "all" (without name)
+            cursor.execute("INSERT into group_descr (partition_id) VALUES (%(pid)s)", { 'pid' : partition_id } )
+            cursor.execute("SELECT group_id from group_descr where oid=%(oid)s", { 'oid' : cursor.lastoid() })
+            group_id = cursor.fetchone()[0]
+            # inscrit etudiants:
+            cursor.execute("INSERT into group_membership (etudid, group_id) SELECT etudid, %(group_id)s from notes_formsemestre_inscription where formsemestre_id=%(formsemestre_id)s", { 'group_id' : group_id, 'formsemestre_id' : formsemestre_id } )
+            
+            # create TD, TP, TA
+            cursor.execute("SELECT distinct(groupetd) from notes_formsemestre_inscription where formsemestre_id=%(formsemestre_id)s", { 'formsemestre_id' : formsemestre_id } )
+            groupetds = [ x[0] for x in cursor.fetchall() if x[0] ]
+            if len(groupetds) > 1 or (len(groupetds)==1 and groupetds[0] != 'A'):
+                # TD : create partition
+                cursor.execute("SELECT * from notes_formsemestre where formsemestre_id=%(formsemestre_id)s", { 'formsemestre_id' : formsemestre_id } )
+                nomgroupetd = cursor.dictfetchone()['nomgroupetd']
+                if not nomgroupetd: # pas de nom ??? on invente un nom stupide et unique
+                    nomgroupetd = 'TD_'+str(time.time()).replace('.','')[-3:]
+                cursor.execute("INSERT into partition (formsemestre_id, partition_name) VALUES (%(formsemestre_id)s,%(nomgroupetd)s)", { 'formsemestre_id' : formsemestre_id, 'nomgroupetd' : nomgroupetd } )
+                cursor.execute("select partition_id from partition where oid=%(oid)s", { 'oid' : cursor.lastoid() })
+                partition_id = cursor.fetchone()[0]
+                # create groups
+                for groupetd in groupetds:
+                    cursor.execute("INSERT into group_descr (partition_id, group_name) VALUES (%(pid)s, %(group_name)s)", { 'pid' : partition_id, 'group_name' : groupetd } )
+                    cursor.execute("SELECT group_id from group_descr where oid=%(oid)s", { 'oid' : cursor.lastoid() })
+                    group_id = cursor.fetchone()[0]
+                    # inscrit les etudiants
+                    cursor.execute("INSERT into group_membership (etudid, group_id) SELECT etudid, %(group_id)s from notes_formsemestre_inscription where formsemestre_id=%(formsemestre_id)s and groupetd=%(groupetd)s", { 'group_id' : group_id, 'formsemestre_id' : formsemestre_id, 'groupetd' : groupetd } )
+            # TA
+            cursor.execute("SELECT distinct(groupeanglais) from notes_formsemestre_inscription where formsemestre_id=%(formsemestre_id)s", { 'formsemestre_id' : formsemestre_id } )
+            groupetds = [ x[0] for x in cursor.fetchall() if x[0] ]            
+            if len(groupetds) > 0:
+                # TA : create partition
+                cursor.execute("SELECT * from notes_formsemestre where formsemestre_id=%(formsemestre_id)s", { 'formsemestre_id' : formsemestre_id } )
+                nomgroupetd = cursor.dictfetchone()['nomgroupeta']
+                if not nomgroupetd: # pas de nom ??? on invente un nom stupide et unique
+                    nomgroupetd = 'TA_'+str(time.time()).replace('.','')[-3:]
+                cursor.execute("INSERT into partition (formsemestre_id, partition_name) VALUES (%(formsemestre_id)s,%(nomgroupeta)s)", { 'formsemestre_id' : formsemestre_id, 'nomgroupeta' : nomgroupetd } )
+                cursor.execute("select partition_id from partition where oid=%(oid)s", { 'oid' : cursor.lastoid() })
+                partition_id = cursor.fetchone()[0]
+                # create groups
+                for groupetd in groupetds:
+                    cursor.execute("INSERT into group_descr (partition_id, group_name) VALUES (%(pid)s, %(group_name)s)", { 'pid' : partition_id, 'group_name' : groupetd } )
+                    cursor.execute("SELECT group_id from group_descr where oid=%(oid)s", { 'oid' : cursor.lastoid() })
+                    group_id = cursor.fetchone()[0]
+                    # inscrit les etudiants
+                    cursor.execute("INSERT into group_membership (etudid, group_id) SELECT etudid, %(group_id)s from notes_formsemestre_inscription where formsemestre_id=%(formsemestre_id)s and groupeanglais=%(groupetd)s", { 'group_id' : group_id, 'formsemestre_id' : formsemestre_id, 'groupetd' : groupetd } )
+            
+            # TP
+            cursor.execute("SELECT distinct(groupetp) from notes_formsemestre_inscription where formsemestre_id=%(formsemestre_id)s", { 'formsemestre_id' : formsemestre_id } )
+            groupetds = [ x[0] for x in cursor.fetchall() if x[0] ]
+            if len(groupetds) > 0:
+                # TP : create partition
+                cursor.execute("SELECT * from notes_formsemestre where formsemestre_id=%(formsemestre_id)s", { 'formsemestre_id' : formsemestre_id } )
+                nomgroupetd = cursor.dictfetchone()['nomgroupetp']
+                if not nomgroupetd: # pas de nom ??? on invente un nom stupide et unique
+                    nomgroupetd = 'TP_'+str(time.time()).replace('.','')[-3:]
+                cursor.execute("INSERT into partition (formsemestre_id, partition_name) VALUES (%(formsemestre_id)s,%(nomgroupeta)s)", { 'formsemestre_id' : formsemestre_id, 'nomgroupeta' : nomgroupetd } )
+                cursor.execute("select partition_id from partition where oid=%(oid)s", { 'oid' : cursor.lastoid() })
+                partition_id = cursor.fetchone()[0]
+                # create groups
+                for groupetd in groupetds:
+                    cursor.execute("INSERT into group_descr (partition_id, group_name) VALUES (%(pid)s, %(group_name)s)", { 'pid' : partition_id, 'group_name' : groupetd } )
+                    cursor.execute("SELECT group_id from group_descr where oid=%(oid)s", { 'oid' : cursor.lastoid() })
+                    group_id = cursor.fetchone()[0]
+                    # inscrit les etudiants
+                    cursor.execute("INSERT into group_membership (etudid, group_id) SELECT etudid, %(group_id)s from notes_formsemestre_inscription where formsemestre_id=%(formsemestre_id)s and groupetp=%(groupetd)s", { 'group_id' : group_id, 'formsemestre_id' : formsemestre_id, 'groupetd' : groupetd } )
+
+        # 3- Suppress obsolete fields
+        cursor.execute( """alter table notes_formsemestre drop column nomgroupetd""" ) 
+        cursor.execute( """alter table notes_formsemestre drop column nomgroupetp""" ) 
+        cursor.execute( """alter table notes_formsemestre drop column nomgroupeta""" )
+        
+        cursor.execute( """alter table notes_formsemestre_inscription drop column groupetd""" )
+        cursor.execute( """alter table notes_formsemestre_inscription drop column groupetp""" )
+        cursor.execute( """alter table notes_formsemestre_inscription drop column groupeanglais""" )
+    # ----------------------- /New groups
+
+    # Add moy_ue to validations:
+    check_field(cnx, 'scolar_formsemestre_validation', 'moy_ue',
+                ['alter table scolar_formsemestre_validation add column moy_ue real',
+                 ])
+    # Add photo_filename
+    check_field(cnx, 'identite', 'photo_filename',
+                ['alter table identite add column photo_filename text',
+                 ])
+    # Add module's ECTS
+    check_field(cnx, 'notes_modules', 'ects',
+                ['alter table notes_modules add column ects real',
+                 ])
+    # Add "statut" to identite (default to NULL)
+    check_field(cnx, 'identite', 'statut',
+                ['alter table identite add column statut text',
+                 ])
+    # Add user-defined expressions
+    check_field(cnx, 'notes_moduleimpl', 'computation_expr',
+                ['alter table notes_moduleimpl add column computation_expr text'])
+    # Add semestre_id to scolar_formsemestre_validation
+    check_field(cnx, 'scolar_formsemestre_validation', 'semestre_id',
+                ['alter table scolar_formsemestre_validation add column semestre_id int'])
+
+    # Add 
+    check_table( cnx, 'absences_notifications', [ """
+     CREATE TABLE absences_notifications (
+       etudid text NOT NULL,
+       notification_date timestamp with time zone DEFAULT now(),
+       email text NOT NULL,
+       nbabs integer,
+       nbabsjust integer    
+      ) WITH OIDS;
+    """] )
+    # rename old preference "send_mail_absence_to_chef"
+    cursor.execute("update sco_prefs set name = 'abs_notify_chief' where name = 'send_mail_absence_to_chef'")
+    
+    check_table( cnx, 'notes_formsemestre_ue_computation_expr', [ """
+     CREATE TABLE notes_formsemestre_ue_computation_expr (
+	notes_formsemestre_ue_computation_expr_id text default notes_newid('UEXPR') PRIMARY KEY,
+	formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id),
+	ue_id  text REFERENCES notes_ue(ue_id),
+	computation_expr text, -- formule de calcul moyenne
+	UNIQUE(formsemestre_id, ue_id)
+       ) WITH OIDS;
+     """] )
+    
+
+    # add moduleimpl_id to absences:
+    check_field(cnx, 'absences', 'moduleimpl_id',
+                ['alter table absences add column moduleimpl_id text'])
+
+    # add type_parcours
+    check_field(cnx, 'notes_formations', 'type_parcours',
+                ['alter table notes_formations add column type_parcours int DEFAULT 0',
+                 'update notes_formations set type_parcours=0 where type_parcours is NULL'
+                 ])
+    
+    # add etape_apo2
+    check_field(cnx, 'notes_formsemestre', 'etape_apo2',
+                ['alter table notes_formsemestre add column etape_apo2 text'])
+    # add etape_apo3
+    check_field(cnx, 'notes_formsemestre', 'etape_apo3',
+                ['alter table notes_formsemestre add column etape_apo3 text'])
+    # add etape_apo4
+    check_field(cnx, 'notes_formsemestre', 'etape_apo4',
+                ['alter table notes_formsemestre add column etape_apo4 text'])
+    # add publish_incomplete
+    check_field(cnx, 'notes_evaluation', 'publish_incomplete',
+                ['alter table notes_evaluation add column  publish_incomplete int DEFAULT 0',
+                 'update notes_evaluation set publish_incomplete=0 where publish_incomplete is NULL'
+                 ])
+
+    # add ens_can_create_eval to notes_formsemestre:
+    check_field(cnx, 'notes_formsemestre', 'ens_can_edit_eval',
+                ['alter table notes_formsemestre add column ens_can_edit_eval int default 0',
+                 'update notes_formsemestre set ens_can_edit_eval=0'])
+
+    # add evaluation_type
+    check_field(cnx, 'notes_evaluation', 'evaluation_type',
+                ['alter table notes_evaluation add column evaluation_type int DEFAULT 0',
+                 'update notes_evaluation set evaluation_type=0 where evaluation_type is NULL'
+                 ])
+    
+    # add partition rank on bulletins
+    check_field(cnx, 'partition', 'bul_show_rank',
+                ['alter table partition add column bul_show_rank int DEFAULT 0',
+                 'update partition set bul_show_rank=0 where bul_show_rank is NULL'])
+    # add formsemestre to abs notifications
+    check_field(cnx, 'absences_notifications', 'formsemestre_id',
+                ['alter table absences_notifications add column formsemestre_id text DEFAULT NULL',
+                 ])
+    # Add "debouche" to admission
+    check_field(cnx, 'admissions', 'debouche',
+                ['alter table admissions add column debouche text DEFAULT NULL',
+                 # et en profite pour corrige le From par defaut des mails:
+                 "update sco_prefs set value='noreply@univ-paris13.fr' where name='email_from_addr' and value='noreply'"
+                 ])
+    # Increase semestre indices
+    for i in range(5,9):
+        cursor.execute("SELECT * from notes_semestres where semestre_id=%(i)s", { 'i' : i } )
+        r = cursor.fetchall()
+        if not r:
+            log("adding semestre_id %s" % i)
+            cursor.execute("INSERT INTO notes_semestres (semestre_id) VALUES (%(i)s)", { 'i' : i } )
+    # ECTS associes aux UE:
+    check_field(cnx, 'notes_ue', 'ects',
+                ['alter table notes_ue add column ects float DEFAULT NULL',
+                 ])
+    # Numeros des evaluations:
+    check_field(cnx, 'notes_evaluation', 'numero',
+                ['alter table notes_evaluation add column numero int DEFAULT 0',
+                 ])
+    # add nom_usuel to identite
+    check_field(cnx, 'identite', 'nom_usuel',
+                ['alter table identite add column nom_usuel text DEFAULT NULL',
+                 ])
+    # add type_admission
+    check_field(cnx, 'admissions', 'type_admission',
+                ['alter table admissions add column type_admission text DEFAULT NULL',
+                 ])
+    check_field(cnx, 'admissions', 'boursier_prec',
+                ['alter table admissions add column boursier_prec integer default NULL',
+                 ])
+    # add modalites formation
+    check_table( cnx, 'notes_form_modalites', [
+        """CREATE TABLE notes_form_modalites (
+    form_modalite_id text default notes_newid('Md') PRIMARY KEY,
+    modalite text, -- la clef dans notes_formsemestre
+    titre text, -- le nom complet de la modalite pour les documents scodoc
+    numero SERIAL -- integer, ordre de presentation
+     );""",
+    """INSERT INTO notes_form_modalites (modalite, titre) VALUES ('', 'Autres formations');""",
+    """INSERT INTO notes_form_modalites (modalite, titre) VALUES ('FI', 'Formation Initiale');""",
+    """INSERT INTO notes_form_modalites (modalite, titre) VALUES ('FC', 'Formation Continue');""",
+    """INSERT INTO notes_form_modalites (modalite, titre) VALUES ('FAP', 'Apprentissage');""",
+    """INSERT INTO notes_form_modalites (modalite, titre) VALUES ('DEC', 'Formation Décalées');""",
+    """INSERT INTO notes_form_modalites (modalite, titre) VALUES ('LIC', 'Licence');"""
+    ] )
+    # Add code_specialite
+    check_field( cnx, 'notes_formations', 'code_specialite',
+                 [ 'alter table notes_formations add column code_specialite text default NULL',
+                   ])
+    # Fix modules without codes
+    cursor.execute("UPDATE notes_modules SET code = 'M_' || coalesce(upper(substring(titre from 1 for 2)), '') || '_' || coalesce(semestre_id,'0') where code is NULL;");
+    
+    # Add ue.is_external
+    check_field( cnx, 'notes_ue', 'is_external',
+                 [ 'alter table notes_ue add column is_external integer default 0',
+                   ])
+    check_field( cnx, 'scolar_formsemestre_validation', 'is_external',
+                 [ 'alter table scolar_formsemestre_validation add column is_external integer default 0',
+                   ])
+    # Add codes apogee
+    check_field( cnx, 'notes_ue', 'code_apogee',
+                 [ 'alter table notes_ue add column code_apogee text UNIQUE',
+                   ])
+    check_field( cnx, 'notes_modules', 'code_apogee',
+                 [ 'alter table notes_modules add column code_apogee text UNIQUE',
+                   ])
+    check_field( cnx, 'notes_formsemestre', 'elt_sem_apo',
+                 [ 'alter table notes_formsemestre add column elt_sem_apo text',
+                   ])
+    check_field( cnx, 'notes_formsemestre', 'elt_annee_apo',
+                 [ 'alter table notes_formsemestre add column elt_annee_apo text',
+                   ])
+    # Classement admission
+    check_field(cnx, 'admissions', 'classement',
+                ['alter table admissions add column classement integer default NULL',
+                 ])
+    # Supprime contraintes erronées sur codes Apogee:
+    if list_constraint( cnx, constraint_name='notes_ue_code_apogee_key' ):
+        log('dropping buggy constraint on notes_ue_code_apogee')
+        cursor.execute("alter  table notes_ue drop CONSTRAINT notes_ue_code_apogee_key;")
+    if list_constraint( cnx, constraint_name='notes_modules_code_apogee_key' ):
+        log('dropping buggy constraint on notes_modules_code_apogee')
+        cursor.execute("alter  table notes_modules drop CONSTRAINT notes_modules_code_apogee_key;")
+    
+    # SemSet:
+    check_table( cnx, 'notes_semset', [
+        """CREATE TABLE notes_semset  (
+    semset_id text default notes_newid('NSS') PRIMARY KEY,
+    title text,
+    annee_scolaire int default NULL, -- 2016
+    sem_id int default NULL -- 0, 1, 2
+    ) WITH OIDS;""", ] )
+    check_field(cnx, 'notes_semset', 'annee_scolaire',
+                ['alter table notes_semset add column annee_scolaire integer default NULL',
+                 ])
+    check_table( cnx, 'notes_semset_formsemestre', [
+        """CREATE TABLE notes_semset_formsemestre (
+    formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id) ON DELETE CASCADE,
+    semset_id text REFERENCES notes_semset (semset_id) ON DELETE CASCADE,
+    PRIMARY KEY (formsemestre_id, semset_id)
+    ) WITH OIDS;""", ] )
+
+    # ModuleTags
+    check_table( cnx, 'notes_tags', [
+        """CREATE TABLE notes_tags (
+	tag_id text default notes_newid('TAG') PRIMARY KEY,
+    title text UNIQUE NOT NULL
+    ) WITH OIDS;""", ] )
+    check_table( cnx, 'notes_modules_tags', [
+        """CREATE TABLE notes_modules_tags (
+	tag_id text REFERENCES notes_tags(tag_id) ON DELETE CASCADE,
+    module_id text REFERENCES notes_modules(module_id) ON DELETE CASCADE,
+    PRIMARY KEY (tag_id, module_id)
+    ) WITH OIDS;""", ] )
+    
+    # add show_in_lists on partition
+    check_field(cnx, 'partition', 'show_in_lists',
+                ['alter table partition add column show_in_lists integer DEFAULT 1',
+                 'update partition set show_in_lists=1 where show_in_lists is NULL'])
+    # table codes etapes apogee semestre
+    check_table( cnx, 'notes_formsemestre_etapes', [
+        """CREATE TABLE notes_formsemestre_etapes (
+    formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id) ON DELETE CASCADE,
+    etape_apo text NOT NULL
+) WITH OIDS;""",
+    """INSERT into notes_formsemestre_etapes (formsemestre_id, etape_apo) SELECT formsemestre_id, etape_apo FROM notes_formsemestre WHERE etape_apo is not NULL;""",
+    """INSERT into notes_formsemestre_etapes (formsemestre_id, etape_apo) SELECT formsemestre_id, etape_apo2 FROM notes_formsemestre WHERE etape_apo2 is not NULL;""",
+    """INSERT into notes_formsemestre_etapes (formsemestre_id, etape_apo) SELECT formsemestre_id, etape_apo3 FROM notes_formsemestre WHERE etape_apo3 is not NULL;""",
+    """INSERT into notes_formsemestre_etapes (formsemestre_id, etape_apo) SELECT formsemestre_id, etape_apo4 FROM notes_formsemestre WHERE etape_apo4 is not NULL;""",
+    """ALTER table notes_formsemestre DROP column etape_apo;""",
+    """ALTER table notes_formsemestre DROP column etape_apo2;""",
+    """ALTER table notes_formsemestre DROP column etape_apo3;""",
+    """ALTER table notes_formsemestre DROP column etape_apo4;""",
+    ] )
+    # Admission APB: groupe et classement dans groupe
+    check_field(cnx, 'admissions', 'apb_groupe',
+                ['alter table admissions add column apb_groupe text default NULL',
+                 ])
+    check_field(cnx, 'admissions', 'apb_classement_gr',
+                ['alter table admissions add column apb_classement_gr integer default NULL',
+                 ])
+    # Adresse mail perso
+    check_field(cnx, 'adresse', 'emailperso',
+                ['alter table adresse add column emailperso text',
+                 ])
+    # Ajout de modalites supplementaires    
+    cursor.execute("SELECT modalite from notes_form_modalites where modalite= 'CP'")
+    if not len(cursor.fetchall()):
+        log('adding modalite "CP"')
+        cursor.execute("INSERT INTO notes_form_modalites (modalite, titre) VALUES ('CP', 'Contrats de Professionnalisation');")
+    # Un index oublié sur notes_notes:
+    if 'notes_notes_evaluation_id_idx' not in list_table_index(cnx, 'notes_notes'):
+        log('creating index on notes_notes')
+        cursor.execute("CREATE INDEX notes_notes_evaluation_id_idx ON notes_notes (evaluation_id)")
+    
+    # boursier (ajout API nov 2017)
+    check_field(cnx, 'identite', 'boursier',
+                ['alter table identite add column boursier text'
+                 ])
+    # Suivi des anciens etudiants (debouche)
+    # cree table suivi et recopie ancien champs debouche de la table admission
+    check_table( cnx, 'itemsuivi', [
+    """CREATE TABLE itemsuivi (
+    itemsuivi_id text DEFAULT notes_newid('SUI'::text) PRIMARY KEY,
+    etudid text NOT NULL,
+    item_date date DEFAULT now(),
+    situation text
+    ) WITH OIDS;""",
+
+    """INSERT INTO itemsuivi (etudid, situation) 
+       SELECT etudid, debouche FROM admissions WHERE debouche is not null;
+    """
+        ] )    
+    check_table( cnx, 'itemsuivi_tags', [
+    """CREATE TABLE itemsuivi_tags (
+    tag_id text default notes_newid('TG') PRIMARY KEY,
+    title text UNIQUE NOT NULL
+    ) WITH OIDS;""",
+        ] )
+    check_table( cnx, 'itemsuivi_tags_assoc', [
+    """CREATE TABLE itemsuivi_tags_assoc (
+    tag_id text REFERENCES itemsuivi_tags(tag_id) ON DELETE CASCADE,
+    itemsuivi_id text REFERENCES itemsuivi(itemsuivi_id) ON DELETE CASCADE,
+    PRIMARY KEY (tag_id, itemsuivi_id)
+    ) WITH OIDS;""",
+        ] )
+    
+    # Types de modules (pour malus)
+    check_field(cnx, 'notes_modules', 'module_type',
+                ['alter table notes_modules add column module_type int',
+                ])
+
+    # Responsables de semestres
+    check_table( cnx, 'notes_formsemestre_responsables', [
+    """CREATE TABLE notes_formsemestre_responsables (
+    formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id) ON DELETE CASCADE,
+    responsable_id text NOT NULL,
+    UNIQUE(formsemestre_id, responsable_id)
+    ) WITH OIDS;""",
+    """INSERT into notes_formsemestre_responsables (formsemestre_id, responsable_id) SELECT formsemestre_id, responsable_id FROM notes_formsemestre WHERE responsable_id is not NULL;""",
+    """ALTER table notes_formsemestre DROP column responsable_id;""",
+        ])
+    # Fonction pour anonymisation:
+    if not function_exists(cnx, 'random_text_md5'):
+        log('creating function random_text_md5')
+        # inspirée par https://www.simononsoftware.com/random-string-in-postgresql/
+        cursor.execute("""CREATE FUNCTION random_text_md5( integer ) returns text        
+        LANGUAGE SQL
+        AS $$ 
+        select upper( substring( (SELECT string_agg(md5(random()::TEXT), '')
+        FROM generate_series(
+           1,
+           CEIL($1 / 32.)::integer) 
+        ), 1, $1) );
+        $$;""")
+    # departement naissance (ajout fev 2020)
+    check_field(
+        cnx, 'identite', 'dept_naissance',
+        ['alter table identite add column dept_naissance text'])
+    # Modalite semestres exterieurs
+    cursor.execute("SELECT modalite from notes_form_modalites where modalite= 'EXT'")
+    if not len(cursor.fetchall()):
+        log('adding modalite "EXT"')
+        cursor.execute("INSERT INTO notes_form_modalites (modalite, titre) VALUES ('EXT', 'Extérieur');")
+    # Coefficients d'UE
+    check_field(cnx, 'notes_ue', 'coefficient',
+                ["alter table notes_ue add column coefficient float",
+                # Initialise les coefficients égaux aux ECTS:
+                "update notes_ue set coefficient=ects",
+                # Force pref locale sur semestres existants:
+                """INSERT INTO sco_prefs (name, value, formsemestre_id) 
+                SELECT DISTINCT 'use_ue_coefs', '0', formsemestre_id FROM notes_formsemestre 
+                ON CONFLICT DO NOTHING
+                """
+                ])
+    # Add here actions to performs after upgrades:
+    cnx.commit()
+    cnx.close()
+
+
+# Base utilisateurs:
+log('\nChecking users database')
+cnx = psycopg2.connect( get_users_cnx_str() )
+cursor = cnx.cursor()
+check_field(cnx, 'sco_users', 'passwd_temp',
+            ['alter table sco_users add column passwd_temp int default 0',
+             'update sco_users set passwd_temp=0' ])
+check_field(cnx, 'sco_users', 'status',
+            ["alter table sco_users add column status text default NULL"])
+check_field(cnx, 'sco_users', 'date_expiration',
+            ["alter table sco_users add column date_expiration date",
+             "update sco_users set status=NULL where status=''" # fix a bug in previous update...
+             ])
+check_field(cnx, 'sco_users', 'login_edt',
+            ["alter table sco_users add column login_edt text default NULL",
+             ])
+cnx.commit()
+cnx.close()
+
+# The end.
+sys.exit(0)
diff --git a/config/postupgrade.py b/config/postupgrade.py
new file mode 100755
index 0000000000000000000000000000000000000000..1137357ac8762550283b12c8c87187ac36936991
--- /dev/null
+++ b/config/postupgrade.py
@@ -0,0 +1,60 @@
+#!/opt/zope213/bin/python
+
+"""
+ScoDoc post-upgrade script.
+
+This script is launched by upgrade.sh after each SVN update.
+
+Run as "root" with Zope shutted down and postgresql up,
+_before_ upgrading the database.
+
+E. Viennet, June 2008
+Mar 2017: suppress upgrade of very old Apache configs
+Aug 2020: move photos to .../var/scodoc/
+"""
+import os
+import sys
+import glob
+import shutil 
+from scodocutils import log, SCODOC_DIR, SCODOC_VAR_DIR, SCODOC_LOGOS_DIR
+
+if os.getuid() != 0:
+    log('postupgrade.py: must be run as root')
+    sys.exit(1)
+
+# ---
+# Migrate photos (2020-08-16, svn 1908)
+old_photo_dir = os.path.join(SCODOC_DIR, "static", "photos")
+photo_dirs = glob.glob( old_photo_dir + "/F*")
+if photo_dirs:
+    log("Moving photos to new <var> directory...")
+    shutil.move(old_photo_dir, SCODOC_VAR_DIR)
+
+# Migrate depts (2020-08-17, svn 1909)
+
+old_depts_dir = os.path.join(SCODOC_DIR, "config", "depts")
+cfg_files = glob.glob( old_depts_dir + "/*.cfg")
+depts_dir = os.path.join(SCODOC_VAR_DIR, "config/depts/")
+for cfg in cfg_files:
+    log("Moving %s to new <var> directory..." % cfg)
+    shutil.move(cfg, depts_dir)
+
+# Move logos
+if not os.path.exists(SCODOC_LOGOS_DIR):
+    old_logos = os.path.join(SCODOC_DIR,"logos")
+    if os.path.exists(old_logos):
+        log("Moving logos to new <var> directory...")
+        dest = os.path.normpath(os.path.join(SCODOC_LOGOS_DIR, ".."))
+        shutil.move(old_logos, dest)
+    else:
+        log("Warning: logos directory is missing (%s)" % SCODOC_LOGOS_DIR)
+
+# Move dept-specific logos
+for d in glob.glob( SCODOC_DIR + "/logos_*" ):
+    log("Moving %s to %s" % (d, SCODOC_LOGOS_DIR))
+    shutil.move(d, SCODOC_LOGOS_DIR)
+
+# Continue here...
+
+# ---
+sys.exit(0)
diff --git a/config/psql_restore_databases.sh b/config/psql_restore_databases.sh
new file mode 100755
index 0000000000000000000000000000000000000000..9eb260a539097fdb9e86ba57bb3c2f4bbd050337
--- /dev/null
+++ b/config/psql_restore_databases.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+# aux script called by restore_scodoc_data.sh as "postgres" user
+# DO NOT CALL DIRECTLY
+
+PG_DUMPFILE=$1
+
+# Check locale of installation. If invalid, reinitialize all system
+
+is_latin1=$(psql -l | grep postgres | grep iso88591 | wc -l)
+if [ $is_latin1 -gt 1 ]
+then
+  echo "Recreating postgres cluster using UTF-8"
+
+  pg_dropcluster --stop 9.1 main
+
+  pg_createcluster --locale en_US.UTF-8 --start 9.1 main
+fi
+
+
+# Drop all current ScoDoc databases, if any:
+for f in $(psql -l --no-align --field-separator . | grep SCO | cut -f 1 -d.); do
+  echo dropping $f
+  dropdb $f
+done
+echo "Restoring postgres data..."
+psql -f "$PG_DUMPFILE" postgres
+
diff --git a/config/restore_scodoc_data.sh b/config/restore_scodoc_data.sh
new file mode 100755
index 0000000000000000000000000000000000000000..aed3a1ea64b3669bcbb9c5ea62b1004bd97076df
--- /dev/null
+++ b/config/restore_scodoc_data.sh
@@ -0,0 +1,145 @@
+#!/bin/bash
+
+#
+# ScoDoc:  restore data (saved by save_scodoc_data) into current install
+# 
+#  Utile pour migrer ScoDoc d'un serveur a un autre
+#  A executer en tant que root sur le nouveau serveur
+#
+# E. Viennet, Sept 2011, Nov 2013, Mar 2017, Aug 2020
+#
+
+
+INSTANCE_DIR=/opt/scodoc/
+SCODOC_DIR="${INSTANCE_DIR}/Products/ScoDoc"
+SCODOC_VAR_DIR="${INSTANCE_DIR}/var/scodoc"
+
+source utils.sh
+check_uid_root $0
+
+# Safety check
+echo "Ce script va remplacer les donnees de votre installation ScoDoc par celles"
+echo "enregistrees dans le fichier fourni."
+echo "Ce fichier doit avoir ete cree par le script save_scodoc_data.sh, sur une autre machine."
+echo 
+echo "Attention: TOUTES LES DONNEES DE CE SERVEUR SERONT REMPLACEES !"
+echo "Notamment, tous les utilisateurs et departements existants seront effaces !"
+echo
+echo "TOUTES LES BASES POSTGRESQL SERONT EFFACEES !!!"
+echo 
+echo -n "Voulez vous poursuivre cette operation ? (y/n) [n]"
+read ans
+if [ ! "$(norm_ans "$ans")" = 'Y' ]
+then
+   echo "Annulation"
+   exit 1
+fi
+
+# Usage
+if [ ! $# -eq 1 ]
+then
+  echo "Usage: $0 directory_or_archive"
+  exit 1
+fi
+
+SRC=$1
+
+if [ "${SRC:0:1}" != "/" ]
+then
+  echo "Usage: $0 directory_or_archive"
+  echo "Erreur: utiliser un chemin absolu (commencant par /)"
+  exit 1
+fi
+
+# Source directory
+if [ "${SRC##*.}" = 'tgz' ]
+then
+  echo "Opening tgz archive..."
+  tmp=$(mktemp -d)
+  chmod a+rx "$tmp"
+  cd "$tmp"
+  tar xfz "$SRC" 
+  SRC=$(ls -1d "$tmp"/*)
+  IS_TMP=1
+  # If source is a tgz, can use mv
+  COPY="mv"
+else
+  IS_TMP=0
+  # If source is a directory, does not modify its content
+  COPY="cp -rp"
+fi
+
+echo "Source is $SRC"
+echo "Stopping ScoDoc..."
+/etc/init.d/scodoc stop
+
+# Erase all postgres databases and load data
+chmod a+rx "$SRC"
+chmod a+r "$SRC"/scodoc.dump.txt
+PG_DUMPFILE="$SRC/scodoc.dump.txt"
+
+su -c "$SCODOC_DIR/config/psql_restore_databases.sh $PG_DUMPFILE" postgres
+
+# 
+echo Copying data files...
+
+rm -rf "$INSTANCE_DIR/var"
+$COPY "$SRC/var" "$INSTANCE_DIR"
+
+if [ ! -e "${SCODOC_VAR_DIR}/config/" ]
+then
+  mkdir "${SCODOC_VAR_DIR}/config/"
+  chown www-data.www-data "${SCODOC_VAR_DIR}/config/"
+  chmod 775 "${SCODOC_VAR_DIR}/config/"
+fi
+
+rm -rf "$SCODOC_DIR/config/depts" 
+if [ -e "$SRC/depts" ]
+then
+  # legacy depts => move them to var
+  $COPY "$SRC/depts" "${SCODOC_VAR_DIR}/config/"
+fi
+
+rm -rf  "$SCODOC_DIR/static/photos" 
+if [ -e "$SRC/photos" ]
+then
+  # legacy photos (in <src>/static/photos) => move them to var
+  $COPY "$SRC/photos" "${SCODOC_VAR_DIR}/"
+fi 
+
+rm -rf "$SCODOC_DIR/logos"
+$COPY "$SRC/logos" "$SCODOC_DIR/"
+
+mv "$SCODOC_DIR/config/scodoc_config.py"  "$SCODOC_DIR/config/scodoc_config.py.$(date +%Y%m%d-%H%M%S)" 
+$COPY "$SRC/scodoc_config.py" "$SCODOC_DIR/config/"
+# Verifie le codage de ce fichier:
+if [ -z "$(file $SCODOC_DIR/config/scodoc_config.py | grep -i UTF-8)" ] 
+then
+   mv "$SCODOC_DIR/config/scodoc_config.py" "$SCODOC_DIR/config/scodoc_config.py.orig"
+   iconv -f iso8859-15 -t utf-8 "$SCODOC_DIR/config/scodoc_config.py.orig" > "$SCODOC_DIR/config/scodoc_config.py"
+fi
+
+rm -rf "$INSTANCE_DIR/log"
+$COPY "$SRC/log" "$INSTANCE_DIR/"
+
+# Fix file ownership and access rights
+chown -R www-data.root "$INSTANCE_DIR/log" 
+chown -R www-data.root "$INSTANCE_DIR/var" 
+chmod 775 "$INSTANCE_DIR./log" "$INSTANCE_DIR./var" 
+chown -R  www-data.root "$SCODOC_DIR"
+chmod -R 775 "$SCODOC_DIR"
+
+# Remove tmp directory
+if [ $IS_TMP = "1" ]
+then
+  rm -rf $tmp
+fi
+
+# Mise a jour BD ScoDoc
+cd $SCODOC_DIR/config
+./upgrade.sh
+
+#
+echo
+echo "Ok. Run \"/etc/init.d/scodoc start\" to start ScoDoc."
+
diff --git a/config/save_scodoc_data.sh b/config/save_scodoc_data.sh
new file mode 100755
index 0000000000000000000000000000000000000000..09cc0644dbd0f568e63df16606422ffd7fe475fa
--- /dev/null
+++ b/config/save_scodoc_data.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+
+#
+# ScoDoc: save all user data (database, configs, images, archives...) in separate directory
+# 
+#  Utile pour migrer ScoDoc d'un serveur a un autre
+#  Executer en tant que root sur le serveur d'origine
+#
+# E. Viennet, Sept 2011, Aug 2020
+#
+
+# Destination directory
+if [ ! $# -eq 1 ]
+then
+  echo "Usage: $0 destination_directory"
+  exit 1
+fi
+DEST=$1
+# remove trailing slashs if needed:
+shopt -s extglob
+DEST="${DEST%%+(/)}"
+
+if [ ! -e "$DEST" ]
+then
+  echo Creating directory "$DEST"
+  mkdir "$DEST"
+else
+  echo "Error: Directory " "$DEST"  " exists"
+  echo "remove it or specify another destination !"
+  exit 2
+fi
+
+INSTANCE_DIR=/opt/scodoc
+SCODOC_DIR="$INSTANCE_DIR/Products/ScoDoc"
+
+source utils.sh
+check_uid_root $0
+
+echo "Stopping ScoDoc..."
+/etc/init.d/scodoc stop
+
+# Dump all postgres databases
+echo "Dumping SQL database..."
+chown postgres "$DEST"
+su -c "pg_dumpall > \"$DEST\"/scodoc.dump.txt" postgres
+if [ ! $? -eq 0 ] 
+then
+  echo "Error dumping postgresql database\nPlease check that SQL server is running\nAborting."
+  exit 1
+fi
+chown root "$DEST"
+
+# Zope DB and ScoDoc archives:
+echo "Copying var/ ..." 
+cp -rp "$INSTANCE_DIR/var" "$DEST"
+
+# Depts db config (now in .../var)
+shopt -s nullglob
+if [ ! -z "$(echo ${SCODOC_DIR}/config/depts/*.cfg)" ]
+then
+  echo "Copying legacy depts configs..."
+  cp -rp "$SCODOC_DIR/config/depts" "$DEST"
+fi
+
+
+
+# Photos des etudiants (now in .../var)
+if [ -e "$SCODOC_DIR/static/photos" ]
+then
+  echo "Copying legacy photos..."
+  cp -rp "$SCODOC_DIR/static/photos" "$DEST"
+fi
+
+echo "Copying logos..."
+cp -rp "$SCODOC_DIR/logos" "$DEST"
+
+echo "Copying configuration file..."
+cp -p "$SCODOC_DIR/config/scodoc_config.py" "$DEST"
+
+echo "Copying server logs..."
+cp -rp "$INSTANCE_DIR/log" "$DEST"
+
+
+# --- Archive all files in a tarball to ease transfer
+echo
+echo "Archiving backup files in a $DEST.tgz..."
+base=$(basename "$DEST")
+(cd "$DEST"/..; tar cfz "$DEST".tgz "$base")
+
+echo "Done (you can copy " "$DEST"".tgz to destination machine)."
diff --git a/config/scodoc_config.py b/config/scodoc_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..32d3fa21692b3d018c89e844ad79dbcd0187966a
--- /dev/null
+++ b/config/scodoc_config.py
@@ -0,0 +1,131 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+#
+# Configuration globale de ScoDoc (version juin 2009)
+#
+
+# La plupart des réglages sont stoqués en base de donnée et accessibles via le web
+# (pages de paramètres ou préférences).
+# Les valeurs indiquées ici sont les valeurs initiales que prendront 
+# les paramètres lors de la création d'un nouveau département, 
+# elles ne sont plus utilisées ensuite.
+
+# Nota: il y a aussi des réglages dans sco_utils.py, mais ils nécessitent 
+# souvent de comprendre le code qui les utilise pour ne pas faire d'erreur: attention.
+
+
+class CFG :
+    pass
+
+CONFIG = CFG()
+
+CONFIG.always_require_ine = 0 # set to 1 if you want to require INE
+
+# The base URL, use only if you are behind a proxy
+#  eg "https://scodoc.example.net/ScoDoc"
+CONFIG.ABSOLUTE_URL = "" 
+
+#
+#   ------------- Documents PDF -------------
+#
+CONFIG.SCOLAR_FONT = 'Helvetica'
+CONFIG.SCOLAR_FONT_SIZE = 10
+CONFIG.SCOLAR_FONT_SIZE_FOOT = 6
+
+# Pour pieds de pages Procès verbaux:
+#  (markup leger reportlab supporté, par ex. <b>blah blah</b>)
+CONFIG.INSTITUTION_NAME="<b>Institut Universitaire de Technologie - Université Paris 13</b>"
+CONFIG.INSTITUTION_ADDRESS="Web <b>www.iutv.univ-paris13.fr</b> - 99 avenue Jean-Baptiste Clément - F 93430 Villetaneuse"
+
+CONFIG.INSTITUTION_CITY="Villetaneuse"
+
+
+# Taille du l'image logo: largeur/hauteur  (ne pas oublier le . !!!)
+CONFIG.LOGO_FOOTER_ASPECT = 326/96. # W/H    XXX provisoire: utilisera PIL pour connaitre la taille de l'image
+CONFIG.LOGO_FOOTER_HEIGHT = 10 # taille dans le document en millimetres
+
+CONFIG.LOGO_HEADER_ASPECT = 549 / 346. # XXX logo IUTV
+CONFIG.LOGO_HEADER_HEIGHT = 28 # taille verticale dans le document en millimetres
+
+# Pied de page PDF : un format Python, %(xxx)s est remplacé par la variable xxx.
+# Les variables définies sont:
+#   day   : Day of the month as a decimal number [01,31]
+#   month : Month as a decimal number [01,12].
+#   year  : Year without century as a decimal number [00,99].
+#   Year  : Year with century as a decimal number.
+#   hour  : Hour (24-hour clock) as a decimal number [00,23].
+#   minute: Minute as a decimal number [00,59].
+#   
+#   server_url: URL du serveur ScoDoc
+#   scodoc_name: le nom du logiciel (ScoDoc actuellement, voir VERSION.py)
+        
+CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s sur %(server_url)s"
+
+
+
+#
+#   ------------- Calcul bonus modules optionnels (sport, culture...) -------------
+#
+from bonus_sport import *
+
+CONFIG.compute_bonus = bonus_iutv
+# Mettre "bonus_demo" pour logguer des informations utiles au developpement...
+
+#
+#   ------------- Capitalisation des UEs -------------
+# Deux écoles:
+#   - règle "DUT": capitalisation des UE obtenues avec moyenne UE >= 10 ET de toutes les UE
+#                   des semestres validés (ADM, ADC, AJ). (conforme à l'arrêté d'août 2005)
+#
+#   - règle "LMD": capitalisation uniquement des UE avec moy. > 10
+
+CONFIG.CAPITALIZE_ALL_UES = True # si vrai, capitalise toutes les UE des semestres validés (règle "LMD").
+
+
+#
+# -----------------------------------------------------
+#
+# -------------- Personnalisation des pages
+#
+# -----------------------------------------------------
+# Nom (chemin complet) d'un fichier .html à inclure juste après le <body>
+#  le <body> des pages ScoDoc
+CONFIG.CUSTOM_HTML_HEADER = ''
+
+# Fichier html a inclure en fin des pages (juste avant le </body>)
+CONFIG.CUSTOM_HTML_FOOTER = ''
+
+# Fichier .html à inclure dans la pages connexion/déconnexion (accueil)
+# si on veut que ce soit différent (par défaut la même chose)
+CONFIG.CUSTOM_HTML_HEADER_CNX = CONFIG.CUSTOM_HTML_HEADER
+CONFIG.CUSTOM_HTML_FOOTER_CNX = CONFIG.CUSTOM_HTML_FOOTER
+
+
+# -----------------------------------------------------
+#
+# -------------- Noms de Lycées
+#
+# -----------------------------------------------------
+
+# Fichier de correspondance codelycee -> noms
+# (chemin relatif au repertoire d'install des sources)
+CONFIG.ETABL_FILENAME = 'config/etablissements.csv'
+
+
+# ----------------------------------------------------
+CONFIG.ALLOW_NULL_PRENOM = False # True for UCAC (étudiants camerounais sans prénoms)
+
+CONFIG.ETUD_MAX_FILE_SIZE = 10*1024*1024 # taille max des fichiers archive etudiants (en octets)
+
+CONFIG.PUBLISH_PORTAL_PHOTO_URL = False # si pas de photo et portail, publie l'url (était vrai jusqu'en oct 2016)
+
+CONFIG.MIN_PASSWORD_LENGTH = 0 # si > 0: longueur minimale requise des nouveaux mots de passe (le test cracklib.FascistCheck s'appliquera dans tous les cas)
+
+# ----------------------------------------------------
+# Ce dictionnaire est fusionné à celui de sco_codes_parcours
+# pour définir les codes jury et explications associées
+CONFIG.CODES_EXPL = {
+    # AJ  : 'Ajourné (échec)',
+    
+    }
diff --git a/config/scodocutils.py b/config/scodocutils.py
new file mode 100644
index 0000000000000000000000000000000000000000..98a5be95949bc11dd83a6d1de467e9041a2670f9
--- /dev/null
+++ b/config/scodocutils.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+
+"""
+    Some utilities used by upgrade scripts
+"""
+
+
+import sys, os, psycopg2, glob, subprocess, traceback, time
+
+sys.path.append("..")
+
+
+def log(msg):
+    sys.stdout.flush()
+    sys.stderr.write(msg + "\n")
+    sys.stderr.flush()
+
+
+SCODOC_DIR = os.environ.get("SCODOC_DIR", "")
+if not SCODOC_DIR:
+    log("Error: environment variable SCODOC_DIR is not defined")
+    sys.exit(1)
+SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "")
+if not SCODOC_VAR_DIR:
+    log("Error: environment variable SCODOC_VAR_DIR is not defined")
+    sys.exit(1)
+SCODOC_LOGOS_DIR = os.environ.get("SCODOC_LOGOS_DIR", "")
+
+def get_dept_cnx_str(dept):
+    "db cnx string for dept"
+    f = os.path.join(SCODOC_VAR_DIR, "config", "depts", dept + ".cfg")
+    try:
+        return open(f).readline().strip()
+    except:
+        log("Error: can't read connexion string for dept %s" % dept)
+        log("(tried to open %s)" % f)
+        raise
+
+
+def get_users_cnx_str():
+    "db cnx string for users database (used only during upgrades to modify db schema)"
+    # uses default in sco_utils
+    # For customized installs, define the value here (used only during upgrades)
+    import sco_utils
+
+    return sco_utils.SCO_DEFAULT_SQL_USERS_CNX
+
+
+def get_depts():
+    "list of defined depts"
+    files = glob.glob(SCODOC_VAR_DIR + "/config/depts/*.cfg")
+    return [os.path.splitext(os.path.split(f)[1])[0] for f in files]
+
+
+def field_exists(cnx, table, field):
+    "true if field exists in sql table"
+    cursor = cnx.cursor()
+    cursor.execute(
+        "SELECT column_name FROM information_schema.columns WHERE table_name = '%s'"
+        % table
+    )
+    r = cursor.fetchall()
+    fields = [f[0] for f in r]
+    return field in fields
+
+
+def list_constraint(cnx, constraint_name=""):
+    "liste la contrainte (utile surtout pour savoir si elle existe)"
+    cursor = cnx.cursor()
+    cursor.execute(
+        "SELECT * FROM information_schema.table_constraints WHERE constraint_name = %(constraint_name)s",
+        {"constraint_name": constraint_name},
+    )
+    return cursor.fetchall()
+
+
+def list_table_index(cnx, table):
+    "liste les index associés à cette table"
+    cursor = cnx.cursor()
+    cursor.execute(
+        """SELECT t.relname as table_name, i.relname as index_name, a.attname as column_name 
+    FROM 
+        pg_class t, pg_class i, pg_index ix, pg_attribute a 
+    WHERE 
+        t.oid = ix.indrelid and i.oid = ix.indexrelid and a.attrelid = t.oid 
+        and a.attnum = ANY(ix.indkey) and t.relkind = 'r' 
+        and t.relname = %(table)s;
+    """,
+        {"table": table},
+    )
+    r = cursor.fetchall()
+    return [x[1] for x in r]  # ne garde que le nom de l'index
+
+
+def _run_sql(sql, cnx):
+    cursor = cnx.cursor()
+    error = False
+    try:
+        for cmd in sql:
+            log("executing SQL: %s" % cmd)
+            cursor.execute(cmd)
+            cnx.commit()
+    except:
+        cnx.rollback()
+        log("check_field: failure. Aborting transaction.")
+        error = True
+        traceback.print_exc()
+    return error
+
+
+def check_field(cnx, table, field, sql_create_commands):
+    "if field does not exists in table, run sql commands"
+    if not field_exists(cnx, table, field):
+        log("missing field %s in table %s: trying to create it" % (field, table))
+        error = _run_sql(sql_create_commands, cnx)
+        if not field_exists(cnx, table, field):
+            log("check_field: new field still missing !")
+            raise Exception("database configuration problem")
+        elif error:
+            log("\n\nAN UNEXPECTED ERROR OCCURRED WHILE UPGRADING DATABASE !\n\n")
+        else:
+            log("field %s added successfully." % field)
+
+
+def table_exists(cnx, table):
+    "true if SQL table exists"
+    cursor = cnx.cursor()
+    cursor.execute(
+        "SELECT table_name FROM information_schema.tables where table_name='%s'" % table
+    )
+    r = cursor.fetchall()
+    return len(r) > 0
+
+
+def check_table(cnx, table, sql_create_commands):
+    "if table does not exists in table, run sql commands"
+    if not table_exists(cnx, table):
+        log("missing table %s: trying to create it" % (table))
+        error = _run_sql(sql_create_commands, cnx)
+        if not table_exists(cnx, table):
+            log("check_table: new table still missing !")
+            raise Exception("database configuration problem")
+        elif error:
+            log("\n\nAN UNEXPECTED ERROR OCCURRED WHILE UPGRADING DATABASE !\n\n")
+        else:
+            log("table %s added successfully." % table)
+
+
+def sequence_exists(cnx, seq_name):
+    "true if SQL sequence exists"
+    cursor = cnx.cursor()
+    cursor.execute(
+        """SELECT relname FROM pg_class
+     WHERE relkind = 'S' and relname = '%s'
+     AND relnamespace IN (
+        SELECT oid FROM pg_namespace WHERE nspname NOT LIKE 'pg_%%' AND nspname != 'information_schema'
+     );
+    """
+        % seq_name
+    )
+    r = cursor.fetchall()
+    return len(r) > 0
+
+
+def function_exists(cnx, func_name):
+    "true if SQL function exists"
+    cursor = cnx.cursor()
+    cursor.execute(
+        """SELECT routine_name FROM information_schema.routines
+      WHERE specific_schema NOT IN ('pg_catalog', 'information_schema')
+      AND type_udt_name != 'trigger' 
+      AND routine_name = '%s';"""
+        % func_name
+    )
+    r = cursor.fetchall()
+    return len(r) > 0
diff --git a/config/set_passwd.sh b/config/set_passwd.sh
new file mode 100755
index 0000000000000000000000000000000000000000..2d024c3a8e86f4e5770a11db5776385bb5031da7
--- /dev/null
+++ b/config/set_passwd.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+#
+# ScoDoc: reglage du mot de passe admin Zope
+# (in Zope terminology, an emergency user)
+#
+# Doit �tre lanc� par l'utilisateur unix root dans le repertoire .../config
+#                       ^^^^^^^^^^^^^^^^^^^^^
+# E. Viennet, Juin 2008, Jul 2019
+#
+
+source config.sh
+source utils.sh
+
+
+if [ "$UID" != "0" ] 
+then
+  echo "Erreur: le script $0 doit etre lance par root"
+  exit 1
+fi
+
+echo "Creation d'un utilisateur d'urgence pour ScoDoc"
+echo "(utile en cas de perte de votre mot de passe admin)"
+
+if [ ${debian_version} != "10" ]
+then
+       mdir=/opt/zope213/lib/python2.7/site-packages/Zope2-2.13.21-py2.7.egg/Zope2/utilities
+else    
+       mdir=/opt/zope213/lib/python2.7/site-packages/Zope2/utilities
+fi
+
+python $mdir/zpasswd.py $SCODOC_DIR/../../access
+
+echo
+echo "redemarrer scodoc pour prendre en compte le mot de passe"
+echo
diff --git a/config/softs/get-iana.sh b/config/softs/get-iana.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f1a9f912a2e3a91c513f52ebccd06585963f48bd
--- /dev/null
+++ b/config/softs/get-iana.sh
@@ -0,0 +1,214 @@
+#!/bin/bash
+
+# $Id: get-iana.sh,v 1.15 2013/01/06 23:49:08 ktsaou Exp $
+#
+# $Log: get-iana.sh,v $
+# Revision 1.15  2013/01/06 23:49:08  ktsaou
+# Removed depedency to get-iana.sh
+# It is not usefull any more.
+#
+# Revision 1.14  2010/06/07 15:44:09  ktsaou
+# Made get-iana.sh support the latest IANA format.
+#
+# Revision 1.13  2010/04/08 22:03:08  ktsaou
+# Removed --proxy=off for wget.
+#
+# Revision 1.12  2008/03/17 22:08:43  ktsaou
+# Updated for latest IANA reservations format.
+#
+# Revision 1.11  2007/06/13 14:40:04  ktsaou
+# *** empty log message ***
+#
+# Revision 1.10  2007/05/05 23:38:31  ktsaou
+# Added support for external definitions of:
+#
+# RESERVED_IPS
+# PRIVATE_IPS
+# MULTICAST_IPS
+# UNROUTABLE_IPS
+#
+# in files under the same name in /etc/firehol/.
+# Only RESERVED_IPS is mandatory (firehol will complain if it is not there,
+# but it will still work without it), and is also the only file that firehol
+# checks how old is it. If it is 90+ days old, firehol will complain again.
+#
+# Changed the supplied get-iana.sh script to generate the RESERVED_IPS file.
+# FireHOL also instructs the user to use this script if the file is missing
+# or is too old.
+#
+# Revision 1.9  2007/04/29 19:34:11  ktsaou
+# *** empty log message ***
+#
+# Revision 1.8  2005/06/02 15:48:52  ktsaou
+# Allowed 127.0.0.1 to be in RESERVED_IPS
+#
+# Revision 1.7  2005/05/08 23:27:23  ktsaou
+# Updated RESERVED_IPS to current IANA reservations.
+#
+# Revision 1.6  2004/01/10 18:44:39  ktsaou
+# Further optimized and reduced PRIVATE_IPS using:
+# http://www.vergenet.net/linux/aggregate/
+#
+# The supplied get-iana.sh uses 'aggregate-flim' if it finds it in the path.
+# (aggregate-flim is the name of this program when installed on Gentoo)
+#
+# Revision 1.5  2003/08/23 23:26:50  ktsaou
+# Bug #793889:
+# Change #!/bin/sh to #!/bin/bash to allow FireHOL run on systems that
+# bash is not linked to /bin/sh.
+#
+# Revision 1.4  2002/10/27 12:44:42  ktsaou
+# CVS test
+#
+
+#
+# Program that downloads the IPv4 address space allocation by IANA
+# and creates a list with all reserved address spaces.
+#
+
+# IPV4_ADDRESS_SPACE_URL="http://www.iana.org/assignments/ipv4-address-space"
+IPV4_ADDRESS_SPACE_URL="http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.txt"
+
+# The program will match all rows in the file which start with a number, have a slash,
+# followed by another number, for which the following pattern will also match on the
+# same rows
+IANA_RESERVED="(RESERVED|UNALLOCATED)"
+
+# which rows that are matched by the above, to ignore
+# (i.e. not include them in RESERVED_IPS)?
+#IANA_IGNORE="(Multicast|Private use|Loopback|Local Identification)"
+IANA_IGNORE="Multicast"
+
+tempfile="/tmp/iana.$$.$RANDOM"
+
+AGGREGATE="`which aggregate-flim 2>/dev/null`"
+if [ -z "${AGGREGATE}" ]
+then
+	AGGREGATE="`which aggregate 2>/dev/null`"
+fi
+
+if [ -z "${AGGREGATE}" ]
+then
+	echo >&2
+	echo >&2
+	echo >&2 "WARNING"
+	echo >&2 "Please install 'aggregate-flim' to shrink the list of IPs."
+	echo >&2
+	echo >&2
+fi
+
+echo >&2
+echo >&2 "Fetching IANA IPv4 Address Space, from:"
+echo >&2 "${IPV4_ADDRESS_SPACE_URL}"
+echo >&2
+
+wget -O - "${IPV4_ADDRESS_SPACE_URL}"	|\
+	egrep "^ *[0-9]+/[0-9]+.*${IANA_RESERVED}"	|\
+	egrep -vi "${IANA_IGNORE}"			|\
+	sed "s/^ \+//g"					|\
+	cut -d ' ' -f 1					|\
+(
+	while IFS="/" read range net
+	do
+		# echo >&2 "$range/$net"
+		
+		if [ ! $net -eq 8 ]
+		then
+			echo >&2 "Cannot handle network masks of $net bits ($range/$net)"
+			continue
+		fi
+		 
+		first=`echo $range | cut -d '-' -f 1`
+		first=`expr $first + 0`
+		last=`echo $range | cut -d '-' -f 2`
+		last=`expr $last + 0`
+		
+		x=$first
+		while [ ! $x -gt $last ]
+		do
+			# test $x -ne 127 && echo "$x.0.0.0/$net"
+			echo "$x.0.0.0/$net"
+			x=$[x + 1]
+		done
+	done
+) | \
+(
+	if [ ! -z "${AGGREGATE}" -a -x "${AGGREGATE}" ]
+	then
+		"${AGGREGATE}"
+	else
+		cat
+	fi
+) >"${tempfile}"
+
+echo >&2 
+echo >&2 
+echo >&2 "FOUND THE FOLLOWING RESERVED IP RANGES:"
+printf "RESERVED_IPS=\""
+i=0
+for x in `cat ${tempfile}`
+do
+	i=$[i + 1]
+	printf "${x} "
+done
+printf "\"\n"
+
+if [ $i -eq 0 ]
+then
+	echo >&2 
+	echo >&2 
+	echo >&2 "Failed to find reserved IPs."
+	echo >&2 "Possibly the file format has been changed, or I cannot fetch the URL."
+	echo >&2 
+	
+	rm -f ${tempfile}
+	exit 1
+fi
+echo >&2
+echo >&2
+echo >&2 "Differences between the fetched list and the list installed in"
+echo >&2 "/etc/firehol/RESERVED_IPS:"
+
+echo >&2 "# diff /etc/firehol/RESERVED_IPS ${tempfile}"
+diff /etc/firehol/RESERVED_IPS ${tempfile}
+
+if [ $? -eq 0 ]
+then
+	touch /etc/firehol/RESERVED_IPS
+	echo >&2
+	echo >&2 "No differences found."
+	echo >&2
+	
+	rm -f ${tempfile}
+	exit 0
+fi
+
+echo >&2 
+echo >&2 
+echo >&2 "Would you like to save this list to /etc/firehol/RESERVED_IPS"
+echo >&2 "so that FireHOL will automatically use it from now on?"
+echo >&2
+while [ 1 = 1 ]
+do
+	printf >&2 "yes or no > "
+	read x
+	
+	case "${x}" in
+		yes)	cp -f /etc/firehol/RESERVED_IPS /etc/firehol/RESERVED_IPS.old 2>/dev/null
+			cat "${tempfile}" >/etc/firehol/RESERVED_IPS || exit 1
+			echo >&2 "New RESERVED_IPS written to '/etc/firehol/RESERVED_IPS'."
+			break
+			;;
+			
+		no)
+			echo >&2 "Saved nothing."
+			break
+			;;
+			
+		*)	echo >&2 "Cannot understand '${x}'."
+			;;
+	esac
+done
+
+rm -f ${tempfile}
+
diff --git a/config/softs/pyExcelerator-0.6.3a.patched.tgz b/config/softs/pyExcelerator-0.6.3a.patched.tgz
new file mode 100644
index 0000000000000000000000000000000000000000..32c6bf31928eb96494f1867db5e881ead16320e5
Binary files /dev/null and b/config/softs/pyExcelerator-0.6.3a.patched.tgz differ
diff --git a/config/softs/pydot-0.9.10.tar.gz b/config/softs/pydot-0.9.10.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..b923b58c6190c4a5ce1d69d5398d67cfef2e33fc
Binary files /dev/null and b/config/softs/pydot-0.9.10.tar.gz differ
diff --git a/config/upgrade.sh b/config/upgrade.sh
new file mode 100755
index 0000000000000000000000000000000000000000..32e34f654d85d153dc9673f96b4fdde1575a3a19
--- /dev/null
+++ b/config/upgrade.sh
@@ -0,0 +1,139 @@
+#!/bin/bash
+
+# Upgrade ScoDoc installation using SVN
+#   SVN must be properly configured and have read access to ScoDoc repository
+# This script STOP and RESTART ScoDoc and should be runned as root
+#
+# Upgrade also the Linux system using apt.
+#
+# Script for ScoDoc 7 (Debian 7, 8, 9, 10)
+#
+# E. Viennet, sep 2013, mar 2017, jun 2019, aug 2020
+
+cd /opt/scodoc/Products/ScoDoc/config
+source config.sh
+source utils.sh
+
+check_uid_root $0
+
+if [ -z "$SCODOC_UPGRADE_RUNNING" ]
+   then
+       apt-get update && apt-get -y dist-upgrade
+fi
+
+# Upgrade svn working copy if possible
+svnver=$(svn --version --quiet)
+if [[ ${svnver} > "1.7" ]]
+then
+ (cd "$SCODOC_DIR"; find . -name .svn -type d -exec dirname {} \; | xargs svn upgrade)
+fi
+
+echo "Stopping ScoDoc..."
+/etc/init.d/scodoc stop
+
+echo
+echo "Using SVN to update $SCODOC_DIR..."
+(cd "$SCODOC_DIR"; svn update)
+
+SVNVERSION=$(cd ..; svnversion)
+
+if [ ! -e "${SCODOC_VERSION_DIR}" ]; then
+  mkdir -p "${SCODOC_VERSION_DIR}"
+  chown www-data.www-data "${SCODOC_VERSION_DIR}"
+fi
+if [ ! -e "${SCODOC_VERSION_DIR}"/scodoc.sn ]; then
+  if [ -e "$SCODOC_DIR"/config/scodoc.sn ]; then
+      # migrate from old scodoc installs
+      mv "$SCODOC_DIR"/config/scodoc.sn "${SCODOC_VERSION_DIR}"
+  fi
+fi
+
+SN=$(cat "$SCODOC_DIR"/config/scodoc.sn 2> /dev/null || cat "${SCODOC_VERSION_DIR}"/scodoc.sn)
+if [ -e "$SCODOC_DIR"/config/scodoc.sn ] || [ -e "${SCODOC_VERSION_DIR}"/scodoc.sn ]
+then
+  if [[ ! "${SN}" =~ ^[0-9].* ]]
+  then
+    SN='' # fix for invalid previous replies
+  fi 
+  mode=upgrade
+else
+  mode=install  
+fi
+
+CMD="curl --fail --connect-timeout 5 --silent http://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/version?mode=$mode\&svn=${SVNVERSION}\&sn=${SN}"
+#echo $CMD
+SVERSION="$(${CMD})"
+
+if [ $? == 0 ]; then
+  #echo "answer=${SVERSION}" 
+  echo "${SVERSION}" > "${SCODOC_VERSION_DIR}"/scodoc.sn
+else
+  echo 'Warning: cannot connect to scodoc release server'  
+fi
+
+
+# Check that no Zope "access" file has been forgotten in the way:
+if [ -e $SCODOC_DIR/../../access ]
+then
+  mv $SCODOC_DIR/../../access $SCODOC_DIR/../../access.bak
+fi
+
+# Fix some permissions which may have been altered in the way:
+chsh -s /bin/sh $POSTGRES_USER # www-data, nologin in Debian 9
+chown root.www-data "$SCODOC_DIR" # important to create .pyc
+chmod 775 "${SCODOC_DIR}"
+chmod a+r "$SCODOC_DIR"/*.py
+
+chown -R root.www-data "$SCODOC_DIR"/config
+chmod 775 "$SCODOC_DIR"/config
+chmod a+rx "$SCODOC_DIR"/config/postupgrade-db.py
+chmod a+r "$SCODOC_DIR"/config/scodocutils.py
+
+chown -R root.www-data "$SCODOC_DIR"/misc
+chmod -R a+r "$SCODOC_DIR"/misc
+# depts dir:
+if [ ! -e "${SCODOC_VAR_DIR}/config/depts" ]
+then
+  mkdir "${SCODOC_VAR_DIR}/config/depts"
+fi
+# ScoDoc must be able to write to var directory:
+chown -R www-data.www-data "${SCODOC_VAR_DIR}"
+chmod -R u+w "${SCODOC_VAR_DIR}"
+
+# Important to create .pyc:
+chgrp -R www-data "${SCODOC_DIR}"/ZopeProducts
+chmod -R g+w "${SCODOC_DIR}"/ZopeProducts
+
+# Se recharge car ce fichier peut avoir change durant le svn up !
+if [ -z "$SCODOC_UPGRADE_RUNNING" ]
+then
+  export SCODOC_UPGRADE_RUNNING=1
+  ./upgrade.sh
+  exit 0
+fi
+
+# Add some python modules:
+"$SCODOC_DIR"/config/install_cal_modules.sh
+/opt/zope213/bin/python -c "import requests" >& /dev/null
+if [ $? -ne 0 ]
+then
+       /opt/zope213/bin/pip install requests
+fi
+
+# Ensure www-data can duplicate databases (for dumps)
+su -c $'psql -c \'alter role "www-data" with CREATEDB;\'' "$POSTGRES_SUPERUSER"
+#'
+
+# post-upgrade scripts
+echo "Executing post-upgrade script..."
+"$SCODOC_DIR"/config/postupgrade.py
+
+echo "Executing post-upgrade database script..."
+su -c "$SCODOC_DIR/config/postupgrade-db.py" $POSTGRES_USER
+
+# 
+echo
+echo "Starting ScoDoc..."
+/etc/init.d/scodoc start
+
+
diff --git a/config/utils.sh b/config/utils.sh
new file mode 100644
index 0000000000000000000000000000000000000000..8637b73638a4f70f79ae13aa1f81796c47f6d021
--- /dev/null
+++ b/config/utils.sh
@@ -0,0 +1,57 @@
+
+# Misc utilities for ScoDoc install shell scripts
+
+to_lower() {
+  echo $1 | tr "[:upper:]" "[:lower:]" 
+} 
+
+to_upper() {
+  echo $1 | tr "[:lower:]" "[:upper:]" 
+} 
+
+norm_ans() {
+  x=$(to_upper $1 | tr O Y)
+  echo ${x:0:1}
+}
+
+check_uid_root() {
+  if [ "$UID" != "0" ] 
+  then
+    echo "Erreur: le script $1 doit etre lance par root"
+    exit 1
+  fi
+}
+
+terminate() {
+  echo 
+  echo "Erreur: $1"
+  echo
+  exit 1
+}
+
+# --- Ensure postgres user www-data exists
+init_postgres_user() { # run as root
+  if [ -z $(echo "select usename from pg_user;" | su -c "$PSQL -d template1  -p $POSTGRES_PORT" $POSTGRES_SUPERUSER | grep $POSTGRES_USER) ]
+  then
+   # add database user
+   echo "Creating postgresql user $POSTGRES_USER"
+   su -c "createuser  -p $POSTGRES_PORT --no-superuser --no-createdb --no-adduser --no-createrole ${POSTGRES_USER}" $POSTGRES_SUPERUSER
+  fi
+}
+
+
+# XXX inutilise 
+gen_passwd() { 
+  PASSWORD_LENGTH="8"
+  ALLOWABLE_ASCII="~@#$%^&*()_+=-?><0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+  SEED=$(head -c4 /dev/urandom | od -t u4 | awk '{ print $2 }')
+  RANDOM=$SEED
+  n=1
+  password=""
+  while [ "$n" -le "$PASSWORD_LENGTH" ]
+  do
+    password="$password${ALLOWABLE_ASCII:$(($RANDOM%${#ALLOWABLE_ASCII})):1}"
+  n=$((n+1))
+  done
+  echo $password
+}
diff --git a/csv2rules.py b/csv2rules.py
new file mode 100755
index 0000000000000000000000000000000000000000..c99c93432518181a6aee272fd68af44bd5f2f100
--- /dev/null
+++ b/csv2rules.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Converti fichier CSV decrivant regles parcours DUT
+en "regles" python.
+
+CSV: utf-8/Euro, tab, no text delimiter
+(le faire avec OpenOffice ou NeoOffice...)
+
+"""
+import sys, pdb
+
+sourcefile = sys.argv[1]  # fichier CSV
+
+ALL = "ALL"
+
+HEAD = """# -*- coding: utf-8 -*-
+#
+# Generated by csv2rules.py    *** DO NOT EDIT ***
+#
+# Command: %s %s
+# 
+from sco_codes_parcours import *
+
+rules_source_file='%s'
+
+""" % (
+    sys.argv[0],
+    sys.argv[1],
+    sourcefile,
+)
+
+from sco_codes_parcours import *
+
+
+def _fmt(s):
+    if not s:
+        return None
+    if strlower(s) in ("ok", "oui", "o", "y", "yes"):
+        return True
+    if strlower(s) in ("no", "non"):
+        return False
+    if s == "*":
+        return ALL
+    return s
+
+
+colidx_code_sem = 7  # index colonne 'code_sem' dans feuille CSV
+iue = colidx_code_sem + 1
+icodeprev = 1  # idex col "Prev"
+
+
+def genrules(csv):
+    r = []
+    linenum = 1
+    try:
+        for line in csv:
+            line = line.strip()
+            if line:
+                if line[0] == "#":
+                    r.append(line)
+                else:
+                    fs = [_fmt(s) for s in line.split("\t")]
+                    if not "," in fs[icodeprev]:
+                        fs[icodeprev] = fs[icodeprev] + ","  # liste codes prev
+                    if fs[iue] and not "," in fs[iue]:
+                        fs[iue] = fs[iue] + ","  # liste codes UE
+                    if fs[iue]:
+                        fs[iue] = "(" + fs[iue] + ")"
+                    else:
+                        fs[iue] = "()"
+                    fs[-1] = "'" + fs[-1].replace("'", "\\'") + "'"
+                    r.append(
+                        "( '%s', ((%s), %s, %s, %s, %s, %s),"
+                        % tuple(fs[:colidx_code_sem])
+                    )
+                    r.append(
+                        "  (%s, %s, %s, %s, %s, %s) )," % tuple(fs[colidx_code_sem:])
+                    )
+            linenum += 1
+    except:
+        sys.stderr.write("error line %d on\n%s\n" % (linenum, csv[linenum]))
+        raise
+    return (
+        HEAD
+        + "DUTRules = [ DUTRule(rid, p, c) for (rid, p,c) in (\n"
+        + "\n".join(r)
+        + "\n)]"
+    )
+
+
+print(genrules(open(sourcefile).readlines()))
diff --git a/debug.py b/debug.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8c0d763fae2bc1e264c772ab9f47bbb6b409226
--- /dev/null
+++ b/debug.py
@@ -0,0 +1,116 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Configuration pour debugguer en mode console
+
+Lancer ScoDoc ainsi: (comme root)
+
+ /opt/scodoc/bin/zopectl debug 
+
+Puis
+
+from debug import *
+context = go(app)  
+# ou 
+context = go_dept(app, 'CJ')
+
+authuser = app.acl_users.getUserById('admin')
+# ou app.ScoDoc.acl_users.getUserById('toto') pour un utilisateur scodoc
+authuser = authuser.__of__(app.acl_users)
+
+Exemple:
+sems = context.Notes.formsemestre_list()
+formsemestre_id = sems[0]['formsemestre_id']
+
+# Affiche tous les semestres:
+for sem in sems:
+    print sem['formsemestre_id'], sem['titre_num']
+
+# Recupere la table de notes:
+nt = context.Notes._getNotesCache().get_NotesTable(context.Notes, formsemestre_id)
+
+
+"""
+from notesdb import *
+from notes_log import log
+from sco_utils import *
+
+from gen_tables import GenTable
+import sco_archives
+import sco_groups
+import sco_evaluations
+import sco_formsemestre
+import sco_formsemestre_edit
+import sco_compute_moy
+import sco_parcours_dut
+import sco_codes_parcours
+import sco_bulletins
+import sco_excel
+import sco_formsemestre_status
+import sco_bulletins_xml
+
+# Prend le premier departement comme context
+
+
+def go(app, n=0):
+    context = app.ScoDoc.objectValues("Folder")[n].Scolarite
+    print("context in dept ", context.DeptId())
+    return context
+
+
+def go_dept(app, dept):
+    objs = app.ScoDoc.objectValues("Folder")
+    for o in objs:
+        context = o.Scolarite
+        if context.DeptId() == dept:
+            print("context in dept ", context.DeptId())
+            return context
+    raise ValueError("dep %s not found" % dept)
+
+
+class FakeUser:
+    def __init__(self, name):
+        self.name = name
+
+    def __str__(self):
+        return self.name
+
+    def has_permission(self, op, context):
+        return True
+
+
+class DummyResponse:
+    """Emulation vide de Reponse http Zope"""
+
+    def __init__(self):
+        self.header = {}
+        self.redirected = ""
+
+    def setHeader(self, name, value):
+        self.header[name] = value
+
+    def redirect(self, url):
+        self.redirected = url
+
+
+class DummyRequest:
+    """Emulation vide de Request Zope"""
+
+    def __init__(self):
+        self.RESPONSE = DummyResponse()
+        self.AUTHENTICATED_USER = FakeUser("admin")
+        self.form = {}
+        self.URL = "http://scodoc/"
+        self.URL1 = self.URL
+        self.URL0 = self.URL
+        self.BASE0 = "localhost"
+        self.REMOTE_ADDR = "127.0.0.1"
+        self.HTTP_REFERER = ""
+        self.REQUEST_METHOD = "get"
+        self.QUERY_STRING = ""
+
+
+REQUEST = DummyRequest()
+
+# handful shorcut
+pp = pprint.pprint
diff --git a/dtml/docLogin.dtml b/dtml/docLogin.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..fd46ec9f8bebb0af92dcca1c6957828f3f7db4a2
--- /dev/null
+++ b/dtml/docLogin.dtml
@@ -0,0 +1,49 @@
+<dtml-var standard_html_header>
+<center>
+<dtml-if authFailedCode>
+<dtml-call "REQUEST.set('loginTitle', getAuthFailedMessage(authFailedCode))">
+<dtml-else>
+<dtml-call "REQUEST.set('loginTitle', 'Identifiez vous sur ScoDoc')">
+</dtml-if>
+<dtml-var "DialogHeader(_.None,_,DialogTitle=loginTitle)">
+<P>
+<dtml-if destination>
+<FORM ACTION="&dtml-destination;" METHOD="POST">
+<dtml-else>
+<FORM ACTION="&dtml-URL;" METHOD="POST">
+</dtml-if>
+
+<dtml-var "query_string_to_form_inputs(QUERY_STRING)"> <dtml-comment> Added by Emmanuel for ScoDoc</dtml-comment>
+
+
+<TABLE>
+<TR>
+  <TD ALIGN="LEFT" VALIGN="TOP">
+  <STRONG><dtml-babel src="'en'">Nom</dtml-babel></STRONG>
+  </TD>
+  <TD ALIGN="LEFT" VALIGN="TOP">
+  <INPUT TYPE="TEXT" NAME="__ac_name" SIZE="20">
+  </TD>
+</TR>
+
+<TR>
+  <TD ALIGN="LEFT" VALIGN="TOP">
+  <STRONG><dtml-babel src="'en'">Mot de passe</dtml-babel></STRONG>
+  </TD>
+  <TD ALIGN="LEFT" VALIGN="TOP">
+  <INPUT TYPE="PASSWORD" NAME="__ac_password" SIZE="20">
+  </TD>
+</TR>
+<TR>
+  <TD ALIGN="LEFT" VALIGN="TOP">
+  </TD>
+</TR>
+</TABLE>
+<center>
+<INPUT TYPE="SUBMIT" NAME="submit" VALUE=" <dtml-babel src="'en'">Ok</dtml-babel> ">
+</center>
+</FORM>
+<br>
+<dtml-var DialogFooter>
+</center>
+<dtml-var standard_html_footer>
diff --git a/dtml/docLogout.dtml b/dtml/docLogout.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..5210fc745ded506c448389a3b7e1dfc4a4703ff3
--- /dev/null
+++ b/dtml/docLogout.dtml
@@ -0,0 +1,16 @@
+<dtml-var standard_html_header>
+<P>
+<CENTER>
+<p>Vous êtes déconnecté de ScoDoc.
+</p>
+
+<p><a href="<dtml-var "BASE0">">revenir à l'accueil</a></p>
+
+<br/>
+<p><em style="color: red;">(Attention: si vous êtes administrateur, vous ne pouvez vous déconnecter complètement qu'en relançant votre navigateur)
+</em></p>
+</CENTER>
+
+
+
+<dtml-var standard_html_footer>
diff --git a/dtml/entreprises/entreprise_contact_create.dtml b/dtml/entreprises/entreprise_contact_create.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..e0c3ab058bdafe082c784d6f17e91361e1c36e1d
--- /dev/null
+++ b/dtml/entreprises/entreprise_contact_create.dtml
@@ -0,0 +1,62 @@
+<dtml-var "entreprise_header(REQUEST=REQUEST)">
+
+<dtml-call "REQUEST.set( 'E', do_entreprise_list( args={ 'entreprise_id' : entreprise_id } )[0])">
+
+<dtml-call "REQUEST.set( 'curtime', ZopeTime().strftime('%d/%m/%Y') )">
+<dtml-call "REQUEST.set( 'link_create_corr', make_link_create_corr(entreprise_id) )">
+<dtml-call "REQUEST.set( 'correspondants', 
+                         do_entreprise_correspondant_listnames( args={ 'entreprise_id' : entreprise_id }) )">
+<dtml-if "len(correspondants) == 0">
+   <dtml-call "REQUEST.set( 'correspondants', [ ('inconnu','') ])">
+</dtml-if>
+
+<h2 class="entreprise_contact">Nouveau "contact" avec l'entreprise <dtml-var "E['nom']"></h2>
+
+
+<dtml-call "REQUEST.set('tf', TrivialFormulator( URL0, REQUEST.form, 
+  ( 
+    ('entreprise_id', { 'input_type' : 'hidden', 'default' : entreprise_id }),
+
+    ('type_contact', { 'input_type' : 'menu', 'title' : 'Objet',
+                       'allowed_values' : ('Prospection', 'Stage étudiant', 
+                                           'Contrat Apprentissage DUT GTR1', 
+                                           'Contrat Apprentissage DUT GTR2',
+                                           'Contrat Apprentissage Licence SQRT',
+                                           'Projet', 'Autre' ),
+                       'default' : 'Stage étudiant'
+                     }),
+
+    ('date', { 'size' : 12, 'title' : 'Date du contact (j/m/a)', 'allow_null' : False, 
+                'default':   curtime }),  
+
+    ('entreprise_corresp_id', {  'input_type' : 'menu', 'title' : 'Correspondant entreprise', 
+                         'explanation' : link_create_corr, 'allow_null' : True,
+                         'labels' : [ x[0] for x in correspondants ],
+                         'allowed_values' : [ x[1] for x in correspondants ] }),
+
+    ('etudiant', { 'size' : 16, 'title' : 'Etudiant concerné', 'allow_null' : True,
+                    'explanation' : 'nom (si pas ambigu) ou code' }),
+    ('enseignant', { 'size' : 16, 'title' : 'Enseignant (tuteur)', 'allow_null' : True }),
+    ('description',     { 'input_type' : 'textarea', 'rows' : 3, 'cols': 40, 'title' : 'Description' }),
+
+   ),
+   cancelbutton='Annuler',
+   submitlabel = 'Ajouter ce contact', readonly = REQUEST['_read_only']
+))">
+
+<dtml-call "REQUEST.set( 'etudok', do_entreprise_check_etudiant(etudiant) )">
+
+<dtml-if "(tf[0] == 0) or (etudok[0] == 0)">
+ <dtml-if "etudok[0] == 0">
+  <p class="entreprise_warning"><dtml-var "etudok[1]"></p>
+ </dtml-if>
+ <dtml-var "tf[1]">
+<dtml-elif "tf[0] == -1">
+ <dtml-call "RESPONSE.redirect( URL1 )">
+<dtml-else>
+  <dtml-call "tf[2].update( { 'etudid' : etudok[1] })">
+  <dtml-var "REQUEST.set( 'matiere_id', do_entreprise_contact_create( tf[2] ) )">
+  <dtml-call "RESPONSE.redirect( URL1 )">
+</dtml-if tf>
+
+<dtml-var "entreprise_footer(REQUEST=REQUEST)">
\ No newline at end of file
diff --git a/dtml/entreprises/entreprise_contact_delete.dtml b/dtml/entreprises/entreprise_contact_delete.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..df73659b9158068a3267802bceee892400b7edfa
--- /dev/null
+++ b/dtml/entreprises/entreprise_contact_delete.dtml
@@ -0,0 +1,27 @@
+<dtml-var "entreprise_header(REQUEST=REQUEST)">
+
+<dtml-call "REQUEST.set( 'F', do_entreprise_contact_list( args={ 'entreprise_contact_id' : entreprise_contact_id } )[0])">
+
+<h2>Suppression du contact</h2>
+
+
+<dtml-call "REQUEST.set('tf', TrivialFormulator( URL0, REQUEST.form, 
+  ( ('entreprise_contact_id', { 'input_type' : 'hidden' }),
+   ),
+   initvalues = F,
+   submitlabel = 'Confirmer la suppression',
+   cancelbutton = 'Annuler', readonly = REQUEST['_read_only']
+))">
+
+<dtml-if "tf[0] == 0">
+ <dtml-var "tf[1]">
+<dtml-else>
+ <dtml-if "tf[0] == -1">
+    <dtml-call "RESPONSE.redirect( URL1 )">
+ <dtml-else>
+    <dtml-call "do_entreprise_contact_delete( F['entreprise_contact_id'] )">
+    <dtml-call "RESPONSE.redirect(URL1)">
+ </dtml-if>
+</dtml-if>
+
+<dtml-var "entreprise_footer(REQUEST=REQUEST)">
\ No newline at end of file
diff --git a/dtml/entreprises/entreprise_contact_edit.dtml b/dtml/entreprises/entreprise_contact_edit.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..5fe437f14b1124cb0c343db357f35a7adc8546e8
--- /dev/null
+++ b/dtml/entreprises/entreprise_contact_edit.dtml
@@ -0,0 +1,62 @@
+<dtml-call "REQUEST.set( 'F', do_entreprise_contact_list( args={ 'entreprise_contact_id' : entreprise_contact_id } )[0])">
+<dtml-call "REQUEST.set( 'entreprise_id', F['entreprise_id'])">
+<dtml-call "REQUEST.set( 'link_create_corr', make_link_create_corr(F['entreprise_id']) )">
+<dtml-call "REQUEST.set( 'E', do_entreprise_list( args={ 'entreprise_id' : F['entreprise_id'] } )[0])">
+<dtml-call "REQUEST.set( 'correspondants', 
+                         do_entreprise_correspondant_listnames( args={ 'entreprise_id' : F['entreprise_id'] })+ [ ('inconnu','') ])">
+
+<dtml-var "entreprise_header(REQUEST=REQUEST)">
+<h2  class="entreprise_contact"><dtml-var title_or_id></h2>
+
+
+<h3>Contact avec entreprise <dtml-var "E['nom']"></h3>
+
+<dtml-call "REQUEST.set('tf', TrivialFormulator( URL0, REQUEST.form, 
+  ( ('entreprise_contact_id', { 'default' : entreprise_contact_id, 'input_type' : 'hidden' }),
+ 
+    ('entreprise_id', { 'input_type' : 'hidden', 'default' : F['entreprise_id'] }),
+
+    ('type_contact', { 'input_type' : 'menu', 'title' : 'Objet',
+                       'allowed_values' : ('Prospection', 'Stage étudiant', 'Contrat Apprentissage', 'Projet', 'Autre' ),
+                     }),
+
+    ('date', { 'size' : 12, 'title' : 'Date du contact (j/m/a)', 'allow_null' : False }),  
+
+    ('entreprise_corresp_id', {  'input_type' : 'menu', 'title' : 'Correspondant entreprise', 
+                         'explanation' : link_create_corr, 
+                         'allow_null' : True,
+                         'labels' : [ x[0] for x in correspondants ],
+                         'allowed_values' : [ x[1] for x in correspondants ] }),
+
+    ('etudiant', { 'size' : 16, 'title' : 'Etudiant concerné', 'allow_null' : True,
+                    'default' : F['etudid'],
+                    'explanation' : 'nom (si pas ambigu) ou code' }),
+    ('enseignant', { 'size' : 16, 'title' : 'Enseignant (tuteur)', 'allow_null' : True }),
+    ('description',     { 'input_type' : 'textarea', 'rows' : 3, 'cols': 40, 'title' : 'Description' }),
+   ),
+   cancelbutton = 'Annuler',
+   initvalues = F,
+   submitlabel = 'Modifier les valeurs', readonly = REQUEST['_read_only']
+))">
+
+<dtml-call "REQUEST.set( 'etudok', do_entreprise_check_etudiant(etudiant) )">
+
+<dtml-if "(tf[0] == 0) or (etudok[0] == 0)">
+ <dtml-if "etudok[0] == 0">
+  <p class="entreprise_warning"><dtml-var "etudok[1]"></p>
+ </dtml-if>
+
+ <dtml-var "tf[1]">
+ <dtml-unless "REQUEST['_read_only']">
+ <p class="entreprise_descr"><a class="entreprise_delete" href="entreprise_contact_delete?entreprise_contact_id=<dtml-var entreprise_contact_id>">Supprimer ce contact</a> </p>
+ </dtml-unless>
+<dtml-elif "tf[0] == -1">
+  <dtml-call "RESPONSE.redirect( URL1 )" >
+<dtml-else>
+  <dtml-call "tf[2].update( { 'etudid' : etudok[1] })">
+  <dtml-call "do_entreprise_contact_edit( tf[2] )">
+  <dtml-call "RESPONSE.redirect( URL1 + '/entreprise_contact_list?entreprise_id=' + str(F['entreprise_id']) )" >
+</dtml-if>
+
+
+<dtml-var "entreprise_footer(REQUEST=REQUEST)">
\ No newline at end of file
diff --git a/dtml/entreprises/entreprise_contact_list.dtml b/dtml/entreprises/entreprise_contact_list.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..cffeb3512dbd95a4922d4138649fe9e5bf9c7e03
--- /dev/null
+++ b/dtml/entreprises/entreprise_contact_list.dtml
@@ -0,0 +1,54 @@
+<dtml-var "entreprise_header(REQUEST=REQUEST)">
+
+<dtml-if entreprise_id>
+<dtml-call "REQUEST.set( 'E', do_entreprise_list( args={ 'entreprise_id' : entreprise_id } )[0])">
+<dtml-call "REQUEST.set( 'C', do_entreprise_contact_list( args={ 'entreprise_id' : entreprise_id }) )">
+
+<h2 class="entreprise_contact">Listes des contacts avec l'entreprise <dtml-var "E['nom']"> </h2>
+
+<dtml-else>
+<h2 class="entreprise_contact">Listes des contacts</h2>
+<dtml-call "REQUEST.set( 'C', do_entreprise_contact_list( args={ }))">
+</dtml-if entreprise_id>
+
+<table class="contact_list">
+
+<tr><th>Date</th><th>Objet</th><dtml-unless entreprise_id><th>Entreprise</th></dtml-unless><th>Etudiant</th><th>Description</th></tr>
+
+<dtml-in "C">
+ <dtml-let F="_['sequence-item']">
+ <dtml-call "REQUEST.set( 'Ec', do_entreprise_list( args={ 'entreprise_id' : F['entreprise_id'] } )[0])">
+
+ <tr class="<dtml-if sequence-odd>contact_list_odd<dtml-else>contact_list_even</dtml-if>">
+
+ <td><a class="contact_edit" href="entreprise_contact_edit?entreprise_contact_id=<dtml-var "F['entreprise_contact_id']">">
+   <dtml-var "F['date']">
+ </td>
+
+ <td class="contact_descr"><dtml-var "F['type_contact']"></td>
+ <dtml-unless entreprise_id><td class="contact_descr"><dtml-var "Ec['nom']"></td></dtml-unless>
+ <td class="contact_descr">
+ <dtml-if "F['etudid']">
+ <dtml-in "getEtudInfo(etudid=F['etudid'],filled=1)" mapping>
+   <a href="<dtml-var ScoURL>/ficheEtud?etudid=<dtml-var "F['etudid']">"><dtml-var nomprenom></a>
+ </dtml-in>
+ </dtml-if etudid>
+ </td>
+ <td class="contact_descr"><dtml-var "F['description']"></td>
+ </tr>
+ </dtml-let F>
+
+<dtml-else>
+<tr><td colspan="4"><font color="red"><em>Aucun contact !</em></font></td></tr>
+</dtml-in>
+</table>
+
+<dtml-unless "REQUEST['_read_only']">
+<dtml-if entreprise_id>
+<p class="entreprise_create">
+<a class="entreprise_create" href="entreprise_contact_create?entreprise_id=<dtml-var "E['entreprise_id']">">nouveau "contact"</a>
+</p>
+</dtml-if>
+</dtml-unless>
+
+<dtml-var "entreprise_footer(REQUEST=REQUEST)">
\ No newline at end of file
diff --git a/dtml/entreprises/entreprise_correspondant_create.dtml b/dtml/entreprises/entreprise_correspondant_create.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..2ffdbba441348a27b0bb882c4d257c4220b3c214
--- /dev/null
+++ b/dtml/entreprises/entreprise_correspondant_create.dtml
@@ -0,0 +1,43 @@
+<dtml-var "entreprise_header(REQUEST=REQUEST)">
+
+<dtml-call "REQUEST.set( 'UE', do_entreprise_list( args={ 'entreprise_id' : entreprise_id } )[0])">
+
+
+<h2 class="entreprise_correspondant"><dtml-var title_or_id> dans l'entreprise <dtml-var "UE['nom']"></h2>
+
+
+
+<dtml-call "REQUEST.set('tf', TrivialFormulator( URL0, REQUEST.form, 
+  ( ('entreprise_id', { 'input_type' : 'hidden', 'default' : entreprise_id }),
+    ('civilite',      { 'input_type' : 'menu', 
+                         'labels' : [ 'M.', 'Mme' ], 'allowed_values' : [ 'M.', 'Mme' ] }),  
+    ('nom',       { 'size' : 25, 'title' : 'Nom', 'allow_null' : False }),  
+    ('prenom',    { 'size' : 25, 'title' : 'Prénom' }),   
+    ('fonction',  { 'input_type' : 'menu', 
+                    'allowed_values' : ('Directeur', 'RH', 'Resp. Administratif', 'Tuteur', 'Autre'),
+                    'default' : 'Tuteur',
+                    'explanation' : 'fonction via à vis de l\'IUT'  }),   
+    ('phone1',   { 'size' : 14, 'title' : 'Téléphone 1', }),
+    ('phone2',   { 'size' : 14, 'title' : 'Téléphone 2', }),
+    ('mobile',   { 'size' : 14, 'title' : 'Tél. mobile', }),
+    ('fax',   { 'size' : 14, 'title' : 'Fax', }),
+    ('mail1',   { 'size' : 25, 'title' : 'e-mail', }),
+    ('mail2',   { 'size' : 25, 'title' : 'e-mail 2', }),
+
+    ('note',     { 'input_type' : 'textarea', 'rows' : 3, 'cols': 40, 'title' : 'Note' }),
+
+   ),
+   cancelbutton='Annuler',
+   submitlabel = 'Ajouter ce correspondant', readonly = REQUEST['_read_only']
+))">
+
+<dtml-if "tf[0] == 0">
+ <dtml-var "tf[1]">
+<dtml-else>
+ <dtml-var "REQUEST.set( 'matiere_id', do_entreprise_correspondant_create( tf[2] ) )">
+
+ <dtml-call "RESPONSE.redirect( URL1 )">
+
+</dtml-if>
+
+<dtml-var "entreprise_footer(REQUEST=REQUEST)">
\ No newline at end of file
diff --git a/dtml/entreprises/entreprise_correspondant_delete.dtml b/dtml/entreprises/entreprise_correspondant_delete.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..ac50550ee3b54faa9399db543a7e9c8cdc354117
--- /dev/null
+++ b/dtml/entreprises/entreprise_correspondant_delete.dtml
@@ -0,0 +1,27 @@
+<dtml-var "entreprise_header(REQUEST=REQUEST)">
+
+<dtml-call "REQUEST.set( 'F', do_entreprise_correspondant_list( args={ 'entreprise_corresp_id' : entreprise_corresp_id } )[0])">
+
+<h2>Suppression du correspondant <dtml-var "F['nom']"> <dtml-var "F['prenom']"></h2>
+
+
+<dtml-call "REQUEST.set('tf', TrivialFormulator( URL0, REQUEST.form, 
+  ( ('entreprise_corresp_id', { 'input_type' : 'hidden' }),
+   ),
+   initvalues = F,
+   submitlabel = 'Confirmer la suppression',
+   cancelbutton = 'Annuler', readonly = REQUEST['_read_only']
+))">
+
+<dtml-if "tf[0] == 0">
+ <dtml-var "tf[1]">
+<dtml-else>
+ <dtml-if "tf[0] == -1">
+    <dtml-call "RESPONSE.redirect( URL1 )">
+ <dtml-else>
+    <dtml-call "do_entreprise_correspondant_delete( F['entreprise_corresp_id'] )">
+    <dtml-call "RESPONSE.redirect(URL1)">
+ </dtml-if>
+</dtml-if>
+
+<dtml-var "entreprise_footer(REQUEST=REQUEST)">
diff --git a/dtml/entreprises/entreprise_correspondant_edit.dtml b/dtml/entreprises/entreprise_correspondant_edit.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..2b254568bfeb5c9171f726cc3558adeaa8520ab6
--- /dev/null
+++ b/dtml/entreprises/entreprise_correspondant_edit.dtml
@@ -0,0 +1,41 @@
+<dtml-call "REQUEST.set( 'F', do_entreprise_correspondant_list( args={ 'entreprise_corresp_id' : entreprise_corresp_id } )[0])">
+<dtml-call "REQUEST.set( 'entreprise_id', F['entreprise_id'])">
+
+<dtml-var "entreprise_header(REQUEST=REQUEST)">
+<h2 class="entreprise_correspondant"><dtml-var title_or_id></h2>
+
+<dtml-call "REQUEST.set('tf', TrivialFormulator( URL0, REQUEST.form, 
+  ( ('entreprise_corresp_id', { 'default' : entreprise_corresp_id, 'input_type' : 'hidden' }),
+    ('civilite',      { 'input_type' : 'menu', 
+                         'labels' : [ 'M.', 'Mme' ], 'allowed_values' : [ 'M.', 'Mme' ] }),  
+   ('nom',       { 'size' : 25, 'title' : 'Nom', 'allow_null' : False }),  
+    ('prenom',    { 'size' : 25, 'title' : 'Prénom' }),   
+    ('fonction',  { 'input_type' : 'menu', 
+                    'allowed_values' : ('Directeur', 'RH', 'Resp. Administratif', 'Tuteur', 'Autre'),
+                    'explanation' : 'fonction via à vis de l\'IUT'  }),   
+    ('phone1',   { 'size' : 14, 'title' : 'Téléphone 1', }),
+    ('phone2',   { 'size' : 14, 'title' : 'Téléphone 2', }),
+    ('mobile',   { 'size' : 14, 'title' : 'Tél. mobile', }),
+    ('fax',   { 'size' : 14, 'title' : 'Fax', }),
+    ('mail1',   { 'size' : 25, 'title' : 'e-mail', }),
+    ('mail2',   { 'size' : 25, 'title' : 'e-mail 2', }),
+
+    ('note',     { 'input_type' : 'textarea', 'rows' : 3, 'cols': 40, 'title' : 'Note' }),
+   ),
+   cancelbutton = 'Annuler',
+   initvalues = F,
+   submitlabel = 'Modifier les valeurs', readonly = REQUEST['_read_only']
+))">
+
+<dtml-if "tf[0] == 0">
+ <dtml-var "tf[1]">
+<dtml-elif "tf[0] == -1">
+  <dtml-call "RESPONSE.redirect( REQUEST.HTTP_REFERER )" >
+    <dtml-call "RESPONSE.redirect( URL1 + '/entreprise_correspondant_list?entreprise_id=' + str(F['entreprise_id']) )" >
+<dtml-else>
+  <dtml-call "do_entreprise_correspondant_edit( tf[2] )">
+    <dtml-call "RESPONSE.redirect( URL1 + '/entreprise_correspondant_list?entreprise_id=' + str(F['entreprise_id']) )" >
+</dtml-if>
+
+
+<dtml-var "entreprise_footer(REQUEST=REQUEST)">
diff --git a/dtml/entreprises/entreprise_correspondant_list.dtml b/dtml/entreprises/entreprise_correspondant_list.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..085d4375a42f46278ba2d0d0216817704604c3b8
--- /dev/null
+++ b/dtml/entreprises/entreprise_correspondant_list.dtml
@@ -0,0 +1,52 @@
+<dtml-var "entreprise_header(REQUEST=REQUEST)">
+
+<dtml-call "REQUEST.set( 'E', do_entreprise_list( args={ 'entreprise_id' : entreprise_id } )[0])">
+
+<h2>Listes des correspondants  dans l'entreprise <dtml-var "E['nom']"> </h2>
+
+<table class="corr_list">
+
+<tr><th>Nom</th><th>Entreprise</th><th>Fonction</th><th>Téléphone</th><th>Mail</th><th>Note</th></tr>
+
+<dtml-in "do_entreprise_correspondant_list( args={ 'entreprise_id' : entreprise_id })">
+ <dtml-let F="_['sequence-item']">
+ 
+ <tr class="<dtml-if sequence-odd>corr_list_odd<dtml-else>corr_list_even</dtml-if>">
+
+ <td><a class="corr_edit" href="entreprise_correspondant_edit?entreprise_corresp_id=<dtml-var "F['entreprise_corresp_id']">">
+   <dtml-if "F['nom']"><dtml-var "F['nom'].lower().capitalize()"></dtml-if>
+   <dtml-if "F['prenom']"> <dtml-var "F['prenom'].lower().capitalize()"></dtml-if>
+   </a>
+ </td>
+
+ <td class="corr_descr"><dtml-var "E['nom']"></td>
+
+ <td class="corr_descr"><dtml-var "F['fonction']"></td>
+ 
+ <td class="corr_descr">
+  <dtml-if "F['phone1']"><dtml-var "F['phone1']"></dtml-if>
+  <dtml-if "F['phone2']"> / <dtml-var "F['phone2']"></dtml-if>
+  <dtml-if "F['mobile']"> / <dtml-var "F['mobile']"></dtml-if>
+ </td>
+ 
+ <td class="corr_descr">
+  <dtml-if "F['mail1']"> <a href="mailto:<dtml-var "F['mail1']">"><dtml-var "F['mail1']"></a></dtml-if>
+  <dtml-if "F['mail1']"> <br> <a href="mailto:<dtml-var "F['mail2']">"><dtml-var "F['mail2']"></a></dtml-if>
+ </td>
+
+ <td class="corr_descr"><dtml-var "F['note']"></td>
+
+ <td class="corr_descr"><a class="corr_delete" href="entreprise_correspondant_edit?entreprise_corresp_id=<dtml-var "F['entreprise_corresp_id']">">modifier</a> </td>
+ <td class="corr_descr"><a class="corr_delete" href="entreprise_correspondant_delete?entreprise_corresp_id=<dtml-var "F['entreprise_corresp_id']">">supprimer</a> </td>
+ </tr>
+ </dtml-let>
+<dtml-else>
+<tr><td colspan="4"><font color="red"><em>Aucun correspondant dans cette entreprise !</em></font></td></tr>
+</dtml-in>
+</table>
+
+<p class="entreprise_create"><a class="entreprise_create" href="<dtml-var "URL1 + '/entreprise_correspondant_create?entreprise_id=' + entreprise_id">">
+Ajouter un correspondant dans l'entreprise <dtml-var "E['nom']"> </a><p>
+
+
+<dtml-var "entreprise_footer(REQUEST=REQUEST)">
diff --git a/dtml/entreprises/entreprise_delete.dtml b/dtml/entreprises/entreprise_delete.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..7f7db5f8bb668879b3df14913fa807fd7d64af83
--- /dev/null
+++ b/dtml/entreprises/entreprise_delete.dtml
@@ -0,0 +1,60 @@
+<dtml-var "entreprise_header(REQUEST=REQUEST)">
+
+<dtml-call "REQUEST.set( 'F', do_entreprise_list( args={ 'entreprise_id' : entreprise_id } )[0])">
+
+
+<h2>Suppression de l'entreprise <dtml-var "F['nom']"> </h2>
+
+<p class="entreprise_warning">Attention: supression définitive de l'entreprise, de ses correspondants et contacts.
+</p>
+
+<dtml-call "REQUEST.set( 'Cl', do_entreprise_correspondant_list( args={ 'entreprise_id' : entreprise_id }))">
+
+<dtml-if "len(Cl)">
+<h3>Correspondants dans l'entreprise <dtml-var "F['nom']"> (seront <em>supprimés</em>):</h3>
+<ul>
+<dtml-in Cl>
+ <dtml-let c="_['sequence-item']">
+ <li><dtml-if "c['nom']"><dtml-var "c['nom'].lower().capitalize()"></dtml-if>
+   <dtml-if "c['prenom']"> <dtml-var "c['prenom'].lower().capitalize()"></dtml-if>
+   (<dtml-var "c['fonction']">)
+  </li>
+ </dtml-let c>
+</dtml-in Cl>
+</ul>
+</dtml-if Cl>
+
+<dtml-call "REQUEST.set( 'Cts', do_entreprise_contact_list( args={ 'entreprise_id' : entreprise_id }))">
+<dtml-if "len(Cts)">
+<h3>Contacts avec l'entreprise <dtml-var "F['nom']"> (seront <em>supprimés</em>):</h3>
+<ul>
+<dtml-in Cts>
+ <dtml-let c="_['sequence-item']">
+ <li><dtml-if "c['date']"><dtml-var "c['date']"></dtml-if>
+      <dtml-if "c['description']">(<dtml-var "c['description']">)</dtml-if>
+  </li>
+ </dtml-let c>
+</dtml-in Cts>
+</ul>
+</dtml-if Cts>
+
+<dtml-call "REQUEST.set('tf', TrivialFormulator( URL0, REQUEST.form, 
+  ( ('entreprise_id', { 'input_type' : 'hidden' }),
+   ),
+   initvalues = F,
+   submitlabel = 'Confirmer la suppression',
+   cancelbutton = 'Annuler', readonly = REQUEST['_read_only']
+))">
+
+<dtml-if "tf[0] == 0">
+ <dtml-var "tf[1]">
+<dtml-else>
+ <dtml-if "tf[0] == -1">
+    <dtml-call "RESPONSE.redirect( URL1 )">
+ <dtml-else>
+    <dtml-call "do_entreprise_delete( F['entreprise_id'] )">
+    <dtml-call "RESPONSE.redirect(URL1)">
+ </dtml-if>
+</dtml-if>
+
+<dtml-var "entreprise_footer(REQUEST=REQUEST)">
diff --git a/dtml/entreprises/index_html.dtml b/dtml/entreprises/index_html.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..1e2fbc25cb9be49cf90c40ee2c221d0b132fc5c2
--- /dev/null
+++ b/dtml/entreprises/index_html.dtml
@@ -0,0 +1,139 @@
+<dtml-var "entreprise_header(REQUEST=REQUEST)">
+
+<script type="text/javascript">
+function entsub(event,ourform) {
+  if (event && event.which == 13)
+    ourform.submit();
+  else
+    return true;}
+</script>
+
+<dtml-if entreprise_page_size> 
+ <dtml-call "REQUEST.set( 'entreprise_page_size', int(entreprise_page_size))">
+<dtml-else>
+ <dtml-call "REQUEST.set( 'entreprise_page_size', 10 )">
+</dtml-if>
+
+<h2><dtml-var title_or_id></h2>
+
+<dtml-comment>
+<p class="entreprise_warning">
+Attention: version test préliminaire. Signaler les problèmes à <a href="mailto:viennet@lipn.univ-paris13.fr">Emmanuel</a>
+</p>
+</dtml-comment>
+
+<dtml-unless sort_type><dtml-call "REQUEST.set( 'sort_type', 'nom' )"></dtml-unless>
+<dtml-call "REQUEST.set( 'sort_on_contact', False )">
+
+<dtml-if "sort_type=='nom'">
+ <dtml-call "REQUEST.set( 'sort_key', 'nom' )">
+<dtml-elif "sort_type=='datecontact'">
+ <dtml-call "REQUEST.set( 'sort_on_contact', True )">
+ <dtml-call "REQUEST.set( 'sort_key', None )">
+<dtml-elif "sort_type=='qualite_relation'">
+ <dtml-call "REQUEST.set( 'sort_key', 'qualite_relation desc, nom asc' )">
+<dtml-elif "sort_type=='date_creation'">
+ <dtml-call "REQUEST.set( 'sort_key', 'date_creation desc, nom asc' )">
+<dtml-else>
+<p class="entreprise_warning">valeur invalide pour 'sort_type' !</p>
+</dtml-if>
+
+<dtml-if etud_nom>
+ <dtml-call "REQUEST.set( 'Elist', do_entreprise_list_by_etud( args=REQUEST.form, sort_on_contact=sort_on_contact ) )">
+<dtml-else>
+ <dtml-call "REQUEST.set( 'Elist', do_entreprise_list( args=REQUEST.form, test='~*', sort_on_contact=sort_on_contact, sortkey=sort_key ) )">
+</dtml-if>
+
+<dtml-unless start><dtml-call "REQUEST.set( 'start', 1)"></dtml-unless>
+
+
+<form action="" method="GET">
+<table><tr>
+ <th>nom</th><td><input type="text" name="nom" size="12" value="<dtml-if nom><dtml-var nom></dtml-if>" onkeypress="return entsub(event,this.form)"></input></td>
+ <th>ville</th><td><input type="text" name="ville" size="12" value="<dtml-if ville><dtml-var ville></dtml-if>" onkeypress="return entsub(event,this.form)"></input></td>
+ <th>étudiant</th><td><input type="text" name="etud_nom" size="12" value="<dtml-if etud_nom><dtml-var etud_nom></dtml-if>" onkeypress="return entsub(event,this.form)"></input></td>
+ 
+ <td><input type="submit" value="rechercher"></input></td>
+ <td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>
+ <td>Tri par: <select name="sort_type" onchange="this.form.submit()">
+    <option value="nom" <dtml-if "sort_type=='nom'">selected</dtml-if>>nom entreprise</option>
+    <option value="datecontact" <dtml-if "sort_type=='datecontact'">selected</dtml-if>>date dernier contact</option>
+    <option value="qualite_relation" <dtml-if "sort_type=='qualite_relation'">selected</dtml-if>>relation IUT/Entreprise</option>
+    <option value="date_creation" <dtml-if "sort_type=='date_creation'">selected</dtml-if>>date saisie entreprise</option>
+ </select></td>
+</tr></table>
+</form>
+
+<table class="entreprise_list">
+
+<dtml-in Elist size=entreprise_page_size start=start overlap=1>
+<dtml-if sequence-start>
+<table class="entreprise_list_title">
+<tr class="entreprise_list_title"><td class="entreprise_list_title">Entreprises</td>
+<td class="entreprise_list_title_res">Résultats <b><dtml-var start></b> - <b><dtml-try><dtml-var "_['next-sequence-start-number']"><dtml-except><dtml-var "len(Elist)"></dtml-try></b> sur <b><dtml-var "len(Elist)"> </b>
+</td>
+</tr>
+</table>
+<table class="entreprise_list">
+</dtml-if sequence-start>
+
+ <dtml-let F="_['sequence-item']">
+ <dtml-call "REQUEST.set( 'Cl', do_entreprise_correspondant_list( args={ 'entreprise_id' : F['entreprise_id'] }))">
+ <dtml-call "REQUEST.set( 'Ct', do_entreprise_contact_list( args={ 'entreprise_id' : F['entreprise_id'] }))">
+
+ <tr class="<dtml-if sequence-odd>entreprise_list_odd<dtml-else>entreprise_list_even</dtml-if>">
+
+ <td colspan="5" class="entreprise_descr_name"><a class="entreprise_edit" href="entreprise_edit?entreprise_id=<dtml-var "F['entreprise_id']">&start=<dtml-var start>"><dtml-var "F['nom']"></a></td>
+ </tr>
+ <tr class="<dtml-if sequence-odd>entreprise_list_odd<dtml-else>entreprise_list_even</dtml-if>">
+ <td>&nbsp;</td>
+ <td class="entreprise_descr"><dtml-var "str_abbrev(F['ville'], 22)"></td>
+ <td class="entreprise_descr"><dtml-var "F['secteur']"> </td>
+
+  <td class="entreprise_descr_link">    
+    <a class="entreprise_delete" href="entreprise_correspondant_list?entreprise_id=<dtml-var "F['entreprise_id']">"><dtml-var "len(Cl)"> corr.</a> 
+    <dtml-if "sort_type=='qualite_relation'"><b><dtml-var "{100:'TB',75:'B',50:'M',25:'!' }.get(F['qualite_relation'],'?')"></b></dtml-if>
+  </td>
+  <td class="entreprise_descr_link"><a class="entreprise_delete" href="entreprise_contact_list?entreprise_id=<dtml-var "F['entreprise_id']">"><dtml-var "len(Ct)"> contacts</a> 
+   <dtml-try>(<dtml-var "F['etud_nom']">)<dtml-except></dtml-try> 
+   <dtml-try>(<dtml-var "F['date']">)<dtml-except></dtml-try> 
+  </td>
+
+ </tr>
+ </dtml-let>
+<dtml-if sequence-end>
+</table>
+</dtml-if sequence-end>
+<dtml-else Elist>
+<p class="entreprise_warning">Aucune entreprise !</p>
+</dtml-in>
+
+
+ <p>
+   <form action="setPageSizeCookie" method="GET">
+      <input type="hidden" name="target_url" value="<dtml-var "URL0+'?'+QUERY_STRING">"></input>
+ 
+  <dtml-in Elist size=entreprise_page_size start=start overlap=1>
+     <dtml-if sequence-start>
+        <dtml-if previous-sequence>
+        <a href="<dtml-var URL><dtml-var sequence-query>start=<dtml-var previous-sequence-start-number>">
+           page précédente</a>&nbsp;&nbsp;&nbsp;&nbsp;
+         </dtml-if previous-sequence>
+     </dtml-if sequence-start>
+     <dtml-if sequence-end>
+         <dtml-if next-sequence>
+            <a href="<dtml-var URL><dtml-var sequence-query>start=<dtml-var next-sequence-start-number>">
+             page suivante</a>
+         </dtml-if next-sequence>
+     </dtml-if sequence-end>
+    </dtml-in Elist>
+   &nbsp; Résultats par page :
+        <select name="entreprise_page_size" onchange="this.form.submit()">
+        <dtml-in "((5,5),(10,10),(15,15),(20,20),(30,30),(50,50),(1000,'Tous'))"  prefix="label">
+            <option value="<dtml-var "label_key">" <dtml-if "label_key == entreprise_page_size">selected</dtml-if>><dtml-var label_item></option>
+        </dtml-in>
+        </select>
+   </form>
+    
+</p>
+<dtml-var "entreprise_footer(REQUEST=REQUEST)">
diff --git a/dtml/manage_addZNotesForm.dtml b/dtml/manage_addZNotesForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..e58b49e021b7d954a87bd2d89e6881bc1ef186fa
--- /dev/null
+++ b/dtml/manage_addZNotesForm.dtml
@@ -0,0 +1,49 @@
+<dtml-var manage_page_header>
+
+<dtml-var "manage_form_title(this(), _,
+           form_title='Add Notes',
+           help_product='ZNotes',
+           help_topic='ZNotes-add.stx'
+           )">
+
+<div class="form-help">
+<p>
+Notes Objects are very usefull thus not documented yet...
+</p>
+</div>
+
+<form name="form" action="manage_addZNotes"><br>
+
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Id
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="id" size="40" value="" />
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40" />
+    </td>
+  </tr>
+
+  <div class="form-element">
+    <input class="form-element" 
+           type="submit" 
+  	   name="submit" 
+	   value=" Add " 
+	   />
+  </div>
+</form>
+
+<dtml-var manage_page_header>
diff --git a/dtml/manage_addZScolarForm.dtml b/dtml/manage_addZScolarForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..580a83e18ab7a693debdffedf6aa3a431b4d78cc
--- /dev/null
+++ b/dtml/manage_addZScolarForm.dtml
@@ -0,0 +1,62 @@
+<dtml-var manage_page_header>
+
+<dtml-var "manage_form_title(this(), _,
+           form_title='Add ZScolar',
+           help_product='ZScolar',
+           help_topic='ZScolar-add.stx'
+           )">
+
+<div class="form-help">
+<p>
+ZScolar: gestion scolarite d'un departement
+</p>
+</div>
+
+<form name="form" action="manage_addZScolar"><br>
+
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Id
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="id" size="40" value="" />
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40" />
+    </td>
+  </tr>
+
+
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    DB connexion string
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="db_cnx_string" size="80" value="user=zopeuser dbname=SCOGTR password=XXXX host=localhost" />
+    </td>
+  </tr>
+
+
+  <div class="form-element">
+    <input class="form-element" 
+           type="submit" 
+  	   name="submit" 
+	   value=" Add " 
+	   />
+  </div>
+</form>
+
+<dtml-var manage_page_header>
diff --git a/dtml/manage_editZNotesForm.dtml b/dtml/manage_editZNotesForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..456b631f2b3e8865022f1b00e27753b1dfda21d9
--- /dev/null
+++ b/dtml/manage_editZNotesForm.dtml
@@ -0,0 +1,21 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<dtml-var "manage_form_title(this(), _,
+           form_title='Edit ZNotes',
+	   help_product='ZNotes',
+	   help_topic='ZNotes-edit.stx'
+	   )">
+
+
+<form name="form" action="manage_editAction" method="post"><br/>
+  id: <dtml-var id><br/>
+  title: <input type="text" name="title:string" size="30" value="<dtml-var title>"><br/>
+  <div class="form-element">
+    <input class="form-element" type="submit" value="Save Changes">
+  </div>
+</form>
+
+
+<dtml-var manage_page_footer>
+
diff --git a/dtml/manage_editZScolarForm.dtml b/dtml/manage_editZScolarForm.dtml
new file mode 100644
index 0000000000000000000000000000000000000000..aab8859e102c5c6fbfb4bb698f8bf1291db77289
--- /dev/null
+++ b/dtml/manage_editZScolarForm.dtml
@@ -0,0 +1,21 @@
+<dtml-var manage_page_header>
+<dtml-var manage_tabs>
+
+<dtml-var "manage_form_title(this(), _,
+           form_title='Edit ZScolar',
+	   help_product='ZScolar',
+	   help_topic='ZScolar-edit.stx'
+	   )">
+
+
+<form name="form" action="manage_editAction" method="post"><br/>
+  id: <dtml-var id><br/>
+  title: <input type="text" name="title:string" size="30" value="<dtml-var title>"><br/>
+  <div class="form-element">
+    <input class="form-element" type="submit" value="Save Changes">
+  </div>
+</form>
+
+
+<dtml-var manage_page_footer>
+
diff --git a/dutrules.py b/dutrules.py
new file mode 100644
index 0000000000000000000000000000000000000000..e0c371151a22d6a798a1bcd24729b2a60f232582
--- /dev/null
+++ b/dutrules.py
@@ -0,0 +1,354 @@
+# -*- coding: utf-8 -*-
+#
+# Generated by csv2rules.py    *** DO NOT EDIT ***
+#
+# Command: ./csv2rules.py misc/parcoursDUT.csv
+#
+from sco_codes_parcours import *
+
+rules_source_file = "misc/parcoursDUT.csv"
+
+DUTRules = [
+    DUTRule(rid, p, c)
+    for (rid, p, c) in (
+        # Id	Prev.	Assiduité	Moy Gen	Barres UE	Comp prev/cur	Suivant	Code SEM	Codes UE	Code prev. (si modifié)	Devenir	Action	Explication
+        # Semestre prec. validé:
+        (
+            "10",
+            ((None, ADM, ADC, ADJ), True, True, True, ALL, ALL),
+            (ADM, (ADM,), None, NEXT, None, "Passage normal"),
+        ),
+        (
+            "20",
+            ((None, ADM, ADC, ADJ), True, False, True, ALL, True),
+            (ATT, (ADM,), None, NEXT, None, "Pas moy: attente suivant pour compenser"),
+        ),
+        (
+            "30",
+            ((None, ADM, ADC, ADJ), True, ALL, False, ALL, ALL),
+            (ATB, (ADM, AJ), None, NEXT, None, "Pas barre UE"),
+        ),
+        (
+            "40",
+            ((None, ADM, ADC, ADJ), False, ALL, ALL, ALL, True),
+            (
+                ATJ,
+                (AJ,),
+                None,
+                NEXT,
+                None,
+                "Pb assiduité, passe sans valider pour l'instant",
+            ),
+        ),
+        (
+            "50",
+            ((ADM, ADJ, ADC), True, False, ALL, True, ALL),
+            (ADC, (ADM, CMP), None, NEXT, None, "Compense avec semestre précédent"),
+        ),
+        # Semestre prec. ATJ (pb assiduité):
+        (
+            "60",
+            ((ATJ,), False, ALL, ALL, ALL, ALL),
+            (NAR, (AJ,), AJ, REO, None, "Pb assiduité persistant: réorientation"),
+        ),
+        (
+            "70",
+            ((ATJ,), False, ALL, ALL, ALL, ALL),
+            (
+                AJ,
+                (AJ,),
+                AJ,
+                REDOANNEE,
+                None,
+                "Pb assiduité persistant: redoublement année",
+            ),
+        ),
+        (
+            "80",
+            ((ALL,), False, ALL, ALL, ALL, ALL),
+            (AJ, (), ADM, REO, None, "Pb assiduité, étudiant en échec."),
+        ),
+        # Semestre prec. ATT (pb moy gen):
+        (
+            "90",
+            ((ATT,), True, True, True, True, ALL),
+            (ADM, (ADM,), ADC, NEXT, None, "Passage, et compense précédent"),
+        ),
+        (
+            "100",
+            ((ATT,), True, True, True, ALL, ALL),
+            (ADM, (ADJ,), ADJ, NEXT, None, "Passage, le jury valide le précédent"),
+        ),
+        (
+            "110",
+            ((ATT,), False, True, True, ALL, True),
+            (
+                ATJ,
+                (AJ,),
+                ADJ,
+                NEXT,
+                None,
+                "Passage, le jury valide le précédent, pb assiduité",
+            ),
+        ),
+        (
+            "120",
+            ((ATT,), True, False, ALL, ALL, ALL),
+            (AJ, (AJ,), AJ, REDOANNEE, None, "Redoublement année"),
+        ),
+        (
+            "130",
+            ((ATT,), ALL, True, True, False, ALL),
+            (
+                AJ,
+                (AJ,),
+                AJ,
+                REDOANNEE,
+                None,
+                "Pas de compensation ni validation du précédent",
+            ),
+        ),
+        (
+            "140",
+            ((ATT,), True, False, True, ALL, ALL),
+            (
+                ATT,
+                (),
+                ADJ,
+                NEXT,
+                None,
+                "Pas moy, le jury valide le précédent, semestre en attente pour compenser",
+            ),
+        ),
+        # Semestre prec. ATB (pb barre UE):
+        (
+            "200",
+            ((ATB,), ALL, ALL, ALL, ALL, ALL),
+            (
+                AJ,
+                (ADM, AJ),
+                AJ,
+                REDOANNEE,
+                None,
+                "Le précédent ne peut pas être validé, redoublement année",
+            ),
+        ),
+        (
+            "210",
+            ((ATB,), ALL, ALL, ALL, ALL, ALL),
+            (
+                NAR,
+                (ADM, AJ),
+                NAR,
+                REO,
+                None,
+                "Le précédent ne peut pas être validé, réorientation",
+            ),
+        ),
+        (
+            "220",
+            ((ATB,), True, True, True, ALL, ALL),
+            (ADM, (ADM,), ADJ, NEXT, None, "Le jury valide le précédent"),
+        ),
+        (
+            "230",
+            ((ATB,), True, False, True, ALL, True),
+            (
+                ATT,
+                (ADM, AJ),
+                ADJ,
+                NEXT,
+                None,
+                "Le jury valide le précédent, pas moyenne gen., attente suivant",
+            ),
+        ),
+        (
+            "240",
+            ((ATB,), True, ALL, False, ALL, True),
+            (
+                ATB,
+                (ADM, AJ),
+                ADJ,
+                NEXT,
+                None,
+                "Le jury valide le précédent, pb barre UE, attente",
+            ),
+        ),
+        (
+            "250",
+            ((ATB,), False, ALL, ALL, ALL, True),
+            (
+                ATJ,
+                (AJ,),
+                ADJ,
+                NEXT,
+                None,
+                "Le jury valide le précédent, mais probleme assiduité.",
+            ),
+        ),
+        (
+            "260",
+            ((ATB, ATT), ALL, True, True, ALL, ALL),
+            (
+                ADJ,
+                (),
+                AJ,
+                REDOANNEE,
+                None,
+                "Le jury valide ce semestre, et fait recommencer le précédent.",
+            ),
+        ),
+        # Semestre prec. AJ (ajourné):
+        (
+            "300",
+            ((AJ,), True, False, ALL, ALL, ALL),
+            (AJ, (), AJ, REDOANNEE, None, "Echec de 2 semestres, redouble année"),
+        ),
+        (
+            "310",
+            ((AJ,), True, True, False, ALL, ALL),
+            (AJ, (), AJ, REDOANNEE, None, "Echec de 2 semestres, redouble année"),
+        ),
+        (
+            "320",
+            ((AJ,), False, ALL, ALL, ALL, ALL),
+            (NAR, (), None, REO, None, "Echec, pas assidu: réorientation"),
+        ),
+        (
+            "330",
+            ((AJ,), True, True, True, ALL, ALL),
+            (
+                ATT,
+                (),
+                None,
+                REDOANNEE,
+                None,
+                "Ne valide pas car mais manque le précédent: redouble (modif 2017)",
+            ),
+        ),
+        # Décisions du jury:
+        (
+            "400",
+            ((ALL,), True, False, ALL, ALL, ALL),
+            (ADJ, (ADM, CMP), None, NEXT, None, "Le jury décide de valider"),
+        ),
+        (
+            "410",
+            ((ATT, ATB), True, False, ALL, ALL, ALL),
+            (
+                ADJ,
+                (ADM, CMP),
+                ADJ,
+                NEXT,
+                None,
+                "Le jury décide de valider ce semestre et le précédent",
+            ),
+        ),
+        (
+            "420",
+            ((ALL,), True, True, False, ALL, ALL),
+            (ADJ, (ADM, CMP), None, NEXT, None, "Le jury décide de valider"),
+        ),
+        (
+            "430",
+            ((ATT, ATB), True, True, False, ALL, ALL),
+            (
+                ADJ,
+                (ADM, CMP),
+                ADJ,
+                NEXT,
+                None,
+                "Le jury décide de valider ce semestre et le précédent",
+            ),
+        ),
+        (
+            "450",
+            ((ATT, ATB), False, False, True, ALL, True),
+            (
+                ATT,
+                (ADM, AJ),
+                ADJ,
+                NEXT,
+                None,
+                "Pb moy: attente, mais le jury valide le précédent",
+            ),
+        ),
+        # Semestres “décales” (REDOSEM)
+        (
+            "500",
+            ((None, ADM, ADC, ADJ, ATT, ATB), True, False, ALL, False, ALL),
+            (AJ, (), None, REDOSEM, None, "Pas moy: redouble ce semestre"),
+        ),
+        (
+            "510",
+            ((None, ADM, ADC, ADJ, ATT, ATB), True, True, False, False, ALL),
+            (AJ, (), None, REDOSEM, None, "Pas barre UE: redouble ce semestre"),
+        ),
+        (
+            "520",
+            ((None, ADM, ADC, ADJ, ATB, ATT), False, ALL, ALL, ALL, ALL),
+            (AJ, (), None, REDOSEM, None, "Pb assiduité: redouble ce semestre"),
+        ),
+        # Nouvelles regles avec plusieurs devenirs en semestres decales:
+        (
+            "550",
+            ((ATT, ATB), ALL, False, ALL, False, ALL),
+            (
+                AJ,
+                (),
+                None,
+                RA_OR_RS,
+                None,
+                "Deux semestres ratés, choix de recommencer le premier ou le second",
+            ),
+        ),
+        (
+            "560",
+            ((ATT, ATB), ALL, True, False, False, ALL),
+            (
+                AJ,
+                (),
+                None,
+                RA_OR_RS,
+                None,
+                "Deux semestres ratés, choix de recommencer le premier ou le second",
+            ),
+        ),
+        (
+            "570",
+            ((None, ADM, ADJ, ADC), ALL, False, True, False, ALL),
+            (
+                ATT,
+                (),
+                None,
+                RS_OR_NEXT,
+                None,
+                "Semestre raté, choix de redoubler le semestre ou de continuer pour éventuellement compenser.",
+            ),
+        ),
+        (
+            "580",
+            ((None, ADM, ADJ, ADC), ALL, ALL, False, False, ALL),
+            (
+                ATB,
+                (),
+                None,
+                RS_OR_NEXT,
+                None,
+                "Semestre raté, choix de redoubler ou de s'en remettre au jury du semestre suivant.",
+            ),
+        ),
+        # Exclusion (art. 22): si precedent non valide et pas les barres dans le courant, on peut ne pas autoriser a redoubler:
+        # (le cas ATB est couvert plus haut)
+        (
+            "600",
+            ((AJ, ATT, NAR), True, False, ALL, ALL, ALL),
+            (NAR, (), NAR, REO, None, "Non autorisé à redoubler"),
+        ),
+        (
+            "610",
+            ((AJ, ATT, NAR), True, True, False, ALL, ALL),
+            (NAR, (), NAR, REO, None, "Non autorisé à redoubler"),
+        ),
+    )
+]
diff --git a/gen_tables.py b/gen_tables.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e14585f7dc4c932c3730f36833093ab3e0f1ca0
--- /dev/null
+++ b/gen_tables.py
@@ -0,0 +1,706 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Géneration de tables aux formats XHTML, PDF, Excel, XML et JSON.
+
+Les données sont fournies comme une liste de dictionnaires, chaque élément de
+cette liste décrivant une ligne du tableau.
+
+Chaque colonne est identifiée par une clé du dictionnaire.
+
+Voir exemple en fin de ce fichier.
+
+Les clés commençant par '_' sont réservées. Certaines altèrent le traitement, notamment
+pour spécifier les styles de mise en forme.
+Par exemple, la clé '_css_row_class' spécifie le style CSS de la ligne.
+
+"""
+
+import random
+from collections import OrderedDict
+
+# XML generation package (apt-get install jaxml)
+import jaxml
+
+import sco_excel
+from sco_pdf import *
+
+
+def mark_paras(L, tags):
+    """Put each (string) element of L between  <b>
+    """
+    for tag in tags:
+        b = "<" + tag + ">"
+        c = "</" + tag.split()[0] + ">"
+        L = [b + (x or "") + c for x in L]
+    return L
+
+
+class DEFAULT_TABLE_PREFERENCES:
+    values = {
+        "SCOLAR_FONT": "Helvetica",  # used for PDF, overriden by preferences argument
+        "SCOLAR_FONT_SIZE": 10,
+        "SCOLAR_FONT_SIZE_FOOT": 6,
+    }
+
+    def __getitem__(self, k):
+        return self.values[k]
+
+
+class GenTable:
+    """Simple 2D tables with export to HTML, PDF, Excel, CSV.
+    Can be sub-classed to generate fancy formats.
+    """
+
+    default_css_class = "gt_table stripe cell-border compact hover order-column"
+
+    def __init__(
+        self,
+        rows=[{}],  # liste de dict { column_id : value }
+        columns_ids=[],  # id des colonnes a afficher, dans l'ordre
+        titles={},  # titres (1ere ligne)
+        bottom_titles={},  # titres derniere ligne (optionnel)
+        caption=None,
+        page_title="",  # titre fenetre html
+        pdf_link=True,
+        xls_link=True,
+        xml_link=False,
+        table_id=None,  # for html and xml
+        html_class=None,  # class de l'element <table> (en plus des classes par defaut,
+        html_class_ignore_default=False,  # sauf si html_class_ignore_default est vrai)
+        html_sortable=False,
+        html_highlight_n=2,  # une ligne sur 2 de classe "gt_hl"
+        html_col_width=None,  # force largeur colonne
+        html_generate_cells=True,  # generate empty <td> cells even if not in rows (useless?)
+        html_title="",  # avant le tableau en html
+        html_caption=None,  # override caption if specified
+        html_header=None,
+        html_next_section="",  # html fragment to put after the table
+        html_with_td_classes=False,  # put class=column_id in each <td>
+        html_before_table="",  # html snippet to put before the <table> in the page
+        html_empty_element="",  # replace table when empty
+        base_url=None,
+        origin=None,  # string added to excel and xml versions
+        filename="table",  # filename, without extension
+        xls_sheet_name="feuille",
+        xls_before_table=[],  # liste de cellules a placer avant la table
+        pdf_title="",  # au dessus du tableau en pdf
+        pdf_table_style=None,
+        pdf_col_widths=None,
+        xml_outer_tag="table",
+        xml_row_tag="row",
+        text_fields_separator="\t",
+        preferences=None,
+    ):
+        self.rows = rows  # [ { col_id : value } ]
+        self.columns_ids = columns_ids  # ordered list of col_id
+        self.titles = titles  # { col_id : title }
+        self.bottom_titles = bottom_titles
+        self.origin = origin
+        self.base_url = base_url
+        self.filename = filename
+        self.caption = caption
+        self.html_header = html_header
+        self.html_before_table = html_before_table
+        self.html_empty_element = html_empty_element
+        self.page_title = page_title
+        self.pdf_link = pdf_link
+        self.xls_link = xls_link
+        self.xml_link = xml_link
+        # HTML parameters:
+        if not table_id:  # random id
+            self.table_id = "gt_" + str(random.randint(0, 1000000))
+        else:
+            self.table_id = table_id
+        self.html_generate_cells = html_generate_cells
+        self.html_title = html_title
+        self.html_caption = html_caption
+        self.html_next_section = html_next_section
+        self.html_with_td_classes = html_with_td_classes
+        if html_class is None:
+            html_class = self.default_css_class
+        if html_class_ignore_default:
+            self.html_class = html_class
+        else:
+            self.html_class = self.default_css_class + " " + html_class
+
+        self.sortable = html_sortable
+        self.html_highlight_n = html_highlight_n
+        self.html_col_width = html_col_width
+        # XLS parameters
+        self.xls_sheet_name = xls_sheet_name
+        self.xls_before_table = xls_before_table
+        # PDF parameters
+        self.pdf_table_style = pdf_table_style
+        self.pdf_col_widths = pdf_col_widths
+        self.pdf_title = pdf_title
+        # XML parameters
+        self.xml_outer_tag = xml_outer_tag
+        self.xml_row_tag = xml_row_tag
+        # TEXT parameters
+        self.text_fields_separator = text_fields_separator
+        #
+        if preferences:
+            self.preferences = preferences
+        else:
+            self.preferences = DEFAULT_TABLE_PREFERENCES()
+
+    def get_nb_cols(self):
+        return len(self.columns_ids)
+
+    def get_nb_rows(self):
+        return len(self.rows)
+
+    def is_empty(self):
+        return len(self.rows) == 0
+
+    def get_data_list(
+        self,
+        with_titles=False,
+        with_lines_titles=True,
+        with_bottom_titles=True,
+        omit_hidden_lines=False,
+        pdf_mode=False,  # apply special pdf reportlab processing
+        pdf_style_list=[],  # modified: list of platypus table style commands
+    ):
+        "table data as a list of lists (rows)"
+        T = []
+        line_num = 0  # line number in input data
+        out_line_num = 0  # line number in output list
+        if with_titles and self.titles:
+            l = []
+            if with_lines_titles:
+                if self.titles.has_key("row_title"):
+                    l = [self.titles["row_title"]]
+
+            T.append(l + [self.titles.get(cid, "") for cid in self.columns_ids])
+
+        for row in self.rows:
+            line_num += 1
+            l = []
+            if with_lines_titles:
+                if row.has_key("row_title"):
+                    l = [row["row_title"]]
+
+            if not (omit_hidden_lines and row.get("_hidden", False)):
+                colspan_count = 0
+                col_num = len(l)
+                for cid in self.columns_ids:
+                    colspan_count -= 1
+                    # if colspan_count > 0:
+                    #    continue # skip cells after a span
+                    content = row.get(cid, "") or ""  # nota: None converted to ''
+                    colspan = row.get("_%s_colspan" % cid, 0)
+                    if colspan > 1:
+                        pdf_style_list.append(
+                            (
+                                "SPAN",
+                                (col_num, out_line_num),
+                                (col_num + colspan - 1, out_line_num),
+                            )
+                        )
+                        colspan_count = colspan
+                    l.append(content)
+                    col_num += 1
+                if pdf_mode:
+                    mk = row.get("_pdf_row_markup", [])  # a list of tags
+                    if mk:
+                        l = mark_paras(l, mk)
+                T.append(l)
+                #
+                for cmd in row.get("_pdf_style", []):  # relocate line numbers
+                    pdf_style_list.append(
+                        (
+                            cmd[0],
+                            (cmd[1][0], cmd[1][1] + out_line_num),
+                            (cmd[2][0], cmd[2][1] + out_line_num),
+                        )
+                        + cmd[3:]
+                    )
+
+                out_line_num += 1
+        if with_bottom_titles and self.bottom_titles:
+            line_num += 1
+            l = []
+            if with_lines_titles:
+                if self.bottom_titles.has_key("row_title"):
+                    l = [self.bottom_titles["row_title"]]
+
+            T.append(l + [self.bottom_titles.get(cid, "") for cid in self.columns_ids])
+        return T
+
+    def get_titles_list(self):
+        "list of titles"
+        l = []
+        return l + [self.titles.get(cid, "") for cid in self.columns_ids]
+
+    def gen(self, format="html", columns_ids=None):
+        """Build representation of the table in the specified format.
+        See make_page() for more sophisticated output.
+        """
+        if format == "html":
+            return self.html()
+        elif format == "xls":
+            return self.excel()
+        elif format == "text" or format == "csv":
+            return self.text()
+        elif format == "pdf":
+            return self.pdf()
+        elif format == "xml":
+            return self.xml()
+        elif format == "json":
+            return self.json()
+        raise ValueError("GenTable: invalid format: %s" % format)
+
+    def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""):
+        "row is a dict, returns a string <tr...>...</tr>"
+        if not row:
+            return "<tr></tr>"  # empty row
+
+        if self.html_col_width:  # XXXX Obsolete ?
+            std = ' style="width:%s;"' % self.html_col_width
+        else:
+            std = ""
+
+        cla = css_classes + " " + row.get("_css_row_class", "")
+        if line_num % self.html_highlight_n:
+            cls = ' class="gt_hl %s"' % cla
+        else:
+            if cla:
+                cls = ' class="%s"' % cla
+            else:
+                cls = ""
+        H = ["<tr%s %s>" % (cls, row.get("_tr_attrs", ""))]
+        # titre ligne
+        if row.has_key("row_title"):
+            content = str(row["row_title"])
+            help = row.get("row_title_help", "")
+            if help:
+                content = '<a class="discretelink" href="" title="%s">%s</a>' % (
+                    help,
+                    content,
+                )
+            H.append('<th class="gt_linetit">' + content + "</th>")
+        r = []
+        colspan_count = 0
+        for cid in self.columns_ids:
+            if not cid in row and not self.html_generate_cells:
+                continue  # skip cell
+            colspan_count -= 1
+            if colspan_count > 0:
+                continue  # skip cells after a span
+            content = row.get("_" + str(cid) + "_html", row.get(cid, ""))
+            if content is None:
+                content = ""
+            else:
+                content = str(content)
+            help = row.get("_%s_help" % cid, "")
+            if help:
+                target = row.get("_%s_target" % cid, "#")
+            else:
+                target = row.get("_%s_target" % cid, "")
+            cell_id = row.get("_%s_id" % cid, None)
+            if cell_id:
+                idstr = ' id="%s"' % cell_id
+            else:
+                idstr = ""
+            cell_link_class = row.get("_%s_link_class" % cid, "discretelink")
+            if help or target:
+                content = '<a class="%s" href="%s" title="%s"%s>%s</a>' % (
+                    cell_link_class,
+                    target,
+                    help,
+                    idstr,
+                    content,
+                )
+            klass = row.get("_%s_class" % cid, "")
+            if self.html_with_td_classes:
+                c = cid
+            else:
+                c = ""
+            if c or klass:
+                klass = ' class="%s"' % (" ".join((klass, c)))
+            else:
+                klass = ""
+            colspan = row.get("_%s_colspan" % cid, 0)
+            if colspan > 1:
+                colspan_txt = ' colspan="%d" ' % colspan
+                colspan_count = colspan
+            else:
+                colspan_txt = ""
+            r.append(
+                "<%s%s %s%s%s>%s</%s>"
+                % (
+                    elem,
+                    std,
+                    row.get("_%s_td_attrs" % cid, ""),
+                    klass,
+                    colspan_txt,
+                    content,
+                    elem,
+                )
+            )
+
+        H.append("".join(r) + "</tr>")
+        return "".join(H)
+
+    def html(self):
+        "Simple HTML representation of the table"
+        if self.is_empty() and self.html_empty_element:
+            return self.html_empty_element + "\n" + self.html_next_section
+        hid = ' id="%s"' % self.table_id
+        tablclasses = []
+        if self.html_class:
+            tablclasses.append(self.html_class)
+        if self.sortable:
+            tablclasses.append("sortable")
+        if tablclasses:
+            cls = ' class="%s"' % " ".join(tablclasses)
+        else:
+            cls = ""
+
+        if self.html_col_width:
+            std = ' style="width:%s;"' % self.html_col_width
+        else:
+            std = ""
+        H = [self.html_before_table, "<table%s%s>" % (hid, cls)]
+
+        line_num = 0
+        # thead
+        H.append("<thead>")
+        if self.titles:
+            H.append(
+                self._gen_html_row(
+                    self.titles, line_num, elem="th", css_classes="gt_firstrow"
+                )
+            )
+        # autres lignes à placer dans la tête:
+        for row in self.rows:
+            if row.get("_table_part") == "head":
+                line_num += 1
+                H.append(self._gen_html_row(row, line_num))  # uses td elements
+        H.append("</thead>")
+
+        H.append("<tbody>")
+        for row in self.rows:
+            if row.get("_table_part", "body") == "body":
+                line_num += 1
+                H.append(self._gen_html_row(row, line_num))
+        H.append("</tbody>")
+
+        H.append("<tfoot>")
+        for row in self.rows:
+            if row.get("_table_part") == "foot":
+                line_num += 1
+                H.append(self._gen_html_row(row, line_num))
+        if self.bottom_titles:
+            H.append(
+                self._gen_html_row(
+                    self.bottom_titles,
+                    line_num + 1,
+                    elem="th",
+                    css_classes="gt_lastrow sortbottom",
+                )
+            )
+        H.append("</tfoot>")
+
+        H.append("</table>")
+
+        caption = self.html_caption or self.caption
+        if caption or self.base_url:
+            H.append('<p class="gt_caption">')
+            if caption:
+                H.append(caption)
+            if self.base_url:
+                if self.xls_link:
+                    H.append(
+                        ' <a href="%s&amp;format=xls">%s</a>'
+                        % (self.base_url, ICON_XLS)
+                    )
+                if self.xls_link and self.pdf_link:
+                    H.append("&nbsp;&nbsp;")
+                if self.pdf_link:
+                    H.append(
+                        ' <a href="%s&amp;format=pdf">%s</a>'
+                        % (self.base_url, ICON_PDF)
+                    )
+            H.append("</p>")
+
+        H.append(self.html_next_section)
+        return "\n".join(H)
+
+    def excel(self, wb=None):
+        "Simple Excel representation of the table"
+        L = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name)
+        style_bold = sco_excel.Excel_MakeStyle(bold=True)
+
+        L.cells += self.xls_before_table
+        L.set_style(style_bold, li=len(L.cells))
+        L.append(self.get_titles_list())
+        L.cells += [[x for x in line] for line in self.get_data_list()]
+        if self.caption:
+            L.append([])  # empty line
+            L.append([self.caption])
+        if self.origin:
+            L.append([])  # empty line
+            L.append([self.origin])
+
+        return L.gen_workbook(wb=wb)
+
+    def text(self):
+        "raw text representation of the table"
+        return "\n".join(
+            [
+                self.text_fields_separator.join([x for x in line])
+                for line in self.get_data_list()
+            ]
+        )
+
+    def pdf(self):
+        "PDF representation: returns a ReportLab's platypus Table instance"
+        r = []
+        try:
+            PDFLOCK.acquire()
+            r = self._pdf()
+        finally:
+            PDFLOCK.release()
+        return r
+
+    def _pdf(self):
+        """PDF representation: returns a list of ReportLab's platypus objects
+        (notably a Table instance)
+        """
+        if not self.pdf_table_style:
+            LINEWIDTH = 0.5
+            self.pdf_table_style = [
+                ("FONTNAME", (0, 0), (-1, 0), self.preferences["SCOLAR_FONT"]),
+                ("LINEBELOW", (0, 0), (-1, 0), LINEWIDTH, Color(0, 0, 0)),
+                ("GRID", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
+                ("VALIGN", (0, 0), (-1, -1), "TOP"),
+            ]
+        nb_cols = len(self.columns_ids)
+        if self.rows and self.rows[0].has_key("row_title"):
+            nb_cols += 1
+        if not self.pdf_col_widths:
+            self.pdf_col_widths = (None,) * nb_cols
+        #
+        CellStyle = styles.ParagraphStyle({})
+        CellStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
+        CellStyle.fontName = self.preferences["SCOLAR_FONT"]
+        CellStyle.leading = 1.0 * self.preferences["SCOLAR_FONT_SIZE"]  # vertical space
+        LINEWIDTH = 0.5
+        #
+        titles = ["<para><b>%s</b></para>" % x for x in self.get_titles_list()]
+        pdf_style_list = []
+        Pt = [
+            [Paragraph(SU(str(x)), CellStyle) for x in line]
+            for line in (
+                self.get_data_list(
+                    pdf_mode=True,
+                    pdf_style_list=pdf_style_list,
+                    with_titles=True,
+                    omit_hidden_lines=True,
+                )
+            )
+        ]
+        pdf_style_list += self.pdf_table_style
+        # log('len(Pt)=%s' % len(Pt))
+        # log( 'line lens=%s' % [ len(x) for x in Pt ] )
+        # log( 'style=\n%s' % pdf_style_list)
+        col_min = min([x[1][0] for x in pdf_style_list])
+        col_max = max([x[2][0] for x in pdf_style_list])
+        lin_min = min([x[1][1] for x in pdf_style_list])
+        lin_max = max([x[2][1] for x in pdf_style_list])
+        # log('col_min=%s col_max=%s lin_min=%s lin_max=%s' % (col_min, col_max, lin_min, lin_max))
+        T = Table(Pt, repeatRows=1, colWidths=self.pdf_col_widths, style=pdf_style_list)
+
+        objects = []
+        StyleSheet = styles.getSampleStyleSheet()
+        if self.pdf_title:
+            objects.append(Paragraph(SU(self.pdf_title), StyleSheet["Heading3"]))
+        if self.caption:
+            objects.append(Paragraph(SU(self.caption), StyleSheet["Normal"]))
+            objects.append(Spacer(0, 0.4 * cm))
+        objects.append(T)
+
+        return objects
+
+    def xml(self):
+        """XML representation of the table.
+        The schema is very simple:
+        <table origin="" id="" caption="">
+        <row title="">
+        <column_id value=""/>
+        </row>
+        </table>
+        The tag names <table> and <row> can be changed using
+        xml_outer_tag and xml_row_tag
+        """
+        doc = jaxml.XML_document(encoding=SCO_ENCODING)
+        getattr(doc, 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()
+            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)()
+            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)
+
+    def json(self):
+        """JSON representation of the table.
+        """
+        d = []
+        for row in self.rows:
+            r = {}
+            for cid in self.columns_ids:
+                v = row.get(cid, None)
+                if v != None:
+                    v = str(v)
+                r[cid] = v
+            d.append(r)
+        return json.dumps(d, encoding=SCO_ENCODING)
+
+    def make_page(
+        self,
+        context,
+        title="",
+        format="html",
+        page_title="",
+        filename=None,
+        REQUEST=None,
+        javascripts=[],
+        with_html_headers=True,
+        publish=True,
+        init_qtip=False,
+    ):
+        """
+        Build page at given format
+        This is a simple page with only a title and the table.
+        If not publish, does not set response header
+        """
+        if not filename:
+            filename = self.filename
+        page_title = page_title or self.page_title
+        html_title = self.html_title or title
+        if format == "html":
+            H = []
+            if with_html_headers:
+                H.append(
+                    self.html_header
+                    or context.sco_header(
+                        REQUEST,
+                        page_title=page_title,
+                        javascripts=javascripts,
+                        init_qtip=init_qtip,
+                    )
+                )
+            if html_title:
+                H.append(html_title)
+            H.append(self.html())
+            if with_html_headers:
+                H.append(context.sco_footer(REQUEST))
+            return "\n".join(H)
+        elif format == "pdf":
+            objects = self.pdf()
+            doc = pdf_basic_page(objects, title=title, preferences=self.preferences)
+            if publish:
+                return sendPDFFile(REQUEST, doc, filename + ".pdf")
+            else:
+                return doc
+        elif format == "xls":
+            xls = self.excel()
+            if publish:
+                return sco_excel.sendExcelFile(REQUEST, xls, filename + ".xls")
+            else:
+                return xls
+        elif format == "text":
+            return self.text()
+        elif format == "csv":
+            return sendCSVFile(REQUEST, self.text(), filename + ".csv")
+        elif format == "xml":
+            xml = self.xml()
+            if REQUEST and publish:
+                REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+            return xml
+        elif format == "json":
+            js = self.json()
+            if REQUEST and publish:
+                REQUEST.RESPONSE.setHeader("content-type", JSON_MIMETYPE)
+            return js
+        else:
+            log("make_page: format=%s" % format)
+            raise ValueError("_make_page: invalid format")
+
+
+# -----
+class SeqGenTable:
+    """Sequence de GenTable: permet de générer un classeur excel avec un tab par table.
+    L'ordre des tabs est conservé (1er tab == 1ere table ajoutée)
+    """
+
+    def __init__(self):
+        self.genTables = OrderedDict()
+
+    def add_genTable(self, name, gentable):
+        self.genTables[name] = gentable
+
+    def get_genTable(self, name):
+        return self.genTables.get(name)
+
+    def excel(self):
+        """Export des genTables dans un unique fichier excel avec plusieurs feuilles tagguées"""
+        book = sco_excel.Workbook()  # Le fichier xls en devenir
+        for (name, gt) in self.genTables.items():
+            gt.excel(wb=book)  # Ecrit dans un fichier excel
+        return book.savetostr()
+
+
+# ----- Exemple d'utilisation minimal.
+if __name__ == "__main__":
+    T = GenTable(
+        rows=[{"nom": "Toto", "age": 26}, {"nom": "Titi", "age": 21}],
+        columns_ids=("nom", "age"),
+    )
+    print("--- HTML:")
+    print(T.gen(format="html"))
+    print("\n--- XML:")
+    print(T.gen(format="xml"))
+    print("\n--- JSON:")
+    print(T.gen(format="json"))
diff --git a/html_sco_header.py b/html_sco_header.py
new file mode 100644
index 0000000000000000000000000000000000000000..c6f4dc053131fcde06a4eb21390337a75a78f13e
--- /dev/null
+++ b/html_sco_header.py
@@ -0,0 +1,262 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+from sco_utils import *
+from sco_formsemestre_status import formsemestre_page_title
+
+"""
+HTML Header/Footer for ScoDoc pages
+"""
+
+
+# Some constants:
+
+# Multiselect menus are used on a few pages and not loaded by default
+BOOTSTRAP_MULTISELECT_JS = [
+    "libjs/bootstrap-3.1.1-dist/js/bootstrap.min.js",
+    "libjs/bootstrap-multiselect/bootstrap-multiselect.js",
+    "libjs/purl.js",
+]
+
+BOOTSTRAP_MULTISELECT_CSS = [
+    "libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css",
+    "libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css",
+    "libjs/bootstrap-multiselect/bootstrap-multiselect.css",
+]
+
+# Header:
+def sco_header(
+    context,
+    REQUEST=None,
+    # optional args
+    container=None,  # objet qui a lancé la demande
+    page_title="",  # page title
+    no_side_bar=False,  # hide sidebar
+    cssstyles=[],  # additionals CSS sheets
+    javascripts=[],  # additionals JS filenames to load
+    scripts=[],  # script to put in page header
+    bodyOnLoad="",  # JS
+    init_jquery=True,  # load and init jQuery
+    init_jquery_ui=True,  # include all stuff for jquery-ui and initialize scripts
+    init_qtip=False,  # include qTip
+    init_google_maps=False,  # Google maps
+    init_datatables=True,
+    titrebandeau="",  # titre dans bandeau superieur
+    head_message="",  # message action (petit cadre jaune en haut)
+    user_check=True,  # verifie passwords temporaires
+):
+    "Main HTML page header for ScoDoc"
+
+    # If running for first time, initialize roles and permissions
+    try:
+        ri = context.roles_initialized
+    except:
+        ri = None  # old instances does not have this attribute
+    if ri == "0":
+        context._setup_initial_roles_and_permissions()
+
+    # context est une instance de ZScolar. container est une instance qui "acquiert" ZScolar
+    if container:
+        context = container  # je pense que cela suffit pour ce qu'on veut.
+
+    # Add a HTTP header (can be used by Apache to log requests)
+    if REQUEST.AUTHENTICATED_USER:
+        REQUEST.RESPONSE.setHeader("X-ScoDoc-User", str(REQUEST.AUTHENTICATED_USER))
+
+    # Get more parameters from REQUEST
+    if not head_message and REQUEST.form.has_key("head_message"):
+        head_message = REQUEST.form["head_message"]
+
+    params = {
+        "page_title": page_title or context.title_or_id(),
+        "no_side_bar": no_side_bar,
+        "ScoURL": context.ScoURL(),
+        "encoding": SCO_ENCODING,
+        "titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
+        "authuser": str(REQUEST.AUTHENTICATED_USER),
+    }
+    if bodyOnLoad:
+        params["bodyOnLoad_mkup"] = """onload="%s" """ % bodyOnLoad
+    else:
+        params["bodyOnLoad_mkup"] = ""
+    if no_side_bar:
+        params["margin_left"] = "1em"
+    else:
+        params["margin_left"] = "140px"
+
+    if init_jquery_ui or init_qtip or init_datatables:
+        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">
+<head>
+<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" />
+
+"""
+        % params
+    ]
+    # jQuery UI
+    if init_jquery_ui:
+        # can modify loaded theme here
+        H.append(
+            '<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
+        )
+    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>'
+        )
+
+    # Feuilles de style additionnelles:
+    for cssstyle in cssstyles:
+        H.append(
+            """<link type="text/css" rel="stylesheet" href="/ScoDoc/static/%s" />\n"""
+            % cssstyle
+        )
+
+    H.append(
+        """
+<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
+<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
+<link href="/ScoDoc/static/css/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">
+ window.onload=function(){enableTooltips("gtrcontent")};
+</script>"""
+        % params
+    )
+
+    # jQuery
+    if init_jquery:
+        H.append(
+            """<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script>
+                  """
+        )
+        H.append(
+            '<script language="javascript" type="text/javascript" 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>'
+        )
+        H.append(
+            '<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />'
+        )
+
+    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>'
+        )
+    if init_google_maps:
+        H.append(
+            '<script type="text/javascript" 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>'
+        )
+    # JS additionels
+    for js in javascripts:
+        H.append(
+            """<script language="javascript" type="text/javascript" src="/ScoDoc/static/%s"></script>\n"""
+            % js
+        )
+
+    H.append(
+        """<style type="text/css">
+.gtrcontent {
+   margin-left: %(margin_left)s;
+   height: 100%%;
+   margin-bottom: 10px;
+}
+</style>
+"""
+        % params
+    )
+    # Scripts de la page:
+    if scripts:
+        H.append("""<script language="javascript" type="text/javascript">""")
+        for script in scripts:
+            H.append(script)
+        H.append("""</script>""")
+
+    H.append("</head>")
+
+    # Body et bandeau haut:
+    H.append("""<body %(bodyOnLoad_mkup)s>""" % params)
+    H.append(CUSTOM_HTML_HEADER)
+    #
+    if not no_side_bar:
+        H.append(context.sidebar(REQUEST))
+    H.append("""<div class="gtrcontent" id="gtrcontent">""")
+    #
+    # Barre menu semestre:
+    H.append(formsemestre_page_title(context, REQUEST))
+
+    # Avertissement si mot de passe à changer
+    if user_check:
+        authuser = REQUEST.AUTHENTICATED_USER
+        passwd_temp = context.Users.user_info(user_name=str(authuser))["passwd_temp"]
+        if passwd_temp:
+            H.append(
+                """<div class="passwd_warn">
+    Attention !<br/>
+    Vous avez reçu un mot de passe temporaire.<br/>
+    Vous devez le changer: <a href="%s/Users/form_change_password?user_name=%s">cliquez ici</a>
+    </div>"""
+                % (context.ScoURL(), str(authuser))
+            )
+    #
+    if head_message:
+        H.append('<div class="head_message">' + cgi.escape(head_message) + "</div>")
+    #
+    # div pour affichage messages temporaires
+    H.append('<div id="sco_msg" class="head_message"></div>')
+    #
+    return "".join(H)
+
+
+def sco_footer(context, REQUEST=None):
+    """Main HTMl pages footer
+    """
+    return """</div><!-- /gtrcontent -->""" + CUSTOM_HTML_FOOTER + """</body></html>"""
diff --git a/html_sidebar.py b/html_sidebar.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf379ce6c4c3762cd20905b6d0f2b4c7005d61b1
--- /dev/null
+++ b/html_sidebar.py
@@ -0,0 +1,185 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+from sco_utils import *
+from ZAbsences import getAbsSemEtud
+
+"""
+Génération de la "sidebar" (marge gauche des pages HTML)
+"""
+
+
+def sidebar_common(context, REQUEST=None):
+    "partie commune a toutes les sidebar"
+    authuser = REQUEST.AUTHENTICATED_USER
+    params = {"ScoURL": context.ScoURL(), "authuser": str(authuser)}
+    H = [
+        '<a class="scodoc_title" href="about">ScoDoc</a>',
+        '<div id="authuser"><a id="authuserlink" href="%(ScoURL)s/Users/userinfo">%(authuser)s</a><br/><a id="deconnectlink" href="%(ScoURL)s/acl_users/logout">déconnexion</a></div>'
+        % params,
+        context.sidebar_dept(REQUEST),
+        """<h2 class="insidebar">Scolarit&eacute;</h2>
+ <a href="%(ScoURL)s" class="sidebar">Semestres</a> <br/> 
+ <a href="%(ScoURL)s/Notes" class="sidebar">Programmes</a> <br/> 
+ <a href="%(ScoURL)s/Absences" class="sidebar">Absences</a> <br/>
+ """
+        % params,
+    ]
+
+    if authuser.has_permission(ScoUsersAdmin, context) or authuser.has_permission(
+        ScoUsersView, context
+    ):
+        H.append(
+            """<a href="%(ScoURL)s/Users" class="sidebar">Utilisateurs</a> <br/>"""
+            % params
+        )
+
+    if 0:  # XXX experimental
+        H.append(
+            """<a href="%(ScoURL)s/Notes/Services" class="sidebar">Services</a> <br/>"""
+            % params
+        )
+
+    if authuser.has_permission(ScoChangePreferences, context):
+        H.append(
+            """<a href="%(ScoURL)s/edit_preferences" class="sidebar">Paramétrage</a> <br/>"""
+            % params
+        )
+
+    return "".join(H)
+
+
+def sidebar(context, REQUEST=None):
+    "Main HTML page sidebar"
+    # rewritten from legacy DTML code
+    params = {"ScoURL": context.ScoURL(), "SCO_USER_MANUAL": SCO_USER_MANUAL}
+
+    H = ['<div class="sidebar">', sidebar_common(context, REQUEST)]
+
+    H.append(
+        """<div class="box-chercheetud">Chercher étudiant:<br/>
+<form id="form-chercheetud" action="%(ScoURL)s/search_etud_in_dept">
+<div><input type="text" size="12" id="in-expnom" name="expnom"></input></div>
+</form></div>
+<div class="etud-insidebar">
+"""
+        % params
+    )
+    # ---- s'il y a un etudiant selectionné:
+    if REQUEST.form.has_key("etudid"):
+        etudid = REQUEST.form["etudid"]
+        etud = context.getEtudInfo(filled=1, etudid=etudid)[0]
+        params.update(etud)
+        # compte les absences du semestre en cours
+        H.append(
+            """<h2 id="insidebar-etud"><a href="%(ScoURL)s/ficheEtud?etudid=%(etudid)s" class="sidebar">
+    <font color="#FF0000">%(sexe)s %(nom_disp)s</font></a>
+    </h2>
+    <b>Absences</b>"""
+            % params
+        )
+        if etud["cursem"]:
+            AbsEtudSem = getAbsSemEtud(context, etud["cursem"], etudid)
+            params["nbabs"] = AbsEtudSem.CountAbs()
+            params["nbabsjust"] = AbsEtudSem.CountAbsJust()
+            params["nbabsnj"] = params["nbabs"] - params["nbabsjust"]
+            params["date_debut"] = etud["cursem"]["date_debut"]
+            params["date_fin"] = etud["cursem"]["date_fin"]
+            H.append(
+                """<span title="absences du %(date_debut)s au %(date_fin)s">(1/2 j.)<br/>%(nbabsjust)s J., %(nbabsnj)s N.J.</span>"""
+                % params
+            )
+
+        H.append("<ul>")
+        if REQUEST.AUTHENTICATED_USER.has_permission(ScoAbsChange, context):
+            H.append(
+                """
+<li>     <a href="%(ScoURL)s/Absences/SignaleAbsenceEtud?etudid=%(etudid)s">Ajouter</a></li>
+<li>     <a href="%(ScoURL)s/Absences/JustifAbsenceEtud?etudid=%(etudid)s">Justifier</a></li>
+<li>     <a href="%(ScoURL)s/Absences/AnnuleAbsenceEtud?etudid=%(etudid)s">Supprimer</a></li>
+"""
+                % params
+            )
+            if context.get_preference("handle_billets_abs"):
+                H.append(
+                    """<li>     <a href="%(ScoURL)s/Absences/listeBilletsEtud?etudid=%(etudid)s">Billets</a></li>"""
+                    % params
+                )
+        H.append(
+            """
+<li>     <a href="%(ScoURL)s/Absences/CalAbs?etudid=%(etudid)s">Calendrier</a></li>
+<li>     <a href="%(ScoURL)s/Absences/ListeAbsEtud?etudid=%(etudid)s">Liste</a></li>
+</ul>
+"""
+            % params
+        )
+    else:
+        pass  # H.append("(pas d'étudiant en cours)")
+    # ---------
+    H.append("</div><br/>&nbsp;")  # /etud-insidebar
+    # Logo
+    scologo_img = icontag("scologo_img")
+    H.append('<div class="logo-insidebar"><div class="logo-logo">%s<br/>' % scologo_img)
+    H.append(
+        """<a href="%(ScoURL)s/about" class="sidebar">A propos</a><br/>
+<a href="%(SCO_USER_MANUAL)s" class="sidebar">Aide</a><br/>
+</div></div>
+
+</div> <!-- end of sidebar -->
+"""
+        % params
+    )  # '
+    #
+    return "".join(H)
+
+
+def sidebar_dept(context, REQUEST=None):
+    """Partie supérieure de la marge de gauche
+    """
+    infos = {
+        "BASE0": REQUEST.BASE0,
+        "DeptIntranetTitle": context.get_preference("DeptIntranetTitle"),
+        "DeptIntranetURL": context.get_preference("DeptIntranetURL"),
+        "DeptName": context.get_preference("DeptName"),
+        "ScoURL": context.ScoURL(),
+    }
+
+    H = [
+        """<h2 class="insidebar">Dépt. %(DeptName)s</h2>
+ <a href="%(BASE0)s" class="sidebar">Accueil</a> <br/> """
+        % infos
+    ]
+    if infos["DeptIntranetURL"]:
+        H.append(
+            '<a href="%(DeptIntranetURL)s" class="sidebar">%(DeptIntranetTitle)s</a> <br/>'
+            % infos
+        )
+    H.append(
+        """<br/><a href="%(ScoURL)s/Entreprises" class="sidebar">Entreprises</a> <br/>"""
+        % infos
+    )
+    return "\n".join(H)
diff --git a/htmlutils.py b/htmlutils.py
new file mode 100644
index 0000000000000000000000000000000000000000..aab3bbb7e3a515fb74cad62cc97f9b84ea35330c
--- /dev/null
+++ b/htmlutils.py
@@ -0,0 +1,72 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Various HTML generation functions
+"""
+
+
+def horizontal_bargraph(value, mark):
+    """html drawing an horizontal bar and a mark
+    used to vizualize the relative level of a student
+    """
+    tmpl = """
+    <span class="graph">
+    <span class="bar" style="width: %(value)d%%;"></span>
+    <span class="mark" style="left: %(mark)d%%; "></span>
+    </span>
+    """
+    return tmpl % {"value": int(value), "mark": int(mark)}
+
+
+import listhistogram
+
+
+def histogram_notes(notes):
+    "HTML code drawing histogram"
+    if not notes:
+        return ""
+    bins, H = listhistogram.ListHistogram(notes, 21, minmax=(0, 20))
+    D = ['<ul id="vhist-q-graph"><li class="vhist-qtr" id="vhist-q1"><ul>']
+    left = 5
+    colwidth = 16  # must match #q-graph li.bar width in stylesheet
+    if max(H) <= 0:
+        return ""
+    hfactor = 95.0 / max(H)  # garde une marge de 5% pour l'esthetique
+    for i in range(len(H)):
+        if H[i] >= 0:
+            x = left + i * (4 + colwidth)
+            heightpercent = H[i] * hfactor
+            if H[i] > 0:
+                nn = "<p>%d</p>" % H[i]
+            else:
+                nn = ""
+            D.append(
+                '<li class="vhist-bar" style="left:%dpx;height:%f%%">%s<p class="leg">%d</p></li>'
+                % (x, heightpercent, nn, i)
+            )
+    D.append("</ul></li></ul>")
+    return "\n".join(D)
diff --git a/imageresize.py b/imageresize.py
new file mode 100644
index 0000000000000000000000000000000000000000..252a6f6dce9acbb5a8e2615bd3e9a1bf3cc07d50
--- /dev/null
+++ b/imageresize.py
@@ -0,0 +1,29 @@
+"""Simple image resize using PIL"""
+
+from PIL import Image as PILImage
+from cStringIO import StringIO
+
+
+def ImageScale(img_file, maxx, maxy):
+    im = PILImage.open(img_file)
+    im.thumbnail((maxx, maxy), PILImage.ANTIALIAS)
+    out_file_str = StringIO()
+    im.save(out_file_str, im.format)
+    out_file_str.seek(0)
+    tmp = out_file_str.read()
+    out_file_str.close()
+    return tmp
+
+
+def ImageScaleH(img_file, W=None, H=90):
+    im = PILImage.open(img_file)
+    if W is None:
+        # keep aspect
+        W = (im.size[0] * H) / im.size[1]
+    im.thumbnail((W, H), PILImage.ANTIALIAS)
+    out_file_str = StringIO()
+    im.save(out_file_str, im.format)
+    out_file_str.seek(0)
+    tmp = out_file_str.read()
+    out_file_str.close()
+    return tmp
diff --git a/intervals.py b/intervals.py
new file mode 100644
index 0000000000000000000000000000000000000000..f7ac47d03a00e5ccdc0293b7ff25317bcb088e06
--- /dev/null
+++ b/intervals.py
@@ -0,0 +1,245 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+
+# Code from http://code.activestate.com/recipes/457411/
+
+from bisect import bisect_left, bisect_right
+from itertools import izip
+
+
+class intervalmap(object):
+    """
+        This class maps a set of intervals to a set of values.
+        
+        >>> i = intervalmap()
+        >>> i[0:5] = '0-5'
+        >>> i[8:12] = '8-12'
+        >>> print i[2]
+        0-5
+        >>> print i[10]
+        8-12
+        >>> print repr(i[-1])
+        None
+        >>> print repr(i[17])
+        None
+        >>> i[4:9] = '4-9'
+        >>> print [(j,i[j]) for j in range(6)]
+        [(0, '0-5'), (1, '0-5'), (2, '0-5'), (3, '0-5'), (4, '4-9'), (5, '4-9')]
+        >>> print list(i.items())
+        [((0, 4), '0-5'), ((4, 9), '4-9'), ((9, 12), '8-12')]
+        >>> i[:0] = 'less than 0'
+        >>> i[-5]
+        'less than 0'
+        >>> i[0]
+        '0-5'
+        >>> print list(i.items())
+        [((None, 0), 'less than 0'), ((0, 4), '0-5'), ((4, 9), '4-9'), ((9, 12), '8-12')]
+        >>> i[21:] = 'more than twenty'
+        >>> i[42]
+        'more than twenty'
+        >>> i[10.5:15.5] = '10.5-15.5'
+        >>> i[11.5]
+        '10.5-15.5'
+        >>> i[0.5]
+        '0-5'
+        >>> print list(i.items())
+        [((None, 0),... ((9, 10.5), '8-12'), ((10.5, 15.5), '10.5-15.5'), ((21, None),...
+        >>> i = intervalmap()
+        >>> i[0:2] = 1
+        >>> i[2:8] = 2
+        >>> i[4:] = 3
+        >>> i[5:6] = 4  
+        >>> i
+        {[0, 2] => 1, [2, 4] => 2, [4, 5] => 3, [5, 6] => 4, [6, None] => 3}
+    """
+
+    def __init__(self):
+        """
+            Initializes an empty intervalmap.
+        """
+        self._bounds = []
+        self._items = []
+        self._upperitem = None
+
+    def __setitem__(self, _slice, _value):
+        """
+            Sets an interval mapping.
+        """
+        assert isinstance(_slice, slice), "The key must be a slice object"
+
+        if _slice.start is None:
+            start_point = -1
+        else:
+            start_point = bisect_left(self._bounds, _slice.start)
+
+        if _slice.stop is None:
+            end_point = -1
+        else:
+            end_point = bisect_left(self._bounds, _slice.stop)
+
+        if start_point >= 0:
+            if (
+                start_point < len(self._bounds)
+                and self._bounds[start_point] < _slice.start
+            ):
+                start_point += 1
+
+            if end_point >= 0:
+                self._bounds[start_point:end_point] = [_slice.start, _slice.stop]
+                if start_point < len(self._items):
+                    self._items[start_point:end_point] = [
+                        self._items[start_point],
+                        _value,
+                    ]
+                else:
+                    self._items[start_point:end_point] = [self._upperitem, _value]
+            else:
+                self._bounds[start_point:] = [_slice.start]
+                if start_point < len(self._items):
+                    self._items[start_point:] = [self._items[start_point], _value]
+                else:
+                    self._items[start_point:] = [self._upperitem]
+                self._upperitem = _value
+        else:
+            if end_point >= 0:
+                self._bounds[:end_point] = [_slice.stop]
+                self._items[:end_point] = [_value]
+            else:
+                self._bounds[:] = []
+                self._items[:] = []
+                self._upperitem = _value
+
+    def __getitem__(self, _point):
+        """
+            Gets a value from the mapping.
+        """
+        assert not isinstance(_point, slice), "The key cannot be a slice object"
+
+        index = bisect_right(self._bounds, _point)
+        if index < len(self._bounds):
+            return self._items[index]
+        else:
+            return self._upperitem
+
+    def items(self):
+        """
+            Returns an iterator with each item being
+            ((low_bound,high_bound), value). The items are returned
+            in order.
+        """
+        previous_bound = None
+        for b, v in izip(self._bounds, self._items):
+            if v is not None:
+                yield (previous_bound, b), v
+            previous_bound = b
+        if self._upperitem is not None:
+            yield (previous_bound, None), self._upperitem
+
+    def values(self):
+        """
+            Returns an iterator with each item being a stored value. The items
+            are returned in order.
+        """
+        for v in self._items:
+            if v is not None:
+                yield v
+        if self._upperitem is not None:
+            yield self._upperitem
+
+    def __repr__(self):
+        s = []
+        for b, v in self.items():
+            if v is not None:
+                s.append("[%r, %r] => %r" % (b[0], b[1], v))
+        return "{" + ", ".join(s) + "}"
+
+
+if __name__ == "__main__":
+    # Test 1
+    i = intervalmap()
+    i[9:] = "!"
+    assert repr(i) == "{[9, None] => '!'}"
+    i[:5] = "Hello"
+    i[6:7] = "World"
+    assert repr(i) == "{[None, 5] => 'Hello', [6, 7] => 'World', [9, None] => '!'}"
+    i[8:10] = "(Test)"
+    assert (
+        repr(i)
+        == "{[None, 5] => 'Hello', [6, 7] => 'World', [8, 10] => '(Test)', [10, None] => '!'}"
+    )
+    i[:3] = "My,"
+    assert (
+        repr(i)
+        == "{[None, 3] => 'My,', [3, 5] => 'Hello', [6, 7] => 'World', [8, 10] => '(Test)', [10, None] => '!'}"
+    )
+    i[5.5:6] = "Cruel"
+    assert (
+        repr(i)
+        == "{[None, 3] => 'My,', [3, 5] => 'Hello', [5.5, 6] => 'Cruel', [6, 7] => 'World', [8, 10] => '(Test)', [10, None] => '!'}"
+    )
+    i[6:6.5] = "And Harsh"
+    assert (
+        repr(i)
+        == "{[None, 3] => 'My,', [3, 5] => 'Hello', [5.5, 6] => 'Cruel', [6, 6.5] => 'And Harsh', [6.5, 7] => 'World', [8, 10] => '(Test)', [10, None] => '!'}"
+    )
+    i[5.9:6.6] = None
+    assert (
+        repr(i)
+        == "{[None, 3] => 'My,', [3, 5] => 'Hello', [5.5, 5.9000000000000004] => 'Cruel', [6.5999999999999996, 7] => 'World', [8, 10] => '(Test)', [10, None] => '!'}"
+    )
+    assert " ".join(i.values()) == "My, Hello Cruel World (Test) !"
+    print("Test 1 OK")
+
+    # Test 2
+    i = intervalmap()
+    i[:0] = "A"
+    i[2:5] = "B"
+    i[8:10] = "C"
+    i[12:] = "D"
+    assert (
+        repr(i)
+        == "{[None, 0] => 'A', [2, 5] => 'B', [8, 10] => 'C', [12, None] => 'D'}"
+    )
+    i[:] = "K"
+    assert repr(i) == "{[None, None] => 'K'}"
+    assert i[5] == "K"
+    i[0:10] = "L"
+    i[6:8] = "M"
+    i[20:] = "J"
+    assert i[-1] == "K"
+    assert i[5] == "L"
+    assert i[7] == "M"
+    assert i[9] == "L"
+    assert i[15] == "K"
+    assert i[42] == "J"
+    print("Test 2 OK")
+
+    # Test 3
+    try:
+        from datetime import datetime
+    except:
+        print("Test 3 skipped")
+    else:
+        i = intervalmap()
+        i[: datetime(2005, 10, 24)] = "A"
+        i[datetime(2005, 11, 11) : datetime(2005, 11, 17)] = "B"
+        i[datetime(2005, 11, 30) :] = "C"
+        assert i[datetime(2005, 9, 25)] == "A"
+        assert i[datetime(2005, 10, 23)] == "A"
+        assert i[datetime(2005, 10, 26)] == None
+        assert i[datetime(2005, 11, 9)] == None
+        assert i[datetime(2005, 11, 16)] == "B"
+        assert i[datetime(2005, 11, 23)] == None
+        assert i[datetime(2005, 11, 29)] == None
+        assert i[datetime(2005, 11, 30)] == "C"
+        assert i[datetime(2005, 12, 3)] == "C"
+        print("Test 3 OK")
+
+    try:
+        import doctest
+    except:
+        print("Skipping the doctests")
+    else:
+        print("And now, the doctests")
+        doctest.testmod(optionflags=doctest.ELLIPSIS)
diff --git a/listhistogram.py b/listhistogram.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e19a6b439ebab23e079cf2056cacd7be8cd3457
--- /dev/null
+++ b/listhistogram.py
@@ -0,0 +1,31 @@
+import math
+
+
+def ListHistogram(L, nbins, minmax=None, normalize=None):
+    """Compute histogram of a list.
+    Does not use Numeric or numarray.
+
+    H[i] is the number of elements from L
+    such that bins[i] <= L[i] < bins[i+1]
+    """
+    n = len(L)
+    if minmax is None:
+        xmin = min(L)
+        xmax = max(L)
+    else:
+        xmin, xmax = minmax
+        # clip data
+        for i in range(n):
+            if L[i] < xmin:
+                L[i] = xmin
+            if L[i] > xmax:
+                L[i] = xmax
+    bin_width = (xmax - xmin) / float(nbins - 1)
+    H = [0] * nbins
+    for i in range(n):
+        idx = int(math.floor((L[i] - xmin) / bin_width))
+        H[idx] += 1
+    bins = []
+    for i in range(nbins):
+        bins.append(xmin + bin_width * i)
+    return bins, H
diff --git a/logos/logo_footer.jpg b/logos/logo_footer.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..1afe1fe65139f29f2b49bd8f41e8db86f80f84e5
Binary files /dev/null and b/logos/logo_footer.jpg differ
diff --git a/logos/logo_header.jpg b/logos/logo_header.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..53117a9fbe8511034a298a250c3cc0b38afd7b0a
Binary files /dev/null and b/logos/logo_header.jpg differ
diff --git a/misc/ArreteDUT2005.txt b/misc/ArreteDUT2005.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4cbf29433764e92213e6deca06b7b14fcf87a938
--- /dev/null
+++ b/misc/ArreteDUT2005.txt
@@ -0,0 +1,88 @@
+
+Source: http://www.education.gouv.fr/bo/2005/31/MENS0501754A.htm
+
+Extrait:
+Titre III - Validation des parcours de formation
+
+Chapitre 2 - Contrôle des connaissances et déroulement des études
+
+Article 19 - Les unités d'enseignement sont définitivement acquises et
+capitalisables dès lors que l'étudiant y a obtenu la moyenne. 
+L'acquisition de l'unité d'enseignement emporte l'acquisition
+des crédits européens correspondants.
+
+Toute unité d'enseignement capitalisée est prise en compte dans le
+dispositif de compensation, au même titre et dans les mêmes conditions
+que les autres unités d'enseignement.
+
+Dans le cas de redoublement d'un semestre, si un étudiant ayant acquis
+une unité d'enseignement souhaite, notamment pour améliorer les
+conditions de réussite de sa formation, suivre les enseignements de
+cette unité d'enseignement et se représenter au contrôle des
+connaissances correspondant, la compensation prend en compte le
+résultat le plus favorable pour l'étudiant.
+
+
+Article 20 - La validation d'un semestre est acquise de droit lorsque
+l'étudiant a obtenu à la fois: 
+
+a) une moyenne générale égale ou supérieure à 10 sur 20 et une moyenne
+égale ou supérieure à 8 sur 20 dans chacune des unités d'enseignement;
+
+b) la validation des semestres précédents, lorsqu'ils existent.
+Lorsque les conditions posées ci-dessus ne sont pas remplies, la
+validation est assurée, sauf opposition de l'étudiant, par une
+compensation organisée entre deux semestres consécutifs sur la base
+d'une moyenne générale égale ou supérieure à 10 sur 20 et d'une
+moyenne égale ou supérieure à 8 sur 20 dans chacune des unités
+d'enseignement constitutives de ces semestres. Le semestre servant à
+compenser ne peut être utilisé qu'une fois au cours du cursus.
+
+En outre, le directeur de l'IUT peut prononcer la validation d'un
+semestre sur proposition du jury.  La validation de tout semestre
+donne lieu à l'obtention de l'ensemble des unités d'enseignement qui
+le composent et des crédits européens correspondants.
+
+
+Article 21 - La poursuite d'études dans un nouveau semestre est de
+droit pour tout étudiant à qui ne manque au maximum que la validation
+d'un seul semestre de son cursus. 
+
+
+Article 22 - Le redoublement est de droit dans les cas où :
+
+- l'étudiant a obtenu la moyenne générale et lorsque celle-ci ne
+suffit pas pour remplir la condition posée au a) de l'article 20
+ci-dessus;
+- l'étudiant a rempli la condition posée au a) de l'article 20
+ci-dessus dans un des deux semestres utilisés dans le processus de
+compensation.
+
+En outre, l'étudiant peut être autorisé à redoubler par décision du
+directeur de l'IUT, sur proposition du jury de passage ou du jury de
+délivrance pour l'obtention du diplôme universitaire de technologie.
+Durant la totalité du cursus conduisant au diplôme universitaire de
+technologie, l'étudiant ne peut être autorisé à redoubler plus de deux
+semestres. En cas de force majeure dûment justifiée et appréciée par
+le directeur de l'IUT, un redoublement supplémentaire peut être
+autorisé.
+
+La décision définitive refusant l'autorisation de redoubler est prise
+après avoir entendu l'étudiant à sa demande. Elle doit être motivée et
+assortie de conseils d'orientation.
+
+
+(...)
+
+
+Chapitre 3 - Jurys, délivrance du diplôme et droits des étudiants
+
+Article 25 -  Les unités d'enseignement dans lesquelles la moyenne de
+10 a été obtenue sont capitalisables en vue de la reprise d'études en
+formation continue.
+
+Les étudiants qui sortent de l'IUT sans avoir obtenu le diplôme
+universitaire de technologie reçoivent une attestation d'études
+comportant la liste des unités d'enseignement capitalisables qu'ils
+ont acquises, ainsi que les crédits européens correspondants, délivrée
+par le directeur de l'IUT.
diff --git a/misc/Interface.xlsm b/misc/Interface.xlsm
new file mode 100644
index 0000000000000000000000000000000000000000..281f7f971c2f2db794adaaea5670c9c1ce9f74cf
Binary files /dev/null and b/misc/Interface.xlsm differ
diff --git a/misc/PublicationBulletins/Bulletins-Orleans/images/code.jpg b/misc/PublicationBulletins/Bulletins-Orleans/images/code.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..95ff057d584fa11ce8573e60742121f9fc22ddc4
Binary files /dev/null and b/misc/PublicationBulletins/Bulletins-Orleans/images/code.jpg differ
diff --git a/misc/PublicationBulletins/Bulletins-Orleans/images/info.png b/misc/PublicationBulletins/Bulletins-Orleans/images/info.png
new file mode 100644
index 0000000000000000000000000000000000000000..822a51328e352055f5be708b259105da7d8c1d15
Binary files /dev/null and b/misc/PublicationBulletins/Bulletins-Orleans/images/info.png differ
diff --git a/misc/PublicationBulletins/Bulletins-Orleans/images/puce-iut-transparent.png b/misc/PublicationBulletins/Bulletins-Orleans/images/puce-iut-transparent.png
new file mode 100644
index 0000000000000000000000000000000000000000..d74977e6f6a2aa7ebdb19a524b576e5d80052513
Binary files /dev/null and b/misc/PublicationBulletins/Bulletins-Orleans/images/puce-iut-transparent.png differ
diff --git a/misc/PublicationBulletins/Bulletins-Orleans/images/stop.png b/misc/PublicationBulletins/Bulletins-Orleans/images/stop.png
new file mode 100644
index 0000000000000000000000000000000000000000..2fe566266ae0ada328e037611588f0fa829c5622
Binary files /dev/null and b/misc/PublicationBulletins/Bulletins-Orleans/images/stop.png differ
diff --git a/misc/PublicationBulletins/Bulletins-Orleans/index.php b/misc/PublicationBulletins/Bulletins-Orleans/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..5e3259b142528ab5d62a013f22488f3f27b10e13
--- /dev/null
+++ b/misc/PublicationBulletins/Bulletins-Orleans/index.php
@@ -0,0 +1,310 @@
+<?php
+// https://trac.lipn.univ-paris13.fr/projects/scodoc/wiki/ScoDocAPI
+// La publication des notes suppose que l'option "Semestres => Menu Semestre => Modifier le semestre => Publication" soit cochée.
+
+// Code contribué par Yann Leboulanger (Université Paris 10), Juin 2013
+// et modifié par  Pascal Legrand <pascal.legrand@univ-orleans.fr> (Nov 2017)
+//
+// Exemple publication des bulletins de notes vers les étudiants
+// L'étudiant est authenfié via le CAS 
+// Le bulletin est récupéré en format XML en interrogeant ScoDoc
+// 
+// Il faut créer un utilisateur ScoDoc n'ayant que des droits de lecture.
+//
+// A adapter à  vos besoins locaux.
+
+include_once 'CAS.php';
+// *********************************************** CONFIGURATION ***************************************************
+phpCAS::client(CAS_VERSION_2_0,'URL_CAS',443,'');
+phpCAS::setNoCasServerValidation();
+phpCAS::forceAuthentication();
+
+$nip = phpCAS::getUser();
+
+// Login information of a scodoc user that can access notes
+$sco_user = 'USER';
+$sco_pw = 'PASS';
+$sco_url = 'https://SERVEUR/ScoDoc/';
+
+// URL où sont stockées les photos, si celle-ci diffère de "$sco_url". 
+// Cette valeur est concaténée avec la valeur de "etudiant['photo_url']". (/ScoDoc/static/photos/.....)
+$photo_url = 'https://SERVEUR/ScoDoc/';
+// *********************************************** CONFIGURATION ***************************************************
+
+// ************************************************* FONCTIONS *****************************************************
+// Définition de la fonction d'encodage des headers
+function http_build_headers( $headers ) {
+         $headers_brut = '';
+         foreach( $headers as $nom => $valeur ) {
+                $headers_brut .= $nom . ': ' . $valeur . "\r\n";
+         }
+         return $headers_brut;
+}
+
+// Récupération du département
+function get_dept($nip) {
+	     global $sco_url;
+         $dept = file_get_contents( $sco_url . 'get_etud_dept?code_nip=' . $nip);
+         return ($dept);
+}
+
+function get_EtudInfos_page($nip, $dept) {
+// Récupération des informations concernant l'étudiant.
+// Nécessite une authentification avec sco_user et sco_pw - Il est possible de choisir le format XML ou JSON.
+// etud_info
+// Paramètres: etudid ou code_nip ou code_ine
+// Résultat: informations sur cet étudiant et les semestres dans lesquels il est (ou a été) inscrit.
+// Exemple: etud_info?format=json&etudid=12345
+         global $sco_user;
+	     global $sco_pw;
+         global $sco_url;
+         $donnees = array('format' => 'xml', 'code_nip' => $nip, '__ac_name' => $sco_user, '__ac_password' => $sco_pw);
+    // Création du contenu brut de la requête
+         $contenu = http_build_query($donnees);
+    // Définition des headers
+         $headers = http_build_headers(array('Content-Type' => 'application/x-www-form-urlencoded', 'Content-Length' => strlen( $contenu)));
+     // Définition du contexte
+         $options = array('http' => array('method' => 'POST', 'content' => $contenu, 'header' => $headers));
+    // Création du contexte
+         $contexte = stream_context_create($options);
+    // Envoi du formulaire POST
+         $retour = file_get_contents($sco_url . $dept . '/Scolarite/Notes/etud_info', false, $contexte);
+         return ($retour);
+}
+
+function get_all_semestres($xml_data)
+// Tous les semestres suivis par l'étudiant
+{
+         $data = array();
+         $xml = simplexml_load_string($xml_data);
+         foreach ($xml->insemestre as $s) {
+                 $sem = (array) $s['formsemestre_id'];
+                 $data[] = $sem[0];
+         }
+         return $data;
+}
+
+function get_current_semestre($xml_data)
+// Semestre courrant suivi par l'étudiant
+{
+         $xml = simplexml_load_string($xml_data);
+         foreach ($xml->insemestre as $s) {
+                 if ($s['current'] == 1)
+                    $sem = (array) $s['formsemestre_id'];
+                    return ($sem[0]);
+         }
+}
+
+function get_semestre_info($sem, $dept) {
+// Renvoi les informations détaillées d'un semestre
+// Ne nécessite pas d'authentification avec sco_user et sco_pw - Il est possible de choisir le format XML ou JSON.
+// formsemestre_list
+// Paramètres (tous optionnels): formsesmestre_id, formation_id, etape_apo, etape_apo2
+// Coquille dans la doc : formsesmestre_id
+// Résultat: liste des semestres correspondant.
+// Exemple: formsemestre_list?format=xml&etape_apo=V1RT 
+         global $sco_pw;
+         global $sco_user;
+         global $sco_url;
+         $donnees = array('format' => 'xml', 'formsemestre_id' => $sem, '__ac_name' => $sco_user, '__ac_password' => $sco_pw);
+    // Création du contenu brut de la requête
+         $contenu = http_build_query( $donnees );
+    // Définition des headers
+         $headers = http_build_headers( array('Content-Type' => 'application/x-www-form-urlencoded', 'Content-Length' => strlen( $contenu) ) );
+     // Définition du contexte
+         $options = array( 'http' => array('method' => 'POST', 'content' => $contenu, 'header' => $headers ) );
+    // Création du contexte
+         $contexte = stream_context_create($options);
+    // Envoi du formulaire POST
+         $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/formsemestre_list', false, $contexte );
+/*
+         echo '<div class="code"><img src="images/code.jpg"><br />';
+         echo '<b>get_semestre_info : </b>';
+         echo '<pre>' . htmlentities($retour) . '</pre>';
+         echo '</div>';
+*/
+         return ($retour);
+}
+
+function print_semestres_list($sems, $dept, $sem) {
+// Affiche le nom (titre_num) de tous les semestres suivis par l'étudiant dans un formulaire
+         echo ' <form action="index.php" method="post">' . "\n";
+         echo '  <fieldset>' . "\n";
+         echo '   <legend>Liste des semestres</legend>' . "\n";
+         echo '   <p><label for="sem">Semestre sélectionné: </label>' . "\n";
+         echo '    <select name="sem" id="sem">' . "\n";
+         for ($i=0; $i < count($sems); $i++) {
+              $s = $sems[$i];
+              $retour = get_semestre_info($s, $dept);
+              $xml = simplexml_load_string($retour);
+              echo '     <option value="' . $s . '"';
+              if ($s == $sem) {
+                   echo ' selected';
+              }
+              echo '>' . htmlentities($xml->formsemestre['titre_num']) . '</option>' . "\n";
+         }
+         echo '    </select>' . "\n";
+         echo '    <br /><input type="radio" name="notes_moy" id="notes_moy_1" value="notes" required ';    if (isset($_POST['notes_moy']) && $_POST['notes_moy']=='notes')    echo 'checked="checked"'; echo '/><label for="notes_moy_1">Notes</label>' . "\n";
+         echo '    <br /><input type="radio" name="notes_moy" id="notes_moy_2" value="moyennes" required '; if (isset($_POST['notes_moy']) && $_POST['notes_moy']=='moyennes') echo 'checked="checked"'; echo '/><label for="notes_moy_2">Moyennes</label>' . "\n";
+         echo '    <p><input class="submit" type="submit" name="submit" value="Valider" />' . "\n";
+         echo '  </fieldset>' . "\n";
+         echo ' </form>' . "\n";
+}
+
+function get_bulletinetud_page($nip, $sem, $dept) {
+// formsemestre_bulletinetud
+// Paramètres: formsemestre_id, etudid, format (xml ou json), version (short, selectedevalsou long)
+// Résultat: bulletin de notes
+// Exemple: ici au format JSON, pour une version courte (version=short) 
+         global $sco_user;
+         global $sco_pw;
+         global $sco_url;
+         $donnees = array('format' => 'xml', 'code_nip' => $nip, 'formsemestre_id' => $sem, 'version' => 'long', '__ac_name' => $sco_user, '__ac_password' => $sco_pw );
+    // Création du contenu brut de la requête
+         $contenu = http_build_query( $donnees );
+    // Définition des headers
+         $headers = http_build_headers( array('Content-Type' => 'application/x-www-form-urlencoded', 'Content-Length' => strlen( $contenu) ) );
+     // Définition du contexte
+         $options = array( 'http' => array('method' => 'POST', 'content' => $contenu, 'header' => $headers ) );
+    // Création du contexte
+         $contexte = stream_context_create($options);
+    // Envoi du formulaire POST
+         $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/formsemestre_bulletinetud', false, $contexte );
+         return ($retour);
+}
+
+function print_semestre($xml_data, $sem, $dept, $show_moy) {
+         global $photo_url;
+         $xml = simplexml_load_string($xml_data);
+         echo ' <h2><img src="' . $photo_url . $xml->etudiant['photo_url'] . '"> ' . $xml->etudiant['sexe'] . ' ' . $xml->etudiant['prenom'] . ' ' . $xml->etudiant['nom'] . '</h2>' . "\n" . ' <br />' . "\n";
+         $retour = get_semestre_info($sem, $dept);
+         $xml2 = simplexml_load_string($retour);
+         $publie= $xml2->formsemestre['bul_hide_xml'];
+         if (isset($xml->absences)) {
+              (isset($xml->absences['nbabs'])) ? $nbabs = $xml->absences['nbabs']: $nbabs = 0;
+              (isset($xml->absences['nbabsjust'])) ? $nbabsjust = $xml->absences['nbabsjust']: $nbabsjust = 0;
+              echo ' <span class="info">Vous avez à  ce jour<span class="nbabs"> ' . $nbabs . ' </span>demi-journée(s) d\'absences, dont<span class="nbabsjust"> ' . $nbabsjust . ' </span>justifiée(s) </span><br />' . "\n";
+         }
+         else {
+              echo ' <span class="info"><img src="images/info.png"> Les absences ne sont pas saisies. <img src="images/info.png"></span><br />' . "\n";
+         }
+         echo ' <h2>' . htmlentities($xml2->formsemestre['titre_num']) . '</h2>' . "\n";
+         if ($publie == 1) {
+              echo '<span class="alert"><img src="images/info.png"> Publication des notes non activée sur ScoDoc pour ce semestre <img src="images/info.png"></span><br />' . "\n";
+         }
+         else {
+              echo ' <br />' . "\n";
+              echo ' <div class="bulletin">' . "\n";
+              echo '  <table cellspacing="0" cellpadding="0">' . "\n";
+              echo '   <tr>' . "\n";
+              echo '    <td class="titre">UE</td>' . "\n";
+              echo '    <td class="titre">Module</td>' . "\n";
+              echo '    <td class="titre">Evaluation</td>' . "\n";
+              echo '    <td class="titre">Note/20</td>' . "\n";
+              echo '    <td class="titre">Coef</td>' . "\n";
+              echo '   </tr>' . "\n";
+              if ($show_moy) {
+                   echo '   <tr>' . "\n";
+                   echo '    <td class="titre" colspan="3">Moyenne générale:</td>' . "\n";
+                   echo '    <td class="titre">' . $xml->note['value'] . '</td>' . "\n";
+                   echo '    <td class="titre"></td>' . "\n";
+                   echo '   </tr>' . "\n";
+              }
+              foreach ($xml->ue as $ue) {
+                   $coef = 0;
+                   foreach ($ue->module as $mod) {
+		     $coef += (float) $mod['coefficient'];
+		   }
+                  echo '   <tr>' . "\n";
+                  echo '    <td class="ue">' . $ue['acronyme'] . ' <br /> ' . htmlentities($ue['titre']) . '</td>' . "\n";
+                  echo '    <td class="titre_vide"></td>' . "\n";
+                  echo '    <td class="titre_vide"></td>' . "\n";
+                  if ($show_moy) {
+                       echo '    <td class="moyennes_bold">' . $ue->note['value'] . '</td>' . "\n";
+                  }
+                  else {
+                       echo '   <td class="titre_vide"></td>' . "\n";
+                  }
+                  echo '    <td class="coef_ue">' . $coef . '</td>   </tr>' . "\n";
+                  foreach ($ue->module as $mod) {
+                       echo '   <tr>' . "\n";
+                       echo '    <td class="ue_vide"></td>' . "\n";
+                       echo '    <td class="module">' . htmlentities($mod['titre']) . '</td>' . "\n";
+                       echo '    <td class="evaluation_vide"></td>' . "\n";
+                       if ($show_moy) {
+                            echo '    <td class="moyennes">' . $mod->note['value'] . '</td>' . "\n";
+                       }
+                       else {
+                            echo '    <td class="note_vide"></td>' . "\n";
+                       }
+                       echo '    <td class="coef">' . $mod['coefficient'] . '</td>' . "\n";
+                       echo '   </tr>' . "\n";
+                       if (!$show_moy) {
+                            foreach ($mod->evaluation as $eval) {
+                                 echo '   <tr>' . "\n";
+                                 echo '    <td class="ue_vide"></td>' . "\n";
+                                 echo '    <td class="module_vide"></td>' . "\n";
+                                 echo '    <td class="evaluation">' . htmlentities($eval['description']) . '</td>' . "\n";
+                                 echo '    <td class="note">' . $eval->note['value'] . '</td>' . "\n";
+                                 echo '    <td class="coef_vide"></td>' . "\n";
+                                 echo '   </tr>' . "\n";
+                            } 
+                       }
+                  }
+              }
+              echo '  </table>' . "\n";
+              echo ' </div>' . "\n";
+              echo ' <br />' . "\n";
+              if ($show_moy) {
+                   echo $xml->situation . "\n";
+              }
+         }
+}
+// ************************************************* FONCTIONS *****************************************************
+
+// **************************************************  HTML    *****************************************************
+echo'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
+ <head>
+  <title>Bulletins de notes</title>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <meta http-equiv="Content-Style-Type" content="text/css" />
+  <link href="style.css" rel="stylesheet" type="text/css" />
+ </head>
+<body>
+';
+
+$dept = get_dept($nip);
+if ($dept) {
+     $etud_info = get_EtudInfos_page($nip, $dept);
+     $sems = get_all_semestres($etud_info);
+     $sem_current = get_current_semestre($etud_info);
+//   (Condition) ? <Condition=True>:<Condition=False>
+     (isset($_POST['sem'])) ? $sem = $_POST['sem']:$sem = $sem_current;
+     print_semestres_list($sems, $dept, $sem);
+     (!isset($_POST['notes_moy'])) ? $_POST['notes_moy']='notes':'';
+     echo ' <br /><span class="info">Affichage des ' . ucfirst($_POST['notes_moy']) . '</span>' . "\n";;
+     $bulletin_page = get_bulletinetud_page($nip, $sem, $dept);
+     ($_POST['notes_moy'] == 'notes') ? print_semestre($bulletin_page, $sem, $dept, False):print_semestre($bulletin_page, $sem, $dept, True);
+//   ($sem == $sem_current) ? print_semestre($bulletin_page, $sem, $dept, False):print_semestre($bulletin_page, $sem, $dept, True);
+}
+else {
+     echo '<span class=alert><img src="images/info.png"> Numéro étudiant inconnu : ' . $nip . ' - Contactez votre Chef de département <img src="images/info.png"></span><br />' . "\n";
+}
+$erreur=0;    // Tout est OK
+/*
+echo '<div class="code"><img src="images/code.jpg"><br />';
+echo '<b>get_etud_info : </b>';
+echo '<pre>' . htmlentities($etud_info) . '</pre>';
+echo '<b>sems : </b>';
+echo '<pre>' . print_r($sems) . '</pre>';
+echo '<b>sem_current : </b>';
+echo '<pre>' . htmlentities($sem_current) . '</pre>';
+echo '<b>get_bulletinetud_page : </b>';
+echo '<pre>' . htmlentities($bulletin_page) . '</pre>';
+echo '</div>';
+*/
+echo '</body>' . "\n";
+echo '</html>' . "\n";
+// **************************************************  HTML    *****************************************************
+?>
diff --git a/misc/PublicationBulletins/Bulletins-Orleans/style.css b/misc/PublicationBulletins/Bulletins-Orleans/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..d9de68f23b658a33d0a476c84d0726d30262cbb9
--- /dev/null
+++ b/misc/PublicationBulletins/Bulletins-Orleans/style.css
@@ -0,0 +1,419 @@
+html:after {
+  /* common custom values */
+  content: "IUT de Chartres Specimen"; /* your site name */
+  font-size: 720%;         /* font size */
+  color: rgba(0, 0, 0, .05);
+  /* alpha, could be even rgba(0,0,0,.02) */
+
+  /* rest of the logic */
+  z-index: 9999;
+  cursor: default;
+  display: block;
+  position: fixed;
+  top: 33%;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-weight: bold;
+  font-style: italic;
+  text-align: center;
+  line-height: 100%;
+
+  /* not sure about who implemented what ..
+    ... so bring it all */
+  -webkit-pointer-events: none;
+  -moz-pointer-events: none;
+  -ms-pointer-events: none;
+  -o-pointer-events: none;
+  pointer-events: none;
+
+  -webkit-transform: rotate(-45deg);
+  -moz-transform: rotate(-45deg);
+  -ms-transform: rotate(-45deg);
+  -o-transform: rotate(-45deg);
+  transform: rotate(-45deg);
+
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  -o-user-select: none;
+  user-select: none;
+}
+
+body {
+  font-weight:light;
+  font-family:arial;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color: #4a6e91;
+  text-align: left;
+}
+
+table {
+   border-collapse: collapse;
+   border: #4a6e91 1px solid;
+   width: 100%;
+   font-family : arial, verdana, sans-serif ;
+   font-size: 12px;
+}
+
+td.titre {
+  border-top: #4a6e91 1px solid; 
+  border-right: #4a6e91 1px solid; 
+  border-left: #4a6e91 1px solid; 
+  border-bottom: #4a6e91 1px solid; 
+  font-weight:bold;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.titre_vide {
+  background-color: #dddddd;
+  border-top: #4a6e91 1px solid; 
+  font-weight:bold;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.ue {
+  background-color: #dddddd;
+  border-top: #4a6e91 1px solid; 
+
+  font-weight:bold;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.ue_vide {
+  background-color: #dddddd;
+  border-right: #4a6e91 1px solid; 
+  font-weight:bold;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.module {
+  background-color: #eeeeee;
+  border-top: #4a6e91 1px solid; 
+  border-right: #4a6e91 0px solid; 
+  border-left: #4a6e91 1px solid; 
+  font-weight:bold;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.module_vide {
+  background-color: #eeeeee;
+  font-weight:light;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.evaluation {
+  background-color: #ffffff;
+  font-weight:light;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.evaluation_vide {
+  background-color: #eeeeee;
+  border-top: #4a6e91 1px solid; 
+  font-weight:light;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.note {
+  background-color: #ffffff;
+  font-weight:light;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.note_vide {
+  background-color: #eeeeee;
+  border-top: #4a6e91 1px solid; 
+  font-weight:light;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.coef {
+  background-color: #eeeeee;
+  border-top: #4a6e91 1px solid; 
+  font-weight:bold;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.coef_ue {
+  background-color: #dddddd;
+  border-top: #4a6e91 1px solid; 
+  border-bottom: #4a6e91 1px solid; 
+  font-weight:bold;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.coef_vide {
+  background-color: #ffffff;
+  font-weight:light;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.moyennes {
+  background-color: #eeeeee;
+  border-top: #4a6e91 1px solid;
+  border-right: #4a6e91 1px solid;   
+  font-weight:light;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+td.moyennes_bold {
+  background-color: #dddddd;
+  border-top: #4a6e91 1px solid; 
+  font-weight:bold;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color:#4a6e91;
+  text-align: left;
+  text-decoration:none;
+  padding:2px;
+}
+
+img { 
+  vertical-align : middle;
+}
+
+form { 
+  display: table;
+  width: auto; /*matching the parents div width*/
+  color: #333333; 
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+}
+
+fieldset { 
+  width: 100%; 
+  background: #F6F6F6; 
+  -webkit-border-radius: 8px; 
+  -moz-border-radius: 8px; 
+  border-radius: 8px; 
+  border: 0; 
+  background: -webkit-gradient(linear, left top, left bottom, from(#EFEFEF), to(#FFFFFF)); 
+  background: -moz-linear-gradient(center top, #EFEFEF, #FFFFFF 100%); 
+  box-shadow: 3px 3px 10px #ccc; 
+  -moz-box-shadow: 3px 3px 10px #ccc; 
+  -webkit-box-shadow: 3px 3px 10px #ccc;
+}
+
+legend { 
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 14px; 
+  font-weight:bold;
+  background-color: #4F709F; 
+  color: white; 
+  -webkit-border-radius: 4px; 
+  -moz-border-radius: 4px; 
+  border-radius: 4px; 
+  box-shadow: 2px 2px 4px #888; 
+  -moz-box-shadow: 2px 2px 4px #888; 
+  -webkit-box-shadow: 2px 2px 4px #888; 
+  text-shadow: 1px 1px 1px #333;
+}
+
+label { 
+  color: #4a6e91; 
+  font-family: Verdana, Geneva, sans-serif; 
+  font-weight:bold;
+  font-size: 12px; 
+  text-align: right; 
+  height: 20px; 
+  line-height: 20px;
+}
+
+input, textarea, select { 
+  border: 1px solid #d9d9d9;
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 12px; 
+  color: #4a6e91;
+}
+
+.submit { 
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 14px; 
+  font-weight:bold;
+  width: 100px; 
+  border: 0; 
+  background: #009900; 
+  color: white; 
+  -webkit-border-radius: 4px; 
+  -moz-border-radius: 4px; 
+  border-radius: 4px; 
+  box-shadow: 2px 2px 4px #888; 
+  -moz-box-shadow: 2px 2px 4px #888; 
+  -webkit-box-shadow: 2px 2px 4px #888; 
+  margin-bottom: 4px; 
+  text-shadow: 1px 1px 1px #333;
+}
+
+.bulletin { 
+  width: 85%;
+  padding: 20px; 
+  background: #F6F6F6; 
+  -webkit-border-radius: 8px; 
+  -moz-border-radius: 8px; 
+  border-radius: 8px; 
+  border: 0; 
+  background: -webkit-gradient(linear, left top, left bottom, from(#EFEFEF), to(#FFFFFF)); 
+  background: -moz-linear-gradient(center top, #EFEFEF, #FFFFFF 100%); 
+  box-shadow: 3px 3px 10px #ccc; 
+  -moz-box-shadow: 3px 3px 10px #ccc; 
+  -webkit-box-shadow: 3px 3px 10px #ccc;
+}
+
+.absences { 
+  width: 75%;
+  padding: 20px; 
+  background: #F6F6F6; 
+  -webkit-border-radius: 8px; 
+  -moz-border-radius: 8px; 
+  border-radius: 8px; 
+  border: 0; 
+  background: -webkit-gradient(linear, left top, left bottom, from(#EFEFEF), to(#FFFFFF)); 
+  background: -moz-linear-gradient(center top, #EFEFEF, #FFFFFF 100%); 
+  box-shadow: 3px 3px 10px #ccc; 
+  -moz-box-shadow: 3px 3px 10px #ccc; 
+  -webkit-box-shadow: 3px 3px 10px #ccc;
+}
+
+.info { 
+  display: table;
+  width: auto; /*matching the parents div width*/
+  padding: 10px; 
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 14px; 
+  font-weight:bold;
+  border: 0; 
+  background: #4F709F; 
+  color: white; 
+  -webkit-border-radius: 4px; 
+  -moz-border-radius: 4px; 
+  border-radius: 4px; 
+  box-shadow: 2px 2px 4px #888; 
+  -moz-box-shadow: 2px 2px 4px #888; 
+  -webkit-box-shadow: 2px 2px 4px #888; 
+  margin-bottom: 4px; 
+  text-shadow: 1px 1px 1px #333;
+}
+
+.alert { 
+  display: table;
+  width: auto; /*matching the parents div width*/
+  padding: 10px; 
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 14px; 
+  font-weight:bold;
+  border: 0; 
+  background: #4F709F; 
+  color: white; 
+  -webkit-border-radius: 4px; 
+  -moz-border-radius: 4px; 
+  border-radius: 4px; 
+  box-shadow: 2px 2px 4px #888; 
+  -moz-box-shadow: 2px 2px 4px #888; 
+  -webkit-box-shadow: 2px 2px 4px #888; 
+  margin-bottom: 4px; 
+  text-shadow: 1px 1px 1px #333;
+}
+
+.code { 
+  border: 1px dotted black;
+  display: table;
+  width: auto; /*matching the parents div width*/
+  padding: 10px; 
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 14px; 
+  font-weight:light;
+  font-style: italic;
+  -webkit-border-radius: 4px; 
+  -moz-border-radius: 4px; 
+  border-radius: 4px; 
+  margin-bottom: 4px; 
+}
+
+.nbabs {
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 14px; 
+  font-weight:bold;
+  color: #FF4500; 
+}
+
+.nbabsjust {
+  font-family: Verdana, Geneva, sans-serif; 
+  font-size: 14px; 
+  font-weight:bold;
+  color: #32CD32; 
+}
+
+
diff --git a/misc/PublicationBulletins/ExemplePHP/index-abs.php b/misc/PublicationBulletins/ExemplePHP/index-abs.php
new file mode 100644
index 0000000000000000000000000000000000000000..06a2850341a8e101865bc5cc0df0105743a35ccc
--- /dev/null
+++ b/misc/PublicationBulletins/ExemplePHP/index-abs.php
@@ -0,0 +1,761 @@
+<?php
+
+// Code contribu� par Yann Leboulanger (Universit� Paris 10), Juin 2013
+// Modifi� par D.SOUDIERE avec le concours de Catherine Hatinguais
+
+// Publication des notes vers les �tudiants
+// Gestion des absences: affichage et gestion des billets d'absences.
+// Les �tudiants signales les absences � venir ou pass�es et justifient en ligne puis physiquement.
+
+//  L'�tudiant est authenfi� via le CAS 
+// Le bulletin, les absences est r�cup�r� en format XML en interrogeant ScoDoc
+// Les billets sont envoy�s � Scodoc et sont g�r�s par le secr�tariat ou autre et valid�.
+// Il faut cocher la case "publier le bulletin sur le portail �tudiants" dans le semestre 
+//  ainsi que Gestion de "billets" d'absence dans les param�tres
+// Pour qu'une �valuation soit visible il faut r�gler celle ci avec la case "Visible sur bulletins" 
+//  et "Prise en compte imm�diate" ou bien que toutes cases soient remplies.
+// Il faut cr�er un utilisateur ScoDoc n'ayant que des droits de lecture.
+//
+// A adapter � vos besoins locaux.
+// penser � mettre les fichiers css et js et les icons utilis�s
+
+// L authentification CAS et donc LDAP est fait par apache 
+// cf /etc/apache2/site-enable/newdi
+
+// il faut le paquet : php5-ldap
+
+
+function convertir_utf8($texte){
+$retour=htmlentities($texte,ENT_NOQUOTES,'UTF-8');
+return ($retour);
+}
+
+
+// D�finition de la fonction d'encodage des headers
+function http_build_headers( $headers ) {
+
+       $headers_brut = '';
+
+       foreach( $headers as $nom => $valeur ) {
+               $headers_brut .= $nom . ': ' . $valeur . "\r\n";
+       }
+
+       return $headers_brut;
+}
+
+
+function get_EtudAbs_page($nip, $dept,$beg_date)
+{
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+   $end_date=date("Y-m-d");  
+    $donnees = array(
+        'format' => 'xml',
+        'code_nip' => $nip,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw,
+        'beg_date' => $beg_date,
+        'end_date' => $end_date);
+
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Absences/XMLgetAbsEtud', false, $contexte );
+
+    return ($retour);
+}
+
+
+function get_BilletAbs_list($nip, $dept)
+{
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+    $donnees = array(
+        'format' => 'xml',
+        'code_nip' => $nip,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw,
+);
+
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Absences/XMLgetBilletsEtud', false, $contexte );
+
+    return ($retour);
+}
+
+
+function Get_EtudAbs_billet($nip, $dept,$begin,$end,$description)
+{
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+   $end_date=date("Y-m-d"); 
+$justified="0";
+    $donnees = array(
+        'format' => 'xml',
+        'code_nip' => $nip,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw,
+        'description' =>$description,
+        'justified' =>$justified,
+        'begin' => $begin,
+        'end' => $end);
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Absences/AddBilletAbsence', false, $contexte );
+
+    return ($retour);
+}
+
+
+function get_EtudInfos_page($nip, $dept)
+{
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+
+    $donnees = array(
+        'code_nip' => $nip,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw );
+
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/XMLgetEtudInfos', false, $contexte );
+
+    return ($retour);
+}
+
+function get_bulletinetud_page($nip, $sem, $dept) {
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+    $donnees = array(
+        'format' => 'xml',
+        'code_nip' => $nip,
+        'formsemestre_id' => $sem,
+        'version' => 'selectedevals',
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw );
+
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/formsemestre_bulletinetud', false, $contexte );
+
+    return ($retour);
+}
+
+function get_semestre_info($sem, $dept)
+{
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+    $donnees = array(
+        'formsemestre_id' => $sem,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw );
+
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/XMLgetFormsemestres', false, $contexte );
+
+    return ($retour);
+}
+
+function get_all_semestres($xml_data)
+{
+    $data = array();
+    $xml = simplexml_load_string($xml_data);
+    foreach ($xml->insemestre as $s) {
+        $sem = (array) $s['formsemestre_id'];
+        $data[] = $sem[0];
+    }
+    return $data;
+}
+
+function get_current_semestre($xml_data)
+{
+    $xml = simplexml_load_string($xml_data);
+    foreach ($xml->insemestre as $s) {
+        if ($s['current'] == 1)
+            $sem = (array) $s['formsemestre_id'];
+            return ($sem[0]);
+    }
+}
+
+function print_semestres_list($sems, $dept, $sem)
+{
+    echo 'Semestre : <select name="sem">';
+    for ($i=0; $i < count($sems); $i++) {
+        $s = $sems[$i];
+        $retour = get_semestre_info($s, $dept);
+    	$xml = simplexml_load_string($retour);
+        echo '<option value="' . $s . '"';
+        if ($s == $sem) {
+            echo ' selected';
+        }
+        echo '>' . convertir_utf8($xml->formsemestre['titre_num']) . '</option>
+';
+    }
+    echo '</select>
+<input type="submit" value="Valider">
+</form>';
+}
+
+function print_semestre($xml_data, $sem, $dept, $show_moy=False)
+{
+    global $etudid;
+    global $nip;
+        global $sco_user;
+	global $sco_pw;
+    	global $sco_url;
+    $xml = simplexml_load_string($xml_data);
+    $etudid= $xml->etudiant['etudid'];
+    
+        if (!$show_moy) {
+    echo '<p><span style="color: red;">Les informations contenues dans ce tableau sont
+        provisoires. L&apos;&eacute;tat n&apos;a pas valeur de bulletin de notes.</span>';}
+
+    echo '<span style="color: red;"><br>Il vous appartient de contacter vos enseignants
+        ou votre d�partement en cas de d�saccord.</span></p>';
+        
+    echo '<h3>' . $xml->etudiant['sexe'] . ' ' . $xml->etudiant['prenom'] . ' ' . $xml->etudiant['nom'] . '</h3>';
+    //echo '<br/>';
+    $retour = get_semestre_info($sem, $dept);
+    $xml2 = simplexml_load_string($retour);
+    $debut=date("Y-m-d",strtotime($xml2->formsemestre['dateord']));
+    
+    echo '<b>'.convertir_utf8($xml2->formsemestre['titre_num']).'</b><br>';
+    if (!$show_moy) {        echo "vous avez � ce jour ".convertir_utf8($xml->absences['nbabs'])." demi-journ�es d'absences dont ".convertir_utf8($xml->absences['nbabsjust']).' justifi�es';}
+       echo '
+<br/>
+<br/>
+';
+    echo '<table class="notes_bulletin" style="background-color: background-color: rgb(255,255,240);">
+<tr>
+  <td class="note_bold">UE</td>
+  <td class="note_bold">Code Module</td>
+    <td class="note_bold">Module</td>
+  <td class="note_bold">Evaluation</td>
+  <td class="note_bold">Note/20</td>
+    <td class="note_bold">(Min/Max)</td>
+  <td class="note_bold">Coef</td>
+</tr>
+';
+    if ($show_moy) {
+        echo '<tr class="gt_hl notes_bulletin_row_gen" ><td  class="titre" colspan="4" >Moyenne g�n�rale:</td><td  class="note">' . $xml->note['value'] . '</td><td class="max">('.$xml->note['min'].'/'.$xml->note['max'].')</td><td  class="coef"></td></tr>';
+    }
+    foreach ($xml->ue as $ue) {
+        $coef = 0;
+        foreach ($ue->module as $mod) {
+            $coef += $mod['coefficient'];
+        }
+        echo '<tr class="notes_bulletin_row_ue">
+  <td class="note_bold"><span onclick="toggle_vis_ue(this);" class="toggle_ue"><img src="imgs/minus_img.png" alt="-" title="" height="13" width="13" border="0" /></span>' . $ue['acronyme'] . '</td>
+  <td></td>
+  <td></td>
+  <td></td>
+';
+
+        if ($show_moy) {
+            echo '  <td>' . $ue->note['value'] . '</td><td class="max">('.$ue->note['min'].'/'.$ue->note['max'].')</td>
+';
+        }
+        else {
+            echo '  <td></td>
+                    <td></td>
+';
+        }
+
+echo '  <td>' . $coef . '</td>
+</tr>';
+        foreach ($ue->module as $mod) {
+            echo '<tr class="notes_bulletin_row_mod">
+  <td></td>
+  <td>' . $mod['code'] . '</td>
+   <td>' . convertir_utf8($mod['titre']) . '</td>
+  <td></td>
+';
+
+            if ($show_moy) {
+            echo '  <td>' . $mod->note['value'] . '</td><td class="max">('.$mod->note['min'].'/'.$mod->note['max'].')</td>
+';
+            }
+            else {
+                echo '  <td></td><td></td>
+';
+            }
+
+            echo '  <td>' . $mod['coefficient'] . '</td>
+</tr>';
+       
+            if (!$show_moy) {
+                foreach ($mod->evaluation as $eval) {
+                    echo '<tr class="notes_bulletin_row_eval">
+  <td></td>
+  <td></td>
+    <td></td>
+  <td class="bull_nom_eval">' . convertir_utf8($eval['description']) . '</td>
+  <td class="note">' . $eval->note['value'] . '</td><td class="max">('.$eval->note['min'].'/'.$eval->note['max'].')</td>
+  <td class="max">(' . $eval['coefficient'] . ')</td>
+</tr>';
+                } 
+            }
+        }
+    }
+    echo '</table>
+<br/>
+';
+$code=$xml->decision['code'];
+
+$date_fin=$xml->decision['date_fin'];
+echo $date_fin;
+
+    if ($show_moy) {
+        echo "Situation sous r�serve de validation par le jury : <br>".convertir_utf8($xml->situation);
+    }
+    else{if($code!=""){echo "Situation sous r�serve de validation par le jury : <br>". convertir_utf8($xml->situation);}}
+  
+    
+    if (!$show_moy) {    
+echo ' 
+<a href="#" id="toggler">
+<h3>Cliquez ici pour afficher/masquer la liste des absences du semestre: </h3></a>';
+
+   $retourabs = get_EtudAbs_page($nip, $dept,$debut);
+   $xmlabs = simplexml_load_string($retourabs);
+   
+
+   
+    echo '   
+    <div id="toggle" style="display:none;">
+    <table class="notes_bulletin" style="background-color: background-color: rgb(255,255,240);">
+
+<tr> 
+  <td class="note_bold">Du </td>
+  <td class="note_bold">Au </td>
+    <td class="note_bold">Justifi�e</td>
+  <td class="note_bold">Motif</td>
+</tr>';   
+
+foreach ($xmlabs->abs as $abs) {
+   if($abs['justified']=="True"){$just="Oui";}else{$just="Non";}
+   if(intval(date("H", strtotime($abs['begin'])))<12){$debmatin="matin";}else{$debmatin="apr&eacute;s midi";}
+    if(intval(date("H", strtotime($abs['end'])))<12){$endmatin="matin";}else{$endmatin="apr&eacute;s midi";}
+  echo "<tr><td>". date("d-m-Y H:i:s", strtotime($abs['begin'])) . ' '.$debmatin.'</td><td> ' .  date("d-m-Y H:i:s", strtotime($abs['end'])) .' '.$endmatin. '</td><td> ' . $just. '</td><td> ' . convertir_utf8($abs['description']) ."</td></tr>";
+}
+    echo '</table>
+</div>';
+
+echo '
+<FORM method=post action=index.php>';
+
+echo ' 
+<h3> D�claration des motifs d&apos;absences:</h3>';
+
+    echo '
+<TABLE BORDER=0>
+
+<TR>
+	<TD>Date et heure de d�but:</TD><TD> 
+	<INPUT type="text" name="begin" size="10" value="" class="datepicker"/>
+	</TD><TD>     
+    <SELECT name="begtime" size="1" value="08:00">
+<OPTION>08:00
+<OPTION>08:30
+<OPTION selected>08:00
+<OPTION>09:00
+<OPTION>09:30
+<OPTION>10:00
+<OPTION>10:30
+<OPTION>11:00
+<OPTION>11:30
+<OPTION>12:00
+<OPTION>12:30
+<OPTION>13:00
+<OPTION>13:30
+<OPTION>14:00
+<OPTION>14:30
+<OPTION>15:00
+<OPTION>15:30
+<OPTION>16:00
+<OPTION>16:30
+<OPTION>17:00
+<OPTION>17:30
+<OPTION>18:00
+<OPTION>18:30
+<OPTION>19:00
+<OPTION>19:30
+</SELECT>
+</TD></TR>
+<TR> 
+    <TD>Date et heure de fin:</TD><TD> 
+	<INPUT type="text" name="end" size="10" value="" class="datepicker"/>
+	</TD>
+    <TD>     
+    <SELECT name="endtime" size="1" value="18:00">
+<OPTION>08:00
+<OPTION>08:30
+<OPTION selected>18:00
+<OPTION>09:00
+<OPTION>09:30
+<OPTION>10:00
+<OPTION>10:30
+<OPTION>11:00
+<OPTION>11:30
+<OPTION>12:00
+<OPTION>12:30
+<OPTION>13:00
+<OPTION>13:30
+<OPTION>14:00
+<OPTION>14:30
+<OPTION>15:00
+<OPTION>15:30
+<OPTION>16:00
+<OPTION>16:30
+<OPTION>17:00
+<OPTION>17:30
+<OPTION>18:00
+<OPTION>18:30
+<OPTION>19:00
+<OPTION>19:30
+</SELECT>
+</TD>
+</TR>
+
+</TABLE>
+
+	Motif:
+    
+    <TABLE><br><TR>
+	<TEXTAREA rows="3"  type="text" name="description"  cols="60"/></TEXTAREA>
+	
+</TR><br>
+<span style="color: red;">Veuillez indiquer les mati�res et enseignants concern�s (pour les absences de courte dur�e).<br> Apportez par ailleurs le num&eacute;ro du billet affich� dans le tableau ci apr�s ainsi que vos justificatifs �ventuels au secr�tariat du d�partement.</span>
+<TR>
+	<TD COLSPAN=1>
+	<INPUT type="submit" value="Envoyer">
+	</TD>
+</TR>
+</TABLE>';
+
+
+
+if (isset($_POST["begin"]) and isset($_POST["end"])  and isset($_POST["begtime"]) and isset($_POST["endtime"]) and isset($_POST["description"]) and $_POST["end"]>=$_POST["begin"]){
+$date1 = new DateTime($_POST["begin"]);
+$date1->setTime(intval(substr($_POST["begtime"],0,2)), intval(substr($_POST["begtime"],-2)));
+
+$date2 = new DateTime($_POST["end"]);
+$date2->setTime(intval(substr($_POST["endtime"],0,2)), intval(substr($_POST["endtime"],-2)));
+Get_EtudAbs_billet($nip, $dept,$date1->format('Y-m-d H:i:s') , $date2->format('Y-m-d H:i:s')  , $_POST["description"]);}
+
+echo ' 
+<h3> Billets d&apos;absences d&eacute;pos&eacute;s: </h3>';
+   $retourbillets = get_BilletAbs_list($nip, $dept);
+   $xmlbillets = simplexml_load_string($retourbillets);
+   
+    echo '<table class="notes_bulletin" style="background-color: background-color: rgb(255,255,240);">
+<tr>
+<td class="note_bold">Billet </td>
+  <td class="note_bold">Du </td>
+  <td class="note_bold">Au </td>
+  <td class="note_bold">Motif</td>
+    <td class="note_bold">Situation</td>
+</tr>';   
+
+foreach ($xmlbillets->row as $billet) {
+  echo "<tr><td>". $billet->billet_id['value'] . '</td><td>'. convertir_utf8($billet->abs_begin_str['value']). '</td><td> ' .  convertir_utf8($billet->abs_end_str['value']) . '</td><td> ' .  convertir_utf8($billet->description['value']) .'</td><td> ' .  convertir_utf8($billet->etat_str['value']) ."</td></tr>
+";
+}
+
+echo '  </table>
+</FORM>'; 
+
+
+}}
+
+
+
+
+function get_dept($nip)
+{
+	global $sco_url;
+    $dept = file_get_contents( $sco_url . 'get_etud_dept?code_nip=' . $nip);
+    return ($dept);
+}
+
+
+// function pour la recuperation des infos ldap
+function search_results($info) {
+  foreach ($info as $inf) {
+    if (is_array($inf)) {
+      foreach ($inf as $key => $in) {
+        if ((count($inf[$key]) - 1) > 0) {
+          if (is_array($in)) {
+            unset($inf[$key]["count"]);
+          }
+          $results[$key] = $inf[$key];
+        }
+      }
+    }
+  }
+  $results["dn"] = explode(',', $info[0]["dn"]);
+  return $results;
+}
+
+
+// Programme principal
+
+
+echo '<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Bulletin de notes</title>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
+<meta http-equiv="Content-Style-Type" content="text/css" />
+<link href="css/scodoc.css" rel="stylesheet" type="text/css" />
+<link type="text/css" rel="stylesheet" href="libjs/jquery-ui/css/custom-theme/jquery-ui-1.7.2.custom.css" />
+<script language="javascript" type="text/javascript" src="js/bulletin.js"></script>
+<script language="javascript" type="text/javascript" src="jQuery/jquery.js"></script>
+<script language="javascript" type="text/javascript" src="jQuery/jquery-migrate-1.2.0.min.js"></script>
+<script language="javascript" type="text/javascript" src="libjs/jquery-ui/js/jquery-ui-1.7.2.custom.min.js"></script>
+<script language="javascript" type="text/javascript" src="libjs/jquery-ui/js/jquery-ui-i18n.js"></script>
+ <script language="javascript" type="text/javascript">
+           $(function() {
+		$(".datepicker").datepicker({
+                      showOn: "button", 
+                      buttonImage: "icons/calendar_img.png", 
+                      buttonImageOnly: true,
+                      dateFormat: "yy-mm-dd",   
+                      duration : "fast",                   
+                  });
+                $(".datepicker").datepicker("option", $.extend({showMonthAfterYear: false},
+				$.datepicker.regional["fr"]));
+    });
+        </script>';
+
+echo "<script type='text/javascript'>
+/* <![CDATA[ */ 
+/*
+|-----------------------------------------------------------------------
+|  jQuery Toggle Script by Matt - skyminds.net
+|-----------------------------------------------------------------------
+|
+| Affiche/cache le contenu d'un bloc une fois qu'un lien est cliqu�.
+|
+*/
+ 
+// On attend que la page soit charg�e 
+jQuery(document).ready(function()
+{
+   // On cache la zone de texte
+   jQuery('#toggle').hide();
+   // toggle() lorsque le lien avec l'ID #toggler est cliqu�
+   jQuery('a#toggler').click(function()
+  {
+      jQuery('#toggle').toggle(400);
+      return false;
+   });
+});
+/* ]]> */ 
+</script>
+<style>
+#toggle{height:auto; background:#eee; border:1px solid #900; margin:1em;text-align:center}
+#toggle p{text-align:center;padding:0}
+</style>
+        
+</head>
+<body>
+";
+
+
+$user = $_SERVER['PHP_AUTH_USER'];
+//echo 'USER: '.$user."\n"."<br>";
+
+//$user = "ei121713";
+//echo "On triche USER = ".$user."\n"."<br>";
+
+$ds = ldap_connect("ldap://ldap");
+if ($ds) {
+	$r = ldap_bind($ds);
+	$sr = ldap_search($ds, "ou=people,dc=univ-lehavre,dc=fr", "(&(objectClass=ulhEtudiant)(uid=$user))");
+	$info = ldap_get_entries($ds, $sr);
+ 
+	//echo $info["count"]." IS Search Result(s) for \"".$user."\"\n";
+	$results = search_results($info);
+	// si pas de reponse de l a nnuaire, ce n est pas un etudiant
+	if ($info["count"] == 0 ) {
+		echo '<html>
+		<head>
+			<title>getEtud</title>
+		</head>
+		<body>
+			<h1>Service de consultation des notes</h1>
+			<div>
+			Il faut &ecirc;tre etudiant de l&apos;IUT pour acc&eacute;der &agrave; ses notes.
+			</div>
+		</body>
+		</html>';
+	} else {
+		foreach ($results as $key => $result) {
+			if ($key == 'supannetuid' ) {
+				//echo " *  ".$key." : \n";
+    				if (is_array($result)){
+					foreach($result as $res){
+						//echo "    ".$res."\n";
+					}
+				}
+				//echo "<br>";
+				$nip=$res;
+			}
+		}
+	}
+	ldap_close($ds);
+}
+// Login information of a scodoc user that can access notes
+$sco_user = 'lecturenotes';
+$sco_pw = 'XXXXXXX';
+$sco_url = 'https://scodoc.XXXXX.fr/ScoDoc/';
+
+$user_agent = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.1) Gecko/20061010 Firefox/2.0';
+
+echo '<form action="index.php" method="post">';
+if ($nip) {
+$dept = get_dept($nip);
+if ($dept) {
+    $retour = get_EtudInfos_page($nip, $dept);
+    $sems = get_all_semestres($retour);
+    $sem_current = get_current_semestre($retour);
+    if (isset($_POST["sem"])) {
+        $sem = $_POST["sem"];
+    }
+    else {
+        $sem = $sem_current;
+    }
+    print_semestres_list($sems, $dept, $sem);
+    $retour = get_bulletinetud_page($nip, $sem, $dept);
+    if ($sem == $sem_current) {
+        print_semestre($retour, $sem, $dept, False);
+    }
+    else {
+        print_semestre($retour, $sem, $dept, True);
+    }
+    $erreur=0;    // Tout est OK
+}
+else {
+    echo "Num�ro �tudiant inconnu : " . $nip . ". Contactez votre Chef de d�partement.";
+}
+}
+
+echo '</form>';
+
+
+echo  '
+          </body>
+</html>';
+
+
+?>
diff --git a/misc/PublicationBulletins/ExemplePHP/index.php b/misc/PublicationBulletins/ExemplePHP/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ecefa6c42729457bdb7c2ba0197b74db3507cc8
--- /dev/null
+++ b/misc/PublicationBulletins/ExemplePHP/index.php
@@ -0,0 +1,320 @@
+<?php
+
+// Code contribué par Yann Leboulanger (Université Paris 10), Juin 2013
+// 
+// Exemple publication des bulletins de notes vers les étudiants
+//  L'étudiant est authenfié via le CAS 
+// Le bulletin est récupéré en format XML en interrogeant ScoDoc
+// 
+// Il faut créer un utilisateur ScoDoc n'ayant que des droits de lecture.
+//
+// A adapter à vos besoins locaux.
+
+include_once 'CAS.php';
+
+phpCAS::setDebug();
+phpCAS::client(CAS_VERSION_2_0,'URL_CAS',443,'');
+phpCAS::setNoCasServerValidation();
+phpCAS::forceAuthentication();
+
+$nip = phpCAS::getUser();
+
+// Login information of a scodoc user that can access notes
+$sco_user = 'USER';
+$sco_pw = 'PASS';
+$sco_url = 'https://SERVEUR/ScoDoc/';
+
+$user_agent = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.1) Gecko/20061010 Firefox/2.0';
+
+// Définition de la fonction d'encodage des headers
+function http_build_headers( $headers ) {
+
+       $headers_brut = '';
+
+       foreach( $headers as $nom => $valeur ) {
+               $headers_brut .= $nom . ': ' . $valeur . "\r\n";
+       }
+
+       return $headers_brut;
+}
+
+function get_EtudInfos_page($nip, $dept)
+{
+	global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+    $donnees = array(
+        'code_nip' => $nip,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw );
+
+    // Création du contenu brut de la requête
+    $contenu = http_build_query( $donnees );
+
+    // Définition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // Définition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Création du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/XMLgetEtudInfos', false, $contexte );
+
+    return ($retour);
+}
+
+function get_bulletinetud_page($nip, $sem, $dept) {
+	global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+    $donnees = array(
+        'format' => 'xml',
+        'code_nip' => $nip,
+        'formsemestre_id' => $sem,
+        'version' => 'selectedevals',
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw );
+
+    // Création du contenu brut de la requête
+    $contenu = http_build_query( $donnees );
+
+    // Définition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // Définition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Création du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/formsemestre_bulletinetud', false, $contexte );
+
+    return ($retour);
+}
+
+function get_semestre_info($sem, $dept)
+{
+	global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+    $donnees = array(
+        'formsemestre_id' => $sem,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw );
+
+    // Création du contenu brut de la requête
+    $contenu = http_build_query( $donnees );
+
+    // Définition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // Définition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Création du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/XMLgetFormsemestres', false, $contexte );
+
+    return ($retour);
+}
+
+function get_all_semestres($xml_data)
+{
+    $data = array();
+    $xml = simplexml_load_string($xml_data);
+    foreach ($xml->insemestre as $s) {
+        $sem = (array) $s['formsemestre_id'];
+        $data[] = $sem[0];
+    }
+    return $data;
+}
+
+function get_current_semestre($xml_data)
+{
+    $xml = simplexml_load_string($xml_data);
+    foreach ($xml->insemestre as $s) {
+        if ($s['current'] == 1)
+            $sem = (array) $s['formsemestre_id'];
+            return ($sem[0]);
+    }
+}
+
+function print_semestres_list($sems, $dept, $sem)
+{
+    echo 'Semestre : <select name="sem">';
+    for ($i=0; $i < count($sems); $i++) {
+        $s = $sems[$i];
+        $retour = get_semestre_info($s, $dept);
+    	$xml = simplexml_load_string($retour);
+        echo '<option value="' . $s . '"';
+        if ($s == $sem) {
+            echo ' selected';
+        }
+        echo '>' . $xml->formsemestre['titre_num'] . '</option>
+';
+    }
+    echo '</select>
+<input type="submit" value="Valider">
+</form>';
+}
+
+function print_semestre($xml_data, $sem, $dept, $show_moy=False)
+{
+    $xml = simplexml_load_string($xml_data);
+    echo '<h2>' . $xml->etudiant['sexe'] . ' ' . $xml->etudiant['prenom'] . ' ' . $xml->etudiant['nom'] . '</h2>';
+    echo '<br/>
+';
+    $retour = get_semestre_info($sem, $dept);
+    $xml2 = simplexml_load_string($retour);
+    echo $xml2->formsemestre['titre_num'];
+    echo '
+<br/>
+<br/>
+';
+    echo '<table class="notes_bulletin" style="background-color: background-color: rgb(255,255,240);">
+<tr>
+  <td class="note_bold">UE</td>
+  <td class="note_bold">Module</td>
+  <td class="note_bold">Evaluation</td>
+  <td class="note_bold">Note/20</td>
+  <td class="note_bold">Coef</td>
+</tr>
+';
+    if ($show_moy) {
+        echo '<tr class="gt_hl notes_bulletin_row_gen" ><td  class="titre" colspan="3" >Moyenne générale:</td><td  class="note">' . $xml->note['value'] . '</td><td  class="coef"></td></tr>';
+    }
+    foreach ($xml->ue as $ue) {
+        $coef = 0;
+        foreach ($ue->module as $mod) {
+            $coef += $mod['coefficient'];
+        }
+        echo '<tr class="notes_bulletin_row_ue">
+  <td class="note_bold"><span onclick="toggle_vis_ue(this);" class="toggle_ue"><img src="imgs/minus_img.png" alt="-" title="" height="13" width="13" border="0" /></span>' . $ue['acronyme'] . '</td>
+  <td></td>
+  <td></td>
+';
+
+        if ($show_moy) {
+            echo '  <td>' . $ue->note['value'] . '</td>
+';
+        }
+        else {
+            echo '  <td></td>
+';
+        }
+
+echo '  <td>' . $coef . '</td>
+</tr>';
+        foreach ($ue->module as $mod) {
+            echo '<tr class="notes_bulletin_row_mod">
+  <td></td>
+  <td>' . $mod['code'] . '</td>
+  <td></td>
+';
+
+            if ($show_moy) {
+                echo '  <td>' . $mod->note['value'] . '</td>
+';
+            }
+            else {
+                echo '  <td></td>
+';
+            }
+
+            echo '  <td>' . $mod['coefficient'] . '</td>
+</tr>';
+       
+            if (!$show_moy) {
+                foreach ($mod->evaluation as $eval) {
+                    echo '<tr class="notes_bulletin_row_eval">
+  <td></td>
+  <td></td>
+  <td class="bull_nom_eval">' . $eval['description'] . '</td>
+  <td class="note">' . $eval->note['value'] . '</td>
+  <td></td>
+</tr>';
+                } 
+            }
+        }
+    }
+    echo '</table>
+<br/>
+';
+    if ($show_moy) {
+        echo $xml->situation;
+    }
+}
+
+function get_dept($nip)
+{
+	global $sco_url;
+    $dept = file_get_contents( $sco_url . 'get_etud_dept?code_nip=' . $nip);
+    return ($dept);
+}
+
+
+
+echo '<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Bulletin de notes</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<meta http-equiv="Content-Style-Type" content="text/css" />
+<link href="css/scodoc.css" rel="stylesheet" type="text/css" />
+<script language="javascript" type="text/javascript" src="js/bulletin.js"></script>
+</head>
+<body>
+';
+
+echo '<form action="index.php" method="post">';
+
+$dept = get_dept($nip);
+if ($dept) {
+    $retour = get_EtudInfos_page($nip, $dept);
+    $sems = get_all_semestres($retour);
+    $sem_current = get_current_semestre($retour);
+    if (isset($_POST["sem"])) {
+        $sem = $_POST["sem"];
+    }
+    else {
+        $sem = $sem_current;
+    }
+    print_semestres_list($sems, $dept, $sem);
+    $retour = get_bulletinetud_page($nip, $sem, $dept);
+    if ($sem == $sem_current) {
+        print_semestre($retour, $sem, $dept, False);
+    }
+    else {
+        print_semestre($retour, $sem, $dept, True);
+    }
+    $erreur=0;    // Tout est OK
+}
+else {
+    echo "Numéro étudiant inconnu : " . $nip . ". Contactez votre Chef de département.";
+}
+
+echo '</form>';
+
+echo '</body>
+</html>';
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/Acces aux notes etudiants.pdf b/misc/PublicationBulletins/Portail-LeHavre/Acces aux notes etudiants.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..7503b0eef9c1a54a7f149efa210e5b26896a4298
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/Acces aux notes etudiants.pdf differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/Billets_etudiants.pdf b/misc/PublicationBulletins/Portail-LeHavre/Billets_etudiants.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..72d824e870138ce389a2165df541a675efa8a4a0
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/Billets_etudiants.pdf differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/PDF_Billet.php b/misc/PublicationBulletins/Portail-LeHavre/PDF_Billet.php
new file mode 100755
index 0000000000000000000000000000000000000000..8bf7a7bfe8247a3b4c6dbe98067d8145e97d0dd6
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/PDF_Billet.php
@@ -0,0 +1,151 @@
+<?php
+
+ 
+include("phpToPDF.php");
+$billet=utf8_decode($_GET['billet']);
+$nom=utf8_decode($_GET['nom']);
+$prenom=utf8_decode($_GET['prenom']);
+$semestre=utf8_decode($_GET['semestre']);
+$groupe=utf8_decode($_GET['groupe']);
+$sexe=utf8_decode($_GET['sexe']);
+$motif=utf8_decode($_GET['motif']);
+$debut=utf8_decode($_GET['debut']);
+$fin=utf8_decode($_GET['fin']);
+$finsemestre=date("d-m-Y",strtotime(($_GET['finsemestre'])));
+$debutsemestre=date("d-m-Y",strtotime(($_GET['debutsemestre'])));
+
+$trait="_____________________________________________________________________________";			
+$etudiant = "Etudiant : ".$sexe." ".$prenom." ".$nom;
+$numbillet="Billet d'absence: ".$billet;
+$dates="Absence du  : ".$debut." au ".$fin;
+$texte3="";
+
+$largeur=200;
+$esp=5;
+$deb= 25;
+$PDF = new phpToPDF();
+$PDF->AddPage();
+
+
+$PDF->SetFont('Arial','B',16);
+
+$fill = 1;
+$PDF->SetFillColor(224,228,200);
+$PDF->SetTextColor(0,0,0);
+
+$PDF->Text(20,15,"$semestre");
+$PDF->SetXY(20,20);
+$PDF->MultiCell(180,5,"(du $debutsemestre au $finsemestre)",0,'C',0);
+
+
+$PDF->SetXY(20,30);
+$PDF->SetFont('Arial','',10);
+$PDF->MultiCell(180,5,"Formulaire � compl�ter par l'�tudiant. \n A faire signer par les enseignants et � d�poser au secr�tariat sans attendre avec les justificatifs s'il y a lieu.",0,'C',0); 
+$PDF->SetFont('Arial','B',12);
+
+$PDF->Text(15,40,"$trait");
+$PDF->Text(20,50,"$etudiant");
+$PDF->Text(100,55,"Groupe (TD/TP): $groupe");
+$PDF->Text(20,55,"$numbillet",0,0,'L');
+$PDF->Text(15,60,"$trait");
+$PDF->SetFont('Arial','',11);
+$PDF->Text(20,70,"$dates");
+
+$PDF->Text(20,75,"Justificatif apport�: Oui   Non");
+$PDF->SetFont('Arial','B',11);
+$PDF->Text(20,80,"Motif: ");
+$PDF->SetFont('Arial','',10);
+$PDF->SetXY(20,82);
+$PDF->MultiCell(180,5,"$motif",1,'L',0); 
+$PDF->SetXY(20,122);
+
+// D�finition des propri�t�s du tableau.
+$larccel=38;
+$larccel2=55;
+//$R=151;$G=190;$B=13;
+$R=224;$G=228;$B=216;
+
+$proprietesTableau = array(
+	'TB_ALIGN' => 'L',
+	'L_MARGIN' => 1,
+	'BRD_COLOR' => array(0,0,0),
+	'BRD_SIZE' => '0.5',
+	);
+ 
+// Definition des proprietes du header du tableau.	
+$proprieteHeader = array(
+	'T_COLOR' => array(0,0,0),
+	'T_SIZE' => 10,
+	'T_FONT' => 'Arial',
+	'T_ALIGN' => 'C',
+	'V_ALIGN' => 'T',
+	'T_TYPE' => 'B',
+	'LN_SIZE' => 7,
+	'BG_COLOR_COL0' => array($R, $G, $B),
+	'BG_COLOR' => array($R, $G, $B),
+	'BRD_COLOR' => array(0,0,0),
+	'BRD_SIZE' => 0.2,
+	'BRD_TYPE' => '1',
+	'BRD_TYPE_NEW_PAGE' => '',
+	);
+
+// Contenu du header du tableau.	
+$contenuHeader = array(
+	$larccel, $larccel, $larccel2, $larccel2, 
+	"Matiere","Enseignant","Emargement Enseignant","Observations"
+	);
+
+    // Contenu du tableau.	
+$contenuTableau = array(
+	"", "", "","", 
+    "",	"", "", "",
+    "",	"", "", "",    
+    "",	"", "", "",
+    "", "",	"","", 
+    "", "", "",	"",
+    0, 0, 0, "",
+	); 
+// Definition des propri�t�s du reste du contenu du tableau.	
+$proprieteContenu = array(
+	'T_COLOR' => array(0,0,0),
+	'T_SIZE' => 10,
+	'T_FONT' => 'Arial',
+	'T_ALIGN_COL0' => 'L',
+	'T_ALIGN' => 'R',
+	'V_ALIGN' => 'M',
+	'T_TYPE' => '',
+	'LN_SIZE' => 6,
+	'BG_COLOR_COL0' => array($R, $G, $B),
+	'BG_COLOR' => array(255,255,255),
+	'BRD_COLOR' => array(0,0,0),
+	'BRD_SIZE' => 0.2,
+	'BRD_TYPE' => '1',
+	'BRD_TYPE_NEW_PAGE' => '',
+	);
+
+
+$PDF->drawTableau($PDF, $proprietesTableau, $proprieteHeader, $contenuHeader, $proprieteContenu, $contenuTableau);
+$PDF->Text(15,180,"Indiquez ci-dessous les Devoirs surveill�s, contr�les TP, interrogations �crites concern�s:");
+$PDF->SetXY(20,182);
+$PDF->drawTableau($PDF, $proprietesTableau, $proprieteHeader, $contenuHeader, $proprieteContenu, $contenuTableau);
+$PDF->SetXY(20,235);
+$PDF->SetFont('Arial','I',10);
+$PDF->MultiCell(180,3,'Je d�clare avoir fait, ou faire express�ment, le n�cessaire pour rattraper tous les cours cit�s ci-dessus, tant au niveau des documents distribu�s, du d�roulement des s�ances de travail et d��ventuelles �valuations. 
+La recevabilit� de l�absence sera appr�ci�e par l��quipe de direction.',0,'L',0);
+$PDF->SetFont('Arial','B',11);
+$PDF->SetFillColor(224,228,216);
+$PDF->SetXY(20,260);
+$PDF->MultiCell(180,5,"Partie r�serv�e � l'administration:
+Absence justifi�e : Oui   Non             Autoris� � rattraper les contr�les:  Oui   Non",1,'C',1); 
+$PDF->Text(15,250,"Signature du Directeur des �tudes:");
+$PDF->Text(125,250,"Signature de l'�tudiant:");
+$PDF->Text(80,290,"Imprim� le ".date("d/n/Y � H:i"));
+
+
+$PDF->Ln();
+$PDF->Output();
+$PDF->Output($nom.".pdf", "D");
+
+
+?>
+<p>&nbsp;</p>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/README.txt b/misc/PublicationBulletins/Portail-LeHavre/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7a757dcd36094848d7e2cfc50257243b6a53e928
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/README.txt
@@ -0,0 +1,6 @@
+Portail-LeHavre 
+
+Auteurs: Code contribué par Yann Leboulanger (Université Paris 10), Juin 2013
+Modifié par D. SOUDIERE avec le concours de Catherine Hatinguais
+
+
diff --git a/misc/PublicationBulletins/Portail-LeHavre/css/radar_bulletin.css b/misc/PublicationBulletins/Portail-LeHavre/css/radar_bulletin.css
new file mode 100644
index 0000000000000000000000000000000000000000..a70e25170a3cfdba7dad4d03677786d12e9bf7ba
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/css/radar_bulletin.css
@@ -0,0 +1,90 @@
+/* # -*- mode: css -*- 
+           ScoDoc, (c) Emmanuel Viennet 1998 - 2012
+ */
+
+div#radar_bulletin {
+    width: 500px;
+    margin-left: auto ;
+    margin-right: auto ;
+}
+
+.radar {
+    font-family: sans-serif;
+    font-size: 12pt;
+}
+
+.radar rect {
+    stroke: white;
+    fill: steelblue;
+}
+
+path {
+    fill:none;
+    stroke:gray;
+}
+
+path.radarnoteslines {
+    stroke: darkblue;
+    stroke-width: 1.5px;
+}
+
+path.radarmoylines {
+    stroke: rgb(20,90,50);;
+    stroke-width: 1.25px;
+    stroke-dasharray: 8, 4;
+}
+
+line.radarrad {
+    stroke: rgb(150, 150, 150);
+}
+
+circle.radar_disk_tick {
+    stroke-width: 1;
+    stroke : rgb(150,150,150);
+    fill : none;
+}
+circle.radar_disk_red {
+    stroke-width: 0px;
+    fill: #FFC0CB;
+    fill-opacity: 0.5;
+}
+
+path#radar_disk_tic_8 {
+    fill: #FF493C;
+    fill-opacity: 0.5;
+}
+
+path#radar_disk_tic_10 {
+    fill: #FFC0CB;
+    fill-opacity: 0.5;
+    stroke-width: 1.5px;
+    stroke: #F44646;
+}
+
+path#radar_disk_tic_20 {
+    fill: grey;
+    fill-opacity: 0.1;
+}
+
+.radar_center_mark {
+    stroke: red;
+    stroke-width : 2;
+    fill: red;
+}
+
+rect.radartip {
+    fill: #F5FB84;
+    font-size: 10pt;
+}
+
+text.textaxis {
+    fill: red;
+    font-size: 8pt;
+    text-anchor: end;
+}
+
+text.note_label {
+    fill: darkblue;
+    font-size: 10pt;
+    font-weight: bold;
+}
diff --git a/misc/PublicationBulletins/Portail-LeHavre/css/scodoc.css b/misc/PublicationBulletins/Portail-LeHavre/css/scodoc.css
new file mode 100644
index 0000000000000000000000000000000000000000..bc50a34362e308bad7ba144c7f7424bbc323cdba
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/css/scodoc.css
@@ -0,0 +1,1900 @@
+/* # -*- mode: css -*- 
+           ScoDoc, (c) Emmanuel Viennet 1998 - 2012
+ */
+
+html,body {
+  margin:0;
+  padding:0;
+  width: 100%;
+  background-color: rgb(238,238,255);
+}
+
+a:link { color:  #008080 }
+a:visited { color : #000080 }
+
+
+@media print {
+.noprint {
+  display:none;
+}
+}
+
+h1, h2, h3 {
+   font-family : arial, verdana, sans-serif ;
+}
+
+/* ----- bandeau haut ------ */
+span.bandeaugtr {
+  width: 100%;
+  margin: 0;
+  border-width: 0;
+  padding-left: 160px;
+/*  background-color: rgb(17,51,85); */
+}
+@media print {
+span.bandeaugtr {
+   display:none;
+}
+}
+tr.bandeaugtr {
+/*  background-color: rgb(17,51,85); */
+  color: rgb(255,215,0);
+ /* font-style: italic; */
+  font-weight: bold;
+  border-width: 0;
+  margin: 0;
+}
+
+#authuser {
+    margin-top: 16px;
+}
+
+#authuserlink {
+  color: rgb(255,0,0);
+  text-decoration: none;
+}
+#authuserlink:hover {
+  text-decoration: underline;
+}
+#deconnectlink {
+  font-size: 75%;
+  font-style: normal;
+  color: rgb(255,0,0);
+  text-decoration: underline;
+}
+
+/* ----- page content ------ */
+
+div.about-logo {
+  text-align: center;
+  padding-top: 10px;
+}
+
+
+div.head_message {
+   margin-top: 2px;
+   margin-bottom: 8px;
+   padding:  5px;
+   margin-left: auto;
+   margin-right: auto;
+   background-color: #ffff73;
+   -moz-border-radius: 8px;
+   -khtml-border-radius: 8px;
+   border-radius: 8px;
+   font-family : arial, verdana, sans-serif ;
+   font-weight: bold;
+   width: 40%;
+   text-align: center;
+}
+
+div.passwd_warn {
+    font-weight: bold;
+    font-size: 200%;
+    background-color: #feb199;
+    width: 80%;
+    height: 200px;
+    text-align: center;
+    padding:  20px;
+    margin-left: auto;
+    margin-right: auto;
+    margin-top: 10px;
+}
+
+
+P.footer {
+    font-size: 80%;
+    color: rgb(60,60,60);
+    margin-top: 10px;
+    border-top: 1px solid rgb(60,60,60);
+}
+
+/* ---- (left) SIDEBAR  ----- */
+
+div.sidebar {
+  position: absolute;
+  top: 5px; left: 5px;
+  width: 130px;
+  border: black 1px;
+  /* debug background-color: rgb(245,245,245); */
+}
+@media print {
+div.sidebar {
+   display:none;
+}
+}
+a.sidebar:link { 
+  color:#358;
+  text-decoration:none;
+} 
+a.sidebar:visited { 
+   color: #358; 
+   text-decoration: none;
+}
+a.sidebar:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+a.scodoc_title {
+    color: rgb(102,102,102);
+    font-family: arial,verdana,sans-serif;
+    font-size: large;
+    font-weight: bold;
+    text-transform: uppercase;
+    text-decoration: none;
+}
+h2.insidebar {
+    color: rgb(102,102,102);
+    font-weight: bold;
+    font-size: large;
+    margin-bottom: 0;
+}
+
+h3.insidebar {
+  color: rgb(102,102,102);
+  font-weight: bold;
+  font-size: medium;
+  margin-bottom: 0;
+  margin-top: 0;
+}
+
+ul.insidebar {
+  padding-left: 1em;
+  list-style: circle;
+}
+
+
+.etud-insidebar {
+   font-size: small;
+   background-color: rgb(220,220,220);
+   width: 100%;
+   -moz-border-radius: 6px;
+   -khtml-border-radius: 6px;
+   border-radius: 6px;
+}
+
+.etud-insidebar h2 {
+  color: rgb(153,51,51);
+  font-size: medium;
+}
+
+
+.etud-insidebar ul {
+   padding-left: 1.5em;
+   margin-left: 0;
+}
+
+div.logo-insidebar {
+   padding-left: 1em;
+}
+
+div.etud_info_div {
+    border: 4px solid gray;
+    height: 90px;
+    background-color: #f7f7ff;
+}
+
+div.eid_left {
+    padding: 4px; 
+    border: 0px;
+    vertical-align: top;
+    margin-right: 100px;
+}
+
+span.eid_right {
+    padding: 0px;
+    border: 0px;
+    position: absolute;
+    right: 5px;
+    top: 5px;
+}
+
+table.listesems th { 
+  text-align: left;  
+  padding-top: 0.5em;
+  padding-left: 0.5em;
+}
+
+table.listesems td.semicon { 
+  padding-left: 1.5em;
+}
+
+table.listesems tr.firstsem td {
+ padding-top: 0.8em;
+}
+
+h2.listesems { 
+  padding-bottom: 0px;
+  margin-bottom: 0px;
+}
+
+/* ----- Liste des news ----- */
+
+div.news {
+   margin-top: 1em;
+   margin-bottom: 0px;
+   margin-right: 16px;
+   margin-left: 16px;
+   padding:  0.5em;
+   background-color: #F5F6CE;
+   -moz-border-radius: 8px;
+   -khtml-border-radius: 8px;
+   border-radius: 8px;
+}
+
+span.newstitle {
+   font-family : arial, verdana, sans-serif ;
+   font-weight: bold;
+}
+
+ul.newslist {
+  padding-left: 1em;
+  padding-bottom: 0em;
+  list-style: circle;
+}
+
+span.newsdate {
+  padding-right: 2em;
+}
+
+span.newstext {
+    font-style: italic; 
+}
+
+/* --- infos sur premiere page Sco --- */
+div.scoinfos {
+   margin-top: 0.5em;
+   margin-bottom: 0px;
+   padding: 2px;
+   padding-bottom: 0px;
+   background-color: #F4F4B2;
+}
+
+/* ----- fiches etudiants ------ */
+
+div.ficheEtud {
+   background-color: #f5edc8; /* rgb(255,240,128); */
+   border: 1px solid gray;
+   width: 910px;
+   padding: 10px;
+   margin-top: 10px;
+}
+
+div.menus_etud {
+   position: absolute;
+   margin-left: 1px;
+   margin-top: 1px;
+}
+div.ficheEtud h2 {
+   padding-top: 10px;
+}
+
+div.code_nip {
+   padding-top: 10px;
+   font-family: "Andale Mono", "Courier";
+}
+
+div.fichesituation {
+  background-color: rgb( 231, 234, 218 ); /* E7EADA */   
+  margin: 0.5em 0 0.5em 0;
+  padding: 0.5em;
+  -moz-border-radius: 8px;
+  -khtml-border-radius: 8px;
+  border-radius: 8px;
+}
+
+div.ficheadmission { 
+  background-color: rgb( 231, 234, 218 ); /* E7EADA */   
+  margin: 0.5em 0 0.5em 0;
+  padding: 0.5em;
+  -moz-border-radius: 8px;
+  -khtml-border-radius: 8px;
+  border-radius: 8px;
+}
+
+div.etudarchive ul {
+    padding:0; 
+    margin:0; 
+    margin-left: 1em;
+    list-style-type:none; 
+}
+
+div.etudarchive ul li {
+    background-image: url(/ScoDoc/static/icons/bullet_arrow.png);
+    background-repeat: no-repeat;
+    background-position: 0 .4em;
+    padding-left: .6em;
+}
+div.etudarchive ul li.addetudarchive {
+    background-image: url(/ScoDoc/static/icons/bullet_plus.png);
+    padding-left: 1.2em
+}
+span.etudarchive_descr {
+    margin-right: .4em;
+}
+span.deletudarchive {
+    margin-left: 0.5em;
+}
+div.fichedebouche { 
+  background-color: rgb( 0, 100, 0 ); 
+  color: gold;
+  margin: 0.5em 0 0.5em 0;
+  padding: 0.5em;
+  -moz-border-radius: 8px;
+  -khtml-border-radius: 8px;
+  border-radius: 8px;
+}
+
+span.debouche_tit {
+  font-weight: bold;
+  padding-right: 1em;
+}
+
+div.ficheinscriptions {
+   background-color: #eae3e2; /* was EADDDA */
+   margin: 0.5em 0 0.5em 0;
+   padding: 0.5em;
+   -moz-border-radius: 8px;
+   -khtml-border-radius: 8px;
+   border-radius: 8px;
+}
+
+.ficheinscriptions a.sem {
+   text-decoration: none;
+   font-weight: bold;
+   color: blue;
+}
+
+.ficheinscriptions a.sem:hover {
+   color: red;
+}
+
+
+td.photocell {
+   padding-left: 32px;
+}
+
+div.fichetitre {
+   font-weight: bold;
+}
+
+td.fichetitre2 {
+   font-weight: bold;
+   vertical-align: top;
+}
+
+td.fichetitre2 .formula {
+   font-weight: normal;
+   color: rgb(0,64,0);
+   border: 1px solid red;
+   padding-left: 1em;
+   padding-right: 1em;
+   padding-top: 3px;
+   padding-bottom: 3px;
+   margin-right: 1em;
+}
+
+span.formula {
+  font-size: 80%;
+  font-family: Courier, monospace;
+  font-weight: normal;
+}
+
+td.fichetitre2 .fl {
+   font-weight: normal;
+}
+
+.ficheannotations {
+  background-color: #ddffdd; 
+  margin: 0.5em 0 0.5em 0;
+  padding: 0.5em;
+  -moz-border-radius: 8px;
+  -khtml-border-radius: 8px;
+  border-radius: 8px;
+}
+
+td.annodel {
+}
+
+/* Page accueil Sco */
+span.infostitresem {
+   font-weight: normal;
+}
+span.linktitresem {
+   font-weight: normal;
+}
+span.linktitresem a:link {color: red;}
+span.linktitresem a:visited {color: red;}
+
+.listegroupelink a:link { color: blue; }
+.listegroupelink a:visited { color: blue; }
+.listegroupelink a:hover { color: red; }
+
+a.stdlink, a.stdlink:visited {
+  color: blue; 
+  text-decoration: underline;
+}
+a.stdlink:hover { 
+  color: red; 
+  text-decoration: underline;
+}
+
+/* markup non semantique pour les cas simples */
+
+.fontred { color: red; }
+.fontorange { color: rgb(215, 90, 0); }
+.fontitalic { font-style: italic;  }
+.redboldtext {
+   font-weight: bold;
+   color: red;
+}
+
+.greenboldtext {
+   font-weight: bold;
+   color: green;
+} 
+
+a.redlink {
+   color: red;
+}
+a.redlink:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+a.discretelink {
+   color: black;
+   text-decoration: none;
+}
+
+a.discretelink:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+.rightcell {
+    text-align: right;
+}
+
+.rightjust {
+  padding-left: 2em;
+}
+
+.centercell { 
+    text-align: center;
+}
+
+.help {
+  font-style: italic; 
+}
+
+p.indent {
+  padding-left: 2em;
+}
+
+.blacktt { 
+  font-family: Courier, monospace;
+  font-weight: normal;
+  color: black;
+}
+
+p.msg { 
+  color: red;
+  font-weight: bold;
+  border: 1px solid blue;
+  background-color: rgb(140,230,250);
+  padding: 10px;
+}
+
+table.tablegrid {
+  border-color: black;
+  border-width: 0 0 1px 1px;
+  border-style: solid;
+  border-collapse: collapse;
+}
+table.tablegrid td, table.tablegrid th {
+  border-color: black;
+  border-width: 1px 1px 0 0;
+  border-style: solid;
+  margin: 0;
+  padding-left: 4px;
+  padding-right: 4px;
+}
+
+/* ----- Notes ------ */
+a.smallbutton {
+    border-width: 0;
+    margin: 0;
+    margin-left: 2px;
+    text-decoration: none;
+}
+span.evallink {
+    font-size: 80%;
+    font-weight: normal;
+}
+.boldredmsg {
+    color: red;
+    font-weight: bold;
+}
+
+tr.etuddem td {
+    color: rgb(100,100,100);
+    font-style: italic; 
+}
+
+td.etudabs, td.etudabs a.discretelink, tr.etudabs td.moyenne a.discretelink {
+    color: rgb(180,0,0);
+}
+tr.moyenne td {
+    font-weight: bold; 
+}
+
+table.notes_evaluation th.eval_complete a.sortheader {
+    color: green;
+}
+table.notes_evaluation th.eval_incomplete a.sortheader {
+    color: red;
+}
+table.notes_evaluation th.eval_attente a.sortheader {
+    color: rgb(215, 90, 0);;
+}
+table.notes_evaluation tr td a.discretelink:hover {
+    text-decoration: none;
+}
+table.notes_evaluation tr td.tdlink a.discretelink:hover {
+    color: red; 
+    text-decoration: underline;
+}
+table.notes_evaluation tr td.tdlink a.discretelink, table.notes_evaluation tr td.tdlink a.discretelink:visited {
+    color: blue; 
+    text-decoration: underline;
+}
+
+table.notes_evaluation tr td {
+    padding-left: 0.5em;
+    padding-right: 0.5em;
+}
+
+div.notes_evaluation_stats {
+    margin-top: -15px;
+}
+
+.eval_description p {
+    margin-bottom: 5px;
+    margin-top: 5px;
+}
+span.eval_info {
+    font-style: italic;
+}
+span.eval_complete {
+    color: green;
+}
+span.eval_incomplete {
+    color: red;
+}
+span.eval_attente {
+    color:  rgb(215, 90, 0);
+}
+
+table.tablenote {
+  border-collapse: collapse;
+  border: 2px solid blue;
+/*  width: 100%;*/
+  margin-bottom: 20px;
+  margin-right: 20px;
+}
+table.tablenote th {  
+  padding-left: 1em;
+}
+.tablenote a {
+   text-decoration: none;
+   color: black;
+}
+.tablenote a:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+table.tablenote_anonyme {
+   border-collapse: collapse;
+   border: 2px solid blue;
+}
+
+tr.tablenote {
+    border: solid blue 1px;
+}
+td.colnote {
+  text-align : right;
+  padding-right: 0.5em;  
+  border: solid blue 1px;
+}
+td.colnotemoy {
+  text-align : right;
+  padding-right: 0.5em;
+  font-weight: bold; 
+}
+td.colcomment, span.colcomment {
+  text-align: left;
+  padding-left: 2em;
+  font-style: italic; 
+  color: rgb(80,100,80);
+}
+
+h2.formsemestre, .gtrcontent h2 {
+    margin-top: 2px;
+    font-size: 130%;
+}
+
+.formsemestre_page_title table.semtitle, .formsemestre_page_title table.semtitle td {
+    padding: 0px;
+    margin-top: 0px; 
+    margin-bottom: 0px; 
+    border-width: 0;
+    border-collapse: collapse;
+}
+.formsemestre_page_title table.semtitle {
+    /* width: 100%; */
+}
+
+.formsemestre_page_title { 
+    width: 100%;
+    padding-top:5px;
+    padding-bottom: 10px;
+}
+
+.formsemestre_page_title table.semtitle td.infos table {
+    padding-top: 10px;
+}
+.formsemestre_menubar {
+    border-top: 3px solid #67A7E3;
+    background-color: #D6E9F8;
+    margin-top: 8px;
+}
+
+.formsemestre_menubar .barrenav ul li a.menu {
+    font-size: 12px;
+}
+
+.formsemestre_page_title  .infos span {
+    padding-right: 25px;
+}
+
+.formsemestre_page_title span.semtitle {
+    font-size: 110%;
+}
+.formsemestre_page_title span.resp {
+    color: red;
+    font-weight: bold;
+}
+.formsemestre_page_title span.nbinscrits {
+    text-align: right;
+    font-weight: bold;
+    padding-right: 1px;
+}
+.formsemestre_page_title span.lock {
+
+}
+
+div.formsemestre_status {
+    -moz-border-radius: 8px;
+    -khtml-border-radius: 8px;
+    border-radius: 8px;
+    padding: 2px 6px 2px 16px;
+    margin-right: 10px;
+}
+
+table.formsemestre_status {
+    border-collapse: collapse;
+}
+tr.formsemestre_status { background-color: rgb(90%,90%,90%); }
+tr.formsemestre_status_green { background-color: #EFF7F2; }
+tr.formsemestre_status_ue { background-color: rgb(90%,90%,90%); }
+table.formsemestre_status td {
+    border-top: 1px solid rgb(80%,80%,80%);
+    border-bottom: 1px solid rgb(80%,80%,80%);
+    border-left: 0px;
+}
+table.formsemestre_status td.evals, table.formsemestre_status th.evals, table.formsemestre_status td.resp, table.formsemestre_status th.resp {
+    padding-left: 1em;
+}
+
+table.formsemestre_status th {
+    font-weight: bold;
+    text-align: left;
+}
+th.formsemestre_status_inscrits {
+    font-weight: bold;
+    text-align: center;
+}
+td.formsemestre_status_code {
+    width: 2em;
+}
+a.formsemestre_status_link {
+    text-decoration:none;
+    color: black;
+}
+a.formsemestre_status_link:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+td.formsemestre_status_inscrits {
+  text-align: center;
+}
+td.formsemestre_status_cell {
+  white-space: nowrap;
+}
+
+span.status_ue_acro { font-weight: bold; }
+span.status_ue_title { font-style: italic; padding-left: 1cm;}
+
+table.formsemestre_inscr td {
+    padding-right: 1.25em;
+}
+
+ul.ue_inscr_list li span.tit {
+    font-weight: bold;
+}
+ul.ue_inscr_list li.tit {
+    padding-top: 1ex;
+}
+ul.ue_inscr_list li.etud {
+    padding-top: 0.7ex;
+}
+
+/* Liste des groupes sur tableau bord semestre */
+.formsemestre_status h3 {
+  border: 0px solid black;
+  margin-bottom: 5px;
+}
+
+#grouplists h4 {
+   font-style: italic;
+   margin-bottom: 0px;
+   margin-top: 5px;
+}
+
+#grouplists table {
+   //border: 1px solid black;
+   border-spacing: 1px;
+}
+
+#grouplists td {
+}
+
+
+/* Modules */
+div.moduleimpl_tableaubord { 
+  padding: 7px;
+  border: 2px solid gray;
+}
+
+table.moduleimpl_evaluations {
+  width: 100%;
+  border-spacing: 0px;
+}
+
+th.moduleimpl_evaluations {
+  font-weight: normal;
+  text-align: left;
+  color: red;
+}
+
+th.moduleimpl_evaluations a, th.moduleimpl_evaluations a:visited {
+  font-weight: normal;
+  color: red;
+  text-decoration: none;
+}
+th.moduleimpl_evaluations a:hover {
+   text-decoration: underline;
+}
+
+tr.moduleimpl_evaluation_row {
+}
+
+td.moduleimpl_evaluation_row {
+}
+
+tr.mievr {
+   background-color:#eeeeee;
+}
+
+tr.mievr_rattr {
+   background-color:#dddddd;
+}
+span.mievr_rattr {
+    font-weight: bold;
+    color: blue;
+    margin-left: 2em;
+    border: 1px solid red;
+    padding: 1px 3px 1px 3px;
+}
+
+tr.mievr td.mievr_tit { 
+  font-weight: bold;
+  background-color: #cccccc;
+}
+tr.mievr td {
+  text-align: left;
+  background-color:white;
+}
+tr.mievr th {
+  background-color:white;
+}
+tr.mievr td.mievr {
+  width: 90px;
+}
+tr.mievr td.mievr_menu {
+  width: 110px;
+}
+tr.mievr td.mievr_dur {
+  width: 60px;
+}
+tr.mievr td.mievr_coef {
+  width: 60px;
+}
+tr.mievr td.mievr_nbnotes {
+  width: 90px;
+}
+tr td.mievr_grtit {
+ vertical-align: top;
+ text-align: right;
+ font-weight: bold;
+}
+span.mievr_lastmodif { 
+ padding-left: 2em;
+ font-weight: normal;
+ font-style: italic; 
+}
+a.mievr_evalnodate {
+ color: rgb(215, 90, 0);
+ font-style: italic;
+ text-decoration: none;
+}
+a.mievr_evalnodate:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+span.evalindex {
+ font-weight: normal;
+ padding-left: 4em;
+}
+
+/* Formulaire edition des partitions */
+form#editpart table {
+   border: 1px solid gray;
+   border-collapse: collapse;
+}
+form#editpart tr.eptit th {
+   font-size: 110%;
+   border-bottom: 1px solid gray;
+}
+form#editpart td {
+   border-bottom: 1px dashed gray;
+}
+form#editpart table td {
+   padding-left: 1em;
+}
+form#editpart table td.epnav {
+   padding-left: 0;
+}
+
+/* Liste des formations */
+ul.notes_formation_list { 
+  list-style-type: none;
+  font-size: 110%;
+}
+li.notes_formation_list { 
+  padding-top: 10px;
+}
+
+/* Presentation formation (ue_list) */
+div.formation_descr {
+  background-color: rgb(250,250,240);
+  padding-left: 0.7em;
+  margin-right: 1em;
+}
+div.formation_descr span.fd_t {
+  font-weight: bold;
+  margin-right: 1em;
+}
+
+div.ue_list_tit {
+  font-weight: bold;
+  margin-top: 5px;
+}
+
+ul.notes_ue_list {
+  background-color: rgb(240,240,240);
+  font-weight: bold;
+  margin-top: 4px;
+  margin-right: 1em;
+}
+
+span.ue_code {
+  font-family: Courier, monospace;
+  font-weight: normal;
+  color: black;
+  font-size: 80%;
+}
+
+span.ue_type {
+  color: green;
+  margin-left: 1.5em;
+  margin-right: 1.5em;
+}
+
+ul.notes_matiere_list {
+  background-color: rgb(220,220,220);
+  font-weight: normal;
+  font-style: italic; 
+}
+
+ul.notes_module_list {
+   background-color: rgb(210,210,210);
+   font-weight: normal;
+   font-style: normal; 
+}
+
+.notes_ue_list a.discretelink, .notes_ue_list a.stdlink  {
+    color: #001084;
+    text-decoration: underline;
+}
+.notes_ue_list span.locked {
+    font-weight: normal;
+}
+
+.notes_ue_list a.smallbutton img {
+    position: relative;
+    top: 2px;
+}
+
+div#ue_list_code {
+    background-color: rgb(220,220,220);
+    font-size: small;
+    padding-left: 4px;
+    padding-bottom: 1px;
+    margin-top: 10px;
+    margin: 3ex;
+}
+
+ul.notes_module_list {
+    list-style-type: none;
+}
+
+div#ue_list_etud_validations {
+    background-color: rgb(220,250,220);
+    padding-left: 4px;
+    padding-bottom: 1px;
+    margin: 3ex;
+}
+div#ue_list_etud_validations span {
+    font-weight: bold;
+}
+
+span.ue_share {
+    font-weight: bold;
+}
+
+div.ue_warning {
+    border: 1px solid red;
+    background-color: rgb(250,220,220);
+    margin: 3ex;
+    padding-left: 1ex;
+    padding-right: 1ex;
+}
+
+div.ue_warning span:before {
+    content: url(/ScoDoc/static/icons/warning_img.png);
+    vertical-align: -80%;  
+}
+
+div.ue_warning span {
+    font-weight: bold;
+}
+
+/* tableau recap notes */
+table.notes_recapcomplet {
+   border: 2px solid blue;
+   border-spacing: 0px 0px;
+   border-collapse: collapse;
+   white-space: nowrap;   
+}
+tr.recap_row_even {
+   background-color: rgb(210,210,210);
+}
+@media print {
+tr.recap_row_even { /* bordures noires pour impression */
+   border-top: 1px solid black;
+   border-bottom: 1px solid black;
+}
+}
+tr.recap_row_moy {
+   border-top: 1px solid blue;
+   font-weight: bold;
+}
+tr.recap_row_min, tr.recap_row_max {
+   font-weight: normal;
+   font-style: italic;
+}
+tr.recap_row_max {
+   border-bottom: 1px solid blue;
+}
+td.recap_tit {
+   font-weight: bold;
+   text-align: left;
+   padding-right: 1.2em;
+}
+td.recap_tit_ue {
+   font-weight: bold;
+   text-align: left;
+   padding-right: 1.2em;
+   padding-left: 2px;
+   border-left: 1px solid blue;
+}
+td.recap_col {
+   padding-right: 1.2em;
+   text-align: left;
+}
+td.recap_col_moy {
+   padding-right: 1.5em;
+   text-align: left;
+   font-weight: bold;
+   color: rgb(80,0,0);
+}
+td.recap_col_moy_inf {
+   padding-right: 1.5em;
+   text-align: left;
+   font-weight: bold;
+   color: rgb(255,0,0);
+}
+td.recap_col_ue {
+   padding-right: 1.2em;
+   padding-left: 4px;
+   text-align: left;
+   font-weight: bold;
+   border-left: 1px solid blue;
+}
+td.recap_col_ue_inf {
+   padding-right: 1.2em;
+   padding-left: 4px;
+   text-align: left;
+   font-weight: bold;
+   color: rgb(255,0,0);
+   border-left: 1px solid blue;
+}
+td.recap_col_ue_val {
+   padding-right: 1.2em;
+   padding-left: 4px;
+   text-align: left;
+   font-weight: bold;
+   color: rgb(0,140,0);
+   border-left: 1px solid blue;
+}
+/* noms des etudiants sur recap complet */
+table.notes_recapcomplet a:link, table.notes_recapcomplet a:visited {
+   text-decoration: none;
+   color: black;
+}
+
+table.notes_recapcomplet a:hover {
+   color: red;
+   text-decoration: underline;
+}
+
+/* bulletin */
+table.notes_bulletin {
+   border-collapse: collapse;
+   border: 2px solid rgb(100,100,240);
+   width: 100%;
+   margin-right: 100px;
+   background-color: rgb(240,250,255);
+   font-family : arial, verdana, sans-serif ;
+   font-size: 13px;
+}
+
+tr.notes_bulletin_row_gen {
+   border-top: 1px solid black;
+   font-weight: bold;
+}
+tr.notes_bulletin_row_rang {
+   font-weight: bold;
+}
+
+tr.notes_bulletin_row_ue {
+   /* background-color: rgb(170,187,204); voir sco_utils.UE_COLORS */
+   font-weight: bold;
+   border-top: 1px solid black;
+}
+
+tr.bul_row_ue_cur {
+   background-color: rgb(180,180,180);
+   color: rgb(50,50,50);
+}
+
+tr.bul_row_ue_cap {
+   background-color: rgb(150,170,200);
+   color: rgb(50,50,50);
+}
+
+tr.notes_bulletin_row_mat {
+  border-top: 2px solid rgb(140,140,140);
+  color: blue;
+}
+
+tr.notes_bulletin_row_mod {
+  border-top: 1px solid rgb(140,140,140);
+}
+
+tr.notes_bulletin_row_mod td.titre, tr.notes_bulletin_row_mat td.titre {
+  padding-left: 1em;
+}
+
+tr.notes_bulletin_row_eval {
+  font-style: italic; 
+  color: rgb(60,60,80);
+}
+tr.notes_bulletin_row_eval_incomplete .discretelink {
+  color: rgb(200,0,0);
+}
+
+tr.b_eval_first td {
+  border-top: 1px dashed rgb(170,170,170);
+}
+tr.b_eval_first td.titre {
+  border-top: 0px;
+}
+tr.notes_bulletin_row_eval td.module {
+  padding-left: 5px;
+  border-left: 1px dashed rgb(170,170,170);
+}
+
+table.notes_bulletin td.note {
+  padding-left: 1em;
+}
+table.notes_bulletin td.min, table.notes_bulletin td.max {
+  font-size: 80%;  
+}
+
+table.notes_bulletin tr.notes_bulletin_row_ue_cur td.note, table.notes_bulletin tr.notes_bulletin_row_ue_cur td.min, table.notes_bulletin tr.notes_bulletin_row_ue_cur td.max {
+  color: rgb(80,80,80);
+  font-style: italic; 
+}
+
+.note_bold {
+   font-weight: bold; 
+}
+td.bull_coef_eval, td.bull_nom_eval {
+   font-style: italic; 
+   color: rgb(60,60,80);
+}
+tr.notes_bulletin_row_eval td.note {
+   font-style: italic; 
+   color: rgb(40,40,40);
+   font-size: 90%;
+}
+
+tr.notes_bulletin_row_eval td.note .note_nd {
+   font-weight: bold; 
+   color: red;
+}
+
+/* --- Bulletins UCAC */
+tr.bul_ucac_row_tit, tr.bul_ucac_row_ue, tr.bul_ucac_row_total, tr.bul_ucac_row_decision, tr.bul_ucac_row_mention {
+   font-weight: bold;
+   border: 1px solid black;
+}
+tr.bul_ucac_row_tit {
+   background-color: rgb(170,187,204);
+}
+tr.bul_ucac_row_total, tr.bul_ucac_row_decision, tr.bul_ucac_row_mention {
+   background-color: rgb(220,220,220);
+}
+
+/* ---- /ucac */
+
+span.bul_minmax {
+   font-weight: normal;
+   font-size: 66%;   
+}
+span.bul_minmax:before {
+   content: " ";
+}
+
+a.bull_link {
+   text-decoration:none;
+   color: rgb(20,30,30);
+}
+a.bull_link:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+table.bull_head {
+   width: 100%;
+}
+td.bull_photo {
+   text-align: right;
+}
+
+div.bulletin_menubar {
+   padding-left: 25px;
+}
+
+.bull_liensemestre {
+   font-weight: bold; 
+}
+.bull_liensemestre a {
+   color: rgb(255,0,0);
+   text-decoration: none;
+}
+.bull_liensemestre a:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+div.bull_appreciations {
+}
+
+.bull_appreciations p {
+   margin: 0;
+   font-style: italic; 
+}
+.bull_appreciations_link {
+   margin-left: 1em;
+}
+span.bull_appreciations_date {
+   margin-right: 1em;
+   font-style: normal;
+   font-size: 75%; 
+}
+
+div.eval_description {
+    color: rgb(20,20,20);
+    /* border: 1px solid rgb(30,100,0); */
+    padding: 3px;
+}
+
+
+/* Saisie des notes */
+div.saisienote_etape1 {
+  border: 2px solid blue;
+  padding: 5px;
+  background-color: rgb( 231, 234, 218 ); /* E7EADA */ 
+}
+div.saisienote_etape2 {
+  border: 2px solid green; 
+  margin-top: 1em;
+  padding: 5px;
+  background-color: rgb(234,221,218); /* EADDDA */
+}
+span.titredivsaisienote { 
+  font-weight: bold;
+  font-size: 115%;
+}
+
+/* ----- Absences ------ */
+td.matin {
+  background-color: rgb(203,242,255);
+}
+
+td.absent {
+  background-color: rgb(255,156,156);
+}
+
+td.present {
+  background-color: rgb(230,230,230);
+}
+
+span.capstr {
+  color: red;
+}
+
+/* ----- Formulator  ------- */
+ul.tf-msg { 
+  color: rgb(6,80,18);
+  border: 1px solid red;
+}
+
+li.tf-msg {  
+  list-style-image: url(/ScoDoc/static/icons/warning16_img.png);
+  padding-top: 5px;
+  padding-bottom: 5px;
+}
+
+p.warning {
+  font-weight: bold;
+  color: red;
+}
+/* ---- ? ne fonctionne pas :  p.warning:before { /ScoDoc/static/icons/warning16_img"); } */
+
+form.sco_pref table.tf {
+  border-spacing: 5px 15px;
+}
+
+td.tf-ro-fieldlabel { 
+  /* font-weight: bold; */
+  vertical-align:top;
+  margin-top: 20px;
+}
+td.tf-ro-fieldlabel:after {
+   content: ' :';
+}
+td.tf-ro-field {
+  vertical-align:top;
+}
+div.tf-ro-textarea {
+  border: 1px solid grey;
+  padding-left: 8px;
+}
+select.tf-selglobal {
+  margin-left: 10px;
+}
+
+td.tf-fieldlabel { 
+  /* font-weight: bold; */
+  vertical-align:top;
+}
+
+.tf-comment {
+    font-size: 80%;
+    font-style: italic;
+}
+
+.tf-explanation {
+    font-style: italic;
+}
+
+.radio_green { 
+   background-color: green;
+}
+
+.radio_red {
+   background-color: red;
+}
+
+td.fvs_val {
+ border-left: 1px solid rgb(80,80,80);
+ text-align: center;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+td.fvs_val_inf {
+ border-left: 1px solid rgb(80,80,80);
+ text-align: center;
+ padding-left: 1em;
+ padding-right: 1em;
+ color: red;
+}
+
+td.fvs_chk {
+}
+
+td.fvs_tit {
+ font-weight: bold;
+ text-align: left;
+ border-left: 1px solid rgb(80,80,80);
+ text-align: center;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+td.fvs_tit_chk {
+ font-weight: bold;
+}
+
+/* ----- Entreprises ------- */
+
+table.entreprise_list, table.corr_list, table.contact_list {
+   width : 100%;
+   border-width: 0px;
+   /* border-style: solid; */
+   border-spacing: 0px 0px;
+   padding: 0px;
+}
+tr.entreprise_list_even, tr.corr_list_even, tr.contact_list_even {
+  background-color: rgb(85%,85%,95%);
+}
+tr.entreprise_list_odd, tr.corr_list_odd, tr.contact_list_odd {
+  background-color: rgb(90%,90%, 90%);
+}
+
+td.entreprise_descr, td.corr_descr, td.contact_descr { 
+  padding-left: 2em;
+}
+td.entreprise_descr_link { 
+  padding-left: 2em;
+   white-space: nowrap;
+}
+td.entreprise_descr_name { white-space: nowrap; }
+
+a.entreprise_delete {  color: black; text-decoration: underline; }
+a.entreprise_delete:visited { color: black; }
+
+a.entreprise_edit { 
+  text-decoration: underline; 
+  color : rgb(0,0,204);
+  font-weight: normal;
+}
+a.entreprise_edit:visited { color :  rgb(0,0,204); }
+a.entreprise_edit:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+p.entreprise_create { padding-top: 2em; }
+a.entreprise_create { color : black; font-weight: bold; }
+a.entreprise_create:visited { color : black; }
+
+table.entreprise_list_title { 
+  width: 100%; 
+  border-top: 1px solid rgb(51,102,204); 
+  border-spacing: 0px 0px; 
+  padding: 0px;
+}
+tr.entreprise_list_title {
+  background-color: rgb(229,236,249);
+  font-weight: bold;
+}
+td.entreprise_list_title {
+  padding-left: 1em;
+}
+td.entreprise_list_title_res {
+  font-weight: normal;
+  text-align : right;
+}
+
+h2.entreprise { 
+  color: rgb(6,102,18);
+  border: 1px solid blue;
+}
+
+h2.entreprise_contact:before { 
+  content: url(/ScoDoc/static/icons/contact_img.png);
+  vertical-align: -80%;  
+  padding-right: 1em; 
+}
+h2.entreprise_correspondant:before { 
+  content: url(/ScoDoc/static/icons/correspondant_img.png);
+  vertical-align: -80%;  
+  padding-right: 1em; 
+}
+
+h2.entreprise_new:before { 
+  content: url(/ScoDoc/static/icons/entreprise_img.png);
+  vertical-align: -80%; 
+  padding-right: 2em; 
+}
+
+p.entreprise_warning, p.gtr_warning, p.gtr_interdit, p.gtr_devel { 
+  color: red; 
+  font-style: italic; 
+  margin-top: -1em;
+}
+P.entreprise_warning:before { 
+  content: url(/ScoDoc/static/icons/warning_img.png);
+  vertical-align: -80%;  
+}
+P.gtr_interdit:before {
+  content: url(/ScoDoc/static/icons/interdit_img.png);
+  vertical-align: -80%;  
+}
+P.gtr_devel:before {
+  content: url(/ScoDoc/static/icons/devel_img.png);
+  vertical-align: -80%;  
+}
+div.entreprise-insidebar { 
+  border: 1px solid blue;
+}
+
+/* ---- Sortable tables --- */
+/* Sortable tables */
+table.sortable a.sortheader {
+    background-color:#E6E6E6;
+    color: black;
+    font-weight: bold;
+    text-decoration: none;
+    display: block;
+}
+table.sortable span.sortarrow {
+    color: black;
+    text-decoration: none;
+}
+
+/* Horizontal bar graph */
+.graph {
+  width: 100px;
+  height: 12px;
+  /* background-color: rgb(200, 200, 250); */
+  padding-bottom: 0px;  
+  margin-bottom: 0;
+  margin-right: 0px;
+  margin-top: 3px;
+  margin-left: 10px;
+  border:1px solid black;
+  position: absolute;
+}
+
+.bar {
+     background-color: rgb(100, 150, 255);
+     margin: 0; 
+     padding: 0; 
+     position: absolute;
+     left: 0;
+     top: 2px;
+     height: 8px;
+     z-index: 2;
+}
+
+.mark {
+     background-color: rgb(0, 150, 0);
+     margin: 0; 
+     padding: 0; 
+     position: absolute;
+     top: 0;
+     width: 2px;
+     height: 100%;
+     z-index: 2;
+}
+
+td.cell_graph {
+    width: 170px;
+}
+
+/* ------------------ Formulaire validation semestre ---------- */
+table.recap_parcours {
+  color: black;
+  border-collapse: collapse;
+}
+
+table.recap_parcours td { 
+  padding-left: 8px;
+  padding-right: 8px;
+}
+
+.recap_parcours tr.sem_courant {
+  background-color: rgb(245, 243, 116);
+}
+
+.recap_parcours tr.sem_precedent {
+  background-color: rgb(90%,95%,90%); 
+}
+
+.recap_parcours tr.sem_autre {
+  background-color: rgb(90%,90%,90%);
+}
+
+.rcp_l1 td {
+  padding-top: 5px;
+  border-top: 3px solid rgb(50%,50%,50%);
+  border-right: 0px;
+  border-left: 0px;
+  color: blue;
+  vertical-align: top;
+}
+td.rcp_dec {
+  color: rgb(0%,0%,50%);;
+}
+td.rcp_nonass {
+  color: red;
+}
+
+.recap_hide_details tr.rcp_l2 { display: none; } 
+table.recap_hide_details td.ue_acro span { display: none; }
+
+.sco_hide { display: none; }
+
+table.recap_hide_details tr.sem_courant,  table.recap_hide_details tr.sem_precedent { 
+    display: table-row; 
+}
+table.recap_hide_details tr.sem_courant td.ue_acro span, table.recap_hide_details tr.sem_precedent td.ue_acro span { 
+    display: inline; 
+}
+
+.recap_parcours tr.sem_courant td.rcp_type_sem {
+    font-weight: bold;
+}
+.recap_parcours tr.sem_autre td.rcp_type_sem {
+    color: rgb(100%,70%,70%);
+}
+
+.rcp_l2 td {
+  padding-bottom: 5px;
+}
+
+td.rcp_moy {  
+}
+table.recap_parcours td.datedebut {
+    color: rgb(0,0,128);
+}
+table.recap_parcours td.datefin {
+    color: rgb(0,0,128);
+}
+table.recap_parcours td.rcp_type_sem {  
+  padding-left: 4px;
+  padding-right: 4px;
+  color: red;
+}
+td.ue_adm {
+  color: green;
+  font-weight: bold;
+}
+
+td.ue_cmp {
+  color: green;
+}
+
+td.ue { 
+}
+
+h3.sfv {   
+  margin-top: 0px;
+}
+
+form.sfv_decisions { 
+  border:1px solid blue;
+  padding: 6px;
+  margin-right: 2px;
+}
+form.sfv_decisions_manuelles { 
+  margin-top: 10px;
+}
+th.sfv_subtitle { 
+  text-align: left;
+  font-style: italic;  
+}
+
+
+tr.sfv_ass { 
+  background-color: rgb(90%,90%,80%);
+}
+
+tr.sfv_pbass { 
+  background-color: rgb(90%,70%,80%);
+}
+
+div.pas_sembox {
+  margin-top: 10px;
+  border: 2px solid #a0522d;
+  padding: 5px;
+  margin-right: 10px;
+  font-family: arial,verdana,sans-serif;
+}
+
+div.pas_empty_sems {
+}
+
+.inscrit {
+/*  font-weight: bold; */
+}
+
+.inscrailleurs {
+  font-weight: bold;
+  color: red !important;
+}
+
+span.paspaye, span.paspaye a {
+  color:  #9400d3 !important;
+}
+
+.pas_sembox_title a {
+  font-weight: bold;
+  font-size: 100%;
+  color: #1C721C;
+}
+
+.pas_sembox_subtitle {
+  font-weight: normal;
+  font-size: 100%;
+  color: blue;
+  border-bottom: 1px solid  rgb(50%,50%,50%);
+  margin-bottom: 8px;
+}
+
+.pas_recap {  
+  font-weight: bold;
+  font-size: 110%;
+  margin-top: 10px;
+}
+
+div.pas_help {
+  float: left;
+}
+
+/* ---- check absences / evaluations ---- */
+div.module_check_absences h2 { 
+  font-size: 100%;
+  color: blue;
+  margin-bottom:0px;
+}
+
+div.module_check_absences h2.eval_check_absences {  
+  font-size: 80%;
+  color: black;
+  margin-left: 20px;
+  margin-top:0px;
+  margin-bottom:5px;
+}
+
+div.module_check_absences h3 {  
+  font-size: 80%;
+  color: rgb(133,0,0);
+  margin-left: 40px;
+  margin-top:0px;
+  margin-bottom:0px;
+}
+
+div.module_check_absences ul { 
+  margin-left: 60px;
+  font-size: 80%;
+  margin-top:0px;
+  margin-bottom:0px;
+}
+
+/* ----------------------------------------------- */
+/*    Help bubbles (aka tooltips)                  */
+/* ----------------------------------------------- */
+.tooltip{
+  width: 200px;
+  color:#000;
+  font:lighter 11px/1.3 Arial,sans-serif;
+  text-decoration:none;
+  text-align:center;
+}
+
+.tooltip span.top{
+  padding: 30px 8px 0;
+  background: url(/ScoDoc/static/icons/bt_gif.png) no-repeat top;
+}
+
+.tooltip b.bottom{
+  padding:3px 8px 15px;
+  color: #548912;
+  background: url(/ScoDoc/static/icons/bt_gif.png) no-repeat bottom;
+}
+/* ----------------------------------------------- */
+
+/* ----------------------------- */
+/* TABLES generees par gen_table */
+/* ----------------------------- */
+/* style pour les tables generiques ne specifiant pas de classes */
+table.gt_table { 
+ border-collapse: collapse;
+ background-color: rgb(90%,90%,90%);
+ /* border: 1px solid black; */
+ padding: 10px;
+ margin-right: 1em;
+}
+
+table.gt_table tr {
+ background-color: rgb(90%,90%,90%);
+}
+
+table.gt_table tr.gt_hl {
+ background-color: rgb(90%,95%,90%);
+}
+table.gt_table td, table.gt_table th {
+ border: 1px solid rgb(80%,80%,80%);
+}
+table.gt_table th {
+  padding-left: 5px;
+  padding-right: 5px;
+}
+table.gt_table td { 
+ text-align: right;
+ padding-left: 2px;
+ padding-right: 4px;
+}
+
+table.table_cohorte tr.sortbottom { 
+  background-color: rgb(85%,90%,100%);
+}
+
+table.table_leftalign tr td {  
+  text-align: left;
+}
+table.table_coldate tr td:first-child { /* largeur col. date/time */
+  width: 12em;
+  color: rgb(0%,0%,50%);
+}
+
+
+table.table_listegroupe tr td { 
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+}
+
+table.list_users td.roles {
+  width: 22em;
+}
+
+table.list_users td.date_modif_passwd {
+  white-space: nowrap;
+}
+
+/* ----------------------------- */
+
+div.form_rename_partition {
+  margin-top: 2em;
+  margin-bottom: 2em;
+}
+
+
+td.calday {
+  text-align: right;
+  vertical-align: top;
+}
+
+div.cal_evaluations table.monthcalendar td.calcol {
+}
+
+div.cal_evaluations table.monthcalendar td.calcell {
+  padding-left: 0.6em;
+  width: 6em;
+}
+
+
+div.cal_evaluations table.monthcalendar td a {
+  color: rgb(128,0,0);
+}
+
+#lyc_map_canvas { 
+   width:  900px;
+   height: 600px; 
+}
+
+/* Mise en garde -------------------------------*/
+.attention{
+    background-color:#CCCC99;
+    border: 2px solid #CCCC99;
+    position:relative;
+    z-index:1;
+    overflow:hidden;
+    list-style:none;
+    padding:0;
+    margin:0 0 0.25em;
+    display:table;
+    margin:10px 10px 5px 10px;
+    padding:5px 5px 5px 25px;
+    border-radius: 10px;
+}
+.attention:before {
+    content:"i";
+    left:6px;
+    width:16px;
+    height:16px;
+    margin-top:-8px;
+    font-size:14px;
+    font-weight:bold;
+    font-style:italic;
+    line-height:15px;
+    text-align:center;
+    color:#fff;
+    background-color:#2385BA;
+    /* css3 */
+    -webkit-border-radius:16px;
+    -moz-border-radius:16px;
+    border-radius:16px;
+    position:absolute;
+    top:13px;
+}
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/deconnexion.php b/misc/PublicationBulletins/Portail-LeHavre/deconnexion.php
new file mode 100755
index 0000000000000000000000000000000000000000..eb906aabe2932529c6f82169a96975f88899694d
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/deconnexion.php
@@ -0,0 +1,27 @@
+
+<?php
+// Initialisation de la session.
+// Si vous utilisez un autre nom
+// session_name("autrenom")
+session_start();
+
+// D�truit toutes les variables de session
+$_SESSION = array();
+
+
+// Si vous voulez d�truire compl�tement la session, effacez �galement
+// le cookie de session.
+// Note : cela d�truira la session et pas seulement les donn�es de session !
+if (ini_get("session.use_cookies")) {
+    $params = session_get_cookie_params();
+    setcookie(session_name(), '', time() - 42000,
+        $params["path"], $params["domain"],
+        $params["secure"], $params["httponly"]
+    );
+}
+session_unset() ; 
+
+// Finalement, on d�truit la session.
+session_destroy();
+echo " <h1>Merci de votre visite. </h1><h3>Pensez � fermer votre navigateur en partant et ne jamais enregistrer votre mot de passe.</h3>"
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/courier.php b/misc/PublicationBulletins/Portail-LeHavre/font/courier.php
new file mode 100755
index 0000000000000000000000000000000000000000..913f9a4533e0047f42e8ac037991b3d44c8cc939
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/courier.php
@@ -0,0 +1,7 @@
+<?php
+for($i=0;$i<=255;$i++)
+	$fpdf_charwidths['courier'][chr($i)]=600;
+$fpdf_charwidths['courierB']=$fpdf_charwidths['courier'];
+$fpdf_charwidths['courierI']=$fpdf_charwidths['courier'];
+$fpdf_charwidths['courierBI']=$fpdf_charwidths['courier'];
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/helvetica.php b/misc/PublicationBulletins/Portail-LeHavre/font/helvetica.php
new file mode 100755
index 0000000000000000000000000000000000000000..ca94cdf21ab299eff088953feb86104aeaa91bae
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/helvetica.php
@@ -0,0 +1,15 @@
+<?php
+$fpdf_charwidths['helvetica']=array(
+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,
+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584,
+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667,
+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,
+	'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833,
+	'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556,
+	chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,
+	chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,
+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667,
+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,
+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556,
+	chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500);
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/helveticab.php b/misc/PublicationBulletins/Portail-LeHavre/font/helveticab.php
new file mode 100755
index 0000000000000000000000000000000000000000..276cfa8cb8c14554fd5d9179a38f137dc06a174c
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/helveticab.php
@@ -0,0 +1,15 @@
+<?php
+$fpdf_charwidths['helveticaB']=array(
+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,
+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584,
+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722,
+	'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,
+	'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889,
+	'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556,
+	chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,
+	chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,
+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,
+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,
+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611,
+	chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556);
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/helveticabi.php b/misc/PublicationBulletins/Portail-LeHavre/font/helveticabi.php
new file mode 100755
index 0000000000000000000000000000000000000000..8d2177432542b3908e37b90a768e7b4905c175db
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/helveticabi.php
@@ -0,0 +1,15 @@
+<?php
+$fpdf_charwidths['helveticaBI']=array(
+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,
+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584,
+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722,
+	'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,
+	'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889,
+	'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556,
+	chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,
+	chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,
+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,
+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,
+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611,
+	chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556);
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/helveticai.php b/misc/PublicationBulletins/Portail-LeHavre/font/helveticai.php
new file mode 100755
index 0000000000000000000000000000000000000000..88bf4371bd458b7fada28136c4324be14c9ea930
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/helveticai.php
@@ -0,0 +1,15 @@
+<?php
+$fpdf_charwidths['helveticaI']=array(
+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,
+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584,
+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667,
+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,
+	'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833,
+	'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556,
+	chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,
+	chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,
+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667,
+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,
+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556,
+	chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500);
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/index.html b/misc/PublicationBulletins/Portail-LeHavre/font/index.html
new file mode 100755
index 0000000000000000000000000000000000000000..3b408bc9ad4fe82ea86af7812b267cf64402b6df
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/index.html
@@ -0,0 +1 @@
+<HTML><BODY><br><br><br><center><img src="http://colloquegeii.gesi.asso.fr/colloque_2011/jdsm_diapo/images/diapo_p2/logo_zz11_diapo.png" alt="Colloque GEII Angoul�me 2011"></center></BODY></HTML>
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1250.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1250.map
new file mode 100755
index 0000000000000000000000000000000000000000..ec110af06108ab961c9eafd5fc45a7488ca6cce0
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1250.map
@@ -0,0 +1,251 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!89 U+2030 perthousand
+!8A U+0160 Scaron
+!8B U+2039 guilsinglleft
+!8C U+015A Sacute
+!8D U+0164 Tcaron
+!8E U+017D Zcaron
+!8F U+0179 Zacute
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!99 U+2122 trademark
+!9A U+0161 scaron
+!9B U+203A guilsinglright
+!9C U+015B sacute
+!9D U+0165 tcaron
+!9E U+017E zcaron
+!9F U+017A zacute
+!A0 U+00A0 space
+!A1 U+02C7 caron
+!A2 U+02D8 breve
+!A3 U+0141 Lslash
+!A4 U+00A4 currency
+!A5 U+0104 Aogonek
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+015E Scedilla
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+017B Zdotaccent
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+02DB ogonek
+!B3 U+0142 lslash
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+0105 aogonek
+!BA U+015F scedilla
+!BB U+00BB guillemotright
+!BC U+013D Lcaron
+!BD U+02DD hungarumlaut
+!BE U+013E lcaron
+!BF U+017C zdotaccent
+!C0 U+0154 Racute
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+0102 Abreve
+!C4 U+00C4 Adieresis
+!C5 U+0139 Lacute
+!C6 U+0106 Cacute
+!C7 U+00C7 Ccedilla
+!C8 U+010C Ccaron
+!C9 U+00C9 Eacute
+!CA U+0118 Eogonek
+!CB U+00CB Edieresis
+!CC U+011A Ecaron
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+010E Dcaron
+!D0 U+0110 Dcroat
+!D1 U+0143 Nacute
+!D2 U+0147 Ncaron
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+0150 Ohungarumlaut
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+0158 Rcaron
+!D9 U+016E Uring
+!DA U+00DA Uacute
+!DB U+0170 Uhungarumlaut
+!DC U+00DC Udieresis
+!DD U+00DD Yacute
+!DE U+0162 Tcommaaccent
+!DF U+00DF germandbls
+!E0 U+0155 racute
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+0103 abreve
+!E4 U+00E4 adieresis
+!E5 U+013A lacute
+!E6 U+0107 cacute
+!E7 U+00E7 ccedilla
+!E8 U+010D ccaron
+!E9 U+00E9 eacute
+!EA U+0119 eogonek
+!EB U+00EB edieresis
+!EC U+011B ecaron
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+010F dcaron
+!F0 U+0111 dcroat
+!F1 U+0144 nacute
+!F2 U+0148 ncaron
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+0151 ohungarumlaut
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+0159 rcaron
+!F9 U+016F uring
+!FA U+00FA uacute
+!FB U+0171 uhungarumlaut
+!FC U+00FC udieresis
+!FD U+00FD yacute
+!FE U+0163 tcommaaccent
+!FF U+02D9 dotaccent
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1251.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1251.map
new file mode 100755
index 0000000000000000000000000000000000000000..de6a198d99d9d17db29f02633e3b0e66c9a60e98
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1251.map
@@ -0,0 +1,255 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0402 afii10051
+!81 U+0403 afii10052
+!82 U+201A quotesinglbase
+!83 U+0453 afii10100
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!88 U+20AC Euro
+!89 U+2030 perthousand
+!8A U+0409 afii10058
+!8B U+2039 guilsinglleft
+!8C U+040A afii10059
+!8D U+040C afii10061
+!8E U+040B afii10060
+!8F U+040F afii10145
+!90 U+0452 afii10099
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!99 U+2122 trademark
+!9A U+0459 afii10106
+!9B U+203A guilsinglright
+!9C U+045A afii10107
+!9D U+045C afii10109
+!9E U+045B afii10108
+!9F U+045F afii10193
+!A0 U+00A0 space
+!A1 U+040E afii10062
+!A2 U+045E afii10110
+!A3 U+0408 afii10057
+!A4 U+00A4 currency
+!A5 U+0490 afii10050
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+0401 afii10023
+!A9 U+00A9 copyright
+!AA U+0404 afii10053
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+0407 afii10056
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+0406 afii10055
+!B3 U+0456 afii10103
+!B4 U+0491 afii10098
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+0451 afii10071
+!B9 U+2116 afii61352
+!BA U+0454 afii10101
+!BB U+00BB guillemotright
+!BC U+0458 afii10105
+!BD U+0405 afii10054
+!BE U+0455 afii10102
+!BF U+0457 afii10104
+!C0 U+0410 afii10017
+!C1 U+0411 afii10018
+!C2 U+0412 afii10019
+!C3 U+0413 afii10020
+!C4 U+0414 afii10021
+!C5 U+0415 afii10022
+!C6 U+0416 afii10024
+!C7 U+0417 afii10025
+!C8 U+0418 afii10026
+!C9 U+0419 afii10027
+!CA U+041A afii10028
+!CB U+041B afii10029
+!CC U+041C afii10030
+!CD U+041D afii10031
+!CE U+041E afii10032
+!CF U+041F afii10033
+!D0 U+0420 afii10034
+!D1 U+0421 afii10035
+!D2 U+0422 afii10036
+!D3 U+0423 afii10037
+!D4 U+0424 afii10038
+!D5 U+0425 afii10039
+!D6 U+0426 afii10040
+!D7 U+0427 afii10041
+!D8 U+0428 afii10042
+!D9 U+0429 afii10043
+!DA U+042A afii10044
+!DB U+042B afii10045
+!DC U+042C afii10046
+!DD U+042D afii10047
+!DE U+042E afii10048
+!DF U+042F afii10049
+!E0 U+0430 afii10065
+!E1 U+0431 afii10066
+!E2 U+0432 afii10067
+!E3 U+0433 afii10068
+!E4 U+0434 afii10069
+!E5 U+0435 afii10070
+!E6 U+0436 afii10072
+!E7 U+0437 afii10073
+!E8 U+0438 afii10074
+!E9 U+0439 afii10075
+!EA U+043A afii10076
+!EB U+043B afii10077
+!EC U+043C afii10078
+!ED U+043D afii10079
+!EE U+043E afii10080
+!EF U+043F afii10081
+!F0 U+0440 afii10082
+!F1 U+0441 afii10083
+!F2 U+0442 afii10084
+!F3 U+0443 afii10085
+!F4 U+0444 afii10086
+!F5 U+0445 afii10087
+!F6 U+0446 afii10088
+!F7 U+0447 afii10089
+!F8 U+0448 afii10090
+!F9 U+0449 afii10091
+!FA U+044A afii10092
+!FB U+044B afii10093
+!FC U+044C afii10094
+!FD U+044D afii10095
+!FE U+044E afii10096
+!FF U+044F afii10097
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1252.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1252.map
new file mode 100755
index 0000000000000000000000000000000000000000..dd490e5961485ea47e527508691007e31e376fe9
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1252.map
@@ -0,0 +1,251 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!83 U+0192 florin
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!88 U+02C6 circumflex
+!89 U+2030 perthousand
+!8A U+0160 Scaron
+!8B U+2039 guilsinglleft
+!8C U+0152 OE
+!8E U+017D Zcaron
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!98 U+02DC tilde
+!99 U+2122 trademark
+!9A U+0161 scaron
+!9B U+203A guilsinglright
+!9C U+0153 oe
+!9E U+017E zcaron
+!9F U+0178 Ydieresis
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+00D0 Eth
+!D1 U+00D1 Ntilde
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+00DD Yacute
+!DE U+00DE Thorn
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+00F0 eth
+!F1 U+00F1 ntilde
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+00FD yacute
+!FE U+00FE thorn
+!FF U+00FF ydieresis
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1253.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1253.map
new file mode 100755
index 0000000000000000000000000000000000000000..4bd826fb2652c285e2d5ada788827e5d0085c31f
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1253.map
@@ -0,0 +1,239 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!83 U+0192 florin
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!89 U+2030 perthousand
+!8B U+2039 guilsinglleft
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!99 U+2122 trademark
+!9B U+203A guilsinglright
+!A0 U+00A0 space
+!A1 U+0385 dieresistonos
+!A2 U+0386 Alphatonos
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+2015 afii00208
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+0384 tonos
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+0388 Epsilontonos
+!B9 U+0389 Etatonos
+!BA U+038A Iotatonos
+!BB U+00BB guillemotright
+!BC U+038C Omicrontonos
+!BD U+00BD onehalf
+!BE U+038E Upsilontonos
+!BF U+038F Omegatonos
+!C0 U+0390 iotadieresistonos
+!C1 U+0391 Alpha
+!C2 U+0392 Beta
+!C3 U+0393 Gamma
+!C4 U+0394 Delta
+!C5 U+0395 Epsilon
+!C6 U+0396 Zeta
+!C7 U+0397 Eta
+!C8 U+0398 Theta
+!C9 U+0399 Iota
+!CA U+039A Kappa
+!CB U+039B Lambda
+!CC U+039C Mu
+!CD U+039D Nu
+!CE U+039E Xi
+!CF U+039F Omicron
+!D0 U+03A0 Pi
+!D1 U+03A1 Rho
+!D3 U+03A3 Sigma
+!D4 U+03A4 Tau
+!D5 U+03A5 Upsilon
+!D6 U+03A6 Phi
+!D7 U+03A7 Chi
+!D8 U+03A8 Psi
+!D9 U+03A9 Omega
+!DA U+03AA Iotadieresis
+!DB U+03AB Upsilondieresis
+!DC U+03AC alphatonos
+!DD U+03AD epsilontonos
+!DE U+03AE etatonos
+!DF U+03AF iotatonos
+!E0 U+03B0 upsilondieresistonos
+!E1 U+03B1 alpha
+!E2 U+03B2 beta
+!E3 U+03B3 gamma
+!E4 U+03B4 delta
+!E5 U+03B5 epsilon
+!E6 U+03B6 zeta
+!E7 U+03B7 eta
+!E8 U+03B8 theta
+!E9 U+03B9 iota
+!EA U+03BA kappa
+!EB U+03BB lambda
+!EC U+03BC mu
+!ED U+03BD nu
+!EE U+03BE xi
+!EF U+03BF omicron
+!F0 U+03C0 pi
+!F1 U+03C1 rho
+!F2 U+03C2 sigma1
+!F3 U+03C3 sigma
+!F4 U+03C4 tau
+!F5 U+03C5 upsilon
+!F6 U+03C6 phi
+!F7 U+03C7 chi
+!F8 U+03C8 psi
+!F9 U+03C9 omega
+!FA U+03CA iotadieresis
+!FB U+03CB upsilondieresis
+!FC U+03CC omicrontonos
+!FD U+03CD upsilontonos
+!FE U+03CE omegatonos
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1254.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1254.map
new file mode 100755
index 0000000000000000000000000000000000000000..829473b28c5e53c7f89c68808151f7e45d5dc89e
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1254.map
@@ -0,0 +1,249 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!83 U+0192 florin
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!88 U+02C6 circumflex
+!89 U+2030 perthousand
+!8A U+0160 Scaron
+!8B U+2039 guilsinglleft
+!8C U+0152 OE
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!98 U+02DC tilde
+!99 U+2122 trademark
+!9A U+0161 scaron
+!9B U+203A guilsinglright
+!9C U+0153 oe
+!9F U+0178 Ydieresis
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+011E Gbreve
+!D1 U+00D1 Ntilde
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+0130 Idotaccent
+!DE U+015E Scedilla
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+011F gbreve
+!F1 U+00F1 ntilde
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+0131 dotlessi
+!FE U+015F scedilla
+!FF U+00FF ydieresis
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1255.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1255.map
new file mode 100755
index 0000000000000000000000000000000000000000..079e10c61cd8e6360bb266cd95cca7672d3872f0
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1255.map
@@ -0,0 +1,233 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!83 U+0192 florin
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!88 U+02C6 circumflex
+!89 U+2030 perthousand
+!8B U+2039 guilsinglleft
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!98 U+02DC tilde
+!99 U+2122 trademark
+!9B U+203A guilsinglright
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+20AA afii57636
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00D7 multiply
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD sfthyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 middot
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00F7 divide
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+05B0 afii57799
+!C1 U+05B1 afii57801
+!C2 U+05B2 afii57800
+!C3 U+05B3 afii57802
+!C4 U+05B4 afii57793
+!C5 U+05B5 afii57794
+!C6 U+05B6 afii57795
+!C7 U+05B7 afii57798
+!C8 U+05B8 afii57797
+!C9 U+05B9 afii57806
+!CB U+05BB afii57796
+!CC U+05BC afii57807
+!CD U+05BD afii57839
+!CE U+05BE afii57645
+!CF U+05BF afii57841
+!D0 U+05C0 afii57842
+!D1 U+05C1 afii57804
+!D2 U+05C2 afii57803
+!D3 U+05C3 afii57658
+!D4 U+05F0 afii57716
+!D5 U+05F1 afii57717
+!D6 U+05F2 afii57718
+!D7 U+05F3 gereshhebrew
+!D8 U+05F4 gershayimhebrew
+!E0 U+05D0 afii57664
+!E1 U+05D1 afii57665
+!E2 U+05D2 afii57666
+!E3 U+05D3 afii57667
+!E4 U+05D4 afii57668
+!E5 U+05D5 afii57669
+!E6 U+05D6 afii57670
+!E7 U+05D7 afii57671
+!E8 U+05D8 afii57672
+!E9 U+05D9 afii57673
+!EA U+05DA afii57674
+!EB U+05DB afii57675
+!EC U+05DC afii57676
+!ED U+05DD afii57677
+!EE U+05DE afii57678
+!EF U+05DF afii57679
+!F0 U+05E0 afii57680
+!F1 U+05E1 afii57681
+!F2 U+05E2 afii57682
+!F3 U+05E3 afii57683
+!F4 U+05E4 afii57684
+!F5 U+05E5 afii57685
+!F6 U+05E6 afii57686
+!F7 U+05E7 afii57687
+!F8 U+05E8 afii57688
+!F9 U+05E9 afii57689
+!FA U+05EA afii57690
+!FD U+200E afii299
+!FE U+200F afii300
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1257.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1257.map
new file mode 100755
index 0000000000000000000000000000000000000000..2f2ecfa21dabe90c8cfa15e1738f2cd3c149d2a2
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1257.map
@@ -0,0 +1,244 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!89 U+2030 perthousand
+!8B U+2039 guilsinglleft
+!8D U+00A8 dieresis
+!8E U+02C7 caron
+!8F U+00B8 cedilla
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!99 U+2122 trademark
+!9B U+203A guilsinglright
+!9D U+00AF macron
+!9E U+02DB ogonek
+!A0 U+00A0 space
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00D8 Oslash
+!A9 U+00A9 copyright
+!AA U+0156 Rcommaaccent
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00C6 AE
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00F8 oslash
+!B9 U+00B9 onesuperior
+!BA U+0157 rcommaaccent
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00E6 ae
+!C0 U+0104 Aogonek
+!C1 U+012E Iogonek
+!C2 U+0100 Amacron
+!C3 U+0106 Cacute
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+0118 Eogonek
+!C7 U+0112 Emacron
+!C8 U+010C Ccaron
+!C9 U+00C9 Eacute
+!CA U+0179 Zacute
+!CB U+0116 Edotaccent
+!CC U+0122 Gcommaaccent
+!CD U+0136 Kcommaaccent
+!CE U+012A Imacron
+!CF U+013B Lcommaaccent
+!D0 U+0160 Scaron
+!D1 U+0143 Nacute
+!D2 U+0145 Ncommaaccent
+!D3 U+00D3 Oacute
+!D4 U+014C Omacron
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+0172 Uogonek
+!D9 U+0141 Lslash
+!DA U+015A Sacute
+!DB U+016A Umacron
+!DC U+00DC Udieresis
+!DD U+017B Zdotaccent
+!DE U+017D Zcaron
+!DF U+00DF germandbls
+!E0 U+0105 aogonek
+!E1 U+012F iogonek
+!E2 U+0101 amacron
+!E3 U+0107 cacute
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+0119 eogonek
+!E7 U+0113 emacron
+!E8 U+010D ccaron
+!E9 U+00E9 eacute
+!EA U+017A zacute
+!EB U+0117 edotaccent
+!EC U+0123 gcommaaccent
+!ED U+0137 kcommaaccent
+!EE U+012B imacron
+!EF U+013C lcommaaccent
+!F0 U+0161 scaron
+!F1 U+0144 nacute
+!F2 U+0146 ncommaaccent
+!F3 U+00F3 oacute
+!F4 U+014D omacron
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+0173 uogonek
+!F9 U+0142 lslash
+!FA U+015B sacute
+!FB U+016B umacron
+!FC U+00FC udieresis
+!FD U+017C zdotaccent
+!FE U+017E zcaron
+!FF U+02D9 dotaccent
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1258.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1258.map
new file mode 100755
index 0000000000000000000000000000000000000000..fed915f7152ca24e30fb33d1922de45177d84428
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp1258.map
@@ -0,0 +1,247 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!83 U+0192 florin
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!88 U+02C6 circumflex
+!89 U+2030 perthousand
+!8B U+2039 guilsinglleft
+!8C U+0152 OE
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!98 U+02DC tilde
+!99 U+2122 trademark
+!9B U+203A guilsinglright
+!9C U+0153 oe
+!9F U+0178 Ydieresis
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+0102 Abreve
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+0300 gravecomb
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+0110 Dcroat
+!D1 U+00D1 Ntilde
+!D2 U+0309 hookabovecomb
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+01A0 Ohorn
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+01AF Uhorn
+!DE U+0303 tildecomb
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+0103 abreve
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+0301 acutecomb
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+0111 dcroat
+!F1 U+00F1 ntilde
+!F2 U+0323 dotbelowcomb
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+01A1 ohorn
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+01B0 uhorn
+!FE U+20AB dong
+!FF U+00FF ydieresis
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp874.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp874.map
new file mode 100755
index 0000000000000000000000000000000000000000..1006e6b17f2a9d3cbbd8fc4fadd1c944c562cc1c
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/cp874.map
@@ -0,0 +1,225 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!85 U+2026 ellipsis
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!A0 U+00A0 space
+!A1 U+0E01 kokaithai
+!A2 U+0E02 khokhaithai
+!A3 U+0E03 khokhuatthai
+!A4 U+0E04 khokhwaithai
+!A5 U+0E05 khokhonthai
+!A6 U+0E06 khorakhangthai
+!A7 U+0E07 ngonguthai
+!A8 U+0E08 chochanthai
+!A9 U+0E09 chochingthai
+!AA U+0E0A chochangthai
+!AB U+0E0B sosothai
+!AC U+0E0C chochoethai
+!AD U+0E0D yoyingthai
+!AE U+0E0E dochadathai
+!AF U+0E0F topatakthai
+!B0 U+0E10 thothanthai
+!B1 U+0E11 thonangmonthothai
+!B2 U+0E12 thophuthaothai
+!B3 U+0E13 nonenthai
+!B4 U+0E14 dodekthai
+!B5 U+0E15 totaothai
+!B6 U+0E16 thothungthai
+!B7 U+0E17 thothahanthai
+!B8 U+0E18 thothongthai
+!B9 U+0E19 nonuthai
+!BA U+0E1A bobaimaithai
+!BB U+0E1B poplathai
+!BC U+0E1C phophungthai
+!BD U+0E1D fofathai
+!BE U+0E1E phophanthai
+!BF U+0E1F fofanthai
+!C0 U+0E20 phosamphaothai
+!C1 U+0E21 momathai
+!C2 U+0E22 yoyakthai
+!C3 U+0E23 roruathai
+!C4 U+0E24 ruthai
+!C5 U+0E25 lolingthai
+!C6 U+0E26 luthai
+!C7 U+0E27 wowaenthai
+!C8 U+0E28 sosalathai
+!C9 U+0E29 sorusithai
+!CA U+0E2A sosuathai
+!CB U+0E2B hohipthai
+!CC U+0E2C lochulathai
+!CD U+0E2D oangthai
+!CE U+0E2E honokhukthai
+!CF U+0E2F paiyannoithai
+!D0 U+0E30 saraathai
+!D1 U+0E31 maihanakatthai
+!D2 U+0E32 saraaathai
+!D3 U+0E33 saraamthai
+!D4 U+0E34 saraithai
+!D5 U+0E35 saraiithai
+!D6 U+0E36 sarauethai
+!D7 U+0E37 saraueethai
+!D8 U+0E38 sarauthai
+!D9 U+0E39 sarauuthai
+!DA U+0E3A phinthuthai
+!DF U+0E3F bahtthai
+!E0 U+0E40 saraethai
+!E1 U+0E41 saraaethai
+!E2 U+0E42 saraothai
+!E3 U+0E43 saraaimaimuanthai
+!E4 U+0E44 saraaimaimalaithai
+!E5 U+0E45 lakkhangyaothai
+!E6 U+0E46 maiyamokthai
+!E7 U+0E47 maitaikhuthai
+!E8 U+0E48 maiekthai
+!E9 U+0E49 maithothai
+!EA U+0E4A maitrithai
+!EB U+0E4B maichattawathai
+!EC U+0E4C thanthakhatthai
+!ED U+0E4D nikhahitthai
+!EE U+0E4E yamakkanthai
+!EF U+0E4F fongmanthai
+!F0 U+0E50 zerothai
+!F1 U+0E51 onethai
+!F2 U+0E52 twothai
+!F3 U+0E53 threethai
+!F4 U+0E54 fourthai
+!F5 U+0E55 fivethai
+!F6 U+0E56 sixthai
+!F7 U+0E57 seventhai
+!F8 U+0E58 eightthai
+!F9 U+0E59 ninethai
+!FA U+0E5A angkhankhuthai
+!FB U+0E5B khomutthai
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/index.html b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/index.html
new file mode 100755
index 0000000000000000000000000000000000000000..3b408bc9ad4fe82ea86af7812b267cf64402b6df
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/index.html
@@ -0,0 +1 @@
+<HTML><BODY><br><br><br><center><img src="http://colloquegeii.gesi.asso.fr/colloque_2011/jdsm_diapo/images/diapo_p2/logo_zz11_diapo.png" alt="Colloque GEII Angoul�me 2011"></center></BODY></HTML>
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-1.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-1.map
new file mode 100755
index 0000000000000000000000000000000000000000..61740a38fa3faa456159466766a92581b976d565
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-1.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+00D0 Eth
+!D1 U+00D1 Ntilde
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+00DD Yacute
+!DE U+00DE Thorn
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+00F0 eth
+!F1 U+00F1 ntilde
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+00FD yacute
+!FE U+00FE thorn
+!FF U+00FF ydieresis
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-11.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-11.map
new file mode 100755
index 0000000000000000000000000000000000000000..91688120667161d4acf8066f456d67d31a2bc0d9
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-11.map
@@ -0,0 +1,248 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+0E01 kokaithai
+!A2 U+0E02 khokhaithai
+!A3 U+0E03 khokhuatthai
+!A4 U+0E04 khokhwaithai
+!A5 U+0E05 khokhonthai
+!A6 U+0E06 khorakhangthai
+!A7 U+0E07 ngonguthai
+!A8 U+0E08 chochanthai
+!A9 U+0E09 chochingthai
+!AA U+0E0A chochangthai
+!AB U+0E0B sosothai
+!AC U+0E0C chochoethai
+!AD U+0E0D yoyingthai
+!AE U+0E0E dochadathai
+!AF U+0E0F topatakthai
+!B0 U+0E10 thothanthai
+!B1 U+0E11 thonangmonthothai
+!B2 U+0E12 thophuthaothai
+!B3 U+0E13 nonenthai
+!B4 U+0E14 dodekthai
+!B5 U+0E15 totaothai
+!B6 U+0E16 thothungthai
+!B7 U+0E17 thothahanthai
+!B8 U+0E18 thothongthai
+!B9 U+0E19 nonuthai
+!BA U+0E1A bobaimaithai
+!BB U+0E1B poplathai
+!BC U+0E1C phophungthai
+!BD U+0E1D fofathai
+!BE U+0E1E phophanthai
+!BF U+0E1F fofanthai
+!C0 U+0E20 phosamphaothai
+!C1 U+0E21 momathai
+!C2 U+0E22 yoyakthai
+!C3 U+0E23 roruathai
+!C4 U+0E24 ruthai
+!C5 U+0E25 lolingthai
+!C6 U+0E26 luthai
+!C7 U+0E27 wowaenthai
+!C8 U+0E28 sosalathai
+!C9 U+0E29 sorusithai
+!CA U+0E2A sosuathai
+!CB U+0E2B hohipthai
+!CC U+0E2C lochulathai
+!CD U+0E2D oangthai
+!CE U+0E2E honokhukthai
+!CF U+0E2F paiyannoithai
+!D0 U+0E30 saraathai
+!D1 U+0E31 maihanakatthai
+!D2 U+0E32 saraaathai
+!D3 U+0E33 saraamthai
+!D4 U+0E34 saraithai
+!D5 U+0E35 saraiithai
+!D6 U+0E36 sarauethai
+!D7 U+0E37 saraueethai
+!D8 U+0E38 sarauthai
+!D9 U+0E39 sarauuthai
+!DA U+0E3A phinthuthai
+!DF U+0E3F bahtthai
+!E0 U+0E40 saraethai
+!E1 U+0E41 saraaethai
+!E2 U+0E42 saraothai
+!E3 U+0E43 saraaimaimuanthai
+!E4 U+0E44 saraaimaimalaithai
+!E5 U+0E45 lakkhangyaothai
+!E6 U+0E46 maiyamokthai
+!E7 U+0E47 maitaikhuthai
+!E8 U+0E48 maiekthai
+!E9 U+0E49 maithothai
+!EA U+0E4A maitrithai
+!EB U+0E4B maichattawathai
+!EC U+0E4C thanthakhatthai
+!ED U+0E4D nikhahitthai
+!EE U+0E4E yamakkanthai
+!EF U+0E4F fongmanthai
+!F0 U+0E50 zerothai
+!F1 U+0E51 onethai
+!F2 U+0E52 twothai
+!F3 U+0E53 threethai
+!F4 U+0E54 fourthai
+!F5 U+0E55 fivethai
+!F6 U+0E56 sixthai
+!F7 U+0E57 seventhai
+!F8 U+0E58 eightthai
+!F9 U+0E59 ninethai
+!FA U+0E5A angkhankhuthai
+!FB U+0E5B khomutthai
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-15.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-15.map
new file mode 100755
index 0000000000000000000000000000000000000000..6c2b5712793d7eed6fec0f72e80ee3cd2ccf79ea
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-15.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+20AC Euro
+!A5 U+00A5 yen
+!A6 U+0160 Scaron
+!A7 U+00A7 section
+!A8 U+0161 scaron
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+017D Zcaron
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+017E zcaron
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+0152 OE
+!BD U+0153 oe
+!BE U+0178 Ydieresis
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+00D0 Eth
+!D1 U+00D1 Ntilde
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+00DD Yacute
+!DE U+00DE Thorn
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+00F0 eth
+!F1 U+00F1 ntilde
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+00FD yacute
+!FE U+00FE thorn
+!FF U+00FF ydieresis
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-16.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-16.map
new file mode 100755
index 0000000000000000000000000000000000000000..202c8fe594186cf762126b1265d7e2f73f7f92ac
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-16.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+0104 Aogonek
+!A2 U+0105 aogonek
+!A3 U+0141 Lslash
+!A4 U+20AC Euro
+!A5 U+201E quotedblbase
+!A6 U+0160 Scaron
+!A7 U+00A7 section
+!A8 U+0161 scaron
+!A9 U+00A9 copyright
+!AA U+0218 Scommaaccent
+!AB U+00AB guillemotleft
+!AC U+0179 Zacute
+!AD U+00AD hyphen
+!AE U+017A zacute
+!AF U+017B Zdotaccent
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+010C Ccaron
+!B3 U+0142 lslash
+!B4 U+017D Zcaron
+!B5 U+201D quotedblright
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+017E zcaron
+!B9 U+010D ccaron
+!BA U+0219 scommaaccent
+!BB U+00BB guillemotright
+!BC U+0152 OE
+!BD U+0153 oe
+!BE U+0178 Ydieresis
+!BF U+017C zdotaccent
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+0102 Abreve
+!C4 U+00C4 Adieresis
+!C5 U+0106 Cacute
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+0110 Dcroat
+!D1 U+0143 Nacute
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+0150 Ohungarumlaut
+!D6 U+00D6 Odieresis
+!D7 U+015A Sacute
+!D8 U+0170 Uhungarumlaut
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+0118 Eogonek
+!DE U+021A Tcommaaccent
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+0103 abreve
+!E4 U+00E4 adieresis
+!E5 U+0107 cacute
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+0111 dcroat
+!F1 U+0144 nacute
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+0151 ohungarumlaut
+!F6 U+00F6 odieresis
+!F7 U+015B sacute
+!F8 U+0171 uhungarumlaut
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+0119 eogonek
+!FE U+021B tcommaaccent
+!FF U+00FF ydieresis
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-2.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-2.map
new file mode 100755
index 0000000000000000000000000000000000000000..65ae09f95819ca5841b87ffe81e0e9326318cd75
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-2.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+0104 Aogonek
+!A2 U+02D8 breve
+!A3 U+0141 Lslash
+!A4 U+00A4 currency
+!A5 U+013D Lcaron
+!A6 U+015A Sacute
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+0160 Scaron
+!AA U+015E Scedilla
+!AB U+0164 Tcaron
+!AC U+0179 Zacute
+!AD U+00AD hyphen
+!AE U+017D Zcaron
+!AF U+017B Zdotaccent
+!B0 U+00B0 degree
+!B1 U+0105 aogonek
+!B2 U+02DB ogonek
+!B3 U+0142 lslash
+!B4 U+00B4 acute
+!B5 U+013E lcaron
+!B6 U+015B sacute
+!B7 U+02C7 caron
+!B8 U+00B8 cedilla
+!B9 U+0161 scaron
+!BA U+015F scedilla
+!BB U+0165 tcaron
+!BC U+017A zacute
+!BD U+02DD hungarumlaut
+!BE U+017E zcaron
+!BF U+017C zdotaccent
+!C0 U+0154 Racute
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+0102 Abreve
+!C4 U+00C4 Adieresis
+!C5 U+0139 Lacute
+!C6 U+0106 Cacute
+!C7 U+00C7 Ccedilla
+!C8 U+010C Ccaron
+!C9 U+00C9 Eacute
+!CA U+0118 Eogonek
+!CB U+00CB Edieresis
+!CC U+011A Ecaron
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+010E Dcaron
+!D0 U+0110 Dcroat
+!D1 U+0143 Nacute
+!D2 U+0147 Ncaron
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+0150 Ohungarumlaut
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+0158 Rcaron
+!D9 U+016E Uring
+!DA U+00DA Uacute
+!DB U+0170 Uhungarumlaut
+!DC U+00DC Udieresis
+!DD U+00DD Yacute
+!DE U+0162 Tcommaaccent
+!DF U+00DF germandbls
+!E0 U+0155 racute
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+0103 abreve
+!E4 U+00E4 adieresis
+!E5 U+013A lacute
+!E6 U+0107 cacute
+!E7 U+00E7 ccedilla
+!E8 U+010D ccaron
+!E9 U+00E9 eacute
+!EA U+0119 eogonek
+!EB U+00EB edieresis
+!EC U+011B ecaron
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+010F dcaron
+!F0 U+0111 dcroat
+!F1 U+0144 nacute
+!F2 U+0148 ncaron
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+0151 ohungarumlaut
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+0159 rcaron
+!F9 U+016F uring
+!FA U+00FA uacute
+!FB U+0171 uhungarumlaut
+!FC U+00FC udieresis
+!FD U+00FD yacute
+!FE U+0163 tcommaaccent
+!FF U+02D9 dotaccent
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-4.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-4.map
new file mode 100755
index 0000000000000000000000000000000000000000..a7d87bf3ef2a97e84de2aa4e1b46c4dbb9fec239
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-4.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+0104 Aogonek
+!A2 U+0138 kgreenlandic
+!A3 U+0156 Rcommaaccent
+!A4 U+00A4 currency
+!A5 U+0128 Itilde
+!A6 U+013B Lcommaaccent
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+0160 Scaron
+!AA U+0112 Emacron
+!AB U+0122 Gcommaaccent
+!AC U+0166 Tbar
+!AD U+00AD hyphen
+!AE U+017D Zcaron
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+0105 aogonek
+!B2 U+02DB ogonek
+!B3 U+0157 rcommaaccent
+!B4 U+00B4 acute
+!B5 U+0129 itilde
+!B6 U+013C lcommaaccent
+!B7 U+02C7 caron
+!B8 U+00B8 cedilla
+!B9 U+0161 scaron
+!BA U+0113 emacron
+!BB U+0123 gcommaaccent
+!BC U+0167 tbar
+!BD U+014A Eng
+!BE U+017E zcaron
+!BF U+014B eng
+!C0 U+0100 Amacron
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+012E Iogonek
+!C8 U+010C Ccaron
+!C9 U+00C9 Eacute
+!CA U+0118 Eogonek
+!CB U+00CB Edieresis
+!CC U+0116 Edotaccent
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+012A Imacron
+!D0 U+0110 Dcroat
+!D1 U+0145 Ncommaaccent
+!D2 U+014C Omacron
+!D3 U+0136 Kcommaaccent
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+0172 Uogonek
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+0168 Utilde
+!DE U+016A Umacron
+!DF U+00DF germandbls
+!E0 U+0101 amacron
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+012F iogonek
+!E8 U+010D ccaron
+!E9 U+00E9 eacute
+!EA U+0119 eogonek
+!EB U+00EB edieresis
+!EC U+0117 edotaccent
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+012B imacron
+!F0 U+0111 dcroat
+!F1 U+0146 ncommaaccent
+!F2 U+014D omacron
+!F3 U+0137 kcommaaccent
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+0173 uogonek
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+0169 utilde
+!FE U+016B umacron
+!FF U+02D9 dotaccent
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-5.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-5.map
new file mode 100755
index 0000000000000000000000000000000000000000..f9cd4edcf85de8e6206ff0ad32d64356101ce723
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-5.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+0401 afii10023
+!A2 U+0402 afii10051
+!A3 U+0403 afii10052
+!A4 U+0404 afii10053
+!A5 U+0405 afii10054
+!A6 U+0406 afii10055
+!A7 U+0407 afii10056
+!A8 U+0408 afii10057
+!A9 U+0409 afii10058
+!AA U+040A afii10059
+!AB U+040B afii10060
+!AC U+040C afii10061
+!AD U+00AD hyphen
+!AE U+040E afii10062
+!AF U+040F afii10145
+!B0 U+0410 afii10017
+!B1 U+0411 afii10018
+!B2 U+0412 afii10019
+!B3 U+0413 afii10020
+!B4 U+0414 afii10021
+!B5 U+0415 afii10022
+!B6 U+0416 afii10024
+!B7 U+0417 afii10025
+!B8 U+0418 afii10026
+!B9 U+0419 afii10027
+!BA U+041A afii10028
+!BB U+041B afii10029
+!BC U+041C afii10030
+!BD U+041D afii10031
+!BE U+041E afii10032
+!BF U+041F afii10033
+!C0 U+0420 afii10034
+!C1 U+0421 afii10035
+!C2 U+0422 afii10036
+!C3 U+0423 afii10037
+!C4 U+0424 afii10038
+!C5 U+0425 afii10039
+!C6 U+0426 afii10040
+!C7 U+0427 afii10041
+!C8 U+0428 afii10042
+!C9 U+0429 afii10043
+!CA U+042A afii10044
+!CB U+042B afii10045
+!CC U+042C afii10046
+!CD U+042D afii10047
+!CE U+042E afii10048
+!CF U+042F afii10049
+!D0 U+0430 afii10065
+!D1 U+0431 afii10066
+!D2 U+0432 afii10067
+!D3 U+0433 afii10068
+!D4 U+0434 afii10069
+!D5 U+0435 afii10070
+!D6 U+0436 afii10072
+!D7 U+0437 afii10073
+!D8 U+0438 afii10074
+!D9 U+0439 afii10075
+!DA U+043A afii10076
+!DB U+043B afii10077
+!DC U+043C afii10078
+!DD U+043D afii10079
+!DE U+043E afii10080
+!DF U+043F afii10081
+!E0 U+0440 afii10082
+!E1 U+0441 afii10083
+!E2 U+0442 afii10084
+!E3 U+0443 afii10085
+!E4 U+0444 afii10086
+!E5 U+0445 afii10087
+!E6 U+0446 afii10088
+!E7 U+0447 afii10089
+!E8 U+0448 afii10090
+!E9 U+0449 afii10091
+!EA U+044A afii10092
+!EB U+044B afii10093
+!EC U+044C afii10094
+!ED U+044D afii10095
+!EE U+044E afii10096
+!EF U+044F afii10097
+!F0 U+2116 afii61352
+!F1 U+0451 afii10071
+!F2 U+0452 afii10099
+!F3 U+0453 afii10100
+!F4 U+0454 afii10101
+!F5 U+0455 afii10102
+!F6 U+0456 afii10103
+!F7 U+0457 afii10104
+!F8 U+0458 afii10105
+!F9 U+0459 afii10106
+!FA U+045A afii10107
+!FB U+045B afii10108
+!FC U+045C afii10109
+!FD U+00A7 section
+!FE U+045E afii10110
+!FF U+045F afii10193
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-7.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-7.map
new file mode 100755
index 0000000000000000000000000000000000000000..e163796b1cad3004dc8f80315217c838a6df77aa
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-7.map
@@ -0,0 +1,250 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+2018 quoteleft
+!A2 U+2019 quoteright
+!A3 U+00A3 sterling
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AF U+2015 afii00208
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+0384 tonos
+!B5 U+0385 dieresistonos
+!B6 U+0386 Alphatonos
+!B7 U+00B7 periodcentered
+!B8 U+0388 Epsilontonos
+!B9 U+0389 Etatonos
+!BA U+038A Iotatonos
+!BB U+00BB guillemotright
+!BC U+038C Omicrontonos
+!BD U+00BD onehalf
+!BE U+038E Upsilontonos
+!BF U+038F Omegatonos
+!C0 U+0390 iotadieresistonos
+!C1 U+0391 Alpha
+!C2 U+0392 Beta
+!C3 U+0393 Gamma
+!C4 U+0394 Delta
+!C5 U+0395 Epsilon
+!C6 U+0396 Zeta
+!C7 U+0397 Eta
+!C8 U+0398 Theta
+!C9 U+0399 Iota
+!CA U+039A Kappa
+!CB U+039B Lambda
+!CC U+039C Mu
+!CD U+039D Nu
+!CE U+039E Xi
+!CF U+039F Omicron
+!D0 U+03A0 Pi
+!D1 U+03A1 Rho
+!D3 U+03A3 Sigma
+!D4 U+03A4 Tau
+!D5 U+03A5 Upsilon
+!D6 U+03A6 Phi
+!D7 U+03A7 Chi
+!D8 U+03A8 Psi
+!D9 U+03A9 Omega
+!DA U+03AA Iotadieresis
+!DB U+03AB Upsilondieresis
+!DC U+03AC alphatonos
+!DD U+03AD epsilontonos
+!DE U+03AE etatonos
+!DF U+03AF iotatonos
+!E0 U+03B0 upsilondieresistonos
+!E1 U+03B1 alpha
+!E2 U+03B2 beta
+!E3 U+03B3 gamma
+!E4 U+03B4 delta
+!E5 U+03B5 epsilon
+!E6 U+03B6 zeta
+!E7 U+03B7 eta
+!E8 U+03B8 theta
+!E9 U+03B9 iota
+!EA U+03BA kappa
+!EB U+03BB lambda
+!EC U+03BC mu
+!ED U+03BD nu
+!EE U+03BE xi
+!EF U+03BF omicron
+!F0 U+03C0 pi
+!F1 U+03C1 rho
+!F2 U+03C2 sigma1
+!F3 U+03C3 sigma
+!F4 U+03C4 tau
+!F5 U+03C5 upsilon
+!F6 U+03C6 phi
+!F7 U+03C7 chi
+!F8 U+03C8 psi
+!F9 U+03C9 omega
+!FA U+03CA iotadieresis
+!FB U+03CB upsilondieresis
+!FC U+03CC omicrontonos
+!FD U+03CD upsilontonos
+!FE U+03CE omegatonos
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-9.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-9.map
new file mode 100755
index 0000000000000000000000000000000000000000..48c123ae6f6b6bee1186517e7d6557fb2fee8055
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/iso-8859-9.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+011E Gbreve
+!D1 U+00D1 Ntilde
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+0130 Idotaccent
+!DE U+015E Scedilla
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+011F gbreve
+!F1 U+00F1 ntilde
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+0131 dotlessi
+!FE U+015F scedilla
+!FF U+00FF ydieresis
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/koi8-r.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/koi8-r.map
new file mode 100755
index 0000000000000000000000000000000000000000..6ad5d05d0dacf74138044384c23f319f830482ae
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/koi8-r.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+2500 SF100000
+!81 U+2502 SF110000
+!82 U+250C SF010000
+!83 U+2510 SF030000
+!84 U+2514 SF020000
+!85 U+2518 SF040000
+!86 U+251C SF080000
+!87 U+2524 SF090000
+!88 U+252C SF060000
+!89 U+2534 SF070000
+!8A U+253C SF050000
+!8B U+2580 upblock
+!8C U+2584 dnblock
+!8D U+2588 block
+!8E U+258C lfblock
+!8F U+2590 rtblock
+!90 U+2591 ltshade
+!91 U+2592 shade
+!92 U+2593 dkshade
+!93 U+2320 integraltp
+!94 U+25A0 filledbox
+!95 U+2219 periodcentered
+!96 U+221A radical
+!97 U+2248 approxequal
+!98 U+2264 lessequal
+!99 U+2265 greaterequal
+!9A U+00A0 space
+!9B U+2321 integralbt
+!9C U+00B0 degree
+!9D U+00B2 twosuperior
+!9E U+00B7 periodcentered
+!9F U+00F7 divide
+!A0 U+2550 SF430000
+!A1 U+2551 SF240000
+!A2 U+2552 SF510000
+!A3 U+0451 afii10071
+!A4 U+2553 SF520000
+!A5 U+2554 SF390000
+!A6 U+2555 SF220000
+!A7 U+2556 SF210000
+!A8 U+2557 SF250000
+!A9 U+2558 SF500000
+!AA U+2559 SF490000
+!AB U+255A SF380000
+!AC U+255B SF280000
+!AD U+255C SF270000
+!AE U+255D SF260000
+!AF U+255E SF360000
+!B0 U+255F SF370000
+!B1 U+2560 SF420000
+!B2 U+2561 SF190000
+!B3 U+0401 afii10023
+!B4 U+2562 SF200000
+!B5 U+2563 SF230000
+!B6 U+2564 SF470000
+!B7 U+2565 SF480000
+!B8 U+2566 SF410000
+!B9 U+2567 SF450000
+!BA U+2568 SF460000
+!BB U+2569 SF400000
+!BC U+256A SF540000
+!BD U+256B SF530000
+!BE U+256C SF440000
+!BF U+00A9 copyright
+!C0 U+044E afii10096
+!C1 U+0430 afii10065
+!C2 U+0431 afii10066
+!C3 U+0446 afii10088
+!C4 U+0434 afii10069
+!C5 U+0435 afii10070
+!C6 U+0444 afii10086
+!C7 U+0433 afii10068
+!C8 U+0445 afii10087
+!C9 U+0438 afii10074
+!CA U+0439 afii10075
+!CB U+043A afii10076
+!CC U+043B afii10077
+!CD U+043C afii10078
+!CE U+043D afii10079
+!CF U+043E afii10080
+!D0 U+043F afii10081
+!D1 U+044F afii10097
+!D2 U+0440 afii10082
+!D3 U+0441 afii10083
+!D4 U+0442 afii10084
+!D5 U+0443 afii10085
+!D6 U+0436 afii10072
+!D7 U+0432 afii10067
+!D8 U+044C afii10094
+!D9 U+044B afii10093
+!DA U+0437 afii10073
+!DB U+0448 afii10090
+!DC U+044D afii10095
+!DD U+0449 afii10091
+!DE U+0447 afii10089
+!DF U+044A afii10092
+!E0 U+042E afii10048
+!E1 U+0410 afii10017
+!E2 U+0411 afii10018
+!E3 U+0426 afii10040
+!E4 U+0414 afii10021
+!E5 U+0415 afii10022
+!E6 U+0424 afii10038
+!E7 U+0413 afii10020
+!E8 U+0425 afii10039
+!E9 U+0418 afii10026
+!EA U+0419 afii10027
+!EB U+041A afii10028
+!EC U+041B afii10029
+!ED U+041C afii10030
+!EE U+041D afii10031
+!EF U+041E afii10032
+!F0 U+041F afii10033
+!F1 U+042F afii10049
+!F2 U+0420 afii10034
+!F3 U+0421 afii10035
+!F4 U+0422 afii10036
+!F5 U+0423 afii10037
+!F6 U+0416 afii10024
+!F7 U+0412 afii10019
+!F8 U+042C afii10046
+!F9 U+042B afii10045
+!FA U+0417 afii10025
+!FB U+0428 afii10042
+!FC U+042D afii10047
+!FD U+0429 afii10043
+!FE U+0427 afii10041
+!FF U+042A afii10044
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/koi8-u.map b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/koi8-u.map
new file mode 100755
index 0000000000000000000000000000000000000000..40a7e4fd7e52a0433e42b5502cf4d9a23cf11e2e
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/koi8-u.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+2500 SF100000
+!81 U+2502 SF110000
+!82 U+250C SF010000
+!83 U+2510 SF030000
+!84 U+2514 SF020000
+!85 U+2518 SF040000
+!86 U+251C SF080000
+!87 U+2524 SF090000
+!88 U+252C SF060000
+!89 U+2534 SF070000
+!8A U+253C SF050000
+!8B U+2580 upblock
+!8C U+2584 dnblock
+!8D U+2588 block
+!8E U+258C lfblock
+!8F U+2590 rtblock
+!90 U+2591 ltshade
+!91 U+2592 shade
+!92 U+2593 dkshade
+!93 U+2320 integraltp
+!94 U+25A0 filledbox
+!95 U+2022 bullet
+!96 U+221A radical
+!97 U+2248 approxequal
+!98 U+2264 lessequal
+!99 U+2265 greaterequal
+!9A U+00A0 space
+!9B U+2321 integralbt
+!9C U+00B0 degree
+!9D U+00B2 twosuperior
+!9E U+00B7 periodcentered
+!9F U+00F7 divide
+!A0 U+2550 SF430000
+!A1 U+2551 SF240000
+!A2 U+2552 SF510000
+!A3 U+0451 afii10071
+!A4 U+0454 afii10101
+!A5 U+2554 SF390000
+!A6 U+0456 afii10103
+!A7 U+0457 afii10104
+!A8 U+2557 SF250000
+!A9 U+2558 SF500000
+!AA U+2559 SF490000
+!AB U+255A SF380000
+!AC U+255B SF280000
+!AD U+0491 afii10098
+!AE U+255D SF260000
+!AF U+255E SF360000
+!B0 U+255F SF370000
+!B1 U+2560 SF420000
+!B2 U+2561 SF190000
+!B3 U+0401 afii10023
+!B4 U+0404 afii10053
+!B5 U+2563 SF230000
+!B6 U+0406 afii10055
+!B7 U+0407 afii10056
+!B8 U+2566 SF410000
+!B9 U+2567 SF450000
+!BA U+2568 SF460000
+!BB U+2569 SF400000
+!BC U+256A SF540000
+!BD U+0490 afii10050
+!BE U+256C SF440000
+!BF U+00A9 copyright
+!C0 U+044E afii10096
+!C1 U+0430 afii10065
+!C2 U+0431 afii10066
+!C3 U+0446 afii10088
+!C4 U+0434 afii10069
+!C5 U+0435 afii10070
+!C6 U+0444 afii10086
+!C7 U+0433 afii10068
+!C8 U+0445 afii10087
+!C9 U+0438 afii10074
+!CA U+0439 afii10075
+!CB U+043A afii10076
+!CC U+043B afii10077
+!CD U+043C afii10078
+!CE U+043D afii10079
+!CF U+043E afii10080
+!D0 U+043F afii10081
+!D1 U+044F afii10097
+!D2 U+0440 afii10082
+!D3 U+0441 afii10083
+!D4 U+0442 afii10084
+!D5 U+0443 afii10085
+!D6 U+0436 afii10072
+!D7 U+0432 afii10067
+!D8 U+044C afii10094
+!D9 U+044B afii10093
+!DA U+0437 afii10073
+!DB U+0448 afii10090
+!DC U+044D afii10095
+!DD U+0449 afii10091
+!DE U+0447 afii10089
+!DF U+044A afii10092
+!E0 U+042E afii10048
+!E1 U+0410 afii10017
+!E2 U+0411 afii10018
+!E3 U+0426 afii10040
+!E4 U+0414 afii10021
+!E5 U+0415 afii10022
+!E6 U+0424 afii10038
+!E7 U+0413 afii10020
+!E8 U+0425 afii10039
+!E9 U+0418 afii10026
+!EA U+0419 afii10027
+!EB U+041A afii10028
+!EC U+041B afii10029
+!ED U+041C afii10030
+!EE U+041D afii10031
+!EF U+041E afii10032
+!F0 U+041F afii10033
+!F1 U+042F afii10049
+!F2 U+0420 afii10034
+!F3 U+0421 afii10035
+!F4 U+0422 afii10036
+!F5 U+0423 afii10037
+!F6 U+0416 afii10024
+!F7 U+0412 afii10019
+!F8 U+042C afii10046
+!F9 U+042B afii10045
+!FA U+0417 afii10025
+!FB U+0428 afii10042
+!FC U+042D afii10047
+!FD U+0429 afii10043
+!FE U+0427 afii10041
+!FF U+042A afii10044
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/makefont/makefont.php b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/makefont.php
new file mode 100755
index 0000000000000000000000000000000000000000..0baa933d174e86cc2725369359093d07141ec4e2
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/makefont/makefont.php
@@ -0,0 +1,419 @@
+<?php
+/*******************************************************************************
+* Utilitaire de g�n�ration de fichier de d�finition de police                  *
+*                                                                              *
+* Version : 1.14                                                               *
+* Date :    03/08/2008                                                         *
+* Auteur :  Olivier PLATHEY                                                    *
+*******************************************************************************/
+
+function ReadMap($enc)
+{
+	//Read a map file
+	$file=dirname(__FILE__).'/'.strtolower($enc).'.map';
+	$a=file($file);
+	if(empty($a))
+		die('<b>Error:</b> encoding not found: '.$enc);
+	$cc2gn=array();
+	foreach($a as $l)
+	{
+		if($l[0]=='!')
+		{
+			$e=preg_split('/[ \\t]+/',rtrim($l));
+			$cc=hexdec(substr($e[0],1));
+			$gn=$e[2];
+			$cc2gn[$cc]=$gn;
+		}
+	}
+	for($i=0;$i<=255;$i++)
+	{
+		if(!isset($cc2gn[$i]))
+			$cc2gn[$i]='.notdef';
+	}
+	return $cc2gn;
+}
+
+function ReadAFM($file, &$map)
+{
+	//Read a font metric file
+	$a=file($file);
+	if(empty($a))
+		die('File not found');
+	$widths=array();
+	$fm=array();
+	$fix=array('Edot'=>'Edotaccent','edot'=>'edotaccent','Idot'=>'Idotaccent','Zdot'=>'Zdotaccent','zdot'=>'zdotaccent',
+		'Odblacute'=>'Ohungarumlaut','odblacute'=>'ohungarumlaut','Udblacute'=>'Uhungarumlaut','udblacute'=>'uhungarumlaut',
+		'Gcedilla'=>'Gcommaaccent','gcedilla'=>'gcommaaccent','Kcedilla'=>'Kcommaaccent','kcedilla'=>'kcommaaccent',
+		'Lcedilla'=>'Lcommaaccent','lcedilla'=>'lcommaaccent','Ncedilla'=>'Ncommaaccent','ncedilla'=>'ncommaaccent',
+		'Rcedilla'=>'Rcommaaccent','rcedilla'=>'rcommaaccent','Scedilla'=>'Scommaaccent','scedilla'=>'scommaaccent',
+		'Tcedilla'=>'Tcommaaccent','tcedilla'=>'tcommaaccent','Dslash'=>'Dcroat','dslash'=>'dcroat','Dmacron'=>'Dcroat','dmacron'=>'dcroat',
+		'combininggraveaccent'=>'gravecomb','combininghookabove'=>'hookabovecomb','combiningtildeaccent'=>'tildecomb',
+		'combiningacuteaccent'=>'acutecomb','combiningdotbelow'=>'dotbelowcomb','dongsign'=>'dong');
+	foreach($a as $l)
+	{
+		$e=explode(' ',rtrim($l));
+		if(count($e)<2)
+			continue;
+		$code=$e[0];
+		$param=$e[1];
+		if($code=='C')
+		{
+			//Character metrics
+			$cc=(int)$e[1];
+			$w=$e[4];
+			$gn=$e[7];
+			if(substr($gn,-4)=='20AC')
+				$gn='Euro';
+			if(isset($fix[$gn]))
+			{
+				//Fix incorrect glyph name
+				foreach($map as $c=>$n)
+				{
+					if($n==$fix[$gn])
+						$map[$c]=$gn;
+				}
+			}
+			if(empty($map))
+			{
+				//Symbolic font: use built-in encoding
+				$widths[$cc]=$w;
+			}
+			else
+			{
+				$widths[$gn]=$w;
+				if($gn=='X')
+					$fm['CapXHeight']=$e[13];
+			}
+			if($gn=='.notdef')
+				$fm['MissingWidth']=$w;
+		}
+		elseif($code=='FontName')
+			$fm['FontName']=$param;
+		elseif($code=='Weight')
+			$fm['Weight']=$param;
+		elseif($code=='ItalicAngle')
+			$fm['ItalicAngle']=(double)$param;
+		elseif($code=='Ascender')
+			$fm['Ascender']=(int)$param;
+		elseif($code=='Descender')
+			$fm['Descender']=(int)$param;
+		elseif($code=='UnderlineThickness')
+			$fm['UnderlineThickness']=(int)$param;
+		elseif($code=='UnderlinePosition')
+			$fm['UnderlinePosition']=(int)$param;
+		elseif($code=='IsFixedPitch')
+			$fm['IsFixedPitch']=($param=='true');
+		elseif($code=='FontBBox')
+			$fm['FontBBox']=array($e[1],$e[2],$e[3],$e[4]);
+		elseif($code=='CapHeight')
+			$fm['CapHeight']=(int)$param;
+		elseif($code=='StdVW')
+			$fm['StdVW']=(int)$param;
+	}
+	if(!isset($fm['FontName']))
+		die('FontName not found');
+	if(!empty($map))
+	{
+		if(!isset($widths['.notdef']))
+			$widths['.notdef']=600;
+		if(!isset($widths['Delta']) && isset($widths['increment']))
+			$widths['Delta']=$widths['increment'];
+		//Order widths according to map
+		for($i=0;$i<=255;$i++)
+		{
+			if(!isset($widths[$map[$i]]))
+			{
+				echo '<b>Warning:</b> character '.$map[$i].' is missing<br>';
+				$widths[$i]=$widths['.notdef'];
+			}
+			else
+				$widths[$i]=$widths[$map[$i]];
+		}
+	}
+	$fm['Widths']=$widths;
+	return $fm;
+}
+
+function MakeFontDescriptor($fm, $symbolic)
+{
+	//Ascent
+	$asc=(isset($fm['Ascender']) ? $fm['Ascender'] : 1000);
+	$fd="array('Ascent'=>".$asc;
+	//Descent
+	$desc=(isset($fm['Descender']) ? $fm['Descender'] : -200);
+	$fd.=",'Descent'=>".$desc;
+	//CapHeight
+	if(isset($fm['CapHeight']))
+		$ch=$fm['CapHeight'];
+	elseif(isset($fm['CapXHeight']))
+		$ch=$fm['CapXHeight'];
+	else
+		$ch=$asc;
+	$fd.=",'CapHeight'=>".$ch;
+	//Flags
+	$flags=0;
+	if(isset($fm['IsFixedPitch']) && $fm['IsFixedPitch'])
+		$flags+=1<<0;
+	if($symbolic)
+		$flags+=1<<2;
+	if(!$symbolic)
+		$flags+=1<<5;
+	if(isset($fm['ItalicAngle']) && $fm['ItalicAngle']!=0)
+		$flags+=1<<6;
+	$fd.=",'Flags'=>".$flags;
+	//FontBBox
+	if(isset($fm['FontBBox']))
+		$fbb=$fm['FontBBox'];
+	else
+		$fbb=array(0,$desc-100,1000,$asc+100);
+	$fd.=",'FontBBox'=>'[".$fbb[0].' '.$fbb[1].' '.$fbb[2].' '.$fbb[3]."]'";
+	//ItalicAngle
+	$ia=(isset($fm['ItalicAngle']) ? $fm['ItalicAngle'] : 0);
+	$fd.=",'ItalicAngle'=>".$ia;
+	//StemV
+	if(isset($fm['StdVW']))
+		$stemv=$fm['StdVW'];
+	elseif(isset($fm['Weight']) && preg_match('/bold|black/i',$fm['Weight']))
+		$stemv=120;
+	else
+		$stemv=70;
+	$fd.=",'StemV'=>".$stemv;
+	//MissingWidth
+	if(isset($fm['MissingWidth']))
+		$fd.=",'MissingWidth'=>".$fm['MissingWidth'];
+	$fd.=')';
+	return $fd;
+}
+
+function MakeWidthArray($fm)
+{
+	//Make character width array
+	$s="array(\n\t";
+	$cw=$fm['Widths'];
+	for($i=0;$i<=255;$i++)
+	{
+		if(chr($i)=="'")
+			$s.="'\\''";
+		elseif(chr($i)=="\\")
+			$s.="'\\\\'";
+		elseif($i>=32 && $i<=126)
+			$s.="'".chr($i)."'";
+		else
+			$s.="chr($i)";
+		$s.='=>'.$fm['Widths'][$i];
+		if($i<255)
+			$s.=',';
+		if(($i+1)%22==0)
+			$s.="\n\t";
+	}
+	$s.=')';
+	return $s;
+}
+
+function MakeFontEncoding($map)
+{
+	//Build differences from reference encoding
+	$ref=ReadMap('cp1252');
+	$s='';
+	$last=0;
+	for($i=32;$i<=255;$i++)
+	{
+		if($map[$i]!=$ref[$i])
+		{
+			if($i!=$last+1)
+				$s.=$i.' ';
+			$last=$i;
+			$s.='/'.$map[$i].' ';
+		}
+	}
+	return rtrim($s);
+}
+
+function SaveToFile($file, $s, $mode)
+{
+	$f=fopen($file,'w'.$mode);
+	if(!$f)
+		die('Can\'t write to file '.$file);
+	fwrite($f,$s,strlen($s));
+	fclose($f);
+}
+
+function ReadShort($f)
+{
+	$a=unpack('n1n',fread($f,2));
+	return $a['n'];
+}
+
+function ReadLong($f)
+{
+	$a=unpack('N1N',fread($f,4));
+	return $a['N'];
+}
+
+function CheckTTF($file)
+{
+	//Check if font license allows embedding
+	$f=fopen($file,'rb');
+	if(!$f)
+		die('<b>Error:</b> Can\'t open '.$file);
+	//Extract number of tables
+	fseek($f,4,SEEK_CUR);
+	$nb=ReadShort($f);
+	fseek($f,6,SEEK_CUR);
+	//Seek OS/2 table
+	$found=false;
+	for($i=0;$i<$nb;$i++)
+	{
+		if(fread($f,4)=='OS/2')
+		{
+			$found=true;
+			break;
+		}
+		fseek($f,12,SEEK_CUR);
+	}
+	if(!$found)
+	{
+		fclose($f);
+		return;
+	}
+	fseek($f,4,SEEK_CUR);
+	$offset=ReadLong($f);
+	fseek($f,$offset,SEEK_SET);
+	//Extract fsType flags
+	fseek($f,8,SEEK_CUR);
+	$fsType=ReadShort($f);
+	$rl=($fsType & 0x02)!=0;
+	$pp=($fsType & 0x04)!=0;
+	$e=($fsType & 0x08)!=0;
+	fclose($f);
+	if($rl && !$pp && !$e)
+		echo '<b>Warning:</b> font license does not allow embedding';
+}
+
+/*******************************************************************************
+* fontfile : chemin du fichier TTF (ou cha�ne vide si pas d'incorporation)     *
+* afmfile :  chemin du fichier AFM                                             *
+* enc :      encodage (ou cha�ne vide si la police est symbolique)             *
+* patch :    patch optionnel pour l'encodage                                   *
+* type :     type de la police si fontfile est vide                            *
+*******************************************************************************/
+function MakeFont($fontfile, $afmfile, $enc='cp1252', $patch=array(), $type='TrueType')
+{
+	//Generate a font definition file
+	if(get_magic_quotes_runtime())
+		@set_magic_quotes_runtime(0);
+	ini_set('auto_detect_line_endings','1');
+	if($enc)
+	{
+		$map=ReadMap($enc);
+		foreach($patch as $cc=>$gn)
+			$map[$cc]=$gn;
+	}
+	else
+		$map=array();
+	if(!file_exists($afmfile))
+		die('<b>Error:</b> AFM file not found: '.$afmfile);
+	$fm=ReadAFM($afmfile,$map);
+	if($enc)
+		$diff=MakeFontEncoding($map);
+	else
+		$diff='';
+	$fd=MakeFontDescriptor($fm,empty($map));
+	//Find font type
+	if($fontfile)
+	{
+		$ext=strtolower(substr($fontfile,-3));
+		if($ext=='ttf')
+			$type='TrueType';
+		elseif($ext=='pfb')
+			$type='Type1';
+		else
+			die('<b>Error:</b> unrecognized font file extension: '.$ext);
+	}
+	else
+	{
+		if($type!='TrueType' && $type!='Type1')
+			die('<b>Error:</b> incorrect font type: '.$type);
+	}
+	//Start generation
+	$s='<?php'."\n";
+	$s.='$type=\''.$type."';\n";
+	$s.='$name=\''.$fm['FontName']."';\n";
+	$s.='$desc='.$fd.";\n";
+	if(!isset($fm['UnderlinePosition']))
+		$fm['UnderlinePosition']=-100;
+	if(!isset($fm['UnderlineThickness']))
+		$fm['UnderlineThickness']=50;
+	$s.='$up='.$fm['UnderlinePosition'].";\n";
+	$s.='$ut='.$fm['UnderlineThickness'].";\n";
+	$w=MakeWidthArray($fm);
+	$s.='$cw='.$w.";\n";
+	$s.='$enc=\''.$enc."';\n";
+	$s.='$diff=\''.$diff."';\n";
+	$basename=substr(basename($afmfile),0,-4);
+	if($fontfile)
+	{
+		//Embedded font
+		if(!file_exists($fontfile))
+			die('<b>Error:</b> font file not found: '.$fontfile);
+		if($type=='TrueType')
+			CheckTTF($fontfile);
+		$f=fopen($fontfile,'rb');
+		if(!$f)
+			die('<b>Error:</b> Can\'t open '.$fontfile);
+		$file=fread($f,filesize($fontfile));
+		fclose($f);
+		if($type=='Type1')
+		{
+			//Find first two sections and discard third one
+			$header=(ord($file[0])==128);
+			if($header)
+			{
+				//Strip first binary header
+				$file=substr($file,6);
+			}
+			$pos=strpos($file,'eexec');
+			if(!$pos)
+				die('<b>Error:</b> font file does not seem to be valid Type1');
+			$size1=$pos+6;
+			if($header && ord($file[$size1])==128)
+			{
+				//Strip second binary header
+				$file=substr($file,0,$size1).substr($file,$size1+6);
+			}
+			$pos=strpos($file,'00000000');
+			if(!$pos)
+				die('<b>Error:</b> font file does not seem to be valid Type1');
+			$size2=$pos-$size1;
+			$file=substr($file,0,$size1+$size2);
+		}
+		if(function_exists('gzcompress'))
+		{
+			$cmp=$basename.'.z';
+			SaveToFile($cmp,gzcompress($file),'b');
+			$s.='$file=\''.$cmp."';\n";
+			echo 'Font file compressed ('.$cmp.')<br>';
+		}
+		else
+		{
+			$s.='$file=\''.basename($fontfile)."';\n";
+			echo '<b>Notice:</b> font file could not be compressed (zlib extension not available)<br>';
+		}
+		if($type=='Type1')
+		{
+			$s.='$size1='.$size1.";\n";
+			$s.='$size2='.$size2.";\n";
+		}
+		else
+			$s.='$originalsize='.filesize($fontfile).";\n";
+	}
+	else
+	{
+		//Not embedded font
+		$s.='$file='."'';\n";
+	}
+	$s.="?>\n";
+	SaveToFile($basename.'.php',$s,'t');
+	echo 'Font definition file generated ('.$basename.'.php'.')<br>';
+}
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/symbol.php b/misc/PublicationBulletins/Portail-LeHavre/font/symbol.php
new file mode 100755
index 0000000000000000000000000000000000000000..43b50e4511f32eb8b3e5b2a5796cd5212f7e6c79
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/symbol.php
@@ -0,0 +1,15 @@
+<?php
+$fpdf_charwidths['symbol']=array(
+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,
+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>713,'#'=>500,'$'=>549,'%'=>833,'&'=>778,'\''=>439,'('=>333,')'=>333,'*'=>500,'+'=>549,
+	','=>250,'-'=>549,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>549,'='=>549,'>'=>549,'?'=>444,'@'=>549,'A'=>722,
+	'B'=>667,'C'=>722,'D'=>612,'E'=>611,'F'=>763,'G'=>603,'H'=>722,'I'=>333,'J'=>631,'K'=>722,'L'=>686,'M'=>889,'N'=>722,'O'=>722,'P'=>768,'Q'=>741,'R'=>556,'S'=>592,'T'=>611,'U'=>690,'V'=>439,'W'=>768,
+	'X'=>645,'Y'=>795,'Z'=>611,'['=>333,'\\'=>863,']'=>333,'^'=>658,'_'=>500,'`'=>500,'a'=>631,'b'=>549,'c'=>549,'d'=>494,'e'=>439,'f'=>521,'g'=>411,'h'=>603,'i'=>329,'j'=>603,'k'=>549,'l'=>549,'m'=>576,
+	'n'=>521,'o'=>549,'p'=>549,'q'=>521,'r'=>549,'s'=>603,'t'=>439,'u'=>576,'v'=>713,'w'=>686,'x'=>493,'y'=>686,'z'=>494,'{'=>480,'|'=>200,'}'=>480,'~'=>549,chr(127)=>0,chr(128)=>0,chr(129)=>0,chr(130)=>0,chr(131)=>0,
+	chr(132)=>0,chr(133)=>0,chr(134)=>0,chr(135)=>0,chr(136)=>0,chr(137)=>0,chr(138)=>0,chr(139)=>0,chr(140)=>0,chr(141)=>0,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0,
+	chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>750,chr(161)=>620,chr(162)=>247,chr(163)=>549,chr(164)=>167,chr(165)=>713,chr(166)=>500,chr(167)=>753,chr(168)=>753,chr(169)=>753,chr(170)=>753,chr(171)=>1042,chr(172)=>987,chr(173)=>603,chr(174)=>987,chr(175)=>603,
+	chr(176)=>400,chr(177)=>549,chr(178)=>411,chr(179)=>549,chr(180)=>549,chr(181)=>713,chr(182)=>494,chr(183)=>460,chr(184)=>549,chr(185)=>549,chr(186)=>549,chr(187)=>549,chr(188)=>1000,chr(189)=>603,chr(190)=>1000,chr(191)=>658,chr(192)=>823,chr(193)=>686,chr(194)=>795,chr(195)=>987,chr(196)=>768,chr(197)=>768,
+	chr(198)=>823,chr(199)=>768,chr(200)=>768,chr(201)=>713,chr(202)=>713,chr(203)=>713,chr(204)=>713,chr(205)=>713,chr(206)=>713,chr(207)=>713,chr(208)=>768,chr(209)=>713,chr(210)=>790,chr(211)=>790,chr(212)=>890,chr(213)=>823,chr(214)=>549,chr(215)=>250,chr(216)=>713,chr(217)=>603,chr(218)=>603,chr(219)=>1042,
+	chr(220)=>987,chr(221)=>603,chr(222)=>987,chr(223)=>603,chr(224)=>494,chr(225)=>329,chr(226)=>790,chr(227)=>790,chr(228)=>786,chr(229)=>713,chr(230)=>384,chr(231)=>384,chr(232)=>384,chr(233)=>384,chr(234)=>384,chr(235)=>384,chr(236)=>494,chr(237)=>494,chr(238)=>494,chr(239)=>494,chr(240)=>0,chr(241)=>329,
+	chr(242)=>274,chr(243)=>686,chr(244)=>686,chr(245)=>686,chr(246)=>384,chr(247)=>384,chr(248)=>384,chr(249)=>384,chr(250)=>384,chr(251)=>384,chr(252)=>494,chr(253)=>494,chr(254)=>494,chr(255)=>0);
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/times.php b/misc/PublicationBulletins/Portail-LeHavre/font/times.php
new file mode 100755
index 0000000000000000000000000000000000000000..837c706e08d966b1109bc6a7709bd4364a96b037
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/times.php
@@ -0,0 +1,15 @@
+<?php
+$fpdf_charwidths['times']=array(
+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,
+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>408,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>180,'('=>333,')'=>333,'*'=>500,'+'=>564,
+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>564,'='=>564,'>'=>564,'?'=>444,'@'=>921,'A'=>722,
+	'B'=>667,'C'=>667,'D'=>722,'E'=>611,'F'=>556,'G'=>722,'H'=>722,'I'=>333,'J'=>389,'K'=>722,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>556,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>722,'W'=>944,
+	'X'=>722,'Y'=>722,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>469,'_'=>500,'`'=>333,'a'=>444,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778,
+	'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>333,'s'=>389,'t'=>278,'u'=>500,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>480,'|'=>200,'}'=>480,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,
+	chr(132)=>444,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>889,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>444,chr(148)=>444,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>980,
+	chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>200,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>564,chr(173)=>333,chr(174)=>760,chr(175)=>333,
+	chr(176)=>400,chr(177)=>564,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>453,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>444,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,
+	chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>564,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722,
+	chr(220)=>722,chr(221)=>722,chr(222)=>556,chr(223)=>500,chr(224)=>444,chr(225)=>444,chr(226)=>444,chr(227)=>444,chr(228)=>444,chr(229)=>444,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500,
+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>564,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>500,chr(254)=>500,chr(255)=>500);
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/timesb.php b/misc/PublicationBulletins/Portail-LeHavre/font/timesb.php
new file mode 100755
index 0000000000000000000000000000000000000000..09cff86ac4a8c4ac25ce3bf8eadfb65f0ba39d78
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/timesb.php
@@ -0,0 +1,15 @@
+<?php
+$fpdf_charwidths['timesB']=array(
+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,
+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>555,'#'=>500,'$'=>500,'%'=>1000,'&'=>833,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570,
+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>930,'A'=>722,
+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>778,'I'=>389,'J'=>500,'K'=>778,'L'=>667,'M'=>944,'N'=>722,'O'=>778,'P'=>611,'Q'=>778,'R'=>722,'S'=>556,'T'=>667,'U'=>722,'V'=>722,'W'=>1000,
+	'X'=>722,'Y'=>722,'Z'=>667,'['=>333,'\\'=>278,']'=>333,'^'=>581,'_'=>500,'`'=>333,'a'=>500,'b'=>556,'c'=>444,'d'=>556,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>333,'k'=>556,'l'=>278,'m'=>833,
+	'n'=>556,'o'=>500,'p'=>556,'q'=>556,'r'=>444,'s'=>389,'t'=>333,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>394,'|'=>220,'}'=>394,'~'=>520,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,
+	chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>667,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,
+	chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>300,chr(171)=>500,chr(172)=>570,chr(173)=>333,chr(174)=>747,chr(175)=>333,
+	chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>556,chr(182)=>540,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>330,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,
+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>570,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,
+	chr(220)=>722,chr(221)=>722,chr(222)=>611,chr(223)=>556,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556,
+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500);
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/timesbi.php b/misc/PublicationBulletins/Portail-LeHavre/font/timesbi.php
new file mode 100755
index 0000000000000000000000000000000000000000..b4e38d763b5b780228ae0141b66fe58b88a9ac47
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/timesbi.php
@@ -0,0 +1,15 @@
+<?php
+$fpdf_charwidths['timesBI']=array(
+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,
+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>389,'"'=>555,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570,
+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>832,'A'=>667,
+	'B'=>667,'C'=>667,'D'=>722,'E'=>667,'F'=>667,'G'=>722,'H'=>778,'I'=>389,'J'=>500,'K'=>667,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>611,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>667,'W'=>889,
+	'X'=>667,'Y'=>611,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>570,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778,
+	'n'=>556,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>556,'v'=>444,'w'=>667,'x'=>500,'y'=>444,'z'=>389,'{'=>348,'|'=>220,'}'=>348,'~'=>570,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,
+	chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,
+	chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>389,chr(159)=>611,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>266,chr(171)=>500,chr(172)=>606,chr(173)=>333,chr(174)=>747,chr(175)=>333,
+	chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>576,chr(182)=>500,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>300,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667,
+	chr(198)=>944,chr(199)=>667,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>570,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722,
+	chr(220)=>722,chr(221)=>611,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556,
+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>444,chr(254)=>500,chr(255)=>444);
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/timesi.php b/misc/PublicationBulletins/Portail-LeHavre/font/timesi.php
new file mode 100755
index 0000000000000000000000000000000000000000..0ba2b7773d84511a1e5d4b3b82d6d697e43d9a7c
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/timesi.php
@@ -0,0 +1,15 @@
+<?php
+$fpdf_charwidths['timesI']=array(
+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,
+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>420,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>214,'('=>333,')'=>333,'*'=>500,'+'=>675,
+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>675,'='=>675,'>'=>675,'?'=>500,'@'=>920,'A'=>611,
+	'B'=>611,'C'=>667,'D'=>722,'E'=>611,'F'=>611,'G'=>722,'H'=>722,'I'=>333,'J'=>444,'K'=>667,'L'=>556,'M'=>833,'N'=>667,'O'=>722,'P'=>611,'Q'=>722,'R'=>611,'S'=>500,'T'=>556,'U'=>722,'V'=>611,'W'=>833,
+	'X'=>611,'Y'=>556,'Z'=>556,'['=>389,'\\'=>278,']'=>389,'^'=>422,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>278,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>444,'l'=>278,'m'=>722,
+	'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>500,'v'=>444,'w'=>667,'x'=>444,'y'=>444,'z'=>389,'{'=>400,'|'=>275,'}'=>400,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,
+	chr(132)=>556,chr(133)=>889,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>500,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>556,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>556,chr(148)=>556,chr(149)=>350,chr(150)=>500,chr(151)=>889,chr(152)=>333,chr(153)=>980,
+	chr(154)=>389,chr(155)=>333,chr(156)=>667,chr(157)=>350,chr(158)=>389,chr(159)=>556,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>275,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>675,chr(173)=>333,chr(174)=>760,chr(175)=>333,
+	chr(176)=>400,chr(177)=>675,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>523,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>611,chr(193)=>611,chr(194)=>611,chr(195)=>611,chr(196)=>611,chr(197)=>611,
+	chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>667,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>675,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722,
+	chr(220)=>722,chr(221)=>556,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500,
+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>675,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>444,chr(254)=>500,chr(255)=>444);
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/font/zapfdingbats.php b/misc/PublicationBulletins/Portail-LeHavre/font/zapfdingbats.php
new file mode 100755
index 0000000000000000000000000000000000000000..1f926a8c3f01ac8ad19d61e7e84cfceb5d4db727
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/font/zapfdingbats.php
@@ -0,0 +1,15 @@
+<?php
+$fpdf_charwidths['zapfdingbats']=array(
+	chr(0)=>0,chr(1)=>0,chr(2)=>0,chr(3)=>0,chr(4)=>0,chr(5)=>0,chr(6)=>0,chr(7)=>0,chr(8)=>0,chr(9)=>0,chr(10)=>0,chr(11)=>0,chr(12)=>0,chr(13)=>0,chr(14)=>0,chr(15)=>0,chr(16)=>0,chr(17)=>0,chr(18)=>0,chr(19)=>0,chr(20)=>0,chr(21)=>0,
+	chr(22)=>0,chr(23)=>0,chr(24)=>0,chr(25)=>0,chr(26)=>0,chr(27)=>0,chr(28)=>0,chr(29)=>0,chr(30)=>0,chr(31)=>0,' '=>278,'!'=>974,'"'=>961,'#'=>974,'$'=>980,'%'=>719,'&'=>789,'\''=>790,'('=>791,')'=>690,'*'=>960,'+'=>939,
+	','=>549,'-'=>855,'.'=>911,'/'=>933,'0'=>911,'1'=>945,'2'=>974,'3'=>755,'4'=>846,'5'=>762,'6'=>761,'7'=>571,'8'=>677,'9'=>763,':'=>760,';'=>759,'<'=>754,'='=>494,'>'=>552,'?'=>537,'@'=>577,'A'=>692,
+	'B'=>786,'C'=>788,'D'=>788,'E'=>790,'F'=>793,'G'=>794,'H'=>816,'I'=>823,'J'=>789,'K'=>841,'L'=>823,'M'=>833,'N'=>816,'O'=>831,'P'=>923,'Q'=>744,'R'=>723,'S'=>749,'T'=>790,'U'=>792,'V'=>695,'W'=>776,
+	'X'=>768,'Y'=>792,'Z'=>759,'['=>707,'\\'=>708,']'=>682,'^'=>701,'_'=>826,'`'=>815,'a'=>789,'b'=>789,'c'=>707,'d'=>687,'e'=>696,'f'=>689,'g'=>786,'h'=>787,'i'=>713,'j'=>791,'k'=>785,'l'=>791,'m'=>873,
+	'n'=>761,'o'=>762,'p'=>762,'q'=>759,'r'=>759,'s'=>892,'t'=>892,'u'=>788,'v'=>784,'w'=>438,'x'=>138,'y'=>277,'z'=>415,'{'=>392,'|'=>392,'}'=>668,'~'=>668,chr(127)=>0,chr(128)=>390,chr(129)=>390,chr(130)=>317,chr(131)=>317,
+	chr(132)=>276,chr(133)=>276,chr(134)=>509,chr(135)=>509,chr(136)=>410,chr(137)=>410,chr(138)=>234,chr(139)=>234,chr(140)=>334,chr(141)=>334,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0,
+	chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>0,chr(161)=>732,chr(162)=>544,chr(163)=>544,chr(164)=>910,chr(165)=>667,chr(166)=>760,chr(167)=>760,chr(168)=>776,chr(169)=>595,chr(170)=>694,chr(171)=>626,chr(172)=>788,chr(173)=>788,chr(174)=>788,chr(175)=>788,
+	chr(176)=>788,chr(177)=>788,chr(178)=>788,chr(179)=>788,chr(180)=>788,chr(181)=>788,chr(182)=>788,chr(183)=>788,chr(184)=>788,chr(185)=>788,chr(186)=>788,chr(187)=>788,chr(188)=>788,chr(189)=>788,chr(190)=>788,chr(191)=>788,chr(192)=>788,chr(193)=>788,chr(194)=>788,chr(195)=>788,chr(196)=>788,chr(197)=>788,
+	chr(198)=>788,chr(199)=>788,chr(200)=>788,chr(201)=>788,chr(202)=>788,chr(203)=>788,chr(204)=>788,chr(205)=>788,chr(206)=>788,chr(207)=>788,chr(208)=>788,chr(209)=>788,chr(210)=>788,chr(211)=>788,chr(212)=>894,chr(213)=>838,chr(214)=>1016,chr(215)=>458,chr(216)=>748,chr(217)=>924,chr(218)=>748,chr(219)=>918,
+	chr(220)=>927,chr(221)=>928,chr(222)=>928,chr(223)=>834,chr(224)=>873,chr(225)=>828,chr(226)=>924,chr(227)=>924,chr(228)=>917,chr(229)=>930,chr(230)=>931,chr(231)=>463,chr(232)=>883,chr(233)=>836,chr(234)=>836,chr(235)=>867,chr(236)=>867,chr(237)=>696,chr(238)=>696,chr(239)=>874,chr(240)=>0,chr(241)=>874,
+	chr(242)=>760,chr(243)=>946,chr(244)=>771,chr(245)=>865,chr(246)=>771,chr(247)=>888,chr(248)=>967,chr(249)=>888,chr(250)=>831,chr(251)=>873,chr(252)=>927,chr(253)=>970,chr(254)=>918,chr(255)=>0);
+?>
diff --git a/misc/PublicationBulletins/Portail-LeHavre/fpdf.php b/misc/PublicationBulletins/Portail-LeHavre/fpdf.php
new file mode 100755
index 0000000000000000000000000000000000000000..cf642dd0040f5454991609fb9c81d901f2eebba0
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/fpdf.php
@@ -0,0 +1,2105 @@
+<?php
+/*******************************************************************************
+* FPDF (based on FPDF 1.6)                                                     *
+* Modification du fichier pour adaptation à code existant                      *
+* Remplacement de la valeur TFPDF par FPDF dans toute la classe                *
+* La Classe TFPDF possède le codage UTF-8 pour les caractères accentués        *
+* Modification: 2011-03-19 Sebastien SOUBIE (sebastien.soubie@univ-poitiers.fr)*
+* Version:  1.03                                                               *
+* Date:     2010-07-25                                                         *
+* Author:   Ian Back <ianb@bpm1.com>                                           *
+* License:  LGPL                                                               *
+*******************************************************************************/
+
+define('FPDF_VERSION','1.03');
+
+class FPDF
+{
+
+var $unifontSubset;
+var $extraFontSubsets = 0;
+var $t1asm;
+
+var $page;               //current page number
+var $n;                  //current object number
+var $offsets;            //array of object offsets
+var $buffer;             //buffer holding in-memory PDF
+var $pages;              //array containing pages
+var $state;              //current document state
+var $compress;           //compression flag
+var $k;                  //scale factor (number of points in user unit)
+var $DefOrientation;     //default orientation
+var $CurOrientation;     //current orientation
+var $PageFormats;        //available page formats
+var $DefPageFormat;      //default page format
+var $CurPageFormat;      //current page format
+var $PageSizes;          //array storing non-default page sizes
+var $wPt,$hPt;           //dimensions of current page in points
+var $w,$h;               //dimensions of current page in user unit
+var $lMargin;            //left margin
+var $tMargin;            //top margin
+var $rMargin;            //right margin
+var $bMargin;            //page break margin
+var $cMargin;            //cell margin
+var $x,$y;               //current position in user unit
+var $lasth;              //height of last printed cell
+var $LineWidth;          //line width in user unit
+var $CoreFonts;          //array of standard font names
+var $fonts;              //array of used fonts
+var $FontFiles;          //array of font files
+var $diffs;              //array of encoding differences
+var $FontFamily;         //current font family
+var $FontStyle;          //current font style
+var $underline;          //underlining flag
+var $CurrentFont;        //current font info
+var $FontSizePt;         //current font size in points
+var $FontSize;           //current font size in user unit
+var $DrawColor;          //commands for drawing color
+var $FillColor;          //commands for filling color
+var $TextColor;          //commands for text color
+var $ColorFlag;          //indicates whether fill and text colors are different
+var $ws;                 //word spacing
+var $images;             //array of used images
+var $PageLinks;          //array of links in pages
+var $links;              //array of internal links
+var $AutoPageBreak;      //automatic page breaking
+var $PageBreakTrigger;   //threshold used to trigger page breaks
+var $InHeader;           //flag set when processing header
+var $InFooter;           //flag set when processing footer
+var $ZoomMode;           //zoom display mode
+var $LayoutMode;         //layout display mode
+var $title;              //title
+var $subject;            //subject
+var $author;             //author
+var $keywords;           //keywords
+var $creator;            //creator
+var $AliasNbPages;       //alias for total number of pages
+var $PDFVersion;         //PDF version number
+
+/*******************************************************************************
+*                                                                              *
+*                               Public methods                                 *
+*                                                                              *
+*******************************************************************************/
+function FPDF($orientation='P', $unit='mm', $format='A4')
+{
+	//Some checks
+	$this->_dochecks();
+	//Initialization of properties
+	$this->page=0;
+	$this->n=2;
+	$this->buffer='';
+	$this->pages=array();
+	$this->PageSizes=array();
+	$this->state=0;
+	$this->fonts=array();
+	$this->FontFiles=array();
+	$this->diffs=array();
+	$this->images=array();
+	$this->links=array();
+	$this->InHeader=false;
+	$this->InFooter=false;
+	$this->lasth=0;
+	$this->FontFamily='';
+	$this->FontStyle='';
+	$this->FontSizePt=12;
+	$this->underline=false;
+	$this->DrawColor='0 G';
+	$this->FillColor='0 g';
+	$this->TextColor='0 g';
+	$this->ColorFlag=false;
+	$this->ws=0;
+	//Standard fonts
+	$this->CoreFonts=array('courier'=>'Courier', 'courierB'=>'Courier-Bold', 'courierI'=>'Courier-Oblique', 'courierBI'=>'Courier-BoldOblique',
+		'helvetica'=>'Helvetica', 'helveticaB'=>'Helvetica-Bold', 'helveticaI'=>'Helvetica-Oblique', 'helveticaBI'=>'Helvetica-BoldOblique',
+		'times'=>'Times-Roman', 'timesB'=>'Times-Bold', 'timesI'=>'Times-Italic', 'timesBI'=>'Times-BoldItalic',
+		'symbol'=>'Symbol', 'zapfdingbats'=>'ZapfDingbats');
+	//Scale factor
+	if($unit=='pt')
+		$this->k=1;
+	elseif($unit=='mm')
+		$this->k=72/25.4;
+	elseif($unit=='cm')
+		$this->k=72/2.54;
+	elseif($unit=='in')
+		$this->k=72;
+	else
+		$this->Error('Incorrect unit: '.$unit);
+
+	//Page format
+	$this->PageFormats=array('a3'=>array(841.89,1190.55), 'a4'=>array(595.28,841.89), 'a5'=>array(420.94,595.28),
+		'letter'=>array(612,792), 'legal'=>array(612,1008));
+	if(is_string($format))
+		$format=$this->_getpageformat($format);
+	$this->DefPageFormat=$format;
+	$this->CurPageFormat=$format;
+	//Page orientation
+	$orientation=strtolower($orientation);
+	if($orientation=='p' || $orientation=='portrait')
+	{
+		$this->DefOrientation='P';
+		$this->w=$this->DefPageFormat[0];
+		$this->h=$this->DefPageFormat[1];
+	}
+	elseif($orientation=='l' || $orientation=='landscape')
+	{
+		$this->DefOrientation='L';
+		$this->w=$this->DefPageFormat[1];
+		$this->h=$this->DefPageFormat[0];
+	}
+	else
+		$this->Error('Incorrect orientation: '.$orientation);
+	$this->CurOrientation=$this->DefOrientation;
+	$this->wPt=$this->w*$this->k;
+	$this->hPt=$this->h*$this->k;
+	//Page margins (1 cm)
+	$margin=28.35/$this->k;
+	$this->SetMargins($margin,$margin);
+	//Interior cell margin (1 mm)
+	$this->cMargin=$margin/10;
+	//Line width (0.2 mm)
+	$this->LineWidth=.567/$this->k;
+	//Automatic page break
+	$this->SetAutoPageBreak(true,2*$margin);
+	//Full width display mode
+	$this->SetDisplayMode('fullwidth');
+	//Enable compression
+	$this->SetCompression(true);
+	//Set default PDF version number
+	$this->PDFVersion='1.3';
+}
+
+function SetMargins($left, $top, $right=null)
+{
+	//Set left, top and right margins
+	$this->lMargin=$left;
+	$this->tMargin=$top;
+	if($right===null)
+		$right=$left;
+	$this->rMargin=$right;
+}
+
+function SetLeftMargin($margin)
+{
+	//Set left margin
+	$this->lMargin=$margin;
+	if($this->page>0 && $this->x<$margin)
+		$this->x=$margin;
+}
+
+function SetTopMargin($margin)
+{
+	//Set top margin
+	$this->tMargin=$margin;
+}
+
+function SetRightMargin($margin)
+{
+	//Set right margin
+	$this->rMargin=$margin;
+}
+
+function SetAutoPageBreak($auto, $margin=0)
+{
+	//Set auto page break mode and triggering margin
+	$this->AutoPageBreak=$auto;
+	$this->bMargin=$margin;
+	$this->PageBreakTrigger=$this->h-$margin;
+}
+
+function SetDisplayMode($zoom, $layout='continuous')
+{
+	//Set display mode in viewer
+	if($zoom=='fullpage' || $zoom=='fullwidth' || $zoom=='real' || $zoom=='default' || !is_string($zoom))
+		$this->ZoomMode=$zoom;
+	else
+		$this->Error('Incorrect zoom display mode: '.$zoom);
+	if($layout=='single' || $layout=='continuous' || $layout=='two' || $layout=='default')
+		$this->LayoutMode=$layout;
+	else
+		$this->Error('Incorrect layout display mode: '.$layout);
+}
+
+function SetCompression($compress)
+{
+	//Set page compression
+	if(function_exists('gzcompress'))
+		$this->compress=$compress;
+	else
+		$this->compress=false;
+}
+
+function SetTitle($title, $isUTF8=false)
+{
+	//Title of document
+	if($isUTF8)
+		$title=$this->_UTF8toUTF16($title);
+	$this->title=$title;
+}
+
+function SetSubject($subject, $isUTF8=false)
+{
+	//Subject of document
+	if($isUTF8)
+		$subject=$this->_UTF8toUTF16($subject);
+	$this->subject=$subject;
+}
+
+function SetAuthor($author, $isUTF8=false)
+{
+	//Author of document
+	if($isUTF8)
+		$author=$this->_UTF8toUTF16($author);
+	$this->author=$author;
+}
+
+function SetKeywords($keywords, $isUTF8=false)
+{
+	//Keywords of document
+	if($isUTF8)
+		$keywords=$this->_UTF8toUTF16($keywords);
+	$this->keywords=$keywords;
+}
+
+function SetCreator($creator, $isUTF8=false)
+{
+	//Creator of document
+	if($isUTF8)
+		$creator=$this->_UTF8toUTF16($creator);
+	$this->creator=$creator;
+}
+
+function AliasNbPages($alias='{nb}')
+{
+	//Define an alias for total number of pages
+	$this->AliasNbPages=$alias;
+}
+
+function Error($msg)
+{
+	//Fatal error
+	die('<b>FPDF error:</b> '.$msg);
+}
+
+function Open()
+{
+	//Begin document
+	$this->state=1;
+}
+
+function Close()
+{
+	//Terminate document
+	if($this->state==3)
+		return;
+	if($this->page==0)
+		$this->AddPage();
+	//Page footer
+	$this->InFooter=true;
+	$this->Footer();
+	$this->InFooter=false;
+	//Close page
+	$this->_endpage();
+	//Close document
+	$this->_enddoc();
+}
+
+function AddPage($orientation='', $format='')
+{
+	//Start a new page
+	if($this->state==0)
+		$this->Open();
+	$family=$this->FontFamily;
+	$style=$this->FontStyle.($this->underline ? 'U' : '');
+	$size=$this->FontSizePt;
+	$lw=$this->LineWidth;
+	$dc=$this->DrawColor;
+	$fc=$this->FillColor;
+	$tc=$this->TextColor;
+	$cf=$this->ColorFlag;
+	if($this->page>0)
+	{
+		//Page footer
+		$this->InFooter=true;
+		$this->Footer();
+		$this->InFooter=false;
+		//Close page
+		$this->_endpage();
+	}
+	//Start new page
+	$this->_beginpage($orientation,$format);
+	//Set line cap style to square
+	$this->_out('2 J');
+	//Set line width
+	$this->LineWidth=$lw;
+	$this->_out(sprintf('%.2F w',$lw*$this->k));
+	//Set font
+	if($family)
+		$this->SetFont($family,$style,$size);
+	//Set colors
+	$this->DrawColor=$dc;
+	if($dc!='0 G')
+		$this->_out($dc);
+	$this->FillColor=$fc;
+	if($fc!='0 g')
+		$this->_out($fc);
+	$this->TextColor=$tc;
+	$this->ColorFlag=$cf;
+	//Page header
+	$this->InHeader=true;
+	$this->Header();
+	$this->InHeader=false;
+	//Restore line width
+	if($this->LineWidth!=$lw)
+	{
+		$this->LineWidth=$lw;
+		$this->_out(sprintf('%.2F w',$lw*$this->k));
+	}
+	//Restore font
+	if($family)
+		$this->SetFont($family,$style,$size);
+	//Restore colors
+	if($this->DrawColor!=$dc)
+	{
+		$this->DrawColor=$dc;
+		$this->_out($dc);
+	}
+	if($this->FillColor!=$fc)
+	{
+		$this->FillColor=$fc;
+		$this->_out($fc);
+	}
+	$this->TextColor=$tc;
+	$this->ColorFlag=$cf;
+}
+
+function Header()
+{
+	//To be implemented in your own inherited class
+}
+
+function Footer()
+{
+	//To be implemented in your own inherited class
+}
+
+function PageNo()
+{
+	//Get current page number
+	return $this->page;
+}
+
+function SetDrawColor($r, $g=null, $b=null)
+{
+	//Set color for all stroking operations
+	if(($r==0 && $g==0 && $b==0) || $g===null)
+		$this->DrawColor=sprintf('%.3F G',$r/255);
+	else
+		$this->DrawColor=sprintf('%.3F %.3F %.3F RG',$r/255,$g/255,$b/255);
+	if($this->page>0)
+		$this->_out($this->DrawColor);
+}
+
+function SetFillColor($r, $g=null, $b=null)
+{
+	//Set color for all filling operations
+	if(($r==0 && $g==0 && $b==0) || $g===null)
+		$this->FillColor=sprintf('%.3F g',$r/255);
+	else
+		$this->FillColor=sprintf('%.3F %.3F %.3F rg',$r/255,$g/255,$b/255);
+	$this->ColorFlag=($this->FillColor!=$this->TextColor);
+	if($this->page>0)
+		$this->_out($this->FillColor);
+}
+
+function SetTextColor($r, $g=null, $b=null)
+{
+	//Set color for text
+	if(($r==0 && $g==0 && $b==0) || $g===null)
+		$this->TextColor=sprintf('%.3F g',$r/255);
+	else
+		$this->TextColor=sprintf('%.3F %.3F %.3F rg',$r/255,$g/255,$b/255);
+	$this->ColorFlag=($this->FillColor!=$this->TextColor);
+}
+
+function GetStringWidth($s)
+{
+	//Get width of a string in the current font
+	$s=(string)$s;
+	$cw=&$this->CurrentFont['cw'];
+	$w=0;
+	if ($this->unifontSubset) {
+		$unicode = $this->UTF8StringToArray($s);
+		foreach($unicode as $char) {
+			if (isset($cw[$char])) { $w+=$cw[$char]; } 
+			else if($char>0 && $char<128 && isset($cw[chr($char)])) { $w+=$cw[chr($char)]; } 
+			else if(isset($this->CurrentFont['desc']['MissingWidth'])) { $w += $this->CurrentFont['desc']['MissingWidth']; }
+			else if(isset($this->CurrentFont['MissingWidth'])) { $w += $this->CurrentFont['MissingWidth']; }
+			else { $w += 500; }
+		}
+	}
+	else {
+		$l=strlen($s);
+		for($i=0;$i<$l;$i++)
+			$w+=$cw[$s[$i]];
+	}
+	return $w*$this->FontSize/1000;
+}
+
+function SetLineWidth($width)
+{
+	//Set line width
+	$this->LineWidth=$width;
+	if($this->page>0)
+		$this->_out(sprintf('%.2F w',$width*$this->k));
+}
+
+function Line($x1, $y1, $x2, $y2)
+{
+	//Draw a line
+	$this->_out(sprintf('%.2F %.2F m %.2F %.2F l S',$x1*$this->k,($this->h-$y1)*$this->k,$x2*$this->k,($this->h-$y2)*$this->k));
+}
+
+function Rect($x, $y, $w, $h, $style='')
+{
+	//Draw a rectangle
+	if($style=='F')
+		$op='f';
+	elseif($style=='FD' || $style=='DF')
+		$op='B';
+	else
+		$op='S';
+	$this->_out(sprintf('%.2F %.2F %.2F %.2F re %s',$x*$this->k,($this->h-$y)*$this->k,$w*$this->k,-$h*$this->k,$op));
+}
+
+
+function AddFont($family, $style='', $file='', $uni=false)
+{
+	//Add a TrueType or Type1 font
+	$family=strtolower($family);
+	if($family=='arial')
+		$family='helvetica';
+	$style=strtoupper($style);
+	if($style=='IB')
+		$style='BI';
+	if($file=='') {
+	   if ($uni) {
+		$file=str_replace(' ','',$family).strtolower($style).'.ttf';
+	   }
+	   else {
+		$file=str_replace(' ','',$family).strtolower($style).'.php';
+	   }
+	}
+	$fontkey=$family.$style;
+	if(isset($this->fonts[$fontkey]))
+		return;
+
+	if ($uni) {
+		if (defined("_SYSTEM_TTFONTS") && file_exists(_SYSTEM_TTFONTS.$file )) { $ttfilename = _SYSTEM_TTFONTS.$file ; }
+		else { $ttfilename = $this->_getfontpath().'unifont/'.$file ; }
+		$filename = $file;
+		$filename =str_replace(' ','',$filename );
+		$filename =str_replace('-','',$filename );
+		$unifilename = $this->_getfontpath().'unifont/'.strtolower(substr($filename ,0,(strpos($filename ,'.'))));
+		$diff = '';
+		$enc = '';
+		if (file_exists($unifilename.'.mtx.php')) {
+			include($unifilename.'.mtx.php');
+		}
+		if (!isset($type) ||  $type != "TrueTypesubset") {
+			include_once($this->_getfontpath().'unifont/ttfonts.php');
+			$ttf = new TTFontFile();
+			$ttf->getMetrics($ttfilename);
+			$cw = $ttf->charWidths;
+			$type = "TrueTypesubset";
+			$name = preg_replace('/ /','',$ttf->fullName);
+			$desc= array('Ascent'=>round($ttf->ascent),
+			'Descent'=>round($ttf->descent),
+			'CapHeight'=>round($ttf->capHeight),
+			'Flags'=>$ttf->flags,
+			'FontBBox'=>'['.round($ttf->bbox[0])." ".round($ttf->bbox[1])." ".round($ttf->bbox[2])." ".round($ttf->bbox[3]).']',
+			'ItalicAngle'=>$ttf->italicAngle,
+			'StemV'=>round($ttf->stemV),
+			'MissingWidth'=>round($ttf->defaultWidth));
+			$up = round($ttf->underlinePosition);
+			$ut = round($ttf->underlineThickness);
+			//Generate metrics .php file
+			$s='<?php'."\n";
+			$s.='$type=\''.$type."';\n";
+			$s.='$name=\''.$name."';\n";
+			$s.='$desc='.var_export($desc,true).";\n";
+			$s.='$up='.$up.";\n";
+			$s.='$ut='.$ut.";\n";
+			$s.='$cw='.var_export($cw,true).";\n";
+			$s.="?>\n";
+			if (is_writable($this->_getfontpath().'unifont')) {
+				$fh = fopen($unifilename.'.mtx.php',"w");
+				fwrite($fh,$s,strlen($s));
+				fclose($fh);
+			}
+			unset($ttf);
+		}
+		if(!isset($name)) {
+			$this->Error('Problem with the font definition file');
+		}
+		$i = count($this->fonts)+$this->extraFontSubsets+1;
+		if(!empty($this->AliasNbPages))
+			$sbarr = range(0,57);
+		else
+			$sbarr = range(0,32);
+		$this->fonts[$fontkey] = array('i'=>$i, 'type'=>$type, 'name'=>$name, 'desc'=>$desc, 'up'=>$up, 'ut'=>$ut, 'cw'=>$cw, 'enc'=>$enc, 'file'=>$ttfilename, 'subsets'=>array(0=>$sbarr), 'subsetfontids'=>array($i), 'used'=>false);
+		unset($cw);
+	}
+	else {
+		include($this->_getfontpath().$file);
+		if(!isset($name))
+			$this->Error('Could not include font definition file');
+		$i=count($this->fonts)+$this->extraFontSubsets+1;
+		$this->fonts[$fontkey]=array('i'=>$i, 'type'=>$type, 'name'=>$name, 'desc'=>$desc, 'up'=>$up, 'ut'=>$ut, 'cw'=>$cw, 'enc'=>$enc, 'file'=>$file);
+	}
+
+	if($diff)
+	{
+		//Search existing encodings
+		$d=0;
+		$nb=count($this->diffs);
+		for($i=1;$i<=$nb;$i++)
+		{
+			if($this->diffs[$i]==$diff)
+			{
+				$d=$i;
+				break;
+			}
+		}
+		if($d==0)
+		{
+			$d=$nb+1;
+			$this->diffs[$d]=$diff;
+		}
+		$this->fonts[$fontkey]['diff']=$d;
+	}
+	if($file)
+	{
+		if($type=='TrueType')
+			$this->FontFiles[$file]=array('length1'=>$originalsize);
+		else if ($uni && $type == "TrueTypesubset")
+			$this->FontFiles[$file]=array('type'=>"TrueTypesubset");
+		else
+			$this->FontFiles[$file]=array('length1'=>$size1, 'length2'=>$size2);
+	}
+}
+
+function SetFont($family, $style='', $size=0)
+{
+	//Select a font; size given in points
+	global $fpdf_charwidths;
+
+	$family=strtolower($family);
+	if($family=='')
+		$family=$this->FontFamily;
+	if($family=='arial')
+		$family='helvetica';
+	elseif($family=='symbol' || $family=='zapfdingbats')
+		$style='';
+	$style=strtoupper($style);
+	if(strpos($style,'U')!==false)
+	{
+		$this->underline=true;
+		$style=str_replace('U','',$style);
+	}
+	else
+		$this->underline=false;
+	if($style=='IB')
+		$style='BI';
+	if($size==0)
+		$size=$this->FontSizePt;
+	//Test if font is already selected
+	if($this->FontFamily==$family && $this->FontStyle==$style && $this->FontSizePt==$size)
+		return;
+	//Test if used for the first time
+	$fontkey=$family.$style;
+	if(!isset($this->fonts[$fontkey]))
+	{
+		//Check if one of the standard fonts
+		if(isset($this->CoreFonts[$fontkey]))
+		{
+			if(!isset($fpdf_charwidths[$fontkey]))
+			{
+				//Load metric file
+				$file=$family;
+				if($family=='times' || $family=='helvetica')
+					$file.=strtolower($style);
+				include($this->_getfontpath().$file.'.php');
+				if(!isset($fpdf_charwidths[$fontkey]))
+					$this->Error('Could not include font metric file');
+			}
+			$i=count($this->fonts)+1+$this->extraFontSubsets;
+			$name=$this->CoreFonts[$fontkey];
+			$cw=$fpdf_charwidths[$fontkey];
+			$this->fonts[$fontkey]=array('i'=>$i, 'type'=>'core', 'name'=>$name, 'up'=>-100, 'ut'=>50, 'cw'=>$cw);
+		}
+		else
+			$this->Error('Undefined font: '.$family.' '.$style);
+	}
+	//Select it
+	$this->FontFamily=$family;
+	$this->FontStyle=$style;
+	$this->FontSizePt=$size;
+	$this->FontSize=$size/$this->k;
+	$this->CurrentFont=&$this->fonts[$fontkey];
+	if ($this->fonts[$fontkey]['type']=='TrueTypesubset') { $this->unifontSubset = true; }
+	else { $this->unifontSubset = false; }
+	if($this->page>0)
+		$this->_out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt));
+}
+
+function SetFontSize($size)
+{
+	//Set font size in points
+	if($this->FontSizePt==$size)
+		return;
+	$this->FontSizePt=$size;
+	$this->FontSize=$size/$this->k;
+	if($this->page>0)
+		$this->_out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt));
+}
+
+function AddLink()
+{
+	//Create a new internal link
+	$n=count($this->links)+1;
+	$this->links[$n]=array(0, 0);
+	return $n;
+}
+
+function SetLink($link, $y=0, $page=-1)
+{
+	//Set destination of internal link
+	if($y==-1)
+		$y=$this->y;
+	if($page==-1)
+		$page=$this->page;
+	$this->links[$link]=array($page, $y);
+}
+
+function Link($x, $y, $w, $h, $link)
+{
+	//Put a link on the page
+	$this->PageLinks[$this->page][]=array($x*$this->k, $this->hPt-$y*$this->k, $w*$this->k, $h*$this->k, $link);
+}
+
+function Text($x, $y, $txt)
+{
+	//Output a string
+	if ($this->unifontSubset)
+		$txt2 = $this->UTF8toSubset($txt);
+	else 
+		$txt2='('.$this->_escape($txt).')';
+	$s=sprintf('BT %.2F %.2F Td %s Tj ET',$x*$this->k,($this->h-$y)*$this->k,$txt2);
+	if($this->underline && $txt!='')
+		$s.=' '.$this->_dounderline($x,$y,$txt);
+	if($this->ColorFlag)
+		$s='q '.$this->TextColor.' '.$s.' Q';
+	$this->_out($s);
+}
+
+function AcceptPageBreak()
+{
+	//Accept automatic page break or not
+	return $this->AutoPageBreak;
+}
+
+function Cell($w, $h=0, $txt='', $border=0, $ln=0, $align='', $fill=false, $link='')
+{
+	//Output a cell
+	$k=$this->k;
+	if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak())
+	{
+		//Automatic page break
+		$x=$this->x;
+		$ws=$this->ws;
+		if($ws>0)
+		{
+			$this->ws=0;
+			$this->_out('0 Tw');
+		}
+		$this->AddPage($this->CurOrientation,$this->CurPageFormat);
+		$this->x=$x;
+		if($ws>0)
+		{
+			$this->ws=$ws;
+			$this->_out(sprintf('%.3F Tw',$ws*$k));
+		}
+	}
+	if($w==0)
+		$w=$this->w-$this->rMargin-$this->x;
+	$s='';
+	if($fill || $border==1)
+	{
+		if($fill)
+			$op=($border==1) ? 'B' : 'f';
+		else
+			$op='S';
+		$s=sprintf('%.2F %.2F %.2F %.2F re %s ',$this->x*$k,($this->h-$this->y)*$k,$w*$k,-$h*$k,$op);
+	}
+	if(is_string($border))
+	{
+		$x=$this->x;
+		$y=$this->y;
+		if(strpos($border,'L')!==false)
+			$s.=sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-$y)*$k,$x*$k,($this->h-($y+$h))*$k);
+		if(strpos($border,'T')!==false)
+			$s.=sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-$y)*$k,($x+$w)*$k,($this->h-$y)*$k);
+		if(strpos($border,'R')!==false)
+			$s.=sprintf('%.2F %.2F m %.2F %.2F l S ',($x+$w)*$k,($this->h-$y)*$k,($x+$w)*$k,($this->h-($y+$h))*$k);
+		if(strpos($border,'B')!==false)
+			$s.=sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-($y+$h))*$k,($x+$w)*$k,($this->h-($y+$h))*$k);
+	}
+	if($txt!=='')
+	{
+		if($align=='R')
+			$dx=$w-$this->cMargin-$this->GetStringWidth($txt);
+		elseif($align=='C')
+			$dx=($w-$this->GetStringWidth($txt))/2;
+		else
+			$dx=$this->cMargin;
+		if($this->ColorFlag)
+			$s.='q '.$this->TextColor.' ';
+		if ($this->unifontSubset)
+			$txt2 = $this->UTF8toSubset($txt);
+		else 
+			$txt2='('.str_replace(')','\\)',str_replace('(','\\(',str_replace('\\','\\\\',$txt))).')';
+		$s.=sprintf('BT %.2F %.2F Td %s Tj ET',($this->x+$dx)*$k,($this->h-($this->y+.5*$h+.3*$this->FontSize))*$k,$txt2);
+		if($this->underline)
+			$s.=' '.$this->_dounderline($this->x+$dx,$this->y+.5*$h+.3*$this->FontSize,$txt);
+		if($this->ColorFlag)
+			$s.=' Q';
+		if($link)
+			$this->Link($this->x+$dx,$this->y+.5*$h-.5*$this->FontSize,$this->GetStringWidth($txt),$this->FontSize,$link);
+	}
+	if($s)
+		$this->_out($s);
+	$this->lasth=$h;
+	if($ln>0)
+	{
+		//Go to next line
+		$this->y+=$h;
+		if($ln==1)
+			$this->x=$this->lMargin;
+	}
+	else
+		$this->x+=$w;
+}
+
+function MultiCell($w, $h, $txt, $border=0, $align='J', $fill=false)
+{
+	//Output text with automatic or explicit line breaks
+	$cw=&$this->CurrentFont['cw'];
+	if($w==0)
+		$w=$this->w-$this->rMargin-$this->x;
+	$wmax=($w-2*$this->cMargin);
+	$s=str_replace("\r",'',$txt);
+	if ($this->unifontSubset) {
+		$nb=mb_strlen($s, 'utf-8');
+		while($nb>0 && mb_substr($s,$nb-1,1,'utf-8')=="\n")	$nb--;
+	}
+	else {
+		$nb=strlen($s);
+		if($nb>0 && $s[$nb-1]=="\n")
+			$nb--;
+	}
+
+	$b=0;
+	if($border)
+	{
+		if($border==1)
+		{
+			$border='LTRB';
+			$b='LRT';
+			$b2='LR';
+		}
+		else
+		{
+			$b2='';
+			if(strpos($border,'L')!==false)
+				$b2.='L';
+			if(strpos($border,'R')!==false)
+				$b2.='R';
+			$b=(strpos($border,'T')!==false) ? $b2.'T' : $b2;
+		}
+	}
+	$sep=-1;
+	$i=0;
+	$j=0;
+	$l=0;
+	$ns=0;
+	$nl=1;
+	while($i<$nb)
+	{
+		//Get next character
+		if ($this->unifontSubset) {
+			$c = mb_substr($s,$i,1,'UTF-8');
+		}
+		else {
+			$c=$s[$i];
+		}
+		if($c=="\n") {
+			//Explicit line break
+			if($this->ws>0)
+			{
+				$this->ws=0;
+				$this->_out('0 Tw');
+			}
+			if ($this->unifontSubset) {
+				$this->Cell($w,$h,mb_substr($s,$j,$i-$j,'UTF-8'),$b,2,$align,$fill);
+			}
+			else {
+				$this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill);
+			}
+			$i++;
+			$sep=-1;
+			$j=$i;
+			$l=0;
+			$ns=0;
+			$nl++;
+			if($border && $nl==2)
+				$b=$b2;
+			continue;
+		}
+		if($c==' ')
+		{
+			$sep=$i;
+			$ls=$l;
+			$ns++;
+		}
+
+		if ($this->unifontSubset) { $l += $this->GetStringWidth($c); }
+		else { $l += $cw[$c]*$this->FontSize/1000; }
+
+		if($l>$wmax)
+		{
+			//Automatic line break
+			if($sep==-1)
+			{
+				if($i==$j)
+					$i++;
+				if($this->ws>0)
+				{
+					$this->ws=0;
+					$this->_out('0 Tw');
+				}
+				if ($this->unifontSubset) {
+					$this->Cell($w,$h,mb_substr($s,$j,$i-$j,'UTF-8'),$b,2,$align,$fill);
+				}
+				else {
+					$this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill);
+				}
+			}
+			else
+			{
+				if($align=='J')
+				{
+					$this->ws=($ns>1) ? ($wmax-$ls)/($ns-1) : 0;
+					$this->_out(sprintf('%.3F Tw',$this->ws*$this->k));
+				}
+				if ($this->unifontSubset) {
+					$this->Cell($w,$h,mb_substr($s,$j,$sep-$j,'UTF-8'),$b,2,$align,$fill);
+				}
+				else {
+					$this->Cell($w,$h,substr($s,$j,$sep-$j),$b,2,$align,$fill);
+				}
+				$i=$sep+1;
+			}
+			$sep=-1;
+			$j=$i;
+			$l=0;
+			$ns=0;
+			$nl++;
+			if($border && $nl==2)
+				$b=$b2;
+		}
+		else
+			$i++;
+	}
+	//Last chunk
+	if($this->ws>0)
+	{
+		$this->ws=0;
+		$this->_out('0 Tw');
+	}
+	if($border && strpos($border,'B')!==false)
+		$b.='B';
+	if ($this->unifontSubset) {
+		$this->Cell($w,$h,mb_substr($s,$j,$i-$j,'UTF-8'),$b,2,$align,$fill);
+	}
+	else {
+		$this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill);
+	}
+	$this->x=$this->lMargin;
+}
+
+function Write($h, $txt, $link='')
+{
+	//Output text in flowing mode
+	$cw=&$this->CurrentFont['cw'];
+	$w=$this->w-$this->rMargin-$this->x;
+
+	$wmax=($w-2*$this->cMargin);
+	$s=str_replace("\r",'',$txt);
+	if ($this->unifontSubset) {
+		$nb=mb_strlen($s, 'UTF-8');
+		if($nb==1 && $s==" ") {
+			$this->x += $this->GetStringWidth($s);
+			return;
+		}
+	}
+	else {
+		$nb=strlen($s);
+	}
+
+	$sep=-1;
+	$i=0;
+	$j=0;
+	$l=0;
+	$nl=1;
+	while($i<$nb)
+	{
+		//Get next character
+		//Get next character
+		if ($this->unifontSubset) {
+			$c = mb_substr($s,$i,1,'UTF-8');
+		}
+		else {
+			$c=$s[$i];
+		}
+		if($c=="\n") {
+			//Explicit line break
+			if ($this->unifontSubset) {
+				$this->Cell($w,$h,mb_substr($s,$j,$i-$j,'UTF-8'),0,2,'',0,$link);
+			}
+			else {
+				$this->Cell($w,$h,substr($s,$j,$i-$j),0,2,'',0,$link);
+			}
+			$i++;
+			$sep=-1;
+			$j=$i;
+			$l=0;
+			if($nl==1)
+			{
+				$this->x=$this->lMargin;
+				$w=$this->w-$this->rMargin-$this->x;
+				$wmax=($w-2*$this->cMargin);
+			}
+			$nl++;
+			continue;
+		}
+		if($c==' ')
+			$sep=$i;
+
+		if ($this->unifontSubset) { $l += $this->GetStringWidth($c); }
+		else { $l += $cw[$c]*$this->FontSize/1000; }
+
+		if($l>$wmax)
+		{
+			//Automatic line break
+			if($sep==-1)
+			{
+				if($this->x>$this->lMargin)
+				{
+					//Move to next line
+					$this->x=$this->lMargin;
+					$this->y+=$h;
+					$w=$this->w-$this->rMargin-$this->x;
+					$wmax=($w-2*$this->cMargin);
+					$i++;
+					$nl++;
+					continue;
+				}
+				if($i==$j)
+					$i++;
+				if ($this->unifontSubset) {
+					$this->Cell($w,$h,mb_substr($s,$j,$i-$j,'UTF-8'),0,2,'',0,$link);
+				}
+				else {
+					$this->Cell($w,$h,substr($s,$j,$i-$j),0,2,'',0,$link);
+				}
+			}
+			else
+			{
+				if ($this->unifontSubset) {
+					$this->Cell($w,$h,mb_substr($s,$j,$sep-$j,'UTF-8'),0,2,'',0,$link);
+				}
+				else {
+					$this->Cell($w,$h,substr($s,$j,$sep-$j),0,2,'',0,$link);
+				}
+				$i=$sep+1;
+			}
+			$sep=-1;
+			$j=$i;
+			$l=0;
+			if($nl==1)
+			{
+				$this->x=$this->lMargin;
+				$w=$this->w-$this->rMargin-$this->x;
+				$wmax=($w-2*$this->cMargin);
+			}
+			$nl++;
+		}
+		else
+			$i++;
+	}
+	//Last chunk
+	if($i!=$j) {
+		if ($this->unifontSubset) {
+			$this->Cell($l,$h,mb_substr($s,$j,$i-$j,'UTF-8'),0,0,'',0,$link);
+		}
+		else {
+			$this->Cell($l,$h,substr($s,$j),0,0,'',0,$link);
+		}
+	}
+}
+
+function Ln($h=null)
+{
+	//Line feed; default value is last cell height
+	$this->x=$this->lMargin;
+	if($h===null)
+		$this->y+=$this->lasth;
+	else
+		$this->y+=$h;
+}
+
+function Image($file, $x=null, $y=null, $w=0, $h=0, $type='', $link='')
+{
+	//Put an image on the page
+	if(!isset($this->images[$file]))
+	{
+		//First use of this image, get info
+		if($type=='')
+		{
+			$pos=strrpos($file,'.');
+			if(!$pos)
+				$this->Error('Image file has no extension and no type was specified: '.$file);
+			$type=substr($file,$pos+1);
+		}
+		$type=strtolower($type);
+		if($type=='jpeg')
+			$type='jpg';
+		$mtd='_parse'.$type;
+		if(!method_exists($this,$mtd))
+			$this->Error('Unsupported image type: '.$type);
+		$info=$this->$mtd($file);
+		$info['i']=count($this->images)+1;
+		$this->images[$file]=$info;
+	}
+	else
+		$info=$this->images[$file];
+	//Automatic width and height calculation if needed
+	if($w==0 && $h==0)
+	{
+		//Put image at 72 dpi
+		$w=$info['w']/$this->k;
+		$h=$info['h']/$this->k;
+	}
+	elseif($w==0)
+		$w=$h*$info['w']/$info['h'];
+	elseif($h==0)
+		$h=$w*$info['h']/$info['w'];
+	//Flowing mode
+	if($y===null)
+	{
+		if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak())
+		{
+			//Automatic page break
+			$x2=$this->x;
+			$this->AddPage($this->CurOrientation,$this->CurPageFormat);
+			$this->x=$x2;
+		}
+		$y=$this->y;
+		$this->y+=$h;
+	}
+	if($x===null)
+		$x=$this->x;
+	$this->_out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q',$w*$this->k,$h*$this->k,$x*$this->k,($this->h-($y+$h))*$this->k,$info['i']));
+	if($link)
+		$this->Link($x,$y,$w,$h,$link);
+}
+
+function GetX()
+{
+	//Get x position
+	return $this->x;
+}
+
+function SetX($x)
+{
+	//Set x position
+	if($x>=0)
+		$this->x=$x;
+	else
+		$this->x=$this->w+$x;
+}
+
+function GetY()
+{
+	//Get y position
+	return $this->y;
+}
+
+function SetY($y)
+{
+	//Set y position and reset x
+	$this->x=$this->lMargin;
+	if($y>=0)
+		$this->y=$y;
+	else
+		$this->y=$this->h+$y;
+}
+
+function SetXY($x, $y)
+{
+	//Set x and y positions
+	$this->SetY($y);
+	$this->SetX($x);
+}
+
+function Output($name='', $dest='')
+{
+	//Output PDF to some destination
+	if($this->state<3)
+		$this->Close();
+	$dest=strtoupper($dest);
+	if($dest=='')
+	{
+		if($name=='')
+		{
+			$name='doc.pdf';
+			$dest='I';
+		}
+		else
+			$dest='F';
+	}
+	switch($dest)
+	{
+		case 'I':
+			//Send to standard output
+			if(ob_get_length())
+				$this->Error('Some data has already been output, can\'t send PDF file');
+			if(php_sapi_name()!='cli')
+			{
+				//We send to a browser
+				header('Content-Type: application/pdf');
+				if(headers_sent())
+					$this->Error('Some data has already been output, can\'t send PDF file');
+				header('Content-Length: '.strlen($this->buffer));
+				header('Content-Disposition: inline; filename="'.$name.'"');
+				header('Cache-Control: private, max-age=0, must-revalidate');
+				header('Pragma: public');
+				ini_set('zlib.output_compression','0');
+			}
+			echo $this->buffer;
+			break;
+		case 'D':
+			//Download file
+			if(ob_get_length())
+				$this->Error('Some data has already been output, can\'t send PDF file');
+			header('Content-Type: application/x-download');
+			if(headers_sent())
+				$this->Error('Some data has already been output, can\'t send PDF file');
+			header('Content-Length: '.strlen($this->buffer));
+			header('Content-Disposition: attachment; filename="'.$name.'"');
+			header('Cache-Control: private, max-age=0, must-revalidate');
+			header('Pragma: public');
+			ini_set('zlib.output_compression','0');
+			echo $this->buffer;
+			break;
+		case 'F':
+			//Save to local file
+			$f=fopen($name,'wb');
+			if(!$f)
+				$this->Error('Unable to create output file: '.$name);
+			fwrite($f,$this->buffer,strlen($this->buffer));
+			fclose($f);
+			break;
+		case 'S':
+			//Return as a string
+			return $this->buffer;
+		default:
+			$this->Error('Incorrect output destination: '.$dest);
+	}
+	return '';
+}
+
+/*******************************************************************************
+*                                                                              *
+*                              Protected methods                               *
+*                                                                              *
+*******************************************************************************/
+function _dochecks()
+{
+	//Check availability of %F
+	if(sprintf('%.1F',1.0)!='1.0')
+		$this->Error('This version of PHP is not supported');
+	//Check availability of mbstring
+	if(!function_exists('mb_strlen'))
+		$this->Error('mbstring extension is not available');
+	//Check mbstring overloading
+	if(ini_get('mbstring.func_overload') & 2)
+		$this->Error('mbstring overloading must be disabled');
+	//Disable runtime magic quotes
+	if(get_magic_quotes_runtime())
+		@set_magic_quotes_runtime(0);
+}
+
+function _getpageformat($format)
+{
+	$format=strtolower($format);
+	if(!isset($this->PageFormats[$format]))
+		$this->Error('Unknown page format: '.$format);
+	$a=$this->PageFormats[$format];
+	return array($a[0]/$this->k, $a[1]/$this->k);
+}
+
+function _getfontpath()
+{
+	if(!defined('FPDF_FONTPATH') && is_dir(dirname(__FILE__).'/font')) 
+		define('FPDF_FONTPATH',dirname(__FILE__).'/font/');
+	return defined('FPDF_FONTPATH') ? FPDF_FONTPATH : '';
+}
+
+function _beginpage($orientation, $format)
+{
+	$this->page++;
+	$this->pages[$this->page]='';
+	$this->state=2;
+	$this->x=$this->lMargin;
+	$this->y=$this->tMargin;
+	$this->FontFamily='';
+	//Check page size
+	if($orientation=='')
+		$orientation=$this->DefOrientation;
+	else
+		$orientation=strtoupper($orientation[0]);
+	if($format=='')
+		$format=$this->DefPageFormat;
+	else
+	{
+		if(is_string($format))
+			$format=$this->_getpageformat($format);
+	}
+	if($orientation!=$this->CurOrientation || $format[0]!=$this->CurPageFormat[0] || $format[1]!=$this->CurPageFormat[1])
+	{
+		//New size
+		if($orientation=='P')
+		{
+			$this->w=$format[0];
+			$this->h=$format[1];
+		}
+		else
+		{
+			$this->w=$format[1];
+			$this->h=$format[0];
+		}
+		$this->wPt=$this->w*$this->k;
+		$this->hPt=$this->h*$this->k;
+		$this->PageBreakTrigger=$this->h-$this->bMargin;
+		$this->CurOrientation=$orientation;
+		$this->CurPageFormat=$format;
+	}
+	if($orientation!=$this->DefOrientation || $format[0]!=$this->DefPageFormat[0] || $format[1]!=$this->DefPageFormat[1])
+		$this->PageSizes[$this->page]=array($this->wPt, $this->hPt);
+}
+
+function _endpage()
+{
+	$this->state=1;
+}
+
+function _escape($s)
+{
+	//Escape special characters in strings
+	$s=str_replace('\\','\\\\',$s);
+	$s=str_replace('(','\\(',$s);
+	$s=str_replace(')','\\)',$s);
+	$s=str_replace("\r",'\\r',$s);
+	return $s;
+}
+
+function _textstring($s)
+{
+	//Format a text string
+	return '('.$this->_escape($s).')';
+}
+
+function _UTF8toUTF16($s)
+{
+	//Convert UTF-8 to UTF-16BE with BOM
+	$res="\xFE\xFF";
+	$nb=strlen($s);
+	$i=0;
+	while($i<$nb)
+	{
+		$c1=ord($s[$i++]);
+		if($c1>=224)
+		{
+			//3-byte character
+			$c2=ord($s[$i++]);
+			$c3=ord($s[$i++]);
+			$res.=chr((($c1 & 0x0F)<<4) + (($c2 & 0x3C)>>2));
+			$res.=chr((($c2 & 0x03)<<6) + ($c3 & 0x3F));
+		}
+		elseif($c1>=192)
+		{
+			//2-byte character
+			$c2=ord($s[$i++]);
+			$res.=chr(($c1 & 0x1C)>>2);
+			$res.=chr((($c1 & 0x03)<<6) + ($c2 & 0x3F));
+		}
+		else
+		{
+			//Single-byte character
+			$res.="\0".chr($c1);
+		}
+	}
+	return $res;
+}
+
+function _dounderline($x, $y, $txt)
+{
+	//Underline text
+	$up=$this->CurrentFont['up'];
+	$ut=$this->CurrentFont['ut'];
+	$w=$this->GetStringWidth($txt)+$this->ws*substr_count($txt,' ');
+	return sprintf('%.2F %.2F %.2F %.2F re f',$x*$this->k,($this->h-($y-$up/1000*$this->FontSize))*$this->k,$w*$this->k,-$ut/1000*$this->FontSizePt);
+}
+
+function _parsejpg($file)
+{
+	//Extract info from a JPEG file
+	$a=GetImageSize($file);
+	if(!$a)
+		$this->Error('Missing or incorrect image file: '.$file);
+	if($a[2]!=2)
+		$this->Error('Not a JPEG file: '.$file);
+	if(!isset($a['channels']) || $a['channels']==3)
+		$colspace='DeviceRGB';
+	elseif($a['channels']==4)
+		$colspace='DeviceCMYK';
+	else
+		$colspace='DeviceGray';
+	$bpc=isset($a['bits']) ? $a['bits'] : 8;
+	//Read whole file
+	$f=fopen($file,'rb');
+	$data='';
+	while(!feof($f))
+		$data.=fread($f,8192);
+	fclose($f);
+	return array('w'=>$a[0], 'h'=>$a[1], 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'DCTDecode', 'data'=>$data);
+}
+
+function _parsepng($file)
+{
+	//Extract info from a PNG file
+	$f=fopen($file,'rb');
+	if(!$f)
+		$this->Error('Can\'t open image file: '.$file);
+	//Check signature
+	if($this->_readstream($f,8)!=chr(137).'PNG'.chr(13).chr(10).chr(26).chr(10))
+		$this->Error('Not a PNG file: '.$file);
+	//Read header chunk
+	$this->_readstream($f,4);
+	if($this->_readstream($f,4)!='IHDR')
+		$this->Error('Incorrect PNG file: '.$file);
+	$w=$this->_readint($f);
+	$h=$this->_readint($f);
+	$bpc=ord($this->_readstream($f,1));
+	if($bpc>8)
+		$this->Error('16-bit depth not supported: '.$file);
+	$ct=ord($this->_readstream($f,1));
+	if($ct==0)
+		$colspace='DeviceGray';
+	elseif($ct==2)
+		$colspace='DeviceRGB';
+	elseif($ct==3)
+		$colspace='Indexed';
+	else
+		$this->Error('Alpha channel not supported: '.$file);
+	if(ord($this->_readstream($f,1))!=0)
+		$this->Error('Unknown compression method: '.$file);
+	if(ord($this->_readstream($f,1))!=0)
+		$this->Error('Unknown filter method: '.$file);
+	if(ord($this->_readstream($f,1))!=0)
+		$this->Error('Interlacing not supported: '.$file);
+	$this->_readstream($f,4);
+	$parms='/DecodeParms <</Predictor 15 /Colors '.($ct==2 ? 3 : 1).' /BitsPerComponent '.$bpc.' /Columns '.$w.'>>';
+	//Scan chunks looking for palette, transparency and image data
+	$pal='';
+	$trns='';
+	$data='';
+	do
+	{
+		$n=$this->_readint($f);
+		$type=$this->_readstream($f,4);
+		if($type=='PLTE')
+		{
+			//Read palette
+			$pal=$this->_readstream($f,$n);
+			$this->_readstream($f,4);
+		}
+		elseif($type=='tRNS')
+		{
+			//Read transparency info
+			$t=$this->_readstream($f,$n);
+			if($ct==0)
+				$trns=array(ord(substr($t,1,1)));
+			elseif($ct==2)
+				$trns=array(ord(substr($t,1,1)), ord(substr($t,3,1)), ord(substr($t,5,1)));
+			else
+			{
+				$pos=strpos($t,chr(0));
+				if($pos!==false)
+					$trns=array($pos);
+			}
+			$this->_readstream($f,4);
+		}
+		elseif($type=='IDAT')
+		{
+			//Read image data block
+			$data.=$this->_readstream($f,$n);
+			$this->_readstream($f,4);
+		}
+		elseif($type=='IEND')
+			break;
+		else
+			$this->_readstream($f,$n+4);
+	}
+	while($n);
+	if($colspace=='Indexed' && empty($pal))
+		$this->Error('Missing palette in '.$file);
+	fclose($f);
+	return array('w'=>$w, 'h'=>$h, 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'FlateDecode', 'parms'=>$parms, 'pal'=>$pal, 'trns'=>$trns, 'data'=>$data);
+}
+
+function _readstream($f, $n)
+{
+	//Read n bytes from stream
+	$res='';
+	while($n>0 && !feof($f))
+	{
+		$s=fread($f,$n);
+		if($s===false)
+			$this->Error('Error while reading stream');
+		$n-=strlen($s);
+		$res.=$s;
+	}
+	if($n>0)
+		$this->Error('Unexpected end of stream');
+	return $res;
+}
+
+function _readint($f)
+{
+	//Read a 4-byte integer from stream
+	$a=unpack('Ni',$this->_readstream($f,4));
+	return $a['i'];
+}
+
+function _parsegif($file)
+{
+	//Extract info from a GIF file (via PNG conversion)
+	if(!function_exists('imagepng'))
+		$this->Error('GD extension is required for GIF support');
+	if(!function_exists('imagecreatefromgif'))
+		$this->Error('GD has no GIF read support');
+	$im=imagecreatefromgif($file);
+	if(!$im)
+		$this->Error('Missing or incorrect image file: '.$file);
+	imageinterlace($im,0);
+	$tmp=tempnam('.','gif');
+	if(!$tmp)
+		$this->Error('Unable to create a temporary file');
+	if(!imagepng($im,$tmp))
+		$this->Error('Error while saving to temporary file');
+	imagedestroy($im);
+	$info=$this->_parsepng($tmp);
+	unlink($tmp);
+	return $info;
+}
+
+function _newobj()
+{
+	//Begin a new object
+	$this->n++;
+	$this->offsets[$this->n]=strlen($this->buffer);
+	$this->_out($this->n.' 0 obj');
+}
+
+function _putstream($s)
+{
+	$this->_out('stream');
+	$this->_out($s);
+	$this->_out('endstream');
+}
+
+function _out($s)
+{
+	//Add a line to the document
+	if($this->state==2)
+		$this->pages[$this->page].=$s."\n";
+	else
+		$this->buffer.=$s."\n";
+}
+
+function _putpages()
+{
+	$nb=$this->page;
+	if(!empty($this->AliasNbPages))
+	{
+		//Replace number of pages in fonts using subsets
+		$r = '';
+		$nstr = "$nb";
+		for($i=0;$i<strlen($nstr);$i++) {
+			$r .= sprintf("%02s", strtoupper(dechex(intval($nstr[$i])+48)));
+		}
+		for($n=1;$n<=$nb;$n++)
+			$this->pages[$n]=str_replace('`'.$this->AliasNbPages.'`',$r,$this->pages[$n]);
+		// Now repeat for no pages in non-subset fonts
+		$r = $nb;
+		//Replace number of pages
+		for($n=1;$n<=$nb;$n++)
+			$this->pages[$n]=str_replace($this->AliasNbPages,$r,$this->pages[$n]);
+	}
+	if($this->DefOrientation=='P')
+	{
+		$wPt=$this->DefPageFormat[0]*$this->k;
+		$hPt=$this->DefPageFormat[1]*$this->k;
+	}
+	else
+	{
+		$wPt=$this->DefPageFormat[1]*$this->k;
+		$hPt=$this->DefPageFormat[0]*$this->k;
+	}
+	$filter=($this->compress) ? '/Filter /FlateDecode ' : '';
+	for($n=1;$n<=$nb;$n++)
+	{
+		//Page
+		$this->_newobj();
+		$this->_out('<</Type /Page');
+		$this->_out('/Parent 1 0 R');
+		if(isset($this->PageSizes[$n]))
+			$this->_out(sprintf('/MediaBox [0 0 %.2F %.2F]',$this->PageSizes[$n][0],$this->PageSizes[$n][1]));
+		$this->_out('/Resources 2 0 R');
+		if(isset($this->PageLinks[$n]))
+		{
+			//Links
+			$annots='/Annots [';
+			foreach($this->PageLinks[$n] as $pl)
+			{
+				$rect=sprintf('%.2F %.2F %.2F %.2F',$pl[0],$pl[1],$pl[0]+$pl[2],$pl[1]-$pl[3]);
+				$annots.='<</Type /Annot /Subtype /Link /Rect ['.$rect.'] /Border [0 0 0] ';
+				if(is_string($pl[4]))
+					$annots.='/A <</S /URI /URI '.$this->_textstring($pl[4]).'>>>>';
+				else
+				{
+					$l=$this->links[$pl[4]];
+					$h=isset($this->PageSizes[$l[0]]) ? $this->PageSizes[$l[0]][1] : $hPt;
+					$annots.=sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]>>',1+2*$l[0],$h-$l[1]*$this->k);
+				}
+			}
+			$this->_out($annots.']');
+		}
+		$this->_out('/Contents '.($this->n+1).' 0 R>>');
+		$this->_out('endobj');
+		//Page content
+		$p=($this->compress) ? gzcompress($this->pages[$n]) : $this->pages[$n];
+		$this->_newobj();
+		$this->_out('<<'.$filter.'/Length '.strlen($p).'>>');
+		$this->_putstream($p);
+		$this->_out('endobj');
+	}
+	//Pages root
+	$this->offsets[1]=strlen($this->buffer);
+	$this->_out('1 0 obj');
+	$this->_out('<</Type /Pages');
+	$kids='/Kids [';
+	for($i=0;$i<$nb;$i++)
+		$kids.=(3+2*$i).' 0 R ';
+	$this->_out($kids.']');
+	$this->_out('/Count '.$nb);
+	$this->_out(sprintf('/MediaBox [0 0 %.2F %.2F]',$wPt,$hPt));
+	$this->_out('>>');
+	$this->_out('endobj');
+}
+
+function _putfonts()
+{
+	$nf=$this->n;
+	foreach($this->diffs as $diff)
+	{
+		//Encodings
+		$this->_newobj();
+		$this->_out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['.$diff.']>>');
+		$this->_out('endobj');
+	}
+	foreach($this->FontFiles as $file=>$info)
+	{
+	   if (!isset($info['type']) || $info['type']!='TrueTypesubset') {
+		//Font file embedding
+		$this->_newobj();
+		$this->FontFiles[$file]['n']=$this->n;
+		$font='';
+		$f=fopen($this->_getfontpath().$file,'rb',1);
+		if(!$f)
+			$this->Error('Font file not found');
+		while(!feof($f))
+			$font.=fread($f,8192);
+		fclose($f);
+		$compressed=(substr($file,-2)=='.z');
+		if(!$compressed && isset($info['length2']))
+		{
+			$header=(ord($font[0])==128);
+			if($header)
+			{
+				//Strip first binary header
+				$font=substr($font,6);
+			}
+			if($header && ord($font[$info['length1']])==128)
+			{
+				//Strip second binary header
+				$font=substr($font,0,$info['length1']).substr($font,$info['length1']+6);
+			}
+		}
+		$this->_out('<</Length '.strlen($font));
+		if($compressed)
+			$this->_out('/Filter /FlateDecode');
+		$this->_out('/Length1 '.$info['length1']);
+		if(isset($info['length2']))
+			$this->_out('/Length2 '.$info['length2'].' /Length3 0');
+		$this->_out('>>');
+		$this->_putstream($font);
+		$this->_out('endobj');
+	   }
+	}
+	foreach($this->fonts as $k=>$font)
+	{
+		//Font objects
+		//$this->fonts[$k]['n']=$this->n+1;
+		$type=$font['type'];
+		$name=$font['name'];
+		if($type=='core')
+		{
+			//Standard font
+			$this->fonts[$k]['n']=$this->n+1;
+			$this->_newobj();
+			$this->_out('<</Type /Font');
+			$this->_out('/BaseFont /'.$name);
+			$this->_out('/Subtype /Type1');
+			if($name!='Symbol' && $name!='ZapfDingbats')
+				$this->_out('/Encoding /WinAnsiEncoding');
+			$this->_out('>>');
+			$this->_out('endobj');
+		}
+		elseif($type=='Type1' || $type=='TrueType')
+		{
+			//Additional Type1 or TrueType font
+			$this->fonts[$k]['n']=$this->n+1;
+			$this->_newobj();
+			$this->_out('<</Type /Font');
+			$this->_out('/BaseFont /'.$name);
+			$this->_out('/Subtype /'.$type);
+			$this->_out('/FirstChar 32 /LastChar 255');
+			$this->_out('/Widths '.($this->n+1).' 0 R');
+			$this->_out('/FontDescriptor '.($this->n+2).' 0 R');
+			if($font['enc'])
+			{
+				if(isset($font['diff']))
+					$this->_out('/Encoding '.($nf+$font['diff']).' 0 R');
+				else
+					$this->_out('/Encoding /WinAnsiEncoding');
+			}
+			$this->_out('>>');
+			$this->_out('endobj');
+			//Widths
+			$this->_newobj();
+			$cw=&$font['cw'];
+			$s='[';
+			for($i=32;$i<=255;$i++)
+				$s.=$cw[chr($i)].' ';
+			$this->_out($s.']');
+			$this->_out('endobj');
+			//Descriptor
+			$this->_newobj();
+			$s='<</Type /FontDescriptor /FontName /'.$name;
+			foreach($font['desc'] as $k=>$v)
+				$s.=' /'.$k.' '.$v;
+			$file=$font['file'];
+			if($file)
+				$s.=' /FontFile'.($type=='Type1' ? '' : '2').' '.$this->FontFiles[$file]['n'].' 0 R';
+			$this->_out($s.'>>');
+			$this->_out('endobj');
+		}
+		// TrueType embedded SUBSETS
+		else if ($type=='TrueTypesubset') {
+		   $ssfaid="A";
+		   include_once($this->_getfontpath().'unifont/ttfonts.php');
+		   $ttf = new TTFontFile();
+		   $ttf->getMetrics($font['file'], 1);
+		   for($sfid=0;$sfid<count($font['subsetfontids']);$sfid++) {
+			$this->fonts[$k]['n'][$sfid]=$this->n+1;		// NB an array for subset
+			$subsetname = 'MPDFA'.$ssfaid.'+'.$font['name'];
+			$ssfaid++;
+			$subset = $font['subsets'][$sfid];
+			unset($subset[0]);
+			$ttfontstream = $ttf->makeSubset($subset);
+			$ttfontsize = strlen($ttfontstream);
+			$fontstream = gzcompress($ttfontstream);
+			$widthstring = '';
+			$toUnistring = '';
+			foreach($font['subsets'][$sfid] AS $cp=>$u) {
+				if (isset($font['cw'][$u])) {
+					$widthstring .= $font['cw'][$u].' ';
+				}
+				else {
+					$widthstring .= $ttf->defaultWidth.' ';
+				}
+				$toUnistring .= sprintf("<%02s> <%04s>\n", strtoupper(dechex($cp)), strtoupper(dechex($u)));
+			}
+
+			//Additional Type1 or TrueType font
+			$this->_newobj();
+			$this->_out('<</Type /Font');
+			$this->_out('/BaseFont /'.$subsetname);
+			$this->_out('/Subtype /TrueType');
+			$this->_out('/FirstChar 0 /LastChar '.(count($font['subsets'][$sfid])));
+			$this->_out('/Widths '.($this->n+1).' 0 R');
+			$this->_out('/FontDescriptor '.($this->n+2).' 0 R');
+			$this->_out('/ToUnicode '.($this->n + 3).' 0 R');
+			$this->_out('>>');
+			$this->_out('endobj');
+
+			//Widths
+			$this->_newobj();
+			$this->_out('['.$widthstring.']');
+			$this->_out('endobj');
+
+			//Descriptor
+			$this->_newobj();
+			$s='<</Type /FontDescriptor /FontName /'.$subsetname."\n";
+			foreach($font['desc'] as $kd=>$v) {
+				if ($kd == 'Flags') { $v = $v | 4; $v = $v & ~32; }
+				$s.=' /'.$kd.' '.$v."\n";
+			}
+
+			$s.='/FontFile2 '.($this->n + 2).' 0 R';
+			$this->_out($s.'>>');
+			$this->_out('endobj');
+
+			// ToUnicode
+			$toUni = "/CIDInit /ProcSet findresource begin\n";
+			$toUni .= "12 dict begin\n";
+			$toUni .= "begincmap\n";
+			$toUni .= "/CIDSystemInfo\n";
+			$toUni .= "<</Registry (Adobe)\n";
+			$toUni .= "/Ordering (UCS)\n";
+			$toUni .= "/Supplement 0\n";
+			$toUni .= ">> def\n";
+			$toUni .= "/CMapName /Adobe-Identity-UCS def\n";
+			$toUni .= "/CMapType 2 def\n";
+			$toUni .= "1 begincodespacerange\n";
+			$toUni .= "<00> <FF>\n";
+			$toUni .= "endcodespacerange\n";
+			$toUni .= count($font['subsets'][$sfid])." beginbfchar\n";
+			$toUni .= $toUnistring;
+			$toUni .= "endbfchar\n";
+			$toUni .= "endcmap\n";
+			$toUni .= "CMapName currentdict /CMap defineresource pop\n";
+			$toUni .= "end\n";
+			$toUni .= "end";
+			$this->_newobj();
+			$this->_out('<</Length '.strlen($toUni).'>>');
+			$this->_putstream($toUni);
+			$this->_out('endobj');
+
+			//Font file 
+			$this->_newobj();
+			$this->_out('<</Length '.strlen($fontstream));
+			$this->_out('/Filter /FlateDecode');
+			$this->_out('/Length1 '.$ttfontsize);
+			$this->_out('>>');
+			$this->_putstream($fontstream);
+			$this->_out('endobj');
+		   }
+		   unset($ttf);
+		} 
+		else
+		{
+			//Allow for additional types
+			$this->fonts[$k]['n']=$this->n+1;
+			$mtd='_put'.strtolower($type);
+			if(!method_exists($this,$mtd))
+				$this->Error('Unsupported font type: '.$type);
+			$this->$mtd($font);
+		}
+	}
+}
+
+function _putimages()
+{
+	$filter=($this->compress) ? '/Filter /FlateDecode ' : '';
+	reset($this->images);
+	while(list($file,$info)=each($this->images))
+	{
+		$this->_newobj();
+		$this->images[$file]['n']=$this->n;
+		$this->_out('<</Type /XObject');
+		$this->_out('/Subtype /Image');
+		$this->_out('/Width '.$info['w']);
+		$this->_out('/Height '.$info['h']);
+		if($info['cs']=='Indexed')
+			$this->_out('/ColorSpace [/Indexed /DeviceRGB '.(strlen($info['pal'])/3-1).' '.($this->n+1).' 0 R]');
+		else
+		{
+			$this->_out('/ColorSpace /'.$info['cs']);
+			if($info['cs']=='DeviceCMYK')
+				$this->_out('/Decode [1 0 1 0 1 0 1 0]');
+		}
+		$this->_out('/BitsPerComponent '.$info['bpc']);
+		if(isset($info['f']))
+			$this->_out('/Filter /'.$info['f']);
+		if(isset($info['parms']))
+			$this->_out($info['parms']);
+		if(isset($info['trns']) && is_array($info['trns']))
+		{
+			$trns='';
+			for($i=0;$i<count($info['trns']);$i++)
+				$trns.=$info['trns'][$i].' '.$info['trns'][$i].' ';
+			$this->_out('/Mask ['.$trns.']');
+		}
+		$this->_out('/Length '.strlen($info['data']).'>>');
+		$this->_putstream($info['data']);
+		unset($this->images[$file]['data']);
+		$this->_out('endobj');
+		//Palette
+		if($info['cs']=='Indexed')
+		{
+			$this->_newobj();
+			$pal=($this->compress) ? gzcompress($info['pal']) : $info['pal'];
+			$this->_out('<<'.$filter.'/Length '.strlen($pal).'>>');
+			$this->_putstream($pal);
+			$this->_out('endobj');
+		}
+	}
+}
+
+function _putxobjectdict()
+{
+	foreach($this->images as $image)
+		$this->_out('/I'.$image['i'].' '.$image['n'].' 0 R');
+}
+
+function _putresourcedict()
+{
+	$this->_out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
+	$this->_out('/Font <<');
+	foreach($this->fonts as $font) {
+		if ($font['type']=='TrueTypesubset') {
+			foreach($font['n'] AS $k => $fid) {
+				$this->_out('/F'.$font['subsetfontids'][$k].' '.$font['n'][$k].' 0 R');
+			}
+		}
+		else { 
+			$this->_out('/F'.$font['i'].' '.$font['n'].' 0 R');
+		}
+	}
+	$this->_out('>>');
+	$this->_out('/XObject <<');
+	$this->_putxobjectdict();
+	$this->_out('>>');
+}
+
+function _putresources()
+{
+	$this->_putfonts();
+	$this->_putimages();
+	//Resource dictionary
+	$this->offsets[2]=strlen($this->buffer);
+	$this->_out('2 0 obj');
+	$this->_out('<<');
+	$this->_putresourcedict();
+	$this->_out('>>');
+	$this->_out('endobj');
+}
+
+function _putinfo()
+{
+	$this->_out('/Producer '.$this->_textstring('FPDF '.FPDF_VERSION));
+	if(!empty($this->title))
+		$this->_out('/Title '.$this->_textstring($this->title));
+	if(!empty($this->subject))
+		$this->_out('/Subject '.$this->_textstring($this->subject));
+	if(!empty($this->author))
+		$this->_out('/Author '.$this->_textstring($this->author));
+	if(!empty($this->keywords))
+		$this->_out('/Keywords '.$this->_textstring($this->keywords));
+	if(!empty($this->creator))
+		$this->_out('/Creator '.$this->_textstring($this->creator));
+	$this->_out('/CreationDate '.$this->_textstring('D:'.@date('YmdHis')));
+}
+
+function _putcatalog()
+{
+	$this->_out('/Type /Catalog');
+	$this->_out('/Pages 1 0 R');
+	if($this->ZoomMode=='fullpage')
+		$this->_out('/OpenAction [3 0 R /Fit]');
+	elseif($this->ZoomMode=='fullwidth')
+		$this->_out('/OpenAction [3 0 R /FitH null]');
+	elseif($this->ZoomMode=='real')
+		$this->_out('/OpenAction [3 0 R /XYZ null null 1]');
+	elseif(!is_string($this->ZoomMode))
+		$this->_out('/OpenAction [3 0 R /XYZ null null '.($this->ZoomMode/100).']');
+	if($this->LayoutMode=='single')
+		$this->_out('/PageLayout /SinglePage');
+	elseif($this->LayoutMode=='continuous')
+		$this->_out('/PageLayout /OneColumn');
+	elseif($this->LayoutMode=='two')
+		$this->_out('/PageLayout /TwoColumnLeft');
+}
+
+function _putheader()
+{
+	$this->_out('%PDF-'.$this->PDFVersion);
+}
+
+function _puttrailer()
+{
+	$this->_out('/Size '.($this->n+1));
+	$this->_out('/Root '.$this->n.' 0 R');
+	$this->_out('/Info '.($this->n-1).' 0 R');
+}
+
+function _enddoc()
+{
+	$this->_putheader();
+	$this->_putpages();
+	$this->_putresources();
+	//Info
+	$this->_newobj();
+	$this->_out('<<');
+	$this->_putinfo();
+	$this->_out('>>');
+	$this->_out('endobj');
+	//Catalog
+	$this->_newobj();
+	$this->_out('<<');
+	$this->_putcatalog();
+	$this->_out('>>');
+	$this->_out('endobj');
+	//Cross-ref
+	$o=strlen($this->buffer);
+	$this->_out('xref');
+	$this->_out('0 '.($this->n+1));
+	$this->_out('0000000000 65535 f ');
+	for($i=1;$i<=$this->n;$i++)
+		$this->_out(sprintf('%010d 00000 n ',$this->offsets[$i]));
+	//Trailer
+	$this->_out('trailer');
+	$this->_out('<<');
+	$this->_puttrailer();
+	$this->_out('>>');
+	$this->_out('startxref');
+	$this->_out($o);
+	$this->_out('%%EOF');
+	$this->state=3;
+}
+
+// ********* NEW FUNCTIONS *********
+
+// Convert utf-8 string to <HHHHHH> for Font Subsets
+function UTF8toSubset($str) {
+	$ret = '<';
+	if ($this->AliasNbPages) 
+		$str = preg_replace('/'.preg_quote($this->AliasNbPages,'/').'/', chr(7), $str );
+	$unicode = $this->UTF8StringToArray($str);
+	$orig_fid = $this->CurrentFont['subsetfontids'][0];
+	$last_fid = $this->CurrentFont['subsetfontids'][0];
+	foreach($unicode as $c) {
+	   if ($c == 7) { 
+			if ($orig_fid != $last_fid) {
+				$ret .= '> Tj /F'.$orig_fid.' '.$this->FontSizePt.' Tf <';
+				$last_fid = $orig_fid;
+			}
+			$ret .= '`'.$this->AliasNbPages.'`';
+			continue;
+	   }
+	   for ($i=0; $i<99; $i++) {
+		// return c as decimal char
+		$init = array_search($c, $this->CurrentFont['subsets'][$i]);
+		if ($init!==false) {
+			if ($this->CurrentFont['subsetfontids'][$i] != $last_fid) {
+				$ret .= '> Tj /F'.$this->CurrentFont['subsetfontids'][$i].' '.$this->FontSizePt.' Tf <';
+				$last_fid = $this->CurrentFont['subsetfontids'][$i];
+			}
+			$ret .= sprintf("%02s", strtoupper(dechex($init)));
+			break;
+		}
+		else if (count($this->CurrentFont['subsets'][$i]) < 255) {
+			$n = count($this->CurrentFont['subsets'][$i]);
+			$this->CurrentFont['subsets'][$i][$n] = $c;
+			if ($this->CurrentFont['subsetfontids'][$i] != $last_fid) {
+				$ret .= '> Tj /F'.$this->CurrentFont['subsetfontids'][$i].' '.$this->FontSizePt.' Tf <';
+				$last_fid = $this->CurrentFont['subsetfontids'][$i];
+			}
+			$ret .= sprintf("%02s", strtoupper(dechex($n)));
+			break;
+		}
+		else if (!isset($this->CurrentFont['subsets'][($i+1)])) {
+			$this->CurrentFont['subsets'][($i+1)] = array(0=>0);
+			$new_fid = count($this->fonts)+$this->extraFontSubsets+1;
+			$this->CurrentFont['subsetfontids'][($i+1)] = $new_fid;
+			$this->extraFontSubsets++;
+		}
+	   }
+	}
+	$ret .= '>';
+	if ($last_fid != $orig_fid) {
+		$ret .= ' Tj /F'.$orig_fid.' '.$this->FontSizePt.' Tf <> ';
+	}
+	return $ret;
+}
+
+// Converts UTF-8 strings to codepoints array
+function UTF8StringToArray($str) {
+   $out = array();
+   $len = strlen($str);
+   for ($i = 0; $i < $len; $i++) {
+      $h = ord($str[$i]);
+      if ( $h <= 0x7F )
+         $out[] = $h;
+      elseif ( $h >= 0xC2 ) {
+         if ( ($h <= 0xDF) && ($i < $len -1) )
+            $out[] = ($h & 0x1F) << 6 | (ord($str{++$i}) & 0x3F);
+         elseif ( ($h <= 0xEF) && ($i < $len -2) )
+            $out[] = ($h & 0x0F) << 12 | (ord($str{++$i}) & 0x3F) << 6
+                                       | (ord($str{++$i}) & 0x3F);
+         elseif ( ($h <= 0xF4) && ($i < $len -3) )
+            $out[] = ($h & 0x0F) << 18 | (ord($str{++$i}) & 0x3F) << 12
+                                       | (ord($str{++$i}) & 0x3F) << 6
+                                       | (ord($str{++$i}) & 0x3F);
+      }
+   }
+   return $out;
+}
+
+
+//End of class
+}
+
+//Handle special IE contype request
+if(isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT']=='contype')
+{
+	header('Content-Type: application/pdf');
+	exit;
+}
+
+?>
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/imgs/Voir_abs.png b/misc/PublicationBulletins/Portail-LeHavre/imgs/Voir_abs.png
new file mode 100755
index 0000000000000000000000000000000000000000..36de4af7e2745aa3fa8c39667f5eaa504f4edda2
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/imgs/Voir_abs.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/imgs/livre.png b/misc/PublicationBulletins/Portail-LeHavre/imgs/livre.png
new file mode 100755
index 0000000000000000000000000000000000000000..3eef403f8f56c60262b2add6b439121f9b56aa1d
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/imgs/livre.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/imgs/minus_img.png b/misc/PublicationBulletins/Portail-LeHavre/imgs/minus_img.png
new file mode 100755
index 0000000000000000000000000000000000000000..9648a2189fc9ce4a0de2a194c2b9c1d09b9ac9c5
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/imgs/minus_img.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/imgs/modifier_texte.png b/misc/PublicationBulletins/Portail-LeHavre/imgs/modifier_texte.png
new file mode 100755
index 0000000000000000000000000000000000000000..894a7eb1ce27903ba744ce1723a332b296b67ef9
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/imgs/modifier_texte.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/imgs/plus_img.png b/misc/PublicationBulletins/Portail-LeHavre/imgs/plus_img.png
new file mode 100755
index 0000000000000000000000000000000000000000..a9ee6407b2a67b6fdffdf31a495e8eea271c9c16
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/imgs/plus_img.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/index.php b/misc/PublicationBulletins/Portail-LeHavre/index.php
new file mode 100755
index 0000000000000000000000000000000000000000..bf432ff9fef4154409fb9efa9a472594144f2efe
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/index.php
@@ -0,0 +1,885 @@
+<?php
+// On d�marre la session
+session_start ();
+// Code contribu� par Yann Leboulanger (Universit� Paris 10), Juin 2013
+// Modifi� par D.SOUDIERE avec le concours de Catherine Hatinguais
+
+// Publication des notes vers les �tudiants
+// Gestion des absences: affichage et gestion des billets d'absences.
+// Les �tudiants signales les absences � venir ou pass�es et justifient en ligne puis physiquement.
+
+//  L'�tudiant est authenfi� via le CAS 
+// Le bulletin, les absences est r�cup�r� en format XML en interrogeant ScoDoc
+// Les billets sont envoy�s � Scodoc et sont g�r�s par le secr�tariat ou autre et valid�.
+// Il faut cocher la case "publier le bulletin sur le portail �tudiants" dans le semestre 
+//  ainsi que Gestion de "billets" d'absence dans les param�tres
+// Pour qu'une �valuation soit visible il faut r�gler celle ci avec la case "Visible sur bulletins" 
+//  et "Prise en compte imm�diate" ou bien que toutes cases soient remplies.
+// Il faut cr�er un utilisateur ScoDoc n'ayant que des droits de lecture.
+//
+// A adapter � vos besoins locaux.
+// penser � mettre les fichiers css et js
+
+// L authentification CAS et donc LDAP est fait par apache 
+// cf /etc/apache2/site-enable/newdi
+
+// il faut le paquet : php5-ldap
+
+
+function convertir_utf8($texte){
+$retour=htmlentities($texte,ENT_NOQUOTES,'UTF-8');
+return ($retour);
+}
+function retire_accents($str, $charset='utf-8')
+{
+    $str = htmlentities($str, ENT_NOQUOTES, $charset);
+    
+    $str = preg_replace('#&([A-za-z])(?:acute|cedil|circ|grave|orn|ring|slash|th|tilde|uml);#', '\1', $str);
+    $str = preg_replace('#&([A-za-z]{2})(?:lig);#', '\1', $str); // pour les ligatures e.g. '&oelig;'
+    $str = preg_replace('#&[^;]+;#', '', $str); // supprime les autres caract�res
+    
+    return $str;
+}
+
+
+// D�finition de la fonction d'encodage des headers
+function http_build_headers( $headers ) {
+
+       $headers_brut = '';
+
+       foreach( $headers as $nom => $valeur ) {
+               $headers_brut .= $nom . ': ' . $valeur . "\r\n";
+       }
+
+       return $headers_brut;
+}
+
+
+function get_EtudAbs_page($nip, $dept,$beg_date)
+{
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+   $end_date=date("Y-m-d");  
+    $donnees = array(
+        'format' => 'xml',
+        'code_nip' => $nip,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw,
+        'beg_date' => $beg_date,
+        'end_date' => $end_date);
+
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Absences/XMLgetAbsEtud', false, $contexte );
+
+    return ($retour);
+}
+
+
+function get_BilletAbs_list($nip, $dept)
+{
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+    $donnees = array(
+        'format' => 'xml',
+        'code_nip' => $nip,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw,
+);
+
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+//echo $sco_url . $dept . '/Scolarite/Absences/XMLgetBilletsEtud'.
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Absences/XMLgetBilletsEtud', false, $contexte );
+
+    return ($retour);
+}
+
+
+function Get_EtudAbs_billet($nip, $dept,$begin,$end,$description)
+{
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+   $end_date=date("Y-m-d"); 
+$justified="0";
+    $donnees = array(
+        'format' => 'xml',
+        'code_nip' => $nip,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw,
+        'description' =>$description,
+        'justified' =>$justified,
+        'begin' => $begin,
+        'end' => $end);
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Absences/AddBilletAbsence', false, $contexte );
+
+    return ($retour);
+}
+
+
+function get_EtudInfos_page($nip, $dept)
+{
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+
+    $donnees = array(
+        'code_nip' => $nip,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw );
+
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/XMLgetEtudInfos', false, $contexte );
+
+    return ($retour);
+}
+
+function get_bulletinetud_page($nip, $sem, $dept) {
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+    $donnees = array(
+        'format' => 'xml',
+        'code_nip' => $nip,
+        'formsemestre_id' => $sem,
+        'version' => 'selectedevals',
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw );
+
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/formsemestre_bulletinetud', false, $contexte );
+
+    return ($retour);
+}
+
+function get_semestre_info($sem, $dept)
+{
+	global $user_agent;
+    global $sco_user;
+	global $sco_pw;
+	global $sco_url;
+    $donnees = array(
+        'formsemestre_id' => $sem,
+        '__ac_name' => $sco_user,
+        '__ac_password' => $sco_pw );
+
+    // Cr�ation du contenu brut de la requ�te
+    $contenu = http_build_query( $donnees );
+
+    // D�finition des headers
+    $headers = http_build_headers( array(
+    'Content-Type' => 'application/x-www-form-urlencoded',
+    'Content-Length' => strlen( $contenu) ) );
+
+     // D�finition du contexte
+     $options = array( 'http' => array( 'user_agent' => $user_agent,
+     'method' => 'POST',
+     'content' => $contenu,
+     'header' => $headers ) );
+
+    // Cr�ation du contexte
+    $contexte = stream_context_create($options);
+
+    // Envoi du formulaire POST
+    $retour = file_get_contents( $sco_url . $dept . '/Scolarite/Notes/XMLgetFormsemestres', false, $contexte );
+
+    return ($retour);
+}
+
+function get_all_semestres($xml_data)
+{
+    $data = array();
+    $xml = simplexml_load_string($xml_data);
+    foreach ($xml->insemestre as $s) {
+        $sem = (array) $s['formsemestre_id'];
+        $data[] = $sem[0];
+    }
+    return $data;
+}
+
+function get_current_semestre($xml_data)
+{
+    $xml = simplexml_load_string($xml_data);
+    foreach ($xml->insemestre as $s) {
+        if ($s['current'] == 1){
+            $sem = (array) $s['formsemestre_id'];
+            return ($sem[0]);}    
+            else{$sem = "";
+            return ($sem);}
+    }
+    
+}
+
+function print_semestres_list($sems, $dept, $sem)
+{
+    echo 'Semestre : <select name="sem">';
+    for ($i=0; $i < count($sems); $i++) {
+        $s = $sems[$i];
+        $retour = get_semestre_info($s, $dept);
+    	$xml = simplexml_load_string($retour);
+        echo '<option value="' . $s . '"';
+        if ($s == $sem) {
+            echo ' selected';
+        }
+        echo '>' . convertir_utf8($xml->formsemestre['titre_num']) . '</option>
+';
+    }
+    echo '</select>
+<input type="submit" value="Valider"> 
+<a href="./deconnexion.php">D�connexion</a>
+</form>';
+}
+
+function print_semestre($xml_data, $sem, $dept, $show_moy=False)
+{
+    global $etudid;
+    global $nip;
+        global $sco_user;
+	global $sco_pw;
+    	global $sco_url;
+        $modules=array();
+        $codesmodules=array();
+        $i=0;
+          if($sem==""){echo '<h2> Il n&apos;y a pas de semestre courant</h2>';} else{   
+    $xml = simplexml_load_string($xml_data);
+    $etudid= $xml->etudiant['etudid'];
+
+        $retour = get_semestre_info($sem, $dept);
+    $xml2 = simplexml_load_string($retour);
+    $debut=date("Y-m-d",strtotime($xml2->formsemestre['dateord']));
+    $finsemestre=$xml2->formsemestre['date_fin_iso'];
+    $fin=strtotime($finsemestre)+3000000;
+    $day=strtotime(date("d-m-Y"));
+     $publie= $xml2->formsemestre['bul_hide_xml'];
+     
+ // teste la publication et affiche un message si non publi�
+ // $showmoy teste si on est avant date de fin du semestre
+ // si la date du jour d�passe de 45 jours la date de fin de semestre on affiche les moyennes
+ // sinon pas encore
+
+     $sexe=$xml->etudiant['sexe']; 
+     $prenom=$xml->etudiant['prenom']; 
+     $nom=$xml->etudiant['nom'];
+     $semestre=$xml2->formsemestre['titre_num']; 
+     
+        if ($publie=="0") {    
+        if (!$show_moy) {
+    echo '<p><center><div class="attention"><span style="color: red;">Les informations contenues dans ces tableaux sont
+        provisoires. L&apos;&eacute;tat n&apos;a pas valeur de bulletin de notes.</span>';}
+
+    echo '<span style="color: red;"><br>Il vous appartient de contacter vos enseignants
+        ou votre d�partement en cas de d�saccord.</span></div></center></p>';
+      
+    echo '<center><h3>' . convertir_utf8($sexe). ' ' . convertir_utf8($prenom). ' ' . convertir_utf8($nom). '</h3>';
+    //echo '<br/>';
+
+    echo '<b>'.convertir_utf8($semestre).'</b><br>';
+    if (!$show_moy) {        echo "vous avez � ce jour ".convertir_utf8($xml->absences['nbabs'])." demi-journ�es d'absences dont ".convertir_utf8($xml->absences['nbabsjust']).' justifi�es';}
+    
+       echo '
+';
+    echo ' <HR noshade size="5" width="100%" align="left" style="color: blue;">
+    </center>  <a href="#" id="toggler"><center><H3><img src="imgs/livre.png" alt="-" title="" height="20" width="20" border="0" /> Cliquez ici pour afficher/masquer le bulletin de notes </H3></center></a>
+        <div id="toggle" style="display:none;">
+
+    <table class="notes_bulletin" style="background-color: background-color: rgb(255,255,240);">
+<tr>
+  <td class="note_bold">UE</td>
+  <td class="note_bold">Code Module</td>
+    <td class="note_bold">Module</td>
+  <td class="note_bold"><a href="#" id="toggler4">Evaluation</a></td>
+  <td class="note_bold">Note</td>
+    <td class="note_bold">(Min/Max)</td>
+  <td class="note_bold">Coef</td>
+</tr>
+';
+    if ($show_moy and $fin<=$day) {
+        echo '<tr class="gt_hl notes_bulletin_row_gen" ><td  class="titre" colspan="4" >Moyenne g�n�rale:</td><td  class="note">' . $xml->note['value'] . '/20</td><td class="max">('.$xml->note['min'].'/'.$xml->note['max'].')</td><td  class="coef"></td></tr>';
+    }
+    foreach ($xml->ue as $ue) {
+        $coef = 0;
+        foreach ($ue->module as $mod) {
+        $i=$i+1;
+            $coef = $coef + strval($mod['coefficient']);
+            $modules[$i]=retire_accents($mod['titre'],'UTF-8');
+            $codesmodules[$i]=retire_accents($mod['code'],'UTF-8');
+        }
+        echo '<tr class="notes_bulletin_row_ue">
+  <td class="note_bold"><span onclick="toggle_vis_ue(this);" class="toggle_ue"><img src="imgs/minus_img.png" alt="-" title="" height="13" width="13" border="0" /></span>' . $ue['acronyme'] . '</td>
+  <td></td>
+  <td></td>
+  <td></td>
+';
+
+        if ($show_moy and $fin<=$day) {
+            echo '  <td>' . $ue->note['value'] . '</td><td class="max">('.$ue->note['min'].'/'.$ue->note['max'].')</td>
+';
+        }
+        else {
+            echo '  <td></td>
+                    <td></td>
+';
+        }
+
+echo '  <td>' . $coef . '</td>
+</tr>';
+        foreach ($ue->module as $mod) {
+            echo '<tr class="notes_bulletin_row_mod">
+  <td></td>
+  <td>' . $mod['code'] . '</td>
+   <td>' . convertir_utf8($mod['titre']) . '</td>
+  <td></td>
+';
+
+
+            echo '  <td>' . $mod->note['value'] . '</td><td class="max">('.$mod->note['min'].'/'.$mod->note['max'].')</td>
+';
+
+
+            echo '  <td>' . $mod['coefficient'] . '</td>
+</tr>';
+       
+             if (!$show_moy or $fin>$day ){
+                foreach ($mod->evaluation as $eval) {
+                if (is_numeric(strval($eval->note['value']))) {
+                $note_eval=round((strval($eval->note['value']))/20*strval($eval['note_max_origin']),2);}
+                else{$note_eval=$eval->note['value'];}
+                
+                    echo '<tr class="toggle4" >
+  <td></td>
+  <td></td>
+    <td></td>
+  <td class="bull_nom_eval">' . convertir_utf8($eval['description']) . '</td>
+  <td class="note">' . $note_eval . ' / '.strval($eval['note_max_origin']).'</td><td class="max"></td>
+  <td class="max">(' . $eval['coefficient'] . ')</td>
+</tr>';
+                } 
+            }
+        }
+    }
+    echo '</table>
+';
+$code=$xml->decision['code'];
+
+// Affichage d�cision seulement apr�s 45 jours de la fin du semestre
+    if ($show_moy and $fin<$day ) {
+        echo "<br>".convertir_utf8($xml->situation);
+    }
+    else{if($code!=""  and $fin<$day){echo "<br>". convertir_utf8($xml->situation);}}
+  
+    
+    if (!$show_moy) {    
+echo '</div>
+ <p> <HR noshade size="5" width="100%" align="left" style="color: blue;">
+<center><span style="color: blue;"> <h3>Gestion des absences</h3></span>
+<div class="attention">Les r�gles de gestion peuvent actuellement d�pendre des d�partements. <span style="text-decoration: underline;">La d�claration en ligne ne suffit pas.</span> </div>
+
+
+<a href="#" id="toggler1">
+<h4><img src="imgs/Voir_abs.png" alt="-" title="" height="30" width="30" border="0" /> Cliquez ici pour afficher/masquer la liste des absences du semestre   </h4></a></center>';
+
+   $retourabs = get_EtudAbs_page($nip, $dept,$debut);
+   $xmlabs = simplexml_load_string($retourabs);
+   
+
+   
+    echo '   
+    <div id="toggle1" style="display:none;">
+    <table class="notes_bulletin" style="background-color: background-color: rgb(255,255,240);">
+
+<tr> 
+  <td class="note_bold">Du </td>
+  <td class="note_bold">Au </td>
+    <td class="note_bold">Justifi�e</td>
+  <td class="note_bold">Motif</td>
+</tr>';   
+
+foreach ($xmlabs->abs as $abs) {
+   if($abs['justified']=="True"){$just="Oui";}else{$just="Non";}
+   if(intval(date("H", strtotime($abs['begin'])))<12){$debmatin="matin";}else{$debmatin="apr&eacute;s midi";}
+    if(intval(date("H", strtotime($abs['end'])))<12){$endmatin="matin";}else{$endmatin="apr&eacute;s midi";}
+  echo "<tr><td>". date("d-m-Y H:i:s", strtotime($abs['begin'])) . ' '.$debmatin.'</td><td> ' .  date("d-m-Y H:i:s", strtotime($abs['end'])) .' '.$endmatin. '</td><td> ' . $just. '</td><td> ' . convertir_utf8($abs['description']) ."</td></tr>";
+}
+    echo '</table>
+</div>';
+   $retourbillets = get_BilletAbs_list($nip, $dept);
+if($retourbillets!=''){
+echo '    <a href="#" id="toggler2"><center><H4> <img src="imgs/modifier_texte.png" alt="-" title="" height="30" width="30" border="0" /> D�claration d&apos;un billet d&apos;absences</H4></center></a>
+        <div class="news" id="toggle2" style="display:none;">
+<FORM method=post action=index.php>';
+
+
+echo '<span style="color: red;"><center>Imprimez par ailleurs le billet en cliquant sur  son identifiant dans le dans le tableau ci apr&egrave;s et <span style="text-decoration: underline;">d&eacute;posez le ainsi que vos justificatifs &eacute;ventuels au secr&eacute;tariat du d&eacute;partement</span>.
+<br><i>En cas d&apos;absence &agrave; un ou plusieurs contr&ocirc;les, l&apos;&eacute;tudiant(e) doit obligatoirement remplir le justificatif et fournir les documents correspondants<br> (Rappel: toute absence � une �valuation, non justifi�e dans les d�lais, est sanctionn�e par un z&eacute;ro d&eacute;finitif)</i></center></br>  </span>';
+    echo '
+<TABLE BORDER=0>
+<TR>
+<TD><span style="color: red;" style="text-decoration: underline;"> <b>V�rifiez bien les dates et heures avant validation</b></span></TD></TR>
+<TR>
+	<TD>Date et heure de d�but:</TD><TD> 
+	<INPUT type="text" name="begin" size="10" value="'.date("d-m-Y").'" class="datepicker"/>
+	</TD><TD>     
+    <SELECT name="begtime" size="1" value="08:00">
+<OPTION>08:00
+<OPTION>08:30
+<OPTION selected>08:00
+<OPTION>09:00
+<OPTION>09:30
+<OPTION>10:00
+<OPTION>10:30
+<OPTION>11:00
+<OPTION>11:30
+<OPTION>12:00
+<OPTION>12:30
+<OPTION>13:00
+<OPTION>13:30
+<OPTION>14:00
+<OPTION>14:30
+<OPTION>15:00
+<OPTION>15:30
+<OPTION>16:00
+<OPTION>16:30
+<OPTION>17:00
+<OPTION>17:30
+<OPTION>18:00
+<OPTION>18:30
+<OPTION>19:00
+<OPTION>19:30
+</SELECT>
+</TD></TR>
+<TR> 
+    <TD>Date et heure de fin:</TD><TD> 
+	<INPUT type="text" name="end" size="10" value="'.date("d-m-Y").'" class="datepicker"/>
+	</TD>
+    <TD>     
+    <SELECT name="endtime" size="1" value="18:00">
+<OPTION>08:00
+<OPTION>08:30
+<OPTION selected>18:00
+<OPTION>09:00
+<OPTION>09:30
+<OPTION>10:00
+<OPTION>10:30
+<OPTION>11:00
+<OPTION>11:30
+<OPTION>12:00
+<OPTION>12:30
+<OPTION>13:00
+<OPTION>13:30
+<OPTION>14:00
+<OPTION>14:30
+<OPTION>15:00
+<OPTION>15:30
+<OPTION>16:00
+<OPTION>16:30
+<OPTION>17:00
+<OPTION>17:30
+<OPTION>18:00
+<OPTION>18:30
+<OPTION>19:00
+<OPTION>19:30
+</SELECT>
+</TD>
+</TR>
+<TR>
+	<TD>GROUPE (TD/TP):</TD><TD> <INPUT rows="1"  type="text" name="groupe"  size="10" value=""/></INPUT></TD>
+</TABLE>
+
+	Motif (� compl�ter avec ou sans justificatif):
+    
+    <TABLE ><br><TR>
+	<TEXTAREA rows="4"  type="text" name="description"  cols="100"/> </TEXTAREA> </TR></TABLE>
+    
+
+   <TABLE >
+    <tr style="color: red;"><td>Cocher ci-dessous les mati�res concern�es par le billet</td><td>Cocher ci-dessous les contr�les concern�s</td></tr>';
+    
+$matcoche=array();
+$dscoche=array();
+  for($i=1;$i<sizeof($modules);$i++){ 
+  $matcoche[$i]=0;
+  $dscoche[$i]=0;
+  }
+  
+  for($i=1;$i<=sizeof($modules);$i++){ echo "<tr><td><input type='checkbox' name='mat".$i."' value=1>".$modules[$i]."<td><input type='checkbox' name='ds".$i."' value=1>".$modules[$i]."</td></tr>";
+  }
+echo '	</TABLE><TABLE><TR>	<TD COLSPAN=1>
+	<INPUT type="submit" name="submit" value="Envoyer" >
+	</TD>
+</TR>
+</TABLE></div>';
+
+
+
+
+if (isset($_POST['submit']) && $_POST['submit'] == "Envoyer"){
+$description=$_POST["description"];
+
+$date1 = new DateTime($_POST["begin"]);
+$date1->setTime(intval(substr($_POST["begtime"],0,2)), intval(substr($_POST["begtime"],-2)))+1;
+
+$date2 = new DateTime($_POST["end"]);
+$date2->setTime(intval(substr($_POST["endtime"],0,2)), intval(substr($_POST["endtime"],-2))-31);
+
+if(!$description){$description="Motif: ".$description."  - Mati�res: " ;}
+  for($i=1;$i<sizeof($modules);$i++){if (isset($_POST["mat".$i]))
+  {$description=$description." ".$codesmodules[$i];}}
+
+ if(substr($description,-12)=="- Mati�res: "){$description=substr($description,0,-12);} 
+$description=$description."   - Contr�les: " ;
+  for($i=1;$i<sizeof($modules);$i++)
+  {if (isset($_POST["ds".$i])){$description=$description." ".$codesmodules[$i];}
+  }
+  if(substr($description,-13)=="- Contr�les: "){$description=substr($description,0,-13);}  
+  $description=$description." (billet d�pos� le ".date("d/n/Y � H:i").")";
+  $description=utf8_encode($description);
+
+$date1=convertir_utf8(date_format($date1, 'Y-m-d H:i:s'));
+$date2=convertir_utf8(date_format($date2, 'Y-m-d H:i:s'));
+echo '</FORM>'; 
+
+  Get_EtudAbs_billet($nip, $dept,$date1 , $date2  , $description);
+ }
+
+// pour tester le renvoi des variables
+ //print_r($_POST); 
+
+echo '
+
+     <a href="#" id="toggler3" ><center><img src="imgs/Voir_abs.png" alt="-" title="" height="30" width="30" border="0" /> Cliquez ici pour afficher/masquer les billets d&apos;absences d&eacute;pos&eacute;s </center></a>
+        <div id="toggle3" style="display:none;">';
+
+   $xmlbillets = simplexml_load_string($retourbillets);
+   
+    echo '    <table class="notes_bulletin" style="background-color: background-color: rgb(255,255,240);">
+<tr>
+<td class="note_bold">Billet </td>
+  <td class="note_bold">Du </td>
+  <td class="note_bold">Au </td>
+  <td class="note_bold">Motif</td>
+    <td class="note_bold">Situation</td>
+</tr>';   
+
+foreach ($xmlbillets->row as $billets) {
+$billet=$billets->billet_id['value'];
+$motif=$billets->description['value'];
+$begbillet=$billets->abs_begin_str['value'];
+$endbillet=$billets->abs_end_str['value'];
+if (isset($_POST["groupe"])){$groupe=$_POST["groupe"];}else{$groupe=".............";}
+  
+  echo "<tr><td><img src='icons/pdficon16x20_img.png' alt='-' title='' height='15' width='15' border='0' /><a href='PDF_Billet.php?billet=".$billet."&sexe=".$sexe."&nom=".$nom."&prenom=".$prenom."&semestre=".$semestre."&groupe=".$groupe."&debutsemestre=".$debut."&finsemestre=".$finsemestre."&motif=".$motif."&debut=".$begbillet."&fin=".$endbillet."' target='_blank'>". $billet . "</a></td><td>". convertir_utf8($begbillet). '</td><td> ' .  convertir_utf8($endbillet) . '</td><td> ' .  convertir_utf8($motif) .'</td><td> ' .  convertir_utf8($billets->etat_str['value']) ."</td></tr>
+";
+}
+
+echo '  </table></div>
+'; 
+}
+
+}}
+else
+{echo '<h2> Votre d&eacute;partement n&apos;a pas autoris&eacute; l&apos;affichage des informations de ce semestre</h2>';}
+
+}}
+
+
+
+function get_dept($nip)
+{
+	global $sco_url;
+    $dept = file_get_contents( $sco_url . 'get_etud_dept?code_nip=' . $nip);
+    return ($dept);
+}
+
+
+// function pour la recuperation des infos ldap
+function search_results($info) {
+  foreach ($info as $inf) {
+    if (is_array($inf)) {
+      foreach ($inf as $key => $in) {
+        if ((count($inf[$key]) - 1) > 0) {
+          if (is_array($in)) {
+            unset($inf[$key]["count"]);
+          }
+          $results[$key] = $inf[$key];
+        }
+      }
+    }
+  }
+  $results["dn"] = explode(',', $info[0]["dn"]);
+  return $results;
+}
+
+
+
+// Programme principal
+// $nip="20121713";
+
+echo '<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Portail Webnotes</title>
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
+<meta http-equiv="Content-Style-Type" content="text/css" />
+<link href="css/scodoc.css" rel="stylesheet" type="text/css" />
+<link type="text/css" rel="stylesheet" href="libjs/jquery-ui/css/custom-theme/jquery-ui-1.7.2.custom.css" />
+<script language="javascript" type="text/javascript" src="js/bulletin.js"></script>
+<script language="javascript" type="text/javascript" src="jQuery/jquery.js"></script>
+<script language="javascript" type="text/javascript" src="jQuery/jquery-migrate-1.2.0.min.js"></script>
+<script language="javascript" type="text/javascript" src="libjs/jquery-ui/js/jquery-ui-1.7.2.custom.min.js"></script>
+<script language="javascript" type="text/javascript" src="libjs/jquery-ui/js/jquery-ui-i18n.js"></script>
+ <script language="javascript" type="text/javascript">
+           $(function() {
+		$(".datepicker").datepicker({
+                      showOn: "button", 
+                      buttonImage: "icons/calendar_img.png", 
+                      buttonImageOnly: true,
+                      dateFormat: "dd-mm-yy",   
+                      duration : "fast",                   
+                  });
+                $(".datepicker").datepicker("option", $.extend({showMonthAfterYear: false},
+				$.datepicker.regional["fr"]));
+    });
+        </script>';
+
+echo "<script type='text/javascript'>
+/* <![CDATA[ */ 
+/*
+|-----------------------------------------------------------------------
+|  jQuery Toggle Script by Matt - skyminds.net
+|-----------------------------------------------------------------------
+|
+| Affiche/cache le contenu d'un bloc une fois qu'un lien est cliqu�.
+|
+*/
+ 
+// On attend que la page soit charg�e 
+jQuery(document).ready(function()
+{
+   // On cache la zone de texte
+    jQuery('#toggle').hide();
+    jQuery('#toggle1').hide();
+    jQuery('#toggle2').hide();
+    jQuery('#toggle3').hide();
+    jQuery('#toggle4').show();
+     
+   // toggle() lorsque le lien avec l'ID #toggler est cliqu�
+   jQuery('a#toggler').click(function()
+  {
+      jQuery('#toggle').toggle(400);
+      return false;
+   });
+      jQuery('a#toggler1').click(function()
+  {
+      jQuery('#toggle1').toggle(400);
+      return false;
+   });
+      jQuery('a#toggler2').click(function()
+  {
+      jQuery('#toggle2').toggle(400);
+      return false;
+   });
+         jQuery('a#toggler3').click(function()
+  {
+      jQuery('#toggle3').toggle(400);
+      return false;
+   });
+            jQuery('a#toggler4').click(function()
+  {
+      jQuery('.toggle4').toggle(400);
+      return false;
+   });
+});
+/* ]]> */ 
+</script>
+<style>
+#toggle{height:auto; background:#eee; border:1px solid #900; margin:1em;text-align:center}
+#toggle p{text-align:center;padding:0}
+tr.toggle{height:auto; background:#eee; border:1px solid #900; margin:1em;text-align:center}
+tr.toggle p{text-align:center;padding:0}
+</style>
+        
+</head>
+<body>
+";
+
+//$user = $_SERVER['PHP_AUTH_USER'];
+//echo 'USER: '.$user."\n"."<br>";
+
+//$user = "ei121713";
+//echo "On triche USER = ".$user."\n"."<br>";
+/*
+$ds = ldap_connect("ldap://ldap");
+if ($ds) {
+	$r = ldap_bind($ds);
+	$sr = ldap_search($ds, "ou=people,dc=univ-lehavre,dc=fr", "(&(objectClass=ulhEtudiant)(uid=$user))");
+	$info = ldap_get_entries($ds, $sr);
+ 
+	//echo $info["count"]." IS Search Result(s) for \"".$user."\"\n";
+	$results = search_results($info);
+	// si pas de reponse de l a nnuaire, ce n est pas un etudiant
+	if ($info["count"] == 0 ) {
+		echo '<html>
+		<head>
+			<title>getEtud</title>
+		</head>
+		<body>
+			<H1>Service de consultation des notes</H1>
+			<div>
+			Il faut &ecirc;tre etudiant de l&apos;IUT pour acc&eacute;der &agrave; ses notes.
+			</div>
+		</body>
+		</html>';
+	} else {
+		foreach ($results as $key => $result) {
+			if ($key == 'supannetuid' ) {
+				//echo " *  ".$key." : \n";
+    				if (is_array($result)){
+					foreach($result as $res){
+						//echo "    ".$res."\n";
+					}
+				}
+				//echo "<br>";
+				$nip=$res;
+			}
+		}
+	}
+	ldap_close($ds);
+}*/
+// Login information of a scodoc user that can access notes
+$sco_user = 'lecturenotes';
+$sco_pw = 'lecture2014';
+//$sco_url = 'https://test1-scodoc.iut.univ-lehavre.fr/ScoDoc/';
+$nip="20121713";
+$sco_url = 'https://scodoc-demo.iut.univ-lehavre.fr/ScoDoc/';
+//$sco_url = 'https://scodoc.univ-lehavre.fr/ScoDoc/'; 
+$user_agent = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.1) Gecko/20061010 Firefox/2.0';
+
+echo '<form action="index.php" method="post">';
+if ($nip) {
+$dept = get_dept($nip);
+if ($dept) {
+    $retour = get_EtudInfos_page($nip, $dept);
+    $sems = get_all_semestres($retour);
+    $sem_current = get_current_semestre($retour);
+    if (isset($_POST["sem"])) {
+        $sem = $_POST["sem"];
+    }
+    else {
+        $sem = $sem_current;
+    }
+    
+    print_semestres_list($sems, $dept, $sem);
+  if($sem==""){echo '<h2> Il n&apos;y a pas de semestre en cours - Choisissez �ventuellement dans la liste.</h2>';} else{       
+    $retour = get_bulletinetud_page($nip, $sem, $dept);
+    if ($sem == $sem_current) {
+        print_semestre($retour, $sem, $dept, False);
+    }
+    else {
+        print_semestre($retour, $sem, $dept, True);
+    }
+    $erreur=0;    // Tout est OK
+}}
+else {
+    echo "Num�ro �tudiant inconnu : " . $nip . ". Contactez votre Chef de d�partement.";
+
+}}
+
+echo '</form>';
+
+
+echo  '
+          </body>
+</html>';
+
+
+?>
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/jQuery/ColVis.js b/misc/PublicationBulletins/Portail-LeHavre/jQuery/ColVis.js
new file mode 100755
index 0000000000000000000000000000000000000000..593ad472768246d5f37322bae810590ed4461327
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/jQuery/ColVis.js
@@ -0,0 +1,1133 @@
+/*
+ * File:        ColVis.js
+ * Version:     1.1.0-dev
+ * CVS:         $Id$
+ * Description: Controls for column visiblity in DataTables
+ * Author:      Allan Jardine (www.sprymedia.co.uk)
+ * Created:     Wed Sep 15 18:23:29 BST 2010
+ * Modified:    $Date$ by $Author$
+ * Language:    Javascript
+ * License:     GPL v2 or BSD 3 point style
+ * Project:     Just a little bit of fun :-)
+ * Contact:     www.sprymedia.co.uk/contact
+ *
+ * Copyright 2010-2011 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, available at:
+ *   http://datatables.net/license_gpl2
+ *   http://datatables.net/license_bsd
+ */
+
+(function($) {
+
+/**
+ * ColVis provides column visiblity control for DataTables
+ * @class ColVis
+ * @constructor
+ * @param {object} DataTables settings object
+ */
+var ColVis = function( oDTSettings, oInit )
+{
+	/* Santiy check that we are a new instance */
+	if ( !this.CLASS || this.CLASS != "ColVis" )
+	{
+		alert( "Warning: ColVis must be initialised with the keyword 'new'" );
+	}
+	
+	if ( typeof oInit == 'undefined' )
+	{
+		oInit = {};
+	}
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public class variables
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/**
+	 * @namespace Settings object which contains customisable information for ColVis instance
+	 */
+	this.s = {
+		/**
+		 * DataTables settings object
+		 *  @property dt
+		 *  @type     Object
+		 *  @default  null
+		 */
+		"dt": null,
+		
+		/**
+		 * Customisation object
+		 *  @property oInit
+		 *  @type     Object
+		 *  @default  passed in
+		 */
+		"oInit": oInit,
+		
+		/**
+		 * Callback function to tell the user when the state has changed
+		 *  @property fnStateChange
+		 *  @type     function
+		 *  @default  null
+		 */
+		"fnStateChange": null,
+		
+		/**
+		 * Mode of activation. Can be 'click' or 'mouseover'
+		 *  @property activate
+		 *  @type     String
+		 *  @default  click
+		 */
+		"activate": "click",
+		
+		/**
+		 * Position of the collection menu when shown - align "left" or "right"
+		 *  @property sAlign
+		 *  @type     String
+		 *  @default  right
+		 */
+		"sAlign": "left",
+		
+		/**
+		 * Text used for the button
+		 *  @property buttonText
+		 *  @type     String
+		 *  @default  Show / hide columns
+		 */
+		"buttonText": "Show / hide columns",
+		
+		/**
+		 * Flag to say if the collection is hidden
+		 *  @property hidden
+		 *  @type     boolean
+		 *  @default  true
+		 */
+		"hidden": true,
+		
+		/**
+		 * List of columns (integers) which should be excluded from the list
+		 *  @property aiExclude
+		 *  @type     Array
+		 *  @default  []
+		 */
+		"aiExclude": [],
+		
+		/**
+		 * Group buttons
+		 *  @property aoGroups
+		 *  @type     Array
+		 *  @default  []
+		 */
+		"aoGroups": [],
+		
+		/**
+		 * Store the original viisbility settings so they could be restored
+		 *  @property abOriginal
+		 *  @type     Array
+		 *  @default  []
+		 */
+		"abOriginal": [],
+		
+		/**
+		 * Show Show-All button
+		 *  @property bShowAll
+		 *  @type     Array
+		 *  @default  []
+		 */
+		"bShowAll": false,
+		
+		/**
+		 * Show All button text
+		 *  @property sShowAll
+		 *  @type     String
+		 *  @default  Restore original
+		 */
+		"sShowAll": "Show All",
+		
+		/**
+		 * Show restore button
+		 *  @property bRestore
+		 *  @type     Array
+		 *  @default  []
+		 */
+		"bRestore": false,
+		
+		/**
+		 * Restore button text
+		 *  @property sRestore
+		 *  @type     String
+		 *  @default  Restore original
+		 */
+		"sRestore": "Restore original",
+		
+		/**
+		 * Overlay animation duration in mS
+		 *  @property iOverlayFade
+		 *  @type     Integer
+		 *  @default  500
+		 */
+		"iOverlayFade": 500,
+		
+		/**
+		 * Label callback for column names. Takes three parameters: 1. the column index, 2. the column
+		 * title detected by DataTables and 3. the TH node for the column
+		 *  @property fnLabel
+		 *  @type     Function
+		 *  @default  null
+		 */
+		"fnLabel": null,
+		
+		/**
+		 * Indicate if ColVis should automatically calculate the size of buttons or not. The default
+		 * is for it to do so. Set to "css" to disable the automatic sizing
+		 *  @property sSize
+		 *  @type     String
+		 *  @default  auto
+		 */
+		"sSize": "auto",
+		
+		/**
+		 * Indicate if the column list should be positioned by Javascript, visually below the button
+		 * or allow CSS to do the positioning
+		 *  @property bCssPosition
+		 *  @type     boolean
+		 *  @default  false
+		 */
+		"bCssPosition": false
+	};
+	
+	
+	/**
+	 * @namespace Common and useful DOM elements for the class instance
+	 */
+	this.dom = {
+		/**
+		 * Wrapper for the button - given back to DataTables as the node to insert
+		 *  @property wrapper
+		 *  @type     Node
+		 *  @default  null
+		 */
+		"wrapper": null,
+		
+		/**
+		 * Activation button
+		 *  @property button
+		 *  @type     Node
+		 *  @default  null
+		 */
+		"button": null,
+		
+		/**
+		 * Collection list node
+		 *  @property collection
+		 *  @type     Node
+		 *  @default  null
+		 */
+		"collection": null,
+		
+		/**
+		 * Background node used for shading the display and event capturing
+		 *  @property background
+		 *  @type     Node
+		 *  @default  null
+		 */
+		"background": null,
+		
+		/**
+		 * Element to position over the activation button to catch mouse events when using mouseover
+		 *  @property catcher
+		 *  @type     Node
+		 *  @default  null
+		 */
+		"catcher": null,
+		
+		/**
+		 * List of button elements
+		 *  @property buttons
+		 *  @type     Array
+		 *  @default  []
+		 */
+		"buttons": [],
+		
+		/**
+		 * List of group button elements
+		 *  @property groupButtons
+		 *  @type     Array
+		 *  @default  []
+		 */
+		"groupButtons": [],
+
+		/**
+		 * Restore button
+		 *  @property restore
+		 *  @type     Node
+		 *  @default  null
+		 */
+		"restore": null
+	};
+	
+	/* Store global reference */
+	ColVis.aInstances.push( this );
+	
+	/* Constructor logic */
+	this.s.dt = oDTSettings;
+	this._fnConstruct();
+	return this;
+};
+
+
+
+ColVis.prototype = {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public methods
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/**
+	 * Rebuild the list of buttons for this instance (i.e. if there is a column header update)
+	 *  @method  fnRebuild
+	 *  @returns void
+	 */
+	"fnRebuild": function ()
+	{
+		/* Remove the old buttons */
+		for ( var i=this.dom.buttons.length-1 ; i>=0 ; i-- )
+		{
+			if ( this.dom.buttons[i] !== null )
+			{
+				this.dom.collection.removeChild( this.dom.buttons[i] );
+			}
+		}
+		this.dom.buttons.splice( 0, this.dom.buttons.length );
+		
+		if ( this.dom.restore )
+		{
+			this.dom.restore.parentNode( this.dom.restore );
+		}
+		
+		/* Re-add them (this is not the optimal way of doing this, it is fast and effective) */
+		this._fnAddGroups();
+		this._fnAddButtons();
+		
+		/* Update the checkboxes */
+		this._fnDrawCallback();
+	},
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods (they are of course public in JS, but recommended as private)
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/**
+	 * Constructor logic
+	 *  @method  _fnConstruct
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnConstruct": function ()
+	{
+		this._fnApplyCustomisation();
+		
+		var that = this;
+		var i, iLen;
+		this.dom.wrapper = document.createElement('div');
+		this.dom.wrapper.className = "ColVis TableTools";
+		
+		this.dom.button = this._fnDomBaseButton( this.s.buttonText );
+		this.dom.button.className += " ColVis_MasterButton";
+		this.dom.wrapper.appendChild( this.dom.button );
+		
+		this.dom.catcher = this._fnDomCatcher();
+		this.dom.collection = this._fnDomCollection();
+		this.dom.background = this._fnDomBackground();
+		
+		this._fnAddGroups();
+		this._fnAddButtons();
+		
+		/* Store the original visbility information */
+		for ( i=0, iLen=this.s.dt.aoColumns.length ; i<iLen ; i++ )
+		{
+			this.s.abOriginal.push( this.s.dt.aoColumns[i].bVisible );
+		}
+		
+		/* Update on each draw */
+		this.s.dt.aoDrawCallback.push( {
+			"fn": function () {
+				that._fnDrawCallback.call( that );
+			},
+			"sName": "ColVis"
+		} );
+
+		/* If columns are reordered, then we need to update our exclude list and
+		 * rebuild the displayed list
+		 */
+		$(this.s.dt.oInstance).bind( 'column-reorder', function ( e, oSettings, oReorder ) {
+			for ( i=0, iLen=that.s.aiExclude.length ; i<iLen ; i++ ) {
+				that.s.aiExclude[i] = oReorder.aiInvertMapping[ that.s.aiExclude[i] ];
+			}
+
+			var mStore = that.s.abOriginal.splice( oReorder.iFrom, 1 )[0];
+			that.s.abOriginal.splice( oReorder.iTo, 0, mStore );
+			
+			that.fnRebuild();
+		} );
+	},
+	
+	
+	/**
+	 * Apply any customisation to the settings from the DataTables initialisation
+	 *  @method  _fnApplyCustomisation
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnApplyCustomisation": function ()
+	{
+		var oConfig = this.s.oInit;
+		
+		if ( typeof oConfig.activate != 'undefined' )
+		{
+			this.s.activate = oConfig.activate;
+		}
+		
+		if ( typeof oConfig.buttonText != 'undefined' )
+		{
+			this.s.buttonText = oConfig.buttonText;
+		}
+		
+		if ( typeof oConfig.aiExclude != 'undefined' )
+		{
+			this.s.aiExclude = oConfig.aiExclude;
+		}
+		
+		if ( typeof oConfig.bRestore != 'undefined' )
+		{
+			this.s.bRestore = oConfig.bRestore;
+		}
+		
+		if ( typeof oConfig.sRestore != 'undefined' )
+		{
+			this.s.sRestore = oConfig.sRestore;
+		}
+		
+		if ( typeof oConfig.bShowAll != 'undefined' )
+		{
+			this.s.bShowAll = oConfig.bShowAll;
+		}
+		
+		if ( typeof oConfig.sShowAll != 'undefined' )
+		{
+			this.s.sShowAll = oConfig.sShowAll;
+		}
+		
+		if ( typeof oConfig.sAlign != 'undefined' )
+		{
+			this.s.sAlign = oConfig.sAlign;
+		}
+		
+		if ( typeof oConfig.fnStateChange != 'undefined' )
+		{
+			this.s.fnStateChange = oConfig.fnStateChange;
+		}
+		
+		if ( typeof oConfig.iOverlayFade != 'undefined' )
+		{
+			this.s.iOverlayFade = oConfig.iOverlayFade;
+		}
+		
+		if ( typeof oConfig.fnLabel != 'undefined' )
+		{
+			this.s.fnLabel = oConfig.fnLabel;
+		}
+		
+		if ( typeof oConfig.sSize != 'undefined' )
+		{
+			this.s.sSize = oConfig.sSize;
+		}
+
+		if ( typeof oConfig.bCssPosition != 'undefined' )
+		{
+			this.s.bCssPosition = oConfig.bCssPosition;
+		}
+
+		if ( typeof oConfig.aoGroups != 'undefined' )
+		{
+			this.s.aoGroups = oConfig.aoGroups;
+		}
+	},
+	
+	
+	/**
+	 * On each table draw, check the visibility checkboxes as needed. This allows any process to
+	 * update the table's column visibility and ColVis will still be accurate.
+	 *  @method  _fnDrawCallback
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnDrawCallback": function ()
+	{
+		var columns = this.s.dt.aoColumns;
+		var buttons = this.dom.buttons;
+		var groups = this.s.aoGroups;
+		
+		for ( var i=0, iLen=columns.length ; i<iLen ; i++ )
+		{
+			if ( buttons[i] !== null )
+			{
+				$('input', buttons[i]).prop( 'checked', columns[i].bVisible );
+			}
+		}
+
+		var allVisible = function ( columnIndeces ) {
+			for ( var k=0, kLen=columnIndeces.length ; k<kLen ; k++ )
+			{
+				if (  columns[columnIndeces[k]].bVisible === false ) { return false; }
+			}
+			return true;
+		};
+		var allHidden = function ( columnIndeces ) {
+			for ( var m=0 , mLen=columnIndeces.length ; m<mLen ; m++ )
+			{
+				if ( columns[columnIndeces[m]].bVisible === true ) { return false; }
+			}
+			return true;
+		};
+
+		for ( var j=0, jLen=groups.length ; j<jLen ; j++ )
+		{
+			if ( allVisible(groups[j].aiColumns) )
+			{
+				$('input', this.dom.groupButtons[j]).prop('checked', true);
+				$('input', this.dom.groupButtons[j]).prop('indeterminate', false);
+			}
+			else if ( allHidden(groups[j].aiColumns) )
+			{
+				$('input', this.dom.groupButtons[j]).prop('checked', false);
+				$('input', this.dom.groupButtons[j]).prop('indeterminate', false);
+			}
+			else
+			{
+				$('input', this.dom.groupButtons[j]).prop('indeterminate', true);
+			}
+		}
+	},
+
+
+	/**
+	 * Loop through the groups (provided in the settings) and create a button for each.
+	 *  @method  _fnAddgroups
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnAddGroups": function ()
+	{
+		var nButton;
+
+		if ( typeof this.s.aoGroups != 'undefined' )
+		{
+			for ( var i=0, iLen=this.s.aoGroups.length ; i<iLen ; i++ )
+			{
+				nButton = this._fnDomGroupButton( i );
+				this.dom.groupButtons.push( nButton );
+				this.dom.collection.appendChild( nButton );
+			}
+		}
+	},
+	
+	
+	/**
+	 * Loop through the columns in the table and as a new button for each one.
+	 *  @method  _fnAddButtons
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnAddButtons": function ()
+	{
+		var
+			nButton,
+			sExclude = ","+this.s.aiExclude.join(',')+",";
+		
+		if ( $.inArray( 'all', this.s.aiExclude ) === -1 ) {
+			for ( var i=0, iLen=this.s.dt.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( sExclude.indexOf( ","+i+"," ) == -1 )
+				{
+					nButton = this._fnDomColumnButton( i );
+					this.dom.buttons.push( nButton );
+					this.dom.collection.appendChild( nButton );
+				}
+				else
+				{
+					this.dom.buttons.push( null );
+				}
+			}
+		}
+		
+		if ( this.s.bRestore )
+		{
+			nButton = this._fnDomRestoreButton();
+			nButton.className += " ColVis_Restore";
+			this.dom.buttons.push( nButton );
+			this.dom.collection.appendChild( nButton );
+		}
+		
+		if ( this.s.bShowAll )
+		{
+			nButton = this._fnDomShowAllButton();
+			nButton.className += " ColVis_ShowAll";
+			this.dom.buttons.push( nButton );
+			this.dom.collection.appendChild( nButton );
+		}
+	},
+	
+	
+	/**
+	 * Create a button which allows a "restore" action
+	 *  @method  _fnDomRestoreButton
+	 *  @returns {Node} Created button
+	 *  @private
+	 */
+	"_fnDomRestoreButton": function ()
+	{
+		var
+			that = this,
+			nButton = document.createElement('button'),
+			nSpan = document.createElement('span');
+		
+		nButton.className = !this.s.dt.bJUI ? "ColVis_Button TableTools_Button" :
+			"ColVis_Button TableTools_Button ui-button ui-state-default";
+		nButton.appendChild( nSpan );
+		$(nSpan).html( '<span class="ColVis_title">'+this.s.sRestore+'</span>' );
+		
+		$(nButton).click( function (e) {
+			for ( var i=0, iLen=that.s.abOriginal.length ; i<iLen ; i++ )
+			{
+				that.s.dt.oInstance.fnSetColumnVis( i, that.s.abOriginal[i], false );
+			}
+			that._fnAdjustOpenRows();
+			that.s.dt.oInstance.fnAdjustColumnSizing( false );
+			that.s.dt.oInstance.fnDraw( false );
+		} );
+		
+		return nButton;
+	},
+	
+	
+	/**
+	 * Create a button which allows a "show all" action
+	 *  @method  _fnDomShowAllButton
+	 *  @returns {Node} Created button
+	 *  @private
+	 */
+	"_fnDomShowAllButton": function ()
+	{
+		var
+			that = this,
+			nButton = document.createElement('button'),
+			nSpan = document.createElement('span');
+		
+		nButton.className = !this.s.dt.bJUI ? "ColVis_Button TableTools_Button" :
+			"ColVis_Button TableTools_Button ui-button ui-state-default";
+		nButton.appendChild( nSpan );
+		$(nSpan).html( '<span class="ColVis_title">'+this.s.sShowAll+'</span>' );
+		
+		$(nButton).click( function (e) {
+			for ( var i=0, iLen=that.s.abOriginal.length ; i<iLen ; i++ )
+			{
+				if (that.s.aiExclude.indexOf(i) === -1)
+				{
+					that.s.dt.oInstance.fnSetColumnVis( i, true, false );
+				}
+			}
+			that._fnAdjustOpenRows();
+			that.s.dt.oInstance.fnAdjustColumnSizing( false );
+			that.s.dt.oInstance.fnDraw( false );
+		} );
+		
+		return nButton;
+	},
+	
+	
+	/**
+	 * Create the DOM for a show / hide group button
+	 *  @method  _fnDomGroupButton
+	 *  @param {int} i Group in question, order based on that provided in settings
+	 *  @returns {Node} Created button
+	 *  @private
+	 */
+	"_fnDomGroupButton": function ( i )
+	{
+		var
+			that = this,
+			oGroup = this.s.aoGroups[i],
+			aoColumns = this.s.dt.aoColumns,
+			nButton = document.createElement('button'),
+			nSpan = document.createElement('span'),
+			dt = this.s.dt;
+
+		nButton.className = !dt.bJUI ? "ColVis_Group ColVis_Button TableTools_Button" :
+			"ColVis_Group ColVis_Button TableTools_Button ui-button ui-state-default";
+		nButton.appendChild( nSpan );
+		var sTitle = oGroup.sTitle;
+		$(nSpan).html(
+			'<span class="ColVis_radio"><input type="checkbox"/></span>'+
+			'<span class="ColVis_title">'+sTitle+'</span>' );
+
+		$(nButton).click( function (e) {
+			var showHide = !$('input', this).is(":checked");
+			if ( e.target.nodeName.toLowerCase() == "input" )
+			{
+				showHide = $('input', this).is(":checked");
+			}
+
+			for ( var j=0 ; j < oGroup.aiColumns.length ; j++ )
+			{
+				that.s.dt.oInstance.fnSetColumnVis( oGroup.aiColumns[j], showHide );
+			}
+		});
+
+		return nButton;
+	},
+
+
+	/**
+	 * Create the DOM for a show / hide button
+	 *  @method  _fnDomColumnButton
+	 *  @param {int} i Column in question
+	 *  @returns {Node} Created button
+	 *  @private
+	 */
+	"_fnDomColumnButton": function ( i )
+	{
+		var
+			that = this,
+			oColumn = this.s.dt.aoColumns[i],
+			nButton = document.createElement('button'),
+			nSpan = document.createElement('span'),
+			dt = this.s.dt;
+		
+		nButton.className = !dt.bJUI ? "ColVis_Button TableTools_Button" :
+			"ColVis_Button TableTools_Button ui-button ui-state-default";
+		nButton.appendChild( nSpan );
+		var sTitle = this.s.fnLabel===null ? oColumn.sTitle : this.s.fnLabel( i, oColumn.sTitle, oColumn.nTh );
+		$(nSpan).html(
+			'<span class="ColVis_radio"><input type="checkbox" checked=""/></span>'+
+			'<span class="ColVis_title">'+sTitle+'</span>' );
+		
+		$(nButton).click( function (e) {
+			var showHide = !$('input', this).is(":checked");
+			if ( e.target.nodeName.toLowerCase() == "input" )
+			{
+				showHide = $('input', this).is(":checked");
+			}
+			
+			/* Need to consider the case where the initialiser created more than one table - change the
+			 * API index that DataTables is using
+			 */
+			var oldIndex = $.fn.dataTableExt.iApiIndex;
+			$.fn.dataTableExt.iApiIndex = that._fnDataTablesApiIndex.call(that);
+
+			// Optimisation for server-side processing when scrolling - don't do a full redraw
+			if ( dt.oFeatures.bServerSide && (dt.oScroll.sX !== "" || dt.oScroll.sY !== "" ) )
+			{
+				that.s.dt.oInstance.fnSetColumnVis( i, showHide, false );
+				that.s.dt.oInstance.fnAdjustColumnSizing( false );
+				that.s.dt.oInstance.oApi._fnScrollDraw( that.s.dt );
+				that._fnDrawCallback();
+			}
+			else
+			{
+				that.s.dt.oInstance.fnSetColumnVis( i, showHide );
+			}
+
+			$.fn.dataTableExt.iApiIndex = oldIndex; /* Restore */
+			
+			if ( that.s.fnStateChange !== null )
+			{
+				that.s.fnStateChange.call( that, i, showHide );
+			}
+		} );
+		
+		return nButton;
+	},
+	
+	
+	/**
+	 * Get the position in the DataTables instance array of the table for this instance of ColVis
+	 *  @method  _fnDataTablesApiIndex
+	 *  @returns {int} Index
+	 *  @private
+	 */
+	"_fnDataTablesApiIndex": function ()
+	{
+		for ( var i=0, iLen=this.s.dt.oInstance.length ; i<iLen ; i++ )
+		{
+			if ( this.s.dt.oInstance[i] == this.s.dt.nTable )
+			{
+				return i;
+			}
+		}
+		return 0;
+	},
+	
+	
+	/**
+	 * Create the DOM needed for the button and apply some base properties. All buttons start here
+	 *  @method  _fnDomBaseButton
+	 *  @param   {String} text Button text
+	 *  @returns {Node} DIV element for the button
+	 *  @private
+	 */
+	"_fnDomBaseButton": function ( text )
+	{
+		var
+			that = this,
+			nButton = document.createElement('button'),
+			nSpan = document.createElement('span'),
+			sEvent = this.s.activate=="mouseover" ? "mouseover" : "click";
+		
+		nButton.className = !this.s.dt.bJUI ? "ColVis_Button TableTools_Button" :
+			"ColVis_Button TableTools_Button ui-button ui-state-default";
+		nButton.appendChild( nSpan );
+		nSpan.innerHTML = text;
+		
+		$(nButton).bind( sEvent, function (e) {
+			that._fnCollectionShow();
+			e.preventDefault();
+		} );
+		
+		return nButton;
+	},
+	
+	
+	/**
+	 * Create the element used to contain list the columns (it is shown and hidden as needed)
+	 *  @method  _fnDomCollection
+	 *  @returns {Node} div container for the collection
+	 *  @private
+	 */
+	"_fnDomCollection": function ()
+	{
+		var that = this;
+		var nHidden = document.createElement('div');
+		nHidden.style.display = "none";
+		nHidden.className = !this.s.dt.bJUI ? "ColVis_collection TableTools_collection" :
+			"ColVis_collection TableTools_collection ui-buttonset ui-buttonset-multi";
+		
+		if ( !this.s.bCssPosition )
+		{
+			nHidden.style.position = "absolute";
+		}
+		$(nHidden).css('opacity', 0);
+		
+		return nHidden;
+	},
+	
+	
+	/**
+	 * An element to be placed on top of the activate button to catch events
+	 *  @method  _fnDomCatcher
+	 *  @returns {Node} div container for the collection
+	 *  @private
+	 */
+	"_fnDomCatcher": function ()
+	{
+		var
+			that = this,
+			nCatcher = document.createElement('div');
+		nCatcher.className = "ColVis_catcher TableTools_catcher";
+		
+		$(nCatcher).click( function () {
+			that._fnCollectionHide.call( that, null, null );
+		} );
+		
+		return nCatcher;
+	},
+	
+	
+	/**
+	 * Create the element used to shade the background, and capture hide events (it is shown and
+	 * hidden as needed)
+	 *  @method  _fnDomBackground
+	 *  @returns {Node} div container for the background
+	 *  @private
+	 */
+	"_fnDomBackground": function ()
+	{
+		var that = this;
+		
+		var background = $('<div></div>')
+			.addClass( 'ColVis_collectionBackground TableTools_collectionBackground' )
+			.css( 'opacity', 0 )
+			.click( function () {
+				that._fnCollectionHide.call( that, null, null );
+			} );
+		
+		/* When considering a mouse over action for the activation, we also consider a mouse out
+		 * which is the same as a mouse over the background - without all the messing around of
+		 * bubbling events. Use the catcher element to avoid messing around with bubbling
+		 */
+		if ( this.s.activate == "mouseover" )
+		{
+			background.mouseover( function () {
+				that.s.overcollection = false;
+				that._fnCollectionHide.call( that, null, null );
+			} );
+		}
+		
+		return background[0];
+	},
+	
+	
+	/**
+	 * Show the show / hide list and the background
+	 *  @method  _fnCollectionShow
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnCollectionShow": function ()
+	{
+		var that = this, i, iLen, iLeft;
+		var oPos = $(this.dom.button).offset();
+		var nHidden = this.dom.collection;
+		var nBackground = this.dom.background;
+		var iDivX = parseInt(oPos.left, 10);
+		var iDivY = parseInt(oPos.top + $(this.dom.button).outerHeight(), 10);
+		
+		if ( !this.s.bCssPosition )
+		{
+			nHidden.style.top = iDivY+"px";
+			nHidden.style.left = iDivX+"px";
+		}
+		
+		$(nHidden).css( {
+			'display': 'block',
+			'opacity': 0
+		} );
+		
+		nBackground.style.bottom ='0px';
+		nBackground.style.right = '0px';
+		
+		var oStyle = this.dom.catcher.style;
+		oStyle.height = $(this.dom.button).outerHeight()+"px";
+		oStyle.width = $(this.dom.button).outerWidth()+"px";
+		oStyle.top = oPos.top+"px";
+		oStyle.left = iDivX+"px";
+		
+		document.body.appendChild( nBackground );
+		document.body.appendChild( nHidden );
+		document.body.appendChild( this.dom.catcher );
+		
+		/* Resize the buttons */
+		if ( this.s.sSize == "auto" )
+		{
+			var aiSizes = [];
+			this.dom.collection.style.width = "auto";
+			for ( i=0, iLen=this.dom.buttons.length ; i<iLen ; i++ )
+			{
+				if ( this.dom.buttons[i] !== null )
+				{
+					this.dom.buttons[i].style.width = "auto";
+					aiSizes.push( $(this.dom.buttons[i]).outerWidth() );
+				}
+			}
+			var iMax = Math.max.apply(window, aiSizes);
+			for ( i=0, iLen=this.dom.buttons.length ; i<iLen ; i++ )
+			{
+				if ( this.dom.buttons[i] !== null )
+				{
+					this.dom.buttons[i].style.width = iMax+"px";
+				}
+			}
+			this.dom.collection.style.width = iMax+"px";
+		}
+		
+		/* This results in a very small delay for the end user but it allows the animation to be
+		 * much smoother. If you don't want the animation, then the setTimeout can be removed
+		 */
+		$(nHidden).animate({"opacity": 1}, that.s.iOverlayFade);
+		$(nBackground).animate({"opacity": 0.1}, that.s.iOverlayFade, 'linear', function () {
+			/* In IE6 if you set the checked attribute of a hidden checkbox, then this is not visually
+			 * reflected. As such, we need to do it here, once it is visible. Unbelievable.
+			 */
+			if ( $.browser && $.browser.msie && $.browser.version == "6.0" )
+			{
+				that._fnDrawCallback();
+			}
+		});
+		
+		/* Visual corrections to try and keep the collection visible */
+		if ( !this.s.bCssPosition )
+		{
+			iLeft = ( this.s.sAlign=="left" ) ?
+				iDivX :
+				iDivX - $(nHidden).outerWidth() + $(this.dom.button).outerWidth();
+
+			nHidden.style.left = iLeft+"px";
+
+			var iDivWidth = $(nHidden).outerWidth();
+			var iDivHeight = $(nHidden).outerHeight();
+			var iDocWidth = $(document).width();
+			
+			if ( iLeft + iDivWidth > iDocWidth )
+			{
+				nHidden.style.left = (iDocWidth-iDivWidth)+"px";
+			}
+		}
+		
+		this.s.hidden = false;
+	},
+	
+	
+	/**
+	 * Hide the show / hide list and the background
+	 *  @method  _fnCollectionHide
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnCollectionHide": function (  )
+	{
+		var that = this;
+		
+		if ( !this.s.hidden && this.dom.collection !== null )
+		{
+			this.s.hidden = true;
+			
+			$(this.dom.collection).animate({"opacity": 0}, that.s.iOverlayFade, function (e) {
+				this.style.display = "none";
+			} );
+			
+			$(this.dom.background).animate({"opacity": 0}, that.s.iOverlayFade, function (e) {
+				document.body.removeChild( that.dom.background );
+				document.body.removeChild( that.dom.catcher );
+			} );
+		}
+	},
+	
+	
+	/**
+	 * Alter the colspan on any fnOpen rows
+	 */
+	"_fnAdjustOpenRows": function ()
+	{
+		var aoOpen = this.s.dt.aoOpenRows;
+		var iVisible = this.s.dt.oApi._fnVisbleColumns( this.s.dt );
+		
+		for ( var i=0, iLen=aoOpen.length ; i<iLen ; i++ ) {
+			aoOpen[i].nTr.getElementsByTagName('td')[0].colSpan = iVisible;
+		}
+	}
+};
+
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Static object methods
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * Rebuild the collection for a given table, or all tables if no parameter given
+ *  @method  ColVis.fnRebuild
+ *  @static
+ *  @param   object oTable DataTable instance to consider - optional
+ *  @returns void
+ */
+ColVis.fnRebuild = function ( oTable )
+{
+	var nTable = null;
+	if ( typeof oTable != 'undefined' )
+	{
+		nTable = oTable.fnSettings().nTable;
+	}
+	
+	for ( var i=0, iLen=ColVis.aInstances.length ; i<iLen ; i++ )
+	{
+		if ( typeof oTable == 'undefined' || nTable == ColVis.aInstances[i].s.dt.nTable )
+		{
+			ColVis.aInstances[i].fnRebuild();
+		}
+	}
+};
+
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Static object properties
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * Collection of all ColVis instances
+ *  @property ColVis.aInstances
+ *  @static
+ *  @type     Array
+ *  @default  []
+ */
+ColVis.aInstances = [];
+
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Constants
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * Name of this class
+ *  @constant CLASS
+ *  @type     String
+ *  @default  ColVis
+ */
+ColVis.prototype.CLASS = "ColVis";
+
+
+/**
+ * ColVis version
+ *  @constant  VERSION
+ *  @type      String
+ *  @default   See code
+ */
+ColVis.VERSION = "1.1.0-dev";
+ColVis.prototype.VERSION = ColVis.VERSION;
+
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Initialisation
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/*
+ * Register a new feature with DataTables
+ */
+if ( typeof $.fn.dataTable == "function" &&
+     typeof $.fn.dataTableExt.fnVersionCheck == "function" &&
+     $.fn.dataTableExt.fnVersionCheck('1.7.0') )
+{
+	$.fn.dataTableExt.aoFeatures.push( {
+		"fnInit": function( oDTSettings ) {
+			var init = (typeof oDTSettings.oInit.oColVis == 'undefined') ?
+				{} : oDTSettings.oInit.oColVis;
+			var oColvis = new ColVis( oDTSettings, init );
+			return oColvis.dom.wrapper;
+		},
+		"cFeature": "C",
+		"sFeature": "ColVis"
+	} );
+}
+else
+{
+	alert( "Warning: ColVis requires DataTables 1.7 or greater - www.datatables.net/download");
+}
+
+
+// Make ColVis accessible from the DataTables instance
+$.fn.dataTable.ColVis = ColVis;
+
+
+})(jQuery);
diff --git a/misc/PublicationBulletins/Portail-LeHavre/jQuery/FixedColumns.js b/misc/PublicationBulletins/Portail-LeHavre/jQuery/FixedColumns.js
new file mode 100755
index 0000000000000000000000000000000000000000..eb601d0698cdf4f0e639dbec3b65550dc2c94493
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/jQuery/FixedColumns.js
@@ -0,0 +1,1226 @@
+/**
+ * @summary     FixedColumns
+ * @description Freeze columns in place on a scrolling DataTable
+ * @file        FixedColumns.js
+ * @version     2.0.3
+ * @author      Allan Jardine (www.sprymedia.co.uk)
+ * @license     GPL v2 or BSD 3 point style
+ * @contact     www.sprymedia.co.uk/contact
+ *
+ * @copyright Copyright 2010-2011 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, available at:
+ *   http://datatables.net/license_gpl2
+ *   http://datatables.net/license_bsd
+ */
+
+
+/* Global scope for FixedColumns */
+var FixedColumns;
+
+(function($, window, document) {
+
+
+/** 
+ * When making use of DataTables' x-axis scrolling feature, you may wish to 
+ * fix the left most column in place. This plug-in for DataTables provides 
+ * exactly this option (note for non-scrolling tables, please use the  
+ * FixedHeader plug-in, which can fix headers, footers and columns). Key 
+ * features include:
+ *   <ul class="limit_length">
+ *     <li>Freezes the left or right most columns to the side of the table</li>
+ *     <li>Option to freeze two or more columns</li>
+ *     <li>Full integration with DataTables' scrolling options</li>
+ *     <li>Speed - FixedColumns is fast in its operation</li>
+ *   </ul>
+ *
+ *  @class
+ *  @constructor
+ *  @param {object} oDT DataTables instance
+ *  @param {object} [oInit={}] Configuration object for FixedColumns. Options are defined by {@link FixedColumns.defaults}
+ * 
+ *  @requires jQuery 1.3+
+ *  @requires DataTables 1.8.0+
+ * 
+ *  @example
+ *  	var oTable = $('#example').dataTable( {
+ *  		"sScrollX": "100%"
+ *  	} );
+ *  	new FixedColumns( oTable );
+ */
+FixedColumns = function ( oDT, oInit ) {
+	/* Sanity check - you just know it will happen */
+	if ( ! this instanceof FixedColumns )
+	{
+		alert( "FixedColumns warning: FixedColumns must be initialised with the 'new' keyword." );
+		return;
+	}
+	
+	if ( typeof oInit == 'undefined' )
+	{
+		oInit = {};
+	}
+	
+	/**
+	 * Settings object which contains customisable information for FixedColumns instance
+	 * @namespace
+	 * @extends FixedColumns.defaults
+	 */
+	this.s = {
+		/** 
+		 * DataTables settings objects
+		 *  @type     object
+		 *  @default  Obtained from DataTables instance
+		 */
+		"dt": oDT.fnSettings(),
+		
+		/** 
+		 * Number of columns in the DataTable - stored for quick access
+		 *  @type     int
+		 *  @default  Obtained from DataTables instance
+		 */
+		"iTableColumns": oDT.fnSettings().aoColumns.length,
+		
+		/** 
+		 * Original widths of the columns as rendered by DataTables
+		 *  @type     array.<int>
+		 *  @default  []
+		 */
+		"aiWidths": [],
+		
+		/** 
+		 * Flag to indicate if we are dealing with IE6/7 as these browsers need a little hack
+		 * in the odd place
+		 *  @type     boolean
+		 *  @default  Automatically calculated
+		 *  @readonly
+		 */
+		"bOldIE": ($.browser.msie && ($.browser.version == "6.0" || $.browser.version == "7.0"))
+	};
+	
+	
+	/**
+	 * DOM elements used by the class instance
+	 * @namespace
+	 * 
+	 */
+	this.dom = {
+		/**
+		 * DataTables scrolling element
+		 *  @type     node
+		 *  @default  null
+		 */
+		"scroller": null,
+		
+		/**
+		 * DataTables header table
+		 *  @type     node
+		 *  @default  null
+		 */
+		"header": null,
+		
+		/**
+		 * DataTables body table
+		 *  @type     node
+		 *  @default  null
+		 */
+		"body": null,
+		
+		/**
+		 * DataTables footer table
+		 *  @type     node
+		 *  @default  null
+		 */
+		"footer": null,
+
+		/**
+		 * Display grid elements
+		 * @namespace
+		 */
+		"grid": {
+			/**
+			 * Grid wrapper. This is the container element for the 3x3 grid
+			 *  @type     node
+			 *  @default  null
+			 */
+			"wrapper": null,
+
+			/**
+			 * DataTables scrolling element. This element is the DataTables
+			 * component in the display grid (making up the main table - i.e.
+			 * not the fixed columns).
+			 *  @type     node
+			 *  @default  null
+			 */
+			"dt": null,
+
+			/**
+			 * Left fixed column grid components
+			 * @namespace
+			 */
+			"left": {
+				"wrapper": null,
+				"head": null,
+				"body": null,
+				"foot": null
+			},
+
+			/**
+			 * Right fixed column grid components
+			 * @namespace
+			 */
+			"right": {
+				"wrapper": null,
+				"head": null,
+				"body": null,
+				"foot": null
+			}
+		},
+		
+		/**
+		 * Cloned table nodes
+		 * @namespace
+		 */
+		"clone": {
+			/**
+			 * Left column cloned table nodes
+			 * @namespace
+			 */
+			"left": {
+				/**
+				 * Cloned header table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"header": null,
+		  	
+				/**
+				 * Cloned body table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"body": null,
+		  	
+				/**
+				 * Cloned footer table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"footer": null
+			},
+			
+			/**
+			 * Right column cloned table nodes
+			 * @namespace
+			 */
+			"right": {
+				/**
+				 * Cloned header table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"header": null,
+		  	
+				/**
+				 * Cloned body table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"body": null,
+		  	
+				/**
+				 * Cloned footer table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"footer": null
+			}
+		}
+	};
+
+	/* Attach the instance to the DataTables instance so it can be accessed easily */
+	this.s.dt.oFixedColumns = this;
+	
+	/* Let's do it */
+	this._fnConstruct( oInit );
+};
+
+
+
+FixedColumns.prototype = {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public methods
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/**
+	 * Update the fixed columns - including headers and footers. Note that FixedColumns will
+	 * automatically update the display whenever the host DataTable redraws.
+	 *  @returns {void}
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	var oFC = new FixedColumns( oTable );
+	 *  	
+	 *  	// at some later point when the table has been manipulated....
+	 *  	oFC.fnUpdate();
+	 */
+	"fnUpdate": function ()
+	{
+		this._fnDraw( true );
+	},
+	
+	
+	/**
+	 * Recalculate the resizes of the 3x3 grid that FixedColumns uses for display of the table.
+	 * This is useful if you update the width of the table container. Note that FixedColumns will
+	 * perform this function automatically when the window.resize event is fired.
+	 *  @returns {void}
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	var oFC = new FixedColumns( oTable );
+	 *  	
+	 *  	// Resize the table container and then have FixedColumns adjust its layout....
+	 *      $('#content').width( 1200 );
+	 *  	oFC.fnRedrawLayout();
+	 */
+	"fnRedrawLayout": function ()
+	{
+		this._fnGridLayout();
+	},
+	
+	
+	/**
+	 * Mark a row such that it's height should be recalculated when using 'semiauto' row
+	 * height matching. This function will have no effect when 'none' or 'auto' row height
+	 * matching is used.
+	 *  @param   {Node} nTr TR element that should have it's height recalculated
+	 *  @returns {void}
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	var oFC = new FixedColumns( oTable );
+	 *  	
+	 *  	// manipulate the table - mark the row as needing an update then update the table
+	 *  	// this allows the redraw performed by DataTables fnUpdate to recalculate the row
+	 *  	// height
+	 *  	oFC.fnRecalculateHeight();
+	 *  	oTable.fnUpdate( $('#example tbody tr:eq(0)')[0], ["insert date", 1, 2, 3 ... ]);
+	 */
+	"fnRecalculateHeight": function ( nTr )
+	{
+		nTr._DTTC_iHeight = null;
+		nTr.style.height = 'auto';
+	},
+	
+	
+	/**
+	 * Set the height of a given row - provides cross browser compatibility
+	 *  @param   {Node} nTarget TR element that should have it's height recalculated
+	 *  @param   {int} iHeight Height in pixels to set
+	 *  @returns {void}
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	var oFC = new FixedColumns( oTable );
+	 *  	
+	 *  	// You may want to do this after manipulating a row in the fixed column
+	 *  	oFC.fnSetRowHeight( $('#example tbody tr:eq(0)')[0], 50 );
+	 */
+	"fnSetRowHeight": function ( nTarget, iHeight )
+	{
+		var jqBoxHack = $(nTarget).children(':first');
+		var iBoxHack = jqBoxHack.outerHeight() - jqBoxHack.height();
+
+		/* Can we use some kind of object detection here?! This is very nasty - damn browsers */
+		if ( $.browser.mozilla || $.browser.opera )
+		{
+			nTarget.style.height = iHeight+"px";
+		}
+		else
+		{
+			$(nTarget).children().height( iHeight-iBoxHack );
+		}
+	},
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods (they are of course public in JS, but recommended as private)
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/**
+	 * Initialisation for FixedColumns
+	 *  @param   {Object} oInit User settings for initialisation
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnConstruct": function ( oInit )
+	{
+		var i, iLen, iWidth,
+			that = this;
+		
+		/* Sanity checking */
+		if ( typeof this.s.dt.oInstance.fnVersionCheck != 'function' ||
+		     this.s.dt.oInstance.fnVersionCheck( '1.8.0' ) !== true )
+		{
+			alert( "FixedColumns "+FixedColumns.VERSION+" required DataTables 1.8.0 or later. "+
+				"Please upgrade your DataTables installation" );
+			return;
+		}
+		
+		if ( this.s.dt.oScroll.sX === "" )
+		{
+			this.s.dt.oInstance.oApi._fnLog( this.s.dt, 1, "FixedColumns is not needed (no "+
+				"x-scrolling in DataTables enabled), so no action will be taken. Use 'FixedHeader' for "+
+				"column fixing when scrolling is not enabled" );
+			return;
+		}
+		
+		/* Apply the settings from the user / defaults */
+		this.s = $.extend( true, this.s, FixedColumns.defaults, oInit );
+
+		/* Set up the DOM as we need it and cache nodes */
+		this.dom.grid.dt = $(this.s.dt.nTable).parents('div.dataTables_scroll')[0];
+		this.dom.scroller = $('div.dataTables_scrollBody', this.dom.grid.dt )[0];
+
+		var iScrollWidth = $(this.dom.grid.dt).width();
+		var iLeftWidth = 0;
+		var iRightWidth = 0;
+
+		$('tbody>tr:eq(0)>td', this.s.dt.nTable).each( function (i) {
+			iWidth = $(this).outerWidth();
+			that.s.aiWidths.push( iWidth );
+			if ( i < that.s.iLeftColumns )
+			{
+				iLeftWidth += iWidth;
+			}
+			if ( that.s.iTableColumns-that.s.iRightColumns <= i )
+			{
+				iRightWidth += iWidth;
+			}
+		} );
+
+		if ( this.s.iLeftWidth === null )
+		{
+			this.s.iLeftWidth = this.s.sLeftWidth == 'fixed' ?
+				iLeftWidth : (iLeftWidth/iScrollWidth) * 100; 
+		}
+		
+		if ( this.s.iRightWidth === null )
+		{
+			this.s.iRightWidth = this.s.sRightWidth == 'fixed' ?
+				iRightWidth : (iRightWidth/iScrollWidth) * 100;
+		}
+		
+		/* Set up the DOM that we want for the fixed column layout grid */
+		this._fnGridSetup();
+
+		/* Use the DataTables API method fnSetColumnVis to hide the columns we are going to fix */
+		for ( i=0 ; i<this.s.iLeftColumns ; i++ )
+		{
+			this.s.dt.oInstance.fnSetColumnVis( i, false );
+		}
+		for ( i=this.s.iTableColumns - this.s.iRightColumns ; i<this.s.iTableColumns ; i++ )
+		{
+			this.s.dt.oInstance.fnSetColumnVis( i, false );
+		}
+
+		/* Event handlers */
+		$(this.dom.scroller).scroll( function () {
+			that.dom.grid.left.body.scrollTop = that.dom.scroller.scrollTop;
+			if ( that.s.iRightColumns > 0 )
+			{
+				that.dom.grid.right.body.scrollTop = that.dom.scroller.scrollTop;
+			}
+		} );
+
+		$(window).resize( function () {
+			that._fnGridLayout.call( that );
+		} );
+		
+		var bFirstDraw = true;
+		this.s.dt.aoDrawCallback = [ {
+			"fn": function () {
+				that._fnDraw.call( that, bFirstDraw );
+				that._fnGridHeight( that );
+				bFirstDraw = false;
+			},
+			"sName": "FixedColumns"
+		} ].concat( this.s.dt.aoDrawCallback );
+		
+		/* Get things right to start with - note that due to adjusting the columns, there must be
+		 * another redraw of the main table. It doesn't need to be a full redraw however.
+		 */
+		this._fnGridLayout();
+		this._fnGridHeight();
+		this.s.dt.oInstance.fnDraw(false);
+	},
+	
+	
+	/**
+	 * Set up the DOM for the fixed column. The way the layout works is to create a 1x3 grid
+	 * for the left column, the DataTable (for which we just reuse the scrolling element DataTable
+	 * puts into the DOM) and the right column. In each of he two fixed column elements there is a
+	 * grouping wrapper element and then a head, body and footer wrapper. In each of these we then
+	 * place the cloned header, body or footer tables. This effectively gives as 3x3 grid structure.
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnGridSetup": function ()
+	{
+		var that = this;
+
+		this.dom.body = this.s.dt.nTable;
+		this.dom.header = this.s.dt.nTHead.parentNode;
+		this.dom.header.parentNode.parentNode.style.position = "relative";
+		
+		var nSWrapper = 
+			$('<div class="DTFC_ScrollWrapper" style="position:relative; clear:both;">'+
+				'<div class="DTFC_LeftWrapper" style="position:absolute; top:0; left:0;">'+
+					'<div class="DTFC_LeftHeadWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div>'+
+					'<div class="DTFC_LeftBodyWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div>'+
+					'<div class="DTFC_LeftFootWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div>'+
+			  	'</div>'+
+				'<div class="DTFC_RightWrapper" style="position:absolute; top:0; left:0;">'+
+					'<div class="DTFC_RightHeadWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div>'+
+					'<div class="DTFC_RightBodyWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div>'+
+					'<div class="DTFC_RightFootWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div>'+
+			  	'</div>'+
+			  '</div>')[0];
+		nLeft = nSWrapper.childNodes[0];
+		nRight = nSWrapper.childNodes[1];
+
+		this.dom.grid.wrapper = nSWrapper;
+		this.dom.grid.left.wrapper = nLeft;
+		this.dom.grid.left.head = nLeft.childNodes[0];
+		this.dom.grid.left.body = nLeft.childNodes[1];
+
+		if ( this.s.iRightColumns > 0 )
+		{
+			this.dom.grid.right.wrapper = nRight;
+			this.dom.grid.right.head = nRight.childNodes[0];
+			this.dom.grid.right.body = nRight.childNodes[1];
+		}
+		
+		if ( this.s.dt.nTFoot )
+		{
+			this.dom.footer = this.s.dt.nTFoot.parentNode;
+			this.dom.grid.left.foot = nLeft.childNodes[2];
+			if ( this.s.iRightColumns > 0 )
+			{
+				this.dom.grid.right.foot = nRight.childNodes[2];
+			}
+		}
+
+		nSWrapper.appendChild( nLeft );
+		this.dom.grid.dt.parentNode.insertBefore( nSWrapper, this.dom.grid.dt );
+		nSWrapper.appendChild( this.dom.grid.dt );
+
+		this.dom.grid.dt.style.position = "absolute";
+		this.dom.grid.dt.style.top = "0px";
+		this.dom.grid.dt.style.left = this.s.iLeftWidth+"px";
+		this.dom.grid.dt.style.width = ($(this.dom.grid.dt).width()-this.s.iLeftWidth-this.s.iRightWidth)+"px";
+	},
+	
+	
+	/**
+	 * Style and position the grid used for the FixedColumns layout based on the instance settings.
+	 * Specifically sLeftWidth ('fixed' or 'absolute'), iLeftWidth (px if fixed, % if absolute) and
+	 * there 'right' counterparts.
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnGridLayout": function ()
+	{
+		var oGrid = this.dom.grid;
+		var iTotal = $(oGrid.wrapper).width();
+		var iLeft = 0, iRight = 0, iRemainder = 0;
+
+		if ( this.s.sLeftWidth == 'fixed' )
+		{
+			iLeft = this.s.iLeftWidth;
+		}
+		else
+		{
+			iLeft = ( this.s.iLeftWidth / 100 ) * iTotal;
+		}
+
+		if ( this.s.sRightWidth == 'fixed' )
+		{
+			iRight = this.s.iRightWidth;
+		}
+		else
+		{
+			iRight = ( this.s.iRightWidth / 100 ) * iTotal;
+		}
+
+		iRemainder = iTotal - iLeft - iRight;
+
+		oGrid.left.wrapper.style.width = iLeft+"px";
+		oGrid.dt.style.width = iRemainder+"px";
+		oGrid.dt.style.left = iLeft+"px";
+
+		if ( this.s.iRightColumns > 0 )
+		{
+			oGrid.right.wrapper.style.width = iRight+"px";
+			oGrid.right.wrapper.style.left = (iTotal-iRight)+"px";
+		}
+	},
+	
+	
+	/**
+	 * Recalculate and set the height of the grid components used for positioning of the 
+	 * FixedColumn display grid.
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnGridHeight": function ()
+	{
+		var oGrid = this.dom.grid;
+		var iHeight = $(this.dom.grid.dt).height();
+
+		oGrid.wrapper.style.height = iHeight+"px";
+		oGrid.left.body.style.height = $(this.dom.scroller).height()+"px";
+		oGrid.left.wrapper.style.height = iHeight+"px";
+		
+		if ( this.s.iRightColumns > 0 )
+		{
+			oGrid.right.wrapper.style.height = iHeight+"px";
+			oGrid.right.body.style.height = $(this.dom.scroller).height()+"px";
+		}
+	},
+	
+	
+	/**
+	 * Clone and position the fixed columns
+	 *  @returns {void}
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnDraw": function ( bAll )
+	{
+		this._fnCloneLeft( bAll );
+		this._fnCloneRight( bAll );
+
+		/* Draw callback function */
+		if ( this.s.fnDrawCallback !== null )
+		{
+			this.s.fnDrawCallback.call( this, this.dom.clone.left, this.dom.clone.right );
+		}
+
+		/* Event triggering */
+		$(this).trigger( 'draw', { 
+			"leftClone": this.dom.clone.left,
+			"rightClone": this.dom.clone.right
+		} );
+	},
+	
+	
+	/**
+	 * Clone the right columns
+	 *  @returns {void}
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnCloneRight": function ( bAll )
+	{
+		if ( this.s.iRightColumns <= 0 )
+		{
+			return;
+		}
+		
+		var that = this,
+			i, jq,
+			aiColumns = [];
+
+		for ( i=this.s.iTableColumns-this.s.iRightColumns ; i<this.s.iTableColumns ; i++ )
+		{
+			aiColumns.push( i );
+		}
+
+		this._fnClone( this.dom.clone.right, this.dom.grid.right, aiColumns, bAll );
+	},
+	
+	
+	/**
+	 * Clone the left columns
+	 *  @returns {void}
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnCloneLeft": function ( bAll )
+	{
+		if ( this.s.iLeftColumns <= 0 )
+		{
+			return;
+		}
+		
+		var that = this,
+			i, jq,
+			aiColumns = [];
+		
+		for ( i=0 ; i<this.s.iLeftColumns ; i++ )
+		{
+			aiColumns.push( i );
+		}
+
+		this._fnClone( this.dom.clone.left, this.dom.grid.left, aiColumns, bAll );
+	},
+	
+	
+	/**
+	 * Make a copy of the layout object for a header or footer element from DataTables. Note that
+	 * this method will clone the nodes in the layout object.
+	 *  @returns {Array} Copy of the layout array
+	 *  @param   {Object} aoOriginal Layout array from DataTables (aoHeader or aoFooter)
+	 *  @param   {Object} aiColumns Columns to copy
+	 *  @private
+	 */
+	"_fnCopyLayout": function ( aoOriginal, aiColumns )
+	{
+		var aReturn = [];
+		var aClones = [];
+		var aCloned = [];
+
+		for ( var i=0, iLen=aoOriginal.length ; i<iLen ; i++ )
+		{
+			var aRow = [];
+			aRow.nTr = $(aoOriginal[i].nTr).clone(true)[0];
+
+			for ( var j=0, jLen=this.s.iTableColumns ; j<jLen ; j++ )
+			{
+				if ( $.inArray( j, aiColumns ) === -1 )
+				{
+					continue;
+				}
+
+				var iCloned = $.inArray( aoOriginal[i][j].cell, aCloned );
+				if ( iCloned === -1 )
+				{
+					var nClone = $(aoOriginal[i][j].cell).clone(true)[0];
+					aClones.push( nClone );
+					aCloned.push( aoOriginal[i][j].cell );
+
+					aRow.push( {
+						"cell": nClone,
+						"unique": aoOriginal[i][j].unique
+					} );
+				}
+				else
+				{
+					aRow.push( {
+						"cell": aClones[ iCloned ],
+						"unique": aoOriginal[i][j].unique
+					} );
+				}
+			}
+			
+			aReturn.push( aRow );
+		}
+
+		return aReturn;
+	},
+	
+	
+	/**
+	 * Clone the DataTable nodes and place them in the DOM (sized correctly)
+	 *  @returns {void}
+	 *  @param   {Object} oClone Object containing the header, footer and body cloned DOM elements
+	 *  @param   {Object} oGrid Grid object containing the display grid elements for the cloned 
+	 *                    column (left or right)
+	 *  @param   {Array} aiColumns Column indexes which should be operated on from the DataTable
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnClone": function ( oClone, oGrid, aiColumns, bAll )
+	{
+		var that = this,
+			i, iLen, j, jLen, jq, nTarget, iColumn, nClone, iIndex;
+
+		/* 
+		 * Header
+		 */
+		if ( bAll )
+		{
+			if ( oClone.header !== null )
+			{
+				oClone.header.parentNode.removeChild( oClone.header );
+			}
+			oClone.header = $(this.dom.header).clone(true)[0];
+			oClone.header.className += " DTFC_Cloned";
+			oClone.header.style.width = "100%";
+			oGrid.head.appendChild( oClone.header );
+			
+			/* Copy the DataTables layout cache for the header for our floating column */
+			var aoCloneLayout = this._fnCopyLayout( this.s.dt.aoHeader, aiColumns );
+			var jqCloneThead = $('>thead', oClone.header);
+			jqCloneThead.empty();
+
+			/* Add the created cloned TR elements to the table */
+			for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+			{
+				jqCloneThead[0].appendChild( aoCloneLayout[i].nTr );
+			}
+
+			/* Use the handy _fnDrawHead function in DataTables to do the rowspan/colspan
+			 * calculations for us
+			 */
+			this.s.dt.oApi._fnDrawHead( this.s.dt, aoCloneLayout, true );
+		}
+		else
+		{
+			/* To ensure that we copy cell classes exactly, regardless of colspan, multiple rows
+			 * etc, we make a copy of the header from the DataTable again, but don't insert the 
+			 * cloned cells, just copy the classes across. To get the matching layout for the
+			 * fixed component, we use the DataTables _fnDetectHeader method, allowing 1:1 mapping
+			 */
+			var aoCloneLayout = this._fnCopyLayout( this.s.dt.aoHeader, aiColumns );
+			var aoCurrHeader=[];
+
+			this.s.dt.oApi._fnDetectHeader( aoCurrHeader, $('>thead', oClone.header)[0] );
+
+			for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+			{
+				for ( j=0, jLen=aoCloneLayout[i].length ; j<jLen ; j++ )
+				{
+					aoCurrHeader[i][j].cell.className = aoCloneLayout[i][j].cell.className;
+
+					// If jQuery UI theming is used we need to copy those elements as well
+					$('span.DataTables_sort_icon', aoCurrHeader[i][j].cell).each( function () {
+						this.className = $('span.DataTables_sort_icon', aoCloneLayout[i][j].cell)[0].className;
+					} );
+				}
+			}
+		}
+		this._fnEqualiseHeights( 'thead', this.dom.header, oClone.header );
+		
+		/* 
+		 * Body
+		 */
+		if ( this.s.sHeightMatch == 'auto' )
+		{
+			/* Remove any heights which have been applied already and let the browser figure it out */
+			$('>tbody>tr', that.dom.body).css('height', 'auto');
+		}
+		
+		if ( oClone.body !== null )
+		{
+			oClone.body.parentNode.removeChild( oClone.body );
+			oClone.body = null;
+		}
+		
+		oClone.body = $(this.dom.body).clone(true)[0];
+		oClone.body.className += " DTFC_Cloned";
+		oClone.body.style.paddingBottom = this.s.dt.oScroll.iBarWidth+"px";
+		oClone.body.style.marginBottom = (this.s.dt.oScroll.iBarWidth*2)+"px"; /* For IE */
+		if ( oClone.body.getAttribute('id') !== null )
+		{
+			oClone.body.removeAttribute('id');
+		}
+		
+		$('>thead>tr', oClone.body).empty();
+		$('>tfoot', oClone.body).remove();
+		
+		var nBody = $('tbody', oClone.body)[0];
+		$(nBody).empty();
+		if ( this.s.dt.aiDisplay.length > 0 )
+		{
+			/* Copy the DataTables' header elements to force the column width in exactly the
+			 * same way that DataTables does it - have the header element, apply the width and
+			 * colapse it down
+			 */
+			var nInnerThead = $('>thead>tr', oClone.body)[0];
+			for ( iIndex=0 ; iIndex<aiColumns.length ; iIndex++ )
+			{
+				iColumn = aiColumns[iIndex];
+
+				nClone = this.s.dt.aoColumns[iColumn].nTh;
+				nClone.innerHTML = "";
+
+				oStyle = nClone.style;
+				oStyle.paddingTop = "0";
+				oStyle.paddingBottom = "0";
+				oStyle.borderTopWidth = "0";
+				oStyle.borderBottomWidth = "0";
+				oStyle.height = 0;
+				oStyle.width = that.s.aiWidths[iColumn]+"px";
+
+				nInnerThead.appendChild( nClone );
+			}
+
+			/* Add in the tbody elements, cloning form the master table */
+			$('>tbody>tr', that.dom.body).each( function (z) {
+				var n = this.cloneNode(false);
+				var i = that.s.dt.oFeatures.bServerSide===false ?
+					that.s.dt.aiDisplay[ that.s.dt._iDisplayStart+z ] : z;
+				for ( iIndex=0 ; iIndex<aiColumns.length ; iIndex++ )
+				{
+					iColumn = aiColumns[iIndex];
+					if ( typeof that.s.dt.aoData[i]._anHidden[iColumn] != 'undefined' )
+					{
+						nClone = $(that.s.dt.aoData[i]._anHidden[iColumn]).clone(true)[0];
+						n.appendChild( nClone );
+					}
+				}
+				nBody.appendChild( n );
+			} );
+		}
+		else
+		{
+			$('>tbody>tr', that.dom.body).each( function (z) {
+				nClone = this.cloneNode(true);
+				nClone.className += ' DTFC_NoData';
+				$('td', nClone).html('');
+				nBody.appendChild( nClone );
+			} );
+		}
+		
+		oClone.body.style.width = "100%";
+		oGrid.body.appendChild( oClone.body );
+
+		this._fnEqualiseHeights( 'tbody', that.dom.body, oClone.body );
+		
+		/*
+		 * Footer
+		 */
+		if ( this.s.dt.nTFoot !== null )
+		{
+			if ( bAll )
+			{
+				if ( oClone.footer !== null )
+				{
+					oClone.footer.parentNode.removeChild( oClone.footer );
+				}
+				oClone.footer = $(this.dom.footer).clone(true)[0];
+				oClone.footer.className += " DTFC_Cloned";
+				oClone.footer.style.width = "100%";
+				oGrid.foot.appendChild( oClone.footer );
+
+				/* Copy the footer just like we do for the header */
+				var aoCloneLayout = this._fnCopyLayout( this.s.dt.aoFooter, aiColumns );
+				var jqCloneTfoot = $('>tfoot', oClone.footer);
+				jqCloneTfoot.empty();
+	
+				for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+				{
+					jqCloneTfoot[0].appendChild( aoCloneLayout[i].nTr );
+				}
+				this.s.dt.oApi._fnDrawHead( this.s.dt, aoCloneLayout, true );
+			}
+			else
+			{
+				var aoCloneLayout = this._fnCopyLayout( this.s.dt.aoFooter, aiColumns );
+				var aoCurrFooter=[];
+
+				this.s.dt.oApi._fnDetectHeader( aoCurrFooter, $('>tfoot', oClone.footer)[0] );
+
+				for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+				{
+					for ( j=0, jLen=aoCloneLayout[i].length ; j<jLen ; j++ )
+					{
+						aoCurrFooter[i][j].cell.className = aoCloneLayout[i][j].cell.className;
+					}
+				}
+			}
+			this._fnEqualiseHeights( 'tfoot', this.dom.footer, oClone.footer );
+		}
+
+		/* Equalise the column widths between the header footer and body - body get's priority */
+		var anUnique = this.s.dt.oApi._fnGetUniqueThs( this.s.dt, $('>thead', oClone.header)[0] );
+		$(anUnique).each( function (i) {
+			iColumn = aiColumns[i];
+			this.style.width = that.s.aiWidths[iColumn]+"px";
+		} );
+
+		if ( that.s.dt.nTFoot !== null )
+		{
+			anUnique = this.s.dt.oApi._fnGetUniqueThs( this.s.dt, $('>tfoot', oClone.footer)[0] );
+			$(anUnique).each( function (i) {
+				iColumn = aiColumns[i];
+				this.style.width = that.s.aiWidths[iColumn]+"px";
+			} );
+		}
+	},
+	
+	
+	/**
+	 * From a given table node (THEAD etc), get a list of TR direct child elements
+	 *  @param   {Node} nIn Table element to search for TR elements (THEAD, TBODY or TFOOT element)
+	 *  @returns {Array} List of TR elements found
+	 *  @private
+	 */
+	"_fnGetTrNodes": function ( nIn )
+	{
+		var aOut = [];
+		for ( var i=0, iLen=nIn.childNodes.length ; i<iLen ; i++ )
+		{
+			if ( nIn.childNodes[i].nodeName.toUpperCase() == "TR" )
+			{
+				aOut.push( nIn.childNodes[i] );
+			}
+		}
+		return aOut;
+	},
+
+	
+	/**
+	 * Equalise the heights of the rows in a given table node in a cross browser way
+	 *  @returns {void}
+	 *  @param   {String} nodeName Node type - thead, tbody or tfoot
+	 *  @param   {Node} original Original node to take the heights from
+	 *  @param   {Node} clone Copy the heights to
+	 *  @private
+	 */
+	"_fnEqualiseHeights": function ( nodeName, original, clone )
+	{
+		if ( this.s.sHeightMatch == 'none' && nodeName !== 'thead' && nodeName !== 'tfoot' )
+		{
+			return;
+		}
+		
+		var that = this,
+			i, iLen, iHeight, iHeight2, iHeightOriginal, iHeightClone,
+			rootOriginal = original.getElementsByTagName(nodeName)[0],
+			rootClone    = clone.getElementsByTagName(nodeName)[0],
+			jqBoxHack    = $('>'+nodeName+'>tr:eq(0)', original).children(':first'),
+			iBoxHack     = jqBoxHack.outerHeight() - jqBoxHack.height(),
+			anOriginal   = this._fnGetTrNodes( rootOriginal ),
+		 	anClone      = this._fnGetTrNodes( rootClone );
+		
+		for ( i=0, iLen=anClone.length ; i<iLen ; i++ )
+		{
+			if ( this.s.sHeightMatch == 'semiauto' && typeof anOriginal[i]._DTTC_iHeight != 'undefined' && 
+				anOriginal[i]._DTTC_iHeight !== null )
+			{
+				/* Oddly enough, IE / Chrome seem not to copy the style height - Mozilla and Opera keep it */
+				if ( $.browser.msie )
+				{
+					$(anClone[i]).children().height( anOriginal[i]._DTTC_iHeight-iBoxHack );
+				}
+				continue;
+			}
+			
+			iHeightOriginal = anOriginal[i].offsetHeight;
+			iHeightClone = anClone[i].offsetHeight;
+			iHeight = iHeightClone > iHeightOriginal ? iHeightClone : iHeightOriginal;
+			
+			if ( this.s.sHeightMatch == 'semiauto' )
+			{
+				anOriginal[i]._DTTC_iHeight = iHeight;
+			}
+			
+			/* Can we use some kind of object detection here?! This is very nasty - damn browsers */
+			if ( $.browser.msie && $.browser.version < 8 )
+			{
+				$(anClone[i]).children().height( iHeight-iBoxHack );
+				$(anOriginal[i]).children().height( iHeight-iBoxHack );	
+			}
+			else
+			{
+				anClone[i].style.height = iHeight+"px";
+				anOriginal[i].style.height = iHeight+"px";
+			}
+		}
+	}
+};
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Statics
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+/**
+ * FixedColumns default settings for initialisation
+ *  @namespace
+ *  @static
+ */
+FixedColumns.defaults = {
+	/** 
+	 * Number of left hand columns to fix in position
+	 *  @type     int
+	 *  @default  1
+	 *  @static
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	new FixedColumns( oTable, {
+	 *  		"iLeftColumns": 2
+	 *  	} );
+	 */
+	"iLeftColumns": 1,
+	
+	/** 
+	 * Number of right hand columns to fix in position
+	 *  @type     int
+	 *  @default  0
+	 *  @static
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	new FixedColumns( oTable, {
+	 *  		"iRightColumns": 1
+	 *  	} );
+	 */
+	"iRightColumns": 0,
+	
+	/** 
+	 * Draw callback function which is called when FixedColumns has redrawn the fixed assets
+	 *  @type     function(object, object):void
+	 *  @default  null
+	 *  @static
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	new FixedColumns( oTable, {
+	 *  		"fnDrawCallback": function () {
+	 *				alert( "FixedColumns redraw" );
+	 *			}
+	 *  	} );
+	 */
+	"fnDrawCallback": null,
+	
+	/** 
+	 * Type of left column size calculation. Can take the values of "fixed", whereby the iLeftWidth
+	 * value will be treated as a pixel value, or "relative" for which case iLeftWidth will be
+	 * treated as a percentage value.
+	 *  @type     string
+	 *  @default  fixed
+	 *  @static
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	new FixedColumns( oTable, {
+	 *  		"sLeftWidth": "relative",
+	 *  		"iLeftWidth": 10 // percentage
+	 *  	} );
+	 */
+	"sLeftWidth": "fixed",
+	
+	/** 
+	 * Width to set for the width of the left fixed column(s) - note that the behaviour of this
+	 * property is directly effected by the sLeftWidth property. If not defined then this property
+	 * is calculated automatically from what has been assigned by DataTables.
+	 *  @type     int
+	 *  @default  null
+	 *  @static
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	new FixedColumns( oTable, {
+	 *  		"iLeftWidth": 100 // pixels
+	 *  	} );
+	 */
+	"iLeftWidth": null,
+	
+	/** 
+	 * Type of right column size calculation. Can take the values of "fixed", whereby the 
+	 * iRightWidth value will be treated as a pixel value, or "relative" for which case 
+	 * iRightWidth will be treated as a percentage value.
+	 *  @type     string
+	 *  @default  fixed
+	 *  @static
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	new FixedColumns( oTable, {
+	 *  		"sRightWidth": "relative",
+	 *  		"iRightWidth": 10 // percentage
+	 *  	} );
+	 */
+	"sRightWidth": "fixed",
+	
+	/**
+	 * Width to set for the width of the right fixed column(s) - note that the behaviour of this
+	 * property is directly effected by the sRightWidth property. If not defined then this property
+	 * is calculated automatically from what has been assigned by DataTables.
+	 *  @type     int
+	 *  @default  null
+	 *  @static
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	new FixedColumns( oTable, {
+	 *  		"iRightWidth": 200 // pixels
+	 *  	} );
+	 */
+	"iRightWidth": null,
+	
+	/** 
+	 * Height matching algorthim to use. This can be "none" which will result in no height
+	 * matching being applied by FixedColumns (height matching could be forced by CSS in this
+	 * case), "semiauto" whereby the height calculation will be performed once, and the result
+	 * cached to be used again (fnRecalculateHeight can be used to force recalculation), or
+	 * "auto" when height matching is performed on every draw (slowest but must accurate)
+	 *  @type     string
+	 *  @default  semiauto
+	 *  @static
+	 *  @example
+	 *  	var oTable = $('#example').dataTable( {
+	 *  		"sScrollX": "100%"
+	 *  	} );
+	 *  	new FixedColumns( oTable, {
+	 *  		"sHeightMatch": "auto"
+	 *  	} );
+	 */
+	"sHeightMatch": "semiauto"
+};
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Constants
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+/**
+ * Name of this class
+ *  @constant CLASS
+ *  @type     String
+ *  @default  FixedColumns
+ */
+FixedColumns.prototype.CLASS = "FixedColumns";
+
+
+/**
+ * FixedColumns version
+ *  @constant  FixedColumns.VERSION
+ *  @type      String
+ *  @default   See code
+ *  @static
+ */
+FixedColumns.VERSION = "2.0.3";
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Fired events (for documentation)
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+/**
+ * Event fired whenever FixedColumns redraws the fixed columns (i.e. clones the table elements from the main DataTable). This will occur whenever the DataTable that the FixedColumns instance is attached does its own draw.
+ * @name FixedColumns#draw
+ * @event
+ * @param {event} e jQuery event object
+ * @param {object} o Event parameters from FixedColumns
+ * @param {object} o.leftClone Instance's object dom.clone.left for easy reference. This object contains references to the left fixed clumn column's nodes
+ * @param {object} o.rightClone Instance's object dom.clone.right for easy reference. This object contains references to the right fixed clumn column's nodes
+ */
+
+})(jQuery, window, document);
diff --git a/misc/PublicationBulletins/Portail-LeHavre/jQuery/TableTools.js b/misc/PublicationBulletins/Portail-LeHavre/jQuery/TableTools.js
new file mode 100755
index 0000000000000000000000000000000000000000..296d3b3b7d5ef7976353f6d344ec866940b1a1bd
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/jQuery/TableTools.js
@@ -0,0 +1,2476 @@
+/*
+ * File:        TableTools.js
+ * Version:     2.1.5
+ * Description: Tools and buttons for DataTables
+ * Author:      Allan Jardine (www.sprymedia.co.uk)
+ * Language:    Javascript
+ * License:	    GPL v2 or BSD 3 point style
+ * Project:	    DataTables
+ * 
+ * Copyright 2009-2013 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, available at:
+ *   http://datatables.net/license_gpl2
+ *   http://datatables.net/license_bsd
+ */
+
+/* Global scope for TableTools */
+var TableTools;
+
+(function($, window, document) {
+
+/** 
+ * TableTools provides flexible buttons and other tools for a DataTables enhanced table
+ * @class TableTools
+ * @constructor
+ * @param {Object} oDT DataTables instance
+ * @param {Object} oOpts TableTools options
+ * @param {String} oOpts.sSwfPath ZeroClipboard SWF path
+ * @param {String} oOpts.sRowSelect Row selection options - 'none', 'single' or 'multi'
+ * @param {Function} oOpts.fnPreRowSelect Callback function just prior to row selection
+ * @param {Function} oOpts.fnRowSelected Callback function just after row selection
+ * @param {Function} oOpts.fnRowDeselected Callback function when row is deselected
+ * @param {Array} oOpts.aButtons List of buttons to be used
+ */
+TableTools = function( oDT, oOpts )
+{
+	/* Santiy check that we are a new instance */
+	if ( ! this instanceof TableTools )
+	{
+		alert( "Warning: TableTools must be initialised with the keyword 'new'" );
+	}
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public class variables
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/**
+	 * @namespace Settings object which contains customisable information for TableTools instance
+	 */
+	this.s = {
+		/**
+		 * Store 'this' so the instance can be retrieved from the settings object
+		 * @property that
+		 * @type	 object
+		 * @default  this
+		 */
+		"that": this,
+		
+		/** 
+		 * DataTables settings objects
+		 * @property dt
+		 * @type	 object
+		 * @default  <i>From the oDT init option</i>
+		 */
+		"dt": oDT.fnSettings(),
+		
+		/**
+		 * @namespace Print specific information
+		 */
+		"print": {
+			/** 
+			 * DataTables draw 'start' point before the printing display was shown
+			 *  @property saveStart
+			 *  @type	 int
+			 *  @default  -1
+		 	 */
+		  "saveStart": -1,
+			
+			/** 
+			 * DataTables draw 'length' point before the printing display was shown
+			 *  @property saveLength
+			 *  @type	 int
+			 *  @default  -1
+		 	 */
+		  "saveLength": -1,
+		
+			/** 
+			 * Page scrolling point before the printing display was shown so it can be restored
+			 *  @property saveScroll
+			 *  @type	 int
+			 *  @default  -1
+		 	 */
+		  "saveScroll": -1,
+		
+			/** 
+			 * Wrapped function to end the print display (to maintain scope)
+			 *  @property funcEnd
+		 	 *  @type	 Function
+			 *  @default  function () {}
+		 	 */
+		  "funcEnd": function () {}
+	  },
+	
+		/**
+		 * A unique ID is assigned to each button in each instance
+		 * @property buttonCounter
+		 *  @type	 int
+		 * @default  0
+		 */
+	  "buttonCounter": 0,
+		
+		/**
+		 * @namespace Select rows specific information
+		 */
+		"select": {
+			/**
+			 * Select type - can be 'none', 'single' or 'multi'
+			 * @property type
+			 *  @type	 string
+			 * @default  ""
+			 */
+			"type": "",
+			
+			/**
+			 * Array of nodes which are currently selected
+			 *  @property selected
+			 *  @type	 array
+			 *  @default  []
+			 */
+			"selected": [],
+			
+			/**
+			 * Function to run before the selection can take place. Will cancel the select if the
+			 * function returns false
+			 *  @property preRowSelect
+			 *  @type	 Function
+			 *  @default  null
+			 */
+			"preRowSelect": null,
+			
+			/**
+			 * Function to run when a row is selected
+			 *  @property postSelected
+			 *  @type	 Function
+			 *  @default  null
+			 */
+			"postSelected": null,
+			
+			/**
+			 * Function to run when a row is deselected
+			 *  @property postDeselected
+			 *  @type	 Function
+			 *  @default  null
+			 */
+			"postDeselected": null,
+			
+			/**
+			 * Indicate if all rows are selected (needed for server-side processing)
+			 *  @property all
+			 *  @type	 boolean
+			 *  @default  false
+			 */
+			"all": false,
+			
+			/**
+			 * Class name to add to selected TR nodes
+			 *  @property selectedClass
+			 *  @type	 String
+			 *  @default  ""
+			 */
+			"selectedClass": ""
+		},
+		
+		/**
+		 * Store of the user input customisation object
+		 *  @property custom
+		 *  @type	 object
+		 *  @default  {}
+		 */
+		"custom": {},
+		
+		/**
+		 * SWF movie path
+		 *  @property swfPath
+		 *  @type	 string
+		 *  @default  ""
+		 */
+		"swfPath": "",
+		
+		/**
+		 * Default button set
+		 *  @property buttonSet
+		 *  @type	 array
+		 *  @default  []
+		 */
+		"buttonSet": [],
+		
+		/**
+		 * When there is more than one TableTools instance for a DataTable, there must be a 
+		 * master which controls events (row selection etc)
+		 *  @property master
+		 *  @type	 boolean
+		 *  @default  false
+		 */
+		"master": false,
+		
+		/**
+		 * Tag names that are used for creating collections and buttons
+		 *  @namesapce
+		 */
+		"tags": {}
+	};
+	
+	
+	/**
+	 * @namespace Common and useful DOM elements for the class instance
+	 */
+	this.dom = {
+		/**
+		 * DIV element that is create and all TableTools buttons (and their children) put into
+		 *  @property container
+		 *  @type	 node
+		 *  @default  null
+		 */
+		"container": null,
+		
+		/**
+		 * The table node to which TableTools will be applied
+		 *  @property table
+		 *  @type	 node
+		 *  @default  null
+		 */
+		"table": null,
+		
+		/**
+		 * @namespace Nodes used for the print display
+		 */
+		"print": {
+			/**
+			 * Nodes which have been removed from the display by setting them to display none
+			 *  @property hidden
+			 *  @type	 array
+		 	 *  @default  []
+			 */
+		  "hidden": [],
+			
+			/**
+			 * The information display saying telling the user about the print display
+			 *  @property message
+			 *  @type	 node
+		 	 *  @default  null
+			 */
+		  "message": null
+	  },
+		
+		/**
+		 * @namespace Nodes used for a collection display. This contains the currently used collection
+		 */
+		"collection": {
+			/**
+			 * The div wrapper containing the buttons in the collection (i.e. the menu)
+			 *  @property collection
+			 *  @type	 node
+		 	 *  @default  null
+			 */
+			"collection": null,
+			
+			/**
+			 * Background display to provide focus and capture events
+			 *  @property background
+			 *  @type	 node
+		 	 *  @default  null
+			 */
+			"background": null
+		}
+	};
+
+	/**
+	 * @namespace Name space for the classes that this TableTools instance will use
+	 * @extends TableTools.classes
+	 */
+	this.classes = $.extend( true, {}, TableTools.classes );
+	if ( this.s.dt.bJUI )
+	{
+		$.extend( true, this.classes, TableTools.classes_themeroller );
+	}
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public class methods
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/**
+	 * Retreieve the settings object from an instance
+	 *  @method fnSettings
+	 *  @returns {object} TableTools settings object
+	 */
+	this.fnSettings = function () {
+		return this.s;
+	};
+	
+	
+	/* Constructor logic */
+	if ( typeof oOpts == 'undefined' )
+	{
+		oOpts = {};
+	}
+	
+	this._fnConstruct( oOpts );
+	
+	return this;
+};
+
+
+
+TableTools.prototype = {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public methods
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/**
+	 * Retreieve the settings object from an instance
+	 *  @returns {array} List of TR nodes which are currently selected
+	 *  @param {boolean} [filtered=false] Get only selected rows which are  
+	 *    available given the filtering applied to the table. By default
+	 *    this is false -  i.e. all rows, regardless of filtering are 
+	      selected.
+	 */
+	"fnGetSelected": function ( filtered )
+	{
+		var
+			out = [],
+			data = this.s.dt.aoData,
+			displayed = this.s.dt.aiDisplay,
+			i, iLen;
+
+		if ( filtered )
+		{
+			// Only consider filtered rows
+			for ( i=0, iLen=displayed.length ; i<iLen ; i++ )
+			{
+				if ( data[ displayed[i] ]._DTTT_selected )
+				{
+					out.push( data[ displayed[i] ].nTr );
+				}
+			}
+		}
+		else
+		{
+			// Use all rows
+			for ( i=0, iLen=data.length ; i<iLen ; i++ )
+			{
+				if ( data[i]._DTTT_selected )
+				{
+					out.push( data[i].nTr );
+				}
+			}
+		}
+
+		return out;
+	},
+
+
+	/**
+	 * Get the data source objects/arrays from DataTables for the selected rows (same as
+	 * fnGetSelected followed by fnGetData on each row from the table)
+	 *  @returns {array} Data from the TR nodes which are currently selected
+	 */
+	"fnGetSelectedData": function ()
+	{
+		var out = [];
+		var data=this.s.dt.aoData;
+		var i, iLen;
+
+		for ( i=0, iLen=data.length ; i<iLen ; i++ )
+		{
+			if ( data[i]._DTTT_selected )
+			{
+				out.push( this.s.dt.oInstance.fnGetData(i) );
+			}
+		}
+
+		return out;
+	},
+	
+	
+	/**
+	 * Check to see if a current row is selected or not
+	 *  @param {Node} n TR node to check if it is currently selected or not
+	 *  @returns {Boolean} true if select, false otherwise
+	 */
+	"fnIsSelected": function ( n )
+	{
+		var pos = this.s.dt.oInstance.fnGetPosition( n );
+		return (this.s.dt.aoData[pos]._DTTT_selected===true) ? true : false;
+	},
+
+	
+	/**
+	 * Select all rows in the table
+	 *  @param {boolean} [filtered=false] Select only rows which are available 
+	 *    given the filtering applied to the table. By default this is false - 
+	 *    i.e. all rows, regardless of filtering are selected.
+	 */
+	"fnSelectAll": function ( filtered )
+	{
+		var s = this._fnGetMasterSettings();
+		
+		this._fnRowSelect( (filtered === true) ?
+			s.dt.aiDisplay :
+			s.dt.aoData
+		);
+	},
+
+	
+	/**
+	 * Deselect all rows in the table
+	 *  @param {boolean} [filtered=false] Deselect only rows which are available 
+	 *    given the filtering applied to the table. By default this is false - 
+	 *    i.e. all rows, regardless of filtering are deselected.
+	 */
+	"fnSelectNone": function ( filtered )
+	{
+		var s = this._fnGetMasterSettings();
+
+		this._fnRowDeselect( this.fnGetSelected(filtered) );
+	},
+
+	
+	/**
+	 * Select row(s)
+	 *  @param {node|object|array} n The row(s) to select. Can be a single DOM
+	 *    TR node, an array of TR nodes or a jQuery object.
+	 */
+	"fnSelect": function ( n )
+	{
+		if ( this.s.select.type == "single" )
+		{
+			this.fnSelectNone();
+			this._fnRowSelect( n );
+		}
+		else if ( this.s.select.type == "multi" )
+		{
+			this._fnRowSelect( n );
+		}
+	},
+
+	
+	/**
+	 * Deselect row(s)
+	 *  @param {node|object|array} n The row(s) to deselect. Can be a single DOM
+	 *    TR node, an array of TR nodes or a jQuery object.
+	 */
+	"fnDeselect": function ( n )
+	{
+		this._fnRowDeselect( n );
+	},
+	
+	
+	/**
+	 * Get the title of the document - useful for file names. The title is retrieved from either
+	 * the configuration object's 'title' parameter, or the HTML document title
+	 *  @param   {Object} oConfig Button configuration object
+	 *  @returns {String} Button title
+	 */
+	"fnGetTitle": function( oConfig )
+	{
+		var sTitle = "";
+		if ( typeof oConfig.sTitle != 'undefined' && oConfig.sTitle !== "" ) {
+			sTitle = oConfig.sTitle;
+		} else {
+			var anTitle = document.getElementsByTagName('title');
+			if ( anTitle.length > 0 )
+			{
+				sTitle = anTitle[0].innerHTML;
+			}
+		}
+		
+		/* Strip characters which the OS will object to - checking for UTF8 support in the scripting
+		 * engine
+		 */
+		if ( "\u00A1".toString().length < 4 ) {
+			return sTitle.replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g, "");
+		} else {
+			return sTitle.replace(/[^a-zA-Z0-9_\.,\-_ !\(\)]/g, "");
+		}
+	},
+	
+	
+	/**
+	 * Calculate a unity array with the column width by proportion for a set of columns to be
+	 * included for a button. This is particularly useful for PDF creation, where we can use the
+	 * column widths calculated by the browser to size the columns in the PDF.
+	 *  @param   {Object} oConfig Button configuration object
+	 *  @returns {Array} Unity array of column ratios
+	 */
+	"fnCalcColRatios": function ( oConfig )
+	{
+		var
+			aoCols = this.s.dt.aoColumns,
+			aColumnsInc = this._fnColumnTargets( oConfig.mColumns ),
+			aColWidths = [],
+			iWidth = 0, iTotal = 0, i, iLen;
+		
+		for ( i=0, iLen=aColumnsInc.length ; i<iLen ; i++ )
+		{
+			if ( aColumnsInc[i] )
+			{
+				iWidth = aoCols[i].nTh.offsetWidth;
+				iTotal += iWidth;
+				aColWidths.push( iWidth );
+			}
+		}
+		
+		for ( i=0, iLen=aColWidths.length ; i<iLen ; i++ )
+		{
+			aColWidths[i] = aColWidths[i] / iTotal;
+		}
+		
+		return aColWidths.join('\t');
+	},
+	
+	
+	/**
+	 * Get the information contained in a table as a string
+	 *  @param   {Object} oConfig Button configuration object
+	 *  @returns {String} Table data as a string
+	 */
+	"fnGetTableData": function ( oConfig )
+	{
+		/* In future this could be used to get data from a plain HTML source as well as DataTables */
+		if ( this.s.dt )
+		{
+			return this._fnGetDataTablesData( oConfig );
+		}
+	},
+	
+	
+	/**
+	 * Pass text to a flash button instance, which will be used on the button's click handler
+	 *  @param   {Object} clip Flash button object
+	 *  @param   {String} text Text to set
+	 */
+	"fnSetText": function ( clip, text )
+	{
+		this._fnFlashSetText( clip, text );
+	},
+	
+	
+	/**
+	 * Resize the flash elements of the buttons attached to this TableTools instance - this is
+	 * useful for when initialising TableTools when it is hidden (display:none) since sizes can't
+	 * be calculated at that time.
+	 */
+	"fnResizeButtons": function ()
+	{
+		for ( var cli in ZeroClipboard_TableTools.clients )
+		{
+			if ( cli )
+			{
+				var client = ZeroClipboard_TableTools.clients[cli];
+				if ( typeof client.domElement != 'undefined' &&
+					 client.domElement.parentNode )
+				{
+					client.positionElement();
+				}
+			}
+		}
+	},
+	
+	
+	/**
+	 * Check to see if any of the ZeroClipboard client's attached need to be resized
+	 */
+	"fnResizeRequired": function ()
+	{
+		for ( var cli in ZeroClipboard_TableTools.clients )
+		{
+			if ( cli )
+			{
+				var client = ZeroClipboard_TableTools.clients[cli];
+				if ( typeof client.domElement != 'undefined' &&
+					 client.domElement.parentNode == this.dom.container &&
+					 client.sized === false )
+				{
+					return true;
+				}
+			}
+		}
+		return false;
+	},
+	
+	
+	/**
+	 * Programmatically enable or disable the print view
+	 *  @param {boolean} [bView=true] Show the print view if true or not given. If false, then
+	 *    terminate the print view and return to normal.
+	 *  @param {object} [oConfig={}] Configuration for the print view
+	 *  @param {boolean} [oConfig.bShowAll=false] Show all rows in the table if true
+	 *  @param {string} [oConfig.sInfo] Information message, displayed as an overlay to the
+	 *    user to let them know what the print view is.
+	 *  @param {string} [oConfig.sMessage] HTML string to show at the top of the document - will
+	 *    be included in the printed document.
+	 */
+	"fnPrint": function ( bView, oConfig )
+	{
+		if ( oConfig === undefined )
+		{
+			oConfig = {};
+		}
+
+		if ( bView === undefined || bView )
+		{
+			this._fnPrintStart( oConfig );
+		}
+		else
+		{
+			this._fnPrintEnd();
+		}
+	},
+	
+	
+	/**
+	 * Show a message to the end user which is nicely styled
+	 *  @param {string} message The HTML string to show to the user
+	 *  @param {int} time The duration the message is to be shown on screen for (mS)
+	 */
+	"fnInfo": function ( message, time ) {
+		var nInfo = document.createElement( "div" );
+		nInfo.className = this.classes.print.info;
+		nInfo.innerHTML = message;
+
+		document.body.appendChild( nInfo );
+		
+		setTimeout( function() {
+			$(nInfo).fadeOut( "normal", function() {
+				document.body.removeChild( nInfo );
+			} );
+		}, time );
+	},
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods (they are of course public in JS, but recommended as private)
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/**
+	 * Constructor logic
+	 *  @method  _fnConstruct
+	 *  @param   {Object} oOpts Same as TableTools constructor
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnConstruct": function ( oOpts )
+	{
+		var that = this;
+		
+		this._fnCustomiseSettings( oOpts );
+		
+		/* Container element */
+		this.dom.container = document.createElement( this.s.tags.container );
+		this.dom.container.className = this.classes.container;
+		
+		/* Row selection config */
+		if ( this.s.select.type != 'none' )
+		{
+			this._fnRowSelectConfig();
+		}
+		
+		/* Buttons */
+		this._fnButtonDefinations( this.s.buttonSet, this.dom.container );
+		
+		/* Destructor */
+		this.s.dt.aoDestroyCallback.push( {
+			"sName": "TableTools",
+			"fn": function () {
+				$(that.s.dt.nTBody).off( 'click.DTTT_Select', 'tr' );
+				$(that.dom.container).empty();
+			}
+		} );
+	},
+	
+	
+	/**
+	 * Take the user defined settings and the default settings and combine them.
+	 *  @method  _fnCustomiseSettings
+	 *  @param   {Object} oOpts Same as TableTools constructor
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnCustomiseSettings": function ( oOpts )
+	{
+		/* Is this the master control instance or not? */
+		if ( typeof this.s.dt._TableToolsInit == 'undefined' )
+		{
+			this.s.master = true;
+			this.s.dt._TableToolsInit = true;
+		}
+		
+		/* We can use the table node from comparisons to group controls */
+		this.dom.table = this.s.dt.nTable;
+		
+		/* Clone the defaults and then the user options */
+		this.s.custom = $.extend( {}, TableTools.DEFAULTS, oOpts );
+		
+		/* Flash file location */
+		this.s.swfPath = this.s.custom.sSwfPath;
+		if ( typeof ZeroClipboard_TableTools != 'undefined' )
+		{
+			ZeroClipboard_TableTools.moviePath = this.s.swfPath;
+		}
+		
+		/* Table row selecting */
+		this.s.select.type = this.s.custom.sRowSelect;
+		this.s.select.preRowSelect = this.s.custom.fnPreRowSelect;
+		this.s.select.postSelected = this.s.custom.fnRowSelected;
+		this.s.select.postDeselected = this.s.custom.fnRowDeselected;
+
+		// Backwards compatibility - allow the user to specify a custom class in the initialiser
+		if ( this.s.custom.sSelectedClass )
+		{
+			this.classes.select.row = this.s.custom.sSelectedClass;
+		}
+
+		this.s.tags = this.s.custom.oTags;
+
+		/* Button set */
+		this.s.buttonSet = this.s.custom.aButtons;
+	},
+	
+	
+	/**
+	 * Take the user input arrays and expand them to be fully defined, and then add them to a given
+	 * DOM element
+	 *  @method  _fnButtonDefinations
+	 *  @param {array} buttonSet Set of user defined buttons
+	 *  @param {node} wrapper Node to add the created buttons to
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnButtonDefinations": function ( buttonSet, wrapper )
+	{
+		var buttonDef;
+		
+		for ( var i=0, iLen=buttonSet.length ; i<iLen ; i++ )
+		{
+			if ( typeof buttonSet[i] == "string" )
+			{
+				if ( typeof TableTools.BUTTONS[ buttonSet[i] ] == 'undefined' )
+				{
+					alert( "TableTools: Warning - unknown button type: "+buttonSet[i] );
+					continue;
+				}
+				buttonDef = $.extend( {}, TableTools.BUTTONS[ buttonSet[i] ], true );
+			}
+			else
+			{
+				if ( typeof TableTools.BUTTONS[ buttonSet[i].sExtends ] == 'undefined' )
+				{
+					alert( "TableTools: Warning - unknown button type: "+buttonSet[i].sExtends );
+					continue;
+				}
+				var o = $.extend( {}, TableTools.BUTTONS[ buttonSet[i].sExtends ], true );
+				buttonDef = $.extend( o, buttonSet[i], true );
+			}
+			
+			wrapper.appendChild( this._fnCreateButton( 
+				buttonDef, 
+				$(wrapper).hasClass(this.classes.collection.container)
+			) );
+		}
+	},
+	
+	
+	/**
+	 * Create and configure a TableTools button
+	 *  @method  _fnCreateButton
+	 *  @param   {Object} oConfig Button configuration object
+	 *  @returns {Node} Button element
+	 *  @private 
+	 */
+	"_fnCreateButton": function ( oConfig, bCollectionButton )
+	{
+	  var nButton = this._fnButtonBase( oConfig, bCollectionButton );
+		
+		if ( oConfig.sAction.match(/flash/) )
+		{
+			this._fnFlashConfig( nButton, oConfig );
+		}
+		else if ( oConfig.sAction == "text" )
+		{
+			this._fnTextConfig( nButton, oConfig );
+		}
+		else if ( oConfig.sAction == "div" )
+		{
+			this._fnTextConfig( nButton, oConfig );
+		}
+		else if ( oConfig.sAction == "collection" )
+		{
+			this._fnTextConfig( nButton, oConfig );
+			this._fnCollectionConfig( nButton, oConfig );
+		}
+		
+		return nButton;
+	},
+	
+	
+	/**
+	 * Create the DOM needed for the button and apply some base properties. All buttons start here
+	 *  @method  _fnButtonBase
+	 *  @param   {o} oConfig Button configuration object
+	 *  @returns {Node} DIV element for the button
+	 *  @private 
+	 */
+	"_fnButtonBase": function ( o, bCollectionButton )
+	{
+		var sTag, sLiner, sClass;
+
+		if ( bCollectionButton )
+		{
+			sTag = o.sTag !== "default" ? o.sTag : this.s.tags.collection.button;
+			sLiner = o.sLinerTag !== "default" ? o.sLiner : this.s.tags.collection.liner;
+			sClass = this.classes.collection.buttons.normal;
+		}
+		else
+		{
+			sTag = o.sTag !== "default" ? o.sTag : this.s.tags.button;
+			sLiner = o.sLinerTag !== "default" ? o.sLiner : this.s.tags.liner;
+			sClass = this.classes.buttons.normal;
+		}
+
+		var
+		  nButton = document.createElement( sTag ),
+		  nSpan = document.createElement( sLiner ),
+		  masterS = this._fnGetMasterSettings();
+		
+		nButton.className = sClass+" "+o.sButtonClass;
+		nButton.setAttribute('id', "ToolTables_"+this.s.dt.sInstance+"_"+masterS.buttonCounter );
+		nButton.appendChild( nSpan );
+		nSpan.innerHTML = o.sButtonText;
+		
+		masterS.buttonCounter++;
+		
+		return nButton;
+	},
+	
+	
+	/**
+	 * Get the settings object for the master instance. When more than one TableTools instance is
+	 * assigned to a DataTable, only one of them can be the 'master' (for the select rows). As such,
+	 * we will typically want to interact with that master for global properties.
+	 *  @method  _fnGetMasterSettings
+	 *  @returns {Object} TableTools settings object
+	 *  @private 
+	 */
+	"_fnGetMasterSettings": function ()
+	{
+		if ( this.s.master )
+		{
+			return this.s;
+		}
+		else
+		{
+			/* Look for the master which has the same DT as this one */
+			var instances = TableTools._aInstances;
+			for ( var i=0, iLen=instances.length ; i<iLen ; i++ )
+			{
+				if ( this.dom.table == instances[i].s.dt.nTable )
+				{
+					return instances[i].s;
+				}
+			}
+		}
+	},
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Button collection functions
+	 */
+	
+	/**
+	 * Create a collection button, when activated will present a drop down list of other buttons
+	 *  @param   {Node} nButton Button to use for the collection activation
+	 *  @param   {Object} oConfig Button configuration object
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnCollectionConfig": function ( nButton, oConfig )
+	{
+		var nHidden = document.createElement( this.s.tags.collection.container );
+		nHidden.style.display = "none";
+		nHidden.className = this.classes.collection.container;
+		oConfig._collection = nHidden;
+		document.body.appendChild( nHidden );
+		
+		this._fnButtonDefinations( oConfig.aButtons, nHidden );
+	},
+	
+	
+	/**
+	 * Show a button collection
+	 *  @param   {Node} nButton Button to use for the collection
+	 *  @param   {Object} oConfig Button configuration object
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnCollectionShow": function ( nButton, oConfig )
+	{
+		var
+			that = this,
+			oPos = $(nButton).offset(),
+			nHidden = oConfig._collection,
+			iDivX = oPos.left,
+			iDivY = oPos.top + $(nButton).outerHeight(),
+			iWinHeight = $(window).height(), iDocHeight = $(document).height(),
+		 	iWinWidth = $(window).width(), iDocWidth = $(document).width();
+		
+		nHidden.style.position = "absolute";
+		nHidden.style.left = iDivX+"px";
+		nHidden.style.top = iDivY+"px";
+		nHidden.style.display = "block";
+		$(nHidden).css('opacity',0);
+		
+		var nBackground = document.createElement('div');
+		nBackground.style.position = "absolute";
+		nBackground.style.left = "0px";
+		nBackground.style.top = "0px";
+		nBackground.style.height = ((iWinHeight>iDocHeight)? iWinHeight : iDocHeight) +"px";
+		nBackground.style.width = ((iWinWidth>iDocWidth)? iWinWidth : iDocWidth) +"px";
+		nBackground.className = this.classes.collection.background;
+		$(nBackground).css('opacity',0);
+		
+		document.body.appendChild( nBackground );
+		document.body.appendChild( nHidden );
+		
+		/* Visual corrections to try and keep the collection visible */
+		var iDivWidth = $(nHidden).outerWidth();
+		var iDivHeight = $(nHidden).outerHeight();
+		
+		if ( iDivX + iDivWidth > iDocWidth )
+		{
+			nHidden.style.left = (iDocWidth-iDivWidth)+"px";
+		}
+		
+		if ( iDivY + iDivHeight > iDocHeight )
+		{
+			nHidden.style.top = (iDivY-iDivHeight-$(nButton).outerHeight())+"px";
+		}
+	
+		this.dom.collection.collection = nHidden;
+		this.dom.collection.background = nBackground;
+		
+		/* This results in a very small delay for the end user but it allows the animation to be
+		 * much smoother. If you don't want the animation, then the setTimeout can be removed
+		 */
+		setTimeout( function () {
+			$(nHidden).animate({"opacity": 1}, 500);
+			$(nBackground).animate({"opacity": 0.25}, 500);
+		}, 10 );
+
+		/* Resize the buttons to the Flash contents fit */
+		this.fnResizeButtons();
+		
+		/* Event handler to remove the collection display */
+		$(nBackground).click( function () {
+			that._fnCollectionHide.call( that, null, null );
+		} );
+	},
+	
+	
+	/**
+	 * Hide a button collection
+	 *  @param   {Node} nButton Button to use for the collection
+	 *  @param   {Object} oConfig Button configuration object
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnCollectionHide": function ( nButton, oConfig )
+	{
+		if ( oConfig !== null && oConfig.sExtends == 'collection' )
+		{
+			return;
+		}
+		
+		if ( this.dom.collection.collection !== null )
+		{
+			$(this.dom.collection.collection).animate({"opacity": 0}, 500, function (e) {
+				this.style.display = "none";
+			} );
+			
+			$(this.dom.collection.background).animate({"opacity": 0}, 500, function (e) {
+				this.parentNode.removeChild( this );
+			} );
+			
+			this.dom.collection.collection = null;
+			this.dom.collection.background = null;
+		}
+	},
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Row selection functions
+	 */
+	
+	/**
+	 * Add event handlers to a table to allow for row selection
+	 *  @method  _fnRowSelectConfig
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnRowSelectConfig": function ()
+	{
+		if ( this.s.master )
+		{
+			var
+				that = this, 
+				i, iLen, 
+				dt = this.s.dt,
+				aoOpenRows = this.s.dt.aoOpenRows;
+			
+			$(dt.nTable).addClass( this.classes.select.table );
+			
+			$(dt.nTBody).on( 'click.DTTT_Select', 'tr', function(e) {
+				/* Sub-table must be ignored (odd that the selector won't do this with >) */
+				if ( this.parentNode != dt.nTBody )
+				{
+					return;
+				}
+				
+				/* Check that we are actually working with a DataTables controlled row */
+				if ( dt.oInstance.fnGetData(this) === null )
+				{
+				    return;
+				}
+
+				if ( that.fnIsSelected( this ) )
+				{
+					that._fnRowDeselect( this, e );
+				}
+				else if ( that.s.select.type == "single" )
+				{
+					that.fnSelectNone();
+					that._fnRowSelect( this, e );
+				}
+				else if ( that.s.select.type == "multi" )
+				{
+					that._fnRowSelect( this, e );
+				}
+			} );
+
+			// Bind a listener to the DataTable for when new rows are created.
+			// This allows rows to be visually selected when they should be and
+			// deferred rendering is used.
+			dt.oApi._fnCallbackReg( dt, 'aoRowCreatedCallback', function (tr, data, index) {
+				if ( dt.aoData[index]._DTTT_selected ) {
+					$(tr).addClass( that.classes.select.row );
+				}
+			}, 'TableTools-SelectAll' );
+		}
+	},
+
+	/**
+	 * Select rows
+	 *  @param   {*} src Rows to select - see _fnSelectData for a description of valid inputs
+	 *  @private 
+	 */
+	"_fnRowSelect": function ( src, e )
+	{
+		var
+			that = this,
+			data = this._fnSelectData( src ),
+			firstTr = data.length===0 ? null : data[0].nTr,
+			anSelected = [],
+			i, len;
+
+		// Get all the rows that will be selected
+		for ( i=0, len=data.length ; i<len ; i++ )
+		{
+			if ( data[i].nTr )
+			{
+				anSelected.push( data[i].nTr );
+			}
+		}
+		
+		// User defined pre-selection function
+		if ( this.s.select.preRowSelect !== null && !this.s.select.preRowSelect.call(this, e, anSelected, true) )
+		{
+			return;
+		}
+
+		// Mark them as selected
+		for ( i=0, len=data.length ; i<len ; i++ )
+		{
+			data[i]._DTTT_selected = true;
+
+			if ( data[i].nTr )
+			{
+				$(data[i].nTr).addClass( that.classes.select.row );
+			}
+		}
+
+		// Post-selection function
+		if ( this.s.select.postSelected !== null )
+		{
+			this.s.select.postSelected.call( this, anSelected );
+		}
+
+		TableTools._fnEventDispatch( this, 'select', anSelected, true );
+	},
+
+	/**
+	 * Deselect rows
+	 *  @param   {*} src Rows to deselect - see _fnSelectData for a description of valid inputs
+	 *  @private 
+	 */
+	"_fnRowDeselect": function ( src, e )
+	{
+		var
+			that = this,
+			data = this._fnSelectData( src ),
+			firstTr = data.length===0 ? null : data[0].nTr,
+			anDeselectedTrs = [],
+			i, len;
+
+		// Get all the rows that will be deselected
+		for ( i=0, len=data.length ; i<len ; i++ )
+		{
+			if ( data[i].nTr )
+			{
+				anDeselectedTrs.push( data[i].nTr );
+			}
+		}
+
+		// User defined pre-selection function
+		if ( this.s.select.preRowSelect !== null && !this.s.select.preRowSelect.call(this, e, anDeselectedTrs, false) )
+		{
+			return;
+		}
+
+		// Mark them as deselected
+		for ( i=0, len=data.length ; i<len ; i++ )
+		{
+			data[i]._DTTT_selected = false;
+
+			if ( data[i].nTr )
+			{
+				$(data[i].nTr).removeClass( that.classes.select.row );
+			}
+		}
+
+		// Post-deselection function
+		if ( this.s.select.postDeselected !== null )
+		{
+			this.s.select.postDeselected.call( this, anDeselectedTrs );
+		}
+
+		TableTools._fnEventDispatch( this, 'select', anDeselectedTrs, false );
+	},
+	
+	/**
+	 * Take a data source for row selection and convert it into aoData points for the DT
+	 *   @param {*} src Can be a single DOM TR node, an array of TR nodes (including a
+	 *     a jQuery object), a single aoData point from DataTables, an array of aoData
+	 *     points or an array of aoData indexes
+	 *   @returns {array} An array of aoData points
+	 */
+	"_fnSelectData": function ( src )
+	{
+		var out = [], pos, i, iLen;
+
+		if ( src.nodeName )
+		{
+			// Single node
+			pos = this.s.dt.oInstance.fnGetPosition( src );
+			out.push( this.s.dt.aoData[pos] );
+		}
+		else if ( typeof src.length !== 'undefined' )
+		{
+			// jQuery object or an array of nodes, or aoData points
+			for ( i=0, iLen=src.length ; i<iLen ; i++ )
+			{
+				if ( src[i].nodeName )
+				{
+					pos = this.s.dt.oInstance.fnGetPosition( src[i] );
+					out.push( this.s.dt.aoData[pos] );
+				}
+				else if ( typeof src[i] === 'number' )
+				{
+					out.push( this.s.dt.aoData[ src[i] ] );
+				}
+				else
+				{
+					out.push( src[i] );
+				}
+			}
+
+			return out;
+		}
+		else
+		{
+			// A single aoData point
+			out.push( src );
+		}
+
+		return out;
+	},
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Text button functions
+	 */
+	
+	/**
+	 * Configure a text based button for interaction events
+	 *  @method  _fnTextConfig
+	 *  @param   {Node} nButton Button element which is being considered
+	 *  @param   {Object} oConfig Button configuration object
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnTextConfig": function ( nButton, oConfig )
+	{
+		var that = this;
+		
+		if ( oConfig.fnInit !== null )
+		{
+			oConfig.fnInit.call( this, nButton, oConfig );
+		}
+		
+		if ( oConfig.sToolTip !== "" )
+		{
+			nButton.title = oConfig.sToolTip;
+		}
+		
+		$(nButton).hover( function () {
+			if ( oConfig.fnMouseover !== null )
+			{
+				oConfig.fnMouseover.call( this, nButton, oConfig, null );
+			}
+		}, function () {
+			if ( oConfig.fnMouseout !== null )
+			{
+				oConfig.fnMouseout.call( this, nButton, oConfig, null );
+			}
+		} );
+		
+		if ( oConfig.fnSelect !== null )
+		{
+			TableTools._fnEventListen( this, 'select', function (n) {
+				oConfig.fnSelect.call( that, nButton, oConfig, n );
+			} );
+		}
+		
+		$(nButton).click( function (e) {
+			//e.preventDefault();
+			
+			if ( oConfig.fnClick !== null )
+			{
+				oConfig.fnClick.call( that, nButton, oConfig, null, e );
+			}
+			
+			/* Provide a complete function to match the behaviour of the flash elements */
+			if ( oConfig.fnComplete !== null )
+			{
+				oConfig.fnComplete.call( that, nButton, oConfig, null, null );
+			}
+			
+			that._fnCollectionHide( nButton, oConfig );
+		} );
+	},
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Flash button functions
+	 */
+	
+	/**
+	 * Configure a flash based button for interaction events
+	 *  @method  _fnFlashConfig
+	 *  @param   {Node} nButton Button element which is being considered
+	 *  @param   {o} oConfig Button configuration object
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnFlashConfig": function ( nButton, oConfig )
+	{
+		var that = this;
+		var flash = new ZeroClipboard_TableTools.Client();
+		
+		if ( oConfig.fnInit !== null )
+		{
+			oConfig.fnInit.call( this, nButton, oConfig );
+		}
+		
+		flash.setHandCursor( true );
+		
+		if ( oConfig.sAction == "flash_save" )
+		{
+			flash.setAction( 'save' );
+			flash.setCharSet( (oConfig.sCharSet=="utf16le") ? 'UTF16LE' : 'UTF8' );
+			flash.setBomInc( oConfig.bBomInc );
+			flash.setFileName( oConfig.sFileName.replace('*', this.fnGetTitle(oConfig)) );
+		}
+		else if ( oConfig.sAction == "flash_pdf" )
+		{
+			flash.setAction( 'pdf' );
+			flash.setFileName( oConfig.sFileName.replace('*', this.fnGetTitle(oConfig)) );
+		}
+		else
+		{
+			flash.setAction( 'copy' );
+		}
+		
+		flash.addEventListener('mouseOver', function(client) {
+			if ( oConfig.fnMouseover !== null )
+			{
+				oConfig.fnMouseover.call( that, nButton, oConfig, flash );
+			}
+		} );
+		
+		flash.addEventListener('mouseOut', function(client) {
+			if ( oConfig.fnMouseout !== null )
+			{
+				oConfig.fnMouseout.call( that, nButton, oConfig, flash );
+			}
+		} );
+		
+		flash.addEventListener('mouseDown', function(client) {
+			if ( oConfig.fnClick !== null )
+			{
+				oConfig.fnClick.call( that, nButton, oConfig, flash );
+			}
+		} );
+		
+		flash.addEventListener('complete', function (client, text) {
+			if ( oConfig.fnComplete !== null )
+			{
+				oConfig.fnComplete.call( that, nButton, oConfig, flash, text );
+			}
+			that._fnCollectionHide( nButton, oConfig );
+		} );
+		
+		this._fnFlashGlue( flash, nButton, oConfig.sToolTip );
+	},
+	
+	
+	/**
+	 * Wait until the id is in the DOM before we "glue" the swf. Note that this function will call
+	 * itself (using setTimeout) until it completes successfully
+	 *  @method  _fnFlashGlue
+	 *  @param   {Object} clip Zero clipboard object
+	 *  @param   {Node} node node to glue swf to
+	 *  @param   {String} text title of the flash movie
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnFlashGlue": function ( flash, node, text )
+	{
+		var that = this;
+		var id = node.getAttribute('id');
+		
+		if ( document.getElementById(id) )
+		{
+			flash.glue( node, text );
+		}
+		else
+		{
+			setTimeout( function () {
+				that._fnFlashGlue( flash, node, text );
+			}, 100 );
+		}
+	},
+	
+	
+	/**
+	 * Set the text for the flash clip to deal with
+	 * 
+	 * This function is required for large information sets. There is a limit on the 
+	 * amount of data that can be transferred between Javascript and Flash in a single call, so
+	 * we use this method to build up the text in Flash by sending over chunks. It is estimated
+	 * that the data limit is around 64k, although it is undocumented, and appears to be different
+	 * between different flash versions. We chunk at 8KiB.
+	 *  @method  _fnFlashSetText
+	 *  @param   {Object} clip the ZeroClipboard object
+	 *  @param   {String} sData the data to be set
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnFlashSetText": function ( clip, sData )
+	{
+		var asData = this._fnChunkData( sData, 8192 );
+		
+		clip.clearText();
+		for ( var i=0, iLen=asData.length ; i<iLen ; i++ )
+		{
+			clip.appendText( asData[i] );
+		}
+	},
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Data retrieval functions
+	 */
+	
+	/**
+	 * Convert the mixed columns variable into a boolean array the same size as the columns, which
+	 * indicates which columns we want to include
+	 *  @method  _fnColumnTargets
+	 *  @param   {String|Array} mColumns The columns to be included in data retrieval. If a string
+	 *			 then it can take the value of "visible" or "hidden" (to include all visible or
+	 *			 hidden columns respectively). Or an array of column indexes
+	 *  @returns {Array} A boolean array the length of the columns of the table, which each value
+	 *			 indicating if the column is to be included or not
+	 *  @private 
+	 */
+	"_fnColumnTargets": function ( mColumns )
+	{
+		var aColumns = [];
+		var dt = this.s.dt;
+		
+		if ( typeof mColumns == "object" )
+		{
+			for ( i=0, iLen=dt.aoColumns.length ; i<iLen ; i++ )
+			{
+				aColumns.push( false );
+			}
+			
+			for ( i=0, iLen=mColumns.length ; i<iLen ; i++ )
+			{
+				aColumns[ mColumns[i] ] = true;
+			}
+		}
+		else if ( mColumns == "visible" )
+		{
+			for ( i=0, iLen=dt.aoColumns.length ; i<iLen ; i++ )
+			{
+				aColumns.push( dt.aoColumns[i].bVisible ? true : false );
+			}
+		}
+		else if ( mColumns == "hidden" )
+		{
+			for ( i=0, iLen=dt.aoColumns.length ; i<iLen ; i++ )
+			{
+				aColumns.push( dt.aoColumns[i].bVisible ? false : true );
+			}
+		}
+		else if ( mColumns == "sortable" )
+		{
+			for ( i=0, iLen=dt.aoColumns.length ; i<iLen ; i++ )
+			{
+				aColumns.push( dt.aoColumns[i].bSortable ? true : false );
+			}
+		}
+		else /* all */
+		{
+			for ( i=0, iLen=dt.aoColumns.length ; i<iLen ; i++ )
+			{
+				aColumns.push( true );
+			}
+		}
+		
+		return aColumns;
+	},
+	
+	
+	/**
+	 * New line character(s) depend on the platforms
+	 *  @method  method
+	 *  @param   {Object} oConfig Button configuration object - only interested in oConfig.sNewLine
+	 *  @returns {String} Newline character
+	 */
+	"_fnNewline": function ( oConfig )
+	{
+		if ( oConfig.sNewLine == "auto" )
+		{
+			return navigator.userAgent.match(/Windows/) ? "\r\n" : "\n";
+		}
+		else
+		{
+			return oConfig.sNewLine;
+		}
+	},
+	
+	
+	/**
+	 * Get data from DataTables' internals and format it for output
+	 *  @method  _fnGetDataTablesData
+	 *  @param   {Object} oConfig Button configuration object
+	 *  @param   {String} oConfig.sFieldBoundary Field boundary for the data cells in the string
+	 *  @param   {String} oConfig.sFieldSeperator Field separator for the data cells
+	 *  @param   {String} oConfig.sNewline New line options
+	 *  @param   {Mixed} oConfig.mColumns Which columns should be included in the output
+	 *  @param   {Boolean} oConfig.bHeader Include the header
+	 *  @param   {Boolean} oConfig.bFooter Include the footer
+	 *  @param   {Boolean} oConfig.bSelectedOnly Include only the selected rows in the output
+	 *  @returns {String} Concatenated string of data
+	 *  @private 
+	 */
+	"_fnGetDataTablesData": function ( oConfig )
+	{
+		var i, iLen, j, jLen;
+		var aRow, aData=[], sLoopData='', arr;
+		var dt = this.s.dt, tr, child;
+		var regex = new RegExp(oConfig.sFieldBoundary, "g"); /* Do it here for speed */
+		var aColumnsInc = this._fnColumnTargets( oConfig.mColumns );
+		var bSelectedOnly = (typeof oConfig.bSelectedOnly != 'undefined') ? oConfig.bSelectedOnly : false;
+		
+		/*
+		 * Header
+		 */
+		if ( oConfig.bHeader )
+		{
+			aRow = [];
+			
+			for ( i=0, iLen=dt.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( aColumnsInc[i] )
+				{
+					sLoopData = dt.aoColumns[i].sTitle.replace(/\n/g," ").replace( /<.*?>/g, "" ).replace(/^\s+|\s+$/g,"");
+					sLoopData = this._fnHtmlDecode( sLoopData );
+					
+					aRow.push( this._fnBoundData( sLoopData, oConfig.sFieldBoundary, regex ) );
+				}
+			}
+
+			aData.push( aRow.join(oConfig.sFieldSeperator) );
+		}
+		
+		/*
+		 * Body
+		 */
+		var aDataIndex = dt.aiDisplay;
+		var aSelected = this.fnGetSelected();
+		if ( this.s.select.type !== "none" && bSelectedOnly && aSelected.length !== 0 )
+		{
+			aDataIndex = [];
+			for ( i=0, iLen=aSelected.length ; i<iLen ; i++ )
+			{
+				aDataIndex.push( dt.oInstance.fnGetPosition( aSelected[i] ) );
+			}
+		}
+		
+		for ( j=0, jLen=aDataIndex.length ; j<jLen ; j++ )
+		{
+			tr = dt.aoData[ aDataIndex[j] ].nTr;
+			aRow = [];
+			
+			/* Columns */
+			for ( i=0, iLen=dt.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( aColumnsInc[i] )
+				{
+					/* Convert to strings (with small optimisation) */
+					var mTypeData = dt.oApi._fnGetCellData( dt, aDataIndex[j], i, 'display' );
+					if ( oConfig.fnCellRender )
+					{
+						sLoopData = oConfig.fnCellRender( mTypeData, i, tr, aDataIndex[j] )+"";
+					}
+					else if ( typeof mTypeData == "string" )
+					{
+						/* Strip newlines, replace img tags with alt attr. and finally strip html... */
+						sLoopData = mTypeData.replace(/\n/g," ");
+						sLoopData =
+						 	sLoopData.replace(/<img.*?\s+alt\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+)).*?>/gi,
+						 		'$1$2$3');
+						sLoopData = sLoopData.replace( /<.*?>/g, "" );
+					}
+					else
+					{
+						sLoopData = mTypeData+"";
+					}
+					
+					/* Trim and clean the data */
+					sLoopData = sLoopData.replace(/^\s+/, '').replace(/\s+$/, '');
+					sLoopData = this._fnHtmlDecode( sLoopData );
+					
+					/* Bound it and add it to the total data */
+					aRow.push( this._fnBoundData( sLoopData, oConfig.sFieldBoundary, regex ) );
+				}
+			}
+      
+			aData.push( aRow.join(oConfig.sFieldSeperator) );
+      
+			/* Details rows from fnOpen */
+			if ( oConfig.bOpenRows )
+			{
+				arr = $.grep(dt.aoOpenRows, function(o) { return o.nParent === tr; });
+				
+				if ( arr.length === 1 )
+				{
+					sLoopData = this._fnBoundData( $('td', arr[0].nTr).html(), oConfig.sFieldBoundary, regex );
+					aData.push( sLoopData );
+				}
+			}
+		}
+		
+		/*
+		 * Footer
+		 */
+		if ( oConfig.bFooter && dt.nTFoot !== null )
+		{
+			aRow = [];
+			
+			for ( i=0, iLen=dt.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( aColumnsInc[i] && dt.aoColumns[i].nTf !== null )
+				{
+					sLoopData = dt.aoColumns[i].nTf.innerHTML.replace(/\n/g," ").replace( /<.*?>/g, "" );
+					sLoopData = this._fnHtmlDecode( sLoopData );
+					
+					aRow.push( this._fnBoundData( sLoopData, oConfig.sFieldBoundary, regex ) );
+				}
+			}
+			
+			aData.push( aRow.join(oConfig.sFieldSeperator) );
+		}
+		
+		_sLastData = aData.join( this._fnNewline(oConfig) );
+		return _sLastData;
+	},
+	
+	
+	/**
+	 * Wrap data up with a boundary string
+	 *  @method  _fnBoundData
+	 *  @param   {String} sData data to bound
+	 *  @param   {String} sBoundary bounding char(s)
+	 *  @param   {RegExp} regex search for the bounding chars - constructed outside for efficiency
+	 *			 in the loop
+	 *  @returns {String} bound data
+	 *  @private 
+	 */
+	"_fnBoundData": function ( sData, sBoundary, regex )
+	{
+		if ( sBoundary === "" )
+		{
+			return sData;
+		}
+		else
+		{
+			return sBoundary + sData.replace(regex, sBoundary+sBoundary) + sBoundary;
+		}
+	},
+	
+	
+	/**
+	 * Break a string up into an array of smaller strings
+	 *  @method  _fnChunkData
+	 *  @param   {String} sData data to be broken up
+	 *  @param   {Int} iSize chunk size
+	 *  @returns {Array} String array of broken up text
+	 *  @private 
+	 */
+	"_fnChunkData": function ( sData, iSize )
+	{
+		var asReturn = [];
+		var iStrlen = sData.length;
+		
+		for ( var i=0 ; i<iStrlen ; i+=iSize )
+		{
+			if ( i+iSize < iStrlen )
+			{
+				asReturn.push( sData.substring( i, i+iSize ) );
+			}
+			else
+			{
+				asReturn.push( sData.substring( i, iStrlen ) );
+			}
+		}
+		
+		return asReturn;
+	},
+	
+	
+	/**
+	 * Decode HTML entities
+	 *  @method  _fnHtmlDecode
+	 *  @param   {String} sData encoded string
+	 *  @returns {String} decoded string
+	 *  @private 
+	 */
+	"_fnHtmlDecode": function ( sData )
+	{
+		if ( sData.indexOf('&') === -1 )
+		{
+			return sData;
+		}
+		
+		var n = document.createElement('div');
+
+		return sData.replace( /&([^\s]*);/g, function( match, match2 ) {
+			if ( match.substr(1, 1) === '#' )
+			{
+				return String.fromCharCode( Number(match2.substr(1)) );
+			}
+			else
+			{
+				n.innerHTML = match;
+				return n.childNodes[0].nodeValue;
+			}
+		} );
+	},
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Printing functions
+	 */
+	
+	/**
+	 * Show print display
+	 *  @method  _fnPrintStart
+	 *  @param   {Event} e Event object
+	 *  @param   {Object} oConfig Button configuration object
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnPrintStart": function ( oConfig )
+	{
+	  var that = this;
+	  var oSetDT = this.s.dt;
+	  
+		/* Parse through the DOM hiding everything that isn't needed for the table */
+		this._fnPrintHideNodes( oSetDT.nTable );
+		
+		/* Show the whole table */
+		this.s.print.saveStart = oSetDT._iDisplayStart;
+		this.s.print.saveLength = oSetDT._iDisplayLength;
+
+		if ( oConfig.bShowAll )
+		{
+			oSetDT._iDisplayStart = 0;
+			oSetDT._iDisplayLength = -1;
+			oSetDT.oApi._fnCalculateEnd( oSetDT );
+			oSetDT.oApi._fnDraw( oSetDT );
+		}
+		
+		/* Adjust the display for scrolling which might be done by DataTables */
+		if ( oSetDT.oScroll.sX !== "" || oSetDT.oScroll.sY !== "" )
+		{
+			this._fnPrintScrollStart( oSetDT );
+
+			// If the table redraws while in print view, the DataTables scrolling
+			// setup would hide the header, so we need to readd it on draw
+			$(this.s.dt.nTable).bind('draw.DTTT_Print', function () {
+				that._fnPrintScrollStart( oSetDT );
+			} );
+		}
+		
+		/* Remove the other DataTables feature nodes - but leave the table! and info div */
+		var anFeature = oSetDT.aanFeatures;
+		for ( var cFeature in anFeature )
+		{
+			if ( cFeature != 'i' && cFeature != 't' && cFeature.length == 1 )
+			{
+				for ( var i=0, iLen=anFeature[cFeature].length ; i<iLen ; i++ )
+				{
+					this.dom.print.hidden.push( {
+						"node": anFeature[cFeature][i],
+						"display": "block"
+					} );
+					anFeature[cFeature][i].style.display = "none";
+				}
+			}
+		}
+		
+		/* Print class can be used for styling */
+		$(document.body).addClass( this.classes.print.body );
+
+		/* Show information message to let the user know what is happening */
+		if ( oConfig.sInfo !== "" )
+		{
+			this.fnInfo( oConfig.sInfo, 3000 );
+		}
+
+		/* Add a message at the top of the page */
+		if ( oConfig.sMessage )
+		{
+			this.dom.print.message = document.createElement( "div" );
+			this.dom.print.message.className = this.classes.print.message;
+			this.dom.print.message.innerHTML = oConfig.sMessage;
+			document.body.insertBefore( this.dom.print.message, document.body.childNodes[0] );
+		}
+		
+		/* Cache the scrolling and the jump to the top of the page */
+		this.s.print.saveScroll = $(window).scrollTop();
+		window.scrollTo( 0, 0 );
+
+		/* Bind a key event listener to the document for the escape key -
+		 * it is removed in the callback
+		 */
+		$(document).bind( "keydown.DTTT", function(e) {
+			/* Only interested in the escape key */
+			if ( e.keyCode == 27 )
+			{
+				e.preventDefault();
+				that._fnPrintEnd.call( that, e );
+			}
+		} );
+	},
+	
+	
+	/**
+	 * Printing is finished, resume normal display
+	 *  @method  _fnPrintEnd
+	 *  @param   {Event} e Event object
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnPrintEnd": function ( e )
+	{
+		var that = this;
+		var oSetDT = this.s.dt;
+		var oSetPrint = this.s.print;
+		var oDomPrint = this.dom.print;
+		
+		/* Show all hidden nodes */
+		this._fnPrintShowNodes();
+		
+		/* Restore DataTables' scrolling */
+		if ( oSetDT.oScroll.sX !== "" || oSetDT.oScroll.sY !== "" )
+		{
+			$(this.s.dt.nTable).unbind('draw.DTTT_Print');
+
+			this._fnPrintScrollEnd();
+		}
+		
+		/* Restore the scroll */
+		window.scrollTo( 0, oSetPrint.saveScroll );
+		
+		/* Drop the print message */
+		if ( oDomPrint.message !== null )
+		{
+			document.body.removeChild( oDomPrint.message );
+			oDomPrint.message = null;
+		}
+		
+		/* Styling class */
+		$(document.body).removeClass( 'DTTT_Print' );
+		
+		/* Restore the table length */
+		oSetDT._iDisplayStart = oSetPrint.saveStart;
+		oSetDT._iDisplayLength = oSetPrint.saveLength;
+		oSetDT.oApi._fnCalculateEnd( oSetDT );
+		oSetDT.oApi._fnDraw( oSetDT );
+		
+		$(document).unbind( "keydown.DTTT" );
+	},
+	
+	
+	/**
+	 * Take account of scrolling in DataTables by showing the full table
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnPrintScrollStart": function ()
+	{
+		var 
+			oSetDT = this.s.dt,
+			nScrollHeadInner = oSetDT.nScrollHead.getElementsByTagName('div')[0],
+			nScrollHeadTable = nScrollHeadInner.getElementsByTagName('table')[0],
+			nScrollBody = oSetDT.nTable.parentNode;
+
+		/* Copy the header in the thead in the body table, this way we show one single table when
+		 * in print view. Note that this section of code is more or less verbatim from DT 1.7.0
+		 */
+		var nTheadSize = oSetDT.nTable.getElementsByTagName('thead');
+		if ( nTheadSize.length > 0 )
+		{
+			oSetDT.nTable.removeChild( nTheadSize[0] );
+		}
+		
+		if ( oSetDT.nTFoot !== null )
+		{
+			var nTfootSize = oSetDT.nTable.getElementsByTagName('tfoot');
+			if ( nTfootSize.length > 0 )
+			{
+				oSetDT.nTable.removeChild( nTfootSize[0] );
+			}
+		}
+		
+		nTheadSize = oSetDT.nTHead.cloneNode(true);
+		oSetDT.nTable.insertBefore( nTheadSize, oSetDT.nTable.childNodes[0] );
+		
+		if ( oSetDT.nTFoot !== null )
+		{
+			nTfootSize = oSetDT.nTFoot.cloneNode(true);
+			oSetDT.nTable.insertBefore( nTfootSize, oSetDT.nTable.childNodes[1] );
+		}
+		
+		/* Now adjust the table's viewport so we can actually see it */
+		if ( oSetDT.oScroll.sX !== "" )
+		{
+			oSetDT.nTable.style.width = $(oSetDT.nTable).outerWidth()+"px";
+			nScrollBody.style.width = $(oSetDT.nTable).outerWidth()+"px";
+			nScrollBody.style.overflow = "visible";
+		}
+		
+		if ( oSetDT.oScroll.sY !== "" )
+		{
+			nScrollBody.style.height = $(oSetDT.nTable).outerHeight()+"px";
+			nScrollBody.style.overflow = "visible";
+		}
+	},
+	
+	
+	/**
+	 * Take account of scrolling in DataTables by showing the full table. Note that the redraw of
+	 * the DataTable that we do will actually deal with the majority of the hard work here
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnPrintScrollEnd": function ()
+	{
+		var 
+			oSetDT = this.s.dt,
+			nScrollBody = oSetDT.nTable.parentNode;
+		
+		if ( oSetDT.oScroll.sX !== "" )
+		{
+			nScrollBody.style.width = oSetDT.oApi._fnStringToCss( oSetDT.oScroll.sX );
+			nScrollBody.style.overflow = "auto";
+		}
+		
+		if ( oSetDT.oScroll.sY !== "" )
+		{
+			nScrollBody.style.height = oSetDT.oApi._fnStringToCss( oSetDT.oScroll.sY );
+			nScrollBody.style.overflow = "auto";
+		}
+	},
+	
+	
+	/**
+	 * Resume the display of all TableTools hidden nodes
+	 *  @method  _fnPrintShowNodes
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnPrintShowNodes": function ( )
+	{
+	  var anHidden = this.dom.print.hidden;
+	  
+		for ( var i=0, iLen=anHidden.length ; i<iLen ; i++ )
+		{
+			anHidden[i].node.style.display = anHidden[i].display;
+		}
+		anHidden.splice( 0, anHidden.length );
+	},
+	
+	
+	/**
+	 * Hide nodes which are not needed in order to display the table. Note that this function is
+	 * recursive
+	 *  @method  _fnPrintHideNodes
+	 *  @param   {Node} nNode Element which should be showing in a 'print' display
+	 *  @returns void
+	 *  @private 
+	 */
+	"_fnPrintHideNodes": function ( nNode )
+	{
+	  var anHidden = this.dom.print.hidden;
+	  
+		var nParent = nNode.parentNode;
+		var nChildren = nParent.childNodes;
+		for ( var i=0, iLen=nChildren.length ; i<iLen ; i++ )
+		{
+			if ( nChildren[i] != nNode && nChildren[i].nodeType == 1 )
+			{
+				/* If our node is shown (don't want to show nodes which were previously hidden) */
+				var sDisplay = $(nChildren[i]).css("display");
+			 	if ( sDisplay != "none" )
+				{
+					/* Cache the node and it's previous state so we can restore it */
+					anHidden.push( {
+						"node": nChildren[i],
+						"display": sDisplay
+					} );
+					nChildren[i].style.display = "none";
+				}
+			}
+		}
+		
+		if ( nParent.nodeName != "BODY" )
+		{
+			this._fnPrintHideNodes( nParent );
+		}
+	}
+};
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Static variables
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * Store of all instances that have been created of TableTools, so one can look up other (when
+ * there is need of a master)
+ *  @property _aInstances
+ *  @type	 Array
+ *  @default  []
+ *  @private
+ */
+TableTools._aInstances = [];
+
+
+/**
+ * Store of all listeners and their callback functions
+ *  @property _aListeners
+ *  @type	 Array
+ *  @default  []
+ */
+TableTools._aListeners = [];
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Static methods
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * Get an array of all the master instances
+ *  @method  fnGetMasters
+ *  @returns {Array} List of master TableTools instances
+ *  @static
+ */
+TableTools.fnGetMasters = function ()
+{
+	var a = [];
+	for ( var i=0, iLen=TableTools._aInstances.length ; i<iLen ; i++ )
+	{
+		if ( TableTools._aInstances[i].s.master )
+		{
+			a.push( TableTools._aInstances[i] );
+		}
+	}
+	return a;
+};
+
+/**
+ * Get the master instance for a table node (or id if a string is given)
+ *  @method  fnGetInstance
+ *  @returns {Object} ID of table OR table node, for which we want the TableTools instance
+ *  @static
+ */
+TableTools.fnGetInstance = function ( node )
+{
+	if ( typeof node != 'object' )
+	{
+		node = document.getElementById(node);
+	}
+	
+	for ( var i=0, iLen=TableTools._aInstances.length ; i<iLen ; i++ )
+	{
+		if ( TableTools._aInstances[i].s.master && TableTools._aInstances[i].dom.table == node )
+		{
+			return TableTools._aInstances[i];
+		}
+	}
+	return null;
+};
+
+
+/**
+ * Add a listener for a specific event
+ *  @method  _fnEventListen
+ *  @param   {Object} that Scope of the listening function (i.e. 'this' in the caller)
+ *  @param   {String} type Event type
+ *  @param   {Function} fn Function
+ *  @returns void
+ *  @private
+ *  @static
+ */
+TableTools._fnEventListen = function ( that, type, fn )
+{
+	TableTools._aListeners.push( {
+		"that": that,
+		"type": type,
+		"fn": fn
+	} );
+};
+	
+
+/**
+ * An event has occurred - look up every listener and fire it off. We check that the event we are
+ * going to fire is attached to the same table (using the table node as reference) before firing
+ *  @method  _fnEventDispatch
+ *  @param   {Object} that Scope of the listening function (i.e. 'this' in the caller)
+ *  @param   {String} type Event type
+ *  @param   {Node} node Element that the event occurred on (may be null)
+ *  @param   {boolean} [selected] Indicate if the node was selected (true) or deselected (false)
+ *  @returns void
+ *  @private
+ *  @static
+ */
+TableTools._fnEventDispatch = function ( that, type, node, selected )
+{
+	var listeners = TableTools._aListeners;
+	for ( var i=0, iLen=listeners.length ; i<iLen ; i++ )
+	{
+		if ( that.dom.table == listeners[i].that.dom.table && listeners[i].type == type )
+		{
+			listeners[i].fn( node, selected );
+		}
+	}
+};
+
+
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Constants
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+
+TableTools.buttonBase = {
+	// Button base
+	"sAction": "text",
+	"sTag": "default",
+	"sLinerTag": "default",
+	"sButtonClass": "DTTT_button_text",
+	"sButtonText": "Button text",
+	"sTitle": "",
+	"sToolTip": "",
+
+	// Common button specific options
+	"sCharSet": "utf8",
+	"bBomInc": false,
+	"sFileName": "*.csv",
+	"sFieldBoundary": "",
+	"sFieldSeperator": "\t",
+	"sNewLine": "auto",
+	"mColumns": "all", /* "all", "visible", "hidden" or array of column integers */
+	"bHeader": true,
+	"bFooter": true,
+	"bOpenRows": false,
+	"bSelectedOnly": false,
+
+	// Callbacks
+	"fnMouseover": null,
+	"fnMouseout": null,
+	"fnClick": null,
+	"fnSelect": null,
+	"fnComplete": null,
+	"fnInit": null,
+	"fnCellRender": null
+};
+
+
+/**
+ * @namespace Default button configurations
+ */
+TableTools.BUTTONS = {
+	"csv": $.extend( {}, TableTools.buttonBase, {
+		"sAction": "flash_save",
+		"sButtonClass": "DTTT_button_csv",
+		"sButtonText": "CSV",
+		"sFieldBoundary": '"',
+		"sFieldSeperator": ",",
+		"fnClick": function( nButton, oConfig, flash ) {
+			this.fnSetText( flash, this.fnGetTableData(oConfig) );
+		}
+	} ),
+
+	"xls": $.extend( {}, TableTools.buttonBase, {
+		"sAction": "flash_save",
+		"sCharSet": "utf16le",
+		"bBomInc": true,
+		"sButtonClass": "DTTT_button_xls",
+		"sButtonText": "Excel",
+		"fnClick": function( nButton, oConfig, flash ) {
+			this.fnSetText( flash, this.fnGetTableData(oConfig) );
+		}
+	} ),
+
+	"copy": $.extend( {}, TableTools.buttonBase, {
+		"sAction": "flash_copy",
+		"sButtonClass": "DTTT_button_copy",
+		"sButtonText": "Copy",
+		"fnClick": function( nButton, oConfig, flash ) {
+			this.fnSetText( flash, this.fnGetTableData(oConfig) );
+		},
+		"fnComplete": function(nButton, oConfig, flash, text) {
+			var
+				lines = text.split('\n').length,
+				len = this.s.dt.nTFoot === null ? lines-1 : lines-2,
+				plural = (len==1) ? "" : "s";
+			this.fnInfo( '<h6>Table copied</h6>'+
+				'<p>Copied '+len+' row'+plural+' to the clipboard.</p>',
+				1500
+			);
+		}
+	} ),
+
+	"pdf": $.extend( {}, TableTools.buttonBase, {
+		"sAction": "flash_pdf",
+		"sNewLine": "\n",
+		"sFileName": "*.pdf",
+		"sButtonClass": "DTTT_button_pdf",
+		"sButtonText": "PDF",
+		"sPdfOrientation": "portrait",
+		"sPdfSize": "A4",
+		"sPdfMessage": "",
+		"fnClick": function( nButton, oConfig, flash ) {
+			this.fnSetText( flash, 
+				"title:"+ this.fnGetTitle(oConfig) +"\n"+
+				"message:"+ oConfig.sPdfMessage +"\n"+
+				"colWidth:"+ this.fnCalcColRatios(oConfig) +"\n"+
+				"orientation:"+ oConfig.sPdfOrientation +"\n"+
+				"size:"+ oConfig.sPdfSize +"\n"+
+				"--/TableToolsOpts--\n" +
+				this.fnGetTableData(oConfig)
+			);
+		}
+	} ),
+
+	"print": $.extend( {}, TableTools.buttonBase, {
+		"sInfo": "<h6>Print view</h6><p>Please use your browser's print function to "+
+		  "print this table. Press escape when finished.",
+		"sMessage": null,
+		"bShowAll": true,
+		"sToolTip": "View print view",
+		"sButtonClass": "DTTT_button_print",
+		"sButtonText": "Print",
+		"fnClick": function ( nButton, oConfig ) {
+			this.fnPrint( true, oConfig );
+		}
+	} ),
+
+	"text": $.extend( {}, TableTools.buttonBase ),
+
+	"select": $.extend( {}, TableTools.buttonBase, {
+		"sButtonText": "Select button",
+		"fnSelect": function( nButton, oConfig ) {
+			if ( this.fnGetSelected().length !== 0 ) {
+				$(nButton).removeClass( this.classes.buttons.disabled );
+			} else {
+				$(nButton).addClass( this.classes.buttons.disabled );
+			}
+		},
+		"fnInit": function( nButton, oConfig ) {
+			$(nButton).addClass( this.classes.buttons.disabled );
+		}
+	} ),
+
+	"select_single": $.extend( {}, TableTools.buttonBase, {
+		"sButtonText": "Select button",
+		"fnSelect": function( nButton, oConfig ) {
+			var iSelected = this.fnGetSelected().length;
+			if ( iSelected == 1 ) {
+				$(nButton).removeClass( this.classes.buttons.disabled );
+			} else {
+				$(nButton).addClass( this.classes.buttons.disabled );
+			}
+		},
+		"fnInit": function( nButton, oConfig ) {
+			$(nButton).addClass( this.classes.buttons.disabled );
+		}
+	} ),
+
+	"select_all": $.extend( {}, TableTools.buttonBase, {
+		"sButtonText": "Select all",
+		"fnClick": function( nButton, oConfig ) {
+			this.fnSelectAll();
+		},
+		"fnSelect": function( nButton, oConfig ) {
+			if ( this.fnGetSelected().length == this.s.dt.fnRecordsDisplay() ) {
+				$(nButton).addClass( this.classes.buttons.disabled );
+			} else {
+				$(nButton).removeClass( this.classes.buttons.disabled );
+			}
+		}
+	} ),
+
+	"select_none": $.extend( {}, TableTools.buttonBase, {
+		"sButtonText": "Deselect all",
+		"fnClick": function( nButton, oConfig ) {
+			this.fnSelectNone();
+		},
+		"fnSelect": function( nButton, oConfig ) {
+			if ( this.fnGetSelected().length !== 0 ) {
+				$(nButton).removeClass( this.classes.buttons.disabled );
+			} else {
+				$(nButton).addClass( this.classes.buttons.disabled );
+			}
+		},
+		"fnInit": function( nButton, oConfig ) {
+			$(nButton).addClass( this.classes.buttons.disabled );
+		}
+	} ),
+
+	"ajax": $.extend( {}, TableTools.buttonBase, {
+		"sAjaxUrl": "/xhr.php",
+		"sButtonText": "Ajax button",
+		"fnClick": function( nButton, oConfig ) {
+			var sData = this.fnGetTableData(oConfig);
+			$.ajax( {
+				"url": oConfig.sAjaxUrl,
+				"data": [
+					{ "name": "tableData", "value": sData }
+				],
+				"success": oConfig.fnAjaxComplete,
+				"dataType": "json",
+				"type": "POST", 
+				"cache": false,
+				"error": function () {
+					alert( "Error detected when sending table data to server" );
+				}
+			} );
+		},
+		"fnAjaxComplete": function( json ) {
+			alert( 'Ajax complete' );
+		}
+	} ),
+
+	"div": $.extend( {}, TableTools.buttonBase, {
+		"sAction": "div",
+		"sTag": "div",
+		"sButtonClass": "DTTT_nonbutton",
+		"sButtonText": "Text button"
+	} ),
+
+	"collection": $.extend( {}, TableTools.buttonBase, {
+		"sAction": "collection",
+		"sButtonClass": "DTTT_button_collection",
+		"sButtonText": "Collection",
+		"fnClick": function( nButton, oConfig ) {
+			this._fnCollectionShow(nButton, oConfig);
+		}
+	} )
+};
+/*
+ *  on* callback parameters:
+ *  	1. node - button element
+ *  	2. object - configuration object for this button
+ *  	3. object - ZeroClipboard reference (flash button only)
+ *  	4. string - Returned string from Flash (flash button only - and only on 'complete')
+ */
+
+
+
+/**
+ * @namespace Classes used by TableTools - allows the styles to be override easily.
+ *   Note that when TableTools initialises it will take a copy of the classes object
+ *   and will use its internal copy for the remainder of its run time.
+ */
+TableTools.classes = {
+	"container": "DTTT_container",
+	"buttons": {
+		"normal": "DTTT_button",
+		"disabled": "DTTT_disabled"
+	},
+	"collection": {
+		"container": "DTTT_collection",
+		"background": "DTTT_collection_background",
+		"buttons": {
+			"normal": "DTTT_button",
+			"disabled": "DTTT_disabled"
+		}
+	},
+	"select": {
+		"table": "DTTT_selectable",
+		"row": "DTTT_selected"
+	},
+	"print": {
+		"body": "DTTT_Print",
+		"info": "DTTT_print_info",
+		"message": "DTTT_PrintMessage"
+	}
+};
+
+
+/**
+ * @namespace ThemeRoller classes - built in for compatibility with DataTables' 
+ *   bJQueryUI option.
+ */
+TableTools.classes_themeroller = {
+	"container": "DTTT_container ui-buttonset ui-buttonset-multi",
+	"buttons": {
+		"normal": "DTTT_button ui-button ui-state-default"
+	},
+	"collection": {
+		"container": "DTTT_collection ui-buttonset ui-buttonset-multi"
+	}
+};
+
+
+/**
+ * @namespace TableTools default settings for initialisation
+ */
+TableTools.DEFAULTS = {
+	"sSwfPath":        "media/swf/copy_csv_xls_pdf.swf",
+	"sRowSelect":      "none",
+	"sSelectedClass":  null,
+	"fnPreRowSelect":  null,
+	"fnRowSelected":   null,
+	"fnRowDeselected": null,
+	"aButtons":        [ "copy", "csv", "xls", "pdf", "print" ],
+	"oTags": {
+		"container": "div",
+		"button": "a", // We really want to use buttons here, but Firefox and IE ignore the
+		                 // click on the Flash element in the button (but not mouse[in|out]).
+		"liner": "span",
+		"collection": {
+			"container": "div",
+			"button": "a",
+			"liner": "span"
+		}
+	}
+};
+
+
+/**
+ * Name of this class
+ *  @constant CLASS
+ *  @type	 String
+ *  @default  TableTools
+ */
+TableTools.prototype.CLASS = "TableTools";
+
+
+/**
+ * TableTools version
+ *  @constant  VERSION
+ *  @type	  String
+ *  @default   See code
+ */
+TableTools.VERSION = "2.1.5";
+TableTools.prototype.VERSION = TableTools.VERSION;
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Initialisation
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/*
+ * Register a new feature with DataTables
+ */
+if ( typeof $.fn.dataTable == "function" &&
+	 typeof $.fn.dataTableExt.fnVersionCheck == "function" &&
+	 $.fn.dataTableExt.fnVersionCheck('1.9.0') )
+{
+	$.fn.dataTableExt.aoFeatures.push( {
+		"fnInit": function( oDTSettings ) {
+			var oOpts = typeof oDTSettings.oInit.oTableTools != 'undefined' ? 
+				oDTSettings.oInit.oTableTools : {};
+			
+			var oTT = new TableTools( oDTSettings.oInstance, oOpts );
+			TableTools._aInstances.push( oTT );
+			
+			return oTT.dom.container;
+		},
+		"cFeature": "T",
+		"sFeature": "TableTools"
+	} );
+}
+else
+{
+	alert( "Warning: TableTools 2 requires DataTables 1.9.0 or newer - www.datatables.net/download");
+}
+
+$.fn.DataTable.TableTools = TableTools;
+
+})(jQuery, window, document);
diff --git a/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery-1.10.1.min.js b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery-1.10.1.min.js
new file mode 100755
index 0000000000000000000000000000000000000000..f65cf1dc4573c51e54d7cf3772d06caf96726616
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery-1.10.1.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v1.8.2 jquery.com | jquery.org/license */
+(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d<e;d++)p.event.add(b,c,h[c][d])}g.data&&(g.data=p.extend({},g.data))}function bE(a,b){var c;if(b.nodeType!==1)return;b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?(b.parentNode&&(b.outerHTML=a.outerHTML),p.support.html5Clone&&a.innerHTML&&!p.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):c==="input"&&bv.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text),b.removeAttribute(p.expando)}function bF(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bG(a){bv.test(a.type)&&(a.defaultChecked=a.checked)}function bY(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=bW.length;while(e--){b=bW[e]+c;if(b in a)return b}return d}function bZ(a,b){return a=b||a,p.css(a,"display")==="none"||!p.contains(a.ownerDocument,a)}function b$(a,b){var c,d,e=[],f=0,g=a.length;for(;f<g;f++){c=a[f];if(!c.style)continue;e[f]=p._data(c,"olddisplay"),b?(!e[f]&&c.style.display==="none"&&(c.style.display=""),c.style.display===""&&bZ(c)&&(e[f]=p._data(c,"olddisplay",cc(c.nodeName)))):(d=bH(c,"display"),!e[f]&&d!=="none"&&p._data(c,"olddisplay",d))}for(f=0;f<g;f++){c=a[f];if(!c.style)continue;if(!b||c.style.display==="none"||c.style.display==="")c.style.display=b?e[f]||"":"none"}return a}function b_(a,b,c){var d=bP.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function ca(a,b,c,d){var e=c===(d?"border":"content")?4:b==="width"?1:0,f=0;for(;e<4;e+=2)c==="margin"&&(f+=p.css(a,c+bV[e],!0)),d?(c==="content"&&(f-=parseFloat(bH(a,"padding"+bV[e]))||0),c!=="margin"&&(f-=parseFloat(bH(a,"border"+bV[e]+"Width"))||0)):(f+=parseFloat(bH(a,"padding"+bV[e]))||0,c!=="padding"&&(f+=parseFloat(bH(a,"border"+bV[e]+"Width"))||0));return f}function cb(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=!0,f=p.support.boxSizing&&p.css(a,"boxSizing")==="border-box";if(d<=0||d==null){d=bH(a,b);if(d<0||d==null)d=a.style[b];if(bQ.test(d))return d;e=f&&(p.support.boxSizingReliable||d===a.style[b]),d=parseFloat(d)||0}return d+ca(a,b,c||(f?"border":"content"),e)+"px"}function cc(a){if(bS[a])return bS[a];var b=p("<"+a+">").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write("<!doctype html><html><body>"),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bS[a]=c,c}function ci(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||ce.test(a)?d(a,e):ci(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ci(a+"["+e+"]",b[e],c,d);else d(a,b)}function cz(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h<i;h++)d=g[h],f=/^\+/.test(d),f&&(d=d.substr(1)||"*"),e=a[d]=a[d]||[],e[f?"unshift":"push"](c)}}function cA(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h,i=a[f],j=0,k=i?i.length:0,l=a===cv;for(;j<k&&(l||!h);j++)h=i[j](c,d,e),typeof h=="string"&&(!l||g[h]?h=b:(c.dataTypes.unshift(h),h=cA(a,c,d,e,h,g)));return(l||!h)&&!g["*"]&&(h=cA(a,c,d,e,"*",g)),h}function cB(a,c){var d,e,f=p.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((f[d]?a:e||(e={}))[d]=c[d]);e&&p.extend(!0,a,e)}function cC(a,c,d){var e,f,g,h,i=a.contents,j=a.dataTypes,k=a.responseFields;for(f in k)f in d&&(c[k[f]]=d[f]);while(j[0]==="*")j.shift(),e===b&&(e=a.mimeType||c.getResponseHeader("content-type"));if(e)for(f in i)if(i[f]&&i[f].test(e)){j.unshift(f);break}if(j[0]in d)g=j[0];else{for(f in d){if(!j[0]||a.converters[f+" "+j[0]]){g=f;break}h||(h=f)}g=g||h}if(g)return g!==j[0]&&j.unshift(g),d[g]}function cD(a,b){var c,d,e,f,g=a.dataTypes.slice(),h=g[0],i={},j=0;a.dataFilter&&(b=a.dataFilter(b,a.dataType));if(g[1])for(c in a.converters)i[c.toLowerCase()]=a.converters[c];for(;e=g[++j];)if(e!=="*"){if(h!=="*"&&h!==e){c=i[h+" "+e]||i["* "+e];if(!c)for(d in i){f=d.split(" ");if(f[1]===e){c=i[h+" "+f[0]]||i["* "+f[0]];if(c){c===!0?c=i[d]:i[d]!==!0&&(e=f[0],g.splice(j--,0,e));break}}}if(c!==!0)if(c&&a["throws"])b=c(b);else try{b=c(b)}catch(k){return{state:"parsererror",error:c?k:"No conversion from "+h+" to "+e}}}h=e}return{state:"success",data:b}}function cL(){try{return new a.XMLHttpRequest}catch(b){}}function cM(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function cU(){return setTimeout(function(){cN=b},0),cN=p.now()}function cV(a,b){p.each(b,function(b,c){var d=(cT[b]||[]).concat(cT["*"]),e=0,f=d.length;for(;e<f;e++)if(d[e].call(a,b,c))return})}function cW(a,b,c){var d,e=0,f=0,g=cS.length,h=p.Deferred().always(function(){delete i.elem}),i=function(){var b=cN||cU(),c=Math.max(0,j.startTime+j.duration-b),d=1-(c/j.duration||0),e=0,f=j.tweens.length;for(;e<f;e++)j.tweens[e].run(d);return h.notifyWith(a,[j,d,c]),d<1&&f?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:p.extend({},b),opts:p.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:cN||cU(),duration:c.duration,tweens:[],createTween:function(b,c,d){var e=p.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(e),e},stop:function(b){var c=0,d=b?j.tweens.length:0;for(;c<d;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;cX(k,j.opts.specialEasing);for(;e<g;e++){d=cS[e].call(j,a,k,j.opts);if(d)return d}return cV(j,k),p.isFunction(j.opts.start)&&j.opts.start.call(a,j),p.fx.timer(p.extend(i,{anim:j,queue:j.opts.queue,elem:a})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}function cX(a,b){var c,d,e,f,g;for(c in a){d=p.camelCase(c),e=b[d],f=a[c],p.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=p.cssHooks[d];if(g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}}function cY(a,b,c){var d,e,f,g,h,i,j,k,l=this,m=a.style,n={},o=[],q=a.nodeType&&bZ(a);c.queue||(j=p._queueHooks(a,"fx"),j.unqueued==null&&(j.unqueued=0,k=j.empty.fire,j.empty.fire=function(){j.unqueued||k()}),j.unqueued++,l.always(function(){l.always(function(){j.unqueued--,p.queue(a,"fx").length||j.empty.fire()})})),a.nodeType===1&&("height"in b||"width"in b)&&(c.overflow=[m.overflow,m.overflowX,m.overflowY],p.css(a,"display")==="inline"&&p.css(a,"float")==="none"&&(!p.support.inlineBlockNeedsLayout||cc(a.nodeName)==="inline"?m.display="inline-block":m.zoom=1)),c.overflow&&(m.overflow="hidden",p.support.shrinkWrapBlocks||l.done(function(){m.overflow=c.overflow[0],m.overflowX=c.overflow[1],m.overflowY=c.overflow[2]}));for(d in b){f=b[d];if(cP.exec(f)){delete b[d];if(f===(q?"hide":"show"))continue;o.push(d)}}g=o.length;if(g){h=p._data(a,"fxshow")||p._data(a,"fxshow",{}),q?p(a).show():l.done(function(){p(a).hide()}),l.done(function(){var b;p.removeData(a,"fxshow",!0);for(b in n)p.style(a,b,n[b])});for(d=0;d<g;d++)e=o[d],i=l.createTween(e,q?h[e]:0),n[e]=h[e]||p.style(a,e),e in h||(h[e]=i.start,q&&(i.end=i.start,i.start=e==="width"||e==="height"?1:0))}}function cZ(a,b,c,d,e){return new cZ.prototype.init(a,b,c,d,e)}function c$(a,b){var c,d={height:a},e=0;b=b?1:0;for(;e<4;e+=2-b)c=bV[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function da(a){return p.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}var c,d,e=a.document,f=a.location,g=a.navigator,h=a.jQuery,i=a.$,j=Array.prototype.push,k=Array.prototype.slice,l=Array.prototype.indexOf,m=Object.prototype.toString,n=Object.prototype.hasOwnProperty,o=String.prototype.trim,p=function(a,b){return new p.fn.init(a,b,c)},q=/[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,r=/\S/,s=/\s+/,t=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,u=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.2",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i<j;i++)if((a=arguments[i])!=null)for(c in a){d=h[c],e=a[c];if(h===e)continue;k&&e&&(p.isPlainObject(e)||(f=p.isArray(e)))?(f?(f=!1,g=d&&p.isArray(d)?d:[]):g=d&&p.isPlainObject(d)?d:{},h[c]=p.extend(k,g,e)):e!==b&&(h[c]=e)}return h},p.extend({noConflict:function(b){return a.$===p&&(a.$=i),b&&a.jQuery===p&&(a.jQuery=h),p},isReady:!1,readyWait:1,holdReady:function(a){a?p.readyWait++:p.ready(!0)},ready:function(a){if(a===!0?--p.readyWait:p.isReady)return;if(!e.body)return setTimeout(p.ready,1);p.isReady=!0;if(a!==!0&&--p.readyWait>0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f<g;)if(c.apply(a[f++],d)===!1)break}else if(h){for(e in a)if(c.call(a[e],e,a[e])===!1)break}else for(;f<g;)if(c.call(a[f],f,a[f++])===!1)break;return a},trim:o&&!o.call(" ")?function(a){return a==null?"":o.call(a)}:function(a){return a==null?"":(a+"").replace(t,"")},makeArray:function(a,b){var c,d=b||[];return a!=null&&(c=p.type(a),a.length==null||c==="string"||c==="function"||c==="regexp"||p.isWindow(a)?j.call(d,a):p.merge(d,a)),d},inArray:function(a,b,c){var d;if(b){if(l)return l.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=c.length,e=a.length,f=0;if(typeof d=="number")for(;f<d;f++)a[e++]=c[f];else while(c[f]!==b)a[e++]=c[f++];return a.length=e,a},grep:function(a,b,c){var d,e=[],f=0,g=a.length;c=!!c;for(;f<g;f++)d=!!b(a[f],f),c!==d&&e.push(a[f]);return e},map:function(a,c,d){var e,f,g=[],h=0,i=a.length,j=a instanceof p||i!==b&&typeof i=="number"&&(i>0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h<i;h++)e=c(a[h],h,d),e!=null&&(g[g.length]=e);else for(f in a)e=c(a[f],f,d),e!=null&&(g[g.length]=e);return g.concat.apply([],g)},guid:1,proxy:function(a,c){var d,e,f;return typeof c=="string"&&(d=a[c],c=a,a=d),p.isFunction(a)?(e=k.call(arguments,2),f=function(){return a.apply(c,e.concat(k.call(arguments)))},f.guid=a.guid=a.guid||p.guid++,f):b},access:function(a,c,d,e,f,g,h){var i,j=d==null,k=0,l=a.length;if(d&&typeof d=="object"){for(k in d)p.access(a,c,k,d[k],1,g,e);f=1}else if(e!==b){i=h===b&&p.isFunction(e),j&&(i?(i=c,c=function(a,b,c){return i.call(p(a),c)}):(c.call(a,e),c=null));if(c)for(;k<l;k++)c(a[k],d,i?e.call(a[k],k,c(a[k],d)):e,h);f=1}return f?a:j?c.call(a):l?c(a[0],d):g},now:function(){return(new Date).getTime()}}),p.ready.promise=function(b){if(!d){d=p.Deferred();if(e.readyState==="complete")setTimeout(p.ready,1);else if(e.addEventListener)e.addEventListener("DOMContentLoaded",D,!1),a.addEventListener("load",p.ready,!1);else{e.attachEvent("onreadystatechange",D),a.attachEvent("onload",p.ready);var c=!1;try{c=a.frameElement==null&&e.documentElement}catch(f){}c&&c.doScroll&&function g(){if(!p.isReady){try{c.doScroll("left")}catch(a){return setTimeout(g,50)}p.ready()}}()}}return d.promise(b)},p.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){E["[object "+b+"]"]=b.toLowerCase()}),c=p(e);var F={};p.Callbacks=function(a){a=typeof a=="string"?F[a]||G(a):p.extend({},a);var c,d,e,f,g,h,i=[],j=!a.once&&[],k=function(b){c=a.memory&&b,d=!0,h=f||0,f=0,g=i.length,e=!0;for(;i&&h<g;h++)if(i[h].apply(b[0],b[1])===!1&&a.stopOnFalse){c=!1;break}e=!1,i&&(j?j.length&&k(j.shift()):c?i=[]:l.disable())},l={add:function(){if(i){var b=i.length;(function d(b){p.each(b,function(b,c){var e=p.type(c);e==="function"&&(!a.unique||!l.has(c))?i.push(c):c&&c.length&&e!=="string"&&d(c)})})(arguments),e?g=i.length:c&&(f=b,k(c))}return this},remove:function(){return i&&p.each(arguments,function(a,b){var c;while((c=p.inArray(b,i,c))>-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return a!=null?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b<d;b++)c[b]&&p.isFunction(c[b].promise)?c[b].promise().done(g(b,j,c)).fail(f.reject).progress(g(b,i,h)):--e}return e||f.resolveWith(j,c),f.promise()}}),p.support=function(){var b,c,d,f,g,h,i,j,k,l,m,n=e.createElement("div");n.setAttribute("className","t"),n.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="<div></div>",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||p.guid++:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e<f;e++)delete d[b[e]];if(!(c?K:p.isEmptyObject)(d))return}}if(!c){delete h[i].data;if(!K(h[i]))return}g?p.cleanData([a],!0):p.support.deleteExpando||h!=h.window?delete h[i]:h[i]=null},_data:function(a,b,c){return p.data(a,b,c,!0)},acceptData:function(a){var b=a.nodeName&&p.noData[a.nodeName.toLowerCase()];return!b||b!==!0&&a.getAttribute("classid")===b}}),p.fn.extend({data:function(a,c){var d,e,f,g,h,i=this[0],j=0,k=null;if(a===b){if(this.length){k=p.data(i);if(i.nodeType===1&&!p._data(i,"parsedAttrs")){f=i.attributes;for(h=f.length;j<h;j++)g=f[j].name,g.indexOf("data-")||(g=p.camelCase(g.substring(5)),J(i,g,k[g]));p._data(i,"parsedAttrs",!0)}}return k}return typeof a=="object"?this.each(function(){p.data(this,a)}):(d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!",p.access(this,function(c){if(c===b)return k=this.triggerHandler("getData"+e,[d[0]]),k===b&&i&&(k=p.data(i,a),k=J(i,a,k)),k===b&&d[1]?this.data(d[0]):k;d[1]=c,this.each(function(){var b=p(this);b.triggerHandler("setData"+e,d),p.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.length,e=c.shift(),f=p._queueHooks(a,b),g=function(){p.dequeue(a,b)};e==="inprogress"&&(e=c.shift(),d--),e&&(b==="fx"&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length<d?p.queue(this[0],a):c===b?this:this.each(function(){var b=p.queue(this,a,c);p._queueHooks(this,a),a==="fx"&&b[0]!=="inprogress"&&p.dequeue(this,a)})},dequeue:function(a){return this.each(function(){p.dequeue(this,a)})},delay:function(a,b){return a=p.fx?p.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){var d,e=1,f=p.Deferred(),g=this,h=this.length,i=function(){--e||f.resolveWith(g,[g])};typeof a!="string"&&(c=a,a=b),a=a||"fx";while(h--)d=p._data(g[h],a+"queueHooks"),d&&d.empty&&(e++,d.empty.add(i));return i(),f.promise(c)}});var L,M,N,O=/[\t\r\n]/g,P=/\r/g,Q=/^(?:button|input)$/i,R=/^(?:button|input|object|select|textarea)$/i,S=/^a(?:rea|)$/i,T=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,U=p.support.getSetAttribute;p.fn.extend({attr:function(a,b){return p.access(this,p.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{f=" "+e.className+" ";for(g=0,h=b.length;g<h;g++)f.indexOf(" "+b[g]+" ")<0&&(f+=b[g]+" ");e.className=p.trim(f)}}}return this},removeClass:function(a){var c,d,e,f,g,h,i;if(p.isFunction(a))return this.each(function(b){p(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(s);for(h=0,i=this.length;h<i;h++){e=this[h];if(e.nodeType===1&&e.className){d=(" "+e.className+" ").replace(O," ");for(f=0,g=c.length;f<g;f++)while(d.indexOf(" "+c[f]+" ")>=0)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(O," ").indexOf(b)>=0)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c<d;c++){e=h[c];if(e.selected&&(p.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!p.nodeName(e.parentNode,"optgroup"))){b=p(e).val();if(i)return b;g.push(b)}}return i&&!g.length&&h.length?p(h[f]).val():g},set:function(a,b){var c=p.makeArray(b);return p(a).find("option").each(function(){this.selected=p.inArray(p(this).val(),c)>=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,d+""),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g<d.length;g++)e=d[g],e&&(c=p.propFix[e]||e,f=T.test(e),f||p.attr(a,e,""),a.removeAttribute(U?e:c),f&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(Q.test(a.nodeName)&&a.parentNode)p.error("type property can't be changed");else if(!p.support.radioValue&&b==="radio"&&p.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}},value:{get:function(a,b){return L&&p.nodeName(a,"button")?L.get(a,b):b in a?a.value:null},set:function(a,b,c){if(L&&p.nodeName(a,"button"))return L.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,f,g,h=a.nodeType;if(!a||h===3||h===8||h===2)return;return g=h!==1||!p.isXMLDoc(a),g&&(c=p.propFix[c]||c,f=p.propHooks[c]),d!==b?f&&"set"in f&&(e=f.set(a,d,c))!==b?e:a[c]=d:f&&"get"in f&&(e=f.get(a,c))!==null?e:a[c]},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):R.test(a.nodeName)||S.test(a.nodeName)&&a.href?0:b}}}}),M={get:function(a,c){var d,e=p.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;return b===!1?p.removeAttr(a,c):(d=p.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase())),c}},U||(N={name:!0,id:!0,coords:!0},L=p.valHooks.button={get:function(a,c){var d;return d=a.getAttributeNode(c),d&&(N[c]?d.value!=="":d.specified)?d.value:b},set:function(a,b,c){var d=a.getAttributeNode(c);return d||(d=e.createAttribute(c),a.setAttributeNode(d)),d.value=b+""}},p.each(["width","height"],function(a,b){p.attrHooks[b]=p.extend(p.attrHooks[b],{set:function(a,c){if(c==="")return a.setAttribute(b,"auto"),c}})}),p.attrHooks.contenteditable={get:L.get,set:function(a,b,c){b===""&&(b="false"),L.set(a,b,c)}}),p.support.hrefNormalized||p.each(["href","src","width","height"],function(a,c){p.attrHooks[c]=p.extend(p.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),p.support.style||(p.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=b+""}}),p.support.optSelected||(p.propHooks.selected=p.extend(p.propHooks.selected,{get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}})),p.support.enctype||(p.propFix.enctype="encoding"),p.support.checkOn||p.each(["radio","checkbox"],function(){p.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),p.each(["radio","checkbox"],function(){p.valHooks[this]=p.extend(p.valHooks[this],{set:function(a,b){if(p.isArray(b))return a.checked=p.inArray(p(a).val(),b)>=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j<c.length;j++){k=W.exec(c[j])||[],l=k[1],m=(k[2]||"").split(".").sort(),r=p.event.special[l]||{},l=(f?r.delegateType:r.bindType)||l,r=p.event.special[l]||{},n=p.extend({type:l,origType:k[1],data:e,handler:d,guid:d.guid,selector:f,needsContext:f&&p.expr.match.needsContext.test(f),namespace:m.join(".")},o),q=i[l];if(!q){q=i[l]=[],q.delegateCount=0;if(!r.setup||r.setup.call(a,e,m,h)===!1)a.addEventListener?a.addEventListener(l,h,!1):a.attachEvent&&a.attachEvent("on"+l,h)}r.add&&(r.add.call(a,n),n.handler.guid||(n.handler.guid=d.guid)),f?q.splice(q.delegateCount++,0,n):q.push(n),p.event.global[l]=!0}a=null},global:{},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,q,r=p.hasData(a)&&p._data(a);if(!r||!(m=r.events))return;b=p.trim(_(b||"")).split(" ");for(f=0;f<b.length;f++){g=W.exec(b[f])||[],h=i=g[1],j=g[2];if(!h){for(h in m)p.event.remove(a,h+b[f],c,d,!0);continue}n=p.event.special[h]||{},h=(d?n.delegateType:n.bindType)||h,o=m[h]||[],k=o.length,j=j?new RegExp("(^|\\.)"+j.split(".").sort().join("\\.(?:.*\\.|)")+"(\\.|$)"):null;for(l=0;l<o.length;l++)q=o[l],(e||i===q.origType)&&(!c||c.guid===q.guid)&&(!j||j.test(q.namespace))&&(!d||d===q.selector||d==="**"&&q.selector)&&(o.splice(l--,1),q.selector&&o.delegateCount--,n.remove&&n.remove.call(a,q));o.length===0&&k!==o.length&&((!n.teardown||n.teardown.call(a,j,r.handle)===!1)&&p.removeEvent(a,h,r.handle),delete m[h])}p.isEmptyObject(m)&&(delete r.handle,p.removeData(a,"events",!0))},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,f,g){if(!f||f.nodeType!==3&&f.nodeType!==8){var h,i,j,k,l,m,n,o,q,r,s=c.type||c,t=[];if($.test(s+p.event.triggered))return;s.indexOf("!")>=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;j<q.length&&!c.isPropagationStopped();j++)k=q[j][0],c.type=q[j][1],o=(p._data(k,"events")||{})[c.type]&&p._data(k,"handle"),o&&o.apply(k,d),o=m&&k[m],o&&p.acceptData(k)&&o.apply&&o.apply(k,d)===!1&&c.preventDefault();return c.type=s,!g&&!c.isDefaultPrevented()&&(!n._default||n._default.apply(f.ownerDocument,d)===!1)&&(s!=="click"||!p.nodeName(f,"a"))&&p.acceptData(f)&&m&&f[s]&&(s!=="focus"&&s!=="blur"||c.target.offsetWidth!==0)&&!p.isWindow(f)&&(l=f[m],l&&(f[m]=null),p.event.triggered=s,f[s](),p.event.triggered=b,l&&(f[m]=l)),c.result}return},dispatch:function(c){c=p.event.fix(c||a.event);var d,e,f,g,h,i,j,l,m,n,o=(p._data(this,"events")||{})[c.type]||[],q=o.delegateCount,r=k.call(arguments),s=!c.exclusive&&!c.namespace,t=p.event.special[c.type]||{},u=[];r[0]=c,c.delegateTarget=this;if(t.preDispatch&&t.preDispatch.call(this,c)===!1)return;if(q&&(!c.button||c.type!=="click"))for(f=c.target;f!=this;f=f.parentNode||this)if(f.disabled!==!0||c.type!=="click"){h={},j=[];for(d=0;d<q;d++)l=o[d],m=l.selector,h[m]===b&&(h[m]=l.needsContext?p(m,this).index(f)>=0:p.find(m,this,null,[f]).length),h[m]&&j.push(l);j.length&&u.push({elem:f,matches:j})}o.length>q&&u.push({elem:this,matches:o.slice(q)});for(d=0;d<u.length&&!c.isPropagationStopped();d++){i=u[d],c.currentTarget=i.elem;for(e=0;e<i.matches.length&&!c.isImmediatePropagationStopped();e++){l=i.matches[e];if(s||!c.namespace&&!l.namespace||c.namespace_re&&c.namespace_re.test(l.namespace))c.data=l.data,c.handleObj=l,g=((p.event.special[l.origType]||{}).handle||l.handler).apply(i.elem,r),g!==b&&(c.result=g,g===!1&&(c.preventDefault(),c.stopPropagation()))}}return t.postDispatch&&t.postDispatch.call(this,c),c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,c){var d,f,g,h=c.button,i=c.fromElement;return a.pageX==null&&c.clientX!=null&&(d=a.target.ownerDocument||e,f=d.documentElement,g=d.body,a.pageX=c.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=c.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?c.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0),a}},fix:function(a){if(a[p.expando])return a;var b,c,d=a,f=p.event.fixHooks[a.type]||{},g=f.props?this.props.concat(f.props):this.props;a=p.Event(d);for(b=g.length;b;)c=g[--b],a[c]=d[c];return a.target||(a.target=d.srcElement||e),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,f.filter?f.filter(a,d):a},special:{load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){p.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=p.extend(new p.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?p.event.trigger(e,null,b):p.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},p.event.handle=p.event.dispatch,p.removeEvent=e.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]=="undefined"&&(a[d]=null),a.detachEvent(d,c))},p.Event=function(a,b){if(this instanceof p.Event)a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?bb:ba):this.type=a,b&&p.extend(this,b),this.timeStamp=a&&a.timeStamp||p.now(),this[p.expando]=!0;else return new p.Event(a,b)},p.Event.prototype={preventDefault:function(){this.isDefaultPrevented=bb;var a=this.originalEvent;if(!a)return;a.preventDefault?a.preventDefault():a.returnValue=!1},stopPropagation:function(){this.isPropagationStopped=bb;var a=this.originalEvent;if(!a)return;a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=bb,this.stopPropagation()},isDefaultPrevented:ba,isPropagationStopped:ba,isImmediatePropagationStopped:ba},p.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){p.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj,g=f.selector;if(!e||e!==d&&!p.contains(d,e))a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b;return c}}}),p.support.submitBubbles||(p.event.special.submit={setup:function(){if(p.nodeName(this,"form"))return!1;p.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=p.nodeName(c,"input")||p.nodeName(c,"button")?c.form:b;d&&!p._data(d,"_submit_attached")&&(p.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),p._data(d,"_submit_attached",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&p.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(p.nodeName(this,"form"))return!1;p.event.remove(this,"._submit")}}),p.support.changeBubbles||(p.event.special.change={setup:function(){if(V.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")p.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),p.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),p.event.simulate("change",this,a,!0)});return!1}p.event.add(this,"beforeactivate._change",function(a){var b=a.target;V.test(b.nodeName)&&!p._data(b,"_change_attached")&&(p.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&p.event.simulate("change",this.parentNode,a,!0)}),p._data(b,"_change_attached",!0))})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){return p.event.remove(this,"._change"),!V.test(this.nodeName)}}),p.support.focusinBubbles||p.each({focus:"focusin",blur:"focusout"},function(a,b){var c=0,d=function(a){p.event.simulate(b,a.target,p.event.fix(a),!0)};p.event.special[b]={setup:function(){c++===0&&e.addEventListener(a,d,!0)},teardown:function(){--c===0&&e.removeEventListener(a,d,!0)}}}),p.fn.extend({on:function(a,c,d,e,f){var g,h;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(h in a)this.on(h,c,d,a[h],f);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=ba;else if(!e)return this;return f===1&&(g=e,e=function(a){return p().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=p.guid++)),this.each(function(){p.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){var e,f;if(a&&a.preventDefault&&a.handleObj)return e=a.handleObj,p(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler),this;if(typeof a=="object"){for(f in a)this.off(f,c,a[f]);return this}if(c===!1||typeof c=="function")d=c,c=b;return d===!1&&(d=ba),this.each(function(){p.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){return p(this.context).on(a,this.selector,b,c),this},die:function(a,b){return p(this.context).off(a,this.selector||"**",b),this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length===1?this.off(a,"**"):this.off(b,a||"**",c)},trigger:function(a,b){return this.each(function(){p.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return p.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||p.guid++,d=0,e=function(c){var e=(p._data(this,"lastToggle"+a.guid)||0)%d;return p._data(this,"lastToggle"+a.guid,e+1),c.preventDefault(),b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),p.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){p.fn[b]=function(a,c){return c==null&&(c=a,a=null),arguments.length>0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function bc(a,b,c,d){c=c||[],b=b||r;var e,f,i,j,k=b.nodeType;if(!a||typeof a!="string")return c;if(k!==1&&k!==9)return[];i=g(b);if(!i&&!d)if(e=P.exec(a))if(j=e[1]){if(k===9){f=b.getElementById(j);if(!f||!f.parentNode)return c;if(f.id===j)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(j))&&h(b,f)&&f.id===j)return c.push(f),c}else{if(e[2])return w.apply(c,x.call(b.getElementsByTagName(a),0)),c;if((j=e[3])&&_&&b.getElementsByClassName)return w.apply(c,x.call(b.getElementsByClassName(j),0)),c}return bp(a.replace(L,"$1"),b,c,d,i)}function bd(a){return function(b){var c=b.nodeName.toLowerCase();return c==="input"&&b.type===a}}function be(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}}function bf(a){return z(function(b){return b=+b,z(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function bg(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}function bh(a,b){var c,d,f,g,h,i,j,k=C[o][a];if(k)return b?0:k.slice(0);h=a,i=[],j=e.preFilter;while(h){if(!c||(d=M.exec(h)))d&&(h=h.slice(d[0].length)),i.push(f=[]);c=!1;if(d=N.exec(h))f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=d[0].replace(L," ");for(g in e.filter)(d=W[g].exec(h))&&(!j[g]||(d=j[g](d,r,!0)))&&(f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=g,c.matches=d);if(!c)break}return b?h.length:h?bc.error(a):C(a,i).slice(0)}function bi(a,b,d){var e=b.dir,f=d&&b.dir==="parentNode",g=u++;return b.first?function(b,c,d){while(b=b[e])if(f||b.nodeType===1)return a(b,c,d)}:function(b,d,h){if(!h){var i,j=t+" "+g+" ",k=j+c;while(b=b[e])if(f||b.nodeType===1){if((i=b[o])===k)return b.sizset;if(typeof i=="string"&&i.indexOf(j)===0){if(b.sizset)return b}else{b[o]=k;if(a(b,d,h))return b.sizset=!0,b;b.sizset=!1}}}else while(b=b[e])if(f||b.nodeType===1)if(a(b,d,h))return b}}function bj(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function bk(a,b,c,d,e){var f,g=[],h=0,i=a.length,j=b!=null;for(;h<i;h++)if(f=a[h])if(!c||c(f,d,e))g.push(f),j&&b.push(h);return g}function bl(a,b,c,d,e,f){return d&&!d[o]&&(d=bl(d)),e&&!e[o]&&(e=bl(e,f)),z(function(f,g,h,i){if(f&&e)return;var j,k,l,m=[],n=[],o=g.length,p=f||bo(b||"*",h.nodeType?[h]:h,[],f),q=a&&(f||!b)?bk(p,m,a,h,i):p,r=c?e||(f?a:o||d)?[]:g:q;c&&c(q,r,h,i);if(d){l=bk(r,n),d(l,[],h,i),j=l.length;while(j--)if(k=l[j])r[n[j]]=!(q[n[j]]=k)}if(f){j=a&&r.length;while(j--)if(k=r[j])f[m[j]]=!(g[m[j]]=k)}else r=bk(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):w.apply(g,r)})}function bm(a){var b,c,d,f=a.length,g=e.relative[a[0].type],h=g||e.relative[" "],i=g?1:0,j=bi(function(a){return a===b},h,!0),k=bi(function(a){return y.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==l)||((b=c).nodeType?j(a,c,d):k(a,c,d))}];for(;i<f;i++)if(c=e.relative[a[i].type])m=[bi(bj(m),c)];else{c=e.filter[a[i].type].apply(null,a[i].matches);if(c[o]){d=++i;for(;d<f;d++)if(e.relative[a[d].type])break;return bl(i>1&&bj(m),i>1&&a.slice(0,i-1).join("").replace(L,"$1"),c,i<d&&bm(a.slice(i,d)),d<f&&bm(a=a.slice(d)),d<f&&a.join(""))}m.push(c)}return bj(m)}function bn(a,b){var d=b.length>0,f=a.length>0,g=function(h,i,j,k,m){var n,o,p,q=[],s=0,u="0",x=h&&[],y=m!=null,z=l,A=h||f&&e.find.TAG("*",m&&i.parentNode||i),B=t+=z==null?1:Math.E;y&&(l=i!==r&&i,c=g.el);for(;(n=A[u])!=null;u++){if(f&&n){for(o=0;p=a[o];o++)if(p(n,i,j)){k.push(n);break}y&&(t=B,c=++g.el)}d&&((n=!p&&n)&&s--,h&&x.push(n))}s+=u;if(d&&u!==s){for(o=0;p=b[o];o++)p(x,q,i,j);if(h){if(s>0)while(u--)!x[u]&&!q[u]&&(q[u]=v.call(k));q=bk(q)}w.apply(k,q),y&&!h&&q.length>0&&s+b.length>1&&bc.uniqueSort(k)}return y&&(t=B,l=z),x};return g.el=0,d?z(g):g}function bo(a,b,c,d){var e=0,f=b.length;for(;e<f;e++)bc(a,b[e],c,d);return c}function bp(a,b,c,d,f){var g,h,j,k,l,m=bh(a),n=m.length;if(!d&&m.length===1){h=m[0]=m[0].slice(0);if(h.length>2&&(j=h[0]).type==="ID"&&b.nodeType===9&&!f&&e.relative[h[1].type]){b=e.find.ID(j.matches[0].replace(V,""),b,f)[0];if(!b)return c;a=a.slice(h.shift().length)}for(g=W.POS.test(a)?-1:h.length-1;g>=0;g--){j=h[g];if(e.relative[k=j.type])break;if(l=e.find[k])if(d=l(j.matches[0].replace(V,""),R.test(h[0].type)&&b.parentNode||b,f)){h.splice(g,1),a=d.length&&h.join("");if(!a)return w.apply(c,x.call(d,0)),c;break}}}return i(a,m)(d,b,f,c,R.test(a)),c}function bq(){}var c,d,e,f,g,h,i,j,k,l,m=!0,n="undefined",o=("sizcache"+Math.random()).replace(".",""),q=String,r=a.document,s=r.documentElement,t=0,u=0,v=[].pop,w=[].push,x=[].slice,y=[].indexOf||function(a){var b=0,c=this.length;for(;b<c;b++)if(this[b]===a)return b;return-1},z=function(a,b){return a[o]=b==null||b,a},A=function(){var a={},b=[];return z(function(c,d){return b.push(c)>e.cacheLength&&delete a[b.shift()],a[c]=d},a)},B=A(),C=A(),D=A(),E="[\\x20\\t\\r\\n\\f]",F="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",G=F.replace("w","w#"),H="([*^$|!~]?=)",I="\\["+E+"*("+F+")"+E+"*(?:"+H+E+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+G+")|)|)"+E+"*\\]",J=":("+F+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+I+")|[^:]|\\\\.)*|.*))\\)|)",K=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+E+"*((?:-\\d)?\\d*)"+E+"*\\)|)(?=[^-]|$)",L=new RegExp("^"+E+"+|((?:^|[^\\\\])(?:\\\\.)*)"+E+"+$","g"),M=new RegExp("^"+E+"*,"+E+"*"),N=new RegExp("^"+E+"*([\\x20\\t\\r\\n\\f>+~])"+E+"*"),O=new RegExp(J),P=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,Q=/^:not/,R=/[\x20\t\r\n\f]*[+~]/,S=/:not\($/,T=/h\d/i,U=/input|select|textarea|button/i,V=/\\(?!\\)/g,W={ID:new RegExp("^#("+F+")"),CLASS:new RegExp("^\\.("+F+")"),NAME:new RegExp("^\\[name=['\"]?("+F+")['\"]?\\]"),TAG:new RegExp("^("+F.replace("w","w*")+")"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+J),POS:new RegExp(K,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+E+"*(even|odd|(([+-]|)(\\d*)n|)"+E+"*(?:([+-]|)"+E+"*(\\d+)|))"+E+"*\\)|)","i"),needsContext:new RegExp("^"+E+"*[>+~]|"+K,"i")},X=function(a){var b=r.createElement("div");try{return a(b)}catch(c){return!1}finally{b=null}},Y=X(function(a){return a.appendChild(r.createComment("")),!a.getElementsByTagName("*").length}),Z=X(function(a){return a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!==n&&a.firstChild.getAttribute("href")==="#"}),$=X(function(a){a.innerHTML="<select></select>";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),_=X(function(a){return a.innerHTML="<div class='hidden e'></div><div class='hidden'></div>",!a.getElementsByClassName||!a.getElementsByClassName("e").length?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length===2)}),ba=X(function(a){a.id=o+0,a.innerHTML="<a name='"+o+"'></a><div name='"+o+"'></div>",s.insertBefore(a,s.firstChild);var b=r.getElementsByName&&r.getElementsByName(o).length===2+r.getElementsByName(o+0).length;return d=!r.getElementById(o),s.removeChild(a),b});try{x.call(s.childNodes,0)[0].nodeType}catch(bb){x=function(a){var b,c=[];for(;b=this[a];a++)c.push(b);return c}}bc.matches=function(a,b){return bc(a,null,null,b)},bc.matchesSelector=function(a,b){return bc(b,null,null,[a]).length>0},f=bc.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(e===1||e===9||e===11){if(typeof a.textContent=="string")return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=f(a)}else if(e===3||e===4)return a.nodeValue}else for(;b=a[d];d++)c+=f(b);return c},g=bc.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?b.nodeName!=="HTML":!1},h=bc.contains=s.contains?function(a,b){var c=a.nodeType===9?a.documentElement:a,d=b&&b.parentNode;return a===d||!!(d&&d.nodeType===1&&c.contains&&c.contains(d))}:s.compareDocumentPosition?function(a,b){return b&&!!(a.compareDocumentPosition(b)&16)}:function(a,b){while(b=b.parentNode)if(b===a)return!0;return!1},bc.attr=function(a,b){var c,d=g(a);return d||(b=b.toLowerCase()),(c=e.attrHandle[b])?c(a):d||$?a.getAttribute(b):(c=a.getAttributeNode(b),c?typeof a[b]=="boolean"?a[b]?b:null:c.specified?c.value:null:null)},e=bc.selectors={cacheLength:50,createPseudo:z,match:W,attrHandle:Z?{}:{href:function(a){return a.getAttribute("href",2)},type:function(a){return a.getAttribute("type")}},find:{ID:d?function(a,b,c){if(typeof b.getElementById!==n&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==n&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==n&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:Y?function(a,b){if(typeof b.getElementsByTagName!==n)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c},NAME:ba&&function(a,b){if(typeof b.getElementsByName!==n)return b.getElementsByName(name)},CLASS:_&&function(a,b,c){if(typeof b.getElementsByClassName!==n&&!c)return b.getElementsByClassName(a)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(V,""),a[3]=(a[4]||a[5]||"").replace(V,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||bc.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&bc.error(a[0]),a},PSEUDO:function(a){var b,c;if(W.CHILD.test(a[0]))return null;if(a[3])a[2]=a[3];else if(b=a[4])O.test(b)&&(c=bh(b,!0))&&(c=b.indexOf(")",b.length-c)-b.length)&&(b=b.slice(0,c),a[0]=a[0].slice(0,c)),a[2]=b;return a.slice(0,3)}},filter:{ID:d?function(a){return a=a.replace(V,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(V,""),function(b){var c=typeof b.getAttributeNode!==n&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(V,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=B[o][a];return b||(b=B(a,new RegExp("(^|"+E+")"+a+"("+E+"|$)"))),function(a){return b.test(a.className||typeof a.getAttribute!==n&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return function(d,e){var f=bc.attr(d,a);return f==null?b==="!=":b?(f+="",b==="="?f===c:b==="!="?f!==c:b==="^="?c&&f.indexOf(c)===0:b==="*="?c&&f.indexOf(c)>-1:b==="$="?c&&f.substr(f.length-c.length)===c:b==="~="?(" "+f+" ").indexOf(c)>-1:b==="|="?f===c||f.substr(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d){return a==="nth"?function(a){var b,e,f=a.parentNode;if(c===1&&d===0)return!0;if(f){e=0;for(b=f.firstChild;b;b=b.nextSibling)if(b.nodeType===1){e++;if(a===b)break}}return e-=d,e===c||e%c===0&&e/c>=0}:function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b){var c,d=e.pseudos[a]||e.setFilters[a.toLowerCase()]||bc.error("unsupported pseudo: "+a);return d[o]?d(b):d.length>1?(c=[a,a,"",b],e.setFilters.hasOwnProperty(a.toLowerCase())?z(function(a,c){var e,f=d(a,b),g=f.length;while(g--)e=y.call(a,f[g]),a[e]=!(c[e]=f[g])}):function(a){return d(a,0,c)}):d}},pseudos:{not:z(function(a){var b=[],c=[],d=i(a.replace(L,"$1"));return d[o]?z(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)if(f=g[h])a[h]=!(b[h]=f)}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:z(function(a){return function(b){return bc(a,b).length>0}}),contains:z(function(a){return function(b){return(b.textContent||b.innerText||f(b)).indexOf(a)>-1}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!e.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},header:function(a){return T.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:bd("radio"),checkbox:bd("checkbox"),file:bd("file"),password:bd("password"),image:bd("image"),submit:be("submit"),reset:be("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return U.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement},first:bf(function(a,b,c){return[0]}),last:bf(function(a,b,c){return[b-1]}),eq:bf(function(a,b,c){return[c<0?c+b:c]}),even:bf(function(a,b,c){for(var d=0;d<b;d+=2)a.push(d);return a}),odd:bf(function(a,b,c){for(var d=1;d<b;d+=2)a.push(d);return a}),lt:bf(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:bf(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},j=s.compareDocumentPosition?function(a,b){return a===b?(k=!0,0):(!a.compareDocumentPosition||!b.compareDocumentPosition?a.compareDocumentPosition:a.compareDocumentPosition(b)&4)?-1:1}:function(a,b){if(a===b)return k=!0,0;if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,h=b.parentNode,i=g;if(g===h)return bg(a,b);if(!g)return-1;if(!h)return 1;while(i)e.unshift(i),i=i.parentNode;i=h;while(i)f.unshift(i),i=i.parentNode;c=e.length,d=f.length;for(var j=0;j<c&&j<d;j++)if(e[j]!==f[j])return bg(e[j],f[j]);return j===c?bg(a,f[j],-1):bg(e[j],b,1)},[0,0].sort(j),m=!k,bc.uniqueSort=function(a){var b,c=1;k=m,a.sort(j);if(k)for(;b=a[c];c++)b===a[c-1]&&a.splice(c--,1);return a},bc.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},i=bc.compile=function(a,b){var c,d=[],e=[],f=D[o][a];if(!f){b||(b=bh(a)),c=b.length;while(c--)f=bm(b[c]),f[o]?d.push(f):e.push(f);f=D(a,bn(e,d))}return f},r.querySelectorAll&&function(){var a,b=bp,c=/'|\\/g,d=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,e=[":focus"],f=[":active",":focus"],h=s.matchesSelector||s.mozMatchesSelector||s.webkitMatchesSelector||s.oMatchesSelector||s.msMatchesSelector;X(function(a){a.innerHTML="<select><option selected=''></option></select>",a.querySelectorAll("[selected]").length||e.push("\\["+E+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),X(function(a){a.innerHTML="<p test=''></p>",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+E+"*(?:\"\"|'')"),a.innerHTML="<input type='hidden'/>",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=new RegExp(e.join("|")),bp=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a))){var i,j,k=!0,l=o,m=d,n=d.nodeType===9&&a;if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){i=bh(a),(k=d.getAttribute("id"))?l=k.replace(c,"\\$&"):d.setAttribute("id",l),l="[id='"+l+"'] ",j=i.length;while(j--)i[j]=l+i[j].join("");m=R.test(a)&&d.parentNode||d,n=i.join(",")}if(n)try{return w.apply(f,x.call(m.querySelectorAll(n),0)),f}catch(p){}finally{k||d.removeAttribute("id")}}return b(a,d,f,g,h)},h&&(X(function(b){a=h.call(b,"div");try{h.call(b,"[test!='']:sizzle"),f.push("!=",J)}catch(c){}}),f=new RegExp(f.join("|")),bc.matchesSelector=function(b,c){c=c.replace(d,"='$1']");if(!g(b)&&!f.test(c)&&(!e||!e.test(c)))try{var i=h.call(b,c);if(i||a||b.document&&b.document.nodeType!==11)return i}catch(j){}return bc(c,null,null,[b]).length>0})}(),e.pseudos.nth=e.pseudos.eq,e.filters=bq.prototype=e.pseudos,e.setFilters=new bq,bc.attr=p.attr,p.find=bc,p.expr=bc.selectors,p.expr[":"]=p.expr.pseudos,p.unique=bc.uniqueSort,p.text=bc.getText,p.isXMLDoc=bc.isXML,p.contains=bc.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b<c;b++)if(p.contains(h[b],this))return!0});g=this.pushStack("","find",a);for(b=0,c=this.length;b<c;b++){d=g.length,p.find(a,this[b],g);if(b>0)for(e=d;e<g.length;e++)for(f=0;f<d;f++)if(g[f]===g[e]){g.splice(e--,1);break}}return g},has:function(a){var b,c=p(a,this),d=c.length;return this.filter(function(){for(b=0;b<d;b++)if(p.contains(this,c[b]))return!0})},not:function(a){return this.pushStack(bj(this,a,!1),"not",a)},filter:function(a){return this.pushStack(bj(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?bf.test(a)?p(a,this.context).index(this[0])>=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d<e;d++){c=this[d];while(c&&c.ownerDocument&&c!==b&&c.nodeType!==11){if(g?g.index(c)>-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/<tbody/i,br=/<|&#?\w+;/,bs=/<(?:script|style|link)/i,bt=/<(?:script|object|embed|option|style)/i,bu=new RegExp("<(?:"+bl+")[\\s/>]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,bz={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X<div>","</div>"]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(f){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){return bh(this[0])?this.length?this.pushStack(p(p.isFunction(a)?a():a),"replaceWith",a):this:p.isFunction(a)?this.each(function(b){var c=p(this),d=c.html();c.replaceWith(a.call(this,b,d))}):(typeof a!="string"&&(a=p(a).detach()),this.each(function(){var b=this.nextSibling,c=this.parentNode;p(this).remove(),b?p(b).before(a):p(c).append(a)}))},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){a=[].concat.apply([],a);var e,f,g,h,i=0,j=a[0],k=[],l=this.length;if(!p.support.checkClone&&l>1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i<l;i++)d.call(c&&p.nodeName(this[i],"table")?bC(this[i],"tbody"):this[i],i===h?g:p.clone(g,!0,!0))}g=f=null,k.length&&p.each(k,function(a,b){b.src?p.ajax?p.ajax({url:b.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):p.error("no ajax"):p.globalEval((b.text||b.textContent||b.innerHTML||"").replace(by,"")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),p.buildFragment=function(a,c,d){var f,g,h,i=a[0];return c=c||e,c=!c.nodeType&&c[0]||c,c=c.ownerDocument||c,a.length===1&&typeof i=="string"&&i.length<512&&c===e&&i.charAt(0)==="<"&&!bt.test(i)&&(p.support.checkClone||!bw.test(i))&&(p.support.html5Clone||!bu.test(i))&&(g=!0,f=p.fragments[i],h=f!==b),f||(f=c.createDocumentFragment(),p.clean(a,c,f,d),g&&(p.fragments[i]=h&&f)),{fragment:f,cacheable:g}},p.fragments={},p.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){p.fn[a]=function(c){var d,e=0,f=[],g=p(c),h=g.length,i=this.length===1&&this[0].parentNode;if((i==null||i&&i.nodeType===11&&i.childNodes.length===1)&&h===1)return g[b](this[0]),this;for(;e<h;e++)d=(e>0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=b===e&&bA,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(f=0;(h=a[f])!=null;f++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{s=s||bk(b),l=b.createElement("div"),s.appendChild(l),h=h.replace(bo,"<$1></$2>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]==="<table>"&&!m?l.childNodes:[];for(g=n.length-1;g>=0;--g)p.nodeName(n[g],"tbody")&&!n[g].childNodes.length&&n[g].parentNode.removeChild(n[g])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l.parentNode.removeChild(l)}h.nodeType?t.push(h):p.merge(t,h)}l&&(h=l=s=null);if(!p.support.appendChecked)for(f=0;(h=t[f])!=null;f++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(f=0;(h=t[f])!=null;f++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[f+1,0].concat(r)),f+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.chrome?b.webkit=!0:b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^(none|table(?!-c[ea]).+)/,bO=/^margin/,bP=new RegExp("^("+q+")(.*)$","i"),bQ=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bR=new RegExp("^([-+])=("+q+")","i"),bS={},bT={position:"absolute",visibility:"hidden",display:"block"},bU={letterSpacing:0,fontWeight:400},bV=["Top","Right","Bottom","Left"],bW=["Webkit","O","Moz","ms"],bX=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return b$(this,!0)},hide:function(){return b$(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bX.apply(this,arguments):this.each(function(){(c?a:bZ(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bY(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bR.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bY(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bU&&(f=bU[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(b,c){var d,e,f,g,h=a.getComputedStyle(b,null),i=b.style;return h&&(d=h[c],d===""&&!p.contains(b.ownerDocument,b)&&(d=p.style(b,c)),bQ.test(d)&&bO.test(c)&&(e=i.width,f=i.minWidth,g=i.maxWidth,i.minWidth=i.maxWidth=i.width=d,d=h.width,i.width=e,i.minWidth=f,i.maxWidth=g)),d}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bQ.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth===0&&bN.test(bH(a,"display"))?p.swap(a,bT,function(){return cb(a,b,d)}):cb(a,b,d)},set:function(a,c,d){return b_(a,c,d?ca(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bQ.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bV[d]+b]=e[d]||e[d-2]||e[0];return f}},bO.test(a)||(p.cssHooks[a+b].set=b_)});var cd=/%20/g,ce=/\[\]$/,cf=/\r?\n/g,cg=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ch=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ch.test(this.nodeName)||cg.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(cf,"\r\n")}}):{name:b.name,value:c.replace(cf,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ci(d,a[d],c,f);return e.join("&").replace(cd,"+")};var cj,ck,cl=/#.*$/,cm=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,co=/^(?:GET|HEAD)$/,cp=/^\/\//,cq=/\?/,cr=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,cs=/([?&])_=[^&]*/,ct=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,cu=p.fn.load,cv={},cw={},cx=["*/"]+["*"];try{ck=f.href}catch(cy){ck=e.createElement("a"),ck.href="",ck=ck.href}cj=ct.exec(ck.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&cu)return cu.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):c&&typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("<div>").append(a.replace(cr,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cB(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cB(a,b),a},ajaxSettings:{url:ck,isLocal:cn.test(cj[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cx},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cz(cv),ajaxTransport:cz(cw),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cC(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cD(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=(c||y)+"",k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cm.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(cl,"").replace(cp,cj[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=ct.exec(l.url.toLowerCase())||!1,l.crossDomain=i&&i.join(":")+(i[3]?"":i[1]==="http:"?80:443)!==cj.join(":")+(cj[3]?"":cj[1]==="http:"?80:443)),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cA(cv,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!co.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cq.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cs,"$1_="+z);l.url=A+(A===l.url?(cq.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cx+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cA(cw,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cE=[],cF=/\?/,cG=/(=)\?(?=&|$)|\?\?/,cH=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cE.pop()||p.expando+"_"+cH++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cG.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cG.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cG,"$1"+f):m?c.data=i.replace(cG,"$1"+f):k&&(c.url+=(cF.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cE.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cI,cJ=a.ActiveXObject?function(){for(var a in cI)cI[a](0,1)}:!1,cK=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cL()||cM()}:cL,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cJ&&delete cI[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cK,cJ&&(cI||(cI={},p(a).unload(cJ)),cI[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cN,cO,cP=/^(?:toggle|show|hide)$/,cQ=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cR=/queueHooks$/,cS=[cY],cT={"*":[function(a,b){var c,d,e=this.createTween(a,b),f=cQ.exec(b),g=e.cur(),h=+g||0,i=1,j=20;if(f){c=+f[2],d=f[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&h){h=p.css(e.elem,a,!0)||c||1;do i=i||".5",h=h/i,p.style(e.elem,a,h+d);while(i!==(i=e.cur()/g)&&i!==1&&--j)}e.unit=d,e.start=h,e.end=f[1]?h+(f[1]+1)*c:c}return e}]};p.Animation=p.extend(cW,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d<e;d++)c=a[d],cT[c]=cT[c]||[],cT[c].unshift(b)},prefilter:function(a,b){b?cS.unshift(a):cS.push(a)}}),p.Tween=cZ,cZ.prototype={constructor:cZ,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(p.cssNumber[c]?"":"px")},cur:function(){var a=cZ.propHooks[this.prop];return a&&a.get?a.get(this):cZ.propHooks._default.get(this)},run:function(a){var b,c=cZ.propHooks[this.prop];return this.options.duration?this.pos=b=p.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):cZ.propHooks._default.set(this),this}},cZ.prototype.init.prototype=cZ.prototype,cZ.propHooks={_default:{get:function(a){var b;return a.elem[a.prop]==null||!!a.elem.style&&a.elem.style[a.prop]!=null?(b=p.css(a.elem,a.prop,!1,""),!b||b==="auto"?0:b):a.elem[a.prop]},set:function(a){p.fx.step[a.prop]?p.fx.step[a.prop](a):a.elem.style&&(a.elem.style[p.cssProps[a.prop]]!=null||p.cssHooks[a.prop])?p.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},cZ.propHooks.scrollTop=cZ.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},p.each(["toggle","show","hide"],function(a,b){var c=p.fn[b];p.fn[b]=function(d,e,f){return d==null||typeof d=="boolean"||!a&&p.isFunction(d)&&p.isFunction(e)?c.apply(this,arguments):this.animate(c$(b,!0),d,e,f)}}),p.fn.extend({fadeTo:function(a,b,c,d){return this.filter(bZ).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=p.isEmptyObject(a),f=p.speed(b,c,d),g=function(){var b=cW(this,p.extend({},a),f);e&&b.stop(!0)};return e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,c,d){var e=function(a){var b=a.stop;delete a.stop,b(d)};return typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,c=a!=null&&a+"queueHooks",f=p.timers,g=p._data(this);if(c)g[c]&&g[c].stop&&e(g[c]);else for(c in g)g[c]&&g[c].stop&&cR.test(c)&&e(g[c]);for(c=f.length;c--;)f[c].elem===this&&(a==null||f[c].queue===a)&&(f[c].anim.stop(d),b=!1,f.splice(c,1));(b||!d)&&p.dequeue(this,a)})}}),p.each({slideDown:c$("show"),slideUp:c$("hide"),slideToggle:c$("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){p.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),p.speed=function(a,b,c){var d=a&&typeof a=="object"?p.extend({},a):{complete:c||!c&&b||p.isFunction(a)&&a,duration:a,easing:c&&b||b&&!p.isFunction(b)&&b};d.duration=p.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in p.fx.speeds?p.fx.speeds[d.duration]:p.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";return d.old=d.complete,d.complete=function(){p.isFunction(d.old)&&d.old.call(this),d.queue&&p.dequeue(this,d.queue)},d},p.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},p.timers=[],p.fx=cZ.prototype.init,p.fx.tick=function(){var a,b=p.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||p.fx.stop()},p.fx.timer=function(a){a()&&p.timers.push(a)&&!cO&&(cO=setInterval(p.fx.tick,p.fx.interval))},p.fx.interval=13,p.fx.stop=function(){clearInterval(cO),cO=null},p.fx.speeds={slow:600,fast:200,_default:400},p.fx.step={},p.expr&&p.expr.filters&&(p.expr.filters.animated=function(a){return p.grep(p.timers,function(b){return a===b.elem}).length});var c_=/^(?:body|html)$/i;p.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){p.offset.setOffset(this,a,b)});var c,d,e,f,g,h,i,j={top:0,left:0},k=this[0],l=k&&k.ownerDocument;if(!l)return;return(d=l.body)===k?p.offset.bodyOffset(k):(c=l.documentElement,p.contains(c,k)?(typeof k.getBoundingClientRect!="undefined"&&(j=k.getBoundingClientRect()),e=da(l),f=c.clientTop||d.clientTop||0,g=c.clientLeft||d.clientLeft||0,h=e.pageYOffset||c.scrollTop,i=e.pageXOffset||c.scrollLeft,{top:j.top+h-f,left:j.left+i-g}):j)},p.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;return p.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(p.css(a,"marginTop"))||0,c+=parseFloat(p.css(a,"marginLeft"))||0),{top:b,left:c}},setOffset:function(a,b,c){var d=p.css(a,"position");d==="static"&&(a.style.position="relative");var e=p(a),f=e.offset(),g=p.css(a,"top"),h=p.css(a,"left"),i=(d==="absolute"||d==="fixed")&&p.inArray("auto",[g,h])>-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c_.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c_.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=da(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g,null)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window);
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery-1.3.2.min.js b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery-1.3.2.min.js
new file mode 100755
index 0000000000000000000000000000000000000000..55c2e6d71613814a2d8add4a655f3c1340306d87
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery-1.3.2.min.js
@@ -0,0 +1,19 @@
+/*
+ * jQuery JavaScript Library v1.3.2
+ * http://jquery.com/
+ *
+ * Copyright (c) 2009 John Resig
+ * Dual licensed under the MIT and GPL licenses.
+ * http://docs.jquery.com/License
+ *
+ * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009)
+ * Revision: 6246
+ */
+(function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.isArray(E)?E:o.makeArray(E))},selector:"",jquery:"1.3.2",size:function(){return this.length},get:function(E){return E===g?Array.prototype.slice.call(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,sort:[].sort,splice:[].splice,find:function(E){if(this.length===1){var F=this.pushStack([],"find",E);F.length=0;o.find(E,this[0],F);return F}else{return this.pushStack(o.unique(o.map(this,function(G){return o.find(E,G)})),"find",E)}},clone:function(G){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.outerHTML;if(!I){var J=this.ownerDocument.createElement("div");J.appendChild(this.cloneNode(true));I=J.innerHTML}return o.clean([I.replace(/ jQuery\d+="(?:\d+|null)"/g,"").replace(/^\s*/,"")])[0]}else{return this.cloneNode(true)}});if(G===true){var H=this.find("*").andSelf(),F=0;E.find("*").andSelf().each(function(){if(this.nodeName!==H[F].nodeName){return}var I=o.data(H[F],"events");for(var K in I){for(var J in I[K]){o.event.add(this,K,I[K][J],I[K][J].data)}}F++})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var G=o.expr.match.POS.test(E)?o(E):null,F=0;return this.map(function(){var H=this;while(H&&H.ownerDocument){if(G?G.index(H)>-1:o(H).is(E)){o.data(H,"closest",F);return H}H=H.parentNode;F++}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F<J;F++){var G=M[F];if(G.selected){K=o(G).val();if(H){return K}L.push(K)}}return L}return(E.value||"").replace(/\r/g,"")}return g}if(typeof K==="number"){K+=""}return this.each(function(){if(this.nodeType!=1){return}if(o.isArray(K)&&/radio|checkbox/.test(this.type)){this.checked=(o.inArray(this.value,K)>=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g,""):null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=o.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild;if(H){for(var G=0,E=this.length;G<E;G++){L.call(K(this[G],H),this.length>1||G>0?I.cloneNode(true):I)}}if(F){o.each(F,z)}}return this;function K(N,O){return M&&o.nodeName(N,"table")&&o.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H<I;H++){if((G=arguments[H])!=null){for(var F in G){var K=J[F],L=G[F];if(J===L){continue}if(E&&L&&typeof L==="object"&&!L.nodeType){J[F]=o.extend(E,K||(L.length!=null?[]:{}),L)}else{if(L!==g){J[F]=L}}}}}return J};var b=/z-?index|font-?weight|opacity|zoom|line-?height/i,q=document.defaultView||{},s=Object.prototype.toString;o.extend({noConflict:function(E){l.$=p;if(E){l.jQuery=y}return o},isFunction:function(E){return s.call(E)==="[object Function]"},isArray:function(E){return s.call(E)==="[object Array]"},isXMLDoc:function(E){return E.nodeType===9&&E.documentElement.nodeName!=="HTML"||!!E.ownerDocument&&o.isXMLDoc(E.ownerDocument)},globalEval:function(G){if(G&&/\S/.test(G)){var F=document.getElementsByTagName("head")[0]||document.documentElement,E=document.createElement("script");E.type="text/javascript";if(o.support.scriptEval){E.appendChild(document.createTextNode(G))}else{E.text=G}F.insertBefore(E,F.firstChild);F.removeChild(E)}},nodeName:function(F,E){return F.nodeName&&F.nodeName.toUpperCase()==E.toUpperCase()},each:function(G,K,F){var E,H=0,I=G.length;if(F){if(I===g){for(E in G){if(K.apply(G[E],F)===false){break}}}else{for(;H<I;){if(K.apply(G[H++],F)===false){break}}}}else{if(I===g){for(E in G){if(K.call(G[E],E,G[E])===false){break}}}else{for(var J=G[0];H<I&&K.call(J,H,J)!==false;J=G[++H]){}}}return G},prop:function(H,I,G,F,E){if(o.isFunction(I)){I=I.call(H,F)}return typeof I==="number"&&G=="curCSS"&&!b.test(E)?I+"px":I},className:{add:function(E,F){o.each((F||"").split(/\s+/),function(G,H){if(E.nodeType==1&&!o.className.has(E.className,H)){E.className+=(E.className?" ":"")+H}})},remove:function(E,F){if(E.nodeType==1){E.className=F!==g?o.grep(E.className.split(/\s+/),function(G){return !o.className.has(F,G)}).join(" "):""}},has:function(F,E){return F&&o.inArray(E,(F.className||F).toString().split(/\s+/))>-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(H,F,J,E){if(F=="width"||F=="height"){var L,G={position:"absolute",visibility:"hidden",display:"block"},K=F=="width"?["Left","Right"]:["Top","Bottom"];function I(){L=F=="width"?H.offsetWidth:H.offsetHeight;if(E==="border"){return}o.each(K,function(){if(!E){L-=parseFloat(o.curCSS(H,"padding"+this,true))||0}if(E==="margin"){L+=parseFloat(o.curCSS(H,"margin"+this,true))||0}else{L-=parseFloat(o.curCSS(H,"border"+this+"Width",true))||0}})}if(H.offsetWidth!==0){I()}else{o.swap(H,G,I)}return Math.max(0,Math.round(L))}return o.curCSS(H,F,J)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,S){if(typeof S==="number"){S+=""}if(!S){return}if(typeof S==="string"){S=S.replace(/(<(\w+)[^>]*?)\/>/g,function(U,V,T){return T.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?U:V+"></"+T+">"});var O=S.replace(/^\s+/,"").substring(0,10).toLowerCase();var Q=!O.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!O.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!O.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!O.indexOf("<td")||!O.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!O.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||!o.support.htmlSerialize&&[1,"div<div>","</div>"]||[0,"",""];L.innerHTML=Q[1]+S+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var R=/<tbody/i.test(S),N=!O.indexOf("<table")&&!R?L.firstChild&&L.firstChild.childNodes:Q[1]=="<table>"&&!R?L.childNodes:[];for(var M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(S)){L.insertBefore(K.createTextNode(S.match(/^\s*/)[0]),L.firstChild)}S=o.makeArray(L.childNodes)}if(S.nodeType){G.push(S)}else{G=o.merge(G,S)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E<F;E++){if(H[E]===G){return E}}return -1},merge:function(H,E){var F=0,G,I=H.length;if(!o.support.getAll){while((G=E[F++])!=null){if(G.nodeType!=8){H[I++]=G}}}else{while((G=E[F++])!=null){H[I++]=G}}return H},unique:function(K){var F=[],E={};try{for(var G=0,H=K.length;G<H;G++){var J=o.data(K[G]);if(!E[J]){E[J]=true;F.push(K[G])}}}catch(I){F=K}return F},grep:function(F,J,E){var G=[];for(var H=0,I=F.length;H<I;H++){if(!E!=!J(F[H],H)){G.push(F[H])}}return G},map:function(E,J){var F=[];for(var G=0,H=E.length;G<H;G++){var I=J(E[G],G);if(I!=null){F[F.length]=I}}return F.concat.apply([],F)}});var C=navigator.userAgent.toLowerCase();o.browser={version:(C.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[0,"0"])[1],safari:/webkit/.test(C),opera:/opera/.test(C),msie:/msie/.test(C)&&!/opera/.test(C),mozilla:/mozilla/.test(C)&&!/(compatible|webkit)/.test(C)};o.each({parent:function(E){return E.parentNode},parents:function(E){return o.dir(E,"parentNode")},next:function(E){return o.nth(E,2,"nextSibling")},prev:function(E){return o.nth(E,2,"previousSibling")},nextAll:function(E){return o.dir(E,"nextSibling")},prevAll:function(E){return o.dir(E,"previousSibling")},siblings:function(E){return o.sibling(E.parentNode.firstChild,E)},children:function(E){return o.sibling(E.firstChild)},contents:function(E){return o.nodeName(E,"iframe")?E.contentDocument||E.contentWindow.document:o.makeArray(E.childNodes)}},function(E,F){o.fn[E]=function(G){var H=o.map(this,F);if(G&&typeof G=="string"){H=o.multiFilter(G,H)}return this.pushStack(o.unique(H),E,G)}});o.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(E,F){o.fn[E]=function(G){var J=[],L=o(G);for(var K=0,H=L.length;K<H;K++){var I=(K>0?this.clone(true):this).get();o.fn[F].apply(o(L[K]),I);J=J.concat(I)}return this.pushStack(J,E,G)}});o.each({removeAttr:function(E){o.attr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(this).children().remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}});
+/*
+ * Sizzle CSS Selector Engine - v0.9.3
+ *  Copyright 2009, The Dojo Foundation
+ *  Released under the MIT, BSD, and GPL Licenses.
+ *  More information: http://sizzlejs.com/
+ */
+(function(){var R=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,L=0,H=Object.prototype.toString;var F=function(Y,U,ab,ac){ab=ab||[];U=U||document;if(U.nodeType!==1&&U.nodeType!==9){return[]}if(!Y||typeof Y!=="string"){return ab}var Z=[],W,af,ai,T,ad,V,X=true;R.lastIndex=0;while((W=R.exec(Y))!==null){Z.push(W[1]);if(W[2]){V=RegExp.rightContext;break}}if(Z.length>1&&M.exec(Y)){if(Z.length===2&&I.relative[Z[0]]){af=J(Z[0]+Z[1],U)}else{af=I.relative[Z[0]]?[U]:F(Z.shift(),U);while(Z.length){Y=Z.shift();if(I.relative[Y]){Y+=Z.shift()}af=J(Y,af)}}}else{var ae=ac?{expr:Z.pop(),set:E(ac)}:F.find(Z.pop(),Z.length===1&&U.parentNode?U.parentNode:U,Q(U));af=F.filter(ae.expr,ae.set);if(Z.length>0){ai=E(af)}else{X=false}while(Z.length){var ah=Z.pop(),ag=ah;if(!I.relative[ah]){ah=""}else{ag=Z.pop()}if(ag==null){ag=U}I.relative[ah](ai,ag,Q(U))}}if(!ai){ai=af}if(!ai){throw"Syntax error, unrecognized expression: "+(ah||Y)}if(H.call(ai)==="[object Array]"){if(!X){ab.push.apply(ab,ai)}else{if(U.nodeType===1){for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&(ai[aa]===true||ai[aa].nodeType===1&&K(U,ai[aa]))){ab.push(af[aa])}}}else{for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&ai[aa].nodeType===1){ab.push(af[aa])}}}}}else{E(ai,ab)}if(V){F(V,U,ab,ac);if(G){hasDuplicate=false;ab.sort(G);if(hasDuplicate){for(var aa=1;aa<ab.length;aa++){if(ab[aa]===ab[aa-1]){ab.splice(aa--,1)}}}}}return ab};F.matches=function(T,U){return F(T,null,null,U)};F.find=function(aa,T,ab){var Z,X;if(!aa){return[]}for(var W=0,V=I.order.length;W<V;W++){var Y=I.order[W],X;if((X=I.match[Y].exec(aa))){var U=RegExp.leftContext;if(U.substr(U.length-1)!=="\\"){X[1]=(X[1]||"").replace(/\\/g,"");Z=I.find[Y](X,T,ab);if(Z!=null){aa=aa.replace(I.match[Y],"");break}}}}if(!Z){Z=T.getElementsByTagName("*")}return{set:Z,expr:aa}};F.filter=function(ad,ac,ag,W){var V=ad,ai=[],aa=ac,Y,T,Z=ac&&ac[0]&&Q(ac[0]);while(ad&&ac.length){for(var ab in I.filter){if((Y=I.match[ab].exec(ad))!=null){var U=I.filter[ab],ah,af;T=false;if(aa==ai){ai=[]}if(I.preFilter[ab]){Y=I.preFilter[ab](Y,aa,ag,ai,W,Z);if(!Y){T=ah=true}else{if(Y===true){continue}}}if(Y){for(var X=0;(af=aa[X])!=null;X++){if(af){ah=U(af,Y,X,aa);var ae=W^!!ah;if(ag&&ah!=null){if(ae){T=true}else{aa[X]=false}}else{if(ae){ai.push(af);T=true}}}}}if(ah!==g){if(!ag){aa=ai}ad=ad.replace(I.match[ab],"");if(!T){return[]}break}}}if(ad==V){if(T==null){throw"Syntax error, unrecognized expression: "+ad}else{break}}V=ad}return aa};var I=F.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(T){return T.getAttribute("href")}},relative:{"+":function(aa,T,Z){var X=typeof T==="string",ab=X&&!/\W/.test(T),Y=X&&!ab;if(ab&&!Z){T=T.toUpperCase()}for(var W=0,V=aa.length,U;W<V;W++){if((U=aa[W])){while((U=U.previousSibling)&&U.nodeType!==1){}aa[W]=Y||U&&U.nodeName===T?U||false:U===T}}if(Y){F.filter(T,aa,true)}},">":function(Z,U,aa){var X=typeof U==="string";if(X&&!/\W/.test(U)){U=aa?U:U.toUpperCase();for(var V=0,T=Z.length;V<T;V++){var Y=Z[V];if(Y){var W=Y.parentNode;Z[V]=W.nodeName===U?W:false}}}else{for(var V=0,T=Z.length;V<T;V++){var Y=Z[V];if(Y){Z[V]=X?Y.parentNode:Y.parentNode===U}}if(X){F.filter(U,Z,true)}}},"":function(W,U,Y){var V=L++,T=S;if(!U.match(/\W/)){var X=U=Y?U:U.toUpperCase();T=P}T("parentNode",U,V,W,X,Y)},"~":function(W,U,Y){var V=L++,T=S;if(typeof U==="string"&&!U.match(/\W/)){var X=U=Y?U:U.toUpperCase();T=P}T("previousSibling",U,V,W,X,Y)}},find:{ID:function(U,V,W){if(typeof V.getElementById!=="undefined"&&!W){var T=V.getElementById(U[1]);return T?[T]:[]}},NAME:function(V,Y,Z){if(typeof Y.getElementsByName!=="undefined"){var U=[],X=Y.getElementsByName(V[1]);for(var W=0,T=X.length;W<T;W++){if(X[W].getAttribute("name")===V[1]){U.push(X[W])}}return U.length===0?null:U}},TAG:function(T,U){return U.getElementsByTagName(T[1])}},preFilter:{CLASS:function(W,U,V,T,Z,aa){W=" "+W[1].replace(/\\/g,"")+" ";if(aa){return W}for(var X=0,Y;(Y=U[X])!=null;X++){if(Y){if(Z^(Y.className&&(" "+Y.className+" ").indexOf(W)>=0)){if(!V){T.push(Y)}}else{if(V){U[X]=false}}}}return false},ID:function(T){return T[1].replace(/\\/g,"")},TAG:function(U,T){for(var V=0;T[V]===false;V++){}return T[V]&&Q(T[V])?U[1]:U[1].toUpperCase()},CHILD:function(T){if(T[1]=="nth"){var U=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(T[2]=="even"&&"2n"||T[2]=="odd"&&"2n+1"||!/\D/.test(T[2])&&"0n+"+T[2]||T[2]);T[2]=(U[1]+(U[2]||1))-0;T[3]=U[3]-0}T[0]=L++;return T},ATTR:function(X,U,V,T,Y,Z){var W=X[1].replace(/\\/g,"");if(!Z&&I.attrMap[W]){X[1]=I.attrMap[W]}if(X[2]==="~="){X[4]=" "+X[4]+" "}return X},PSEUDO:function(X,U,V,T,Y){if(X[1]==="not"){if(X[3].match(R).length>1||/^\w/.test(X[3])){X[3]=F(X[3],null,null,U)}else{var W=F.filter(X[3],U,V,true^Y);if(!V){T.push.apply(T,W)}return false}}else{if(I.match.POS.test(X[0])||I.match.CHILD.test(X[0])){return true}}return X},POS:function(T){T.unshift(true);return T}},filters:{enabled:function(T){return T.disabled===false&&T.type!=="hidden"},disabled:function(T){return T.disabled===true},checked:function(T){return T.checked===true},selected:function(T){T.parentNode.selectedIndex;return T.selected===true},parent:function(T){return !!T.firstChild},empty:function(T){return !T.firstChild},has:function(V,U,T){return !!F(T[3],V).length},header:function(T){return/h\d/i.test(T.nodeName)},text:function(T){return"text"===T.type},radio:function(T){return"radio"===T.type},checkbox:function(T){return"checkbox"===T.type},file:function(T){return"file"===T.type},password:function(T){return"password"===T.type},submit:function(T){return"submit"===T.type},image:function(T){return"image"===T.type},reset:function(T){return"reset"===T.type},button:function(T){return"button"===T.type||T.nodeName.toUpperCase()==="BUTTON"},input:function(T){return/input|select|textarea|button/i.test(T.nodeName)}},setFilters:{first:function(U,T){return T===0},last:function(V,U,T,W){return U===W.length-1},even:function(U,T){return T%2===0},odd:function(U,T){return T%2===1},lt:function(V,U,T){return U<T[3]-0},gt:function(V,U,T){return U>T[3]-0},nth:function(V,U,T){return T[3]-0==U},eq:function(V,U,T){return T[3]-0==U}},filter:{PSEUDO:function(Z,V,W,aa){var U=V[1],X=I.filters[U];if(X){return X(Z,W,V,aa)}else{if(U==="contains"){return(Z.textContent||Z.innerText||"").indexOf(V[3])>=0}else{if(U==="not"){var Y=V[3];for(var W=0,T=Y.length;W<T;W++){if(Y[W]===Z){return false}}return true}}}},CHILD:function(T,W){var Z=W[1],U=T;switch(Z){case"only":case"first":while(U=U.previousSibling){if(U.nodeType===1){return false}}if(Z=="first"){return true}U=T;case"last":while(U=U.nextSibling){if(U.nodeType===1){return false}}return true;case"nth":var V=W[2],ac=W[3];if(V==1&&ac==0){return true}var Y=W[0],ab=T.parentNode;if(ab&&(ab.sizcache!==Y||!T.nodeIndex)){var X=0;for(U=ab.firstChild;U;U=U.nextSibling){if(U.nodeType===1){U.nodeIndex=++X}}ab.sizcache=Y}var aa=T.nodeIndex-ac;if(V==0){return aa==0}else{return(aa%V==0&&aa/V>=0)}}},ID:function(U,T){return U.nodeType===1&&U.getAttribute("id")===T},TAG:function(U,T){return(T==="*"&&U.nodeType===1)||U.nodeName===T},CLASS:function(U,T){return(" "+(U.className||U.getAttribute("class"))+" ").indexOf(T)>-1},ATTR:function(Y,W){var V=W[1],T=I.attrHandle[V]?I.attrHandle[V](Y):Y[V]!=null?Y[V]:Y.getAttribute(V),Z=T+"",X=W[2],U=W[4];return T==null?X==="!=":X==="="?Z===U:X==="*="?Z.indexOf(U)>=0:X==="~="?(" "+Z+" ").indexOf(U)>=0:!U?Z&&T!==false:X==="!="?Z!=U:X==="^="?Z.indexOf(U)===0:X==="$="?Z.substr(Z.length-U.length)===U:X==="|="?Z===U||Z.substr(0,U.length+1)===U+"-":false},POS:function(X,U,V,Y){var T=U[2],W=I.setFilters[T];if(W){return W(X,V,U,Y)}}}};var M=I.match.POS;for(var O in I.match){I.match[O]=RegExp(I.match[O].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(U,T){U=Array.prototype.slice.call(U);if(T){T.push.apply(T,U);return T}return U};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(N){E=function(X,W){var U=W||[];if(H.call(X)==="[object Array]"){Array.prototype.push.apply(U,X)}else{if(typeof X.length==="number"){for(var V=0,T=X.length;V<T;V++){U.push(X[V])}}else{for(var V=0;X[V];V++){U.push(X[V])}}}return U}}var G;if(document.documentElement.compareDocumentPosition){G=function(U,T){var V=U.compareDocumentPosition(T)&4?-1:U===T?0:1;if(V===0){hasDuplicate=true}return V}}else{if("sourceIndex" in document.documentElement){G=function(U,T){var V=U.sourceIndex-T.sourceIndex;if(V===0){hasDuplicate=true}return V}}else{if(document.createRange){G=function(W,U){var V=W.ownerDocument.createRange(),T=U.ownerDocument.createRange();V.selectNode(W);V.collapse(true);T.selectNode(U);T.collapse(true);var X=V.compareBoundaryPoints(Range.START_TO_END,T);if(X===0){hasDuplicate=true}return X}}}}(function(){var U=document.createElement("form"),V="script"+(new Date).getTime();U.innerHTML="<input name='"+V+"'/>";var T=document.documentElement;T.insertBefore(U,T.firstChild);if(!!document.getElementById(V)){I.find.ID=function(X,Y,Z){if(typeof Y.getElementById!=="undefined"&&!Z){var W=Y.getElementById(X[1]);return W?W.id===X[1]||typeof W.getAttributeNode!=="undefined"&&W.getAttributeNode("id").nodeValue===X[1]?[W]:g:[]}};I.filter.ID=function(Y,W){var X=typeof Y.getAttributeNode!=="undefined"&&Y.getAttributeNode("id");return Y.nodeType===1&&X&&X.nodeValue===W}}T.removeChild(U)})();(function(){var T=document.createElement("div");T.appendChild(document.createComment(""));if(T.getElementsByTagName("*").length>0){I.find.TAG=function(U,Y){var X=Y.getElementsByTagName(U[1]);if(U[1]==="*"){var W=[];for(var V=0;X[V];V++){if(X[V].nodeType===1){W.push(X[V])}}X=W}return X}}T.innerHTML="<a href='#'></a>";if(T.firstChild&&typeof T.firstChild.getAttribute!=="undefined"&&T.firstChild.getAttribute("href")!=="#"){I.attrHandle.href=function(U){return U.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var T=F,U=document.createElement("div");U.innerHTML="<p class='TEST'></p>";if(U.querySelectorAll&&U.querySelectorAll(".TEST").length===0){return}F=function(Y,X,V,W){X=X||document;if(!W&&X.nodeType===9&&!Q(X)){try{return E(X.querySelectorAll(Y),V)}catch(Z){}}return T(Y,X,V,W)};F.find=T.find;F.filter=T.filter;F.selectors=T.selectors;F.matches=T.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var T=document.createElement("div");T.innerHTML="<div class='test e'></div><div class='test'></div>";if(T.getElementsByClassName("e").length===0){return}T.lastChild.className="e";if(T.getElementsByClassName("e").length===1){return}I.order.splice(1,0,"CLASS");I.find.CLASS=function(U,V,W){if(typeof V.getElementsByClassName!=="undefined"&&!W){return V.getElementsByClassName(U[1])}}})()}function P(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W<V;W++){var T=ad[W];if(T){if(ab&&T.nodeType===1){T.sizcache=Y;T.sizset=W}T=T[U];var X=false;while(T){if(T.sizcache===Y){X=ad[T.sizset];break}if(T.nodeType===1&&!ac){T.sizcache=Y;T.sizset=W}if(T.nodeName===Z){X=T;break}T=T[U]}ad[W]=X}}}function S(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W<V;W++){var T=ad[W];if(T){if(ab&&T.nodeType===1){T.sizcache=Y;T.sizset=W}T=T[U];var X=false;while(T){if(T.sizcache===Y){X=ad[T.sizset];break}if(T.nodeType===1){if(!ac){T.sizcache=Y;T.sizset=W}if(typeof Z!=="string"){if(T===Z){X=true;break}}else{if(F.filter(Z,[T]).length>0){X=T;break}}}T=T[U]}ad[W]=X}}}var K=document.compareDocumentPosition?function(U,T){return U.compareDocumentPosition(T)&16}:function(U,T){return U!==T&&(U.contains?U.contains(T):true)};var Q=function(T){return T.nodeType===9&&T.documentElement.nodeName!=="HTML"||!!T.ownerDocument&&Q(T.ownerDocument)};var J=function(T,aa){var W=[],X="",Y,V=aa.nodeType?[aa]:aa;while((Y=I.match.PSEUDO.exec(T))){X+=Y[0];T=T.replace(I.match.PSEUDO,"")}T=I.relative[T]?T+"*":T;for(var Z=0,U=V.length;Z<U;Z++){F(T,V[Z],W)}return F.filter(X,W)};o.find=F;o.filter=F.filter;o.expr=F.selectors;o.expr[":"]=o.expr.filters;F.selectors.filters.hidden=function(T){return T.offsetWidth===0||T.offsetHeight===0};F.selectors.filters.visible=function(T){return T.offsetWidth>0||T.offsetHeight>0};F.selectors.filters.animated=function(T){return o.grep(o.timers,function(U){return T===U.elem}).length};o.multiFilter=function(V,T,U){if(U){V=":not("+V+")"}return F.matches(V,T)};o.dir=function(V,U){var T=[],W=V[U];while(W&&W!=document){if(W.nodeType==1){T.push(W)}W=W[U]}return T};o.nth=function(X,T,V,W){T=T||1;var U=0;for(;X;X=X[V]){if(X.nodeType==1&&++U==T){break}}return X};o.sibling=function(V,U){var T=[];for(;V;V=V.nextSibling){if(V.nodeType==1&&V!=U){T.push(V)}}return T};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var G=H;H=this.proxy(G);H.data=K}var E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",function(){return typeof o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var Q=O.split(".");O=Q.shift();var N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var P in G[O]){if(N.test(G[O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete G[O]}}})}for(F in G){break}if(!F){var L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var G=I.type||I;if(!E){I=typeof I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);K.currentTarget=this;var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(F<E.length){o.event.proxy(G,E[F++])}return this.click(o.event.proxy(G,function(H){this.lastToggle=(this.lastToggle||0)%F;H.preventDefault();return E[this.lastToggle++].apply(this,arguments)||false}))},hover:function(E,F){return this.mouseenter(E).mouseleave(F)},ready:function(E){B();if(o.isReady){E.call(document,o)}else{o.readyList.push(E)}return this},live:function(G,F){var E=o.event.proxy(F);E.guid+=this.selector+G;o(document).bind(i(G,this.selector),this.selector,E);return this},die:function(F,E){o(document).unbind(i(F,this.selector),E?{guid:E.guid+this.selector+F}:null);return this}});function c(H){var E=RegExp("(^|\\.)"+H.type+"(\\.|$)"),G=true,F=[];o.each(o.data(this,"events").live||[],function(I,J){if(E.test(J.type)){var K=o(H.target).closest(J.data)[0];if(K){F.push({elem:K,fn:J})}}});F.sort(function(J,I){return o.data(J.elem,"closest")-o.data(I.elem,"closest")});o.each(F,function(){if(this.fn.call(this.elem,H,this.fn.data)===false){return(G=false)}});return G}function i(F,E){return["live",F,E.replace(/\./g,"`").replace(/ /g,"|")].join(".")}o.extend({isReady:false,readyList:[],ready:function(){if(!o.isReady){o.isReady=true;if(o.readyList){o.each(o.readyList,function(){this.call(document,o)});o.readyList=null}o(document).triggerHandler("ready")}}});var x=false;function B(){if(x){return}x=true;if(document.addEventListener){document.addEventListener("DOMContentLoaded",function(){document.removeEventListener("DOMContentLoaded",arguments.callee,false);o.ready()},false)}else{if(document.attachEvent){document.attachEvent("onreadystatechange",function(){if(document.readyState==="complete"){document.detachEvent("onreadystatechange",arguments.callee);o.ready()}});if(document.documentElement.doScroll&&l==l.top){(function(){if(o.isReady){return}try{document.documentElement.doScroll("left")}catch(E){setTimeout(arguments.callee,0);return}o.ready()})()}}}o.event.add(l,"load",o.ready)}o.each(("blur,focus,load,resize,scroll,unload,click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave,change,select,submit,keydown,keypress,keyup,error").split(","),function(F,E){o.fn[E]=function(G){return G?this.bind(E,G):this.trigger(E)}});o(l).bind("unload",function(){for(var E in o.cache){if(E!=1&&o.cache[E].handle){o.event.remove(o.cache[E].handle.elem)}}});(function(){o.support={};var F=document.documentElement,G=document.createElement("script"),K=document.createElement("div"),J="script"+(new Date).getTime();K.style.display="none";K.innerHTML='   <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';var H=K.getElementsByTagName("*"),E=K.getElementsByTagName("a")[0];if(!H||!H.length||!E){return}o.support={leadingWhitespace:K.firstChild.nodeType==3,tbody:!K.getElementsByTagName("tbody").length,objectAll:!!K.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!K.getElementsByTagName("link").length,style:/red/.test(E.getAttribute("style")),hrefNormalized:E.getAttribute("href")==="/a",opacity:E.style.opacity==="0.5",cssFloat:!!E.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};G.type="text/javascript";try{G.appendChild(document.createTextNode("window."+J+"=1;"))}catch(I){}F.insertBefore(G,F.firstChild);if(l[J]){o.support.scriptEval=true;delete l[J]}F.removeChild(G);if(K.attachEvent&&K.fireEvent){K.attachEvent("onclick",function(){o.support.noCloneEvent=false;K.detachEvent("onclick",arguments.callee)});K.cloneNode(true).fireEvent("onclick")}o(function(){var L=document.createElement("div");L.style.width=L.style.paddingLeft="1px";document.body.appendChild(L);o.boxModel=o.support.boxModel=L.offsetWidth===2;document.body.removeChild(L).style.display="none"})})();var w=o.support.cssFloat?"cssFloat":"styleFloat";o.props={"for":"htmlFor","class":"className","float":w,cssFloat:w,styleFloat:w,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabindex:"tabIndex"};o.fn.extend({_load:o.fn.load,load:function(G,J,K){if(typeof G!=="string"){return this._load(G)}var I=G.indexOf(" ");if(I>=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("<div/>").append(M.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password|search/i.test(this.type))}).map(function(E,F){var G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return o.get(E,null,F,"script")},getJSON:function(E,F,G){return o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var E=e();var U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?"&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var H=document.getElementsByTagName("head")[0];var T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();T.onload=T.onreadystatechange=null;H.removeChild(T)}}}H.appendChild(T);return g}var K=false;var J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.dataType]+", */*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.async){N()}function I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return !F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof I==="string"){if(H=="script"){o.globalEval(I)}if(H=="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var F in E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return G.join("&").replace(/%20/g,"+")}});var m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var H=0,F=this.length;H<F;H++){var E=o.data(this[H],"olddisplay");this[H].style.display=E||"";if(o.css(this[H],"display")==="none"){var G=this[H].tagName,K;if(m[G]){K=m[G]}else{var I=o("<"+G+" />").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove();m[G]=K}o.data(this[H],"olddisplay",K)}}for(var H=0,F=this.length;H<F;H++){this[H].style.display=o.data(this[H],"olddisplay")||""}return this}},hide:function(H,I){if(H){return this.animate(t("hide",3),H,I)}else{for(var G=0,F=this.length;G<F;G++){var E=o.data(this[G],"olddisplay");if(!E&&E!=="none"){o.data(this[G],"olddisplay",o.css(this[G],"display"))}}for(var G=0,F=this.length;G<F;G++){this[G].style.display="none"}return this}},_toggle:o.fn.toggle,toggle:function(G,F){var E=typeof G==="boolean";return o.isFunction(G)&&o.isFunction(F)?this._toggle.apply(this,arguments):G==null||E?this.each(function(){var H=E?G:o(this).is(":hidden");o(this)[H?"show":"hide"]()}):this.animate(t("toggle",3),G,F)},fadeTo:function(E,G,F){return this.animate({opacity:G},E,F)},animate:function(I,F,H,G){var E=o.speed(F,H,G);return this[E.queue===false?"each":"queue"](function(){var K=o.extend({},E),M,L=this.nodeType==1&&o(this).is(":hidden"),J=this;for(M in I){if(I[M]=="hide"&&L||I[M]=="show"&&!L){return K.complete.call(this)}if((M=="height"||M=="width")&&this.style){K.display=o.css(this,"display");K.overflow=this.style.overflow}}if(K.overflow!=null){this.style.overflow="hidden"}K.curAnim=o.extend({},I);o.each(I,function(O,S){var R=new o.fx(J,K,O);if(/toggle|show|hide/.test(S)){R[S=="toggle"?L?"show":"hide":S](I)}else{var Q=S.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),T=R.cur(true)||0;if(Q){var N=parseFloat(Q[2]),P=Q[3]||"px";if(P!="px"){J.style[O]=(N||1)+P;T=((N||1)/R.cur(true))*T;J.style[O]=T+P}if(Q[1]){N=((Q[1]=="-="?-1:1)*N)+T}R.custom(T,N,P)}else{R.custom(T,S,"")}}});return true})},stop:function(F,E){var G=o.timers;if(F){this.queue([])}this.each(function(){for(var H=G.length-1;H>=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,duration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return E},easing:{linear:function(G,H,E,F){return E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},custom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var E=this;function F(J){return E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)&&!n){n=setInterval(function(){var K=o.timers;for(var J=0;J<K.length;J++){if(!K[J]()){K.splice(J--,1)}}if(!K.length){clearInterval(n);n=g}},13)}},show:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.show=true;this.custom(this.prop=="width"||this.prop=="height"?1:0,this.cur());o(this.elem).show()},hide:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(H){var G=e();if(H||G>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var E=true;for(var F in this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var I in this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return false}else{var J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E in M){F.style[E]=M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var I=0,H=0,F;if(this[0]){var G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j(G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return null}return H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(I,G){var E=I?"Left":"Top",H=I?"Right":"Bottom",F=G.toLowerCase();o.fn["inner"+G]=function(){return this[0]?o.css(this[0],F,false,"padding"):null};o.fn["outer"+G]=function(K){return this[0]?o.css(this[0],F,false,K?"margin":"border"):null};var J=G.toLowerCase();o.fn[J]=function(K){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+G]||document.body["client"+G]:this[0]==document?Math.max(document.documentElement["client"+G],document.body["scroll"+G],document.documentElement["scroll"+G],document.body["offset"+G],document.documentElement["offset"+G]):K===g?(this.length?o.css(this[0],J):null):this.css(J,typeof K==="string"?K:K+"px")}})})();
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery-1.8.2.min.js b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery-1.8.2.min.js
new file mode 100755
index 0000000000000000000000000000000000000000..f65cf1dc4573c51e54d7cf3772d06caf96726616
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery-1.8.2.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v1.8.2 jquery.com | jquery.org/license */
+(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d<e;d++)p.event.add(b,c,h[c][d])}g.data&&(g.data=p.extend({},g.data))}function bE(a,b){var c;if(b.nodeType!==1)return;b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?(b.parentNode&&(b.outerHTML=a.outerHTML),p.support.html5Clone&&a.innerHTML&&!p.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):c==="input"&&bv.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text),b.removeAttribute(p.expando)}function bF(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bG(a){bv.test(a.type)&&(a.defaultChecked=a.checked)}function bY(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=bW.length;while(e--){b=bW[e]+c;if(b in a)return b}return d}function bZ(a,b){return a=b||a,p.css(a,"display")==="none"||!p.contains(a.ownerDocument,a)}function b$(a,b){var c,d,e=[],f=0,g=a.length;for(;f<g;f++){c=a[f];if(!c.style)continue;e[f]=p._data(c,"olddisplay"),b?(!e[f]&&c.style.display==="none"&&(c.style.display=""),c.style.display===""&&bZ(c)&&(e[f]=p._data(c,"olddisplay",cc(c.nodeName)))):(d=bH(c,"display"),!e[f]&&d!=="none"&&p._data(c,"olddisplay",d))}for(f=0;f<g;f++){c=a[f];if(!c.style)continue;if(!b||c.style.display==="none"||c.style.display==="")c.style.display=b?e[f]||"":"none"}return a}function b_(a,b,c){var d=bP.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function ca(a,b,c,d){var e=c===(d?"border":"content")?4:b==="width"?1:0,f=0;for(;e<4;e+=2)c==="margin"&&(f+=p.css(a,c+bV[e],!0)),d?(c==="content"&&(f-=parseFloat(bH(a,"padding"+bV[e]))||0),c!=="margin"&&(f-=parseFloat(bH(a,"border"+bV[e]+"Width"))||0)):(f+=parseFloat(bH(a,"padding"+bV[e]))||0,c!=="padding"&&(f+=parseFloat(bH(a,"border"+bV[e]+"Width"))||0));return f}function cb(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=!0,f=p.support.boxSizing&&p.css(a,"boxSizing")==="border-box";if(d<=0||d==null){d=bH(a,b);if(d<0||d==null)d=a.style[b];if(bQ.test(d))return d;e=f&&(p.support.boxSizingReliable||d===a.style[b]),d=parseFloat(d)||0}return d+ca(a,b,c||(f?"border":"content"),e)+"px"}function cc(a){if(bS[a])return bS[a];var b=p("<"+a+">").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write("<!doctype html><html><body>"),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bS[a]=c,c}function ci(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||ce.test(a)?d(a,e):ci(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ci(a+"["+e+"]",b[e],c,d);else d(a,b)}function cz(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h<i;h++)d=g[h],f=/^\+/.test(d),f&&(d=d.substr(1)||"*"),e=a[d]=a[d]||[],e[f?"unshift":"push"](c)}}function cA(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h,i=a[f],j=0,k=i?i.length:0,l=a===cv;for(;j<k&&(l||!h);j++)h=i[j](c,d,e),typeof h=="string"&&(!l||g[h]?h=b:(c.dataTypes.unshift(h),h=cA(a,c,d,e,h,g)));return(l||!h)&&!g["*"]&&(h=cA(a,c,d,e,"*",g)),h}function cB(a,c){var d,e,f=p.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((f[d]?a:e||(e={}))[d]=c[d]);e&&p.extend(!0,a,e)}function cC(a,c,d){var e,f,g,h,i=a.contents,j=a.dataTypes,k=a.responseFields;for(f in k)f in d&&(c[k[f]]=d[f]);while(j[0]==="*")j.shift(),e===b&&(e=a.mimeType||c.getResponseHeader("content-type"));if(e)for(f in i)if(i[f]&&i[f].test(e)){j.unshift(f);break}if(j[0]in d)g=j[0];else{for(f in d){if(!j[0]||a.converters[f+" "+j[0]]){g=f;break}h||(h=f)}g=g||h}if(g)return g!==j[0]&&j.unshift(g),d[g]}function cD(a,b){var c,d,e,f,g=a.dataTypes.slice(),h=g[0],i={},j=0;a.dataFilter&&(b=a.dataFilter(b,a.dataType));if(g[1])for(c in a.converters)i[c.toLowerCase()]=a.converters[c];for(;e=g[++j];)if(e!=="*"){if(h!=="*"&&h!==e){c=i[h+" "+e]||i["* "+e];if(!c)for(d in i){f=d.split(" ");if(f[1]===e){c=i[h+" "+f[0]]||i["* "+f[0]];if(c){c===!0?c=i[d]:i[d]!==!0&&(e=f[0],g.splice(j--,0,e));break}}}if(c!==!0)if(c&&a["throws"])b=c(b);else try{b=c(b)}catch(k){return{state:"parsererror",error:c?k:"No conversion from "+h+" to "+e}}}h=e}return{state:"success",data:b}}function cL(){try{return new a.XMLHttpRequest}catch(b){}}function cM(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function cU(){return setTimeout(function(){cN=b},0),cN=p.now()}function cV(a,b){p.each(b,function(b,c){var d=(cT[b]||[]).concat(cT["*"]),e=0,f=d.length;for(;e<f;e++)if(d[e].call(a,b,c))return})}function cW(a,b,c){var d,e=0,f=0,g=cS.length,h=p.Deferred().always(function(){delete i.elem}),i=function(){var b=cN||cU(),c=Math.max(0,j.startTime+j.duration-b),d=1-(c/j.duration||0),e=0,f=j.tweens.length;for(;e<f;e++)j.tweens[e].run(d);return h.notifyWith(a,[j,d,c]),d<1&&f?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:p.extend({},b),opts:p.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:cN||cU(),duration:c.duration,tweens:[],createTween:function(b,c,d){var e=p.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(e),e},stop:function(b){var c=0,d=b?j.tweens.length:0;for(;c<d;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;cX(k,j.opts.specialEasing);for(;e<g;e++){d=cS[e].call(j,a,k,j.opts);if(d)return d}return cV(j,k),p.isFunction(j.opts.start)&&j.opts.start.call(a,j),p.fx.timer(p.extend(i,{anim:j,queue:j.opts.queue,elem:a})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}function cX(a,b){var c,d,e,f,g;for(c in a){d=p.camelCase(c),e=b[d],f=a[c],p.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=p.cssHooks[d];if(g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}}function cY(a,b,c){var d,e,f,g,h,i,j,k,l=this,m=a.style,n={},o=[],q=a.nodeType&&bZ(a);c.queue||(j=p._queueHooks(a,"fx"),j.unqueued==null&&(j.unqueued=0,k=j.empty.fire,j.empty.fire=function(){j.unqueued||k()}),j.unqueued++,l.always(function(){l.always(function(){j.unqueued--,p.queue(a,"fx").length||j.empty.fire()})})),a.nodeType===1&&("height"in b||"width"in b)&&(c.overflow=[m.overflow,m.overflowX,m.overflowY],p.css(a,"display")==="inline"&&p.css(a,"float")==="none"&&(!p.support.inlineBlockNeedsLayout||cc(a.nodeName)==="inline"?m.display="inline-block":m.zoom=1)),c.overflow&&(m.overflow="hidden",p.support.shrinkWrapBlocks||l.done(function(){m.overflow=c.overflow[0],m.overflowX=c.overflow[1],m.overflowY=c.overflow[2]}));for(d in b){f=b[d];if(cP.exec(f)){delete b[d];if(f===(q?"hide":"show"))continue;o.push(d)}}g=o.length;if(g){h=p._data(a,"fxshow")||p._data(a,"fxshow",{}),q?p(a).show():l.done(function(){p(a).hide()}),l.done(function(){var b;p.removeData(a,"fxshow",!0);for(b in n)p.style(a,b,n[b])});for(d=0;d<g;d++)e=o[d],i=l.createTween(e,q?h[e]:0),n[e]=h[e]||p.style(a,e),e in h||(h[e]=i.start,q&&(i.end=i.start,i.start=e==="width"||e==="height"?1:0))}}function cZ(a,b,c,d,e){return new cZ.prototype.init(a,b,c,d,e)}function c$(a,b){var c,d={height:a},e=0;b=b?1:0;for(;e<4;e+=2-b)c=bV[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function da(a){return p.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}var c,d,e=a.document,f=a.location,g=a.navigator,h=a.jQuery,i=a.$,j=Array.prototype.push,k=Array.prototype.slice,l=Array.prototype.indexOf,m=Object.prototype.toString,n=Object.prototype.hasOwnProperty,o=String.prototype.trim,p=function(a,b){return new p.fn.init(a,b,c)},q=/[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,r=/\S/,s=/\s+/,t=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,u=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.2",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i<j;i++)if((a=arguments[i])!=null)for(c in a){d=h[c],e=a[c];if(h===e)continue;k&&e&&(p.isPlainObject(e)||(f=p.isArray(e)))?(f?(f=!1,g=d&&p.isArray(d)?d:[]):g=d&&p.isPlainObject(d)?d:{},h[c]=p.extend(k,g,e)):e!==b&&(h[c]=e)}return h},p.extend({noConflict:function(b){return a.$===p&&(a.$=i),b&&a.jQuery===p&&(a.jQuery=h),p},isReady:!1,readyWait:1,holdReady:function(a){a?p.readyWait++:p.ready(!0)},ready:function(a){if(a===!0?--p.readyWait:p.isReady)return;if(!e.body)return setTimeout(p.ready,1);p.isReady=!0;if(a!==!0&&--p.readyWait>0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f<g;)if(c.apply(a[f++],d)===!1)break}else if(h){for(e in a)if(c.call(a[e],e,a[e])===!1)break}else for(;f<g;)if(c.call(a[f],f,a[f++])===!1)break;return a},trim:o&&!o.call(" ")?function(a){return a==null?"":o.call(a)}:function(a){return a==null?"":(a+"").replace(t,"")},makeArray:function(a,b){var c,d=b||[];return a!=null&&(c=p.type(a),a.length==null||c==="string"||c==="function"||c==="regexp"||p.isWindow(a)?j.call(d,a):p.merge(d,a)),d},inArray:function(a,b,c){var d;if(b){if(l)return l.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=c.length,e=a.length,f=0;if(typeof d=="number")for(;f<d;f++)a[e++]=c[f];else while(c[f]!==b)a[e++]=c[f++];return a.length=e,a},grep:function(a,b,c){var d,e=[],f=0,g=a.length;c=!!c;for(;f<g;f++)d=!!b(a[f],f),c!==d&&e.push(a[f]);return e},map:function(a,c,d){var e,f,g=[],h=0,i=a.length,j=a instanceof p||i!==b&&typeof i=="number"&&(i>0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h<i;h++)e=c(a[h],h,d),e!=null&&(g[g.length]=e);else for(f in a)e=c(a[f],f,d),e!=null&&(g[g.length]=e);return g.concat.apply([],g)},guid:1,proxy:function(a,c){var d,e,f;return typeof c=="string"&&(d=a[c],c=a,a=d),p.isFunction(a)?(e=k.call(arguments,2),f=function(){return a.apply(c,e.concat(k.call(arguments)))},f.guid=a.guid=a.guid||p.guid++,f):b},access:function(a,c,d,e,f,g,h){var i,j=d==null,k=0,l=a.length;if(d&&typeof d=="object"){for(k in d)p.access(a,c,k,d[k],1,g,e);f=1}else if(e!==b){i=h===b&&p.isFunction(e),j&&(i?(i=c,c=function(a,b,c){return i.call(p(a),c)}):(c.call(a,e),c=null));if(c)for(;k<l;k++)c(a[k],d,i?e.call(a[k],k,c(a[k],d)):e,h);f=1}return f?a:j?c.call(a):l?c(a[0],d):g},now:function(){return(new Date).getTime()}}),p.ready.promise=function(b){if(!d){d=p.Deferred();if(e.readyState==="complete")setTimeout(p.ready,1);else if(e.addEventListener)e.addEventListener("DOMContentLoaded",D,!1),a.addEventListener("load",p.ready,!1);else{e.attachEvent("onreadystatechange",D),a.attachEvent("onload",p.ready);var c=!1;try{c=a.frameElement==null&&e.documentElement}catch(f){}c&&c.doScroll&&function g(){if(!p.isReady){try{c.doScroll("left")}catch(a){return setTimeout(g,50)}p.ready()}}()}}return d.promise(b)},p.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){E["[object "+b+"]"]=b.toLowerCase()}),c=p(e);var F={};p.Callbacks=function(a){a=typeof a=="string"?F[a]||G(a):p.extend({},a);var c,d,e,f,g,h,i=[],j=!a.once&&[],k=function(b){c=a.memory&&b,d=!0,h=f||0,f=0,g=i.length,e=!0;for(;i&&h<g;h++)if(i[h].apply(b[0],b[1])===!1&&a.stopOnFalse){c=!1;break}e=!1,i&&(j?j.length&&k(j.shift()):c?i=[]:l.disable())},l={add:function(){if(i){var b=i.length;(function d(b){p.each(b,function(b,c){var e=p.type(c);e==="function"&&(!a.unique||!l.has(c))?i.push(c):c&&c.length&&e!=="string"&&d(c)})})(arguments),e?g=i.length:c&&(f=b,k(c))}return this},remove:function(){return i&&p.each(arguments,function(a,b){var c;while((c=p.inArray(b,i,c))>-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return a!=null?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b<d;b++)c[b]&&p.isFunction(c[b].promise)?c[b].promise().done(g(b,j,c)).fail(f.reject).progress(g(b,i,h)):--e}return e||f.resolveWith(j,c),f.promise()}}),p.support=function(){var b,c,d,f,g,h,i,j,k,l,m,n=e.createElement("div");n.setAttribute("className","t"),n.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="<div></div>",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||p.guid++:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e<f;e++)delete d[b[e]];if(!(c?K:p.isEmptyObject)(d))return}}if(!c){delete h[i].data;if(!K(h[i]))return}g?p.cleanData([a],!0):p.support.deleteExpando||h!=h.window?delete h[i]:h[i]=null},_data:function(a,b,c){return p.data(a,b,c,!0)},acceptData:function(a){var b=a.nodeName&&p.noData[a.nodeName.toLowerCase()];return!b||b!==!0&&a.getAttribute("classid")===b}}),p.fn.extend({data:function(a,c){var d,e,f,g,h,i=this[0],j=0,k=null;if(a===b){if(this.length){k=p.data(i);if(i.nodeType===1&&!p._data(i,"parsedAttrs")){f=i.attributes;for(h=f.length;j<h;j++)g=f[j].name,g.indexOf("data-")||(g=p.camelCase(g.substring(5)),J(i,g,k[g]));p._data(i,"parsedAttrs",!0)}}return k}return typeof a=="object"?this.each(function(){p.data(this,a)}):(d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!",p.access(this,function(c){if(c===b)return k=this.triggerHandler("getData"+e,[d[0]]),k===b&&i&&(k=p.data(i,a),k=J(i,a,k)),k===b&&d[1]?this.data(d[0]):k;d[1]=c,this.each(function(){var b=p(this);b.triggerHandler("setData"+e,d),p.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.length,e=c.shift(),f=p._queueHooks(a,b),g=function(){p.dequeue(a,b)};e==="inprogress"&&(e=c.shift(),d--),e&&(b==="fx"&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length<d?p.queue(this[0],a):c===b?this:this.each(function(){var b=p.queue(this,a,c);p._queueHooks(this,a),a==="fx"&&b[0]!=="inprogress"&&p.dequeue(this,a)})},dequeue:function(a){return this.each(function(){p.dequeue(this,a)})},delay:function(a,b){return a=p.fx?p.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){var d,e=1,f=p.Deferred(),g=this,h=this.length,i=function(){--e||f.resolveWith(g,[g])};typeof a!="string"&&(c=a,a=b),a=a||"fx";while(h--)d=p._data(g[h],a+"queueHooks"),d&&d.empty&&(e++,d.empty.add(i));return i(),f.promise(c)}});var L,M,N,O=/[\t\r\n]/g,P=/\r/g,Q=/^(?:button|input)$/i,R=/^(?:button|input|object|select|textarea)$/i,S=/^a(?:rea|)$/i,T=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,U=p.support.getSetAttribute;p.fn.extend({attr:function(a,b){return p.access(this,p.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{f=" "+e.className+" ";for(g=0,h=b.length;g<h;g++)f.indexOf(" "+b[g]+" ")<0&&(f+=b[g]+" ");e.className=p.trim(f)}}}return this},removeClass:function(a){var c,d,e,f,g,h,i;if(p.isFunction(a))return this.each(function(b){p(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(s);for(h=0,i=this.length;h<i;h++){e=this[h];if(e.nodeType===1&&e.className){d=(" "+e.className+" ").replace(O," ");for(f=0,g=c.length;f<g;f++)while(d.indexOf(" "+c[f]+" ")>=0)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(O," ").indexOf(b)>=0)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c<d;c++){e=h[c];if(e.selected&&(p.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!p.nodeName(e.parentNode,"optgroup"))){b=p(e).val();if(i)return b;g.push(b)}}return i&&!g.length&&h.length?p(h[f]).val():g},set:function(a,b){var c=p.makeArray(b);return p(a).find("option").each(function(){this.selected=p.inArray(p(this).val(),c)>=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,d+""),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g<d.length;g++)e=d[g],e&&(c=p.propFix[e]||e,f=T.test(e),f||p.attr(a,e,""),a.removeAttribute(U?e:c),f&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(Q.test(a.nodeName)&&a.parentNode)p.error("type property can't be changed");else if(!p.support.radioValue&&b==="radio"&&p.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}},value:{get:function(a,b){return L&&p.nodeName(a,"button")?L.get(a,b):b in a?a.value:null},set:function(a,b,c){if(L&&p.nodeName(a,"button"))return L.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,f,g,h=a.nodeType;if(!a||h===3||h===8||h===2)return;return g=h!==1||!p.isXMLDoc(a),g&&(c=p.propFix[c]||c,f=p.propHooks[c]),d!==b?f&&"set"in f&&(e=f.set(a,d,c))!==b?e:a[c]=d:f&&"get"in f&&(e=f.get(a,c))!==null?e:a[c]},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):R.test(a.nodeName)||S.test(a.nodeName)&&a.href?0:b}}}}),M={get:function(a,c){var d,e=p.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;return b===!1?p.removeAttr(a,c):(d=p.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase())),c}},U||(N={name:!0,id:!0,coords:!0},L=p.valHooks.button={get:function(a,c){var d;return d=a.getAttributeNode(c),d&&(N[c]?d.value!=="":d.specified)?d.value:b},set:function(a,b,c){var d=a.getAttributeNode(c);return d||(d=e.createAttribute(c),a.setAttributeNode(d)),d.value=b+""}},p.each(["width","height"],function(a,b){p.attrHooks[b]=p.extend(p.attrHooks[b],{set:function(a,c){if(c==="")return a.setAttribute(b,"auto"),c}})}),p.attrHooks.contenteditable={get:L.get,set:function(a,b,c){b===""&&(b="false"),L.set(a,b,c)}}),p.support.hrefNormalized||p.each(["href","src","width","height"],function(a,c){p.attrHooks[c]=p.extend(p.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),p.support.style||(p.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=b+""}}),p.support.optSelected||(p.propHooks.selected=p.extend(p.propHooks.selected,{get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}})),p.support.enctype||(p.propFix.enctype="encoding"),p.support.checkOn||p.each(["radio","checkbox"],function(){p.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),p.each(["radio","checkbox"],function(){p.valHooks[this]=p.extend(p.valHooks[this],{set:function(a,b){if(p.isArray(b))return a.checked=p.inArray(p(a).val(),b)>=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j<c.length;j++){k=W.exec(c[j])||[],l=k[1],m=(k[2]||"").split(".").sort(),r=p.event.special[l]||{},l=(f?r.delegateType:r.bindType)||l,r=p.event.special[l]||{},n=p.extend({type:l,origType:k[1],data:e,handler:d,guid:d.guid,selector:f,needsContext:f&&p.expr.match.needsContext.test(f),namespace:m.join(".")},o),q=i[l];if(!q){q=i[l]=[],q.delegateCount=0;if(!r.setup||r.setup.call(a,e,m,h)===!1)a.addEventListener?a.addEventListener(l,h,!1):a.attachEvent&&a.attachEvent("on"+l,h)}r.add&&(r.add.call(a,n),n.handler.guid||(n.handler.guid=d.guid)),f?q.splice(q.delegateCount++,0,n):q.push(n),p.event.global[l]=!0}a=null},global:{},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,q,r=p.hasData(a)&&p._data(a);if(!r||!(m=r.events))return;b=p.trim(_(b||"")).split(" ");for(f=0;f<b.length;f++){g=W.exec(b[f])||[],h=i=g[1],j=g[2];if(!h){for(h in m)p.event.remove(a,h+b[f],c,d,!0);continue}n=p.event.special[h]||{},h=(d?n.delegateType:n.bindType)||h,o=m[h]||[],k=o.length,j=j?new RegExp("(^|\\.)"+j.split(".").sort().join("\\.(?:.*\\.|)")+"(\\.|$)"):null;for(l=0;l<o.length;l++)q=o[l],(e||i===q.origType)&&(!c||c.guid===q.guid)&&(!j||j.test(q.namespace))&&(!d||d===q.selector||d==="**"&&q.selector)&&(o.splice(l--,1),q.selector&&o.delegateCount--,n.remove&&n.remove.call(a,q));o.length===0&&k!==o.length&&((!n.teardown||n.teardown.call(a,j,r.handle)===!1)&&p.removeEvent(a,h,r.handle),delete m[h])}p.isEmptyObject(m)&&(delete r.handle,p.removeData(a,"events",!0))},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,f,g){if(!f||f.nodeType!==3&&f.nodeType!==8){var h,i,j,k,l,m,n,o,q,r,s=c.type||c,t=[];if($.test(s+p.event.triggered))return;s.indexOf("!")>=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;j<q.length&&!c.isPropagationStopped();j++)k=q[j][0],c.type=q[j][1],o=(p._data(k,"events")||{})[c.type]&&p._data(k,"handle"),o&&o.apply(k,d),o=m&&k[m],o&&p.acceptData(k)&&o.apply&&o.apply(k,d)===!1&&c.preventDefault();return c.type=s,!g&&!c.isDefaultPrevented()&&(!n._default||n._default.apply(f.ownerDocument,d)===!1)&&(s!=="click"||!p.nodeName(f,"a"))&&p.acceptData(f)&&m&&f[s]&&(s!=="focus"&&s!=="blur"||c.target.offsetWidth!==0)&&!p.isWindow(f)&&(l=f[m],l&&(f[m]=null),p.event.triggered=s,f[s](),p.event.triggered=b,l&&(f[m]=l)),c.result}return},dispatch:function(c){c=p.event.fix(c||a.event);var d,e,f,g,h,i,j,l,m,n,o=(p._data(this,"events")||{})[c.type]||[],q=o.delegateCount,r=k.call(arguments),s=!c.exclusive&&!c.namespace,t=p.event.special[c.type]||{},u=[];r[0]=c,c.delegateTarget=this;if(t.preDispatch&&t.preDispatch.call(this,c)===!1)return;if(q&&(!c.button||c.type!=="click"))for(f=c.target;f!=this;f=f.parentNode||this)if(f.disabled!==!0||c.type!=="click"){h={},j=[];for(d=0;d<q;d++)l=o[d],m=l.selector,h[m]===b&&(h[m]=l.needsContext?p(m,this).index(f)>=0:p.find(m,this,null,[f]).length),h[m]&&j.push(l);j.length&&u.push({elem:f,matches:j})}o.length>q&&u.push({elem:this,matches:o.slice(q)});for(d=0;d<u.length&&!c.isPropagationStopped();d++){i=u[d],c.currentTarget=i.elem;for(e=0;e<i.matches.length&&!c.isImmediatePropagationStopped();e++){l=i.matches[e];if(s||!c.namespace&&!l.namespace||c.namespace_re&&c.namespace_re.test(l.namespace))c.data=l.data,c.handleObj=l,g=((p.event.special[l.origType]||{}).handle||l.handler).apply(i.elem,r),g!==b&&(c.result=g,g===!1&&(c.preventDefault(),c.stopPropagation()))}}return t.postDispatch&&t.postDispatch.call(this,c),c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,c){var d,f,g,h=c.button,i=c.fromElement;return a.pageX==null&&c.clientX!=null&&(d=a.target.ownerDocument||e,f=d.documentElement,g=d.body,a.pageX=c.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=c.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?c.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0),a}},fix:function(a){if(a[p.expando])return a;var b,c,d=a,f=p.event.fixHooks[a.type]||{},g=f.props?this.props.concat(f.props):this.props;a=p.Event(d);for(b=g.length;b;)c=g[--b],a[c]=d[c];return a.target||(a.target=d.srcElement||e),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,f.filter?f.filter(a,d):a},special:{load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){p.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=p.extend(new p.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?p.event.trigger(e,null,b):p.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},p.event.handle=p.event.dispatch,p.removeEvent=e.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]=="undefined"&&(a[d]=null),a.detachEvent(d,c))},p.Event=function(a,b){if(this instanceof p.Event)a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?bb:ba):this.type=a,b&&p.extend(this,b),this.timeStamp=a&&a.timeStamp||p.now(),this[p.expando]=!0;else return new p.Event(a,b)},p.Event.prototype={preventDefault:function(){this.isDefaultPrevented=bb;var a=this.originalEvent;if(!a)return;a.preventDefault?a.preventDefault():a.returnValue=!1},stopPropagation:function(){this.isPropagationStopped=bb;var a=this.originalEvent;if(!a)return;a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=bb,this.stopPropagation()},isDefaultPrevented:ba,isPropagationStopped:ba,isImmediatePropagationStopped:ba},p.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){p.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj,g=f.selector;if(!e||e!==d&&!p.contains(d,e))a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b;return c}}}),p.support.submitBubbles||(p.event.special.submit={setup:function(){if(p.nodeName(this,"form"))return!1;p.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=p.nodeName(c,"input")||p.nodeName(c,"button")?c.form:b;d&&!p._data(d,"_submit_attached")&&(p.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),p._data(d,"_submit_attached",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&p.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(p.nodeName(this,"form"))return!1;p.event.remove(this,"._submit")}}),p.support.changeBubbles||(p.event.special.change={setup:function(){if(V.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")p.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),p.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),p.event.simulate("change",this,a,!0)});return!1}p.event.add(this,"beforeactivate._change",function(a){var b=a.target;V.test(b.nodeName)&&!p._data(b,"_change_attached")&&(p.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&p.event.simulate("change",this.parentNode,a,!0)}),p._data(b,"_change_attached",!0))})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){return p.event.remove(this,"._change"),!V.test(this.nodeName)}}),p.support.focusinBubbles||p.each({focus:"focusin",blur:"focusout"},function(a,b){var c=0,d=function(a){p.event.simulate(b,a.target,p.event.fix(a),!0)};p.event.special[b]={setup:function(){c++===0&&e.addEventListener(a,d,!0)},teardown:function(){--c===0&&e.removeEventListener(a,d,!0)}}}),p.fn.extend({on:function(a,c,d,e,f){var g,h;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(h in a)this.on(h,c,d,a[h],f);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=ba;else if(!e)return this;return f===1&&(g=e,e=function(a){return p().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=p.guid++)),this.each(function(){p.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){var e,f;if(a&&a.preventDefault&&a.handleObj)return e=a.handleObj,p(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler),this;if(typeof a=="object"){for(f in a)this.off(f,c,a[f]);return this}if(c===!1||typeof c=="function")d=c,c=b;return d===!1&&(d=ba),this.each(function(){p.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){return p(this.context).on(a,this.selector,b,c),this},die:function(a,b){return p(this.context).off(a,this.selector||"**",b),this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length===1?this.off(a,"**"):this.off(b,a||"**",c)},trigger:function(a,b){return this.each(function(){p.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return p.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||p.guid++,d=0,e=function(c){var e=(p._data(this,"lastToggle"+a.guid)||0)%d;return p._data(this,"lastToggle"+a.guid,e+1),c.preventDefault(),b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),p.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){p.fn[b]=function(a,c){return c==null&&(c=a,a=null),arguments.length>0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function bc(a,b,c,d){c=c||[],b=b||r;var e,f,i,j,k=b.nodeType;if(!a||typeof a!="string")return c;if(k!==1&&k!==9)return[];i=g(b);if(!i&&!d)if(e=P.exec(a))if(j=e[1]){if(k===9){f=b.getElementById(j);if(!f||!f.parentNode)return c;if(f.id===j)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(j))&&h(b,f)&&f.id===j)return c.push(f),c}else{if(e[2])return w.apply(c,x.call(b.getElementsByTagName(a),0)),c;if((j=e[3])&&_&&b.getElementsByClassName)return w.apply(c,x.call(b.getElementsByClassName(j),0)),c}return bp(a.replace(L,"$1"),b,c,d,i)}function bd(a){return function(b){var c=b.nodeName.toLowerCase();return c==="input"&&b.type===a}}function be(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}}function bf(a){return z(function(b){return b=+b,z(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function bg(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}function bh(a,b){var c,d,f,g,h,i,j,k=C[o][a];if(k)return b?0:k.slice(0);h=a,i=[],j=e.preFilter;while(h){if(!c||(d=M.exec(h)))d&&(h=h.slice(d[0].length)),i.push(f=[]);c=!1;if(d=N.exec(h))f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=d[0].replace(L," ");for(g in e.filter)(d=W[g].exec(h))&&(!j[g]||(d=j[g](d,r,!0)))&&(f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=g,c.matches=d);if(!c)break}return b?h.length:h?bc.error(a):C(a,i).slice(0)}function bi(a,b,d){var e=b.dir,f=d&&b.dir==="parentNode",g=u++;return b.first?function(b,c,d){while(b=b[e])if(f||b.nodeType===1)return a(b,c,d)}:function(b,d,h){if(!h){var i,j=t+" "+g+" ",k=j+c;while(b=b[e])if(f||b.nodeType===1){if((i=b[o])===k)return b.sizset;if(typeof i=="string"&&i.indexOf(j)===0){if(b.sizset)return b}else{b[o]=k;if(a(b,d,h))return b.sizset=!0,b;b.sizset=!1}}}else while(b=b[e])if(f||b.nodeType===1)if(a(b,d,h))return b}}function bj(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function bk(a,b,c,d,e){var f,g=[],h=0,i=a.length,j=b!=null;for(;h<i;h++)if(f=a[h])if(!c||c(f,d,e))g.push(f),j&&b.push(h);return g}function bl(a,b,c,d,e,f){return d&&!d[o]&&(d=bl(d)),e&&!e[o]&&(e=bl(e,f)),z(function(f,g,h,i){if(f&&e)return;var j,k,l,m=[],n=[],o=g.length,p=f||bo(b||"*",h.nodeType?[h]:h,[],f),q=a&&(f||!b)?bk(p,m,a,h,i):p,r=c?e||(f?a:o||d)?[]:g:q;c&&c(q,r,h,i);if(d){l=bk(r,n),d(l,[],h,i),j=l.length;while(j--)if(k=l[j])r[n[j]]=!(q[n[j]]=k)}if(f){j=a&&r.length;while(j--)if(k=r[j])f[m[j]]=!(g[m[j]]=k)}else r=bk(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):w.apply(g,r)})}function bm(a){var b,c,d,f=a.length,g=e.relative[a[0].type],h=g||e.relative[" "],i=g?1:0,j=bi(function(a){return a===b},h,!0),k=bi(function(a){return y.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==l)||((b=c).nodeType?j(a,c,d):k(a,c,d))}];for(;i<f;i++)if(c=e.relative[a[i].type])m=[bi(bj(m),c)];else{c=e.filter[a[i].type].apply(null,a[i].matches);if(c[o]){d=++i;for(;d<f;d++)if(e.relative[a[d].type])break;return bl(i>1&&bj(m),i>1&&a.slice(0,i-1).join("").replace(L,"$1"),c,i<d&&bm(a.slice(i,d)),d<f&&bm(a=a.slice(d)),d<f&&a.join(""))}m.push(c)}return bj(m)}function bn(a,b){var d=b.length>0,f=a.length>0,g=function(h,i,j,k,m){var n,o,p,q=[],s=0,u="0",x=h&&[],y=m!=null,z=l,A=h||f&&e.find.TAG("*",m&&i.parentNode||i),B=t+=z==null?1:Math.E;y&&(l=i!==r&&i,c=g.el);for(;(n=A[u])!=null;u++){if(f&&n){for(o=0;p=a[o];o++)if(p(n,i,j)){k.push(n);break}y&&(t=B,c=++g.el)}d&&((n=!p&&n)&&s--,h&&x.push(n))}s+=u;if(d&&u!==s){for(o=0;p=b[o];o++)p(x,q,i,j);if(h){if(s>0)while(u--)!x[u]&&!q[u]&&(q[u]=v.call(k));q=bk(q)}w.apply(k,q),y&&!h&&q.length>0&&s+b.length>1&&bc.uniqueSort(k)}return y&&(t=B,l=z),x};return g.el=0,d?z(g):g}function bo(a,b,c,d){var e=0,f=b.length;for(;e<f;e++)bc(a,b[e],c,d);return c}function bp(a,b,c,d,f){var g,h,j,k,l,m=bh(a),n=m.length;if(!d&&m.length===1){h=m[0]=m[0].slice(0);if(h.length>2&&(j=h[0]).type==="ID"&&b.nodeType===9&&!f&&e.relative[h[1].type]){b=e.find.ID(j.matches[0].replace(V,""),b,f)[0];if(!b)return c;a=a.slice(h.shift().length)}for(g=W.POS.test(a)?-1:h.length-1;g>=0;g--){j=h[g];if(e.relative[k=j.type])break;if(l=e.find[k])if(d=l(j.matches[0].replace(V,""),R.test(h[0].type)&&b.parentNode||b,f)){h.splice(g,1),a=d.length&&h.join("");if(!a)return w.apply(c,x.call(d,0)),c;break}}}return i(a,m)(d,b,f,c,R.test(a)),c}function bq(){}var c,d,e,f,g,h,i,j,k,l,m=!0,n="undefined",o=("sizcache"+Math.random()).replace(".",""),q=String,r=a.document,s=r.documentElement,t=0,u=0,v=[].pop,w=[].push,x=[].slice,y=[].indexOf||function(a){var b=0,c=this.length;for(;b<c;b++)if(this[b]===a)return b;return-1},z=function(a,b){return a[o]=b==null||b,a},A=function(){var a={},b=[];return z(function(c,d){return b.push(c)>e.cacheLength&&delete a[b.shift()],a[c]=d},a)},B=A(),C=A(),D=A(),E="[\\x20\\t\\r\\n\\f]",F="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",G=F.replace("w","w#"),H="([*^$|!~]?=)",I="\\["+E+"*("+F+")"+E+"*(?:"+H+E+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+G+")|)|)"+E+"*\\]",J=":("+F+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+I+")|[^:]|\\\\.)*|.*))\\)|)",K=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+E+"*((?:-\\d)?\\d*)"+E+"*\\)|)(?=[^-]|$)",L=new RegExp("^"+E+"+|((?:^|[^\\\\])(?:\\\\.)*)"+E+"+$","g"),M=new RegExp("^"+E+"*,"+E+"*"),N=new RegExp("^"+E+"*([\\x20\\t\\r\\n\\f>+~])"+E+"*"),O=new RegExp(J),P=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,Q=/^:not/,R=/[\x20\t\r\n\f]*[+~]/,S=/:not\($/,T=/h\d/i,U=/input|select|textarea|button/i,V=/\\(?!\\)/g,W={ID:new RegExp("^#("+F+")"),CLASS:new RegExp("^\\.("+F+")"),NAME:new RegExp("^\\[name=['\"]?("+F+")['\"]?\\]"),TAG:new RegExp("^("+F.replace("w","w*")+")"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+J),POS:new RegExp(K,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+E+"*(even|odd|(([+-]|)(\\d*)n|)"+E+"*(?:([+-]|)"+E+"*(\\d+)|))"+E+"*\\)|)","i"),needsContext:new RegExp("^"+E+"*[>+~]|"+K,"i")},X=function(a){var b=r.createElement("div");try{return a(b)}catch(c){return!1}finally{b=null}},Y=X(function(a){return a.appendChild(r.createComment("")),!a.getElementsByTagName("*").length}),Z=X(function(a){return a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!==n&&a.firstChild.getAttribute("href")==="#"}),$=X(function(a){a.innerHTML="<select></select>";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),_=X(function(a){return a.innerHTML="<div class='hidden e'></div><div class='hidden'></div>",!a.getElementsByClassName||!a.getElementsByClassName("e").length?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length===2)}),ba=X(function(a){a.id=o+0,a.innerHTML="<a name='"+o+"'></a><div name='"+o+"'></div>",s.insertBefore(a,s.firstChild);var b=r.getElementsByName&&r.getElementsByName(o).length===2+r.getElementsByName(o+0).length;return d=!r.getElementById(o),s.removeChild(a),b});try{x.call(s.childNodes,0)[0].nodeType}catch(bb){x=function(a){var b,c=[];for(;b=this[a];a++)c.push(b);return c}}bc.matches=function(a,b){return bc(a,null,null,b)},bc.matchesSelector=function(a,b){return bc(b,null,null,[a]).length>0},f=bc.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(e===1||e===9||e===11){if(typeof a.textContent=="string")return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=f(a)}else if(e===3||e===4)return a.nodeValue}else for(;b=a[d];d++)c+=f(b);return c},g=bc.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?b.nodeName!=="HTML":!1},h=bc.contains=s.contains?function(a,b){var c=a.nodeType===9?a.documentElement:a,d=b&&b.parentNode;return a===d||!!(d&&d.nodeType===1&&c.contains&&c.contains(d))}:s.compareDocumentPosition?function(a,b){return b&&!!(a.compareDocumentPosition(b)&16)}:function(a,b){while(b=b.parentNode)if(b===a)return!0;return!1},bc.attr=function(a,b){var c,d=g(a);return d||(b=b.toLowerCase()),(c=e.attrHandle[b])?c(a):d||$?a.getAttribute(b):(c=a.getAttributeNode(b),c?typeof a[b]=="boolean"?a[b]?b:null:c.specified?c.value:null:null)},e=bc.selectors={cacheLength:50,createPseudo:z,match:W,attrHandle:Z?{}:{href:function(a){return a.getAttribute("href",2)},type:function(a){return a.getAttribute("type")}},find:{ID:d?function(a,b,c){if(typeof b.getElementById!==n&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==n&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==n&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:Y?function(a,b){if(typeof b.getElementsByTagName!==n)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c},NAME:ba&&function(a,b){if(typeof b.getElementsByName!==n)return b.getElementsByName(name)},CLASS:_&&function(a,b,c){if(typeof b.getElementsByClassName!==n&&!c)return b.getElementsByClassName(a)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(V,""),a[3]=(a[4]||a[5]||"").replace(V,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||bc.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&bc.error(a[0]),a},PSEUDO:function(a){var b,c;if(W.CHILD.test(a[0]))return null;if(a[3])a[2]=a[3];else if(b=a[4])O.test(b)&&(c=bh(b,!0))&&(c=b.indexOf(")",b.length-c)-b.length)&&(b=b.slice(0,c),a[0]=a[0].slice(0,c)),a[2]=b;return a.slice(0,3)}},filter:{ID:d?function(a){return a=a.replace(V,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(V,""),function(b){var c=typeof b.getAttributeNode!==n&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(V,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=B[o][a];return b||(b=B(a,new RegExp("(^|"+E+")"+a+"("+E+"|$)"))),function(a){return b.test(a.className||typeof a.getAttribute!==n&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return function(d,e){var f=bc.attr(d,a);return f==null?b==="!=":b?(f+="",b==="="?f===c:b==="!="?f!==c:b==="^="?c&&f.indexOf(c)===0:b==="*="?c&&f.indexOf(c)>-1:b==="$="?c&&f.substr(f.length-c.length)===c:b==="~="?(" "+f+" ").indexOf(c)>-1:b==="|="?f===c||f.substr(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d){return a==="nth"?function(a){var b,e,f=a.parentNode;if(c===1&&d===0)return!0;if(f){e=0;for(b=f.firstChild;b;b=b.nextSibling)if(b.nodeType===1){e++;if(a===b)break}}return e-=d,e===c||e%c===0&&e/c>=0}:function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b){var c,d=e.pseudos[a]||e.setFilters[a.toLowerCase()]||bc.error("unsupported pseudo: "+a);return d[o]?d(b):d.length>1?(c=[a,a,"",b],e.setFilters.hasOwnProperty(a.toLowerCase())?z(function(a,c){var e,f=d(a,b),g=f.length;while(g--)e=y.call(a,f[g]),a[e]=!(c[e]=f[g])}):function(a){return d(a,0,c)}):d}},pseudos:{not:z(function(a){var b=[],c=[],d=i(a.replace(L,"$1"));return d[o]?z(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)if(f=g[h])a[h]=!(b[h]=f)}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:z(function(a){return function(b){return bc(a,b).length>0}}),contains:z(function(a){return function(b){return(b.textContent||b.innerText||f(b)).indexOf(a)>-1}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!e.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},header:function(a){return T.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:bd("radio"),checkbox:bd("checkbox"),file:bd("file"),password:bd("password"),image:bd("image"),submit:be("submit"),reset:be("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return U.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement},first:bf(function(a,b,c){return[0]}),last:bf(function(a,b,c){return[b-1]}),eq:bf(function(a,b,c){return[c<0?c+b:c]}),even:bf(function(a,b,c){for(var d=0;d<b;d+=2)a.push(d);return a}),odd:bf(function(a,b,c){for(var d=1;d<b;d+=2)a.push(d);return a}),lt:bf(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:bf(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},j=s.compareDocumentPosition?function(a,b){return a===b?(k=!0,0):(!a.compareDocumentPosition||!b.compareDocumentPosition?a.compareDocumentPosition:a.compareDocumentPosition(b)&4)?-1:1}:function(a,b){if(a===b)return k=!0,0;if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,h=b.parentNode,i=g;if(g===h)return bg(a,b);if(!g)return-1;if(!h)return 1;while(i)e.unshift(i),i=i.parentNode;i=h;while(i)f.unshift(i),i=i.parentNode;c=e.length,d=f.length;for(var j=0;j<c&&j<d;j++)if(e[j]!==f[j])return bg(e[j],f[j]);return j===c?bg(a,f[j],-1):bg(e[j],b,1)},[0,0].sort(j),m=!k,bc.uniqueSort=function(a){var b,c=1;k=m,a.sort(j);if(k)for(;b=a[c];c++)b===a[c-1]&&a.splice(c--,1);return a},bc.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},i=bc.compile=function(a,b){var c,d=[],e=[],f=D[o][a];if(!f){b||(b=bh(a)),c=b.length;while(c--)f=bm(b[c]),f[o]?d.push(f):e.push(f);f=D(a,bn(e,d))}return f},r.querySelectorAll&&function(){var a,b=bp,c=/'|\\/g,d=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,e=[":focus"],f=[":active",":focus"],h=s.matchesSelector||s.mozMatchesSelector||s.webkitMatchesSelector||s.oMatchesSelector||s.msMatchesSelector;X(function(a){a.innerHTML="<select><option selected=''></option></select>",a.querySelectorAll("[selected]").length||e.push("\\["+E+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),X(function(a){a.innerHTML="<p test=''></p>",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+E+"*(?:\"\"|'')"),a.innerHTML="<input type='hidden'/>",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=new RegExp(e.join("|")),bp=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a))){var i,j,k=!0,l=o,m=d,n=d.nodeType===9&&a;if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){i=bh(a),(k=d.getAttribute("id"))?l=k.replace(c,"\\$&"):d.setAttribute("id",l),l="[id='"+l+"'] ",j=i.length;while(j--)i[j]=l+i[j].join("");m=R.test(a)&&d.parentNode||d,n=i.join(",")}if(n)try{return w.apply(f,x.call(m.querySelectorAll(n),0)),f}catch(p){}finally{k||d.removeAttribute("id")}}return b(a,d,f,g,h)},h&&(X(function(b){a=h.call(b,"div");try{h.call(b,"[test!='']:sizzle"),f.push("!=",J)}catch(c){}}),f=new RegExp(f.join("|")),bc.matchesSelector=function(b,c){c=c.replace(d,"='$1']");if(!g(b)&&!f.test(c)&&(!e||!e.test(c)))try{var i=h.call(b,c);if(i||a||b.document&&b.document.nodeType!==11)return i}catch(j){}return bc(c,null,null,[b]).length>0})}(),e.pseudos.nth=e.pseudos.eq,e.filters=bq.prototype=e.pseudos,e.setFilters=new bq,bc.attr=p.attr,p.find=bc,p.expr=bc.selectors,p.expr[":"]=p.expr.pseudos,p.unique=bc.uniqueSort,p.text=bc.getText,p.isXMLDoc=bc.isXML,p.contains=bc.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b<c;b++)if(p.contains(h[b],this))return!0});g=this.pushStack("","find",a);for(b=0,c=this.length;b<c;b++){d=g.length,p.find(a,this[b],g);if(b>0)for(e=d;e<g.length;e++)for(f=0;f<d;f++)if(g[f]===g[e]){g.splice(e--,1);break}}return g},has:function(a){var b,c=p(a,this),d=c.length;return this.filter(function(){for(b=0;b<d;b++)if(p.contains(this,c[b]))return!0})},not:function(a){return this.pushStack(bj(this,a,!1),"not",a)},filter:function(a){return this.pushStack(bj(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?bf.test(a)?p(a,this.context).index(this[0])>=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d<e;d++){c=this[d];while(c&&c.ownerDocument&&c!==b&&c.nodeType!==11){if(g?g.index(c)>-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/<tbody/i,br=/<|&#?\w+;/,bs=/<(?:script|style|link)/i,bt=/<(?:script|object|embed|option|style)/i,bu=new RegExp("<(?:"+bl+")[\\s/>]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,bz={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X<div>","</div>"]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(f){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){return bh(this[0])?this.length?this.pushStack(p(p.isFunction(a)?a():a),"replaceWith",a):this:p.isFunction(a)?this.each(function(b){var c=p(this),d=c.html();c.replaceWith(a.call(this,b,d))}):(typeof a!="string"&&(a=p(a).detach()),this.each(function(){var b=this.nextSibling,c=this.parentNode;p(this).remove(),b?p(b).before(a):p(c).append(a)}))},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){a=[].concat.apply([],a);var e,f,g,h,i=0,j=a[0],k=[],l=this.length;if(!p.support.checkClone&&l>1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i<l;i++)d.call(c&&p.nodeName(this[i],"table")?bC(this[i],"tbody"):this[i],i===h?g:p.clone(g,!0,!0))}g=f=null,k.length&&p.each(k,function(a,b){b.src?p.ajax?p.ajax({url:b.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):p.error("no ajax"):p.globalEval((b.text||b.textContent||b.innerHTML||"").replace(by,"")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),p.buildFragment=function(a,c,d){var f,g,h,i=a[0];return c=c||e,c=!c.nodeType&&c[0]||c,c=c.ownerDocument||c,a.length===1&&typeof i=="string"&&i.length<512&&c===e&&i.charAt(0)==="<"&&!bt.test(i)&&(p.support.checkClone||!bw.test(i))&&(p.support.html5Clone||!bu.test(i))&&(g=!0,f=p.fragments[i],h=f!==b),f||(f=c.createDocumentFragment(),p.clean(a,c,f,d),g&&(p.fragments[i]=h&&f)),{fragment:f,cacheable:g}},p.fragments={},p.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){p.fn[a]=function(c){var d,e=0,f=[],g=p(c),h=g.length,i=this.length===1&&this[0].parentNode;if((i==null||i&&i.nodeType===11&&i.childNodes.length===1)&&h===1)return g[b](this[0]),this;for(;e<h;e++)d=(e>0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=b===e&&bA,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(f=0;(h=a[f])!=null;f++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{s=s||bk(b),l=b.createElement("div"),s.appendChild(l),h=h.replace(bo,"<$1></$2>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]==="<table>"&&!m?l.childNodes:[];for(g=n.length-1;g>=0;--g)p.nodeName(n[g],"tbody")&&!n[g].childNodes.length&&n[g].parentNode.removeChild(n[g])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l.parentNode.removeChild(l)}h.nodeType?t.push(h):p.merge(t,h)}l&&(h=l=s=null);if(!p.support.appendChecked)for(f=0;(h=t[f])!=null;f++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(f=0;(h=t[f])!=null;f++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[f+1,0].concat(r)),f+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.chrome?b.webkit=!0:b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^(none|table(?!-c[ea]).+)/,bO=/^margin/,bP=new RegExp("^("+q+")(.*)$","i"),bQ=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bR=new RegExp("^([-+])=("+q+")","i"),bS={},bT={position:"absolute",visibility:"hidden",display:"block"},bU={letterSpacing:0,fontWeight:400},bV=["Top","Right","Bottom","Left"],bW=["Webkit","O","Moz","ms"],bX=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return b$(this,!0)},hide:function(){return b$(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bX.apply(this,arguments):this.each(function(){(c?a:bZ(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bY(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bR.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bY(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bU&&(f=bU[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(b,c){var d,e,f,g,h=a.getComputedStyle(b,null),i=b.style;return h&&(d=h[c],d===""&&!p.contains(b.ownerDocument,b)&&(d=p.style(b,c)),bQ.test(d)&&bO.test(c)&&(e=i.width,f=i.minWidth,g=i.maxWidth,i.minWidth=i.maxWidth=i.width=d,d=h.width,i.width=e,i.minWidth=f,i.maxWidth=g)),d}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bQ.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth===0&&bN.test(bH(a,"display"))?p.swap(a,bT,function(){return cb(a,b,d)}):cb(a,b,d)},set:function(a,c,d){return b_(a,c,d?ca(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bQ.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bV[d]+b]=e[d]||e[d-2]||e[0];return f}},bO.test(a)||(p.cssHooks[a+b].set=b_)});var cd=/%20/g,ce=/\[\]$/,cf=/\r?\n/g,cg=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ch=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ch.test(this.nodeName)||cg.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(cf,"\r\n")}}):{name:b.name,value:c.replace(cf,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ci(d,a[d],c,f);return e.join("&").replace(cd,"+")};var cj,ck,cl=/#.*$/,cm=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,co=/^(?:GET|HEAD)$/,cp=/^\/\//,cq=/\?/,cr=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,cs=/([?&])_=[^&]*/,ct=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,cu=p.fn.load,cv={},cw={},cx=["*/"]+["*"];try{ck=f.href}catch(cy){ck=e.createElement("a"),ck.href="",ck=ck.href}cj=ct.exec(ck.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&cu)return cu.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):c&&typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("<div>").append(a.replace(cr,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cB(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cB(a,b),a},ajaxSettings:{url:ck,isLocal:cn.test(cj[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cx},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cz(cv),ajaxTransport:cz(cw),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cC(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cD(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=(c||y)+"",k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cm.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(cl,"").replace(cp,cj[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=ct.exec(l.url.toLowerCase())||!1,l.crossDomain=i&&i.join(":")+(i[3]?"":i[1]==="http:"?80:443)!==cj.join(":")+(cj[3]?"":cj[1]==="http:"?80:443)),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cA(cv,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!co.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cq.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cs,"$1_="+z);l.url=A+(A===l.url?(cq.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cx+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cA(cw,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cE=[],cF=/\?/,cG=/(=)\?(?=&|$)|\?\?/,cH=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cE.pop()||p.expando+"_"+cH++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cG.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cG.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cG,"$1"+f):m?c.data=i.replace(cG,"$1"+f):k&&(c.url+=(cF.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cE.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cI,cJ=a.ActiveXObject?function(){for(var a in cI)cI[a](0,1)}:!1,cK=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cL()||cM()}:cL,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cJ&&delete cI[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cK,cJ&&(cI||(cI={},p(a).unload(cJ)),cI[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cN,cO,cP=/^(?:toggle|show|hide)$/,cQ=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cR=/queueHooks$/,cS=[cY],cT={"*":[function(a,b){var c,d,e=this.createTween(a,b),f=cQ.exec(b),g=e.cur(),h=+g||0,i=1,j=20;if(f){c=+f[2],d=f[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&h){h=p.css(e.elem,a,!0)||c||1;do i=i||".5",h=h/i,p.style(e.elem,a,h+d);while(i!==(i=e.cur()/g)&&i!==1&&--j)}e.unit=d,e.start=h,e.end=f[1]?h+(f[1]+1)*c:c}return e}]};p.Animation=p.extend(cW,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d<e;d++)c=a[d],cT[c]=cT[c]||[],cT[c].unshift(b)},prefilter:function(a,b){b?cS.unshift(a):cS.push(a)}}),p.Tween=cZ,cZ.prototype={constructor:cZ,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(p.cssNumber[c]?"":"px")},cur:function(){var a=cZ.propHooks[this.prop];return a&&a.get?a.get(this):cZ.propHooks._default.get(this)},run:function(a){var b,c=cZ.propHooks[this.prop];return this.options.duration?this.pos=b=p.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):cZ.propHooks._default.set(this),this}},cZ.prototype.init.prototype=cZ.prototype,cZ.propHooks={_default:{get:function(a){var b;return a.elem[a.prop]==null||!!a.elem.style&&a.elem.style[a.prop]!=null?(b=p.css(a.elem,a.prop,!1,""),!b||b==="auto"?0:b):a.elem[a.prop]},set:function(a){p.fx.step[a.prop]?p.fx.step[a.prop](a):a.elem.style&&(a.elem.style[p.cssProps[a.prop]]!=null||p.cssHooks[a.prop])?p.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},cZ.propHooks.scrollTop=cZ.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},p.each(["toggle","show","hide"],function(a,b){var c=p.fn[b];p.fn[b]=function(d,e,f){return d==null||typeof d=="boolean"||!a&&p.isFunction(d)&&p.isFunction(e)?c.apply(this,arguments):this.animate(c$(b,!0),d,e,f)}}),p.fn.extend({fadeTo:function(a,b,c,d){return this.filter(bZ).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=p.isEmptyObject(a),f=p.speed(b,c,d),g=function(){var b=cW(this,p.extend({},a),f);e&&b.stop(!0)};return e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,c,d){var e=function(a){var b=a.stop;delete a.stop,b(d)};return typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,c=a!=null&&a+"queueHooks",f=p.timers,g=p._data(this);if(c)g[c]&&g[c].stop&&e(g[c]);else for(c in g)g[c]&&g[c].stop&&cR.test(c)&&e(g[c]);for(c=f.length;c--;)f[c].elem===this&&(a==null||f[c].queue===a)&&(f[c].anim.stop(d),b=!1,f.splice(c,1));(b||!d)&&p.dequeue(this,a)})}}),p.each({slideDown:c$("show"),slideUp:c$("hide"),slideToggle:c$("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){p.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),p.speed=function(a,b,c){var d=a&&typeof a=="object"?p.extend({},a):{complete:c||!c&&b||p.isFunction(a)&&a,duration:a,easing:c&&b||b&&!p.isFunction(b)&&b};d.duration=p.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in p.fx.speeds?p.fx.speeds[d.duration]:p.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";return d.old=d.complete,d.complete=function(){p.isFunction(d.old)&&d.old.call(this),d.queue&&p.dequeue(this,d.queue)},d},p.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},p.timers=[],p.fx=cZ.prototype.init,p.fx.tick=function(){var a,b=p.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||p.fx.stop()},p.fx.timer=function(a){a()&&p.timers.push(a)&&!cO&&(cO=setInterval(p.fx.tick,p.fx.interval))},p.fx.interval=13,p.fx.stop=function(){clearInterval(cO),cO=null},p.fx.speeds={slow:600,fast:200,_default:400},p.fx.step={},p.expr&&p.expr.filters&&(p.expr.filters.animated=function(a){return p.grep(p.timers,function(b){return a===b.elem}).length});var c_=/^(?:body|html)$/i;p.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){p.offset.setOffset(this,a,b)});var c,d,e,f,g,h,i,j={top:0,left:0},k=this[0],l=k&&k.ownerDocument;if(!l)return;return(d=l.body)===k?p.offset.bodyOffset(k):(c=l.documentElement,p.contains(c,k)?(typeof k.getBoundingClientRect!="undefined"&&(j=k.getBoundingClientRect()),e=da(l),f=c.clientTop||d.clientTop||0,g=c.clientLeft||d.clientLeft||0,h=e.pageYOffset||c.scrollTop,i=e.pageXOffset||c.scrollLeft,{top:j.top+h-f,left:j.left+i-g}):j)},p.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;return p.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(p.css(a,"marginTop"))||0,c+=parseFloat(p.css(a,"marginLeft"))||0),{top:b,left:c}},setOffset:function(a,b,c){var d=p.css(a,"position");d==="static"&&(a.style.position="relative");var e=p(a),f=e.offset(),g=p.css(a,"top"),h=p.css(a,"left"),i=(d==="absolute"||d==="fixed")&&p.inArray("auto",[g,h])>-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c_.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c_.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=da(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g,null)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window);
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery.dataTables.js b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery.dataTables.js
new file mode 100755
index 0000000000000000000000000000000000000000..27f1c2787c558b119e123f8663a94b815577a404
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery.dataTables.js
@@ -0,0 +1,12099 @@
+/**
+ * @summary     DataTables
+ * @description Paginate, search and sort HTML tables
+ * @version     1.9.4
+ * @file        jquery.dataTables.js
+ * @author      Allan Jardine (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ *
+ * @copyright Copyright 2008-2012 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, available at:
+ *   http://datatables.net/license_gpl2
+ *   http://datatables.net/license_bsd
+ * 
+ * This source file 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 license files for details.
+ * 
+ * For details please refer to: http://www.datatables.net
+ */
+
+/*jslint evil: true, undef: true, browser: true */
+/*globals $, jQuery,define,_fnExternApiFunc,_fnInitialise,_fnInitComplete,_fnLanguageCompat,_fnAddColumn,_fnColumnOptions,_fnAddData,_fnCreateTr,_fnGatherData,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnServerParams,_fnAddOptionsHtml,_fnFeatureHtmlTable,_fnScrollDraw,_fnAdjustColumnSizing,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnBuildSearchArray,_fnBuildSearchRow,_fnFilterCreateSearch,_fnDataToSearch,_fnSort,_fnSortAttachListener,_fnSortingClasses,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnFeatureHtmlLength,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnNodeToDataIndex,_fnVisbleColumns,_fnCalculateEnd,_fnConvertToWidth,_fnCalculateColumnWidths,_fnScrollingWidthAdjust,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnDetectType,_fnSettingsFromNode,_fnGetDataMaster,_fnGetTrNodes,_fnGetTdNodes,_fnEscapeRegex,_fnDeleteIndex,_fnReOrderIndex,_fnColumnOrdering,_fnLog,_fnClearTable,_fnSaveState,_fnLoadState,_fnCreateCookie,_fnReadCookie,_fnDetectHeader,_fnGetUniqueThs,_fnScrollBarWidth,_fnApplyToChildren,_fnMap,_fnGetRowData,_fnGetCellData,_fnSetCellData,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnApplyColumnDefs,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnJsonString,_fnRender,_fnNodeToColumnIndex,_fnInfoMacros,_fnBrowserDetect,_fnGetColumns*/
+
+(/** @lends <global> */function( window, document, undefined ) {
+
+(function( factory ) {
+	"use strict";
+
+	// Define as an AMD module if possible
+	if ( typeof define === 'function' && define.amd )
+	{
+		define( ['jquery'], factory );
+	}
+	/* Define using browser globals otherwise
+	 * Prevent multiple instantiations if the script is loaded twice
+	 */
+	else if ( jQuery && !jQuery.fn.dataTable )
+	{
+		factory( jQuery );
+	}
+}
+(/** @lends <global> */function( $ ) {
+	"use strict";
+	/** 
+	 * DataTables is a plug-in for the jQuery Javascript library. It is a 
+	 * highly flexible tool, based upon the foundations of progressive 
+	 * enhancement, which will add advanced interaction controls to any 
+	 * HTML table. For a full list of features please refer to
+	 * <a href="http://datatables.net">DataTables.net</a>.
+	 *
+	 * Note that the <i>DataTable</i> object is not a global variable but is
+	 * aliased to <i>jQuery.fn.DataTable</i> and <i>jQuery.fn.dataTable</i> through which 
+	 * it may be  accessed.
+	 *
+	 *  @class
+	 *  @param {object} [oInit={}] Configuration object for DataTables. Options
+	 *    are defined by {@link DataTable.defaults}
+	 *  @requires jQuery 1.3+
+	 * 
+	 *  @example
+	 *    // Basic initialisation
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable();
+	 *    } );
+	 *  
+	 *  @example
+	 *    // Initialisation with configuration options - in this case, disable
+	 *    // pagination and sorting.
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable( {
+	 *        "bPaginate": false,
+	 *        "bSort": false 
+	 *      } );
+	 *    } );
+	 */
+	var DataTable = function( oInit )
+	{
+		
+		
+		/**
+		 * Add a column to the list used for the table with default values
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nTh The th element for this column
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddColumn( oSettings, nTh )
+		{
+			var oDefaults = DataTable.defaults.columns;
+			var iCol = oSettings.aoColumns.length;
+			var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, {
+				"sSortingClass": oSettings.oClasses.sSortable,
+				"sSortingClassJUI": oSettings.oClasses.sSortJUI,
+				"nTh": nTh ? nTh : document.createElement('th'),
+				"sTitle":    oDefaults.sTitle    ? oDefaults.sTitle    : nTh ? nTh.innerHTML : '',
+				"aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol],
+				"mData": oDefaults.mData ? oDefaults.oDefaults : iCol
+			} );
+			oSettings.aoColumns.push( oCol );
+			
+			/* Add a column specific filter */
+			if ( oSettings.aoPreSearchCols[ iCol ] === undefined || oSettings.aoPreSearchCols[ iCol ] === null )
+			{
+				oSettings.aoPreSearchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch );
+			}
+			else
+			{
+				var oPre = oSettings.aoPreSearchCols[ iCol ];
+				
+				/* Don't require that the user must specify bRegex, bSmart or bCaseInsensitive */
+				if ( oPre.bRegex === undefined )
+				{
+					oPre.bRegex = true;
+				}
+				
+				if ( oPre.bSmart === undefined )
+				{
+					oPre.bSmart = true;
+				}
+				
+				if ( oPre.bCaseInsensitive === undefined )
+				{
+					oPre.bCaseInsensitive = true;
+				}
+			}
+			
+			/* Use the column options function to initialise classes etc */
+			_fnColumnOptions( oSettings, iCol, null );
+		}
+		
+		
+		/**
+		 * Apply options for a column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column index to consider
+		 *  @param {object} oOptions object with sType, bVisible and bSearchable etc
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnOptions( oSettings, iCol, oOptions )
+		{
+			var oCol = oSettings.aoColumns[ iCol ];
+			
+			/* User specified column options */
+			if ( oOptions !== undefined && oOptions !== null )
+			{
+				/* Backwards compatibility for mDataProp */
+				if ( oOptions.mDataProp && !oOptions.mData )
+				{
+					oOptions.mData = oOptions.mDataProp;
+				}
+		
+				if ( oOptions.sType !== undefined )
+				{
+					oCol.sType = oOptions.sType;
+					oCol._bAutoType = false;
+				}
+				
+				$.extend( oCol, oOptions );
+				_fnMap( oCol, oOptions, "sWidth", "sWidthOrig" );
+		
+				/* iDataSort to be applied (backwards compatibility), but aDataSort will take
+				 * priority if defined
+				 */
+				if ( oOptions.iDataSort !== undefined )
+				{
+					oCol.aDataSort = [ oOptions.iDataSort ];
+				}
+				_fnMap( oCol, oOptions, "aDataSort" );
+			}
+		
+			/* Cache the data get and set functions for speed */
+			var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null;
+			var mData = _fnGetObjectDataFn( oCol.mData );
+		
+			oCol.fnGetData = function (oData, sSpecific) {
+				var innerData = mData( oData, sSpecific );
+		
+				if ( oCol.mRender && (sSpecific && sSpecific !== '') )
+				{
+					return mRender( innerData, sSpecific, oData );
+				}
+				return innerData;
+			};
+			oCol.fnSetData = _fnSetObjectDataFn( oCol.mData );
+			
+			/* Feature sorting overrides column specific when off */
+			if ( !oSettings.oFeatures.bSort )
+			{
+				oCol.bSortable = false;
+			}
+			
+			/* Check that the class assignment is correct for sorting */
+			if ( !oCol.bSortable ||
+				 ($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1) )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableNone;
+				oCol.sSortingClassJUI = "";
+			}
+			else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortable;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUI;
+			}
+			else if ( $.inArray('asc', oCol.asSorting) != -1 && $.inArray('desc', oCol.asSorting) == -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableAsc;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIAscAllowed;
+			}
+			else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) != -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableDesc;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIDescAllowed;
+			}
+		}
+		
+		
+		/**
+		 * Adjust the table column widths for new data. Note: you would probably want to 
+		 * do a redraw after calling this function!
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAdjustColumnSizing ( oSettings )
+		{
+			/* Not interested in doing column width calculation if auto-width is disabled */
+			if ( oSettings.oFeatures.bAutoWidth === false )
+			{
+				return false;
+			}
+			
+			_fnCalculateColumnWidths( oSettings );
+			for ( var i=0 , iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oSettings.aoColumns[i].nTh.style.width = oSettings.aoColumns[i].sWidth;
+			}
+		}
+		
+		
+		/**
+		 * Covert the index of a visible column to the index in the data array (take account
+		 * of hidden columns)
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iMatch Visible column index to lookup
+		 *  @returns {int} i the data index
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnVisibleToColumnIndex( oSettings, iMatch )
+		{
+			var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+		
+			return typeof aiVis[iMatch] === 'number' ?
+				aiVis[iMatch] :
+				null;
+		}
+		
+		
+		/**
+		 * Covert the index of an index in the data array and convert it to the visible
+		 *   column index (take account of hidden columns)
+		 *  @param {int} iMatch Column index to lookup
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {int} i the data index
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnIndexToVisible( oSettings, iMatch )
+		{
+			var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+			var iPos = $.inArray( iMatch, aiVis );
+		
+			return iPos !== -1 ? iPos : null;
+		}
+		
+		
+		/**
+		 * Get the number of visible columns
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {int} i the number of visible columns
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnVisbleColumns( oSettings )
+		{
+			return _fnGetColumns( oSettings, 'bVisible' ).length;
+		}
+		
+		
+		/**
+		 * Get an array of column indexes that match a given property
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sParam Parameter in aoColumns to look for - typically 
+		 *    bVisible or bSearchable
+		 *  @returns {array} Array of indexes with matched properties
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetColumns( oSettings, sParam )
+		{
+			var a = [];
+		
+			$.map( oSettings.aoColumns, function(val, i) {
+				if ( val[sParam] ) {
+					a.push( i );
+				}
+			} );
+		
+			return a;
+		}
+		
+		
+		/**
+		 * Get the sort type based on an input string
+		 *  @param {string} sData data we wish to know the type of
+		 *  @returns {string} type (defaults to 'string' if no type can be detected)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDetectType( sData )
+		{
+			var aTypes = DataTable.ext.aTypes;
+			var iLen = aTypes.length;
+			
+			for ( var i=0 ; i<iLen ; i++ )
+			{
+				var sType = aTypes[i]( sData );
+				if ( sType !== null )
+				{
+					return sType;
+				}
+			}
+			
+			return 'string';
+		}
+		
+		
+		/**
+		 * Figure out how to reorder a display list
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns array {int} aiReturn index list for reordering
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReOrderIndex ( oSettings, sColumns )
+		{
+			var aColumns = sColumns.split(',');
+			var aiReturn = [];
+			
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				for ( var j=0 ; j<iLen ; j++ )
+				{
+					if ( oSettings.aoColumns[i].sName == aColumns[j] )
+					{
+						aiReturn.push( j );
+						break;
+					}
+				}
+			}
+			
+			return aiReturn;
+		}
+		
+		
+		/**
+		 * Get the column ordering that DataTables expects
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {string} comma separated list of names
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnOrdering ( oSettings )
+		{
+			var sNames = '';
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				sNames += oSettings.aoColumns[i].sName+',';
+			}
+			if ( sNames.length == iLen )
+			{
+				return "";
+			}
+			return sNames.slice(0, -1);
+		}
+		
+		
+		/**
+		 * Take the column definitions and static columns arrays and calculate how
+		 * they relate to column indexes. The callback function will then apply the
+		 * definition found for a column to a suitable configuration object.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aoColDefs The aoColumnDefs array that is to be applied
+		 *  @param {array} aoCols The aoColumns array that defines columns individually
+		 *  @param {function} fn Callback function - takes two parameters, the calculated
+		 *    column index and the definition for that column.
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, fn )
+		{
+			var i, iLen, j, jLen, k, kLen;
+		
+			// Column definitions with aTargets
+			if ( aoColDefs )
+			{
+				/* Loop over the definitions array - loop in reverse so first instance has priority */
+				for ( i=aoColDefs.length-1 ; i>=0 ; i-- )
+				{
+					/* Each definition can target multiple columns, as it is an array */
+					var aTargets = aoColDefs[i].aTargets;
+					if ( !$.isArray( aTargets ) )
+					{
+						_fnLog( oSettings, 1, 'aTargets must be an array of targets, not a '+(typeof aTargets) );
+					}
+		
+					for ( j=0, jLen=aTargets.length ; j<jLen ; j++ )
+					{
+						if ( typeof aTargets[j] === 'number' && aTargets[j] >= 0 )
+						{
+							/* Add columns that we don't yet know about */
+							while( oSettings.aoColumns.length <= aTargets[j] )
+							{
+								_fnAddColumn( oSettings );
+							}
+		
+							/* Integer, basic index */
+							fn( aTargets[j], aoColDefs[i] );
+						}
+						else if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 )
+						{
+							/* Negative integer, right to left column counting */
+							fn( oSettings.aoColumns.length+aTargets[j], aoColDefs[i] );
+						}
+						else if ( typeof aTargets[j] === 'string' )
+						{
+							/* Class name matching on TH element */
+							for ( k=0, kLen=oSettings.aoColumns.length ; k<kLen ; k++ )
+							{
+								if ( aTargets[j] == "_all" ||
+								     $(oSettings.aoColumns[k].nTh).hasClass( aTargets[j] ) )
+								{
+									fn( k, aoColDefs[i] );
+								}
+							}
+						}
+					}
+				}
+			}
+		
+			// Statically defined columns array
+			if ( aoCols )
+			{
+				for ( i=0, iLen=aoCols.length ; i<iLen ; i++ )
+				{
+					fn( i, aoCols[i] );
+				}
+			}
+		}
+		
+		/**
+		 * Add a data array to the table, creating DOM node etc. This is the parallel to 
+		 * _fnGatherData, but for adding rows from a Javascript source, rather than a
+		 * DOM source.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aData data array to be added
+		 *  @returns {int} >=0 if successful (index of new aoData entry), -1 if failed
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddData ( oSettings, aDataSupplied )
+		{
+			var oCol;
+			
+			/* Take an independent copy of the data source so we can bash it about as we wish */
+			var aDataIn = ($.isArray(aDataSupplied)) ?
+				aDataSupplied.slice() :
+				$.extend( true, {}, aDataSupplied );
+			
+			/* Create the object for storing information about this new row */
+			var iRow = oSettings.aoData.length;
+			var oData = $.extend( true, {}, DataTable.models.oRow );
+			oData._aData = aDataIn;
+			oSettings.aoData.push( oData );
+		
+			/* Create the cells */
+			var nTd, sThisType;
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oCol = oSettings.aoColumns[i];
+		
+				/* Use rendered data for filtering / sorting */
+				if ( typeof oCol.fnRender === 'function' && oCol.bUseRendered && oCol.mData !== null )
+				{
+					_fnSetCellData( oSettings, iRow, i, _fnRender(oSettings, iRow, i) );
+				}
+				else
+				{
+					_fnSetCellData( oSettings, iRow, i, _fnGetCellData( oSettings, iRow, i ) );
+				}
+				
+				/* See if we should auto-detect the column type */
+				if ( oCol._bAutoType && oCol.sType != 'string' )
+				{
+					/* Attempt to auto detect the type - same as _fnGatherData() */
+					var sVarType = _fnGetCellData( oSettings, iRow, i, 'type' );
+					if ( sVarType !== null && sVarType !== '' )
+					{
+						sThisType = _fnDetectType( sVarType );
+						if ( oCol.sType === null )
+						{
+							oCol.sType = sThisType;
+						}
+						else if ( oCol.sType != sThisType && oCol.sType != "html" )
+						{
+							/* String is always the 'fallback' option */
+							oCol.sType = 'string';
+						}
+					}
+				}
+			}
+			
+			/* Add to the display array */
+			oSettings.aiDisplayMaster.push( iRow );
+		
+			/* Create the DOM information */
+			if ( !oSettings.oFeatures.bDeferRender )
+			{
+				_fnCreateTr( oSettings, iRow );
+			}
+		
+			return iRow;
+		}
+		
+		
+		/**
+		 * Read in the data from the target table from the DOM
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGatherData( oSettings )
+		{
+			var iLoop, i, iLen, j, jLen, jInner,
+			 	nTds, nTrs, nTd, nTr, aLocalData, iThisIndex,
+				iRow, iRows, iColumn, iColumns, sNodeName,
+				oCol, oData;
+			
+			/*
+			 * Process by row first
+			 * Add the data object for the whole table - storing the tr node. Note - no point in getting
+			 * DOM based data if we are going to go and replace it with Ajax source data.
+			 */
+			if ( oSettings.bDeferLoading || oSettings.sAjaxSource === null )
+			{
+				nTr = oSettings.nTBody.firstChild;
+				while ( nTr )
+				{
+					if ( nTr.nodeName.toUpperCase() == "TR" )
+					{
+						iThisIndex = oSettings.aoData.length;
+						nTr._DT_RowIndex = iThisIndex;
+						oSettings.aoData.push( $.extend( true, {}, DataTable.models.oRow, {
+							"nTr": nTr
+						} ) );
+		
+						oSettings.aiDisplayMaster.push( iThisIndex );
+						nTd = nTr.firstChild;
+						jInner = 0;
+						while ( nTd )
+						{
+							sNodeName = nTd.nodeName.toUpperCase();
+							if ( sNodeName == "TD" || sNodeName == "TH" )
+							{
+								_fnSetCellData( oSettings, iThisIndex, jInner, $.trim(nTd.innerHTML) );
+								jInner++;
+							}
+							nTd = nTd.nextSibling;
+						}
+					}
+					nTr = nTr.nextSibling;
+				}
+			}
+			
+			/* Gather in the TD elements of the Table - note that this is basically the same as
+			 * fnGetTdNodes, but that function takes account of hidden columns, which we haven't yet
+			 * setup!
+			 */
+			nTrs = _fnGetTrNodes( oSettings );
+			nTds = [];
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				nTd = nTrs[i].firstChild;
+				while ( nTd )
+				{
+					sNodeName = nTd.nodeName.toUpperCase();
+					if ( sNodeName == "TD" || sNodeName == "TH" )
+					{
+						nTds.push( nTd );
+					}
+					nTd = nTd.nextSibling;
+				}
+			}
+			
+			/* Now process by column */
+			for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )
+			{
+				oCol = oSettings.aoColumns[iColumn];
+		
+				/* Get the title of the column - unless there is a user set one */
+				if ( oCol.sTitle === null )
+				{
+					oCol.sTitle = oCol.nTh.innerHTML;
+				}
+				
+				var
+					bAutoType = oCol._bAutoType,
+					bRender = typeof oCol.fnRender === 'function',
+					bClass = oCol.sClass !== null,
+					bVisible = oCol.bVisible,
+					nCell, sThisType, sRendered, sValType;
+				
+				/* A single loop to rule them all (and be more efficient) */
+				if ( bAutoType || bRender || bClass || !bVisible )
+				{
+					for ( iRow=0, iRows=oSettings.aoData.length ; iRow<iRows ; iRow++ )
+					{
+						oData = oSettings.aoData[iRow];
+						nCell = nTds[ (iRow*iColumns) + iColumn ];
+						
+						/* Type detection */
+						if ( bAutoType && oCol.sType != 'string' )
+						{
+							sValType = _fnGetCellData( oSettings, iRow, iColumn, 'type' );
+							if ( sValType !== '' )
+							{
+								sThisType = _fnDetectType( sValType );
+								if ( oCol.sType === null )
+								{
+									oCol.sType = sThisType;
+								}
+								else if ( oCol.sType != sThisType && 
+								          oCol.sType != "html" )
+								{
+									/* String is always the 'fallback' option */
+									oCol.sType = 'string';
+								}
+							}
+						}
+		
+						if ( oCol.mRender )
+						{
+							// mRender has been defined, so we need to get the value and set it
+							nCell.innerHTML = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+						}
+						else if ( oCol.mData !== iColumn )
+						{
+							// If mData is not the same as the column number, then we need to
+							// get the dev set value. If it is the column, no point in wasting
+							// time setting the value that is already there!
+							nCell.innerHTML = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+						}
+						
+						/* Rendering */
+						if ( bRender )
+						{
+							sRendered = _fnRender( oSettings, iRow, iColumn );
+							nCell.innerHTML = sRendered;
+							if ( oCol.bUseRendered )
+							{
+								/* Use the rendered data for filtering / sorting */
+								_fnSetCellData( oSettings, iRow, iColumn, sRendered );
+							}
+						}
+						
+						/* Classes */
+						if ( bClass )
+						{
+							nCell.className += ' '+oCol.sClass;
+						}
+						
+						/* Column visibility */
+						if ( !bVisible )
+						{
+							oData._anHidden[iColumn] = nCell;
+							nCell.parentNode.removeChild( nCell );
+						}
+						else
+						{
+							oData._anHidden[iColumn] = null;
+						}
+		
+						if ( oCol.fnCreatedCell )
+						{
+							oCol.fnCreatedCell.call( oSettings.oInstance,
+								nCell, _fnGetCellData( oSettings, iRow, iColumn, 'display' ), oData._aData, iRow, iColumn
+							);
+						}
+					}
+				}
+			}
+		
+			/* Row created callbacks */
+			if ( oSettings.aoRowCreatedCallback.length !== 0 )
+			{
+				for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+				{
+					oData = oSettings.aoData[i];
+					_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [oData.nTr, oData._aData, i] );
+				}
+			}
+		}
+		
+		
+		/**
+		 * Take a TR element and convert it to an index in aoData
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} n the TR element to find
+		 *  @returns {int} index if the node is found, null if not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnNodeToDataIndex( oSettings, n )
+		{
+			return (n._DT_RowIndex!==undefined) ? n._DT_RowIndex : null;
+		}
+		
+		
+		/**
+		 * Take a TD element and convert it into a column data index (not the visible index)
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow The row number the TD/TH can be found in
+		 *  @param {node} n The TD/TH element to find
+		 *  @returns {int} index if the node is found, -1 if not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnNodeToColumnIndex( oSettings, iRow, n )
+		{
+			var anCells = _fnGetTdNodes( oSettings, iRow );
+		
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( anCells[i] === n )
+				{
+					return i;
+				}
+			}
+			return -1;
+		}
+		
+		
+		/**
+		 * Get an array of data for a given row from the internal data cache
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {string} sSpecific data get type ('type' 'filter' 'sort')
+		 *  @param {array} aiColumns Array of column indexes to get data from
+		 *  @returns {array} Data array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetRowData( oSettings, iRow, sSpecific, aiColumns )
+		{
+			var out = [];
+			for ( var i=0, iLen=aiColumns.length ; i<iLen ; i++ )
+			{
+				out.push( _fnGetCellData( oSettings, iRow, aiColumns[i], sSpecific ) );
+			}
+			return out;
+		}
+		
+		
+		/**
+		 * Get the data for a given cell from the internal cache, taking into account data mapping
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {int} iCol Column index
+		 *  @param {string} sSpecific data get type ('display', 'type' 'filter' 'sort')
+		 *  @returns {*} Cell data
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetCellData( oSettings, iRow, iCol, sSpecific )
+		{
+			var sData;
+			var oCol = oSettings.aoColumns[iCol];
+			var oData = oSettings.aoData[iRow]._aData;
+		
+			if ( (sData=oCol.fnGetData( oData, sSpecific )) === undefined )
+			{
+				if ( oSettings.iDrawError != oSettings.iDraw && oCol.sDefaultContent === null )
+				{
+					_fnLog( oSettings, 0, "Requested unknown parameter "+
+						(typeof oCol.mData=='function' ? '{mData function}' : "'"+oCol.mData+"'")+
+						" from the data source for row "+iRow );
+					oSettings.iDrawError = oSettings.iDraw;
+				}
+				return oCol.sDefaultContent;
+			}
+		
+			/* When the data source is null, we can use default column data */
+			if ( sData === null && oCol.sDefaultContent !== null )
+			{
+				sData = oCol.sDefaultContent;
+			}
+			else if ( typeof sData === 'function' )
+			{
+				/* If the data source is a function, then we run it and use the return */
+				return sData();
+			}
+		
+			if ( sSpecific == 'display' && sData === null )
+			{
+				return '';
+			}
+			return sData;
+		}
+		
+		
+		/**
+		 * Set the value for a specific cell, into the internal data cache
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {int} iCol Column index
+		 *  @param {*} val Value to set
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSetCellData( oSettings, iRow, iCol, val )
+		{
+			var oCol = oSettings.aoColumns[iCol];
+			var oData = oSettings.aoData[iRow]._aData;
+		
+			oCol.fnSetData( oData, val );
+		}
+		
+		
+		// Private variable that is used to match array syntax in the data property object
+		var __reArray = /\[.*?\]$/;
+		
+		/**
+		 * Return a function that can be used to get data from a source object, taking
+		 * into account the ability to use nested objects as a source
+		 *  @param {string|int|function} mSource The data source for the object
+		 *  @returns {function} Data get function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetObjectDataFn( mSource )
+		{
+			if ( mSource === null )
+			{
+				/* Give an empty string for rendering / sorting etc */
+				return function (data, type) {
+					return null;
+				};
+			}
+			else if ( typeof mSource === 'function' )
+			{
+				return function (data, type, extra) {
+					return mSource( data, type, extra );
+				};
+			}
+			else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || mSource.indexOf('[') !== -1) )
+			{
+				/* If there is a . in the source string then the data source is in a 
+				 * nested object so we loop over the data for each level to get the next
+				 * level down. On each loop we test for undefined, and if found immediately
+				 * return. This allows entire objects to be missing and sDefaultContent to
+				 * be used if defined, rather than throwing an error
+				 */
+				var fetchData = function (data, type, src) {
+					var a = src.split('.');
+					var arrayNotation, out, innerSrc;
+		
+					if ( src !== "" )
+					{
+						for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+						{
+							// Check if we are dealing with an array notation request
+							arrayNotation = a[i].match(__reArray);
+		
+							if ( arrayNotation ) {
+								a[i] = a[i].replace(__reArray, '');
+		
+								// Condition allows simply [] to be passed in
+								if ( a[i] !== "" ) {
+									data = data[ a[i] ];
+								}
+								out = [];
+								
+								// Get the remainder of the nested object to get
+								a.splice( 0, i+1 );
+								innerSrc = a.join('.');
+		
+								// Traverse each entry in the array getting the properties requested
+								for ( var j=0, jLen=data.length ; j<jLen ; j++ ) {
+									out.push( fetchData( data[j], type, innerSrc ) );
+								}
+		
+								// If a string is given in between the array notation indicators, that
+								// is used to join the strings together, otherwise an array is returned
+								var join = arrayNotation[0].substring(1, arrayNotation[0].length-1);
+								data = (join==="") ? out : out.join(join);
+		
+								// The inner call to fetchData has already traversed through the remainder
+								// of the source requested, so we exit from the loop
+								break;
+							}
+		
+							if ( data === null || data[ a[i] ] === undefined )
+							{
+								return undefined;
+							}
+							data = data[ a[i] ];
+						}
+					}
+		
+					return data;
+				};
+		
+				return function (data, type) {
+					return fetchData( data, type, mSource );
+				};
+			}
+			else
+			{
+				/* Array or flat object mapping */
+				return function (data, type) {
+					return data[mSource];	
+				};
+			}
+		}
+		
+		
+		/**
+		 * Return a function that can be used to set data from a source object, taking
+		 * into account the ability to use nested objects as a source
+		 *  @param {string|int|function} mSource The data source for the object
+		 *  @returns {function} Data set function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSetObjectDataFn( mSource )
+		{
+			if ( mSource === null )
+			{
+				/* Nothing to do when the data source is null */
+				return function (data, val) {};
+			}
+			else if ( typeof mSource === 'function' )
+			{
+				return function (data, val) {
+					mSource( data, 'set', val );
+				};
+			}
+			else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || mSource.indexOf('[') !== -1) )
+			{
+				/* Like the get, we need to get data from a nested object */
+				var setData = function (data, val, src) {
+					var a = src.split('.'), b;
+					var arrayNotation, o, innerSrc;
+		
+					for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ )
+					{
+						// Check if we are dealing with an array notation request
+						arrayNotation = a[i].match(__reArray);
+		
+						if ( arrayNotation )
+						{
+							a[i] = a[i].replace(__reArray, '');
+							data[ a[i] ] = [];
+							
+							// Get the remainder of the nested object to set so we can recurse
+							b = a.slice();
+							b.splice( 0, i+1 );
+							innerSrc = b.join('.');
+		
+							// Traverse each entry in the array setting the properties requested
+							for ( var j=0, jLen=val.length ; j<jLen ; j++ )
+							{
+								o = {};
+								setData( o, val[j], innerSrc );
+								data[ a[i] ].push( o );
+							}
+		
+							// The inner call to setData has already traversed through the remainder
+							// of the source and has set the data, thus we can exit here
+							return;
+						}
+		
+						// If the nested object doesn't currently exist - since we are
+						// trying to set the value - create it
+						if ( data[ a[i] ] === null || data[ a[i] ] === undefined )
+						{
+							data[ a[i] ] = {};
+						}
+						data = data[ a[i] ];
+					}
+		
+					// If array notation is used, we just want to strip it and use the property name
+					// and assign the value. If it isn't used, then we get the result we want anyway
+					data[ a[a.length-1].replace(__reArray, '') ] = val;
+				};
+		
+				return function (data, val) {
+					return setData( data, val, mSource );
+				};
+			}
+			else
+			{
+				/* Array or flat object mapping */
+				return function (data, val) {
+					data[mSource] = val;	
+				};
+			}
+		}
+		
+		
+		/**
+		 * Return an array with the full table data
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns array {array} aData Master data array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetDataMaster ( oSettings )
+		{
+			var aData = [];
+			var iLen = oSettings.aoData.length;
+			for ( var i=0 ; i<iLen; i++ )
+			{
+				aData.push( oSettings.aoData[i]._aData );
+			}
+			return aData;
+		}
+		
+		
+		/**
+		 * Nuke the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnClearTable( oSettings )
+		{
+			oSettings.aoData.splice( 0, oSettings.aoData.length );
+			oSettings.aiDisplayMaster.splice( 0, oSettings.aiDisplayMaster.length );
+			oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length );
+			_fnCalculateEnd( oSettings );
+		}
+		
+		
+		 /**
+		 * Take an array of integers (index array) and remove a target integer (value - not 
+		 * the key!)
+		 *  @param {array} a Index array to target
+		 *  @param {int} iTarget value to find
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDeleteIndex( a, iTarget )
+		{
+			var iTargetIndex = -1;
+			
+			for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+			{
+				if ( a[i] == iTarget )
+				{
+					iTargetIndex = i;
+				}
+				else if ( a[i] > iTarget )
+				{
+					a[i]--;
+				}
+			}
+			
+			if ( iTargetIndex != -1 )
+			{
+				a.splice( iTargetIndex, 1 );
+			}
+		}
+		
+		
+		 /**
+		 * Call the developer defined fnRender function for a given cell (row/column) with
+		 * the required parameters and return the result.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData index for the row
+		 *  @param {int} iCol aoColumns index for the column
+		 *  @returns {*} Return of the developer's fnRender function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnRender( oSettings, iRow, iCol )
+		{
+			var oCol = oSettings.aoColumns[iCol];
+		
+			return oCol.fnRender( {
+				"iDataRow":    iRow,
+				"iDataColumn": iCol,
+				"oSettings":   oSettings,
+				"aData":       oSettings.aoData[iRow]._aData,
+				"mDataProp":   oCol.mData
+			}, _fnGetCellData(oSettings, iRow, iCol, 'display') );
+		}
+		/**
+		 * Create a new TR element (and it's TD children) for a row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow Row to consider
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCreateTr ( oSettings, iRow )
+		{
+			var oData = oSettings.aoData[iRow];
+			var nTd;
+		
+			if ( oData.nTr === null )
+			{
+				oData.nTr = document.createElement('tr');
+		
+				/* Use a private property on the node to allow reserve mapping from the node
+				 * to the aoData array for fast look up
+				 */
+				oData.nTr._DT_RowIndex = iRow;
+		
+				/* Special parameters can be given by the data source to be used on the row */
+				if ( oData._aData.DT_RowId )
+				{
+					oData.nTr.id = oData._aData.DT_RowId;
+				}
+		
+				if ( oData._aData.DT_RowClass )
+				{
+					oData.nTr.className = oData._aData.DT_RowClass;
+				}
+		
+				/* Process each column */
+				for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					var oCol = oSettings.aoColumns[i];
+					nTd = document.createElement( oCol.sCellType );
+		
+					/* Render if needed - if bUseRendered is true then we already have the rendered
+					 * value in the data source - so can just use that
+					 */
+					nTd.innerHTML = (typeof oCol.fnRender === 'function' && (!oCol.bUseRendered || oCol.mData === null)) ?
+						_fnRender( oSettings, iRow, i ) :
+						_fnGetCellData( oSettings, iRow, i, 'display' );
+				
+					/* Add user defined class */
+					if ( oCol.sClass !== null )
+					{
+						nTd.className = oCol.sClass;
+					}
+					
+					if ( oCol.bVisible )
+					{
+						oData.nTr.appendChild( nTd );
+						oData._anHidden[i] = null;
+					}
+					else
+					{
+						oData._anHidden[i] = nTd;
+					}
+		
+					if ( oCol.fnCreatedCell )
+					{
+						oCol.fnCreatedCell.call( oSettings.oInstance,
+							nTd, _fnGetCellData( oSettings, iRow, i, 'display' ), oData._aData, iRow, i
+						);
+					}
+				}
+		
+				_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [oData.nTr, oData._aData, iRow] );
+			}
+		}
+		
+		
+		/**
+		 * Create the HTML header for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildHead( oSettings )
+		{
+			var i, nTh, iLen, j, jLen;
+			var iThs = $('th, td', oSettings.nTHead).length;
+			var iCorrector = 0;
+			var jqChildren;
+			
+			/* If there is a header in place - then use it - otherwise it's going to get nuked... */
+			if ( iThs !== 0 )
+			{
+				/* We've got a thead from the DOM, so remove hidden columns and apply width to vis cols */
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					nTh.setAttribute('role', 'columnheader');
+					if ( oSettings.aoColumns[i].bSortable )
+					{
+						nTh.setAttribute('tabindex', oSettings.iTabIndex);
+						nTh.setAttribute('aria-controls', oSettings.sTableId);
+					}
+		
+					if ( oSettings.aoColumns[i].sClass !== null )
+					{
+						$(nTh).addClass( oSettings.aoColumns[i].sClass );
+					}
+					
+					/* Set the title of the column if it is user defined (not what was auto detected) */
+					if ( oSettings.aoColumns[i].sTitle != nTh.innerHTML )
+					{
+						nTh.innerHTML = oSettings.aoColumns[i].sTitle;
+					}
+				}
+			}
+			else
+			{
+				/* We don't have a header in the DOM - so we are going to have to create one */
+				var nTr = document.createElement( "tr" );
+				
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					nTh.innerHTML = oSettings.aoColumns[i].sTitle;
+					nTh.setAttribute('tabindex', '0');
+					
+					if ( oSettings.aoColumns[i].sClass !== null )
+					{
+						$(nTh).addClass( oSettings.aoColumns[i].sClass );
+					}
+					
+					nTr.appendChild( nTh );
+				}
+				$(oSettings.nTHead).html( '' )[0].appendChild( nTr );
+				_fnDetectHeader( oSettings.aoHeader, oSettings.nTHead );
+			}
+			
+			/* ARIA role for the rows */	
+			$(oSettings.nTHead).children('tr').attr('role', 'row');
+			
+			/* Add the extra markup needed by jQuery UI's themes */
+			if ( oSettings.bJUI )
+			{
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					
+					var nDiv = document.createElement('div');
+					nDiv.className = oSettings.oClasses.sSortJUIWrapper;
+					$(nTh).contents().appendTo(nDiv);
+					
+					var nSpan = document.createElement('span');
+					nSpan.className = oSettings.oClasses.sSortIcon;
+					nDiv.appendChild( nSpan );
+					nTh.appendChild( nDiv );
+				}
+			}
+			
+			if ( oSettings.oFeatures.bSort )
+			{
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bSortable !== false )
+					{
+						_fnSortAttachListener( oSettings, oSettings.aoColumns[i].nTh, i );
+					}
+					else
+					{
+						$(oSettings.aoColumns[i].nTh).addClass( oSettings.oClasses.sSortableNone );
+					}
+				}
+			}
+			
+			/* Deal with the footer - add classes if required */
+			if ( oSettings.oClasses.sFooterTH !== "" )
+			{
+				$(oSettings.nTFoot).children('tr').children('th').addClass( oSettings.oClasses.sFooterTH );
+			}
+			
+			/* Cache the footer elements */
+			if ( oSettings.nTFoot !== null )
+			{
+				var anCells = _fnGetUniqueThs( oSettings, null, oSettings.aoFooter );
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					if ( anCells[i] )
+					{
+						oSettings.aoColumns[i].nTf = anCells[i];
+						if ( oSettings.aoColumns[i].sClass )
+						{
+							$(anCells[i]).addClass( oSettings.aoColumns[i].sClass );
+						}
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Draw the header (or footer) element based on the column visibility states. The
+		 * methodology here is to use the layout array from _fnDetectHeader, modified for
+		 * the instantaneous column visibility, to construct the new layout. The grid is
+		 * traversed over cell at a time in a rows x columns grid fashion, although each 
+		 * cell insert can cover multiple elements in the grid - which is tracks using the
+		 * aApplied array. Cell inserts in the grid will only occur where there isn't
+		 * already a cell in that position.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param array {objects} aoSource Layout array from _fnDetectHeader
+		 *  @param {boolean} [bIncludeHidden=false] If true then include the hidden columns in the calc, 
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDrawHead( oSettings, aoSource, bIncludeHidden )
+		{
+			var i, iLen, j, jLen, k, kLen, n, nLocalTr;
+			var aoLocal = [];
+			var aApplied = [];
+			var iColumns = oSettings.aoColumns.length;
+			var iRowspan, iColspan;
+		
+			if (  bIncludeHidden === undefined )
+			{
+				bIncludeHidden = false;
+			}
+		
+			/* Make a copy of the master layout array, but without the visible columns in it */
+			for ( i=0, iLen=aoSource.length ; i<iLen ; i++ )
+			{
+				aoLocal[i] = aoSource[i].slice();
+				aoLocal[i].nTr = aoSource[i].nTr;
+		
+				/* Remove any columns which are currently hidden */
+				for ( j=iColumns-1 ; j>=0 ; j-- )
+				{
+					if ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden )
+					{
+						aoLocal[i].splice( j, 1 );
+					}
+				}
+		
+				/* Prep the applied array - it needs an element for each row */
+				aApplied.push( [] );
+			}
+		
+			for ( i=0, iLen=aoLocal.length ; i<iLen ; i++ )
+			{
+				nLocalTr = aoLocal[i].nTr;
+				
+				/* All cells are going to be replaced, so empty out the row */
+				if ( nLocalTr )
+				{
+					while( (n = nLocalTr.firstChild) )
+					{
+						nLocalTr.removeChild( n );
+					}
+				}
+		
+				for ( j=0, jLen=aoLocal[i].length ; j<jLen ; j++ )
+				{
+					iRowspan = 1;
+					iColspan = 1;
+		
+					/* Check to see if there is already a cell (row/colspan) covering our target
+					 * insert point. If there is, then there is nothing to do.
+					 */
+					if ( aApplied[i][j] === undefined )
+					{
+						nLocalTr.appendChild( aoLocal[i][j].cell );
+						aApplied[i][j] = 1;
+		
+						/* Expand the cell to cover as many rows as needed */
+						while ( aoLocal[i+iRowspan] !== undefined &&
+						        aoLocal[i][j].cell == aoLocal[i+iRowspan][j].cell )
+						{
+							aApplied[i+iRowspan][j] = 1;
+							iRowspan++;
+						}
+		
+						/* Expand the cell to cover as many columns as needed */
+						while ( aoLocal[i][j+iColspan] !== undefined &&
+						        aoLocal[i][j].cell == aoLocal[i][j+iColspan].cell )
+						{
+							/* Must update the applied array over the rows for the columns */
+							for ( k=0 ; k<iRowspan ; k++ )
+							{
+								aApplied[i+k][j+iColspan] = 1;
+							}
+							iColspan++;
+						}
+		
+						/* Do the actual expansion in the DOM */
+						aoLocal[i][j].cell.rowSpan = iRowspan;
+						aoLocal[i][j].cell.colSpan = iColspan;
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Insert the required TR nodes into the table for display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDraw( oSettings )
+		{
+			/* Provide a pre-callback function which can be used to cancel the draw is false is returned */
+			var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] );
+			if ( $.inArray( false, aPreDraw ) !== -1 )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				return;
+			}
+			
+			var i, iLen, n;
+			var anRows = [];
+			var iRowCount = 0;
+			var iStripes = oSettings.asStripeClasses.length;
+			var iOpenRows = oSettings.aoOpenRows.length;
+			
+			oSettings.bDrawing = true;
+			
+			/* Check and see if we have an initial draw position from state saving */
+			if ( oSettings.iInitDisplayStart !== undefined && oSettings.iInitDisplayStart != -1 )
+			{
+				if ( oSettings.oFeatures.bServerSide )
+				{
+					oSettings._iDisplayStart = oSettings.iInitDisplayStart;
+				}
+				else
+				{
+					oSettings._iDisplayStart = (oSettings.iInitDisplayStart >= oSettings.fnRecordsDisplay()) ?
+						0 : oSettings.iInitDisplayStart;
+				}
+				oSettings.iInitDisplayStart = -1;
+				_fnCalculateEnd( oSettings );
+			}
+			
+			/* Server-side processing draw intercept */
+			if ( oSettings.bDeferLoading )
+			{
+				oSettings.bDeferLoading = false;
+				oSettings.iDraw++;
+			}
+			else if ( !oSettings.oFeatures.bServerSide )
+			{
+				oSettings.iDraw++;
+			}
+			else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) )
+			{
+				return;
+			}
+			
+			if ( oSettings.aiDisplay.length !== 0 )
+			{
+				var iStart = oSettings._iDisplayStart;
+				var iEnd = oSettings._iDisplayEnd;
+				
+				if ( oSettings.oFeatures.bServerSide )
+				{
+					iStart = 0;
+					iEnd = oSettings.aoData.length;
+				}
+				
+				for ( var j=iStart ; j<iEnd ; j++ )
+				{
+					var aoData = oSettings.aoData[ oSettings.aiDisplay[j] ];
+					if ( aoData.nTr === null )
+					{
+						_fnCreateTr( oSettings, oSettings.aiDisplay[j] );
+					}
+		
+					var nRow = aoData.nTr;
+					
+					/* Remove the old striping classes and then add the new one */
+					if ( iStripes !== 0 )
+					{
+						var sStripe = oSettings.asStripeClasses[ iRowCount % iStripes ];
+						if ( aoData._sRowStripe != sStripe )
+						{
+							$(nRow).removeClass( aoData._sRowStripe ).addClass( sStripe );
+							aoData._sRowStripe = sStripe;
+						}
+					}
+					
+					/* Row callback functions - might want to manipulate the row */
+					_fnCallbackFire( oSettings, 'aoRowCallback', null, 
+						[nRow, oSettings.aoData[ oSettings.aiDisplay[j] ]._aData, iRowCount, j] );
+					
+					anRows.push( nRow );
+					iRowCount++;
+					
+					/* If there is an open row - and it is attached to this parent - attach it on redraw */
+					if ( iOpenRows !== 0 )
+					{
+						for ( var k=0 ; k<iOpenRows ; k++ )
+						{
+							if ( nRow == oSettings.aoOpenRows[k].nParent )
+							{
+								anRows.push( oSettings.aoOpenRows[k].nTr );
+								break;
+							}
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Table is empty - create a row with an empty message in it */
+				anRows[ 0 ] = document.createElement( 'tr' );
+				
+				if ( oSettings.asStripeClasses[0] )
+				{
+					anRows[ 0 ].className = oSettings.asStripeClasses[0];
+				}
+		
+				var oLang = oSettings.oLanguage;
+				var sZero = oLang.sZeroRecords;
+				if ( oSettings.iDraw == 1 && oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide )
+				{
+					sZero = oLang.sLoadingRecords;
+				}
+				else if ( oLang.sEmptyTable && oSettings.fnRecordsTotal() === 0 )
+				{
+					sZero = oLang.sEmptyTable;
+				}
+		
+				var nTd = document.createElement( 'td' );
+				nTd.setAttribute( 'valign', "top" );
+				nTd.colSpan = _fnVisbleColumns( oSettings );
+				nTd.className = oSettings.oClasses.sRowEmpty;
+				nTd.innerHTML = _fnInfoMacros( oSettings, sZero );
+				
+				anRows[ iRowCount ].appendChild( nTd );
+			}
+			
+			/* Header and footer callbacks */
+			_fnCallbackFire( oSettings, 'aoHeaderCallback', 'header', [ $(oSettings.nTHead).children('tr')[0], 
+				_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(), oSettings.aiDisplay ] );
+			
+			_fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0], 
+				_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(), oSettings.aiDisplay ] );
+			
+			/* 
+			 * Need to remove any old row from the display - note we can't just empty the tbody using
+			 * $().html('') since this will unbind the jQuery event handlers (even although the node 
+			 * still exists!) - equally we can't use innerHTML, since IE throws an exception.
+			 */
+			var
+				nAddFrag = document.createDocumentFragment(),
+				nRemoveFrag = document.createDocumentFragment(),
+				nBodyPar, nTrs;
+			
+			if ( oSettings.nTBody )
+			{
+				nBodyPar = oSettings.nTBody.parentNode;
+				nRemoveFrag.appendChild( oSettings.nTBody );
+				
+				/* When doing infinite scrolling, only remove child rows when sorting, filtering or start
+				 * up. When not infinite scroll, always do it.
+				 */
+				if ( !oSettings.oScroll.bInfinite || !oSettings._bInitComplete ||
+				 	oSettings.bSorted || oSettings.bFiltered )
+				{
+					while( (n = oSettings.nTBody.firstChild) )
+					{
+						oSettings.nTBody.removeChild( n );
+					}
+				}
+				
+				/* Put the draw table into the dom */
+				for ( i=0, iLen=anRows.length ; i<iLen ; i++ )
+				{
+					nAddFrag.appendChild( anRows[i] );
+				}
+				
+				oSettings.nTBody.appendChild( nAddFrag );
+				if ( nBodyPar !== null )
+				{
+					nBodyPar.appendChild( oSettings.nTBody );
+				}
+			}
+			
+			/* Call all required callback functions for the end of a draw */
+			_fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] );
+			
+			/* Draw is complete, sorting and filtering must be as well */
+			oSettings.bSorted = false;
+			oSettings.bFiltered = false;
+			oSettings.bDrawing = false;
+			
+			if ( oSettings.oFeatures.bServerSide )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				if ( !oSettings._bInitComplete )
+				{
+					_fnInitComplete( oSettings );
+				}
+			}
+		}
+		
+		
+		/**
+		 * Redraw the table - taking account of the various features which are enabled
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReDraw( oSettings )
+		{
+			if ( oSettings.oFeatures.bSort )
+			{
+				/* Sorting will refilter and draw for us */
+				_fnSort( oSettings, oSettings.oPreviousSearch );
+			}
+			else if ( oSettings.oFeatures.bFilter )
+			{
+				/* Filtering will redraw for us */
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch );
+			}
+			else
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Add the options to the page HTML for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddOptionsHtml ( oSettings )
+		{
+			/*
+			 * Create a temporary, empty, div which we can later on replace with what we have generated
+			 * we do it this way to rendering the 'options' html offline - speed :-)
+			 */
+			var nHolding = $('<div></div>')[0];
+			oSettings.nTable.parentNode.insertBefore( nHolding, oSettings.nTable );
+			
+			/* 
+			 * All DataTables are wrapped in a div
+			 */
+			oSettings.nTableWrapper = $('<div id="'+oSettings.sTableId+'_wrapper" class="'+oSettings.oClasses.sWrapper+'" role="grid"></div>')[0];
+			oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling;
+		
+			/* Track where we want to insert the option */
+			var nInsertNode = oSettings.nTableWrapper;
+			
+			/* Loop over the user set positioning and place the elements as needed */
+			var aDom = oSettings.sDom.split('');
+			var nTmp, iPushFeature, cOption, nNewNode, cNext, sAttr, j;
+			for ( var i=0 ; i<aDom.length ; i++ )
+			{
+				iPushFeature = 0;
+				cOption = aDom[i];
+				
+				if ( cOption == '<' )
+				{
+					/* New container div */
+					nNewNode = $('<div></div>')[0];
+					
+					/* Check to see if we should append an id and/or a class name to the container */
+					cNext = aDom[i+1];
+					if ( cNext == "'" || cNext == '"' )
+					{
+						sAttr = "";
+						j = 2;
+						while ( aDom[i+j] != cNext )
+						{
+							sAttr += aDom[i+j];
+							j++;
+						}
+						
+						/* Replace jQuery UI constants */
+						if ( sAttr == "H" )
+						{
+							sAttr = oSettings.oClasses.sJUIHeader;
+						}
+						else if ( sAttr == "F" )
+						{
+							sAttr = oSettings.oClasses.sJUIFooter;
+						}
+						
+						/* The attribute can be in the format of "#id.class", "#id" or "class" This logic
+						 * breaks the string into parts and applies them as needed
+						 */
+						if ( sAttr.indexOf('.') != -1 )
+						{
+							var aSplit = sAttr.split('.');
+							nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1);
+							nNewNode.className = aSplit[1];
+						}
+						else if ( sAttr.charAt(0) == "#" )
+						{
+							nNewNode.id = sAttr.substr(1, sAttr.length-1);
+						}
+						else
+						{
+							nNewNode.className = sAttr;
+						}
+						
+						i += j; /* Move along the position array */
+					}
+					
+					nInsertNode.appendChild( nNewNode );
+					nInsertNode = nNewNode;
+				}
+				else if ( cOption == '>' )
+				{
+					/* End container div */
+					nInsertNode = nInsertNode.parentNode;
+				}
+				else if ( cOption == 'l' && oSettings.oFeatures.bPaginate && oSettings.oFeatures.bLengthChange )
+				{
+					/* Length */
+					nTmp = _fnFeatureHtmlLength( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'f' && oSettings.oFeatures.bFilter )
+				{
+					/* Filter */
+					nTmp = _fnFeatureHtmlFilter( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'r' && oSettings.oFeatures.bProcessing )
+				{
+					/* pRocessing */
+					nTmp = _fnFeatureHtmlProcessing( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 't' )
+				{
+					/* Table */
+					nTmp = _fnFeatureHtmlTable( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption ==  'i' && oSettings.oFeatures.bInfo )
+				{
+					/* Info */
+					nTmp = _fnFeatureHtmlInfo( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'p' && oSettings.oFeatures.bPaginate )
+				{
+					/* Pagination */
+					nTmp = _fnFeatureHtmlPaginate( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( DataTable.ext.aoFeatures.length !== 0 )
+				{
+					/* Plug-in features */
+					var aoFeatures = DataTable.ext.aoFeatures;
+					for ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ )
+					{
+						if ( cOption == aoFeatures[k].cFeature )
+						{
+							nTmp = aoFeatures[k].fnInit( oSettings );
+							if ( nTmp )
+							{
+								iPushFeature = 1;
+							}
+							break;
+						}
+					}
+				}
+				
+				/* Add to the 2D features array */
+				if ( iPushFeature == 1 && nTmp !== null )
+				{
+					if ( typeof oSettings.aanFeatures[cOption] !== 'object' )
+					{
+						oSettings.aanFeatures[cOption] = [];
+					}
+					oSettings.aanFeatures[cOption].push( nTmp );
+					nInsertNode.appendChild( nTmp );
+				}
+			}
+			
+			/* Built our DOM structure - replace the holding div with what we want */
+			nHolding.parentNode.replaceChild( oSettings.nTableWrapper, nHolding );
+		}
+		
+		
+		/**
+		 * Use the DOM source to create up an array of header cells. The idea here is to
+		 * create a layout grid (array) of rows x columns, which contains a reference
+		 * to the cell that that point in the grid (regardless of col/rowspan), such that
+		 * any column / row could be removed and the new grid constructed
+		 *  @param array {object} aLayout Array to store the calculated layout in
+		 *  @param {node} nThead The header/footer element for the table
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDetectHeader ( aLayout, nThead )
+		{
+			var nTrs = $(nThead).children('tr');
+			var nTr, nCell;
+			var i, k, l, iLen, jLen, iColShifted, iColumn, iColspan, iRowspan;
+			var bUnique;
+			var fnShiftCol = function ( a, i, j ) {
+				var k = a[i];
+		                while ( k[j] ) {
+					j++;
+				}
+				return j;
+			};
+		
+			aLayout.splice( 0, aLayout.length );
+			
+			/* We know how many rows there are in the layout - so prep it */
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				aLayout.push( [] );
+			}
+			
+			/* Calculate a layout array */
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				nTr = nTrs[i];
+				iColumn = 0;
+				
+				/* For every cell in the row... */
+				nCell = nTr.firstChild;
+				while ( nCell ) {
+					if ( nCell.nodeName.toUpperCase() == "TD" ||
+					     nCell.nodeName.toUpperCase() == "TH" )
+					{
+						/* Get the col and rowspan attributes from the DOM and sanitise them */
+						iColspan = nCell.getAttribute('colspan') * 1;
+						iRowspan = nCell.getAttribute('rowspan') * 1;
+						iColspan = (!iColspan || iColspan===0 || iColspan===1) ? 1 : iColspan;
+						iRowspan = (!iRowspan || iRowspan===0 || iRowspan===1) ? 1 : iRowspan;
+		
+						/* There might be colspan cells already in this row, so shift our target 
+						 * accordingly
+						 */
+						iColShifted = fnShiftCol( aLayout, i, iColumn );
+						
+						/* Cache calculation for unique columns */
+						bUnique = iColspan === 1 ? true : false;
+						
+						/* If there is col / rowspan, copy the information into the layout grid */
+						for ( l=0 ; l<iColspan ; l++ )
+						{
+							for ( k=0 ; k<iRowspan ; k++ )
+							{
+								aLayout[i+k][iColShifted+l] = {
+									"cell": nCell,
+									"unique": bUnique
+								};
+								aLayout[i+k].nTr = nTr;
+							}
+						}
+					}
+					nCell = nCell.nextSibling;
+				}
+			}
+		}
+		
+		
+		/**
+		 * Get an array of unique th elements, one for each column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nHeader automatically detect the layout from this node - optional
+		 *  @param {array} aLayout thead/tfoot layout from _fnDetectHeader - optional
+		 *  @returns array {node} aReturn list of unique th's
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetUniqueThs ( oSettings, nHeader, aLayout )
+		{
+			var aReturn = [];
+			if ( !aLayout )
+			{
+				aLayout = oSettings.aoHeader;
+				if ( nHeader )
+				{
+					aLayout = [];
+					_fnDetectHeader( aLayout, nHeader );
+				}
+			}
+		
+			for ( var i=0, iLen=aLayout.length ; i<iLen ; i++ )
+			{
+				for ( var j=0, jLen=aLayout[i].length ; j<jLen ; j++ )
+				{
+					if ( aLayout[i][j].unique && 
+						 (!aReturn[j] || !oSettings.bSortCellsTop) )
+					{
+						aReturn[j] = aLayout[i][j].cell;
+					}
+				}
+			}
+			
+			return aReturn;
+		}
+		
+		
+		
+		/**
+		 * Update the table using an Ajax call
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {boolean} Block the table drawing or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxUpdate( oSettings )
+		{
+			if ( oSettings.bAjaxDataGet )
+			{
+				oSettings.iDraw++;
+				_fnProcessingDisplay( oSettings, true );
+				var iColumns = oSettings.aoColumns.length;
+				var aoData = _fnAjaxParameters( oSettings );
+				_fnServerParams( oSettings, aoData );
+				
+				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aoData,
+					function(json) {
+						_fnAjaxUpdateDraw( oSettings, json );
+					}, oSettings );
+				return false;
+			}
+			else
+			{
+				return true;
+			}
+		}
+		
+		
+		/**
+		 * Build up the parameters in an object needed for a server-side processing request
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {bool} block the table drawing or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxParameters( oSettings )
+		{
+			var iColumns = oSettings.aoColumns.length;
+			var aoData = [], mDataProp, aaSort, aDataSort;
+			var i, j;
+			
+			aoData.push( { "name": "sEcho",          "value": oSettings.iDraw } );
+			aoData.push( { "name": "iColumns",       "value": iColumns } );
+			aoData.push( { "name": "sColumns",       "value": _fnColumnOrdering(oSettings) } );
+			aoData.push( { "name": "iDisplayStart",  "value": oSettings._iDisplayStart } );
+			aoData.push( { "name": "iDisplayLength", "value": oSettings.oFeatures.bPaginate !== false ?
+				oSettings._iDisplayLength : -1 } );
+				
+			for ( i=0 ; i<iColumns ; i++ )
+			{
+			  mDataProp = oSettings.aoColumns[i].mData;
+				aoData.push( { "name": "mDataProp_"+i, "value": typeof(mDataProp)==="function" ? 'function' : mDataProp } );
+			}
+			
+			/* Filtering */
+			if ( oSettings.oFeatures.bFilter !== false )
+			{
+				aoData.push( { "name": "sSearch", "value": oSettings.oPreviousSearch.sSearch } );
+				aoData.push( { "name": "bRegex",  "value": oSettings.oPreviousSearch.bRegex } );
+				for ( i=0 ; i<iColumns ; i++ )
+				{
+					aoData.push( { "name": "sSearch_"+i,     "value": oSettings.aoPreSearchCols[i].sSearch } );
+					aoData.push( { "name": "bRegex_"+i,      "value": oSettings.aoPreSearchCols[i].bRegex } );
+					aoData.push( { "name": "bSearchable_"+i, "value": oSettings.aoColumns[i].bSearchable } );
+				}
+			}
+			
+			/* Sorting */
+			if ( oSettings.oFeatures.bSort !== false )
+			{
+				var iCounter = 0;
+		
+				aaSort = ( oSettings.aaSortingFixed !== null ) ?
+					oSettings.aaSortingFixed.concat( oSettings.aaSorting ) :
+					oSettings.aaSorting.slice();
+				
+				for ( i=0 ; i<aaSort.length ; i++ )
+				{
+					aDataSort = oSettings.aoColumns[ aaSort[i][0] ].aDataSort;
+					
+					for ( j=0 ; j<aDataSort.length ; j++ )
+					{
+						aoData.push( { "name": "iSortCol_"+iCounter,  "value": aDataSort[j] } );
+						aoData.push( { "name": "sSortDir_"+iCounter,  "value": aaSort[i][1] } );
+						iCounter++;
+					}
+				}
+				aoData.push( { "name": "iSortingCols",   "value": iCounter } );
+				
+				for ( i=0 ; i<iColumns ; i++ )
+				{
+					aoData.push( { "name": "bSortable_"+i,  "value": oSettings.aoColumns[i].bSortable } );
+				}
+			}
+			
+			return aoData;
+		}
+		
+		
+		/**
+		 * Add Ajax parameters from plug-ins
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param array {objects} aoData name/value pairs to send to the server
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnServerParams( oSettings, aoData )
+		{
+			_fnCallbackFire( oSettings, 'aoServerParams', 'serverParams', [aoData] );
+		}
+		
+		
+		/**
+		 * Data the data from the server (nuking the old) and redraw the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} json json data return from the server.
+		 *  @param {string} json.sEcho Tracking flag for DataTables to match requests
+		 *  @param {int} json.iTotalRecords Number of records in the data set, not accounting for filtering
+		 *  @param {int} json.iTotalDisplayRecords Number of records in the data set, accounting for filtering
+		 *  @param {array} json.aaData The data to display on this page
+		 *  @param {string} [json.sColumns] Column ordering (sName, comma separated)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxUpdateDraw ( oSettings, json )
+		{
+			if ( json.sEcho !== undefined )
+			{
+				/* Protect against old returns over-writing a new one. Possible when you get
+				 * very fast interaction, and later queries are completed much faster
+				 */
+				if ( json.sEcho*1 < oSettings.iDraw )
+				{
+					return;
+				}
+				else
+				{
+					oSettings.iDraw = json.sEcho * 1;
+				}
+			}
+			
+			if ( !oSettings.oScroll.bInfinite ||
+				   (oSettings.oScroll.bInfinite && (oSettings.bSorted || oSettings.bFiltered)) )
+			{
+				_fnClearTable( oSettings );
+			}
+			oSettings._iRecordsTotal = parseInt(json.iTotalRecords, 10);
+			oSettings._iRecordsDisplay = parseInt(json.iTotalDisplayRecords, 10);
+			
+			/* Determine if reordering is required */
+			var sOrdering = _fnColumnOrdering(oSettings);
+			var bReOrder = (json.sColumns !== undefined && sOrdering !== "" && json.sColumns != sOrdering );
+			var aiIndex;
+			if ( bReOrder )
+			{
+				aiIndex = _fnReOrderIndex( oSettings, json.sColumns );
+			}
+			
+			var aData = _fnGetObjectDataFn( oSettings.sAjaxDataProp )( json );
+			for ( var i=0, iLen=aData.length ; i<iLen ; i++ )
+			{
+				if ( bReOrder )
+				{
+					/* If we need to re-order, then create a new array with the correct order and add it */
+					var aDataSorted = [];
+					for ( var j=0, jLen=oSettings.aoColumns.length ; j<jLen ; j++ )
+					{
+						aDataSorted.push( aData[i][ aiIndex[j] ] );
+					}
+					_fnAddData( oSettings, aDataSorted );
+				}
+				else
+				{
+					/* No re-order required, sever got it "right" - just straight add */
+					_fnAddData( oSettings, aData[i] );
+				}
+			}
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			oSettings.bAjaxDataGet = false;
+			_fnDraw( oSettings );
+			oSettings.bAjaxDataGet = true;
+			_fnProcessingDisplay( oSettings, false );
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for filtering text
+		 *  @returns {node} Filter control element
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlFilter ( oSettings )
+		{
+			var oPreviousSearch = oSettings.oPreviousSearch;
+			
+			var sSearchStr = oSettings.oLanguage.sSearch;
+			sSearchStr = (sSearchStr.indexOf('_INPUT_') !== -1) ?
+			  sSearchStr.replace('_INPUT_', '<input type="text" />') :
+			  sSearchStr==="" ? '<input type="text" />' : sSearchStr+' <input type="text" />';
+			
+			var nFilter = document.createElement( 'div' );
+			nFilter.className = oSettings.oClasses.sFilter;
+			nFilter.innerHTML = '<label>'+sSearchStr+'</label>';
+			if ( !oSettings.aanFeatures.f )
+			{
+				nFilter.id = oSettings.sTableId+'_filter';
+			}
+			
+			var jqFilter = $('input[type="text"]', nFilter);
+		
+			// Store a reference to the input element, so other input elements could be
+			// added to the filter wrapper if needed (submit button for example)
+			nFilter._DT_Input = jqFilter[0];
+		
+			jqFilter.val( oPreviousSearch.sSearch.replace('"','&quot;') );
+			jqFilter.bind( 'keyup.DT', function(e) {
+				/* Update all other filter input elements for the new display */
+				var n = oSettings.aanFeatures.f;
+				var val = this.value==="" ? "" : this.value; // mental IE8 fix :-(
+		
+				for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+				{
+					if ( n[i] != $(this).parents('div.dataTables_filter')[0] )
+					{
+						$(n[i]._DT_Input).val( val );
+					}
+				}
+				
+				/* Now do the filter */
+				if ( val != oPreviousSearch.sSearch )
+				{
+					_fnFilterComplete( oSettings, { 
+						"sSearch": val, 
+						"bRegex": oPreviousSearch.bRegex,
+						"bSmart": oPreviousSearch.bSmart ,
+						"bCaseInsensitive": oPreviousSearch.bCaseInsensitive 
+					} );
+				}
+			} );
+		
+			jqFilter
+				.attr('aria-controls', oSettings.sTableId)
+				.bind( 'keypress.DT', function(e) {
+					/* Prevent form submission */
+					if ( e.keyCode == 13 )
+					{
+						return false;
+					}
+				}
+			);
+			
+			return nFilter;
+		}
+		
+		
+		/**
+		 * Filter the table using both the global filter and column based filtering
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} oSearch search information
+		 *  @param {int} [iForce] force a research of the master array (1) or not (undefined or 0)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterComplete ( oSettings, oInput, iForce )
+		{
+			var oPrevSearch = oSettings.oPreviousSearch;
+			var aoPrevSearch = oSettings.aoPreSearchCols;
+			var fnSaveFilter = function ( oFilter ) {
+				/* Save the filtering values */
+				oPrevSearch.sSearch = oFilter.sSearch;
+				oPrevSearch.bRegex = oFilter.bRegex;
+				oPrevSearch.bSmart = oFilter.bSmart;
+				oPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive;
+			};
+		
+			/* In server-side processing all filtering is done by the server, so no point hanging around here */
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				/* Global filter */
+				_fnFilter( oSettings, oInput.sSearch, iForce, oInput.bRegex, oInput.bSmart, oInput.bCaseInsensitive );
+				fnSaveFilter( oInput );
+		
+				/* Now do the individual column filter */
+				for ( var i=0 ; i<oSettings.aoPreSearchCols.length ; i++ )
+				{
+					_fnFilterColumn( oSettings, aoPrevSearch[i].sSearch, i, aoPrevSearch[i].bRegex, 
+						aoPrevSearch[i].bSmart, aoPrevSearch[i].bCaseInsensitive );
+				}
+				
+				/* Custom filtering */
+				_fnFilterCustom( oSettings );
+			}
+			else
+			{
+				fnSaveFilter( oInput );
+			}
+			
+			/* Tell the draw function we have been filtering */
+			oSettings.bFiltered = true;
+			$(oSettings.oInstance).trigger('filter', oSettings);
+			
+			/* Redraw the table */
+			oSettings._iDisplayStart = 0;
+			_fnCalculateEnd( oSettings );
+			_fnDraw( oSettings );
+			
+			/* Rebuild search array 'offline' */
+			_fnBuildSearchArray( oSettings, 0 );
+		}
+		
+		
+		/**
+		 * Apply custom filtering functions
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterCustom( oSettings )
+		{
+			var afnFilters = DataTable.ext.afnFiltering;
+			var aiFilterColumns = _fnGetColumns( oSettings, 'bSearchable' );
+		
+			for ( var i=0, iLen=afnFilters.length ; i<iLen ; i++ )
+			{
+				var iCorrector = 0;
+				for ( var j=0, jLen=oSettings.aiDisplay.length ; j<jLen ; j++ )
+				{
+					var iDisIndex = oSettings.aiDisplay[j-iCorrector];
+					var bTest = afnFilters[i](
+						oSettings,
+						_fnGetRowData( oSettings, iDisIndex, 'filter', aiFilterColumns ),
+						iDisIndex
+					);
+					
+					/* Check if we should use this row based on the filtering function */
+					if ( !bTest )
+					{
+						oSettings.aiDisplay.splice( j-iCorrector, 1 );
+						iCorrector++;
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Filter the table on a per-column basis
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sInput string to filter on
+		 *  @param {int} iColumn column to filter
+		 *  @param {bool} bRegex treat search string as a regular expression or not
+		 *  @param {bool} bSmart use smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insenstive matching or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterColumn ( oSettings, sInput, iColumn, bRegex, bSmart, bCaseInsensitive )
+		{
+			if ( sInput === "" )
+			{
+				return;
+			}
+			
+			var iIndexCorrector = 0;
+			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart, bCaseInsensitive );
+			
+			for ( var i=oSettings.aiDisplay.length-1 ; i>=0 ; i-- )
+			{
+				var sData = _fnDataToSearch( _fnGetCellData( oSettings, oSettings.aiDisplay[i], iColumn, 'filter' ),
+					oSettings.aoColumns[iColumn].sType );
+				if ( ! rpSearch.test( sData ) )
+				{
+					oSettings.aiDisplay.splice( i, 1 );
+					iIndexCorrector++;
+				}
+			}
+		}
+		
+		
+		/**
+		 * Filter the data table based on user input and draw the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sInput string to filter on
+		 *  @param {int} iForce optional - force a research of the master array (1) or not (undefined or 0)
+		 *  @param {bool} bRegex treat as a regular expression or not
+		 *  @param {bool} bSmart perform smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insenstive matching or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilter( oSettings, sInput, iForce, bRegex, bSmart, bCaseInsensitive )
+		{
+			var i;
+			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart, bCaseInsensitive );
+			var oPrevSearch = oSettings.oPreviousSearch;
+			
+			/* Check if we are forcing or not - optional parameter */
+			if ( !iForce )
+			{
+				iForce = 0;
+			}
+			
+			/* Need to take account of custom filtering functions - always filter */
+			if ( DataTable.ext.afnFiltering.length !== 0 )
+			{
+				iForce = 1;
+			}
+			
+			/*
+			 * If the input is blank - we want the full data set
+			 */
+			if ( sInput.length <= 0 )
+			{
+				oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			}
+			else
+			{
+				/*
+				 * We are starting a new search or the new search string is smaller 
+				 * then the old one (i.e. delete). Search from the master array
+			 	 */
+				if ( oSettings.aiDisplay.length == oSettings.aiDisplayMaster.length ||
+					   oPrevSearch.sSearch.length > sInput.length || iForce == 1 ||
+					   sInput.indexOf(oPrevSearch.sSearch) !== 0 )
+				{
+					/* Nuke the old display array - we are going to rebuild it */
+					oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);
+					
+					/* Force a rebuild of the search array */
+					_fnBuildSearchArray( oSettings, 1 );
+					
+					/* Search through all records to populate the search array
+					 * The the oSettings.aiDisplayMaster and asDataSearch arrays have 1 to 1 
+					 * mapping
+					 */
+					for ( i=0 ; i<oSettings.aiDisplayMaster.length ; i++ )
+					{
+						if ( rpSearch.test(oSettings.asDataSearch[i]) )
+						{
+							oSettings.aiDisplay.push( oSettings.aiDisplayMaster[i] );
+						}
+					}
+			  }
+			  else
+				{
+			  	/* Using old search array - refine it - do it this way for speed
+			  	 * Don't have to search the whole master array again
+					 */
+			  	var iIndexCorrector = 0;
+			  	
+			  	/* Search the current results */
+			  	for ( i=0 ; i<oSettings.asDataSearch.length ; i++ )
+					{
+			  		if ( ! rpSearch.test(oSettings.asDataSearch[i]) )
+						{
+			  			oSettings.aiDisplay.splice( i-iIndexCorrector, 1 );
+			  			iIndexCorrector++;
+			  		}
+			  	}
+			  }
+			}
+		}
+		
+		
+		/**
+		 * Create an array which can be quickly search through
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iMaster use the master data array - optional
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildSearchArray ( oSettings, iMaster )
+		{
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				/* Clear out the old data */
+				oSettings.asDataSearch = [];
+		
+				var aiFilterColumns = _fnGetColumns( oSettings, 'bSearchable' );
+				var aiIndex = (iMaster===1) ?
+				 	oSettings.aiDisplayMaster :
+				 	oSettings.aiDisplay;
+				
+				for ( var i=0, iLen=aiIndex.length ; i<iLen ; i++ )
+				{
+					oSettings.asDataSearch[i] = _fnBuildSearchRow(
+						oSettings,
+						_fnGetRowData( oSettings, aiIndex[i], 'filter', aiFilterColumns )
+					);
+				}
+			}
+		}
+		
+		
+		/**
+		 * Create a searchable string from a single data row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aData Row data array to use for the data to search
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildSearchRow( oSettings, aData )
+		{
+			var sSearch = aData.join('  ');
+			
+			/* If it looks like there is an HTML entity in the string, attempt to decode it */
+			if ( sSearch.indexOf('&') !== -1 )
+			{
+				sSearch = $('<div>').html(sSearch).text();
+			}
+			
+			// Strip newline characters
+			return sSearch.replace( /[\n\r]/g, " " );
+		}
+		
+		/**
+		 * Build a regular expression object suitable for searching a table
+		 *  @param {string} sSearch string to search for
+		 *  @param {bool} bRegex treat as a regular expression or not
+		 *  @param {bool} bSmart perform smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insensitive matching or not
+		 *  @returns {RegExp} constructed object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterCreateSearch( sSearch, bRegex, bSmart, bCaseInsensitive )
+		{
+			var asSearch, sRegExpString;
+			
+			if ( bSmart )
+			{
+				/* Generate the regular expression to use. Something along the lines of:
+				 * ^(?=.*?\bone\b)(?=.*?\btwo\b)(?=.*?\bthree\b).*$
+				 */
+				asSearch = bRegex ? sSearch.split( ' ' ) : _fnEscapeRegex( sSearch ).split( ' ' );
+				sRegExpString = '^(?=.*?'+asSearch.join( ')(?=.*?' )+').*$';
+				return new RegExp( sRegExpString, bCaseInsensitive ? "i" : "" );
+			}
+			else
+			{
+				sSearch = bRegex ? sSearch : _fnEscapeRegex( sSearch );
+				return new RegExp( sSearch, bCaseInsensitive ? "i" : "" );
+			}
+		}
+		
+		
+		/**
+		 * Convert raw data into something that the user can search on
+		 *  @param {string} sData data to be modified
+		 *  @param {string} sType data type
+		 *  @returns {string} search string
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDataToSearch ( sData, sType )
+		{
+			if ( typeof DataTable.ext.ofnSearch[sType] === "function" )
+			{
+				return DataTable.ext.ofnSearch[sType]( sData );
+			}
+			else if ( sData === null )
+			{
+				return '';
+			}
+			else if ( sType == "html" )
+			{
+				return sData.replace(/[\r\n]/g," ").replace( /<.*?>/g, "" );
+			}
+			else if ( typeof sData === "string" )
+			{
+				return sData.replace(/[\r\n]/g," ");
+			}
+			return sData;
+		}
+		
+		
+		/**
+		 * scape a string such that it can be used in a regular expression
+		 *  @param {string} sVal string to escape
+		 *  @returns {string} escaped string
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnEscapeRegex ( sVal )
+		{
+			var acEscape = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ];
+			var reReplace = new RegExp( '(\\' + acEscape.join('|\\') + ')', 'g' );
+			return sVal.replace(reReplace, '\\$1');
+		}
+		
+		
+		/**
+		 * Generate the node required for the info display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Information element
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlInfo ( oSettings )
+		{
+			var nInfo = document.createElement( 'div' );
+			nInfo.className = oSettings.oClasses.sInfo;
+			
+			/* Actions that are to be taken once only for this feature */
+			if ( !oSettings.aanFeatures.i )
+			{
+				/* Add draw callback */
+				oSettings.aoDrawCallback.push( {
+					"fn": _fnUpdateInfo,
+					"sName": "information"
+				} );
+				
+				/* Add id */
+				nInfo.id = oSettings.sTableId+'_info';
+			}
+			oSettings.nTable.setAttribute( 'aria-describedby', oSettings.sTableId+'_info' );
+			
+			return nInfo;
+		}
+		
+		
+		/**
+		 * Update the information elements in the display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnUpdateInfo ( oSettings )
+		{
+			/* Show information about the table */
+			if ( !oSettings.oFeatures.bInfo || oSettings.aanFeatures.i.length === 0 )
+			{
+				return;
+			}
+			
+			var
+				oLang = oSettings.oLanguage,
+				iStart = oSettings._iDisplayStart+1,
+				iEnd = oSettings.fnDisplayEnd(),
+				iMax = oSettings.fnRecordsTotal(),
+				iTotal = oSettings.fnRecordsDisplay(),
+				sOut;
+			
+			if ( iTotal === 0 )
+			{
+				/* Empty record set */
+				sOut = oLang.sInfoEmpty;
+			}
+			else {
+				/* Normal record set */
+				sOut = oLang.sInfo;
+			}
+		
+			if ( iTotal != iMax )
+			{
+				/* Record set after filtering */
+				sOut += ' ' + oLang.sInfoFiltered;
+			}
+		
+			// Convert the macros
+			sOut += oLang.sInfoPostFix;
+			sOut = _fnInfoMacros( oSettings, sOut );
+			
+			if ( oLang.fnInfoCallback !== null )
+			{
+				sOut = oLang.fnInfoCallback.call( oSettings.oInstance, 
+					oSettings, iStart, iEnd, iMax, iTotal, sOut );
+			}
+			
+			var n = oSettings.aanFeatures.i;
+			for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+			{
+				$(n[i]).html( sOut );
+			}
+		}
+		
+		
+		function _fnInfoMacros ( oSettings, str )
+		{
+			var
+				iStart = oSettings._iDisplayStart+1,
+				sStart = oSettings.fnFormatNumber( iStart ),
+				iEnd = oSettings.fnDisplayEnd(),
+				sEnd = oSettings.fnFormatNumber( iEnd ),
+				iTotal = oSettings.fnRecordsDisplay(),
+				sTotal = oSettings.fnFormatNumber( iTotal ),
+				iMax = oSettings.fnRecordsTotal(),
+				sMax = oSettings.fnFormatNumber( iMax );
+		
+			// When infinite scrolling, we are always starting at 1. _iDisplayStart is used only
+			// internally
+			if ( oSettings.oScroll.bInfinite )
+			{
+				sStart = oSettings.fnFormatNumber( 1 );
+			}
+		
+			return str.
+				replace(/_START_/g, sStart).
+				replace(/_END_/g,   sEnd).
+				replace(/_TOTAL_/g, sTotal).
+				replace(/_MAX_/g,   sMax);
+		}
+		
+		
+		
+		/**
+		 * Draw the table for the first time, adding all required features
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnInitialise ( oSettings )
+		{
+			var i, iLen, iAjaxStart=oSettings.iInitDisplayStart;
+			
+			/* Ensure that the table data is fully initialised */
+			if ( oSettings.bInitialised === false )
+			{
+				setTimeout( function(){ _fnInitialise( oSettings ); }, 200 );
+				return;
+			}
+			
+			/* Show the display HTML options */
+			_fnAddOptionsHtml( oSettings );
+			
+			/* Build and draw the header / footer for the table */
+			_fnBuildHead( oSettings );
+			_fnDrawHead( oSettings, oSettings.aoHeader );
+			if ( oSettings.nTFoot )
+			{
+				_fnDrawHead( oSettings, oSettings.aoFooter );
+			}
+		
+			/* Okay to show that something is going on now */
+			_fnProcessingDisplay( oSettings, true );
+			
+			/* Calculate sizes for columns */
+			if ( oSettings.oFeatures.bAutoWidth )
+			{
+				_fnCalculateColumnWidths( oSettings );
+			}
+			
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoColumns[i].sWidth !== null )
+				{
+					oSettings.aoColumns[i].nTh.style.width = _fnStringToCss( oSettings.aoColumns[i].sWidth );
+				}
+			}
+			
+			/* If there is default sorting required - let's do it. The sort function will do the
+			 * drawing for us. Otherwise we draw the table regardless of the Ajax source - this allows
+			 * the table to look initialised for Ajax sourcing data (show 'loading' message possibly)
+			 */
+			if ( oSettings.oFeatures.bSort )
+			{
+				_fnSort( oSettings );
+			}
+			else if ( oSettings.oFeatures.bFilter )
+			{
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch );
+			}
+			else
+			{
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			/* if there is an ajax source load the data */
+			if ( oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide )
+			{
+				var aoData = [];
+				_fnServerParams( oSettings, aoData );
+				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aoData, function(json) {
+					var aData = (oSettings.sAjaxDataProp !== "") ?
+					 	_fnGetObjectDataFn( oSettings.sAjaxDataProp )(json) : json;
+		
+					/* Got the data - add it to the table */
+					for ( i=0 ; i<aData.length ; i++ )
+					{
+						_fnAddData( oSettings, aData[i] );
+					}
+					
+					/* Reset the init display for cookie saving. We've already done a filter, and
+					 * therefore cleared it before. So we need to make it appear 'fresh'
+					 */
+					oSettings.iInitDisplayStart = iAjaxStart;
+					
+					if ( oSettings.oFeatures.bSort )
+					{
+						_fnSort( oSettings );
+					}
+					else
+					{
+						oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+						_fnCalculateEnd( oSettings );
+						_fnDraw( oSettings );
+					}
+					
+					_fnProcessingDisplay( oSettings, false );
+					_fnInitComplete( oSettings, json );
+				}, oSettings );
+				return;
+			}
+			
+			/* Server-side processing initialisation complete is done at the end of _fnDraw */
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				_fnInitComplete( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Draw the table for the first time, adding all required features
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} [json] JSON from the server that completed the table, if using Ajax source
+		 *    with client-side processing (optional)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnInitComplete ( oSettings, json )
+		{
+			oSettings._bInitComplete = true;
+			_fnCallbackFire( oSettings, 'aoInitComplete', 'init', [oSettings, json] );
+		}
+		
+		
+		/**
+		 * Language compatibility - when certain options are given, and others aren't, we
+		 * need to duplicate the values over, in order to provide backwards compatibility
+		 * with older language files.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLanguageCompat( oLanguage )
+		{
+			var oDefaults = DataTable.defaults.oLanguage;
+		
+			/* Backwards compatibility - if there is no sEmptyTable given, then use the same as
+			 * sZeroRecords - assuming that is given.
+			 */
+			if ( !oLanguage.sEmptyTable && oLanguage.sZeroRecords &&
+				oDefaults.sEmptyTable === "No data available in table" )
+			{
+				_fnMap( oLanguage, oLanguage, 'sZeroRecords', 'sEmptyTable' );
+			}
+		
+			/* Likewise with loading records */
+			if ( !oLanguage.sLoadingRecords && oLanguage.sZeroRecords &&
+				oDefaults.sLoadingRecords === "Loading..." )
+			{
+				_fnMap( oLanguage, oLanguage, 'sZeroRecords', 'sLoadingRecords' );
+			}
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for user display length changing
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Display length feature node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlLength ( oSettings )
+		{
+			if ( oSettings.oScroll.bInfinite )
+			{
+				return null;
+			}
+			
+			/* This can be overruled by not using the _MENU_ var/macro in the language variable */
+			var sName = 'name="'+oSettings.sTableId+'_length"';
+			var sStdMenu = '<select size="1" '+sName+'>';
+			var i, iLen;
+			var aLengthMenu = oSettings.aLengthMenu;
+			
+			if ( aLengthMenu.length == 2 && typeof aLengthMenu[0] === 'object' && 
+					typeof aLengthMenu[1] === 'object' )
+			{
+				for ( i=0, iLen=aLengthMenu[0].length ; i<iLen ; i++ )
+				{
+					sStdMenu += '<option value="'+aLengthMenu[0][i]+'">'+aLengthMenu[1][i]+'</option>';
+				}
+			}
+			else
+			{
+				for ( i=0, iLen=aLengthMenu.length ; i<iLen ; i++ )
+				{
+					sStdMenu += '<option value="'+aLengthMenu[i]+'">'+aLengthMenu[i]+'</option>';
+				}
+			}
+			sStdMenu += '</select>';
+			
+			var nLength = document.createElement( 'div' );
+			if ( !oSettings.aanFeatures.l )
+			{
+				nLength.id = oSettings.sTableId+'_length';
+			}
+			nLength.className = oSettings.oClasses.sLength;
+			nLength.innerHTML = '<label>'+oSettings.oLanguage.sLengthMenu.replace( '_MENU_', sStdMenu )+'</label>';
+			
+			/*
+			 * Set the length to the current display length - thanks to Andrea Pavlovic for this fix,
+			 * and Stefan Skopnik for fixing the fix!
+			 */
+			$('select option[value="'+oSettings._iDisplayLength+'"]', nLength).attr("selected", true);
+			
+			$('select', nLength).bind( 'change.DT', function(e) {
+				var iVal = $(this).val();
+				
+				/* Update all other length options for the new display */
+				var n = oSettings.aanFeatures.l;
+				for ( i=0, iLen=n.length ; i<iLen ; i++ )
+				{
+					if ( n[i] != this.parentNode )
+					{
+						$('select', n[i]).val( iVal );
+					}
+				}
+				
+				/* Redraw the table */
+				oSettings._iDisplayLength = parseInt(iVal, 10);
+				_fnCalculateEnd( oSettings );
+				
+				/* If we have space to show extra rows (backing up from the end point - then do so */
+				if ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() )
+				{
+					oSettings._iDisplayStart = oSettings.fnDisplayEnd() - oSettings._iDisplayLength;
+					if ( oSettings._iDisplayStart < 0 )
+					{
+						oSettings._iDisplayStart = 0;
+					}
+				}
+				
+				if ( oSettings._iDisplayLength == -1 )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+				
+				_fnDraw( oSettings );
+			} );
+		
+		
+			$('select', nLength).attr('aria-controls', oSettings.sTableId);
+			
+			return nLength;
+		}
+		
+		
+		/**
+		 * Recalculate the end point based on the start point
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCalculateEnd( oSettings )
+		{
+			if ( oSettings.oFeatures.bPaginate === false )
+			{
+				oSettings._iDisplayEnd = oSettings.aiDisplay.length;
+			}
+			else
+			{
+				/* Set the end point of the display - based on how many elements there are
+				 * still to display
+				 */
+				if ( oSettings._iDisplayStart + oSettings._iDisplayLength > oSettings.aiDisplay.length ||
+					   oSettings._iDisplayLength == -1 )
+				{
+					oSettings._iDisplayEnd = oSettings.aiDisplay.length;
+				}
+				else
+				{
+					oSettings._iDisplayEnd = oSettings._iDisplayStart + oSettings._iDisplayLength;
+				}
+			}
+		}
+		
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Note that most of the paging logic is done in 
+		 * DataTable.ext.oPagination
+		 */
+		
+		/**
+		 * Generate the node required for default pagination
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Pagination feature node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlPaginate ( oSettings )
+		{
+			if ( oSettings.oScroll.bInfinite )
+			{
+				return null;
+			}
+			
+			var nPaginate = document.createElement( 'div' );
+			nPaginate.className = oSettings.oClasses.sPaging+oSettings.sPaginationType;
+			
+			DataTable.ext.oPagination[ oSettings.sPaginationType ].fnInit( oSettings, nPaginate, 
+				function( oSettings ) {
+					_fnCalculateEnd( oSettings );
+					_fnDraw( oSettings );
+				}
+			);
+			
+			/* Add a draw callback for the pagination on first instance, to update the paging display */
+			if ( !oSettings.aanFeatures.p )
+			{
+				oSettings.aoDrawCallback.push( {
+					"fn": function( oSettings ) {
+						DataTable.ext.oPagination[ oSettings.sPaginationType ].fnUpdate( oSettings, function( oSettings ) {
+							_fnCalculateEnd( oSettings );
+							_fnDraw( oSettings );
+						} );
+					},
+					"sName": "pagination"
+				} );
+			}
+			return nPaginate;
+		}
+		
+		
+		/**
+		 * Alter the display settings to change the page
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+		 *    or page number to jump to (integer)
+		 *  @returns {bool} true page has changed, false - no change (no effect) eg 'first' on page 1
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnPageChange ( oSettings, mAction )
+		{
+			var iOldStart = oSettings._iDisplayStart;
+			
+			if ( typeof mAction === "number" )
+			{
+				oSettings._iDisplayStart = mAction * oSettings._iDisplayLength;
+				if ( oSettings._iDisplayStart > oSettings.fnRecordsDisplay() )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "first" )
+			{
+				oSettings._iDisplayStart = 0;
+			}
+			else if ( mAction == "previous" )
+			{
+				oSettings._iDisplayStart = oSettings._iDisplayLength>=0 ?
+					oSettings._iDisplayStart - oSettings._iDisplayLength :
+					0;
+				
+				/* Correct for under-run */
+				if ( oSettings._iDisplayStart < 0 )
+				{
+				  oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "next" )
+			{
+				if ( oSettings._iDisplayLength >= 0 )
+				{
+					/* Make sure we are not over running the display array */
+					if ( oSettings._iDisplayStart + oSettings._iDisplayLength < oSettings.fnRecordsDisplay() )
+					{
+						oSettings._iDisplayStart += oSettings._iDisplayLength;
+					}
+				}
+				else
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "last" )
+			{
+				if ( oSettings._iDisplayLength >= 0 )
+				{
+					var iPages = parseInt( (oSettings.fnRecordsDisplay()-1) / oSettings._iDisplayLength, 10 ) + 1;
+					oSettings._iDisplayStart = (iPages-1) * oSettings._iDisplayLength;
+				}
+				else
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else
+			{
+				_fnLog( oSettings, 0, "Unknown paging action: "+mAction );
+			}
+			$(oSettings.oInstance).trigger('page', oSettings);
+			
+			return iOldStart != oSettings._iDisplayStart;
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for the processing node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Processing element
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlProcessing ( oSettings )
+		{
+			var nProcessing = document.createElement( 'div' );
+			
+			if ( !oSettings.aanFeatures.r )
+			{
+				nProcessing.id = oSettings.sTableId+'_processing';
+			}
+			nProcessing.innerHTML = oSettings.oLanguage.sProcessing;
+			nProcessing.className = oSettings.oClasses.sProcessing;
+			oSettings.nTable.parentNode.insertBefore( nProcessing, oSettings.nTable );
+			
+			return nProcessing;
+		}
+		
+		
+		/**
+		 * Display or hide the processing indicator
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {bool} bShow Show the processing indicator (true) or not (false)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnProcessingDisplay ( oSettings, bShow )
+		{
+			if ( oSettings.oFeatures.bProcessing )
+			{
+				var an = oSettings.aanFeatures.r;
+				for ( var i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					an[i].style.visibility = bShow ? "visible" : "hidden";
+				}
+			}
+		
+			$(oSettings.oInstance).trigger('processing', [oSettings, bShow]);
+		}
+		
+		/**
+		 * Add any control elements for the table - specifically scrolling
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Node to add to the DOM
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlTable ( oSettings )
+		{
+			/* Check if scrolling is enabled or not - if not then leave the DOM unaltered */
+			if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "" )
+			{
+				return oSettings.nTable;
+			}
+			
+			/*
+			 * The HTML structure that we want to generate in this function is:
+			 *  div - nScroller
+			 *    div - nScrollHead
+			 *      div - nScrollHeadInner
+			 *        table - nScrollHeadTable
+			 *          thead - nThead
+			 *    div - nScrollBody
+			 *      table - oSettings.nTable
+			 *        thead - nTheadSize
+			 *        tbody - nTbody
+			 *    div - nScrollFoot
+			 *      div - nScrollFootInner
+			 *        table - nScrollFootTable
+			 *          tfoot - nTfoot
+			 */
+			var
+			 	nScroller = document.createElement('div'),
+			 	nScrollHead = document.createElement('div'),
+			 	nScrollHeadInner = document.createElement('div'),
+			 	nScrollBody = document.createElement('div'),
+			 	nScrollFoot = document.createElement('div'),
+			 	nScrollFootInner = document.createElement('div'),
+			 	nScrollHeadTable = oSettings.nTable.cloneNode(false),
+			 	nScrollFootTable = oSettings.nTable.cloneNode(false),
+				nThead = oSettings.nTable.getElementsByTagName('thead')[0],
+			 	nTfoot = oSettings.nTable.getElementsByTagName('tfoot').length === 0 ? null : 
+					oSettings.nTable.getElementsByTagName('tfoot')[0],
+				oClasses = oSettings.oClasses;
+			
+			nScrollHead.appendChild( nScrollHeadInner );
+			nScrollFoot.appendChild( nScrollFootInner );
+			nScrollBody.appendChild( oSettings.nTable );
+			nScroller.appendChild( nScrollHead );
+			nScroller.appendChild( nScrollBody );
+			nScrollHeadInner.appendChild( nScrollHeadTable );
+			nScrollHeadTable.appendChild( nThead );
+			if ( nTfoot !== null )
+			{
+				nScroller.appendChild( nScrollFoot );
+				nScrollFootInner.appendChild( nScrollFootTable );
+				nScrollFootTable.appendChild( nTfoot );
+			}
+			
+			nScroller.className = oClasses.sScrollWrapper;
+			nScrollHead.className = oClasses.sScrollHead;
+			nScrollHeadInner.className = oClasses.sScrollHeadInner;
+			nScrollBody.className = oClasses.sScrollBody;
+			nScrollFoot.className = oClasses.sScrollFoot;
+			nScrollFootInner.className = oClasses.sScrollFootInner;
+			
+			if ( oSettings.oScroll.bAutoCss )
+			{
+				nScrollHead.style.overflow = "hidden";
+				nScrollHead.style.position = "relative";
+				nScrollFoot.style.overflow = "hidden";
+				nScrollBody.style.overflow = "auto";
+			}
+			
+			nScrollHead.style.border = "0";
+			nScrollHead.style.width = "100%";
+			nScrollFoot.style.border = "0";
+			nScrollHeadInner.style.width = oSettings.oScroll.sXInner !== "" ?
+				oSettings.oScroll.sXInner : "100%"; /* will be overwritten */
+			
+			/* Modify attributes to respect the clones */
+			nScrollHeadTable.removeAttribute('id');
+			nScrollHeadTable.style.marginLeft = "0";
+			oSettings.nTable.style.marginLeft = "0";
+			if ( nTfoot !== null )
+			{
+				nScrollFootTable.removeAttribute('id');
+				nScrollFootTable.style.marginLeft = "0";
+			}
+			
+			/* Move caption elements from the body to the header, footer or leave where it is
+			 * depending on the configuration. Note that the DTD says there can be only one caption */
+			var nCaption = $(oSettings.nTable).children('caption');
+			if ( nCaption.length > 0 )
+			{
+				nCaption = nCaption[0];
+				if ( nCaption._captionSide === "top" )
+				{
+					nScrollHeadTable.appendChild( nCaption );
+				}
+				else if ( nCaption._captionSide === "bottom" && nTfoot )
+				{
+					nScrollFootTable.appendChild( nCaption );
+				}
+			}
+			
+			/*
+			 * Sizing
+			 */
+			/* When x-scrolling add the width and a scroller to move the header with the body */
+			if ( oSettings.oScroll.sX !== "" )
+			{
+				nScrollHead.style.width = _fnStringToCss( oSettings.oScroll.sX );
+				nScrollBody.style.width = _fnStringToCss( oSettings.oScroll.sX );
+				
+				if ( nTfoot !== null )
+				{
+					nScrollFoot.style.width = _fnStringToCss( oSettings.oScroll.sX );	
+				}
+				
+				/* When the body is scrolled, then we also want to scroll the headers */
+				$(nScrollBody).scroll( function (e) {
+					nScrollHead.scrollLeft = this.scrollLeft;
+					
+					if ( nTfoot !== null )
+					{
+						nScrollFoot.scrollLeft = this.scrollLeft;
+					}
+				} );
+			}
+			
+			/* When yscrolling, add the height */
+			if ( oSettings.oScroll.sY !== "" )
+			{
+				nScrollBody.style.height = _fnStringToCss( oSettings.oScroll.sY );
+			}
+			
+			/* Redraw - align columns across the tables */
+			oSettings.aoDrawCallback.push( {
+				"fn": _fnScrollDraw,
+				"sName": "scrolling"
+			} );
+			
+			/* Infinite scrolling event handlers */
+			if ( oSettings.oScroll.bInfinite )
+			{
+				$(nScrollBody).scroll( function() {
+					/* Use a blocker to stop scrolling from loading more data while other data is still loading */
+					if ( !oSettings.bDrawing && $(this).scrollTop() !== 0 )
+					{
+						/* Check if we should load the next data set */
+						if ( $(this).scrollTop() + $(this).height() > 
+							$(oSettings.nTable).height() - oSettings.oScroll.iLoadGap )
+						{
+							/* Only do the redraw if we have to - we might be at the end of the data */
+							if ( oSettings.fnDisplayEnd() < oSettings.fnRecordsDisplay() )
+							{
+								_fnPageChange( oSettings, 'next' );
+								_fnCalculateEnd( oSettings );
+								_fnDraw( oSettings );
+							}
+						}
+					}
+				} );
+			}
+			
+			oSettings.nScrollHead = nScrollHead;
+			oSettings.nScrollFoot = nScrollFoot;
+			
+			return nScroller;
+		}
+		
+		
+		/**
+		 * Update the various tables for resizing. It's a bit of a pig this function, but
+		 * basically the idea to:
+		 *   1. Re-create the table inside the scrolling div
+		 *   2. Take live measurements from the DOM
+		 *   3. Apply the measurements
+		 *   4. Clean up
+		 *  @param {object} o dataTables settings object
+		 *  @returns {node} Node to add to the DOM
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollDraw ( o )
+		{
+			var
+				nScrollHeadInner = o.nScrollHead.getElementsByTagName('div')[0],
+				nScrollHeadTable = nScrollHeadInner.getElementsByTagName('table')[0],
+				nScrollBody = o.nTable.parentNode,
+				i, iLen, j, jLen, anHeadToSize, anHeadSizers, anFootSizers, anFootToSize, oStyle, iVis,
+				nTheadSize, nTfootSize,
+				iWidth, aApplied=[], aAppliedFooter=[], iSanityWidth,
+				nScrollFootInner = (o.nTFoot !== null) ? o.nScrollFoot.getElementsByTagName('div')[0] : null,
+				nScrollFootTable = (o.nTFoot !== null) ? nScrollFootInner.getElementsByTagName('table')[0] : null,
+				ie67 = o.oBrowser.bScrollOversize,
+				zeroOut = function(nSizer) {
+					oStyle = nSizer.style;
+					oStyle.paddingTop = "0";
+					oStyle.paddingBottom = "0";
+					oStyle.borderTopWidth = "0";
+					oStyle.borderBottomWidth = "0";
+					oStyle.height = 0;
+				};
+			
+			/*
+			 * 1. Re-create the table inside the scrolling div
+			 */
+			
+			/* Remove the old minimised thead and tfoot elements in the inner table */
+			$(o.nTable).children('thead, tfoot').remove();
+		
+			/* Clone the current header and footer elements and then place it into the inner table */
+			nTheadSize = $(o.nTHead).clone()[0];
+			o.nTable.insertBefore( nTheadSize, o.nTable.childNodes[0] );
+			anHeadToSize = o.nTHead.getElementsByTagName('tr');
+			anHeadSizers = nTheadSize.getElementsByTagName('tr');
+			
+			if ( o.nTFoot !== null )
+			{
+				nTfootSize = $(o.nTFoot).clone()[0];
+				o.nTable.insertBefore( nTfootSize, o.nTable.childNodes[1] );
+				anFootToSize = o.nTFoot.getElementsByTagName('tr');
+				anFootSizers = nTfootSize.getElementsByTagName('tr');
+			}
+			
+			/*
+			 * 2. Take live measurements from the DOM - do not alter the DOM itself!
+			 */
+			
+			/* Remove old sizing and apply the calculated column widths
+			 * Get the unique column headers in the newly created (cloned) header. We want to apply the
+			 * calculated sizes to this header
+			 */
+			if ( o.oScroll.sX === "" )
+			{
+				nScrollBody.style.width = '100%';
+				nScrollHeadInner.parentNode.style.width = '100%';
+			}
+			
+			var nThs = _fnGetUniqueThs( o, nTheadSize );
+			for ( i=0, iLen=nThs.length ; i<iLen ; i++ )
+			{
+				iVis = _fnVisibleToColumnIndex( o, i );
+				nThs[i].style.width = o.aoColumns[iVis].sWidth;
+			}
+			
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( function(n) {
+					n.style.width = "";
+				}, anFootSizers );
+			}
+		
+			// If scroll collapse is enabled, when we put the headers back into the body for sizing, we
+			// will end up forcing the scrollbar to appear, making our measurements wrong for when we
+			// then hide it (end of this function), so add the header height to the body scroller.
+			if ( o.oScroll.bCollapse && o.oScroll.sY !== "" )
+			{
+				nScrollBody.style.height = (nScrollBody.offsetHeight + o.nTHead.offsetHeight)+"px";
+			}
+			
+			/* Size the table as a whole */
+			iSanityWidth = $(o.nTable).outerWidth();
+			if ( o.oScroll.sX === "" )
+			{
+				/* No x scrolling */
+				o.nTable.style.width = "100%";
+				
+				/* I know this is rubbish - but IE7 will make the width of the table when 100% include
+				 * the scrollbar - which is shouldn't. When there is a scrollbar we need to take this
+				 * into account.
+				 */
+				if ( ie67 && ($('tbody', nScrollBody).height() > nScrollBody.offsetHeight || 
+					$(nScrollBody).css('overflow-y') == "scroll")  )
+				{
+					o.nTable.style.width = _fnStringToCss( $(o.nTable).outerWidth() - o.oScroll.iBarWidth);
+				}
+			}
+			else
+			{
+				if ( o.oScroll.sXInner !== "" )
+				{
+					/* x scroll inner has been given - use it */
+					o.nTable.style.width = _fnStringToCss(o.oScroll.sXInner);
+				}
+				else if ( iSanityWidth == $(nScrollBody).width() &&
+				   $(nScrollBody).height() < $(o.nTable).height() )
+				{
+					/* There is y-scrolling - try to take account of the y scroll bar */
+					o.nTable.style.width = _fnStringToCss( iSanityWidth-o.oScroll.iBarWidth );
+					if ( $(o.nTable).outerWidth() > iSanityWidth-o.oScroll.iBarWidth )
+					{
+						/* Not possible to take account of it */
+						o.nTable.style.width = _fnStringToCss( iSanityWidth );
+					}
+				}
+				else
+				{
+					/* All else fails */
+					o.nTable.style.width = _fnStringToCss( iSanityWidth );
+				}
+			}
+			
+			/* Recalculate the sanity width - now that we've applied the required width, before it was
+			 * a temporary variable. This is required because the column width calculation is done
+			 * before this table DOM is created.
+			 */
+			iSanityWidth = $(o.nTable).outerWidth();
+			
+			/* We want the hidden header to have zero height, so remove padding and borders. Then
+			 * set the width based on the real headers
+			 */
+			
+			// Apply all styles in one pass. Invalidates layout only once because we don't read any 
+			// DOM properties.
+			_fnApplyToChildren( zeroOut, anHeadSizers );
+			 
+			// Read all widths in next pass. Forces layout only once because we do not change 
+			// any DOM properties.
+			_fnApplyToChildren( function(nSizer) {
+				aApplied.push( _fnStringToCss( $(nSizer).width() ) );
+			}, anHeadSizers );
+			 
+			// Apply all widths in final pass. Invalidates layout only once because we do not
+			// read any DOM properties.
+			_fnApplyToChildren( function(nToSize, i) {
+				nToSize.style.width = aApplied[i];
+			}, anHeadToSize );
+		
+			$(anHeadSizers).height(0);
+			
+			/* Same again with the footer if we have one */
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( zeroOut, anFootSizers );
+				 
+				_fnApplyToChildren( function(nSizer) {
+					aAppliedFooter.push( _fnStringToCss( $(nSizer).width() ) );
+				}, anFootSizers );
+				 
+				_fnApplyToChildren( function(nToSize, i) {
+					nToSize.style.width = aAppliedFooter[i];
+				}, anFootToSize );
+		
+				$(anFootSizers).height(0);
+			}
+			
+			/*
+			 * 3. Apply the measurements
+			 */
+			
+			/* "Hide" the header and footer that we used for the sizing. We want to also fix their width
+			 * to what they currently are
+			 */
+			_fnApplyToChildren( function(nSizer, i) {
+				nSizer.innerHTML = "";
+				nSizer.style.width = aApplied[i];
+			}, anHeadSizers );
+			
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( function(nSizer, i) {
+					nSizer.innerHTML = "";
+					nSizer.style.width = aAppliedFooter[i];
+				}, anFootSizers );
+			}
+			
+			/* Sanity check that the table is of a sensible width. If not then we are going to get
+			 * misalignment - try to prevent this by not allowing the table to shrink below its min width
+			 */
+			if ( $(o.nTable).outerWidth() < iSanityWidth )
+			{
+				/* The min width depends upon if we have a vertical scrollbar visible or not */
+				var iCorrection = ((nScrollBody.scrollHeight > nScrollBody.offsetHeight || 
+					$(nScrollBody).css('overflow-y') == "scroll")) ?
+						iSanityWidth+o.oScroll.iBarWidth : iSanityWidth;
+				
+				/* IE6/7 are a law unto themselves... */
+				if ( ie67 && (nScrollBody.scrollHeight > 
+					nScrollBody.offsetHeight || $(nScrollBody).css('overflow-y') == "scroll")  )
+				{
+					o.nTable.style.width = _fnStringToCss( iCorrection-o.oScroll.iBarWidth );
+				}
+				
+				/* Apply the calculated minimum width to the table wrappers */
+				nScrollBody.style.width = _fnStringToCss( iCorrection );
+				o.nScrollHead.style.width = _fnStringToCss( iCorrection );
+				
+				if ( o.nTFoot !== null )
+				{
+					o.nScrollFoot.style.width = _fnStringToCss( iCorrection );
+				}
+				
+				/* And give the user a warning that we've stopped the table getting too small */
+				if ( o.oScroll.sX === "" )
+				{
+					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
+						" misalignment. The table has been drawn at its minimum possible width." );
+				}
+				else if ( o.oScroll.sXInner !== "" )
+				{
+					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
+						" misalignment. Increase the sScrollXInner value or remove it to allow automatic"+
+						" calculation" );
+				}
+			}
+			else
+			{
+				nScrollBody.style.width = _fnStringToCss( '100%' );
+				o.nScrollHead.style.width = _fnStringToCss( '100%' );
+				
+				if ( o.nTFoot !== null )
+				{
+					o.nScrollFoot.style.width = _fnStringToCss( '100%' );
+				}
+			}
+			
+			
+			/*
+			 * 4. Clean up
+			 */
+			if ( o.oScroll.sY === "" )
+			{
+				/* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting
+				 * the scrollbar height from the visible display, rather than adding it on. We need to
+				 * set the height in order to sort this. Don't want to do it in any other browsers.
+				 */
+				if ( ie67 )
+				{
+					nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+o.oScroll.iBarWidth );
+				}
+			}
+			
+			if ( o.oScroll.sY !== "" && o.oScroll.bCollapse )
+			{
+				nScrollBody.style.height = _fnStringToCss( o.oScroll.sY );
+				
+				var iExtra = (o.oScroll.sX !== "" && o.nTable.offsetWidth > nScrollBody.offsetWidth) ?
+				 	o.oScroll.iBarWidth : 0;
+				if ( o.nTable.offsetHeight < nScrollBody.offsetHeight )
+				{
+					nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+iExtra );
+				}
+			}
+			
+			/* Finally set the width's of the header and footer tables */
+			var iOuterWidth = $(o.nTable).outerWidth();
+			nScrollHeadTable.style.width = _fnStringToCss( iOuterWidth );
+			nScrollHeadInner.style.width = _fnStringToCss( iOuterWidth );
+		
+			// Figure out if there are scrollbar present - if so then we need a the header and footer to
+			// provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar)
+			var bScrolling = $(o.nTable).height() > nScrollBody.clientHeight || $(nScrollBody).css('overflow-y') == "scroll";
+			nScrollHeadInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px";
+			
+			if ( o.nTFoot !== null )
+			{
+				nScrollFootTable.style.width = _fnStringToCss( iOuterWidth );
+				nScrollFootInner.style.width = _fnStringToCss( iOuterWidth );
+				nScrollFootInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px";
+			}
+		
+			/* Adjust the position of the header in case we loose the y-scrollbar */
+			$(nScrollBody).scroll();
+			
+			/* If sorting or filtering has occurred, jump the scrolling back to the top */
+			if ( o.bSorted || o.bFiltered )
+			{
+				nScrollBody.scrollTop = 0;
+			}
+		}
+		
+		
+		/**
+		 * Apply a given function to the display child nodes of an element array (typically
+		 * TD children of TR rows
+		 *  @param {function} fn Method to apply to the objects
+		 *  @param array {nodes} an1 List of elements to look through for display children
+		 *  @param array {nodes} an2 Another list (identical structure to the first) - optional
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnApplyToChildren( fn, an1, an2 )
+		{
+			var index=0, i=0, iLen=an1.length;
+			var nNode1, nNode2;
+		
+			while ( i < iLen )
+			{
+				nNode1 = an1[i].firstChild;
+				nNode2 = an2 ? an2[i].firstChild : null;
+				while ( nNode1 )
+				{
+					if ( nNode1.nodeType === 1 )
+					{
+						if ( an2 )
+						{
+							fn( nNode1, nNode2, index );
+						}
+						else
+						{
+							fn( nNode1, index );
+						}
+						index++;
+					}
+					nNode1 = nNode1.nextSibling;
+					nNode2 = an2 ? nNode2.nextSibling : null;
+				}
+				i++;
+			}
+		}
+		
+		/**
+		 * Convert a CSS unit width to pixels (e.g. 2em)
+		 *  @param {string} sWidth width to be converted
+		 *  @param {node} nParent parent to get the with for (required for relative widths) - optional
+		 *  @returns {int} iWidth width in pixels
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnConvertToWidth ( sWidth, nParent )
+		{
+			if ( !sWidth || sWidth === null || sWidth === '' )
+			{
+				return 0;
+			}
+			
+			if ( !nParent )
+			{
+				nParent = document.body;
+			}
+			
+			var iWidth;
+			var nTmp = document.createElement( "div" );
+			nTmp.style.width = _fnStringToCss( sWidth );
+			
+			nParent.appendChild( nTmp );
+			iWidth = nTmp.offsetWidth;
+			nParent.removeChild( nTmp );
+			
+			return ( iWidth );
+		}
+		
+		
+		/**
+		 * Calculate the width of columns for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCalculateColumnWidths ( oSettings )
+		{
+			var iTableWidth = oSettings.nTable.offsetWidth;
+			var iUserInputs = 0;
+			var iTmpWidth;
+			var iVisibleColumns = 0;
+			var iColums = oSettings.aoColumns.length;
+			var i, iIndex, iCorrector, iWidth;
+			var oHeaders = $('th', oSettings.nTHead);
+			var widthAttr = oSettings.nTable.getAttribute('width');
+			var nWrapper = oSettings.nTable.parentNode;
+			
+			/* Convert any user input sizes into pixel sizes */
+			for ( i=0 ; i<iColums ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bVisible )
+				{
+					iVisibleColumns++;
+					
+					if ( oSettings.aoColumns[i].sWidth !== null )
+					{
+						iTmpWidth = _fnConvertToWidth( oSettings.aoColumns[i].sWidthOrig, 
+							nWrapper );
+						if ( iTmpWidth !== null )
+						{
+							oSettings.aoColumns[i].sWidth = _fnStringToCss( iTmpWidth );
+						}
+							
+						iUserInputs++;
+					}
+				}
+			}
+			
+			/* If the number of columns in the DOM equals the number that we have to process in 
+			 * DataTables, then we can use the offsets that are created by the web-browser. No custom 
+			 * sizes can be set in order for this to happen, nor scrolling used
+			 */
+			if ( iColums == oHeaders.length && iUserInputs === 0 && iVisibleColumns == iColums &&
+				oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "" )
+			{
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					iTmpWidth = $(oHeaders[i]).width();
+					if ( iTmpWidth !== null )
+					{
+						oSettings.aoColumns[i].sWidth = _fnStringToCss( iTmpWidth );
+					}
+				}
+			}
+			else
+			{
+				/* Otherwise we are going to have to do some calculations to get the width of each column.
+				 * Construct a 1 row table with the widest node in the data, and any user defined widths,
+				 * then insert it into the DOM and allow the browser to do all the hard work of
+				 * calculating table widths.
+				 */
+				var
+					nCalcTmp = oSettings.nTable.cloneNode( false ),
+					nTheadClone = oSettings.nTHead.cloneNode(true),
+					nBody = document.createElement( 'tbody' ),
+					nTr = document.createElement( 'tr' ),
+					nDivSizing;
+				
+				nCalcTmp.removeAttribute( "id" );
+				nCalcTmp.appendChild( nTheadClone );
+				if ( oSettings.nTFoot !== null )
+				{
+					nCalcTmp.appendChild( oSettings.nTFoot.cloneNode(true) );
+					_fnApplyToChildren( function(n) {
+						n.style.width = "";
+					}, nCalcTmp.getElementsByTagName('tr') );
+				}
+				
+				nCalcTmp.appendChild( nBody );
+				nBody.appendChild( nTr );
+				
+				/* Remove any sizing that was previously applied by the styles */
+				var jqColSizing = $('thead th', nCalcTmp);
+				if ( jqColSizing.length === 0 )
+				{
+					jqColSizing = $('tbody tr:eq(0)>td', nCalcTmp);
+				}
+		
+				/* Apply custom sizing to the cloned header */
+				var nThs = _fnGetUniqueThs( oSettings, nTheadClone );
+				iCorrector = 0;
+				for ( i=0 ; i<iColums ; i++ )
+				{
+					var oColumn = oSettings.aoColumns[i];
+					if ( oColumn.bVisible && oColumn.sWidthOrig !== null && oColumn.sWidthOrig !== "" )
+					{
+						nThs[i-iCorrector].style.width = _fnStringToCss( oColumn.sWidthOrig );
+					}
+					else if ( oColumn.bVisible )
+					{
+						nThs[i-iCorrector].style.width = "";
+					}
+					else
+					{
+						iCorrector++;
+					}
+				}
+		
+				/* Find the biggest td for each column and put it into the table */
+				for ( i=0 ; i<iColums ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						var nTd = _fnGetWidestNode( oSettings, i );
+						if ( nTd !== null )
+						{
+							nTd = nTd.cloneNode(true);
+							if ( oSettings.aoColumns[i].sContentPadding !== "" )
+							{
+								nTd.innerHTML += oSettings.aoColumns[i].sContentPadding;
+							}
+							nTr.appendChild( nTd );
+						}
+					}
+				}
+				
+				/* Build the table and 'display' it */
+				nWrapper.appendChild( nCalcTmp );
+				
+				/* When scrolling (X or Y) we want to set the width of the table as appropriate. However,
+				 * when not scrolling leave the table width as it is. This results in slightly different,
+				 * but I think correct behaviour
+				 */
+				if ( oSettings.oScroll.sX !== "" && oSettings.oScroll.sXInner !== "" )
+				{
+					nCalcTmp.style.width = _fnStringToCss(oSettings.oScroll.sXInner);
+				}
+				else if ( oSettings.oScroll.sX !== "" )
+				{
+					nCalcTmp.style.width = "";
+					if ( $(nCalcTmp).width() < nWrapper.offsetWidth )
+					{
+						nCalcTmp.style.width = _fnStringToCss( nWrapper.offsetWidth );
+					}
+				}
+				else if ( oSettings.oScroll.sY !== "" )
+				{
+					nCalcTmp.style.width = _fnStringToCss( nWrapper.offsetWidth );
+				}
+				else if ( widthAttr )
+				{
+					nCalcTmp.style.width = _fnStringToCss( widthAttr );
+				}
+				nCalcTmp.style.visibility = "hidden";
+				
+				/* Scrolling considerations */
+				_fnScrollingWidthAdjust( oSettings, nCalcTmp );
+				
+				/* Read the width's calculated by the browser and store them for use by the caller. We
+				 * first of all try to use the elements in the body, but it is possible that there are
+				 * no elements there, under which circumstances we use the header elements
+				 */
+				var oNodes = $("tbody tr:eq(0)", nCalcTmp).children();
+				if ( oNodes.length === 0 )
+				{
+					oNodes = _fnGetUniqueThs( oSettings, $('thead', nCalcTmp)[0] );
+				}
+		
+				/* Browsers need a bit of a hand when a width is assigned to any columns when 
+				 * x-scrolling as they tend to collapse the table to the min-width, even if
+				 * we sent the column widths. So we need to keep track of what the table width
+				 * should be by summing the user given values, and the automatic values
+				 */
+				if ( oSettings.oScroll.sX !== "" )
+				{
+					var iTotal = 0;
+					iCorrector = 0;
+					for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+					{
+						if ( oSettings.aoColumns[i].bVisible )
+						{
+							if ( oSettings.aoColumns[i].sWidthOrig === null )
+							{
+								iTotal += $(oNodes[iCorrector]).outerWidth();
+							}
+							else
+							{
+								iTotal += parseInt(oSettings.aoColumns[i].sWidth.replace('px',''), 10) +
+									($(oNodes[iCorrector]).outerWidth() - $(oNodes[iCorrector]).width());
+							}
+							iCorrector++;
+						}
+					}
+					
+					nCalcTmp.style.width = _fnStringToCss( iTotal );
+					oSettings.nTable.style.width = _fnStringToCss( iTotal );
+				}
+		
+				iCorrector = 0;
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						iWidth = $(oNodes[iCorrector]).width();
+						if ( iWidth !== null && iWidth > 0 )
+						{
+							oSettings.aoColumns[i].sWidth = _fnStringToCss( iWidth );
+						}
+						iCorrector++;
+					}
+				}
+		
+				var cssWidth = $(nCalcTmp).css('width');
+				oSettings.nTable.style.width = (cssWidth.indexOf('%') !== -1) ?
+				    cssWidth : _fnStringToCss( $(nCalcTmp).outerWidth() );
+				nCalcTmp.parentNode.removeChild( nCalcTmp );
+			}
+		
+			if ( widthAttr )
+			{
+				oSettings.nTable.style.width = _fnStringToCss( widthAttr );
+			}
+		}
+		
+		
+		/**
+		 * Adjust a table's width to take account of scrolling
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} n table node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollingWidthAdjust ( oSettings, n )
+		{
+			if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY !== "" )
+			{
+				/* When y-scrolling only, we want to remove the width of the scroll bar so the table
+				 * + scroll bar will fit into the area avaialble.
+				 */
+				var iOrigWidth = $(n).width();
+				n.style.width = _fnStringToCss( $(n).outerWidth()-oSettings.oScroll.iBarWidth );
+			}
+			else if ( oSettings.oScroll.sX !== "" )
+			{
+				/* When x-scrolling both ways, fix the table at it's current size, without adjusting */
+				n.style.width = _fnStringToCss( $(n).outerWidth() );
+			}
+		}
+		
+		
+		/**
+		 * Get the widest node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column of interest
+		 *  @returns {node} widest table node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetWidestNode( oSettings, iCol )
+		{
+			var iMaxIndex = _fnGetMaxLenString( oSettings, iCol );
+			if ( iMaxIndex < 0 )
+			{
+				return null;
+			}
+		
+			if ( oSettings.aoData[iMaxIndex].nTr === null )
+			{
+				var n = document.createElement('td');
+				n.innerHTML = _fnGetCellData( oSettings, iMaxIndex, iCol, '' );
+				return n;
+			}
+			return _fnGetTdNodes(oSettings, iMaxIndex)[iCol];
+		}
+		
+		
+		/**
+		 * Get the maximum strlen for each data column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column of interest
+		 *  @returns {string} max string length for each column
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetMaxLenString( oSettings, iCol )
+		{
+			var iMax = -1;
+			var iMaxIndex = -1;
+			
+			for ( var i=0 ; i<oSettings.aoData.length ; i++ )
+			{
+				var s = _fnGetCellData( oSettings, i, iCol, 'display' )+"";
+				s = s.replace( /<.*?>/g, "" );
+				if ( s.length > iMax )
+				{
+					iMax = s.length;
+					iMaxIndex = i;
+				}
+			}
+			
+			return iMaxIndex;
+		}
+		
+		
+		/**
+		 * Append a CSS unit (only if required) to a string
+		 *  @param {array} aArray1 first array
+		 *  @param {array} aArray2 second array
+		 *  @returns {int} 0 if match, 1 if length is different, 2 if no match
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnStringToCss( s )
+		{
+			if ( s === null )
+			{
+				return "0px";
+			}
+			
+			if ( typeof s == 'number' )
+			{
+				if ( s < 0 )
+				{
+					return "0px";
+				}
+				return s+"px";
+			}
+			
+			/* Check if the last character is not 0-9 */
+			var c = s.charCodeAt( s.length-1 );
+			if (c < 0x30 || c > 0x39)
+			{
+				return s;
+			}
+			return s+"px";
+		}
+		
+		
+		/**
+		 * Get the width of a scroll bar in this browser being used
+		 *  @returns {int} width in pixels
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollBarWidth ()
+		{  
+			var inner = document.createElement('p');
+			var style = inner.style;
+			style.width = "100%";
+			style.height = "200px";
+			style.padding = "0px";
+			
+			var outer = document.createElement('div');
+			style = outer.style;
+			style.position = "absolute";
+			style.top = "0px";
+			style.left = "0px";
+			style.visibility = "hidden";
+			style.width = "200px";
+			style.height = "150px";
+			style.padding = "0px";
+			style.overflow = "hidden";
+			outer.appendChild(inner);
+			
+			document.body.appendChild(outer);
+			var w1 = inner.offsetWidth;
+			outer.style.overflow = 'scroll';
+			var w2 = inner.offsetWidth;
+			if ( w1 == w2 )
+			{
+				w2 = outer.clientWidth;
+			}
+			
+			document.body.removeChild(outer);
+			return (w1 - w2);  
+		}
+		
+		/**
+		 * Change the order of the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {bool} bApplyClasses optional - should we apply classes or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSort ( oSettings, bApplyClasses )
+		{
+			var
+				i, iLen, j, jLen, k, kLen,
+				sDataType, nTh,
+				aaSort = [],
+			 	aiOrig = [],
+				oSort = DataTable.ext.oSort,
+				aoData = oSettings.aoData,
+				aoColumns = oSettings.aoColumns,
+				oAria = oSettings.oLanguage.oAria;
+			
+			/* No sorting required if server-side or no sorting array */
+			if ( !oSettings.oFeatures.bServerSide && 
+				(oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null) )
+			{
+				aaSort = ( oSettings.aaSortingFixed !== null ) ?
+					oSettings.aaSortingFixed.concat( oSettings.aaSorting ) :
+					oSettings.aaSorting.slice();
+				
+				/* If there is a sorting data type, and a function belonging to it, then we need to
+				 * get the data from the developer's function and apply it for this column
+				 */
+				for ( i=0 ; i<aaSort.length ; i++ )
+				{
+					var iColumn = aaSort[i][0];
+					var iVisColumn = _fnColumnIndexToVisible( oSettings, iColumn );
+					sDataType = oSettings.aoColumns[ iColumn ].sSortDataType;
+					if ( DataTable.ext.afnSortData[sDataType] )
+					{
+						var aData = DataTable.ext.afnSortData[sDataType].call( 
+							oSettings.oInstance, oSettings, iColumn, iVisColumn
+						);
+						if ( aData.length === aoData.length )
+						{
+							for ( j=0, jLen=aoData.length ; j<jLen ; j++ )
+							{
+								_fnSetCellData( oSettings, j, iColumn, aData[j] );
+							}
+						}
+						else
+						{
+							_fnLog( oSettings, 0, "Returned data sort array (col "+iColumn+") is the wrong length" );
+						}
+					}
+				}
+				
+				/* Create a value - key array of the current row positions such that we can use their
+				 * current position during the sort, if values match, in order to perform stable sorting
+				 */
+				for ( i=0, iLen=oSettings.aiDisplayMaster.length ; i<iLen ; i++ )
+				{
+					aiOrig[ oSettings.aiDisplayMaster[i] ] = i;
+				}
+		
+				/* Build an internal data array which is specific to the sort, so we can get and prep
+				 * the data to be sorted only once, rather than needing to do it every time the sorting
+				 * function runs. This make the sorting function a very simple comparison
+				 */
+				var iSortLen = aaSort.length;
+				var fnSortFormat, aDataSort;
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					for ( j=0 ; j<iSortLen ; j++ )
+					{
+						aDataSort = aoColumns[ aaSort[j][0] ].aDataSort;
+		
+						for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ )
+						{
+							sDataType = aoColumns[ aDataSort[k] ].sType;
+							fnSortFormat = oSort[ (sDataType ? sDataType : 'string')+"-pre" ];
+							
+							aoData[i]._aSortData[ aDataSort[k] ] = fnSortFormat ?
+								fnSortFormat( _fnGetCellData( oSettings, i, aDataSort[k], 'sort' ) ) :
+								_fnGetCellData( oSettings, i, aDataSort[k], 'sort' );
+						}
+					}
+				}
+				
+				/* Do the sort - here we want multi-column sorting based on a given data source (column)
+				 * and sorting function (from oSort) in a certain direction. It's reasonably complex to
+				 * follow on it's own, but this is what we want (example two column sorting):
+				 *  fnLocalSorting = function(a,b){
+				 *  	var iTest;
+				 *  	iTest = oSort['string-asc']('data11', 'data12');
+				 *  	if (iTest !== 0)
+				 *  		return iTest;
+				 *    iTest = oSort['numeric-desc']('data21', 'data22');
+				 *    if (iTest !== 0)
+				 *  		return iTest;
+				 *  	return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+				 *  }
+				 * Basically we have a test for each sorting column, if the data in that column is equal,
+				 * test the next column. If all columns match, then we use a numeric sort on the row 
+				 * positions in the original data array to provide a stable sort.
+				 */
+				oSettings.aiDisplayMaster.sort( function ( a, b ) {
+					var k, l, lLen, iTest, aDataSort, sDataType;
+					for ( k=0 ; k<iSortLen ; k++ )
+					{
+						aDataSort = aoColumns[ aaSort[k][0] ].aDataSort;
+		
+						for ( l=0, lLen=aDataSort.length ; l<lLen ; l++ )
+						{
+							sDataType = aoColumns[ aDataSort[l] ].sType;
+							
+							iTest = oSort[ (sDataType ? sDataType : 'string')+"-"+aaSort[k][1] ](
+								aoData[a]._aSortData[ aDataSort[l] ],
+								aoData[b]._aSortData[ aDataSort[l] ]
+							);
+						
+							if ( iTest !== 0 )
+							{
+								return iTest;
+							}
+						}
+					}
+					
+					return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+				} );
+			}
+			
+			/* Alter the sorting classes to take account of the changes */
+			if ( (bApplyClasses === undefined || bApplyClasses) && !oSettings.oFeatures.bDeferRender )
+			{
+				_fnSortingClasses( oSettings );
+			}
+		
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				var sTitle = aoColumns[i].sTitle.replace( /<.*?>/g, "" );
+				nTh = aoColumns[i].nTh;
+				nTh.removeAttribute('aria-sort');
+				nTh.removeAttribute('aria-label');
+				
+				/* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */
+				if ( aoColumns[i].bSortable )
+				{
+					if ( aaSort.length > 0 && aaSort[0][0] == i )
+					{
+						nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" );
+						
+						var nextSort = (aoColumns[i].asSorting[ aaSort[0][2]+1 ]) ? 
+							aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0];
+						nTh.setAttribute('aria-label', sTitle+
+							(nextSort=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
+					}
+					else
+					{
+						nTh.setAttribute('aria-label', sTitle+
+							(aoColumns[i].asSorting[0]=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
+					}
+				}
+				else
+				{
+					nTh.setAttribute('aria-label', sTitle);
+				}
+			}
+			
+			/* Tell the draw function that we have sorted the data */
+			oSettings.bSorted = true;
+			$(oSettings.oInstance).trigger('sort', oSettings);
+			
+			/* Copy the master data into the draw array and re-draw */
+			if ( oSettings.oFeatures.bFilter )
+			{
+				/* _fnFilter() will redraw the table for us */
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
+			}
+			else
+			{
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+				oSettings._iDisplayStart = 0; /* reset display back to page 0 */
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Attach a sort handler (click) to a node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nNode node to attach the handler to
+		 *  @param {int} iDataIndex column sorting index
+		 *  @param {function} [fnCallback] callback function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback )
+		{
+			_fnBindAction( nNode, {}, function (e) {
+				/* If the column is not sortable - don't to anything */
+				if ( oSettings.aoColumns[iDataIndex].bSortable === false )
+				{
+					return;
+				}
+				
+				/*
+				 * This is a little bit odd I admit... I declare a temporary function inside the scope of
+				 * _fnBuildHead and the click handler in order that the code presented here can be used 
+				 * twice - once for when bProcessing is enabled, and another time for when it is 
+				 * disabled, as we need to perform slightly different actions.
+				 *   Basically the issue here is that the Javascript engine in modern browsers don't 
+				 * appear to allow the rendering engine to update the display while it is still executing
+				 * it's thread (well - it does but only after long intervals). This means that the 
+				 * 'processing' display doesn't appear for a table sort. To break the js thread up a bit
+				 * I force an execution break by using setTimeout - but this breaks the expected 
+				 * thread continuation for the end-developer's point of view (their code would execute
+				 * too early), so we only do it when we absolutely have to.
+				 */
+				var fnInnerSorting = function () {
+					var iColumn, iNextSort;
+					
+					/* If the shift key is pressed then we are multiple column sorting */
+					if ( e.shiftKey )
+					{
+						/* Are we already doing some kind of sort on this column? */
+						var bFound = false;
+						for ( var i=0 ; i<oSettings.aaSorting.length ; i++ )
+						{
+							if ( oSettings.aaSorting[i][0] == iDataIndex )
+							{
+								bFound = true;
+								iColumn = oSettings.aaSorting[i][0];
+								iNextSort = oSettings.aaSorting[i][2]+1;
+								
+								if ( !oSettings.aoColumns[iColumn].asSorting[iNextSort] )
+								{
+									/* Reached the end of the sorting options, remove from multi-col sort */
+									oSettings.aaSorting.splice( i, 1 );
+								}
+								else
+								{
+									/* Move onto next sorting direction */
+									oSettings.aaSorting[i][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];
+									oSettings.aaSorting[i][2] = iNextSort;
+								}
+								break;
+							}
+						}
+						
+						/* No sort yet - add it in */
+						if ( bFound === false )
+						{
+							oSettings.aaSorting.push( [ iDataIndex, 
+								oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] );
+						}
+					}
+					else
+					{
+						/* If no shift key then single column sort */
+						if ( oSettings.aaSorting.length == 1 && oSettings.aaSorting[0][0] == iDataIndex )
+						{
+							iColumn = oSettings.aaSorting[0][0];
+							iNextSort = oSettings.aaSorting[0][2]+1;
+							if ( !oSettings.aoColumns[iColumn].asSorting[iNextSort] )
+							{
+								iNextSort = 0;
+							}
+							oSettings.aaSorting[0][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];
+							oSettings.aaSorting[0][2] = iNextSort;
+						}
+						else
+						{
+							oSettings.aaSorting.splice( 0, oSettings.aaSorting.length );
+							oSettings.aaSorting.push( [ iDataIndex, 
+								oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] );
+						}
+					}
+					
+					/* Run the sort */
+					_fnSort( oSettings );
+				}; /* /fnInnerSorting */
+				
+				if ( !oSettings.oFeatures.bProcessing )
+				{
+					fnInnerSorting();
+				}
+				else
+				{
+					_fnProcessingDisplay( oSettings, true );
+					setTimeout( function() {
+						fnInnerSorting();
+						if ( !oSettings.oFeatures.bServerSide )
+						{
+							_fnProcessingDisplay( oSettings, false );
+						}
+					}, 0 );
+				}
+				
+				/* Call the user specified callback function - used for async user interaction */
+				if ( typeof fnCallback == 'function' )
+				{
+					fnCallback( oSettings );
+				}
+			} );
+		}
+		
+		
+		/**
+		 * Set the sorting classes on the header, Note: it is safe to call this function 
+		 * when bSort and bSortClasses are false
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSortingClasses( oSettings )
+		{
+			var i, iLen, j, jLen, iFound;
+			var aaSort, sClass;
+			var iColumns = oSettings.aoColumns.length;
+			var oClasses = oSettings.oClasses;
+			
+			for ( i=0 ; i<iColumns ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bSortable )
+				{
+					$(oSettings.aoColumns[i].nTh).removeClass( oClasses.sSortAsc +" "+ oClasses.sSortDesc +
+						" "+ oSettings.aoColumns[i].sSortingClass );
+				}
+			}
+			
+			if ( oSettings.aaSortingFixed !== null )
+			{
+				aaSort = oSettings.aaSortingFixed.concat( oSettings.aaSorting );
+			}
+			else
+			{
+				aaSort = oSettings.aaSorting.slice();
+			}
+			
+			/* Apply the required classes to the header */
+			for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bSortable )
+				{
+					sClass = oSettings.aoColumns[i].sSortingClass;
+					iFound = -1;
+					for ( j=0 ; j<aaSort.length ; j++ )
+					{
+						if ( aaSort[j][0] == i )
+						{
+							sClass = ( aaSort[j][1] == "asc" ) ?
+								oClasses.sSortAsc : oClasses.sSortDesc;
+							iFound = j;
+							break;
+						}
+					}
+					$(oSettings.aoColumns[i].nTh).addClass( sClass );
+					
+					if ( oSettings.bJUI )
+					{
+						/* jQuery UI uses extra markup */
+						var jqSpan = $("span."+oClasses.sSortIcon,  oSettings.aoColumns[i].nTh);
+						jqSpan.removeClass(oClasses.sSortJUIAsc +" "+ oClasses.sSortJUIDesc +" "+ 
+							oClasses.sSortJUI +" "+ oClasses.sSortJUIAscAllowed +" "+ oClasses.sSortJUIDescAllowed );
+						
+						var sSpanClass;
+						if ( iFound == -1 )
+						{
+						 	sSpanClass = oSettings.aoColumns[i].sSortingClassJUI;
+						}
+						else if ( aaSort[iFound][1] == "asc" )
+						{
+							sSpanClass = oClasses.sSortJUIAsc;
+						}
+						else
+						{
+							sSpanClass = oClasses.sSortJUIDesc;
+						}
+						
+						jqSpan.addClass( sSpanClass );
+					}
+				}
+				else
+				{
+					/* No sorting on this column, so add the base class. This will have been assigned by
+					 * _fnAddColumn
+					 */
+					$(oSettings.aoColumns[i].nTh).addClass( oSettings.aoColumns[i].sSortingClass );
+				}
+			}
+			
+			/* 
+			 * Apply the required classes to the table body
+			 * Note that this is given as a feature switch since it can significantly slow down a sort
+			 * on large data sets (adding and removing of classes is always slow at the best of times..)
+			 * Further to this, note that this code is admittedly fairly ugly. It could be made a lot 
+			 * simpler using jQuery selectors and add/removeClass, but that is significantly slower
+			 * (on the order of 5 times slower) - hence the direct DOM manipulation here.
+			 * Note that for deferred drawing we do use jQuery - the reason being that taking the first
+			 * row found to see if the whole column needs processed can miss classes since the first
+			 * column might be new.
+			 */
+			sClass = oClasses.sSortColumn;
+			
+			if ( oSettings.oFeatures.bSort && oSettings.oFeatures.bSortClasses )
+			{
+				var nTds = _fnGetTdNodes( oSettings );
+				
+				/* Determine what the sorting class for each column should be */
+				var iClass, iTargetCol;
+				var asClasses = [];
+				for (i = 0; i < iColumns; i++)
+				{
+					asClasses.push("");
+				}
+				for (i = 0, iClass = 1; i < aaSort.length; i++)
+				{
+					iTargetCol = parseInt( aaSort[i][0], 10 );
+					asClasses[iTargetCol] = sClass + iClass;
+					
+					if ( iClass < 3 )
+					{
+						iClass++;
+					}
+				}
+				
+				/* Make changes to the classes for each cell as needed */
+				var reClass = new RegExp(sClass + "[123]");
+				var sTmpClass, sCurrentClass, sNewClass;
+				for ( i=0, iLen=nTds.length; i<iLen; i++ )
+				{
+					/* Determine which column we're looking at */
+					iTargetCol = i % iColumns;
+					
+					/* What is the full list of classes now */
+					sCurrentClass = nTds[i].className;
+					/* What sorting class should be applied? */
+					sNewClass = asClasses[iTargetCol];
+					/* What would the new full list be if we did a replacement? */
+					sTmpClass = sCurrentClass.replace(reClass, sNewClass);
+					
+					if ( sTmpClass != sCurrentClass )
+					{
+						/* We changed something */
+						nTds[i].className = $.trim( sTmpClass );
+					}
+					else if ( sNewClass.length > 0 && sCurrentClass.indexOf(sNewClass) == -1 )
+					{
+						/* We need to add a class */
+						nTds[i].className = sCurrentClass + " " + sNewClass;
+					}
+				}
+			}
+		}
+		
+		
+		
+		/**
+		 * Save the state of a table in a cookie such that the page can be reloaded
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSaveState ( oSettings )
+		{
+			if ( !oSettings.oFeatures.bStateSave || oSettings.bDestroying )
+			{
+				return;
+			}
+		
+			/* Store the interesting variables */
+			var i, iLen, bInfinite=oSettings.oScroll.bInfinite;
+			var oState = {
+				"iCreate":      new Date().getTime(),
+				"iStart":       (bInfinite ? 0 : oSettings._iDisplayStart),
+				"iEnd":         (bInfinite ? oSettings._iDisplayLength : oSettings._iDisplayEnd),
+				"iLength":      oSettings._iDisplayLength,
+				"aaSorting":    $.extend( true, [], oSettings.aaSorting ),
+				"oSearch":      $.extend( true, {}, oSettings.oPreviousSearch ),
+				"aoSearchCols": $.extend( true, [], oSettings.aoPreSearchCols ),
+				"abVisCols":    []
+			};
+		
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oState.abVisCols.push( oSettings.aoColumns[i].bVisible );
+			}
+		
+			_fnCallbackFire( oSettings, "aoStateSaveParams", 'stateSaveParams', [oSettings, oState] );
+			
+			oSettings.fnStateSave.call( oSettings.oInstance, oSettings, oState );
+		}
+		
+		
+		/**
+		 * Attempt to load a saved table state from a cookie
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} oInit DataTables init object so we can override settings
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLoadState ( oSettings, oInit )
+		{
+			if ( !oSettings.oFeatures.bStateSave )
+			{
+				return;
+			}
+		
+			var oData = oSettings.fnStateLoad.call( oSettings.oInstance, oSettings );
+			if ( !oData )
+			{
+				return;
+			}
+			
+			/* Allow custom and plug-in manipulation functions to alter the saved data set and
+			 * cancelling of loading by returning false
+			 */
+			var abStateLoad = _fnCallbackFire( oSettings, 'aoStateLoadParams', 'stateLoadParams', [oSettings, oData] );
+			if ( $.inArray( false, abStateLoad ) !== -1 )
+			{
+				return;
+			}
+			
+			/* Store the saved state so it might be accessed at any time */
+			oSettings.oLoadedState = $.extend( true, {}, oData );
+			
+			/* Restore key features */
+			oSettings._iDisplayStart    = oData.iStart;
+			oSettings.iInitDisplayStart = oData.iStart;
+			oSettings._iDisplayEnd      = oData.iEnd;
+			oSettings._iDisplayLength   = oData.iLength;
+			oSettings.aaSorting         = oData.aaSorting.slice();
+			oSettings.saved_aaSorting   = oData.aaSorting.slice();
+			
+			/* Search filtering  */
+			$.extend( oSettings.oPreviousSearch, oData.oSearch );
+			$.extend( true, oSettings.aoPreSearchCols, oData.aoSearchCols );
+			
+			/* Column visibility state
+			 * Pass back visibility settings to the init handler, but to do not here override
+			 * the init object that the user might have passed in
+			 */
+			oInit.saved_aoColumns = [];
+			for ( var i=0 ; i<oData.abVisCols.length ; i++ )
+			{
+				oInit.saved_aoColumns[i] = {};
+				oInit.saved_aoColumns[i].bVisible = oData.abVisCols[i];
+			}
+		
+			_fnCallbackFire( oSettings, 'aoStateLoaded', 'stateLoaded', [oSettings, oData] );
+		}
+		
+		
+		/**
+		 * Create a new cookie with a value to store the state of a table
+		 *  @param {string} sName name of the cookie to create
+		 *  @param {string} sValue the value the cookie should take
+		 *  @param {int} iSecs duration of the cookie
+		 *  @param {string} sBaseName sName is made up of the base + file name - this is the base
+		 *  @param {function} fnCallback User definable function to modify the cookie
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCreateCookie ( sName, sValue, iSecs, sBaseName, fnCallback )
+		{
+			var date = new Date();
+			date.setTime( date.getTime()+(iSecs*1000) );
+			
+			/* 
+			 * Shocking but true - it would appear IE has major issues with having the path not having
+			 * a trailing slash on it. We need the cookie to be available based on the path, so we
+			 * have to append the file name to the cookie name. Appalling. Thanks to vex for adding the
+			 * patch to use at least some of the path
+			 */
+			var aParts = window.location.pathname.split('/');
+			var sNameFile = sName + '_' + aParts.pop().replace(/[\/:]/g,"").toLowerCase();
+			var sFullCookie, oData;
+			
+			if ( fnCallback !== null )
+			{
+				oData = (typeof $.parseJSON === 'function') ? 
+					$.parseJSON( sValue ) : eval( '('+sValue+')' );
+				sFullCookie = fnCallback( sNameFile, oData, date.toGMTString(),
+					aParts.join('/')+"/" );
+			}
+			else
+			{
+				sFullCookie = sNameFile + "=" + encodeURIComponent(sValue) +
+					"; expires=" + date.toGMTString() +"; path=" + aParts.join('/')+"/";
+			}
+			
+			/* Are we going to go over the cookie limit of 4KiB? If so, try to delete a cookies
+			 * belonging to DataTables.
+			 */
+			var
+				aCookies =document.cookie.split(';'),
+				iNewCookieLen = sFullCookie.split(';')[0].length,
+				aOldCookies = [];
+			
+			if ( iNewCookieLen+document.cookie.length+10 > 4096 ) /* Magic 10 for padding */
+			{
+				for ( var i=0, iLen=aCookies.length ; i<iLen ; i++ )
+				{
+					if ( aCookies[i].indexOf( sBaseName ) != -1 )
+					{
+						/* It's a DataTables cookie, so eval it and check the time stamp */
+						var aSplitCookie = aCookies[i].split('=');
+						try {
+							oData = eval( '('+decodeURIComponent(aSplitCookie[1])+')' );
+		
+							if ( oData && oData.iCreate )
+							{
+								aOldCookies.push( {
+									"name": aSplitCookie[0],
+									"time": oData.iCreate
+								} );
+							}
+						}
+						catch( e ) {}
+					}
+				}
+		
+				// Make sure we delete the oldest ones first
+				aOldCookies.sort( function (a, b) {
+					return b.time - a.time;
+				} );
+		
+				// Eliminate as many old DataTables cookies as we need to
+				while ( iNewCookieLen + document.cookie.length + 10 > 4096 ) {
+					if ( aOldCookies.length === 0 ) {
+						// Deleted all DT cookies and still not enough space. Can't state save
+						return;
+					}
+					
+					var old = aOldCookies.pop();
+					document.cookie = old.name+"=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path="+
+						aParts.join('/') + "/";
+				}
+			}
+			
+			document.cookie = sFullCookie;
+		}
+		
+		
+		/**
+		 * Read an old cookie to get a cookie with an old table state
+		 *  @param {string} sName name of the cookie to read
+		 *  @returns {string} contents of the cookie - or null if no cookie with that name found
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReadCookie ( sName )
+		{
+			var
+				aParts = window.location.pathname.split('/'),
+				sNameEQ = sName + '_' + aParts[aParts.length-1].replace(/[\/:]/g,"").toLowerCase() + '=',
+			 	sCookieContents = document.cookie.split(';');
+			
+			for( var i=0 ; i<sCookieContents.length ; i++ )
+			{
+				var c = sCookieContents[i];
+				
+				while (c.charAt(0)==' ')
+				{
+					c = c.substring(1,c.length);
+				}
+				
+				if (c.indexOf(sNameEQ) === 0)
+				{
+					return decodeURIComponent( c.substring(sNameEQ.length,c.length) );
+				}
+			}
+			return null;
+		}
+		
+		
+		/**
+		 * Return the settings object for a particular table
+		 *  @param {node} nTable table we are using as a dataTable
+		 *  @returns {object} Settings object - or null if not found
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSettingsFromNode ( nTable )
+		{
+			for ( var i=0 ; i<DataTable.settings.length ; i++ )
+			{
+				if ( DataTable.settings[i].nTable === nTable )
+				{
+					return DataTable.settings[i];
+				}
+			}
+			
+			return null;
+		}
+		
+		
+		/**
+		 * Return an array with the TR nodes for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {array} TR array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetTrNodes ( oSettings )
+		{
+			var aNodes = [];
+			var aoData = oSettings.aoData;
+			for ( var i=0, iLen=aoData.length ; i<iLen ; i++ )
+			{
+				if ( aoData[i].nTr !== null )
+				{
+					aNodes.push( aoData[i].nTr );
+				}
+			}
+			return aNodes;
+		}
+		
+		
+		/**
+		 * Return an flat array with all TD nodes for the table, or row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} [iIndividualRow] aoData index to get the nodes for - optional 
+		 *    if not given then the return array will contain all nodes for the table
+		 *  @returns {array} TD array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetTdNodes ( oSettings, iIndividualRow )
+		{
+			var anReturn = [];
+			var iCorrector;
+			var anTds, nTd;
+			var iRow, iRows=oSettings.aoData.length,
+				iColumn, iColumns, oData, sNodeName, iStart=0, iEnd=iRows;
+			
+			/* Allow the collection to be limited to just one row */
+			if ( iIndividualRow !== undefined )
+			{
+				iStart = iIndividualRow;
+				iEnd = iIndividualRow+1;
+			}
+		
+			for ( iRow=iStart ; iRow<iEnd ; iRow++ )
+			{
+				oData = oSettings.aoData[iRow];
+				if ( oData.nTr !== null )
+				{
+					/* get the TD child nodes - taking into account text etc nodes */
+					anTds = [];
+					nTd = oData.nTr.firstChild;
+					while ( nTd )
+					{
+						sNodeName = nTd.nodeName.toLowerCase();
+						if ( sNodeName == 'td' || sNodeName == 'th' )
+						{
+							anTds.push( nTd );
+						}
+						nTd = nTd.nextSibling;
+					}
+		
+					iCorrector = 0;
+					for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )
+					{
+						if ( oSettings.aoColumns[iColumn].bVisible )
+						{
+							anReturn.push( anTds[iColumn-iCorrector] );
+						}
+						else
+						{
+							anReturn.push( oData._anHidden[iColumn] );
+							iCorrector++;
+						}
+					}
+				}
+			}
+		
+			return anReturn;
+		}
+		
+		
+		/**
+		 * Log an error message
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iLevel log error messages, or display them to the user
+		 *  @param {string} sMesg error message
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLog( oSettings, iLevel, sMesg )
+		{
+			var sAlert = (oSettings===null) ?
+				"DataTables warning: "+sMesg :
+				"DataTables warning (table id = '"+oSettings.sTableId+"'): "+sMesg;
+			
+			if ( iLevel === 0 )
+			{
+				if ( DataTable.ext.sErrMode == 'alert' )
+				{
+					alert( sAlert );
+				}
+				else
+				{
+					throw new Error(sAlert);
+				}
+				return;
+			}
+			else if ( window.console && console.log )
+			{
+				console.log( sAlert );
+			}
+		}
+		
+		
+		/**
+		 * See if a property is defined on one object, if so assign it to the other object
+		 *  @param {object} oRet target object
+		 *  @param {object} oSrc source object
+		 *  @param {string} sName property
+		 *  @param {string} [sMappedName] name to map too - optional, sName used if not given
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnMap( oRet, oSrc, sName, sMappedName )
+		{
+			if ( sMappedName === undefined )
+			{
+				sMappedName = sName;
+			}
+			if ( oSrc[sName] !== undefined )
+			{
+				oRet[sMappedName] = oSrc[sName];
+			}
+		}
+		
+		
+		/**
+		 * Extend objects - very similar to jQuery.extend, but deep copy objects, and shallow
+		 * copy arrays. The reason we need to do this, is that we don't want to deep copy array
+		 * init values (such as aaSorting) since the dev wouldn't be able to override them, but
+		 * we do want to deep copy arrays.
+		 *  @param {object} oOut Object to extend
+		 *  @param {object} oExtender Object from which the properties will be applied to oOut
+		 *  @returns {object} oOut Reference, just for convenience - oOut === the return.
+		 *  @memberof DataTable#oApi
+		 *  @todo This doesn't take account of arrays inside the deep copied objects.
+		 */
+		function _fnExtend( oOut, oExtender )
+		{
+			var val;
+			
+			for ( var prop in oExtender )
+			{
+				if ( oExtender.hasOwnProperty(prop) )
+				{
+					val = oExtender[prop];
+		
+					if ( typeof oInit[prop] === 'object' && val !== null && $.isArray(val) === false )
+					{
+						$.extend( true, oOut[prop], val );
+					}
+					else
+					{
+						oOut[prop] = val;
+					}
+				}
+			}
+		
+			return oOut;
+		}
+		
+		
+		/**
+		 * Bind an event handers to allow a click or return key to activate the callback.
+		 * This is good for accessibility since a return on the keyboard will have the
+		 * same effect as a click, if the element has focus.
+		 *  @param {element} n Element to bind the action to
+		 *  @param {object} oData Data object to pass to the triggered function
+		 *  @param {function} fn Callback function for when the event is triggered
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBindAction( n, oData, fn )
+		{
+			$(n)
+				.bind( 'click.DT', oData, function (e) {
+						n.blur(); // Remove focus outline for mouse users
+						fn(e);
+					} )
+				.bind( 'keypress.DT', oData, function (e){
+					if ( e.which === 13 ) {
+						fn(e);
+					} } )
+				.bind( 'selectstart.DT', function () {
+					/* Take the brutal approach to cancelling text selection */
+					return false;
+					} );
+		}
+		
+		
+		/**
+		 * Register a callback function. Easily allows a callback function to be added to
+		 * an array store of callback functions that can then all be called together.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sStore Name of the array storage for the callbacks in oSettings
+		 *  @param {function} fn Function to be called back
+		 *  @param {string} sName Identifying name for the callback (i.e. a label)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCallbackReg( oSettings, sStore, fn, sName )
+		{
+			if ( fn )
+			{
+				oSettings[sStore].push( {
+					"fn": fn,
+					"sName": sName
+				} );
+			}
+		}
+		
+		
+		/**
+		 * Fire callback functions and trigger events. Note that the loop over the callback
+		 * array store is done backwards! Further note that you do not want to fire off triggers
+		 * in time sensitive applications (for example cell creation) as its slow.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sStore Name of the array storage for the callbacks in oSettings
+		 *  @param {string} sTrigger Name of the jQuery custom event to trigger. If null no trigger
+		 *    is fired
+		 *  @param {array} aArgs Array of arguments to pass to the callback function / trigger
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCallbackFire( oSettings, sStore, sTrigger, aArgs )
+		{
+			var aoStore = oSettings[sStore];
+			var aRet =[];
+		
+			for ( var i=aoStore.length-1 ; i>=0 ; i-- )
+			{
+				aRet.push( aoStore[i].fn.apply( oSettings.oInstance, aArgs ) );
+			}
+		
+			if ( sTrigger !== null )
+			{
+				$(oSettings.oInstance).trigger(sTrigger, aArgs);
+			}
+		
+			return aRet;
+		}
+		
+		
+		/**
+		 * JSON stringify. If JSON.stringify it provided by the browser, json2.js or any other
+		 * library, then we use that as it is fast, safe and accurate. If the function isn't 
+		 * available then we need to built it ourselves - the inspiration for this function comes
+		 * from Craig Buckler ( http://www.sitepoint.com/javascript-json-serialization/ ). It is
+		 * not perfect and absolutely should not be used as a replacement to json2.js - but it does
+		 * do what we need, without requiring a dependency for DataTables.
+		 *  @param {object} o JSON object to be converted
+		 *  @returns {string} JSON string
+		 *  @memberof DataTable#oApi
+		 */
+		var _fnJsonString = (window.JSON) ? JSON.stringify : function( o )
+		{
+			/* Not an object or array */
+			var sType = typeof o;
+			if (sType !== "object" || o === null)
+			{
+				// simple data type
+				if (sType === "string")
+				{
+					o = '"'+o+'"';
+				}
+				return o+"";
+			}
+		
+			/* If object or array, need to recurse over it */
+			var
+				sProp, mValue,
+				json = [],
+				bArr = $.isArray(o);
+			
+			for (sProp in o)
+			{
+				mValue = o[sProp];
+				sType = typeof mValue;
+		
+				if (sType === "string")
+				{
+					mValue = '"'+mValue+'"';
+				}
+				else if (sType === "object" && mValue !== null)
+				{
+					mValue = _fnJsonString(mValue);
+				}
+		
+				json.push((bArr ? "" : '"'+sProp+'":') + mValue);
+			}
+		
+			return (bArr ? "[" : "{") + json + (bArr ? "]" : "}");
+		};
+		
+		
+		/**
+		 * From some browsers (specifically IE6/7) we need special handling to work around browser
+		 * bugs - this function is used to detect when these workarounds are needed.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBrowserDetect( oSettings )
+		{
+			/* IE6/7 will oversize a width 100% element inside a scrolling element, to include the
+			 * width of the scrollbar, while other browsers ensure the inner element is contained
+			 * without forcing scrolling
+			 */
+			var n = $(
+				'<div style="position:absolute; top:0; left:0; height:1px; width:1px; overflow:hidden">'+
+					'<div style="position:absolute; top:1px; left:1px; width:100px; overflow:scroll;">'+
+						'<div id="DT_BrowserTest" style="width:100%; height:10px;"></div>'+
+					'</div>'+
+				'</div>')[0];
+		
+			document.body.appendChild( n );
+			oSettings.oBrowser.bScrollOversize = $('#DT_BrowserTest', n)[0].offsetWidth === 100 ? true : false;
+			document.body.removeChild( n );
+		}
+		
+
+		/**
+		 * Perform a jQuery selector action on the table's TR elements (from the tbody) and
+		 * return the resulting jQuery object.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select TR elements that meet the current filter
+		 *    criterion ("applied") or all TR elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the TR elements in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be 
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {object} jQuery object, filtered by the given selector.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Highlight every second row
+		 *      oTable.$('tr:odd').css('backgroundColor', 'blue');
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to rows with 'Webkit' in them, add a background colour and then
+		 *      // remove the filter, thus highlighting the 'Webkit' rows only.
+		 *      oTable.fnFilter('Webkit');
+		 *      oTable.$('tr', {"filter": "applied"}).css('backgroundColor', 'blue');
+		 *      oTable.fnFilter('');
+		 *    } );
+		 */
+		this.$ = function ( sSelector, oOpts )
+		{
+			var i, iLen, a = [], tr;
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var aoData = oSettings.aoData;
+			var aiDisplay = oSettings.aiDisplay;
+			var aiDisplayMaster = oSettings.aiDisplayMaster;
+		
+			if ( !oOpts )
+			{
+				oOpts = {};
+			}
+		
+			oOpts = $.extend( {}, {
+				"filter": "none", // applied
+				"order": "current", // "original"
+				"page": "all" // current
+			}, oOpts );
+		
+			// Current page implies that order=current and fitler=applied, since it is fairly
+			// senseless otherwise
+			if ( oOpts.page == 'current' )
+			{
+				for ( i=oSettings._iDisplayStart, iLen=oSettings.fnDisplayEnd() ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplay[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "current" && oOpts.filter == "none" )
+			{
+				for ( i=0, iLen=aiDisplayMaster.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplayMaster[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "current" && oOpts.filter == "applied" )
+			{
+				for ( i=0, iLen=aiDisplay.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplay[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "original" && oOpts.filter == "none" )
+			{
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ i ].nTr ;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "original" && oOpts.filter == "applied" )
+			{
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ i ].nTr;
+					if ( $.inArray( i, aiDisplay ) !== -1 && tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else
+			{
+				_fnLog( oSettings, 1, "Unknown selection options" );
+			}
+		
+			/* We need to filter on the TR elements and also 'find' in their descendants
+			 * to make the selector act like it would in a full table - so we need
+			 * to build both results and then combine them together
+			 */
+			var jqA = $(a);
+			var jqTRs = jqA.filter( sSelector );
+			var jqDescendants = jqA.find( sSelector );
+		
+			return $( [].concat($.makeArray(jqTRs), $.makeArray(jqDescendants)) );
+		};
+		
+		
+		/**
+		 * Almost identical to $ in operation, but in this case returns the data for the matched
+		 * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes
+		 * rather than any descendants, so the data can be obtained for the row/cell. If matching
+		 * rows are found, the data returned is the original data array/object that was used to  
+		 * create the row (or a generated array if from a DOM source).
+		 *
+		 * This method is often useful in-combination with $ where both functions are given the
+		 * same parameters and the array indexes will match identically.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select elements that meet the current filter
+		 *    criterion ("applied") or all elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the data in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be 
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {array} Data for the matched elements. If any elements, as a result of the
+		 *    selector, were not TR, TD or TH elements in the DataTable, they will have a null 
+		 *    entry in the array.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Get the data from the first row in the table
+		 *      var data = oTable._('tr:first');
+		 *
+		 *      // Do something useful with the data
+		 *      alert( "First cell is: "+data[0] );
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to 'Webkit' and get all data for 
+		 *      oTable.fnFilter('Webkit');
+		 *      var data = oTable._('tr', {"filter": "applied"});
+		 *      
+		 *      // Do something with the data
+		 *      alert( data.length+" rows matched the filter" );
+		 *    } );
+		 */
+		this._ = function ( sSelector, oOpts )
+		{
+			var aOut = [];
+			var i, iLen, iIndex;
+			var aTrs = this.$( sSelector, oOpts );
+		
+			for ( i=0, iLen=aTrs.length ; i<iLen ; i++ )
+			{
+				aOut.push( this.fnGetData(aTrs[i]) );
+			}
+		
+			return aOut;
+		};
+		
+		
+		/**
+		 * Add a single new row or multiple rows of data to the table. Please note
+		 * that this is suitable for client-side processing only - if you are using 
+		 * server-side processing (i.e. "bServerSide": true), then to add data, you
+		 * must add it to the data source, i.e. the server-side, through an Ajax call.
+		 *  @param {array|object} mData The data to be added to the table. This can be:
+		 *    <ul>
+		 *      <li>1D array of data - add a single row with the data provided</li>
+		 *      <li>2D array of arrays - add multiple rows in a single call</li>
+		 *      <li>object - data object when using <i>mData</i></li>
+		 *      <li>array of objects - multiple data objects when using <i>mData</i></li>
+		 *    </ul>
+		 *  @param {bool} [bRedraw=true] redraw the table or not
+		 *  @returns {array} An array of integers, representing the list of indexes in 
+		 *    <i>aoData</i> ({@link DataTable.models.oSettings}) that have been added to 
+		 *    the table.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    // Global var for counter
+		 *    var giCount = 2;
+		 *    
+		 *    $(document).ready(function() {
+		 *      $('#example').dataTable();
+		 *    } );
+		 *    
+		 *    function fnClickAddRow() {
+		 *      $('#example').dataTable().fnAddData( [
+		 *        giCount+".1",
+		 *        giCount+".2",
+		 *        giCount+".3",
+		 *        giCount+".4" ]
+		 *      );
+		 *        
+		 *      giCount++;
+		 *    }
+		 */
+		this.fnAddData = function( mData, bRedraw )
+		{
+			if ( mData.length === 0 )
+			{
+				return [];
+			}
+			
+			var aiReturn = [];
+			var iTest;
+			
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			/* Check if we want to add multiple rows or not */
+			if ( typeof mData[0] === "object" && mData[0] !== null )
+			{
+				for ( var i=0 ; i<mData.length ; i++ )
+				{
+					iTest = _fnAddData( oSettings, mData[i] );
+					if ( iTest == -1 )
+					{
+						return aiReturn;
+					}
+					aiReturn.push( iTest );
+				}
+			}
+			else
+			{
+				iTest = _fnAddData( oSettings, mData );
+				if ( iTest == -1 )
+				{
+					return aiReturn;
+				}
+				aiReturn.push( iTest );
+			}
+			
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnReDraw( oSettings );
+			}
+			return aiReturn;
+		};
+		
+		
+		/**
+		 * This function will make DataTables recalculate the column sizes, based on the data 
+		 * contained in the table and the sizes applied to the columns (in the DOM, CSS or 
+		 * through the sWidth parameter). This can be useful when the width of the table's 
+		 * parent element changes (for example a window resize).
+		 *  @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *      
+		 *      $(window).bind('resize', function () {
+		 *        oTable.fnAdjustColumnSizing();
+		 *      } );
+		 *    } );
+		 */
+		this.fnAdjustColumnSizing = function ( bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode(this[DataTable.ext.iApiIndex]);
+			_fnAdjustColumnSizing( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				this.fnDraw( false );
+			}
+			else if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" )
+			{
+				/* If not redrawing, but scrolling, we want to apply the new column sizes anyway */
+				this.oApi._fnScrollDraw(oSettings);
+			}
+		};
+		
+		
+		/**
+		 * Quickly and simply clear a table
+		 *  @param {bool} [bRedraw=true] redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...)
+		 *      oTable.fnClearTable();
+		 *    } );
+		 */
+		this.fnClearTable = function( bRedraw )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			_fnClearTable( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * The exact opposite of 'opening' a row, this function will close any rows which 
+		 * are currently 'open'.
+		 *  @param {node} nTr the table row to 'close'
+		 *  @returns {int} 0 on success, or 1 if failed (can't find the row)
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnClose = function( nTr )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			for ( var i=0 ; i<oSettings.aoOpenRows.length ; i++ )
+			{
+				if ( oSettings.aoOpenRows[i].nParent == nTr )
+				{
+					var nTrParent = oSettings.aoOpenRows[i].nTr.parentNode;
+					if ( nTrParent )
+					{
+						/* Remove it if it is currently on display */
+						nTrParent.removeChild( oSettings.aoOpenRows[i].nTr );
+					}
+					oSettings.aoOpenRows.splice( i, 1 );
+					return 0;
+				}
+			}
+			return 1;
+		};
+		
+		
+		/**
+		 * Remove a row for the table
+		 *  @param {mixed} mTarget The index of the row from aoData to be deleted, or
+		 *    the TR element you want to delete
+		 *  @param {function|null} [fnCallBack] Callback function
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @returns {array} The row that was deleted
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Immediately remove the first row
+		 *      oTable.fnDeleteRow( 0 );
+		 *    } );
+		 */
+		this.fnDeleteRow = function( mTarget, fnCallBack, bRedraw )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen, iAODataIndex;
+			
+			iAODataIndex = (typeof mTarget === 'object') ? 
+				_fnNodeToDataIndex(oSettings, mTarget) : mTarget;
+			
+			/* Return the data array from this row */
+			var oData = oSettings.aoData.splice( iAODataIndex, 1 );
+		
+			/* Update the _DT_RowIndex parameter */
+			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoData[i].nTr !== null )
+				{
+					oSettings.aoData[i].nTr._DT_RowIndex = i;
+				}
+			}
+			
+			/* Remove the target row from the search array */
+			var iDisplayIndex = $.inArray( iAODataIndex, oSettings.aiDisplay );
+			oSettings.asDataSearch.splice( iDisplayIndex, 1 );
+			
+			/* Delete from the display arrays */
+			_fnDeleteIndex( oSettings.aiDisplayMaster, iAODataIndex );
+			_fnDeleteIndex( oSettings.aiDisplay, iAODataIndex );
+			
+			/* If there is a user callback function - call it */
+			if ( typeof fnCallBack === "function" )
+			{
+				fnCallBack.call( this, oSettings, oData );
+			}
+			
+			/* Check for an 'overflow' they case for displaying the table */
+			if ( oSettings._iDisplayStart >= oSettings.fnRecordsDisplay() )
+			{
+				oSettings._iDisplayStart -= oSettings._iDisplayLength;
+				if ( oSettings._iDisplayStart < 0 )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			return oData;
+		};
+		
+		
+		/**
+		 * Restore the table to it's original state in the DOM by removing all of DataTables 
+		 * enhancements, alterations to the DOM structure of the table and event listeners.
+		 *  @param {boolean} [bRemove=false] Completely remove the table from the DOM
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      // This example is fairly pointless in reality, but shows how fnDestroy can be used
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnDestroy();
+		 *    } );
+		 */
+		this.fnDestroy = function ( bRemove )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var nOrig = oSettings.nTableWrapper.parentNode;
+			var nBody = oSettings.nTBody;
+			var i, iLen;
+		
+			bRemove = (bRemove===undefined) ? false : bRemove;
+			
+			/* Flag to note that the table is currently being destroyed - no action should be taken */
+			oSettings.bDestroying = true;
+			
+			/* Fire off the destroy callbacks for plug-ins etc */
+			_fnCallbackFire( oSettings, "aoDestroyCallback", "destroy", [oSettings] );
+		
+			/* If the table is not being removed, restore the hidden columns */
+			if ( !bRemove )
+			{
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible === false )
+					{
+						this.fnSetColumnVis( i, true );
+					}
+				}
+			}
+			
+			/* Blitz all DT events */
+			$(oSettings.nTableWrapper).find('*').andSelf().unbind('.DT');
+			
+			/* If there is an 'empty' indicator row, remove it */
+			$('tbody>tr>td.'+oSettings.oClasses.sRowEmpty, oSettings.nTable).parent().remove();
+			
+			/* When scrolling we had to break the table up - restore it */
+			if ( oSettings.nTable != oSettings.nTHead.parentNode )
+			{
+				$(oSettings.nTable).children('thead').remove();
+				oSettings.nTable.appendChild( oSettings.nTHead );
+			}
+			
+			if ( oSettings.nTFoot && oSettings.nTable != oSettings.nTFoot.parentNode )
+			{
+				$(oSettings.nTable).children('tfoot').remove();
+				oSettings.nTable.appendChild( oSettings.nTFoot );
+			}
+			
+			/* Remove the DataTables generated nodes, events and classes */
+			oSettings.nTable.parentNode.removeChild( oSettings.nTable );
+			$(oSettings.nTableWrapper).remove();
+			
+			oSettings.aaSorting = [];
+			oSettings.aaSortingFixed = [];
+			_fnSortingClasses( oSettings );
+			
+			$(_fnGetTrNodes( oSettings )).removeClass( oSettings.asStripeClasses.join(' ') );
+			
+			$('th, td', oSettings.nTHead).removeClass( [
+				oSettings.oClasses.sSortable,
+				oSettings.oClasses.sSortableAsc,
+				oSettings.oClasses.sSortableDesc,
+				oSettings.oClasses.sSortableNone ].join(' ')
+			);
+			if ( oSettings.bJUI )
+			{
+				$('th span.'+oSettings.oClasses.sSortIcon
+					+ ', td span.'+oSettings.oClasses.sSortIcon, oSettings.nTHead).remove();
+		
+				$('th, td', oSettings.nTHead).each( function () {
+					var jqWrapper = $('div.'+oSettings.oClasses.sSortJUIWrapper, this);
+					var kids = jqWrapper.contents();
+					$(this).append( kids );
+					jqWrapper.remove();
+				} );
+			}
+			
+			/* Add the TR elements back into the table in their original order */
+			if ( !bRemove && oSettings.nTableReinsertBefore )
+			{
+				nOrig.insertBefore( oSettings.nTable, oSettings.nTableReinsertBefore );
+			}
+			else if ( !bRemove )
+			{
+				nOrig.appendChild( oSettings.nTable );
+			}
+		
+			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoData[i].nTr !== null )
+				{
+					nBody.appendChild( oSettings.aoData[i].nTr );
+				}
+			}
+			
+			/* Restore the width of the original table */
+			if ( oSettings.oFeatures.bAutoWidth === true )
+			{
+			  oSettings.nTable.style.width = _fnStringToCss(oSettings.sDestroyWidth);
+			}
+			
+			/* If the were originally stripe classes - then we add them back here. Note
+			 * this is not fool proof (for example if not all rows had stripe classes - but
+			 * it's a good effort without getting carried away
+			 */
+			iLen = oSettings.asDestroyStripes.length;
+			if (iLen)
+			{
+				var anRows = $(nBody).children('tr');
+				for ( i=0 ; i<iLen ; i++ )
+				{
+					anRows.filter(':nth-child(' + iLen + 'n + ' + i + ')').addClass( oSettings.asDestroyStripes[i] );
+				}
+			}
+			
+			/* Remove the settings object from the settings array */
+			for ( i=0, iLen=DataTable.settings.length ; i<iLen ; i++ )
+			{
+				if ( DataTable.settings[i] == oSettings )
+				{
+					DataTable.settings.splice( i, 1 );
+				}
+			}
+			
+			/* End it all */
+			oSettings = null;
+			oInit = null;
+		};
+		
+		
+		/**
+		 * Redraw the table
+		 *  @param {bool} [bComplete=true] Re-filter and resort (if enabled) the table before the draw.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Re-draw the table - you wouldn't want to do it here, but it's an example :-)
+		 *      oTable.fnDraw();
+		 *    } );
+		 */
+		this.fnDraw = function( bComplete )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			if ( bComplete === false )
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			else
+			{
+				_fnReDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * Filter the input based on data
+		 *  @param {string} sInput String to filter the table on
+		 *  @param {int|null} [iColumn] Column to limit filtering to
+		 *  @param {bool} [bRegex=false] Treat as regular expression or not
+		 *  @param {bool} [bSmart=true] Perform smart filtering or not
+		 *  @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es)
+		 *  @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false)
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sometime later - filter...
+		 *      oTable.fnFilter( 'test string' );
+		 *    } );
+		 */
+		this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( !oSettings.oFeatures.bFilter )
+			{
+				return;
+			}
+			
+			if ( bRegex === undefined || bRegex === null )
+			{
+				bRegex = false;
+			}
+			
+			if ( bSmart === undefined || bSmart === null )
+			{
+				bSmart = true;
+			}
+			
+			if ( bShowGlobal === undefined || bShowGlobal === null )
+			{
+				bShowGlobal = true;
+			}
+			
+			if ( bCaseInsensitive === undefined || bCaseInsensitive === null )
+			{
+				bCaseInsensitive = true;
+			}
+			
+			if ( iColumn === undefined || iColumn === null )
+			{
+				/* Global filter */
+				_fnFilterComplete( oSettings, {
+					"sSearch":sInput+"",
+					"bRegex": bRegex,
+					"bSmart": bSmart,
+					"bCaseInsensitive": bCaseInsensitive
+				}, 1 );
+				
+				if ( bShowGlobal && oSettings.aanFeatures.f )
+				{
+					var n = oSettings.aanFeatures.f;
+					for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+					{
+						// IE9 throws an 'unknown error' if document.activeElement is used
+						// inside an iframe or frame...
+						try {
+							if ( n[i]._DT_Input != document.activeElement )
+							{
+								$(n[i]._DT_Input).val( sInput );
+							}
+						}
+						catch ( e ) {
+							$(n[i]._DT_Input).val( sInput );
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Single column filter */
+				$.extend( oSettings.aoPreSearchCols[ iColumn ], {
+					"sSearch": sInput+"",
+					"bRegex": bRegex,
+					"bSmart": bSmart,
+					"bCaseInsensitive": bCaseInsensitive
+				} );
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
+			}
+		};
+		
+		
+		/**
+		 * Get the data for the whole table, an individual row or an individual cell based on the 
+		 * provided parameters.
+		 *  @param {int|node} [mRow] A TR row node, TD/TH cell node or an integer. If given as
+		 *    a TR node then the data source for the whole row will be returned. If given as a
+		 *    TD/TH cell node then iCol will be automatically calculated and the data for the
+		 *    cell returned. If given as an integer, then this is treated as the aoData internal
+		 *    data index for the row (see fnGetPosition) and the data for that row used.
+		 *  @param {int} [iCol] Optional column index that you want the data of.
+		 *  @returns {array|object|string} If mRow is undefined, then the data for all rows is
+		 *    returned. If mRow is defined, just data for that row, and is iCol is
+		 *    defined, only data for the designated cell is returned.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    // Row data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('tr').click( function () {
+		 *        var data = oTable.fnGetData( this );
+		 *        // ... do something with the array / object of data for the row
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Individual cell data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('td').click( function () {
+		 *        var sData = oTable.fnGetData( this );
+		 *        alert( 'The cell clicked on had the value of '+sData );
+		 *      } );
+		 *    } );
+		 */
+		this.fnGetData = function( mRow, iCol )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( mRow !== undefined )
+			{
+				var iRow = mRow;
+				if ( typeof mRow === 'object' )
+				{
+					var sNode = mRow.nodeName.toLowerCase();
+					if (sNode === "tr" )
+					{
+						iRow = _fnNodeToDataIndex(oSettings, mRow);
+					}
+					else if ( sNode === "td" )
+					{
+						iRow = _fnNodeToDataIndex(oSettings, mRow.parentNode);
+						iCol = _fnNodeToColumnIndex( oSettings, iRow, mRow );
+					}
+				}
+		
+				if ( iCol !== undefined )
+				{
+					return _fnGetCellData( oSettings, iRow, iCol, '' );
+				}
+				return (oSettings.aoData[iRow]!==undefined) ?
+					oSettings.aoData[iRow]._aData : null;
+			}
+			return _fnGetDataMaster( oSettings );
+		};
+		
+		
+		/**
+		 * Get an array of the TR nodes that are used in the table's body. Note that you will 
+		 * typically want to use the '$' API method in preference to this as it is more 
+		 * flexible.
+		 *  @param {int} [iRow] Optional row index for the TR element you want
+		 *  @returns {array|node} If iRow is undefined, returns an array of all TR elements
+		 *    in the table's body, or iRow is defined, just the TR element requested.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Get the nodes from the table
+		 *      var nNodes = oTable.fnGetNodes( );
+		 *    } );
+		 */
+		this.fnGetNodes = function( iRow )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( iRow !== undefined ) {
+				return (oSettings.aoData[iRow]!==undefined) ?
+					oSettings.aoData[iRow].nTr : null;
+			}
+			return _fnGetTrNodes( oSettings );
+		};
+		
+		
+		/**
+		 * Get the array indexes of a particular cell from it's DOM element
+		 * and column index including hidden columns
+		 *  @param {node} nNode this can either be a TR, TD or TH in the table's body
+		 *  @returns {int} If nNode is given as a TR, then a single index is returned, or
+		 *    if given as a cell, an array of [row index, column index (visible), 
+		 *    column index (all)] is given.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      $('#example tbody td').click( function () {
+		 *        // Get the position of the current data from the node
+		 *        var aPos = oTable.fnGetPosition( this );
+		 *        
+		 *        // Get the data array for this row
+		 *        var aData = oTable.fnGetData( aPos[0] );
+		 *        
+		 *        // Update the data array and return the value
+		 *        aData[ aPos[1] ] = 'clicked';
+		 *        this.innerHTML = 'clicked';
+		 *      } );
+		 *      
+		 *      // Init DataTables
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnGetPosition = function( nNode )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var sNodeName = nNode.nodeName.toUpperCase();
+			
+			if ( sNodeName == "TR" )
+			{
+				return _fnNodeToDataIndex(oSettings, nNode);
+			}
+			else if ( sNodeName == "TD" || sNodeName == "TH" )
+			{
+				var iDataIndex = _fnNodeToDataIndex( oSettings, nNode.parentNode );
+				var iColumnIndex = _fnNodeToColumnIndex( oSettings, iDataIndex, nNode );
+				return [ iDataIndex, _fnColumnIndexToVisible(oSettings, iColumnIndex ), iColumnIndex ];
+			}
+			return null;
+		};
+		
+		
+		/**
+		 * Check to see if a row is 'open' or not.
+		 *  @param {node} nTr the table row to check
+		 *  @returns {boolean} true if the row is currently open, false otherwise
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnIsOpen = function( nTr )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var aoOpenRows = oSettings.aoOpenRows;
+			
+			for ( var i=0 ; i<oSettings.aoOpenRows.length ; i++ )
+			{
+				if ( oSettings.aoOpenRows[i].nParent == nTr )
+				{
+					return true;
+				}
+			}
+			return false;
+		};
+		
+		
+		/**
+		 * This function will place a new row directly after a row which is currently 
+		 * on display on the page, with the HTML contents that is passed into the 
+		 * function. This can be used, for example, to ask for confirmation that a 
+		 * particular record should be deleted.
+		 *  @param {node} nTr The table row to 'open'
+		 *  @param {string|node|jQuery} mHtml The HTML to put into the row
+		 *  @param {string} sClass Class to give the new TD cell
+		 *  @returns {node} The row opened. Note that if the table row passed in as the
+		 *    first parameter, is not found in the table, this method will silently
+		 *    return.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnOpen = function( nTr, mHtml, sClass )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+		
+			/* Check that the row given is in the table */
+			var nTableRows = _fnGetTrNodes( oSettings );
+			if ( $.inArray(nTr, nTableRows) === -1 )
+			{
+				return;
+			}
+			
+			/* the old open one if there is one */
+			this.fnClose( nTr );
+			
+			var nNewRow = document.createElement("tr");
+			var nNewCell = document.createElement("td");
+			nNewRow.appendChild( nNewCell );
+			nNewCell.className = sClass;
+			nNewCell.colSpan = _fnVisbleColumns( oSettings );
+		
+			if (typeof mHtml === "string")
+			{
+				nNewCell.innerHTML = mHtml;
+			}
+			else
+			{
+				$(nNewCell).html( mHtml );
+			}
+		
+			/* If the nTr isn't on the page at the moment - then we don't insert at the moment */
+			var nTrs = $('tr', oSettings.nTBody);
+			if ( $.inArray(nTr, nTrs) != -1  )
+			{
+				$(nNewRow).insertAfter(nTr);
+			}
+			
+			oSettings.aoOpenRows.push( {
+				"nTr": nNewRow,
+				"nParent": nTr
+			} );
+			
+			return nNewRow;
+		};
+		
+		
+		/**
+		 * Change the pagination - provides the internal logic for pagination in a simple API 
+		 * function. With this function you can have a DataTables table go to the next, 
+		 * previous, first or last pages.
+		 *  @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+		 *    or page number to jump to (integer), note that page 0 is the first page.
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnPageChange( 'next' );
+		 *    } );
+		 */
+		this.fnPageChange = function ( mAction, bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			_fnPageChange( oSettings, mAction );
+			_fnCalculateEnd( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * Show a particular column
+		 *  @param {int} iCol The column whose display should be changed
+		 *  @param {bool} bShow Show (true) or hide (false) the column
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Hide the second column after initialisation
+		 *      oTable.fnSetColumnVis( 1, false );
+		 *    } );
+		 */
+		this.fnSetColumnVis = function ( iCol, bShow, bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen;
+			var aoColumns = oSettings.aoColumns;
+			var aoData = oSettings.aoData;
+			var nTd, bAppend, iBefore;
+			
+			/* No point in doing anything if we are requesting what is already true */
+			if ( aoColumns[iCol].bVisible == bShow )
+			{
+				return;
+			}
+			
+			/* Show the column */
+			if ( bShow )
+			{
+				var iInsert = 0;
+				for ( i=0 ; i<iCol ; i++ )
+				{
+					if ( aoColumns[i].bVisible )
+					{
+						iInsert++;
+					}
+				}
+				
+				/* Need to decide if we should use appendChild or insertBefore */
+				bAppend = (iInsert >= _fnVisbleColumns( oSettings ));
+		
+				/* Which coloumn should we be inserting before? */
+				if ( !bAppend )
+				{
+					for ( i=iCol ; i<aoColumns.length ; i++ )
+					{
+						if ( aoColumns[i].bVisible )
+						{
+							iBefore = i;
+							break;
+						}
+					}
+				}
+		
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					if ( aoData[i].nTr !== null )
+					{
+						if ( bAppend )
+						{
+							aoData[i].nTr.appendChild( 
+								aoData[i]._anHidden[iCol]
+							);
+						}
+						else
+						{
+							aoData[i].nTr.insertBefore(
+								aoData[i]._anHidden[iCol], 
+								_fnGetTdNodes( oSettings, i )[iBefore] );
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Remove a column from display */
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					if ( aoData[i].nTr !== null )
+					{
+						nTd = _fnGetTdNodes( oSettings, i )[iCol];
+						aoData[i]._anHidden[iCol] = nTd;
+						nTd.parentNode.removeChild( nTd );
+					}
+				}
+			}
+		
+			/* Clear to set the visible flag */
+			aoColumns[iCol].bVisible = bShow;
+		
+			/* Redraw the header and footer based on the new column visibility */
+			_fnDrawHead( oSettings, oSettings.aoHeader );
+			if ( oSettings.nTFoot )
+			{
+				_fnDrawHead( oSettings, oSettings.aoFooter );
+			}
+			
+			/* If there are any 'open' rows, then we need to alter the colspan for this col change */
+			for ( i=0, iLen=oSettings.aoOpenRows.length ; i<iLen ; i++ )
+			{
+				oSettings.aoOpenRows[i].nTr.colSpan = _fnVisbleColumns( oSettings );
+			}
+			
+			/* Do a redraw incase anything depending on the table columns needs it 
+			 * (built-in: scrolling) 
+			 */
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnAdjustColumnSizing( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			_fnSaveState( oSettings );
+		};
+		
+		
+		/**
+		 * Get the settings for a particular table for external manipulation
+		 *  @returns {object} DataTables settings object. See 
+		 *    {@link DataTable.models.oSettings}
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      var oSettings = oTable.fnSettings();
+		 *      
+		 *      // Show an example parameter from the settings
+		 *      alert( oSettings._iDisplayStart );
+		 *    } );
+		 */
+		this.fnSettings = function()
+		{
+			return _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+		};
+		
+		
+		/**
+		 * Sort the table by a particular column
+		 *  @param {int} iCol the data index to sort on. Note that this will not match the 
+		 *    'display index' if you have hidden data entries
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sort immediately with columns 0 and 1
+		 *      oTable.fnSort( [ [0,'asc'], [1,'asc'] ] );
+		 *    } );
+		 */
+		this.fnSort = function( aaSort )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			oSettings.aaSorting = aaSort;
+			_fnSort( oSettings );
+		};
+		
+		
+		/**
+		 * Attach a sort listener to an element for a given column
+		 *  @param {node} nNode the element to attach the sort listener to
+		 *  @param {int} iColumn the column that a click on this node will sort on
+		 *  @param {function} [fnCallback] callback function when sort is run
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sort on column 1, when 'sorter' is clicked on
+		 *      oTable.fnSortListener( document.getElementById('sorter'), 1 );
+		 *    } );
+		 */
+		this.fnSortListener = function( nNode, iColumn, fnCallback )
+		{
+			_fnSortAttachListener( _fnSettingsFromNode( this[DataTable.ext.iApiIndex] ), nNode, iColumn,
+			 	fnCallback );
+		};
+		
+		
+		/**
+		 * Update a table cell or row - this method will accept either a single value to
+		 * update the cell with, an array of values with one element for each column or
+		 * an object in the same format as the original data source. The function is
+		 * self-referencing in order to make the multi column updates easier.
+		 *  @param {object|array|string} mData Data to update the cell/row with
+		 *  @param {node|int} mRow TR element you want to update or the aoData index
+		 *  @param {int} [iColumn] The column to update (not used of mData is an array or object)
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @param {bool} [bAction=true] Perform pre-draw actions or not
+		 *  @returns {int} 0 on success, 1 on error
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell
+		 *      oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], 1, 0 ); // Row
+		 *    } );
+		 */
+		this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen, sDisplay;
+			var iRow = (typeof mRow === 'object') ? 
+				_fnNodeToDataIndex(oSettings, mRow) : mRow;
+			
+			if ( $.isArray(mData) && iColumn === undefined )
+			{
+				/* Array update - update the whole row */
+				oSettings.aoData[iRow]._aData = mData.slice();
+				
+				/* Flag to the function that we are recursing */
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					this.fnUpdate( _fnGetCellData( oSettings, iRow, i ), iRow, i, false, false );
+				}
+			}
+			else if ( $.isPlainObject(mData) && iColumn === undefined )
+			{
+				/* Object update - update the whole row - assume the developer gets the object right */
+				oSettings.aoData[iRow]._aData = $.extend( true, {}, mData );
+		
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					this.fnUpdate( _fnGetCellData( oSettings, iRow, i ), iRow, i, false, false );
+				}
+			}
+			else
+			{
+				/* Individual cell update */
+				_fnSetCellData( oSettings, iRow, iColumn, mData );
+				sDisplay = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+				
+				var oCol = oSettings.aoColumns[iColumn];
+				if ( oCol.fnRender !== null )
+				{
+					sDisplay = _fnRender( oSettings, iRow, iColumn );
+					if ( oCol.bUseRendered )
+					{
+						_fnSetCellData( oSettings, iRow, iColumn, sDisplay );
+					}
+				}
+				
+				if ( oSettings.aoData[iRow].nTr !== null )
+				{
+					/* Do the actual HTML update */
+					_fnGetTdNodes( oSettings, iRow )[iColumn].innerHTML = sDisplay;
+				}
+			}
+			
+			/* Modify the search index for this row (strictly this is likely not needed, since fnReDraw
+			 * will rebuild the search array - however, the redraw might be disabled by the user)
+			 */
+			var iDisplayIndex = $.inArray( iRow, oSettings.aiDisplay );
+			oSettings.asDataSearch[iDisplayIndex] = _fnBuildSearchRow(
+				oSettings, 
+				_fnGetRowData( oSettings, iRow, 'filter', _fnGetColumns( oSettings, 'bSearchable' ) )
+			);
+			
+			/* Perform pre-draw actions */
+			if ( bAction === undefined || bAction )
+			{
+				_fnAdjustColumnSizing( oSettings );
+			}
+			
+			/* Redraw the table */
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnReDraw( oSettings );
+			}
+			return 0;
+		};
+		
+		
+		/**
+		 * Provide a common method for plug-ins to check the version of DataTables being used, in order
+		 * to ensure compatibility.
+		 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+		 *    formats "X" and "X.Y" are also acceptable.
+		 *  @returns {boolean} true if this version of DataTables is greater or equal to the required
+		 *    version, or false if this version of DataTales is not suitable
+		 *  @method
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      alert( oTable.fnVersionCheck( '1.9.0' ) );
+		 *    } );
+		 */
+		this.fnVersionCheck = DataTable.ext.fnVersionCheck;
+		
+		
+		/*
+		 * This is really a good bit rubbish this method of exposing the internal methods
+		 * publicly... - To be fixed in 2.0 using methods on the prototype
+		 */
+		
+		
+		/**
+		 * Create a wrapper function for exporting an internal functions to an external API.
+		 *  @param {string} sFunc API function name
+		 *  @returns {function} wrapped function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnExternApiFunc (sFunc)
+		{
+			return function() {
+				var aArgs = [_fnSettingsFromNode(this[DataTable.ext.iApiIndex])].concat( 
+					Array.prototype.slice.call(arguments) );
+				return DataTable.ext.oApi[sFunc].apply( this, aArgs );
+			};
+		}
+		
+		
+		/**
+		 * Reference to internal functions for use by plug-in developers. Note that these
+		 * methods are references to internal functions and are considered to be private.
+		 * If you use these methods, be aware that they are liable to change between versions
+		 * (check the upgrade notes).
+		 *  @namespace
+		 */
+		this.oApi = {
+			"_fnExternApiFunc": _fnExternApiFunc,
+			"_fnInitialise": _fnInitialise,
+			"_fnInitComplete": _fnInitComplete,
+			"_fnLanguageCompat": _fnLanguageCompat,
+			"_fnAddColumn": _fnAddColumn,
+			"_fnColumnOptions": _fnColumnOptions,
+			"_fnAddData": _fnAddData,
+			"_fnCreateTr": _fnCreateTr,
+			"_fnGatherData": _fnGatherData,
+			"_fnBuildHead": _fnBuildHead,
+			"_fnDrawHead": _fnDrawHead,
+			"_fnDraw": _fnDraw,
+			"_fnReDraw": _fnReDraw,
+			"_fnAjaxUpdate": _fnAjaxUpdate,
+			"_fnAjaxParameters": _fnAjaxParameters,
+			"_fnAjaxUpdateDraw": _fnAjaxUpdateDraw,
+			"_fnServerParams": _fnServerParams,
+			"_fnAddOptionsHtml": _fnAddOptionsHtml,
+			"_fnFeatureHtmlTable": _fnFeatureHtmlTable,
+			"_fnScrollDraw": _fnScrollDraw,
+			"_fnAdjustColumnSizing": _fnAdjustColumnSizing,
+			"_fnFeatureHtmlFilter": _fnFeatureHtmlFilter,
+			"_fnFilterComplete": _fnFilterComplete,
+			"_fnFilterCustom": _fnFilterCustom,
+			"_fnFilterColumn": _fnFilterColumn,
+			"_fnFilter": _fnFilter,
+			"_fnBuildSearchArray": _fnBuildSearchArray,
+			"_fnBuildSearchRow": _fnBuildSearchRow,
+			"_fnFilterCreateSearch": _fnFilterCreateSearch,
+			"_fnDataToSearch": _fnDataToSearch,
+			"_fnSort": _fnSort,
+			"_fnSortAttachListener": _fnSortAttachListener,
+			"_fnSortingClasses": _fnSortingClasses,
+			"_fnFeatureHtmlPaginate": _fnFeatureHtmlPaginate,
+			"_fnPageChange": _fnPageChange,
+			"_fnFeatureHtmlInfo": _fnFeatureHtmlInfo,
+			"_fnUpdateInfo": _fnUpdateInfo,
+			"_fnFeatureHtmlLength": _fnFeatureHtmlLength,
+			"_fnFeatureHtmlProcessing": _fnFeatureHtmlProcessing,
+			"_fnProcessingDisplay": _fnProcessingDisplay,
+			"_fnVisibleToColumnIndex": _fnVisibleToColumnIndex,
+			"_fnColumnIndexToVisible": _fnColumnIndexToVisible,
+			"_fnNodeToDataIndex": _fnNodeToDataIndex,
+			"_fnVisbleColumns": _fnVisbleColumns,
+			"_fnCalculateEnd": _fnCalculateEnd,
+			"_fnConvertToWidth": _fnConvertToWidth,
+			"_fnCalculateColumnWidths": _fnCalculateColumnWidths,
+			"_fnScrollingWidthAdjust": _fnScrollingWidthAdjust,
+			"_fnGetWidestNode": _fnGetWidestNode,
+			"_fnGetMaxLenString": _fnGetMaxLenString,
+			"_fnStringToCss": _fnStringToCss,
+			"_fnDetectType": _fnDetectType,
+			"_fnSettingsFromNode": _fnSettingsFromNode,
+			"_fnGetDataMaster": _fnGetDataMaster,
+			"_fnGetTrNodes": _fnGetTrNodes,
+			"_fnGetTdNodes": _fnGetTdNodes,
+			"_fnEscapeRegex": _fnEscapeRegex,
+			"_fnDeleteIndex": _fnDeleteIndex,
+			"_fnReOrderIndex": _fnReOrderIndex,
+			"_fnColumnOrdering": _fnColumnOrdering,
+			"_fnLog": _fnLog,
+			"_fnClearTable": _fnClearTable,
+			"_fnSaveState": _fnSaveState,
+			"_fnLoadState": _fnLoadState,
+			"_fnCreateCookie": _fnCreateCookie,
+			"_fnReadCookie": _fnReadCookie,
+			"_fnDetectHeader": _fnDetectHeader,
+			"_fnGetUniqueThs": _fnGetUniqueThs,
+			"_fnScrollBarWidth": _fnScrollBarWidth,
+			"_fnApplyToChildren": _fnApplyToChildren,
+			"_fnMap": _fnMap,
+			"_fnGetRowData": _fnGetRowData,
+			"_fnGetCellData": _fnGetCellData,
+			"_fnSetCellData": _fnSetCellData,
+			"_fnGetObjectDataFn": _fnGetObjectDataFn,
+			"_fnSetObjectDataFn": _fnSetObjectDataFn,
+			"_fnApplyColumnDefs": _fnApplyColumnDefs,
+			"_fnBindAction": _fnBindAction,
+			"_fnExtend": _fnExtend,
+			"_fnCallbackReg": _fnCallbackReg,
+			"_fnCallbackFire": _fnCallbackFire,
+			"_fnJsonString": _fnJsonString,
+			"_fnRender": _fnRender,
+			"_fnNodeToColumnIndex": _fnNodeToColumnIndex,
+			"_fnInfoMacros": _fnInfoMacros,
+			"_fnBrowserDetect": _fnBrowserDetect,
+			"_fnGetColumns": _fnGetColumns
+		};
+		
+		$.extend( DataTable.ext.oApi, this.oApi );
+		
+		for ( var sFunc in DataTable.ext.oApi )
+		{
+			if ( sFunc )
+			{
+				this[sFunc] = _fnExternApiFunc(sFunc);
+			}
+		}
+		
+		
+		var _that = this;
+		this.each(function() {
+			var i=0, iLen, j, jLen, k, kLen;
+			var sId = this.getAttribute( 'id' );
+			var bInitHandedOff = false;
+			var bUsePassedData = false;
+			
+			
+			/* Sanity check */
+			if ( this.nodeName.toLowerCase() != 'table' )
+			{
+				_fnLog( null, 0, "Attempted to initialise DataTables on a node which is not a "+
+					"table: "+this.nodeName );
+				return;
+			}
+			
+			/* Check to see if we are re-initialising a table */
+			for ( i=0, iLen=DataTable.settings.length ; i<iLen ; i++ )
+			{
+				/* Base check on table node */
+				if ( DataTable.settings[i].nTable == this )
+				{
+					if ( oInit === undefined || oInit.bRetrieve )
+					{
+						return DataTable.settings[i].oInstance;
+					}
+					else if ( oInit.bDestroy )
+					{
+						DataTable.settings[i].oInstance.fnDestroy();
+						break;
+					}
+					else
+					{
+						_fnLog( DataTable.settings[i], 0, "Cannot reinitialise DataTable.\n\n"+
+							"To retrieve the DataTables object for this table, pass no arguments or see "+
+							"the docs for bRetrieve and bDestroy" );
+						return;
+					}
+				}
+				
+				/* If the element we are initialising has the same ID as a table which was previously
+				 * initialised, but the table nodes don't match (from before) then we destroy the old
+				 * instance by simply deleting it. This is under the assumption that the table has been
+				 * destroyed by other methods. Anyone using non-id selectors will need to do this manually
+				 */
+				if ( DataTable.settings[i].sTableId == this.id )
+				{
+					DataTable.settings.splice( i, 1 );
+					break;
+				}
+			}
+			
+			/* Ensure the table has an ID - required for accessibility */
+			if ( sId === null || sId === "" )
+			{
+				sId = "DataTables_Table_"+(DataTable.ext._oExternConfig.iNextUnique++);
+				this.id = sId;
+			}
+			
+			/* Create the settings object for this table and set some of the default parameters */
+			var oSettings = $.extend( true, {}, DataTable.models.oSettings, {
+				"nTable":        this,
+				"oApi":          _that.oApi,
+				"oInit":         oInit,
+				"sDestroyWidth": $(this).width(),
+				"sInstance":     sId,
+				"sTableId":      sId
+			} );
+			DataTable.settings.push( oSettings );
+			
+			// Need to add the instance after the instance after the settings object has been added
+			// to the settings array, so we can self reference the table instance if more than one
+			oSettings.oInstance = (_that.length===1) ? _that : $(this).dataTable();
+			
+			/* Setting up the initialisation object */
+			if ( !oInit )
+			{
+				oInit = {};
+			}
+			
+			// Backwards compatibility, before we apply all the defaults
+			if ( oInit.oLanguage )
+			{
+				_fnLanguageCompat( oInit.oLanguage );
+			}
+			
+			oInit = _fnExtend( $.extend(true, {}, DataTable.defaults), oInit );
+			
+			// Map the initialisation options onto the settings object
+			_fnMap( oSettings.oFeatures, oInit, "bPaginate" );
+			_fnMap( oSettings.oFeatures, oInit, "bLengthChange" );
+			_fnMap( oSettings.oFeatures, oInit, "bFilter" );
+			_fnMap( oSettings.oFeatures, oInit, "bSort" );
+			_fnMap( oSettings.oFeatures, oInit, "bInfo" );
+			_fnMap( oSettings.oFeatures, oInit, "bProcessing" );
+			_fnMap( oSettings.oFeatures, oInit, "bAutoWidth" );
+			_fnMap( oSettings.oFeatures, oInit, "bSortClasses" );
+			_fnMap( oSettings.oFeatures, oInit, "bServerSide" );
+			_fnMap( oSettings.oFeatures, oInit, "bDeferRender" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollX", "sX" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollXInner", "sXInner" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollY", "sY" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollCollapse", "bCollapse" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollInfinite", "bInfinite" );
+			_fnMap( oSettings.oScroll, oInit, "iScrollLoadGap", "iLoadGap" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollAutoCss", "bAutoCss" );
+			_fnMap( oSettings, oInit, "asStripeClasses" );
+			_fnMap( oSettings, oInit, "asStripClasses", "asStripeClasses" ); // legacy
+			_fnMap( oSettings, oInit, "fnServerData" );
+			_fnMap( oSettings, oInit, "fnFormatNumber" );
+			_fnMap( oSettings, oInit, "sServerMethod" );
+			_fnMap( oSettings, oInit, "aaSorting" );
+			_fnMap( oSettings, oInit, "aaSortingFixed" );
+			_fnMap( oSettings, oInit, "aLengthMenu" );
+			_fnMap( oSettings, oInit, "sPaginationType" );
+			_fnMap( oSettings, oInit, "sAjaxSource" );
+			_fnMap( oSettings, oInit, "sAjaxDataProp" );
+			_fnMap( oSettings, oInit, "iCookieDuration" );
+			_fnMap( oSettings, oInit, "sCookiePrefix" );
+			_fnMap( oSettings, oInit, "sDom" );
+			_fnMap( oSettings, oInit, "bSortCellsTop" );
+			_fnMap( oSettings, oInit, "iTabIndex" );
+			_fnMap( oSettings, oInit, "oSearch", "oPreviousSearch" );
+			_fnMap( oSettings, oInit, "aoSearchCols", "aoPreSearchCols" );
+			_fnMap( oSettings, oInit, "iDisplayLength", "_iDisplayLength" );
+			_fnMap( oSettings, oInit, "bJQueryUI", "bJUI" );
+			_fnMap( oSettings, oInit, "fnCookieCallback" );
+			_fnMap( oSettings, oInit, "fnStateLoad" );
+			_fnMap( oSettings, oInit, "fnStateSave" );
+			_fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" );
+			
+			/* Callback functions which are array driven */
+			_fnCallbackReg( oSettings, 'aoDrawCallback',       oInit.fnDrawCallback,      'user' );
+			_fnCallbackReg( oSettings, 'aoServerParams',       oInit.fnServerParams,      'user' );
+			_fnCallbackReg( oSettings, 'aoStateSaveParams',    oInit.fnStateSaveParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoadParams',    oInit.fnStateLoadParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoaded',        oInit.fnStateLoaded,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCallback',        oInit.fnRowCallback,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow,        'user' );
+			_fnCallbackReg( oSettings, 'aoHeaderCallback',     oInit.fnHeaderCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoFooterCallback',     oInit.fnFooterCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoInitComplete',       oInit.fnInitComplete,      'user' );
+			_fnCallbackReg( oSettings, 'aoPreDrawCallback',    oInit.fnPreDrawCallback,   'user' );
+			
+			if ( oSettings.oFeatures.bServerSide && oSettings.oFeatures.bSort &&
+				   oSettings.oFeatures.bSortClasses )
+			{
+				/* Enable sort classes for server-side processing. Safe to do it here, since server-side
+				 * processing must be enabled by the developer
+				 */
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSortingClasses, 'server_side_sort_classes' );
+			}
+			else if ( oSettings.oFeatures.bDeferRender )
+			{
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSortingClasses, 'defer_sort_classes' );
+			}
+			
+			if ( oInit.bJQueryUI )
+			{
+				/* Use the JUI classes object for display. You could clone the oStdClasses object if 
+				 * you want to have multiple tables with multiple independent classes 
+				 */
+				$.extend( oSettings.oClasses, DataTable.ext.oJUIClasses );
+				
+				if ( oInit.sDom === DataTable.defaults.sDom && DataTable.defaults.sDom === "lfrtip" )
+				{
+					/* Set the DOM to use a layout suitable for jQuery UI's theming */
+					oSettings.sDom = '<"H"lfr>t<"F"ip>';
+				}
+			}
+			else
+			{
+				$.extend( oSettings.oClasses, DataTable.ext.oStdClasses );
+			}
+			$(this).addClass( oSettings.oClasses.sTable );
+			
+			/* Calculate the scroll bar width and cache it for use later on */
+			if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" )
+			{
+				oSettings.oScroll.iBarWidth = _fnScrollBarWidth();
+			}
+			
+			if ( oSettings.iInitDisplayStart === undefined )
+			{
+				/* Display start point, taking into account the save saving */
+				oSettings.iInitDisplayStart = oInit.iDisplayStart;
+				oSettings._iDisplayStart = oInit.iDisplayStart;
+			}
+			
+			/* Must be done after everything which can be overridden by a cookie! */
+			if ( oInit.bStateSave )
+			{
+				oSettings.oFeatures.bStateSave = true;
+				_fnLoadState( oSettings, oInit );
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' );
+			}
+			
+			if ( oInit.iDeferLoading !== null )
+			{
+				oSettings.bDeferLoading = true;
+				var tmp = $.isArray( oInit.iDeferLoading );
+				oSettings._iRecordsDisplay = tmp ? oInit.iDeferLoading[0] : oInit.iDeferLoading;
+				oSettings._iRecordsTotal = tmp ? oInit.iDeferLoading[1] : oInit.iDeferLoading;
+			}
+			
+			if ( oInit.aaData !== null )
+			{
+				bUsePassedData = true;
+			}
+			
+			/* Language definitions */
+			if ( oInit.oLanguage.sUrl !== "" )
+			{
+				/* Get the language definitions from a file - because this Ajax call makes the language
+				 * get async to the remainder of this function we use bInitHandedOff to indicate that 
+				 * _fnInitialise will be fired by the returned Ajax handler, rather than the constructor
+				 */
+				oSettings.oLanguage.sUrl = oInit.oLanguage.sUrl;
+				$.getJSON( oSettings.oLanguage.sUrl, null, function( json ) {
+					_fnLanguageCompat( json );
+					$.extend( true, oSettings.oLanguage, oInit.oLanguage, json );
+					_fnInitialise( oSettings );
+				} );
+				bInitHandedOff = true;
+			}
+			else
+			{
+				$.extend( true, oSettings.oLanguage, oInit.oLanguage );
+			}
+			
+			
+			/*
+			 * Stripes
+			 */
+			if ( oInit.asStripeClasses === null )
+			{
+				oSettings.asStripeClasses =[
+					oSettings.oClasses.sStripeOdd,
+					oSettings.oClasses.sStripeEven
+				];
+			}
+			
+			/* Remove row stripe classes if they are already on the table row */
+			iLen=oSettings.asStripeClasses.length;
+			oSettings.asDestroyStripes = [];
+			if (iLen)
+			{
+				var bStripeRemove = false;
+				var anRows = $(this).children('tbody').children('tr:lt(' + iLen + ')');
+				for ( i=0 ; i<iLen ; i++ )
+				{
+					if ( anRows.hasClass( oSettings.asStripeClasses[i] ) )
+					{
+						bStripeRemove = true;
+						
+						/* Store the classes which we are about to remove so they can be re-added on destroy */
+						oSettings.asDestroyStripes.push( oSettings.asStripeClasses[i] );
+					}
+				}
+				
+				if ( bStripeRemove )
+				{
+					anRows.removeClass( oSettings.asStripeClasses.join(' ') );
+				}
+			}
+			
+			/*
+			 * Columns
+			 * See if we should load columns automatically or use defined ones
+			 */
+			var anThs = [];
+			var aoColumnsInit;
+			var nThead = this.getElementsByTagName('thead');
+			if ( nThead.length !== 0 )
+			{
+				_fnDetectHeader( oSettings.aoHeader, nThead[0] );
+				anThs = _fnGetUniqueThs( oSettings );
+			}
+			
+			/* If not given a column array, generate one with nulls */
+			if ( oInit.aoColumns === null )
+			{
+				aoColumnsInit = [];
+				for ( i=0, iLen=anThs.length ; i<iLen ; i++ )
+				{
+					aoColumnsInit.push( null );
+				}
+			}
+			else
+			{
+				aoColumnsInit = oInit.aoColumns;
+			}
+			
+			/* Add the columns */
+			for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ )
+			{
+				/* Short cut - use the loop to check if we have column visibility state to restore */
+				if ( oInit.saved_aoColumns !== undefined && oInit.saved_aoColumns.length == iLen )
+				{
+					if ( aoColumnsInit[i] === null )
+					{
+						aoColumnsInit[i] = {};
+					}
+					aoColumnsInit[i].bVisible = oInit.saved_aoColumns[i].bVisible;
+				}
+				
+				_fnAddColumn( oSettings, anThs ? anThs[i] : null );
+			}
+			
+			/* Apply the column definitions */
+			_fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, aoColumnsInit, function (iCol, oDef) {
+				_fnColumnOptions( oSettings, iCol, oDef );
+			} );
+			
+			
+			/*
+			 * Sorting
+			 * Check the aaSorting array
+			 */
+			for ( i=0, iLen=oSettings.aaSorting.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aaSorting[i][0] >= oSettings.aoColumns.length )
+				{
+					oSettings.aaSorting[i][0] = 0;
+				}
+				var oColumn = oSettings.aoColumns[ oSettings.aaSorting[i][0] ];
+				
+				/* Add a default sorting index */
+				if ( oSettings.aaSorting[i][2] === undefined )
+				{
+					oSettings.aaSorting[i][2] = 0;
+				}
+				
+				/* If aaSorting is not defined, then we use the first indicator in asSorting */
+				if ( oInit.aaSorting === undefined && oSettings.saved_aaSorting === undefined )
+				{
+					oSettings.aaSorting[i][1] = oColumn.asSorting[0];
+				}
+				
+				/* Set the current sorting index based on aoColumns.asSorting */
+				for ( j=0, jLen=oColumn.asSorting.length ; j<jLen ; j++ )
+				{
+					if ( oSettings.aaSorting[i][1] == oColumn.asSorting[j] )
+					{
+						oSettings.aaSorting[i][2] = j;
+						break;
+					}
+				}
+			}
+				
+			/* Do a first pass on the sorting classes (allows any size changes to be taken into
+			 * account, and also will apply sorting disabled classes if disabled
+			 */
+			_fnSortingClasses( oSettings );
+			
+			
+			/*
+			 * Final init
+			 * Cache the header, body and footer as required, creating them if needed
+			 */
+			
+			/* Browser support detection */
+			_fnBrowserDetect( oSettings );
+			
+			// Work around for Webkit bug 83867 - store the caption-side before removing from doc
+			var captions = $(this).children('caption').each( function () {
+				this._captionSide = $(this).css('caption-side');
+			} );
+			
+			var thead = $(this).children('thead');
+			if ( thead.length === 0 )
+			{
+				thead = [ document.createElement( 'thead' ) ];
+				this.appendChild( thead[0] );
+			}
+			oSettings.nTHead = thead[0];
+			
+			var tbody = $(this).children('tbody');
+			if ( tbody.length === 0 )
+			{
+				tbody = [ document.createElement( 'tbody' ) ];
+				this.appendChild( tbody[0] );
+			}
+			oSettings.nTBody = tbody[0];
+			oSettings.nTBody.setAttribute( "role", "alert" );
+			oSettings.nTBody.setAttribute( "aria-live", "polite" );
+			oSettings.nTBody.setAttribute( "aria-relevant", "all" );
+			
+			var tfoot = $(this).children('tfoot');
+			if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") )
+			{
+				// If we are a scrolling table, and no footer has been given, then we need to create
+				// a tfoot element for the caption element to be appended to
+				tfoot = [ document.createElement( 'tfoot' ) ];
+				this.appendChild( tfoot[0] );
+			}
+			
+			if ( tfoot.length > 0 )
+			{
+				oSettings.nTFoot = tfoot[0];
+				_fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot );
+			}
+			
+			/* Check if there is data passing into the constructor */
+			if ( bUsePassedData )
+			{
+				for ( i=0 ; i<oInit.aaData.length ; i++ )
+				{
+					_fnAddData( oSettings, oInit.aaData[ i ] );
+				}
+			}
+			else
+			{
+				/* Grab the data from the page */
+				_fnGatherData( oSettings );
+			}
+			
+			/* Copy the data index array */
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			/* Initialisation complete - table can be drawn */
+			oSettings.bInitialised = true;
+			
+			/* Check if we need to initialise the table (it might not have been handed off to the
+			 * language processor)
+			 */
+			if ( bInitHandedOff === false )
+			{
+				_fnInitialise( oSettings );
+			}
+		} );
+		_that = null;
+		return this;
+	};
+
+	
+	
+	/**
+	 * Provide a common method for plug-ins to check the version of DataTables being used, in order
+	 * to ensure compatibility.
+	 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+	 *    formats "X" and "X.Y" are also acceptable.
+	 *  @returns {boolean} true if this version of DataTables is greater or equal to the required
+	 *    version, or false if this version of DataTales is not suitable
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    alert( $.fn.dataTable.fnVersionCheck( '1.9.0' ) );
+	 */
+	DataTable.fnVersionCheck = function( sVersion )
+	{
+		/* This is cheap, but effective */
+		var fnZPad = function (Zpad, count)
+		{
+			while(Zpad.length < count) {
+				Zpad += '0';
+			}
+			return Zpad;
+		};
+		var aThis = DataTable.ext.sVersion.split('.');
+		var aThat = sVersion.split('.');
+		var sThis = '', sThat = '';
+		
+		for ( var i=0, iLen=aThat.length ; i<iLen ; i++ )
+		{
+			sThis += fnZPad( aThis[i], 3 );
+			sThat += fnZPad( aThat[i], 3 );
+		}
+		
+		return parseInt(sThis, 10) >= parseInt(sThat, 10);
+	};
+	
+	
+	/**
+	 * Check if a TABLE node is a DataTable table already or not.
+	 *  @param {node} nTable The TABLE node to check if it is a DataTable or not (note that other
+	 *    node types can be passed in, but will always return false).
+	 *  @returns {boolean} true the table given is a DataTable, or false otherwise
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    var ex = document.getElementById('example');
+	 *    if ( ! $.fn.DataTable.fnIsDataTable( ex ) ) {
+	 *      $(ex).dataTable();
+	 *    }
+	 */
+	DataTable.fnIsDataTable = function ( nTable )
+	{
+		var o = DataTable.settings;
+	
+		for ( var i=0 ; i<o.length ; i++ )
+		{
+			if ( o[i].nTable === nTable || o[i].nScrollHead === nTable || o[i].nScrollFoot === nTable )
+			{
+				return true;
+			}
+		}
+	
+		return false;
+	};
+	
+	
+	/**
+	 * Get all DataTable tables that have been initialised - optionally you can select to
+	 * get only currently visible tables.
+	 *  @param {boolean} [bVisible=false] Flag to indicate if you want all (default) or 
+	 *    visible tables only.
+	 *  @returns {array} Array of TABLE nodes (not DataTable instances) which are DataTables
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    var table = $.fn.dataTable.fnTables(true);
+	 *    if ( table.length > 0 ) {
+	 *      $(table).dataTable().fnAdjustColumnSizing();
+	 *    }
+	 */
+	DataTable.fnTables = function ( bVisible )
+	{
+		var out = [];
+	
+		jQuery.each( DataTable.settings, function (i, o) {
+			if ( !bVisible || (bVisible === true && $(o.nTable).is(':visible')) )
+			{
+				out.push( o.nTable );
+			}
+		} );
+	
+		return out;
+	};
+	
+
+	/**
+	 * Version string for plug-ins to check compatibility. Allowed format is
+	 * a.b.c.d.e where: a:int, b:int, c:int, d:string(dev|beta), e:int. d and
+	 * e are optional
+	 *  @member
+	 *  @type string
+	 *  @default Version number
+	 */
+	DataTable.version = "1.9.4";
+
+	/**
+	 * Private data store, containing all of the settings objects that are created for the
+	 * tables on a given page.
+	 * 
+	 * Note that the <i>DataTable.settings</i> object is aliased to <i>jQuery.fn.dataTableExt</i> 
+	 * through which it may be accessed and manipulated, or <i>jQuery.fn.dataTable.settings</i>.
+	 *  @member
+	 *  @type array
+	 *  @default []
+	 *  @private
+	 */
+	DataTable.settings = [];
+
+	/**
+	 * Object models container, for the various models that DataTables has available
+	 * to it. These models define the objects that are used to hold the active state 
+	 * and configuration of the table.
+	 *  @namespace
+	 */
+	DataTable.models = {};
+	
+	
+	/**
+	 * DataTables extension options and plug-ins. This namespace acts as a collection "area"
+	 * for plug-ins that can be used to extend the default DataTables behaviour - indeed many
+	 * of the build in methods use this method to provide their own capabilities (sorting methods
+	 * for example).
+	 * 
+	 * Note that this namespace is aliased to jQuery.fn.dataTableExt so it can be readily accessed
+	 * and modified by plug-ins.
+	 *  @namespace
+	 */
+	DataTable.models.ext = {
+		/**
+		 * Plug-in filtering functions - this method of filtering is complimentary to the default
+		 * type based filtering, and a lot more comprehensive as it allows you complete control
+		 * over the filtering logic. Each element in this array is a function (parameters
+		 * described below) that is called for every row in the table, and your logic decides if
+		 * it should be included in the filtered data set or not.
+		 *   <ul>
+		 *     <li>
+		 *       Function input parameters:
+		 *       <ul>
+		 *         <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *         <li>{array|object} Data for the row to be processed (same as the original format
+		 *           that was passed in as the data source, or an array from a DOM data source</li>
+		 *         <li>{int} Row index in aoData ({@link DataTable.models.oSettings.aoData}), which can
+		 *           be useful to retrieve the TR element if you need DOM interaction.</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{boolean} Include the row in the filtered result set (true) or not (false)</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 *
+		 *  @example
+		 *    // The following example shows custom filtering being applied to the fourth column (i.e.
+		 *    // the aData[3] index) based on two input values from the end-user, matching the data in 
+		 *    // a certain range.
+		 *    $.fn.dataTableExt.afnFiltering.push(
+		 *      function( oSettings, aData, iDataIndex ) {
+		 *        var iMin = document.getElementById('min').value * 1;
+		 *        var iMax = document.getElementById('max').value * 1;
+		 *        var iVersion = aData[3] == "-" ? 0 : aData[3]*1;
+		 *        if ( iMin == "" && iMax == "" ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin == "" && iVersion < iMax ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin < iVersion && "" == iMax ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin < iVersion && iVersion < iMax ) {
+		 *          return true;
+		 *        }
+		 *        return false;
+		 *      }
+		 *    );
+		 */
+		"afnFiltering": [],
+	
+	
+		/**
+		 * Plug-in sorting functions - this method of sorting is complimentary to the default type
+		 * based sorting that DataTables does automatically, allowing much greater control over the
+		 * the data that is being used to sort a column. This is useful if you want to do sorting
+		 * based on live data (for example the contents of an 'input' element) rather than just the
+		 * static string that DataTables knows of. The way these plug-ins work is that you create
+		 * an array of the values you wish to be sorted for the column in question and then return
+		 * that array. Which pre-sorting function is run here depends on the sSortDataType parameter
+		 * that is used for the column (if any). This is the corollary of <i>ofnSearch</i> for sort 
+		 * data.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+	     *         <li>{int} Target column index</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{array} Data for the column to be sorted upon</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  
+		 * Note that as of v1.9, it is typically preferable to use <i>mData</i> to prepare data for
+		 * the different uses that DataTables can put the data to. Specifically <i>mData</i> when
+		 * used as a function will give you a 'type' (sorting, filtering etc) that you can use to 
+		 * prepare the data as required for the different types. As such, this method is deprecated.
+		 *  @type array
+		 *  @default []
+		 *  @deprecated
+		 *
+		 *  @example
+		 *    // Updating the cached sorting information with user entered values in HTML input elements
+		 *    jQuery.fn.dataTableExt.afnSortData['dom-text'] = function ( oSettings, iColumn )
+		 *    {
+		 *      var aData = [];
+		 *      $( 'td:eq('+iColumn+') input', oSettings.oApi._fnGetTrNodes(oSettings) ).each( function () {
+		 *        aData.push( this.value );
+		 *      } );
+		 *      return aData;
+		 *    }
+		 */
+		"afnSortData": [],
+	
+	
+		/**
+		 * Feature plug-ins - This is an array of objects which describe the feature plug-ins that are
+		 * available to DataTables. These feature plug-ins are accessible through the sDom initialisation
+		 * option. As such, each feature plug-in must describe a function that is used to initialise
+		 * itself (fnInit), a character so the feature can be enabled by sDom (cFeature) and the name
+		 * of the feature (sFeature). Thus the objects attached to this method must provide:
+		 *   <ul>
+		 *     <li>{function} fnInit Initialisation of the plug-in
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>{node|null} The element which contains your feature. Note that the return
+		 *                may also be void if your plug-in does not require to inject any DOM elements 
+		 *                into DataTables control (sDom) - for example this might be useful when 
+		 *                developing a plug-in which allows table control via keyboard entry.</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </li>
+		 *     <li>{character} cFeature Character that will be matched in sDom - case sensitive</li>
+		 *     <li>{string} sFeature Feature name</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 * 
+		 *  @example
+		 *    // How TableTools initialises itself.
+		 *    $.fn.dataTableExt.aoFeatures.push( {
+		 *      "fnInit": function( oSettings ) {
+		 *        return new TableTools( { "oDTSettings": oSettings } );
+		 *      },
+		 *      "cFeature": "T",
+		 *      "sFeature": "TableTools"
+		 *    } );
+		 */
+		"aoFeatures": [],
+	
+	
+		/**
+		 * Type detection plug-in functions - DataTables utilises types to define how sorting and
+		 * filtering behave, and types can be either  be defined by the developer (sType for the
+		 * column) or they can be automatically detected by the methods in this array. The functions
+		 * defined in the array are quite simple, taking a single parameter (the data to analyse) 
+		 * and returning the type if it is a known type, or null otherwise.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data from the column cell to be analysed</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{string|null} Data type detected, or null if unknown (and thus pass it
+		 *           on to the other type detection functions.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 *  
+		 *  @example
+		 *    // Currency type detection plug-in:
+		 *    jQuery.fn.dataTableExt.aTypes.push(
+		 *      function ( sData ) {
+		 *        var sValidChars = "0123456789.-";
+		 *        var Char;
+		 *        
+		 *        // Check the numeric part
+		 *        for ( i=1 ; i<sData.length ; i++ ) {
+		 *          Char = sData.charAt(i); 
+		 *          if (sValidChars.indexOf(Char) == -1) {
+		 *            return null;
+		 *          }
+		 *        }
+		 *        
+		 *        // Check prefixed by currency
+		 *        if ( sData.charAt(0) == '$' || sData.charAt(0) == '&pound;' ) {
+		 *          return 'currency';
+		 *        }
+		 *        return null;
+		 *      }
+		 *    );
+		 */
+		"aTypes": [],
+	
+	
+		/**
+		 * Provide a common method for plug-ins to check the version of DataTables being used, 
+		 * in order to ensure compatibility.
+		 *  @type function
+		 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note 
+		 *    that the formats "X" and "X.Y" are also acceptable.
+		 *  @returns {boolean} true if this version of DataTables is greater or equal to the 
+		 *    required version, or false if this version of DataTales is not suitable
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      alert( oTable.fnVersionCheck( '1.9.0' ) );
+		 *    } );
+		 */
+		"fnVersionCheck": DataTable.fnVersionCheck,
+	
+	
+		/**
+		 * Index for what 'this' index API functions should use
+		 *  @type int
+		 *  @default 0
+		 */
+		"iApiIndex": 0,
+	
+	
+		/**
+		 * Pre-processing of filtering data plug-ins - When you assign the sType for a column
+		 * (or have it automatically detected for you by DataTables or a type detection plug-in), 
+		 * you will typically be using this for custom sorting, but it can also be used to provide 
+		 * custom filtering by allowing you to pre-processing the data and returning the data in
+		 * the format that should be filtered upon. This is done by adding functions this object 
+		 * with a parameter name which matches the sType for that target column. This is the
+		 * corollary of <i>afnSortData</i> for filtering data.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data from the column cell to be prepared for filtering</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{string|null} Formatted string that will be used for the filtering.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 * 
+		 * Note that as of v1.9, it is typically preferable to use <i>mData</i> to prepare data for
+		 * the different uses that DataTables can put the data to. Specifically <i>mData</i> when
+		 * used as a function will give you a 'type' (sorting, filtering etc) that you can use to 
+		 * prepare the data as required for the different types. As such, this method is deprecated.
+		 *  @type object
+		 *  @default {}
+		 *  @deprecated
+		 *
+		 *  @example
+		 *    $.fn.dataTableExt.ofnSearch['title-numeric'] = function ( sData ) {
+		 *      return sData.replace(/\n/g," ").replace( /<.*?>/g, "" );
+		 *    }
+		 */
+		"ofnSearch": {},
+	
+	
+		/**
+		 * Container for all private functions in DataTables so they can be exposed externally
+		 *  @type object
+		 *  @default {}
+		 */
+		"oApi": {},
+	
+	
+		/**
+		 * Storage for the various classes that DataTables uses
+		 *  @type object
+		 *  @default {}
+		 */
+		"oStdClasses": {},
+		
+	
+		/**
+		 * Storage for the various classes that DataTables uses - jQuery UI suitable
+		 *  @type object
+		 *  @default {}
+		 */
+		"oJUIClasses": {},
+	
+	
+		/**
+		 * Pagination plug-in methods - The style and controls of the pagination can significantly 
+		 * impact on how the end user interacts with the data in your table, and DataTables allows 
+		 * the addition of pagination controls by extending this object, which can then be enabled
+		 * through the <i>sPaginationType</i> initialisation parameter. Each pagination type that
+		 * is added is an object (the property name of which is what <i>sPaginationType</i> refers
+		 * to) that has two properties, both methods that are used by DataTables to update the
+		 * control's state.
+		 *   <ul>
+		 *     <li>
+		 *       fnInit -  Initialisation of the paging controls. Called only during initialisation 
+		 *         of the table. It is expected that this function will add the required DOM elements 
+		 *         to the page for the paging controls to work. The element pointer 
+		 *         'oSettings.aanFeatures.p' array is provided by DataTables to contain the paging 
+		 *         controls (note that this is a 2D array to allow for multiple instances of each 
+		 *         DataTables DOM element). It is suggested that you add the controls to this element 
+		 *         as children
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *             <li>{node} Container into which the pagination controls must be inserted</li>
+		 *             <li>{function} Draw callback function - whenever the controls cause a page
+		 *               change, this method must be called to redraw the table.</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>No return required</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </il>
+		 *     <li>
+		 *       fnInit -  This function is called whenever the paging status of the table changes and is
+		 *         typically used to update classes and/or text of the paging controls to reflex the new 
+		 *         status.
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *             <li>{function} Draw callback function - in case you need to redraw the table again
+		 *               or attach new event listeners</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>No return required</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type object
+		 *  @default {}
+		 *
+		 *  @example
+		 *    $.fn.dataTableExt.oPagination.four_button = {
+		 *      "fnInit": function ( oSettings, nPaging, fnCallbackDraw ) {
+		 *        nFirst = document.createElement( 'span' );
+		 *        nPrevious = document.createElement( 'span' );
+		 *        nNext = document.createElement( 'span' );
+		 *        nLast = document.createElement( 'span' );
+		 *        
+		 *        nFirst.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sFirst ) );
+		 *        nPrevious.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sPrevious ) );
+		 *        nNext.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sNext ) );
+		 *        nLast.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sLast ) );
+		 *        
+		 *        nFirst.className = "paginate_button first";
+		 *        nPrevious.className = "paginate_button previous";
+		 *        nNext.className="paginate_button next";
+		 *        nLast.className = "paginate_button last";
+		 *        
+		 *        nPaging.appendChild( nFirst );
+		 *        nPaging.appendChild( nPrevious );
+		 *        nPaging.appendChild( nNext );
+		 *        nPaging.appendChild( nLast );
+		 *        
+		 *        $(nFirst).click( function () {
+		 *          oSettings.oApi._fnPageChange( oSettings, "first" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nPrevious).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "previous" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nNext).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "next" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nLast).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "last" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nFirst).bind( 'selectstart', function () { return false; } );
+		 *        $(nPrevious).bind( 'selectstart', function () { return false; } );
+		 *        $(nNext).bind( 'selectstart', function () { return false; } );
+		 *        $(nLast).bind( 'selectstart', function () { return false; } );
+		 *      },
+		 *      
+		 *      "fnUpdate": function ( oSettings, fnCallbackDraw ) {
+		 *        if ( !oSettings.aanFeatures.p ) {
+		 *          return;
+		 *        }
+		 *        
+		 *        // Loop over each instance of the pager
+		 *        var an = oSettings.aanFeatures.p;
+		 *        for ( var i=0, iLen=an.length ; i<iLen ; i++ ) {
+		 *          var buttons = an[i].getElementsByTagName('span');
+		 *          if ( oSettings._iDisplayStart === 0 ) {
+		 *            buttons[0].className = "paginate_disabled_previous";
+		 *            buttons[1].className = "paginate_disabled_previous";
+		 *          }
+		 *          else {
+		 *            buttons[0].className = "paginate_enabled_previous";
+		 *            buttons[1].className = "paginate_enabled_previous";
+		 *          }
+		 *          
+		 *          if ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) {
+		 *            buttons[2].className = "paginate_disabled_next";
+		 *            buttons[3].className = "paginate_disabled_next";
+		 *          }
+		 *          else {
+		 *            buttons[2].className = "paginate_enabled_next";
+		 *            buttons[3].className = "paginate_enabled_next";
+		 *          }
+		 *        }
+		 *      }
+		 *    };
+		 */
+		"oPagination": {},
+	
+	
+		/**
+		 * Sorting plug-in methods - Sorting in DataTables is based on the detected type of the
+		 * data column (you can add your own type detection functions, or override automatic 
+		 * detection using sType). With this specific type given to the column, DataTables will 
+		 * apply the required sort from the functions in the object. Each sort type must provide
+		 * two mandatory methods, one each for ascending and descending sorting, and can optionally
+		 * provide a pre-formatting method that will help speed up sorting by allowing DataTables
+		 * to pre-format the sort data only once (rather than every time the actual sort functions
+		 * are run). The two sorting functions are typical Javascript sort methods:
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data to compare to the second parameter</li>
+		 *         <li>{*} Data to compare to the first parameter</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{int} Sorting match: <0 if first parameter should be sorted lower than
+		 *           the second parameter, ===0 if the two parameters are equal and >0 if
+		 *           the first parameter should be sorted height than the second parameter.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type object
+		 *  @default {}
+		 *
+		 *  @example
+		 *    // Case-sensitive string sorting, with no pre-formatting method
+		 *    $.extend( $.fn.dataTableExt.oSort, {
+		 *      "string-case-asc": function(x,y) {
+		 *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		 *      },
+		 *      "string-case-desc": function(x,y) {
+		 *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		 *      }
+		 *    } );
+		 *
+		 *  @example
+		 *    // Case-insensitive string sorting, with pre-formatting
+		 *    $.extend( $.fn.dataTableExt.oSort, {
+		 *      "string-pre": function(x) {
+		 *        return x.toLowerCase();
+		 *      },
+		 *      "string-asc": function(x,y) {
+		 *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		 *      },
+		 *      "string-desc": function(x,y) {
+		 *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		 *      }
+		 *    } );
+		 */
+		"oSort": {},
+	
+	
+		/**
+		 * Version string for plug-ins to check compatibility. Allowed format is
+		 * a.b.c.d.e where: a:int, b:int, c:int, d:string(dev|beta), e:int. d and
+		 * e are optional
+		 *  @type string
+		 *  @default Version number
+		 */
+		"sVersion": DataTable.version,
+	
+	
+		/**
+		 * How should DataTables report an error. Can take the value 'alert' or 'throw'
+		 *  @type string
+		 *  @default alert
+		 */
+		"sErrMode": "alert",
+	
+	
+		/**
+		 * Store information for DataTables to access globally about other instances
+		 *  @namespace
+		 *  @private
+		 */
+		"_oExternConfig": {
+			/* int:iNextUnique - next unique number for an instance */
+			"iNextUnique": 0
+		}
+	};
+	
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * search information for the global filter and individual column filters.
+	 *  @namespace
+	 */
+	DataTable.models.oSearch = {
+		/**
+		 * Flag to indicate if the filtering should be case insensitive or not
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bCaseInsensitive": true,
+	
+		/**
+		 * Applied search term
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sSearch": "",
+	
+		/**
+		 * Flag to indicate if the search term should be interpreted as a
+		 * regular expression (true) or not (false) and therefore and special
+		 * regex characters escaped.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bRegex": false,
+	
+		/**
+		 * Flag to indicate if DataTables is to use its smart filtering or not.
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bSmart": true
+	};
+	
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * each individual row. This is the object format used for the settings 
+	 * aoData array.
+	 *  @namespace
+	 */
+	DataTable.models.oRow = {
+		/**
+		 * TR element for the row
+		 *  @type node
+		 *  @default null
+		 */
+		"nTr": null,
+	
+		/**
+		 * Data object from the original data source for the row. This is either
+		 * an array if using the traditional form of DataTables, or an object if
+		 * using mData options. The exact type will depend on the passed in
+		 * data from the data source, or will be an array if using DOM a data 
+		 * source.
+		 *  @type array|object
+		 *  @default []
+		 */
+		"_aData": [],
+	
+		/**
+		 * Sorting data cache - this array is ostensibly the same length as the
+		 * number of columns (although each index is generated only as it is 
+		 * needed), and holds the data that is used for sorting each column in the
+		 * row. We do this cache generation at the start of the sort in order that
+		 * the formatting of the sort data need be done only once for each cell
+		 * per sort. This array should not be read from or written to by anything
+		 * other than the master sorting methods.
+		 *  @type array
+		 *  @default []
+		 *  @private
+		 */
+		"_aSortData": [],
+	
+		/**
+		 * Array of TD elements that are cached for hidden rows, so they can be
+		 * reinserted into the table if a column is made visible again (or to act
+		 * as a store if a column is made hidden). Only hidden columns have a 
+		 * reference in the array. For non-hidden columns the value is either
+		 * undefined or null.
+		 *  @type array nodes
+		 *  @default []
+		 *  @private
+		 */
+		"_anHidden": [],
+	
+		/**
+		 * Cache of the class name that DataTables has applied to the row, so we
+		 * can quickly look at this variable rather than needing to do a DOM check
+		 * on className for the nTr property.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @private
+		 */
+		"_sRowStripe": ""
+	};
+	
+	
+	
+	/**
+	 * Template object for the column information object in DataTables. This object
+	 * is held in the settings aoColumns array and contains all the information that
+	 * DataTables needs about each individual column.
+	 * 
+	 * Note that this object is related to {@link DataTable.defaults.columns} 
+	 * but this one is the internal data store for DataTables's cache of columns.
+	 * It should NOT be manipulated outside of DataTables. Any configuration should
+	 * be done through the initialisation options.
+	 *  @namespace
+	 */
+	DataTable.models.oColumn = {
+		/**
+		 * A list of the columns that sorting should occur on when this column
+		 * is sorted. That this property is an array allows multi-column sorting
+		 * to be defined for a column (for example first name / last name columns
+		 * would benefit from this). The values are integers pointing to the
+		 * columns to be sorted on (typically it will be a single integer pointing
+		 * at itself, but that doesn't need to be the case).
+		 *  @type array
+		 */
+		"aDataSort": null,
+	
+		/**
+		 * Define the sorting directions that are applied to the column, in sequence
+		 * as the column is repeatedly sorted upon - i.e. the first value is used
+		 * as the sorting direction when the column if first sorted (clicked on).
+		 * Sort it again (click again) and it will move on to the next index.
+		 * Repeat until loop.
+		 *  @type array
+		 */
+		"asSorting": null,
+		
+		/**
+		 * Flag to indicate if the column is searchable, and thus should be included
+		 * in the filtering or not.
+		 *  @type boolean
+		 */
+		"bSearchable": null,
+		
+		/**
+		 * Flag to indicate if the column is sortable or not.
+		 *  @type boolean
+		 */
+		"bSortable": null,
+		
+		/**
+		 * <code>Deprecated</code> When using fnRender, you have two options for what 
+		 * to do with the data, and this property serves as the switch. Firstly, you 
+		 * can have the sorting and filtering use the rendered value (true - default), 
+		 * or you can have the sorting and filtering us the original value (false).
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type boolean
+		 *  @deprecated
+		 */
+		"bUseRendered": null,
+		
+		/**
+		 * Flag to indicate if the column is currently visible in the table or not
+		 *  @type boolean
+		 */
+		"bVisible": null,
+		
+		/**
+		 * Flag to indicate to the type detection method if the automatic type
+		 * detection should be used, or if a column type (sType) has been specified
+		 *  @type boolean
+		 *  @default true
+		 *  @private
+		 */
+		"_bAutoType": true,
+		
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} nTd The TD node that has been created
+		 *  @param {*} sData The Data for the cell
+		 *  @param {array|object} oData The data for the whole row
+		 *  @param {int} iRow The row index for the aoData data store
+		 *  @default null
+		 */
+		"fnCreatedCell": null,
+		
+		/**
+		 * Function to get data from a cell in a column. You should <b>never</b>
+		 * access data directly through _aData internally in DataTables - always use
+		 * the method attached to this property. It allows mData to function as
+		 * required. This function is automatically assigned by the column 
+		 * initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array 
+		 *    (i.e. aoData[]._aData)
+		 *  @param {string} sSpecific The specific data type you want to get - 
+		 *    'display', 'type' 'filter' 'sort'
+		 *  @returns {*} The data for the cell from the given row's data
+		 *  @default null
+		 */
+		"fnGetData": null,
+		
+		/**
+		 * <code>Deprecated</code> Custom display function that will be called for the 
+		 * display of each cell in this column.
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type function
+		 *  @param {object} o Object with the following parameters:
+		 *  @param {int}    o.iDataRow The row in aoData
+		 *  @param {int}    o.iDataColumn The column in question
+		 *  @param {array}  o.aData The data for the row in question
+		 *  @param {object} o.oSettings The settings object for this DataTables instance
+		 *  @returns {string} The string you which to use in the display
+		 *  @default null
+		 *  @deprecated
+		 */
+		"fnRender": null,
+		
+		/**
+		 * Function to set data for a cell in the column. You should <b>never</b> 
+		 * set the data directly to _aData internally in DataTables - always use
+		 * this method. It allows mData to function as required. This function
+		 * is automatically assigned by the column initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array 
+		 *    (i.e. aoData[]._aData)
+		 *  @param {*} sValue Value to set
+		 *  @default null
+		 */
+		"fnSetData": null,
+		
+		/**
+		 * Property to read the value for the cells in the column from the data 
+		 * source array / object. If null, then the default content is used, if a
+		 * function is given then the return from the function is used.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mData": null,
+		
+		/**
+		 * Partner property to mData which is used (only when defined) to get
+		 * the data - i.e. it is basically the same as mData, but without the
+		 * 'set' option, and also the data fed to it is the result from mData.
+		 * This is the rendering method to match the data method of mData.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mRender": null,
+		
+		/**
+		 * Unique header TH/TD element for this column - this is what the sorting
+		 * listener is attached to (if sorting is enabled.)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTh": null,
+		
+		/**
+		 * Unique footer TH/TD element for this column (if there is one). Not used 
+		 * in DataTables as such, but can be used for plug-ins to reference the 
+		 * footer for each column.
+		 *  @type node
+		 *  @default null
+		 */
+		"nTf": null,
+		
+		/**
+		 * The class to apply to all TD elements in the table's TBODY for the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sClass": null,
+		
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer 
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 *  @type string
+		 */
+		"sContentPadding": null,
+		
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because mData
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 */
+		"sDefaultContent": null,
+		
+		/**
+		 * Name for the column, allowing reference to the column by name as well as
+		 * by index (needs a lookup to work by name).
+		 *  @type string
+		 */
+		"sName": null,
+		
+		/**
+		 * Custom sorting data type - defines which of the available plug-ins in
+		 * afnSortData the custom sorting will use - if any is defined.
+		 *  @type string
+		 *  @default std
+		 */
+		"sSortDataType": 'std',
+		
+		/**
+		 * Class to be applied to the header element when sorting on this column
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClass": null,
+		
+		/**
+		 * Class to be applied to the header element when sorting on this column -
+		 * when jQuery UI theming is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClassJUI": null,
+		
+		/**
+		 * Title of the column - what is seen in the TH element (nTh).
+		 *  @type string
+		 */
+		"sTitle": null,
+		
+		/**
+		 * Column sorting and filtering type
+		 *  @type string
+		 *  @default null
+		 */
+		"sType": null,
+		
+		/**
+		 * Width of the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidth": null,
+		
+		/**
+		 * Width of the column when it was first "encountered"
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidthOrig": null
+	};
+	
+	
+	
+	/**
+	 * Initialisation options that can be given to DataTables at initialisation 
+	 * time.
+	 *  @namespace
+	 */
+	DataTable.defaults = {
+		/**
+		 * An array of data to use for the table, passed in at initialisation which 
+		 * will be used in preference to any data which is already in the DOM. This is
+		 * particularly useful for constructing tables purely in Javascript, for
+		 * example with a custom Ajax call.
+		 *  @type array
+		 *  @default null
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    // Using a 2D array data source
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "aaData": [
+		 *          ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'],
+		 *          ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'],
+		 *        ],
+		 *        "aoColumns": [
+		 *          { "sTitle": "Engine" },
+		 *          { "sTitle": "Browser" },
+		 *          { "sTitle": "Platform" },
+		 *          { "sTitle": "Version" },
+		 *          { "sTitle": "Grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using an array of objects as a data source (mData)
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "aaData": [
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 4.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  4,
+		 *            "grade":    "X"
+		 *          },
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 5.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  5,
+		 *            "grade":    "C"
+		 *          }
+		 *        ],
+		 *        "aoColumns": [
+		 *          { "sTitle": "Engine",   "mData": "engine" },
+		 *          { "sTitle": "Browser",  "mData": "browser" },
+		 *          { "sTitle": "Platform", "mData": "platform" },
+		 *          { "sTitle": "Version",  "mData": "version" },
+		 *          { "sTitle": "Grade",    "mData": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aaData": null,
+	
+	
+		/**
+		 * If sorting is enabled, then DataTables will perform a first pass sort on 
+		 * initialisation. You can define which column(s) the sort is performed upon, 
+		 * and the sorting direction, with this variable. The aaSorting array should 
+		 * contain an array for each column to be sorted initially containing the 
+		 * column's index and a direction string ('asc' or 'desc').
+		 *  @type array
+		 *  @default [[0,'asc']]
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    // Sort by 3rd column first, and then 4th column
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSorting": [[2,'asc'], [3,'desc']]
+		 *      } );
+		 *    } );
+		 *    
+		 *    // No initial sorting
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSorting": []
+		 *      } );
+		 *    } );
+		 */
+		"aaSorting": [[0,'asc']],
+	
+	
+		/**
+		 * This parameter is basically identical to the aaSorting parameter, but 
+		 * cannot be overridden by user interaction with the table. What this means 
+		 * is that you could have a column (visible or hidden) which the sorting will 
+		 * always be forced on first - any sorting after that (from the user) will 
+		 * then be performed as required. This can be useful for grouping rows 
+		 * together.
+		 *  @type array
+		 *  @default null
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSortingFixed": [[0,'asc']]
+		 *      } );
+		 *    } )
+		 */
+		"aaSortingFixed": null,
+	
+	
+		/**
+		 * This parameter allows you to readily specify the entries in the length drop
+		 * down menu that DataTables shows when pagination is enabled. It can be 
+		 * either a 1D array of options which will be used for both the displayed 
+		 * option and the value, or a 2D array which will use the array in the first 
+		 * position as the value, and the array in the second position as the 
+		 * displayed options (useful for language strings such as 'All').
+		 *  @type array
+		 *  @default [ 10, 25, 50, 100 ]
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aLengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]]
+		 *      } );
+		 *    } );
+		 *  
+		 *  @example
+		 *    // Setting the default display length as well as length menu
+		 *    // This is likely to be wanted if you remove the '10' option which
+		 *    // is the iDisplayLength default.
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayLength": 25,
+		 *        "aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]]
+		 *      } );
+		 *    } );
+		 */
+		"aLengthMenu": [ 10, 25, 50, 100 ],
+	
+	
+		/**
+		 * The aoColumns option in the initialisation parameter allows you to define
+		 * details about the way individual columns behave. For a full list of
+		 * column options that can be set, please see 
+		 * {@link DataTable.defaults.columns}. Note that if you use aoColumns to
+		 * define your columns, you must have an entry in the array for every single
+		 * column that you have in your table (these can be null if you don't which
+		 * to specify any options).
+		 *  @member
+		 */
+		"aoColumns": null,
+	
+		/**
+		 * Very similar to aoColumns, aoColumnDefs allows you to target a specific 
+		 * column, multiple columns, or all columns, using the aTargets property of 
+		 * each object in the array. This allows great flexibility when creating 
+		 * tables, as the aoColumnDefs arrays can be of any length, targeting the 
+		 * columns you specifically want. aoColumnDefs may use any of the column 
+		 * options available: {@link DataTable.defaults.columns}, but it _must_
+		 * have aTargets defined in each object in the array. Values in the aTargets
+		 * array may be:
+		 *   <ul>
+		 *     <li>a string - class name will be matched on the TH for the column</li>
+		 *     <li>0 or a positive integer - column index counting from the left</li>
+		 *     <li>a negative integer - column index counting from the right</li>
+		 *     <li>the string "_all" - all columns (i.e. assign a default)</li>
+		 *   </ul>
+		 *  @member
+		 */
+		"aoColumnDefs": null,
+	
+	
+		/**
+		 * Basically the same as oSearch, this parameter defines the individual column
+		 * filtering state at initialisation time. The array must be of the same size 
+		 * as the number of columns, and each element be an object with the parameters
+		 * "sSearch" and "bEscapeRegex" (the latter is optional). 'null' is also
+		 * accepted and the default will be used.
+		 *  @type array
+		 *  @default []
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoSearchCols": [
+		 *          null,
+		 *          { "sSearch": "My filter" },
+		 *          null,
+		 *          { "sSearch": "^[0-9]", "bEscapeRegex": false }
+		 *        ]
+		 *      } );
+		 *    } )
+		 */
+		"aoSearchCols": [],
+	
+	
+		/**
+		 * An array of CSS classes that should be applied to displayed rows. This 
+		 * array may be of any length, and DataTables will apply each class 
+		 * sequentially, looping when required.
+		 *  @type array
+		 *  @default null <i>Will take the values determined by the oClasses.sStripe*
+		 *    options</i>
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "asStripeClasses": [ 'strip1', 'strip2', 'strip3' ]
+		 *      } );
+		 *    } )
+		 */
+		"asStripeClasses": null,
+	
+	
+		/**
+		 * Enable or disable automatic column width calculation. This can be disabled
+		 * as an optimisation (it takes some time to calculate the widths) if the
+		 * tables widths are passed in using aoColumns.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bAutoWidth": false
+		 *      } );
+		 *    } );
+		 */
+		"bAutoWidth": true,
+	
+	
+		/**
+		 * Deferred rendering can provide DataTables with a huge speed boost when you
+		 * are using an Ajax or JS data source for the table. This option, when set to
+		 * true, will cause DataTables to defer the creation of the table elements for
+		 * each row until they are needed for a draw - saving a significant amount of
+		 * time.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/arrays.txt",
+		 *        "bDeferRender": true
+		 *      } );
+		 *    } );
+		 */
+		"bDeferRender": false,
+	
+	
+		/**
+		 * Replace a DataTable which matches the given selector and replace it with 
+		 * one which has the properties of the new initialisation object passed. If no
+		 * table matches the selector, then the new DataTable will be constructed as
+		 * per normal.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *      
+		 *      // Some time later....
+		 *      $('#example').dataTable( {
+		 *        "bFilter": false,
+		 *        "bDestroy": true
+		 *      } );
+		 *    } );
+		 */
+		"bDestroy": false,
+	
+	
+		/**
+		 * Enable or disable filtering of data. Filtering in DataTables is "smart" in
+		 * that it allows the end user to input multiple words (space separated) and
+		 * will match a row containing those words, even if not in the order that was
+		 * specified (this allow matching across multiple columns). Note that if you
+		 * wish to use filtering in DataTables this must remain 'true' - to remove the
+		 * default filtering input box and retain filtering abilities, please use
+		 * {@link DataTable.defaults.sDom}.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bFilter": false
+		 *      } );
+		 *    } );
+		 */
+		"bFilter": true,
+	
+	
+		/**
+		 * Enable or disable the table information display. This shows information 
+		 * about the data that is currently visible on the page, including information
+		 * about filtered data if that action is being performed.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bInfo": false
+		 *      } );
+		 *    } );
+		 */
+		"bInfo": true,
+	
+	
+		/**
+		 * Enable jQuery UI ThemeRoller support (required as ThemeRoller requires some
+		 * slightly different and additional mark-up from what DataTables has
+		 * traditionally used).
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bJQueryUI": true
+		 *      } );
+		 *    } );
+		 */
+		"bJQueryUI": false,
+	
+	
+		/**
+		 * Allows the end user to select the size of a formatted page from a select
+		 * menu (sizes are 10, 25, 50 and 100). Requires pagination (bPaginate).
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bLengthChange": false
+		 *      } );
+		 *    } );
+		 */
+		"bLengthChange": true,
+	
+	
+		/**
+		 * Enable or disable pagination.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bPaginate": false
+		 *      } );
+		 *    } );
+		 */
+		"bPaginate": true,
+	
+	
+		/**
+		 * Enable or disable the display of a 'processing' indicator when the table is
+		 * being processed (e.g. a sort). This is particularly useful for tables with
+		 * large amounts of data where it can take a noticeable amount of time to sort
+		 * the entries.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true
+		 *      } );
+		 *    } );
+		 */
+		"bProcessing": false,
+	
+	
+		/**
+		 * Retrieve the DataTables object for the given selector. Note that if the
+		 * table has already been initialised, this parameter will cause DataTables
+		 * to simply return the object that has already been set up - it will not take
+		 * account of any changes you might have made to the initialisation object
+		 * passed to DataTables (setting this parameter to true is an acknowledgement
+		 * that you understand this). bDestroy can be used to reinitialise a table if
+		 * you need.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      initTable();
+		 *      tableActions();
+		 *    } );
+		 *    
+		 *    function initTable ()
+		 *    {
+		 *      return $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false,
+		 *        "bRetrieve": true
+		 *      } );
+		 *    }
+		 *    
+		 *    function tableActions ()
+		 *    {
+		 *      var oTable = initTable();
+		 *      // perform API operations with oTable 
+		 *    }
+		 */
+		"bRetrieve": false,
+	
+	
+		/**
+		 * Indicate if DataTables should be allowed to set the padding / margin
+		 * etc for the scrolling header elements or not. Typically you will want
+		 * this.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollAutoCss": false,
+		 *        "sScrollY": "200px"
+		 *      } );
+		 *    } );
+		 */
+		"bScrollAutoCss": true,
+	
+	
+		/**
+		 * When vertical (y) scrolling is enabled, DataTables will force the height of
+		 * the table's viewport to the given height at all times (useful for layout).
+		 * However, this can look odd when filtering data down to a small data set,
+		 * and the footer is left "floating" further down. This parameter (when
+		 * enabled) will cause DataTables to collapse the table's viewport down when
+		 * the result set will fit within the given Y height.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200",
+		 *        "bScrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"bScrollCollapse": false,
+	
+	
+		/**
+		 * Enable infinite scrolling for DataTables (to be used in combination with
+		 * sScrollY). Infinite scrolling means that DataTables will continually load
+		 * data as a user scrolls through a table, which is very useful for large
+		 * dataset. This cannot be used with pagination, which is automatically
+		 * disabled. Note - the Scroller extra for DataTables is recommended in
+		 * in preference to this option.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollInfinite": true,
+		 *        "bScrollCollapse": true,
+		 *        "sScrollY": "200px"
+		 *      } );
+		 *    } );
+		 */
+		"bScrollInfinite": false,
+	
+	
+		/**
+		 * Configure DataTables to use server-side processing. Note that the
+		 * sAjaxSource parameter must also be given in order to give DataTables a
+		 * source to obtain the required data for each draw.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "xhr.php"
+		 *      } );
+		 *    } );
+		 */
+		"bServerSide": false,
+	
+	
+		/**
+		 * Enable or disable sorting of columns. Sorting of individual columns can be
+		 * disabled by the "bSortable" option for each column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bSort": false
+		 *      } );
+		 *    } );
+		 */
+		"bSort": true,
+	
+	
+		/**
+		 * Allows control over whether DataTables should use the top (true) unique
+		 * cell that is found for a single column, or the bottom (false - default).
+		 * This is useful when using complex headers.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bSortCellsTop": true
+		 *      } );
+		 *    } );
+		 */
+		"bSortCellsTop": false,
+	
+	
+		/**
+		 * Enable or disable the addition of the classes 'sorting_1', 'sorting_2' and
+		 * 'sorting_3' to the columns which are currently being sorted on. This is
+		 * presented as a feature switch as it can increase processing time (while
+		 * classes are removed and added) so for large data sets you might want to
+		 * turn this off.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bSortClasses": false
+		 *      } );
+		 *    } );
+		 */
+		"bSortClasses": true,
+	
+	
+		/**
+		 * Enable or disable state saving. When enabled a cookie will be used to save
+		 * table display information such as pagination information, display length,
+		 * filtering and sorting. As such when the end user reloads the page the
+		 * display display will match what thy had previously set up.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true
+		 *      } );
+		 *    } );
+		 */
+		"bStateSave": false,
+	
+	
+		/**
+		 * Customise the cookie and / or the parameters being stored when using
+		 * DataTables with state saving enabled. This function is called whenever
+		 * the cookie is modified, and it expects a fully formed cookie string to be
+		 * returned. Note that the data object passed in is a Javascript object which
+		 * must be converted to a string (JSON.stringify for example).
+		 *  @type function
+		 *  @param {string} sName Name of the cookie defined by DataTables
+		 *  @param {object} oData Data to be stored in the cookie
+		 *  @param {string} sExpires Cookie expires string
+		 *  @param {string} sPath Path of the cookie to set
+		 *  @returns {string} Cookie formatted string (which should be encoded by
+		 *    using encodeURIComponent())
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "fnCookieCallback": function (sName, oData, sExpires, sPath) {
+		 *          // Customise oData or sName or whatever else here
+		 *          return sName + "="+JSON.stringify(oData)+"; expires=" + sExpires +"; path=" + sPath;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnCookieCallback": null,
+	
+	
+		/**
+		 * This function is called when a TR element is created (and all TD child
+		 * elements have been inserted), or registered if using a DOM source, allowing
+		 * manipulation of the TR element (adding classes etc).
+		 *  @type function
+		 *  @param {node} nRow "TR" element for the current row
+		 *  @param {array} aData Raw data array for this row
+		 *  @param {int} iDataIndex The index of this row in aoData
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnCreatedRow": function( nRow, aData, iDataIndex ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( aData[4] == "A" )
+		 *          {
+		 *            $('td:eq(4)', nRow).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnCreatedRow": null,
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify any aspect you want about the created DOM.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnDrawCallback": function( oSettings ) {
+		 *          alert( 'DataTables has redrawn the table' );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnDrawCallback": null,
+	
+	
+		/**
+		 * Identical to fnHeaderCallback() but for the table footer this function
+		 * allows you to modify the table footer on every 'draw' even.
+		 *  @type function
+		 *  @param {node} nFoot "TR" element for the footer
+		 *  @param {array} aData Full table data (as derived from the original HTML)
+		 *  @param {int} iStart Index for the current display starting point in the 
+		 *    display array
+		 *  @param {int} iEnd Index for the current display ending point in the 
+		 *    display array
+		 *  @param {array int} aiDisplay Index array to translate the visual position
+		 *    to the full data array
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnFooterCallback": function( nFoot, aData, iStart, iEnd, aiDisplay ) {
+		 *          nFoot.getElementsByTagName('th')[0].innerHTML = "Starting index is "+iStart;
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnFooterCallback": null,
+	
+	
+		/**
+		 * When rendering large numbers in the information element for the table
+		 * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers
+		 * to have a comma separator for the 'thousands' units (e.g. 1 million is
+		 * rendered as "1,000,000") to help readability for the end user. This
+		 * function will override the default method DataTables uses.
+		 *  @type function
+		 *  @member
+		 *  @param {int} iIn number to be formatted
+		 *  @returns {string} formatted string for DataTables to show the number
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnFormatNumber": function ( iIn ) {
+		 *          if ( iIn &lt; 1000 ) {
+		 *            return iIn;
+		 *          } else {
+		 *            var 
+		 *              s=(iIn+""), 
+		 *              a=s.split(""), out="", 
+		 *              iLen=s.length;
+		 *            
+		 *            for ( var i=0 ; i&lt;iLen ; i++ ) {
+		 *              if ( i%3 === 0 &amp;&amp; i !== 0 ) {
+		 *                out = "'"+out;
+		 *              }
+		 *              out = a[iLen-i-1]+out;
+		 *            }
+		 *          }
+		 *          return out;
+		 *        };
+		 *      } );
+		 *    } );
+		 */
+		"fnFormatNumber": function ( iIn ) {
+			if ( iIn < 1000 )
+			{
+				// A small optimisation for what is likely to be the majority of use cases
+				return iIn;
+			}
+	
+			var s=(iIn+""), a=s.split(""), out="", iLen=s.length;
+			
+			for ( var i=0 ; i<iLen ; i++ )
+			{
+				if ( i%3 === 0 && i !== 0 )
+				{
+					out = this.oLanguage.sInfoThousands+out;
+				}
+				out = a[iLen-i-1]+out;
+			}
+			return out;
+		},
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify the header row. This can be used to calculate and
+		 * display useful information about the table.
+		 *  @type function
+		 *  @param {node} nHead "TR" element for the header
+		 *  @param {array} aData Full table data (as derived from the original HTML)
+		 *  @param {int} iStart Index for the current display starting point in the
+		 *    display array
+		 *  @param {int} iEnd Index for the current display ending point in the
+		 *    display array
+		 *  @param {array int} aiDisplay Index array to translate the visual position
+		 *    to the full data array
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnHeaderCallback": function( nHead, aData, iStart, iEnd, aiDisplay ) {
+		 *          nHead.getElementsByTagName('th')[0].innerHTML = "Displaying "+(iEnd-iStart)+" records";
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnHeaderCallback": null,
+	
+	
+		/**
+		 * The information element can be used to convey information about the current
+		 * state of the table. Although the internationalisation options presented by
+		 * DataTables are quite capable of dealing with most customisations, there may
+		 * be times where you wish to customise the string further. This callback
+		 * allows you to do exactly that.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {int} iStart Starting position in data for the draw
+		 *  @param {int} iEnd End position in data for the draw
+		 *  @param {int} iMax Total number of rows in the table (regardless of
+		 *    filtering)
+		 *  @param {int} iTotal Total number of rows in the data set, after filtering
+		 *  @param {string} sPre The string that DataTables has formatted using it's
+		 *    own rules
+		 *  @returns {string} The string to be displayed in the information element.
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $('#example').dataTable( {
+		 *      "fnInfoCallback": function( oSettings, iStart, iEnd, iMax, iTotal, sPre ) {
+		 *        return iStart +" to "+ iEnd;
+		 *      }
+		 *    } );
+		 */
+		"fnInfoCallback": null,
+	
+	
+		/**
+		 * Called when the table has been initialised. Normally DataTables will
+		 * initialise sequentially and there will be no need for this function,
+		 * however, this does not hold true when using external language information
+		 * since that is obtained using an async XHR call.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} json The JSON object request from the server - only
+		 *    present if client-side Ajax sourced data is used
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnInitComplete": function(oSettings, json) {
+		 *          alert( 'DataTables has finished its initialisation.' );
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnInitComplete": null,
+	
+	
+		/**
+		 * Called at the very start of each table draw and can be used to cancel the
+		 * draw by returning false, any other return (including undefined) results in
+		 * the full draw occurring).
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @returns {boolean} False will cancel the draw, anything else (including no
+		 *    return) will allow it to complete.
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnPreDrawCallback": function( oSettings ) {
+		 *          if ( $('#test').val() == 1 ) {
+		 *            return false;
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnPreDrawCallback": null,
+	
+	
+		/**
+		 * This function allows you to 'post process' each row after it have been
+		 * generated for each table draw, but before it is rendered on screen. This
+		 * function might be used for setting the row class name etc.
+		 *  @type function
+		 *  @param {node} nRow "TR" element for the current row
+		 *  @param {array} aData Raw data array for this row
+		 *  @param {int} iDisplayIndex The display index for the current table draw
+		 *  @param {int} iDisplayIndexFull The index of the data in the full list of
+		 *    rows (after filtering)
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( aData[4] == "A" )
+		 *          {
+		 *            $('td:eq(4)', nRow).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnRowCallback": null,
+	
+	
+		/**
+		 * This parameter allows you to override the default function which obtains
+		 * the data from the server ($.getJSON) so something more suitable for your
+		 * application. For example you could use POST data, or pull information from
+		 * a Gears or AIR database.
+		 *  @type function
+		 *  @member
+		 *  @param {string} sSource HTTP source to obtain the data from (sAjaxSource)
+		 *  @param {array} aoData A key/value pair object containing the data to send
+		 *    to the server
+		 *  @param {function} fnCallback to be called on completion of the data get
+		 *    process that will draw the data on the page.
+		 *  @param {object} oSettings DataTables settings object
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    // POST data to server
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true,
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "xhr.php",
+		 *        "fnServerData": function ( sSource, aoData, fnCallback, oSettings ) {
+		 *          oSettings.jqXHR = $.ajax( {
+		 *            "dataType": 'json', 
+		 *            "type": "POST", 
+		 *            "url": sSource, 
+		 *            "data": aoData, 
+		 *            "success": fnCallback
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnServerData": function ( sUrl, aoData, fnCallback, oSettings ) {
+			oSettings.jqXHR = $.ajax( {
+				"url":  sUrl,
+				"data": aoData,
+				"success": function (json) {
+					if ( json.sError ) {
+						oSettings.oApi._fnLog( oSettings, 0, json.sError );
+					}
+					
+					$(oSettings.oInstance).trigger('xhr', [oSettings, json]);
+					fnCallback( json );
+				},
+				"dataType": "json",
+				"cache": false,
+				"type": oSettings.sServerMethod,
+				"error": function (xhr, error, thrown) {
+					if ( error == "parsererror" ) {
+						oSettings.oApi._fnLog( oSettings, 0, "DataTables warning: JSON data from "+
+							"server could not be parsed. This is caused by a JSON formatting error." );
+					}
+				}
+			} );
+		},
+	
+	
+		/**
+		 * It is often useful to send extra data to the server when making an Ajax
+		 * request - for example custom filtering information, and this callback
+		 * function makes it trivial to send extra information to the server. The
+		 * passed in parameter is the data set that has been constructed by
+		 * DataTables, and you can add to this or modify it as you require.
+		 *  @type function
+		 *  @param {array} aoData Data array (array of objects which are name/value
+		 *    pairs) that has been constructed by DataTables and will be sent to the
+		 *    server. In the case of Ajax sourced data with server-side processing
+		 *    this will be an empty array, for server-side processing there will be a
+		 *    significant number of parameters!
+		 *  @returns {undefined} Ensure that you modify the aoData array passed in,
+		 *    as this is passed by reference.
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true,
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "fnServerParams": function ( aoData ) {
+		 *          aoData.push( { "name": "more_data", "value": "my_value" } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnServerParams": null,
+	
+	
+		/**
+		 * Load the table state. With this function you can define from where, and how, the
+		 * state of a table is loaded. By default DataTables will load from its state saving
+		 * cookie, but you might wish to use local storage (HTML5) or a server-side database.
+		 *  @type function
+		 *  @member
+		 *  @param {object} oSettings DataTables settings object
+		 *  @return {object} The DataTables state object to be loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoad": function (oSettings) {
+		 *          var o;
+		 *          
+		 *          // Send an Ajax request to the server to get the data. Note that
+		 *          // this is a synchronous request.
+		 *          $.ajax( {
+		 *            "url": "/state_load",
+		 *            "async": false,
+		 *            "dataType": "json",
+		 *            "success": function (json) {
+		 *              o = json;
+		 *            }
+		 *          } );
+		 *          
+		 *          return o;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoad": function ( oSettings ) {
+			var sData = this.oApi._fnReadCookie( oSettings.sCookiePrefix+oSettings.sInstance );
+			var oData;
+	
+			try {
+				oData = (typeof $.parseJSON === 'function') ? 
+					$.parseJSON(sData) : eval( '('+sData+')' );
+			} catch (e) {
+				oData = null;
+			}
+	
+			return oData;
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the saved state prior to loading that state.
+		 * This callback is called when the table is loading state from the stored data, but
+		 * prior to the settings object being modified by the saved state. Note that for 
+		 * plug-in authors, you should use the 'stateLoadParams' event to load parameters for 
+		 * a plug-in.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object that is to be loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never loaded
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoadParams": function (oSettings, oData) {
+		 *          oData.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Disallow state loading by returning false
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoadParams": function (oSettings, oData) {
+		 *          return false;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoadParams": null,
+	
+	
+		/**
+		 * Callback that is called when the state has been loaded from the state saving method
+		 * and the DataTables settings object has been modified as a result of the loaded state.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object that was loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Show an alert with the filtering value that was saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoaded": function (oSettings, oData) {
+		 *          alert( 'Saved filter was: '+oData.oSearch.sSearch );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoaded": null,
+	
+	
+		/**
+		 * Save the table state. This function allows you to define where and how the state
+		 * information for the table is stored - by default it will use a cookie, but you
+		 * might want to use local storage (HTML5) or a server-side database.
+		 *  @type function
+		 *  @member
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object to be saved
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateSave": function (oSettings, oData) {
+		 *          // Send an Ajax request to the server with the state object
+		 *          $.ajax( {
+		 *            "url": "/state_save",
+		 *            "data": oData,
+		 *            "dataType": "json",
+		 *            "method": "POST"
+		 *            "success": function () {}
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSave": function ( oSettings, oData ) {
+			this.oApi._fnCreateCookie( 
+				oSettings.sCookiePrefix+oSettings.sInstance, 
+				this.oApi._fnJsonString(oData), 
+				oSettings.iCookieDuration, 
+				oSettings.sCookiePrefix, 
+				oSettings.fnCookieCallback
+			);
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the state to be saved. Called when the table 
+		 * has changed state a new state save is required. This method allows modification of
+		 * the state saving object prior to actually doing the save, including addition or 
+		 * other state properties or modification. Note that for plug-in authors, you should 
+		 * use the 'stateSaveParams' event to save parameters for a plug-in.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object to be saved
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateSaveParams": function (oSettings, oData) {
+		 *          oData.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSaveParams": null,
+	
+	
+		/**
+		 * Duration of the cookie which is used for storing session information. This
+		 * value is given in seconds.
+		 *  @type int
+		 *  @default 7200 <i>(2 hours)</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iCookieDuration": 60*60*24; // 1 day
+		 *      } );
+		 *    } )
+		 */
+		"iCookieDuration": 7200,
+	
+	
+		/**
+		 * When enabled DataTables will not make a request to the server for the first
+		 * page draw - rather it will use the data already on the page (no sorting etc
+		 * will be applied to it), thus saving on an XHR at load time. iDeferLoading
+		 * is used to indicate that deferred loading is required, but it is also used
+		 * to tell DataTables how many records there are in the full table (allowing
+		 * the information element and pagination to be displayed correctly). In the case
+		 * where a filtering is applied to the table on initial load, this can be
+		 * indicated by giving the parameter as an array, where the first element is
+		 * the number of records available after filtering and the second element is the
+		 * number of records without filtering (allowing the table information element
+		 * to be shown correctly).
+		 *  @type int | array
+		 *  @default null
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    // 57 records available in the table, no filtering applied
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "iDeferLoading": 57
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // 57 records after filtering, 100 without filtering (an initial filter applied)
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "iDeferLoading": [ 57, 100 ],
+		 *        "oSearch": {
+		 *          "sSearch": "my_filter"
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"iDeferLoading": null,
+	
+	
+		/**
+		 * Number of rows to display on a single page when using pagination. If
+		 * feature enabled (bLengthChange) then the end user will be able to override
+		 * this to a custom setting using a pop-up menu.
+		 *  @type int
+		 *  @default 10
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayLength": 50
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayLength": 10,
+	
+	
+		/**
+		 * Define the starting point for data display when using DataTables with
+		 * pagination. Note that this parameter is the number of records, rather than
+		 * the page number, so if you have 10 records per page and want to start on
+		 * the third page, it should be "20".
+		 *  @type int
+		 *  @default 0
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayStart": 20
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayStart": 0,
+	
+	
+		/**
+		 * The scroll gap is the amount of scrolling that is left to go before
+		 * DataTables will load the next 'page' of data automatically. You typically
+		 * want a gap which is big enough that the scrolling will be smooth for the
+		 * user, while not so large that it will load more data than need.
+		 *  @type int
+		 *  @default 100
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollInfinite": true,
+		 *        "bScrollCollapse": true,
+		 *        "sScrollY": "200px",
+		 *        "iScrollLoadGap": 50
+		 *      } );
+		 *    } );
+		 */
+		"iScrollLoadGap": 100,
+	
+	
+		/**
+		 * By default DataTables allows keyboard navigation of the table (sorting, paging,
+		 * and filtering) by adding a tabindex attribute to the required elements. This
+		 * allows you to tab through the controls and press the enter key to activate them.
+		 * The tabindex is default 0, meaning that the tab follows the flow of the document.
+		 * You can overrule this using this parameter if you wish. Use a value of -1 to
+		 * disable built-in keyboard navigation.
+		 *  @type int
+		 *  @default 0
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iTabIndex": 1
+		 *      } );
+		 *    } );
+		 */
+		"iTabIndex": 0,
+	
+	
+		/**
+		 * All strings that DataTables uses in the user interface that it creates
+		 * are defined in this object, allowing you to modified them individually or
+		 * completely replace them all as required.
+		 *  @namespace
+		 */
+		"oLanguage": {
+			/**
+			 * Strings that are used for WAI-ARIA labels and controls only (these are not
+			 * actually visible on the page, but will be read by screenreaders, and thus
+			 * must be internationalised as well).
+			 *  @namespace
+			 */
+			"oAria": {
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted ascending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oAria": {
+				 *            "sSortAscending": " - click/return to sort ascending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortAscending": ": activate to sort column ascending",
+	
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted descending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oAria": {
+				 *            "sSortDescending": " - click/return to sort descending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortDescending": ": activate to sort column descending"
+			},
+	
+			/**
+			 * Pagination string used by DataTables for the two built-in pagination
+			 * control types ("two_button" and "full_numbers")
+			 *  @namespace
+			 */
+			"oPaginate": {
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the first page.
+				 *  @type string
+				 *  @default First
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sFirst": "First page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sFirst": "First",
+			
+			
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the last page.
+				 *  @type string
+				 *  @default Last
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sLast": "Last page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sLast": "Last",
+			
+			
+				/**
+				 * Text to use for the 'next' pagination button (to take the user to the 
+				 * next page).
+				 *  @type string
+				 *  @default Next
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sNext": "Next page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sNext": "Next",
+			
+			
+				/**
+				 * Text to use for the 'previous' pagination button (to take the user to  
+				 * the previous page).
+				 *  @type string
+				 *  @default Previous
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sPrevious": "Previous page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sPrevious": "Previous"
+			},
+		
+			/**
+			 * This string is shown in preference to sZeroRecords when the table is
+			 * empty of data (regardless of filtering). Note that this is an optional
+			 * parameter - if it is not given, the value of sZeroRecords will be used
+			 * instead (either the default or given value).
+			 *  @type string
+			 *  @default No data available in table
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sEmptyTable": "No data available in table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sEmptyTable": "No data available in table",
+		
+		
+			/**
+			 * This string gives information to the end user about the information that 
+			 * is current on display on the page. The _START_, _END_ and _TOTAL_ 
+			 * variables are all dynamically replaced as the table display updates, and 
+			 * can be freely moved or removed as the language requirements change.
+			 *  @type string
+			 *  @default Showing _START_ to _END_ of _TOTAL_ entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfo": "Got a total of _TOTAL_ entries to show (_START_ to _END_)"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
+		
+		
+			/**
+			 * Display information string for when the table is empty. Typically the 
+			 * format of this string should match sInfo.
+			 *  @type string
+			 *  @default Showing 0 to 0 of 0 entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoEmpty": "No entries to show"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoEmpty": "Showing 0 to 0 of 0 entries",
+		
+		
+			/**
+			 * When a user filters the information in a table, this string is appended 
+			 * to the information (sInfo) to give an idea of how strong the filtering 
+			 * is. The variable _MAX_ is dynamically updated.
+			 *  @type string
+			 *  @default (filtered from _MAX_ total entries)
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoFiltered": " - filtering from _MAX_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoFiltered": "(filtered from _MAX_ total entries)",
+		
+		
+			/**
+			 * If can be useful to append extra information to the info string at times,
+			 * and this variable does exactly that. This information will be appended to
+			 * the sInfo (sInfoEmpty and sInfoFiltered in whatever combination they are
+			 * being used) at all times.
+			 *  @type string
+			 *  @default <i>Empty string</i>
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoPostFix": "All records shown are derived from real information."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoPostFix": "",
+		
+		
+			/**
+			 * DataTables has a build in number formatter (fnFormatNumber) which is used
+			 * to format large numbers that are used in the table information. By
+			 * default a comma is used, but this can be trivially changed to any
+			 * character you wish with this parameter.
+			 *  @type string
+			 *  @default ,
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoThousands": "'"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoThousands": ",",
+		
+		
+			/**
+			 * Detail the action that will be taken when the drop down menu for the
+			 * pagination length option is changed. The '_MENU_' variable is replaced
+			 * with a default select list of 10, 25, 50 and 100, and can be replaced
+			 * with a custom select box if required.
+			 *  @type string
+			 *  @default Show _MENU_ entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    // Language change only
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLengthMenu": "Display _MENU_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 *    
+			 *  @example
+			 *    // Language and options change
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLengthMenu": 'Display <select>'+
+			 *            '<option value="10">10</option>'+
+			 *            '<option value="20">20</option>'+
+			 *            '<option value="30">30</option>'+
+			 *            '<option value="40">40</option>'+
+			 *            '<option value="50">50</option>'+
+			 *            '<option value="-1">All</option>'+
+			 *            '</select> records'
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLengthMenu": "Show _MENU_ entries",
+		
+		
+			/**
+			 * When using Ajax sourced data and during the first draw when DataTables is
+			 * gathering the data, this message is shown in an empty row in the table to
+			 * indicate to the end user the the data is being loaded. Note that this
+			 * parameter is not used when loading data by server-side processing, just
+			 * Ajax sourced data with client-side processing.
+			 *  @type string
+			 *  @default Loading...
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLoadingRecords": "Please wait - loading..."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLoadingRecords": "Loading...",
+		
+		
+			/**
+			 * Text which is displayed when the table is processing a user action
+			 * (usually a sort command or similar).
+			 *  @type string
+			 *  @default Processing...
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sProcessing": "DataTables is currently busy"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sProcessing": "Processing...",
+		
+		
+			/**
+			 * Details the actions that will be taken when the user types into the
+			 * filtering input text box. The variable "_INPUT_", if used in the string,
+			 * is replaced with the HTML text box for the filtering input allowing
+			 * control over where it appears in the string. If "_INPUT_" is not given
+			 * then the input box is appended to the string automatically.
+			 *  @type string
+			 *  @default Search:
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    // Input text box will be appended at the end automatically
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sSearch": "Filter records:"
+			 *        }
+			 *      } );
+			 *    } );
+			 *    
+			 *  @example
+			 *    // Specify where the filter should appear
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sSearch": "Apply filter _INPUT_ to table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sSearch": "Search:",
+		
+		
+			/**
+			 * All of the language information can be stored in a file on the
+			 * server-side, which DataTables will look up if this parameter is passed.
+			 * It must store the URL of the language file, which is in a JSON format,
+			 * and the object has the same properties as the oLanguage object in the
+			 * initialiser object (i.e. the above parameters). Please refer to one of
+			 * the example language files to see how this works in action.
+			 *  @type string
+			 *  @default <i>Empty string - i.e. disabled</i>
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sUrl": "http://www.sprymedia.co.uk/dataTables/lang.txt"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sUrl": "",
+		
+		
+			/**
+			 * Text shown inside the table records when the is no information to be
+			 * displayed after filtering. sEmptyTable is shown when there is simply no
+			 * information in the table at all (regardless of filtering).
+			 *  @type string
+			 *  @default No matching records found
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sZeroRecords": "No records to display"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sZeroRecords": "No matching records found"
+		},
+	
+	
+		/**
+		 * This parameter allows you to have define the global filtering state at
+		 * initialisation time. As an object the "sSearch" parameter must be
+		 * defined, but all other parameters are optional. When "bRegex" is true,
+		 * the search string will be treated as a regular expression, when false
+		 * (default) it will be treated as a straight string. When "bSmart"
+		 * DataTables will use it's smart filtering methods (to word match at
+		 * any point in the data), when false this will not be done.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "oSearch": {"sSearch": "Initial search"}
+		 *      } );
+		 *    } )
+		 */
+		"oSearch": $.extend( {}, DataTable.models.oSearch ),
+	
+	
+		/**
+		 * By default DataTables will look for the property 'aaData' when obtaining
+		 * data from an Ajax source or for server-side processing - this parameter
+		 * allows that property to be changed. You can use Javascript dotted object
+		 * notation to get a data source for multiple levels of nesting.
+		 *  @type string
+		 *  @default aaData
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    // Get data from { "data": [...] }
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/data.txt",
+		 *        "sAjaxDataProp": "data"
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Get data from { "data": { "inner": [...] } }
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/data.txt",
+		 *        "sAjaxDataProp": "data.inner"
+		 *      } );
+		 *    } );
+		 */
+		"sAjaxDataProp": "aaData",
+	
+	
+		/**
+		 * You can instruct DataTables to load data from an external source using this
+		 * parameter (use aData if you want to pass data in you already have). Simply
+		 * provide a url a JSON object can be obtained from. This object must include
+		 * the parameter 'aaData' which is the data source for the table.
+		 *  @type string
+		 *  @default null
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sAjaxSource": "http://www.sprymedia.co.uk/dataTables/json.php"
+		 *      } );
+		 *    } )
+		 */
+		"sAjaxSource": null,
+	
+	
+		/**
+		 * This parameter can be used to override the default prefix that DataTables
+		 * assigns to a cookie when state saving is enabled.
+		 *  @type string
+		 *  @default SpryMedia_DataTables_
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sCookiePrefix": "my_datatable_",
+		 *      } );
+		 *    } );
+		 */
+		"sCookiePrefix": "SpryMedia_DataTables_",
+	
+	
+		/**
+		 * This initialisation variable allows you to specify exactly where in the
+		 * DOM you want DataTables to inject the various controls it adds to the page
+		 * (for example you might want the pagination controls at the top of the
+		 * table). DIV elements (with or without a custom class) can also be added to
+		 * aid styling. The follow syntax is used:
+		 *   <ul>
+		 *     <li>The following options are allowed:	
+		 *       <ul>
+		 *         <li>'l' - Length changing</li
+		 *         <li>'f' - Filtering input</li>
+		 *         <li>'t' - The table!</li>
+		 *         <li>'i' - Information</li>
+		 *         <li>'p' - Pagination</li>
+		 *         <li>'r' - pRocessing</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following constants are allowed:
+		 *       <ul>
+		 *         <li>'H' - jQueryUI theme "header" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')</li>
+		 *         <li>'F' - jQueryUI theme "footer" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following syntax is expected:
+		 *       <ul>
+		 *         <li>'&lt;' and '&gt;' - div elements</li>
+		 *         <li>'&lt;"class" and '&gt;' - div with a class</li>
+		 *         <li>'&lt;"#id" and '&gt;' - div with an ID</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>Examples:
+		 *       <ul>
+		 *         <li>'&lt;"wrapper"flipt&gt;'</li>
+		 *         <li>'&lt;lf&lt;t&gt;ip&gt;'</li>
+		 *       </ul>
+		 *     </li>
+		 *   </ul>
+		 *  @type string
+		 *  @default lfrtip <i>(when bJQueryUI is false)</i> <b>or</b> 
+		 *    <"H"lfr>t<"F"ip> <i>(when bJQueryUI is true)</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sDom": '&lt;"top"i&gt;rt&lt;"bottom"flp&gt;&lt;"clear"&gt;'
+		 *      } );
+		 *    } );
+		 */
+		"sDom": "lfrtip",
+	
+	
+		/**
+		 * DataTables features two different built-in pagination interaction methods
+		 * ('two_button' or 'full_numbers') which present different page controls to
+		 * the end user. Further methods can be added using the API (see below).
+		 *  @type string
+		 *  @default two_button
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sPaginationType": "full_numbers"
+		 *      } );
+		 *    } )
+		 */
+		"sPaginationType": "two_button",
+	
+	
+		/**
+		 * Enable horizontal scrolling. When a table is too wide to fit into a certain
+		 * layout, or you have a large number of columns in the table, you can enable
+		 * x-scrolling to show the table in a viewport, which can be scrolled. This
+		 * property can be any CSS unit, or a number (in which case it will be treated
+		 * as a pixel measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollX": "100%",
+		 *        "bScrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"sScrollX": "",
+	
+	
+		/**
+		 * This property can be used to force a DataTable to use more width than it
+		 * might otherwise do when x-scrolling is enabled. For example if you have a
+		 * table which requires to be well spaced, this parameter is useful for
+		 * "over-sizing" the table, and thus forcing scrolling. This property can by
+		 * any CSS unit, or a number (in which case it will be treated as a pixel
+		 * measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollX": "100%",
+		 *        "sScrollXInner": "110%"
+		 *      } );
+		 *    } );
+		 */
+		"sScrollXInner": "",
+	
+	
+		/**
+		 * Enable vertical scrolling. Vertical scrolling will constrain the DataTable
+		 * to the given height, and enable scrolling for any data which overflows the
+		 * current viewport. This can be used as an alternative to paging to display
+		 * a lot of data in a small area (although paging and scrolling can both be
+		 * enabled at the same time). This property can be any CSS unit, or a number
+		 * (in which case it will be treated as a pixel measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *    } );
+		 */
+		"sScrollY": "",
+	
+	
+		/**
+		 * Set the HTTP method that is used to make the Ajax call for server-side
+		 * processing or Ajax sourced data.
+		 *  @type string
+		 *  @default GET
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/post.php",
+		 *        "sServerMethod": "POST"
+		 *      } );
+		 *    } );
+		 */
+		"sServerMethod": "GET"
+	};
+	
+	
+	
+	/**
+	 * Column options that can be given to DataTables at initialisation time.
+	 *  @namespace
+	 */
+	DataTable.defaults.columns = {
+		/**
+		 * Allows a column's sorting to take multiple columns into account when 
+		 * doing a sort. For example first name / last name columns make sense to 
+		 * do a multi-column sort over the two columns.
+		 *  @type array
+		 *  @default null <i>Takes the value of the column index automatically</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "aDataSort": [ 0, 1 ], "aTargets": [ 0 ] },
+		 *          { "aDataSort": [ 1, 0 ], "aTargets": [ 1 ] },
+		 *          { "aDataSort": [ 2, 3, 4 ], "aTargets": [ 2 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          { "aDataSort": [ 0, 1 ] },
+		 *          { "aDataSort": [ 1, 0 ] },
+		 *          { "aDataSort": [ 2, 3, 4 ] },
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aDataSort": null,
+	
+	
+		/**
+		 * You can control the default sorting direction, and even alter the behaviour
+		 * of the sort handler (i.e. only allow ascending sorting etc) using this
+		 * parameter.
+		 *  @type array
+		 *  @default [ 'asc', 'desc' ]
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "asSorting": [ "asc" ], "aTargets": [ 1 ] },
+		 *          { "asSorting": [ "desc", "asc", "asc" ], "aTargets": [ 2 ] },
+		 *          { "asSorting": [ "desc" ], "aTargets": [ 3 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          null,
+		 *          { "asSorting": [ "asc" ] },
+		 *          { "asSorting": [ "desc", "asc", "asc" ] },
+		 *          { "asSorting": [ "desc" ] },
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"asSorting": [ 'asc', 'desc' ],
+	
+	
+		/**
+		 * Enable or disable filtering on the data in this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bSearchable": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bSearchable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSearchable": true,
+	
+	
+		/**
+		 * Enable or disable sorting on this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bSortable": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bSortable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSortable": true,
+	
+	
+		/**
+		 * <code>Deprecated</code> When using fnRender() for a column, you may wish 
+		 * to use the original data (before rendering) for sorting and filtering 
+		 * (the default is to used the rendered data that the user can see). This 
+		 * may be useful for dates etc.
+		 * 
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 *  @deprecated
+		 */
+		"bUseRendered": true,
+	
+	
+		/**
+		 * Enable or disable the display of this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bVisible": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bVisible": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bVisible": true,
+		
+		
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} nTd The TD node that has been created
+		 *  @param {*} sData The Data for the cell
+		 *  @param {array|object} oData The data for the whole row
+		 *  @param {int} iRow The row index for the aoData data store
+		 *  @param {int} iCol The column index for aoColumns
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [3],
+		 *          "fnCreatedCell": function (nTd, sData, oData, iRow, iCol) {
+		 *            if ( sData == "1.7" ) {
+		 *              $(nTd).css('color', 'blue')
+		 *            }
+		 *          }
+		 *        } ]
+		 *      });
+		 *    } );
+		 */
+		"fnCreatedCell": null,
+	
+	
+		/**
+		 * <code>Deprecated</code> Custom display function that will be called for the 
+		 * display of each cell in this column.
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type function
+		 *  @param {object} o Object with the following parameters:
+		 *  @param {int}    o.iDataRow The row in aoData
+		 *  @param {int}    o.iDataColumn The column in question
+		 *  @param {array}  o.aData The data for the row in question
+		 *  @param {object} o.oSettings The settings object for this DataTables instance
+		 *  @param {object} o.mDataProp The data property used for this column
+		 *  @param {*}      val The current cell value
+		 *  @returns {string} The string you which to use in the display
+		 *  @dtopt Columns
+		 *  @deprecated
+		 */
+		"fnRender": null,
+	
+	
+		/**
+		 * The column index (starting from 0!) that you wish a sort to be performed
+		 * upon when this column is selected for sorting. This can be used for sorting
+		 * on hidden columns for example.
+		 *  @type int
+		 *  @default -1 <i>Use automatically calculated column index</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "iDataSort": 1, "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "iDataSort": 1 },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"iDataSort": -1,
+	
+	
+		/**
+		 * This parameter has been replaced by mData in DataTables to ensure naming
+		 * consistency. mDataProp can still be used, as there is backwards compatibility
+		 * in DataTables for this option, but it is strongly recommended that you use
+		 * mData in preference to mDataProp.
+		 *  @name DataTable.defaults.columns.mDataProp
+		 */
+	
+	
+		/**
+		 * This property can be used to read data from any JSON data source property,
+		 * including deeply nested objects / properties. mData can be given in a
+		 * number of different ways which effect its behaviour:
+		 *   <ul>
+		 *     <li>integer - treated as an array index for the data source. This is the
+		 *       default that DataTables uses (incrementally increased for each column).</li>
+		 *     <li>string - read an object property from the data source. Note that you can
+		 *       use Javascript dotted notation to read deep properties / arrays from the
+		 *       data source.</li>
+		 *     <li>null - the sDefaultContent option will be used for the cell (null
+		 *       by default, so you will need to specify the default content you want -
+		 *       typically an empty string). This can be useful on generated columns such 
+		 *       as edit / delete action columns.</li>
+		 *     <li>function - the function given will be executed whenever DataTables 
+		 *       needs to set or get the data for a cell in the column. The function 
+		 *       takes three parameters:
+		 *       <ul>
+		 *         <li>{array|object} The data source for the row</li>
+		 *         <li>{string} The type call data requested - this will be 'set' when
+		 *           setting data or 'filter', 'display', 'type', 'sort' or undefined when 
+		 *           gathering data. Note that when <i>undefined</i> is given for the type
+		 *           DataTables expects to get the raw data for the object back</li>
+		 *         <li>{*} Data to set when the second parameter is 'set'.</li>
+		 *       </ul>
+		 *       The return value from the function is not required when 'set' is the type
+		 *       of call, but otherwise the return is what will be used for the data
+		 *       requested.</li>
+		 *    </ul>
+		 *
+		 * Note that prior to DataTables 1.9.2 mData was called mDataProp. The name change
+		 * reflects the flexibility of this property and is consistent with the naming of
+		 * mRender. If 'mDataProp' is given, then it will still be used by DataTables, as
+		 * it automatically maps the old name to the new if required.
+		 *  @type string|int|function|null
+		 *  @default null <i>Use automatically calculated column index</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Read table data from objects
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/deep.txt",
+		 *        "aoColumns": [
+		 *          { "mData": "engine" },
+		 *          { "mData": "browser" },
+		 *          { "mData": "platform.inner" },
+		 *          { "mData": "platform.details.0" },
+		 *          { "mData": "platform.details.1" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Using mData as a function to provide different information for
+		 *    // sorting, filtering and display. In this case, currency (price)
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [ 0 ],
+		 *          "mData": function ( source, type, val ) {
+		 *            if (type === 'set') {
+		 *              source.price = val;
+		 *              // Store the computed dislay and filter values for efficiency
+		 *              source.price_display = val=="" ? "" : "$"+numberFormat(val);
+		 *              source.price_filter  = val=="" ? "" : "$"+numberFormat(val)+" "+val;
+		 *              return;
+		 *            }
+		 *            else if (type === 'display') {
+		 *              return source.price_display;
+		 *            }
+		 *            else if (type === 'filter') {
+		 *              return source.price_filter;
+		 *            }
+		 *            // 'sort', 'type' and undefined all just use the integer
+		 *            return source.price;
+		 *          }
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"mData": null,
+	
+	
+		/**
+		 * This property is the rendering partner to mData and it is suggested that
+		 * when you want to manipulate data for display (including filtering, sorting etc)
+		 * but not altering the underlying data for the table, use this property. mData
+		 * can actually do everything this property can and more, but this parameter is
+		 * easier to use since there is no 'set' option. Like mData is can be given
+		 * in a number of different ways to effect its behaviour, with the addition of 
+		 * supporting array syntax for easy outputting of arrays (including arrays of
+		 * objects):
+		 *   <ul>
+		 *     <li>integer - treated as an array index for the data source. This is the
+		 *       default that DataTables uses (incrementally increased for each column).</li>
+		 *     <li>string - read an object property from the data source. Note that you can
+		 *       use Javascript dotted notation to read deep properties / arrays from the
+		 *       data source and also array brackets to indicate that the data reader should
+		 *       loop over the data source array. When characters are given between the array
+		 *       brackets, these characters are used to join the data source array together.
+		 *       For example: "accounts[, ].name" would result in a comma separated list with
+		 *       the 'name' value from the 'accounts' array of objects.</li>
+		 *     <li>function - the function given will be executed whenever DataTables 
+		 *       needs to set or get the data for a cell in the column. The function 
+		 *       takes three parameters:
+		 *       <ul>
+		 *         <li>{array|object} The data source for the row (based on mData)</li>
+		 *         <li>{string} The type call data requested - this will be 'filter', 'display', 
+		 *           'type' or 'sort'.</li>
+		 *         <li>{array|object} The full data source for the row (not based on mData)</li>
+		 *       </ul>
+		 *       The return value from the function is what will be used for the data
+		 *       requested.</li>
+		 *    </ul>
+		 *  @type string|int|function|null
+		 *  @default null <i>Use mData</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Create a comma separated list from an array of objects
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/deep.txt",
+		 *        "aoColumns": [
+		 *          { "mData": "engine" },
+		 *          { "mData": "browser" },
+		 *          {
+		 *            "mData": "platform",
+		 *            "mRender": "[, ].name"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Use as a function to create a link from the data source
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *        {
+		 *          "aTargets": [ 0 ],
+		 *          "mData": "download_link",
+		 *          "mRender": function ( data, type, full ) {
+		 *            return '<a href="'+data+'">Download</a>';
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"mRender": null,
+	
+	
+		/**
+		 * Change the cell type created for the column - either TD cells or TH cells. This
+		 * can be useful as TH cells have semantic meaning in the table body, allowing them
+		 * to act as a header for a row (you may wish to add scope='row' to the TH elements).
+		 *  @type string
+		 *  @default td
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Make the first column use TH cells
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [ 0 ],
+		 *          "sCellType": "th"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"sCellType": "td",
+	
+	
+		/**
+		 * Class to give to each cell in this column.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sClass": "my_class", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sClass": "my_class" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sClass": "",
+		
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer 
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 * Generally you shouldn't need this, and it is not documented on the 
+		 * general DataTables.net documentation
+		 *  @type string
+		 *  @default <i>Empty string<i>
+		 *  @dtopt Columns
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "sContentPadding": "mmm"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sContentPadding": "",
+	
+	
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because mData
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          {
+		 *            "mData": null,
+		 *            "sDefaultContent": "Edit",
+		 *            "aTargets": [ -1 ]
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "mData": null,
+		 *            "sDefaultContent": "Edit"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sDefaultContent": null,
+	
+	
+		/**
+		 * This parameter is only used in DataTables' server-side processing. It can
+		 * be exceptionally useful to know what columns are being displayed on the
+		 * client side, and to map these to database fields. When defined, the names
+		 * also allow DataTables to reorder information from the server if it comes
+		 * back in an unexpected order (i.e. if you switch your columns around on the
+		 * client-side, your server-side code does not also need updating).
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sName": "engine", "aTargets": [ 0 ] },
+		 *          { "sName": "browser", "aTargets": [ 1 ] },
+		 *          { "sName": "platform", "aTargets": [ 2 ] },
+		 *          { "sName": "version", "aTargets": [ 3 ] },
+		 *          { "sName": "grade", "aTargets": [ 4 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sName": "engine" },
+		 *          { "sName": "browser" },
+		 *          { "sName": "platform" },
+		 *          { "sName": "version" },
+		 *          { "sName": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sName": "",
+	
+	
+		/**
+		 * Defines a data source type for the sorting which can be used to read
+		 * real-time information from the table (updating the internally cached
+		 * version) prior to sorting. This allows sorting to occur on user editable
+		 * elements such as form inputs.
+		 *  @type string
+		 *  @default std
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "sSortDataType": "dom-text", "aTargets": [ 2, 3 ] },
+		 *          { "sType": "numeric", "aTargets": [ 3 ] },
+		 *          { "sSortDataType": "dom-select", "aTargets": [ 4 ] },
+		 *          { "sSortDataType": "dom-checkbox", "aTargets": [ 5 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          null,
+		 *          null,
+		 *          { "sSortDataType": "dom-text" },
+		 *          { "sSortDataType": "dom-text", "sType": "numeric" },
+		 *          { "sSortDataType": "dom-select" },
+		 *          { "sSortDataType": "dom-checkbox" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sSortDataType": "std",
+	
+	
+		/**
+		 * The title of this column.
+		 *  @type string
+		 *  @default null <i>Derived from the 'TH' value for this column in the 
+		 *    original HTML table.</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sTitle": "My column title", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sTitle": "My column title" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sTitle": null,
+	
+	
+		/**
+		 * The type allows you to specify how the data for this column will be sorted.
+		 * Four types (string, numeric, date and html (which will strip HTML tags
+		 * before sorting)) are currently available. Note that only date formats
+		 * understood by Javascript's Date() object will be accepted as type date. For
+		 * example: "Mar 26, 2008 5:03 PM". May take the values: 'string', 'numeric',
+		 * 'date' or 'html' (by default). Further types can be adding through
+		 * plug-ins.
+		 *  @type string
+		 *  @default null <i>Auto-detected from raw data</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sType": "html", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sType": "html" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sType": null,
+	
+	
+		/**
+		 * Defining the width of the column, this parameter may take any CSS value
+		 * (3em, 20px etc). DataTables apples 'smart' widths to columns which have not
+		 * been given a specific width through this interface ensuring that the table
+		 * remains readable.
+		 *  @type string
+		 *  @default null <i>Automatic</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sWidth": "20%", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sWidth": "20%" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sWidth": null
+	};
+	
+	
+	
+	/**
+	 * DataTables settings object - this holds all the information needed for a
+	 * given table, including configuration, data and current application of the
+	 * table options. DataTables does not have a single instance for each DataTable
+	 * with the settings attached to that instance, but rather instances of the
+	 * DataTable "class" are created on-the-fly as needed (typically by a 
+	 * $().dataTable() call) and the settings object is then applied to that
+	 * instance.
+	 * 
+	 * Note that this object is related to {@link DataTable.defaults} but this 
+	 * one is the internal data store for DataTables's cache of columns. It should
+	 * NOT be manipulated outside of DataTables. Any configuration should be done
+	 * through the initialisation options.
+	 *  @namespace
+	 *  @todo Really should attach the settings object to individual instances so we
+	 *    don't need to create new instances on each $().dataTable() call (if the
+	 *    table already exists). It would also save passing oSettings around and
+	 *    into every single function. However, this is a very significant 
+	 *    architecture change for DataTables and will almost certainly break
+	 *    backwards compatibility with older installations. This is something that
+	 *    will be done in 2.0.
+	 */
+	DataTable.models.oSettings = {
+		/**
+		 * Primary features of DataTables and their enablement state.
+		 *  @namespace
+		 */
+		"oFeatures": {
+			
+			/**
+			 * Flag to say if DataTables should automatically try to calculate the
+			 * optimum table and columns widths (true) or not (false).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bAutoWidth": null,
+	
+			/**
+			 * Delay the creation of TR and TD elements until they are actually
+			 * needed by a driven page draw. This can give a significant speed
+			 * increase for Ajax source and Javascript source data, but makes no
+			 * difference at all fro DOM and server-side processing tables.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bDeferRender": null,
+			
+			/**
+			 * Enable filtering on the table or not. Note that if this is disabled
+			 * then there is no filtering at all on the table, including fnFilter.
+			 * To just remove the filtering input use sDom and remove the 'f' option.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bFilter": null,
+			
+			/**
+			 * Table information element (the 'Showing x of y records' div) enable
+			 * flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bInfo": null,
+			
+			/**
+			 * Present a user control allowing the end user to change the page size
+			 * when pagination is enabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bLengthChange": null,
+	
+			/**
+			 * Pagination enabled or not. Note that if this is disabled then length
+			 * changing must also be disabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bPaginate": null,
+			
+			/**
+			 * Processing indicator enable flag whenever DataTables is enacting a
+			 * user request - typically an Ajax request for server-side processing.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bProcessing": null,
+			
+			/**
+			 * Server-side processing enabled flag - when enabled DataTables will
+			 * get all data from the server for every draw - there is no filtering,
+			 * sorting or paging done on the client-side.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bServerSide": null,
+			
+			/**
+			 * Sorting enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSort": null,
+			
+			/**
+			 * Apply a class to the columns which are being sorted to provide a
+			 * visual highlight or not. This can slow things down when enabled since
+			 * there is a lot of DOM interaction.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSortClasses": null,
+			
+			/**
+			 * State saving enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bStateSave": null
+		},
+		
+	
+		/**
+		 * Scrolling settings for a table.
+		 *  @namespace
+		 */
+		"oScroll": {
+			/**
+			 * Indicate if DataTables should be allowed to set the padding / margin
+			 * etc for the scrolling header elements or not. Typically you will want
+			 * this.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bAutoCss": null,
+			
+			/**
+			 * When the table is shorter in height than sScrollY, collapse the
+			 * table container down to the height of the table (when true).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bCollapse": null,
+			
+			/**
+			 * Infinite scrolling enablement flag. Now deprecated in favour of
+			 * using the Scroller plug-in.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bInfinite": null,
+			
+			/**
+			 * Width of the scrollbar for the web-browser's platform. Calculated
+			 * during table initialisation.
+			 *  @type int
+			 *  @default 0
+			 */
+			"iBarWidth": 0,
+			
+			/**
+			 * Space (in pixels) between the bottom of the scrolling container and 
+			 * the bottom of the scrolling viewport before the next page is loaded
+			 * when using infinite scrolling.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type int
+			 */
+			"iLoadGap": null,
+			
+			/**
+			 * Viewport width for horizontal scrolling. Horizontal scrolling is 
+			 * disabled if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sX": null,
+			
+			/**
+			 * Width to expand the table to when using x-scrolling. Typically you
+			 * should not need to use this.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 *  @deprecated
+			 */
+			"sXInner": null,
+			
+			/**
+			 * Viewport height for vertical scrolling. Vertical scrolling is disabled
+			 * if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sY": null
+		},
+		
+		/**
+		 * Language information for the table.
+		 *  @namespace
+		 *  @extends DataTable.defaults.oLanguage
+		 */
+		"oLanguage": {
+			/**
+			 * Information callback function. See 
+			 * {@link DataTable.defaults.fnInfoCallback}
+			 *  @type function
+			 *  @default null
+			 */
+			"fnInfoCallback": null
+		},
+		
+		/**
+		 * Browser support parameters
+		 *  @namespace
+		 */
+		"oBrowser": {
+			/**
+			 * Indicate if the browser incorrectly calculates width:100% inside a
+			 * scrolling element (IE6/7)
+			 *  @type boolean
+			 *  @default false
+			 */
+			"bScrollOversize": false
+		},
+		
+		/**
+		 * Array referencing the nodes which are used for the features. The 
+		 * parameters of this object match what is allowed by sDom - i.e.
+		 *   <ul>
+		 *     <li>'l' - Length changing</li>
+		 *     <li>'f' - Filtering input</li>
+		 *     <li>'t' - The table!</li>
+		 *     <li>'i' - Information</li>
+		 *     <li>'p' - Pagination</li>
+		 *     <li>'r' - pRocessing</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aanFeatures": [],
+		
+		/**
+		 * Store data information - see {@link DataTable.models.oRow} for detailed
+		 * information.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoData": [],
+		
+		/**
+		 * Array of indexes which are in the current display (after filtering etc)
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplay": [],
+		
+		/**
+		 * Array of indexes for display - no filtering
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplayMaster": [],
+		
+		/**
+		 * Store information about each column that is in use
+		 *  @type array
+		 *  @default []
+		 */
+		"aoColumns": [],
+		
+		/**
+		 * Store information about the table's header
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeader": [],
+		
+		/**
+		 * Store information about the table's footer
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooter": [],
+		
+		/**
+		 * Search data array for regular expression searching
+		 *  @type array
+		 *  @default []
+		 */
+		"asDataSearch": [],
+		
+		/**
+		 * Store the applied global search information in case we want to force a 
+		 * research or compare the old search to a new one.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 */
+		"oPreviousSearch": {},
+		
+		/**
+		 * Store the applied search for each column - see 
+		 * {@link DataTable.models.oSearch} for the format that is used for the
+		 * filtering information for each column.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreSearchCols": [],
+		
+		/**
+		 * Sorting that is applied to the table. Note that the inner arrays are
+		 * used in the following manner:
+		 * <ul>
+		 *   <li>Index 0 - column number</li>
+		 *   <li>Index 1 - current sorting direction</li>
+		 *   <li>Index 2 - index of asSorting for this column</li>
+		 * </ul>
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @todo These inner arrays should really be objects
+		 */
+		"aaSorting": null,
+		
+		/**
+		 * Sorting that is always applied to the table (i.e. prefixed in front of
+		 * aaSorting).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array|null
+		 *  @default null
+		 */
+		"aaSortingFixed": null,
+		
+		/**
+		 * Classes to use for the striping of a table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"asStripeClasses": null,
+		
+		/**
+		 * If restoring a table - we should restore its striping classes as well
+		 *  @type array
+		 *  @default []
+		 */
+		"asDestroyStripes": [],
+		
+		/**
+		 * If restoring a table - we should restore its width 
+		 *  @type int
+		 *  @default 0
+		 */
+		"sDestroyWidth": 0,
+		
+		/**
+		 * Callback functions array for every time a row is inserted (i.e. on a draw).
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCallback": [],
+		
+		/**
+		 * Callback functions for the header on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeaderCallback": [],
+		
+		/**
+		 * Callback function for the footer on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooterCallback": [],
+		
+		/**
+		 * Array of callback functions for draw callback functions
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDrawCallback": [],
+		
+		/**
+		 * Array of callback functions for row created function
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCreatedCallback": [],
+		
+		/**
+		 * Callback functions for just before the table is redrawn. A return of 
+		 * false will be used to cancel the draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreDrawCallback": [],
+		
+		/**
+		 * Callback functions for when the table has been initialised.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoInitComplete": [],
+	
+		
+		/**
+		 * Callbacks for modifying the settings to be stored for state saving, prior to
+		 * saving state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSaveParams": [],
+		
+		/**
+		 * Callbacks for modifying the settings that have been stored for state saving
+		 * prior to using the stored values to restore the state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoadParams": [],
+		
+		/**
+		 * Callbacks for operating on the settings object once the saved state has been
+		 * loaded
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoaded": [],
+		
+		/**
+		 * Cache the table ID for quick access
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sTableId": "",
+		
+		/**
+		 * The TABLE node for the main table
+		 *  @type node
+		 *  @default null
+		 */
+		"nTable": null,
+		
+		/**
+		 * Permanent ref to the thead element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTHead": null,
+		
+		/**
+		 * Permanent ref to the tfoot element - if it exists
+		 *  @type node
+		 *  @default null
+		 */
+		"nTFoot": null,
+		
+		/**
+		 * Permanent ref to the tbody element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTBody": null,
+		
+		/**
+		 * Cache the wrapper node (contains all DataTables controlled elements)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTableWrapper": null,
+		
+		/**
+		 * Indicate if when using server-side processing the loading of data 
+		 * should be deferred until the second draw.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDeferLoading": false,
+		
+		/**
+		 * Indicate if all required information has been read in
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bInitialised": false,
+		
+		/**
+		 * Information about open rows. Each object in the array has the parameters
+		 * 'nTr' and 'nParent'
+		 *  @type array
+		 *  @default []
+		 */
+		"aoOpenRows": [],
+		
+		/**
+		 * Dictate the positioning of DataTables' control elements - see
+		 * {@link DataTable.model.oInit.sDom}.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sDom": null,
+		
+		/**
+		 * Which type of pagination should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string 
+		 *  @default two_button
+		 */
+		"sPaginationType": "two_button",
+		
+		/**
+		 * The cookie duration (for bStateSave) in seconds.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type int
+		 *  @default 0
+		 */
+		"iCookieDuration": 0,
+		
+		/**
+		 * The cookie name prefix.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sCookiePrefix": "",
+		
+		/**
+		 * Callback function for cookie creation.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 *  @default null
+		 */
+		"fnCookieCallback": null,
+		
+		/**
+		 * Array of callback functions for state saving. Each array element is an 
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings
+		 *       and the JSON string to save that has been thus far created. Returns
+		 *       a JSON string to be inserted into a json object 
+		 *       (i.e. '"param": [ 0, 1, 2]')</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSave": [],
+		
+		/**
+		 * Array of callback functions for state loading. Each array element is an 
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings 
+		 *       and the object stored. May return false to cancel state loading</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoad": [],
+		
+		/**
+		 * State that was loaded from the cookie. Useful for back reference
+		 *  @type object
+		 *  @default null
+		 */
+		"oLoadedState": null,
+		
+		/**
+		 * Source url for AJAX data for the table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sAjaxSource": null,
+		
+		/**
+		 * Property from a given object from which to read the table data from. This
+		 * can be an empty string (when not server-side processing), in which case 
+		 * it is  assumed an an array is given directly.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sAjaxDataProp": null,
+		
+		/**
+		 * Note if draw should be blocked while getting data
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bAjaxDataGet": true,
+		
+		/**
+		 * The last jQuery XHR object that was used for server-side data gathering. 
+		 * This can be used for working with the XHR information in one of the 
+		 * callbacks
+		 *  @type object
+		 *  @default null
+		 */
+		"jqXHR": null,
+		
+		/**
+		 * Function to get the server-side data.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnServerData": null,
+		
+		/**
+		 * Functions which are called prior to sending an Ajax request so extra 
+		 * parameters can easily be sent to the server
+		 *  @type array
+		 *  @default []
+		 */
+		"aoServerParams": [],
+		
+		/**
+		 * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if 
+		 * required).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sServerMethod": null,
+		
+		/**
+		 * Format numbers for display.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnFormatNumber": null,
+		
+		/**
+		 * List of options that can be used for the user selectable length menu.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"aLengthMenu": null,
+		
+		/**
+		 * Counter for the draws that the table does. Also used as a tracker for
+		 * server-side processing
+		 *  @type int
+		 *  @default 0
+		 */
+		"iDraw": 0,
+		
+		/**
+		 * Indicate if a redraw is being done - useful for Ajax
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDrawing": false,
+		
+		/**
+		 * Draw index (iDraw) of the last error when parsing the returned data
+		 *  @type int
+		 *  @default -1
+		 */
+		"iDrawError": -1,
+		
+		/**
+		 * Paging display length
+		 *  @type int
+		 *  @default 10
+		 */
+		"_iDisplayLength": 10,
+	
+		/**
+		 * Paging start point - aiDisplay index
+		 *  @type int
+		 *  @default 0
+		 */
+		"_iDisplayStart": 0,
+	
+		/**
+		 * Paging end point - aiDisplay index. Use fnDisplayEnd rather than
+		 * this property to get the end point
+		 *  @type int
+		 *  @default 10
+		 *  @private
+		 */
+		"_iDisplayEnd": 10,
+		
+		/**
+		 * Server-side processing - number of records in the result set
+		 * (i.e. before filtering), Use fnRecordsTotal rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type int
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsTotal": 0,
+	
+		/**
+		 * Server-side processing - number of records in the current display set
+		 * (i.e. after filtering). Use fnRecordsDisplay rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type boolean
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsDisplay": 0,
+		
+		/**
+		 * Flag to indicate if jQuery UI marking and classes should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bJUI": null,
+		
+		/**
+		 * The classes to use for the table
+		 *  @type object
+		 *  @default {}
+		 */
+		"oClasses": {},
+		
+		/**
+		 * Flag attached to the settings object so you can check in the draw 
+		 * callback if filtering has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bFiltered": false,
+		
+		/**
+		 * Flag attached to the settings object so you can check in the draw 
+		 * callback if sorting has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bSorted": false,
+		
+		/**
+		 * Indicate that if multiple rows are in the header and there is more than 
+		 * one unique cell per column, if the top one (true) or bottom one (false) 
+		 * should be used for sorting / title by DataTables.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bSortCellsTop": null,
+		
+		/**
+		 * Initialisation object that is used for the table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInit": null,
+		
+		/**
+		 * Destroy callback functions - for plug-ins to attach themselves to the
+		 * destroy so they can clean up markup and events.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDestroyCallback": [],
+	
+		
+		/**
+		 * Get the number of records in the current record set, before filtering
+		 *  @type function
+		 */
+		"fnRecordsTotal": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				return parseInt(this._iRecordsTotal, 10);
+			} else {
+				return this.aiDisplayMaster.length;
+			}
+		},
+		
+		/**
+		 * Get the number of records in the current record set, after filtering
+		 *  @type function
+		 */
+		"fnRecordsDisplay": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				return parseInt(this._iRecordsDisplay, 10);
+			} else {
+				return this.aiDisplay.length;
+			}
+		},
+		
+		/**
+		 * Set the display end point - aiDisplay index
+		 *  @type function
+		 *  @todo Should do away with _iDisplayEnd and calculate it on-the-fly here
+		 */
+		"fnDisplayEnd": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				if ( this.oFeatures.bPaginate === false || this._iDisplayLength == -1 ) {
+					return this._iDisplayStart+this.aiDisplay.length;
+				} else {
+					return Math.min( this._iDisplayStart+this._iDisplayLength, 
+						this._iRecordsDisplay );
+				}
+			} else {
+				return this._iDisplayEnd;
+			}
+		},
+		
+		/**
+		 * The DataTables object for this table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInstance": null,
+		
+		/**
+		 * Unique identifier for each instance of the DataTables object. If there
+		 * is an ID on the table node, then it takes that value, otherwise an
+		 * incrementing internal counter is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sInstance": null,
+	
+		/**
+		 * tabindex attribute value that is added to DataTables control elements, allowing
+		 * keyboard navigation of the table and its controls.
+		 */
+		"iTabIndex": 0,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollHead": null,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollFoot": null
+	};
+
+	/**
+	 * Extension object for DataTables that is used to provide all extension options.
+	 * 
+	 * Note that the <i>DataTable.ext</i> object is available through
+	 * <i>jQuery.fn.dataTable.ext</i> where it may be accessed and manipulated. It is
+	 * also aliased to <i>jQuery.fn.dataTableExt</i> for historic reasons.
+	 *  @namespace
+	 *  @extends DataTable.models.ext
+	 */
+	DataTable.ext = $.extend( true, {}, DataTable.models.ext );
+	
+	$.extend( DataTable.ext.oStdClasses, {
+		"sTable": "dataTable",
+	
+		/* Two buttons buttons */
+		"sPagePrevEnabled": "paginate_enabled_previous",
+		"sPagePrevDisabled": "paginate_disabled_previous",
+		"sPageNextEnabled": "paginate_enabled_next",
+		"sPageNextDisabled": "paginate_disabled_next",
+		"sPageJUINext": "",
+		"sPageJUIPrev": "",
+		
+		/* Full numbers paging buttons */
+		"sPageButton": "paginate_button",
+		"sPageButtonActive": "paginate_active",
+		"sPageButtonStaticDisabled": "paginate_button paginate_button_disabled",
+		"sPageFirst": "first",
+		"sPagePrevious": "previous",
+		"sPageNext": "next",
+		"sPageLast": "last",
+		
+		/* Striping classes */
+		"sStripeOdd": "odd",
+		"sStripeEven": "even",
+		
+		/* Empty row */
+		"sRowEmpty": "dataTables_empty",
+		
+		/* Features */
+		"sWrapper": "dataTables_wrapper",
+		"sFilter": "dataTables_filter",
+		"sInfo": "dataTables_info",
+		"sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */
+		"sLength": "dataTables_length",
+		"sProcessing": "dataTables_processing",
+		
+		/* Sorting */
+		"sSortAsc": "sorting_asc",
+		"sSortDesc": "sorting_desc",
+		"sSortable": "sorting", /* Sortable in both directions */
+		"sSortableAsc": "sorting_asc_disabled",
+		"sSortableDesc": "sorting_desc_disabled",
+		"sSortableNone": "sorting_disabled",
+		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
+		"sSortJUIAsc": "",
+		"sSortJUIDesc": "",
+		"sSortJUI": "",
+		"sSortJUIAscAllowed": "",
+		"sSortJUIDescAllowed": "",
+		"sSortJUIWrapper": "",
+		"sSortIcon": "",
+		
+		/* Scrolling */
+		"sScrollWrapper": "dataTables_scroll",
+		"sScrollHead": "dataTables_scrollHead",
+		"sScrollHeadInner": "dataTables_scrollHeadInner",
+		"sScrollBody": "dataTables_scrollBody",
+		"sScrollFoot": "dataTables_scrollFoot",
+		"sScrollFootInner": "dataTables_scrollFootInner",
+		
+		/* Misc */
+		"sFooterTH": "",
+		"sJUIHeader": "",
+		"sJUIFooter": ""
+	} );
+	
+	
+	$.extend( DataTable.ext.oJUIClasses, DataTable.ext.oStdClasses, {
+		/* Two buttons buttons */
+		"sPagePrevEnabled": "fg-button ui-button ui-state-default ui-corner-left",
+		"sPagePrevDisabled": "fg-button ui-button ui-state-default ui-corner-left ui-state-disabled",
+		"sPageNextEnabled": "fg-button ui-button ui-state-default ui-corner-right",
+		"sPageNextDisabled": "fg-button ui-button ui-state-default ui-corner-right ui-state-disabled",
+		"sPageJUINext": "ui-icon ui-icon-circle-arrow-e",
+		"sPageJUIPrev": "ui-icon ui-icon-circle-arrow-w",
+		
+		/* Full numbers paging buttons */
+		"sPageButton": "fg-button ui-button ui-state-default",
+		"sPageButtonActive": "fg-button ui-button ui-state-default ui-state-disabled",
+		"sPageButtonStaticDisabled": "fg-button ui-button ui-state-default ui-state-disabled",
+		"sPageFirst": "first ui-corner-tl ui-corner-bl",
+		"sPageLast": "last ui-corner-tr ui-corner-br",
+		
+		/* Features */
+		"sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+
+			"ui-buttonset-multi paging_", /* Note that the type is postfixed */
+		
+		/* Sorting */
+		"sSortAsc": "ui-state-default",
+		"sSortDesc": "ui-state-default",
+		"sSortable": "ui-state-default",
+		"sSortableAsc": "ui-state-default",
+		"sSortableDesc": "ui-state-default",
+		"sSortableNone": "ui-state-default",
+		"sSortJUIAsc": "css_right ui-icon ui-icon-triangle-1-n",
+		"sSortJUIDesc": "css_right ui-icon ui-icon-triangle-1-s",
+		"sSortJUI": "css_right ui-icon ui-icon-carat-2-n-s",
+		"sSortJUIAscAllowed": "css_right ui-icon ui-icon-carat-1-n",
+		"sSortJUIDescAllowed": "css_right ui-icon ui-icon-carat-1-s",
+		"sSortJUIWrapper": "DataTables_sort_wrapper",
+		"sSortIcon": "DataTables_sort_icon",
+		
+		/* Scrolling */
+		"sScrollHead": "dataTables_scrollHead ui-state-default",
+		"sScrollFoot": "dataTables_scrollFoot ui-state-default",
+		
+		/* Misc */
+		"sFooterTH": "ui-state-default",
+		"sJUIHeader": "fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix",
+		"sJUIFooter": "fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix"
+	} );
+	
+	/*
+	 * Variable: oPagination
+	 * Purpose:  
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	$.extend( DataTable.ext.oPagination, {
+		/*
+		 * Variable: two_button
+		 * Purpose:  Standard two button (forward/back) pagination
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"two_button": {
+			/*
+			 * Function: oPagination.two_button.fnInit
+			 * Purpose:  Initialise dom elements required for pagination with forward/back buttons only
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           node:nPaging - the DIV which contains this pagination control
+			 *           function:fnCallbackDraw - draw function which must be called on update
+			 */
+			"fnInit": function ( oSettings, nPaging, fnCallbackDraw )
+			{
+				var oLang = oSettings.oLanguage.oPaginate;
+				var oClasses = oSettings.oClasses;
+				var fnClickHandler = function ( e ) {
+					if ( oSettings.oApi._fnPageChange( oSettings, e.data.action ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				};
+	
+				var sAppend = (!oSettings.bJUI) ?
+					'<a class="'+oSettings.oClasses.sPagePrevDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button">'+oLang.sPrevious+'</a>'+
+					'<a class="'+oSettings.oClasses.sPageNextDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button">'+oLang.sNext+'</a>'
+					:
+					'<a class="'+oSettings.oClasses.sPagePrevDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button"><span class="'+oSettings.oClasses.sPageJUIPrev+'"></span></a>'+
+					'<a class="'+oSettings.oClasses.sPageNextDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button"><span class="'+oSettings.oClasses.sPageJUINext+'"></span></a>';
+				$(nPaging).append( sAppend );
+				
+				var els = $('a', nPaging);
+				var nPrevious = els[0],
+					nNext = els[1];
+				
+				oSettings.oApi._fnBindAction( nPrevious, {action: "previous"}, fnClickHandler );
+				oSettings.oApi._fnBindAction( nNext,     {action: "next"},     fnClickHandler );
+				
+				/* ID the first elements only */
+				if ( !oSettings.aanFeatures.p )
+				{
+					nPaging.id = oSettings.sTableId+'_paginate';
+					nPrevious.id = oSettings.sTableId+'_previous';
+					nNext.id = oSettings.sTableId+'_next';
+	
+					nPrevious.setAttribute('aria-controls', oSettings.sTableId);
+					nNext.setAttribute('aria-controls', oSettings.sTableId);
+				}
+			},
+			
+			/*
+			 * Function: oPagination.two_button.fnUpdate
+			 * Purpose:  Update the two button pagination at the end of the draw
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           function:fnCallbackDraw - draw function to call on page change
+			 */
+			"fnUpdate": function ( oSettings, fnCallbackDraw )
+			{
+				if ( !oSettings.aanFeatures.p )
+				{
+					return;
+				}
+				
+				var oClasses = oSettings.oClasses;
+				var an = oSettings.aanFeatures.p;
+				var nNode;
+	
+				/* Loop over each instance of the pager */
+				for ( var i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					nNode = an[i].firstChild;
+					if ( nNode )
+					{
+						/* Previous page */
+						nNode.className = ( oSettings._iDisplayStart === 0 ) ?
+						    oClasses.sPagePrevDisabled : oClasses.sPagePrevEnabled;
+						    
+						/* Next page */
+						nNode = nNode.nextSibling;
+						nNode.className = ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) ?
+						    oClasses.sPageNextDisabled : oClasses.sPageNextEnabled;
+					}
+				}
+			}
+		},
+		
+		
+		/*
+		 * Variable: iFullNumbersShowPages
+		 * Purpose:  Change the number of pages which can be seen
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"iFullNumbersShowPages": 5,
+		
+		/*
+		 * Variable: full_numbers
+		 * Purpose:  Full numbers pagination
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"full_numbers": {
+			/*
+			 * Function: oPagination.full_numbers.fnInit
+			 * Purpose:  Initialise dom elements required for pagination with a list of the pages
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           node:nPaging - the DIV which contains this pagination control
+			 *           function:fnCallbackDraw - draw function which must be called on update
+			 */
+			"fnInit": function ( oSettings, nPaging, fnCallbackDraw )
+			{
+				var oLang = oSettings.oLanguage.oPaginate;
+				var oClasses = oSettings.oClasses;
+				var fnClickHandler = function ( e ) {
+					if ( oSettings.oApi._fnPageChange( oSettings, e.data.action ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				};
+	
+				$(nPaging).append(
+					'<a  tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageFirst+'">'+oLang.sFirst+'</a>'+
+					'<a  tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPagePrevious+'">'+oLang.sPrevious+'</a>'+
+					'<span></span>'+
+					'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageNext+'">'+oLang.sNext+'</a>'+
+					'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageLast+'">'+oLang.sLast+'</a>'
+				);
+				var els = $('a', nPaging);
+				var nFirst = els[0],
+					nPrev = els[1],
+					nNext = els[2],
+					nLast = els[3];
+				
+				oSettings.oApi._fnBindAction( nFirst, {action: "first"},    fnClickHandler );
+				oSettings.oApi._fnBindAction( nPrev,  {action: "previous"}, fnClickHandler );
+				oSettings.oApi._fnBindAction( nNext,  {action: "next"},     fnClickHandler );
+				oSettings.oApi._fnBindAction( nLast,  {action: "last"},     fnClickHandler );
+				
+				/* ID the first elements only */
+				if ( !oSettings.aanFeatures.p )
+				{
+					nPaging.id = oSettings.sTableId+'_paginate';
+					nFirst.id =oSettings.sTableId+'_first';
+					nPrev.id =oSettings.sTableId+'_previous';
+					nNext.id =oSettings.sTableId+'_next';
+					nLast.id =oSettings.sTableId+'_last';
+				}
+			},
+			
+			/*
+			 * Function: oPagination.full_numbers.fnUpdate
+			 * Purpose:  Update the list of page buttons shows
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           function:fnCallbackDraw - draw function to call on page change
+			 */
+			"fnUpdate": function ( oSettings, fnCallbackDraw )
+			{
+				if ( !oSettings.aanFeatures.p )
+				{
+					return;
+				}
+				
+				var iPageCount = DataTable.ext.oPagination.iFullNumbersShowPages;
+				var iPageCountHalf = Math.floor(iPageCount / 2);
+				var iPages = Math.ceil((oSettings.fnRecordsDisplay()) / oSettings._iDisplayLength);
+				var iCurrentPage = Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength) + 1;
+				var sList = "";
+				var iStartButton, iEndButton, i, iLen;
+				var oClasses = oSettings.oClasses;
+				var anButtons, anStatic, nPaginateList, nNode;
+				var an = oSettings.aanFeatures.p;
+				var fnBind = function (j) {
+					oSettings.oApi._fnBindAction( this, {"page": j+iStartButton-1}, function(e) {
+						/* Use the information in the element to jump to the required page */
+						oSettings.oApi._fnPageChange( oSettings, e.data.page );
+						fnCallbackDraw( oSettings );
+						e.preventDefault();
+					} );
+				};
+				
+				/* Pages calculation */
+				if ( oSettings._iDisplayLength === -1 )
+				{
+					iStartButton = 1;
+					iEndButton = 1;
+					iCurrentPage = 1;
+				}
+				else if (iPages < iPageCount)
+				{
+					iStartButton = 1;
+					iEndButton = iPages;
+				}
+				else if (iCurrentPage <= iPageCountHalf)
+				{
+					iStartButton = 1;
+					iEndButton = iPageCount;
+				}
+				else if (iCurrentPage >= (iPages - iPageCountHalf))
+				{
+					iStartButton = iPages - iPageCount + 1;
+					iEndButton = iPages;
+				}
+				else
+				{
+					iStartButton = iCurrentPage - Math.ceil(iPageCount / 2) + 1;
+					iEndButton = iStartButton + iPageCount - 1;
+				}
+	
+				
+				/* Build the dynamic list */
+				for ( i=iStartButton ; i<=iEndButton ; i++ )
+				{
+					sList += (iCurrentPage !== i) ?
+						'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+'">'+oSettings.fnFormatNumber(i)+'</a>' :
+						'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButtonActive+'">'+oSettings.fnFormatNumber(i)+'</a>';
+				}
+				
+				/* Loop over each instance of the pager */
+				for ( i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					nNode = an[i];
+					if ( !nNode.hasChildNodes() )
+					{
+						continue;
+					}
+					
+					/* Build up the dynamic list first - html and listeners */
+					$('span:eq(0)', nNode)
+						.html( sList )
+						.children('a').each( fnBind );
+					
+					/* Update the permanent button's classes */
+					anButtons = nNode.getElementsByTagName('a');
+					anStatic = [
+						anButtons[0], anButtons[1], 
+						anButtons[anButtons.length-2], anButtons[anButtons.length-1]
+					];
+	
+					$(anStatic).removeClass( oClasses.sPageButton+" "+oClasses.sPageButtonActive+" "+oClasses.sPageButtonStaticDisabled );
+					$([anStatic[0], anStatic[1]]).addClass( 
+						(iCurrentPage==1) ?
+							oClasses.sPageButtonStaticDisabled :
+							oClasses.sPageButton
+					);
+					$([anStatic[2], anStatic[3]]).addClass(
+						(iPages===0 || iCurrentPage===iPages || oSettings._iDisplayLength===-1) ?
+							oClasses.sPageButtonStaticDisabled :
+							oClasses.sPageButton
+					);
+				}
+			}
+		}
+	} );
+	
+	$.extend( DataTable.ext.oSort, {
+		/*
+		 * text sorting
+		 */
+		"string-pre": function ( a )
+		{
+			if ( typeof a != 'string' ) {
+				a = (a !== null && a.toString) ? a.toString() : '';
+			}
+			return a.toLowerCase();
+		},
+	
+		"string-asc": function ( x, y )
+		{
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+		
+		"string-desc": function ( x, y )
+		{
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		},
+		
+		
+		/*
+		 * html sorting (ignore html tags)
+		 */
+		"html-pre": function ( a )
+		{
+			return a.replace( /<.*?>/g, "" ).toLowerCase();
+		},
+		
+		"html-asc": function ( x, y )
+		{
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+		
+		"html-desc": function ( x, y )
+		{
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		},
+		
+		
+		/*
+		 * date sorting
+		 */
+		"date-pre": function ( a )
+		{
+			var x = Date.parse( a );
+			
+			if ( isNaN(x) || x==="" )
+			{
+				x = Date.parse( "01/01/1970 00:00:00" );
+			}
+			return x;
+		},
+	
+		"date-asc": function ( x, y )
+		{
+			return x - y;
+		},
+		
+		"date-desc": function ( x, y )
+		{
+			return y - x;
+		},
+		
+		
+		/*
+		 * numerical sorting
+		 */
+		"numeric-pre": function ( a )
+		{
+			return (a=="-" || a==="") ? 0 : a*1;
+		},
+	
+		"numeric-asc": function ( x, y )
+		{
+			return x - y;
+		},
+		
+		"numeric-desc": function ( x, y )
+		{
+			return y - x;
+		}
+	} );
+	
+	
+	$.extend( DataTable.ext.aTypes, [
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string is numeric
+		 * Returns:  string:'numeric' or null
+		 * Inputs:   mixed:sText - string to check
+		 */
+		function ( sData )
+		{
+			/* Allow zero length strings as a number */
+			if ( typeof sData === 'number' )
+			{
+				return 'numeric';
+			}
+			else if ( typeof sData !== 'string' )
+			{
+				return null;
+			}
+			
+			var sValidFirstChars = "0123456789-";
+			var sValidChars = "0123456789.";
+			var Char;
+			var bDecimal = false;
+			
+			/* Check for a valid first char (no period and allow negatives) */
+			Char = sData.charAt(0); 
+			if (sValidFirstChars.indexOf(Char) == -1) 
+			{
+				return null;
+			}
+			
+			/* Check all the other characters are valid */
+			for ( var i=1 ; i<sData.length ; i++ ) 
+			{
+				Char = sData.charAt(i); 
+				if (sValidChars.indexOf(Char) == -1) 
+				{
+					return null;
+				}
+				
+				/* Only allowed one decimal place... */
+				if ( Char == "." )
+				{
+					if ( bDecimal )
+					{
+						return null;
+					}
+					bDecimal = true;
+				}
+			}
+			
+			return 'numeric';
+		},
+		
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string is actually a formatted date
+		 * Returns:  string:'date' or null
+		 * Inputs:   string:sText - string to check
+		 */
+		function ( sData )
+		{
+			var iParse = Date.parse(sData);
+			if ( (iParse !== null && !isNaN(iParse)) || (typeof sData === 'string' && sData.length === 0) )
+			{
+				return 'date';
+			}
+			return null;
+		},
+		
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string should be treated as an HTML string
+		 * Returns:  string:'html' or null
+		 * Inputs:   string:sText - string to check
+		 */
+		function ( sData )
+		{
+			if ( typeof sData === 'string' && sData.indexOf('<') != -1 && sData.indexOf('>') != -1 )
+			{
+				return 'html';
+			}
+			return null;
+		}
+	] );
+	
+
+	// jQuery aliases
+	$.fn.DataTable = DataTable;
+	$.fn.dataTable = DataTable;
+	$.fn.dataTableSettings = DataTable.settings;
+	$.fn.dataTableExt = DataTable.ext;
+
+
+	// Information about events fired by DataTables - for documentation.
+	/**
+	 * Draw event, fired whenever the table is redrawn on the page, at the same point as
+	 * fnDrawCallback. This may be useful for binding events or performing calculations when
+	 * the table is altered at all.
+	 *  @name DataTable#draw
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Filter event, fired when the filtering applied to the table (using the build in global
+	 * global filter, or column filters) is altered.
+	 *  @name DataTable#filter
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Page change event, fired when the paging of the table is altered.
+	 *  @name DataTable#page
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Sort event, fired when the sorting applied to the table is altered.
+	 *  @name DataTable#sort
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * DataTables initialisation complete event, fired when the table is fully drawn,
+	 * including Ajax data loaded, if Ajax data is required.
+	 *  @name DataTable#init
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The JSON object request from the server - only
+	 *    present if client-side Ajax sourced data is used</li></ol>
+	 */
+
+	/**
+	 * State save event, fired when the table has changed state a new state save is required.
+	 * This method allows modification of the state saving object prior to actually doing the
+	 * save, including addition or other state properties (for plug-ins) or modification
+	 * of a DataTables core property.
+	 *  @name DataTable#stateSaveParams
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The state information to be saved
+	 */
+
+	/**
+	 * State load event, fired when the table is loading state from the stored data, but
+	 * prior to the settings object being modified by the saved state - allowing modification
+	 * of the saved state is required or loading of state for a plug-in.
+	 *  @name DataTable#stateLoadParams
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * State loaded event, fired when state has been loaded from stored data and the settings
+	 * object has been modified by the loaded data.
+	 *  @name DataTable#stateLoaded
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * Processing event, fired when DataTables is doing some kind of processing (be it,
+	 * sort, filter or anything else). Can be used to indicate to the end user that
+	 * there is something happening, or that something has finished.
+	 *  @name DataTable#processing
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {boolean} bShow Flag for if DataTables is doing processing or not
+	 */
+
+	/**
+	 * Ajax (XHR) event, fired whenever an Ajax request is completed from a request to 
+	 * made to the server for new data (note that this trigger is called in fnServerData,
+	 * if you override fnServerData and which to use this event, you need to trigger it in
+	 * you success function).
+	 *  @name DataTable#xhr
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 *  @param {object} json JSON returned from the server
+	 */
+
+	/**
+	 * Destroy event, fired when the DataTable is destroyed by calling fnDestroy or passing
+	 * the bDestroy:true parameter in the initialisation object. This can be used to remove
+	 * bound events, added DOM nodes, etc.
+	 *  @name DataTable#destroy
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+}));
+
+}(window, document));
+
diff --git a/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery.js b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery.js
new file mode 100755
index 0000000000000000000000000000000000000000..f65cf1dc4573c51e54d7cf3772d06caf96726616
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery.js
@@ -0,0 +1,2 @@
+/*! jQuery v1.8.2 jquery.com | jquery.org/license */
+(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d<e;d++)p.event.add(b,c,h[c][d])}g.data&&(g.data=p.extend({},g.data))}function bE(a,b){var c;if(b.nodeType!==1)return;b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?(b.parentNode&&(b.outerHTML=a.outerHTML),p.support.html5Clone&&a.innerHTML&&!p.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):c==="input"&&bv.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text),b.removeAttribute(p.expando)}function bF(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bG(a){bv.test(a.type)&&(a.defaultChecked=a.checked)}function bY(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=bW.length;while(e--){b=bW[e]+c;if(b in a)return b}return d}function bZ(a,b){return a=b||a,p.css(a,"display")==="none"||!p.contains(a.ownerDocument,a)}function b$(a,b){var c,d,e=[],f=0,g=a.length;for(;f<g;f++){c=a[f];if(!c.style)continue;e[f]=p._data(c,"olddisplay"),b?(!e[f]&&c.style.display==="none"&&(c.style.display=""),c.style.display===""&&bZ(c)&&(e[f]=p._data(c,"olddisplay",cc(c.nodeName)))):(d=bH(c,"display"),!e[f]&&d!=="none"&&p._data(c,"olddisplay",d))}for(f=0;f<g;f++){c=a[f];if(!c.style)continue;if(!b||c.style.display==="none"||c.style.display==="")c.style.display=b?e[f]||"":"none"}return a}function b_(a,b,c){var d=bP.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function ca(a,b,c,d){var e=c===(d?"border":"content")?4:b==="width"?1:0,f=0;for(;e<4;e+=2)c==="margin"&&(f+=p.css(a,c+bV[e],!0)),d?(c==="content"&&(f-=parseFloat(bH(a,"padding"+bV[e]))||0),c!=="margin"&&(f-=parseFloat(bH(a,"border"+bV[e]+"Width"))||0)):(f+=parseFloat(bH(a,"padding"+bV[e]))||0,c!=="padding"&&(f+=parseFloat(bH(a,"border"+bV[e]+"Width"))||0));return f}function cb(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=!0,f=p.support.boxSizing&&p.css(a,"boxSizing")==="border-box";if(d<=0||d==null){d=bH(a,b);if(d<0||d==null)d=a.style[b];if(bQ.test(d))return d;e=f&&(p.support.boxSizingReliable||d===a.style[b]),d=parseFloat(d)||0}return d+ca(a,b,c||(f?"border":"content"),e)+"px"}function cc(a){if(bS[a])return bS[a];var b=p("<"+a+">").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write("<!doctype html><html><body>"),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bS[a]=c,c}function ci(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||ce.test(a)?d(a,e):ci(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ci(a+"["+e+"]",b[e],c,d);else d(a,b)}function cz(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h<i;h++)d=g[h],f=/^\+/.test(d),f&&(d=d.substr(1)||"*"),e=a[d]=a[d]||[],e[f?"unshift":"push"](c)}}function cA(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h,i=a[f],j=0,k=i?i.length:0,l=a===cv;for(;j<k&&(l||!h);j++)h=i[j](c,d,e),typeof h=="string"&&(!l||g[h]?h=b:(c.dataTypes.unshift(h),h=cA(a,c,d,e,h,g)));return(l||!h)&&!g["*"]&&(h=cA(a,c,d,e,"*",g)),h}function cB(a,c){var d,e,f=p.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((f[d]?a:e||(e={}))[d]=c[d]);e&&p.extend(!0,a,e)}function cC(a,c,d){var e,f,g,h,i=a.contents,j=a.dataTypes,k=a.responseFields;for(f in k)f in d&&(c[k[f]]=d[f]);while(j[0]==="*")j.shift(),e===b&&(e=a.mimeType||c.getResponseHeader("content-type"));if(e)for(f in i)if(i[f]&&i[f].test(e)){j.unshift(f);break}if(j[0]in d)g=j[0];else{for(f in d){if(!j[0]||a.converters[f+" "+j[0]]){g=f;break}h||(h=f)}g=g||h}if(g)return g!==j[0]&&j.unshift(g),d[g]}function cD(a,b){var c,d,e,f,g=a.dataTypes.slice(),h=g[0],i={},j=0;a.dataFilter&&(b=a.dataFilter(b,a.dataType));if(g[1])for(c in a.converters)i[c.toLowerCase()]=a.converters[c];for(;e=g[++j];)if(e!=="*"){if(h!=="*"&&h!==e){c=i[h+" "+e]||i["* "+e];if(!c)for(d in i){f=d.split(" ");if(f[1]===e){c=i[h+" "+f[0]]||i["* "+f[0]];if(c){c===!0?c=i[d]:i[d]!==!0&&(e=f[0],g.splice(j--,0,e));break}}}if(c!==!0)if(c&&a["throws"])b=c(b);else try{b=c(b)}catch(k){return{state:"parsererror",error:c?k:"No conversion from "+h+" to "+e}}}h=e}return{state:"success",data:b}}function cL(){try{return new a.XMLHttpRequest}catch(b){}}function cM(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function cU(){return setTimeout(function(){cN=b},0),cN=p.now()}function cV(a,b){p.each(b,function(b,c){var d=(cT[b]||[]).concat(cT["*"]),e=0,f=d.length;for(;e<f;e++)if(d[e].call(a,b,c))return})}function cW(a,b,c){var d,e=0,f=0,g=cS.length,h=p.Deferred().always(function(){delete i.elem}),i=function(){var b=cN||cU(),c=Math.max(0,j.startTime+j.duration-b),d=1-(c/j.duration||0),e=0,f=j.tweens.length;for(;e<f;e++)j.tweens[e].run(d);return h.notifyWith(a,[j,d,c]),d<1&&f?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:p.extend({},b),opts:p.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:cN||cU(),duration:c.duration,tweens:[],createTween:function(b,c,d){var e=p.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(e),e},stop:function(b){var c=0,d=b?j.tweens.length:0;for(;c<d;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;cX(k,j.opts.specialEasing);for(;e<g;e++){d=cS[e].call(j,a,k,j.opts);if(d)return d}return cV(j,k),p.isFunction(j.opts.start)&&j.opts.start.call(a,j),p.fx.timer(p.extend(i,{anim:j,queue:j.opts.queue,elem:a})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}function cX(a,b){var c,d,e,f,g;for(c in a){d=p.camelCase(c),e=b[d],f=a[c],p.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=p.cssHooks[d];if(g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}}function cY(a,b,c){var d,e,f,g,h,i,j,k,l=this,m=a.style,n={},o=[],q=a.nodeType&&bZ(a);c.queue||(j=p._queueHooks(a,"fx"),j.unqueued==null&&(j.unqueued=0,k=j.empty.fire,j.empty.fire=function(){j.unqueued||k()}),j.unqueued++,l.always(function(){l.always(function(){j.unqueued--,p.queue(a,"fx").length||j.empty.fire()})})),a.nodeType===1&&("height"in b||"width"in b)&&(c.overflow=[m.overflow,m.overflowX,m.overflowY],p.css(a,"display")==="inline"&&p.css(a,"float")==="none"&&(!p.support.inlineBlockNeedsLayout||cc(a.nodeName)==="inline"?m.display="inline-block":m.zoom=1)),c.overflow&&(m.overflow="hidden",p.support.shrinkWrapBlocks||l.done(function(){m.overflow=c.overflow[0],m.overflowX=c.overflow[1],m.overflowY=c.overflow[2]}));for(d in b){f=b[d];if(cP.exec(f)){delete b[d];if(f===(q?"hide":"show"))continue;o.push(d)}}g=o.length;if(g){h=p._data(a,"fxshow")||p._data(a,"fxshow",{}),q?p(a).show():l.done(function(){p(a).hide()}),l.done(function(){var b;p.removeData(a,"fxshow",!0);for(b in n)p.style(a,b,n[b])});for(d=0;d<g;d++)e=o[d],i=l.createTween(e,q?h[e]:0),n[e]=h[e]||p.style(a,e),e in h||(h[e]=i.start,q&&(i.end=i.start,i.start=e==="width"||e==="height"?1:0))}}function cZ(a,b,c,d,e){return new cZ.prototype.init(a,b,c,d,e)}function c$(a,b){var c,d={height:a},e=0;b=b?1:0;for(;e<4;e+=2-b)c=bV[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function da(a){return p.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}var c,d,e=a.document,f=a.location,g=a.navigator,h=a.jQuery,i=a.$,j=Array.prototype.push,k=Array.prototype.slice,l=Array.prototype.indexOf,m=Object.prototype.toString,n=Object.prototype.hasOwnProperty,o=String.prototype.trim,p=function(a,b){return new p.fn.init(a,b,c)},q=/[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,r=/\S/,s=/\s+/,t=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,u=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.2",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i<j;i++)if((a=arguments[i])!=null)for(c in a){d=h[c],e=a[c];if(h===e)continue;k&&e&&(p.isPlainObject(e)||(f=p.isArray(e)))?(f?(f=!1,g=d&&p.isArray(d)?d:[]):g=d&&p.isPlainObject(d)?d:{},h[c]=p.extend(k,g,e)):e!==b&&(h[c]=e)}return h},p.extend({noConflict:function(b){return a.$===p&&(a.$=i),b&&a.jQuery===p&&(a.jQuery=h),p},isReady:!1,readyWait:1,holdReady:function(a){a?p.readyWait++:p.ready(!0)},ready:function(a){if(a===!0?--p.readyWait:p.isReady)return;if(!e.body)return setTimeout(p.ready,1);p.isReady=!0;if(a!==!0&&--p.readyWait>0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f<g;)if(c.apply(a[f++],d)===!1)break}else if(h){for(e in a)if(c.call(a[e],e,a[e])===!1)break}else for(;f<g;)if(c.call(a[f],f,a[f++])===!1)break;return a},trim:o&&!o.call(" ")?function(a){return a==null?"":o.call(a)}:function(a){return a==null?"":(a+"").replace(t,"")},makeArray:function(a,b){var c,d=b||[];return a!=null&&(c=p.type(a),a.length==null||c==="string"||c==="function"||c==="regexp"||p.isWindow(a)?j.call(d,a):p.merge(d,a)),d},inArray:function(a,b,c){var d;if(b){if(l)return l.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=c.length,e=a.length,f=0;if(typeof d=="number")for(;f<d;f++)a[e++]=c[f];else while(c[f]!==b)a[e++]=c[f++];return a.length=e,a},grep:function(a,b,c){var d,e=[],f=0,g=a.length;c=!!c;for(;f<g;f++)d=!!b(a[f],f),c!==d&&e.push(a[f]);return e},map:function(a,c,d){var e,f,g=[],h=0,i=a.length,j=a instanceof p||i!==b&&typeof i=="number"&&(i>0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h<i;h++)e=c(a[h],h,d),e!=null&&(g[g.length]=e);else for(f in a)e=c(a[f],f,d),e!=null&&(g[g.length]=e);return g.concat.apply([],g)},guid:1,proxy:function(a,c){var d,e,f;return typeof c=="string"&&(d=a[c],c=a,a=d),p.isFunction(a)?(e=k.call(arguments,2),f=function(){return a.apply(c,e.concat(k.call(arguments)))},f.guid=a.guid=a.guid||p.guid++,f):b},access:function(a,c,d,e,f,g,h){var i,j=d==null,k=0,l=a.length;if(d&&typeof d=="object"){for(k in d)p.access(a,c,k,d[k],1,g,e);f=1}else if(e!==b){i=h===b&&p.isFunction(e),j&&(i?(i=c,c=function(a,b,c){return i.call(p(a),c)}):(c.call(a,e),c=null));if(c)for(;k<l;k++)c(a[k],d,i?e.call(a[k],k,c(a[k],d)):e,h);f=1}return f?a:j?c.call(a):l?c(a[0],d):g},now:function(){return(new Date).getTime()}}),p.ready.promise=function(b){if(!d){d=p.Deferred();if(e.readyState==="complete")setTimeout(p.ready,1);else if(e.addEventListener)e.addEventListener("DOMContentLoaded",D,!1),a.addEventListener("load",p.ready,!1);else{e.attachEvent("onreadystatechange",D),a.attachEvent("onload",p.ready);var c=!1;try{c=a.frameElement==null&&e.documentElement}catch(f){}c&&c.doScroll&&function g(){if(!p.isReady){try{c.doScroll("left")}catch(a){return setTimeout(g,50)}p.ready()}}()}}return d.promise(b)},p.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){E["[object "+b+"]"]=b.toLowerCase()}),c=p(e);var F={};p.Callbacks=function(a){a=typeof a=="string"?F[a]||G(a):p.extend({},a);var c,d,e,f,g,h,i=[],j=!a.once&&[],k=function(b){c=a.memory&&b,d=!0,h=f||0,f=0,g=i.length,e=!0;for(;i&&h<g;h++)if(i[h].apply(b[0],b[1])===!1&&a.stopOnFalse){c=!1;break}e=!1,i&&(j?j.length&&k(j.shift()):c?i=[]:l.disable())},l={add:function(){if(i){var b=i.length;(function d(b){p.each(b,function(b,c){var e=p.type(c);e==="function"&&(!a.unique||!l.has(c))?i.push(c):c&&c.length&&e!=="string"&&d(c)})})(arguments),e?g=i.length:c&&(f=b,k(c))}return this},remove:function(){return i&&p.each(arguments,function(a,b){var c;while((c=p.inArray(b,i,c))>-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return a!=null?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b<d;b++)c[b]&&p.isFunction(c[b].promise)?c[b].promise().done(g(b,j,c)).fail(f.reject).progress(g(b,i,h)):--e}return e||f.resolveWith(j,c),f.promise()}}),p.support=function(){var b,c,d,f,g,h,i,j,k,l,m,n=e.createElement("div");n.setAttribute("className","t"),n.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="<div></div>",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||p.guid++:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e<f;e++)delete d[b[e]];if(!(c?K:p.isEmptyObject)(d))return}}if(!c){delete h[i].data;if(!K(h[i]))return}g?p.cleanData([a],!0):p.support.deleteExpando||h!=h.window?delete h[i]:h[i]=null},_data:function(a,b,c){return p.data(a,b,c,!0)},acceptData:function(a){var b=a.nodeName&&p.noData[a.nodeName.toLowerCase()];return!b||b!==!0&&a.getAttribute("classid")===b}}),p.fn.extend({data:function(a,c){var d,e,f,g,h,i=this[0],j=0,k=null;if(a===b){if(this.length){k=p.data(i);if(i.nodeType===1&&!p._data(i,"parsedAttrs")){f=i.attributes;for(h=f.length;j<h;j++)g=f[j].name,g.indexOf("data-")||(g=p.camelCase(g.substring(5)),J(i,g,k[g]));p._data(i,"parsedAttrs",!0)}}return k}return typeof a=="object"?this.each(function(){p.data(this,a)}):(d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!",p.access(this,function(c){if(c===b)return k=this.triggerHandler("getData"+e,[d[0]]),k===b&&i&&(k=p.data(i,a),k=J(i,a,k)),k===b&&d[1]?this.data(d[0]):k;d[1]=c,this.each(function(){var b=p(this);b.triggerHandler("setData"+e,d),p.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.length,e=c.shift(),f=p._queueHooks(a,b),g=function(){p.dequeue(a,b)};e==="inprogress"&&(e=c.shift(),d--),e&&(b==="fx"&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length<d?p.queue(this[0],a):c===b?this:this.each(function(){var b=p.queue(this,a,c);p._queueHooks(this,a),a==="fx"&&b[0]!=="inprogress"&&p.dequeue(this,a)})},dequeue:function(a){return this.each(function(){p.dequeue(this,a)})},delay:function(a,b){return a=p.fx?p.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){var d,e=1,f=p.Deferred(),g=this,h=this.length,i=function(){--e||f.resolveWith(g,[g])};typeof a!="string"&&(c=a,a=b),a=a||"fx";while(h--)d=p._data(g[h],a+"queueHooks"),d&&d.empty&&(e++,d.empty.add(i));return i(),f.promise(c)}});var L,M,N,O=/[\t\r\n]/g,P=/\r/g,Q=/^(?:button|input)$/i,R=/^(?:button|input|object|select|textarea)$/i,S=/^a(?:rea|)$/i,T=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,U=p.support.getSetAttribute;p.fn.extend({attr:function(a,b){return p.access(this,p.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{f=" "+e.className+" ";for(g=0,h=b.length;g<h;g++)f.indexOf(" "+b[g]+" ")<0&&(f+=b[g]+" ");e.className=p.trim(f)}}}return this},removeClass:function(a){var c,d,e,f,g,h,i;if(p.isFunction(a))return this.each(function(b){p(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(s);for(h=0,i=this.length;h<i;h++){e=this[h];if(e.nodeType===1&&e.className){d=(" "+e.className+" ").replace(O," ");for(f=0,g=c.length;f<g;f++)while(d.indexOf(" "+c[f]+" ")>=0)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(O," ").indexOf(b)>=0)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c<d;c++){e=h[c];if(e.selected&&(p.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!p.nodeName(e.parentNode,"optgroup"))){b=p(e).val();if(i)return b;g.push(b)}}return i&&!g.length&&h.length?p(h[f]).val():g},set:function(a,b){var c=p.makeArray(b);return p(a).find("option").each(function(){this.selected=p.inArray(p(this).val(),c)>=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,d+""),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g<d.length;g++)e=d[g],e&&(c=p.propFix[e]||e,f=T.test(e),f||p.attr(a,e,""),a.removeAttribute(U?e:c),f&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(Q.test(a.nodeName)&&a.parentNode)p.error("type property can't be changed");else if(!p.support.radioValue&&b==="radio"&&p.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}},value:{get:function(a,b){return L&&p.nodeName(a,"button")?L.get(a,b):b in a?a.value:null},set:function(a,b,c){if(L&&p.nodeName(a,"button"))return L.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,f,g,h=a.nodeType;if(!a||h===3||h===8||h===2)return;return g=h!==1||!p.isXMLDoc(a),g&&(c=p.propFix[c]||c,f=p.propHooks[c]),d!==b?f&&"set"in f&&(e=f.set(a,d,c))!==b?e:a[c]=d:f&&"get"in f&&(e=f.get(a,c))!==null?e:a[c]},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):R.test(a.nodeName)||S.test(a.nodeName)&&a.href?0:b}}}}),M={get:function(a,c){var d,e=p.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;return b===!1?p.removeAttr(a,c):(d=p.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase())),c}},U||(N={name:!0,id:!0,coords:!0},L=p.valHooks.button={get:function(a,c){var d;return d=a.getAttributeNode(c),d&&(N[c]?d.value!=="":d.specified)?d.value:b},set:function(a,b,c){var d=a.getAttributeNode(c);return d||(d=e.createAttribute(c),a.setAttributeNode(d)),d.value=b+""}},p.each(["width","height"],function(a,b){p.attrHooks[b]=p.extend(p.attrHooks[b],{set:function(a,c){if(c==="")return a.setAttribute(b,"auto"),c}})}),p.attrHooks.contenteditable={get:L.get,set:function(a,b,c){b===""&&(b="false"),L.set(a,b,c)}}),p.support.hrefNormalized||p.each(["href","src","width","height"],function(a,c){p.attrHooks[c]=p.extend(p.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),p.support.style||(p.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=b+""}}),p.support.optSelected||(p.propHooks.selected=p.extend(p.propHooks.selected,{get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}})),p.support.enctype||(p.propFix.enctype="encoding"),p.support.checkOn||p.each(["radio","checkbox"],function(){p.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),p.each(["radio","checkbox"],function(){p.valHooks[this]=p.extend(p.valHooks[this],{set:function(a,b){if(p.isArray(b))return a.checked=p.inArray(p(a).val(),b)>=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j<c.length;j++){k=W.exec(c[j])||[],l=k[1],m=(k[2]||"").split(".").sort(),r=p.event.special[l]||{},l=(f?r.delegateType:r.bindType)||l,r=p.event.special[l]||{},n=p.extend({type:l,origType:k[1],data:e,handler:d,guid:d.guid,selector:f,needsContext:f&&p.expr.match.needsContext.test(f),namespace:m.join(".")},o),q=i[l];if(!q){q=i[l]=[],q.delegateCount=0;if(!r.setup||r.setup.call(a,e,m,h)===!1)a.addEventListener?a.addEventListener(l,h,!1):a.attachEvent&&a.attachEvent("on"+l,h)}r.add&&(r.add.call(a,n),n.handler.guid||(n.handler.guid=d.guid)),f?q.splice(q.delegateCount++,0,n):q.push(n),p.event.global[l]=!0}a=null},global:{},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,q,r=p.hasData(a)&&p._data(a);if(!r||!(m=r.events))return;b=p.trim(_(b||"")).split(" ");for(f=0;f<b.length;f++){g=W.exec(b[f])||[],h=i=g[1],j=g[2];if(!h){for(h in m)p.event.remove(a,h+b[f],c,d,!0);continue}n=p.event.special[h]||{},h=(d?n.delegateType:n.bindType)||h,o=m[h]||[],k=o.length,j=j?new RegExp("(^|\\.)"+j.split(".").sort().join("\\.(?:.*\\.|)")+"(\\.|$)"):null;for(l=0;l<o.length;l++)q=o[l],(e||i===q.origType)&&(!c||c.guid===q.guid)&&(!j||j.test(q.namespace))&&(!d||d===q.selector||d==="**"&&q.selector)&&(o.splice(l--,1),q.selector&&o.delegateCount--,n.remove&&n.remove.call(a,q));o.length===0&&k!==o.length&&((!n.teardown||n.teardown.call(a,j,r.handle)===!1)&&p.removeEvent(a,h,r.handle),delete m[h])}p.isEmptyObject(m)&&(delete r.handle,p.removeData(a,"events",!0))},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,f,g){if(!f||f.nodeType!==3&&f.nodeType!==8){var h,i,j,k,l,m,n,o,q,r,s=c.type||c,t=[];if($.test(s+p.event.triggered))return;s.indexOf("!")>=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;j<q.length&&!c.isPropagationStopped();j++)k=q[j][0],c.type=q[j][1],o=(p._data(k,"events")||{})[c.type]&&p._data(k,"handle"),o&&o.apply(k,d),o=m&&k[m],o&&p.acceptData(k)&&o.apply&&o.apply(k,d)===!1&&c.preventDefault();return c.type=s,!g&&!c.isDefaultPrevented()&&(!n._default||n._default.apply(f.ownerDocument,d)===!1)&&(s!=="click"||!p.nodeName(f,"a"))&&p.acceptData(f)&&m&&f[s]&&(s!=="focus"&&s!=="blur"||c.target.offsetWidth!==0)&&!p.isWindow(f)&&(l=f[m],l&&(f[m]=null),p.event.triggered=s,f[s](),p.event.triggered=b,l&&(f[m]=l)),c.result}return},dispatch:function(c){c=p.event.fix(c||a.event);var d,e,f,g,h,i,j,l,m,n,o=(p._data(this,"events")||{})[c.type]||[],q=o.delegateCount,r=k.call(arguments),s=!c.exclusive&&!c.namespace,t=p.event.special[c.type]||{},u=[];r[0]=c,c.delegateTarget=this;if(t.preDispatch&&t.preDispatch.call(this,c)===!1)return;if(q&&(!c.button||c.type!=="click"))for(f=c.target;f!=this;f=f.parentNode||this)if(f.disabled!==!0||c.type!=="click"){h={},j=[];for(d=0;d<q;d++)l=o[d],m=l.selector,h[m]===b&&(h[m]=l.needsContext?p(m,this).index(f)>=0:p.find(m,this,null,[f]).length),h[m]&&j.push(l);j.length&&u.push({elem:f,matches:j})}o.length>q&&u.push({elem:this,matches:o.slice(q)});for(d=0;d<u.length&&!c.isPropagationStopped();d++){i=u[d],c.currentTarget=i.elem;for(e=0;e<i.matches.length&&!c.isImmediatePropagationStopped();e++){l=i.matches[e];if(s||!c.namespace&&!l.namespace||c.namespace_re&&c.namespace_re.test(l.namespace))c.data=l.data,c.handleObj=l,g=((p.event.special[l.origType]||{}).handle||l.handler).apply(i.elem,r),g!==b&&(c.result=g,g===!1&&(c.preventDefault(),c.stopPropagation()))}}return t.postDispatch&&t.postDispatch.call(this,c),c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,c){var d,f,g,h=c.button,i=c.fromElement;return a.pageX==null&&c.clientX!=null&&(d=a.target.ownerDocument||e,f=d.documentElement,g=d.body,a.pageX=c.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=c.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?c.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0),a}},fix:function(a){if(a[p.expando])return a;var b,c,d=a,f=p.event.fixHooks[a.type]||{},g=f.props?this.props.concat(f.props):this.props;a=p.Event(d);for(b=g.length;b;)c=g[--b],a[c]=d[c];return a.target||(a.target=d.srcElement||e),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,f.filter?f.filter(a,d):a},special:{load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){p.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=p.extend(new p.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?p.event.trigger(e,null,b):p.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},p.event.handle=p.event.dispatch,p.removeEvent=e.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]=="undefined"&&(a[d]=null),a.detachEvent(d,c))},p.Event=function(a,b){if(this instanceof p.Event)a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?bb:ba):this.type=a,b&&p.extend(this,b),this.timeStamp=a&&a.timeStamp||p.now(),this[p.expando]=!0;else return new p.Event(a,b)},p.Event.prototype={preventDefault:function(){this.isDefaultPrevented=bb;var a=this.originalEvent;if(!a)return;a.preventDefault?a.preventDefault():a.returnValue=!1},stopPropagation:function(){this.isPropagationStopped=bb;var a=this.originalEvent;if(!a)return;a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=bb,this.stopPropagation()},isDefaultPrevented:ba,isPropagationStopped:ba,isImmediatePropagationStopped:ba},p.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){p.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj,g=f.selector;if(!e||e!==d&&!p.contains(d,e))a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b;return c}}}),p.support.submitBubbles||(p.event.special.submit={setup:function(){if(p.nodeName(this,"form"))return!1;p.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=p.nodeName(c,"input")||p.nodeName(c,"button")?c.form:b;d&&!p._data(d,"_submit_attached")&&(p.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),p._data(d,"_submit_attached",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&p.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(p.nodeName(this,"form"))return!1;p.event.remove(this,"._submit")}}),p.support.changeBubbles||(p.event.special.change={setup:function(){if(V.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")p.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),p.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),p.event.simulate("change",this,a,!0)});return!1}p.event.add(this,"beforeactivate._change",function(a){var b=a.target;V.test(b.nodeName)&&!p._data(b,"_change_attached")&&(p.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&p.event.simulate("change",this.parentNode,a,!0)}),p._data(b,"_change_attached",!0))})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){return p.event.remove(this,"._change"),!V.test(this.nodeName)}}),p.support.focusinBubbles||p.each({focus:"focusin",blur:"focusout"},function(a,b){var c=0,d=function(a){p.event.simulate(b,a.target,p.event.fix(a),!0)};p.event.special[b]={setup:function(){c++===0&&e.addEventListener(a,d,!0)},teardown:function(){--c===0&&e.removeEventListener(a,d,!0)}}}),p.fn.extend({on:function(a,c,d,e,f){var g,h;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(h in a)this.on(h,c,d,a[h],f);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=ba;else if(!e)return this;return f===1&&(g=e,e=function(a){return p().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=p.guid++)),this.each(function(){p.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){var e,f;if(a&&a.preventDefault&&a.handleObj)return e=a.handleObj,p(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler),this;if(typeof a=="object"){for(f in a)this.off(f,c,a[f]);return this}if(c===!1||typeof c=="function")d=c,c=b;return d===!1&&(d=ba),this.each(function(){p.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){return p(this.context).on(a,this.selector,b,c),this},die:function(a,b){return p(this.context).off(a,this.selector||"**",b),this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length===1?this.off(a,"**"):this.off(b,a||"**",c)},trigger:function(a,b){return this.each(function(){p.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return p.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||p.guid++,d=0,e=function(c){var e=(p._data(this,"lastToggle"+a.guid)||0)%d;return p._data(this,"lastToggle"+a.guid,e+1),c.preventDefault(),b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),p.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){p.fn[b]=function(a,c){return c==null&&(c=a,a=null),arguments.length>0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function bc(a,b,c,d){c=c||[],b=b||r;var e,f,i,j,k=b.nodeType;if(!a||typeof a!="string")return c;if(k!==1&&k!==9)return[];i=g(b);if(!i&&!d)if(e=P.exec(a))if(j=e[1]){if(k===9){f=b.getElementById(j);if(!f||!f.parentNode)return c;if(f.id===j)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(j))&&h(b,f)&&f.id===j)return c.push(f),c}else{if(e[2])return w.apply(c,x.call(b.getElementsByTagName(a),0)),c;if((j=e[3])&&_&&b.getElementsByClassName)return w.apply(c,x.call(b.getElementsByClassName(j),0)),c}return bp(a.replace(L,"$1"),b,c,d,i)}function bd(a){return function(b){var c=b.nodeName.toLowerCase();return c==="input"&&b.type===a}}function be(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}}function bf(a){return z(function(b){return b=+b,z(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function bg(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}function bh(a,b){var c,d,f,g,h,i,j,k=C[o][a];if(k)return b?0:k.slice(0);h=a,i=[],j=e.preFilter;while(h){if(!c||(d=M.exec(h)))d&&(h=h.slice(d[0].length)),i.push(f=[]);c=!1;if(d=N.exec(h))f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=d[0].replace(L," ");for(g in e.filter)(d=W[g].exec(h))&&(!j[g]||(d=j[g](d,r,!0)))&&(f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=g,c.matches=d);if(!c)break}return b?h.length:h?bc.error(a):C(a,i).slice(0)}function bi(a,b,d){var e=b.dir,f=d&&b.dir==="parentNode",g=u++;return b.first?function(b,c,d){while(b=b[e])if(f||b.nodeType===1)return a(b,c,d)}:function(b,d,h){if(!h){var i,j=t+" "+g+" ",k=j+c;while(b=b[e])if(f||b.nodeType===1){if((i=b[o])===k)return b.sizset;if(typeof i=="string"&&i.indexOf(j)===0){if(b.sizset)return b}else{b[o]=k;if(a(b,d,h))return b.sizset=!0,b;b.sizset=!1}}}else while(b=b[e])if(f||b.nodeType===1)if(a(b,d,h))return b}}function bj(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function bk(a,b,c,d,e){var f,g=[],h=0,i=a.length,j=b!=null;for(;h<i;h++)if(f=a[h])if(!c||c(f,d,e))g.push(f),j&&b.push(h);return g}function bl(a,b,c,d,e,f){return d&&!d[o]&&(d=bl(d)),e&&!e[o]&&(e=bl(e,f)),z(function(f,g,h,i){if(f&&e)return;var j,k,l,m=[],n=[],o=g.length,p=f||bo(b||"*",h.nodeType?[h]:h,[],f),q=a&&(f||!b)?bk(p,m,a,h,i):p,r=c?e||(f?a:o||d)?[]:g:q;c&&c(q,r,h,i);if(d){l=bk(r,n),d(l,[],h,i),j=l.length;while(j--)if(k=l[j])r[n[j]]=!(q[n[j]]=k)}if(f){j=a&&r.length;while(j--)if(k=r[j])f[m[j]]=!(g[m[j]]=k)}else r=bk(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):w.apply(g,r)})}function bm(a){var b,c,d,f=a.length,g=e.relative[a[0].type],h=g||e.relative[" "],i=g?1:0,j=bi(function(a){return a===b},h,!0),k=bi(function(a){return y.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==l)||((b=c).nodeType?j(a,c,d):k(a,c,d))}];for(;i<f;i++)if(c=e.relative[a[i].type])m=[bi(bj(m),c)];else{c=e.filter[a[i].type].apply(null,a[i].matches);if(c[o]){d=++i;for(;d<f;d++)if(e.relative[a[d].type])break;return bl(i>1&&bj(m),i>1&&a.slice(0,i-1).join("").replace(L,"$1"),c,i<d&&bm(a.slice(i,d)),d<f&&bm(a=a.slice(d)),d<f&&a.join(""))}m.push(c)}return bj(m)}function bn(a,b){var d=b.length>0,f=a.length>0,g=function(h,i,j,k,m){var n,o,p,q=[],s=0,u="0",x=h&&[],y=m!=null,z=l,A=h||f&&e.find.TAG("*",m&&i.parentNode||i),B=t+=z==null?1:Math.E;y&&(l=i!==r&&i,c=g.el);for(;(n=A[u])!=null;u++){if(f&&n){for(o=0;p=a[o];o++)if(p(n,i,j)){k.push(n);break}y&&(t=B,c=++g.el)}d&&((n=!p&&n)&&s--,h&&x.push(n))}s+=u;if(d&&u!==s){for(o=0;p=b[o];o++)p(x,q,i,j);if(h){if(s>0)while(u--)!x[u]&&!q[u]&&(q[u]=v.call(k));q=bk(q)}w.apply(k,q),y&&!h&&q.length>0&&s+b.length>1&&bc.uniqueSort(k)}return y&&(t=B,l=z),x};return g.el=0,d?z(g):g}function bo(a,b,c,d){var e=0,f=b.length;for(;e<f;e++)bc(a,b[e],c,d);return c}function bp(a,b,c,d,f){var g,h,j,k,l,m=bh(a),n=m.length;if(!d&&m.length===1){h=m[0]=m[0].slice(0);if(h.length>2&&(j=h[0]).type==="ID"&&b.nodeType===9&&!f&&e.relative[h[1].type]){b=e.find.ID(j.matches[0].replace(V,""),b,f)[0];if(!b)return c;a=a.slice(h.shift().length)}for(g=W.POS.test(a)?-1:h.length-1;g>=0;g--){j=h[g];if(e.relative[k=j.type])break;if(l=e.find[k])if(d=l(j.matches[0].replace(V,""),R.test(h[0].type)&&b.parentNode||b,f)){h.splice(g,1),a=d.length&&h.join("");if(!a)return w.apply(c,x.call(d,0)),c;break}}}return i(a,m)(d,b,f,c,R.test(a)),c}function bq(){}var c,d,e,f,g,h,i,j,k,l,m=!0,n="undefined",o=("sizcache"+Math.random()).replace(".",""),q=String,r=a.document,s=r.documentElement,t=0,u=0,v=[].pop,w=[].push,x=[].slice,y=[].indexOf||function(a){var b=0,c=this.length;for(;b<c;b++)if(this[b]===a)return b;return-1},z=function(a,b){return a[o]=b==null||b,a},A=function(){var a={},b=[];return z(function(c,d){return b.push(c)>e.cacheLength&&delete a[b.shift()],a[c]=d},a)},B=A(),C=A(),D=A(),E="[\\x20\\t\\r\\n\\f]",F="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",G=F.replace("w","w#"),H="([*^$|!~]?=)",I="\\["+E+"*("+F+")"+E+"*(?:"+H+E+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+G+")|)|)"+E+"*\\]",J=":("+F+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+I+")|[^:]|\\\\.)*|.*))\\)|)",K=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+E+"*((?:-\\d)?\\d*)"+E+"*\\)|)(?=[^-]|$)",L=new RegExp("^"+E+"+|((?:^|[^\\\\])(?:\\\\.)*)"+E+"+$","g"),M=new RegExp("^"+E+"*,"+E+"*"),N=new RegExp("^"+E+"*([\\x20\\t\\r\\n\\f>+~])"+E+"*"),O=new RegExp(J),P=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,Q=/^:not/,R=/[\x20\t\r\n\f]*[+~]/,S=/:not\($/,T=/h\d/i,U=/input|select|textarea|button/i,V=/\\(?!\\)/g,W={ID:new RegExp("^#("+F+")"),CLASS:new RegExp("^\\.("+F+")"),NAME:new RegExp("^\\[name=['\"]?("+F+")['\"]?\\]"),TAG:new RegExp("^("+F.replace("w","w*")+")"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+J),POS:new RegExp(K,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+E+"*(even|odd|(([+-]|)(\\d*)n|)"+E+"*(?:([+-]|)"+E+"*(\\d+)|))"+E+"*\\)|)","i"),needsContext:new RegExp("^"+E+"*[>+~]|"+K,"i")},X=function(a){var b=r.createElement("div");try{return a(b)}catch(c){return!1}finally{b=null}},Y=X(function(a){return a.appendChild(r.createComment("")),!a.getElementsByTagName("*").length}),Z=X(function(a){return a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!==n&&a.firstChild.getAttribute("href")==="#"}),$=X(function(a){a.innerHTML="<select></select>";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),_=X(function(a){return a.innerHTML="<div class='hidden e'></div><div class='hidden'></div>",!a.getElementsByClassName||!a.getElementsByClassName("e").length?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length===2)}),ba=X(function(a){a.id=o+0,a.innerHTML="<a name='"+o+"'></a><div name='"+o+"'></div>",s.insertBefore(a,s.firstChild);var b=r.getElementsByName&&r.getElementsByName(o).length===2+r.getElementsByName(o+0).length;return d=!r.getElementById(o),s.removeChild(a),b});try{x.call(s.childNodes,0)[0].nodeType}catch(bb){x=function(a){var b,c=[];for(;b=this[a];a++)c.push(b);return c}}bc.matches=function(a,b){return bc(a,null,null,b)},bc.matchesSelector=function(a,b){return bc(b,null,null,[a]).length>0},f=bc.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(e===1||e===9||e===11){if(typeof a.textContent=="string")return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=f(a)}else if(e===3||e===4)return a.nodeValue}else for(;b=a[d];d++)c+=f(b);return c},g=bc.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?b.nodeName!=="HTML":!1},h=bc.contains=s.contains?function(a,b){var c=a.nodeType===9?a.documentElement:a,d=b&&b.parentNode;return a===d||!!(d&&d.nodeType===1&&c.contains&&c.contains(d))}:s.compareDocumentPosition?function(a,b){return b&&!!(a.compareDocumentPosition(b)&16)}:function(a,b){while(b=b.parentNode)if(b===a)return!0;return!1},bc.attr=function(a,b){var c,d=g(a);return d||(b=b.toLowerCase()),(c=e.attrHandle[b])?c(a):d||$?a.getAttribute(b):(c=a.getAttributeNode(b),c?typeof a[b]=="boolean"?a[b]?b:null:c.specified?c.value:null:null)},e=bc.selectors={cacheLength:50,createPseudo:z,match:W,attrHandle:Z?{}:{href:function(a){return a.getAttribute("href",2)},type:function(a){return a.getAttribute("type")}},find:{ID:d?function(a,b,c){if(typeof b.getElementById!==n&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==n&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==n&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:Y?function(a,b){if(typeof b.getElementsByTagName!==n)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c},NAME:ba&&function(a,b){if(typeof b.getElementsByName!==n)return b.getElementsByName(name)},CLASS:_&&function(a,b,c){if(typeof b.getElementsByClassName!==n&&!c)return b.getElementsByClassName(a)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(V,""),a[3]=(a[4]||a[5]||"").replace(V,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||bc.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&bc.error(a[0]),a},PSEUDO:function(a){var b,c;if(W.CHILD.test(a[0]))return null;if(a[3])a[2]=a[3];else if(b=a[4])O.test(b)&&(c=bh(b,!0))&&(c=b.indexOf(")",b.length-c)-b.length)&&(b=b.slice(0,c),a[0]=a[0].slice(0,c)),a[2]=b;return a.slice(0,3)}},filter:{ID:d?function(a){return a=a.replace(V,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(V,""),function(b){var c=typeof b.getAttributeNode!==n&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(V,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=B[o][a];return b||(b=B(a,new RegExp("(^|"+E+")"+a+"("+E+"|$)"))),function(a){return b.test(a.className||typeof a.getAttribute!==n&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return function(d,e){var f=bc.attr(d,a);return f==null?b==="!=":b?(f+="",b==="="?f===c:b==="!="?f!==c:b==="^="?c&&f.indexOf(c)===0:b==="*="?c&&f.indexOf(c)>-1:b==="$="?c&&f.substr(f.length-c.length)===c:b==="~="?(" "+f+" ").indexOf(c)>-1:b==="|="?f===c||f.substr(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d){return a==="nth"?function(a){var b,e,f=a.parentNode;if(c===1&&d===0)return!0;if(f){e=0;for(b=f.firstChild;b;b=b.nextSibling)if(b.nodeType===1){e++;if(a===b)break}}return e-=d,e===c||e%c===0&&e/c>=0}:function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b){var c,d=e.pseudos[a]||e.setFilters[a.toLowerCase()]||bc.error("unsupported pseudo: "+a);return d[o]?d(b):d.length>1?(c=[a,a,"",b],e.setFilters.hasOwnProperty(a.toLowerCase())?z(function(a,c){var e,f=d(a,b),g=f.length;while(g--)e=y.call(a,f[g]),a[e]=!(c[e]=f[g])}):function(a){return d(a,0,c)}):d}},pseudos:{not:z(function(a){var b=[],c=[],d=i(a.replace(L,"$1"));return d[o]?z(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)if(f=g[h])a[h]=!(b[h]=f)}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:z(function(a){return function(b){return bc(a,b).length>0}}),contains:z(function(a){return function(b){return(b.textContent||b.innerText||f(b)).indexOf(a)>-1}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!e.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},header:function(a){return T.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:bd("radio"),checkbox:bd("checkbox"),file:bd("file"),password:bd("password"),image:bd("image"),submit:be("submit"),reset:be("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return U.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement},first:bf(function(a,b,c){return[0]}),last:bf(function(a,b,c){return[b-1]}),eq:bf(function(a,b,c){return[c<0?c+b:c]}),even:bf(function(a,b,c){for(var d=0;d<b;d+=2)a.push(d);return a}),odd:bf(function(a,b,c){for(var d=1;d<b;d+=2)a.push(d);return a}),lt:bf(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:bf(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},j=s.compareDocumentPosition?function(a,b){return a===b?(k=!0,0):(!a.compareDocumentPosition||!b.compareDocumentPosition?a.compareDocumentPosition:a.compareDocumentPosition(b)&4)?-1:1}:function(a,b){if(a===b)return k=!0,0;if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,h=b.parentNode,i=g;if(g===h)return bg(a,b);if(!g)return-1;if(!h)return 1;while(i)e.unshift(i),i=i.parentNode;i=h;while(i)f.unshift(i),i=i.parentNode;c=e.length,d=f.length;for(var j=0;j<c&&j<d;j++)if(e[j]!==f[j])return bg(e[j],f[j]);return j===c?bg(a,f[j],-1):bg(e[j],b,1)},[0,0].sort(j),m=!k,bc.uniqueSort=function(a){var b,c=1;k=m,a.sort(j);if(k)for(;b=a[c];c++)b===a[c-1]&&a.splice(c--,1);return a},bc.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},i=bc.compile=function(a,b){var c,d=[],e=[],f=D[o][a];if(!f){b||(b=bh(a)),c=b.length;while(c--)f=bm(b[c]),f[o]?d.push(f):e.push(f);f=D(a,bn(e,d))}return f},r.querySelectorAll&&function(){var a,b=bp,c=/'|\\/g,d=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,e=[":focus"],f=[":active",":focus"],h=s.matchesSelector||s.mozMatchesSelector||s.webkitMatchesSelector||s.oMatchesSelector||s.msMatchesSelector;X(function(a){a.innerHTML="<select><option selected=''></option></select>",a.querySelectorAll("[selected]").length||e.push("\\["+E+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),X(function(a){a.innerHTML="<p test=''></p>",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+E+"*(?:\"\"|'')"),a.innerHTML="<input type='hidden'/>",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=new RegExp(e.join("|")),bp=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a))){var i,j,k=!0,l=o,m=d,n=d.nodeType===9&&a;if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){i=bh(a),(k=d.getAttribute("id"))?l=k.replace(c,"\\$&"):d.setAttribute("id",l),l="[id='"+l+"'] ",j=i.length;while(j--)i[j]=l+i[j].join("");m=R.test(a)&&d.parentNode||d,n=i.join(",")}if(n)try{return w.apply(f,x.call(m.querySelectorAll(n),0)),f}catch(p){}finally{k||d.removeAttribute("id")}}return b(a,d,f,g,h)},h&&(X(function(b){a=h.call(b,"div");try{h.call(b,"[test!='']:sizzle"),f.push("!=",J)}catch(c){}}),f=new RegExp(f.join("|")),bc.matchesSelector=function(b,c){c=c.replace(d,"='$1']");if(!g(b)&&!f.test(c)&&(!e||!e.test(c)))try{var i=h.call(b,c);if(i||a||b.document&&b.document.nodeType!==11)return i}catch(j){}return bc(c,null,null,[b]).length>0})}(),e.pseudos.nth=e.pseudos.eq,e.filters=bq.prototype=e.pseudos,e.setFilters=new bq,bc.attr=p.attr,p.find=bc,p.expr=bc.selectors,p.expr[":"]=p.expr.pseudos,p.unique=bc.uniqueSort,p.text=bc.getText,p.isXMLDoc=bc.isXML,p.contains=bc.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b<c;b++)if(p.contains(h[b],this))return!0});g=this.pushStack("","find",a);for(b=0,c=this.length;b<c;b++){d=g.length,p.find(a,this[b],g);if(b>0)for(e=d;e<g.length;e++)for(f=0;f<d;f++)if(g[f]===g[e]){g.splice(e--,1);break}}return g},has:function(a){var b,c=p(a,this),d=c.length;return this.filter(function(){for(b=0;b<d;b++)if(p.contains(this,c[b]))return!0})},not:function(a){return this.pushStack(bj(this,a,!1),"not",a)},filter:function(a){return this.pushStack(bj(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?bf.test(a)?p(a,this.context).index(this[0])>=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d<e;d++){c=this[d];while(c&&c.ownerDocument&&c!==b&&c.nodeType!==11){if(g?g.index(c)>-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/<tbody/i,br=/<|&#?\w+;/,bs=/<(?:script|style|link)/i,bt=/<(?:script|object|embed|option|style)/i,bu=new RegExp("<(?:"+bl+")[\\s/>]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,bz={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X<div>","</div>"]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(f){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){return bh(this[0])?this.length?this.pushStack(p(p.isFunction(a)?a():a),"replaceWith",a):this:p.isFunction(a)?this.each(function(b){var c=p(this),d=c.html();c.replaceWith(a.call(this,b,d))}):(typeof a!="string"&&(a=p(a).detach()),this.each(function(){var b=this.nextSibling,c=this.parentNode;p(this).remove(),b?p(b).before(a):p(c).append(a)}))},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){a=[].concat.apply([],a);var e,f,g,h,i=0,j=a[0],k=[],l=this.length;if(!p.support.checkClone&&l>1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i<l;i++)d.call(c&&p.nodeName(this[i],"table")?bC(this[i],"tbody"):this[i],i===h?g:p.clone(g,!0,!0))}g=f=null,k.length&&p.each(k,function(a,b){b.src?p.ajax?p.ajax({url:b.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):p.error("no ajax"):p.globalEval((b.text||b.textContent||b.innerHTML||"").replace(by,"")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),p.buildFragment=function(a,c,d){var f,g,h,i=a[0];return c=c||e,c=!c.nodeType&&c[0]||c,c=c.ownerDocument||c,a.length===1&&typeof i=="string"&&i.length<512&&c===e&&i.charAt(0)==="<"&&!bt.test(i)&&(p.support.checkClone||!bw.test(i))&&(p.support.html5Clone||!bu.test(i))&&(g=!0,f=p.fragments[i],h=f!==b),f||(f=c.createDocumentFragment(),p.clean(a,c,f,d),g&&(p.fragments[i]=h&&f)),{fragment:f,cacheable:g}},p.fragments={},p.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){p.fn[a]=function(c){var d,e=0,f=[],g=p(c),h=g.length,i=this.length===1&&this[0].parentNode;if((i==null||i&&i.nodeType===11&&i.childNodes.length===1)&&h===1)return g[b](this[0]),this;for(;e<h;e++)d=(e>0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=b===e&&bA,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(f=0;(h=a[f])!=null;f++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{s=s||bk(b),l=b.createElement("div"),s.appendChild(l),h=h.replace(bo,"<$1></$2>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]==="<table>"&&!m?l.childNodes:[];for(g=n.length-1;g>=0;--g)p.nodeName(n[g],"tbody")&&!n[g].childNodes.length&&n[g].parentNode.removeChild(n[g])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l.parentNode.removeChild(l)}h.nodeType?t.push(h):p.merge(t,h)}l&&(h=l=s=null);if(!p.support.appendChecked)for(f=0;(h=t[f])!=null;f++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(f=0;(h=t[f])!=null;f++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[f+1,0].concat(r)),f+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.chrome?b.webkit=!0:b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^(none|table(?!-c[ea]).+)/,bO=/^margin/,bP=new RegExp("^("+q+")(.*)$","i"),bQ=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bR=new RegExp("^([-+])=("+q+")","i"),bS={},bT={position:"absolute",visibility:"hidden",display:"block"},bU={letterSpacing:0,fontWeight:400},bV=["Top","Right","Bottom","Left"],bW=["Webkit","O","Moz","ms"],bX=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return b$(this,!0)},hide:function(){return b$(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bX.apply(this,arguments):this.each(function(){(c?a:bZ(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bY(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bR.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bY(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bU&&(f=bU[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(b,c){var d,e,f,g,h=a.getComputedStyle(b,null),i=b.style;return h&&(d=h[c],d===""&&!p.contains(b.ownerDocument,b)&&(d=p.style(b,c)),bQ.test(d)&&bO.test(c)&&(e=i.width,f=i.minWidth,g=i.maxWidth,i.minWidth=i.maxWidth=i.width=d,d=h.width,i.width=e,i.minWidth=f,i.maxWidth=g)),d}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bQ.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth===0&&bN.test(bH(a,"display"))?p.swap(a,bT,function(){return cb(a,b,d)}):cb(a,b,d)},set:function(a,c,d){return b_(a,c,d?ca(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bQ.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bV[d]+b]=e[d]||e[d-2]||e[0];return f}},bO.test(a)||(p.cssHooks[a+b].set=b_)});var cd=/%20/g,ce=/\[\]$/,cf=/\r?\n/g,cg=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ch=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ch.test(this.nodeName)||cg.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(cf,"\r\n")}}):{name:b.name,value:c.replace(cf,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ci(d,a[d],c,f);return e.join("&").replace(cd,"+")};var cj,ck,cl=/#.*$/,cm=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,co=/^(?:GET|HEAD)$/,cp=/^\/\//,cq=/\?/,cr=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,cs=/([?&])_=[^&]*/,ct=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,cu=p.fn.load,cv={},cw={},cx=["*/"]+["*"];try{ck=f.href}catch(cy){ck=e.createElement("a"),ck.href="",ck=ck.href}cj=ct.exec(ck.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&cu)return cu.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):c&&typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("<div>").append(a.replace(cr,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cB(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cB(a,b),a},ajaxSettings:{url:ck,isLocal:cn.test(cj[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cx},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cz(cv),ajaxTransport:cz(cw),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cC(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cD(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=(c||y)+"",k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cm.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(cl,"").replace(cp,cj[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=ct.exec(l.url.toLowerCase())||!1,l.crossDomain=i&&i.join(":")+(i[3]?"":i[1]==="http:"?80:443)!==cj.join(":")+(cj[3]?"":cj[1]==="http:"?80:443)),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cA(cv,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!co.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cq.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cs,"$1_="+z);l.url=A+(A===l.url?(cq.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cx+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cA(cw,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cE=[],cF=/\?/,cG=/(=)\?(?=&|$)|\?\?/,cH=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cE.pop()||p.expando+"_"+cH++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cG.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cG.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cG,"$1"+f):m?c.data=i.replace(cG,"$1"+f):k&&(c.url+=(cF.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cE.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cI,cJ=a.ActiveXObject?function(){for(var a in cI)cI[a](0,1)}:!1,cK=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cL()||cM()}:cL,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cJ&&delete cI[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cK,cJ&&(cI||(cI={},p(a).unload(cJ)),cI[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cN,cO,cP=/^(?:toggle|show|hide)$/,cQ=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cR=/queueHooks$/,cS=[cY],cT={"*":[function(a,b){var c,d,e=this.createTween(a,b),f=cQ.exec(b),g=e.cur(),h=+g||0,i=1,j=20;if(f){c=+f[2],d=f[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&h){h=p.css(e.elem,a,!0)||c||1;do i=i||".5",h=h/i,p.style(e.elem,a,h+d);while(i!==(i=e.cur()/g)&&i!==1&&--j)}e.unit=d,e.start=h,e.end=f[1]?h+(f[1]+1)*c:c}return e}]};p.Animation=p.extend(cW,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d<e;d++)c=a[d],cT[c]=cT[c]||[],cT[c].unshift(b)},prefilter:function(a,b){b?cS.unshift(a):cS.push(a)}}),p.Tween=cZ,cZ.prototype={constructor:cZ,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(p.cssNumber[c]?"":"px")},cur:function(){var a=cZ.propHooks[this.prop];return a&&a.get?a.get(this):cZ.propHooks._default.get(this)},run:function(a){var b,c=cZ.propHooks[this.prop];return this.options.duration?this.pos=b=p.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):cZ.propHooks._default.set(this),this}},cZ.prototype.init.prototype=cZ.prototype,cZ.propHooks={_default:{get:function(a){var b;return a.elem[a.prop]==null||!!a.elem.style&&a.elem.style[a.prop]!=null?(b=p.css(a.elem,a.prop,!1,""),!b||b==="auto"?0:b):a.elem[a.prop]},set:function(a){p.fx.step[a.prop]?p.fx.step[a.prop](a):a.elem.style&&(a.elem.style[p.cssProps[a.prop]]!=null||p.cssHooks[a.prop])?p.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},cZ.propHooks.scrollTop=cZ.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},p.each(["toggle","show","hide"],function(a,b){var c=p.fn[b];p.fn[b]=function(d,e,f){return d==null||typeof d=="boolean"||!a&&p.isFunction(d)&&p.isFunction(e)?c.apply(this,arguments):this.animate(c$(b,!0),d,e,f)}}),p.fn.extend({fadeTo:function(a,b,c,d){return this.filter(bZ).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=p.isEmptyObject(a),f=p.speed(b,c,d),g=function(){var b=cW(this,p.extend({},a),f);e&&b.stop(!0)};return e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,c,d){var e=function(a){var b=a.stop;delete a.stop,b(d)};return typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,c=a!=null&&a+"queueHooks",f=p.timers,g=p._data(this);if(c)g[c]&&g[c].stop&&e(g[c]);else for(c in g)g[c]&&g[c].stop&&cR.test(c)&&e(g[c]);for(c=f.length;c--;)f[c].elem===this&&(a==null||f[c].queue===a)&&(f[c].anim.stop(d),b=!1,f.splice(c,1));(b||!d)&&p.dequeue(this,a)})}}),p.each({slideDown:c$("show"),slideUp:c$("hide"),slideToggle:c$("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){p.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),p.speed=function(a,b,c){var d=a&&typeof a=="object"?p.extend({},a):{complete:c||!c&&b||p.isFunction(a)&&a,duration:a,easing:c&&b||b&&!p.isFunction(b)&&b};d.duration=p.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in p.fx.speeds?p.fx.speeds[d.duration]:p.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";return d.old=d.complete,d.complete=function(){p.isFunction(d.old)&&d.old.call(this),d.queue&&p.dequeue(this,d.queue)},d},p.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},p.timers=[],p.fx=cZ.prototype.init,p.fx.tick=function(){var a,b=p.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||p.fx.stop()},p.fx.timer=function(a){a()&&p.timers.push(a)&&!cO&&(cO=setInterval(p.fx.tick,p.fx.interval))},p.fx.interval=13,p.fx.stop=function(){clearInterval(cO),cO=null},p.fx.speeds={slow:600,fast:200,_default:400},p.fx.step={},p.expr&&p.expr.filters&&(p.expr.filters.animated=function(a){return p.grep(p.timers,function(b){return a===b.elem}).length});var c_=/^(?:body|html)$/i;p.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){p.offset.setOffset(this,a,b)});var c,d,e,f,g,h,i,j={top:0,left:0},k=this[0],l=k&&k.ownerDocument;if(!l)return;return(d=l.body)===k?p.offset.bodyOffset(k):(c=l.documentElement,p.contains(c,k)?(typeof k.getBoundingClientRect!="undefined"&&(j=k.getBoundingClientRect()),e=da(l),f=c.clientTop||d.clientTop||0,g=c.clientLeft||d.clientLeft||0,h=e.pageYOffset||c.scrollTop,i=e.pageXOffset||c.scrollLeft,{top:j.top+h-f,left:j.left+i-g}):j)},p.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;return p.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(p.css(a,"marginTop"))||0,c+=parseFloat(p.css(a,"marginLeft"))||0),{top:b,left:c}},setOffset:function(a,b,c){var d=p.css(a,"position");d==="static"&&(a.style.position="relative");var e=p(a),f=e.offset(),g=p.css(a,"top"),h=p.css(a,"left"),i=(d==="absolute"||d==="fixed")&&p.inArray("auto",[g,h])>-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c_.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c_.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=da(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g,null)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window);
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery.mobile-1.3.1.js b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery.mobile-1.3.1.js
new file mode 100755
index 0000000000000000000000000000000000000000..fec04bc4d5a9212fb222a2dcc04061ccd5fcc6fc
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/jQuery/jquery.mobile-1.3.1.js
@@ -0,0 +1,11191 @@
+/*
+* jQuery Mobile 1.3.1
+* Git HEAD hash: 74b4bec049fd93e4fe40205e6157de16eb64eb46 <> Date: Wed Apr 10 2013 21:57:23 UTC
+* http://jquerymobile.com
+*
+* Copyright 2010, 2013 jQuery Foundation, Inc. and other contributors
+* Released under the MIT license.
+* http://jquery.org/license
+*
+*/
+
+
+(function ( root, doc, factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery" ], function ( $ ) {
+			factory( $, root, doc );
+			return $.mobile;
+		});
+	} else {
+		// Browser globals
+		factory( root.jQuery, root, doc );
+	}
+}( this, document, function ( jQuery, window, document, undefined ) {
+(function( $ ) {
+	$.mobile = {};
+}( jQuery ));
+(function( $, window, undefined ) {
+	var nsNormalizeDict = {};
+
+	// jQuery.mobile configurable options
+	$.mobile = $.extend($.mobile, {
+
+		// Version of the jQuery Mobile Framework
+		version: "1.3.1",
+
+		// Namespace used framework-wide for data-attrs. Default is no namespace
+		ns: "",
+
+		// Define the url parameter used for referencing widget-generated sub-pages.
+		// Translates to to example.html&ui-page=subpageIdentifier
+		// hash segment before &ui-page= is used to make Ajax request
+		subPageUrlKey: "ui-page",
+
+		// Class assigned to page currently in view, and during transitions
+		activePageClass: "ui-page-active",
+
+		// Class used for "active" button state, from CSS framework
+		activeBtnClass: "ui-btn-active",
+
+		// Class used for "focus" form element state, from CSS framework
+		focusClass: "ui-focus",
+
+		// Automatically handle clicks and form submissions through Ajax, when same-domain
+		ajaxEnabled: true,
+
+		// Automatically load and show pages based on location.hash
+		hashListeningEnabled: true,
+
+		// disable to prevent jquery from bothering with links
+		linkBindingEnabled: true,
+
+		// Set default page transition - 'none' for no transitions
+		defaultPageTransition: "fade",
+
+		// Set maximum window width for transitions to apply - 'false' for no limit
+		maxTransitionWidth: false,
+
+		// Minimum scroll distance that will be remembered when returning to a page
+		minScrollBack: 250,
+
+		// DEPRECATED: the following property is no longer in use, but defined until 2.0 to prevent conflicts
+		touchOverflowEnabled: false,
+
+		// Set default dialog transition - 'none' for no transitions
+		defaultDialogTransition: "pop",
+
+		// Error response message - appears when an Ajax page request fails
+		pageLoadErrorMessage: "Error Loading Page",
+
+		// For error messages, which theme does the box uses?
+		pageLoadErrorMessageTheme: "e",
+
+		// replace calls to window.history.back with phonegaps navigation helper
+		// where it is provided on the window object
+		phonegapNavigationEnabled: false,
+
+		//automatically initialize the DOM when it's ready
+		autoInitializePage: true,
+
+		pushStateEnabled: true,
+
+		// allows users to opt in to ignoring content by marking a parent element as
+		// data-ignored
+		ignoreContentEnabled: false,
+
+		// turn of binding to the native orientationchange due to android orientation behavior
+		orientationChangeEnabled: true,
+
+		buttonMarkup: {
+			hoverDelay: 200
+		},
+
+		// define the window and the document objects
+		window: $( window ),
+		document: $( document ),
+
+		// TODO might be useful upstream in jquery itself ?
+		keyCode: {
+			ALT: 18,
+			BACKSPACE: 8,
+			CAPS_LOCK: 20,
+			COMMA: 188,
+			COMMAND: 91,
+			COMMAND_LEFT: 91, // COMMAND
+			COMMAND_RIGHT: 93,
+			CONTROL: 17,
+			DELETE: 46,
+			DOWN: 40,
+			END: 35,
+			ENTER: 13,
+			ESCAPE: 27,
+			HOME: 36,
+			INSERT: 45,
+			LEFT: 37,
+			MENU: 93, // COMMAND_RIGHT
+			NUMPAD_ADD: 107,
+			NUMPAD_DECIMAL: 110,
+			NUMPAD_DIVIDE: 111,
+			NUMPAD_ENTER: 108,
+			NUMPAD_MULTIPLY: 106,
+			NUMPAD_SUBTRACT: 109,
+			PAGE_DOWN: 34,
+			PAGE_UP: 33,
+			PERIOD: 190,
+			RIGHT: 39,
+			SHIFT: 16,
+			SPACE: 32,
+			TAB: 9,
+			UP: 38,
+			WINDOWS: 91 // COMMAND
+		},
+
+		// Place to store various widget extensions
+		behaviors: {},
+
+		// Scroll page vertically: scroll to 0 to hide iOS address bar, or pass a Y value
+		silentScroll: function( ypos ) {
+			if ( $.type( ypos ) !== "number" ) {
+				ypos = $.mobile.defaultHomeScroll;
+			}
+
+			// prevent scrollstart and scrollstop events
+			$.event.special.scrollstart.enabled = false;
+
+			setTimeout( function() {
+				window.scrollTo( 0, ypos );
+				$.mobile.document.trigger( "silentscroll", { x: 0, y: ypos });
+			}, 20 );
+
+			setTimeout( function() {
+				$.event.special.scrollstart.enabled = true;
+			}, 150 );
+		},
+
+		// Expose our cache for testing purposes.
+		nsNormalizeDict: nsNormalizeDict,
+
+		// Take a data attribute property, prepend the namespace
+		// and then camel case the attribute string. Add the result
+		// to our nsNormalizeDict so we don't have to do this again.
+		nsNormalize: function( prop ) {
+			if ( !prop ) {
+				return;
+			}
+
+			return nsNormalizeDict[ prop ] || ( nsNormalizeDict[ prop ] = $.camelCase( $.mobile.ns + prop ) );
+		},
+
+		// Find the closest parent with a theme class on it. Note that
+		// we are not using $.fn.closest() on purpose here because this
+		// method gets called quite a bit and we need it to be as fast
+		// as possible.
+		getInheritedTheme: function( el, defaultTheme ) {
+			var e = el[ 0 ],
+				ltr = "",
+				re = /ui-(bar|body|overlay)-([a-z])\b/,
+				c, m;
+
+			while ( e ) {
+				c = e.className || "";
+				if ( c && ( m = re.exec( c ) ) && ( ltr = m[ 2 ] ) ) {
+					// We found a parent with a theme class
+					// on it so bail from this loop.
+					break;
+				}
+
+				e = e.parentNode;
+			}
+
+			// Return the theme letter we found, if none, return the
+			// specified default.
+
+			return ltr || defaultTheme || "a";
+		},
+
+		// TODO the following $ and $.fn extensions can/probably should be moved into jquery.mobile.core.helpers
+		//
+		// Find the closest javascript page element to gather settings data jsperf test
+		// http://jsperf.com/single-complex-selector-vs-many-complex-selectors/edit
+		// possibly naive, but it shows that the parsing overhead for *just* the page selector vs
+		// the page and dialog selector is negligable. This could probably be speed up by
+		// doing a similar parent node traversal to the one found in the inherited theme code above
+		closestPageData: function( $target ) {
+			return $target
+				.closest( ':jqmData(role="page"), :jqmData(role="dialog")' )
+				.data( "mobile-page" );
+		},
+
+		enhanceable: function( $set ) {
+			return this.haveParents( $set, "enhance" );
+		},
+
+		hijackable: function( $set ) {
+			return this.haveParents( $set, "ajax" );
+		},
+
+		haveParents: function( $set, attr ) {
+			if ( !$.mobile.ignoreContentEnabled ) {
+				return $set;
+			}
+
+			var count = $set.length,
+				$newSet = $(),
+				e, $element, excluded;
+
+			for ( var i = 0; i < count; i++ ) {
+				$element = $set.eq( i );
+				excluded = false;
+				e = $set[ i ];
+
+				while ( e ) {
+					var c = e.getAttribute ? e.getAttribute( "data-" + $.mobile.ns + attr ) : "";
+
+					if ( c === "false" ) {
+						excluded = true;
+						break;
+					}
+
+					e = e.parentNode;
+				}
+
+				if ( !excluded ) {
+					$newSet = $newSet.add( $element );
+				}
+			}
+
+			return $newSet;
+		},
+
+		getScreenHeight: function() {
+			// Native innerHeight returns more accurate value for this across platforms,
+			// jQuery version is here as a normalized fallback for platforms like Symbian
+			return window.innerHeight || $.mobile.window.height();
+		}
+	}, $.mobile );
+
+	// Mobile version of data and removeData and hasData methods
+	// ensures all data is set and retrieved using jQuery Mobile's data namespace
+	$.fn.jqmData = function( prop, value ) {
+		var result;
+		if ( typeof prop !== "undefined" ) {
+			if ( prop ) {
+				prop = $.mobile.nsNormalize( prop );
+			}
+
+			// undefined is permitted as an explicit input for the second param
+			// in this case it returns the value and does not set it to undefined
+			if( arguments.length < 2 || value === undefined ){
+				result = this.data( prop );
+			} else {
+				result = this.data( prop, value );
+			}
+		}
+		return result;
+	};
+
+	$.jqmData = function( elem, prop, value ) {
+		var result;
+		if ( typeof prop !== "undefined" ) {
+			result = $.data( elem, prop ? $.mobile.nsNormalize( prop ) : prop, value );
+		}
+		return result;
+	};
+
+	$.fn.jqmRemoveData = function( prop ) {
+		return this.removeData( $.mobile.nsNormalize( prop ) );
+	};
+
+	$.jqmRemoveData = function( elem, prop ) {
+		return $.removeData( elem, $.mobile.nsNormalize( prop ) );
+	};
+
+	$.fn.removeWithDependents = function() {
+		$.removeWithDependents( this );
+	};
+
+	$.removeWithDependents = function( elem ) {
+		var $elem = $( elem );
+
+		( $elem.jqmData( 'dependents' ) || $() ).remove();
+		$elem.remove();
+	};
+
+	$.fn.addDependents = function( newDependents ) {
+		$.addDependents( $( this ), newDependents );
+	};
+
+	$.addDependents = function( elem, newDependents ) {
+		var dependents = $( elem ).jqmData( 'dependents' ) || $();
+
+		$( elem ).jqmData( 'dependents', $.merge( dependents, newDependents ) );
+	};
+
+	// note that this helper doesn't attempt to handle the callback
+	// or setting of an html element's text, its only purpose is
+	// to return the html encoded version of the text in all cases. (thus the name)
+	$.fn.getEncodedText = function() {
+		return $( "<div/>" ).text( $( this ).text() ).html();
+	};
+
+	// fluent helper function for the mobile namespaced equivalent
+	$.fn.jqmEnhanceable = function() {
+		return $.mobile.enhanceable( this );
+	};
+
+	$.fn.jqmHijackable = function() {
+		return $.mobile.hijackable( this );
+	};
+
+	// Monkey-patching Sizzle to filter the :jqmData selector
+	var oldFind = $.find,
+		jqmDataRE = /:jqmData\(([^)]*)\)/g;
+
+	$.find = function( selector, context, ret, extra ) {
+		selector = selector.replace( jqmDataRE, "[data-" + ( $.mobile.ns || "" ) + "$1]" );
+
+		return oldFind.call( this, selector, context, ret, extra );
+	};
+
+	$.extend( $.find, oldFind );
+
+	$.find.matches = function( expr, set ) {
+		return $.find( expr, null, null, set );
+	};
+
+	$.find.matchesSelector = function( node, expr ) {
+		return $.find( expr, null, null, [ node ] ).length > 0;
+	};
+})( jQuery, this );
+
+
+/*!
+ * jQuery UI Widget v1.10.0pre - 2012-11-13 (ff055a0c353c3c8ce6e5bfa07ad7cb03e8885bc5)
+ * http://jqueryui.com
+ *
+ * Copyright 2010, 2013 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/jQuery.widget/
+ */
+(function( $, undefined ) {
+
+var uuid = 0,
+	slice = Array.prototype.slice,
+	_cleanData = $.cleanData;
+$.cleanData = function( elems ) {
+	for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+		try {
+			$( elem ).triggerHandler( "remove" );
+		// http://bugs.jquery.com/ticket/8235
+		} catch( e ) {}
+	}
+	_cleanData( elems );
+};
+
+$.widget = function( name, base, prototype ) {
+	var fullName, existingConstructor, constructor, basePrototype,
+		namespace = name.split( "." )[ 0 ];
+
+	name = name.split( "." )[ 1 ];
+	fullName = namespace + "-" + name;
+
+	if ( !prototype ) {
+		prototype = base;
+		base = $.Widget;
+	}
+
+	// create selector for plugin
+	$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
+		return !!$.data( elem, fullName );
+	};
+
+	$[ namespace ] = $[ namespace ] || {};
+	existingConstructor = $[ namespace ][ name ];
+	constructor = $[ namespace ][ name ] = function( options, element ) {
+		// allow instantiation without "new" keyword
+		if ( !this._createWidget ) {
+			return new constructor( options, element );
+		}
+
+		// allow instantiation without initializing for simple inheritance
+		// must use "new" keyword (the code above always passes args)
+		if ( arguments.length ) {
+			this._createWidget( options, element );
+		}
+	};
+	// extend with the existing constructor to carry over any static properties
+	$.extend( constructor, existingConstructor, {
+		version: prototype.version,
+		// copy the object used to create the prototype in case we need to
+		// redefine the widget later
+		_proto: $.extend( {}, prototype ),
+		// track widgets that inherit from this widget in case this widget is
+		// redefined after a widget inherits from it
+		_childConstructors: []
+	});
+
+	basePrototype = new base();
+	// we need to make the options hash a property directly on the new instance
+	// otherwise we'll modify the options hash on the prototype that we're
+	// inheriting from
+	basePrototype.options = $.widget.extend( {}, basePrototype.options );
+	$.each( prototype, function( prop, value ) {
+		if ( $.isFunction( value ) ) {
+			prototype[ prop ] = (function() {
+				var _super = function() {
+						return base.prototype[ prop ].apply( this, arguments );
+					},
+					_superApply = function( args ) {
+						return base.prototype[ prop ].apply( this, args );
+					};
+				return function() {
+					var __super = this._super,
+						__superApply = this._superApply,
+						returnValue;
+
+					this._super = _super;
+					this._superApply = _superApply;
+
+					returnValue = value.apply( this, arguments );
+
+					this._super = __super;
+					this._superApply = __superApply;
+
+					return returnValue;
+				};
+			})();
+		}
+	});
+	constructor.prototype = $.widget.extend( basePrototype, {
+		// TODO: remove support for widgetEventPrefix
+		// always use the name + a colon as the prefix, e.g., draggable:start
+		// don't prefix for widgets that aren't DOM-based
+		widgetEventPrefix: existingConstructor ? basePrototype.widgetEventPrefix : name
+	}, prototype, {
+		constructor: constructor,
+		namespace: namespace,
+		widgetName: name,
+		widgetFullName: fullName
+	});
+
+	// If this widget is being redefined then we need to find all widgets that
+	// are inheriting from it and redefine all of them so that they inherit from
+	// the new version of this widget. We're essentially trying to replace one
+	// level in the prototype chain.
+	if ( existingConstructor ) {
+		$.each( existingConstructor._childConstructors, function( i, child ) {
+			var childPrototype = child.prototype;
+
+			// redefine the child widget using the same prototype that was
+			// originally used, but inherit from the new version of the base
+			$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
+		});
+		// remove the list of existing child constructors from the old constructor
+		// so the old child constructors can be garbage collected
+		delete existingConstructor._childConstructors;
+	} else {
+		base._childConstructors.push( constructor );
+	}
+
+	$.widget.bridge( name, constructor );
+};
+
+$.widget.extend = function( target ) {
+	var input = slice.call( arguments, 1 ),
+		inputIndex = 0,
+		inputLength = input.length,
+		key,
+		value;
+	for ( ; inputIndex < inputLength; inputIndex++ ) {
+		for ( key in input[ inputIndex ] ) {
+			value = input[ inputIndex ][ key ];
+			if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
+				// Clone objects
+				if ( $.isPlainObject( value ) ) {
+					target[ key ] = $.isPlainObject( target[ key ] ) ?
+						$.widget.extend( {}, target[ key ], value ) :
+						// Don't extend strings, arrays, etc. with objects
+						$.widget.extend( {}, value );
+				// Copy everything else by reference
+				} else {
+					target[ key ] = value;
+				}
+			}
+		}
+	}
+	return target;
+};
+
+$.widget.bridge = function( name, object ) {
+	var fullName = object.prototype.widgetFullName || name;
+	$.fn[ name ] = function( options ) {
+		var isMethodCall = typeof options === "string",
+			args = slice.call( arguments, 1 ),
+			returnValue = this;
+
+		// allow multiple hashes to be passed on init
+		options = !isMethodCall && args.length ?
+			$.widget.extend.apply( null, [ options ].concat(args) ) :
+			options;
+
+		if ( isMethodCall ) {
+			this.each(function() {
+				var methodValue,
+					instance = $.data( this, fullName );
+				if ( !instance ) {
+					return $.error( "cannot call methods on " + name + " prior to initialization; " +
+						"attempted to call method '" + options + "'" );
+				}
+				if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
+					return $.error( "no such method '" + options + "' for " + name + " widget instance" );
+				}
+				methodValue = instance[ options ].apply( instance, args );
+				if ( methodValue !== instance && methodValue !== undefined ) {
+					returnValue = methodValue && methodValue.jquery ?
+						returnValue.pushStack( methodValue.get() ) :
+						methodValue;
+					return false;
+				}
+			});
+		} else {
+			this.each(function() {
+				var instance = $.data( this, fullName );
+				if ( instance ) {
+					instance.option( options || {} )._init();
+				} else {
+					$.data( this, fullName, new object( options, this ) );
+				}
+			});
+		}
+
+		return returnValue;
+	};
+};
+
+$.Widget = function( /* options, element */ ) {};
+$.Widget._childConstructors = [];
+
+$.Widget.prototype = {
+	widgetName: "widget",
+	widgetEventPrefix: "",
+	defaultElement: "<div>",
+	options: {
+		disabled: false,
+
+		// callbacks
+		create: null
+	},
+	_createWidget: function( options, element ) {
+		element = $( element || this.defaultElement || this )[ 0 ];
+		this.element = $( element );
+		this.uuid = uuid++;
+		this.eventNamespace = "." + this.widgetName + this.uuid;
+		this.options = $.widget.extend( {},
+			this.options,
+			this._getCreateOptions(),
+			options );
+
+		this.bindings = $();
+		this.hoverable = $();
+		this.focusable = $();
+
+		if ( element !== this ) {
+			$.data( element, this.widgetFullName, this );
+			this._on( true, this.element, {
+				remove: function( event ) {
+					if ( event.target === element ) {
+						this.destroy();
+					}
+				}
+			});
+			this.document = $( element.style ?
+				// element within the document
+				element.ownerDocument :
+				// element is window or document
+				element.document || element );
+			this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
+		}
+
+		this._create();
+		this._trigger( "create", null, this._getCreateEventData() );
+		this._init();
+	},
+	_getCreateOptions: $.noop,
+	_getCreateEventData: $.noop,
+	_create: $.noop,
+	_init: $.noop,
+
+	destroy: function() {
+		this._destroy();
+		// we can probably remove the unbind calls in 2.0
+		// all event bindings should go through this._on()
+		this.element
+			.unbind( this.eventNamespace )
+			// 1.9 BC for #7810
+			// TODO remove dual storage
+			.removeData( this.widgetName )
+			.removeData( this.widgetFullName )
+			// support: jquery <1.6.3
+			// http://bugs.jquery.com/ticket/9413
+			.removeData( $.camelCase( this.widgetFullName ) );
+		this.widget()
+			.unbind( this.eventNamespace )
+			.removeAttr( "aria-disabled" )
+			.removeClass(
+				this.widgetFullName + "-disabled " +
+				"ui-state-disabled" );
+
+		// clean up events and states
+		this.bindings.unbind( this.eventNamespace );
+		this.hoverable.removeClass( "ui-state-hover" );
+		this.focusable.removeClass( "ui-state-focus" );
+	},
+	_destroy: $.noop,
+
+	widget: function() {
+		return this.element;
+	},
+
+	option: function( key, value ) {
+		var options = key,
+			parts,
+			curOption,
+			i;
+
+		if ( arguments.length === 0 ) {
+			// don't return a reference to the internal hash
+			return $.widget.extend( {}, this.options );
+		}
+
+		if ( typeof key === "string" ) {
+			// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
+			options = {};
+			parts = key.split( "." );
+			key = parts.shift();
+			if ( parts.length ) {
+				curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
+				for ( i = 0; i < parts.length - 1; i++ ) {
+					curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
+					curOption = curOption[ parts[ i ] ];
+				}
+				key = parts.pop();
+				if ( value === undefined ) {
+					return curOption[ key ] === undefined ? null : curOption[ key ];
+				}
+				curOption[ key ] = value;
+			} else {
+				if ( value === undefined ) {
+					return this.options[ key ] === undefined ? null : this.options[ key ];
+				}
+				options[ key ] = value;
+			}
+		}
+
+		this._setOptions( options );
+
+		return this;
+	},
+	_setOptions: function( options ) {
+		var key;
+
+		for ( key in options ) {
+			this._setOption( key, options[ key ] );
+		}
+
+		return this;
+	},
+	_setOption: function( key, value ) {
+		this.options[ key ] = value;
+
+		if ( key === "disabled" ) {
+			this.widget()
+				.toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value )
+				.attr( "aria-disabled", value );
+			this.hoverable.removeClass( "ui-state-hover" );
+			this.focusable.removeClass( "ui-state-focus" );
+		}
+
+		return this;
+	},
+
+	enable: function() {
+		return this._setOption( "disabled", false );
+	},
+	disable: function() {
+		return this._setOption( "disabled", true );
+	},
+
+	_on: function( suppressDisabledCheck, element, handlers ) {
+		var delegateElement,
+			instance = this;
+
+		// no suppressDisabledCheck flag, shuffle arguments
+		if ( typeof suppressDisabledCheck !== "boolean" ) {
+			handlers = element;
+			element = suppressDisabledCheck;
+			suppressDisabledCheck = false;
+		}
+
+		// no element argument, shuffle and use this.element
+		if ( !handlers ) {
+			handlers = element;
+			element = this.element;
+			delegateElement = this.widget();
+		} else {
+			// accept selectors, DOM elements
+			element = delegateElement = $( element );
+			this.bindings = this.bindings.add( element );
+		}
+
+		$.each( handlers, function( event, handler ) {
+			function handlerProxy() {
+				// allow widgets to customize the disabled handling
+				// - disabled as an array instead of boolean
+				// - disabled class as method for disabling individual parts
+				if ( !suppressDisabledCheck &&
+						( instance.options.disabled === true ||
+							$( this ).hasClass( "ui-state-disabled" ) ) ) {
+					return;
+				}
+				return ( typeof handler === "string" ? instance[ handler ] : handler )
+					.apply( instance, arguments );
+			}
+
+			// copy the guid so direct unbinding works
+			if ( typeof handler !== "string" ) {
+				handlerProxy.guid = handler.guid =
+					handler.guid || handlerProxy.guid || $.guid++;
+			}
+
+			var match = event.match( /^(\w+)\s*(.*)$/ ),
+				eventName = match[1] + instance.eventNamespace,
+				selector = match[2];
+			if ( selector ) {
+				delegateElement.delegate( selector, eventName, handlerProxy );
+			} else {
+				element.bind( eventName, handlerProxy );
+			}
+		});
+	},
+
+	_off: function( element, eventName ) {
+		eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace;
+		element.unbind( eventName ).undelegate( eventName );
+	},
+
+	_delay: function( handler, delay ) {
+		function handlerProxy() {
+			return ( typeof handler === "string" ? instance[ handler ] : handler )
+				.apply( instance, arguments );
+		}
+		var instance = this;
+		return setTimeout( handlerProxy, delay || 0 );
+	},
+
+	_hoverable: function( element ) {
+		this.hoverable = this.hoverable.add( element );
+		this._on( element, {
+			mouseenter: function( event ) {
+				$( event.currentTarget ).addClass( "ui-state-hover" );
+			},
+			mouseleave: function( event ) {
+				$( event.currentTarget ).removeClass( "ui-state-hover" );
+			}
+		});
+	},
+
+	_focusable: function( element ) {
+		this.focusable = this.focusable.add( element );
+		this._on( element, {
+			focusin: function( event ) {
+				$( event.currentTarget ).addClass( "ui-state-focus" );
+			},
+			focusout: function( event ) {
+				$( event.currentTarget ).removeClass( "ui-state-focus" );
+			}
+		});
+	},
+
+	_trigger: function( type, event, data ) {
+		var prop, orig,
+			callback = this.options[ type ];
+
+		data = data || {};
+		event = $.Event( event );
+		event.type = ( type === this.widgetEventPrefix ?
+			type :
+			this.widgetEventPrefix + type ).toLowerCase();
+		// the original event may come from any element
+		// so we need to reset the target on the new event
+		event.target = this.element[ 0 ];
+
+		// copy original event properties over to the new event
+		orig = event.originalEvent;
+		if ( orig ) {
+			for ( prop in orig ) {
+				if ( !( prop in event ) ) {
+					event[ prop ] = orig[ prop ];
+				}
+			}
+		}
+
+		this.element.trigger( event, data );
+		return !( $.isFunction( callback ) &&
+			callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
+			event.isDefaultPrevented() );
+	}
+};
+
+$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
+	$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
+		if ( typeof options === "string" ) {
+			options = { effect: options };
+		}
+		var hasOptions,
+			effectName = !options ?
+				method :
+				options === true || typeof options === "number" ?
+					defaultEffect :
+					options.effect || defaultEffect;
+		options = options || {};
+		if ( typeof options === "number" ) {
+			options = { duration: options };
+		}
+		hasOptions = !$.isEmptyObject( options );
+		options.complete = callback;
+		if ( options.delay ) {
+			element.delay( options.delay );
+		}
+		if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
+			element[ method ]( options );
+		} else if ( effectName !== method && element[ effectName ] ) {
+			element[ effectName ]( options.duration, options.easing, callback );
+		} else {
+			element.queue(function( next ) {
+				$( this )[ method ]();
+				if ( callback ) {
+					callback.call( element[ 0 ] );
+				}
+				next();
+			});
+		}
+	};
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.widget( "mobile.widget", {
+	// decorate the parent _createWidget to trigger `widgetinit` for users
+	// who wish to do post post `widgetcreate` alterations/additions
+	//
+	// TODO create a pull request for jquery ui to trigger this event
+	// in the original _createWidget
+	_createWidget: function() {
+		$.Widget.prototype._createWidget.apply( this, arguments );
+		this._trigger( 'init' );
+	},
+
+	_getCreateOptions: function() {
+
+		var elem = this.element,
+			options = {};
+
+		$.each( this.options, function( option ) {
+
+			var value = elem.jqmData( option.replace( /[A-Z]/g, function( c ) {
+							return "-" + c.toLowerCase();
+						})
+					);
+
+			if ( value !== undefined ) {
+				options[ option ] = value;
+			}
+		});
+
+		return options;
+	},
+
+	enhanceWithin: function( target, useKeepNative ) {
+		this.enhance( $( this.options.initSelector, $( target )), useKeepNative );
+	},
+
+	enhance: function( targets, useKeepNative ) {
+		var page, keepNative, $widgetElements = $( targets ), self = this;
+
+		// if ignoreContentEnabled is set to true the framework should
+		// only enhance the selected elements when they do NOT have a
+		// parent with the data-namespace-ignore attribute
+		$widgetElements = $.mobile.enhanceable( $widgetElements );
+
+		if ( useKeepNative && $widgetElements.length ) {
+			// TODO remove dependency on the page widget for the keepNative.
+			// Currently the keepNative value is defined on the page prototype so
+			// the method is as well
+			page = $.mobile.closestPageData( $widgetElements );
+			keepNative = ( page && page.keepNativeSelector()) || "";
+
+			$widgetElements = $widgetElements.not( keepNative );
+		}
+
+		$widgetElements[ this.widgetName ]();
+	},
+
+	raise: function( msg ) {
+		throw "Widget [" + this.widgetName + "]: " + msg;
+	}
+});
+
+})( jQuery );
+
+
+(function( $, window ) {
+	// DEPRECATED
+	// NOTE global mobile object settings
+	$.extend( $.mobile, {
+		// DEPRECATED Should the text be visble in the loading message?
+		loadingMessageTextVisible: undefined,
+
+		// DEPRECATED When the text is visible, what theme does the loading box use?
+		loadingMessageTheme: undefined,
+
+		// DEPRECATED default message setting
+		loadingMessage: undefined,
+
+		// DEPRECATED
+		// Turn on/off page loading message. Theme doubles as an object argument
+		// with the following shape: { theme: '', text: '', html: '', textVisible: '' }
+		// NOTE that the $.mobile.loading* settings and params past the first are deprecated
+		showPageLoadingMsg: function( theme, msgText, textonly ) {
+			$.mobile.loading( 'show', theme, msgText, textonly );
+		},
+
+		// DEPRECATED
+		hidePageLoadingMsg: function() {
+			$.mobile.loading( 'hide' );
+		},
+
+		loading: function() {
+			this.loaderWidget.loader.apply( this.loaderWidget, arguments );
+		}
+	});
+
+	// TODO move loader class down into the widget settings
+	var loaderClass = "ui-loader", $html = $( "html" ), $window = $.mobile.window;
+
+	$.widget( "mobile.loader", {
+		// NOTE if the global config settings are defined they will override these
+		//      options
+		options: {
+			// the theme for the loading message
+			theme: "a",
+
+			// whether the text in the loading message is shown
+			textVisible: false,
+
+			// custom html for the inner content of the loading message
+			html: "",
+
+			// the text to be displayed when the popup is shown
+			text: "loading"
+		},
+
+		defaultHtml: "<div class='" + loaderClass + "'>" +
+			"<span class='ui-icon ui-icon-loading'></span>" +
+			"<h1></h1>" +
+			"</div>",
+
+		// For non-fixed supportin browsers. Position at y center (if scrollTop supported), above the activeBtn (if defined), or just 100px from top
+		fakeFixLoader: function() {
+			var activeBtn = $( "." + $.mobile.activeBtnClass ).first();
+
+			this.element
+				.css({
+					top: $.support.scrollTop && $window.scrollTop() + $window.height() / 2 ||
+						activeBtn.length && activeBtn.offset().top || 100
+				});
+		},
+
+		// check position of loader to see if it appears to be "fixed" to center
+		// if not, use abs positioning
+		checkLoaderPosition: function() {
+			var offset = this.element.offset(),
+				scrollTop = $window.scrollTop(),
+				screenHeight = $.mobile.getScreenHeight();
+
+			if ( offset.top < scrollTop || ( offset.top - scrollTop ) > screenHeight ) {
+				this.element.addClass( "ui-loader-fakefix" );
+				this.fakeFixLoader();
+				$window
+					.unbind( "scroll", this.checkLoaderPosition )
+					.bind( "scroll", $.proxy( this.fakeFixLoader, this ) );
+			}
+		},
+
+		resetHtml: function() {
+			this.element.html( $( this.defaultHtml ).html() );
+		},
+
+		// Turn on/off page loading message. Theme doubles as an object argument
+		// with the following shape: { theme: '', text: '', html: '', textVisible: '' }
+		// NOTE that the $.mobile.loading* settings and params past the first are deprecated
+		// TODO sweet jesus we need to break some of this out
+		show: function( theme, msgText, textonly ) {
+			var textVisible, message, $header, loadSettings;
+
+			this.resetHtml();
+
+			// use the prototype options so that people can set them globally at
+			// mobile init. Consistency, it's what's for dinner
+			if ( $.type(theme) === "object" ) {
+				loadSettings = $.extend( {}, this.options, theme );
+
+				// prefer object property from the param then the old theme setting
+				theme = loadSettings.theme || $.mobile.loadingMessageTheme;
+			} else {
+				loadSettings = this.options;
+
+				// here we prefer the them value passed as a string argument, then
+				// we prefer the global option because we can't use undefined default
+				// prototype options, then the prototype option
+				theme = theme || $.mobile.loadingMessageTheme || loadSettings.theme;
+			}
+
+			// set the message text, prefer the param, then the settings object
+			// then loading message
+			message = msgText || $.mobile.loadingMessage || loadSettings.text;
+
+			// prepare the dom
+			$html.addClass( "ui-loading" );
+
+			if ( $.mobile.loadingMessage !== false || loadSettings.html ) {
+				// boolean values require a bit more work :P, supports object properties
+				// and old settings
+				if ( $.mobile.loadingMessageTextVisible !== undefined ) {
+					textVisible = $.mobile.loadingMessageTextVisible;
+				} else {
+					textVisible = loadSettings.textVisible;
+				}
+
+				// add the proper css given the options (theme, text, etc)
+				// Force text visibility if the second argument was supplied, or
+				// if the text was explicitly set in the object args
+				this.element.attr("class", loaderClass +
+					" ui-corner-all ui-body-" + theme +
+					" ui-loader-" + ( textVisible || msgText || theme.text ? "verbose" : "default" ) +
+					( loadSettings.textonly || textonly ? " ui-loader-textonly" : "" ) );
+
+				// TODO verify that jquery.fn.html is ok to use in both cases here
+				//      this might be overly defensive in preventing unknowing xss
+				// if the html attribute is defined on the loading settings, use that
+				// otherwise use the fallbacks from above
+				if ( loadSettings.html ) {
+					this.element.html( loadSettings.html );
+				} else {
+					this.element.find( "h1" ).text( message );
+				}
+
+				// attach the loader to the DOM
+				this.element.appendTo( $.mobile.pageContainer );
+
+				// check that the loader is visible
+				this.checkLoaderPosition();
+
+				// on scroll check the loader position
+				$window.bind( "scroll", $.proxy( this.checkLoaderPosition, this ) );
+			}
+		},
+
+		hide: function() {
+			$html.removeClass( "ui-loading" );
+
+			if ( $.mobile.loadingMessage ) {
+				this.element.removeClass( "ui-loader-fakefix" );
+			}
+
+			$.mobile.window.unbind( "scroll", this.fakeFixLoader );
+			$.mobile.window.unbind( "scroll", this.checkLoaderPosition );
+		}
+	});
+
+	$window.bind( 'pagecontainercreate', function() {
+		$.mobile.loaderWidget = $.mobile.loaderWidget || $( $.mobile.loader.prototype.defaultHtml ).loader();
+	});
+})(jQuery, this);
+
+
+// Script: jQuery hashchange event
+// 
+// *Version: 1.3, Last updated: 7/21/2010*
+// 
+// Project Home - http://benalman.com/projects/jquery-hashchange-plugin/
+// GitHub       - http://github.com/cowboy/jquery-hashchange/
+// Source       - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js
+// (Minified)   - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped)
+// 
+// About: License
+// 
+// Copyright (c) 2010 "Cowboy" Ben Alman,
+// Dual licensed under the MIT and GPL licenses.
+// http://benalman.com/about/license/
+// 
+// About: Examples
+// 
+// These working examples, complete with fully commented code, illustrate a few
+// ways in which this plugin can be used.
+// 
+// hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/
+// document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/
+// 
+// About: Support and Testing
+// 
+// Information about what version or versions of jQuery this plugin has been
+// tested with, what browsers it has been tested in, and where the unit tests
+// reside (so you can test it yourself).
+// 
+// jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2
+// Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5,
+//                   Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5.
+// Unit Tests      - http://benalman.com/code/projects/jquery-hashchange/unit/
+// 
+// About: Known issues
+// 
+// While this jQuery hashchange event implementation is quite stable and
+// robust, there are a few unfortunate browser bugs surrounding expected
+// hashchange event-based behaviors, independent of any JavaScript
+// window.onhashchange abstraction. See the following examples for more
+// information:
+// 
+// Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/
+// Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/
+// WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/
+// Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/
+// 
+// Also note that should a browser natively support the window.onhashchange 
+// event, but not report that it does, the fallback polling loop will be used.
+// 
+// About: Release History
+// 
+// 1.3   - (7/21/2010) Reorganized IE6/7 Iframe code to make it more
+//         "removable" for mobile-only development. Added IE6/7 document.title
+//         support. Attempted to make Iframe as hidden as possible by using
+//         techniques from http://www.paciellogroup.com/blog/?p=604. Added 
+//         support for the "shortcut" format $(window).hashchange( fn ) and
+//         $(window).hashchange() like jQuery provides for built-in events.
+//         Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and
+//         lowered its default value to 50. Added <jQuery.fn.hashchange.domain>
+//         and <jQuery.fn.hashchange.src> properties plus document-domain.html
+//         file to address access denied issues when setting document.domain in
+//         IE6/7.
+// 1.2   - (2/11/2010) Fixed a bug where coming back to a page using this plugin
+//         from a page on another domain would cause an error in Safari 4. Also,
+//         IE6/7 Iframe is now inserted after the body (this actually works),
+//         which prevents the page from scrolling when the event is first bound.
+//         Event can also now be bound before DOM ready, but it won't be usable
+//         before then in IE6/7.
+// 1.1   - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug
+//         where browser version is incorrectly reported as 8.0, despite
+//         inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag.
+// 1.0   - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special
+//         window.onhashchange functionality into a separate plugin for users
+//         who want just the basic event & back button support, without all the
+//         extra awesomeness that BBQ provides. This plugin will be included as
+//         part of jQuery BBQ, but also be available separately.
+
+(function( $, window, undefined ) {
+  // Reused string.
+  var str_hashchange = 'hashchange',
+    
+    // Method / object references.
+    doc = document,
+    fake_onhashchange,
+    special = $.event.special,
+    
+    // Does the browser support window.onhashchange? Note that IE8 running in
+    // IE7 compatibility mode reports true for 'onhashchange' in window, even
+    // though the event isn't supported, so also test document.documentMode.
+    doc_mode = doc.documentMode,
+    supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 );
+  
+  // Get location.hash (or what you'd expect location.hash to be) sans any
+  // leading #. Thanks for making this necessary, Firefox!
+  function get_fragment( url ) {
+    url = url || location.href;
+    return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' );
+  };
+  
+  // Method: jQuery.fn.hashchange
+  // 
+  // Bind a handler to the window.onhashchange event or trigger all bound
+  // window.onhashchange event handlers. This behavior is consistent with
+  // jQuery's built-in event handlers.
+  // 
+  // Usage:
+  // 
+  // > jQuery(window).hashchange( [ handler ] );
+  // 
+  // Arguments:
+  // 
+  //  handler - (Function) Optional handler to be bound to the hashchange
+  //    event. This is a "shortcut" for the more verbose form:
+  //    jQuery(window).bind( 'hashchange', handler ). If handler is omitted,
+  //    all bound window.onhashchange event handlers will be triggered. This
+  //    is a shortcut for the more verbose
+  //    jQuery(window).trigger( 'hashchange' ). These forms are described in
+  //    the <hashchange event> section.
+  // 
+  // Returns:
+  // 
+  //  (jQuery) The initial jQuery collection of elements.
+  
+  // Allow the "shortcut" format $(elem).hashchange( fn ) for binding and
+  // $(elem).hashchange() for triggering, like jQuery does for built-in events.
+  $.fn[ str_hashchange ] = function( fn ) {
+    return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange );
+  };
+  
+  // Property: jQuery.fn.hashchange.delay
+  // 
+  // The numeric interval (in milliseconds) at which the <hashchange event>
+  // polling loop executes. Defaults to 50.
+  
+  // Property: jQuery.fn.hashchange.domain
+  // 
+  // If you're setting document.domain in your JavaScript, and you want hash
+  // history to work in IE6/7, not only must this property be set, but you must
+  // also set document.domain BEFORE jQuery is loaded into the page. This
+  // property is only applicable if you are supporting IE6/7 (or IE8 operating
+  // in "IE7 compatibility" mode).
+  // 
+  // In addition, the <jQuery.fn.hashchange.src> property must be set to the
+  // path of the included "document-domain.html" file, which can be renamed or
+  // modified if necessary (note that the document.domain specified must be the
+  // same in both your main JavaScript as well as in this file).
+  // 
+  // Usage:
+  // 
+  // jQuery.fn.hashchange.domain = document.domain;
+  
+  // Property: jQuery.fn.hashchange.src
+  // 
+  // If, for some reason, you need to specify an Iframe src file (for example,
+  // when setting document.domain as in <jQuery.fn.hashchange.domain>), you can
+  // do so using this property. Note that when using this property, history
+  // won't be recorded in IE6/7 until the Iframe src file loads. This property
+  // is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7
+  // compatibility" mode).
+  // 
+  // Usage:
+  // 
+  // jQuery.fn.hashchange.src = 'path/to/file.html';
+  
+  $.fn[ str_hashchange ].delay = 50;
+  /*
+  $.fn[ str_hashchange ].domain = null;
+  $.fn[ str_hashchange ].src = null;
+  */
+  
+  // Event: hashchange event
+  // 
+  // Fired when location.hash changes. In browsers that support it, the native
+  // HTML5 window.onhashchange event is used, otherwise a polling loop is
+  // initialized, running every <jQuery.fn.hashchange.delay> milliseconds to
+  // see if the hash has changed. In IE6/7 (and IE8 operating in "IE7
+  // compatibility" mode), a hidden Iframe is created to allow the back button
+  // and hash-based history to work.
+  // 
+  // Usage as described in <jQuery.fn.hashchange>:
+  // 
+  // > // Bind an event handler.
+  // > jQuery(window).hashchange( function(e) {
+  // >   var hash = location.hash;
+  // >   ...
+  // > });
+  // > 
+  // > // Manually trigger the event handler.
+  // > jQuery(window).hashchange();
+  // 
+  // A more verbose usage that allows for event namespacing:
+  // 
+  // > // Bind an event handler.
+  // > jQuery(window).bind( 'hashchange', function(e) {
+  // >   var hash = location.hash;
+  // >   ...
+  // > });
+  // > 
+  // > // Manually trigger the event handler.
+  // > jQuery(window).trigger( 'hashchange' );
+  // 
+  // Additional Notes:
+  // 
+  // * The polling loop and Iframe are not created until at least one handler
+  //   is actually bound to the 'hashchange' event.
+  // * If you need the bound handler(s) to execute immediately, in cases where
+  //   a location.hash exists on page load, via bookmark or page refresh for
+  //   example, use jQuery(window).hashchange() or the more verbose 
+  //   jQuery(window).trigger( 'hashchange' ).
+  // * The event can be bound before DOM ready, but since it won't be usable
+  //   before then in IE6/7 (due to the necessary Iframe), recommended usage is
+  //   to bind it inside a DOM ready handler.
+  
+  // Override existing $.event.special.hashchange methods (allowing this plugin
+  // to be defined after jQuery BBQ in BBQ's source code).
+  special[ str_hashchange ] = $.extend( special[ str_hashchange ], {
+    
+    // Called only when the first 'hashchange' event is bound to window.
+    setup: function() {
+      // If window.onhashchange is supported natively, there's nothing to do..
+      if ( supports_onhashchange ) { return false; }
+      
+      // Otherwise, we need to create our own. And we don't want to call this
+      // until the user binds to the event, just in case they never do, since it
+      // will create a polling loop and possibly even a hidden Iframe.
+      $( fake_onhashchange.start );
+    },
+    
+    // Called only when the last 'hashchange' event is unbound from window.
+    teardown: function() {
+      // If window.onhashchange is supported natively, there's nothing to do..
+      if ( supports_onhashchange ) { return false; }
+      
+      // Otherwise, we need to stop ours (if possible).
+      $( fake_onhashchange.stop );
+    }
+    
+  });
+  
+  // fake_onhashchange does all the work of triggering the window.onhashchange
+  // event for browsers that don't natively support it, including creating a
+  // polling loop to watch for hash changes and in IE 6/7 creating a hidden
+  // Iframe to enable back and forward.
+  fake_onhashchange = (function() {
+    var self = {},
+      timeout_id,
+      
+      // Remember the initial hash so it doesn't get triggered immediately.
+      last_hash = get_fragment(),
+      
+      fn_retval = function( val ) { return val; },
+      history_set = fn_retval,
+      history_get = fn_retval;
+    
+    // Start the polling loop.
+    self.start = function() {
+      timeout_id || poll();
+    };
+    
+    // Stop the polling loop.
+    self.stop = function() {
+      timeout_id && clearTimeout( timeout_id );
+      timeout_id = undefined;
+    };
+    
+    // This polling loop checks every $.fn.hashchange.delay milliseconds to see
+    // if location.hash has changed, and triggers the 'hashchange' event on
+    // window when necessary.
+    function poll() {
+      var hash = get_fragment(),
+        history_hash = history_get( last_hash );
+      
+      if ( hash !== last_hash ) {
+        history_set( last_hash = hash, history_hash );
+        
+        $(window).trigger( str_hashchange );
+        
+      } else if ( history_hash !== last_hash ) {
+        location.href = location.href.replace( /#.*/, '' ) + history_hash;
+      }
+      
+      timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay );
+    };
+    
+    // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+    // vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv
+    // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+    window.attachEvent && !window.addEventListener && !supports_onhashchange && (function() {
+      // Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8
+      // when running in "IE7 compatibility" mode.
+      
+      var iframe,
+        iframe_src;
+      
+      // When the event is bound and polling starts in IE 6/7, create a hidden
+      // Iframe for history handling.
+      self.start = function() {
+        if ( !iframe ) {
+          iframe_src = $.fn[ str_hashchange ].src;
+          iframe_src = iframe_src && iframe_src + get_fragment();
+          
+          // Create hidden Iframe. Attempt to make Iframe as hidden as possible
+          // by using techniques from http://www.paciellogroup.com/blog/?p=604.
+          iframe = $('<iframe tabindex="-1" title="empty"/>').hide()
+            
+            // When Iframe has completely loaded, initialize the history and
+            // start polling.
+            .one( 'load', function() {
+              iframe_src || history_set( get_fragment() );
+              poll();
+            })
+            
+            // Load Iframe src if specified, otherwise nothing.
+            .attr( 'src', iframe_src || 'javascript:0' )
+            
+            // Append Iframe after the end of the body to prevent unnecessary
+            // initial page scrolling (yes, this works).
+            .insertAfter( 'body' )[0].contentWindow;
+          
+          // Whenever `document.title` changes, update the Iframe's title to
+          // prettify the back/next history menu entries. Since IE sometimes
+          // errors with "Unspecified error" the very first time this is set
+          // (yes, very useful) wrap this with a try/catch block.
+          doc.onpropertychange = function() {
+            try {
+              if ( event.propertyName === 'title' ) {
+                iframe.document.title = doc.title;
+              }
+            } catch(e) {}
+          };
+          
+        }
+      };
+      
+      // Override the "stop" method since an IE6/7 Iframe was created. Even
+      // if there are no longer any bound event handlers, the polling loop
+      // is still necessary for back/next to work at all!
+      self.stop = fn_retval;
+      
+      // Get history by looking at the hidden Iframe's location.hash.
+      history_get = function() {
+        return get_fragment( iframe.location.href );
+      };
+      
+      // Set a new history item by opening and then closing the Iframe
+      // document, *then* setting its location.hash. If document.domain has
+      // been set, update that as well.
+      history_set = function( hash, history_hash ) {
+        var iframe_doc = iframe.document,
+          domain = $.fn[ str_hashchange ].domain;
+        
+        if ( hash !== history_hash ) {
+          // Update Iframe with any initial `document.title` that might be set.
+          iframe_doc.title = doc.title;
+          
+          // Opening the Iframe's document after it has been closed is what
+          // actually adds a history entry.
+          iframe_doc.open();
+          
+          // Set document.domain for the Iframe document as well, if necessary.
+          domain && iframe_doc.write( '<script>document.domain="' + domain + '"</script>' );
+          
+          iframe_doc.close();
+          
+          // Update the Iframe's hash, for great justice.
+          iframe.location.hash = hash;
+        }
+      };
+      
+    })();
+    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    // ^^^^^^^^^^^^^^^^^^^ REMOVE IF NOT SUPPORTING IE6/7/8 ^^^^^^^^^^^^^^^^^^^
+    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    
+    return self;
+  })();
+  
+})(jQuery,this);
+
+(function( $, undefined ) {
+
+	/*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */
+	window.matchMedia = window.matchMedia || (function( doc, undefined ) {
+
+		
+
+		var bool,
+			docElem = doc.documentElement,
+			refNode = docElem.firstElementChild || docElem.firstChild,
+			// fakeBody required for <FF4 when executed in <head>
+			fakeBody = doc.createElement( "body" ),
+			div = doc.createElement( "div" );
+
+		div.id = "mq-test-1";
+		div.style.cssText = "position:absolute;top:-100em";
+		fakeBody.style.background = "none";
+		fakeBody.appendChild(div);
+
+		return function(q){
+
+			div.innerHTML = "&shy;<style media=\"" + q + "\"> #mq-test-1 { width: 42px; }</style>";
+
+			docElem.insertBefore( fakeBody, refNode );
+			bool = div.offsetWidth === 42;
+			docElem.removeChild( fakeBody );
+
+			return {
+				matches: bool,
+				media: q
+			};
+
+		};
+
+	}( document ));
+
+	// $.mobile.media uses matchMedia to return a boolean.
+	$.mobile.media = function( q ) {
+		return window.matchMedia( q ).matches;
+	};
+
+})(jQuery);
+
+	(function( $, undefined ) {
+		var support = {
+			touch: "ontouchend" in document
+		};
+
+		$.mobile.support = $.mobile.support || {};
+		$.extend( $.support, support );
+		$.extend( $.mobile.support, support );
+	}( jQuery ));
+
+	(function( $, undefined ) {
+		$.extend( $.support, {
+			orientation: "orientation" in window && "onorientationchange" in window
+		});
+	}( jQuery ));
+
+(function( $, undefined ) {
+
+// thx Modernizr
+function propExists( prop ) {
+	var uc_prop = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ),
+		props = ( prop + " " + vendors.join( uc_prop + " " ) + uc_prop ).split( " " );
+
+	for ( var v in props ) {
+		if ( fbCSS[ props[ v ] ] !== undefined ) {
+			return true;
+		}
+	}
+}
+
+var fakeBody = $( "<body>" ).prependTo( "html" ),
+	fbCSS = fakeBody[ 0 ].style,
+	vendors = [ "Webkit", "Moz", "O" ],
+	webos = "palmGetResource" in window, //only used to rule out scrollTop
+	opera = window.opera,
+	operamini = window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]",
+	bb = window.blackberry && !propExists( "-webkit-transform" ); //only used to rule out box shadow, as it's filled opaque on BB 5 and lower
+
+
+function validStyle( prop, value, check_vend ) {
+	var div = document.createElement( 'div' ),
+		uc = function( txt ) {
+			return txt.charAt( 0 ).toUpperCase() + txt.substr( 1 );
+		},
+		vend_pref = function( vend ) {
+			if( vend === "" ) {
+				return "";
+			} else {
+				return  "-" + vend.charAt( 0 ).toLowerCase() + vend.substr( 1 ) + "-";
+			}
+		},
+		check_style = function( vend ) {
+			var vend_prop = vend_pref( vend ) + prop + ": " + value + ";",
+				uc_vend = uc( vend ),
+				propStyle = uc_vend + ( uc_vend === "" ? prop : uc( prop ) );
+
+			div.setAttribute( "style", vend_prop );
+
+			if ( !!div.style[ propStyle ] ) {
+				ret = true;
+			}
+		},
+		check_vends = check_vend ? check_vend : vendors,
+		ret;
+
+	for( var i = 0; i < check_vends.length; i++ ) {
+		check_style( check_vends[i] );
+	}
+	return !!ret;
+}
+
+function transform3dTest() {
+	var mqProp = "transform-3d",
+		// Because the `translate3d` test below throws false positives in Android:
+		ret = $.mobile.media( "(-" + vendors.join( "-" + mqProp + "),(-" ) + "-" + mqProp + "),(" + mqProp + ")" );
+
+	if( ret ) {
+		return !!ret;
+	}
+
+	var el = document.createElement( "div" ),
+		transforms = {
+			// We’re omitting Opera for the time being; MS uses unprefixed.
+			'MozTransform':'-moz-transform',
+			'transform':'transform'
+		};
+
+	fakeBody.append( el );
+
+	for ( var t in transforms ) {
+		if( el.style[ t ] !== undefined ){
+			el.style[ t ] = 'translate3d( 100px, 1px, 1px )';
+			ret = window.getComputedStyle( el ).getPropertyValue( transforms[ t ] );
+		}
+	}
+	return ( !!ret && ret !== "none" );
+}
+
+// Test for dynamic-updating base tag support ( allows us to avoid href,src attr rewriting )
+function baseTagTest() {
+	var fauxBase = location.protocol + "//" + location.host + location.pathname + "ui-dir/",
+		base = $( "head base" ),
+		fauxEle = null,
+		href = "",
+		link, rebase;
+
+	if ( !base.length ) {
+		base = fauxEle = $( "<base>", { "href": fauxBase }).appendTo( "head" );
+	} else {
+		href = base.attr( "href" );
+	}
+
+	link = $( "<a href='testurl' />" ).prependTo( fakeBody );
+	rebase = link[ 0 ].href;
+	base[ 0 ].href = href || location.pathname;
+
+	if ( fauxEle ) {
+		fauxEle.remove();
+	}
+	return rebase.indexOf( fauxBase ) === 0;
+}
+
+// Thanks Modernizr
+function cssPointerEventsTest() {
+	var element = document.createElement( 'x' ),
+		documentElement = document.documentElement,
+		getComputedStyle = window.getComputedStyle,
+		supports;
+
+	if ( !( 'pointerEvents' in element.style ) ) {
+		return false;
+	}
+
+	element.style.pointerEvents = 'auto';
+	element.style.pointerEvents = 'x';
+	documentElement.appendChild( element );
+	supports = getComputedStyle &&
+	getComputedStyle( element, '' ).pointerEvents === 'auto';
+	documentElement.removeChild( element );
+	return !!supports;
+}
+
+function boundingRect() {
+	var div = document.createElement( "div" );
+	return typeof div.getBoundingClientRect !== "undefined";
+}
+
+// non-UA-based IE version check by James Padolsey, modified by jdalton - from http://gist.github.com/527683
+// allows for inclusion of IE 6+, including Windows Mobile 7
+$.extend( $.mobile, { browser: {} } );
+$.mobile.browser.oldIE = (function() {
+	var v = 3,
+		div = document.createElement( "div" ),
+		a = div.all || [];
+
+	do {
+		div.innerHTML = "<!--[if gt IE " + ( ++v ) + "]><br><![endif]-->";
+	} while( a[0] );
+
+	return v > 4 ? v : !v;
+})();
+
+function fixedPosition() {
+	var w = window,
+		ua = navigator.userAgent,
+		platform = navigator.platform,
+		// Rendering engine is Webkit, and capture major version
+		wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ),
+		wkversion = !!wkmatch && wkmatch[ 1 ],
+		ffmatch = ua.match( /Fennec\/([0-9]+)/ ),
+		ffversion = !!ffmatch && ffmatch[ 1 ],
+		operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ ),
+		omversion = !!operammobilematch && operammobilematch[ 1 ];
+
+	if(
+		// iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5)
+		( ( platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1  || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534 ) ||
+		// Opera Mini
+		( w.operamini && ({}).toString.call( w.operamini ) === "[object OperaMini]" ) ||
+		( operammobilematch && omversion < 7458 )	||
+		//Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2)
+		( ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533 ) ||
+		// Firefox Mobile before 6.0 -
+		( ffversion && ffversion < 6 ) ||
+		// WebOS less than 3
+		( "palmGetResource" in window && wkversion && wkversion < 534 )	||
+		// MeeGo
+		( ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1 ) ) {
+		return false;
+	}
+
+	return true;
+}
+
+$.extend( $.support, {
+	cssTransitions: "WebKitTransitionEvent" in window ||
+		validStyle( 'transition', 'height 100ms linear', [ "Webkit", "Moz", "" ] ) &&
+		!$.mobile.browser.oldIE && !opera,
+
+	// Note, Chrome for iOS has an extremely quirky implementation of popstate.
+	// We've chosen to take the shortest path to a bug fix here for issue #5426
+	// See the following link for information about the regex chosen
+	// https://developers.google.com/chrome/mobile/docs/user-agent#chrome_for_ios_user-agent
+	pushState: "pushState" in history &&
+		"replaceState" in history &&
+		// When running inside a FF iframe, calling replaceState causes an error
+		!( window.navigator.userAgent.indexOf( "Firefox" ) >= 0 && window.top !== window ) &&
+		( window.navigator.userAgent.search(/CriOS/) === -1 ),
+
+	mediaquery: $.mobile.media( "only all" ),
+	cssPseudoElement: !!propExists( "content" ),
+	touchOverflow: !!propExists( "overflowScrolling" ),
+	cssTransform3d: transform3dTest(),
+	boxShadow: !!propExists( "boxShadow" ) && !bb,
+	fixedPosition: fixedPosition(),
+	scrollTop: ("pageXOffset" in window ||
+		"scrollTop" in document.documentElement ||
+		"scrollTop" in fakeBody[ 0 ]) && !webos && !operamini,
+
+	dynamicBaseTag: baseTagTest(),
+	cssPointerEvents: cssPointerEventsTest(),
+	boundingRect: boundingRect()
+});
+
+fakeBody.remove();
+
+
+// $.mobile.ajaxBlacklist is used to override ajaxEnabled on platforms that have known conflicts with hash history updates (BB5, Symbian)
+// or that generally work better browsing in regular http for full page refreshes (Opera Mini)
+// Note: This detection below is used as a last resort.
+// We recommend only using these detection methods when all other more reliable/forward-looking approaches are not possible
+var nokiaLTE7_3 = (function() {
+
+	var ua = window.navigator.userAgent;
+
+	//The following is an attempt to match Nokia browsers that are running Symbian/s60, with webkit, version 7.3 or older
+	return ua.indexOf( "Nokia" ) > -1 &&
+			( ua.indexOf( "Symbian/3" ) > -1 || ua.indexOf( "Series60/5" ) > -1 ) &&
+			ua.indexOf( "AppleWebKit" ) > -1 &&
+			ua.match( /(BrowserNG|NokiaBrowser)\/7\.[0-3]/ );
+})();
+
+// Support conditions that must be met in order to proceed
+// default enhanced qualifications are media query support OR IE 7+
+
+$.mobile.gradeA = function() {
+	return ( $.support.mediaquery || $.mobile.browser.oldIE && $.mobile.browser.oldIE >= 7 ) && ( $.support.boundingRect || $.fn.jquery.match(/1\.[0-7+]\.[0-9+]?/) !== null );
+};
+
+$.mobile.ajaxBlacklist =
+			// BlackBerry browsers, pre-webkit
+			window.blackberry && !window.WebKitPoint ||
+			// Opera Mini
+			operamini ||
+			// Symbian webkits pre 7.3
+			nokiaLTE7_3;
+
+// Lastly, this workaround is the only way we've found so far to get pre 7.3 Symbian webkit devices
+// to render the stylesheets when they're referenced before this script, as we'd recommend doing.
+// This simply reappends the CSS in place, which for some reason makes it apply
+if ( nokiaLTE7_3 ) {
+	$(function() {
+		$( "head link[rel='stylesheet']" ).attr( "rel", "alternate stylesheet" ).attr( "rel", "stylesheet" );
+	});
+}
+
+// For ruling out shadows via css
+if ( !$.support.boxShadow ) {
+	$( "html" ).addClass( "ui-mobile-nosupport-boxshadow" );
+}
+
+})( jQuery );
+
+
+(function( $, undefined ) {
+	var $win = $.mobile.window, self, history;
+
+	$.event.special.navigate = self = {
+		bound: false,
+
+		pushStateEnabled: true,
+
+		originalEventName: undefined,
+
+		// If pushstate support is present and push state support is defined to
+		// be true on the mobile namespace.
+		isPushStateEnabled: function() {
+			return $.support.pushState &&
+				$.mobile.pushStateEnabled === true &&
+				this.isHashChangeEnabled();
+		},
+
+		// !! assumes mobile namespace is present
+		isHashChangeEnabled: function() {
+			return $.mobile.hashListeningEnabled === true;
+		},
+
+		// TODO a lot of duplication between popstate and hashchange
+		popstate: function( event ) {
+			var newEvent = new $.Event( "navigate" ),
+				beforeNavigate = new $.Event( "beforenavigate" ),
+				state = event.originalEvent.state || {},
+				href = location.href;
+
+			$win.trigger( beforeNavigate );
+
+			if( beforeNavigate.isDefaultPrevented() ){
+				return;
+			}
+
+			if( event.historyState ){
+				$.extend(state, event.historyState);
+			}
+
+			// Make sure the original event is tracked for the end
+			// user to inspect incase they want to do something special
+			newEvent.originalEvent = event;
+
+			// NOTE we let the current stack unwind because any assignment to
+			//      location.hash will stop the world and run this event handler. By
+			//      doing this we create a similar behavior to hashchange on hash
+			//      assignment
+			setTimeout(function() {
+				$win.trigger( newEvent, {
+					state: state
+				});
+			}, 0);
+		},
+
+		hashchange: function( event, data ) {
+			var newEvent = new $.Event( "navigate" ),
+				beforeNavigate = new $.Event( "beforenavigate" );
+
+			$win.trigger( beforeNavigate );
+
+			if( beforeNavigate.isDefaultPrevented() ){
+				return;
+			}
+
+			// Make sure the original event is tracked for the end
+			// user to inspect incase they want to do something special
+			newEvent.originalEvent = event;
+
+			// Trigger the hashchange with state provided by the user
+			// that altered the hash
+			$win.trigger( newEvent, {
+				// Users that want to fully normalize the two events
+				// will need to do history management down the stack and
+				// add the state to the event before this binding is fired
+				// TODO consider allowing for the explicit addition of callbacks
+				//      to be fired before this value is set to avoid event timing issues
+				state: event.hashchangeState || {}
+			});
+		},
+
+		// TODO We really only want to set this up once
+		//      but I'm not clear if there's a beter way to achieve
+		//      this with the jQuery special event structure
+		setup: function( data, namespaces ) {
+			if( self.bound ) {
+				return;
+			}
+
+			self.bound = true;
+
+			if( self.isPushStateEnabled() ) {
+				self.originalEventName = "popstate";
+				$win.bind( "popstate.navigate", self.popstate );
+			} else if ( self.isHashChangeEnabled() ){
+				self.originalEventName = "hashchange";
+				$win.bind( "hashchange.navigate", self.hashchange );
+			}
+		}
+	};
+})( jQuery );
+
+
+
+(function( $, undefined ) {
+		var path, documentBase, $base, dialogHashKey = "&ui-state=dialog";
+
+		$.mobile.path = path = {
+			uiStateKey: "&ui-state",
+
+			// This scary looking regular expression parses an absolute URL or its relative
+			// variants (protocol, site, document, query, and hash), into the various
+			// components (protocol, host, path, query, fragment, etc that make up the
+			// URL as well as some other commonly used sub-parts. When used with RegExp.exec()
+			// or String.match, it parses the URL into a results array that looks like this:
+			//
+			//     [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content
+			//     [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
+			//     [2]: http://jblas:password@mycompany.com:8080/mail/inbox
+			//     [3]: http://jblas:password@mycompany.com:8080
+			//     [4]: http:
+			//     [5]: //
+			//     [6]: jblas:password@mycompany.com:8080
+			//     [7]: jblas:password
+			//     [8]: jblas
+			//     [9]: password
+			//    [10]: mycompany.com:8080
+			//    [11]: mycompany.com
+			//    [12]: 8080
+			//    [13]: /mail/inbox
+			//    [14]: /mail/
+			//    [15]: inbox
+			//    [16]: ?msg=1234&type=unread
+			//    [17]: #msg-content
+			//
+			urlParseRE: /^\s*(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
+
+			// Abstraction to address xss (Issue #4787) by removing the authority in
+			// browsers that auto	decode it. All references to location.href should be
+			// replaced with a call to this method so that it can be dealt with properly here
+			getLocation: function( url ) {
+				var uri = url ? this.parseUrl( url ) : location,
+					hash = this.parseUrl( url || location.href ).hash;
+
+				// mimic the browser with an empty string when the hash is empty
+				hash = hash === "#" ? "" : hash;
+
+				// Make sure to parse the url or the location object for the hash because using location.hash
+				// is autodecoded in firefox, the rest of the url should be from the object (location unless
+				// we're testing) to avoid the inclusion of the authority
+				return uri.protocol + "//" + uri.host + uri.pathname + uri.search + hash;
+			},
+
+			parseLocation: function() {
+				return this.parseUrl( this.getLocation() );
+			},
+
+			//Parse a URL into a structure that allows easy access to
+			//all of the URL components by name.
+			parseUrl: function( url ) {
+				// If we're passed an object, we'll assume that it is
+				// a parsed url object and just return it back to the caller.
+				if ( $.type( url ) === "object" ) {
+					return url;
+				}
+
+				var matches = path.urlParseRE.exec( url || "" ) || [];
+
+					// Create an object that allows the caller to access the sub-matches
+					// by name. Note that IE returns an empty string instead of undefined,
+					// like all other browsers do, so we normalize everything so its consistent
+					// no matter what browser we're running on.
+					return {
+						href:         matches[  0 ] || "",
+						hrefNoHash:   matches[  1 ] || "",
+						hrefNoSearch: matches[  2 ] || "",
+						domain:       matches[  3 ] || "",
+						protocol:     matches[  4 ] || "",
+						doubleSlash:  matches[  5 ] || "",
+						authority:    matches[  6 ] || "",
+						username:     matches[  8 ] || "",
+						password:     matches[  9 ] || "",
+						host:         matches[ 10 ] || "",
+						hostname:     matches[ 11 ] || "",
+						port:         matches[ 12 ] || "",
+						pathname:     matches[ 13 ] || "",
+						directory:    matches[ 14 ] || "",
+						filename:     matches[ 15 ] || "",
+						search:       matches[ 16 ] || "",
+						hash:         matches[ 17 ] || ""
+					};
+			},
+
+			//Turn relPath into an asbolute path. absPath is
+			//an optional absolute path which describes what
+			//relPath is relative to.
+			makePathAbsolute: function( relPath, absPath ) {
+				if ( relPath && relPath.charAt( 0 ) === "/" ) {
+					return relPath;
+				}
+
+				relPath = relPath || "";
+				absPath = absPath ? absPath.replace( /^\/|(\/[^\/]*|[^\/]+)$/g, "" ) : "";
+
+				var absStack = absPath ? absPath.split( "/" ) : [],
+					relStack = relPath.split( "/" );
+				for ( var i = 0; i < relStack.length; i++ ) {
+					var d = relStack[ i ];
+					switch ( d ) {
+						case ".":
+							break;
+						case "..":
+							if ( absStack.length ) {
+								absStack.pop();
+							}
+							break;
+						default:
+							absStack.push( d );
+							break;
+					}
+				}
+				return "/" + absStack.join( "/" );
+			},
+
+			//Returns true if both urls have the same domain.
+			isSameDomain: function( absUrl1, absUrl2 ) {
+				return path.parseUrl( absUrl1 ).domain === path.parseUrl( absUrl2 ).domain;
+			},
+
+			//Returns true for any relative variant.
+			isRelativeUrl: function( url ) {
+				// All relative Url variants have one thing in common, no protocol.
+				return path.parseUrl( url ).protocol === "";
+			},
+
+			//Returns true for an absolute url.
+			isAbsoluteUrl: function( url ) {
+				return path.parseUrl( url ).protocol !== "";
+			},
+
+			//Turn the specified realtive URL into an absolute one. This function
+			//can handle all relative variants (protocol, site, document, query, fragment).
+			makeUrlAbsolute: function( relUrl, absUrl ) {
+				if ( !path.isRelativeUrl( relUrl ) ) {
+					return relUrl;
+				}
+
+				if ( absUrl === undefined ) {
+					absUrl = this.documentBase;
+				}
+
+				var relObj = path.parseUrl( relUrl ),
+					absObj = path.parseUrl( absUrl ),
+					protocol = relObj.protocol || absObj.protocol,
+					doubleSlash = relObj.protocol ? relObj.doubleSlash : ( relObj.doubleSlash || absObj.doubleSlash ),
+					authority = relObj.authority || absObj.authority,
+					hasPath = relObj.pathname !== "",
+					pathname = path.makePathAbsolute( relObj.pathname || absObj.filename, absObj.pathname ),
+					search = relObj.search || ( !hasPath && absObj.search ) || "",
+					hash = relObj.hash;
+
+				return protocol + doubleSlash + authority + pathname + search + hash;
+			},
+
+			//Add search (aka query) params to the specified url.
+			addSearchParams: function( url, params ) {
+				var u = path.parseUrl( url ),
+					p = ( typeof params === "object" ) ? $.param( params ) : params,
+					s = u.search || "?";
+				return u.hrefNoSearch + s + ( s.charAt( s.length - 1 ) !== "?" ? "&" : "" ) + p + ( u.hash || "" );
+			},
+
+			convertUrlToDataUrl: function( absUrl ) {
+				var u = path.parseUrl( absUrl );
+				if ( path.isEmbeddedPage( u ) ) {
+					// For embedded pages, remove the dialog hash key as in getFilePath(),
+					// and remove otherwise the Data Url won't match the id of the embedded Page.
+					return u.hash
+						.split( dialogHashKey )[0]
+						.replace( /^#/, "" )
+						.replace( /\?.*$/, "" );
+				} else if ( path.isSameDomain( u, this.documentBase ) ) {
+					return u.hrefNoHash.replace( this.documentBase.domain, "" ).split( dialogHashKey )[0];
+				}
+
+				return window.decodeURIComponent(absUrl);
+			},
+
+			//get path from current hash, or from a file path
+			get: function( newPath ) {
+				if ( newPath === undefined ) {
+					newPath = path.parseLocation().hash;
+				}
+				return path.stripHash( newPath ).replace( /[^\/]*\.[^\/*]+$/, '' );
+			},
+
+			//set location hash to path
+			set: function( path ) {
+				location.hash = path;
+			},
+
+			//test if a given url (string) is a path
+			//NOTE might be exceptionally naive
+			isPath: function( url ) {
+				return ( /\// ).test( url );
+			},
+
+			//return a url path with the window's location protocol/hostname/pathname removed
+			clean: function( url ) {
+				return url.replace( this.documentBase.domain, "" );
+			},
+
+			//just return the url without an initial #
+			stripHash: function( url ) {
+				return url.replace( /^#/, "" );
+			},
+
+			stripQueryParams: function( url ) {
+				return url.replace( /\?.*$/, "" );
+			},
+
+			//remove the preceding hash, any query params, and dialog notations
+			cleanHash: function( hash ) {
+				return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) );
+			},
+
+			isHashValid: function( hash ) {
+				return ( /^#[^#]+$/ ).test( hash );
+			},
+
+			//check whether a url is referencing the same domain, or an external domain or different protocol
+			//could be mailto, etc
+			isExternal: function( url ) {
+				var u = path.parseUrl( url );
+				return u.protocol && u.domain !== this.documentUrl.domain ? true : false;
+			},
+
+			hasProtocol: function( url ) {
+				return ( /^(:?\w+:)/ ).test( url );
+			},
+
+			isEmbeddedPage: function( url ) {
+				var u = path.parseUrl( url );
+
+				//if the path is absolute, then we need to compare the url against
+				//both the this.documentUrl and the documentBase. The main reason for this
+				//is that links embedded within external documents will refer to the
+				//application document, whereas links embedded within the application
+				//document will be resolved against the document base.
+				if ( u.protocol !== "" ) {
+					return ( !this.isPath(u.hash) && u.hash && ( u.hrefNoHash === this.documentUrl.hrefNoHash || ( this.documentBaseDiffers && u.hrefNoHash === this.documentBase.hrefNoHash ) ) );
+				}
+				return ( /^#/ ).test( u.href );
+			},
+
+			squash: function( url, resolutionUrl ) {
+				var state, href, cleanedUrl, search, stateIndex,
+					isPath = this.isPath( url ),
+					uri = this.parseUrl( url ),
+					preservedHash = uri.hash,
+					uiState = "";
+
+				// produce a url against which we can resole the provided path
+				resolutionUrl = resolutionUrl || (path.isPath(url) ? path.getLocation() : path.getDocumentUrl());
+
+				// If the url is anything but a simple string, remove any preceding hash
+				// eg #foo/bar -> foo/bar
+				//    #foo -> #foo
+				cleanedUrl = isPath ? path.stripHash( url ) : url;
+
+				// If the url is a full url with a hash check if the parsed hash is a path
+				// if it is, strip the #, and use it otherwise continue without change
+				cleanedUrl = path.isPath( uri.hash ) ? path.stripHash( uri.hash ) : cleanedUrl;
+
+				// Split the UI State keys off the href
+				stateIndex = cleanedUrl.indexOf( this.uiStateKey );
+
+				// store the ui state keys for use
+				if( stateIndex > -1 ){
+					uiState = cleanedUrl.slice( stateIndex );
+					cleanedUrl = cleanedUrl.slice( 0, stateIndex );
+				}
+
+				// make the cleanedUrl absolute relative to the resolution url
+				href = path.makeUrlAbsolute( cleanedUrl, resolutionUrl );
+
+				// grab the search from the resolved url since parsing from
+				// the passed url may not yield the correct result
+				search = this.parseUrl( href ).search;
+
+				// TODO all this crap is terrible, clean it up
+				if ( isPath ) {
+					// reject the hash if it's a path or it's just a dialog key
+					if( path.isPath( preservedHash ) || preservedHash.replace("#", "").indexOf( this.uiStateKey ) === 0) {
+						preservedHash = "";
+					}
+
+					// Append the UI State keys where it exists and it's been removed
+					// from the url
+					if( uiState && preservedHash.indexOf( this.uiStateKey ) === -1){
+						preservedHash += uiState;
+					}
+
+					// make sure that pound is on the front of the hash
+					if( preservedHash.indexOf( "#" ) === -1 && preservedHash !== "" ){
+						preservedHash = "#" + preservedHash;
+					}
+
+					// reconstruct each of the pieces with the new search string and hash
+					href = path.parseUrl( href );
+					href = href.protocol + "//" + href.host + href.pathname + search + preservedHash;
+				} else {
+					href += href.indexOf( "#" ) > -1 ? uiState : "#" + uiState;
+				}
+
+				return href;
+			},
+
+			isPreservableHash: function( hash ) {
+				return hash.replace( "#", "" ).indexOf( this.uiStateKey ) === 0;
+			}
+		};
+
+		path.documentUrl = path.parseLocation();
+
+		$base = $( "head" ).find( "base" );
+
+		path.documentBase = $base.length ?
+			path.parseUrl( path.makeUrlAbsolute( $base.attr( "href" ), path.documentUrl.href ) ) :
+			path.documentUrl;
+
+		path.documentBaseDiffers = (path.documentUrl.hrefNoHash !== path.documentBase.hrefNoHash);
+
+		//return the original document url
+		path.getDocumentUrl = function( asParsedObject ) {
+			return asParsedObject ? $.extend( {}, path.documentUrl ) : path.documentUrl.href;
+		};
+
+		//return the original document base url
+		path.getDocumentBase = function( asParsedObject ) {
+			return asParsedObject ? $.extend( {}, path.documentBase ) : path.documentBase.href;
+		};
+})( jQuery );
+
+
+
+(function( $, undefined ) {
+	var path = $.mobile.path;
+
+	$.mobile.History = function( stack, index ) {
+		this.stack = stack || [];
+		this.activeIndex = index || 0;
+	};
+
+	$.extend($.mobile.History.prototype, {
+		getActive: function() {
+			return this.stack[ this.activeIndex ];
+		},
+
+		getLast: function() {
+			return this.stack[ this.previousIndex ];
+		},
+
+		getNext: function() {
+			return this.stack[ this.activeIndex + 1 ];
+		},
+
+		getPrev: function() {
+			return this.stack[ this.activeIndex - 1 ];
+		},
+
+		// addNew is used whenever a new page is added
+		add: function( url, data ){
+			data = data || {};
+
+			//if there's forward history, wipe it
+			if ( this.getNext() ) {
+				this.clearForward();
+			}
+
+			// if the hash is included in the data make sure the shape
+			// is consistent for comparison
+			if( data.hash && data.hash.indexOf( "#" ) === -1) {
+				data.hash = "#" + data.hash;
+			}
+
+			data.url = url;
+			this.stack.push( data );
+			this.activeIndex = this.stack.length - 1;
+		},
+
+		//wipe urls ahead of active index
+		clearForward: function() {
+			this.stack = this.stack.slice( 0, this.activeIndex + 1 );
+		},
+
+		find: function( url, stack, earlyReturn ) {
+			stack = stack || this.stack;
+
+			var entry, i, length = stack.length, index;
+
+			for ( i = 0; i < length; i++ ) {
+				entry = stack[i];
+
+				if ( decodeURIComponent(url) === decodeURIComponent(entry.url) ||
+					decodeURIComponent(url) === decodeURIComponent(entry.hash) ) {
+					index = i;
+
+					if( earlyReturn ) {
+						return index;
+					}
+				}
+			}
+
+			return index;
+		},
+
+		closest: function( url ) {
+			var closest, a = this.activeIndex;
+
+			// First, take the slice of the history stack before the current index and search
+			// for a url match. If one is found, we'll avoid avoid looking through forward history
+			// NOTE the preference for backward history movement is driven by the fact that
+			//      most mobile browsers only have a dedicated back button, and users rarely use
+			//      the forward button in desktop browser anyhow
+			closest = this.find( url, this.stack.slice(0, a) );
+
+			// If nothing was found in backward history check forward. The `true`
+			// value passed as the third parameter causes the find method to break
+			// on the first match in the forward history slice. The starting index
+			// of the slice must then be added to the result to get the element index
+			// in the original history stack :( :(
+			//
+			// TODO this is hyper confusing and should be cleaned up (ugh so bad)
+			if( closest === undefined ) {
+				closest = this.find( url, this.stack.slice(a), true );
+				closest = closest === undefined ? closest : closest + a;
+			}
+
+			return closest;
+		},
+
+		direct: function( opts ) {
+			var newActiveIndex = this.closest( opts.url ), a = this.activeIndex;
+
+			// save new page index, null check to prevent falsey 0 result
+			// record the previous index for reference
+			if( newActiveIndex !== undefined ) {
+				this.activeIndex = newActiveIndex;
+				this.previousIndex = a;
+			}
+
+			// invoke callbacks where appropriate
+			//
+			// TODO this is also convoluted and confusing
+			if ( newActiveIndex < a ) {
+				( opts.present || opts.back || $.noop )( this.getActive(), 'back' );
+			} else if ( newActiveIndex > a ) {
+				( opts.present || opts.forward || $.noop )( this.getActive(), 'forward' );
+			} else if ( newActiveIndex === undefined && opts.missing ){
+				opts.missing( this.getActive() );
+			}
+		}
+	});
+})( jQuery );
+
+
+(function( $, undefined ) {
+	var path = $.mobile.path,
+		initialHref = location.href;
+
+	$.mobile.Navigator = function( history ) {
+		this.history = history;
+		this.ignoreInitialHashChange = true;
+
+		$.mobile.window.bind({
+			"popstate.history": $.proxy( this.popstate, this ),
+			"hashchange.history": $.proxy( this.hashchange, this )
+		});
+	};
+
+	$.extend($.mobile.Navigator.prototype, {
+		squash: function( url, data ) {
+			var state, href, hash = path.isPath(url) ? path.stripHash(url) : url;
+
+			href = path.squash( url );
+
+			// make sure to provide this information when it isn't explicitly set in the
+			// data object that was passed to the squash method
+			state = $.extend({
+				hash: hash,
+				url: href
+			}, data);
+
+			// replace the current url with the new href and store the state
+			// Note that in some cases we might be replacing an url with the
+			// same url. We do this anyways because we need to make sure that
+			// all of our history entries have a state object associated with
+			// them. This allows us to work around the case where $.mobile.back()
+			// is called to transition from an external page to an embedded page.
+			// In that particular case, a hashchange event is *NOT* generated by the browser.
+			// Ensuring each history entry has a state object means that onPopState()
+			// will always trigger our hashchange callback even when a hashchange event
+			// is not fired.
+			window.history.replaceState( state, state.title || document.title, href );
+
+			return state;
+		},
+
+		hash: function( url, href ) {
+			var parsed, loc, hash;
+
+			// Grab the hash for recording. If the passed url is a path
+			// we used the parsed version of the squashed url to reconstruct,
+			// otherwise we assume it's a hash and store it directly
+			parsed = path.parseUrl( url );
+			loc = path.parseLocation();
+
+			if( loc.pathname + loc.search === parsed.pathname + parsed.search ) {
+				// If the pathname and search of the passed url is identical to the current loc
+				// then we must use the hash. Otherwise there will be no event
+				// eg, url = "/foo/bar?baz#bang", location.href = "http://example.com/foo/bar?baz"
+				hash = parsed.hash ? parsed.hash : parsed.pathname + parsed.search;
+			} else if ( path.isPath(url) ) {
+				var resolved = path.parseUrl( href );
+				// If the passed url is a path, make it domain relative and remove any trailing hash
+				hash = resolved.pathname + resolved.search + (path.isPreservableHash( resolved.hash )? resolved.hash.replace( "#", "" ) : "");
+			} else {
+				hash = url;
+			}
+
+			return hash;
+		},
+
+		// TODO reconsider name
+		go: function( url, data, noEvents ) {
+			var state, href, hash, popstateEvent,
+				isPopStateEvent = $.event.special.navigate.isPushStateEnabled();
+
+			// Get the url as it would look squashed on to the current resolution url
+			href = path.squash( url );
+
+			// sort out what the hash sould be from the url
+			hash = this.hash( url, href );
+
+			// Here we prevent the next hash change or popstate event from doing any
+			// history management. In the case of hashchange we don't swallow it
+			// if there will be no hashchange fired (since that won't reset the value)
+			// and will swallow the following hashchange
+			if( noEvents && hash !== path.stripHash(path.parseLocation().hash) ) {
+				this.preventNextHashChange = noEvents;
+			}
+
+			// IMPORTANT in the case where popstate is supported the event will be triggered
+			//      directly, stopping further execution - ie, interupting the flow of this
+			//      method call to fire bindings at this expression. Below the navigate method
+			//      there is a binding to catch this event and stop its propagation.
+			//
+			//      We then trigger a new popstate event on the window with a null state
+			//      so that the navigate events can conclude their work properly
+			//
+			// if the url is a path we want to preserve the query params that are available on
+			// the current url.
+			this.preventHashAssignPopState = true;
+			window.location.hash = hash;
+
+			// If popstate is enabled and the browser triggers `popstate` events when the hash
+			// is set (this often happens immediately in browsers like Chrome), then the
+			// this flag will be set to false already. If it's a browser that does not trigger
+			// a `popstate` on hash assignement or `replaceState` then we need avoid the branch
+			// that swallows the event created by the popstate generated by the hash assignment
+			// At the time of this writing this happens with Opera 12 and some version of IE
+			this.preventHashAssignPopState = false;
+
+			state = $.extend({
+				url: href,
+				hash: hash,
+				title: document.title
+			}, data);
+
+			if( isPopStateEvent ) {
+				popstateEvent = new $.Event( "popstate" );
+				popstateEvent.originalEvent = {
+					type: "popstate",
+					state: null
+				};
+
+				this.squash( url, state );
+
+				// Trigger a new faux popstate event to replace the one that we
+				// caught that was triggered by the hash setting above.
+				if( !noEvents ) {
+					this.ignorePopState = true;
+					$.mobile.window.trigger( popstateEvent );
+				}
+			}
+
+			// record the history entry so that the information can be included
+			// in hashchange event driven navigate events in a similar fashion to
+			// the state that's provided by popstate
+			this.history.add( state.url, state );
+		},
+
+
+		// This binding is intended to catch the popstate events that are fired
+		// when execution of the `$.navigate` method stops at window.location.hash = url;
+		// and completely prevent them from propagating. The popstate event will then be
+		// retriggered after execution resumes
+		//
+		// TODO grab the original event here and use it for the synthetic event in the
+		//      second half of the navigate execution that will follow this binding
+		popstate: function( event ) {
+			var active, hash, state, closestIndex;
+
+			// Partly to support our test suite which manually alters the support
+			// value to test hashchange. Partly to prevent all around weirdness
+			if( !$.event.special.navigate.isPushStateEnabled() ){
+				return;
+			}
+
+			// If this is the popstate triggered by the actual alteration of the hash
+			// prevent it completely. History is tracked manually
+			if( this.preventHashAssignPopState ) {
+				this.preventHashAssignPopState = false;
+				event.stopImmediatePropagation();
+				return;
+			}
+
+			// if this is the popstate triggered after the `replaceState` call in the go
+			// method, then simply ignore it. The history entry has already been captured
+			if( this.ignorePopState ) {
+				this.ignorePopState = false;
+				return;
+			}
+
+			// If there is no state, and the history stack length is one were
+			// probably getting the page load popstate fired by browsers like chrome
+			// avoid it and set the one time flag to false.
+			// TODO: Do we really need all these conditions? Comparing location hrefs
+			// should be sufficient.
+			if( !event.originalEvent.state &&
+				this.history.stack.length === 1 &&
+				this.ignoreInitialHashChange ) {
+				this.ignoreInitialHashChange = false;
+
+				if ( location.href === initialHref ) {
+					event.preventDefault();
+					return;
+				}
+			}
+
+			// account for direct manipulation of the hash. That is, we will receive a popstate
+			// when the hash is changed by assignment, and it won't have a state associated. We
+			// then need to squash the hash. See below for handling of hash assignment that
+			// matches an existing history entry
+			// TODO it might be better to only add to the history stack
+			//      when the hash is adjacent to the active history entry
+			hash = path.parseLocation().hash;
+			if( !event.originalEvent.state && hash ) {
+				// squash the hash that's been assigned on the URL with replaceState
+				// also grab the resulting state object for storage
+				state = this.squash( hash );
+
+				// record the new hash as an additional history entry
+				// to match the browser's treatment of hash assignment
+				this.history.add( state.url, state );
+
+				// pass the newly created state information
+				// along with the event
+				event.historyState = state;
+
+				// do not alter history, we've added a new history entry
+				// so we know where we are
+				return;
+			}
+
+			// If all else fails this is a popstate that comes from the back or forward buttons
+			// make sure to set the state of our history stack properly, and record the directionality
+			this.history.direct({
+				url: (event.originalEvent.state || {}).url || hash,
+
+				// When the url is either forward or backward in history include the entry
+				// as data on the event object for merging as data in the navigate event
+				present: function( historyEntry, direction ) {
+					// make sure to create a new object to pass down as the navigate event data
+					event.historyState = $.extend({}, historyEntry);
+					event.historyState.direction = direction;
+				}
+			});
+		},
+
+		// NOTE must bind before `navigate` special event hashchange binding otherwise the
+		//      navigation data won't be attached to the hashchange event in time for those
+		//      bindings to attach it to the `navigate` special event
+		// TODO add a check here that `hashchange.navigate` is bound already otherwise it's
+		//      broken (exception?)
+		hashchange: function( event ) {
+			var history, hash;
+
+			// If hashchange listening is explicitly disabled or pushstate is supported
+			// avoid making use of the hashchange handler.
+			if(!$.event.special.navigate.isHashChangeEnabled() ||
+				$.event.special.navigate.isPushStateEnabled() ) {
+				return;
+			}
+
+			// On occasion explicitly want to prevent the next hash from propogating because we only
+			// with to alter the url to represent the new state do so here
+			if( this.preventNextHashChange ){
+				this.preventNextHashChange = false;
+				event.stopImmediatePropagation();
+				return;
+			}
+
+			history = this.history;
+			hash = path.parseLocation().hash;
+
+			// If this is a hashchange caused by the back or forward button
+			// make sure to set the state of our history stack properly
+			this.history.direct({
+				url: hash,
+
+				// When the url is either forward or backward in history include the entry
+				// as data on the event object for merging as data in the navigate event
+				present: function( historyEntry, direction ) {
+					// make sure to create a new object to pass down as the navigate event data
+					event.hashchangeState = $.extend({}, historyEntry);
+					event.hashchangeState.direction = direction;
+				},
+
+				// When we don't find a hash in our history clearly we're aiming to go there
+				// record the entry as new for future traversal
+				//
+				// NOTE it's not entirely clear that this is the right thing to do given that we
+				//      can't know the users intention. It might be better to explicitly _not_
+				//      support location.hash assignment in preference to $.navigate calls
+				// TODO first arg to add should be the href, but it causes issues in identifying
+				//      embeded pages
+				missing: function() {
+					history.add( hash, {
+						hash: hash,
+						title: document.title
+					});
+				}
+			});
+		}
+	});
+})( jQuery );
+
+
+
+(function( $, undefined ) {
+	// TODO consider queueing navigation activity until previous activities have completed
+	//      so that end users don't have to think about it. Punting for now
+	// TODO !! move the event bindings into callbacks on the navigate event
+	$.mobile.navigate = function( url, data, noEvents ) {
+		$.mobile.navigate.navigator.go( url, data, noEvents );
+	};
+
+	// expose the history on the navigate method in anticipation of full integration with
+	// existing navigation functionalty that is tightly coupled to the history information
+	$.mobile.navigate.history = new $.mobile.History();
+
+	// instantiate an instance of the navigator for use within the $.navigate method
+	$.mobile.navigate.navigator = new $.mobile.Navigator( $.mobile.navigate.history );
+
+	var loc = $.mobile.path.parseLocation();
+	$.mobile.navigate.history.add( loc.href, {hash: loc.hash} );
+})( jQuery );
+
+
+// This plugin is an experiment for abstracting away the touch and mouse
+// events so that developers don't have to worry about which method of input
+// the device their document is loaded on supports.
+//
+// The idea here is to allow the developer to register listeners for the
+// basic mouse events, such as mousedown, mousemove, mouseup, and click,
+// and the plugin will take care of registering the correct listeners
+// behind the scenes to invoke the listener at the fastest possible time
+// for that device, while still retaining the order of event firing in
+// the traditional mouse environment, should multiple handlers be registered
+// on the same element for different events.
+//
+// The current version exposes the following virtual events to jQuery bind methods:
+// "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel"
+
+(function( $, window, document, undefined ) {
+
+var dataPropertyName = "virtualMouseBindings",
+	touchTargetPropertyName = "virtualTouchID",
+	virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split( " " ),
+	touchEventProps = "clientX clientY pageX pageY screenX screenY".split( " " ),
+	mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [],
+	mouseEventProps = $.event.props.concat( mouseHookProps ),
+	activeDocHandlers = {},
+	resetTimerID = 0,
+	startX = 0,
+	startY = 0,
+	didScroll = false,
+	clickBlockList = [],
+	blockMouseTriggers = false,
+	blockTouchTriggers = false,
+	eventCaptureSupported = "addEventListener" in document,
+	$document = $( document ),
+	nextTouchID = 1,
+	lastTouchID = 0, threshold;
+
+$.vmouse = {
+	moveDistanceThreshold: 10,
+	clickDistanceThreshold: 10,
+	resetTimerDuration: 1500
+};
+
+function getNativeEvent( event ) {
+
+	while ( event && typeof event.originalEvent !== "undefined" ) {
+		event = event.originalEvent;
+	}
+	return event;
+}
+
+function createVirtualEvent( event, eventType ) {
+
+	var t = event.type,
+		oe, props, ne, prop, ct, touch, i, j, len;
+
+	event = $.Event( event );
+	event.type = eventType;
+
+	oe = event.originalEvent;
+	props = $.event.props;
+
+	// addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280
+	// https://github.com/jquery/jquery-mobile/issues/3280
+	if ( t.search( /^(mouse|click)/ ) > -1 ) {
+		props = mouseEventProps;
+	}
+
+	// copy original event properties over to the new event
+	// this would happen if we could call $.event.fix instead of $.Event
+	// but we don't have a way to force an event to be fixed multiple times
+	if ( oe ) {
+		for ( i = props.length, prop; i; ) {
+			prop = props[ --i ];
+			event[ prop ] = oe[ prop ];
+		}
+	}
+
+	// make sure that if the mouse and click virtual events are generated
+	// without a .which one is defined
+	if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ) {
+		event.which = 1;
+	}
+
+	if ( t.search(/^touch/) !== -1 ) {
+		ne = getNativeEvent( oe );
+		t = ne.touches;
+		ct = ne.changedTouches;
+		touch = ( t && t.length ) ? t[0] : ( ( ct && ct.length ) ? ct[ 0 ] : undefined );
+
+		if ( touch ) {
+			for ( j = 0, len = touchEventProps.length; j < len; j++) {
+				prop = touchEventProps[ j ];
+				event[ prop ] = touch[ prop ];
+			}
+		}
+	}
+
+	return event;
+}
+
+function getVirtualBindingFlags( element ) {
+
+	var flags = {},
+		b, k;
+
+	while ( element ) {
+
+		b = $.data( element, dataPropertyName );
+
+		for (  k in b ) {
+			if ( b[ k ] ) {
+				flags[ k ] = flags.hasVirtualBinding = true;
+			}
+		}
+		element = element.parentNode;
+	}
+	return flags;
+}
+
+function getClosestElementWithVirtualBinding( element, eventType ) {
+	var b;
+	while ( element ) {
+
+		b = $.data( element, dataPropertyName );
+
+		if ( b && ( !eventType || b[ eventType ] ) ) {
+			return element;
+		}
+		element = element.parentNode;
+	}
+	return null;
+}
+
+function enableTouchBindings() {
+	blockTouchTriggers = false;
+}
+
+function disableTouchBindings() {
+	blockTouchTriggers = true;
+}
+
+function enableMouseBindings() {
+	lastTouchID = 0;
+	clickBlockList.length = 0;
+	blockMouseTriggers = false;
+
+	// When mouse bindings are enabled, our
+	// touch bindings are disabled.
+	disableTouchBindings();
+}
+
+function disableMouseBindings() {
+	// When mouse bindings are disabled, our
+	// touch bindings are enabled.
+	enableTouchBindings();
+}
+
+function startResetTimer() {
+	clearResetTimer();
+	resetTimerID = setTimeout( function() {
+		resetTimerID = 0;
+		enableMouseBindings();
+	}, $.vmouse.resetTimerDuration );
+}
+
+function clearResetTimer() {
+	if ( resetTimerID ) {
+		clearTimeout( resetTimerID );
+		resetTimerID = 0;
+	}
+}
+
+function triggerVirtualEvent( eventType, event, flags ) {
+	var ve;
+
+	if ( ( flags && flags[ eventType ] ) ||
+				( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) {
+
+		ve = createVirtualEvent( event, eventType );
+
+		$( event.target).trigger( ve );
+	}
+
+	return ve;
+}
+
+function mouseEventCallback( event ) {
+	var touchID = $.data( event.target, touchTargetPropertyName );
+
+	if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ) {
+		var ve = triggerVirtualEvent( "v" + event.type, event );
+		if ( ve ) {
+			if ( ve.isDefaultPrevented() ) {
+				event.preventDefault();
+			}
+			if ( ve.isPropagationStopped() ) {
+				event.stopPropagation();
+			}
+			if ( ve.isImmediatePropagationStopped() ) {
+				event.stopImmediatePropagation();
+			}
+		}
+	}
+}
+
+function handleTouchStart( event ) {
+
+	var touches = getNativeEvent( event ).touches,
+		target, flags;
+
+	if ( touches && touches.length === 1 ) {
+
+		target = event.target;
+		flags = getVirtualBindingFlags( target );
+
+		if ( flags.hasVirtualBinding ) {
+
+			lastTouchID = nextTouchID++;
+			$.data( target, touchTargetPropertyName, lastTouchID );
+
+			clearResetTimer();
+
+			disableMouseBindings();
+			didScroll = false;
+
+			var t = getNativeEvent( event ).touches[ 0 ];
+			startX = t.pageX;
+			startY = t.pageY;
+
+			triggerVirtualEvent( "vmouseover", event, flags );
+			triggerVirtualEvent( "vmousedown", event, flags );
+		}
+	}
+}
+
+function handleScroll( event ) {
+	if ( blockTouchTriggers ) {
+		return;
+	}
+
+	if ( !didScroll ) {
+		triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) );
+	}
+
+	didScroll = true;
+	startResetTimer();
+}
+
+function handleTouchMove( event ) {
+	if ( blockTouchTriggers ) {
+		return;
+	}
+
+	var t = getNativeEvent( event ).touches[ 0 ],
+		didCancel = didScroll,
+		moveThreshold = $.vmouse.moveDistanceThreshold,
+		flags = getVirtualBindingFlags( event.target );
+
+		didScroll = didScroll ||
+			( Math.abs( t.pageX - startX ) > moveThreshold ||
+				Math.abs( t.pageY - startY ) > moveThreshold );
+
+
+	if ( didScroll && !didCancel ) {
+		triggerVirtualEvent( "vmousecancel", event, flags );
+	}
+
+	triggerVirtualEvent( "vmousemove", event, flags );
+	startResetTimer();
+}
+
+function handleTouchEnd( event ) {
+	if ( blockTouchTriggers ) {
+		return;
+	}
+
+	disableTouchBindings();
+
+	var flags = getVirtualBindingFlags( event.target ),
+		t;
+	triggerVirtualEvent( "vmouseup", event, flags );
+
+	if ( !didScroll ) {
+		var ve = triggerVirtualEvent( "vclick", event, flags );
+		if ( ve && ve.isDefaultPrevented() ) {
+			// The target of the mouse events that follow the touchend
+			// event don't necessarily match the target used during the
+			// touch. This means we need to rely on coordinates for blocking
+			// any click that is generated.
+			t = getNativeEvent( event ).changedTouches[ 0 ];
+			clickBlockList.push({
+				touchID: lastTouchID,
+				x: t.clientX,
+				y: t.clientY
+			});
+
+			// Prevent any mouse events that follow from triggering
+			// virtual event notifications.
+			blockMouseTriggers = true;
+		}
+	}
+	triggerVirtualEvent( "vmouseout", event, flags);
+	didScroll = false;
+
+	startResetTimer();
+}
+
+function hasVirtualBindings( ele ) {
+	var bindings = $.data( ele, dataPropertyName ),
+		k;
+
+	if ( bindings ) {
+		for ( k in bindings ) {
+			if ( bindings[ k ] ) {
+				return true;
+			}
+		}
+	}
+	return false;
+}
+
+function dummyMouseHandler() {}
+
+function getSpecialEventObject( eventType ) {
+	var realType = eventType.substr( 1 );
+
+	return {
+		setup: function( data, namespace ) {
+			// If this is the first virtual mouse binding for this element,
+			// add a bindings object to its data.
+
+			if ( !hasVirtualBindings( this ) ) {
+				$.data( this, dataPropertyName, {} );
+			}
+
+			// If setup is called, we know it is the first binding for this
+			// eventType, so initialize the count for the eventType to zero.
+			var bindings = $.data( this, dataPropertyName );
+			bindings[ eventType ] = true;
+
+			// If this is the first virtual mouse event for this type,
+			// register a global handler on the document.
+
+			activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1;
+
+			if ( activeDocHandlers[ eventType ] === 1 ) {
+				$document.bind( realType, mouseEventCallback );
+			}
+
+			// Some browsers, like Opera Mini, won't dispatch mouse/click events
+			// for elements unless they actually have handlers registered on them.
+			// To get around this, we register dummy handlers on the elements.
+
+			$( this ).bind( realType, dummyMouseHandler );
+
+			// For now, if event capture is not supported, we rely on mouse handlers.
+			if ( eventCaptureSupported ) {
+				// If this is the first virtual mouse binding for the document,
+				// register our touchstart handler on the document.
+
+				activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1;
+
+				if ( activeDocHandlers[ "touchstart" ] === 1 ) {
+					$document.bind( "touchstart", handleTouchStart )
+						.bind( "touchend", handleTouchEnd )
+
+						// On touch platforms, touching the screen and then dragging your finger
+						// causes the window content to scroll after some distance threshold is
+						// exceeded. On these platforms, a scroll prevents a click event from being
+						// dispatched, and on some platforms, even the touchend is suppressed. To
+						// mimic the suppression of the click event, we need to watch for a scroll
+						// event. Unfortunately, some platforms like iOS don't dispatch scroll
+						// events until *AFTER* the user lifts their finger (touchend). This means
+						// we need to watch both scroll and touchmove events to figure out whether
+						// or not a scroll happenens before the touchend event is fired.
+
+						.bind( "touchmove", handleTouchMove )
+						.bind( "scroll", handleScroll );
+				}
+			}
+		},
+
+		teardown: function( data, namespace ) {
+			// If this is the last virtual binding for this eventType,
+			// remove its global handler from the document.
+
+			--activeDocHandlers[ eventType ];
+
+			if ( !activeDocHandlers[ eventType ] ) {
+				$document.unbind( realType, mouseEventCallback );
+			}
+
+			if ( eventCaptureSupported ) {
+				// If this is the last virtual mouse binding in existence,
+				// remove our document touchstart listener.
+
+				--activeDocHandlers[ "touchstart" ];
+
+				if ( !activeDocHandlers[ "touchstart" ] ) {
+					$document.unbind( "touchstart", handleTouchStart )
+						.unbind( "touchmove", handleTouchMove )
+						.unbind( "touchend", handleTouchEnd )
+						.unbind( "scroll", handleScroll );
+				}
+			}
+
+			var $this = $( this ),
+				bindings = $.data( this, dataPropertyName );
+
+			// teardown may be called when an element was
+			// removed from the DOM. If this is the case,
+			// jQuery core may have already stripped the element
+			// of any data bindings so we need to check it before
+			// using it.
+			if ( bindings ) {
+				bindings[ eventType ] = false;
+			}
+
+			// Unregister the dummy event handler.
+
+			$this.unbind( realType, dummyMouseHandler );
+
+			// If this is the last virtual mouse binding on the
+			// element, remove the binding data from the element.
+
+			if ( !hasVirtualBindings( this ) ) {
+				$this.removeData( dataPropertyName );
+			}
+		}
+	};
+}
+
+// Expose our custom events to the jQuery bind/unbind mechanism.
+
+for ( var i = 0; i < virtualEventNames.length; i++ ) {
+	$.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] );
+}
+
+// Add a capture click handler to block clicks.
+// Note that we require event capture support for this so if the device
+// doesn't support it, we punt for now and rely solely on mouse events.
+if ( eventCaptureSupported ) {
+	document.addEventListener( "click", function( e ) {
+		var cnt = clickBlockList.length,
+			target = e.target,
+			x, y, ele, i, o, touchID;
+
+		if ( cnt ) {
+			x = e.clientX;
+			y = e.clientY;
+			threshold = $.vmouse.clickDistanceThreshold;
+
+			// The idea here is to run through the clickBlockList to see if
+			// the current click event is in the proximity of one of our
+			// vclick events that had preventDefault() called on it. If we find
+			// one, then we block the click.
+			//
+			// Why do we have to rely on proximity?
+			//
+			// Because the target of the touch event that triggered the vclick
+			// can be different from the target of the click event synthesized
+			// by the browser. The target of a mouse/click event that is syntehsized
+			// from a touch event seems to be implementation specific. For example,
+			// some browsers will fire mouse/click events for a link that is near
+			// a touch event, even though the target of the touchstart/touchend event
+			// says the user touched outside the link. Also, it seems that with most
+			// browsers, the target of the mouse/click event is not calculated until the
+			// time it is dispatched, so if you replace an element that you touched
+			// with another element, the target of the mouse/click will be the new
+			// element underneath that point.
+			//
+			// Aside from proximity, we also check to see if the target and any
+			// of its ancestors were the ones that blocked a click. This is necessary
+			// because of the strange mouse/click target calculation done in the
+			// Android 2.1 browser, where if you click on an element, and there is a
+			// mouse/click handler on one of its ancestors, the target will be the
+			// innermost child of the touched element, even if that child is no where
+			// near the point of touch.
+
+			ele = target;
+
+			while ( ele ) {
+				for ( i = 0; i < cnt; i++ ) {
+					o = clickBlockList[ i ];
+					touchID = 0;
+
+					if ( ( ele === target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) ||
+								$.data( ele, touchTargetPropertyName ) === o.touchID ) {
+						// XXX: We may want to consider removing matches from the block list
+						//      instead of waiting for the reset timer to fire.
+						e.preventDefault();
+						e.stopPropagation();
+						return;
+					}
+				}
+				ele = ele.parentNode;
+			}
+		}
+	}, true);
+}
+})( jQuery, window, document );
+
+
+(function( $, window, undefined ) {
+	var $document = $( document );
+
+	// add new event shortcuts
+	$.each( ( "touchstart touchmove touchend " +
+		"tap taphold " +
+		"swipe swipeleft swiperight " +
+		"scrollstart scrollstop" ).split( " " ), function( i, name ) {
+
+		$.fn[ name ] = function( fn ) {
+			return fn ? this.bind( name, fn ) : this.trigger( name );
+		};
+
+		// jQuery < 1.8
+		if ( $.attrFn ) {
+			$.attrFn[ name ] = true;
+		}
+	});
+
+	var supportTouch = $.mobile.support.touch,
+		scrollEvent = "touchmove scroll",
+		touchStartEvent = supportTouch ? "touchstart" : "mousedown",
+		touchStopEvent = supportTouch ? "touchend" : "mouseup",
+		touchMoveEvent = supportTouch ? "touchmove" : "mousemove";
+
+	function triggerCustomEvent( obj, eventType, event ) {
+		var originalType = event.type;
+		event.type = eventType;
+		$.event.dispatch.call( obj, event );
+		event.type = originalType;
+	}
+
+	// also handles scrollstop
+	$.event.special.scrollstart = {
+
+		enabled: true,
+
+		setup: function() {
+
+			var thisObject = this,
+				$this = $( thisObject ),
+				scrolling,
+				timer;
+
+			function trigger( event, state ) {
+				scrolling = state;
+				triggerCustomEvent( thisObject, scrolling ? "scrollstart" : "scrollstop", event );
+			}
+
+			// iPhone triggers scroll after a small delay; use touchmove instead
+			$this.bind( scrollEvent, function( event ) {
+
+				if ( !$.event.special.scrollstart.enabled ) {
+					return;
+				}
+
+				if ( !scrolling ) {
+					trigger( event, true );
+				}
+
+				clearTimeout( timer );
+				timer = setTimeout( function() {
+					trigger( event, false );
+				}, 50 );
+			});
+		}
+	};
+
+	// also handles taphold
+	$.event.special.tap = {
+		tapholdThreshold: 750,
+
+		setup: function() {
+			var thisObject = this,
+				$this = $( thisObject );
+
+			$this.bind( "vmousedown", function( event ) {
+
+				if ( event.which && event.which !== 1 ) {
+					return false;
+				}
+
+				var origTarget = event.target,
+					origEvent = event.originalEvent,
+					timer;
+
+				function clearTapTimer() {
+					clearTimeout( timer );
+				}
+
+				function clearTapHandlers() {
+					clearTapTimer();
+
+					$this.unbind( "vclick", clickHandler )
+						.unbind( "vmouseup", clearTapTimer );
+					$document.unbind( "vmousecancel", clearTapHandlers );
+				}
+
+				function clickHandler( event ) {
+					clearTapHandlers();
+
+					// ONLY trigger a 'tap' event if the start target is
+					// the same as the stop target.
+					if ( origTarget === event.target ) {
+						triggerCustomEvent( thisObject, "tap", event );
+					}
+				}
+
+				$this.bind( "vmouseup", clearTapTimer )
+					.bind( "vclick", clickHandler );
+				$document.bind( "vmousecancel", clearTapHandlers );
+
+				timer = setTimeout( function() {
+					triggerCustomEvent( thisObject, "taphold", $.Event( "taphold", { target: origTarget } ) );
+				}, $.event.special.tap.tapholdThreshold );
+			});
+		}
+	};
+
+	// also handles swipeleft, swiperight
+	$.event.special.swipe = {
+		scrollSupressionThreshold: 30, // More than this horizontal displacement, and we will suppress scrolling.
+
+		durationThreshold: 1000, // More time than this, and it isn't a swipe.
+
+		horizontalDistanceThreshold: 30,  // Swipe horizontal displacement must be more than this.
+
+		verticalDistanceThreshold: 75,  // Swipe vertical displacement must be less than this.
+
+		start: function( event ) {
+			var data = event.originalEvent.touches ?
+					event.originalEvent.touches[ 0 ] : event;
+			return {
+						time: ( new Date() ).getTime(),
+						coords: [ data.pageX, data.pageY ],
+						origin: $( event.target )
+					};
+		},
+
+		stop: function( event ) {
+			var data = event.originalEvent.touches ?
+					event.originalEvent.touches[ 0 ] : event;
+			return {
+						time: ( new Date() ).getTime(),
+						coords: [ data.pageX, data.pageY ]
+					};
+		},
+
+		handleSwipe: function( start, stop ) {
+			if ( stop.time - start.time < $.event.special.swipe.durationThreshold &&
+				Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.horizontalDistanceThreshold &&
+				Math.abs( start.coords[ 1 ] - stop.coords[ 1 ] ) < $.event.special.swipe.verticalDistanceThreshold ) {
+
+				start.origin.trigger( "swipe" )
+					.trigger( start.coords[0] > stop.coords[ 0 ] ? "swipeleft" : "swiperight" );
+			}
+		},
+
+		setup: function() {
+			var thisObject = this,
+				$this = $( thisObject );
+
+			$this.bind( touchStartEvent, function( event ) {
+				var start = $.event.special.swipe.start( event ),
+					stop;
+
+				function moveHandler( event ) {
+					if ( !start ) {
+						return;
+					}
+
+					stop = $.event.special.swipe.stop( event );
+
+					// prevent scrolling
+					if ( Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.scrollSupressionThreshold ) {
+						event.preventDefault();
+					}
+				}
+
+				$this.bind( touchMoveEvent, moveHandler )
+					.one( touchStopEvent, function() {
+						$this.unbind( touchMoveEvent, moveHandler );
+
+						if ( start && stop ) {
+							$.event.special.swipe.handleSwipe( start, stop );
+						}
+						start = stop = undefined;
+					});
+			});
+		}
+	};
+	$.each({
+		scrollstop: "scrollstart",
+		taphold: "tap",
+		swipeleft: "swipe",
+		swiperight: "swipe"
+	}, function( event, sourceEvent ) {
+
+		$.event.special[ event ] = {
+			setup: function() {
+				$( this ).bind( sourceEvent, $.noop );
+			}
+		};
+	});
+
+})( jQuery, this );
+
+
+	// throttled resize event
+	(function( $ ) {
+		$.event.special.throttledresize = {
+			setup: function() {
+				$( this ).bind( "resize", handler );
+			},
+			teardown: function() {
+				$( this ).unbind( "resize", handler );
+			}
+		};
+
+		var throttle = 250,
+			handler = function() {
+				curr = ( new Date() ).getTime();
+				diff = curr - lastCall;
+
+				if ( diff >= throttle ) {
+
+					lastCall = curr;
+					$( this ).trigger( "throttledresize" );
+
+				} else {
+
+					if ( heldCall ) {
+						clearTimeout( heldCall );
+					}
+
+					// Promise a held call will still execute
+					heldCall = setTimeout( handler, throttle - diff );
+				}
+			},
+			lastCall = 0,
+			heldCall,
+			curr,
+			diff;
+	})( jQuery );
+
+(function( $, window ) {
+	var win = $( window ),
+		event_name = "orientationchange",
+		special_event,
+		get_orientation,
+		last_orientation,
+		initial_orientation_is_landscape,
+		initial_orientation_is_default,
+		portrait_map = { "0": true, "180": true };
+
+	// It seems that some device/browser vendors use window.orientation values 0 and 180 to
+	// denote the "default" orientation. For iOS devices, and most other smart-phones tested,
+	// the default orientation is always "portrait", but in some Android and RIM based tablets,
+	// the default orientation is "landscape". The following code attempts to use the window
+	// dimensions to figure out what the current orientation is, and then makes adjustments
+	// to the to the portrait_map if necessary, so that we can properly decode the
+	// window.orientation value whenever get_orientation() is called.
+	//
+	// Note that we used to use a media query to figure out what the orientation the browser
+	// thinks it is in:
+	//
+	//     initial_orientation_is_landscape = $.mobile.media("all and (orientation: landscape)");
+	//
+	// but there was an iPhone/iPod Touch bug beginning with iOS 4.2, up through iOS 5.1,
+	// where the browser *ALWAYS* applied the landscape media query. This bug does not
+	// happen on iPad.
+
+	if ( $.support.orientation ) {
+
+		// Check the window width and height to figure out what the current orientation
+		// of the device is at this moment. Note that we've initialized the portrait map
+		// values to 0 and 180, *AND* we purposely check for landscape so that if we guess
+		// wrong, , we default to the assumption that portrait is the default orientation.
+		// We use a threshold check below because on some platforms like iOS, the iPhone
+		// form-factor can report a larger width than height if the user turns on the
+		// developer console. The actual threshold value is somewhat arbitrary, we just
+		// need to make sure it is large enough to exclude the developer console case.
+
+		var ww = window.innerWidth || win.width(),
+			wh = window.innerHeight || win.height(),
+			landscape_threshold = 50;
+
+		initial_orientation_is_landscape = ww > wh && ( ww - wh ) > landscape_threshold;
+
+
+		// Now check to see if the current window.orientation is 0 or 180.
+		initial_orientation_is_default = portrait_map[ window.orientation ];
+
+		// If the initial orientation is landscape, but window.orientation reports 0 or 180, *OR*
+		// if the initial orientation is portrait, but window.orientation reports 90 or -90, we
+		// need to flip our portrait_map values because landscape is the default orientation for
+		// this device/browser.
+		if ( ( initial_orientation_is_landscape && initial_orientation_is_default ) || ( !initial_orientation_is_landscape && !initial_orientation_is_default ) ) {
+			portrait_map = { "-90": true, "90": true };
+		}
+	}
+
+	$.event.special.orientationchange = $.extend( {}, $.event.special.orientationchange, {
+		setup: function() {
+			// If the event is supported natively, return false so that jQuery
+			// will bind to the event using DOM methods.
+			if ( $.support.orientation && !$.event.special.orientationchange.disabled ) {
+				return false;
+			}
+
+			// Get the current orientation to avoid initial double-triggering.
+			last_orientation = get_orientation();
+
+			// Because the orientationchange event doesn't exist, simulate the
+			// event by testing window dimensions on resize.
+			win.bind( "throttledresize", handler );
+		},
+		teardown: function() {
+			// If the event is not supported natively, return false so that
+			// jQuery will unbind the event using DOM methods.
+			if ( $.support.orientation && !$.event.special.orientationchange.disabled ) {
+				return false;
+			}
+
+			// Because the orientationchange event doesn't exist, unbind the
+			// resize event handler.
+			win.unbind( "throttledresize", handler );
+		},
+		add: function( handleObj ) {
+			// Save a reference to the bound event handler.
+			var old_handler = handleObj.handler;
+
+
+			handleObj.handler = function( event ) {
+				// Modify event object, adding the .orientation property.
+				event.orientation = get_orientation();
+
+				// Call the originally-bound event handler and return its result.
+				return old_handler.apply( this, arguments );
+			};
+		}
+	});
+
+	// If the event is not supported natively, this handler will be bound to
+	// the window resize event to simulate the orientationchange event.
+	function handler() {
+		// Get the current orientation.
+		var orientation = get_orientation();
+
+		if ( orientation !== last_orientation ) {
+			// The orientation has changed, so trigger the orientationchange event.
+			last_orientation = orientation;
+			win.trigger( event_name );
+		}
+	}
+
+	// Get the current page orientation. This method is exposed publicly, should it
+	// be needed, as jQuery.event.special.orientationchange.orientation()
+	$.event.special.orientationchange.orientation = get_orientation = function() {
+		var isPortrait = true, elem = document.documentElement;
+
+		// prefer window orientation to the calculation based on screensize as
+		// the actual screen resize takes place before or after the orientation change event
+		// has been fired depending on implementation (eg android 2.3 is before, iphone after).
+		// More testing is required to determine if a more reliable method of determining the new screensize
+		// is possible when orientationchange is fired. (eg, use media queries + element + opacity)
+		if ( $.support.orientation ) {
+			// if the window orientation registers as 0 or 180 degrees report
+			// portrait, otherwise landscape
+			isPortrait = portrait_map[ window.orientation ];
+		} else {
+			isPortrait = elem && elem.clientWidth / elem.clientHeight < 1.1;
+		}
+
+		return isPortrait ? "portrait" : "landscape";
+	};
+
+	$.fn[ event_name ] = function( fn ) {
+		return fn ? this.bind( event_name, fn ) : this.trigger( event_name );
+	};
+
+	// jQuery < 1.8
+	if ( $.attrFn ) {
+		$.attrFn[ event_name ] = true;
+	}
+
+}( jQuery, this ));
+
+
+
+(function( $, undefined ) {
+
+$.widget( "mobile.page", $.mobile.widget, {
+	options: {
+		theme: "c",
+		domCache: false,
+		keepNativeDefault: ":jqmData(role='none'), :jqmData(role='nojs')"
+	},
+
+	_create: function() {
+		// if false is returned by the callbacks do not create the page
+		if ( this._trigger( "beforecreate" ) === false ) {
+			return false;
+		}
+
+		this.element
+			.attr( "tabindex", "0" )
+			.addClass( "ui-page ui-body-" + this.options.theme );
+
+		this._on( this.element, {
+			pagebeforehide: "removeContainerBackground",
+			pagebeforeshow: "_handlePageBeforeShow"
+		});
+	},
+
+	_handlePageBeforeShow: function( e ) {
+		this.setContainerBackground();
+	},
+
+	removeContainerBackground: function() {
+		$.mobile.pageContainer.removeClass( "ui-overlay-" + $.mobile.getInheritedTheme( this.element.parent() ) );
+	},
+
+	// set the page container background to the page theme
+	setContainerBackground: function( theme ) {
+		if ( this.options.theme ) {
+			$.mobile.pageContainer.addClass( "ui-overlay-" + ( theme || this.options.theme ) );
+		}
+	},
+
+	keepNativeSelector: function() {
+		var options = this.options,
+			keepNativeDefined = options.keepNative && $.trim( options.keepNative );
+
+		if ( keepNativeDefined && options.keepNative !== options.keepNativeDefault ) {
+			return [options.keepNative, options.keepNativeDefault].join( ", " );
+		}
+
+		return options.keepNativeDefault;
+	}
+});
+})( jQuery );
+
+(function( $, window, undefined ) {
+
+var createHandler = function( sequential ) {
+
+	// Default to sequential
+	if ( sequential === undefined ) {
+		sequential = true;
+	}
+
+	return function( name, reverse, $to, $from ) {
+
+		var deferred = new $.Deferred(),
+			reverseClass = reverse ? " reverse" : "",
+			active	= $.mobile.urlHistory.getActive(),
+			toScroll = active.lastScroll || $.mobile.defaultHomeScroll,
+			screenHeight = $.mobile.getScreenHeight(),
+			maxTransitionOverride = $.mobile.maxTransitionWidth !== false && $.mobile.window.width() > $.mobile.maxTransitionWidth,
+			none = !$.support.cssTransitions || maxTransitionOverride || !name || name === "none" || Math.max( $.mobile.window.scrollTop(), toScroll ) > $.mobile.getMaxScrollForTransition(),
+			toPreClass = " ui-page-pre-in",
+			toggleViewportClass = function() {
+				$.mobile.pageContainer.toggleClass( "ui-mobile-viewport-transitioning viewport-" + name );
+			},
+			scrollPage = function() {
+				// By using scrollTo instead of silentScroll, we can keep things better in order
+				// Just to be precautios, disable scrollstart listening like silentScroll would
+				$.event.special.scrollstart.enabled = false;
+
+				window.scrollTo( 0, toScroll );
+
+				// reenable scrollstart listening like silentScroll would
+				setTimeout( function() {
+					$.event.special.scrollstart.enabled = true;
+				}, 150 );
+			},
+			cleanFrom = function() {
+				$from
+					.removeClass( $.mobile.activePageClass + " out in reverse " + name )
+					.height( "" );
+			},
+			startOut = function() {
+				// if it's not sequential, call the doneOut transition to start the TO page animating in simultaneously
+				if ( !sequential ) {
+					doneOut();
+				}
+				else {
+					$from.animationComplete( doneOut );
+				}
+
+				// Set the from page's height and start it transitioning out
+				// Note: setting an explicit height helps eliminate tiling in the transitions
+				$from
+					.height( screenHeight + $.mobile.window.scrollTop() )
+					.addClass( name + " out" + reverseClass );
+			},
+
+			doneOut = function() {
+
+				if ( $from && sequential ) {
+					cleanFrom();
+				}
+
+				startIn();
+			},
+
+			startIn = function() {
+
+				// Prevent flickering in phonegap container: see comments at #4024 regarding iOS
+				$to.css( "z-index", -10 );
+
+				$to.addClass( $.mobile.activePageClass + toPreClass );
+
+				// Send focus to page as it is now display: block
+				$.mobile.focusPage( $to );
+
+				// Set to page height
+				$to.height( screenHeight + toScroll );
+
+				scrollPage();
+
+				// Restores visibility of the new page: added together with $to.css( "z-index", -10 );
+				$to.css( "z-index", "" );
+
+				if ( !none ) {
+					$to.animationComplete( doneIn );
+				}
+
+				$to
+					.removeClass( toPreClass )
+					.addClass( name + " in" + reverseClass );
+
+				if ( none ) {
+					doneIn();
+				}
+
+			},
+
+			doneIn = function() {
+
+				if ( !sequential ) {
+
+					if ( $from ) {
+						cleanFrom();
+					}
+				}
+
+				$to
+					.removeClass( "out in reverse " + name )
+					.height( "" );
+
+				toggleViewportClass();
+
+				// In some browsers (iOS5), 3D transitions block the ability to scroll to the desired location during transition
+				// This ensures we jump to that spot after the fact, if we aren't there already.
+				if ( $.mobile.window.scrollTop() !== toScroll ) {
+					scrollPage();
+				}
+
+				deferred.resolve( name, reverse, $to, $from, true );
+			};
+
+		toggleViewportClass();
+
+		if ( $from && !none ) {
+			startOut();
+		}
+		else {
+			doneOut();
+		}
+
+		return deferred.promise();
+	};
+};
+
+// generate the handlers from the above
+var sequentialHandler = createHandler(),
+	simultaneousHandler = createHandler( false ),
+	defaultGetMaxScrollForTransition = function() {
+		return $.mobile.getScreenHeight() * 3;
+	};
+
+// Make our transition handler the public default.
+$.mobile.defaultTransitionHandler = sequentialHandler;
+
+//transition handler dictionary for 3rd party transitions
+$.mobile.transitionHandlers = {
+	"default": $.mobile.defaultTransitionHandler,
+	"sequential": sequentialHandler,
+	"simultaneous": simultaneousHandler
+};
+
+$.mobile.transitionFallbacks = {};
+
+// If transition is defined, check if css 3D transforms are supported, and if not, if a fallback is specified
+$.mobile._maybeDegradeTransition = function( transition ) {
+		if ( transition && !$.support.cssTransform3d && $.mobile.transitionFallbacks[ transition ] ) {
+			transition = $.mobile.transitionFallbacks[ transition ];
+		}
+
+		return transition;
+};
+
+// Set the getMaxScrollForTransition to default if no implementation was set by user
+$.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defaultGetMaxScrollForTransition;
+})( jQuery, this );
+
+(function( $, undefined ) {
+
+	//define vars for interal use
+	var $window = $.mobile.window,
+		$html = $( 'html' ),
+		$head = $( 'head' ),
+
+		// NOTE: path extensions dependent on core attributes. Moved here to remove deps from
+		//       $.mobile.path definition
+		path = $.extend($.mobile.path, {
+
+			//return the substring of a filepath before the sub-page key, for making a server request
+			getFilePath: function( path ) {
+				var splitkey = '&' + $.mobile.subPageUrlKey;
+				return path && path.split( splitkey )[0].split( dialogHashKey )[0];
+			},
+
+			//check if the specified url refers to the first page in the main application document.
+			isFirstPageUrl: function( url ) {
+				// We only deal with absolute paths.
+				var u = path.parseUrl( path.makeUrlAbsolute( url, this.documentBase ) ),
+
+					// Does the url have the same path as the document?
+					samePath = u.hrefNoHash === this.documentUrl.hrefNoHash || ( this.documentBaseDiffers && u.hrefNoHash === this.documentBase.hrefNoHash ),
+
+					// Get the first page element.
+					fp = $.mobile.firstPage,
+
+					// Get the id of the first page element if it has one.
+					fpId = fp && fp[0] ? fp[0].id : undefined;
+
+				// The url refers to the first page if the path matches the document and
+				// it either has no hash value, or the hash is exactly equal to the id of the
+				// first page element.
+				return samePath && ( !u.hash || u.hash === "#" || ( fpId && u.hash.replace( /^#/, "" ) === fpId ) );
+			},
+
+			// Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR
+			// requests if the document doing the request was loaded via the file:// protocol.
+			// This is usually to allow the application to "phone home" and fetch app specific
+			// data. We normally let the browser handle external/cross-domain urls, but if the
+			// allowCrossDomainPages option is true, we will allow cross-domain http/https
+			// requests to go through our page loading logic.
+			isPermittedCrossDomainRequest: function( docUrl, reqUrl ) {
+				return $.mobile.allowCrossDomainPages &&
+					docUrl.protocol === "file:" &&
+					reqUrl.search( /^https?:/ ) !== -1;
+			}
+		}),
+
+		// used to track last vclicked element to make sure its value is added to form data
+		$lastVClicked = null,
+
+		//will be defined when a link is clicked and given an active class
+		$activeClickedLink = null,
+
+		// resolved on domready
+		domreadyDeferred = $.Deferred(),
+
+		//urlHistory is purely here to make guesses at whether the back or forward button was clicked
+		//and provide an appropriate transition
+		urlHistory = $.mobile.navigate.history,
+
+		//define first selector to receive focus when a page is shown
+		focusable = "[tabindex],a,button:visible,select:visible,input",
+
+		//queue to hold simultanious page transitions
+		pageTransitionQueue = [],
+
+		//indicates whether or not page is in process of transitioning
+		isPageTransitioning = false,
+
+		//nonsense hash change key for dialogs, so they create a history entry
+		dialogHashKey = "&ui-state=dialog",
+
+		//existing base tag?
+		$base = $head.children( "base" ),
+
+		//tuck away the original document URL minus any fragment.
+		documentUrl = path.documentUrl,
+
+		//if the document has an embedded base tag, documentBase is set to its
+		//initial value. If a base tag does not exist, then we default to the documentUrl.
+		documentBase = path.documentBase,
+
+		//cache the comparison once.
+		documentBaseDiffers = path.documentBaseDiffers,
+
+		getScreenHeight = $.mobile.getScreenHeight;
+
+		//base element management, defined depending on dynamic base tag support
+		var base = $.support.dynamicBaseTag ? {
+
+			//define base element, for use in routing asset urls that are referenced in Ajax-requested markup
+			element: ( $base.length ? $base : $( "<base>", { href: documentBase.hrefNoHash } ).prependTo( $head ) ),
+
+			//set the generated BASE element's href attribute to a new page's base path
+			set: function( href ) {
+				href = path.parseUrl(href).hrefNoHash;
+				base.element.attr( "href", path.makeUrlAbsolute( href, documentBase ) );
+			},
+
+			//set the generated BASE element's href attribute to a new page's base path
+			reset: function() {
+				base.element.attr( "href", documentBase.hrefNoSearch );
+			}
+
+		} : undefined;
+
+
+	//return the original document url
+	$.mobile.getDocumentUrl = path.getDocumentUrl;
+
+	//return the original document base url
+	$.mobile.getDocumentBase = path.getDocumentBase;
+
+	/* internal utility functions */
+
+	// NOTE Issue #4950 Android phonegap doesn't navigate back properly
+	//      when a full page refresh has taken place. It appears that hashchange
+	//      and replacestate history alterations work fine but we need to support
+	//      both forms of history traversal in our code that uses backward history
+	//      movement
+	$.mobile.back = function() {
+		var nav = window.navigator;
+
+		// if the setting is on and the navigator object is
+		// available use the phonegap navigation capability
+		if( this.phonegapNavigationEnabled &&
+			nav &&
+			nav.app &&
+			nav.app.backHistory ){
+			nav.app.backHistory();
+		} else {
+			window.history.back();
+		}
+	};
+
+	//direct focus to the page title, or otherwise first focusable element
+	$.mobile.focusPage = function ( page ) {
+		var autofocus = page.find( "[autofocus]" ),
+			pageTitle = page.find( ".ui-title:eq(0)" );
+
+		if ( autofocus.length ) {
+			autofocus.focus();
+			return;
+		}
+
+		if ( pageTitle.length ) {
+			pageTitle.focus();
+		} else{
+			page.focus();
+		}
+	};
+
+	//remove active classes after page transition or error
+	function removeActiveLinkClass( forceRemoval ) {
+		if ( !!$activeClickedLink && ( !$activeClickedLink.closest( "." + $.mobile.activePageClass ).length || forceRemoval ) ) {
+			$activeClickedLink.removeClass( $.mobile.activeBtnClass );
+		}
+		$activeClickedLink = null;
+	}
+
+	function releasePageTransitionLock() {
+		isPageTransitioning = false;
+		if ( pageTransitionQueue.length > 0 ) {
+			$.mobile.changePage.apply( null, pageTransitionQueue.pop() );
+		}
+	}
+
+	// Save the last scroll distance per page, before it is hidden
+	var setLastScrollEnabled = true,
+		setLastScroll, delayedSetLastScroll;
+
+	setLastScroll = function() {
+		// this barrier prevents setting the scroll value based on the browser
+		// scrolling the window based on a hashchange
+		if ( !setLastScrollEnabled ) {
+			return;
+		}
+
+		var active = $.mobile.urlHistory.getActive();
+
+		if ( active ) {
+			var lastScroll = $window.scrollTop();
+
+			// Set active page's lastScroll prop.
+			// If the location we're scrolling to is less than minScrollBack, let it go.
+			active.lastScroll = lastScroll < $.mobile.minScrollBack ? $.mobile.defaultHomeScroll : lastScroll;
+		}
+	};
+
+	// bind to scrollstop to gather scroll position. The delay allows for the hashchange
+	// event to fire and disable scroll recording in the case where the browser scrolls
+	// to the hash targets location (sometimes the top of the page). once pagechange fires
+	// getLastScroll is again permitted to operate
+	delayedSetLastScroll = function() {
+		setTimeout( setLastScroll, 100 );
+	};
+
+	// disable an scroll setting when a hashchange has been fired, this only works
+	// because the recording of the scroll position is delayed for 100ms after
+	// the browser might have changed the position because of the hashchange
+	$window.bind( $.support.pushState ? "popstate" : "hashchange", function() {
+		setLastScrollEnabled = false;
+	});
+
+	// handle initial hashchange from chrome :(
+	$window.one( $.support.pushState ? "popstate" : "hashchange", function() {
+		setLastScrollEnabled = true;
+	});
+
+	// wait until the mobile page container has been determined to bind to pagechange
+	$window.one( "pagecontainercreate", function() {
+		// once the page has changed, re-enable the scroll recording
+		$.mobile.pageContainer.bind( "pagechange", function() {
+
+			setLastScrollEnabled = true;
+
+			// remove any binding that previously existed on the get scroll
+			// which may or may not be different than the scroll element determined for
+			// this page previously
+			$window.unbind( "scrollstop", delayedSetLastScroll );
+
+			// determine and bind to the current scoll element which may be the window
+			// or in the case of touch overflow the element with touch overflow
+			$window.bind( "scrollstop", delayedSetLastScroll );
+		});
+	});
+
+	// bind to scrollstop for the first page as "pagechange" won't be fired in that case
+	$window.bind( "scrollstop", delayedSetLastScroll );
+
+	// No-op implementation of transition degradation
+	$.mobile._maybeDegradeTransition = $.mobile._maybeDegradeTransition || function( transition ) {
+		return transition;
+	};
+
+	//function for transitioning between two existing pages
+	function transitionPages( toPage, fromPage, transition, reverse ) {
+		if ( fromPage ) {
+			//trigger before show/hide events
+			fromPage.data( "mobile-page" )._trigger( "beforehide", null, { nextPage: toPage } );
+		}
+
+		toPage.data( "mobile-page" )._trigger( "beforeshow", null, { prevPage: fromPage || $( "" ) } );
+
+		//clear page loader
+		$.mobile.hidePageLoadingMsg();
+
+		transition = $.mobile._maybeDegradeTransition( transition );
+
+		//find the transition handler for the specified transition. If there
+		//isn't one in our transitionHandlers dictionary, use the default one.
+		//call the handler immediately to kick-off the transition.
+		var th = $.mobile.transitionHandlers[ transition || "default" ] || $.mobile.defaultTransitionHandler,
+			promise = th( transition, reverse, toPage, fromPage );
+
+		promise.done(function() {
+			//trigger show/hide events
+			if ( fromPage ) {
+				fromPage.data( "mobile-page" )._trigger( "hide", null, { nextPage: toPage } );
+			}
+
+			//trigger pageshow, define prevPage as either fromPage or empty jQuery obj
+			toPage.data( "mobile-page" )._trigger( "show", null, { prevPage: fromPage || $( "" ) } );
+		});
+
+		return promise;
+	}
+
+	//simply set the active page's minimum height to screen height, depending on orientation
+	$.mobile.resetActivePageHeight = function resetActivePageHeight( height ) {
+		var aPage = $( "." + $.mobile.activePageClass ),
+			aPagePadT = parseFloat( aPage.css( "padding-top" ) ),
+			aPagePadB = parseFloat( aPage.css( "padding-bottom" ) ),
+			aPageBorderT = parseFloat( aPage.css( "border-top-width" ) ),
+			aPageBorderB = parseFloat( aPage.css( "border-bottom-width" ) );
+
+		height = ( typeof height === "number" )? height : getScreenHeight();
+		
+		aPage.css( "min-height", height - aPagePadT - aPagePadB - aPageBorderT - aPageBorderB );
+	};
+
+	//shared page enhancements
+	function enhancePage( $page, role ) {
+		// If a role was specified, make sure the data-role attribute
+		// on the page element is in sync.
+		if ( role ) {
+			$page.attr( "data-" + $.mobile.ns + "role", role );
+		}
+
+		//run page plugin
+		$page.page();
+	}
+
+	// determine the current base url
+	function findBaseWithDefault() {
+		var closestBase = ( $.mobile.activePage && getClosestBaseUrl( $.mobile.activePage ) );
+		return closestBase || documentBase.hrefNoHash;
+	}
+
+	/* exposed $.mobile methods */
+
+	//animation complete callback
+	$.fn.animationComplete = function( callback ) {
+		if ( $.support.cssTransitions ) {
+			return $( this ).one( 'webkitAnimationEnd animationend', callback );
+		}
+		else{
+			// defer execution for consistency between webkit/non webkit
+			setTimeout( callback, 0 );
+			return $( this );
+		}
+	};
+
+	//expose path object on $.mobile
+	$.mobile.path = path;
+
+	//expose base object on $.mobile
+	$.mobile.base = base;
+
+	//history stack
+	$.mobile.urlHistory = urlHistory;
+
+	$.mobile.dialogHashKey = dialogHashKey;
+
+	//enable cross-domain page support
+	$.mobile.allowCrossDomainPages = false;
+
+	$.mobile._bindPageRemove = function() {
+		var page = $( this );
+
+		// when dom caching is not enabled or the page is embedded bind to remove the page on hide
+		if ( !page.data( "mobile-page" ).options.domCache &&
+			page.is( ":jqmData(external-page='true')" ) ) {
+
+			page.bind( 'pagehide.remove', function( e ) {
+				var $this = $( this ),
+					prEvent = new $.Event( "pageremove" );
+
+				$this.trigger( prEvent );
+
+				if ( !prEvent.isDefaultPrevented() ) {
+					$this.removeWithDependents();
+				}
+			});
+		}
+	};
+
+	// Load a page into the DOM.
+	$.mobile.loadPage = function( url, options ) {
+		// This function uses deferred notifications to let callers
+		// know when the page is done loading, or if an error has occurred.
+		var deferred = $.Deferred(),
+
+			// The default loadPage options with overrides specified by
+			// the caller.
+			settings = $.extend( {}, $.mobile.loadPage.defaults, options ),
+
+			// The DOM element for the page after it has been loaded.
+			page = null,
+
+			// If the reloadPage option is true, and the page is already
+			// in the DOM, dupCachedPage will be set to the page element
+			// so that it can be removed after the new version of the
+			// page is loaded off the network.
+			dupCachedPage = null,
+
+			// The absolute version of the URL passed into the function. This
+			// version of the URL may contain dialog/subpage params in it.
+			absUrl = path.makeUrlAbsolute( url, findBaseWithDefault() );
+
+		// If the caller provided data, and we're using "get" request,
+		// append the data to the URL.
+		if ( settings.data && settings.type === "get" ) {
+			absUrl = path.addSearchParams( absUrl, settings.data );
+			settings.data = undefined;
+		}
+
+		// If the caller is using a "post" request, reloadPage must be true
+		if ( settings.data && settings.type === "post" ) {
+			settings.reloadPage = true;
+		}
+
+		// The absolute version of the URL minus any dialog/subpage params.
+		// In otherwords the real URL of the page to be loaded.
+		var fileUrl = path.getFilePath( absUrl ),
+
+			// The version of the Url actually stored in the data-url attribute of
+			// the page. For embedded pages, it is just the id of the page. For pages
+			// within the same domain as the document base, it is the site relative
+			// path. For cross-domain pages (Phone Gap only) the entire absolute Url
+			// used to load the page.
+			dataUrl = path.convertUrlToDataUrl( absUrl );
+
+		// Make sure we have a pageContainer to work with.
+		settings.pageContainer = settings.pageContainer || $.mobile.pageContainer;
+
+		// Check to see if the page already exists in the DOM.
+		// NOTE do _not_ use the :jqmData psuedo selector because parenthesis
+		//      are a valid url char and it breaks on the first occurence
+		page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" );
+
+		// If we failed to find the page, check to see if the url is a
+		// reference to an embedded page. If so, it may have been dynamically
+		// injected by a developer, in which case it would be lacking a data-url
+		// attribute and in need of enhancement.
+		if ( page.length === 0 && dataUrl && !path.isPath( dataUrl ) ) {
+			page = settings.pageContainer.children( "#" + dataUrl )
+				.attr( "data-" + $.mobile.ns + "url", dataUrl )
+				.jqmData( "url", dataUrl );
+		}
+
+		
+		// If we failed to find a page in the DOM, check the URL to see if it
+		// refers to the first page in the application. If it isn't a reference
+		// to the first page and refers to non-existent embedded page, error out.
+		if ( page.length === 0 ) {
+			if ( $.mobile.firstPage && path.isFirstPageUrl( fileUrl ) ) {
+				// Check to make sure our cached-first-page is actually
+				// in the DOM. Some user deployed apps are pruning the first
+				// page from the DOM for various reasons, we check for this
+				// case here because we don't want a first-page with an id
+				// falling through to the non-existent embedded page error
+				// case. If the first-page is not in the DOM, then we let
+				// things fall through to the ajax loading code below so
+				// that it gets reloaded.
+				if ( $.mobile.firstPage.parent().length ) {
+					page = $( $.mobile.firstPage );
+				}
+			} else if ( path.isEmbeddedPage( fileUrl )  ) {
+				deferred.reject( absUrl, options );
+				return deferred.promise();
+			}
+		}
+		
+		// If the page we are interested in is already in the DOM,
+		// and the caller did not indicate that we should force a
+		// reload of the file, we are done. Otherwise, track the
+		// existing page as a duplicated.
+		if ( page.length ) {
+			if ( !settings.reloadPage ) {
+				enhancePage( page, settings.role );
+				deferred.resolve( absUrl, options, page );
+				//if we are reloading the page make sure we update the base if its not a prefetch 
+				if( base && !options.prefetch ){
+					base.set(url);
+				}
+				return deferred.promise();
+			}
+			dupCachedPage = page;
+		}
+		var mpc = settings.pageContainer,
+			pblEvent = new $.Event( "pagebeforeload" ),
+			triggerData = { url: url, absUrl: absUrl, dataUrl: dataUrl, deferred: deferred, options: settings };
+
+		// Let listeners know we're about to load a page.
+		mpc.trigger( pblEvent, triggerData );
+
+		// If the default behavior is prevented, stop here!
+		if ( pblEvent.isDefaultPrevented() ) {
+			return deferred.promise();
+		}
+
+		if ( settings.showLoadMsg ) {
+
+			// This configurable timeout allows cached pages a brief delay to load without showing a message
+			var loadMsgDelay = setTimeout(function() {
+					$.mobile.showPageLoadingMsg();
+				}, settings.loadMsgDelay ),
+
+				// Shared logic for clearing timeout and removing message.
+				hideMsg = function() {
+
+					// Stop message show timer
+					clearTimeout( loadMsgDelay );
+
+					// Hide loading message
+					$.mobile.hidePageLoadingMsg();
+				};
+		}
+		// Reset base to the default document base.
+		// only reset if we are not prefetching 
+		if ( base && typeof options.prefetch === "undefined" ) {
+			base.reset();
+		}
+
+		if ( !( $.mobile.allowCrossDomainPages || path.isSameDomain( documentUrl, absUrl ) ) ) {
+			deferred.reject( absUrl, options );
+		} else {
+			// Load the new page.
+			$.ajax({
+				url: fileUrl,
+				type: settings.type,
+				data: settings.data,
+				contentType: settings.contentType,
+				dataType: "html",
+				success: function( html, textStatus, xhr ) {
+					//pre-parse html to check for a data-url,
+					//use it as the new fileUrl, base path, etc
+					var all = $( "<div></div>" ),
+
+						//page title regexp
+						newPageTitle = html.match( /<title[^>]*>([^<]*)/ ) && RegExp.$1,
+
+						// TODO handle dialogs again
+						pageElemRegex = new RegExp( "(<[^>]+\\bdata-" + $.mobile.ns + "role=[\"']?page[\"']?[^>]*>)" ),
+						dataUrlRegex = new RegExp( "\\bdata-" + $.mobile.ns + "url=[\"']?([^\"'>]*)[\"']?" );
+
+
+					// data-url must be provided for the base tag so resource requests can be directed to the
+					// correct url. loading into a temprorary element makes these requests immediately
+					if ( pageElemRegex.test( html ) &&
+							RegExp.$1 &&
+							dataUrlRegex.test( RegExp.$1 ) &&
+							RegExp.$1 ) {
+						url = fileUrl = path.getFilePath( $( "<div>" + RegExp.$1 + "</div>" ).text() );
+					}
+					//dont update the base tag if we are prefetching
+					if ( base && typeof options.prefetch === "undefined") {
+						base.set( fileUrl );
+					}
+
+					//workaround to allow scripts to execute when included in page divs
+					all.get( 0 ).innerHTML = html;
+					page = all.find( ":jqmData(role='page'), :jqmData(role='dialog')" ).first();
+
+					//if page elem couldn't be found, create one and insert the body element's contents
+					if ( !page.length ) {
+						page = $( "<div data-" + $.mobile.ns + "role='page'>" + ( html.split( /<\/?body[^>]*>/gmi )[1] || "" ) + "</div>" );
+					}
+
+					if ( newPageTitle && !page.jqmData( "title" ) ) {
+						if ( ~newPageTitle.indexOf( "&" ) ) {
+							newPageTitle = $( "<div>" + newPageTitle + "</div>" ).text();
+						}
+						page.jqmData( "title", newPageTitle );
+					}
+
+					//rewrite src and href attrs to use a base url
+					if ( !$.support.dynamicBaseTag ) {
+						var newPath = path.get( fileUrl );
+						page.find( "[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]" ).each(function() {
+							var thisAttr = $( this ).is( '[href]' ) ? 'href' :
+									$( this ).is( '[src]' ) ? 'src' : 'action',
+								thisUrl = $( this ).attr( thisAttr );
+
+							// XXX_jblas: We need to fix this so that it removes the document
+							//            base URL, and then prepends with the new page URL.
+							//if full path exists and is same, chop it - helps IE out
+							thisUrl = thisUrl.replace( location.protocol + '//' + location.host + location.pathname, '' );
+
+							if ( !/^(\w+:|#|\/)/.test( thisUrl ) ) {
+								$( this ).attr( thisAttr, newPath + thisUrl );
+							}
+						});
+					}
+
+					//append to page and enhance
+					// TODO taging a page with external to make sure that embedded pages aren't removed
+					//      by the various page handling code is bad. Having page handling code in many
+					//      places is bad. Solutions post 1.0
+					page
+						.attr( "data-" + $.mobile.ns + "url", path.convertUrlToDataUrl( fileUrl ) )
+						.attr( "data-" + $.mobile.ns + "external-page", true )
+						.appendTo( settings.pageContainer );
+
+					// wait for page creation to leverage options defined on widget
+					page.one( 'pagecreate', $.mobile._bindPageRemove );
+
+					enhancePage( page, settings.role );
+
+					// Enhancing the page may result in new dialogs/sub pages being inserted
+					// into the DOM. If the original absUrl refers to a sub-page, that is the
+					// real page we are interested in.
+					if ( absUrl.indexOf( "&" + $.mobile.subPageUrlKey ) > -1 ) {
+						page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" );
+					}
+
+					// Remove loading message.
+					if ( settings.showLoadMsg ) {
+						hideMsg();
+					}
+
+					// Add the page reference and xhr to our triggerData.
+					triggerData.xhr = xhr;
+					triggerData.textStatus = textStatus;
+					triggerData.page = page;
+
+					// Let listeners know the page loaded successfully.
+					settings.pageContainer.trigger( "pageload", triggerData );
+
+					deferred.resolve( absUrl, options, page, dupCachedPage );
+				},
+				error: function( xhr, textStatus, errorThrown ) {
+					//set base back to current path
+					if ( base ) {
+						base.set( path.get() );
+					}
+
+					// Add error info to our triggerData.
+					triggerData.xhr = xhr;
+					triggerData.textStatus = textStatus;
+					triggerData.errorThrown = errorThrown;
+
+					var plfEvent = new $.Event( "pageloadfailed" );
+
+					// Let listeners know the page load failed.
+					settings.pageContainer.trigger( plfEvent, triggerData );
+
+					// If the default behavior is prevented, stop here!
+					// Note that it is the responsibility of the listener/handler
+					// that called preventDefault(), to resolve/reject the
+					// deferred object within the triggerData.
+					if ( plfEvent.isDefaultPrevented() ) {
+						return;
+					}
+
+					// Remove loading message.
+					if ( settings.showLoadMsg ) {
+
+						// Remove loading message.
+						hideMsg();
+
+						// show error message
+						$.mobile.showPageLoadingMsg( $.mobile.pageLoadErrorMessageTheme, $.mobile.pageLoadErrorMessage, true );
+
+						// hide after delay
+						setTimeout( $.mobile.hidePageLoadingMsg, 1500 );
+					}
+
+					deferred.reject( absUrl, options );
+				}
+			});
+		}
+
+		return deferred.promise();
+	};
+
+	$.mobile.loadPage.defaults = {
+		type: "get",
+		data: undefined,
+		reloadPage: false,
+		role: undefined, // By default we rely on the role defined by the @data-role attribute.
+		showLoadMsg: false,
+		pageContainer: undefined,
+		loadMsgDelay: 50 // This delay allows loads that pull from browser cache to occur without showing the loading message.
+	};
+
+	// Show a specific page in the page container.
+	$.mobile.changePage = function( toPage, options ) {
+		// If we are in the midst of a transition, queue the current request.
+		// We'll call changePage() once we're done with the current transition to
+		// service the request.
+		if ( isPageTransitioning ) {
+			pageTransitionQueue.unshift( arguments );
+			return;
+		}
+
+		var settings = $.extend( {}, $.mobile.changePage.defaults, options ), isToPageString;
+
+		// Make sure we have a pageContainer to work with.
+		settings.pageContainer = settings.pageContainer || $.mobile.pageContainer;
+
+		// Make sure we have a fromPage.
+		settings.fromPage = settings.fromPage || $.mobile.activePage;
+
+		isToPageString = (typeof toPage === "string");
+
+		var mpc = settings.pageContainer,
+			pbcEvent = new $.Event( "pagebeforechange" ),
+			triggerData = { toPage: toPage, options: settings };
+
+		// NOTE: preserve the original target as the dataUrl value will be simplified
+		//       eg, removing ui-state, and removing query params from the hash
+		//       this is so that users who want to use query params have access to them
+		//       in the event bindings for the page life cycle See issue #5085
+		if ( isToPageString ) {
+			// if the toPage is a string simply convert it
+			triggerData.absUrl = path.makeUrlAbsolute( toPage, findBaseWithDefault() );
+		} else {
+			// if the toPage is a jQuery object grab the absolute url stored
+			// in the loadPage callback where it exists
+			triggerData.absUrl = toPage.data( 'absUrl' );
+		}
+
+		// Let listeners know we're about to change the current page.
+		mpc.trigger( pbcEvent, triggerData );
+
+		// If the default behavior is prevented, stop here!
+		if ( pbcEvent.isDefaultPrevented() ) {
+			return;
+		}
+
+		// We allow "pagebeforechange" observers to modify the toPage in the trigger
+		// data to allow for redirects. Make sure our toPage is updated.
+		//
+		// We also need to re-evaluate whether it is a string, because an object can
+		// also be replaced by a string
+
+		toPage = triggerData.toPage;
+		isToPageString = (typeof toPage === "string");
+
+		// Set the isPageTransitioning flag to prevent any requests from
+		// entering this method while we are in the midst of loading a page
+		// or transitioning.
+		isPageTransitioning = true;
+
+		// If the caller passed us a url, call loadPage()
+		// to make sure it is loaded into the DOM. We'll listen
+		// to the promise object it returns so we know when
+		// it is done loading or if an error ocurred.
+		if ( isToPageString ) {
+			// preserve the original target as the dataUrl value will be simplified
+			// eg, removing ui-state, and removing query params from the hash
+			// this is so that users who want to use query params have access to them
+			// in the event bindings for the page life cycle See issue #5085
+			settings.target = toPage;
+
+			$.mobile.loadPage( toPage, settings )
+				.done(function( url, options, newPage, dupCachedPage ) {
+					isPageTransitioning = false;
+					options.duplicateCachedPage = dupCachedPage;
+
+					// store the original absolute url so that it can be provided
+					// to events in the triggerData of the subsequent changePage call
+					newPage.data( 'absUrl', triggerData.absUrl );
+					$.mobile.changePage( newPage, options );
+				})
+				.fail(function( url, options ) {
+
+					//clear out the active button state
+					removeActiveLinkClass( true );
+
+					//release transition lock so navigation is free again
+					releasePageTransitionLock();
+					settings.pageContainer.trigger( "pagechangefailed", triggerData );
+				});
+			return;
+		}
+
+		// If we are going to the first-page of the application, we need to make
+		// sure settings.dataUrl is set to the application document url. This allows
+		// us to avoid generating a document url with an id hash in the case where the
+		// first-page of the document has an id attribute specified.
+		if ( toPage[ 0 ] === $.mobile.firstPage[ 0 ] && !settings.dataUrl ) {
+			settings.dataUrl = documentUrl.hrefNoHash;
+		}
+
+		// The caller passed us a real page DOM element. Update our
+		// internal state and then trigger a transition to the page.
+		var fromPage = settings.fromPage,
+			url = ( settings.dataUrl && path.convertUrlToDataUrl( settings.dataUrl ) ) || toPage.jqmData( "url" ),
+			// The pageUrl var is usually the same as url, except when url is obscured as a dialog url. pageUrl always contains the file path
+			pageUrl = url,
+			fileUrl = path.getFilePath( url ),
+			active = urlHistory.getActive(),
+			activeIsInitialPage = urlHistory.activeIndex === 0,
+			historyDir = 0,
+			pageTitle = document.title,
+			isDialog = settings.role === "dialog" || toPage.jqmData( "role" ) === "dialog";
+
+
+		// By default, we prevent changePage requests when the fromPage and toPage
+		// are the same element, but folks that generate content manually/dynamically
+		// and reuse pages want to be able to transition to the same page. To allow
+		// this, they will need to change the default value of allowSamePageTransition
+		// to true, *OR*, pass it in as an option when they manually call changePage().
+		// It should be noted that our default transition animations assume that the
+		// formPage and toPage are different elements, so they may behave unexpectedly.
+		// It is up to the developer that turns on the allowSamePageTransitiona option
+		// to either turn off transition animations, or make sure that an appropriate
+		// animation transition is used.
+		if ( fromPage && fromPage[0] === toPage[0] && !settings.allowSamePageTransition ) {
+			isPageTransitioning = false;
+			mpc.trigger( "pagechange", triggerData );
+
+			// Even if there is no page change to be done, we should keep the urlHistory in sync with the hash changes
+			if ( settings.fromHashChange ) {
+				urlHistory.direct({ url: url });
+			}
+
+			return;
+		}
+
+		// We need to make sure the page we are given has already been enhanced.
+		enhancePage( toPage, settings.role );
+
+		// If the changePage request was sent from a hashChange event, check to see if the
+		// page is already within the urlHistory stack. If so, we'll assume the user hit
+		// the forward/back button and will try to match the transition accordingly.
+		if ( settings.fromHashChange ) {
+			historyDir = options.direction === "back" ? -1 : 1;
+		}
+
+		// Kill the keyboard.
+		// XXX_jblas: We need to stop crawling the entire document to kill focus. Instead,
+		//            we should be tracking focus with a delegate() handler so we already have
+		//            the element in hand at this point.
+		// Wrap this in a try/catch block since IE9 throw "Unspecified error" if document.activeElement
+		// is undefined when we are in an IFrame.
+		try {
+			if ( document.activeElement && document.activeElement.nodeName.toLowerCase() !== 'body' ) {
+				$( document.activeElement ).blur();
+			} else {
+				$( "input:focus, textarea:focus, select:focus" ).blur();
+			}
+		} catch( e ) {}
+
+		// Record whether we are at a place in history where a dialog used to be - if so, do not add a new history entry and do not change the hash either
+		var alreadyThere = false;
+
+		// If we're displaying the page as a dialog, we don't want the url
+		// for the dialog content to be used in the hash. Instead, we want
+		// to append the dialogHashKey to the url of the current page.
+		if ( isDialog && active ) {
+			// on the initial page load active.url is undefined and in that case should
+			// be an empty string. Moving the undefined -> empty string back into
+			// urlHistory.addNew seemed imprudent given undefined better represents
+			// the url state
+
+			// If we are at a place in history that once belonged to a dialog, reuse
+			// this state without adding to urlHistory and without modifying the hash.
+			// However, if a dialog is already displayed at this point, and we're
+			// about to display another dialog, then we must add another hash and
+			// history entry on top so that one may navigate back to the original dialog
+			if ( active.url &&
+				active.url.indexOf( dialogHashKey ) > -1 &&
+				$.mobile.activePage &&
+				!$.mobile.activePage.is( ".ui-dialog" ) &&
+				urlHistory.activeIndex > 0 ) {
+				settings.changeHash = false;
+				alreadyThere = true;
+			}
+
+			// Normally, we tack on a dialog hash key, but if this is the location of a stale dialog,
+			// we reuse the URL from the entry
+			url = ( active.url || "" );
+
+			// account for absolute urls instead of just relative urls use as hashes
+			if( !alreadyThere && url.indexOf("#") > -1 ) {
+				url += dialogHashKey;
+			} else {
+				url += "#" + dialogHashKey;
+			}
+
+			// tack on another dialogHashKey if this is the same as the initial hash
+			// this makes sure that a history entry is created for this dialog
+			if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) {
+				url += dialogHashKey;
+			}
+		}
+
+		// if title element wasn't found, try the page div data attr too
+		// If this is a deep-link or a reload ( active === undefined ) then just use pageTitle
+		var newPageTitle = ( !active )? pageTitle : toPage.jqmData( "title" ) || toPage.children( ":jqmData(role='header')" ).find( ".ui-title" ).text();
+		if ( !!newPageTitle && pageTitle === document.title ) {
+			pageTitle = newPageTitle;
+		}
+		if ( !toPage.jqmData( "title" ) ) {
+			toPage.jqmData( "title", pageTitle );
+		}
+
+		// Make sure we have a transition defined.
+		settings.transition = settings.transition ||
+			( ( historyDir && !activeIsInitialPage ) ? active.transition : undefined ) ||
+			( isDialog ? $.mobile.defaultDialogTransition : $.mobile.defaultPageTransition );
+
+		//add page to history stack if it's not back or forward
+		if ( !historyDir && alreadyThere ) {
+			urlHistory.getActive().pageUrl = pageUrl;
+		}
+
+		// Set the location hash.
+		if ( url && !settings.fromHashChange ) {
+			var params;
+
+			// rebuilding the hash here since we loose it earlier on
+			// TODO preserve the originally passed in path
+			if( !path.isPath( url ) && url.indexOf( "#" ) < 0 ) {
+				url = "#" + url;
+			}
+
+			// TODO the property names here are just silly
+			params = {
+				transition: settings.transition,
+				title: pageTitle,
+				pageUrl: pageUrl,
+				role: settings.role
+			};
+
+			if ( settings.changeHash !== false && $.mobile.hashListeningEnabled ) {
+				$.mobile.navigate( url, params, true);
+			} else if ( toPage[ 0 ] !== $.mobile.firstPage[ 0 ] ) {
+				$.mobile.navigate.history.add( url, params );
+			}
+		}
+
+		//set page title
+		document.title = pageTitle;
+
+		//set "toPage" as activePage
+		$.mobile.activePage = toPage;
+
+		// If we're navigating back in the URL history, set reverse accordingly.
+		settings.reverse = settings.reverse || historyDir < 0;
+
+		transitionPages( toPage, fromPage, settings.transition, settings.reverse )
+			.done(function( name, reverse, $to, $from, alreadyFocused ) {
+				removeActiveLinkClass();
+
+				//if there's a duplicateCachedPage, remove it from the DOM now that it's hidden
+				if ( settings.duplicateCachedPage ) {
+					settings.duplicateCachedPage.remove();
+				}
+
+				// Send focus to the newly shown page. Moved from promise .done binding in transitionPages
+				// itself to avoid ie bug that reports offsetWidth as > 0 (core check for visibility)
+				// despite visibility: hidden addresses issue #2965
+				// https://github.com/jquery/jquery-mobile/issues/2965
+				if ( !alreadyFocused ) {
+					$.mobile.focusPage( toPage );
+				}
+
+				releasePageTransitionLock();
+				mpc.trigger( "pagechange", triggerData );
+			});
+	};
+
+	$.mobile.changePage.defaults = {
+		transition: undefined,
+		reverse: false,
+		changeHash: true,
+		fromHashChange: false,
+		role: undefined, // By default we rely on the role defined by the @data-role attribute.
+		duplicateCachedPage: undefined,
+		pageContainer: undefined,
+		showLoadMsg: true, //loading message shows by default when pages are being fetched during changePage
+		dataUrl: undefined,
+		fromPage: undefined,
+		allowSamePageTransition: false
+	};
+
+/* Event Bindings - hashchange, submit, and click */
+	function findClosestLink( ele )
+	{
+		while ( ele ) {
+			// Look for the closest element with a nodeName of "a".
+			// Note that we are checking if we have a valid nodeName
+			// before attempting to access it. This is because the
+			// node we get called with could have originated from within
+			// an embedded SVG document where some symbol instance elements
+			// don't have nodeName defined on them, or strings are of type
+			// SVGAnimatedString.
+			if ( ( typeof ele.nodeName === "string" ) && ele.nodeName.toLowerCase() === "a" ) {
+				break;
+			}
+			ele = ele.parentNode;
+		}
+		return ele;
+	}
+
+	// The base URL for any given element depends on the page it resides in.
+	function getClosestBaseUrl( ele )
+	{
+		// Find the closest page and extract out its url.
+		var url = $( ele ).closest( ".ui-page" ).jqmData( "url" ),
+			base = documentBase.hrefNoHash;
+
+		if ( !url || !path.isPath( url ) ) {
+			url = base;
+		}
+
+		return path.makeUrlAbsolute( url, base);
+	}
+
+	//The following event bindings should be bound after mobileinit has been triggered
+	//the following deferred is resolved in the init file
+	$.mobile.navreadyDeferred = $.Deferred();
+	$.mobile._registerInternalEvents = function() {
+		var getAjaxFormData = function( $form, calculateOnly ) {
+			var url, ret = true, formData, vclickedName, method;
+			
+			if ( !$.mobile.ajaxEnabled ||
+					// test that the form is, itself, ajax false
+					$form.is( ":jqmData(ajax='false')" ) ||
+					// test that $.mobile.ignoreContentEnabled is set and
+					// the form or one of it's parents is ajax=false
+					!$form.jqmHijackable().length ||
+					$form.attr( "target" ) ) {
+				return false;
+			}
+
+			url = $form.attr( "action" );
+			method = ( $form.attr( "method" ) || "get" ).toLowerCase();
+
+			// If no action is specified, browsers default to using the
+			// URL of the document containing the form. Since we dynamically
+			// pull in pages from external documents, the form should submit
+			// to the URL for the source document of the page containing
+			// the form.
+			if ( !url ) {
+				// Get the @data-url for the page containing the form.
+				url = getClosestBaseUrl( $form );
+
+				// NOTE: If the method is "get", we need to strip off the query string
+				// because it will get replaced with the new form data. See issue #5710.
+				if ( method === "get" ) {
+					url = path.parseUrl( url ).hrefNoSearch;
+				}
+
+				if ( url === documentBase.hrefNoHash ) {
+					// The url we got back matches the document base,
+					// which means the page must be an internal/embedded page,
+					// so default to using the actual document url as a browser
+					// would.
+					url = documentUrl.hrefNoSearch;
+				}
+			}
+
+			url = path.makeUrlAbsolute(  url, getClosestBaseUrl( $form ) );
+
+			if ( ( path.isExternal( url ) && !path.isPermittedCrossDomainRequest( documentUrl, url ) ) ) {
+				return false;
+			}
+
+			if ( !calculateOnly ) {
+				formData = $form.serializeArray();
+
+				if ( $lastVClicked && $lastVClicked[ 0 ].form === $form[ 0 ] ) {
+					vclickedName = $lastVClicked.attr( "name" );
+					if ( vclickedName ) {
+						// Make sure the last clicked element is included in the form
+						$.each( formData, function( key, value ) {
+							if ( value.name === vclickedName ) {
+								// Unset vclickedName - we've found it in the serialized data already
+								vclickedName = "";
+								return false;
+							}
+						});
+						if ( vclickedName ) {
+							formData.push( { name: vclickedName, value: $lastVClicked.attr( "value" ) } );
+						}
+					}
+				}
+
+				ret = {
+					url: url,
+					options: {
+						type:		method,
+						data:		$.param( formData ),
+						transition:	$form.jqmData( "transition" ),
+						reverse:	$form.jqmData( "direction" ) === "reverse",
+						reloadPage:	true
+					}
+				};
+			}
+
+			return ret;
+		};
+
+		//bind to form submit events, handle with Ajax
+		$.mobile.document.delegate( "form", "submit", function( event ) {
+			var formData = getAjaxFormData( $( this ) );
+
+			if ( formData ) {
+				$.mobile.changePage( formData.url, formData.options );
+				event.preventDefault();
+			}
+		});
+
+		//add active state on vclick
+		$.mobile.document.bind( "vclick", function( event ) {
+			var $btn, btnEls, target = event.target, needClosest = false;
+			// if this isn't a left click we don't care. Its important to note
+			// that when the virtual event is generated it will create the which attr
+			if ( event.which > 1 || !$.mobile.linkBindingEnabled ) {
+				return;
+			}
+
+			// Record that this element was clicked, in case we need it for correct
+			// form submission during the "submit" handler above
+			$lastVClicked = $( target );
+
+			// Try to find a target element to which the active class will be applied
+			if ( $.data( target, "mobile-button" ) ) {
+				// If the form will not be submitted via AJAX, do not add active class
+				if ( !getAjaxFormData( $( target ).closest( "form" ), true ) ) {
+					return;
+				}
+				// We will apply the active state to this button widget - the parent
+				// of the input that was clicked will have the associated data
+				if ( target.parentNode ) {
+					target = target.parentNode;
+				}
+			} else {
+				target = findClosestLink( target );
+				if ( !( target && path.parseUrl( target.getAttribute( "href" ) || "#" ).hash !== "#" ) ) {
+					return;
+				}
+
+				// TODO teach $.mobile.hijackable to operate on raw dom elements so the
+				// link wrapping can be avoided
+				if ( !$( target ).jqmHijackable().length ) {
+					return;
+				}
+			}
+
+			// Avoid calling .closest by using the data set during .buttonMarkup()
+			// List items have the button data in the parent of the element clicked
+			if ( !!~target.className.indexOf( "ui-link-inherit" ) ) {
+				if ( target.parentNode ) {
+					btnEls = $.data( target.parentNode, "buttonElements" );
+				}
+			// Otherwise, look for the data on the target itself
+			} else {
+				btnEls = $.data( target, "buttonElements" );
+			}
+			// If found, grab the button's outer element
+			if ( btnEls ) {
+				target = btnEls.outer;
+			} else {
+				needClosest = true;
+			}
+
+			$btn = $( target );
+			// If the outer element wasn't found by the our heuristics, use .closest()
+			if ( needClosest ) {
+				$btn = $btn.closest( ".ui-btn" );
+			}
+
+			if ( $btn.length > 0 && !$btn.hasClass( "ui-disabled" ) ) {
+				removeActiveLinkClass( true );
+				$activeClickedLink = $btn;
+				$activeClickedLink.addClass( $.mobile.activeBtnClass );
+			}
+		});
+
+		// click routing - direct to HTTP or Ajax, accordingly
+		$.mobile.document.bind( "click", function( event ) {
+			if ( !$.mobile.linkBindingEnabled || event.isDefaultPrevented() ) {
+				return;
+			}
+
+			var link = findClosestLink( event.target ), $link = $( link ), httpCleanup;
+
+			// If there is no link associated with the click or its not a left
+			// click we want to ignore the click
+			// TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping
+			// can be avoided
+			if ( !link || event.which > 1 || !$link.jqmHijackable().length ) {
+				return;
+			}
+
+			//remove active link class if external (then it won't be there if you come back)
+			httpCleanup = function() {
+				window.setTimeout(function() { removeActiveLinkClass( true ); }, 200 );
+			};
+
+			//if there's a data-rel=back attr, go back in history
+			if ( $link.is( ":jqmData(rel='back')" ) ) {
+				$.mobile.back();
+				return false;
+			}
+
+			var baseUrl = getClosestBaseUrl( $link ),
+
+				//get href, if defined, otherwise default to empty hash
+				href = path.makeUrlAbsolute( $link.attr( "href" ) || "#", baseUrl );
+
+			//if ajax is disabled, exit early
+			if ( !$.mobile.ajaxEnabled && !path.isEmbeddedPage( href ) ) {
+				httpCleanup();
+				//use default click handling
+				return;
+			}
+
+			// XXX_jblas: Ideally links to application pages should be specified as
+			//            an url to the application document with a hash that is either
+			//            the site relative path or id to the page. But some of the
+			//            internal code that dynamically generates sub-pages for nested
+			//            lists and select dialogs, just write a hash in the link they
+			//            create. This means the actual URL path is based on whatever
+			//            the current value of the base tag is at the time this code
+			//            is called. For now we are just assuming that any url with a
+			//            hash in it is an application page reference.
+			if ( href.search( "#" ) !== -1 ) {
+				href = href.replace( /[^#]*#/, "" );
+				if ( !href ) {
+					//link was an empty hash meant purely
+					//for interaction, so we ignore it.
+					event.preventDefault();
+					return;
+				} else if ( path.isPath( href ) ) {
+					//we have apath so make it the href we want to load.
+					href = path.makeUrlAbsolute( href, baseUrl );
+				} else {
+					//we have a simple id so use the documentUrl as its base.
+					href = path.makeUrlAbsolute( "#" + href, documentUrl.hrefNoHash );
+				}
+			}
+
+				// Should we handle this link, or let the browser deal with it?
+			var useDefaultUrlHandling = $link.is( "[rel='external']" ) || $link.is( ":jqmData(ajax='false')" ) || $link.is( "[target]" ),
+
+				// Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR
+				// requests if the document doing the request was loaded via the file:// protocol.
+				// This is usually to allow the application to "phone home" and fetch app specific
+				// data. We normally let the browser handle external/cross-domain urls, but if the
+				// allowCrossDomainPages option is true, we will allow cross-domain http/https
+				// requests to go through our page loading logic.
+
+				//check for protocol or rel and its not an embedded page
+				//TODO overlap in logic from isExternal, rel=external check should be
+				//     moved into more comprehensive isExternalLink
+				isExternal = useDefaultUrlHandling || ( path.isExternal( href ) && !path.isPermittedCrossDomainRequest( documentUrl, href ) );
+
+			if ( isExternal ) {
+				httpCleanup();
+				//use default click handling
+				return;
+			}
+
+			//use ajax
+			var transition = $link.jqmData( "transition" ),
+				reverse = $link.jqmData( "direction" ) === "reverse" ||
+							// deprecated - remove by 1.0
+							$link.jqmData( "back" ),
+
+				//this may need to be more specific as we use data-rel more
+				role = $link.attr( "data-" + $.mobile.ns + "rel" ) || undefined;
+
+			$.mobile.changePage( href, { transition: transition, reverse: reverse, role: role, link: $link } );
+			event.preventDefault();
+		});
+
+		//prefetch pages when anchors with data-prefetch are encountered
+		$.mobile.document.delegate( ".ui-page", "pageshow.prefetch", function() {
+			var urls = [];
+			$( this ).find( "a:jqmData(prefetch)" ).each(function() {
+				var $link = $( this ),
+					url = $link.attr( "href" );
+
+				if ( url && $.inArray( url, urls ) === -1 ) {
+					urls.push( url );
+
+					$.mobile.loadPage( url, { role: $link.attr( "data-" + $.mobile.ns + "rel" ),prefetch: true } );
+				}
+			});
+		});
+
+		$.mobile._handleHashChange = function( url, data ) {
+			//find first page via hash
+			var to = path.stripHash(url),
+				//transition is false if it's the first page, undefined otherwise (and may be overridden by default)
+				transition = $.mobile.urlHistory.stack.length === 0 ? "none" : undefined,
+
+				// default options for the changPage calls made after examining the current state
+				// of the page and the hash, NOTE that the transition is derived from the previous
+				// history entry
+				changePageOptions = {
+					changeHash: false,
+					fromHashChange: true,
+					reverse: data.direction === "back"
+				};
+
+			$.extend( changePageOptions, data, {
+				transition: (urlHistory.getLast() || {}).transition || transition
+			});
+
+			// special case for dialogs
+			if ( urlHistory.activeIndex > 0 && to.indexOf( dialogHashKey ) > -1 && urlHistory.initialDst !== to ) {
+
+				// If current active page is not a dialog skip the dialog and continue
+				// in the same direction
+				if ( $.mobile.activePage && !$.mobile.activePage.is( ".ui-dialog" ) ) {
+					//determine if we're heading forward or backward and continue accordingly past
+					//the current dialog
+					if( data.direction === "back" ) {
+						$.mobile.back();
+					} else {
+						window.history.forward();
+					}
+
+					// prevent changePage call
+					return;
+				} else {
+					// if the current active page is a dialog and we're navigating
+					// to a dialog use the dialog objected saved in the stack
+					to = data.pageUrl;
+					var active = $.mobile.urlHistory.getActive();
+
+					// make sure to set the role, transition and reversal
+					// as most of this is lost by the domCache cleaning
+					$.extend( changePageOptions, {
+						role: active.role,
+						transition: active.transition,
+						reverse: data.direction === "back"
+					});
+				}
+			}
+
+			//if to is defined, load it
+			if ( to ) {
+				// At this point, 'to' can be one of 3 things, a cached page element from
+				// a history stack entry, an id, or site-relative/absolute URL. If 'to' is
+				// an id, we need to resolve it against the documentBase, not the location.href,
+				// since the hashchange could've been the result of a forward/backward navigation
+				// that crosses from an external page/dialog to an internal page/dialog.
+				to = !path.isPath( to ) ? ( path.makeUrlAbsolute( '#' + to, documentBase ) ) : to;
+
+				// If we're about to go to an initial URL that contains a reference to a non-existent
+				// internal page, go to the first page instead. We know that the initial hash refers to a
+				// non-existent page, because the initial hash did not end up in the initial urlHistory entry
+				if ( to === path.makeUrlAbsolute( '#' + urlHistory.initialDst, documentBase ) &&
+					urlHistory.stack.length && urlHistory.stack[0].url !== urlHistory.initialDst.replace( dialogHashKey, "" ) ) {
+					to = $.mobile.firstPage;
+				}
+
+				$.mobile.changePage( to, changePageOptions );
+			}	else {
+
+				//there's no hash, go to the first page in the dom
+				$.mobile.changePage( $.mobile.firstPage, changePageOptions );
+			}
+		};
+
+		// TODO roll the logic here into the handleHashChange method
+		$window.bind( "navigate", function( e, data ) {
+			var url;
+
+			if ( e.originalEvent && e.originalEvent.isDefaultPrevented() ) {
+				return;
+			}
+
+			url = $.event.special.navigate.originalEventName.indexOf( "hashchange" ) > -1 ? data.state.hash : data.state.url;
+
+			if( !url ) {
+				url = $.mobile.path.parseLocation().hash;
+			}
+
+			if( !url || url === "#" || url.indexOf( "#" + $.mobile.path.uiStateKey ) === 0 ){
+				url = location.href;
+			}
+
+			$.mobile._handleHashChange( url, data.state );
+		});
+
+		//set page min-heights to be device specific
+		$.mobile.document.bind( "pageshow", $.mobile.resetActivePageHeight );
+		$.mobile.window.bind( "throttledresize", $.mobile.resetActivePageHeight );
+
+	};//navreadyDeferred done callback
+
+	$( function() { domreadyDeferred.resolve(); } );
+
+	$.when( domreadyDeferred, $.mobile.navreadyDeferred ).done( function() { $.mobile._registerInternalEvents(); } );
+})( jQuery );
+
+/*
+* fallback transition for flip in non-3D supporting browsers (which tend to handle complex transitions poorly in general
+*/
+
+(function( $, window, undefined ) {
+
+$.mobile.transitionFallbacks.flip = "fade";
+
+})( jQuery, this );
+/*
+* fallback transition for flow in non-3D supporting browsers (which tend to handle complex transitions poorly in general
+*/
+
+(function( $, window, undefined ) {
+
+$.mobile.transitionFallbacks.flow = "fade";
+
+})( jQuery, this );
+/*
+* fallback transition for pop in non-3D supporting browsers (which tend to handle complex transitions poorly in general
+*/
+
+(function( $, window, undefined ) {
+
+$.mobile.transitionFallbacks.pop = "fade";
+
+})( jQuery, this );
+/*
+* fallback transition for slide in non-3D supporting browsers (which tend to handle complex transitions poorly in general
+*/
+
+(function( $, window, undefined ) {
+
+// Use the simultaneous transitions handler for slide transitions
+$.mobile.transitionHandlers.slide = $.mobile.transitionHandlers.simultaneous;
+
+// Set the slide transitions's fallback to "fade"
+$.mobile.transitionFallbacks.slide = "fade";
+
+})( jQuery, this );
+/*
+* fallback transition for slidedown in non-3D supporting browsers (which tend to handle complex transitions poorly in general
+*/
+
+(function( $, window, undefined ) {
+
+$.mobile.transitionFallbacks.slidedown = "fade";
+
+})( jQuery, this );
+/*
+* fallback transition for slidefade in non-3D supporting browsers (which tend to handle complex transitions poorly in general
+*/
+
+(function( $, window, undefined ) {
+
+// Set the slide transitions's fallback to "fade"
+$.mobile.transitionFallbacks.slidefade = "fade";
+
+})( jQuery, this );
+/*
+* fallback transition for slideup in non-3D supporting browsers (which tend to handle complex transitions poorly in general
+*/
+
+(function( $, window, undefined ) {
+
+$.mobile.transitionFallbacks.slideup = "fade";
+
+})( jQuery, this );
+/*
+* fallback transition for turn in non-3D supporting browsers (which tend to handle complex transitions poorly in general
+*/
+
+(function( $, window, undefined ) {
+
+$.mobile.transitionFallbacks.turn = "fade";
+
+})( jQuery, this );
+
+(function( $, undefined ) {
+
+$.mobile.page.prototype.options.degradeInputs = {
+	color: false,
+	date: false,
+	datetime: false,
+	"datetime-local": false,
+	email: false,
+	month: false,
+	number: false,
+	range: "number",
+	search: "text",
+	tel: false,
+	time: false,
+	url: false,
+	week: false
+};
+
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+
+	var page = $.mobile.closestPageData( $( e.target ) ), options;
+
+	if ( !page ) {
+		return;
+	}
+
+	options = page.options;
+
+	// degrade inputs to avoid poorly implemented native functionality
+	$( e.target ).find( "input" ).not( page.keepNativeSelector() ).each(function() {
+		var $this = $( this ),
+			type = this.getAttribute( "type" ),
+			optType = options.degradeInputs[ type ] || "text";
+
+		if ( options.degradeInputs[ type ] ) {
+			var html = $( "<div>" ).html( $this.clone() ).html(),
+				// In IE browsers, the type sometimes doesn't exist in the cloned markup, so we replace the closing tag instead
+				hasType = html.indexOf( " type=" ) > -1,
+				findstr = hasType ? /\s+type=["']?\w+['"]?/ : /\/?>/,
+				repstr = " type=\"" + optType + "\" data-" + $.mobile.ns + "type=\"" + type + "\"" + ( hasType ? "" : ">" );
+
+			$this.replaceWith( html.replace( findstr, repstr ) );
+		}
+	});
+
+});
+
+})( jQuery );
+
+(function( $, window, undefined ) {
+
+$.widget( "mobile.dialog", $.mobile.widget, {
+	options: {
+		closeBtn: "left",
+		closeBtnText: "Close",
+		overlayTheme: "a",
+		corners: true,
+		initSelector: ":jqmData(role='dialog')"
+	},
+
+	// Override the theme set by the page plugin on pageshow
+	_handlePageBeforeShow: function() {
+		this._isCloseable = true;
+		if ( this.options.overlayTheme ) {
+			this.element
+				.page( "removeContainerBackground" )
+				.page( "setContainerBackground", this.options.overlayTheme );
+		}
+	},
+
+	_create: function() {
+		var self = this,
+			$el = this.element,
+			cornerClass = !!this.options.corners ? " ui-corner-all" : "",
+			dialogWrap = $( "<div/>", {
+					"role" : "dialog",
+					"class" : "ui-dialog-contain ui-overlay-shadow" + cornerClass
+				});
+
+		$el.addClass( "ui-dialog ui-overlay-" + this.options.overlayTheme );
+
+		// Class the markup for dialog styling
+		// Set aria role
+		$el.wrapInner( dialogWrap );
+
+		/* bind events
+			- clicks and submits should use the closing transition that the dialog opened with
+				unless a data-transition is specified on the link/form
+			- if the click was on the close button, or the link has a data-rel="back" it'll go back in history naturally
+		*/
+		$el.bind( "vclick submit", function( event ) {
+			var $target = $( event.target ).closest( event.type === "vclick" ? "a" : "form" ),
+				active;
+
+			if ( $target.length && !$target.jqmData( "transition" ) ) {
+
+				active = $.mobile.urlHistory.getActive() || {};
+
+				$target.attr( "data-" + $.mobile.ns + "transition", ( active.transition || $.mobile.defaultDialogTransition ) )
+					.attr( "data-" + $.mobile.ns + "direction", "reverse" );
+			}
+		});
+
+		this._on( $el, {
+			pagebeforeshow: "_handlePageBeforeShow"
+		});
+
+		$.extend( this, {
+			_createComplete: false
+		});
+
+		this._setCloseBtn( this.options.closeBtn );
+	},
+
+	_setCloseBtn: function( value ) {
+		var self = this, btn, location;
+
+		if ( this._headerCloseButton ) {
+			this._headerCloseButton.remove();
+			this._headerCloseButton = null;
+		}
+		if ( value !== "none" ) {
+			// Sanitize value
+			location = ( value === "left" ? "left" : "right" );
+			btn = $( "<a href='#' class='ui-btn-" + location + "' data-" + $.mobile.ns + "icon='delete' data-" + $.mobile.ns + "iconpos='notext'>"+ this.options.closeBtnText + "</a>" );
+			this.element.children().find( ":jqmData(role='header')" ).first().prepend( btn );
+			if ( this._createComplete && $.fn.buttonMarkup ) {
+				btn.buttonMarkup();
+			}
+			this._createComplete = true;
+
+			// this must be an anonymous function so that select menu dialogs can replace
+			// the close method. This is a change from previously just defining data-rel=back
+			// on the button and letting nav handle it
+			//
+			// Use click rather than vclick in order to prevent the possibility of unintentionally
+			// reopening the dialog if the dialog opening item was directly under the close button.
+			btn.bind( "click", function() {
+				self.close();
+			});
+
+			this._headerCloseButton = btn;
+		}
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "closeBtn" ) {
+			this._setCloseBtn( value );
+		}
+		this._super( key, value );
+	},
+
+	// Close method goes back in history
+	close: function() {
+		var idx, dst, hist = $.mobile.navigate.history;
+
+		if ( this._isCloseable ) {
+			this._isCloseable = false;
+			// If the hash listening is enabled and there is at least one preceding history
+			// entry it's ok to go back. Initial pages with the dialog hash state are an example
+			// where the stack check is necessary
+			if ( $.mobile.hashListeningEnabled && hist.activeIndex > 0 ) {
+				$.mobile.back();
+			} else {
+				idx = Math.max( 0, hist.activeIndex - 1 );
+				dst = hist.stack[ idx ].pageUrl || hist.stack[ idx ].url;
+				hist.previousIndex = hist.activeIndex;
+				hist.activeIndex = idx;
+				if ( !$.mobile.path.isPath( dst ) ) {
+					dst = $.mobile.path.makeUrlAbsolute( "#" + dst );
+				}
+
+				$.mobile.changePage( dst, { direction: "back", changeHash: false, fromHashChange: true } );
+			}
+		}
+	}
+});
+
+//auto self-init widgets
+$.mobile.document.delegate( $.mobile.dialog.prototype.options.initSelector, "pagecreate", function() {
+	$.mobile.dialog.prototype.enhance( this );
+});
+
+})( jQuery, this );
+
+(function( $, undefined ) {
+
+$.mobile.page.prototype.options.backBtnText  = "Back";
+$.mobile.page.prototype.options.addBackBtn   = false;
+$.mobile.page.prototype.options.backBtnTheme = null;
+$.mobile.page.prototype.options.headerTheme  = "a";
+$.mobile.page.prototype.options.footerTheme  = "a";
+$.mobile.page.prototype.options.contentTheme = null;
+
+// NOTE bind used to force this binding to run before the buttonMarkup binding
+//      which expects .ui-footer top be applied in its gigantic selector
+// TODO remove the buttonMarkup giant selector and move it to the various modules
+//      on which it depends
+$.mobile.document.bind( "pagecreate", function( e ) {
+	var $page = $( e.target ),
+		o = $page.data( "mobile-page" ).options,
+		pageRole = $page.jqmData( "role" ),
+		pageTheme = o.theme;
+
+	$( ":jqmData(role='header'), :jqmData(role='footer'), :jqmData(role='content')", $page )
+		.jqmEnhanceable()
+		.each(function() {
+
+		var $this = $( this ),
+			role = $this.jqmData( "role" ),
+			theme = $this.jqmData( "theme" ),
+			contentTheme = theme || o.contentTheme || ( pageRole === "dialog" && pageTheme ),
+			$headeranchors,
+			leftbtn,
+			rightbtn,
+			backBtn;
+
+		$this.addClass( "ui-" + role );
+
+		//apply theming and markup modifications to page,header,content,footer
+		if ( role === "header" || role === "footer" ) {
+
+			var thisTheme = theme || ( role === "header" ? o.headerTheme : o.footerTheme ) || pageTheme;
+
+			$this
+				//add theme class
+				.addClass( "ui-bar-" + thisTheme )
+				// Add ARIA role
+				.attr( "role", role === "header" ? "banner" : "contentinfo" );
+
+			if ( role === "header") {
+				// Right,left buttons
+				$headeranchors	= $this.children( "a, button" );
+				leftbtn	= $headeranchors.hasClass( "ui-btn-left" );
+				rightbtn = $headeranchors.hasClass( "ui-btn-right" );
+
+				leftbtn = leftbtn || $headeranchors.eq( 0 ).not( ".ui-btn-right" ).addClass( "ui-btn-left" ).length;
+
+				rightbtn = rightbtn || $headeranchors.eq( 1 ).addClass( "ui-btn-right" ).length;
+			}
+
+			// Auto-add back btn on pages beyond first view
+			if ( o.addBackBtn &&
+				role === "header" &&
+				$( ".ui-page" ).length > 1 &&
+				$page.jqmData( "url" ) !== $.mobile.path.stripHash( location.hash ) &&
+				!leftbtn ) {
+
+				backBtn = $( "<a href='javascript:void(0);' class='ui-btn-left' data-"+ $.mobile.ns +"rel='back' data-"+ $.mobile.ns +"icon='arrow-l'>"+ o.backBtnText +"</a>" )
+					// If theme is provided, override default inheritance
+					.attr( "data-"+ $.mobile.ns +"theme", o.backBtnTheme || thisTheme )
+					.prependTo( $this );
+			}
+
+			// Page title
+			$this.children( "h1, h2, h3, h4, h5, h6" )
+				.addClass( "ui-title" )
+				// Regardless of h element number in src, it becomes h1 for the enhanced page
+				.attr({
+					"role": "heading",
+					"aria-level": "1"
+				});
+
+		} else if ( role === "content" ) {
+			if ( contentTheme ) {
+				$this.addClass( "ui-body-" + ( contentTheme ) );
+			}
+
+			// Add ARIA role
+			$this.attr( "role", "main" );
+		}
+	});
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+// This function calls getAttribute, which should be safe for data-* attributes
+var getAttrFixed = function( e, key ) {
+	var value = e.getAttribute( key );
+
+	return value === "true" ? true :
+		value === "false" ? false :
+		value === null ? undefined : value;
+};
+
+$.fn.buttonMarkup = function( options ) {
+	var $workingSet = this,
+		nsKey = "data-" + $.mobile.ns,
+		key;
+
+	// Enforce options to be of type string
+	options = ( options && ( $.type( options ) === "object" ) )? options : {};
+	for ( var i = 0; i < $workingSet.length; i++ ) {
+		var el = $workingSet.eq( i ),
+			e = el[ 0 ],
+			o = $.extend( {}, $.fn.buttonMarkup.defaults, {
+				icon:       options.icon       !== undefined ? options.icon       : getAttrFixed( e, nsKey + "icon" ),
+				iconpos:    options.iconpos    !== undefined ? options.iconpos    : getAttrFixed( e, nsKey + "iconpos" ),
+				theme:      options.theme      !== undefined ? options.theme      : getAttrFixed( e, nsKey + "theme" ) || $.mobile.getInheritedTheme( el, "c" ),
+				inline:     options.inline     !== undefined ? options.inline     : getAttrFixed( e, nsKey + "inline" ),
+				shadow:     options.shadow     !== undefined ? options.shadow     : getAttrFixed( e, nsKey + "shadow" ),
+				corners:    options.corners    !== undefined ? options.corners    : getAttrFixed( e, nsKey + "corners" ),
+				iconshadow: options.iconshadow !== undefined ? options.iconshadow : getAttrFixed( e, nsKey + "iconshadow" ),
+				mini:       options.mini       !== undefined ? options.mini       : getAttrFixed( e, nsKey + "mini" )
+			}, options ),
+
+			// Classes Defined
+			innerClass = "ui-btn-inner",
+			textClass = "ui-btn-text",
+			buttonClass, iconClass,
+			hover = false,
+			state = "up",
+			// Button inner markup
+			buttonInner,
+			buttonText,
+			buttonIcon,
+			buttonElements;
+
+		for ( key in o ) {
+			if ( o[ key ] === undefined || o[ key ] === null ) {
+				el.removeAttr( nsKey + key );
+			} else {
+				e.setAttribute( nsKey + key, o[ key ] );
+			}
+		}
+
+		if ( getAttrFixed( e, nsKey + "rel" ) === "popup" && el.attr( "href" ) ) {
+			e.setAttribute( "aria-haspopup", true );
+			e.setAttribute( "aria-owns", el.attr( "href" ) );
+		}
+
+		// Check if this element is already enhanced
+		buttonElements = $.data( ( ( e.tagName === "INPUT" || e.tagName === "BUTTON" ) ? e.parentNode : e ), "buttonElements" );
+
+		if ( buttonElements ) {
+			e = buttonElements.outer;
+			el = $( e );
+			buttonInner = buttonElements.inner;
+			buttonText = buttonElements.text;
+			// We will recreate this icon below
+			$( buttonElements.icon ).remove();
+			buttonElements.icon = null;
+			hover = buttonElements.hover;
+			state = buttonElements.state;
+		}
+		else {
+			buttonInner = document.createElement( o.wrapperEls );
+			buttonText = document.createElement( o.wrapperEls );
+		}
+		buttonIcon = o.icon ? document.createElement( "span" ) : null;
+
+		if ( attachEvents && !buttonElements ) {
+			attachEvents();
+		}
+
+		// if not, try to find closest theme container
+		if ( !o.theme ) {
+			o.theme = $.mobile.getInheritedTheme( el, "c" );
+		}
+
+		buttonClass = "ui-btn ";
+		buttonClass += ( hover ? "ui-btn-hover-" + o.theme : "" );
+		buttonClass += ( state ? " ui-btn-" + state + "-" + o.theme : "" );
+		buttonClass += o.shadow ? " ui-shadow" : "";
+		buttonClass += o.corners ? " ui-btn-corner-all" : "";
+
+		if ( o.mini !== undefined ) {
+			// Used to control styling in headers/footers, where buttons default to `mini` style.
+			buttonClass += o.mini === true ? " ui-mini" : " ui-fullsize";
+		}
+
+		if ( o.inline !== undefined ) {
+			// Used to control styling in headers/footers, where buttons default to `inline` style.
+			buttonClass += o.inline === true ? " ui-btn-inline" : " ui-btn-block";
+		}
+
+		if ( o.icon ) {
+			o.icon = "ui-icon-" + o.icon;
+			o.iconpos = o.iconpos || "left";
+
+			iconClass = "ui-icon " + o.icon;
+
+			if ( o.iconshadow ) {
+				iconClass += " ui-icon-shadow";
+			}
+		}
+
+		if ( o.iconpos ) {
+			buttonClass += " ui-btn-icon-" + o.iconpos;
+
+			if ( o.iconpos === "notext" && !el.attr( "title" ) ) {
+				el.attr( "title", el.getEncodedText() );
+			}
+		}
+
+		if ( buttonElements ) {
+			el.removeClass( buttonElements.bcls || "" );
+		}
+		el.removeClass( "ui-link" ).addClass( buttonClass );
+
+		buttonInner.className = innerClass;
+		buttonText.className = textClass;
+		if ( !buttonElements ) {
+			buttonInner.appendChild( buttonText );
+		}
+		if ( buttonIcon ) {
+			buttonIcon.className = iconClass;
+			if ( !( buttonElements && buttonElements.icon ) ) {
+				buttonIcon.innerHTML = "&#160;";
+				buttonInner.appendChild( buttonIcon );
+			}
+		}
+
+		while ( e.firstChild && !buttonElements ) {
+			buttonText.appendChild( e.firstChild );
+		}
+
+		if ( !buttonElements ) {
+			e.appendChild( buttonInner );
+		}
+
+		// Assign a structure containing the elements of this button to the elements of this button. This
+		// will allow us to recognize this as an already-enhanced button in future calls to buttonMarkup().
+		buttonElements = {
+			hover : hover,
+			state : state,
+			bcls  : buttonClass,
+			outer : e,
+			inner : buttonInner,
+			text  : buttonText,
+			icon  : buttonIcon
+		};
+
+		$.data( e,           'buttonElements', buttonElements );
+		$.data( buttonInner, 'buttonElements', buttonElements );
+		$.data( buttonText,  'buttonElements', buttonElements );
+		if ( buttonIcon ) {
+			$.data( buttonIcon, 'buttonElements', buttonElements );
+		}
+	}
+
+	return this;
+};
+
+$.fn.buttonMarkup.defaults = {
+	corners: true,
+	shadow: true,
+	iconshadow: true,
+	wrapperEls: "span"
+};
+
+function closestEnabledButton( element ) {
+    var cname;
+
+    while ( element ) {
+		// Note that we check for typeof className below because the element we
+		// handed could be in an SVG DOM where className on SVG elements is defined to
+		// be of a different type (SVGAnimatedString). We only operate on HTML DOM
+		// elements, so we look for plain "string".
+        cname = ( typeof element.className === 'string' ) && ( element.className + ' ' );
+        if ( cname && cname.indexOf( "ui-btn " ) > -1 && cname.indexOf( "ui-disabled " ) < 0 ) {
+            break;
+        }
+
+        element = element.parentNode;
+    }
+
+    return element;
+}
+
+function updateButtonClass( $btn, classToRemove, classToAdd, hover, state ) {
+	var buttonElements = $.data( $btn[ 0 ], "buttonElements" );
+	$btn.removeClass( classToRemove ).addClass( classToAdd );
+	if ( buttonElements ) {
+		buttonElements.bcls = $( document.createElement( "div" ) )
+			.addClass( buttonElements.bcls + " " + classToAdd )
+			.removeClass( classToRemove )
+			.attr( "class" );
+		if ( hover !== undefined ) {
+			buttonElements.hover = hover;
+		}
+		buttonElements.state = state;
+	}
+}
+
+var attachEvents = function() {
+	var hoverDelay = $.mobile.buttonMarkup.hoverDelay, hov, foc;
+
+	$.mobile.document.bind( {
+		"vmousedown vmousecancel vmouseup vmouseover vmouseout focus blur scrollstart": function( event ) {
+			var theme,
+				$btn = $( closestEnabledButton( event.target ) ),
+				isTouchEvent = event.originalEvent && /^touch/.test( event.originalEvent.type ),
+				evt = event.type;
+
+			if ( $btn.length ) {
+				theme = $btn.attr( "data-" + $.mobile.ns + "theme" );
+
+				if ( evt === "vmousedown" ) {
+					if ( isTouchEvent ) {
+						// Use a short delay to determine if the user is scrolling before highlighting
+						hov = setTimeout( function() {
+							updateButtonClass( $btn, "ui-btn-up-" + theme, "ui-btn-down-" + theme, undefined, "down" );
+						}, hoverDelay );
+					} else {
+						updateButtonClass( $btn, "ui-btn-up-" + theme, "ui-btn-down-" + theme, undefined, "down" );
+					}
+				} else if ( evt === "vmousecancel" || evt === "vmouseup" ) {
+					updateButtonClass( $btn, "ui-btn-down-" + theme, "ui-btn-up-" + theme, undefined, "up" );
+				} else if ( evt === "vmouseover" || evt === "focus" ) {
+					if ( isTouchEvent ) {
+						// Use a short delay to determine if the user is scrolling before highlighting
+						foc = setTimeout( function() {
+							updateButtonClass( $btn, "ui-btn-up-" + theme, "ui-btn-hover-" + theme, true, "" );
+						}, hoverDelay );
+					} else {
+						updateButtonClass( $btn, "ui-btn-up-" + theme, "ui-btn-hover-" + theme, true, "" );
+					}
+				} else if ( evt === "vmouseout" || evt === "blur" || evt === "scrollstart" ) {
+					updateButtonClass( $btn, "ui-btn-hover-" + theme  + " ui-btn-down-" + theme, "ui-btn-up-" + theme, false, "up" );
+					if ( hov ) {
+						clearTimeout( hov );
+					}
+					if ( foc ) {
+						clearTimeout( foc );
+					}
+				}
+			}
+		},
+		"focusin focus": function( event ) {
+			$( closestEnabledButton( event.target ) ).addClass( $.mobile.focusClass );
+		},
+		"focusout blur": function( event ) {
+			$( closestEnabledButton( event.target ) ).removeClass( $.mobile.focusClass );
+		}
+	});
+
+	attachEvents = null;
+};
+
+//links in bars, or those with  data-role become buttons
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+
+	$( ":jqmData(role='button'), .ui-bar > a, .ui-header > a, .ui-footer > a, .ui-bar > :jqmData(role='controlgroup') > a", e.target )
+		.jqmEnhanceable()
+		.not( "button, input, .ui-btn, :jqmData(role='none'), :jqmData(role='nojs')" )
+		.buttonMarkup();
+});
+
+})( jQuery );
+
+
+(function( $, undefined ) {
+
+$.widget( "mobile.collapsible", $.mobile.widget, {
+	options: {
+		expandCueText: " click to expand contents",
+		collapseCueText: " click to collapse contents",
+		collapsed: true,
+		heading: "h1,h2,h3,h4,h5,h6,legend",
+		collapsedIcon: "plus",
+		expandedIcon: "minus",
+		iconpos: "left",
+		theme: null,
+		contentTheme: null,
+		inset: true,
+		corners: true,
+		mini: false,
+		initSelector: ":jqmData(role='collapsible')"
+	},
+	_create: function() {
+
+		var $el = this.element,
+			o = this.options,
+			collapsible = $el.addClass( "ui-collapsible" ),
+			collapsibleHeading = $el.children( o.heading ).first(),
+			collapsibleContent = collapsible.wrapInner( "<div class='ui-collapsible-content'></div>" ).children( ".ui-collapsible-content" ),
+			collapsibleSet = $el.closest( ":jqmData(role='collapsible-set')" ).addClass( "ui-collapsible-set" ),
+			collapsibleClasses = "";
+
+		// Replace collapsibleHeading if it's a legend
+		if ( collapsibleHeading.is( "legend" ) ) {
+			collapsibleHeading = $( "<div role='heading'>"+ collapsibleHeading.html() +"</div>" ).insertBefore( collapsibleHeading );
+			collapsibleHeading.next().remove();
+		}
+
+		// If we are in a collapsible set
+		if ( collapsibleSet.length ) {
+			// Inherit the theme from collapsible-set
+			if ( !o.theme ) {
+				o.theme = collapsibleSet.jqmData( "theme" ) || $.mobile.getInheritedTheme( collapsibleSet, "c" );
+			}
+			// Inherit the content-theme from collapsible-set
+			if ( !o.contentTheme ) {
+				o.contentTheme = collapsibleSet.jqmData( "content-theme" );
+			}
+
+			// Get the preference for collapsed icon in the set, but override with data- attribute on the individual collapsible
+			o.collapsedIcon = $el.jqmData( "collapsed-icon" ) || collapsibleSet.jqmData( "collapsed-icon" ) || o.collapsedIcon;
+
+			// Get the preference for expanded icon in the set, but override with data- attribute on the individual collapsible
+			o.expandedIcon = $el.jqmData( "expanded-icon" ) || collapsibleSet.jqmData( "expanded-icon" ) || o.expandedIcon;
+
+			// Gets the preference icon position in the set, but override with data- attribute on the individual collapsible
+			o.iconpos = $el.jqmData( "iconpos" ) || collapsibleSet.jqmData( "iconpos" ) || o.iconpos;
+
+			// Inherit the preference for inset from collapsible-set or set the default value to ensure equalty within a set
+			if ( collapsibleSet.jqmData( "inset" ) !== undefined ) {
+				o.inset = collapsibleSet.jqmData( "inset" );
+			} else {
+				o.inset = true;
+			}
+			// Set corners for individual collapsibles to false when in a collapsible-set
+			o.corners = false;
+			// Gets the preference for mini in the set
+			if ( !o.mini ) {
+				o.mini = collapsibleSet.jqmData( "mini" );
+			}
+		} else {
+			// get inherited theme if not a set and no theme has been set
+			if ( !o.theme ) {
+				o.theme = $.mobile.getInheritedTheme( $el, "c" );
+			}
+		}
+
+		if ( !!o.inset ) {
+			collapsibleClasses += " ui-collapsible-inset";
+			if ( !!o.corners ) {
+				collapsibleClasses += " ui-corner-all" ;
+			}
+		}
+		if ( o.contentTheme ) {
+			collapsibleClasses += " ui-collapsible-themed-content";
+			collapsibleContent.addClass( "ui-body-" + o.contentTheme );
+		}
+		if ( collapsibleClasses !== "" ) {
+			collapsible.addClass( collapsibleClasses );
+		}
+		
+		collapsibleHeading
+			//drop heading in before content
+			.insertBefore( collapsibleContent )
+			//modify markup & attributes
+			.addClass( "ui-collapsible-heading" )
+			.append( "<span class='ui-collapsible-heading-status'></span>" )
+			.wrapInner( "<a href='#' class='ui-collapsible-heading-toggle'></a>" )
+			.find( "a" )
+				.first()
+				.buttonMarkup({
+					shadow: false,
+					corners: false,
+					iconpos: o.iconpos,
+					icon: o.collapsedIcon,
+					mini: o.mini,
+					theme: o.theme
+				});
+
+		//events
+		collapsible
+			.bind( "expand collapse", function( event ) {
+				if ( !event.isDefaultPrevented() ) {
+					var $this = $( this ),
+						isCollapse = ( event.type === "collapse" );
+
+					event.preventDefault();
+
+					collapsibleHeading
+						.toggleClass( "ui-collapsible-heading-collapsed", isCollapse )
+						.find( ".ui-collapsible-heading-status" )
+							.text( isCollapse ? o.expandCueText : o.collapseCueText )
+						.end()
+						.find( ".ui-icon" )
+							.toggleClass( "ui-icon-" + o.expandedIcon, !isCollapse )
+							// logic or cause same icon for expanded/collapsed state would remove the ui-icon-class
+							.toggleClass( "ui-icon-" + o.collapsedIcon, ( isCollapse || o.expandedIcon === o.collapsedIcon ) )
+						.end()
+						.find( "a" ).first().removeClass( $.mobile.activeBtnClass );
+
+					$this.toggleClass( "ui-collapsible-collapsed", isCollapse );
+					collapsibleContent.toggleClass( "ui-collapsible-content-collapsed", isCollapse ).attr( "aria-hidden", isCollapse );
+
+					collapsibleContent.trigger( "updatelayout" );
+				}
+			})
+			.trigger( o.collapsed ? "collapse" : "expand" );
+
+		collapsibleHeading
+			.bind( "tap", function( event ) {
+				collapsibleHeading.find( "a" ).first().addClass( $.mobile.activeBtnClass );
+			})
+			.bind( "click", function( event ) {
+
+				var type = collapsibleHeading.is( ".ui-collapsible-heading-collapsed" ) ? "expand" : "collapse";
+
+				collapsible.trigger( type );
+
+				event.preventDefault();
+				event.stopPropagation();
+			});
+	}
+});
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+	$.mobile.collapsible.prototype.enhanceWithin( e.target );
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.mobile.behaviors.addFirstLastClasses = {
+	_getVisibles: function( $els, create ) {
+		var visibles;
+
+		if ( create ) {
+			visibles = $els.not( ".ui-screen-hidden" );
+		} else {
+			visibles = $els.filter( ":visible" );
+			if ( visibles.length === 0 ) {
+				visibles = $els.not( ".ui-screen-hidden" );
+			}
+		}
+
+		return visibles;
+	},
+
+	_addFirstLastClasses: function( $els, $visibles, create ) {
+		$els.removeClass( "ui-first-child ui-last-child" );
+		$visibles.eq( 0 ).addClass( "ui-first-child" ).end().last().addClass( "ui-last-child" );
+		if ( !create ) {
+			this.element.trigger( "updatelayout" );
+		}
+	}
+};
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.widget( "mobile.collapsibleset", $.mobile.widget, $.extend( {
+	options: {
+		initSelector: ":jqmData(role='collapsible-set')"
+	},
+	_create: function() {
+		var $el = this.element.addClass( "ui-collapsible-set" ),
+			o = this.options;
+
+		// Inherit the theme from collapsible-set
+		if ( !o.theme ) {
+			o.theme = $.mobile.getInheritedTheme( $el, "c" );
+		}
+		// Inherit the content-theme from collapsible-set
+		if ( !o.contentTheme ) {
+			o.contentTheme = $el.jqmData( "content-theme" );
+		}
+		// Inherit the corner styling from collapsible-set
+		if ( !o.corners ) {
+			o.corners = $el.jqmData( "corners" );
+		}
+		
+		if ( $el.jqmData( "inset" ) !== undefined ) {
+			o.inset = $el.jqmData( "inset" );
+		}
+		o.inset = o.inset !== undefined ? o.inset : true;
+		o.corners = o.corners !== undefined ? o.corners : true;
+		
+		if ( !!o.corners && !!o.inset ) {
+			$el.addClass( "ui-corner-all" );
+		}
+
+		// Initialize the collapsible set if it's not already initialized
+		if ( !$el.jqmData( "collapsiblebound" ) ) {
+			$el
+				.jqmData( "collapsiblebound", true )
+				.bind( "expand", function( event ) {
+					var closestCollapsible = $( event.target )
+						.closest( ".ui-collapsible" );
+					if ( closestCollapsible.parent().is( ":jqmData(role='collapsible-set')" ) ) {
+						closestCollapsible
+							.siblings( ".ui-collapsible" )
+							.trigger( "collapse" );
+					}
+				});
+		}
+	},
+
+	_init: function() {
+		var $el = this.element,
+			collapsiblesInSet = $el.children( ":jqmData(role='collapsible')" ),
+			expanded = collapsiblesInSet.filter( ":jqmData(collapsed='false')" );
+		this._refresh( "true" );
+
+		// Because the corners are handled by the collapsible itself and the default state is collapsed
+		// That was causing https://github.com/jquery/jquery-mobile/issues/4116
+		expanded.trigger( "expand" );
+	},
+
+	_refresh: function( create ) {
+		var collapsiblesInSet = this.element.children( ":jqmData(role='collapsible')" );
+
+		$.mobile.collapsible.prototype.enhance( collapsiblesInSet.not( ".ui-collapsible" ) );
+
+		this._addFirstLastClasses( collapsiblesInSet, this._getVisibles( collapsiblesInSet, create ), create );
+	},
+
+	refresh: function() {
+		this._refresh( false );
+	}
+}, $.mobile.behaviors.addFirstLastClasses ) );
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+	$.mobile.collapsibleset.prototype.enhanceWithin( e.target );
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+// filter function removes whitespace between label and form element so we can use inline-block (nodeType 3 = text)
+$.fn.fieldcontain = function( options ) {
+	return this
+		.addClass( "ui-field-contain ui-body ui-br" )
+		.contents().filter( function() {
+			return ( this.nodeType === 3 && !/\S/.test( this.nodeValue ) );
+		}).remove();
+};
+
+//auto self-init widgets
+$( document ).bind( "pagecreate create", function( e ) {
+	$( ":jqmData(role='fieldcontain')", e.target ).jqmEnhanceable().fieldcontain();
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.fn.grid = function( options ) {
+	return this.each(function() {
+
+		var $this = $( this ),
+			o = $.extend({
+				grid: null
+			}, options ),
+			$kids = $this.children(),
+			gridCols = { solo:1, a:2, b:3, c:4, d:5 },
+			grid = o.grid,
+			iterator;
+
+			if ( !grid ) {
+				if ( $kids.length <= 5 ) {
+					for ( var letter in gridCols ) {
+						if ( gridCols[ letter ] === $kids.length ) {
+							grid = letter;
+						}
+					}
+				} else {
+					grid = "a";
+					$this.addClass( "ui-grid-duo" );
+				}
+			}
+			iterator = gridCols[grid];
+
+		$this.addClass( "ui-grid-" + grid );
+
+		$kids.filter( ":nth-child(" + iterator + "n+1)" ).addClass( "ui-block-a" );
+
+		if ( iterator > 1 ) {
+			$kids.filter( ":nth-child(" + iterator + "n+2)" ).addClass( "ui-block-b" );
+		}
+		if ( iterator > 2 ) {
+			$kids.filter( ":nth-child(" + iterator + "n+3)" ).addClass( "ui-block-c" );
+		}
+		if ( iterator > 3 ) {
+			$kids.filter( ":nth-child(" + iterator + "n+4)" ).addClass( "ui-block-d" );
+		}
+		if ( iterator > 4 ) {
+			$kids.filter( ":nth-child(" + iterator + "n+5)" ).addClass( "ui-block-e" );
+		}
+	});
+};
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.widget( "mobile.navbar", $.mobile.widget, {
+	options: {
+		iconpos: "top",
+		grid: null,
+		initSelector: ":jqmData(role='navbar')"
+	},
+
+	_create: function() {
+
+		var $navbar = this.element,
+			$navbtns = $navbar.find( "a" ),
+			iconpos = $navbtns.filter( ":jqmData(icon)" ).length ?
+									this.options.iconpos : undefined;
+
+		$navbar.addClass( "ui-navbar ui-mini" )
+			.attr( "role", "navigation" )
+			.find( "ul" )
+			.jqmEnhanceable()
+			.grid({ grid: this.options.grid });
+
+		$navbtns.buttonMarkup({
+			corners:	false,
+			shadow:		false,
+			inline:     true,
+			iconpos:	iconpos
+		});
+
+		$navbar.delegate( "a", "vclick", function( event ) {
+			// ui-btn-inner is returned as target
+			var target = $( event.target ).is( "a" ) ? $( this ) : $( this ).parent( "a" );
+			
+			if ( !target.is( ".ui-disabled, .ui-btn-active" ) ) {
+				$navbtns.removeClass( $.mobile.activeBtnClass );
+				$( this ).addClass( $.mobile.activeBtnClass );
+				
+				// The code below is a workaround to fix #1181
+				var activeBtn = $( this );
+				
+				$( document ).one( "pagehide", function() {
+					activeBtn.removeClass( $.mobile.activeBtnClass );
+				});
+			}
+		});
+
+		// Buttons in the navbar with ui-state-persist class should regain their active state before page show
+		$navbar.closest( ".ui-page" ).bind( "pagebeforeshow", function() {
+			$navbtns.filter( ".ui-state-persist" ).addClass( $.mobile.activeBtnClass );
+		});
+	}
+});
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+	$.mobile.navbar.prototype.enhanceWithin( e.target );
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+//Keeps track of the number of lists per page UID
+//This allows support for multiple nested list in the same page
+//https://github.com/jquery/jquery-mobile/issues/1617
+var listCountPerPage = {};
+
+$.widget( "mobile.listview", $.mobile.widget, $.extend( {
+
+	options: {
+		theme: null,
+		countTheme: "c",
+		headerTheme: "b",
+		dividerTheme: "b",
+		icon: "arrow-r",
+		splitIcon: "arrow-r",
+		splitTheme: "b",
+		corners: true,
+		shadow: true,
+		inset: false,
+		initSelector: ":jqmData(role='listview')"
+	},
+
+	_create: function() {
+		var t = this,
+			listviewClasses = "";
+
+		listviewClasses += t.options.inset ? " ui-listview-inset" : "";
+
+		if ( !!t.options.inset ) {
+			listviewClasses += t.options.corners ? " ui-corner-all" : "";
+			listviewClasses += t.options.shadow ? " ui-shadow" : "";
+		}
+
+		// create listview markup
+		t.element.addClass(function( i, orig ) {
+			return orig + " ui-listview" + listviewClasses;
+		});
+
+		t.refresh( true );
+	},
+
+	// This is a generic utility method for finding the first
+	// node with a given nodeName. It uses basic DOM traversal
+	// to be fast and is meant to be a substitute for simple
+	// $.fn.closest() and $.fn.children() calls on a single
+	// element. Note that callers must pass both the lowerCase
+	// and upperCase version of the nodeName they are looking for.
+	// The main reason for this is that this function will be
+	// called many times and we want to avoid having to lowercase
+	// the nodeName from the element every time to ensure we have
+	// a match. Note that this function lives here for now, but may
+	// be moved into $.mobile if other components need a similar method.
+	_findFirstElementByTagName: function( ele, nextProp, lcName, ucName ) {
+		var dict = {};
+		dict[ lcName ] = dict[ ucName ] = true;
+		while ( ele ) {
+			if ( dict[ ele.nodeName ] ) {
+				return ele;
+			}
+			ele = ele[ nextProp ];
+		}
+		return null;
+	},
+	_getChildrenByTagName: function( ele, lcName, ucName ) {
+		var results = [],
+			dict = {};
+		dict[ lcName ] = dict[ ucName ] = true;
+		ele = ele.firstChild;
+		while ( ele ) {
+			if ( dict[ ele.nodeName ] ) {
+				results.push( ele );
+			}
+			ele = ele.nextSibling;
+		}
+		return $( results );
+	},
+
+	_addThumbClasses: function( containers ) {
+		var i, img, len = containers.length;
+		for ( i = 0; i < len; i++ ) {
+			img = $( this._findFirstElementByTagName( containers[ i ].firstChild, "nextSibling", "img", "IMG" ) );
+			if ( img.length ) {
+				img.addClass( "ui-li-thumb" );
+				$( this._findFirstElementByTagName( img[ 0 ].parentNode, "parentNode", "li", "LI" ) ).addClass( img.is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" );
+			}
+		}
+	},
+
+	refresh: function( create ) {
+		this.parentPage = this.element.closest( ".ui-page" );
+		this._createSubPages();
+
+		var o = this.options,
+			$list = this.element,
+			self = this,
+			dividertheme = $list.jqmData( "dividertheme" ) || o.dividerTheme,
+			listsplittheme = $list.jqmData( "splittheme" ),
+			listspliticon = $list.jqmData( "spliticon" ),
+			listicon = $list.jqmData( "icon" ),
+			li = this._getChildrenByTagName( $list[ 0 ], "li", "LI" ),
+			ol = !!$.nodeName( $list[ 0 ], "ol" ),
+			jsCount = !$.support.cssPseudoElement,
+			start = $list.attr( "start" ),
+			itemClassDict = {},
+			item, itemClass, itemTheme,
+			a, last, splittheme, counter, startCount, newStartCount, countParent, icon, imgParents, img, linkIcon;
+
+		if ( ol && jsCount ) {
+			$list.find( ".ui-li-dec" ).remove();
+		}
+
+		if ( ol ) {
+			// Check if a start attribute has been set while taking a value of 0 into account
+			if ( start || start === 0 ) {
+				if ( !jsCount ) {
+					startCount = parseInt( start , 10 ) - 1;
+					$list.css( "counter-reset", "listnumbering " + startCount );
+				} else {
+					counter = parseInt( start , 10 );
+				}
+			} else if ( jsCount ) {
+					counter = 1;
+			}
+		}
+
+		if ( !o.theme ) {
+			o.theme = $.mobile.getInheritedTheme( this.element, "c" );
+		}
+
+		for ( var pos = 0, numli = li.length; pos < numli; pos++ ) {
+			item = li.eq( pos );
+			itemClass = "ui-li";
+
+			// If we're creating the element, we update it regardless
+			if ( create || !item.hasClass( "ui-li" ) ) {
+				itemTheme = item.jqmData( "theme" ) || o.theme;
+				a = this._getChildrenByTagName( item[ 0 ], "a", "A" );
+				var isDivider = ( item.jqmData( "role" ) === "list-divider" );
+
+				if ( a.length && !isDivider ) {
+					icon = item.jqmData( "icon" );
+
+					item.buttonMarkup({
+						wrapperEls: "div",
+						shadow: false,
+						corners: false,
+						iconpos: "right",
+						icon: a.length > 1 || icon === false ? false : icon || listicon || o.icon,
+						theme: itemTheme
+					});
+
+					if ( ( icon !== false ) && ( a.length === 1 ) ) {
+						item.addClass( "ui-li-has-arrow" );
+					}
+
+					a.first().removeClass( "ui-link" ).addClass( "ui-link-inherit" );
+
+					if ( a.length > 1 ) {
+						itemClass += " ui-li-has-alt";
+
+						last = a.last();
+						splittheme = listsplittheme || last.jqmData( "theme" ) || o.splitTheme;
+						linkIcon = last.jqmData( "icon" );
+
+						last.appendTo( item )
+							.attr( "title", $.trim(last.getEncodedText()) )
+							.addClass( "ui-li-link-alt" )
+							.empty()
+							.buttonMarkup({
+								shadow: false,
+								corners: false,
+								theme: itemTheme,
+								icon: false,
+								iconpos: "notext"
+							})
+							.find( ".ui-btn-inner" )
+								.append(
+									$( document.createElement( "span" ) ).buttonMarkup({
+										shadow: true,
+										corners: true,
+										theme: splittheme,
+										iconpos: "notext",
+										// link icon overrides list item icon overrides ul element overrides options
+										icon: linkIcon || icon || listspliticon || o.splitIcon
+									})
+								);
+					}
+				} else if ( isDivider ) {
+
+					itemClass += " ui-li-divider ui-bar-" + ( item.jqmData( "theme" ) || dividertheme );
+					item.attr( "role", "heading" );
+
+					if ( ol ) {
+						//reset counter when a divider heading is encountered
+						if ( start || start === 0 ) {
+							if ( !jsCount ) {
+								newStartCount = parseInt( start , 10 ) - 1;
+								item.css( "counter-reset", "listnumbering " + newStartCount );
+							} else {
+								counter = parseInt( start , 10 );
+							}
+						} else if ( jsCount ) {
+								counter = 1;
+						}
+					}
+
+				} else {
+					itemClass += " ui-li-static ui-btn-up-" + itemTheme;
+				}
+			}
+
+			if ( ol && jsCount && itemClass.indexOf( "ui-li-divider" ) < 0 ) {
+				countParent = itemClass.indexOf( "ui-li-static" ) > 0 ? item : item.find( ".ui-link-inherit" );
+
+				countParent.addClass( "ui-li-jsnumbering" )
+					.prepend( "<span class='ui-li-dec'>" + ( counter++ ) + ". </span>" );
+			}
+
+			// Instead of setting item class directly on the list item and its
+			// btn-inner at this point in time, push the item into a dictionary
+			// that tells us what class to set on it so we can do this after this
+			// processing loop is finished.
+
+			if ( !itemClassDict[ itemClass ] ) {
+				itemClassDict[ itemClass ] = [];
+			}
+
+			itemClassDict[ itemClass ].push( item[ 0 ] );
+		}
+
+		// Set the appropriate listview item classes on each list item
+		// and their btn-inner elements. The main reason we didn't do this
+		// in the for-loop above is because we can eliminate per-item function overhead
+		// by calling addClass() and children() once or twice afterwards. This
+		// can give us a significant boost on platforms like WP7.5.
+
+		for ( itemClass in itemClassDict ) {
+			$( itemClassDict[ itemClass ] ).addClass( itemClass ).children( ".ui-btn-inner" ).addClass( itemClass );
+		}
+
+		$list.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" )
+			.end()
+
+			.find( "p, dl" ).addClass( "ui-li-desc" )
+			.end()
+
+			.find( ".ui-li-aside" ).each(function() {
+					var $this = $( this );
+					$this.prependTo( $this.parent() ); //shift aside to front for css float
+				})
+			.end()
+
+			.find( ".ui-li-count" ).each(function() {
+					$( this ).closest( "li" ).addClass( "ui-li-has-count" );
+				}).addClass( "ui-btn-up-" + ( $list.jqmData( "counttheme" ) || this.options.countTheme) + " ui-btn-corner-all" );
+
+		// The idea here is to look at the first image in the list item
+		// itself, and any .ui-link-inherit element it may contain, so we
+		// can place the appropriate classes on the image and list item.
+		// Note that we used to use something like:
+		//
+		//    li.find(">img:eq(0), .ui-link-inherit>img:eq(0)").each( ... );
+		//
+		// But executing a find() like that on Windows Phone 7.5 took a
+		// really long time. Walking things manually with the code below
+		// allows the 400 listview item page to load in about 3 seconds as
+		// opposed to 30 seconds.
+
+		this._addThumbClasses( li );
+		this._addThumbClasses( $list.find( ".ui-link-inherit" ) );
+
+		this._addFirstLastClasses( li, this._getVisibles( li, create ), create );
+		// autodividers binds to this to redraw dividers after the listview refresh
+		this._trigger( "afterrefresh" );
+	},
+
+	//create a string for ID/subpage url creation
+	_idStringEscape: function( str ) {
+		return str.replace(/[^a-zA-Z0-9]/g, '-');
+	},
+
+	_createSubPages: function() {
+		var parentList = this.element,
+			parentPage = parentList.closest( ".ui-page" ),
+			parentUrl = parentPage.jqmData( "url" ),
+			parentId = parentUrl || parentPage[ 0 ][ $.expando ],
+			parentListId = parentList.attr( "id" ),
+			o = this.options,
+			dns = "data-" + $.mobile.ns,
+			self = this,
+			persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ),
+			hasSubPages;
+
+		if ( typeof listCountPerPage[ parentId ] === "undefined" ) {
+			listCountPerPage[ parentId ] = -1;
+		}
+
+		parentListId = parentListId || ++listCountPerPage[ parentId ];
+
+		$( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function( i ) {
+			var self = this,
+				list = $( this ),
+				listId = list.attr( "id" ) || parentListId + "-" + i,
+				parent = list.parent(),
+				nodeElsFull = $( list.prevAll().toArray().reverse() ),
+				nodeEls = nodeElsFull.length ? nodeElsFull : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" ),
+				title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text
+				id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId,
+				theme = list.jqmData( "theme" ) || o.theme,
+				countTheme = list.jqmData( "counttheme" ) || parentList.jqmData( "counttheme" ) || o.countTheme,
+				newPage, anchor;
+
+			//define hasSubPages for use in later removal
+			hasSubPages = true;
+
+			newPage = list.detach()
+						.wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" )
+						.parent()
+							.before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" )
+							.after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='"+ persistentFooterID +"'>" ) : "" )
+							.parent()
+								.appendTo( $.mobile.pageContainer );
+
+			newPage.page();
+
+			anchor = parent.find( 'a:first' );
+
+			if ( !anchor.length ) {
+				anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() );
+			}
+
+			anchor.attr( "href", "#" + id );
+
+		}).listview();
+
+		// on pagehide, remove any nested pages along with the parent page, as long as they aren't active
+		// and aren't embedded
+		if ( hasSubPages &&
+			parentPage.is( ":jqmData(external-page='true')" ) &&
+			parentPage.data( "mobile-page" ).options.domCache === false ) {
+
+			var newRemove = function( e, ui ) {
+				var nextPage = ui.nextPage, npURL,
+					prEvent = new $.Event( "pageremove" );
+
+				if ( ui.nextPage ) {
+					npURL = nextPage.jqmData( "url" );
+					if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) {
+						self.childPages().remove();
+						parentPage.trigger( prEvent );
+						if ( !prEvent.isDefaultPrevented() ) {
+							parentPage.removeWithDependents();
+						}
+					}
+				}
+			};
+
+			// unbind the original page remove and replace with our specialized version
+			parentPage
+				.unbind( "pagehide.remove" )
+				.bind( "pagehide.remove", newRemove);
+		}
+	},
+
+	// TODO sort out a better way to track sub pages of the listview this is brittle
+	childPages: function() {
+		var parentUrl = this.parentPage.jqmData( "url" );
+
+		return $( ":jqmData(url^='"+  parentUrl + "&" + $.mobile.subPageUrlKey + "')" );
+	}
+}, $.mobile.behaviors.addFirstLastClasses ) );
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+	$.mobile.listview.prototype.enhanceWithin( e.target );
+});
+
+})( jQuery );
+
+(function( $ ) {
+	var	meta = $( "meta[name=viewport]" ),
+		initialContent = meta.attr( "content" ),
+		disabledZoom = initialContent + ",maximum-scale=1, user-scalable=no",
+		enabledZoom = initialContent + ",maximum-scale=10, user-scalable=yes",
+		disabledInitially = /(user-scalable[\s]*=[\s]*no)|(maximum-scale[\s]*=[\s]*1)[$,\s]/.test( initialContent );
+
+	$.mobile.zoom = $.extend( {}, {
+		enabled: !disabledInitially,
+		locked: false,
+		disable: function( lock ) {
+			if ( !disabledInitially && !$.mobile.zoom.locked ) {
+				meta.attr( "content", disabledZoom );
+				$.mobile.zoom.enabled = false;
+				$.mobile.zoom.locked = lock || false;
+			}
+		},
+		enable: function( unlock ) {
+			if ( !disabledInitially && ( !$.mobile.zoom.locked || unlock === true ) ) {
+				meta.attr( "content", enabledZoom );
+				$.mobile.zoom.enabled = true;
+				$.mobile.zoom.locked = false;
+			}
+		},
+		restore: function() {
+			if ( !disabledInitially ) {
+				meta.attr( "content", initialContent );
+				$.mobile.zoom.enabled = true;
+			}
+		}
+	});
+
+}( jQuery ));
+
+(function( $, undefined ) {
+
+$.widget( "mobile.textinput", $.mobile.widget, {
+	options: {
+		theme: null,
+		mini: false,
+		// This option defaults to true on iOS devices.
+		preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1,
+		initSelector: "input[type='text'], input[type='search'], :jqmData(type='search'), input[type='number'], :jqmData(type='number'), input[type='password'], input[type='email'], input[type='url'], input[type='tel'], textarea, input[type='time'], input[type='date'], input[type='month'], input[type='week'], input[type='datetime'], input[type='datetime-local'], input[type='color'], input:not([type]), input[type='file']",
+		clearBtn: false,
+		clearSearchButtonText: null, //deprecating for 1.3...
+		clearBtnText: "clear text",
+		disabled: false
+	},
+
+	_create: function() {
+
+		var self = this,
+			input = this.element,
+			o = this.options,
+			theme = o.theme || $.mobile.getInheritedTheme( this.element, "c" ),
+			themeclass  = " ui-body-" + theme,
+			miniclass = o.mini ? " ui-mini" : "",
+			isSearch = input.is( "[type='search'], :jqmData(type='search')" ),
+			focusedEl,
+			clearbtn,
+			clearBtnText = o.clearSearchButtonText || o.clearBtnText,
+			clearBtnBlacklist = input.is( "textarea, :jqmData(type='range')" ),
+			inputNeedsClearBtn = !!o.clearBtn && !clearBtnBlacklist,
+			inputNeedsWrap = input.is( "input" ) && !input.is( ":jqmData(type='range')" );
+
+		function toggleClear() {
+			setTimeout( function() {
+				clearbtn.toggleClass( "ui-input-clear-hidden", !input.val() );
+			}, 0 );
+		}
+
+		$( "label[for='" + input.attr( "id" ) + "']" ).addClass( "ui-input-text" );
+
+		focusedEl = input.addClass( "ui-input-text ui-body-"+ theme );
+
+		// XXX: Temporary workaround for issue 785 (Apple bug 8910589).
+		//      Turn off autocorrect and autocomplete on non-iOS 5 devices
+		//      since the popup they use can't be dismissed by the user. Note
+		//      that we test for the presence of the feature by looking for
+		//      the autocorrect property on the input element. We currently
+		//      have no test for iOS 5 or newer so we're temporarily using
+		//      the touchOverflow support flag for jQM 1.0. Yes, I feel dirty. - jblas
+		if ( typeof input[0].autocorrect !== "undefined" && !$.support.touchOverflow ) {
+			// Set the attribute instead of the property just in case there
+			// is code that attempts to make modifications via HTML.
+			input[0].setAttribute( "autocorrect", "off" );
+			input[0].setAttribute( "autocomplete", "off" );
+		}
+
+		//"search" and "text" input widgets
+		if ( isSearch ) {
+			focusedEl = input.wrap( "<div class='ui-input-search ui-shadow-inset ui-btn-corner-all ui-btn-shadow ui-icon-searchfield" + themeclass + miniclass + "'></div>" ).parent();
+		} else if ( inputNeedsWrap ) {
+			focusedEl = input.wrap( "<div class='ui-input-text ui-shadow-inset ui-corner-all ui-btn-shadow" + themeclass + miniclass + "'></div>" ).parent();
+		}
+
+		if( inputNeedsClearBtn || isSearch ) {
+			clearbtn = $( "<a href='#' class='ui-input-clear' title='" + clearBtnText + "'>" + clearBtnText + "</a>" )
+				.bind( "click", function( event ) {
+					input
+						.val( "" )
+						.focus()
+						.trigger( "change" );
+					clearbtn.addClass( "ui-input-clear-hidden" );
+					event.preventDefault();
+				})
+				.appendTo( focusedEl )
+				.buttonMarkup({
+					icon: "delete",
+					iconpos: "notext",
+					corners: true,
+					shadow: true,
+					mini: o.mini
+				});
+				
+			if ( !isSearch ) {
+				focusedEl.addClass( "ui-input-has-clear" );
+			}
+
+			toggleClear();
+
+			input.bind( "paste cut keyup input focus change blur", toggleClear );
+		}
+		else if ( !inputNeedsWrap && !isSearch ) {
+			input.addClass( "ui-corner-all ui-shadow-inset" + themeclass + miniclass );
+		}
+
+		input.focus(function() {
+				// In many situations, iOS will zoom into the input upon tap, this prevents that from happening
+				if ( o.preventFocusZoom ) {
+					$.mobile.zoom.disable( true );
+				}			
+				focusedEl.addClass( $.mobile.focusClass );
+			})
+			.blur(function() {
+				focusedEl.removeClass( $.mobile.focusClass );
+				if ( o.preventFocusZoom ) {
+					$.mobile.zoom.enable( true );
+				}				
+			});
+
+		// Autogrow
+		if ( input.is( "textarea" ) ) {
+			var extraLineHeight = 15,
+				keyupTimeoutBuffer = 100,
+				keyupTimeout;
+
+			this._keyup = function() {
+				var scrollHeight = input[ 0 ].scrollHeight,
+					clientHeight = input[ 0 ].clientHeight;
+
+				if ( clientHeight < scrollHeight ) {
+					var paddingTop = parseFloat( input.css( "padding-top" ) ),
+						paddingBottom = parseFloat( input.css( "padding-bottom" ) ),
+						paddingHeight = paddingTop + paddingBottom;
+					
+					input.height( scrollHeight - paddingHeight + extraLineHeight );
+				}
+			};
+
+			input.on( "keyup change input paste", function() {
+				clearTimeout( keyupTimeout );
+				keyupTimeout = setTimeout( self._keyup, keyupTimeoutBuffer );
+			});
+
+			// binding to pagechange here ensures that for pages loaded via
+			// ajax the height is recalculated without user input
+			this._on( true, $.mobile.document, { "pagechange": "_keyup" });
+
+			// Issue 509: the browser is not providing scrollHeight properly until the styles load
+			if ( $.trim( input.val() ) ) {
+				// bind to the window load to make sure the height is calculated based on BOTH
+				// the DOM and CSS
+				this._on( true, $.mobile.window, {"load": "_keyup"});
+			}
+		}
+		if ( input.attr( "disabled" ) ) {
+			this.disable();
+		}
+	},
+
+	disable: function() {
+		var $el,
+			isSearch = this.element.is( "[type='search'], :jqmData(type='search')" ),
+			inputNeedsWrap = this.element.is( "input" ) && !this.element.is( ":jqmData(type='range')" ),
+			parentNeedsDisabled = this.element.attr( "disabled", true )	&& ( inputNeedsWrap || isSearch );
+			
+		if ( parentNeedsDisabled ) {
+			$el = this.element.parent();
+		} else {
+			$el = this.element;
+		}
+		$el.addClass( "ui-disabled" );
+		return this._setOption( "disabled", true );
+	},
+
+	enable: function() {
+		var $el,
+			isSearch = this.element.is( "[type='search'], :jqmData(type='search')" ),
+			inputNeedsWrap = this.element.is( "input" ) && !this.element.is( ":jqmData(type='range')" ),
+			parentNeedsEnabled = this.element.attr( "disabled", false )	&& ( inputNeedsWrap || isSearch );
+
+		if ( parentNeedsEnabled ) {
+			$el = this.element.parent();
+		} else {
+			$el = this.element;
+		}
+		$el.removeClass( "ui-disabled" );
+		return this._setOption( "disabled", false );
+	}
+});
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+	$.mobile.textinput.prototype.enhanceWithin( e.target, true );
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.mobile.listview.prototype.options.filter = false;
+$.mobile.listview.prototype.options.filterPlaceholder = "Filter items...";
+$.mobile.listview.prototype.options.filterTheme = "c";
+$.mobile.listview.prototype.options.filterReveal = false;
+// TODO rename callback/deprecate and default to the item itself as the first argument
+var defaultFilterCallback = function( text, searchValue, item ) {
+		return text.toString().toLowerCase().indexOf( searchValue ) === -1;
+	};
+
+$.mobile.listview.prototype.options.filterCallback = defaultFilterCallback;
+
+$.mobile.document.delegate( "ul, ol", "listviewcreate", function() {
+	var list = $( this ),
+		listview = list.data( "mobile-listview" );
+
+	if ( !listview || !listview.options.filter ) {
+		return;
+	}
+
+	if ( listview.options.filterReveal ) {
+		list.children().addClass( "ui-screen-hidden" );
+	}
+
+	var wrapper = $( "<form>", {
+			"class": "ui-listview-filter ui-bar-" + listview.options.filterTheme,
+			"role": "search"
+		}).submit( function( e ) {
+			e.preventDefault();
+			search.blur();
+		}),
+		onKeyUp = function( e ) {
+			var $this = $( this ),
+				val = this.value.toLowerCase(),
+				listItems = null,
+				li = list.children(),
+				lastval = $this.jqmData( "lastval" ) + "",
+				childItems = false,
+				itemtext = "",
+				item,
+				// Check if a custom filter callback applies
+				isCustomFilterCallback = listview.options.filterCallback !== defaultFilterCallback;
+
+			if ( lastval && lastval === val ) {
+				// Execute the handler only once per value change
+				return;
+			}
+
+			listview._trigger( "beforefilter", "beforefilter", { input: this } );
+
+			// Change val as lastval for next execution
+			$this.jqmData( "lastval" , val );
+			if ( isCustomFilterCallback || val.length < lastval.length || val.indexOf( lastval ) !== 0 ) {
+
+				// Custom filter callback applies or removed chars or pasted something totally different, check all items
+				listItems = list.children();
+			} else {
+
+				// Only chars added, not removed, only use visible subset
+				listItems = list.children( ":not(.ui-screen-hidden)" );
+
+				if ( !listItems.length && listview.options.filterReveal ) {
+					listItems = list.children( ".ui-screen-hidden" );
+				}
+			}
+
+			if ( val ) {
+
+				// This handles hiding regular rows without the text we search for
+				// and any list dividers without regular rows shown under it
+
+				for ( var i = listItems.length - 1; i >= 0; i-- ) {
+					item = $( listItems[ i ] );
+					itemtext = item.jqmData( "filtertext" ) || item.text();
+
+					if ( item.is( "li:jqmData(role=list-divider)" ) ) {
+
+						item.toggleClass( "ui-filter-hidequeue" , !childItems );
+
+						// New bucket!
+						childItems = false;
+
+					} else if ( listview.options.filterCallback( itemtext, val, item ) ) {
+
+						//mark to be hidden
+						item.toggleClass( "ui-filter-hidequeue" , true );
+					} else {
+
+						// There's a shown item in the bucket
+						childItems = true;
+					}
+				}
+
+				// Show items, not marked to be hidden
+				listItems
+					.filter( ":not(.ui-filter-hidequeue)" )
+					.toggleClass( "ui-screen-hidden", false );
+
+				// Hide items, marked to be hidden
+				listItems
+					.filter( ".ui-filter-hidequeue" )
+					.toggleClass( "ui-screen-hidden", true )
+					.toggleClass( "ui-filter-hidequeue", false );
+
+			} else {
+
+				//filtervalue is empty => show all
+				listItems.toggleClass( "ui-screen-hidden", !!listview.options.filterReveal );
+			}
+			listview._addFirstLastClasses( li, listview._getVisibles( li, false ), false );
+		},
+		search = $( "<input>", {
+			placeholder: listview.options.filterPlaceholder
+		})
+		.attr( "data-" + $.mobile.ns + "type", "search" )
+		.jqmData( "lastval", "" )
+		.bind( "keyup change input", onKeyUp )
+		.appendTo( wrapper )
+		.textinput();
+
+	if ( listview.options.inset ) {
+		wrapper.addClass( "ui-listview-filter-inset" );
+	}
+
+	wrapper.bind( "submit", function() {
+		return false;
+	})
+	.insertBefore( list );
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.mobile.listview.prototype.options.autodividers = false;
+$.mobile.listview.prototype.options.autodividersSelector = function( elt ) {
+	// look for the text in the given element
+	var text = $.trim( elt.text() ) || null;
+
+	if ( !text ) {
+		return null;
+	}
+
+	// create the text for the divider (first uppercased letter)
+	text = text.slice( 0, 1 ).toUpperCase();
+
+	return text;
+};
+
+$.mobile.document.delegate( "ul,ol", "listviewcreate", function() {
+
+	var list = $( this ),
+			listview = list.data( "mobile-listview" );
+
+	if ( !listview || !listview.options.autodividers ) {
+		return;
+	}
+
+	var replaceDividers = function () {
+		list.find( "li:jqmData(role='list-divider')" ).remove();
+
+		var lis = list.find( 'li' ),
+			lastDividerText = null, li, dividerText;
+
+		for ( var i = 0; i < lis.length ; i++ ) {
+			li = lis[i];
+			dividerText = listview.options.autodividersSelector( $( li ) );
+
+			if ( dividerText && lastDividerText !== dividerText ) {
+				var divider = document.createElement( 'li' );
+				divider.appendChild( document.createTextNode( dividerText ) );
+				divider.setAttribute( 'data-' + $.mobile.ns + 'role', 'list-divider' );
+				li.parentNode.insertBefore( divider, li );
+			}
+
+			lastDividerText = dividerText;
+		}
+	};
+
+	var afterListviewRefresh = function () {
+		list.unbind( 'listviewafterrefresh', afterListviewRefresh );
+		replaceDividers();
+		listview.refresh();
+		list.bind( 'listviewafterrefresh', afterListviewRefresh );
+	};
+
+	afterListviewRefresh();
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$( document ).bind( "pagecreate create", function( e ) {
+	$( ":jqmData(role='nojs')", e.target ).addClass( "ui-nojs" );
+	
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.mobile.behaviors.formReset = {
+	_handleFormReset: function() {
+		this._on( this.element.closest( "form" ), {
+			reset: function() {
+				this._delay( "_reset" );
+			}
+		});
+	}
+};
+
+})( jQuery );
+
+/*
+* "checkboxradio" plugin
+*/
+
+(function( $, undefined ) {
+
+$.widget( "mobile.checkboxradio", $.mobile.widget, $.extend( {
+	options: {
+		theme: null,
+		mini: false,
+		initSelector: "input[type='checkbox'],input[type='radio']"
+	},
+	_create: function() {
+		var self = this,
+			input = this.element,
+			o = this.options,
+			inheritAttr = function( input, dataAttr ) {
+				return input.jqmData( dataAttr ) || input.closest( "form, fieldset" ).jqmData( dataAttr );
+			},
+			// NOTE: Windows Phone could not find the label through a selector
+			// filter works though.
+			parentLabel = $( input ).closest( "label" ),
+			label = parentLabel.length ? parentLabel : $( input ).closest( "form, fieldset, :jqmData(role='page'), :jqmData(role='dialog')" ).find( "label" ).filter( "[for='" + input[0].id + "']" ).first(),
+			inputtype = input[0].type,
+			mini = inheritAttr( input, "mini" ) || o.mini,
+			checkedState = inputtype + "-on",
+			uncheckedState = inputtype + "-off",
+			iconpos = inheritAttr( input, "iconpos" ),
+			checkedClass = "ui-" + checkedState,
+			uncheckedClass = "ui-" + uncheckedState;
+
+		if ( inputtype !== "checkbox" && inputtype !== "radio" ) {
+			return;
+		}
+
+		// Expose for other methods
+		$.extend( this, {
+			label: label,
+			inputtype: inputtype,
+			checkedClass: checkedClass,
+			uncheckedClass: uncheckedClass,
+			checkedicon: checkedState,
+			uncheckedicon: uncheckedState
+		});
+
+		// If there's no selected theme check the data attr
+		if ( !o.theme ) {
+			o.theme = $.mobile.getInheritedTheme( this.element, "c" );
+		}
+
+		label.buttonMarkup({
+			theme: o.theme,
+			icon: uncheckedState,
+			shadow: false,
+			mini: mini,
+			iconpos: iconpos
+		});
+
+		// Wrap the input + label in a div
+		var wrapper = document.createElement('div');
+		wrapper.className = 'ui-' + inputtype;
+
+		input.add( label ).wrapAll( wrapper );
+
+		label.bind({
+			vmouseover: function( event ) {
+				if ( $( this ).parent().is( ".ui-disabled" ) ) {
+					event.stopPropagation();
+				}
+			},
+
+			vclick: function( event ) {
+				if ( input.is( ":disabled" ) ) {
+					event.preventDefault();
+					return;
+				}
+
+				self._cacheVals();
+
+				input.prop( "checked", inputtype === "radio" && true || !input.prop( "checked" ) );
+
+				// trigger click handler's bound directly to the input as a substitute for
+				// how label clicks behave normally in the browsers
+				// TODO: it would be nice to let the browser's handle the clicks and pass them
+				//       through to the associate input. we can swallow that click at the parent
+				//       wrapper element level
+				input.triggerHandler( 'click' );
+
+				// Input set for common radio buttons will contain all the radio
+				// buttons, but will not for checkboxes. clearing the checked status
+				// of other radios ensures the active button state is applied properly
+				self._getInputSet().not( input ).prop( "checked", false );
+
+				self._updateAll();
+				return false;
+			}
+		});
+
+		input
+			.bind({
+				vmousedown: function() {
+					self._cacheVals();
+				},
+
+				vclick: function() {
+					var $this = $( this );
+
+					// Adds checked attribute to checked input when keyboard is used
+					if ( $this.is( ":checked" ) ) {
+
+						$this.prop( "checked", true);
+						self._getInputSet().not( $this ).prop( "checked", false );
+					} else {
+
+						$this.prop( "checked", false );
+					}
+
+					self._updateAll();
+				},
+
+				focus: function() {
+					label.addClass( $.mobile.focusClass );
+				},
+
+				blur: function() {
+					label.removeClass( $.mobile.focusClass );
+				}
+			});
+
+		this._handleFormReset();
+		this.refresh();
+	},
+
+	_cacheVals: function() {
+		this._getInputSet().each(function() {
+			$( this ).jqmData( "cacheVal", this.checked );
+		});
+	},
+
+	//returns either a set of radios with the same name attribute, or a single checkbox
+	_getInputSet: function() {
+		if ( this.inputtype === "checkbox" ) {
+			return this.element;
+		}
+
+		return this.element.closest( "form, :jqmData(role='page'), :jqmData(role='dialog')" )
+			.find( "input[name='" + this.element[0].name + "'][type='" + this.inputtype + "']" );
+	},
+
+	_updateAll: function() {
+		var self = this;
+
+		this._getInputSet().each(function() {
+			var $this = $( this );
+
+			if ( this.checked || self.inputtype === "checkbox" ) {
+				$this.trigger( "change" );
+			}
+		})
+		.checkboxradio( "refresh" );
+	},
+
+	_reset: function() {
+		this.refresh();
+	},
+
+	refresh: function() {
+		var input = this.element[ 0 ],
+			active = " " + $.mobile.activeBtnClass,
+			checkedClass = this.checkedClass + ( this.element.parents( ".ui-controlgroup-horizontal" ).length ? active : "" ),
+			label = this.label;
+
+		if ( input.checked ) {
+			label.removeClass( this.uncheckedClass + active ).addClass( checkedClass ).buttonMarkup( { icon: this.checkedicon } );
+		} else {
+			label.removeClass( checkedClass ).addClass( this.uncheckedClass ).buttonMarkup( { icon: this.uncheckedicon } );
+		}
+
+		if ( input.disabled ) {
+			this.disable();
+		} else {
+			this.enable();
+		}
+	},
+
+	disable: function() {
+		this.element.prop( "disabled", true ).parent().addClass( "ui-disabled" );
+	},
+
+	enable: function() {
+		this.element.prop( "disabled", false ).parent().removeClass( "ui-disabled" );
+	}
+}, $.mobile.behaviors.formReset ) );
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+	$.mobile.checkboxradio.prototype.enhanceWithin( e.target, true );
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.widget( "mobile.button", $.mobile.widget, {
+	options: {
+		theme: null,
+		icon: null,
+		iconpos: null,
+		corners: true,
+		shadow: true,
+		iconshadow: true,
+		inline: null,
+		mini: null,
+		initSelector: "button, [type='button'], [type='submit'], [type='reset']"
+	},
+	_create: function() {
+		var $el = this.element,
+			$button,
+			// create a copy of this.options we can pass to buttonMarkup
+			o = ( function( tdo ) {
+				var key, ret = {};
+
+				for ( key in tdo ) {
+					if ( tdo[ key ] !== null && key !== "initSelector" ) {
+						ret[ key ] = tdo[ key ];
+					}
+				}
+
+				return ret;
+			} )( this.options ),
+			classes = "",
+			$buttonPlaceholder;
+
+		// if this is a link, check if it's been enhanced and, if not, use the right function
+		if ( $el[ 0 ].tagName === "A" ) {
+			if ( !$el.hasClass( "ui-btn" ) ) {
+				$el.buttonMarkup();
+			}
+			return;
+		}
+
+		// get the inherited theme
+		// TODO centralize for all widgets
+		if ( !this.options.theme ) {
+			this.options.theme = $.mobile.getInheritedTheme( this.element, "c" );
+		}
+
+		// TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577
+		/* if ( $el[0].className.length ) {
+			classes = $el[0].className;
+		} */
+		if ( !!~$el[0].className.indexOf( "ui-btn-left" ) ) {
+			classes = "ui-btn-left";
+		}
+
+		if (  !!~$el[0].className.indexOf( "ui-btn-right" ) ) {
+			classes = "ui-btn-right";
+		}
+
+		if (  $el.attr( "type" ) === "submit" || $el.attr( "type" ) === "reset" ) {
+			if ( classes ) {
+				classes += " ui-submit";
+			} else {
+				classes = "ui-submit";
+			}
+		}
+		$( "label[for='" + $el.attr( "id" ) + "']" ).addClass( "ui-submit" );
+
+		// Add ARIA role
+		this.button = $( "<div></div>" )
+			[ $el.html() ? "html" : "text" ]( $el.html() || $el.val() )
+			.insertBefore( $el )
+			.buttonMarkup( o )
+			.addClass( classes )
+			.append( $el.addClass( "ui-btn-hidden" ) );
+
+        $button = this.button;
+
+		$el.bind({
+			focus: function() {
+				$button.addClass( $.mobile.focusClass );
+			},
+
+			blur: function() {
+				$button.removeClass( $.mobile.focusClass );
+			}
+		});
+
+		this.refresh();
+	},
+
+	_setOption: function( key, value ) {
+		var op = {};
+
+		op[ key ] = value;
+		if ( key !== "initSelector" ) {
+			this.button.buttonMarkup( op );
+			// Record the option change in the options and in the DOM data-* attributes
+			this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value );
+		}
+		this._super( "_setOption", key, value );
+	},
+
+	enable: function() {
+		this.element.attr( "disabled", false );
+		this.button.removeClass( "ui-disabled" ).attr( "aria-disabled", false );
+		return this._setOption( "disabled", false );
+	},
+
+	disable: function() {
+		this.element.attr( "disabled", true );
+		this.button.addClass( "ui-disabled" ).attr( "aria-disabled", true );
+		return this._setOption( "disabled", true );
+	},
+
+	refresh: function() {
+		var $el = this.element;
+
+		if ( $el.prop("disabled") ) {
+			this.disable();
+		} else {
+			this.enable();
+		}
+
+		// Grab the button's text element from its implementation-independent data item
+		$( this.button.data( 'buttonElements' ).text )[ $el.html() ? "html" : "text" ]( $el.html() || $el.val() );
+	}
+});
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+	$.mobile.button.prototype.enhanceWithin( e.target, true );
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.widget( "mobile.slider", $.mobile.widget, $.extend( {
+	widgetEventPrefix: "slide",
+
+	options: {
+		theme: null,
+		trackTheme: null,
+		disabled: false,
+		initSelector: "input[type='range'], :jqmData(type='range'), :jqmData(role='slider')",
+		mini: false,
+		highlight: false
+	},
+
+	_create: function() {
+
+		// TODO: Each of these should have comments explain what they're for
+		var self = this,
+			control = this.element,
+			parentTheme = $.mobile.getInheritedTheme( control, "c" ),
+			theme = this.options.theme || parentTheme,
+			trackTheme = this.options.trackTheme || parentTheme,
+			cType = control[ 0 ].nodeName.toLowerCase(),
+			isSelect = this.isToggleSwitch = cType === "select",
+			isRangeslider = control.parent().is( ":jqmData(role='rangeslider')" ),
+			selectClass = ( this.isToggleSwitch ) ? "ui-slider-switch" : "",
+			controlID = control.attr( "id" ),
+			$label = $( "[for='" + controlID + "']" ),
+			labelID = $label.attr( "id" ) || controlID + "-label",
+			label = $label.attr( "id", labelID ),
+			min = !this.isToggleSwitch ? parseFloat( control.attr( "min" ) ) : 0,
+			max =  !this.isToggleSwitch ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length-1,
+			step = window.parseFloat( control.attr( "step" ) || 1 ),
+			miniClass = ( this.options.mini || control.jqmData( "mini" ) ) ? " ui-mini" : "",
+			domHandle = document.createElement( "a" ),
+			handle = $( domHandle ),
+			domSlider = document.createElement( "div" ),
+			slider = $( domSlider ),
+			valuebg = this.options.highlight && !this.isToggleSwitch ? (function() {
+				var bg = document.createElement( "div" );
+				bg.className = "ui-slider-bg " + $.mobile.activeBtnClass + " ui-btn-corner-all";
+				return $( bg ).prependTo( slider );
+			})() : false,
+			options,
+			wrapper;
+			
+		domHandle.setAttribute( "href", "#" );
+		domSlider.setAttribute( "role", "application" );
+		domSlider.className = [this.isToggleSwitch ? "ui-slider " : "ui-slider-track ",selectClass," ui-btn-down-",trackTheme," ui-btn-corner-all", miniClass].join( "" );
+		domHandle.className = "ui-slider-handle";
+		domSlider.appendChild( domHandle );
+
+		handle.buttonMarkup({ corners: true, theme: theme, shadow: true })
+				.attr({
+					"role": "slider",
+					"aria-valuemin": min,
+					"aria-valuemax": max,
+					"aria-valuenow": this._value(),
+					"aria-valuetext": this._value(),
+					"title": this._value(),
+					"aria-labelledby": labelID
+				});
+
+		$.extend( this, {
+			slider: slider,
+			handle: handle,
+			type: cType,
+			step: step,
+			max: max,
+			min: min,
+			valuebg: valuebg,
+			isRangeslider: isRangeslider,
+			dragging: false,
+			beforeStart: null,
+			userModified: false,
+			mouseMoved: false
+		});
+
+		if ( this.isToggleSwitch ) {
+			wrapper = document.createElement( "div" );
+			wrapper.className = "ui-slider-inneroffset";
+
+			for ( var j = 0, length = domSlider.childNodes.length; j < length; j++ ) {
+				wrapper.appendChild( domSlider.childNodes[j] );
+			}
+
+			domSlider.appendChild( wrapper );
+
+			// slider.wrapInner( "<div class='ui-slider-inneroffset'></div>" );
+
+			// make the handle move with a smooth transition
+			handle.addClass( "ui-slider-handle-snapping" );
+
+			options = control.find( "option" );
+
+			for ( var i = 0, optionsCount = options.length; i < optionsCount; i++ ) {
+				var side = !i ? "b" : "a",
+					sliderTheme = !i ? " ui-btn-down-" + trackTheme : ( " " + $.mobile.activeBtnClass ),
+					sliderLabel = document.createElement( "div" ),
+					sliderImg = document.createElement( "span" );
+
+				sliderImg.className = ["ui-slider-label ui-slider-label-", side, sliderTheme, " ui-btn-corner-all"].join( "" );
+				sliderImg.setAttribute( "role", "img" );
+				sliderImg.appendChild( document.createTextNode( options[i].innerHTML ) );
+				$( sliderImg ).prependTo( slider );
+			}
+
+			self._labels = $( ".ui-slider-label", slider );
+
+		}
+
+		label.addClass( "ui-slider" );
+		
+		// monitor the input for updated values
+		control.addClass( this.isToggleSwitch ? "ui-slider-switch" : "ui-slider-input" );
+
+		this._on( control, {
+			"change": "_controlChange",
+			"keyup": "_controlKeyup",
+			"blur": "_controlBlur",
+			"vmouseup": "_controlVMouseUp"
+		});
+
+		slider.bind( "vmousedown", $.proxy( this._sliderVMouseDown, this ) )
+			.bind( "vclick", false );
+
+		// We have to instantiate a new function object for the unbind to work properly
+		// since the method itself is defined in the prototype (causing it to unbind everything)
+		this._on( document, { "vmousemove": "_preventDocumentDrag" });
+		this._on( slider.add( document ), { "vmouseup": "_sliderVMouseUp" });
+
+		slider.insertAfter( control );
+
+		// wrap in a div for styling purposes
+		if ( !this.isToggleSwitch && !isRangeslider ) {
+			wrapper = this.options.mini ? "<div class='ui-slider ui-mini'>" : "<div class='ui-slider'>";
+			
+			control.add( slider ).wrapAll( wrapper );
+		}
+
+		// Only add focus class to toggle switch, sliders get it automatically from ui-btn
+		if ( this.isToggleSwitch ) {
+			this.handle.bind({
+				focus: function() {
+					slider.addClass( $.mobile.focusClass );
+				},
+
+				blur: function() {
+					slider.removeClass( $.mobile.focusClass );
+				}
+			});
+		}
+
+		// bind the handle event callbacks and set the context to the widget instance
+		this._on( this.handle, {
+			"vmousedown": "_handleVMouseDown",
+			"keydown": "_handleKeydown",
+			"keyup": "_handleKeyup"
+		});
+
+		this.handle.bind( "vclick", false );
+
+		this._handleFormReset();
+
+		this.refresh( undefined, undefined, true );
+	},
+
+	_controlChange: function( event ) {
+		// if the user dragged the handle, the "change" event was triggered from inside refresh(); don't call refresh() again
+		if ( this._trigger( "controlchange", event ) === false ) {
+			return false;
+		}
+		if ( !this.mouseMoved ) {
+			this.refresh( this._value(), true );
+		}
+	},
+
+	_controlKeyup: function( event ) { // necessary?
+		this.refresh( this._value(), true, true );
+	},
+
+	_controlBlur: function( event ) {
+		this.refresh( this._value(), true );
+	},
+
+	// it appears the clicking the up and down buttons in chrome on
+	// range/number inputs doesn't trigger a change until the field is
+	// blurred. Here we check thif the value has changed and refresh
+	_controlVMouseUp: function( event ) {
+		this._checkedRefresh();
+	},
+
+	// NOTE force focus on handle
+	_handleVMouseDown: function( event ) {
+		this.handle.focus();
+	},
+
+	_handleKeydown: function( event ) {
+		var index = this._value();
+		if ( this.options.disabled ) {
+			return;
+		}
+
+		// In all cases prevent the default and mark the handle as active
+		switch ( event.keyCode ) {
+			case $.mobile.keyCode.HOME:
+			case $.mobile.keyCode.END:
+			case $.mobile.keyCode.PAGE_UP:
+			case $.mobile.keyCode.PAGE_DOWN:
+			case $.mobile.keyCode.UP:
+			case $.mobile.keyCode.RIGHT:
+			case $.mobile.keyCode.DOWN:
+			case $.mobile.keyCode.LEFT:
+				event.preventDefault();
+
+				if ( !this._keySliding ) {
+					this._keySliding = true;
+					this.handle.addClass( "ui-state-active" );
+				}
+
+				break;
+		}
+
+		// move the slider according to the keypress
+		switch ( event.keyCode ) {
+			case $.mobile.keyCode.HOME:
+				this.refresh( this.min );
+				break;
+			case $.mobile.keyCode.END:
+				this.refresh( this.max );
+				break;
+			case $.mobile.keyCode.PAGE_UP:
+			case $.mobile.keyCode.UP:
+			case $.mobile.keyCode.RIGHT:
+				this.refresh( index + this.step );
+				break;
+			case $.mobile.keyCode.PAGE_DOWN:
+			case $.mobile.keyCode.DOWN:
+			case $.mobile.keyCode.LEFT:
+				this.refresh( index - this.step );
+				break;
+		}
+	}, // remove active mark
+
+	_handleKeyup: function( event ) {
+		if ( this._keySliding ) {
+			this._keySliding = false;
+			this.handle.removeClass( "ui-state-active" );
+		}
+	},
+
+	_sliderVMouseDown: function( event ) {
+		// NOTE: we don't do this in refresh because we still want to
+		//       support programmatic alteration of disabled inputs
+		if ( this.options.disabled || !( event.which === 1 || event.which === 0 ) ) {
+			return false;
+		}
+		if ( this._trigger( "beforestart", event ) === false ) {
+			return false;
+		}
+		this.dragging = true;
+		this.userModified = false;
+		this.mouseMoved = false;
+
+		if ( this.isToggleSwitch ) {
+			this.beforeStart = this.element[0].selectedIndex;
+		}
+
+		
+		this.refresh( event );
+		this._trigger( "start" );
+		return false;
+	},
+
+	_sliderVMouseUp: function() {
+		if ( this.dragging ) {
+			this.dragging = false;
+
+			if ( this.isToggleSwitch ) {
+				// make the handle move with a smooth transition
+				this.handle.addClass( "ui-slider-handle-snapping" );
+
+				if ( this.mouseMoved ) {
+					// this is a drag, change the value only if user dragged enough
+					if ( this.userModified ) {
+						this.refresh( this.beforeStart === 0 ? 1 : 0 );
+					} else {
+						this.refresh( this.beforeStart );
+					}
+				} else {
+					// this is just a click, change the value
+					this.refresh( this.beforeStart === 0 ? 1 : 0 );
+				}
+			}
+
+			this.mouseMoved = false;
+			this._trigger( "stop" );
+			return false;
+		}
+	},
+
+	_preventDocumentDrag: function( event ) {
+			// NOTE: we don't do this in refresh because we still want to
+			//       support programmatic alteration of disabled inputs
+			if ( this._trigger( "drag", event ) === false) {
+				return false;
+			}
+			if ( this.dragging && !this.options.disabled ) {
+				
+				// this.mouseMoved must be updated before refresh() because it will be used in the control "change" event
+				this.mouseMoved = true;
+
+				if ( this.isToggleSwitch ) {
+					// make the handle move in sync with the mouse
+					this.handle.removeClass( "ui-slider-handle-snapping" );
+				}
+				
+				this.refresh( event );
+
+				// only after refresh() you can calculate this.userModified
+				this.userModified = this.beforeStart !== this.element[0].selectedIndex;
+				return false;
+			}
+		},
+
+	_checkedRefresh: function() {
+		if ( this.value !== this._value() ) {
+			this.refresh( this._value() );
+		}
+	},
+
+	_value: function() {
+		return  this.isToggleSwitch ? this.element[0].selectedIndex : parseFloat( this.element.val() ) ;
+	},
+
+
+	_reset: function() {
+		this.refresh( undefined, false, true );
+	},
+
+	refresh: function( val, isfromControl, preventInputUpdate ) {
+		// NOTE: we don't return here because we want to support programmatic
+		//       alteration of the input value, which should still update the slider
+		
+		var self = this,
+			parentTheme = $.mobile.getInheritedTheme( this.element, "c" ),
+			theme = this.options.theme || parentTheme,
+			trackTheme = this.options.trackTheme || parentTheme,
+			left, width, data, tol;
+
+		self.slider[0].className = [ this.isToggleSwitch ? "ui-slider ui-slider-switch" : "ui-slider-track"," ui-btn-down-" + trackTheme,' ui-btn-corner-all', ( this.options.mini ) ? " ui-mini":""].join( "" );
+		if ( this.options.disabled || this.element.attr( "disabled" ) ) {
+			this.disable();
+		}
+
+		// set the stored value for comparison later
+		this.value = this._value();
+		if ( this.options.highlight && !this.isToggleSwitch && this.slider.find( ".ui-slider-bg" ).length === 0 ) {
+			this.valuebg = (function() {
+				var bg = document.createElement( "div" );
+				bg.className = "ui-slider-bg " + $.mobile.activeBtnClass + " ui-btn-corner-all";
+				return $( bg ).prependTo( self.slider );
+			})();
+		}
+		this.handle.buttonMarkup({ corners: true, theme: theme, shadow: true });
+
+		var pxStep, percent,
+			control = this.element,
+			isInput = !this.isToggleSwitch,
+			optionElements = isInput ? [] : control.find( "option" ),
+			min =  isInput ? parseFloat( control.attr( "min" ) ) : 0,
+			max = isInput ? parseFloat( control.attr( "max" ) ) : optionElements.length - 1,
+			step = ( isInput && parseFloat( control.attr( "step" ) ) > 0 ) ? parseFloat( control.attr( "step" ) ) : 1;
+			
+		if ( typeof val === "object" ) {
+			data = val;
+			// a slight tolerance helped get to the ends of the slider
+			tol = 8;
+
+			left = this.slider.offset().left;
+			width = this.slider.width();
+			pxStep = width/((max-min)/step);
+			if ( !this.dragging ||
+					data.pageX < left - tol ||
+					data.pageX > left + width + tol ) {
+				return;
+			}
+			if ( pxStep > 1 ) {
+				percent = ( ( data.pageX - left ) / width ) * 100;
+			} else {
+				percent = Math.round( ( ( data.pageX - left ) / width ) * 100 );
+			}
+		} else {
+			if ( val == null ) {
+				val = isInput ? parseFloat( control.val() || 0 ) : control[0].selectedIndex;
+			}
+			percent = ( parseFloat( val ) - min ) / ( max - min ) * 100;
+		}
+
+		if ( isNaN( percent ) ) {
+			return;
+		}
+
+		var newval = ( percent / 100 ) * ( max - min ) + min;
+
+		//from jQuery UI slider, the following source will round to the nearest step
+		var valModStep = ( newval - min ) % step;
+		var alignValue = newval - valModStep;
+
+		if ( Math.abs( valModStep ) * 2 >= step ) {
+			alignValue += ( valModStep > 0 ) ? step : ( -step );
+		}
+
+		var percentPerStep = 100/((max-min)/step);
+		// Since JavaScript has problems with large floats, round
+		// the final value to 5 digits after the decimal point (see jQueryUI: #4124)
+		newval = parseFloat( alignValue.toFixed(5) );
+
+		if ( typeof pxStep === "undefined" ) {
+			pxStep = width / ( (max-min) / step );
+		}
+		if ( pxStep > 1 && isInput ) {
+			percent = ( newval - min ) * percentPerStep * ( 1 / step );
+		}
+		if ( percent < 0 ) {
+			percent = 0;
+		}
+
+		if ( percent > 100 ) {
+			percent = 100;
+		}
+
+		if ( newval < min ) {
+			newval = min;
+		}
+
+		if ( newval > max ) {
+			newval = max;
+		}
+
+		this.handle.css( "left", percent + "%" );
+
+		this.handle[0].setAttribute( "aria-valuenow", isInput ? newval : optionElements.eq( newval ).attr( "value" ) );
+
+		this.handle[0].setAttribute( "aria-valuetext", isInput ? newval : optionElements.eq( newval ).getEncodedText() );
+
+		this.handle[0].setAttribute( "title", isInput ? newval : optionElements.eq( newval ).getEncodedText() );
+
+		if ( this.valuebg ) {
+			this.valuebg.css( "width", percent + "%" );
+		}
+
+		// drag the label widths
+		if ( this._labels ) {
+			var handlePercent = this.handle.width() / this.slider.width() * 100,
+				aPercent = percent && handlePercent + ( 100 - handlePercent ) * percent / 100,
+				bPercent = percent === 100 ? 0 : Math.min( handlePercent + 100 - aPercent, 100 );
+
+			this._labels.each(function() {
+				var ab = $( this ).is( ".ui-slider-label-a" );
+				$( this ).width( ( ab ? aPercent : bPercent  ) + "%" );
+			});
+		}
+
+		if ( !preventInputUpdate ) {
+			var valueChanged = false;
+
+			// update control"s value
+			if ( isInput ) {
+				valueChanged = control.val() !== newval;
+				control.val( newval );
+			} else {
+				valueChanged = control[ 0 ].selectedIndex !== newval;
+				control[ 0 ].selectedIndex = newval;
+			}
+			if ( this._trigger( "beforechange", val ) === false) {
+					return false;
+			}
+			if ( !isfromControl && valueChanged ) {
+				control.trigger( "change" );
+			}
+		}
+	},
+
+	enable: function() {
+		this.element.attr( "disabled", false );
+		this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false );
+		return this._setOption( "disabled", false );
+	},
+
+	disable: function() {
+		this.element.attr( "disabled", true );
+		this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true );
+		return this._setOption( "disabled", true );
+	}
+
+}, $.mobile.behaviors.formReset ) );
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+	$.mobile.slider.prototype.enhanceWithin( e.target, true );
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+	$.widget( "mobile.rangeslider", $.mobile.widget, {
+
+		options: {
+			theme: null,
+			trackTheme: null,
+			disabled: false,
+			initSelector: ":jqmData(role='rangeslider')",
+			mini: false,
+			highlight: true
+		},
+
+		_create: function() {
+			var secondLabel,
+			$el = this.element,
+			elClass = this.options.mini ? "ui-rangeslider ui-mini" : "ui-rangeslider",
+			_inputFirst = $el.find( "input" ).first(),
+			_inputLast = $el.find( "input" ).last(),
+			label = $el.find( "label" ).first(),
+			_sliderFirst = $.data( _inputFirst.get(0), "mobileSlider" ).slider,
+			_sliderLast = $.data( _inputLast.get(0), "mobileSlider" ).slider,
+			firstHandle = $.data( _inputFirst.get(0), "mobileSlider" ).handle,
+			_sliders = $( "<div class=\"ui-rangeslider-sliders\" />" ).appendTo( $el );
+			
+			if ( $el.find( "label" ).length > 1 ) {
+				secondLabel = $el.find( "label" ).last().hide();
+			}
+
+			_inputFirst.addClass( "ui-rangeslider-first" );
+			_inputLast.addClass( "ui-rangeslider-last" );
+			$el.addClass( elClass );
+			
+			_sliderFirst.appendTo( _sliders );
+			_sliderLast.appendTo( _sliders );
+			label.prependTo( $el );
+			firstHandle.prependTo( _sliderLast );
+
+			$.extend( this, {
+				_inputFirst: _inputFirst,
+				_inputLast: _inputLast,
+				_sliderFirst: _sliderFirst,
+				_sliderLast: _sliderLast,
+				_targetVal: null,
+				_sliderTarget: false,
+				_sliders: _sliders,
+				_proxy: false
+			});
+			
+			this.refresh();
+			this._on( this.element.find( "input.ui-slider-input" ), {
+				"slidebeforestart": "_slidebeforestart",
+				"slidestop": "_slidestop",
+				"slidedrag": "_slidedrag",
+				"slidebeforechange": "_change",
+				"blur": "_change",
+				"keyup": "_change"
+			});
+			this._on({
+				"mousedown":"_change"
+			});
+			this._on( this.element.closest( "form" ), {
+				"reset":"_handleReset"
+			});
+			this._on( firstHandle, {
+				"vmousedown": "_dragFirstHandle"
+			});
+		},
+		_handleReset: function(){
+			var self = this;
+			//we must wait for the stack to unwind before updateing other wise sliders will not have updated yet
+			setTimeout( function(){
+				self._updateHighlight();
+			},0);
+		},
+
+		_dragFirstHandle: function( event ) {
+			//if the first handle is dragged send the event to the first slider
+			$.data( this._inputFirst.get(0), "mobileSlider" ).dragging = true;
+			$.data( this._inputFirst.get(0), "mobileSlider" ).refresh( event );
+			return false;
+		},
+
+		_slidedrag: function( event ) {
+			var first = $( event.target ).is( this._inputFirst ),
+				otherSlider = ( first ) ? this._inputLast : this._inputFirst;
+
+			this._sliderTarget = false;
+			//if the drag was initiated on an extreme and the other handle is focused send the events to
+			//the closest handle
+			if ( ( this._proxy === "first" && first ) || ( this._proxy === "last" && !first ) ) {
+				$.data( otherSlider.get(0), "mobileSlider" ).dragging = true;
+				$.data( otherSlider.get(0), "mobileSlider" ).refresh( event );
+				return false;
+			}
+		},
+
+		_slidestop: function( event ) {
+			var first = $( event.target ).is( this._inputFirst );
+			
+			this._proxy = false;
+			//this stops dragging of the handle and brings the active track to the front 
+			//this makes clicks on the track go the the last handle used
+			this.element.find( "input" ).trigger( "vmouseup" );
+			this._sliderFirst.css( "z-index", first ? 1 : "" );
+		},
+
+		_slidebeforestart: function( event ) {
+			this._sliderTarget = false;
+			//if the track is the target remember this and the original value
+			if ( $( event.originalEvent.target ).hasClass( "ui-slider-track" ) ) {
+				this._sliderTarget = true;
+				this._targetVal = $( event.target ).val();
+			}
+		},
+
+		_setOption: function( options ) {
+			this._superApply( options );
+			this.refresh();
+		},
+
+		refresh: function() {
+			var $el = this.element,
+				o = this.options;
+
+			$el.find( "input" ).slider({
+				theme: o.theme,
+				trackTheme: o.trackTheme,
+				disabled: o.disabled,
+				mini: o.mini,
+				highlight: o.highlight
+			}).slider( "refresh" );
+			this._updateHighlight();
+		},
+
+		_change: function( event ) {
+			if ( event.type === "keyup" ) {
+				this._updateHighlight();
+				return false;
+			}
+
+			var self = this,
+				min = parseFloat( this._inputFirst.val(), 10 ),
+				max = parseFloat( this._inputLast.val(), 10 ),
+				first = $( event.target ).hasClass( "ui-rangeslider-first" ),
+				thisSlider = first ? this._inputFirst : this._inputLast,
+				otherSlider = first ? this._inputLast : this._inputFirst;
+			
+			
+			if( ( this._inputFirst.val() > this._inputLast.val() && event.type === "mousedown" && !$(event.target).hasClass("ui-slider-handle")) ){
+				thisSlider.blur();
+			} else if( event.type === "mousedown" ){
+				return;
+			}
+			if ( min > max && !this._sliderTarget ) {
+				//this prevents min from being greater then max
+				thisSlider.val( first ? max: min ).slider( "refresh" );
+				this._trigger( "normalize" );
+			} else if ( min > max ) {
+				//this makes it so clicks on the target on either extreme go to the closest handle
+				thisSlider.val( this._targetVal ).slider( "refresh" );
+
+				//You must wait for the stack to unwind so first slider is updated before updating second
+				setTimeout( function() {
+					otherSlider.val( first ? min: max ).slider( "refresh" );
+					$.data( otherSlider.get(0), "mobileSlider" ).handle.focus();
+					self._sliderFirst.css( "z-index", first ? "" : 1 );
+					self._trigger( "normalize" );
+				}, 0 );
+				this._proxy = ( first ) ? "first" : "last";
+			}
+			//fixes issue where when both _sliders are at min they cannot be adjusted
+			if ( min === max ) {
+				$.data( thisSlider.get(0), "mobileSlider" ).handle.css( "z-index", 1 );
+				$.data( otherSlider.get(0), "mobileSlider" ).handle.css( "z-index", 0 );
+			} else {
+				$.data( otherSlider.get(0), "mobileSlider" ).handle.css( "z-index", "" );
+				$.data( thisSlider.get(0), "mobileSlider" ).handle.css( "z-index", "" );
+			}
+			
+			this._updateHighlight();
+			
+			if ( min >= max ) {
+				return false;
+			}
+		},
+
+		_updateHighlight: function() {
+			var min = parseInt( $.data( this._inputFirst.get(0), "mobileSlider" ).handle.get(0).style.left, 10 ),
+				max = parseInt( $.data( this._inputLast.get(0), "mobileSlider" ).handle.get(0).style.left, 10 ),
+				width = (max - min);
+
+			this.element.find( ".ui-slider-bg" ).css({
+				"margin-left": min + "%",
+				"width": width + "%"
+			});
+		},
+
+		_destroy: function() {
+			this.element.removeClass( "ui-rangeslider ui-mini" ).find( "label" ).show();
+			this._inputFirst.after( this._sliderFirst );
+			this._inputLast.after( this._sliderLast );
+			this._sliders.remove();
+			this.element.find( "input" ).removeClass( "ui-rangeslider-first ui-rangeslider-last" ).slider( "destroy" );
+		}
+
+	});
+
+$.widget( "mobile.rangeslider", $.mobile.rangeslider, $.mobile.behaviors.formReset );
+
+//auto self-init widgets
+$( document ).bind( "pagecreate create", function( e ) {
+	$.mobile.rangeslider.prototype.enhanceWithin( e.target, true );
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.widget( "mobile.selectmenu", $.mobile.widget, $.extend( {
+	options: {
+		theme: null,
+		disabled: false,
+		icon: "arrow-d",
+		iconpos: "right",
+		inline: false,
+		corners: true,
+		shadow: true,
+		iconshadow: true,
+		overlayTheme: "a",
+		dividerTheme: "b",
+		hidePlaceholderMenuItems: true,
+		closeText: "Close",
+		nativeMenu: true,
+		// This option defaults to true on iOS devices.
+		preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1,
+		initSelector: "select:not( :jqmData(role='slider') )",
+		mini: false
+	},
+
+	_button: function() {
+		return $( "<div/>" );
+	},
+
+	_setDisabled: function( value ) {
+		this.element.attr( "disabled", value );
+		this.button.attr( "aria-disabled", value );
+		return this._setOption( "disabled", value );
+	},
+
+	_focusButton : function() {
+		var self = this;
+
+		setTimeout( function() {
+			self.button.focus();
+		}, 40);
+	},
+
+	_selectOptions: function() {
+		return this.select.find( "option" );
+	},
+
+	// setup items that are generally necessary for select menu extension
+	_preExtension: function() {
+		var classes = "";
+		// TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577
+		/* if ( $el[0].className.length ) {
+			classes = $el[0].className;
+		} */
+		if ( !!~this.element[0].className.indexOf( "ui-btn-left" ) ) {
+			classes = " ui-btn-left";
+		}
+
+		if (  !!~this.element[0].className.indexOf( "ui-btn-right" ) ) {
+			classes = " ui-btn-right";
+		}
+
+		this.select = this.element.removeClass( "ui-btn-left ui-btn-right" ).wrap( "<div class='ui-select" + classes + "'>" );
+		this.selectID  = this.select.attr( "id" );
+		this.label = $( "label[for='"+ this.selectID +"']" ).addClass( "ui-select" );
+		this.isMultiple = this.select[ 0 ].multiple;
+		if ( !this.options.theme ) {
+			this.options.theme = $.mobile.getInheritedTheme( this.select, "c" );
+		}
+	},
+
+	_destroy: function() {
+		var wrapper = this.element.parents( ".ui-select" );
+		if ( wrapper.length > 0 ) {
+			if ( wrapper.is( ".ui-btn-left, .ui-btn-right" ) ) {
+				this.element.addClass( wrapper.is( ".ui-btn-left" ) ? "ui-btn-left" : "ui-btn-right" );
+			}
+			this.element.insertAfter( wrapper );
+			wrapper.remove();
+		}
+	},
+
+	_create: function() {
+		this._preExtension();
+
+		// Allows for extension of the native select for custom selects and other plugins
+		// see select.custom for example extension
+		// TODO explore plugin registration
+		this._trigger( "beforeCreate" );
+
+		this.button = this._button();
+
+		var self = this,
+
+			options = this.options,
+
+			inline = options.inline || this.select.jqmData( "inline" ),
+			mini = options.mini || this.select.jqmData( "mini" ),
+			iconpos = options.icon ? ( options.iconpos || this.select.jqmData( "iconpos" ) ) : false,
+
+			// IE throws an exception at options.item() function when
+			// there is no selected item
+			// select first in this case
+			selectedIndex = this.select[ 0 ].selectedIndex === -1 ? 0 : this.select[ 0 ].selectedIndex,
+
+			// TODO values buttonId and menuId are undefined here
+			button = this.button
+				.insertBefore( this.select )
+				.buttonMarkup( {
+					theme: options.theme,
+					icon: options.icon,
+					iconpos: iconpos,
+					inline: inline,
+					corners: options.corners,
+					shadow: options.shadow,
+					iconshadow: options.iconshadow,
+					mini: mini
+				});
+
+		this.setButtonText();
+
+		// Opera does not properly support opacity on select elements
+		// In Mini, it hides the element, but not its text
+		// On the desktop,it seems to do the opposite
+		// for these reasons, using the nativeMenu option results in a full native select in Opera
+		if ( options.nativeMenu && window.opera && window.opera.version ) {
+			button.addClass( "ui-select-nativeonly" );
+		}
+
+		// Add counter for multi selects
+		if ( this.isMultiple ) {
+			this.buttonCount = $( "<span>" )
+				.addClass( "ui-li-count ui-btn-up-c ui-btn-corner-all" )
+				.hide()
+				.appendTo( button.addClass('ui-li-has-count') );
+		}
+
+		// Disable if specified
+		if ( options.disabled || this.element.attr('disabled')) {
+			this.disable();
+		}
+
+		// Events on native select
+		this.select.change(function() {
+			self.refresh();
+			
+			if ( !!options.nativeMenu ) {
+				this.blur();
+			}
+		});
+
+		this._handleFormReset();
+
+		this.build();
+	},
+
+	build: function() {
+		var self = this;
+
+		this.select
+			.appendTo( self.button )
+			.bind( "vmousedown", function() {
+				// Add active class to button
+				self.button.addClass( $.mobile.activeBtnClass );
+			})
+			.bind( "focus", function() {
+				self.button.addClass( $.mobile.focusClass );
+			})
+			.bind( "blur", function() {
+				self.button.removeClass( $.mobile.focusClass );
+			})
+			.bind( "focus vmouseover", function() {
+				self.button.trigger( "vmouseover" );
+			})
+			.bind( "vmousemove", function() {
+				// Remove active class on scroll/touchmove
+				self.button.removeClass( $.mobile.activeBtnClass );
+			})
+			.bind( "change blur vmouseout", function() {
+				self.button.trigger( "vmouseout" )
+					.removeClass( $.mobile.activeBtnClass );
+			})
+			.bind( "change blur", function() {
+				self.button.removeClass( "ui-btn-down-" + self.options.theme );
+			});
+
+		// In many situations, iOS will zoom into the select upon tap, this prevents that from happening
+		self.button.bind( "vmousedown", function() {
+			if ( self.options.preventFocusZoom ) {
+					$.mobile.zoom.disable( true );
+			}
+		});
+		self.label.bind( "click focus", function() {
+			if ( self.options.preventFocusZoom ) {
+					$.mobile.zoom.disable( true );
+			}
+		});
+		self.select.bind( "focus", function() {
+			if ( self.options.preventFocusZoom ) {
+					$.mobile.zoom.disable( true );
+			}
+		});
+		self.button.bind( "mouseup", function() {
+			if ( self.options.preventFocusZoom ) {				
+				setTimeout(function() {
+					$.mobile.zoom.enable( true );
+				}, 0 );
+			}
+		});
+		self.select.bind( "blur", function() {
+			if ( self.options.preventFocusZoom ) {				
+				$.mobile.zoom.enable( true );
+			}
+		});
+
+	},
+
+	selected: function() {
+		return this._selectOptions().filter( ":selected" );
+	},
+
+	selectedIndices: function() {
+		var self = this;
+
+		return this.selected().map(function() {
+			return self._selectOptions().index( this );
+		}).get();
+	},
+
+	setButtonText: function() {
+		var self = this,
+			selected = this.selected(),
+			text = this.placeholder,
+			span = $( document.createElement( "span" ) );
+
+		this.button.find( ".ui-btn-text" ).html(function() {
+			if ( selected.length ) {
+				text = selected.map(function() {
+					return $( this ).text();
+				}).get().join( ", " );
+			} else {
+				text = self.placeholder;
+			}
+
+			// TODO possibly aggregate multiple select option classes
+			return span.text( text )
+				.addClass( self.select.attr( "class" ) )
+				.addClass( selected.attr( "class" ) );
+		});
+	},
+
+	setButtonCount: function() {
+		var selected = this.selected();
+
+		// multiple count inside button
+		if ( this.isMultiple ) {
+			this.buttonCount[ selected.length > 1 ? "show" : "hide" ]().text( selected.length );
+		}
+	},
+
+	_reset: function() {
+		this.refresh();
+	},
+
+	refresh: function() {
+		this.setButtonText();
+		this.setButtonCount();
+	},
+
+	// open and close preserved in native selects
+	// to simplify users code when looping over selects
+	open: $.noop,
+	close: $.noop,
+
+	disable: function() {
+		this._setDisabled( true );
+		this.button.addClass( "ui-disabled" );
+	},
+
+	enable: function() {
+		this._setDisabled( false );
+		this.button.removeClass( "ui-disabled" );
+	}
+}, $.mobile.behaviors.formReset ) );
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+	$.mobile.selectmenu.prototype.enhanceWithin( e.target, true );
+});
+})( jQuery );
+
+(function( $, undefined ) {
+
+	function fitSegmentInsideSegment( winSize, segSize, offset, desired ) {
+		var ret = desired;
+
+		if ( winSize < segSize ) {
+			// Center segment if it's bigger than the window
+			ret = offset + ( winSize - segSize ) / 2;
+		} else {
+			// Otherwise center it at the desired coordinate while keeping it completely inside the window
+			ret = Math.min( Math.max( offset, desired - segSize / 2 ), offset + winSize - segSize );
+		}
+
+		return ret;
+	}
+
+	function windowCoords() {
+		var $win = $.mobile.window;
+
+		return {
+			x: $win.scrollLeft(),
+			y: $win.scrollTop(),
+			cx: ( window.innerWidth || $win.width() ),
+			cy: ( window.innerHeight || $win.height() )
+		};
+	}
+
+	$.widget( "mobile.popup", $.mobile.widget, {
+		options: {
+			theme: null,
+			overlayTheme: null,
+			shadow: true,
+			corners: true,
+			transition: "none",
+			positionTo: "origin",
+			tolerance: null,
+			initSelector: ":jqmData(role='popup')",
+			closeLinkSelector: "a:jqmData(rel='back')",
+			closeLinkEvents: "click.popup",
+			navigateEvents: "navigate.popup",
+			closeEvents: "navigate.popup pagebeforechange.popup",
+			dismissible: true,
+
+			// NOTE Windows Phone 7 has a scroll position caching issue that
+			//      requires us to disable popup history management by default
+			//      https://github.com/jquery/jquery-mobile/issues/4784
+			//
+			// NOTE this option is modified in _create!
+			history: !$.mobile.browser.oldIE
+		},
+
+		_eatEventAndClose: function( e ) {
+			e.preventDefault();
+			e.stopImmediatePropagation();
+			if ( this.options.dismissible ) {
+				this.close();
+			}
+			return false;
+		},
+
+		// Make sure the screen size is increased beyond the page height if the popup's causes the document to increase in height
+		_resizeScreen: function() {
+			var popupHeight = this._ui.container.outerHeight( true );
+
+			this._ui.screen.removeAttr( "style" );
+			if ( popupHeight > this._ui.screen.height() ) {
+				this._ui.screen.height( popupHeight );
+			}
+		},
+
+		_handleWindowKeyUp: function( e ) {
+			if ( this._isOpen && e.keyCode === $.mobile.keyCode.ESCAPE ) {
+				return this._eatEventAndClose( e );
+			}
+		},
+
+		_expectResizeEvent: function() {
+			var winCoords = windowCoords();
+
+			if ( this._resizeData ) {
+				if ( winCoords.x === this._resizeData.winCoords.x &&
+					winCoords.y === this._resizeData.winCoords.y &&
+					winCoords.cx === this._resizeData.winCoords.cx &&
+					winCoords.cy === this._resizeData.winCoords.cy ) {
+					// timeout not refreshed
+					return false;
+				} else {
+					// clear existing timeout - it will be refreshed below
+					clearTimeout( this._resizeData.timeoutId );
+				}
+			}
+
+			this._resizeData = {
+				timeoutId: setTimeout( $.proxy( this, "_resizeTimeout" ), 200 ),
+				winCoords: winCoords
+			};
+
+			return true;
+		},
+
+		_resizeTimeout: function() {
+			if ( this._isOpen ) {
+				if ( !this._expectResizeEvent() ) {
+					if ( this._ui.container.hasClass( "ui-popup-hidden" ) ) {
+						// effectively rapid-open the popup while leaving the screen intact
+						this._ui.container.removeClass( "ui-popup-hidden" );
+						this.reposition( { positionTo: "window" } );
+						this._ignoreResizeEvents();
+					}
+
+					this._resizeScreen();
+					this._resizeData = null;
+					this._orientationchangeInProgress = false;
+				}
+			} else {
+				this._resizeData = null;
+				this._orientationchangeInProgress = false;
+			}
+		},
+
+		_ignoreResizeEvents: function() {
+			var self = this;
+
+			if ( this._ignoreResizeTo ) {
+				clearTimeout( this._ignoreResizeTo );
+			}
+			this._ignoreResizeTo = setTimeout( function() { self._ignoreResizeTo = 0; }, 1000 );
+		},
+
+		_handleWindowResize: function( e ) {
+			if ( this._isOpen && this._ignoreResizeTo === 0 ) {
+				if ( ( this._expectResizeEvent() || this._orientationchangeInProgress ) &&
+					!this._ui.container.hasClass( "ui-popup-hidden" ) ) {
+					// effectively rapid-close the popup while leaving the screen intact
+					this._ui.container
+						.addClass( "ui-popup-hidden" )
+						.removeAttr( "style" );
+				}
+			}
+		},
+
+		_handleWindowOrientationchange: function( e ) {
+			if ( !this._orientationchangeInProgress && this._isOpen && this._ignoreResizeTo === 0 ) {
+				this._expectResizeEvent();
+				this._orientationchangeInProgress = true;
+			}
+		},
+
+		// When the popup is open, attempting to focus on an element that is not a
+		// child of the popup will redirect focus to the popup
+		_handleDocumentFocusIn: function( e ) {
+			var tgt = e.target, $tgt, ui = this._ui;
+
+			if ( !this._isOpen ) {
+				return;
+			}
+
+			if ( tgt !== ui.container[ 0 ] ) {
+				$tgt = $( e.target );
+				if ( 0 === $tgt.parents().filter( ui.container[ 0 ] ).length ) {
+					$( document.activeElement ).one( "focus", function( e ) {
+						$tgt.blur();
+					});
+					ui.focusElement.focus();
+					e.preventDefault();
+					e.stopImmediatePropagation();
+					return false;
+				} else if ( ui.focusElement[ 0 ] === ui.container[ 0 ] ) {
+					ui.focusElement = $tgt;
+				}
+			}
+
+			this._ignoreResizeEvents();
+		},
+
+		_create: function() {
+			var ui = {
+					screen: $( "<div class='ui-screen-hidden ui-popup-screen'></div>" ),
+					placeholder: $( "<div style='display: none;'><!-- placeholder --></div>" ),
+					container: $( "<div class='ui-popup-container ui-popup-hidden'></div>" )
+				},
+				thisPage = this.element.closest( ".ui-page" ),
+				myId = this.element.attr( "id" ),
+				self = this;
+
+			// We need to adjust the history option to be false if there's no AJAX nav.
+			// We can't do it in the option declarations because those are run before
+			// it is determined whether there shall be AJAX nav.
+			this.options.history = this.options.history && $.mobile.ajaxEnabled && $.mobile.hashListeningEnabled;
+
+			if ( thisPage.length === 0 ) {
+				thisPage = $( "body" );
+			}
+
+			// define the container for navigation event bindings
+			// TODO this would be nice at the the mobile widget level
+			this.options.container = this.options.container || $.mobile.pageContainer;
+
+			// Apply the proto
+			thisPage.append( ui.screen );
+			ui.container.insertAfter( ui.screen );
+			// Leave a placeholder where the element used to be
+			ui.placeholder.insertAfter( this.element );
+			if ( myId ) {
+				ui.screen.attr( "id", myId + "-screen" );
+				ui.container.attr( "id", myId + "-popup" );
+				ui.placeholder.html( "<!-- placeholder for " + myId + " -->" );
+			}
+			ui.container.append( this.element );
+			ui.focusElement = ui.container;
+
+			// Add class to popup element
+			this.element.addClass( "ui-popup" );
+
+			// Define instance variables
+			$.extend( this, {
+				_scrollTop: 0,
+				_page: thisPage,
+				_ui: ui,
+				_fallbackTransition: "",
+				_currentTransition: false,
+				_prereqs: null,
+				_isOpen: false,
+				_tolerance: null,
+				_resizeData: null,
+				_ignoreResizeTo: 0,
+				_orientationchangeInProgress: false
+			});
+
+			$.each( this.options, function( key, value ) {
+				// Cause initial options to be applied by their handler by temporarily setting the option to undefined
+				// - the handler then sets it to the initial value
+				self.options[ key ] = undefined;
+				self._setOption( key, value, true );
+			});
+
+			ui.screen.bind( "vclick", $.proxy( this, "_eatEventAndClose" ) );
+
+			this._on( $.mobile.window, {
+				orientationchange: $.proxy( this, "_handleWindowOrientationchange" ),
+				resize: $.proxy( this, "_handleWindowResize" ),
+				keyup: $.proxy( this, "_handleWindowKeyUp" )
+			});
+			this._on( $.mobile.document, {
+				focusin: $.proxy( this, "_handleDocumentFocusIn" )
+			});
+		},
+
+		_applyTheme: function( dst, theme, prefix ) {
+			var classes = ( dst.attr( "class" ) || "").split( " " ),
+				alreadyAdded = true,
+				currentTheme = null,
+				matches,
+				themeStr = String( theme );
+
+			while ( classes.length > 0 ) {
+				currentTheme = classes.pop();
+				matches = ( new RegExp( "^ui-" + prefix + "-([a-z])$" ) ).exec( currentTheme );
+				if ( matches && matches.length > 1 ) {
+					currentTheme = matches[ 1 ];
+					break;
+				} else {
+					currentTheme = null;
+				}
+			}
+
+			if ( theme !== currentTheme ) {
+				dst.removeClass( "ui-" + prefix + "-" + currentTheme );
+				if ( ! ( theme === null || theme === "none" ) ) {
+					dst.addClass( "ui-" + prefix + "-" + themeStr );
+				}
+			}
+		},
+
+		_setTheme: function( value ) {
+			this._applyTheme( this.element, value, "body" );
+		},
+
+		_setOverlayTheme: function( value ) {
+			this._applyTheme( this._ui.screen, value, "overlay" );
+
+			if ( this._isOpen ) {
+				this._ui.screen.addClass( "in" );
+			}
+		},
+
+		_setShadow: function( value ) {
+			this.element.toggleClass( "ui-overlay-shadow", value );
+		},
+
+		_setCorners: function( value ) {
+			this.element.toggleClass( "ui-corner-all", value );
+		},
+
+		_applyTransition: function( value ) {
+			this._ui.container.removeClass( this._fallbackTransition );
+			if ( value && value !== "none" ) {
+				this._fallbackTransition = $.mobile._maybeDegradeTransition( value );
+				if ( this._fallbackTransition === "none" ) {
+					this._fallbackTransition = "";
+				}
+				this._ui.container.addClass( this._fallbackTransition );
+			}
+		},
+
+		_setTransition: function( value ) {
+			if ( !this._currentTransition ) {
+				this._applyTransition( value );
+			}
+		},
+
+		_setTolerance: function( value ) {
+			var tol = { t: 30, r: 15, b: 30, l: 15 };
+
+			if ( value !== undefined ) {
+				var ar = String( value ).split( "," );
+
+				$.each( ar, function( idx, val ) { ar[ idx ] = parseInt( val, 10 ); } );
+
+				switch( ar.length ) {
+					// All values are to be the same
+					case 1:
+						if ( !isNaN( ar[ 0 ] ) ) {
+							tol.t = tol.r = tol.b = tol.l = ar[ 0 ];
+						}
+						break;
+
+					// The first value denotes top/bottom tolerance, and the second value denotes left/right tolerance
+					case 2:
+						if ( !isNaN( ar[ 0 ] ) ) {
+							tol.t = tol.b = ar[ 0 ];
+						}
+						if ( !isNaN( ar[ 1 ] ) ) {
+							tol.l = tol.r = ar[ 1 ];
+						}
+						break;
+
+					// The array contains values in the order top, right, bottom, left
+					case 4:
+						if ( !isNaN( ar[ 0 ] ) ) {
+							tol.t = ar[ 0 ];
+						}
+						if ( !isNaN( ar[ 1 ] ) ) {
+							tol.r = ar[ 1 ];
+						}
+						if ( !isNaN( ar[ 2 ] ) ) {
+							tol.b = ar[ 2 ];
+						}
+						if ( !isNaN( ar[ 3 ] ) ) {
+							tol.l = ar[ 3 ];
+						}
+						break;
+
+					default:
+						break;
+				}
+			}
+
+			this._tolerance = tol;
+		},
+
+		_setOption: function( key, value ) {
+			var exclusions, setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 );
+
+			if ( this[ setter ] !== undefined ) {
+				this[ setter ]( value );
+			}
+
+			// TODO REMOVE FOR 1.2.1 by moving them out to a default options object
+			exclusions = [
+				"initSelector",
+				"closeLinkSelector",
+				"closeLinkEvents",
+				"navigateEvents",
+				"closeEvents",
+				"history",
+				"container"
+			];
+
+			$.mobile.widget.prototype._setOption.apply( this, arguments );
+			if ( $.inArray( key, exclusions ) === -1 ) {
+				// Record the option change in the options and in the DOM data-* attributes
+				this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value );
+			}
+		},
+
+		// Try and center the overlay over the given coordinates
+		_placementCoords: function( desired ) {
+			// rectangle within which the popup must fit
+			var
+				winCoords = windowCoords(),
+				rc = {
+					x: this._tolerance.l,
+					y: winCoords.y + this._tolerance.t,
+					cx: winCoords.cx - this._tolerance.l - this._tolerance.r,
+					cy: winCoords.cy - this._tolerance.t - this._tolerance.b
+				},
+				menuSize, ret;
+
+			// Clamp the width of the menu before grabbing its size
+			this._ui.container.css( "max-width", rc.cx );
+			menuSize = {
+				cx: this._ui.container.outerWidth( true ),
+				cy: this._ui.container.outerHeight( true )
+			};
+
+			// Center the menu over the desired coordinates, while not going outside
+			// the window tolerances. This will center wrt. the window if the popup is too large.
+			ret = {
+				x: fitSegmentInsideSegment( rc.cx, menuSize.cx, rc.x, desired.x ),
+				y: fitSegmentInsideSegment( rc.cy, menuSize.cy, rc.y, desired.y )
+			};
+
+			// Make sure the top of the menu is visible
+			ret.y = Math.max( 0, ret.y );
+
+			// If the height of the menu is smaller than the height of the document
+			// align the bottom with the bottom of the document
+
+			// fix for $.mobile.document.height() bug in core 1.7.2.
+			var docEl = document.documentElement, docBody = document.body,
+				docHeight = Math.max( docEl.clientHeight, docBody.scrollHeight, docBody.offsetHeight, docEl.scrollHeight, docEl.offsetHeight );
+
+			ret.y -= Math.min( ret.y, Math.max( 0, ret.y + menuSize.cy - docHeight ) );
+
+			return { left: ret.x, top: ret.y };
+		},
+
+		_createPrereqs: function( screenPrereq, containerPrereq, whenDone ) {
+			var self = this, prereqs;
+
+			// It is important to maintain both the local variable prereqs and self._prereqs. The local variable remains in
+			// the closure of the functions which call the callbacks passed in. The comparison between the local variable and
+			// self._prereqs is necessary, because once a function has been passed to .animationComplete() it will be called
+			// next time an animation completes, even if that's not the animation whose end the function was supposed to catch
+			// (for example, if an abort happens during the opening animation, the .animationComplete handler is not called for
+			// that animation anymore, but the handler remains attached, so it is called the next time the popup is opened
+			// - making it stale. Comparing the local variable prereqs to the widget-level variable self._prereqs ensures that
+			// callbacks triggered by a stale .animationComplete will be ignored.
+
+			prereqs = {
+				screen: $.Deferred(),
+				container: $.Deferred()
+			};
+
+			prereqs.screen.then( function() {
+				if ( prereqs === self._prereqs ) {
+					screenPrereq();
+				}
+			});
+
+			prereqs.container.then( function() {
+				if ( prereqs === self._prereqs ) {
+					containerPrereq();
+				}
+			});
+
+			$.when( prereqs.screen, prereqs.container ).done( function() {
+				if ( prereqs === self._prereqs ) {
+					self._prereqs = null;
+					whenDone();
+				}
+			});
+
+			self._prereqs = prereqs;
+		},
+
+		_animate: function( args ) {
+			// NOTE before removing the default animation of the screen
+			//      this had an animate callback that would resolve the deferred
+			//      now the deferred is resolved immediately
+			// TODO remove the dependency on the screen deferred
+			this._ui.screen
+				.removeClass( args.classToRemove )
+				.addClass( args.screenClassToAdd );
+
+			args.prereqs.screen.resolve();
+
+			if ( args.transition && args.transition !== "none" ) {
+				if ( args.applyTransition ) {
+					this._applyTransition( args.transition );
+				}
+				if ( this._fallbackTransition ) {
+					this._ui.container
+						.animationComplete( $.proxy( args.prereqs.container, "resolve" ) )
+						.addClass( args.containerClassToAdd )
+						.removeClass( args.classToRemove );
+					return;
+				}
+			}
+			this._ui.container.removeClass( args.classToRemove );
+			args.prereqs.container.resolve();
+		},
+
+		// The desired coordinates passed in will be returned untouched if no reference element can be identified via
+		// desiredPosition.positionTo. Nevertheless, this function ensures that its return value always contains valid
+		// x and y coordinates by specifying the center middle of the window if the coordinates are absent.
+		// options: { x: coordinate, y: coordinate, positionTo: string: "origin", "window", or jQuery selector
+		_desiredCoords: function( o ) {
+			var dst = null, offset, winCoords = windowCoords(), x = o.x, y = o.y, pTo = o.positionTo;
+
+			// Establish which element will serve as the reference
+			if ( pTo && pTo !== "origin" ) {
+				if ( pTo === "window" ) {
+					x = winCoords.cx / 2 + winCoords.x;
+					y = winCoords.cy / 2 + winCoords.y;
+				} else {
+					try {
+						dst = $( pTo );
+					} catch( e ) {
+						dst = null;
+					}
+					if ( dst ) {
+						dst.filter( ":visible" );
+						if ( dst.length === 0 ) {
+							dst = null;
+						}
+					}
+				}
+			}
+
+			// If an element was found, center over it
+			if ( dst ) {
+				offset = dst.offset();
+				x = offset.left + dst.outerWidth() / 2;
+				y = offset.top + dst.outerHeight() / 2;
+			}
+
+			// Make sure x and y are valid numbers - center over the window
+			if ( $.type( x ) !== "number" || isNaN( x ) ) {
+				x = winCoords.cx / 2 + winCoords.x;
+			}
+			if ( $.type( y ) !== "number" || isNaN( y ) ) {
+				y = winCoords.cy / 2 + winCoords.y;
+			}
+
+			return { x: x, y: y };
+		},
+
+		_reposition: function( o ) {
+			// We only care about position-related parameters for repositioning
+			o = { x: o.x, y: o.y, positionTo: o.positionTo };
+			this._trigger( "beforeposition", o );
+			this._ui.container.offset( this._placementCoords( this._desiredCoords( o ) ) );
+		},
+
+		reposition: function( o ) {
+			if ( this._isOpen ) {
+				this._reposition( o );
+			}
+		},
+
+		_openPrereqsComplete: function() {
+			this._ui.container.addClass( "ui-popup-active" );
+			this._isOpen = true;
+			this._resizeScreen();
+			this._ui.container.attr( "tabindex", "0" ).focus();
+			this._ignoreResizeEvents();
+			this._trigger( "afteropen" );
+		},
+
+		_open: function( options ) {
+			var o = $.extend( {}, this.options, options ),
+				// TODO move blacklist to private method
+				androidBlacklist = ( function() {
+					var w = window,
+						ua = navigator.userAgent,
+						// Rendering engine is Webkit, and capture major version
+						wkmatch = ua.match( /AppleWebKit\/([0-9\.]+)/ ),
+						wkversion = !!wkmatch && wkmatch[ 1 ],
+						androidmatch = ua.match( /Android (\d+(?:\.\d+))/ ),
+						andversion = !!androidmatch && androidmatch[ 1 ],
+						chromematch = ua.indexOf( "Chrome" ) > -1;
+
+					// Platform is Android, WebKit version is greater than 534.13 ( Android 3.2.1 ) and not Chrome.
+					if( androidmatch !== null && andversion === "4.0" && wkversion && wkversion > 534.13 && !chromematch ) {
+						return true;
+					}
+					return false;
+				}());
+
+			// Count down to triggering "popupafteropen" - we have two prerequisites:
+			// 1. The popup window animation completes (container())
+			// 2. The screen opacity animation completes (screen())
+			this._createPrereqs(
+				$.noop,
+				$.noop,
+				$.proxy( this, "_openPrereqsComplete" ) );
+
+			this._currentTransition = o.transition;
+			this._applyTransition( o.transition );
+
+			if ( !this.options.theme ) {
+				this._setTheme( this._page.jqmData( "theme" ) || $.mobile.getInheritedTheme( this._page, "c" ) );
+			}
+
+			this._ui.screen.removeClass( "ui-screen-hidden" );
+			this._ui.container.removeClass( "ui-popup-hidden" );
+
+			// Give applications a chance to modify the contents of the container before it appears
+			this._reposition( o );
+
+			if ( this.options.overlayTheme && androidBlacklist ) {
+				/* TODO:
+				The native browser on Android 4.0.X ("Ice Cream Sandwich") suffers from an issue where the popup overlay appears to be z-indexed
+				above the popup itself when certain other styles exist on the same page -- namely, any element set to `position: fixed` and certain
+				types of input. These issues are reminiscent of previously uncovered bugs in older versions of Android's native browser:
+				https://github.com/scottjehl/Device-Bugs/issues/3
+
+				This fix closes the following bugs ( I use "closes" with reluctance, and stress that this issue should be revisited as soon as possible ):
+
+				https://github.com/jquery/jquery-mobile/issues/4816
+				https://github.com/jquery/jquery-mobile/issues/4844
+				https://github.com/jquery/jquery-mobile/issues/4874
+				*/
+
+				// TODO sort out why this._page isn't working
+				this.element.closest( ".ui-page" ).addClass( "ui-popup-open" );
+			}
+			this._animate({
+				additionalCondition: true,
+				transition: o.transition,
+				classToRemove: "",
+				screenClassToAdd: "in",
+				containerClassToAdd: "in",
+				applyTransition: false,
+				prereqs: this._prereqs
+			});
+		},
+
+		_closePrereqScreen: function() {
+			this._ui.screen
+				.removeClass( "out" )
+				.addClass( "ui-screen-hidden" );
+		},
+
+		_closePrereqContainer: function() {
+			this._ui.container
+				.removeClass( "reverse out" )
+				.addClass( "ui-popup-hidden" )
+				.removeAttr( "style" );
+		},
+
+		_closePrereqsDone: function() {
+			var opts = this.options;
+
+			this._ui.container.removeAttr( "tabindex" );
+
+			// remove the global mutex for popups
+			$.mobile.popup.active = undefined;
+
+			// alert users that the popup is closed
+			this._trigger( "afterclose" );
+		},
+
+		_close: function( immediate ) {
+			this._ui.container.removeClass( "ui-popup-active" );
+			this._page.removeClass( "ui-popup-open" );
+
+			this._isOpen = false;
+
+			// Count down to triggering "popupafterclose" - we have two prerequisites:
+			// 1. The popup window reverse animation completes (container())
+			// 2. The screen opacity animation completes (screen())
+			this._createPrereqs(
+				$.proxy( this, "_closePrereqScreen" ),
+				$.proxy( this, "_closePrereqContainer" ),
+				$.proxy( this, "_closePrereqsDone" ) );
+
+			this._animate( {
+				additionalCondition: this._ui.screen.hasClass( "in" ),
+				transition: ( immediate ? "none" : ( this._currentTransition ) ),
+				classToRemove: "in",
+				screenClassToAdd: "out",
+				containerClassToAdd: "reverse out",
+				applyTransition: true,
+				prereqs: this._prereqs
+			});
+		},
+
+		_unenhance: function() {
+			// Put the element back to where the placeholder was and remove the "ui-popup" class
+			this._setTheme( "none" );
+			this.element
+				// Cannot directly insertAfter() - we need to detach() first, because
+				// insertAfter() will do nothing if the payload div was not attached
+				// to the DOM at the time the widget was created, and so the payload
+				// will remain inside the container even after we call insertAfter().
+				// If that happens and we remove the container a few lines below, we
+				// will cause an infinite recursion - #5244
+				.detach()
+				.insertAfter( this._ui.placeholder )
+				.removeClass( "ui-popup ui-overlay-shadow ui-corner-all" );
+			this._ui.screen.remove();
+			this._ui.container.remove();
+			this._ui.placeholder.remove();
+		},
+
+		_destroy: function() {
+			if ( $.mobile.popup.active === this ) {
+				this.element.one( "popupafterclose", $.proxy( this, "_unenhance" ) );
+				this.close();
+			} else {
+				this._unenhance();
+			}
+		},
+
+		_closePopup: function( e, data ) {
+			var parsedDst, toUrl, o = this.options, immediate = false;
+
+			// restore location on screen
+			window.scrollTo( 0, this._scrollTop );
+
+			if ( e && e.type === "pagebeforechange" && data ) {
+				// Determine whether we need to rapid-close the popup, or whether we can
+				// take the time to run the closing transition
+				if ( typeof data.toPage === "string" ) {
+					parsedDst = data.toPage;
+				} else {
+					parsedDst = data.toPage.jqmData( "url" );
+				}
+				parsedDst = $.mobile.path.parseUrl( parsedDst );
+				toUrl = parsedDst.pathname + parsedDst.search + parsedDst.hash;
+
+				if ( this._myUrl !== $.mobile.path.makeUrlAbsolute( toUrl ) ) {
+					// Going to a different page - close immediately
+					immediate = true;
+				} else {
+					e.preventDefault();
+				}
+			}
+
+			// remove nav bindings
+			o.container.unbind( o.closeEvents );
+			// unbind click handlers added when history is disabled
+			this.element.undelegate( o.closeLinkSelector, o.closeLinkEvents );
+
+			this._close( immediate );
+		},
+
+		// any navigation event after a popup is opened should close the popup
+		// NOTE the pagebeforechange is bound to catch navigation events that don't
+		//      alter the url (eg, dialogs from popups)
+		_bindContainerClose: function() {
+			this.options.container
+				.one( this.options.closeEvents, $.proxy( this, "_closePopup" ) );
+		},
+
+		// TODO no clear deliniation of what should be here and
+		// what should be in _open. Seems to be "visual" vs "history" for now
+		open: function( options ) {
+			var self = this, opts = this.options, url, hashkey, activePage, currentIsDialog, hasHash, urlHistory;
+
+			// make sure open is idempotent
+			if( $.mobile.popup.active ) {
+				return;
+			}
+
+			// set the global popup mutex
+			$.mobile.popup.active = this;
+			this._scrollTop = $.mobile.window.scrollTop();
+
+			// if history alteration is disabled close on navigate events
+			// and leave the url as is
+			if( !( opts.history ) ) {
+				self._open( options );
+				self._bindContainerClose();
+
+				// When histoy is disabled we have to grab the data-rel
+				// back link clicks so we can close the popup instead of
+				// relying on history to do it for us
+				self.element
+					.delegate( opts.closeLinkSelector, opts.closeLinkEvents, function( e ) {
+						self.close();
+						e.preventDefault();
+					});
+
+				return;
+			}
+
+			// cache some values for min/readability
+			urlHistory = $.mobile.urlHistory;
+			hashkey = $.mobile.dialogHashKey;
+			activePage = $.mobile.activePage;
+			currentIsDialog = activePage.is( ".ui-dialog" );
+			this._myUrl = url = urlHistory.getActive().url;
+			hasHash = ( url.indexOf( hashkey ) > -1 ) && !currentIsDialog && ( urlHistory.activeIndex > 0 );
+
+			if ( hasHash ) {
+				self._open( options );
+				self._bindContainerClose();
+				return;
+			}
+
+			// if the current url has no dialog hash key proceed as normal
+			// otherwise, if the page is a dialog simply tack on the hash key
+			if ( url.indexOf( hashkey ) === -1 && !currentIsDialog ){
+				url = url + (url.indexOf( "#" ) > -1 ? hashkey : "#" + hashkey);
+			} else {
+				url = $.mobile.path.parseLocation().hash + hashkey;
+			}
+
+			// Tack on an extra hashkey if this is the first page and we've just reconstructed the initial hash
+			if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) {
+				url += hashkey;
+			}
+
+			// swallow the the initial navigation event, and bind for the next
+			$(window).one( "beforenavigate", function( e ) {
+				e.preventDefault();
+				self._open( options );
+				self._bindContainerClose();
+			});
+
+			this.urlAltered = true;
+			$.mobile.navigate( url, {role: "dialog"} );
+		},
+
+		close: function() {
+			// make sure close is idempotent
+			if( $.mobile.popup.active !== this ) {
+				return;
+			}
+
+			this._scrollTop = $.mobile.window.scrollTop();
+
+			if( this.options.history && this.urlAltered ) {
+				$.mobile.back();
+				this.urlAltered = false;
+			} else {
+				// simulate the nav bindings having fired
+				this._closePopup();
+			}
+		}
+	});
+
+
+	// TODO this can be moved inside the widget
+	$.mobile.popup.handleLink = function( $link ) {
+		var closestPage = $link.closest( ":jqmData(role='page')" ),
+			scope = ( ( closestPage.length === 0 ) ? $( "body" ) : closestPage ),
+			// NOTE make sure to get only the hash, ie7 (wp7) return the absolute href
+			//      in this case ruining the element selection
+			popup = $( $.mobile.path.parseUrl($link.attr( "href" )).hash, scope[0] ),
+			offset;
+
+		if ( popup.data( "mobile-popup" ) ) {
+			offset = $link.offset();
+			popup.popup( "open", {
+				x: offset.left + $link.outerWidth() / 2,
+				y: offset.top + $link.outerHeight() / 2,
+				transition: $link.jqmData( "transition" ),
+				positionTo: $link.jqmData( "position-to" )
+			});
+		}
+
+		//remove after delay
+		setTimeout( function() {
+			// Check if we are in a listview
+			var $parent = $link.parent().parent();
+			if ($parent.hasClass("ui-li")) {
+				$link = $parent.parent();
+			}
+			$link.removeClass( $.mobile.activeBtnClass );
+		}, 300 );
+	};
+
+	// TODO move inside _create
+	$.mobile.document.bind( "pagebeforechange", function( e, data ) {
+		if ( data.options.role === "popup" ) {
+			$.mobile.popup.handleLink( data.options.link );
+			e.preventDefault();
+		}
+	});
+
+	$.mobile.document.bind( "pagecreate create", function( e )  {
+		$.mobile.popup.prototype.enhanceWithin( e.target, true );
+	});
+
+})( jQuery );
+
+/*
+* custom "selectmenu" plugin
+*/
+
+(function( $, undefined ) {
+	var extendSelect = function( widget ) {
+
+		var select = widget.select,
+			origDestroy = widget._destroy,
+			selectID  = widget.selectID,
+			prefix = ( selectID ? selectID : ( ( $.mobile.ns || "" ) + "uuid-" + widget.uuid ) ),
+			popupID = prefix + "-listbox",
+			dialogID = prefix + "-dialog",
+			label = widget.label,
+			thisPage = widget.select.closest( ".ui-page" ),
+			selectOptions = widget._selectOptions(),
+			isMultiple = widget.isMultiple = widget.select[ 0 ].multiple,
+			buttonId = selectID + "-button",
+			menuId = selectID + "-menu",
+			menuPage = $( "<div data-" + $.mobile.ns + "role='dialog' id='" + dialogID + "' data-" +$.mobile.ns + "theme='"+ widget.options.theme +"' data-" +$.mobile.ns + "overlay-theme='"+ widget.options.overlayTheme +"'>" +
+				"<div data-" + $.mobile.ns + "role='header'>" +
+				"<div class='ui-title'>" + label.getEncodedText() + "</div>"+
+				"</div>"+
+				"<div data-" + $.mobile.ns + "role='content'></div>"+
+				"</div>" ),
+
+			listbox =  $( "<div id='" + popupID + "' class='ui-selectmenu'>" ).insertAfter( widget.select ).popup( { theme: widget.options.overlayTheme } ),
+
+			list = $( "<ul>", {
+				"class": "ui-selectmenu-list",
+				"id": menuId,
+				"role": "listbox",
+				"aria-labelledby": buttonId
+				}).attr( "data-" + $.mobile.ns + "theme", widget.options.theme )
+					.attr( "data-" + $.mobile.ns + "divider-theme", widget.options.dividerTheme )
+					.appendTo( listbox ),
+
+
+			header = $( "<div>", {
+				"class": "ui-header ui-bar-" + widget.options.theme
+			}).prependTo( listbox ),
+
+			headerTitle = $( "<h1>", {
+				"class": "ui-title"
+			}).appendTo( header ),
+
+			menuPageContent,
+			menuPageClose,
+			headerClose;
+
+		if ( widget.isMultiple ) {
+			headerClose = $( "<a>", {
+				"text": widget.options.closeText,
+				"href": "#",
+				"class": "ui-btn-left"
+			}).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup();
+		}
+
+		$.extend( widget, {
+			select: widget.select,
+			selectID: selectID,
+			buttonId: buttonId,
+			menuId: menuId,
+			popupID: popupID,
+			dialogID: dialogID,
+			thisPage: thisPage,
+			menuPage: menuPage,
+			label: label,
+			selectOptions: selectOptions,
+			isMultiple: isMultiple,
+			theme: widget.options.theme,
+			listbox: listbox,
+			list: list,
+			header: header,
+			headerTitle: headerTitle,
+			headerClose: headerClose,
+			menuPageContent: menuPageContent,
+			menuPageClose: menuPageClose,
+			placeholder: "",
+
+			build: function() {
+				var self = this;
+
+				// Create list from select, update state
+				self.refresh();
+
+				if ( self._origTabIndex === undefined ) {
+					// Map undefined to false, because self._origTabIndex === undefined
+					// indicates that we have not yet checked whether the select has
+					// originally had a tabindex attribute, whereas false indicates that
+					// we have checked the select for such an attribute, and have found
+					// none present.
+					self._origTabIndex = ( self.select[ 0 ].getAttribute( "tabindex" ) === null ) ? false : self.select.attr( "tabindex" );
+				}
+				self.select.attr( "tabindex", "-1" ).focus(function() {
+					$( this ).blur();
+					self.button.focus();
+				});
+
+				// Button events
+				self.button.bind( "vclick keydown" , function( event ) {
+					if ( self.options.disabled || self.isOpen ) {
+						return;
+					}
+
+					if (event.type === "vclick" ||
+							event.keyCode && (event.keyCode === $.mobile.keyCode.ENTER ||
+																event.keyCode === $.mobile.keyCode.SPACE)) {
+
+						self._decideFormat();
+						if ( self.menuType === "overlay" ) {
+							self.button.attr( "href", "#" + self.popupID ).attr( "data-" + ( $.mobile.ns || "" ) + "rel", "popup" );
+						} else {
+							self.button.attr( "href", "#" + self.dialogID ).attr( "data-" + ( $.mobile.ns || "" ) + "rel", "dialog" );
+						}
+						self.isOpen = true;
+						// Do not prevent default, so the navigation may have a chance to actually open the chosen format
+					}
+				});
+
+				// Events for list items
+				self.list.attr( "role", "listbox" )
+					.bind( "focusin", function( e ) {
+						$( e.target )
+							.attr( "tabindex", "0" )
+							.trigger( "vmouseover" );
+
+					})
+					.bind( "focusout", function( e ) {
+						$( e.target )
+							.attr( "tabindex", "-1" )
+							.trigger( "vmouseout" );
+					})
+					.delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) {
+
+						// index of option tag to be selected
+						var oldIndex = self.select[ 0 ].selectedIndex,
+							newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ),
+							option = self._selectOptions().eq( newIndex )[ 0 ];
+
+						// toggle selected status on the tag for multi selects
+						option.selected = self.isMultiple ? !option.selected : true;
+
+						// toggle checkbox class for multiple selects
+						if ( self.isMultiple ) {
+							$( this ).find( ".ui-icon" )
+								.toggleClass( "ui-icon-checkbox-on", option.selected )
+								.toggleClass( "ui-icon-checkbox-off", !option.selected );
+						}
+
+						// trigger change if value changed
+						if ( self.isMultiple || oldIndex !== newIndex ) {
+							self.select.trigger( "change" );
+						}
+
+						// hide custom select for single selects only - otherwise focus clicked item
+						// We need to grab the clicked item the hard way, because the list may have been rebuilt
+						if ( self.isMultiple ) {
+							self.list.find( "li:not(.ui-li-divider)" ).eq( newIndex )
+								.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
+						}
+						else {
+							self.close();
+						}
+
+						event.preventDefault();
+					})
+					.keydown(function( event ) {  //keyboard events for menu items
+						var target = $( event.target ),
+							li = target.closest( "li" ),
+							prev, next;
+
+						// switch logic based on which key was pressed
+						switch ( event.keyCode ) {
+							// up or left arrow keys
+						case 38:
+							prev = li.prev().not( ".ui-selectmenu-placeholder" );
+
+							if ( prev.is( ".ui-li-divider" ) ) {
+								prev = prev.prev();
+							}
+
+							// if there's a previous option, focus it
+							if ( prev.length ) {
+								target
+									.blur()
+									.attr( "tabindex", "-1" );
+
+								prev.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
+							}
+
+							return false;
+							// down or right arrow keys
+						case 40:
+							next = li.next();
+
+							if ( next.is( ".ui-li-divider" ) ) {
+								next = next.next();
+							}
+
+							// if there's a next option, focus it
+							if ( next.length ) {
+								target
+									.blur()
+									.attr( "tabindex", "-1" );
+
+								next.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
+							}
+
+							return false;
+							// If enter or space is pressed, trigger click
+						case 13:
+						case 32:
+							target.trigger( "click" );
+
+							return false;
+						}
+					});
+
+				// button refocus ensures proper height calculation
+				// by removing the inline style and ensuring page inclusion
+				self.menuPage.bind( "pagehide", function() {
+					// TODO centralize page removal binding / handling in the page plugin.
+					// Suggestion from @jblas to do refcounting
+					//
+					// TODO extremely confusing dependency on the open method where the pagehide.remove
+					// bindings are stripped to prevent the parent page from disappearing. The way
+					// we're keeping pages in the DOM right now sucks
+					//
+					// rebind the page remove that was unbound in the open function
+					// to allow for the parent page removal from actions other than the use
+					// of a dialog sized custom select
+					//
+					// doing this here provides for the back button on the custom select dialog
+					$.mobile._bindPageRemove.call( self.thisPage );
+				});
+
+				// Events on the popup
+				self.listbox.bind( "popupafterclose", function( event ) {
+					self.close();
+				});
+
+				// Close button on small overlays
+				if ( self.isMultiple ) {
+					self.headerClose.click(function() {
+						if ( self.menuType === "overlay" ) {
+							self.close();
+							return false;
+						}
+					});
+				}
+
+				// track this dependency so that when the parent page
+				// is removed on pagehide it will also remove the menupage
+				self.thisPage.addDependents( this.menuPage );
+			},
+
+			_isRebuildRequired: function() {
+				var list = this.list.find( "li" ),
+					options = this._selectOptions();
+
+				// TODO exceedingly naive method to determine difference
+				// ignores value changes etc in favor of a forcedRebuild
+				// from the user in the refresh method
+				return options.text() !== list.text();
+			},
+
+			selected: function() {
+				return this._selectOptions().filter( ":selected:not( :jqmData(placeholder='true') )" );
+			},
+
+			refresh: function( forceRebuild , foo ) {
+				var self = this,
+				select = this.element,
+				isMultiple = this.isMultiple,
+				indicies;
+
+				if (  forceRebuild || this._isRebuildRequired() ) {
+					self._buildList();
+				}
+
+				indicies = this.selectedIndices();
+
+				self.setButtonText();
+				self.setButtonCount();
+
+				self.list.find( "li:not(.ui-li-divider)" )
+					.removeClass( $.mobile.activeBtnClass )
+					.attr( "aria-selected", false )
+					.each(function( i ) {
+
+						if ( $.inArray( i, indicies ) > -1 ) {
+							var item = $( this );
+
+							// Aria selected attr
+							item.attr( "aria-selected", true );
+
+							// Multiple selects: add the "on" checkbox state to the icon
+							if ( self.isMultiple ) {
+								item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" );
+							} else {
+								if ( item.is( ".ui-selectmenu-placeholder" ) ) {
+									item.next().addClass( $.mobile.activeBtnClass );
+								} else {
+									item.addClass( $.mobile.activeBtnClass );
+								}
+							}
+						}
+					});
+			},
+
+			close: function() {
+				if ( this.options.disabled || !this.isOpen ) {
+					return;
+				}
+
+				var self = this;
+
+				if ( self.menuType === "page" ) {
+					self.menuPage.dialog( "close" );
+					self.list.appendTo( self.listbox );
+				} else {
+					self.listbox.popup( "close" );
+				}
+
+				self._focusButton();
+				// allow the dialog to be closed again
+				self.isOpen = false;
+			},
+
+			open: function() {
+				this.button.click();
+			},
+
+			_decideFormat: function() {
+				var self = this,
+					$window = $.mobile.window,
+					selfListParent = self.list.parent(),
+					menuHeight = selfListParent.outerHeight(),
+					menuWidth = selfListParent.outerWidth(),
+					activePage = $( "." + $.mobile.activePageClass ),
+					scrollTop = $window.scrollTop(),
+					btnOffset = self.button.offset().top,
+					screenHeight = $window.height(),
+					screenWidth = $window.width();
+
+				function focusMenuItem() {
+					var selector = self.list.find( "." + $.mobile.activeBtnClass + " a" );
+					if ( selector.length === 0 ) {
+						selector = self.list.find( "li.ui-btn:not( :jqmData(placeholder='true') ) a" );
+					}
+					selector.first().focus().closest( "li" ).addClass( "ui-btn-down-" + widget.options.theme );
+				}
+
+				if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) {
+
+					self.menuPage.appendTo( $.mobile.pageContainer ).page();
+					self.menuPageContent = menuPage.find( ".ui-content" );
+					self.menuPageClose = menuPage.find( ".ui-header a" );
+
+					// prevent the parent page from being removed from the DOM,
+					// otherwise the results of selecting a list item in the dialog
+					// fall into a black hole
+					self.thisPage.unbind( "pagehide.remove" );
+
+					//for WebOS/Opera Mini (set lastscroll using button offset)
+					if ( scrollTop === 0 && btnOffset > screenHeight ) {
+						self.thisPage.one( "pagehide", function() {
+							$( this ).jqmData( "lastScroll", btnOffset );
+						});
+					}
+
+					self.menuPage
+						.one( "pageshow", function() {
+							focusMenuItem();
+						})
+						.one( "pagehide", function() {
+							self.close();
+						});
+
+					self.menuType = "page";
+					self.menuPageContent.append( self.list );
+					self.menuPage.find("div .ui-title").text(self.label.text());
+				} else {
+					self.menuType = "overlay";
+
+					self.listbox.one( "popupafteropen", focusMenuItem );
+				}
+			},
+
+			_buildList: function() {
+				var self = this,
+					o = this.options,
+					placeholder = this.placeholder,
+					needPlaceholder = true,
+					optgroups = [],
+					lis = [],
+					dataIcon = self.isMultiple ? "checkbox-off" : "false";
+
+				self.list.empty().filter( ".ui-listview" ).listview( "destroy" );
+
+				var $options = self.select.find( "option" ),
+					numOptions = $options.length,
+					select = this.select[ 0 ],
+					dataPrefix = 'data-' + $.mobile.ns,
+					dataIndexAttr = dataPrefix + 'option-index',
+					dataIconAttr = dataPrefix + 'icon',
+					dataRoleAttr = dataPrefix + 'role',
+					dataPlaceholderAttr = dataPrefix + 'placeholder',
+					fragment = document.createDocumentFragment(),
+					isPlaceholderItem = false,
+					optGroup;
+
+				for (var i = 0; i < numOptions;i++, isPlaceholderItem = false) {
+					var option = $options[i],
+						$option = $( option ),
+						parent = option.parentNode,
+						text = $option.text(),
+						anchor  = document.createElement( 'a' ),
+						classes = [];
+
+					anchor.setAttribute( 'href', '#' );
+					anchor.appendChild( document.createTextNode( text ) );
+
+					// Are we inside an optgroup?
+					if ( parent !== select && parent.nodeName.toLowerCase() === "optgroup" ) {
+						var optLabel = parent.getAttribute( 'label' );
+						if ( optLabel !== optGroup ) {
+							var divider = document.createElement( 'li' );
+							divider.setAttribute( dataRoleAttr, 'list-divider' );
+							divider.setAttribute( 'role', 'option' );
+							divider.setAttribute( 'tabindex', '-1' );
+							divider.appendChild( document.createTextNode( optLabel ) );
+							fragment.appendChild( divider );
+							optGroup = optLabel;
+						}
+					}
+
+					if ( needPlaceholder && ( !option.getAttribute( "value" ) || text.length === 0 || $option.jqmData( "placeholder" ) ) ) {
+						needPlaceholder = false;
+						isPlaceholderItem = true;
+
+						// If we have identified a placeholder, record the fact that it was
+						// us who have added the placeholder to the option and mark it
+						// retroactively in the select as well
+						if ( null === option.getAttribute( dataPlaceholderAttr ) ) {
+							this._removePlaceholderAttr = true;
+						}
+						option.setAttribute( dataPlaceholderAttr, true );
+						if ( o.hidePlaceholderMenuItems ) {
+							classes.push( "ui-selectmenu-placeholder" );
+						}
+						if ( placeholder !== text ) {
+							placeholder = self.placeholder = text;
+						}
+					}
+
+					var item = document.createElement('li');
+					if ( option.disabled ) {
+						classes.push( "ui-disabled" );
+						item.setAttribute('aria-disabled',true);
+					}
+					item.setAttribute( dataIndexAttr,i );
+					item.setAttribute( dataIconAttr, dataIcon );
+					if ( isPlaceholderItem ) {
+						item.setAttribute( dataPlaceholderAttr, true );
+					}
+					item.className = classes.join( " " );
+					item.setAttribute( 'role', 'option' );
+					anchor.setAttribute( 'tabindex', '-1' );
+					item.appendChild( anchor );
+					fragment.appendChild( item );
+				}
+
+				self.list[0].appendChild( fragment );
+
+				// Hide header if it's not a multiselect and there's no placeholder
+				if ( !this.isMultiple && !placeholder.length ) {
+					this.header.hide();
+				} else {
+					this.headerTitle.text( this.placeholder );
+				}
+
+				// Now populated, create listview
+				self.list.listview();
+			},
+
+			_button: function() {
+				return $( "<a>", {
+					"href": "#",
+					"role": "button",
+					// TODO value is undefined at creation
+					"id": this.buttonId,
+					"aria-haspopup": "true",
+
+					// TODO value is undefined at creation
+					"aria-owns": this.menuId
+				});
+			},
+
+			_destroy: function() {
+				this.close();
+
+				// Restore the tabindex attribute to its original value
+				if ( this._origTabIndex !== undefined ) {
+					if ( this._origTabIndex !== false ) {
+						this.select.attr( "tabindex", this._origTabIndex );
+					} else {
+						this.select.removeAttr( "tabindex" );
+					}
+				}
+
+				// Remove the placeholder attribute if we were the ones to add it
+				if ( this._removePlaceholderAttr ) {
+					this._selectOptions().removeAttr( "data-" + $.mobile.ns + "placeholder" );
+				}
+
+				// Remove the popup
+				this.listbox.remove();
+
+				// Chain up
+				origDestroy.apply( this, arguments );
+			}
+		});
+	};
+
+	// issue #3894 - core doesn't trigger events on disabled delegates
+	$.mobile.document.bind( "selectmenubeforecreate", function( event ) {
+		var selectmenuWidget = $( event.target ).data( "mobile-selectmenu" );
+
+		if ( !selectmenuWidget.options.nativeMenu &&
+				selectmenuWidget.element.parents( ":jqmData(role='popup')" ).length === 0 ) {
+			extendSelect( selectmenuWidget );
+		}
+	});
+})( jQuery );
+
+(function( $, undefined ) {
+
+	$.widget( "mobile.controlgroup", $.mobile.widget, $.extend( {
+		options: {
+			shadow: false,
+			corners: true,
+			excludeInvisible: true,
+			type: "vertical",
+			mini: false,
+			initSelector: ":jqmData(role='controlgroup')"
+		},
+
+		_create: function() {
+			var $el = this.element,
+				ui = {
+					inner: $( "<div class='ui-controlgroup-controls'></div>" ),
+					legend: $( "<div role='heading' class='ui-controlgroup-label'></div>" )
+				},
+				grouplegend = $el.children( "legend" ),
+				self = this;
+
+			// Apply the proto
+			$el.wrapInner( ui.inner );
+			if ( grouplegend.length ) {
+				ui.legend.append( grouplegend ).insertBefore( $el.children( 0 ) );
+			}
+			$el.addClass( "ui-corner-all ui-controlgroup" );
+
+			$.extend( this, {
+				_initialRefresh: true
+			});
+
+			$.each( this.options, function( key, value ) {
+				// Cause initial options to be applied by their handler by temporarily setting the option to undefined
+				// - the handler then sets it to the initial value
+				self.options[ key ] = undefined;
+				self._setOption( key, value, true );
+			});
+		},
+
+		_init: function() {
+			this.refresh();
+		},
+
+		_setOption: function( key, value ) {
+			var setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 );
+
+			if ( this[ setter ] !== undefined ) {
+				this[ setter ]( value );
+			}
+
+			this._super( key, value );
+			this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value );
+		},
+
+		_setType: function( value ) {
+			this.element
+				.removeClass( "ui-controlgroup-horizontal ui-controlgroup-vertical" )
+				.addClass( "ui-controlgroup-" + value );
+			this.refresh();
+		},
+
+		_setCorners: function( value ) {
+			this.element.toggleClass( "ui-corner-all", value );
+		},
+
+		_setShadow: function( value ) {
+			this.element.toggleClass( "ui-shadow", value );
+		},
+
+		_setMini: function( value ) {
+			this.element.toggleClass( "ui-mini", value );
+		},
+
+		container: function() {
+			return this.element.children( ".ui-controlgroup-controls" );
+		},
+
+		refresh: function() {
+			var els = this.element.find( ".ui-btn" ).not( ".ui-slider-handle" ),
+				create = this._initialRefresh;
+			if ( $.mobile.checkboxradio ) {
+				this.element.find( ":mobile-checkboxradio" ).checkboxradio( "refresh" );
+			}
+			this._addFirstLastClasses( els, this.options.excludeInvisible ? this._getVisibles( els, create ) : els, create );
+			this._initialRefresh = false;
+		}
+	}, $.mobile.behaviors.addFirstLastClasses ) );
+
+	// TODO: Implement a mechanism to allow widgets to become enhanced in the
+	// correct order when their correct enhancement depends on other widgets in
+	// the page being correctly enhanced already.
+	//
+	// For now, we wait until dom-ready to attach the controlgroup's enhancement
+	// hook, because by that time, all the other widgets' enhancement hooks should
+	// already be in place, ensuring that all widgets that need to be grouped will
+	// already have been enhanced by the time the controlgroup is created.
+	$( function() {
+		$.mobile.document.bind( "pagecreate create", function( e )  {
+			$.mobile.controlgroup.prototype.enhanceWithin( e.target, true );
+		});
+	});
+})(jQuery);
+
+(function( $, undefined ) {
+
+$( document ).bind( "pagecreate create", function( e ) {
+
+	//links within content areas, tests included with page
+	$( e.target )
+		.find( "a" )
+		.jqmEnhanceable()
+		.not( ".ui-btn, .ui-link-inherit, :jqmData(role='none'), :jqmData(role='nojs')" )
+		.addClass( "ui-link" );
+
+});
+
+})( jQuery );
+
+
+(function( $, undefined ) {
+
+
+	$.widget( "mobile.fixedtoolbar", $.mobile.widget, {
+		options: {
+			visibleOnPageShow: true,
+			disablePageZoom: true,
+			transition: "slide", //can be none, fade, slide (slide maps to slideup or slidedown)
+			fullscreen: false,
+			tapToggle: true,
+			tapToggleBlacklist: "a, button, input, select, textarea, .ui-header-fixed, .ui-footer-fixed, .ui-popup, .ui-panel, .ui-panel-dismiss-open",
+			hideDuringFocus: "input, textarea, select",
+			updatePagePadding: true,
+			trackPersistentToolbars: true,
+
+			// Browser detection! Weeee, here we go...
+			// Unfortunately, position:fixed is costly, not to mention probably impossible, to feature-detect accurately.
+			// Some tests exist, but they currently return false results in critical devices and browsers, which could lead to a broken experience.
+			// Testing fixed positioning is also pretty obtrusive to page load, requiring injected elements and scrolling the window
+			// The following function serves to rule out some popular browsers with known fixed-positioning issues
+			// This is a plugin option like any other, so feel free to improve or overwrite it
+			supportBlacklist: function() {
+				return !$.support.fixedPosition;
+			},
+			initSelector: ":jqmData(position='fixed')"
+		},
+
+		_create: function() {
+
+			var self = this,
+				o = self.options,
+				$el = self.element,
+				tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer",
+				$page = $el.closest( ".ui-page" );
+
+			// Feature detecting support for
+			if ( o.supportBlacklist() ) {
+				self.destroy();
+				return;
+			}
+
+			$el.addClass( "ui-"+ tbtype +"-fixed" );
+
+			// "fullscreen" overlay positioning
+			if ( o.fullscreen ) {
+				$el.addClass( "ui-"+ tbtype +"-fullscreen" );
+				$page.addClass( "ui-page-" + tbtype + "-fullscreen" );
+			}
+			// If not fullscreen, add class to page to set top or bottom padding
+			else{
+				$page.addClass( "ui-page-" + tbtype + "-fixed" );
+			}
+
+			$.extend( this, {
+				_thisPage: null
+			});
+ 
+			self._addTransitionClass();
+			self._bindPageEvents();
+			self._bindToggleHandlers();
+		},
+
+		_addTransitionClass: function() {
+			var tclass = this.options.transition;
+
+			if ( tclass && tclass !== "none" ) {
+				// use appropriate slide for header or footer
+				if ( tclass === "slide" ) {
+					tclass = this.element.is( ".ui-header" ) ? "slidedown" : "slideup";
+				}
+
+				this.element.addClass( tclass );
+			}
+		},
+
+		_bindPageEvents: function() {
+			this._thisPage = this.element.closest( ".ui-page" );
+			//page event bindings
+			// Fixed toolbars require page zoom to be disabled, otherwise usability issues crop up
+			// This method is meant to disable zoom while a fixed-positioned toolbar page is visible
+			this._on( this._thisPage, {
+				"pagebeforeshow": "_handlePageBeforeShow",
+				"webkitAnimationStart":"_handleAnimationStart",
+				"animationstart":"_handleAnimationStart",
+				"updatelayout": "_handleAnimationStart",
+				"pageshow": "_handlePageShow",
+				"pagebeforehide": "_handlePageBeforeHide"
+			});
+		},
+
+		_handlePageBeforeShow: function() {
+			var o = this.options;
+			if ( o.disablePageZoom ) {
+				$.mobile.zoom.disable( true );
+			}
+			if ( !o.visibleOnPageShow ) {
+				this.hide( true );
+			}
+		},
+
+		_handleAnimationStart: function() {
+			if ( this.options.updatePagePadding ) {
+				this.updatePagePadding( this._thisPage );
+			}
+		},
+
+		_handlePageShow: function() {
+			this.updatePagePadding( this._thisPage );
+			if ( this.options.updatePagePadding ) {
+				this._on( $.mobile.window, { "throttledresize": "updatePagePadding" } );
+			}
+		},
+
+		_handlePageBeforeHide: function( e, ui ) {
+			var o = this.options;
+
+			if ( o.disablePageZoom ) {
+				$.mobile.zoom.enable( true );
+			}
+			if ( o.updatePagePadding ) {
+				this._off( $.mobile.window, "throttledresize" );
+			}
+
+			if ( o.trackPersistentToolbars ) {
+				var thisFooter = $( ".ui-footer-fixed:jqmData(id)", this._thisPage ),
+					thisHeader = $( ".ui-header-fixed:jqmData(id)", this._thisPage ),
+					nextFooter = thisFooter.length && ui.nextPage && $( ".ui-footer-fixed:jqmData(id='" + thisFooter.jqmData( "id" ) + "')", ui.nextPage ) || $(),
+					nextHeader = thisHeader.length && ui.nextPage && $( ".ui-header-fixed:jqmData(id='" + thisHeader.jqmData( "id" ) + "')", ui.nextPage ) || $();
+
+				if ( nextFooter.length || nextHeader.length ) {
+
+					nextFooter.add( nextHeader ).appendTo( $.mobile.pageContainer );
+
+					ui.nextPage.one( "pageshow", function() {
+						nextHeader.prependTo( this );
+						nextFooter.appendTo( this );
+					});
+				}
+			}
+		},
+
+		_visible: true,
+
+		// This will set the content element's top or bottom padding equal to the toolbar's height
+		updatePagePadding: function( tbPage ) {
+			var $el = this.element,
+				header = $el.is( ".ui-header" ),
+				pos = parseFloat( $el.css( header ? "top" : "bottom" ) );
+
+			// This behavior only applies to "fixed", not "fullscreen"
+			if ( this.options.fullscreen ) { return; }
+
+			// tbPage argument can be a Page object or an event, if coming from throttled resize. 
+			tbPage = ( tbPage && tbPage.type === undefined && tbPage ) || this._thisPage || $el.closest( ".ui-page" );
+			$( tbPage ).css( "padding-" + ( header ? "top" : "bottom" ), $el.outerHeight() + pos );
+		},
+
+		_useTransition: function( notransition ) {
+			var $win = $.mobile.window,
+				$el = this.element,
+				scroll = $win.scrollTop(),
+				elHeight = $el.height(),
+				pHeight = $el.closest( ".ui-page" ).height(),
+				viewportHeight = $.mobile.getScreenHeight(),
+				tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer";
+
+			return !notransition &&
+				( this.options.transition && this.options.transition !== "none" &&
+				(
+					( tbtype === "header" && !this.options.fullscreen && scroll > elHeight ) ||
+					( tbtype === "footer" && !this.options.fullscreen && scroll + viewportHeight < pHeight - elHeight )
+				) || this.options.fullscreen
+				);
+		},
+
+		show: function( notransition ) {
+			var hideClass = "ui-fixed-hidden",
+				$el = this.element;
+
+			if ( this._useTransition( notransition ) ) {
+				$el
+					.removeClass( "out " + hideClass )
+					.addClass( "in" )
+					.animationComplete(function () {
+						$el.removeClass('in');
+					});
+			}
+			else {
+				$el.removeClass( hideClass );
+			}
+			this._visible = true;
+		},
+
+		hide: function( notransition ) {
+			var hideClass = "ui-fixed-hidden",
+				$el = this.element,
+				// if it's a slide transition, our new transitions need the reverse class as well to slide outward
+				outclass = "out" + ( this.options.transition === "slide" ? " reverse" : "" );
+
+			if( this._useTransition( notransition ) ) {
+				$el
+					.addClass( outclass )
+					.removeClass( "in" )
+					.animationComplete(function() {
+						$el.addClass( hideClass ).removeClass( outclass );
+					});
+			}
+			else {
+				$el.addClass( hideClass ).removeClass( outclass );
+			}
+			this._visible = false;
+		},
+
+		toggle: function() {
+			this[ this._visible ? "hide" : "show" ]();
+		},
+
+		_bindToggleHandlers: function() {
+			var self = this,
+				o = self.options,
+				$el = self.element,
+				delayShow, delayHide,
+				isVisible = true;
+
+			// tap toggle
+			$el.closest( ".ui-page" )
+				.bind( "vclick", function( e ) {
+					if ( o.tapToggle && !$( e.target ).closest( o.tapToggleBlacklist ).length ) {
+						self.toggle();
+					}
+				})
+				.bind( "focusin focusout", function( e ) {
+					//this hides the toolbars on a keyboard pop to give more screen room and prevent ios bug which 
+					//positions fixed toolbars in the middle of the screen on pop if the input is near the top or
+					//bottom of the screen addresses issues #4410 Footer navbar moves up when clicking on a textbox in an Android environment
+					//and issue #4113 Header and footer change their position after keyboard popup - iOS
+					//and issue #4410 Footer navbar moves up when clicking on a textbox in an Android environment
+					if ( screen.width < 1025 && $( e.target ).is( o.hideDuringFocus ) && !$( e.target ).closest( ".ui-header-fixed, .ui-footer-fixed" ).length ) {
+						//Fix for issue #4724 Moving through form in Mobile Safari with "Next" and "Previous" system 
+						//controls causes fixed position, tap-toggle false Header to reveal itself
+						// isVisible instead of self._visible because the focusin and focusout events fire twice at the same time
+						// Also use a delay for hiding the toolbars because on Android native browser focusin is direclty followed
+						// by a focusout when a native selects opens and the other way around when it closes.
+						if ( e.type === "focusout" && !isVisible ) {
+							isVisible = true;
+							//wait for the stack to unwind and see if we have jumped to another input
+							clearTimeout( delayHide );
+							delayShow = setTimeout( function() {
+								self.show();
+							}, 0 ); 
+						} else if ( e.type === "focusin" && !!isVisible ) {
+							//if we have jumped to another input clear the time out to cancel the show.
+							clearTimeout( delayShow );
+							isVisible = false;
+							delayHide = setTimeout( function() {
+								self.hide();
+							}, 0 ); 
+						}
+					}
+				});
+		},
+
+		_destroy: function() {
+			var $el = this.element,
+				header = $el.is( ".ui-header" );
+
+			$el.closest( ".ui-page" ).css( "padding-" + ( header ? "top" : "bottom" ), "" );
+			$el.removeClass( "ui-header-fixed ui-footer-fixed ui-header-fullscreen ui-footer-fullscreen in out fade slidedown slideup ui-fixed-hidden" );
+			$el.closest( ".ui-page" ).removeClass( "ui-page-header-fixed ui-page-footer-fixed ui-page-header-fullscreen ui-page-footer-fullscreen" );
+		}
+
+	});
+
+	//auto self-init widgets
+	$.mobile.document
+		.bind( "pagecreate create", function( e ) {
+
+			// DEPRECATED in 1.1: support for data-fullscreen=true|false on the page element.
+			// This line ensures it still works, but we recommend moving the attribute to the toolbars themselves.
+			if ( $( e.target ).jqmData( "fullscreen" ) ) {
+				$( $.mobile.fixedtoolbar.prototype.options.initSelector, e.target ).not( ":jqmData(fullscreen)" ).jqmData( "fullscreen", true );
+			}
+
+			$.mobile.fixedtoolbar.prototype.enhanceWithin( e.target );
+		});
+
+})( jQuery );
+
+(function( $, undefined ) {
+	$.widget( "mobile.fixedtoolbar", $.mobile.fixedtoolbar, {
+
+			_create: function() {
+				this._super();
+				this._workarounds();
+			},
+
+			//check the browser and version and run needed workarounds
+			_workarounds: function() {
+				var ua = navigator.userAgent,
+				platform = navigator.platform,
+				// Rendering engine is Webkit, and capture major version
+				wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ),
+				wkversion = !!wkmatch && wkmatch[ 1 ],
+				os = null,
+				self = this;
+				//set the os we are working in if it dosent match one with workarounds return
+				if( platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1  || platform.indexOf( "iPod" ) > -1 ){
+					os = "ios";
+				} else if( ua.indexOf( "Android" ) > -1 ){
+					os = "android";
+				} else {
+					return;
+				}
+				//check os version if it dosent match one with workarounds return
+				if( os === "ios" ) {
+					//iOS  workarounds
+					self._bindScrollWorkaround();
+				} else if( os === "android" && wkversion && wkversion < 534 ) {
+					//Android 2.3 run all Android 2.3 workaround
+					self._bindScrollWorkaround();
+					self._bindListThumbWorkaround();
+				} else {
+					return;
+				}
+			},
+
+			//Utility class for checking header and footer positions relative to viewport
+			_viewportOffset: function() {
+				var $el = this.element,
+					header = $el.is( ".ui-header" ),
+					offset = Math.abs($el.offset().top - $.mobile.window.scrollTop());
+				if( !header ) {
+					offset = Math.round(offset - $.mobile.window.height() + $el.outerHeight())-60;
+				}
+				return offset;
+			},
+
+			//bind events for _triggerRedraw() function 
+			_bindScrollWorkaround: function() {
+				var self = this;
+				//bind to scrollstop and check if the toolbars are correctly positioned
+				this._on( $.mobile.window, { scrollstop: function() {
+					var viewportOffset = self._viewportOffset();
+					//check if the header is visible and if its in the right place
+					if( viewportOffset > 2 && self._visible) {
+						self._triggerRedraw();
+					}
+				}});
+			},
+
+			//this addresses issue #4250 Persistent footer instability in v1.1 with long select lists in Android 2.3.3
+			//and issue #3748 Android 2.x: Page transitions broken when fixed toolbars used
+			//the absolutely positioned thumbnail in a list view causes problems with fixed position buttons above in a nav bar
+			//setting the li's to -webkit-transform:translate3d(0,0,0); solves this problem to avoide potential issues in other
+			//platforms we scope this with the class ui-android-2x-fix
+			_bindListThumbWorkaround: function() {
+				this.element.closest(".ui-page").addClass( "ui-android-2x-fixed" );
+			},
+			//this addresses issues #4337 Fixed header problem after scrolling content on iOS and Android
+			//and device bugs project issue #1 Form elements can lose click hit area in position: fixed containers.
+			//this also addresses not on fixed toolbars page in docs
+			//adding 1px of padding to the bottom then removing it causes a "redraw"
+			//which positions the toolbars correctly (they will always be visually correct) 
+			_triggerRedraw: function() {
+				var paddingBottom = parseFloat( $( ".ui-page-active" ).css( "padding-bottom" ) );
+				//trigger page redraw to fix incorrectly positioned fixed elements
+				$( ".ui-page-active" ).css( "padding-bottom", ( paddingBottom + 1 ) +"px" );
+				//if the padding is reset with out a timeout the reposition will not occure.
+				//this is independant of JQM the browser seems to need the time to react.
+				setTimeout( function() {
+					$( ".ui-page-active" ).css( "padding-bottom", paddingBottom + "px" );
+				}, 0 );
+			},
+
+			destroy: function() {
+				this._super();
+				//Remove the class we added to the page previously in android 2.x 
+				this.element.closest(".ui-page-active").removeClass( "ui-android-2x-fix" );
+			}
+	});
+
+	})( jQuery );
+
+(function( $, undefined ) {
+
+$.widget( "mobile.panel", $.mobile.widget, {
+	options: {
+		classes: {
+			panel: "ui-panel",
+			panelOpen: "ui-panel-open",
+			panelClosed: "ui-panel-closed",
+			panelFixed: "ui-panel-fixed",
+			panelInner: "ui-panel-inner",
+			modal: "ui-panel-dismiss",
+			modalOpen: "ui-panel-dismiss-open",
+			pagePanel: "ui-page-panel",
+			pagePanelOpen: "ui-page-panel-open",
+			contentWrap: "ui-panel-content-wrap",
+			contentWrapOpen: "ui-panel-content-wrap-open",
+			contentWrapClosed: "ui-panel-content-wrap-closed",
+			contentFixedToolbar: "ui-panel-content-fixed-toolbar",
+			contentFixedToolbarOpen: "ui-panel-content-fixed-toolbar-open",
+			contentFixedToolbarClosed: "ui-panel-content-fixed-toolbar-closed",
+			animate: "ui-panel-animate"
+		},
+		animate: true,
+		theme: "c",
+		position: "left",
+		dismissible: true,
+		display: "reveal", //accepts reveal, push, overlay
+		initSelector: ":jqmData(role='panel')",
+		swipeClose: true,
+		positionFixed: false
+	},
+
+	_panelID: null,
+	_closeLink: null,
+	_page: null,
+	_modal: null,
+	_panelInner: null,
+	_wrapper: null,
+	_fixedToolbar: null,
+
+	_create: function() {
+		var self = this,
+			$el = self.element,
+			page = $el.closest( ":jqmData(role='page')" ),
+			_getPageTheme = function() {
+				var $theme = $.data( page[0], "mobilePage" ).options.theme,
+				$pageThemeClass = "ui-body-" + $theme;
+				return $pageThemeClass;
+			},
+			_getPanelInner = function() {
+				var $panelInner = $el.find( "." + self.options.classes.panelInner );
+				if ( $panelInner.length === 0 ) {
+					$panelInner = $el.children().wrapAll( '<div class="' + self.options.classes.panelInner + '" />' ).parent();
+				}
+				return $panelInner;
+			},
+			_getWrapper = function() {
+				var $wrapper = page.find( "." + self.options.classes.contentWrap );
+				if ( $wrapper.length === 0 ) {
+					$wrapper = page.children( ".ui-header:not(:jqmData(position='fixed')), .ui-content:not(:jqmData(role='popup')), .ui-footer:not(:jqmData(position='fixed'))" ).wrapAll( '<div class="' + self.options.classes.contentWrap + ' ' + _getPageTheme() + '" />' ).parent();
+					if ( $.support.cssTransform3d && !!self.options.animate ) {
+						$wrapper.addClass( self.options.classes.animate );
+					}
+				}
+				return $wrapper;
+			},
+			_getFixedToolbar = function() {
+				var $fixedToolbar = page.find( "." + self.options.classes.contentFixedToolbar );
+				if ( $fixedToolbar.length === 0 ) {
+					$fixedToolbar = page.find( ".ui-header:jqmData(position='fixed'), .ui-footer:jqmData(position='fixed')" ).addClass( self.options.classes.contentFixedToolbar );
+					if ( $.support.cssTransform3d && !!self.options.animate ) {
+						$fixedToolbar.addClass( self.options.classes.animate );
+					}
+				}
+				return $fixedToolbar;
+			};
+
+		// expose some private props to other methods
+		$.extend( this, {
+			_panelID: $el.attr( "id" ),
+			_closeLink: $el.find( ":jqmData(rel='close')" ),
+			_page: $el.closest( ":jqmData(role='page')" ),
+			_pageTheme: _getPageTheme(),
+			_panelInner: _getPanelInner(),
+			_wrapper: _getWrapper(),
+			_fixedToolbar: _getFixedToolbar()
+		});
+		
+		self._addPanelClasses();
+		self._wrapper.addClass( this.options.classes.contentWrapClosed );
+		self._fixedToolbar.addClass( this.options.classes.contentFixedToolbarClosed );
+		// add class to page so we can set "overflow-x: hidden;" for it to fix Android zoom issue
+		self._page.addClass( self.options.classes.pagePanel );
+		
+		// if animating, add the class to do so
+		if ( $.support.cssTransform3d && !!self.options.animate ) {
+			this.element.addClass( self.options.classes.animate );
+		}
+		
+		self._bindUpdateLayout();
+		self._bindCloseEvents();
+		self._bindLinkListeners();
+		self._bindPageEvents();
+
+		if ( !!self.options.dismissible ) {
+			self._createModal();
+		}
+
+		self._bindSwipeEvents();
+	},
+
+	_createModal: function( options ) {
+		var self = this;
+		
+		self._modal = $( "<div class='" + self.options.classes.modal + "' data-panelid='" + self._panelID + "'></div>" )
+			.on( "mousedown", function() {
+				self.close();
+			})
+			.appendTo( this._page );
+	},
+
+	_getPosDisplayClasses: function( prefix ) {
+		return prefix + "-position-" + this.options.position + " " + prefix + "-display-" + this.options.display;
+	},
+
+	_getPanelClasses: function() {
+		var panelClasses = this.options.classes.panel +
+			" " + this._getPosDisplayClasses( this.options.classes.panel ) +
+			" " + this.options.classes.panelClosed;
+
+		if ( this.options.theme ) {
+			panelClasses += " ui-body-" + this.options.theme;
+		}
+		if ( !!this.options.positionFixed ) {
+			panelClasses += " " + this.options.classes.panelFixed;
+		}
+		return panelClasses;
+	},
+
+	_addPanelClasses: function() {
+		this.element.addClass( this._getPanelClasses() );
+	},
+
+	_bindCloseEvents: function() {
+		var self = this;
+		
+		self._closeLink.on( "click.panel" , function( e ) {
+			e.preventDefault();
+			self.close();
+			return false;
+		});
+		self.element.on( "click.panel" , "a:jqmData(ajax='false')", function( e ) {
+			self.close();
+		});		
+	},
+
+	_positionPanel: function() {
+		var self = this,
+			panelInnerHeight = self._panelInner.outerHeight(),
+			expand = panelInnerHeight > $.mobile.getScreenHeight();
+
+		if ( expand || !self.options.positionFixed ) {
+			if ( expand ) {
+				self._unfixPanel();
+				$.mobile.resetActivePageHeight( panelInnerHeight );
+			}
+			self._scrollIntoView( panelInnerHeight );
+		} else {
+			self._fixPanel();
+		}
+	},
+
+	_scrollIntoView: function( panelInnerHeight ) {
+		if ( panelInnerHeight < $( window ).scrollTop() ) {
+			window.scrollTo( 0, 0 );
+		}	
+	},
+
+	_bindFixListener: function() {
+		this._on( $( window ), { "throttledresize": "_positionPanel" });
+	},
+
+	_unbindFixListener: function() {
+		this._off( $( window ), "throttledresize" );
+	},
+
+	_unfixPanel: function() {
+		if ( !!this.options.positionFixed && $.support.fixedPosition ) {
+			this.element.removeClass( this.options.classes.panelFixed );
+		}
+	},
+
+	_fixPanel: function() {
+		if ( !!this.options.positionFixed && $.support.fixedPosition ) {
+			this.element.addClass( this.options.classes.panelFixed );
+		}
+	},
+	
+	_bindUpdateLayout: function() {
+		var self = this;
+		
+		self.element.on( "updatelayout", function( e ) {
+			if ( self._open ) {
+				self._positionPanel();
+			}
+		});
+	},
+
+	_bindLinkListeners: function() {
+		var self = this;
+
+		self._page.on( "click.panel" , "a", function( e ) {
+			if ( this.href.split( "#" )[ 1 ] === self._panelID && self._panelID !== undefined ) {
+				e.preventDefault();
+				var $link = $( this );
+				if ( ! $link.hasClass( "ui-link" ) ) {
+					$link.addClass( $.mobile.activeBtnClass );
+					self.element.one( "panelopen panelclose", function() {
+						$link.removeClass( $.mobile.activeBtnClass );
+					});
+				}
+				self.toggle();
+				return false;
+			}
+		});
+	},
+	
+	_bindSwipeEvents: function() {
+		var self = this,
+			area = self._modal ? self.element.add( self._modal ) : self.element;
+		
+		// on swipe, close the panel
+		if( !!self.options.swipeClose ) {
+			if ( self.options.position === "left" ) {
+				area.on( "swipeleft.panel", function( e ) {
+					self.close();
+				});
+			} else {
+				area.on( "swiperight.panel", function( e ) {
+					self.close();
+				});
+			}
+		}
+	},
+
+	_bindPageEvents: function() {
+		var self = this;
+			
+		self._page
+			// Close the panel if another panel on the page opens
+			.on( "panelbeforeopen", function( e ) {
+				if ( self._open && e.target !== self.element[ 0 ] ) {
+					self.close();
+				}
+			})
+			// clean up open panels after page hide
+			.on( "pagehide", function( e ) {
+				if ( self._open ) {
+					self.close( true );
+				}
+			})
+			// on escape, close? might need to have a target check too...
+			.on( "keyup.panel", function( e ) {
+				if ( e.keyCode === 27 && self._open ) {
+					self.close();
+				}
+			});
+	},
+
+	// state storage of open or closed
+	_open: false,
+
+	_contentWrapOpenClasses: null,
+	_fixedToolbarOpenClasses: null,
+	_modalOpenClasses: null,
+
+	open: function( immediate ) {
+		if ( !this._open ) {
+			var self = this,
+				o = self.options,
+				_openPanel = function() {
+					self._page.off( "panelclose" );
+					self._page.jqmData( "panel", "open" );
+					
+					if ( !immediate && $.support.cssTransform3d && !!o.animate ) {
+						self.element.add( self._wrapper ).on( self._transitionEndEvents, complete );
+					} else {
+						setTimeout( complete, 0 );
+					}
+					
+					if ( self.options.theme && self.options.display !== "overlay" ) {
+						self._page
+							.removeClass( self._pageTheme )
+							.addClass( "ui-body-" + self.options.theme );
+					}
+					
+					self.element.removeClass( o.classes.panelClosed ).addClass( o.classes.panelOpen );
+					
+					self._positionPanel();
+					
+					// Fix for IE7 min-height bug
+					if ( self.options.theme && self.options.display !== "overlay" ) {
+						self._wrapper.css( "min-height", self._page.css( "min-height" ) );
+					}
+					
+					self._contentWrapOpenClasses = self._getPosDisplayClasses( o.classes.contentWrap );
+					self._wrapper
+						.removeClass( o.classes.contentWrapClosed )
+						.addClass( self._contentWrapOpenClasses + " " + o.classes.contentWrapOpen );
+						
+					self._fixedToolbarOpenClasses = self._getPosDisplayClasses( o.classes.contentFixedToolbar );
+					self._fixedToolbar
+						.removeClass( o.classes.contentFixedToolbarClosed )
+						.addClass( self._fixedToolbarOpenClasses + " " + o.classes.contentFixedToolbarOpen );
+						
+					self._modalOpenClasses = self._getPosDisplayClasses( o.classes.modal ) + " " + o.classes.modalOpen;
+					if ( self._modal ) {
+						self._modal.addClass( self._modalOpenClasses );
+					}
+				},
+				complete = function() {
+					self.element.add( self._wrapper ).off( self._transitionEndEvents, complete );
+
+					self._page.addClass( o.classes.pagePanelOpen );
+					
+					self._bindFixListener();
+					
+					self._trigger( "open" );
+				};
+
+			if ( this.element.closest( ".ui-page-active" ).length < 0 ) {
+				immediate = true;
+			}
+			
+			self._trigger( "beforeopen" );
+			
+			if ( self._page.jqmData('panel') === "open" ) {
+				self._page.on( "panelclose", function() {
+					_openPanel();
+				});
+			} else {
+				_openPanel();
+			}
+			
+			self._open = true;
+		}
+	},
+
+	close: function( immediate ) {
+		if ( this._open ) {
+			var o = this.options,
+				self = this,
+				_closePanel = function() {
+					if ( !immediate && $.support.cssTransform3d && !!o.animate ) {
+						self.element.add( self._wrapper ).on( self._transitionEndEvents, complete );
+					} else {
+						setTimeout( complete, 0 );
+					}
+					
+					self._page.removeClass( o.classes.pagePanelOpen );
+					self.element.removeClass( o.classes.panelOpen );
+					self._wrapper.removeClass( o.classes.contentWrapOpen );
+					self._fixedToolbar.removeClass( o.classes.contentFixedToolbarOpen );
+					
+					if ( self._modal ) {
+						self._modal.removeClass( self._modalOpenClasses );
+					}
+				},
+				complete = function() {
+					if ( self.options.theme && self.options.display !== "overlay" ) {
+						self._page.removeClass( "ui-body-" + self.options.theme ).addClass( self._pageTheme );
+						// reset fix for IE7 min-height bug
+						self._wrapper.css( "min-height", "" );
+					}
+					self.element.add( self._wrapper ).off( self._transitionEndEvents, complete );
+					self.element.addClass( o.classes.panelClosed );
+					
+					self._wrapper
+						.removeClass( self._contentWrapOpenClasses )
+						.addClass( o.classes.contentWrapClosed );
+						
+					self._fixedToolbar
+						.removeClass( self._fixedToolbarOpenClasses )
+						.addClass( o.classes.contentFixedToolbarClosed );
+						
+					self._fixPanel();
+					self._unbindFixListener();
+					$.mobile.resetActivePageHeight();
+					
+					self._page.jqmRemoveData( "panel" );
+					self._trigger( "close" );
+				};
+				
+			if ( this.element.closest( ".ui-page-active" ).length < 0 ) {
+				immediate = true;
+			}
+			self._trigger( "beforeclose" );
+
+			_closePanel();
+
+			self._open = false;
+		}
+	},
+	
+	toggle: function( options ) {
+		this[ this._open ? "close" : "open" ]();
+	},
+
+	_transitionEndEvents: "webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd",
+
+	_destroy: function() {
+		var classes = this.options.classes,
+			theme = this.options.theme,
+			hasOtherSiblingPanels = this.element.siblings( "." + classes.panel ).length;
+
+		// create
+		if ( !hasOtherSiblingPanels ) {
+			this._wrapper.children().unwrap();
+			this._page.find( "a" ).unbind( "panelopen panelclose" );
+			this._page.removeClass( classes.pagePanel );
+			if ( this._open ) {
+				this._page.jqmRemoveData( "panel" );
+				this._page.removeClass( classes.pagePanelOpen );
+				if ( theme ) {
+					this._page.removeClass( "ui-body-" + theme ).addClass( this._pageTheme );
+				}
+				$.mobile.resetActivePageHeight();
+			}
+		} else if ( this._open ) {
+			this._wrapper.removeClass( classes.contentWrapOpen );
+			this._fixedToolbar.removeClass( classes.contentFixedToolbarOpen );
+			this._page.jqmRemoveData( "panel" );
+			this._page.removeClass( classes.pagePanelOpen );
+			if ( theme ) {
+				this._page.removeClass( "ui-body-" + theme ).addClass( this._pageTheme );
+			}
+		}
+		
+		this._panelInner.children().unwrap();
+
+		this.element.removeClass( [ this._getPanelClasses(), classes.panelAnimate ].join( " " ) )
+			.off( "swipeleft.panel swiperight.panel" )
+			.off( "panelbeforeopen" )
+			.off( "panelhide" )
+			.off( "keyup.panel" )
+			.off( "updatelayout" );
+
+		this._closeLink.off( "click.panel" );
+
+		if ( this._modal ) {
+			this._modal.remove();
+		}
+
+		// open and close
+		this.element.off( this._transitionEndEvents )
+			.removeClass( [ classes.panelUnfixed, classes.panelClosed, classes.panelOpen ].join( " " ) );
+	}
+});
+
+//auto self-init widgets
+$( document ).bind( "pagecreate create", function( e ) {
+	$.mobile.panel.prototype.enhanceWithin( e.target );
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.widget( "mobile.table", $.mobile.widget, {
+
+		options: {
+			classes: {
+				table: "ui-table"
+			},
+			initSelector: ":jqmData(role='table')"
+		},
+
+		_create: function() {
+			var self = this;
+			self.refresh( true );
+		},
+
+		refresh: function (create) {
+			var self = this,
+				trs = this.element.find( "thead tr" );
+
+			if ( create ) {
+				this.element.addClass( this.options.classes.table );
+			}
+
+			// Expose headers and allHeaders properties on the widget
+			// headers references the THs within the first TR in the table
+			self.headers = this.element.find( "tr:eq(0)" ).children();
+
+			// allHeaders references headers, plus all THs in the thead, which may include several rows, or not
+			self.allHeaders = self.headers.add( trs.children() );
+
+			trs.each(function(){
+
+				var coltally = 0;
+
+				$( this ).children().each(function( i ){
+
+					var span = parseInt( $( this ).attr( "colspan" ), 10 ),
+						sel = ":nth-child(" + ( coltally + 1 ) + ")";
+					$( this )
+						.jqmData( "colstart", coltally + 1 );
+
+					if( span ){
+						for( var j = 0; j < span - 1; j++ ){
+							coltally++;
+							sel += ", :nth-child(" + ( coltally + 1 ) + ")";
+						}
+					}
+
+					if ( create === undefined ) {
+						$(this).jqmData("cells", "");
+					}
+					// Store "cells" data on header as a reference to all cells in the same column as this TH
+					$( this )
+						.jqmData( "cells", self.element.find( "tr" ).not( trs.eq(0) ).not( this ).children( sel ) );
+
+					coltally++;
+
+				});
+
+			});
+
+			// update table modes
+			if ( create === undefined ) {
+				this.element.trigger( 'refresh' );
+			}
+	}
+
+});
+
+//auto self-init widgets
+$.mobile.document.bind( "pagecreate create", function( e ) {
+	$.mobile.table.prototype.enhanceWithin( e.target );
+});
+
+})( jQuery );
+
+
+(function( $, undefined ) {
+
+$.mobile.table.prototype.options.mode = "columntoggle";
+
+$.mobile.table.prototype.options.columnBtnTheme = null;
+
+$.mobile.table.prototype.options.columnPopupTheme = null;
+
+$.mobile.table.prototype.options.columnBtnText = "Columns...";
+
+$.mobile.table.prototype.options.classes = $.extend(
+	$.mobile.table.prototype.options.classes,
+	{
+		popup: "ui-table-columntoggle-popup",
+		columnBtn: "ui-table-columntoggle-btn",
+		priorityPrefix: "ui-table-priority-",
+		columnToggleTable: "ui-table-columntoggle"
+	}
+);
+
+$.mobile.document.delegate( ":jqmData(role='table')", "tablecreate refresh", function( e ) {
+	
+	var $table = $( this ),
+		self = $table.data( "mobile-table" ),
+		event = e.type,
+		o = self.options,
+		ns = $.mobile.ns,
+		id = ( $table.attr( "id" ) || o.classes.popup ) + "-popup", /* TODO BETTER FALLBACK ID HERE */
+		$menuButton,
+		$popup,
+		$menu,
+		$switchboard;
+
+	if ( o.mode !== "columntoggle" ) {
+		return;
+	}
+
+	if ( event !== "refresh" ) {
+		self.element.addClass( o.classes.columnToggleTable );
+	
+		$menuButton = $( "<a href='#" + id + "' class='" + o.classes.columnBtn + "' data-" + ns + "rel='popup' data-" + ns + "mini='true'>" + o.columnBtnText + "</a>" ),
+		$popup = $( "<div data-" + ns + "role='popup' data-" + ns + "role='fieldcontain' class='" + o.classes.popup + "' id='" + id + "'></div>"),
+		$menu = $("<fieldset data-" + ns + "role='controlgroup'></fieldset>");
+	}
+	
+	// create the hide/show toggles
+	self.headers.not( "td" ).each(function( i ) {
+
+		var priority = $( this ).jqmData( "priority" ),
+			$cells = $( this ).add( $( this ).jqmData( "cells" ) );
+
+		if ( priority ) {
+
+			$cells.addClass( o.classes.priorityPrefix + priority );
+
+			if ( event !== "refresh" ) {
+				$("<label><input type='checkbox' checked />" + $( this ).text() + "</label>" )
+					.appendTo( $menu )
+					.children( 0 )
+					.jqmData( "cells", $cells )
+					.checkboxradio({
+						theme: o.columnPopupTheme
+					});
+			} else {
+				$( '#' + id + ' fieldset div:eq(' + i +')').find('input').jqmData( 'cells', $cells );
+			}
+		}
+	});
+	
+	if ( event !== "refresh" ) {
+		$menu.appendTo( $popup );
+	}
+
+	// bind change event listeners to inputs - TODO: move to a private method?
+	if ( $menu === undefined ) {
+		$switchboard = $('#' + id + ' fieldset');
+	} else {
+		$switchboard = $menu;
+	}
+
+	if ( event !== "refresh" ) {
+		$switchboard.on( "change", "input", function( e ){
+			if( this.checked ){
+				$( this ).jqmData( "cells" ).removeClass( "ui-table-cell-hidden" ).addClass( "ui-table-cell-visible" );
+			} else {
+				$( this ).jqmData( "cells" ).removeClass( "ui-table-cell-visible" ).addClass( "ui-table-cell-hidden" );
+			}
+		});
+
+		$menuButton
+			.insertBefore( $table )
+			.buttonMarkup({
+				theme: o.columnBtnTheme
+			});
+
+		$popup
+			.insertBefore( $table )
+			.popup();
+	}
+
+	// refresh method
+	self.update = function(){
+		$switchboard.find( "input" ).each( function(){
+			if (this.checked) {
+				this.checked = $( this ).jqmData( "cells" ).eq(0).css( "display" ) === "table-cell";
+				if (event === "refresh") {
+					$( this ).jqmData( "cells" ).addClass('ui-table-cell-visible');
+				}
+			} else {
+				$( this ).jqmData( "cells" ).addClass('ui-table-cell-hidden');
+			}
+			$( this ).checkboxradio( "refresh" );
+		});
+	};
+
+	$.mobile.window.on( "throttledresize", self.update );
+
+	self.update();
+
+});
+
+})( jQuery );
+
+(function( $, undefined ) {
+
+$.mobile.table.prototype.options.mode = "reflow";
+
+$.mobile.table.prototype.options.classes = $.extend(
+	$.mobile.table.prototype.options.classes,
+	{
+		reflowTable: "ui-table-reflow",
+		cellLabels: "ui-table-cell-label"
+	}
+);
+
+$.mobile.document.delegate( ":jqmData(role='table')", "tablecreate refresh", function( e ) {
+
+	var $table = $( this ),
+		event = e.type,
+		self = $table.data( "mobile-table" ),
+		o = self.options;
+
+	// If it's not reflow mode, return here.
+	if( o.mode !== "reflow" ){
+		return;
+	}
+
+	if ( event !== "refresh" ) {
+		self.element.addClass( o.classes.reflowTable );
+	}
+
+	// get headers in reverse order so that top-level headers are appended last
+	var reverseHeaders =  $( self.allHeaders.get().reverse() );
+
+	// create the hide/show toggles
+	reverseHeaders.each(function( i ){
+		var $cells = $( this ).jqmData( "cells" ),
+			colstart = $( this ).jqmData( "colstart" ),
+			hierarchyClass = $cells.not( this ).filter( "thead th" ).length && " ui-table-cell-label-top",
+			text = $(this).text();
+
+			if( text !== ""  ){
+
+				if( hierarchyClass ){
+					var iteration = parseInt( $( this ).attr( "colspan" ), 10 ),
+						filter = "";
+
+					if( iteration ){
+						filter = "td:nth-child("+ iteration +"n + " + ( colstart ) +")";
+					}
+					$cells.filter( filter ).prepend( "<b class='" + o.classes.cellLabels + hierarchyClass + "'>" + text + "</b>"  );
+				}
+				else {
+					$cells.prepend( "<b class='" + o.classes.cellLabels + "'>" + text + "</b>"  );
+				}
+
+			}
+	});
+
+});
+
+})( jQuery );
+
+(function( $, window ) {
+
+	$.mobile.iosorientationfixEnabled = true;
+
+	// This fix addresses an iOS bug, so return early if the UA claims it's something else.
+	var ua = navigator.userAgent;
+	if( !( /iPhone|iPad|iPod/.test( navigator.platform ) && /OS [1-5]_[0-9_]* like Mac OS X/i.test( ua ) && ua.indexOf( "AppleWebKit" ) > -1 ) ){
+		$.mobile.iosorientationfixEnabled = false;
+		return;
+	}
+
+	var zoom = $.mobile.zoom,
+		evt, x, y, z, aig;
+
+	function checkTilt( e ) {
+		evt = e.originalEvent;
+		aig = evt.accelerationIncludingGravity;
+
+		x = Math.abs( aig.x );
+		y = Math.abs( aig.y );
+		z = Math.abs( aig.z );
+
+		// If portrait orientation and in one of the danger zones
+		if ( !window.orientation && ( x > 7 || ( ( z > 6 && y < 8 || z < 8 && y > 6 ) && x > 5 ) ) ) {
+				if ( zoom.enabled ) {
+					zoom.disable();
+				}
+		}	else if ( !zoom.enabled ) {
+				zoom.enable();
+		}
+	}
+
+	$.mobile.document.on( "mobileinit", function(){
+		if( $.mobile.iosorientationfixEnabled ){
+			$.mobile.window
+				.bind( "orientationchange.iosorientationfix", zoom.enable )
+				.bind( "devicemotion.iosorientationfix", checkTilt );
+		}
+	});
+
+}( jQuery, this ));
+
+(function( $, window, undefined ) {
+	var	$html = $( "html" ),
+			$head = $( "head" ),
+			$window = $.mobile.window;
+
+	//remove initial build class (only present on first pageshow)
+	function hideRenderingClass() {
+		$html.removeClass( "ui-mobile-rendering" );
+	}
+
+	// trigger mobileinit event - useful hook for configuring $.mobile settings before they're used
+	$( window.document ).trigger( "mobileinit" );
+
+	// support conditions
+	// if device support condition(s) aren't met, leave things as they are -> a basic, usable experience,
+	// otherwise, proceed with the enhancements
+	if ( !$.mobile.gradeA() ) {
+		return;
+	}
+
+	// override ajaxEnabled on platforms that have known conflicts with hash history updates
+	// or generally work better browsing in regular http for full page refreshes (BB5, Opera Mini)
+	if ( $.mobile.ajaxBlacklist ) {
+		$.mobile.ajaxEnabled = false;
+	}
+
+	// Add mobile, initial load "rendering" classes to docEl
+	$html.addClass( "ui-mobile ui-mobile-rendering" );
+
+	// This is a fallback. If anything goes wrong (JS errors, etc), or events don't fire,
+	// this ensures the rendering class is removed after 5 seconds, so content is visible and accessible
+	setTimeout( hideRenderingClass, 5000 );
+
+	$.extend( $.mobile, {
+		// find and enhance the pages in the dom and transition to the first page.
+		initializePage: function() {
+			// find present pages
+			var path = $.mobile.path,
+				$pages = $( ":jqmData(role='page'), :jqmData(role='dialog')" ),
+				hash = path.stripHash( path.stripQueryParams(path.parseLocation().hash) ),
+				hashPage = document.getElementById( hash );
+
+			// if no pages are found, create one with body's inner html
+			if ( !$pages.length ) {
+				$pages = $( "body" ).wrapInner( "<div data-" + $.mobile.ns + "role='page'></div>" ).children( 0 );
+			}
+
+			// add dialogs, set data-url attrs
+			$pages.each(function() {
+				var $this = $( this );
+
+				// unless the data url is already set set it to the pathname
+				if ( !$this.jqmData( "url" ) ) {
+					$this.attr( "data-" + $.mobile.ns + "url", $this.attr( "id" ) || location.pathname + location.search );
+				}
+			});
+
+			// define first page in dom case one backs out to the directory root (not always the first page visited, but defined as fallback)
+			$.mobile.firstPage = $pages.first();
+
+			// define page container
+			$.mobile.pageContainer = $.mobile.firstPage.parent().addClass( "ui-mobile-viewport" );
+
+			// alert listeners that the pagecontainer has been determined for binding
+			// to events triggered on it
+			$window.trigger( "pagecontainercreate" );
+
+			// cue page loading message
+			$.mobile.showPageLoadingMsg();
+
+			//remove initial build class (only present on first pageshow)
+			hideRenderingClass();
+
+			// if hashchange listening is disabled, there's no hash deeplink,
+			// the hash is not valid (contains more than one # or does not start with #)
+			// or there is no page with that hash, change to the first page in the DOM
+			// Remember, however, that the hash can also be a path!
+			if ( ! ( $.mobile.hashListeningEnabled &&
+				$.mobile.path.isHashValid( location.hash ) &&
+				( $( hashPage ).is( ':jqmData(role="page")' ) ||
+					$.mobile.path.isPath( hash ) ||
+					hash === $.mobile.dialogHashKey ) ) ) {
+
+				// Store the initial destination
+				if ( $.mobile.path.isHashValid( location.hash ) ) {
+					$.mobile.urlHistory.initialDst = hash.replace( "#", "" );
+				}
+
+				// make sure to set initial popstate state if it exists
+				// so that navigation back to the initial page works properly
+				if( $.event.special.navigate.isPushStateEnabled() ) {
+					$.mobile.navigate.navigator.squash( path.parseLocation().href );
+				}
+
+				$.mobile.changePage( $.mobile.firstPage, {
+					transition: "none",
+					reverse: true,
+					changeHash: false,
+					fromHashChange: true
+				});
+			} else {
+				// trigger hashchange or navigate to squash and record the correct
+				// history entry for an initial hash path
+				if( !$.event.special.navigate.isPushStateEnabled() ) {
+					$window.trigger( "hashchange", [true] );
+				} else {
+					// TODO figure out how to simplify this interaction with the initial history entry
+					// at the bottom js/navigate/navigate.js
+					$.mobile.navigate.history.stack = [];
+					$.mobile.navigate( $.mobile.path.isPath( location.hash ) ? location.hash : location.href );
+				}
+			}
+		}
+	});
+
+	// initialize events now, after mobileinit has occurred
+	$.mobile.navreadyDeferred.resolve();
+
+	// check which scrollTop value should be used by scrolling to 1 immediately at domready
+	// then check what the scroll top is. Android will report 0... others 1
+	// note that this initial scroll won't hide the address bar. It's just for the check.
+	$(function() {
+		window.scrollTo( 0, 1 );
+
+		// if defaultHomeScroll hasn't been set yet, see if scrollTop is 1
+		// it should be 1 in most browsers, but android treats 1 as 0 (for hiding addr bar)
+		// so if it's 1, use 0 from now on
+		$.mobile.defaultHomeScroll = ( !$.support.scrollTop || $.mobile.window.scrollTop() === 1 ) ? 0 : 1;
+
+		//dom-ready inits
+		if ( $.mobile.autoInitializePage ) {
+			$.mobile.initializePage();
+		}
+
+		// window load event
+		// hide iOS browser chrome on load
+		$window.load( $.mobile.silentScroll );
+
+		if ( !$.support.cssPointerEvents ) {
+			// IE and Opera don't support CSS pointer-events: none that we use to disable link-based buttons
+			// by adding the 'ui-disabled' class to them. Using a JavaScript workaround for those browser.
+			// https://github.com/jquery/jquery-mobile/issues/3558
+
+			$.mobile.document.delegate( ".ui-disabled", "vclick",
+				function( e ) {
+					e.preventDefault();
+					e.stopImmediatePropagation();
+				}
+			);
+		}
+	});
+}( jQuery, this ));
+
+
+}));
diff --git a/misc/PublicationBulletins/Portail-LeHavre/js/bulletin.js b/misc/PublicationBulletins/Portail-LeHavre/js/bulletin.js
new file mode 100755
index 0000000000000000000000000000000000000000..e73ee8bb46cd753f37134e21260971161a17e009
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/js/bulletin.js
@@ -0,0 +1,50 @@
+// Affichage bulletin de notes
+// (uses jQuery)
+
+
+// Change visibility of UE details (les <tr> de classe "notes_bulletin_row_mod" suivant)
+// La table a la structure suivante:
+//  <tr class="notes_bulletin_row_ue"><td><span class="toggle_ue">+/-</span>...</td>...</tr>
+//  <tr class="notes_bulletin_row_mod">...</tr>
+//  <tr class="notes_bulletin_row_eval">...</tr>
+//
+// On change la visi de tous les <tr> jusqu'au notes_bulletin_row_ue suivant.
+//
+function toggle_vis_ue(e, new_state) { 
+    // e is the span containg the clicked +/- icon
+    var tr = e.parentNode.parentNode;
+    if (new_state == undefined) {
+	// current state: use alt attribute of current image
+	if (e.childNodes[0].alt == '+') {
+            new_state=false;
+	} else {
+            new_state=true;
+	}
+    } 
+    // find next tr in siblings
+    var tr = tr.nextSibling;
+    //while ((tr != null) && sibl.tagName == 'TR') {
+    var current = true;
+    while ((tr != null) && current) {
+	if ((tr.nodeType==1) && (tr.tagName == 'TR')) {
+	    for (var i=0; i < tr.classList.length; i++) {
+		if (tr.classList[i] == 'notes_bulletin_row_ue') 
+		    current = false;
+ 	    }
+	    if (current) {
+		if (new_state) {
+		    tr.style.display = 'none';
+		} else {
+		    tr.style.display = 'table-row';
+		}
+	    }
+        }
+        tr = tr.nextSibling;	
+    }
+    if (new_state) {
+	e.innerHTML = '<img width="13" height="13" border="0" title="" alt="+" src="icons/plus_img.png"/>';
+    } else {
+	e.innerHTML = '<img width="13" height="13" border="0" title="" alt="-" src="icons/minus_img.png"/>';
+    }
+}
+
diff --git a/misc/PublicationBulletins/Portail-LeHavre/js/radar_bulletin.js b/misc/PublicationBulletins/Portail-LeHavre/js/radar_bulletin.js
new file mode 100755
index 0000000000000000000000000000000000000000..0d44e1a47457cfaf4cb3a2b5b126210044479df8
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/js/radar_bulletin.js
@@ -0,0 +1,182 @@
+//
+// Diagramme "radar" montrant les notes d'un étudiant
+//
+// ScoDoc, (c) E. Viennet 2012
+//
+// Ce code utilise d3.js
+
+
+
+var WIDTH = 460; // taille du canvas SVG
+var HEIGHT = WIDTH;
+var CX = WIDTH/2; // coordonnees centre du cercle
+var CY = HEIGHT/2; 
+var RR = 0.4*WIDTH; // Rayon du cercle exterieur
+
+/* Emplacements des marques (polygones et axe gradué) */
+var R_TICS = [ 8, 10, 20 ]; /* [6, 8, 10, 12, 14, 16, 18, 20]; */
+var R_AXIS_TICS = [4, 6, 8, 10, 12, 14, 16, 18, 20];
+var NB_TICS = R_TICS.length;
+
+
+        draw_radar(notes); 
+
+
+function draw_radar(notes) {
+    /* Calcul coordonnées des éléments */
+    var nmod = notes.length;
+    var angle = 2*Math.PI/nmod;
+
+    for (var i=0; i<notes.length; i++) {
+        var d = notes[i];
+        var cx = Math.sin(i*angle);
+        var cy = - Math.cos(i*angle);
+        d["x_v"] = CX + RR * d.note/20 * cx;
+        d["y_v"] = CY + RR * d.note/20 * cy;
+        d["x_moy"] = CX + RR * d.moy/20 * cx;
+        d["y_moy"] = CY + RR * d.moy/20 * cy;
+        d["x_20"] = CX + RR * cx;
+        d["y_20"] = CY + RR * cy;
+        d["x_label"] = CX + (RR + 25) * cx - 10
+        d["y_label"] = CY + (RR + 25) * cy + 10;
+        d["tics"] = [];
+        // Coords des tics sur chaque axe
+        for (var j=0; j < NB_TICS; j++) {
+            var r = R_TICS[j]/20 * RR;
+            d["tics"][j] = { "x" : CX + r * cx, "y" : CY + r * cy };        
+        }
+    }
+
+    var notes_circ = notes.slice(0);
+    notes_circ.push(notes[0])
+    var notes_circ_valid = notes_circ.filter( function(e,i,a) { return e.note != 'NA' && e.note != '-'; } );
+    var notes_valid = notes.filter( function(e,i,a) { return e.note != 'NA' && e.note != '-'; } )
+
+    /* Crée l'élément SVG */
+    g = d3.select("#radar_bulletin").append("svg")
+        .attr("class", "radar")
+        .attr("width", WIDTH)
+        .attr("height", HEIGHT);
+
+    /* Centre */
+    g.append( "circle" ).attr("cy", CY)
+        .attr("cx", CX)
+        .attr("r", 2)
+        .attr("class", "radar_center_mark");
+
+    /* Lignes "tics" */
+    for (var j=0; j < NB_TICS; j++) {
+        var ligne_tics = d3.svg.line() 
+            .x(function(d) { return d["tics"][j]["x"]; })
+            .y(function(d) { return d["tics"][j]["y"]; });
+        g.append( "svg:path" )
+            .attr("class", "radar_disk_tic")
+            .attr("id", "radar_disk_tic_" +  R_TICS[j])
+            .attr("d", ligne_tics(notes_circ));
+    }
+
+    /* Lignes radiales pour chaque module */
+    g.selectAll("radar_rad")
+        .data(notes)
+        .enter().append("line")
+        .attr("x1", CX)
+        .attr("y1", CY)
+        .attr("x2", function(d) { return d["x_20"]; })
+        .attr("y2", function(d) { return d["y_20"]; })
+        .attr("class", "radarrad");
+
+
+    /* Lignes entre notes */
+    var ligne = d3.svg.line() 
+        .x(function(d) { return d["x_v"]; })
+        .y(function(d) { return d["y_v"]; });
+
+    g.append( "svg:path" )
+        .attr("class", "radarnoteslines")
+        .attr("d", ligne(notes_circ_valid));
+
+    var ligne_moy = d3.svg.line() 
+        .x(function(d) { return d["x_moy"]; })
+        .y(function(d) { return d["y_moy"]; })
+
+    g.append( "svg:path" )
+        .attr("class", "radarmoylines")
+        .attr("d", ligne_moy(notes_circ_valid));
+
+    /* Points (notes) */
+    g.selectAll("circle1")
+        .data(notes_valid)
+        .enter().append("circle")
+        .attr("cx", function(d) { return d["x_v"]; })
+        .attr("cy", function(d) { return d["y_v"]; })
+        .attr("r", function(x, i) { return 3; } )
+        .style("stroke-width", 1)
+        .style("stroke", "black")
+        .style("fill", "blue")
+        .on("mouseover", function(d) {
+	        var rwidth = 290;
+	        var x = d["x_v"];
+	        if (x + rwidth + 12 > WIDTH) {
+	            x = WIDTH - rwidth - 12;
+	        }
+	        var r = g.append("rect")
+	            .attr('class','radartip')
+	            .attr("x", x + 5)
+                .attr("y", d["y_v"] + 5 );
+	        
+	        var txt = g.append("text").text("Note: " +  d.note + "/20, moyenne promo: " + d.moy + "/20")
+	            .attr('class','radartip')
+	            .attr("x", x + 5 + 5)
+                .attr("y", d["y_v"] + 5 + 16 );
+	        r.attr("width", rwidth).attr("height", 20);
+        })
+        .on("mouseout", function(d){
+            d3.selectAll(".radartip").remove()
+        });
+
+    /* Valeurs des notes */
+    g.selectAll("notes_labels")
+        .data(notes_valid)
+        .enter().append("text")
+        .text(function(d) { return d["note"]; })
+        .attr("x", function(d) { 
+            return d["x_v"]; 
+        })
+        .attr("y", function(d) { 
+            if (d["y_v"] > CY)
+                return d["y_v"] + 16;
+            else
+                return d["y_v"] - 8;
+        })
+        .attr("class", "note_label");
+
+    /* Petits points sur les poyennes */
+    g.selectAll("circle2")
+        .data(notes_valid)
+        .enter().append("circle")
+        .attr("cx", function(d) { return d["x_moy"]; })
+        .attr("cy", function(d) { return d["y_moy"]; })
+        .attr("r", function(x, i) { return 2; } )
+        .style("stroke-width", 0)
+        .style("stroke", "black")
+        .style("fill", "rgb(20,90,50)");
+
+    /* Valeurs sur axe */
+    g.selectAll("textaxis")
+        .data( R_AXIS_TICS )
+        .enter().append("text")
+        .text(String)
+        .attr("x", CX - 10)
+        .attr("y", function(x, i) { return CY - x*RR/20 + 6; })
+        .attr("class", "textaxis");
+
+    /* Noms des modules */
+    g.selectAll("text_modules")
+        .data(notes)
+        .enter().append("text")
+        .text( function(d) { return d['code']; } )
+        .attr("x", function(d) { return d['x_label']; } )
+        .attr("y", function(d) { return d['y_label']; })
+        .attr("dx", 0)
+        .attr("dy", 0);
+}
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/AutoSuggest.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/AutoSuggest.js
new file mode 100644
index 0000000000000000000000000000000000000000..a7a83c77aa624fd913f112dad0bf28c7e590dba3
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/AutoSuggest.js
@@ -0,0 +1 @@
+if(typeof(bsn)=="undefined")_b=bsn={};if(typeof(_b.Autosuggest)=="undefined")_b.Autosuggest={};else alert("Autosuggest is already set!");_b.AutoSuggest=function(b,c){if(!document.getElementById)return 0;this.fld=_b.DOM.gE(b);if(!this.fld)return 0;this.sInp="";this.nInpC=0;this.aSug=[];this.iHigh=0;this.oP=c?c:{};var k,def={minchars:1,meth:"get",varname:"input",className:"autosuggest",timeout:2500,delay:500,offsety:-5,shownoresults:true,noresults:"No results!",maxheight:250,cache:true,maxentries:25};for(k in def){if(typeof(this.oP[k])!=typeof(def[k]))this.oP[k]=def[k]}var p=this;this.fld.onkeypress=function(a){return p.onKeyPress(a)};this.fld.onkeyup=function(a){return p.onKeyUp(a)};this.fld.setAttribute("autocomplete","off")};_b.AutoSuggest.prototype.onKeyPress=function(a){var b=(window.event)?window.event.keyCode:a.keyCode;var c=13;var d=9;var e=27;var f=1;switch(b){case c:this.setHighlightedValue();f=0;return false;break;case e:this.clearSuggestions();break}return f};_b.AutoSuggest.prototype.onKeyUp=function(a){var b=(window.event)?window.event.keyCode:a.keyCode;var c=38;var d=40;var e=1;switch(b){case c:this.changeHighlight(b);e=0;break;case d:this.changeHighlight(b);e=0;break;default:this.getSuggestions(this.fld.value)}return e};_b.AutoSuggest.prototype.getSuggestions=function(a){if(a==this.sInp)return 0;_b.DOM.remE(this.idAs);this.sInp=a;if(a.length<this.oP.minchars){this.aSug=[];this.nInpC=a.length;return 0}var b=this.nInpC;this.nInpC=a.length?a.length:0;var l=this.aSug.length;if(this.nInpC>b&&l&&l<this.oP.maxentries&&this.oP.cache){var c=[];for(var i=0;i<l;i++){if(this.aSug[i].value.substr(0,a.length).toLowerCase()==a.toLowerCase())c.push(this.aSug[i])}this.aSug=c;this.createList(this.aSug);return false}else{var d=this;var e=this.sInp;clearTimeout(this.ajID);this.ajID=setTimeout(function(){d.doAjaxRequest(e)},this.oP.delay)}return false};_b.AutoSuggest.prototype.doAjaxRequest=function(b){if(b!=this.fld.value)return false;var c=this;if(typeof(this.oP.script)=="function")var d=this.oP.script(encodeURIComponent(this.sInp));else var d=this.oP.script+this.oP.varname+"="+encodeURIComponent(this.sInp);if(!d)return false;var e=this.oP.meth;var b=this.sInp;var f=function(a){c.setSuggestions(a,b)};var g=function(a){alert("AJAX error: "+a)};var h=new _b.Ajax();h.makeRequest(d,e,f,g)};_b.AutoSuggest.prototype.setSuggestions=function(a,b){if(b!=this.fld.value)return false;this.aSug=[];if(this.oP.json){var c=eval('('+a.responseText+')');for(var i=0;i<c.results.length;i++){this.aSug.push({'id':c.results[i].id,'value':c.results[i].value,'info':c.results[i].info})}}else{var d=a.responseXML;var e=d.getElementsByTagName('results')[0].childNodes;for(var i=0;i<e.length;i++){if(e[i].hasChildNodes())this.aSug.push({'id':e[i].getAttribute('id'),'value':e[i].childNodes[0].nodeValue,'info':e[i].getAttribute('info')})}}this.idAs="as_"+this.fld.id;this.createList(this.aSug)};_b.AutoSuggest.prototype.createList=function(b){var c=this;_b.DOM.remE(this.idAs);this.killTimeout();if(b.length==0&&!this.oP.shownoresults)return false;var d=_b.DOM.cE("div",{id:this.idAs,className:this.oP.className});var e=_b.DOM.cE("div",{className:"as_corner"});var f=_b.DOM.cE("div",{className:"as_bar"});var g=_b.DOM.cE("div",{className:"as_header"});g.appendChild(e);g.appendChild(f);d.appendChild(g);var h=_b.DOM.cE("ul",{id:"as_ul"});for(var i=0;i<b.length;i++){var j=b[i].value;var k=j.toLowerCase().indexOf(this.sInp.toLowerCase());var l=j.substring(0,k)+"<em>"+j.substring(k,k+this.sInp.length)+"</em>"+j.substring(k+this.sInp.length);var m=_b.DOM.cE("span",{},l,true);if(b[i].info!=""){var n=_b.DOM.cE("br",{});m.appendChild(n);var o=_b.DOM.cE("small",{},b[i].info);m.appendChild(o)}var a=_b.DOM.cE("a",{href:"#"});var p=_b.DOM.cE("span",{className:"tl"}," ");var q=_b.DOM.cE("span",{className:"tr"}," ");a.appendChild(p);a.appendChild(q);a.appendChild(m);a.name=i+1;a.onclick=function(){c.setHighlightedValue();return false};a.onmouseover=function(){c.setHighlight(this.name)};var r=_b.DOM.cE("li",{},a);h.appendChild(r)}if(b.length==0&&this.oP.shownoresults){var r=_b.DOM.cE("li",{className:"as_warning"},this.oP.noresults);h.appendChild(r)}d.appendChild(h);var s=_b.DOM.cE("div",{className:"as_corner"});var t=_b.DOM.cE("div",{className:"as_bar"});var u=_b.DOM.cE("div",{className:"as_footer"});u.appendChild(s);u.appendChild(t);d.appendChild(u);var v=_b.DOM.getPos(this.fld);d.style.left=v.x+"px";d.style.top=(v.y+this.fld.offsetHeight+this.oP.offsety)+"px";d.style.width=this.fld.offsetWidth+"px";d.onmouseover=function(){c.killTimeout()};d.onmouseout=function(){c.resetTimeout()};document.getElementsByTagName("body")[0].appendChild(d);this.iHigh=0;var c=this;this.toID=setTimeout(function(){c.clearSuggestions()},this.oP.timeout)};_b.AutoSuggest.prototype.changeHighlight=function(a){var b=_b.DOM.gE("as_ul");if(!b)return false;var n;if(a==40)n=this.iHigh+1;else if(a==38)n=this.iHigh-1;if(n>b.childNodes.length)n=b.childNodes.length;if(n<1)n=1;this.setHighlight(n)};_b.AutoSuggest.prototype.setHighlight=function(n){var a=_b.DOM.gE("as_ul");if(!a)return false;if(this.iHigh>0)this.clearHighlight();this.iHigh=Number(n);a.childNodes[this.iHigh-1].className="as_highlight";this.killTimeout()};_b.AutoSuggest.prototype.clearHighlight=function(){var a=_b.DOM.gE("as_ul");if(!a)return false;if(this.iHigh>0){a.childNodes[this.iHigh-1].className="";this.iHigh=0}};_b.AutoSuggest.prototype.setHighlightedValue=function(){if(this.iHigh){this.sInp=this.fld.value=this.aSug[this.iHigh-1].value;this.fld.focus();if(this.fld.selectionStart)this.fld.setSelectionRange(this.sInp.length,this.sInp.length);this.clearSuggestions();if(typeof(this.oP.callback)=="function")this.oP.callback(this.aSug[this.iHigh-1])}};_b.AutoSuggest.prototype.killTimeout=function(){clearTimeout(this.toID)};_b.AutoSuggest.prototype.resetTimeout=function(){clearTimeout(this.toID);var a=this;this.toID=setTimeout(function(){a.clearSuggestions()},1000)};_b.AutoSuggest.prototype.clearSuggestions=function(){this.killTimeout();var a=_b.DOM.gE(this.idAs);var b=this;if(a){var c=new _b.Fader(a,1,0,250,function(){_b.DOM.remE(b.idAs)})}};if(typeof(_b.Ajax)=="undefined")_b.Ajax={};_b.Ajax=function(){this.req={};this.isIE=false};_b.Ajax.prototype.makeRequest=function(a,b,c,d){if(b!="POST")b="GET";this.onComplete=c;this.onError=d;var e=this;if(window.XMLHttpRequest){this.req=new XMLHttpRequest();this.req.onreadystatechange=function(){e.processReqChange()};this.req.open("GET",a,true);this.req.send(null)}else if(window.ActiveXObject){this.req=new ActiveXObject("Microsoft.XMLHTTP");if(this.req){this.req.onreadystatechange=function(){e.processReqChange()};this.req.open(b,a,true);this.req.send()}}};_b.Ajax.prototype.processReqChange=function(){if(this.req.readyState==4){if(this.req.status==200){this.onComplete(this.req)}else{this.onError(this.req.status)}}};if(typeof(_b.DOM)=="undefined")_b.DOM={};_b.DOM.cE=function(b,c,d,e){var f=document.createElement(b);if(!f)return 0;for(var a in c)f[a]=c[a];var t=typeof(d);if(t=="string"&&!e)f.appendChild(document.createTextNode(d));else if(t=="string"&&e)f.innerHTML=d;else if(t=="object")f.appendChild(d);return f};_b.DOM.gE=function(e){var t=typeof(e);if(t=="undefined")return 0;else if(t=="string"){var a=document.getElementById(e);if(!a)return 0;else if(typeof(a.appendChild)!="undefined")return a;else return 0}else if(typeof(e.appendChild)!="undefined")return e;else return 0};_b.DOM.remE=function(a){var e=this.gE(a);if(!e)return 0;else if(e.parentNode.removeChild(e))return true;else return 0};_b.DOM.getPos=function(e){var e=this.gE(e);var a=e;var b=0;if(a.offsetParent){while(a.offsetParent){b+=a.offsetLeft;a=a.offsetParent}}else if(a.x)b+=a.x;var a=e;var c=0;if(a.offsetParent){while(a.offsetParent){c+=a.offsetTop;a=a.offsetParent}}else if(a.y)c+=a.y;return{x:b,y:c}};if(typeof(_b.Fader)=="undefined")_b.Fader={};_b.Fader=function(a,b,c,d,e){if(!a)return 0;this.e=a;this.from=b;this.to=c;this.cb=e;this.nDur=d;this.nInt=50;this.nTime=0;var p=this;this.nID=setInterval(function(){p._fade()},this.nInt)};_b.Fader.prototype._fade=function(){this.nTime+=this.nInt;var a=Math.round(this._tween(this.nTime,this.from,this.to,this.nDur)*100);var b=a/100;if(this.e.filters){try{this.e.filters.item("DXImageTransform.Microsoft.Alpha").opacity=a}catch(e){this.e.style.filter='progid:DXImageTransform.Microsoft.Alpha(opacity='+a+')'}}else{this.e.style.opacity=b}if(this.nTime==this.nDur){clearInterval(this.nID);if(this.cb!=undefined)this.cb()}};_b.Fader.prototype._tween=function(t,b,c,d){return b+((c-b)*(t/d))};
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/AutoSuggest_2.1.3_comp.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/AutoSuggest_2.1.3_comp.js
new file mode 100644
index 0000000000000000000000000000000000000000..a7a83c77aa624fd913f112dad0bf28c7e590dba3
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/AutoSuggest_2.1.3_comp.js
@@ -0,0 +1 @@
+if(typeof(bsn)=="undefined")_b=bsn={};if(typeof(_b.Autosuggest)=="undefined")_b.Autosuggest={};else alert("Autosuggest is already set!");_b.AutoSuggest=function(b,c){if(!document.getElementById)return 0;this.fld=_b.DOM.gE(b);if(!this.fld)return 0;this.sInp="";this.nInpC=0;this.aSug=[];this.iHigh=0;this.oP=c?c:{};var k,def={minchars:1,meth:"get",varname:"input",className:"autosuggest",timeout:2500,delay:500,offsety:-5,shownoresults:true,noresults:"No results!",maxheight:250,cache:true,maxentries:25};for(k in def){if(typeof(this.oP[k])!=typeof(def[k]))this.oP[k]=def[k]}var p=this;this.fld.onkeypress=function(a){return p.onKeyPress(a)};this.fld.onkeyup=function(a){return p.onKeyUp(a)};this.fld.setAttribute("autocomplete","off")};_b.AutoSuggest.prototype.onKeyPress=function(a){var b=(window.event)?window.event.keyCode:a.keyCode;var c=13;var d=9;var e=27;var f=1;switch(b){case c:this.setHighlightedValue();f=0;return false;break;case e:this.clearSuggestions();break}return f};_b.AutoSuggest.prototype.onKeyUp=function(a){var b=(window.event)?window.event.keyCode:a.keyCode;var c=38;var d=40;var e=1;switch(b){case c:this.changeHighlight(b);e=0;break;case d:this.changeHighlight(b);e=0;break;default:this.getSuggestions(this.fld.value)}return e};_b.AutoSuggest.prototype.getSuggestions=function(a){if(a==this.sInp)return 0;_b.DOM.remE(this.idAs);this.sInp=a;if(a.length<this.oP.minchars){this.aSug=[];this.nInpC=a.length;return 0}var b=this.nInpC;this.nInpC=a.length?a.length:0;var l=this.aSug.length;if(this.nInpC>b&&l&&l<this.oP.maxentries&&this.oP.cache){var c=[];for(var i=0;i<l;i++){if(this.aSug[i].value.substr(0,a.length).toLowerCase()==a.toLowerCase())c.push(this.aSug[i])}this.aSug=c;this.createList(this.aSug);return false}else{var d=this;var e=this.sInp;clearTimeout(this.ajID);this.ajID=setTimeout(function(){d.doAjaxRequest(e)},this.oP.delay)}return false};_b.AutoSuggest.prototype.doAjaxRequest=function(b){if(b!=this.fld.value)return false;var c=this;if(typeof(this.oP.script)=="function")var d=this.oP.script(encodeURIComponent(this.sInp));else var d=this.oP.script+this.oP.varname+"="+encodeURIComponent(this.sInp);if(!d)return false;var e=this.oP.meth;var b=this.sInp;var f=function(a){c.setSuggestions(a,b)};var g=function(a){alert("AJAX error: "+a)};var h=new _b.Ajax();h.makeRequest(d,e,f,g)};_b.AutoSuggest.prototype.setSuggestions=function(a,b){if(b!=this.fld.value)return false;this.aSug=[];if(this.oP.json){var c=eval('('+a.responseText+')');for(var i=0;i<c.results.length;i++){this.aSug.push({'id':c.results[i].id,'value':c.results[i].value,'info':c.results[i].info})}}else{var d=a.responseXML;var e=d.getElementsByTagName('results')[0].childNodes;for(var i=0;i<e.length;i++){if(e[i].hasChildNodes())this.aSug.push({'id':e[i].getAttribute('id'),'value':e[i].childNodes[0].nodeValue,'info':e[i].getAttribute('info')})}}this.idAs="as_"+this.fld.id;this.createList(this.aSug)};_b.AutoSuggest.prototype.createList=function(b){var c=this;_b.DOM.remE(this.idAs);this.killTimeout();if(b.length==0&&!this.oP.shownoresults)return false;var d=_b.DOM.cE("div",{id:this.idAs,className:this.oP.className});var e=_b.DOM.cE("div",{className:"as_corner"});var f=_b.DOM.cE("div",{className:"as_bar"});var g=_b.DOM.cE("div",{className:"as_header"});g.appendChild(e);g.appendChild(f);d.appendChild(g);var h=_b.DOM.cE("ul",{id:"as_ul"});for(var i=0;i<b.length;i++){var j=b[i].value;var k=j.toLowerCase().indexOf(this.sInp.toLowerCase());var l=j.substring(0,k)+"<em>"+j.substring(k,k+this.sInp.length)+"</em>"+j.substring(k+this.sInp.length);var m=_b.DOM.cE("span",{},l,true);if(b[i].info!=""){var n=_b.DOM.cE("br",{});m.appendChild(n);var o=_b.DOM.cE("small",{},b[i].info);m.appendChild(o)}var a=_b.DOM.cE("a",{href:"#"});var p=_b.DOM.cE("span",{className:"tl"}," ");var q=_b.DOM.cE("span",{className:"tr"}," ");a.appendChild(p);a.appendChild(q);a.appendChild(m);a.name=i+1;a.onclick=function(){c.setHighlightedValue();return false};a.onmouseover=function(){c.setHighlight(this.name)};var r=_b.DOM.cE("li",{},a);h.appendChild(r)}if(b.length==0&&this.oP.shownoresults){var r=_b.DOM.cE("li",{className:"as_warning"},this.oP.noresults);h.appendChild(r)}d.appendChild(h);var s=_b.DOM.cE("div",{className:"as_corner"});var t=_b.DOM.cE("div",{className:"as_bar"});var u=_b.DOM.cE("div",{className:"as_footer"});u.appendChild(s);u.appendChild(t);d.appendChild(u);var v=_b.DOM.getPos(this.fld);d.style.left=v.x+"px";d.style.top=(v.y+this.fld.offsetHeight+this.oP.offsety)+"px";d.style.width=this.fld.offsetWidth+"px";d.onmouseover=function(){c.killTimeout()};d.onmouseout=function(){c.resetTimeout()};document.getElementsByTagName("body")[0].appendChild(d);this.iHigh=0;var c=this;this.toID=setTimeout(function(){c.clearSuggestions()},this.oP.timeout)};_b.AutoSuggest.prototype.changeHighlight=function(a){var b=_b.DOM.gE("as_ul");if(!b)return false;var n;if(a==40)n=this.iHigh+1;else if(a==38)n=this.iHigh-1;if(n>b.childNodes.length)n=b.childNodes.length;if(n<1)n=1;this.setHighlight(n)};_b.AutoSuggest.prototype.setHighlight=function(n){var a=_b.DOM.gE("as_ul");if(!a)return false;if(this.iHigh>0)this.clearHighlight();this.iHigh=Number(n);a.childNodes[this.iHigh-1].className="as_highlight";this.killTimeout()};_b.AutoSuggest.prototype.clearHighlight=function(){var a=_b.DOM.gE("as_ul");if(!a)return false;if(this.iHigh>0){a.childNodes[this.iHigh-1].className="";this.iHigh=0}};_b.AutoSuggest.prototype.setHighlightedValue=function(){if(this.iHigh){this.sInp=this.fld.value=this.aSug[this.iHigh-1].value;this.fld.focus();if(this.fld.selectionStart)this.fld.setSelectionRange(this.sInp.length,this.sInp.length);this.clearSuggestions();if(typeof(this.oP.callback)=="function")this.oP.callback(this.aSug[this.iHigh-1])}};_b.AutoSuggest.prototype.killTimeout=function(){clearTimeout(this.toID)};_b.AutoSuggest.prototype.resetTimeout=function(){clearTimeout(this.toID);var a=this;this.toID=setTimeout(function(){a.clearSuggestions()},1000)};_b.AutoSuggest.prototype.clearSuggestions=function(){this.killTimeout();var a=_b.DOM.gE(this.idAs);var b=this;if(a){var c=new _b.Fader(a,1,0,250,function(){_b.DOM.remE(b.idAs)})}};if(typeof(_b.Ajax)=="undefined")_b.Ajax={};_b.Ajax=function(){this.req={};this.isIE=false};_b.Ajax.prototype.makeRequest=function(a,b,c,d){if(b!="POST")b="GET";this.onComplete=c;this.onError=d;var e=this;if(window.XMLHttpRequest){this.req=new XMLHttpRequest();this.req.onreadystatechange=function(){e.processReqChange()};this.req.open("GET",a,true);this.req.send(null)}else if(window.ActiveXObject){this.req=new ActiveXObject("Microsoft.XMLHTTP");if(this.req){this.req.onreadystatechange=function(){e.processReqChange()};this.req.open(b,a,true);this.req.send()}}};_b.Ajax.prototype.processReqChange=function(){if(this.req.readyState==4){if(this.req.status==200){this.onComplete(this.req)}else{this.onError(this.req.status)}}};if(typeof(_b.DOM)=="undefined")_b.DOM={};_b.DOM.cE=function(b,c,d,e){var f=document.createElement(b);if(!f)return 0;for(var a in c)f[a]=c[a];var t=typeof(d);if(t=="string"&&!e)f.appendChild(document.createTextNode(d));else if(t=="string"&&e)f.innerHTML=d;else if(t=="object")f.appendChild(d);return f};_b.DOM.gE=function(e){var t=typeof(e);if(t=="undefined")return 0;else if(t=="string"){var a=document.getElementById(e);if(!a)return 0;else if(typeof(a.appendChild)!="undefined")return a;else return 0}else if(typeof(e.appendChild)!="undefined")return e;else return 0};_b.DOM.remE=function(a){var e=this.gE(a);if(!e)return 0;else if(e.parentNode.removeChild(e))return true;else return 0};_b.DOM.getPos=function(e){var e=this.gE(e);var a=e;var b=0;if(a.offsetParent){while(a.offsetParent){b+=a.offsetLeft;a=a.offsetParent}}else if(a.x)b+=a.x;var a=e;var c=0;if(a.offsetParent){while(a.offsetParent){c+=a.offsetTop;a=a.offsetParent}}else if(a.y)c+=a.y;return{x:b,y:c}};if(typeof(_b.Fader)=="undefined")_b.Fader={};_b.Fader=function(a,b,c,d,e){if(!a)return 0;this.e=a;this.from=b;this.to=c;this.cb=e;this.nDur=d;this.nInt=50;this.nTime=0;var p=this;this.nID=setInterval(function(){p._fade()},this.nInt)};_b.Fader.prototype._fade=function(){this.nTime+=this.nInt;var a=Math.round(this._tween(this.nTime,this.from,this.to,this.nDur)*100);var b=a/100;if(this.e.filters){try{this.e.filters.item("DXImageTransform.Microsoft.Alpha").opacity=a}catch(e){this.e.style.filter='progid:DXImageTransform.Microsoft.Alpha(opacity='+a+')'}}else{this.e.style.opacity=b}if(this.nTime==this.nDur){clearInterval(this.nID);if(this.cb!=undefined)this.cb()}};_b.Fader.prototype._tween=function(t,b,c,d){return b+((c-b)*(t/d))};
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/bubble.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/bubble.js
new file mode 100644
index 0000000000000000000000000000000000000000..11421a1df705f3dddaf8ef285dd47765b2b33efb
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/bubble.js
@@ -0,0 +1,100 @@
+/* -*- mode: javascript -*-
+ *                 JavaScript for Help Bubbles (aka tooltips) 
+ */
+
+function enableTooltips(id){
+var links,i,h;
+if(!document.getElementById || !document.getElementsByTagName) return;
+// AddCss();
+h=document.createElement("span");
+h.id="btc";
+h.setAttribute("id","btc");
+h.style.position="absolute";
+document.getElementsByTagName("body")[0].appendChild(h);
+if(id==null) links=document.getElementsByTagName("a");
+else links=document.getElementById(id).getElementsByTagName("a");
+for(i=0;i<links.length;i++){
+    Prepare(links[i]);
+    }
+}
+
+function Prepare(el){
+  var tooltip,t,b,s,l;
+  t=el.getAttribute("title");
+  if(t==null || t.length==0) 
+    return; /* rien si pas de title. Was: t="link:"; */
+  el.removeAttribute("title");
+  tooltip=CreateEl("span","tooltip");
+  s=CreateEl("span","top");
+  s.appendChild(document.createTextNode(t));
+  tooltip.appendChild(s);
+
+  b=CreateEl("b","bottom");
+  /* url du lien:
+  l=el.getAttribute("href");
+  if(l.length>30) 
+    l=l.substr(0,27)+"...";
+  b.appendChild(document.createTextNode(l));
+  */
+  tooltip.appendChild(b);
+  setOpacity(tooltip);
+  el.tooltip=tooltip;
+  el.onmouseover=showTooltip;
+  el.onmouseout=hideTooltip;
+  el.onmousemove=Locate;
+}
+
+function showTooltip(e){
+  document.getElementById("btc").appendChild(this.tooltip);
+  Locate(e);
+}
+
+function hideTooltip(e){
+  var d=document.getElementById("btc");
+  if(d.childNodes.length>0) 
+    d.removeChild(d.firstChild);
+}
+
+function setOpacity(el){
+  el.style.filter="alpha(opacity:95)";
+  el.style.KHTMLOpacity="0.95";
+  el.style.MozOpacity="0.95";
+  el.style.opacity="0.95";
+}
+
+function CreateEl(t,c){
+  var x=document.createElement(t);
+  x.className=c;
+  x.style.display="block";
+  return(x);
+}
+
+function AddCss(){
+  var l=CreateEl("link");
+  l.setAttribute("type","text/css");
+  l.setAttribute("rel","stylesheet");
+  l.setAttribute("href","bubble.js");
+  l.setAttribute("media","screen");
+  document.getElementsByTagName("head")[0].appendChild(l);
+}
+
+function Locate(e){
+  var posx=0,posy=0;
+  if(e==null) 
+    e=window.event;
+  if(e.pageX || e.pageY){
+    posx=e.pageX; posy=e.pageY;
+  }
+  else if(e.clientX || e.clientY){
+    if(document.documentElement.scrollTop){
+        posx=e.clientX+document.documentElement.scrollLeft;
+        posy=e.clientY+document.documentElement.scrollTop;
+    }
+    else{
+      posx=e.clientX+document.body.scrollLeft;
+      posy=e.clientY+document.body.scrollTop;
+    }
+  }
+  document.getElementById("btc").style.top=(posy+10)+"px";
+  document.getElementById("btc").style.left=(posx-20)+"px";
+}
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/d3.v2.min.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/d3.v2.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..a8b1e2ce3bcdc885350760154d7da30cb5cad4af
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/d3.v2.min.js
@@ -0,0 +1,4 @@
+(function(){function e(e,t){try{for(var n in t)Object.defineProperty(e.prototype,n,{value:t[n],enumerable:!1})}catch(r){e.prototype=t}}function t(e){var t=-1,n=e.length,r=[];while(++t<n)r.push(e[t]);return r}function n(e){return Array.prototype.slice.call(e)}function r(){}function i(e){return e}function s(){return this}function o(){return!0}function u(e){return typeof e=="function"?e:function(){return e}}function a(e,t,n){return function(){var r=n.apply(t,arguments);return arguments.length?e:r}}function f(e){return e!=null&&!isNaN(e)}function l(e){return e.length}function c(e){return e==null}function h(e){return e.trim().replace(/\s+/g," ")}function p(e){var t=1;while(e*t%1)t*=10;return t}function d(){}function v(e){function t(){var t=n,r=-1,i=t.length,s;while(++r<i)(s=t[r].on)&&s.apply(this,arguments);return e}var n=[],i=new r;return t.on=function(t,r){var s=i.get(t),o;return arguments.length<2?s&&s.on:(s&&(s.on=null,n=n.slice(0,o=n.indexOf(s)).concat(n.slice(o+1)),i.remove(t)),r&&n.push(i.set(t,{on:r})),e)},t}function m(e,t){return t-(e?1+Math.floor(Math.log(e+Math.pow(10,1+Math.floor(Math.log(e)/Math.LN10)-t))/Math.LN10):1)}function g(e){return e+""}function y(e){var t=e.lastIndexOf("."),n=t>=0?e.substring(t):(t=e.length,""),r=[];while(t>0)r.push(e.substring(t-=3,t+3));return r.reverse().join(",")+n}function b(e,t){var n=Math.pow(10,Math.abs(8-t)*3);return{scale:t>8?function(e){return e/n}:function(e){return e*n},symbol:e}}function w(e){return function(t){return t<=0?0:t>=1?1:e(t)}}function E(e){return function(t){return 1-e(1-t)}}function S(e){return function(t){return.5*(t<.5?e(2*t):2-e(2-2*t))}}function x(e){return e}function T(e){return function(t){return Math.pow(t,e)}}function N(e){return 1-Math.cos(e*Math.PI/2)}function C(e){return Math.pow(2,10*(e-1))}function k(e){return 1-Math.sqrt(1-e*e)}function L(e,t){var n;return arguments.length<2&&(t=.45),arguments.length<1?(e=1,n=t/4):n=t/(2*Math.PI)*Math.asin(1/e),function(r){return 1+e*Math.pow(2,10*-r)*Math.sin((r-n)*2*Math.PI/t)}}function A(e){return e||(e=1.70158),function(t){return t*t*((e+1)*t-e)}}function O(e){return e<1/2.75?7.5625*e*e:e<2/2.75?7.5625*(e-=1.5/2.75)*e+.75:e<2.5/2.75?7.5625*(e-=2.25/2.75)*e+.9375:7.5625*(e-=2.625/2.75)*e+.984375}function M(){d3.event.stopPropagation(),d3.event.preventDefault()}function _(){var e=d3.event,t;while(t=e.sourceEvent)e=t;return e}function D(e){var t=new d,n=0,r=arguments.length;while(++n<r)t[arguments[n]]=v(t);return t.of=function(n,r){return function(i){try{var s=i.sourceEvent=d3.event;i.target=e,d3.event=i,t[i.type].apply(n,r)}finally{d3.event=s}}},t}function P(e){var t=[e.a,e.b],n=[e.c,e.d],r=B(t),i=H(t,n),s=B(j(n,t,-i))||0;t[0]*n[1]<n[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,i*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-n[0],n[1]))*ls,this.translate=[e.e,e.f],this.scale=[r,s],this.skew=s?Math.atan2(i,s)*ls:0}function H(e,t){return e[0]*t[0]+e[1]*t[1]}function B(e){var t=Math.sqrt(H(e,e));return t&&(e[0]/=t,e[1]/=t),t}function j(e,t,n){return e[0]+=n*t[0],e[1]+=n*t[1],e}function F(e){return e=="transform"?d3.interpolateTransform:d3.interpolate}function I(e,t){return t=t-(e=+e)?1/(t-e):0,function(n){return(n-e)*t}}function q(e,t){return t=t-(e=+e)?1/(t-e):0,function(n){return Math.max(0,Math.min(1,(n-e)*t))}}function R(){}function U(e,t,n){return new z(e,t,n)}function z(e,t,n){this.r=e,this.g=t,this.b=n}function W(e){return e<16?"0"+Math.max(0,e).toString(16):Math.min(255,e).toString(16)}function X(e,t,n){var r=0,i=0,s=0,o,u,a;o=/([a-z]+)\((.*)\)/i.exec(e);if(o){u=o[2].split(",");switch(o[1]){case"hsl":return n(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(K(u[0]),K(u[1]),K(u[2]))}}return(a=ds.get(e))?t(a.r,a.g,a.b):(e!=null&&e.charAt(0)==="#"&&(e.length===4?(r=e.charAt(1),r+=r,i=e.charAt(2),i+=i,s=e.charAt(3),s+=s):e.length===7&&(r=e.substring(1,3),i=e.substring(3,5),s=e.substring(5,7)),r=parseInt(r,16),i=parseInt(i,16),s=parseInt(s,16)),t(r,i,s))}function V(e,t,n){var r=Math.min(e/=255,t/=255,n/=255),i=Math.max(e,t,n),s=i-r,o,u,a=(i+r)/2;return s?(u=a<.5?s/(i+r):s/(2-i-r),e==i?o=(t-n)/s+(t<n?6:0):t==i?o=(n-e)/s+2:o=(e-t)/s+4,o*=60):u=o=0,Q(o,u,a)}function $(e,t,n){e=J(e),t=J(t),n=J(n);var r=ut((.4124564*e+.3575761*t+.1804375*n)/ys),i=ut((.2126729*e+.7151522*t+.072175*n)/bs),s=ut((.0193339*e+.119192*t+.9503041*n)/ws);return nt(116*i-16,500*(r-i),200*(i-s))}function J(e){return(e/=255)<=.04045?e/12.92:Math.pow((e+.055)/1.055,2.4)}function K(e){var t=parseFloat(e);return e.charAt(e.length-1)==="%"?Math.round(t*2.55):t}function Q(e,t,n){return new G(e,t,n)}function G(e,t,n){this.h=e,this.s=t,this.l=n}function Y(e,t,n){function r(e){return e>360?e-=360:e<0&&(e+=360),e<60?s+(o-s)*e/60:e<180?o:e<240?s+(o-s)*(240-e)/60:s}function i(e){return Math.round(r(e)*255)}var s,o;return e%=360,e<0&&(e+=360),t=t<0?0:t>1?1:t,n=n<0?0:n>1?1:n,o=n<=.5?n*(1+t):n+t-n*t,s=2*n-o,U(i(e+120),i(e),i(e-120))}function Z(e,t,n){return new et(e,t,n)}function et(e,t,n){this.h=e,this.c=t,this.l=n}function tt(e,t,n){return nt(n,Math.cos(e*=Math.PI/180)*t,Math.sin(e)*t)}function nt(e,t,n){return new rt(e,t,n)}function rt(e,t,n){this.l=e,this.a=t,this.b=n}function it(e,t,n){var r=(e+16)/116,i=r+t/500,s=r-n/200;return i=ot(i)*ys,r=ot(r)*bs,s=ot(s)*ws,U(at(3.2404542*i-1.5371385*r-.4985314*s),at(-0.969266*i+1.8760108*r+.041556*s),at(.0556434*i-.2040259*r+1.0572252*s))}function st(e,t,n){return Z(Math.atan2(n,t)/Math.PI*180,Math.sqrt(t*t+n*n),e)}function ot(e){return e>.206893034?e*e*e:(e-4/29)/7.787037}function ut(e){return e>.008856?Math.pow(e,1/3):7.787037*e+4/29}function at(e){return Math.round(255*(e<=.00304?12.92*e:1.055*Math.pow(e,1/2.4)-.055))}function ft(e){return Qi(e,ks),e}function lt(e){return function(){return Ss(e,this)}}function ct(e){return function(){return xs(e,this)}}function ht(e,t){function n(){this.removeAttribute(e)}function r(){this.removeAttributeNS(e.space,e.local)}function i(){this.setAttribute(e,t)}function s(){this.setAttributeNS(e.space,e.local,t)}function o(){var n=t.apply(this,arguments);n==null?this.removeAttribute(e):this.setAttribute(e,n)}function u(){var n=t.apply(this,arguments);n==null?this.removeAttributeNS(e.space,e.local):this.setAttributeNS(e.space,e.local,n)}return e=d3.ns.qualify(e),t==null?e.local?r:n:typeof t=="function"?e.local?u:o:e.local?s:i}function pt(e){return new RegExp("(?:^|\\s+)"+d3.requote(e)+"(?:\\s+|$)","g")}function dt(e,t){function n(){var n=-1;while(++n<i)e[n](this,t)}function r(){var n=-1,r=t.apply(this,arguments);while(++n<i)e[n](this,r)}e=e.trim().split(/\s+/).map(vt);var i=e.length;return typeof t=="function"?r:n}function vt(e){var t=pt(e);return function(n,r){if(i=n.classList)return r?i.add(e):i.remove(e);var i=n.className,s=i.baseVal!=null,o=s?i.baseVal:i;r?(t.lastIndex=0,t.test(o)||(o=h(o+" "+e),s?i.baseVal=o:n.className=o)):o&&(o=h(o.replace(t," ")),s?i.baseVal=o:n.className=o)}}function mt(e,t,n){function r(){this.style.removeProperty(e)}function i(){this.style.setProperty(e,t,n)}function s(){var r=t.apply(this,arguments);r==null?this.style.removeProperty(e):this.style.setProperty(e,r,n)}return t==null?r:typeof t=="function"?s:i}function gt(e,t){function n(){delete this[e]}function r(){this[e]=t}function i(){var n=t.apply(this,arguments);n==null?delete this[e]:this[e]=n}return t==null?n:typeof t=="function"?i:r}function yt(e){return{__data__:e}}function bt(e){return function(){return Cs(this,e)}}function wt(e){return arguments.length||(e=d3.ascending),function(t,n){return e(t&&t.__data__,n&&n.__data__)}}function Et(e,t,n){function r(){var t=this[s];t&&(this.removeEventListener(e,t,t.$),delete this[s])}function i(){function i(e){var n=d3.event;d3.event=e,u[0]=o.__data__;try{t.apply(o,u)}finally{d3.event=n}}var o=this,u=arguments;r.call(this),this.addEventListener(e,this[s]=i,i.$=n),i._=t}var s="__on"+e,o=e.indexOf(".");return o>0&&(e=e.substring(0,o)),t?i:r}function St(e,t){for(var n=0,r=e.length;n<r;n++)for(var i=e[n],s=0,o=i.length,u;s<o;s++)(u=i[s])&&t(u,s,n);return e}function xt(e){return Qi(e,As),e}function Tt(e,t,n){Qi(e,Os);var i=new r,s=d3.dispatch("start","end"),o=Fs;return e.id=t,e.time=n,e.tween=function(t,n){return arguments.length<2?i.get(t):(n==null?i.remove(t):i.set(t,n),e)},e.ease=function(t){return arguments.length?(o=typeof t=="function"?t:d3.ease.apply(d3,arguments),e):o},e.each=function(t,n){return arguments.length<2?Nt.call(e,t):(s.on(t,n),e)},d3.timer(function(r){return St(e,function(e,u,a){function f(r){return v.active>t?c():(v.active=t,i.forEach(function(t,n){(n=n.call(e,m,u))&&h.push(n)}),s.start.call(e,m,u),l(r)||d3.timer(l,0,n),1)}function l(n){if(v.active!==t)return c();var r=(n-p)/d,i=o(r),a=h.length;while(a>0)h[--a].call(e,i);if(r>=1)return c(),_s=t,s.end.call(e,m,u),_s=0,1}function c(){return--v.count||delete e.__transition__,1}var h=[],p=e.delay,d=e.duration,v=(e=e.node).__transition__||(e.__transition__={active:0,count:0}),m=e.__data__;++v.count,p<=r?f(r):d3.timer(f,p,n)})},0,n),e}function Nt(e){var t=_s,n=Fs,r=Bs,i=js;return _s=this.id,Fs=this.ease(),St(this,function(t,n,r){Bs=t.delay,js=t.duration,e.call(t=t.node,t.__data__,n,r)}),_s=t,Fs=n,Bs=r,js=i,this}function Ct(e,t,n){return n!=""&&Is}function kt(e,t){return d3.tween(e,F(t))}function Lt(){var e,t=Date.now(),n=Us;while(n)e=t-n.then,e>=n.delay&&(n.flush=n.callback(e)),n=n.next;var r=At()-t;r>24?(isFinite(r)&&(clearTimeout(Ws),Ws=setTimeout(Lt,r)),zs=0):(zs=1,Xs(Lt))}function At(){var e=null,t=Us,n=Infinity;while(t)t.flush?(delete Rs[t.callback.id],t=e?e.next=t.next:Us=t.next):(n=Math.min(n,t.then+t.delay),t=(e=t).next);return n}function Ot(e,t){var n=e.ownerSVGElement||e;if(n.createSVGPoint){var r=n.createSVGPoint();if(Vs<0&&(window.scrollX||window.scrollY)){n=d3.select(document.body).append("svg").style("position","absolute").style("top",0).style("left",0);var i=n[0][0].getScreenCTM();Vs=!i.f&&!i.e,n.remove()}return Vs?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(e.getScreenCTM().inverse()),[r.x,r.y]}var s=e.getBoundingClientRect();return[t.clientX-s.left-e.clientLeft,t.clientY-s.top-e.clientTop]}function Mt(){}function _t(e){var t=e[0],n=e[e.length-1];return t<n?[t,n]:[n,t]}function Dt(e){return e.rangeExtent?e.rangeExtent():_t(e.range())}function Pt(e,t){var n=0,r=e.length-1,i=e[n],s=e[r],o;s<i&&(o=n,n=r,r=o,o=i,i=s,s=o);if(t=t(s-i))e[n]=t.floor(i),e[r]=t.ceil(s);return e}function Ht(){return Math}function Bt(e,t,n,r){function i(){var i=Math.min(e.length,t.length)>2?zt:Ut,a=r?q:I;return o=i(e,t,a,n),u=i(t,e,a,d3.interpolate),s}function s(e){return o(e)}var o,u;return s.invert=function(e){return u(e)},s.domain=function(t){return arguments.length?(e=t.map(Number),i()):e},s.range=function(e){return arguments.length?(t=e,i()):t},s.rangeRound=function(e){return s.range(e).interpolate(d3.interpolateRound)},s.clamp=function(e){return arguments.length?(r=e,i()):r},s.interpolate=function(e){return arguments.length?(n=e,i()):n},s.ticks=function(t){return qt(e,t)},s.tickFormat=function(t){return Rt(e,t)},s.nice=function(){return Pt(e,Ft),i()},s.copy=function(){return Bt(e,t,n,r)},i()}function jt(e,t){return d3.rebind(e,t,"range","rangeRound","interpolate","clamp")}function Ft(e){return e=Math.pow(10,Math.round(Math.log(e)/Math.LN10)-1),e&&{floor:function(t){return Math.floor(t/e)*e},ceil:function(t){return Math.ceil(t/e)*e}}}function It(e,t){var n=_t(e),r=n[1]-n[0],i=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),s=t/r*i;return s<=.15?i*=10:s<=.35?i*=5:s<=.75&&(i*=2),n[0]=Math.ceil(n[0]/i)*i,n[1]=Math.floor(n[1]/i)*i+i*.5,n[2]=i,n}function qt(e,t){return d3.range.apply(d3,It(e,t))}function Rt(e,t){return d3.format(",."+Math.max(0,-Math.floor(Math.log(It(e,t)[2])/Math.LN10+.01))+"f")}function Ut(e,t,n,r){var i=n(e[0],e[1]),s=r(t[0],t[1]);return function(e){return s(i(e))}}function zt(e,t,n,r){var i=[],s=[],o=0,u=Math.min(e.length,t.length)-1;e[u]<e[0]&&(e=e.slice().reverse(),t=t.slice().reverse());while(++o<=u)i.push(n(e[o-1],e[o])),s.push(r(t[o-1],t[o]));return function(t){var n=d3.bisect(e,t,1,u)-1;return s[n](i[n](t))}}function Wt(e,t){function n(n){return e(t(n))}var r=t.pow;return n.invert=function(t){return r(e.invert(t))},n.domain=function(i){return arguments.length?(t=i[0]<0?Vt:Xt,r=t.pow,e.domain(i.map(t)),n):e.domain().map(r)},n.nice=function(){return e.domain(Pt(e.domain(),Ht)),n},n.ticks=function(){var n=_t(e.domain()),i=[];if(n.every(isFinite)){var s=Math.floor(n[0]),o=Math.ceil(n[1]),u=r(n[0]),a=r(n[1]);if(t===Vt){i.push(r(s));for(;s++<o;)for(var f=9;f>0;f--)i.push(r(s)*f)}else{for(;s<o;s++)for(var f=1;f<10;f++)i.push(r(s)*f);i.push(r(s))}for(s=0;i[s]<u;s++);for(o=i.length;i[o-1]>a;o--);i=i.slice(s,o)}return i},n.tickFormat=function(e,i){arguments.length<2&&(i=$s);if(arguments.length<1)return i;var s=Math.max(.1,e/n.ticks().length),o=t===Vt?(u=-1e-12,Math.floor):(u=1e-12,Math.ceil),u;return function(e){return e/r(o(t(e)+u))<=s?i(e):""}},n.copy=function(){return Wt(e.copy(),t)},jt(n,e)}function Xt(e){return Math.log(e<0?0:e)/Math.LN10}function Vt(e){return-Math.log(e>0?0:-e)/Math.LN10}function $t(e,t){function n(t){return e(r(t))}var r=Jt(t),i=Jt(1/t);return n.invert=function(t){return i(e.invert(t))},n.domain=function(t){return arguments.length?(e.domain(t.map(r)),n):e.domain().map(i)},n.ticks=function(e){return qt(n.domain(),e)},n.tickFormat=function(e){return Rt(n.domain(),e)},n.nice=function(){return n.domain(Pt(n.domain(),Ft))},n.exponent=function(e){if(!arguments.length)return t;var s=n.domain();return r=Jt(t=e),i=Jt(1/t),n.domain(s)},n.copy=function(){return $t(e.copy(),t)},jt(n,e)}function Jt(e){return function(t){return t<0?-Math.pow(-t,e):Math.pow(t,e)}}function Kt(e,t){function n(t){return o[((s.get(t)||s.set(t,e.push(t)))-1)%o.length]}function i(t,n){return d3.range(e.length).map(function(e){return t+n*e})}var s,o,u;return n.domain=function(i){if(!arguments.length)return e;e=[],s=new r;var o=-1,u=i.length,a;while(++o<u)s.has(a=i[o])||s.set(a,e.push(a));return n[t.t].apply(n,t.a)},n.range=function(e){return arguments.length?(o=e,u=0,t={t:"range",a:arguments},n):o},n.rangePoints=function(r,s){arguments.length<2&&(s=0);var a=r[0],f=r[1],l=(f-a)/(Math.max(1,e.length-1)+s);return o=i(e.length<2?(a+f)/2:a+l*s/2,l),u=0,t={t:"rangePoints",a:arguments},n},n.rangeBands=function(r,s,a){arguments.length<2&&(s=0),arguments.length<3&&(a=s);var f=r[1]<r[0],l=r[f-0],c=r[1-f],h=(c-l)/(e.length-s+2*a);return o=i(l+h*a,h),f&&o.reverse(),u=h*(1-s),t={t:"rangeBands",a:arguments},n},n.rangeRoundBands=function(r,s,a){arguments.length<2&&(s=0),arguments.length<3&&(a=s);var f=r[1]<r[0],l=r[f-0],c=r[1-f],h=Math.floor((c-l)/(e.length-s+2*a)),p=c-l-(e.length-s)*h;return o=i(l+Math.round(p/2),h),f&&o.reverse(),u=Math.round(h*(1-s)),t={t:"rangeRoundBands",a:arguments},n},n.rangeBand=function(){return u},n.rangeExtent=function(){return _t(t.a[0])},n.copy=function(){return Kt(e,t)},n.domain(e)}function Qt(e,t){function n(){var n=0,s=e.length,o=t.length;i=[];while(++n<o)i[n-1]=d3.quantile(e,n/o);return r}function r(e){return isNaN(e=+e)?NaN:t[d3.bisect(i,e)]}var i;return r.domain=function(t){return arguments.length?(e=t.filter(function(e){return!isNaN(e)}).sort(d3.ascending),n()):e},r.range=function(e){return arguments.length?(t=e,n()):t},r.quantiles=function(){return i},r.copy=function(){return Qt(e,t)},n()}function Gt(e,t,n){function r(t){return n[Math.max(0,Math.min(o,Math.floor(s*(t-e))))]}function i(){return s=n.length/(t-e),o=n.length-1,r}var s,o;return r.domain=function(n){return arguments.length?(e=+n[0],t=+n[n.length-1],i()):[e,t]},r.range=function(e){return arguments.length?(n=e,i()):n},r.copy=function(){return Gt(e,t,n)},i()}function Yt(e,t){function n(n){return t[d3.bisect(e,n)]}return n.domain=function(t){return arguments.length?(e=t,n):e},n.range=function(e){return arguments.length?(t=e,n):t},n.copy=function(){return Yt(e,t)},n}function Zt(e){function t(e){return+e}return t.invert=t,t.domain=t.range=function(n){return arguments.length?(e=n.map(t),t):e},t.ticks=function(t){return qt(e,t)},t.tickFormat=function(t){return Rt(e,t)},t.copy=function(){return Zt(e)},t}function en(e){return e.innerRadius}function tn(e){return e.outerRadius}function nn(e){return e.startAngle}function rn(e){return e.endAngle}function sn(e){function t(t){function o(){a.push("M",s(e(l),f))}var a=[],l=[],c=-1,h=t.length,p,d=u(n),v=u(r);while(++c<h)i.call(this,p=t[c],c)?l.push([+d.call(this,p,c),+v.call(this,p,c)]):l.length&&(o(),l=[]);return l.length&&o(),a.length?a.join(""):null}var n=on,r=un,i=o,s=an,a=s.key,f=.7;return t.x=function(e){return arguments.length?(n=e,t):n},t.y=function(e){return arguments.length?(r=e,t):r},t.defined=function(e){return arguments.length?(i=e,t):i},t.interpolate=function(e){return arguments.length?(typeof e=="function"?a=s=e:a=(s=eo.get(e)||an).key,t):a},t.tension=function(e){return arguments.length?(f=e,t):f},t}function on(e){return e[0]}function un(e){return e[1]}function an(e){return e.join("L")}function fn(e){return an(e)+"Z"}function ln(e){var t=0,n=e.length,r=e[0],i=[r[0],",",r[1]];while(++t<n)i.push("V",(r=e[t])[1],"H",r[0]);return i.join("")}function cn(e){var t=0,n=e.length,r=e[0],i=[r[0],",",r[1]];while(++t<n)i.push("H",(r=e[t])[0],"V",r[1]);return i.join("")}function hn(e,t){return e.length<4?an(e):e[1]+vn(e.slice(1,e.length-1),mn(e,t))}function pn(e,t){return e.length<3?an(e):e[0]+vn((e.push(e[0]),e),mn([e[e.length-2]].concat(e,[e[1]]),t))}function dn(e,t,n){return e.length<3?an(e):e[0]+vn(e,mn(e,t))}function vn(e,t){if(t.length<1||e.length!=t.length&&e.length!=t.length+2)return an(e);var n=e.length!=t.length,r="",i=e[0],s=e[1],o=t[0],u=o,a=1;n&&(r+="Q"+(s[0]-o[0]*2/3)+","+(s[1]-o[1]*2/3)+","+s[0]+","+s[1],i=e[1],a=2);if(t.length>1){u=t[1],s=e[a],a++,r+="C"+(i[0]+o[0])+","+(i[1]+o[1])+","+(s[0]-u[0])+","+(s[1]-u[1])+","+s[0]+","+s[1];for(var f=2;f<t.length;f++,a++)s=e[a],u=t[f],r+="S"+(s[0]-u[0])+","+(s[1]-u[1])+","+s[0]+","+s[1]}if(n){var l=e[a];r+="Q"+(s[0]+u[0]*2/3)+","+(s[1]+u[1]*2/3)+","+l[0]+","+l[1]}return r}function mn(e,t){var n=[],r=(1-t)/2,i,s=e[0],o=e[1],u=1,a=e.length;while(++u<a)i=s,s=o,o=e[u],n.push([r*(o[0]-i[0]),r*(o[1]-i[1])]);return n}function gn(e){if(e.length<3)return an(e);var t=1,n=e.length,r=e[0],i=r[0],s=r[1],o=[i,i,i,(r=e[1])[0]],u=[s,s,s,r[1]],a=[i,",",s];Sn(a,o,u);while(++t<n)r=e[t],o.shift(),o.push(r[0]),u.shift(),u.push(r[1]),Sn(a,o,u);t=-1;while(++t<2)o.shift(),o.push(r[0]),u.shift(),u.push(r[1]),Sn(a,o,u);return a.join("")}function yn(e){if(e.length<4)return an(e);var t=[],n=-1,r=e.length,i,s=[0],o=[0];while(++n<3)i=e[n],s.push(i[0]),o.push(i[1]);t.push(En(ro,s)+","+En(ro,o)),--n;while(++n<r)i=e[n],s.shift(),s.push(i[0]),o.shift(),o.push(i[1]),Sn(t,s,o);return t.join("")}function bn(e){var t,n=-1,r=e.length,i=r+4,s,o=[],u=[];while(++n<4)s=e[n%r],o.push(s[0]),u.push(s[1]);t=[En(ro,o),",",En(ro,u)],--n;while(++n<i)s=e[n%r],o.shift(),o.push(s[0]),u.shift(),u.push(s[1]),Sn(t,o,u);return t.join("")}function wn(e,t){var n=e.length-1;if(n){var r=e[0][0],i=e[0][1],s=e[n][0]-r,o=e[n][1]-i,u=-1,a,f;while(++u<=n)a=e[u],f=u/n,a[0]=t*a[0]+(1-t)*(r+f*s),a[1]=t*a[1]+(1-t)*(i+f*o)}return gn(e)}function En(e,t){return e[0]*t[0]+e[1]*t[1]+e[2]*t[2]+e[3]*t[3]}function Sn(e,t,n){e.push("C",En(to,t),",",En(to,n),",",En(no,t),",",En(no,n),",",En(ro,t),",",En(ro,n))}function xn(e,t){return(t[1]-e[1])/(t[0]-e[0])}function Tn(e){var t=0,n=e.length-1,r=[],i=e[0],s=e[1],o=r[0]=xn(i,s);while(++t<n)r[t]=(o+(o=xn(i=s,s=e[t+1])))/2;return r[t]=o,r}function Nn(e){var t=[],n,r,i,s,o=Tn(e),u=-1,a=e.length-1;while(++u<a)n=xn(e[u],e[u+1]),Math.abs(n)<1e-6?o[u]=o[u+1]=0:(r=o[u]/n,i=o[u+1]/n,s=r*r+i*i,s>9&&(s=n*3/Math.sqrt(s),o[u]=s*r,o[u+1]=s*i));u=-1;while(++u<=a)s=(e[Math.min(a,u+1)][0]-e[Math.max(0,u-1)][0])/(6*(1+o[u]*o[u])),t.push([s||0,o[u]*s||0]);return t}function Cn(e){return e.length<3?an(e):e[0]+vn(e,Nn(e))}function kn(e){var t,n=-1,r=e.length,i,s;while(++n<r)t=e[n],i=t[0],s=t[1]+Ys,t[0]=i*Math.cos(s),t[1]=i*Math.sin(s);return e}function Ln(e){function t(t){function o(){l.push("M",f(e(v),p),h,c(e(d.reverse()),p),"Z")}var l=[],d=[],v=[],m=-1,g=t.length,y,b=u(n),w=u(i),E=n===r?function(){return x}:u(r),S=i===s?function(){return T}:u(s),x,T;while(++m<g)a.call(this,y=t[m],m)?(d.push([x=+b.call(this,y,m),T=+w.call(this,y,m)]),v.push([+E.call(this,y,m),+S.call(this,y,m)])):d.length&&(o(),d=[],v=[]);return d.length&&o(),l.length?l.join(""):null}var n=on,r=on,i=0,s=un,a=o,f=an,l=f.key,c=f,h="L",p=.7;return t.x=function(e){return arguments.length?(n=r=e,t):r},t.x0=function(e){return arguments.length?(n=e,t):n},t.x1=function(e){return arguments.length?(r=e,t):r},t.y=function(e){return arguments.length?(i=s=e,t):s},t.y0=function(e){return arguments.length?(i=e,t):i},t.y1=function(e){return arguments.length?(s=e,t):s},t.defined=function(e){return arguments.length?(a=e,t):a},t.interpolate=function(e){return arguments.length?(typeof e=="function"?l=f=e:l=(f=eo.get(e)||an).key,c=f.reverse||f,h=f.closed?"M":"L",t):l},t.tension=function(e){return arguments.length?(p=e,t):p},t}function An(e){return e.source}function On(e){return e.target}function Mn(e){return e.radius}function _n(e){return e.startAngle}function Dn(e){return e.endAngle}function Pn(e){return[e.x,e.y]}function Hn(e){return function(){var t=e.apply(this,arguments),n=t[0],r=t[1]+Ys;return[n*Math.cos(r),n*Math.sin(r)]}}function Bn(){return 64}function jn(){return"circle"}function Fn(e){var t=Math.sqrt(e/Math.PI);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+ -t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function In(e,t){e.attr("transform",function(e){return"translate("+t(e)+",0)"})}function qn(e,t){e.attr("transform",function(e){return"translate(0,"+t(e)+")"})}function Rn(e,t,n){i=[];if(n&&t.length>1){var r=_t(e.domain()),i,s=-1,o=t.length,u=(t[1]-t[0])/++n,a,f;while(++s<o)for(a=n;--a>0;)(f=+t[s]-a*u)>=r[0]&&i.push(f);for(--s,a=0;++a<n&&(f=+t[s]+a*u)<r[1];)i.push(f)}return i}function Un(){fo||(fo=d3.select("body").append("div").style("visibility","hidden").style("top",0).style("height",0).style("width",0).style("overflow-y","scroll").append("div").style("height","2000px").node().parentNode);var e=d3.event,t;try{fo.scrollTop=1e3,fo.dispatchEvent(e),t=1e3-fo.scrollTop}catch(n){t=e.wheelDelta||-e.detail*5}return t}function zn(e){var t=e.source,n=e.target,r=Xn(t,n),i=[t];while(t!==r)t=t.parent,i.push(t);var s=i.length;while(n!==r)i.splice(s,0,n),n=n.parent;return i}function Wn(e){var t=[],n=e.parent;while(n!=null)t.push(e),e=n,n=n.parent;return t.push(e),t}function Xn(e,t){if(e===t)return e;var n=Wn(e),r=Wn(t),i=n.pop(),s=r.pop(),o=null;while(i===s)o=i,i=n.pop(),s=r.pop();return o}function Vn(e){e.fixed|=2}function $n(e){e.fixed&=1}function Jn(e){e.fixed|=4}function Kn(e){e.fixed&=3}function Qn(e,t,n){var r=0,i=0;e.charge=0;if(!e.leaf){var s=e.nodes,o=s.length,u=-1,a;while(++u<o){a=s[u];if(a==null)continue;Qn(a,t,n),e.charge+=a.charge,r+=a.charge*a.cx,i+=a.charge*a.cy}}if(e.point){e.leaf||(e.point.x+=Math.random()-.5,e.point.y+=Math.random()-.5);var f=t*n[e.point.index];e.charge+=e.pointCharge=f,r+=f*e.point.x,i+=f*e.point.y}e.cx=r/e.charge,e.cy=i/e.charge}function Gn(e){return 20}function Yn(e){return 1}function Zn(e){return e.x}function er(e){return e.y}function tr(e,t,n){e.y0=t,e.y=n}function nr(e){return d3.range(e.length)}function rr(e){var t=-1,n=e[0].length,r=[];while(++t<n)r[t]=0;return r}function ir(e){var t=1,n=0,r=e[0][1],i,s=e.length;for(;t<s;++t)(i=e[t][1])>r&&(n=t,r=i);return n}function sr(e){return e.reduce(or,0)}function or(e,t){return e+t[1]}function ur(e,t){return ar(e,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ar(e,t){var n=-1,r=+e[0],i=(e[1]-r)/t,s=[];while(++n<=t)s[n]=i*n+r;return s}function fr(e){return[d3.min(e),d3.max(e)]}function lr(e,t){return d3.rebind(e,t,"sort","children","value"),e.links=dr,e.nodes=function(t){return vo=!0,(e.nodes=e)(t)},e}function cr(e){return e.children}function hr(e){return e.value}function pr(e,t){return t.value-e.value}function dr(e){return d3.merge(e.map(function(e){return(e.children||[]).map(function(t){return{source:e,target:t}})}))}function vr(e,t){return e.value-t.value}function mr(e,t){var n=e._pack_next;e._pack_next=t,t._pack_prev=e,t._pack_next=n,n._pack_prev=t}function gr(e,t){e._pack_next=t,t._pack_prev=e}function yr(e,t){var n=t.x-e.x,r=t.y-e.y,i=e.r+t.r;return i*i-n*n-r*r>.001}function br(e){function t(e){r=Math.min(e.x-e.r,r),i=Math.max(e.x+e.r,i),s=Math.min(e.y-e.r,s),o=Math.max(e.y+e.r,o)}if(!(n=e.children)||!(p=n.length))return;var n,r=Infinity,i=-Infinity,s=Infinity,o=-Infinity,u,a,f,l,c,h,p;n.forEach(wr),u=n[0],u.x=-u.r,u.y=0,t(u);if(p>1){a=n[1],a.x=a.r,a.y=0,t(a);if(p>2){f=n[2],xr(u,a,f),t(f),mr(u,f),u._pack_prev=f,mr(f,a),a=u._pack_next;for(l=3;l<p;l++){xr(u,a,f=n[l]);var d=0,v=1,m=1;for(c=a._pack_next;c!==a;c=c._pack_next,v++)if(yr(c,f)){d=1;break}if(d==1)for(h=u._pack_prev;h!==c._pack_prev;h=h._pack_prev,m++)if(yr(h,f))break;d?(v<m||v==m&&a.r<u.r?gr(u,a=c):gr(u=h,a),l--):(mr(u,f),a=f,t(f))}}}var g=(r+i)/2,y=(s+o)/2,b=0;for(l=0;l<p;l++)f=n[l],f.x-=g,f.y-=y,b=Math.max(b,f.r+Math.sqrt(f.x*f.x+f.y*f.y));e.r=b,n.forEach(Er)}function wr(e){e._pack_next=e._pack_prev=e}function Er(e){delete e._pack_next,delete e._pack_prev}function Sr(e,t,n,r){var i=e.children;e.x=t+=r*e.x,e.y=n+=r*e.y,e.r*=r;if(i){var s=-1,o=i.length;while(++s<o)Sr(i[s],t,n,r)}}function xr(e,t,n){var r=e.r+n.r,i=t.x-e.x,s=t.y-e.y;if(r&&(i||s)){var o=t.r+n.r,u=i*i+s*s;o*=o,r*=r;var a=.5+(r-o)/(2*u),f=Math.sqrt(Math.max(0,2*o*(r+u)-(r-=u)*r-o*o))/(2*u);n.x=e.x+a*i+f*s,n.y=e.y+a*s-f*i}else n.x=e.x+r,n.y=e.y}function Tr(e){return 1+d3.max(e,function(e){return e.y})}function Nr(e){return e.reduce(function(e,t){return e+t.x},0)/e.length}function Cr(e){var t=e.children;return t&&t.length?Cr(t[0]):e}function kr(e){var t=e.children,n;return t&&(n=t.length)?kr(t[n-1]):e}function Lr(e,t){return e.parent==t.parent?1:2}function Ar(e){var t=e.children;return t&&t.length?t[0]:e._tree.thread}function Or(e){var t=e.children,n;return t&&(n=t.length)?t[n-1]:e._tree.thread}function Mr(e,t){var n=e.children;if(n&&(i=n.length)){var r,i,s=-1;while(++s<i)t(r=Mr(n[s],t),e)>0&&(e=r)}return e}function _r(e,t){return e.x-t.x}function Dr(e,t){return t.x-e.x}function Pr(e,t){return e.depth-t.depth}function Hr(e,t){function n(e,r){var i=e.children;if(i&&(a=i.length)){var s,o=null,u=-1,a;while(++u<a)s=i[u],n(s,o),o=s}t(e,r)}n(e,null)}function Br(e){var t=0,n=0,r=e.children,i=r.length,s;while(--i>=0)s=r[i]._tree,s.prelim+=t,s.mod+=t,t+=s.shift+(n+=s.change)}function jr(e,t,n){e=e._tree,t=t._tree;var r=n/(t.number-e.number);e.change+=r,t.change-=r,t.shift+=n,t.prelim+=n,t.mod+=n}function Fr(e,t,n){return e._tree.ancestor.parent==t.parent?e._tree.ancestor:n}function Ir(e){return{x:e.x,y:e.y,dx:e.dx,dy:e.dy}}function qr(e,t){var n=e.x+t[3],r=e.y+t[0],i=e.dx-t[1]-t[3],s=e.dy-t[0]-t[2];return i<0&&(n+=i/2,i=0),s<0&&(r+=s/2,s=0),{x:n,y:r,dx:i,dy:s}}function Rr(e,t){function n(e,r){d3.text(e,t,function(e){r(e&&n.parse(e))})}function r(t){return t.map(i).join(e)}function i(e){return o.test(e)?'"'+e.replace(/\"/g,'""')+'"':e}var s=new RegExp("\r\n|["+e+"\r\n]","g"),o=new RegExp('["'+e+"\n]"),u=e.charCodeAt(0);return n.parse=function(e){var t;return n.parseRows(e,function(e,n){if(n){var r={},i=-1,s=t.length;while(++i<s)r[t[i]]=e[i];return r}return t=e,null})},n.parseRows=function(e,t){function n(){if(s.lastIndex>=e.length)return i;if(l)return l=!1,r;var t=s.lastIndex;if(e.charCodeAt(t)===34){var n=t;while(n++<e.length)if(e.charCodeAt(n)===34){if(e.charCodeAt(n+1)!==34)break;n++}s.lastIndex=n+2;var o=e.charCodeAt(n+1);return o===13?(l=!0,e.charCodeAt(n+2)===10&&s.lastIndex++):o===10&&(l=!0),e.substring(t+1,n).replace(/""/g,'"')}var a=s.exec(e);return a?(l=a[0].charCodeAt(0)!==u,e.substring(t,a.index)):(s.lastIndex=e.length,e.substring(t))}var r={},i={},o=[],a=0,f,l;s.lastIndex=0;while((f=n())!==i){var c=[];while(f!==r&&f!==i)c.push(f),f=n();if(t&&!(c=t(c,a++)))continue;o.push(c)}return o},n.format=function(e){return e.map(r).join("\n")},n}function Ur(e,t){return function(n){return n&&e.hasOwnProperty(n.type)?e[n.type](n):t}}function zr(e){return"m0,"+e+"a"+e+","+e+" 0 1,1 0,"+ -2*e+"a"+e+","+e+" 0 1,1 0,"+2*e+"z"}function Wr(e,t){go.hasOwnProperty(e.type)&&go[e.type](e,t)}function Xr(e,t){Wr(e.geometry,t)}function Vr(e,t){for(var n=e.features,r=0,i=n.length;r<i;r++)Wr(n[r].geometry,t)}function $r(e,t){for(var n=e.geometries,r=0,i=n.length;r<i;r++)Wr(n[r],t)}function Jr(e,t){for(var n=e.coordinates,r=0,i=n.length;r<i;r++)t.apply(null,n[r])}function Kr(e,t){for(var n=e.coordinates,r=0,i=n.length;r<i;r++)for(var s=n[r],o=0,u=s.length;o<u;o++)t.apply(null,s[o])}function Qr(e,t){for(var n=e.coordinates,r=0,i=n.length;r<i;r++)for(var s=n[r][0],o=0,u=s.length;o<u;o++)t.apply(null,s[o])}function Gr(e,t){t.apply(null,e.coordinates)}function Yr(e,t){for(var n=e.coordinates[0],r=0,i=n.length;r<i;r++)t.apply(null,n[r])}function Zr(e){return e.source}function ei(e){return e.target}function ti(){function e(e){var t=Math.sin(e*=p)*d,n=Math.sin(p-e)*d,r=n*s+t*c,u=n*o+t*h,a=n*i+t*l;return[Math.atan2(u,r)/mo,Math.atan2(a,Math.sqrt(r*r+u*u))/mo]}var t,n,r,i,s,o,u,a,f,l,c,h,p,d;return e.distance=function(){return p==null&&(d=1/Math.sin(p=Math.acos(Math.max(-1,Math.min(1,i*l+r*f*Math.cos(u-t)))))),p},e.source=function(u){var a=Math.cos(t=u[0]*mo),f=Math.sin(t);return r=Math.cos(n=u[1]*mo),i=Math.sin(n),s=r*a,o=r*f,p=null,e},e.target=function(t){var n=Math.cos(u=t[0]*mo),r=Math.sin(u);return f=Math.cos(a=t[1]*mo),l=Math.sin(a),c=f*n,h=f*r,p=null,e},e}function ni(e,t){var n=ti().source(e).target(t);return n.distance(),n}function ri(e){var t=0,n=0;for(;;){if(e(t,n))return[t,n];t===0?(t=n+1,n=0):(t-=1,n+=1)}}function ii(e,t,n,r){var i,s,o,u,a,f,l;return i=r[e],s=i[0],o=i[1],i=r[t],u=i[0],a=i[1],i=r[n],f=i[0],l=i[1],(l-o)*(u-s)-(a-o)*(f-s)>0}function si(e,t,n){return(n[0]-t[0])*(e[1]-t[1])<(n[1]-t[1])*(e[0]-t[0])}function oi(e,t,n,r){var i=e[0],s=t[0],o=n[0],u=r[0],a=e[1],f=t[1],l=n[1],c=r[1],h=i-o,p=s-i,d=u-o,v=a-l,m=f-a,g=c-l,y=(d*v-g*h)/(g*p-d*m);return[i+y*p,a+y*m]}function ui(e,t){var n={list:e.map(function(e,t){return{index:t,x:e[0],y:e[1]}}).sort(function(e,t){return e.y<t.y?-1:e.y>t.y?1:e.x<t.x?-1:e.x>t.x?1:0}),bottomSite:null},r={list:[],leftEnd:null,rightEnd:null,init:function(){r.leftEnd=r.createHalfEdge(null,"l"),r.rightEnd=r.createHalfEdge(null,"l"),r.leftEnd.r=r.rightEnd,r.rightEnd.l=r.leftEnd,r.list.unshift(r.leftEnd,r.rightEnd)},createHalfEdge:function(e,t){return{edge:e,side:t,vertex:null,l:null,r:null}},insert:function(e,t){t.l=e,t.r=e.r,e.r.l=t,e.r=t},leftBound:function(e){var t=r.leftEnd;do t=t.r;while(t!=r.rightEnd&&i.rightOf(t,e));return t=t.l,t},del:function(e){e.l.r=e.r,e.r.l=e.l,e.edge=null},right:function(e){return e.r},left:function(e){return e.l},leftRegion:function(e){return e.edge==null?n.bottomSite:e.edge.region[e.side]},rightRegion:function(e){return e.edge==null?n.bottomSite:e.edge.region[wo[e.side]]}},i={bisect:function(e,t){var n={region:{l:e,r:t},ep:{l:null,r:null}},r=t.x-e.x,i=t.y-e.y,s=r>0?r:-r,o=i>0?i:-i;return n.c=e.x*r+e.y*i+(r*r+i*i)*.5,s>o?(n.a=1,n.b=i/r,n.c/=r):(n.b=1,n.a=r/i,n.c/=i),n},intersect:function(e,t){var n=e.edge,r=t.edge;if(!n||!r||n.region.r==r.region.r)return null;var i=n.a*r.b-n.b*r.a;if(Math.abs(i)<1e-10)return null;var s=(n.c*r.b-r.c*n.b)/i,o=(r.c*n.a-n.c*r.a)/i,u=n.region.r,a=r.region.r,f,l;u.y<a.y||u.y==a.y&&u.x<a.x?(f=e,l=n):(f=t,l=r);var c=s>=l.region.r.x;return c&&f.side==="l"||!c&&f.side==="r"?null:{x:s,y:o}},rightOf:function(e,t){var n=e.edge,r=n.region.r,i=t.x>r.x;if(i&&e.side==="l")return 1;if(!i&&e.side==="r")return 0;if(n.a===1){var s=t.y-r.y,o=t.x-r.x,u=0,a=0;!i&&n.b<0||i&&n.b>=0?a=u=s>=n.b*o:(a=t.x+t.y*n.b>n.c,n.b<0&&(a=!a),a||(u=1));if(!u){var f=r.x-n.region.l.x;a=n.b*(o*o-s*s)<f*s*(1+2*o/f+n.b*n.b),n.b<0&&(a=!a)}}else{var l=n.c-n.a*t.x,c=t.y-l,h=t.x-r.x,p=l-r.y;a=c*c>h*h+p*p}return e.side==="l"?a:!a},endPoint:function(e,n,r){e.ep[n]=r;if(!e.ep[wo[n]])return;t(e)},distance:function(e,t){var n=e.x-t.x,r=e.y-t.y;return Math.sqrt(n*n+r*r)}},s={list:[],insert:function(e,t,n){e.vertex=t,e.ystar=t.y+n;for(var r=0,i=s.list,o=i.length;r<o;r++){var u=i[r];if(e.ystar>u.ystar||e.ystar==u.ystar&&t.x>u.vertex.x)continue;break}i.splice(r,0,e)},del:function(e){for(var t=0,n=s.list,r=n.length;t<r&&n[t]!=e;++t);n.splice(t,1)},empty:function(){return s.list.length===0},nextEvent:function(e){for(var t=0,n=s.list,r=n.length;t<r;++t)if(n[t]==e)return n[t+1];return null},min:function(){var e=s.list[0];return{x:e.vertex.x,y:e.ystar}},extractMin:function(){return s.list.shift()}};r.init(),n.bottomSite=n.list.shift();var o=n.list.shift(),u,a,f,l,c,h,p,d,v,m,g,y,b;for(;;){s.empty()||(u=s.min());if(o&&(s.empty()||o.y<u.y||o.y==u.y&&o.x<u.x))a=r.leftBound(o),f=r.right(a),p=r.rightRegion(a),y=i.bisect(p,o),h=r.createHalfEdge(y,"l"),r.insert(a,h),m=i.intersect(a,h),m&&(s.del(a),s.insert(a,m,i.
+distance(m,o))),a=h,h=r.createHalfEdge(y,"r"),r.insert(a,h),m=i.intersect(h,f),m&&s.insert(h,m,i.distance(m,o)),o=n.list.shift();else{if(!!s.empty())break;a=s.extractMin(),l=r.left(a),f=r.right(a),c=r.right(f),p=r.leftRegion(a),d=r.rightRegion(f),g=a.vertex,i.endPoint(a.edge,a.side,g),i.endPoint(f.edge,f.side,g),r.del(a),s.del(f),r.del(f),b="l",p.y>d.y&&(v=p,p=d,d=v,b="r"),y=i.bisect(p,d),h=r.createHalfEdge(y,b),r.insert(l,h),i.endPoint(y,wo[b],g),m=i.intersect(l,h),m&&(s.del(l),s.insert(l,m,i.distance(m,p))),m=i.intersect(h,c),m&&s.insert(h,m,i.distance(m,p))}}for(a=r.right(r.leftEnd);a!=r.rightEnd;a=r.right(a))t(a.edge)}function ai(){return{leaf:!0,nodes:[],point:null}}function fi(e,t,n,r,i,s){if(!e(t,n,r,i,s)){var o=(n+i)*.5,u=(r+s)*.5,a=t.nodes;a[0]&&fi(e,a[0],n,r,o,u),a[1]&&fi(e,a[1],o,r,i,u),a[2]&&fi(e,a[2],n,u,o,s),a[3]&&fi(e,a[3],o,u,i,s)}}function li(e){return{x:e[0],y:e[1]}}function ci(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function hi(e){return e.substring(0,3)}function pi(e,t,n,r){var i,s,o=0,u=t.length,a=n.length;while(o<u){if(r>=a)return-1;i=t.charCodeAt(o++);if(i==37){s=Uo[t.charAt(o++)];if(!s||(r=s(e,n,r))<0)return-1}else if(i!=n.charCodeAt(r++))return-1}return r}function di(e){return new RegExp("^(?:"+e.map(d3.requote).join("|")+")","i")}function vi(e){var t=new r,n=-1,i=e.length;while(++n<i)t.set(e[n].toLowerCase(),n);return t}function mi(e,t,n){Bo.lastIndex=0;var r=Bo.exec(t.substring(n));return r?n+=r[0].length:-1}function gi(e,t,n){Ho.lastIndex=0;var r=Ho.exec(t.substring(n));return r?n+=r[0].length:-1}function yi(e,t,n){Io.lastIndex=0;var r=Io.exec(t.substring(n));return r?(e.m=qo.get(r[0].toLowerCase()),n+=r[0].length):-1}function bi(e,t,n){jo.lastIndex=0;var r=jo.exec(t.substring(n));return r?(e.m=Fo.get(r[0].toLowerCase()),n+=r[0].length):-1}function wi(e,t,n){return pi(e,Ro.c.toString(),t,n)}function Ei(e,t,n){return pi(e,Ro.x.toString(),t,n)}function Si(e,t,n){return pi(e,Ro.X.toString(),t,n)}function xi(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+4));return r?(e.y=+r[0],n+=r[0].length):-1}function Ti(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.y=Ni(+r[0]),n+=r[0].length):-1}function Ni(e){return e+(e>68?1900:2e3)}function Ci(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.m=r[0]-1,n+=r[0].length):-1}function ki(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.d=+r[0],n+=r[0].length):-1}function Li(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.H=+r[0],n+=r[0].length):-1}function Ai(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.M=+r[0],n+=r[0].length):-1}function Oi(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+2));return r?(e.S=+r[0],n+=r[0].length):-1}function Mi(e,t,n){zo.lastIndex=0;var r=zo.exec(t.substring(n,n+3));return r?(e.L=+r[0],n+=r[0].length):-1}function _i(e,t,n){var r=Wo.get(t.substring(n,n+=2).toLowerCase());return r==null?-1:(e.p=r,n)}function Di(e){var t=e.getTimezoneOffset(),n=t>0?"-":"+",r=~~(Math.abs(t)/60),i=Math.abs(t)%60;return n+Mo(r)+Mo(i)}function Pi(e){return e.toISOString()}function Hi(e,t,n){function r(t){var n=e(t),r=s(n,1);return t-n<r-t?n:r}function i(n){return t(n=e(new Eo(n-1)),1),n}function s(e,n){return t(e=new Eo(+e),n),e}function o(e,r,s){var o=i(e),u=[];if(s>1)while(o<r)n(o)%s||u.push(new Date(+o)),t(o,1);else while(o<r)u.push(new Date(+o)),t(o,1);return u}function u(e,t,n){try{Eo=ci;var r=new ci;return r._=e,o(r,t,n)}finally{Eo=Date}}e.floor=e,e.round=r,e.ceil=i,e.offset=s,e.range=o;var a=e.utc=Bi(e);return a.floor=a,a.round=Bi(r),a.ceil=Bi(i),a.offset=Bi(s),a.range=u,e}function Bi(e){return function(t,n){try{Eo=ci;var r=new ci;return r._=t,e(r,n)._}finally{Eo=Date}}}function ji(e,t,n){function r(t){return e(t)}return r.invert=function(t){return Ii(e.invert(t))},r.domain=function(t){return arguments.length?(e.domain(t),r):e.domain().map(Ii)},r.nice=function(e){return r.domain(Pt(r.domain(),function(){return e}))},r.ticks=function(n,i){var s=Fi(r.domain());if(typeof n!="function"){var o=s[1]-s[0],u=o/n,a=d3.bisect(Vo,u);if(a==Vo.length)return t.year(s,n);if(!a)return e.ticks(n).map(Ii);Math.log(u/Vo[a-1])<Math.log(Vo[a]/u)&&--a,n=t[a],i=n[1],n=n[0].range}return n(s[0],new Date(+s[1]+1),i)},r.tickFormat=function(){return n},r.copy=function(){return ji(e.copy(),t,n)},d3.rebind(r,e,"range","rangeRound","interpolate","clamp")}function Fi(e){var t=e[0],n=e[e.length-1];return t<n?[t,n]:[n,t]}function Ii(e){return new Date(e)}function qi(e){return function(t){var n=e.length-1,r=e[n];while(!r[1](t))r=e[--n];return r[0](t)}}function Ri(e){var t=new Date(e,0,1);return t.setFullYear(e),t}function Ui(e){var t=e.getFullYear(),n=Ri(t),r=Ri(t+1);return t+(e-n)/(r-n)}function zi(e){var t=new Date(Date.UTC(e,0,1));return t.setUTCFullYear(e),t}function Wi(e){var t=e.getUTCFullYear(),n=zi(t),r=zi(t+1);return t+(e-n)/(r-n)}Date.now||(Date.now=function(){return+(new Date)});try{document.createElement("div").style.setProperty("opacity",0,"")}catch(Xi){var Vi=CSSStyleDeclaration.prototype,$i=Vi.setProperty;Vi.setProperty=function(e,t,n){$i.call(this,e,t+"",n)}}d3={version:"2.10.3"};var Ji=n;try{Ji(document.documentElement.childNodes)[0].nodeType}catch(Ki){Ji=t}var Qi=[].__proto__?function(e,t){e.__proto__=t}:function(e,t){for(var n in t)e[n]=t[n]};d3.map=function(e){var t=new r;for(var n in e)t.set(n,e[n]);return t},e(r,{has:function(e){return Gi+e in this},get:function(e){return this[Gi+e]},set:function(e,t){return this[Gi+e]=t},remove:function(e){return e=Gi+e,e in this&&delete this[e]},keys:function(){var e=[];return this.forEach(function(t){e.push(t)}),e},values:function(){var e=[];return this.forEach(function(t,n){e.push(n)}),e},entries:function(){var e=[];return this.forEach(function(t,n){e.push({key:t,value:n})}),e},forEach:function(e){for(var t in this)t.charCodeAt(0)===Yi&&e.call(this,t.substring(1),this[t])}});var Gi="\0",Yi=Gi.charCodeAt(0);d3.functor=u,d3.rebind=function(e,t){var n=1,r=arguments.length,i;while(++n<r)e[i=arguments[n]]=a(e,t,t[i]);return e},d3.ascending=function(e,t){return e<t?-1:e>t?1:e>=t?0:NaN},d3.descending=function(e,t){return t<e?-1:t>e?1:t>=e?0:NaN},d3.mean=function(e,t){var n=e.length,r,i=0,s=-1,o=0;if(arguments.length===1)while(++s<n)f(r=e[s])&&(i+=(r-i)/++o);else while(++s<n)f(r=t.call(e,e[s],s))&&(i+=(r-i)/++o);return o?i:undefined},d3.median=function(e,t){return arguments.length>1&&(e=e.map(t)),e=e.filter(f),e.length?d3.quantile(e.sort(d3.ascending),.5):undefined},d3.min=function(e,t){var n=-1,r=e.length,i,s;if(arguments.length===1){while(++n<r&&((i=e[n])==null||i!=i))i=undefined;while(++n<r)(s=e[n])!=null&&i>s&&(i=s)}else{while(++n<r&&((i=t.call(e,e[n],n))==null||i!=i))i=undefined;while(++n<r)(s=t.call(e,e[n],n))!=null&&i>s&&(i=s)}return i},d3.max=function(e,t){var n=-1,r=e.length,i,s;if(arguments.length===1){while(++n<r&&((i=e[n])==null||i!=i))i=undefined;while(++n<r)(s=e[n])!=null&&s>i&&(i=s)}else{while(++n<r&&((i=t.call(e,e[n],n))==null||i!=i))i=undefined;while(++n<r)(s=t.call(e,e[n],n))!=null&&s>i&&(i=s)}return i},d3.extent=function(e,t){var n=-1,r=e.length,i,s,o;if(arguments.length===1){while(++n<r&&((i=o=e[n])==null||i!=i))i=o=undefined;while(++n<r)(s=e[n])!=null&&(i>s&&(i=s),o<s&&(o=s))}else{while(++n<r&&((i=o=t.call(e,e[n],n))==null||i!=i))i=undefined;while(++n<r)(s=t.call(e,e[n],n))!=null&&(i>s&&(i=s),o<s&&(o=s))}return[i,o]},d3.random={normal:function(e,t){var n=arguments.length;return n<2&&(t=1),n<1&&(e=0),function(){var n,r,i;do n=Math.random()*2-1,r=Math.random()*2-1,i=n*n+r*r;while(!i||i>1);return e+t*n*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(e,t){var n=arguments.length;n<2&&(t=1),n<1&&(e=0);var r=d3.random.normal();return function(){return Math.exp(e+t*r())}},irwinHall:function(e){return function(){for(var t=0,n=0;n<e;n++)t+=Math.random();return t/e}}},d3.sum=function(e,t){var n=0,r=e.length,i,s=-1;if(arguments.length===1)while(++s<r)isNaN(i=+e[s])||(n+=i);else while(++s<r)isNaN(i=+t.call(e,e[s],s))||(n+=i);return n},d3.quantile=function(e,t){var n=(e.length-1)*t+1,r=Math.floor(n),i=e[r-1],s=n-r;return s?i+s*(e[r]-i):i},d3.transpose=function(e){return d3.zip.apply(d3,e)},d3.zip=function(){if(!(i=arguments.length))return[];for(var e=-1,t=d3.min(arguments,l),n=new Array(t);++e<t;)for(var r=-1,i,s=n[e]=new Array(i);++r<i;)s[r]=arguments[r][e];return n},d3.bisector=function(e){return{left:function(t,n,r,i){arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);while(r<i){var s=r+i>>>1;e.call(t,t[s],s)<n?r=s+1:i=s}return r},right:function(t,n,r,i){arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);while(r<i){var s=r+i>>>1;n<e.call(t,t[s],s)?i=s:r=s+1}return r}}};var Zi=d3.bisector(function(e){return e});d3.bisectLeft=Zi.left,d3.bisect=d3.bisectRight=Zi.right,d3.first=function(e,t){var n=0,r=e.length,i=e[0],s;arguments.length===1&&(t=d3.ascending);while(++n<r)t.call(e,i,s=e[n])>0&&(i=s);return i},d3.last=function(e,t){var n=0,r=e.length,i=e[0],s;arguments.length===1&&(t=d3.ascending);while(++n<r)t.call(e,i,s=e[n])<=0&&(i=s);return i},d3.nest=function(){function e(t,s){if(s>=i.length)return u?u.call(n,t):o?t.sort(o):t;var a=-1,f=t.length,l=i[s++],c,h,p=new r,d,v={};while(++a<f)(d=p.get(c=l(h=t[a])))?d.push(h):p.set(c,[h]);return p.forEach(function(t,n){v[t]=e(n,s)}),v}function t(e,n){if(n>=i.length)return e;var r=[],o=s[n++],u;for(u in e)r.push({key:u,values:t(e[u],n)});return o&&r.sort(function(e,t){return o(e.key,t.key)}),r}var n={},i=[],s=[],o,u;return n.map=function(t){return e(t,0)},n.entries=function(n){return t(e(n,0),0)},n.key=function(e){return i.push(e),n},n.sortKeys=function(e){return s[i.length-1]=e,n},n.sortValues=function(e){return o=e,n},n.rollup=function(e){return u=e,n},n},d3.keys=function(e){var t=[];for(var n in e)t.push(n);return t},d3.values=function(e){var t=[];for(var n in e)t.push(e[n]);return t},d3.entries=function(e){var t=[];for(var n in e)t.push({key:n,value:e[n]});return t},d3.permute=function(e,t){var n=[],r=-1,i=t.length;while(++r<i)n[r]=e[t[r]];return n},d3.merge=function(e){return Array.prototype.concat.apply([],e)},d3.split=function(e,t){var n=[],r=[],i,s=-1,o=e.length;arguments.length<2&&(t=c);while(++s<o)t.call(r,i=e[s],s)?r=[]:(r.length||n.push(r),r.push(i));return n},d3.range=function(e,t,n){arguments.length<3&&(n=1,arguments.length<2&&(t=e,e=0));if((t-e)/n===Infinity)throw new Error("infinite range");var r=[],i=p(Math.abs(n)),s=-1,o;e*=i,t*=i,n*=i;if(n<0)while((o=e+n*++s)>t)r.push(o/i);else while((o=e+n*++s)<t)r.push(o/i);return r},d3.requote=function(e){return e.replace(es,"\\$&")};var es=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;d3.round=function(e,t){return t?Math.round(e*(t=Math.pow(10,t)))/t:Math.round(e)},d3.xhr=function(e,t,n){var r=new XMLHttpRequest;arguments.length<3?(n=t,t=null):t&&r.overrideMimeType&&r.overrideMimeType(t),r.open("GET",e,!0),t&&r.setRequestHeader("Accept",t),r.onreadystatechange=function(){if(r.readyState===4){var e=r.status;n(!e&&r.response||e>=200&&e<300||e===304?r:null)}},r.send(null)},d3.text=function(e,t,n){function r(e){n(e&&e.responseText)}arguments.length<3&&(n=t,t=null),d3.xhr(e,t,r)},d3.json=function(e,t){d3.text(e,"application/json",function(e){t(e?JSON.parse(e):null)})},d3.html=function(e,t){d3.text(e,"text/html",function(e){if(e!=null){var n=document.createRange();n.selectNode(document.body),e=n.createContextualFragment(e)}t(e)})},d3.xml=function(e,t,n){function r(e){n(e&&e.responseXML)}arguments.length<3&&(n=t,t=null),d3.xhr(e,t,r)};var ts={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};d3.ns={prefix:ts,qualify:function(e){var t=e.indexOf(":"),n=e;return t>=0&&(n=e.substring(0,t),e=e.substring(t+1)),ts.hasOwnProperty(n)?{space:ts[n],local:e}:e}},d3.dispatch=function(){var e=new d,t=-1,n=arguments.length;while(++t<n)e[arguments[t]]=v(e);return e},d.prototype.on=function(e,t){var n=e.indexOf("."),r="";return n>0&&(r=e.substring(n+1),e=e.substring(0,n)),arguments.length<2?this[e].on(r):this[e].on(r,t)},d3.format=function(e){var t=ns.exec(e),n=t[1]||" ",r=t[3]||"",i=t[5],s=+t[6],o=t[7],u=t[8],a=t[9],f=1,l="",c=!1;u&&(u=+u.substring(1)),i&&(n="0",o&&(s-=Math.floor((s-1)/4)));switch(a){case"n":o=!0,a="g";break;case"%":f=100,l="%",a="f";break;case"p":f=100,l="%",a="r";break;case"d":c=!0,u=0;break;case"s":f=-1,a="r"}return a=="r"&&!u&&(a="g"),a=rs.get(a)||g,function(e){if(c&&e%1)return"";var t=e<0&&(e=-e)?"-":r;if(f<0){var h=d3.formatPrefix(e,u);e=h.scale(e),l=h.symbol}else e*=f;e=a(e,u);if(i){var p=e.length+t.length;p<s&&(e=(new Array(s-p+1)).join(n)+e),o&&(e=y(e)),e=t+e}else{o&&(e=y(e)),e=t+e;var p=e.length;p<s&&(e=(new Array(s-p+1)).join(n)+e)}return e+l}};var ns=/(?:([^{])?([<>=^]))?([+\- ])?(#)?(0)?([0-9]+)?(,)?(\.[0-9]+)?([a-zA-Z%])?/,rs=d3.map({g:function(e,t){return e.toPrecision(t)},e:function(e,t){return e.toExponential(t)},f:function(e,t){return e.toFixed(t)},r:function(e,t){return d3.round(e,t=m(e,t)).toFixed(Math.max(0,Math.min(20,t)))}}),is=["y","z","a","f","p","n","μ","m","","k","M","G","T","P","E","Z","Y"].map(b);d3.formatPrefix=function(e,t){var n=0;return e&&(e<0&&(e*=-1),t&&(e=d3.round(e,m(e,t))),n=1+Math.floor(1e-12+Math.log(e)/Math.LN10),n=Math.max(-24,Math.min(24,Math.floor((n<=0?n+1:n-1)/3)*3))),is[8+n/3]};var ss=T(2),os=T(3),us=function(){return x},as=d3.map({linear:us,poly:T,quad:function(){return ss},cubic:function(){return os},sin:function(){return N},exp:function(){return C},circle:function(){return k},elastic:L,back:A,bounce:function(){return O}}),fs=d3.map({"in":x,out:E,"in-out":S,"out-in":function(e){return S(E(e))}});d3.ease=function(e){var t=e.indexOf("-"),n=t>=0?e.substring(0,t):e,r=t>=0?e.substring(t+1):"in";return n=as.get(n)||us,r=fs.get(r)||x,w(r(n.apply(null,Array.prototype.slice.call(arguments,1))))},d3.event=null,d3.transform=function(e){var t=document.createElementNS(d3.ns.prefix.svg,"g");return(d3.transform=function(e){t.setAttribute("transform",e);var n=t.transform.baseVal.consolidate();return new P(n?n.matrix:cs)})(e)},P.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var ls=180/Math.PI,cs={a:1,b:0,c:0,d:1,e:0,f:0};d3.interpolate=function(e,t){var n=d3.interpolators.length,r;while(--n>=0&&!(r=d3.interpolators[n](e,t)));return r},d3.interpolateNumber=function(e,t){return t-=e,function(n){return e+t*n}},d3.interpolateRound=function(e,t){return t-=e,function(n){return Math.round(e+t*n)}},d3.interpolateString=function(e,t){var n,r,i,s=0,o=0,u=[],a=[],f,l;hs.lastIndex=0;for(r=0;n=hs.exec(t);++r)n.index&&u.push(t.substring(s,o=n.index)),a.push({i:u.length,x:n[0]}),u.push(null),s=hs.lastIndex;s<t.length&&u.push(t.substring(s));for(r=0,f=a.length;(n=hs.exec(e))&&r<f;++r){l=a[r];if(l.x==n[0]){if(l.i)if(u[l.i+1]==null){u[l.i-1]+=l.x,u.splice(l.i,1);for(i=r+1;i<f;++i)a[i].i--}else{u[l.i-1]+=l.x+u[l.i+1],u.splice(l.i,2);for(i=r+1;i<f;++i)a[i].i-=2}else if(u[l.i+1]==null)u[l.i]=l.x;else{u[l.i]=l.x+u[l.i+1],u.splice(l.i+1,1);for(i=r+1;i<f;++i)a[i].i--}a.splice(r,1),f--,r--}else l.x=d3.interpolateNumber(parseFloat(n[0]),parseFloat(l.x))}while(r<f)l=a.pop(),u[l.i+1]==null?u[l.i]=l.x:(u[l.i]=l.x+u[l.i+1],u.splice(l.i+1,1)),f--;return u.length===1?u[0]==null?a[0].x:function(){return t}:function(e){for(r=0;r<f;++r)u[(l=a[r]).i]=l.x(e);return u.join("")}},d3.interpolateTransform=function(e,t){var n=[],r=[],i,s=d3.transform(e),o=d3.transform(t),u=s.translate,a=o.translate,f=s.rotate,l=o.rotate,c=s.skew,h=o.skew,p=s.scale,d=o.scale;return u[0]!=a[0]||u[1]!=a[1]?(n.push("translate(",null,",",null,")"),r.push({i:1,x:d3.interpolateNumber(u[0],a[0])},{i:3,x:d3.interpolateNumber(u[1],a[1])})):a[0]||a[1]?n.push("translate("+a+")"):n.push(""),f!=l?(f-l>180?l+=360:l-f>180&&(f+=360),r.push({i:n.push(n.pop()+"rotate(",null,")")-2,x:d3.interpolateNumber(f,l)})):l&&n.push(n.pop()+"rotate("+l+")"),c!=h?r.push({i:n.push(n.pop()+"skewX(",null,")")-2,x:d3.interpolateNumber(c,h)}):h&&n.push(n.pop()+"skewX("+h+")"),p[0]!=d[0]||p[1]!=d[1]?(i=n.push(n.pop()+"scale(",null,",",null,")"),r.push({i:i-4,x:d3.interpolateNumber(p[0],d[0])},{i:i-2,x:d3.interpolateNumber(p[1],d[1])})):(d[0]!=1||d[1]!=1)&&n.push(n.pop()+"scale("+d+")"),i=r.length,function(e){var t=-1,s;while(++t<i)n[(s=r[t]).i]=s.x(e);return n.join("")}},d3.interpolateRgb=function(e,t){e=d3.rgb(e),t=d3.rgb(t);var n=e.r,r=e.g,i=e.b,s=t.r-n,o=t.g-r,u=t.b-i;return function(e){return"#"+W(Math.round(n+s*e))+W(Math.round(r+o*e))+W(Math.round(i+u*e))}},d3.interpolateHsl=function(e,t){e=d3.hsl(e),t=d3.hsl(t);var n=e.h,r=e.s,i=e.l,s=t.h-n,o=t.s-r,u=t.l-i;return s>180?s-=360:s<-180&&(s+=360),function(e){return Y(n+s*e,r+o*e,i+u*e)+""}},d3.interpolateLab=function(e,t){e=d3.lab(e),t=d3.lab(t);var n=e.l,r=e.a,i=e.b,s=t.l-n,o=t.a-r,u=t.b-i;return function(e){return it(n+s*e,r+o*e,i+u*e)+""}},d3.interpolateHcl=function(e,t){e=d3.hcl(e),t=d3.hcl(t);var n=e.h,r=e.c,i=e.l,s=t.h-n,o=t.c-r,u=t.l-i;return s>180?s-=360:s<-180&&(s+=360),function(e){return tt(n+s*e,r+o*e,i+u*e)+""}},d3.interpolateArray=function(e,t){var n=[],r=[],i=e.length,s=t.length,o=Math.min(e.length,t.length),u;for(u=0;u<o;++u)n.push(d3.interpolate(e[u],t[u]));for(;u<i;++u)r[u]=e[u];for(;u<s;++u)r[u]=t[u];return function(e){for(u=0;u<o;++u)r[u]=n[u](e);return r}},d3.interpolateObject=function(e,t){var n={},r={},i;for(i in e)i in t?n[i]=F(i)(e[i],t[i]):r[i]=e[i];for(i in t)i in e||(r[i]=t[i]);return function(e){for(i in n)r[i]=n[i](e);return r}};var hs=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;d3.interpolators=[d3.interpolateObject,function(e,t){return t instanceof Array&&d3.interpolateArray(e,t)},function(e,t){return(typeof e=="string"||typeof t=="string")&&d3.interpolateString(e+"",t+"")},function(e,t){return(typeof t=="string"?ds.has(t)||/^(#|rgb\(|hsl\()/.test(t):t instanceof R)&&d3.interpolateRgb(e,t)},function(e,t){return!isNaN(e=+e)&&!isNaN(t=+t)&&d3.interpolateNumber(e,t)}],R.prototype.toString=function(){return this.rgb()+""},d3.rgb=function(e,t,n){return arguments.length===1?e instanceof z?U(e.r,e.g,e.b):X(""+e,U,Y):U(~~e,~~t,~~n)};var ps=z.prototype=new R;ps.brighter=function(e){e=Math.pow(.7,arguments.length?e:1);var t=this.r,n=this.g,r=this.b,i=30;return!t&&!n&&!r?U(i,i,i):(t&&t<i&&(t=i),n&&n<i&&(n=i),r&&r<i&&(r=i),U(Math.min(255,Math.floor(t/e)),Math.min(255,Math.floor(n/e)),Math.min(255,Math.floor(r/e))))},ps.darker=function(e){return e=Math.pow(.7,arguments.length?e:1),U(Math.floor(e*this.r),Math.floor(e*this.g),Math.floor(e*this.b))},ps.hsl=function(){return V(this.r,this.g,this.b)},ps.toString=function(){return"#"+W(this.r)+W(this.g)+W(this.b)};var ds=d3.map({aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"});ds.forEach(function(e,t){ds.set(e,X(t,U,Y))}),d3.hsl=function(e,t,n){return arguments.length===1?e instanceof G?Q(e.h,e.s,e.l):X(""+e,V,Q):Q(+e,+t,+n)};var vs=G.prototype=new R;vs.brighter=function(e){return e=Math.pow(.7,arguments.length?e:1),Q(this.h,this.s,this.l/e)},vs.darker=function(e){return e=Math.pow(.7,arguments.length?e:1),Q(this.h,this.s,e*this.l)},vs.rgb=function(){return Y(this.h,this.s,this.l)},d3.hcl=function(e,t,n){return arguments.length===1?e instanceof et?Z(e.h,e.c,e.l):e instanceof rt?st(e.l,e.a,e.b):st((e=$((e=d3.rgb(e)).r,e.g,e.b)).l,e.a,e.b):Z(+e,+t,+n)};var ms=et.prototype=new R;ms.brighter=function(e){return Z(this.h,this.c,Math.min(100,this.l+gs*(arguments.length?e:1)))},ms.darker=function(e){return Z(this.h,this.c,Math.max(0,this.l-gs*(arguments.length?e:1)))},ms.rgb=function(){return tt(this.h,this.c,this.l).rgb()},d3.lab=function(e,t,n){return arguments.length===1?e instanceof rt?nt(e.l,e.a,e.b):e instanceof et?tt(e.l,e.c,e.h):$((e=d3.rgb(e)).r,e.g,e.b):nt(+e,+t,+n)};var gs=18,ys=.95047,bs=1,ws=1.08883,Es=rt.prototype=new R;Es.brighter=function(e){return nt(Math.min(100,this.l+gs*(arguments.length?e:1)),this.a,this.b)},Es.darker=function(e){return nt(Math.max(0,this.l-gs*(arguments.length?e:1)),this.a,this.b)},Es.rgb=function(){return it(this.l,this.a,this.b)};var Ss=function(e,t){return t.querySelector(e)},xs=function(e,t){return t.querySelectorAll(e)},Ts=document.documentElement,Ns=Ts.matchesSelector||Ts.webkitMatchesSelector||Ts.mozMatchesSelector||Ts.msMatchesSelector||Ts.oMatchesSelector,Cs=function(e,t){return Ns.call(e,t)};typeof Sizzle=="function"&&(Ss=function(e,t){return Sizzle(e,t)[0]||null},xs=function(e,t){return Sizzle.uniqueSort(Sizzle(e,t))},Cs=Sizzle.matchesSelector);var ks=[];d3.selection=function(){return Ls},d3.selection.prototype=ks,ks.select=function(e){var t=[],n,r,i,s;typeof e!="function"&&(e=lt(e));for(var o=-1,u=this.length;++o<u;){t.push(n=[]),n.parentNode=(i=this[o]).parentNode;for(var a=-1,f=i.length;++a<f;)(s=i[a])?(n.push(r=e.call(s,s.__data__,a)),r&&"__data__"in s&&(r.__data__=s.__data__)):n.push(null)}return ft(t)},ks.selectAll=function(e){var t=[],n,r;typeof e!="function"&&(e=ct(e));for(var i=-1,s=this.length;++i<s;)for(var o=this[i],u=-1,a=o.length;++u<a;)if(r=o[u])t.push(n=Ji(e.call(r,r.__data__,u))),n.parentNode=r;return ft(t)},ks.attr=function(e,t){if(arguments.length<2){if(typeof e=="string"){var n=this.node();return e=d3.ns.qualify(e),e.local?n.getAttributeNS(e.space,e.local):n.getAttribute(e)}for(t in e)this.each(ht(t,e[t]));return this}return this.each(ht(e,t))},ks.classed=function(e,t){if(arguments.length<2){if(typeof e=="string"){var n=this.node(),r=(e=e.trim().split(/^|\s+/g)).length,i=-1;if(t=n.classList){while(++i<r)if(!t.contains(e[i]))return!1}else{t=n.className,t.baseVal!=null&&(t=t.baseVal);while(++i<r)if(!pt(e[i]).test(t))return!1}return!0}for(t in e)this.each(dt(t,e[t]));return this}return this.each(dt(e,t))},ks.style=function(e,t,n){var r=arguments.length;if(r<3){if(typeof e!="string"){r<2&&(t="");for(n in e)this.each(mt(n,e[n],t));return this}if(r<2)return window.getComputedStyle(this.node(),null).getPropertyValue(e);n=""}return this.each(mt(e,t,n))},ks.property=function(e,t){if(arguments.length<2){if(typeof e=="string")return this.node()[e];for(t in e)this.each(gt(t,e[t]));return this}return this.each(gt(e,t))},ks.text=function(e){return arguments.length<1?this.node().textContent:this.each(typeof e=="function"?function(){var t=e.apply(this,arguments);this.textContent=t==null?"":t}:e==null?function(){this.textContent=""}:function(){this.textContent=e})},ks.html=function(e){return arguments.length<1?this.node().innerHTML:this.each(typeof e=="function"?function(){var t=e.apply(this,arguments);this.innerHTML=t==null?"":t}:e==null?function(){this.innerHTML=""}:function(){this.innerHTML=e})},ks.append=function(e){function t(){return this.appendChild(document.createElementNS(this.namespaceURI,e))}function n(){return this.appendChild(document.createElementNS(e.space,e.local))}return e=d3.ns.qualify(e),this.select(e.local?n:t)},ks.insert=function(e,t){function n(){return this.insertBefore(document.createElementNS(this.namespaceURI,e),Ss(t,this))}function r(){return this.insertBefore(document.createElementNS(e.space,e.local),Ss(t,this))}return e=d3.ns.qualify(e),this.select(e.local?r:n)},ks.remove=function(){return this.each(function(){var e=this.parentNode;e&&e.removeChild(this)})},ks.data=function(e,t){function n(e,n){var i,s=e.length,o=n.length,u=Math.min(s,o),c=Math.max(s,o),h=[],p=[],d=[],v,m;if(t){var g=new r,y=[],b,w=n.length;for(i=-1;++i<s;)b=t.call(v=e[i],v.__data__,i),g.has(b)?d[w++]=v:g.set(b,v),y.push(b);for(i=-1;++i<o;)b=t.call(n,m=n[i],i),g.has(b)?(h[i]=v=g.get(b),v.__data__=m,p[i]=d[i]=null):(p[i]=yt(m),h[i]=d[i]=null),g.remove(b);for(i=-1;++i<s;)g.has(y[i])&&(d[i]=e[i])}else{for(i=-1;++i<u;)v=e[i],m=n[i],v?(v.__data__=m,h[i]=v,p[i]=d[i]=null):(p[i]=yt(m),h[i]=d[i]=null);for(;i<o;++i)p[i]=yt(n[i]),h[i]=d[i]=null;for(;i<c;++i)d[i]=e[i],p[i]=h[i]=null}p.update=h,p.parentNode=h.parentNode=d.parentNode=e.parentNode,a.push(p),f.push(h),l.push(d)}var i=-1,s=this.length,o,u;if(!arguments.length){e=new Array(s=(o=this[0]).length);while(++i<s)if(u=o[i])e[i]=u.__data__;return e}var a=xt([]),f=ft([]),l=ft([]);if(typeof e=="function")while(++i<s)n(o=this[i],e.call(o,o.parentNode.__data__,i));else while(++i<s)n(o=this[i],e);return f.enter=function(){return a},f.exit=function(){return l},f},ks.datum=ks.map=function(e){return arguments.length<1?this.property("__data__"):this.property("__data__",e)},ks.filter=function(e){var t=[],n,r,i;typeof e!="function"&&(e=bt(e));for(var s=0,o=this.length;s<o;s++){t.push(n=[]),n.parentNode=(r=this[s]).parentNode;for(var u=0,a=r.length;u<a;u++)(i=r[u])&&e.call(i,i.__data__,u)&&n.push(i)}return ft(t)},ks.order=function(){for(var e=-1,t=this.length;++e<t;)for(var n=this[e],r=n.length-1,i=n[r],s;--r>=0;)if(s=n[r])i&&i!==s.nextSibling&&i.parentNode.insertBefore(s,i),i=s;return this},ks.sort=function(e){e=wt.apply(this,arguments);for(var t=-1,n=this.length;++t<n;)this[t].sort(e);return this.order()},ks.on=function(e,t,n){var r=arguments.length;if(r<3){if(typeof e!="string"){r<2&&(t=!1);for(n in e)this.each(Et(n,e[n],t));return this}if(r<2)return(r=this.node()["__on"+e])&&r._;n=!1}return this.each(Et(e,t,n))},ks.each=function(e){return St(this,function(t,n,r){e.call(t,t.__data__,n,r)})},ks.call=function(e){return e.apply(this,(arguments[0]=this,arguments)),this},ks.empty=function(){return!this.node()},ks.node=function(e){for(var t=0,n=this.length;t<n;t++)for(var r=this[t],i=0,s=r.length;i<s;i++){var o=r[i];if(o)return o}return null},ks.transition=function(){var e=[],t,n;for(var r=-1,i=this.length;++r<i;){e.push(t=[]);for(var s=this[r],o=-1,u=s.length;++o<u;)t.push((n=s[o])?{node:n,delay:Bs,duration:js}:null)}return Tt(e,_s||++Ms,Date.now())};var Ls=ft([[document]]);Ls[0].parentNode=Ts,d3.select=function(e){return typeof e=="string"?Ls.select(e):ft([[e]])},d3.selectAll=function(e){return typeof e=="string"?Ls.selectAll(e):ft([Ji(e)])};var As=[];d3.selection.enter=xt,d3.selection.enter.prototype=As,As.append=ks.append,As.insert=ks.insert,As.empty=ks.empty,As.node=ks.node,As.select=function(e){var t=[],n,r,i,s,o;for(var u=-1,a=this.length;++u<a;){i=(s=this[u]).update,t.push(n=[]),n.parentNode=s.parentNode;for(var f=-1,l=s.length;++f<l;)(o=s[f])?(n.push(i[f]=r=e.call(s.parentNode,o.__data__,f)),r.__data__=o.__data__):n.push(null)}return ft(t)};var Os=[],Ms=0,_s=0,Ds=0,Ps=250,Hs=d3.ease("cubic-in-out"),Bs=Ds,js=Ps,Fs=Hs;Os.call=ks.call,d3.transition=function(e){return arguments.length?_s?e.transition():e:Ls.transition()},d3.transition.prototype=Os,Os.select=function(e){var t=[],n,r,i;typeof e!="function"&&(e=lt(e));for(var s=-1,o=this.length;++s<o;){t.push(n=[]);for(var u=this[s],a=-1,f=u.length;++a<f;)(i=u[a])&&(r=e.call(i.node,i.node.__data__,a))?("__data__"in i.node&&(r.__data__=i.node.__data__),n.push({node:r,delay:i.delay,duration:i.duration})):n.push(null)}return Tt(t,this.id,this.time).ease(this.ease())},Os.selectAll=function(e){var t=[],n,r,i;typeof e!="function"&&(e=ct(e));for(var s=-1,o=this.length;++s<o;)for(var u=this[s],a=-1,f=u.length;++a<f;)if(i=u[a]){r=e.call(i.node,i.node.__data__,a),t.push(n=[]);for(var l=-1,c=r.length;++l<c;)n.push({node:r[l],delay:i.delay,duration:i.duration})}return Tt(t,this.id,this.time).ease(this.ease())},Os.filter=function(e){var t=[],n,r,i;typeof e!="function"&&(e=bt(e));for(var s=0,o=this.length;s<o;s++){t.push(n=[]);for(var r=this[s],u=0,a=r.length;u<a;u++)(i=r[u])&&e.call(i.node,i.node.__data__,u)&&n.push(i)}return Tt(t,this.id,this.time).ease(this.ease())},Os.attr=function(e,t){if(arguments.length<2){for(t in e)this.attrTween(t,kt(e[t],t));return this}return this.attrTween(e,kt(t,e))},Os.attrTween=function(e,t){function n(e,n){var r=t.call(this,e,n,this.getAttribute(i));return r===Is?(this.removeAttribute(i),null):r&&function(e){this.setAttribute(i,r(e))}}function r(e,n){var r=t.call(this,e,n,this.getAttributeNS(i.space,i.local));return r===Is?(this.removeAttributeNS(i.space,i.local),null):r&&function(e){this.setAttributeNS(i.space,i.local,r(e))}}var i=d3.ns.qualify(e);return this.tween("attr."+e,i.local?r:n)},Os.style=function(e,t,n){var r=arguments.length;if(r<3){if(typeof e!="string"){r<2&&(t="");for(n in e)this.styleTween(n,kt(e[n],n),t);return this}n=""}return this.styleTween(e,kt(t,e),n)},Os.styleTween=function(e,t,n){return arguments.length<3&&(n=""),this.tween("style."+e,function(r,i){var s=t.call(this,r,i,window.getComputedStyle(this,null).getPropertyValue(e));return s===Is?(this.style.removeProperty(e),null):s&&function(t){this.style.setProperty(e,s(t),n)}})},Os.text=function(e){return this.tween("text",function(t,n){this.textContent=typeof e=="function"?e.call(this,t,n):e})},Os.remove=function(){return this.each("end.transition",function(){var e;!this.__transition__&&(e=this.parentNode)&&e.removeChild(this)})},Os.delay=function(e){return St(this,typeof e=="function"?function(t,n,r){t.delay=e.call(t=t.node,t.__data__,n,r)|0}:(e|=0,function(t){t.delay=e}))},Os.duration=function(e){return St(this,typeof e=="function"?function(t,n,r){t.duration=Math.max(1,e.call(t=t.node,t.__data__,n,r)|0)}:(e=Math.max(1,e|0),function(t){t.duration=e}))},Os.transition=function(){return this.select(s)},d3.tween=function(e,t){function n(n,r,i){var s=e.call(this,n,r);return s==null?i!=""&&Is:i!=s&&t(i,s+"")}function r(n,r,i){return i!=e&&t(i,e)}return typeof e=="function"?n:e==null?Ct:(e+="",r)};var Is={},qs=0,Rs={},Us=null,zs,Ws;d3.timer=function(e,t,n){if(arguments.length<3){if(arguments.length<2)t=0;else if(!isFinite(t))return;n=Date.now()}var r=Rs[e.id];r&&r.callback===e?(r.then=n,r.delay=t):Rs[e.id=++qs]=Us={callback:e,then:n,delay:t,next:Us},zs||(Ws=clearTimeout(Ws),zs=1,Xs(Lt))},d3.timer.flush=function(){var e,t=Date.now(),n=Us;while(n)e=t-n.then,n.delay||(n.flush=n.callback(e)),n=n.next;At()};var Xs=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){setTimeout
+(e,17)};d3.mouse=function(e){return Ot(e,_())};var Vs=/WebKit/.test(navigator.userAgent)?-1:0;d3.touches=function(e,t){return arguments.length<2&&(t=_().touches),t?Ji(t).map(function(t){var n=Ot(e,t);return n.identifier=t.identifier,n}):[]},d3.scale={},d3.scale.linear=function(){return Bt([0,1],[0,1],d3.interpolate,!1)},d3.scale.log=function(){return Wt(d3.scale.linear(),Xt)};var $s=d3.format(".0e");Xt.pow=function(e){return Math.pow(10,e)},Vt.pow=function(e){return-Math.pow(10,-e)},d3.scale.pow=function(){return $t(d3.scale.linear(),1)},d3.scale.sqrt=function(){return d3.scale.pow().exponent(.5)},d3.scale.ordinal=function(){return Kt([],{t:"range",a:[[]]})},d3.scale.category10=function(){return d3.scale.ordinal().range(Js)},d3.scale.category20=function(){return d3.scale.ordinal().range(Ks)},d3.scale.category20b=function(){return d3.scale.ordinal().range(Qs)},d3.scale.category20c=function(){return d3.scale.ordinal().range(Gs)};var Js=["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"],Ks=["#1f77b4","#aec7e8","#ff7f0e","#ffbb78","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5","#8c564b","#c49c94","#e377c2","#f7b6d2","#7f7f7f","#c7c7c7","#bcbd22","#dbdb8d","#17becf","#9edae5"],Qs=["#393b79","#5254a3","#6b6ecf","#9c9ede","#637939","#8ca252","#b5cf6b","#cedb9c","#8c6d31","#bd9e39","#e7ba52","#e7cb94","#843c39","#ad494a","#d6616b","#e7969c","#7b4173","#a55194","#ce6dbd","#de9ed6"],Gs=["#3182bd","#6baed6","#9ecae1","#c6dbef","#e6550d","#fd8d3c","#fdae6b","#fdd0a2","#31a354","#74c476","#a1d99b","#c7e9c0","#756bb1","#9e9ac8","#bcbddc","#dadaeb","#636363","#969696","#bdbdbd","#d9d9d9"];d3.scale.quantile=function(){return Qt([],[])},d3.scale.quantize=function(){return Gt(0,1,[0,1])},d3.scale.threshold=function(){return Yt([.5],[0,1])},d3.scale.identity=function(){return Zt([0,1])},d3.svg={},d3.svg.arc=function(){function e(){var e=t.apply(this,arguments),s=n.apply(this,arguments),o=r.apply(this,arguments)+Ys,u=i.apply(this,arguments)+Ys,a=(u<o&&(a=o,o=u,u=a),u-o),f=a<Math.PI?"0":"1",l=Math.cos(o),c=Math.sin(o),h=Math.cos(u),p=Math.sin(u);return a>=Zs?e?"M0,"+s+"A"+s+","+s+" 0 1,1 0,"+ -s+"A"+s+","+s+" 0 1,1 0,"+s+"M0,"+e+"A"+e+","+e+" 0 1,0 0,"+ -e+"A"+e+","+e+" 0 1,0 0,"+e+"Z":"M0,"+s+"A"+s+","+s+" 0 1,1 0,"+ -s+"A"+s+","+s+" 0 1,1 0,"+s+"Z":e?"M"+s*l+","+s*c+"A"+s+","+s+" 0 "+f+",1 "+s*h+","+s*p+"L"+e*h+","+e*p+"A"+e+","+e+" 0 "+f+",0 "+e*l+","+e*c+"Z":"M"+s*l+","+s*c+"A"+s+","+s+" 0 "+f+",1 "+s*h+","+s*p+"L0,0"+"Z"}var t=en,n=tn,r=nn,i=rn;return e.innerRadius=function(n){return arguments.length?(t=u(n),e):t},e.outerRadius=function(t){return arguments.length?(n=u(t),e):n},e.startAngle=function(t){return arguments.length?(r=u(t),e):r},e.endAngle=function(t){return arguments.length?(i=u(t),e):i},e.centroid=function(){var e=(t.apply(this,arguments)+n.apply(this,arguments))/2,s=(r.apply(this,arguments)+i.apply(this,arguments))/2+Ys;return[Math.cos(s)*e,Math.sin(s)*e]},e};var Ys=-Math.PI/2,Zs=2*Math.PI-1e-6;d3.svg.line=function(){return sn(i)};var eo=d3.map({linear:an,"linear-closed":fn,"step-before":ln,"step-after":cn,basis:gn,"basis-open":yn,"basis-closed":bn,bundle:wn,cardinal:dn,"cardinal-open":hn,"cardinal-closed":pn,monotone:Cn});eo.forEach(function(e,t){t.key=e,t.closed=/-closed$/.test(e)});var to=[0,2/3,1/3,0],no=[0,1/3,2/3,0],ro=[0,1/6,2/3,1/6];d3.svg.line.radial=function(){var e=sn(kn);return e.radius=e.x,delete e.x,e.angle=e.y,delete e.y,e},ln.reverse=cn,cn.reverse=ln,d3.svg.area=function(){return Ln(i)},d3.svg.area.radial=function(){var e=Ln(kn);return e.radius=e.x,delete e.x,e.innerRadius=e.x0,delete e.x0,e.outerRadius=e.x1,delete e.x1,e.angle=e.y,delete e.y,e.startAngle=e.y0,delete e.y0,e.endAngle=e.y1,delete e.y1,e},d3.svg.chord=function(){function e(e,u){var a=t(this,s,e,u),f=t(this,o,e,u);return"M"+a.p0+r(a.r,a.p1,a.a1-a.a0)+(n(a,f)?i(a.r,a.p1,a.r,a.p0):i(a.r,a.p1,f.r,f.p0)+r(f.r,f.p1,f.a1-f.a0)+i(f.r,f.p1,a.r,a.p0))+"Z"}function t(e,t,n,r){var i=t.call(e,n,r),s=a.call(e,i,r),o=f.call(e,i,r)+Ys,u=l.call(e,i,r)+Ys;return{r:s,a0:o,a1:u,p0:[s*Math.cos(o),s*Math.sin(o)],p1:[s*Math.cos(u),s*Math.sin(u)]}}function n(e,t){return e.a0==t.a0&&e.a1==t.a1}function r(e,t,n){return"A"+e+","+e+" 0 "+ +(n>Math.PI)+",1 "+t}function i(e,t,n,r){return"Q 0,0 "+r}var s=An,o=On,a=Mn,f=nn,l=rn;return e.radius=function(t){return arguments.length?(a=u(t),e):a},e.source=function(t){return arguments.length?(s=u(t),e):s},e.target=function(t){return arguments.length?(o=u(t),e):o},e.startAngle=function(t){return arguments.length?(f=u(t),e):f},e.endAngle=function(t){return arguments.length?(l=u(t),e):l},e},d3.svg.diagonal=function(){function e(e,i){var s=t.call(this,e,i),o=n.call(this,e,i),u=(s.y+o.y)/2,a=[s,{x:s.x,y:u},{x:o.x,y:u},o];return a=a.map(r),"M"+a[0]+"C"+a[1]+" "+a[2]+" "+a[3]}var t=An,n=On,r=Pn;return e.source=function(n){return arguments.length?(t=u(n),e):t},e.target=function(t){return arguments.length?(n=u(t),e):n},e.projection=function(t){return arguments.length?(r=t,e):r},e},d3.svg.diagonal.radial=function(){var e=d3.svg.diagonal(),t=Pn,n=e.projection;return e.projection=function(e){return arguments.length?n(Hn(t=e)):t},e},d3.svg.mouse=d3.mouse,d3.svg.touches=d3.touches,d3.svg.symbol=function(){function e(e,r){return(io.get(t.call(this,e,r))||Fn)(n.call(this,e,r))}var t=jn,n=Bn;return e.type=function(n){return arguments.length?(t=u(n),e):t},e.size=function(t){return arguments.length?(n=u(t),e):n},e};var io=d3.map({circle:Fn,cross:function(e){var t=Math.sqrt(e/5)/2;return"M"+ -3*t+","+ -t+"H"+ -t+"V"+ -3*t+"H"+t+"V"+ -t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+ -t+"V"+t+"H"+ -3*t+"Z"},diamond:function(e){var t=Math.sqrt(e/(2*oo)),n=t*oo;return"M0,"+ -t+"L"+n+",0"+" 0,"+t+" "+ -n+",0"+"Z"},square:function(e){var t=Math.sqrt(e)/2;return"M"+ -t+","+ -t+"L"+t+","+ -t+" "+t+","+t+" "+ -t+","+t+"Z"},"triangle-down":function(e){var t=Math.sqrt(e/so),n=t*so/2;return"M0,"+n+"L"+t+","+ -n+" "+ -t+","+ -n+"Z"},"triangle-up":function(e){var t=Math.sqrt(e/so),n=t*so/2;return"M0,"+ -n+"L"+t+","+n+" "+ -t+","+n+"Z"}});d3.svg.symbolTypes=io.keys();var so=Math.sqrt(3),oo=Math.tan(30*Math.PI/180);d3.svg.axis=function(){function e(e){e.each(function(){var e=d3.select(this),c=a==null?t.ticks?t.ticks.apply(t,u):t.domain():a,h=f==null?t.tickFormat?t.tickFormat.apply(t,u):String:f,p=Rn(t,c,l),d=e.selectAll(".minor").data(p,String),v=d.enter().insert("line","g").attr("class","tick minor").style("opacity",1e-6),m=d3.transition(d.exit()).style("opacity",1e-6).remove(),g=d3.transition(d).style("opacity",1),y=e.selectAll("g").data(c,String),b=y.enter().insert("g","path").style("opacity",1e-6),w=d3.transition(y.exit()).style("opacity",1e-6).remove(),E=d3.transition(y).style("opacity",1),S,x=Dt(t),T=e.selectAll(".domain").data([0]),N=T.enter().append("path").attr("class","domain"),C=d3.transition(T),k=t.copy(),L=this.__chart__||k;this.__chart__=k,b.append("line").attr("class","tick"),b.append("text");var A=b.select("line"),O=E.select("line"),M=y.select("text").text(h),_=b.select("text"),D=E.select("text");switch(n){case"bottom":S=In,v.attr("y2",i),g.attr("x2",0).attr("y2",i),A.attr("y2",r),_.attr("y",Math.max(r,0)+o),O.attr("x2",0).attr("y2",r),D.attr("x",0).attr("y",Math.max(r,0)+o),M.attr("dy",".71em").attr("text-anchor","middle"),C.attr("d","M"+x[0]+","+s+"V0H"+x[1]+"V"+s);break;case"top":S=In,v.attr("y2",-i),g.attr("x2",0).attr("y2",-i),A.attr("y2",-r),_.attr("y",-(Math.max(r,0)+o)),O.attr("x2",0).attr("y2",-r),D.attr("x",0).attr("y",-(Math.max(r,0)+o)),M.attr("dy","0em").attr("text-anchor","middle"),C.attr("d","M"+x[0]+","+ -s+"V0H"+x[1]+"V"+ -s);break;case"left":S=qn,v.attr("x2",-i),g.attr("x2",-i).attr("y2",0),A.attr("x2",-r),_.attr("x",-(Math.max(r,0)+o)),O.attr("x2",-r).attr("y2",0),D.attr("x",-(Math.max(r,0)+o)).attr("y",0),M.attr("dy",".32em").attr("text-anchor","end"),C.attr("d","M"+ -s+","+x[0]+"H0V"+x[1]+"H"+ -s);break;case"right":S=qn,v.attr("x2",i),g.attr("x2",i).attr("y2",0),A.attr("x2",r),_.attr("x",Math.max(r,0)+o),O.attr("x2",r).attr("y2",0),D.attr("x",Math.max(r,0)+o).attr("y",0),M.attr("dy",".32em").attr("text-anchor","start"),C.attr("d","M"+s+","+x[0]+"H0V"+x[1]+"H"+s)}if(t.ticks)b.call(S,L),E.call(S,k),w.call(S,k),v.call(S,L),g.call(S,k),m.call(S,k);else{var P=k.rangeBand()/2,H=function(e){return k(e)+P};b.call(S,H),E.call(S,H)}})}var t=d3.scale.linear(),n="bottom",r=6,i=6,s=6,o=3,u=[10],a=null,f,l=0;return e.scale=function(n){return arguments.length?(t=n,e):t},e.orient=function(t){return arguments.length?(n=t,e):n},e.ticks=function(){return arguments.length?(u=arguments,e):u},e.tickValues=function(t){return arguments.length?(a=t,e):a},e.tickFormat=function(t){return arguments.length?(f=t,e):f},e.tickSize=function(t,n,o){if(!arguments.length)return r;var u=arguments.length-1;return r=+t,i=u>1?+n:r,s=u>0?+arguments[u]:r,e},e.tickPadding=function(t){return arguments.length?(o=+t,e):o},e.tickSubdivide=function(t){return arguments.length?(l=+t,e):l},e},d3.svg.brush=function(){function e(s){s.each(function(){var s=d3.select(this),f=s.selectAll(".background").data([0]),l=s.selectAll(".extent").data([0]),c=s.selectAll(".resize").data(a,String),h;s.style("pointer-events","all").on("mousedown.brush",i).on("touchstart.brush",i),f.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),l.enter().append("rect").attr("class","extent").style("cursor","move"),c.enter().append("g").attr("class",function(e){return"resize "+e}).style("cursor",function(e){return uo[e]}).append("rect").attr("x",function(e){return/[ew]$/.test(e)?-3:null}).attr("y",function(e){return/^[ns]/.test(e)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),c.style("display",e.empty()?"none":null),c.exit().remove(),o&&(h=Dt(o),f.attr("x",h[0]).attr("width",h[1]-h[0]),n(s)),u&&(h=Dt(u),f.attr("y",h[0]).attr("height",h[1]-h[0]),r(s)),t(s)})}function t(e){e.selectAll(".resize").attr("transform",function(e){return"translate("+f[+/e$/.test(e)][0]+","+f[+/^s/.test(e)][1]+")"})}function n(e){e.select(".extent").attr("x",f[0][0]),e.selectAll(".extent,.n>rect,.s>rect").attr("width",f[1][0]-f[0][0])}function r(e){e.select(".extent").attr("y",f[0][1]),e.selectAll(".extent,.e>rect,.w>rect").attr("height",f[1][1]-f[0][1])}function i(){function i(){var e=d3.event.changedTouches;return e?d3.touches(v,e)[0]:d3.mouse(v)}function a(){d3.event.keyCode==32&&(S||(x=null,T[0]-=f[1][0],T[1]-=f[1][1],S=2),M())}function c(){d3.event.keyCode==32&&S==2&&(T[0]+=f[1][0],T[1]+=f[1][1],S=0,M())}function h(){var e=i(),s=!1;N&&(e[0]+=N[0],e[1]+=N[1]),S||(d3.event.altKey?(x||(x=[(f[0][0]+f[1][0])/2,(f[0][1]+f[1][1])/2]),T[0]=f[+(e[0]<x[0])][0],T[1]=f[+(e[1]<x[1])][1]):x=null),w&&p(e,o,0)&&(n(y),s=!0),E&&p(e,u,1)&&(r(y),s=!0),s&&(t(y),g({type:"brush",mode:S?"move":"resize"}))}function p(e,t,n){var r=Dt(t),i=r[0],s=r[1],o=T[n],u=f[1][n]-f[0][n],a,c;S&&(i-=o,s-=u+o),a=Math.max(i,Math.min(s,e[n])),S?c=(a+=o)+u:(x&&(o=Math.max(i,Math.min(s,2*x[n]-a))),o<a?(c=a,a=o):c=o);if(f[0][n]!==a||f[1][n]!==c)return l=null,f[0][n]=a,f[1][n]=c,!0}function d(){h(),y.style("pointer-events","all").selectAll(".resize").style("display",e.empty()?"none":null),d3.select("body").style("cursor",null),C.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),g({type:"brushend"}),M()}var v=this,m=d3.select(d3.event.target),g=s.of(v,arguments),y=d3.select(v),b=m.datum(),w=!/^(n|s)$/.test(b)&&o,E=!/^(e|w)$/.test(b)&&u,S=m.classed("extent"),x,T=i(),N,C=d3.select(window).on("mousemove.brush",h).on("mouseup.brush",d).on("touchmove.brush",h).on("touchend.brush",d).on("keydown.brush",a).on("keyup.brush",c);if(S)T[0]=f[0][0]-T[0],T[1]=f[0][1]-T[1];else if(b){var k=+/w$/.test(b),L=+/^n/.test(b);N=[f[1-k][0]-T[0],f[1-L][1]-T[1]],T[0]=f[k][0],T[1]=f[L][1]}else d3.event.altKey&&(x=T.slice());y.style("pointer-events","none").selectAll(".resize").style("display",null),d3.select("body").style("cursor",m.style("cursor")),g({type:"brushstart"}),h(),M()}var s=D(e,"brushstart","brush","brushend"),o=null,u=null,a=ao[0],f=[[0,0],[0,0]],l;return e.x=function(t){return arguments.length?(o=t,a=ao[!o<<1|!u],e):o},e.y=function(t){return arguments.length?(u=t,a=ao[!o<<1|!u],e):u},e.extent=function(t){var n,r,i,s,a;return arguments.length?(l=[[0,0],[0,0]],o&&(n=t[0],r=t[1],u&&(n=n[0],r=r[0]),l[0][0]=n,l[1][0]=r,o.invert&&(n=o(n),r=o(r)),r<n&&(a=n,n=r,r=a),f[0][0]=n|0,f[1][0]=r|0),u&&(i=t[0],s=t[1],o&&(i=i[1],s=s[1]),l[0][1]=i,l[1][1]=s,u.invert&&(i=u(i),s=u(s)),s<i&&(a=i,i=s,s=a),f[0][1]=i|0,f[1][1]=s|0),e):(t=l||f,o&&(n=t[0][0],r=t[1][0],l||(n=f[0][0],r=f[1][0],o.invert&&(n=o.invert(n),r=o.invert(r)),r<n&&(a=n,n=r,r=a))),u&&(i=t[0][1],s=t[1][1],l||(i=f[0][1],s=f[1][1],u.invert&&(i=u.invert(i),s=u.invert(s)),s<i&&(a=i,i=s,s=a))),o&&u?[[n,i],[r,s]]:o?[n,r]:u&&[i,s])},e.clear=function(){return l=null,f[0][0]=f[0][1]=f[1][0]=f[1][1]=0,e},e.empty=function(){return o&&f[0][0]===f[1][0]||u&&f[0][1]===f[1][1]},d3.rebind(e,s,"on")};var uo={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},ao=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]];d3.behavior={},d3.behavior.drag=function(){function e(){this.on("mousedown.drag",t).on("touchstart.drag",t)}function t(){function e(){var e=o.parentNode;return f?d3.touches(e).filter(function(e){return e.identifier===f})[0]:d3.mouse(e)}function t(){if(!o.parentNode)return i();var t=e(),n=t[0]-c[0],r=t[1]-c[1];h|=n|r,c=t,M(),u({type:"drag",x:t[0]+l[0],y:t[1]+l[1],dx:n,dy:r})}function i(){u({type:"dragend"}),h&&(M(),d3.event.target===a&&p.on("click.drag",s,!0)),p.on(f?"touchmove.drag-"+f:"mousemove.drag",null).on(f?"touchend.drag-"+f:"mouseup.drag",null)}function s(){M(),p.on("click.drag",null)}var o=this,u=n.of(o,arguments),a=d3.event.target,f=d3.event.touches&&d3.event.changedTouches[0].identifier,l,c=e(),h=0,p=d3.select(window).on(f?"touchmove.drag-"+f:"mousemove.drag",t).on(f?"touchend.drag-"+f:"mouseup.drag",i,!0);r?(l=r.apply(o,arguments),l=[l.x-c[0],l.y-c[1]]):l=[0,0],f||M(),u({type:"dragstart"})}var n=D(e,"drag","dragstart","dragend"),r=null;return e.origin=function(t){return arguments.length?(r=t,e):r},d3.rebind(e,n,"on")},d3.behavior.zoom=function(){function e(){this.on("mousedown.zoom",o).on("mousewheel.zoom",u).on("mousemove.zoom",a).on("DOMMouseScroll.zoom",u).on("dblclick.zoom",f).on("touchstart.zoom",l).on("touchmove.zoom",c).on("touchend.zoom",l)}function t(e){return[(e[0]-h[0])/d,(e[1]-h[1])/d]}function n(e){return[e[0]*d+h[0],e[1]*d+h[1]]}function r(e){d=Math.max(m[0],Math.min(m[1],e))}function i(e,t){t=n(t),h[0]+=e[0]-t[0],h[1]+=e[1]-t[1]}function s(e){b&&b.domain(y.range().map(function(e){return(e-h[0])/d}).map(y.invert)),E&&E.domain(w.range().map(function(e){return(e-h[1])/d}).map(w.invert)),d3.event.preventDefault(),e({type:"zoom",scale:d,translate:h})}function o(){function e(){f=1,i(d3.mouse(o),c),s(u)}function n(){f&&M(),l.on("mousemove.zoom",null).on("mouseup.zoom",null),f&&d3.event.target===a&&l.on("click.zoom",r,!0)}function r(){M(),l.on("click.zoom",null)}var o=this,u=g.of(o,arguments),a=d3.event.target,f=0,l=d3.select(window).on("mousemove.zoom",e).on("mouseup.zoom",n),c=t(d3.mouse(o));window.focus(),M()}function u(){p||(p=t(d3.mouse(this))),r(Math.pow(2,Un()*.002)*d),i(d3.mouse(this),p),s(g.of(this,arguments))}function a(){p=null}function f(){var e=d3.mouse(this),n=t(e);r(d3.event.shiftKey?d/2:d*2),i(e,n),s(g.of(this,arguments))}function l(){var e=d3.touches(this),n=Date.now();v=d,p={},e.forEach(function(e){p[e.identifier]=t(e)}),M();if(e.length===1){if(n-S<500){var o=e[0],u=t(e[0]);r(d*2),i(o,u),s(g.of(this,arguments))}S=n}}function c(){var e=d3.touches(this),t=e[0],n=p[t.identifier];if(o=e[1]){var o,u=p[o.identifier];t=[(t[0]+o[0])/2,(t[1]+o[1])/2],n=[(n[0]+u[0])/2,(n[1]+u[1])/2],r(d3.event.scale*v)}i(t,n),S=null,s(g.of(this,arguments))}var h=[0,0],p,d=1,v,m=lo,g=D(e,"zoom"),y,b,w,E,S;return e.translate=function(t){return arguments.length?(h=t.map(Number),e):h},e.scale=function(t){return arguments.length?(d=+t,e):d},e.scaleExtent=function(t){return arguments.length?(m=t==null?lo:t.map(Number),e):m},e.x=function(t){return arguments.length?(b=t,y=t.copy(),e):b},e.y=function(t){return arguments.length?(E=t,w=t.copy(),e):E},d3.rebind(e,g,"on")};var fo,lo=[0,Infinity];d3.layout={},d3.layout.bundle=function(){return function(e){var t=[],n=-1,r=e.length;while(++n<r)t.push(zn(e[n]));return t}},d3.layout.chord=function(){function e(){var e={},n=[],c=d3.range(o),h=[],p,d,v,m,g;r=[],i=[],p=0,m=-1;while(++m<o){d=0,g=-1;while(++g<o)d+=s[m][g];n.push(d),h.push(d3.range(o)),p+=d}a&&c.sort(function(e,t){return a(n[e],n[t])}),f&&h.forEach(function(e,t){e.sort(function(e,n){return f(s[t][e],s[t][n])})}),p=(2*Math.PI-u*o)/p,d=0,m=-1;while(++m<o){v=d,g=-1;while(++g<o){var y=c[m],b=h[y][g],w=s[y][b],E=d,S=d+=w*p;e[y+"-"+b]={index:y,subindex:b,startAngle:E,endAngle:S,value:w}}i[y]={index:y,startAngle:v,endAngle:d,value:(d-v)/p},d+=u}m=-1;while(++m<o){g=m-1;while(++g<o){var x=e[m+"-"+g],T=e[g+"-"+m];(x.value||T.value)&&r.push(x.value<T.value?{source:T,target:x}:{source:x,target:T})}}l&&t()}function t(){r.sort(function(e,t){return l((e.source.value+e.target.value)/2,(t.source.value+t.target.value)/2)})}var n={},r,i,s,o,u=0,a,f,l;return n.matrix=function(e){return arguments.length?(o=(s=e)&&s.length,r=i=null,n):s},n.padding=function(e){return arguments.length?(u=e,r=i=null,n):u},n.sortGroups=function(e){return arguments.length?(a=e,r=i=null,n):a},n.sortSubgroups=function(e){return arguments.length?(f=e,r=null,n):f},n.sortChords=function(e){return arguments.length?(l=e,r&&t(),n):l},n.chords=function(){return r||e(),r},n.groups=function(){return i||e(),i},n},d3.layout.force=function(){function e(e){return function(t,n,r,i,s){if(t.point!==e){var o=t.cx-e.x,u=t.cy-e.y,a=1/Math.sqrt(o*o+u*u);if((i-n)*a<d){var f=t.charge*a*a;return e.px-=o*f,e.py-=u*f,!0}if(t.point&&isFinite(a)){var f=t.pointCharge*a*a;e.px-=o*f,e.py-=u*f}}return!t.charge}}function t(e){e.px=d3.event.x,e.py=d3.event.y,n.resume()}var n={},r=d3.dispatch("start","tick","end"),s=[1,1],o,a,f=.9,l=Gn,c=Yn,h=-30,p=.1,d=.8,v,m=[],g=[],y,b,w;return n.tick=function(){if((a*=.99)<.005)return r.end({type:"end",alpha:a=0}),!0;var t=m.length,n=g.length,i,o,u,l,c,d,v,E,S;for(o=0;o<n;++o){u=g[o],l=u.source,c=u.target,E=c.x-l.x,S=c.y-l.y;if(d=E*E+S*S)d=a*b[o]*((d=Math.sqrt(d))-y[o])/d,E*=d,S*=d,c.x-=E*(v=l.weight/(c.weight+l.weight)),c.y-=S*v,l.x+=E*(v=1-v),l.y+=S*v}if(v=a*p){E=s[0]/2,S=s[1]/2,o=-1;if(v)while(++o<t)u=m[o],u.x+=(E-u.x)*v,u.y+=(S-u.y)*v}if(h){Qn(i=d3.geom.quadtree(m),a,w),o=-1;while(++o<t)(u=m[o]).fixed||i.visit(e(u))}o=-1;while(++o<t)u=m[o],u.fixed?(u.x=u.px,u.y=u.py):(u.x-=(u.px-(u.px=u.x))*f,u.y-=(u.py-(u.py=u.y))*f);r.tick({type:"tick",alpha:a})},n.nodes=function(e){return arguments.length?(m=e,n):m},n.links=function(e){return arguments.length?(g=e,n):g},n.size=function(e){return arguments.length?(s=e,n):s},n.linkDistance=function(e){return arguments.length?(l=u(e),n):l},n.distance=n.linkDistance,n.linkStrength=function(e){return arguments.length?(c=u(e),n):c},n.friction=function(e){return arguments.length?(f=e,n):f},n.charge=function(e){return arguments.length?(h=typeof e=="function"?e:+e,n):h},n.gravity=function(e){return arguments.length?(p=e,n):p},n.theta=function(e){return arguments.length?(d=e,n):d},n.alpha=function(e){return arguments.length?(a?e>0?a=e:a=0:e>0&&(r.start({type:"start",alpha:a=e}),d3.timer(n.tick)),n):a},n.start=function(){function e(e,n){var i=t(r),s=-1,o=i.length,u;while(++s<o)if(!isNaN(u=i[s][e]))return u;return Math.random()*n}function t(){if(!p){p=[];for(i=0;i<o;++i)p[i]=[];for(i=0;i<u;++i){var e=g[i];p[e.source.index].push(e.target),p[e.target.index].push(e.source)}}return p[r]}var r,i,o=m.length,u=g.length,a=s[0],f=s[1],p,d;for(r=0;r<o;++r)(d=m[r]).index=r,d.weight=0;y=[],b=[];for(r=0;r<u;++r)d=g[r],typeof d.source=="number"&&(d.source=m[d.source]),typeof d.target=="number"&&(d.target=m[d.target]),y[r]=l.call(this,d,r),b[r]=c.call(this,d,r),++d.source.weight,++d.target.weight;for(r=0;r<o;++r)d=m[r],isNaN(d.x)&&(d.x=e("x",a)),isNaN(d.y)&&(d.y=e("y",f)),isNaN(d.px)&&(d.px=d.x),isNaN(d.py)&&(d.py=d.y);w=[];if(typeof h=="function")for(r=0;r<o;++r)w[r]=+h.call(this,m[r],r);else for(r=0;r<o;++r)w[r]=h;return n.resume()},n.resume=function(){return n.alpha(.1)},n.stop=function(){return n.alpha(0)},n.drag=function(){o||(o=d3.behavior.drag().origin(i).on("dragstart",Vn).on("drag",t).on("dragend",$n)),this.on("mouseover.force",Jn).on("mouseout.force",Kn).call(o)},d3.rebind(n,r,"on")},d3.layout.partition=function(){function e(t,n,r,i){var s=t.children;t.x=n,t.y=t.depth*i,t.dx=r,t.dy=i;if(s&&(u=s.length)){var o=-1,u,a,f;r=t.value?r/t.value:0;while(++o<u)e(a=s[o],n,f=a.value*r,i),n+=f}}function t(e){var n=e.children,r=0;if(n&&(s=n.length)){var i=-1,s;while(++i<s)r=Math.max(r,t(n[i]))}return 1+r}function n(n,s){var o=r.call(this,n,s);return e(o[0],0,i[0],i[1]/t(o[0])),o}var r=d3.layout.hierarchy(),i=[1,1];return n.size=function(e){return arguments.length?(i=e,n):i},lr(n,r)},d3.layout.pie=function(){function e(s,o){var u=s.map(function(n,r){return+t.call(e,n,r)}),a=+(typeof r=="function"?r.apply(this,arguments):r),f=((typeof i=="function"?i.apply(this,arguments):i)-r)/d3.sum(u),l=d3.range(s.length);n!=null&&l.sort(n===co?function(e,t){return u[t]-u[e]}:function(e,t){return n(s[e],s[t])});var c=[];return l.forEach(function(e){var t;c[e]={data:s[e],value:t=u[e],startAngle:a,endAngle:a+=t*f}}),c}var t=Number,n=co,r=0,i=2*Math.PI;return e.value=function(n){return arguments.length?(t=n,e):t},e.sort=function(t){return arguments.length?(n=t,e):n},e.startAngle=function(t){return arguments.length?(r=t,e):r},e.endAngle=function(t){return arguments.length?(i=t,e):i},e};var co={};d3.layout.stack=function(){function e(i,a){var f=i.map(function(n,r){return t.call(e,n,r)}),l=f.map(function(t,n){return t.map(function(t,n){return[o.call(e,t,n),u.call(e,t,n)]})}),c=n.call(e,l,a);f=d3.permute(f,c),l=d3.permute(l,c);var h=r.call(e,l,a),p=f.length,d=f[0].length,v,m,g;for(m=0;m<d;++m){s.call(e,f[0][m],g=h[m],l[0][m][1]);for(v=1;v<p;++v)s.call(e,f[v][m],g+=l[v-1][m][1],l[v][m][1])}return i}var t=i,n=nr,r=rr,s=tr,o=Zn,u=er;return e.values=function(n){return arguments.length?(t=n,e):t},e.order=function(t){return arguments.length?(n=typeof t=="function"?t:ho.get(t)||nr,e):n},e.offset=function(t){return arguments.length?(r=typeof t=="function"?t:po.get(t)||rr,e):r},e.x=function(t){return arguments.length?(o=t,e):o},e.y=function(t){return arguments.length?(u=t,e):u},e.out=function(t){return arguments.length?(s=t,e):s},e};var ho=d3.map({"inside-out":function(e){var t=e.length,n,r,i=e.map(ir),s=e.map(sr),o=d3.range(t).sort(function(e,t){return i[e]-i[t]}),u=0,a=0,f=[],l=[];for(n=0;n<t;++n)r=o[n],u<a?(u+=s[r],f.push(r)):(a+=s[r],l.push(r));return l.reverse().concat(f)},reverse:function(e){return d3.range(e.length).reverse()},"default":nr}),po=d3.map({silhouette:function(e){var t=e.length,n=e[0].length,r=[],i=0,s,o,u,a=[];for(o=0;o<n;++o){for(s=0,u=0;s<t;s++)u+=e[s][o][1];u>i&&(i=u),r.push(u)}for(o=0;o<n;++o)a[o]=(i-r[o])/2;return a},wiggle:function(e){var t=e.length,n=e[0],r=n.length,i=0,s,o,u,a,f,l,c,h,p,d=[];d[0]=h=p=0;for(o=1;o<r;++o){for(s=0,a=0;s<t;++s)a+=e[s][o][1];for(s=0,f=0,c=n[o][0]-n[o-1][0];s<t;++s){for(u=0,l=(e[s][o][1]-e[s][o-1][1])/(2*c);u<s;++u)l+=(e[u][o][1]-e[u][o-1][1])/c;f+=l*e[s][o][1]}d[o]=h-=a?f/a*c:0,h<p&&(p=h)}for(o=0;o<r;++o)d[o]-=p;return d},expand:function(e){var t=e.length,n=e[0].length,r=1/t,i,s,o,u=[];for(s=0;s<n;++s){for(i=0,o=0;i<t;i++)o+=e[i][s][1];if(o)for(i=0;i<t;i++)e[i][s][1]/=o;else for(i=0;i<t;i++)e[i][s][1]=r}for(s=0;s<n;++s)u[s]=0;return u},zero:rr});d3.layout.histogram=function(){function e(e,s){var o=[],u=e.map(n,this),a=r.call(this,u,s),f=i.call(this,a,u,s),l,s=-1,c=u.length,h=f.length-1,p=t?1:1/c,d;while(++s<h)l=o[s]=[],l.dx=f[s+1]-(l.x=f[s]),l.y=0;if(h>0){s=-1;while(++s<c)d=u[s],d>=a[0]&&d<=a[1]&&(l=o[d3.bisect(f,d,1,h)-1],l.y+=p,l.push(e[s]))}return o}var t=!0,n=Number,r=fr,i=ur;return e.value=function(t){return arguments.length?(n=t,e):n},e.range=function(t){return arguments.length?(r=u(t),e):r},e.bins=function(t){return arguments.length?(i=typeof t=="number"?function(e){return ar(e,t)}:u(t),e):i},e.frequency=function(n){return arguments.length?(t=!!n,e):t},e},d3.layout.hierarchy=function(){function e(t,o,u){var a=i.call(n,t,o),f=vo?t:{data:t};f.depth=o,u.push(f);if(a&&(c=a.length)){var l=-1,c,h=f.children=[],p=0,d=o+1,v;while(++l<c)v=e(a[l],d,u),v.parent=f,h.push(v),p+=v.value;r&&h.sort(r),s&&(f.value=p)}else s&&(f.value=+s.call(n,t,o)||0);return f}function t(e,r){var i=e.children,o=0;if(i&&(a=i.length)){var u=-1,a,f=r+1;while(++u<a)o+=t(i[u],f)}else s&&(o=+s.call(n,vo?e:e.data,r)||0);return s&&(e.value=o),o}function n(t){var n=[];return e(t,0,n),n}var r=pr,i=cr,s=hr;return n.sort=function(e){return arguments.length?(r=e,n):r},n.children=function(e){return arguments.length?(i=e,n):i},n.value=function(e){return arguments.length?(s=e,n):s},n.revalue=function(e){return t(e,0),e},n};var vo=!1;d3.layout.pack=function(){function e(e,i){var s=t.call(this,e,i),o=s[0];o.x=0,o.y=0,Hr(o,function(e){e.r=Math.sqrt(e.value)}),Hr(o,br);var u=r[0],a=r[1],f=Math.max(2*o.r/u,2*o.r/a);if(n>0){var l=n*f/2;Hr(o,function(e){e.r+=l}),Hr(o,br),Hr(o,function(e){e.r-=l}),f=Math.max(2*o.r/u,2*o.r/a)}return Sr(o,u/2,a/2,1/f),s}var t=d3.layout.hierarchy().sort(vr),n=0,r=[1,1];return e.size=function(t){return arguments.length?(r=t,e):r},e.padding=function(t){return arguments.length?(n=+t,e):n},lr(e,t)},d3.layout.cluster=function(){function e(e,i){var s=t.call(this,e,i),o=s[0],u,a=0,f,l;Hr(o,function(e){var t=e.children;t&&t.length?(e.x=Nr(t),e.y=Tr(t)):(e.x=u?a+=n(e,u):0,e.y=0,u=e)});var c=Cr(o),h=kr(o),p=c.x-n(c,h)/2,d=h.x+n(h,c)/2;return Hr(o,function(e){e.x=(e.x-p)/(d-p)*r[0],e.y=(1-(o.y?e.y/o.y:1))*r[1]}),s}var t=d3.layout.hierarchy().sort(null).value(null),n=Lr,r=[1,1];return e.separation=function(t){return arguments.length?(n=t,e):n},e.size=function(t){return arguments.length?(r=t,e):r},lr(e,t)},d3.layout.tree=function(){function e(e,i){function s(e,t){var r=e.children,i=e._tree;if(r&&(o=r.length)){var o,a=r[0],f,l=a,c,h=-1;while(++h<o)c=r[h],s(c,f),l=u(c,f,l),f=c;Br(e);var p=.5*(a._tree.prelim+c._tree.prelim);t?(i.prelim=t._tree.prelim+n(e,t),i.mod=i.prelim-p):i.prelim=p}else t&&(i.prelim=t._tree.prelim+n(e,t))}function o(e,t){e.x=e._tree.prelim+t;var n=e.children;if(n&&(i=n.length)){var r=-1,i;t+=e._tree.mod;while(++r<i)o(n[r],t)}}function u(e,t,r){if(t){var i=e,s=e,o=t,u=e.parent.children[0],a=i._tree.mod,f=s._tree.mod,l=o._tree.mod,c=u._tree.mod,h;while(o=Or(o),i=Ar(i),o&&i)u=Ar(u),s=Or(s),s._tree.ancestor=e,h=o._tree.prelim+l-i._tree.prelim-a+n(o,i),h>0&&(jr(Fr(o,e,r),e,h),a+=h,f+=h),l+=o._tree.mod,a+=i._tree.mod,c+=u._tree.mod,f+=s._tree.mod;o&&!Or(s)&&(s._tree.thread=o,s._tree.mod+=l-f),i&&!Ar(u)&&(u._tree.thread=i,u._tree.mod+=a-c,r=e)}return r}var a=t.call(this,e,i),f=a[0];Hr(f,function(e,t){e._tree={ancestor:e,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),s(f),o(f,-f._tree.prelim);var l=Mr(f,Dr),c=Mr(f,_r),h=Mr(f,Pr),p=l.x-n(l,c)/2,d=c.x+n(c,l)/2,v=h.depth||1;return Hr(f,function(e){e.x=(e.x-p)/(d-p)*r[0],e.y=e.depth/v*r[1],delete e._tree}),a}var t=d3.layout.hierarchy().sort(null).value(null),n=Lr,r=[1,1];return e.separation=function(t){return arguments.length?(n=t,e):n},e.size=function(t){return arguments.length?(r=t,e):r},lr(e,t)},d3.layout.treemap=function(){function e(e,t){var n=-1,r=e.length,i,s;while(++n<r)s=(i=e[n]).value*(t<0?0:t),i.area=isNaN(s)||s<=0?0:s}function t(n){var s=n.children;if(s&&s.length){var o=l(n),u=[],a=s.slice(),f,c=Infinity,h,p=Math.min(o.dx,o.dy),d;e(a,o.dx*o.dy/n.value),u.area=0;while((d=a.length)>0)u.push(f=a[d-1]),u.area+=f.area,(h=r(u,p))<=c?(a.pop(),c=h):(u.area-=u.pop().area,i(u,p,o,!1),p=Math.min(o.dx,o.dy),u.length=u.area=0,c=Infinity);u.length&&(i(u,p,o,!0),u.length=u.area=0),s.forEach(t)}}function n(t){var r=t.children;if(r&&r.length){var s=l(t),o=r.slice(),u,a=[];e(o,s.dx*s.dy/t.value),a.area=0;while(u=o.pop())a.push(u),a.area+=u.area,u.z!=null&&(i(a,u.z?s.dx:s.dy,s,!o.length),a.length=a.area=0);r.forEach(n)}}function r(e,t){var n=e.area,r,i=0,s=Infinity,o=-1,u=e.length;while(++o<u){if(!(r=e[o].area))continue;r<s&&(s=r),r>i&&(i=r)}return n*=n,t*=t,n?Math.max(t*i*p/n,n/(t*s*p)):Infinity}function i(e,t,n,r){var i=-1,s=e.length,o=n.x,a=n.y,f=t?u(e.area/t):0,l;if(t==n.dx){if(r||f>n.dy)f=n.dy;while(++i<s)l=e[i],l.x=o,l.y=a,l.dy=f,o+=l.dx=Math.min(n.x+n.dx-o,f?u(l.area/f):0);l.z=!0,l.dx+=n.x+n.dx-o,n.y+=f,n.dy-=f}else{if(r||f>n.dx)f=n.dx;while(++i<s)l=e[i],l.x=o,l.y=a,l.dx=f,a+=l.dy=Math.min(n.y+n.dy-a,f?u(l.area/f):0);l.z=!1,l.dy+=n.y+n.dy-a,n.x+=f,n.dx-=f}}function s(r){var i=h||o(r),s=i[0];return s.x=0,s.y=0,s.dx=a[0],s.dy=a[1],h&&o.revalue(s),e([s],s.dx*s.dy/s.value),(h?n:t)(s),c&&(h=i),i}var o=d3.layout.hierarchy(),u=Math.round,a=[1,1],f=null,l=Ir,c=!1,h,p=.5*(1+Math.sqrt(5));return s.size=function(e){return arguments.length?(a=e,s):a},s.padding=function(e){function t(t){var n=e.call(s,t,t.depth);return n==null?Ir(t):qr(t,typeof n=="number"?[n,n,n,n]:n)}function n(t){return qr(t,e)}if(!arguments.length)return f;var r;return l=(f=e)==null?Ir:(r=typeof e)==="function"?t:r==="number"?(e=[e,e,e,e],n):n,s},s.round=function(e){return arguments.length?(u=e?Math.round:Number,s):u!=Number},s.sticky=function(e){return arguments.length?(c=e,h=null,s):c},s.ratio=function(e){return arguments.length?(p=e,s):p},lr(s,o)},d3.csv=Rr(",","text/csv"),d3.tsv=Rr("	","text/tab-separated-values"),d3.geo={};var mo=Math.PI/180;d3.geo.azimuthal=function(){function e(e){var n=e[0]*mo-s,o=e[1]*mo,f=Math.cos(n),l=Math.sin(n),c=Math.cos(o),h=Math.sin(o),p=t!=="orthographic"?a*h+u*c*f:null,d,v=t==="stereographic"?1/(1+p):t==="gnomonic"?1/p:t==="equidistant"?(d=Math.acos(p),d?d/Math.sin(d):0):t==="equalarea"?Math.sqrt(2/(1+p)):1,m=v*c*l,g=v*(a*c*f-u*h);return[r*m+i[0],r*g+i[1]]}var t="orthographic",n,r=200,i=[480,250],s,o,u,a;return e.invert=function(e){var n=(e[0]-i[0])/r,o=(e[1]-i[1])/r,f=Math.sqrt(n*n+o*o),l=t==="stereographic"?2*Math.atan(f):t==="gnomonic"?Math.atan(f):t==="equidistant"?f:t==="equalarea"?2*Math.asin(.5*f):Math.asin(f),c=Math.sin(l),h=Math.cos(l);return[(s+Math.atan2(n*c,f*u*h+o*a*c))/mo,Math.asin(h*a-(f?o*c*u/f:0))/mo]},e.mode=function(n){return arguments.length?(t=n+"",e):t},e.origin=function(t){return arguments.length?(n=t,s=n[0]*mo,o=n[1]*mo,u=Math.cos(o),a=Math.sin(o),e):n},e.scale=function(t){return arguments.length?(r=+t,e):r},e.translate=function(t){return arguments.length?(i=[+t[0],+t[1]],e):i},e.origin([0,0])},d3.geo.albers=function(){function e(e){var t=u*(mo*e[0]-o),n=Math.sqrt(a-2*u*Math.sin(mo*e[1]))/u;return[i*n*Math.sin(t)+s[0],i*(n*Math.cos(t)-f)+s[1]]}function t(){var t=mo*r[0],i=mo*r[1],s=mo*n[1],l=Math.sin(t),c=Math.cos(t);return o=mo*n[0],u=.5*(l+Math.sin(i)),a=c*c+2*u*l,f=Math.sqrt(a-2*u*Math.sin(s))/u,e}var n=[-98,38],r=[29.5,45.5],i=1e3,s=[480,250],o,u,a,f;return e.invert=function(e){var t=(e[0]-s[0])/i,n=(e[1]-s[1])/i,r=f+n,l=Math.atan2(t,r),c=Math.sqrt(t*t+r*r);return[(o+l/u)/mo,Math.asin((a-c*c*u*u)/(2*u))/mo]},e.origin=function(e){return arguments.length?(n=[+e[0],+e[1]],t()):n},e.parallels=function(e){return arguments.length?(r=[+e[0],+e[1]],t()):r},e.scale=function(t){return arguments.length?(i=+t,e):i},e.translate=function(t){return arguments.length?(s=[+t[0],+t[1]],e):s},t()},d3.geo.albersUsa=function(){function e(e){var s=e[0],o=e[1];return(o>50?n:s<-140?r:o<21?i:t)(e)}var t=d3.geo.albers(),n=d3.geo.albers().origin([-160,60]).parallels([55,65]),r=d3.geo.albers().origin([-160,20]).parallels([8,18]),i=d3.geo.albers().origin([-60,10]).parallels([8,18]);return e.scale=function(s){return arguments.length?(t.scale(s),n.scale(s*.6),r.scale(s),i.scale(s*1.5),e.translate(t.translate())):t.scale()},e.translate=function(s){if(!arguments.length)return t.translate();var o=t.scale()/1e3,u=s[0],a=s[1];return t.translate(s),n.translate([u-400*o,a+170*o]),r.translate([u-190*o,a+200*o]),i.translate([u+580*o,a+430*o]),e},e.scale(t.scale())},d3.geo.bonne=function(){function e(e){var u=e[0]*mo-r,a=e[1]*mo-i;if(s){var f=o+s-a,l=u*Math.cos(a)/f;u=f*Math.sin(l),a=f*Math.cos(l)-o}else u*=Math.cos(a),a*=-1;return[t*u+n[0],t*a+n[1]]}var t=200,n=[480,250],r,i,s,o;return e.invert=function(e){var i=(e[0]-n[0])/t,u=(e[1]-n[1])/t;if(s){var a=o+u,f=Math.sqrt(i*i+a*a);u=o+s-f,i=r+f*Math.atan2(i,a)/Math.cos(u)}else u*=-1,i/=Math.cos(u);return[i/mo,u/mo]},e.parallel=function(t){return arguments.length?(o=1/Math.tan(s=t*mo),e):s/mo},e.origin=function(t){return arguments.length?(r=t[0]*mo,i=t[1]*mo,e):[r/mo,i/mo]},e.scale=function(
+n){return arguments.length?(t=+n,e):t},e.translate=function(t){return arguments.length?(n=[+t[0],+t[1]],e):n},e.origin([0,0]).parallel(45)},d3.geo.equirectangular=function(){function e(e){var r=e[0]/360,i=-e[1]/360;return[t*r+n[0],t*i+n[1]]}var t=500,n=[480,250];return e.invert=function(e){var r=(e[0]-n[0])/t,i=(e[1]-n[1])/t;return[360*r,-360*i]},e.scale=function(n){return arguments.length?(t=+n,e):t},e.translate=function(t){return arguments.length?(n=[+t[0],+t[1]],e):n},e},d3.geo.mercator=function(){function e(e){var r=e[0]/360,i=-(Math.log(Math.tan(Math.PI/4+e[1]*mo/2))/mo)/360;return[t*r+n[0],t*Math.max(-0.5,Math.min(.5,i))+n[1]]}var t=500,n=[480,250];return e.invert=function(e){var r=(e[0]-n[0])/t,i=(e[1]-n[1])/t;return[360*r,2*Math.atan(Math.exp(-360*i*mo))/mo-90]},e.scale=function(n){return arguments.length?(t=+n,e):t},e.translate=function(t){return arguments.length?(n=[+t[0],+t[1]],e):n},e},d3.geo.path=function(){function e(e,t){typeof s=="function"&&(o=zr(s.apply(this,arguments))),f(e);var n=a.length?a.join(""):null;return a=[],n}function t(e){return u(e).join(",")}function n(e){var t=i(e[0]),n=0,r=e.length;while(++n<r)t-=i(e[n]);return t}function r(e){var t=d3.geom.polygon(e[0].map(u)),n=t.area(),r=t.centroid(n<0?(n*=-1,1):-1),i=r[0],s=r[1],o=n,a=0,f=e.length;while(++a<f)t=d3.geom.polygon(e[a].map(u)),n=t.area(),r=t.centroid(n<0?(n*=-1,1):-1),i-=r[0],s-=r[1],o-=n;return[i,s,6*o]}function i(e){return Math.abs(d3.geom.polygon(e.map(u)).area())}var s=4.5,o=zr(s),u=d3.geo.albersUsa(),a=[],f=Ur({FeatureCollection:function(e){var t=e.features,n=-1,r=t.length;while(++n<r)a.push(f(t[n].geometry))},Feature:function(e){f(e.geometry)},Point:function(e){a.push("M",t(e.coordinates),o)},MultiPoint:function(e){var n=e.coordinates,r=-1,i=n.length;while(++r<i)a.push("M",t(n[r]),o)},LineString:function(e){var n=e.coordinates,r=-1,i=n.length;a.push("M");while(++r<i)a.push(t(n[r]),"L");a.pop()},MultiLineString:function(e){var n=e.coordinates,r=-1,i=n.length,s,o,u;while(++r<i){s=n[r],o=-1,u=s.length,a.push("M");while(++o<u)a.push(t(s[o]),"L");a.pop()}},Polygon:function(e){var n=e.coordinates,r=-1,i=n.length,s,o,u;while(++r<i){s=n[r],o=-1;if((u=s.length-1)>0){a.push("M");while(++o<u)a.push(t(s[o]),"L");a[a.length-1]="Z"}}},MultiPolygon:function(e){var n=e.coordinates,r=-1,i=n.length,s,o,u,f,l,c;while(++r<i){s=n[r],o=-1,u=s.length;while(++o<u){f=s[o],l=-1;if((c=f.length-1)>0){a.push("M");while(++l<c)a.push(t(f[l]),"L");a[a.length-1]="Z"}}}},GeometryCollection:function(e){var t=e.geometries,n=-1,r=t.length;while(++n<r)a.push(f(t[n]))}}),l=e.area=Ur({FeatureCollection:function(e){var t=0,n=e.features,r=-1,i=n.length;while(++r<i)t+=l(n[r]);return t},Feature:function(e){return l(e.geometry)},Polygon:function(e){return n(e.coordinates)},MultiPolygon:function(e){var t=0,r=e.coordinates,i=-1,s=r.length;while(++i<s)t+=n(r[i]);return t},GeometryCollection:function(e){var t=0,n=e.geometries,r=-1,i=n.length;while(++r<i)t+=l(n[r]);return t}},0),c=e.centroid=Ur({Feature:function(e){return c(e.geometry)},Polygon:function(e){var t=r(e.coordinates);return[t[0]/t[2],t[1]/t[2]]},MultiPolygon:function(e){var t=0,n=e.coordinates,i,s=0,o=0,u=0,a=-1,f=n.length;while(++a<f)i=r(n[a]),s+=i[0],o+=i[1],u+=i[2];return[s/u,o/u]}});return e.projection=function(t){return u=t,e},e.pointRadius=function(t){return typeof t=="function"?s=t:(s=+t,o=zr(s)),e},e},d3.geo.bounds=function(e){var t=Infinity,n=Infinity,r=-Infinity,i=-Infinity;return Wr(e,function(e,s){e<t&&(t=e),e>r&&(r=e),s<n&&(n=s),s>i&&(i=s)}),[[t,n],[r,i]]};var go={Feature:Xr,FeatureCollection:Vr,GeometryCollection:$r,LineString:Jr,MultiLineString:Kr,MultiPoint:Jr,MultiPolygon:Qr,Point:Gr,Polygon:Yr};d3.geo.circle=function(){function e(){}function t(e){return a.distance(e)<u}function n(e){var t=-1,n=e.length,i=[],s,o,f,l,c;while(++t<n)c=a.distance(f=e[t]),c<u?(o&&i.push(ni(o,f)((l-u)/(l-c))),i.push(f),s=o=null):(o=f,!s&&i.length&&(i.push(ni(i[i.length-1],o)((u-l)/(c-l))),s=o)),l=c;return s=e[0],o=i[0],o&&f[0]===s[0]&&f[1]===s[1]&&(f[0]!==o[0]||f[1]!==o[1])&&i.push(o),r(i)}function r(e){var t=0,n=e.length,r,i,s=n?[e[0]]:e,o,u=a.source();while(++t<n){o=a.source(e[t-1])(e[t]).coordinates;for(r=0,i=o.length;++r<i;)s.push(o[r])}return a.source(u),s}var s=[0,0],o=89.99,u=o*mo,a=d3.geo.greatArc().source(s).target(i);e.clip=function(e){return typeof s=="function"&&a.source(s.apply(this,arguments)),f(e)||null};var f=Ur({FeatureCollection:function(e){var t=e.features.map(f).filter(i);return t&&(e=Object.create(e),e.features=t,e)},Feature:function(e){var t=f(e.geometry);return t&&(e=Object.create(e),e.geometry=t,e)},Point:function(e){return t(e.coordinates)&&e},MultiPoint:function(e){var n=e.coordinates.filter(t);return n.length&&{type:e.type,coordinates:n}},LineString:function(e){var t=n(e.coordinates);return t.length&&(e=Object.create(e),e.coordinates=t,e)},MultiLineString:function(e){var t=e.coordinates.map(n).filter(function(e){return e.length});return t.length&&(e=Object.create(e),e.coordinates=t,e)},Polygon:function(e){var t=e.coordinates.map(n);return t[0].length&&(e=Object.create(e),e.coordinates=t,e)},MultiPolygon:function(e){var t=e.coordinates.map(function(e){return e.map(n)}).filter(function(e){return e[0].length});return t.length&&(e=Object.create(e),e.coordinates=t,e)},GeometryCollection:function(e){var t=e.geometries.map(f).filter(i);return t.length&&(e=Object.create(e),e.geometries=t,e)}});return e.origin=function(t){return arguments.length?(s=t,typeof s!="function"&&a.source(s),e):s},e.angle=function(t){return arguments.length?(u=(o=+t)*mo,e):o},d3.rebind(e,a,"precision")},d3.geo.greatArc=function(){function e(){var t=e.distance.apply(this,arguments),r=0,u=s/t,a=[n];while((r+=u)<1)a.push(o(r));return a.push(i),{type:"LineString",coordinates:a}}var t=Zr,n,r=ei,i,s=6*mo,o=ti();return e.distance=function(){return typeof t=="function"&&o.source(n=t.apply(this,arguments)),typeof r=="function"&&o.target(i=r.apply(this,arguments)),o.distance()},e.source=function(r){return arguments.length?(t=r,typeof t!="function"&&o.source(n=t),e):t},e.target=function(t){return arguments.length?(r=t,typeof r!="function"&&o.target(i=r),e):r},e.precision=function(t){return arguments.length?(s=t*mo,e):s/mo},e},d3.geo.greatCircle=d3.geo.circle,d3.geom={},d3.geom.contour=function(e,t){var n=t||ri(e),r=[],i=n[0],s=n[1],o=0,u=0,a=NaN,f=NaN,l=0;do l=0,e(i-1,s-1)&&(l+=1),e(i,s-1)&&(l+=2),e(i-1,s)&&(l+=4),e(i,s)&&(l+=8),l===6?(o=f===-1?-1:1,u=0):l===9?(o=0,u=a===1?-1:1):(o=yo[l],u=bo[l]),o!=a&&u!=f&&(r.push([i,s]),a=o,f=u),i+=o,s+=u;while(n[0]!=i||n[1]!=s);return r};var yo=[1,0,1,1,-1,0,-1,1,0,0,0,0,-1,0,-1,NaN],bo=[0,-1,0,0,0,-1,0,0,1,-1,1,1,0,-1,0,NaN];d3.geom.hull=function(e){if(e.length<3)return[];var t=e.length,n=t-1,r=[],i=[],s,o,u=0,a,f,l,c,h,p,d,v;for(s=1;s<t;++s)e[s][1]<e[u][1]?u=s:e[s][1]==e[u][1]&&(u=e[s][0]<e[u][0]?s:u);for(s=0;s<t;++s){if(s===u)continue;f=e[s][1]-e[u][1],a=e[s][0]-e[u][0],r.push({angle:Math.atan2(f,a),index:s})}r.sort(function(e,t){return e.angle-t.angle}),d=r[0].angle,p=r[0].index,h=0;for(s=1;s<n;++s)o=r[s].index,d==r[s].angle?(a=e[p][0]-e[u][0],f=e[p][1]-e[u][1],l=e[o][0]-e[u][0],c=e[o][1]-e[u][1],a*a+f*f>=l*l+c*c?r[s].index=-1:(r[h].index=-1,d=r[s].angle,h=s,p=o)):(d=r[s].angle,h=s,p=o);i.push(u);for(s=0,o=0;s<2;++o)r[o].index!==-1&&(i.push(r[o].index),s++);v=i.length;for(;o<n;++o){if(r[o].index===-1)continue;while(!ii(i[v-2],i[v-1],r[o].index,e))--v;i[v++]=r[o].index}var m=[];for(s=0;s<v;++s)m.push(e[i[s]]);return m},d3.geom.polygon=function(e){return e.area=function(){var t=0,n=e.length,r=e[n-1][0]*e[0][1],i=e[n-1][1]*e[0][0];while(++t<n)r+=e[t-1][0]*e[t][1],i+=e[t-1][1]*e[t][0];return(i-r)*.5},e.centroid=function(t){var n=-1,r=e.length,i=0,s=0,o,u=e[r-1],a;arguments.length||(t=-1/(6*e.area()));while(++n<r)o=u,u=e[n],a=o[0]*u[1]-u[0]*o[1],i+=(o[0]+u[0])*a,s+=(o[1]+u[1])*a;return[i*t,s*t]},e.clip=function(t){var n,r=-1,i=e.length,s,o,u=e[i-1],a,f,l;while(++r<i){n=t.slice(),t.length=0,a=e[r],f=n[(o=n.length)-1],s=-1;while(++s<o)l=n[s],si(l,u,a)?(si(f,u,a)||t.push(oi(f,l,u,a)),t.push(l)):si(f,u,a)&&t.push(oi(f,l,u,a)),f=l;u=a}return t},e},d3.geom.voronoi=function(e){var t=e.map(function(){return[]});return ui(e,function(e){var n,r,i,s,o,u;e.a===1&&e.b>=0?(n=e.ep.r,r=e.ep.l):(n=e.ep.l,r=e.ep.r),e.a===1?(o=n?n.y:-1e6,i=e.c-e.b*o,u=r?r.y:1e6,s=e.c-e.b*u):(i=n?n.x:-1e6,o=e.c-e.a*i,s=r?r.x:1e6,u=e.c-e.a*s);var a=[i,o],f=[s,u];t[e.region.l.index].push(a,f),t[e.region.r.index].push(a,f)}),t.map(function(t,n){var r=e[n][0],i=e[n][1];return t.forEach(function(e){e.angle=Math.atan2(e[0]-r,e[1]-i)}),t.sort(function(e,t){return e.angle-t.angle}).filter(function(e,n){return!n||e.angle-t[n-1].angle>1e-10})})};var wo={l:"r",r:"l"};d3.geom.delaunay=function(e){var t=e.map(function(){return[]}),n=[];return ui(e,function(n){t[n.region.l.index].push(e[n.region.r.index])}),t.forEach(function(t,r){var i=e[r],s=i[0],o=i[1];t.forEach(function(e){e.angle=Math.atan2(e[0]-s,e[1]-o)}),t.sort(function(e,t){return e.angle-t.angle});for(var u=0,a=t.length-1;u<a;u++)n.push([i,t[u],t[u+1]])}),n},d3.geom.quadtree=function(e,t,n,r,i){function s(e,t,n,r,i,s){if(isNaN(t.x)||isNaN(t.y))return;if(e.leaf){var u=e.point;u?Math.abs(u.x-t.x)+Math.abs(u.y-t.y)<.01?o(e,t,n,r,i,s):(e.point=null,o(e,u,n,r,i,s),o(e,t,n,r,i,s)):e.point=t}else o(e,t,n,r,i,s)}function o(e,t,n,r,i,o){var u=(n+i)*.5,a=(r+o)*.5,f=t.x>=u,l=t.y>=a,c=(l<<1)+f;e.leaf=!1,e=e.nodes[c]||(e.nodes[c]=ai()),f?n=u:i=u,l?r=a:o=a,s(e,t,n,r,i,o)}var u,a=-1,f=e.length;f&&isNaN(e[0].x)&&(e=e.map(li));if(arguments.length<5)if(arguments.length===3)i=r=n,n=t;else{t=n=Infinity,r=i=-Infinity;while(++a<f)u=e[a],u.x<t&&(t=u.x),u.y<n&&(n=u.y),u.x>r&&(r=u.x),u.y>i&&(i=u.y);var l=r-t,c=i-n;l>c?i=n+l:r=t+c}var h=ai();return h.add=function(e){s(h,e,t,n,r,i)},h.visit=function(e){fi(e,h,t,n,r,i)},e.forEach(h.add),h},d3.time={};var Eo=Date,So=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];ci.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){xo.setUTCDate.apply(this._,arguments)},setDay:function(){xo.setUTCDay.apply(this._,arguments)},setFullYear:function(){xo.setUTCFullYear.apply(this._,arguments)},setHours:function(){xo.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){xo.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){xo.setUTCMinutes.apply(this._,arguments)},setMonth:function(){xo.setUTCMonth.apply(this._,arguments)},setSeconds:function(){xo.setUTCSeconds.apply(this._,arguments)},setTime:function(){xo.setTime.apply(this._,arguments)}};var xo=Date.prototype,To="%a %b %e %H:%M:%S %Y",No="%m/%d/%y",Co="%H:%M:%S",ko=So,Lo=ko.map(hi),Ao=["January","February","March","April","May","June","July","August","September","October","November","December"],Oo=Ao.map(hi);d3.time.format=function(e){function t(t){var r=[],i=-1,s=0,o,u;while(++i<n)e.charCodeAt(i)==37&&(r.push(e.substring(s,i),(u=Ro[o=e.charAt(++i)])?u(t):o),s=i+1);return r.push(e.substring(s,i)),r.join("")}var n=e.length;return t.parse=function(t){var n={y:1900,m:0,d:1,H:0,M:0,S:0,L:0},r=pi(n,e,t,0);if(r!=t.length)return null;"p"in n&&(n.H=n.H%12+n.p*12);var i=new Eo;return i.setFullYear(n.y,n.m,n.d),i.setHours(n.H,n.M,n.S,n.L),i},t.toString=function(){return e},t};var Mo=d3.format("02d"),_o=d3.format("03d"),Do=d3.format("04d"),Po=d3.format("2d"),Ho=di(ko),Bo=di(Lo),jo=di(Ao),Fo=vi(Ao),Io=di(Oo),qo=vi(Oo),Ro={a:function(e){return Lo[e.getDay()]},A:function(e){return ko[e.getDay()]},b:function(e){return Oo[e.getMonth()]},B:function(e){return Ao[e.getMonth()]},c:d3.time.format(To),d:function(e){return Mo(e.getDate())},e:function(e){return Po(e.getDate())},H:function(e){return Mo(e.getHours())},I:function(e){return Mo(e.getHours()%12||12)},j:function(e){return _o(1+d3.time.dayOfYear(e))},L:function(e){return _o(e.getMilliseconds())},m:function(e){return Mo(e.getMonth()+1)},M:function(e){return Mo(e.getMinutes())},p:function(e){return e.getHours()>=12?"PM":"AM"},S:function(e){return Mo(e.getSeconds())},U:function(e){return Mo(d3.time.sundayOfYear(e))},w:function(e){return e.getDay()},W:function(e){return Mo(d3.time.mondayOfYear(e))},x:d3.time.format(No),X:d3.time.format(Co),y:function(e){return Mo(e.getFullYear()%100)},Y:function(e){return Do(e.getFullYear()%1e4)},Z:Di,"%":function(e){return"%"}},Uo={a:mi,A:gi,b:yi,B:bi,c:wi,d:ki,e:ki,H:Li,I:Li,L:Mi,m:Ci,M:Ai,p:_i,S:Oi,x:Ei,X:Si,y:Ti,Y:xi},zo=/^\s*\d+/,Wo=d3.map({am:0,pm:1});d3.time.format.utc=function(e){function t(e){try{Eo=ci;var t=new Eo;return t._=e,n(t)}finally{Eo=Date}}var n=d3.time.format(e);return t.parse=function(e){try{Eo=ci;var t=n.parse(e);return t&&t._}finally{Eo=Date}},t.toString=n.toString,t};var Xo=d3.time.format.utc("%Y-%m-%dT%H:%M:%S.%LZ");d3.time.format.iso=Date.prototype.toISOString?Pi:Xo,Pi.parse=function(e){var t=new Date(e);return isNaN(t)?null:t},Pi.toString=Xo.toString,d3.time.second=Hi(function(e){return new Eo(Math.floor(e/1e3)*1e3)},function(e,t){e.setTime(e.getTime()+Math.floor(t)*1e3)},function(e){return e.getSeconds()}),d3.time.seconds=d3.time.second.range,d3.time.seconds.utc=d3.time.second.utc.range,d3.time.minute=Hi(function(e){return new Eo(Math.floor(e/6e4)*6e4)},function(e,t){e.setTime(e.getTime()+Math.floor(t)*6e4)},function(e){return e.getMinutes()}),d3.time.minutes=d3.time.minute.range,d3.time.minutes.utc=d3.time.minute.utc.range,d3.time.hour=Hi(function(e){var t=e.getTimezoneOffset()/60;return new Eo((Math.floor(e/36e5-t)+t)*36e5)},function(e,t){e.setTime(e.getTime()+Math.floor(t)*36e5)},function(e){return e.getHours()}),d3.time.hours=d3.time.hour.range,d3.time.hours.utc=d3.time.hour.utc.range,d3.time.day=Hi(function(e){var t=new Eo(1970,0);return t.setFullYear(e.getFullYear(),e.getMonth(),e.getDate()),t},function(e,t){e.setDate(e.getDate()+t)},function(e){return e.getDate()-1}),d3.time.days=d3.time.day.range,d3.time.days.utc=d3.time.day.utc.range,d3.time.dayOfYear=function(e){var t=d3.time.year(e);return Math.floor((e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*6e4)/864e5)},So.forEach(function(e,t){e=e.toLowerCase(),t=7-t;var n=d3.time[e]=Hi(function(e){return(e=d3.time.day(e)).setDate(e.getDate()-(e.getDay()+t)%7),e},function(e,t){e.setDate(e.getDate()+Math.floor(t)*7)},function(e){var n=d3.time.year(e).getDay();return Math.floor((d3.time.dayOfYear(e)+(n+t)%7)/7)-(n!==t)});d3.time[e+"s"]=n.range,d3.time[e+"s"].utc=n.utc.range,d3.time[e+"OfYear"]=function(e){var n=d3.time.year(e).getDay();return Math.floor((d3.time.dayOfYear(e)+(n+t)%7)/7)}}),d3.time.week=d3.time.sunday,d3.time.weeks=d3.time.sunday.range,d3.time.weeks.utc=d3.time.sunday.utc.range,d3.time.weekOfYear=d3.time.sundayOfYear,d3.time.month=Hi(function(e){return e=d3.time.day(e),e.setDate(1),e},function(e,t){e.setMonth(e.getMonth()+t)},function(e){return e.getMonth()}),d3.time.months=d3.time.month.range,d3.time.months.utc=d3.time.month.utc.range,d3.time.year=Hi(function(e){return e=d3.time.day(e),e.setMonth(0,1),e},function(e,t){e.setFullYear(e.getFullYear()+t)},function(e){return e.getFullYear()}),d3.time.years=d3.time.year.range,d3.time.years.utc=d3.time.year.utc.range;var Vo=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],$o=[[d3.time.second,1],[d3.time.second,5],[d3.time.second,15],[d3.time.second,30],[d3.time.minute,1],[d3.time.minute,5],[d3.time.minute,15],[d3.time.minute,30],[d3.time.hour,1],[d3.time.hour,3],[d3.time.hour,6],[d3.time.hour,12],[d3.time.day,1],[d3.time.day,2],[d3.time.week,1],[d3.time.month,1],[d3.time.month,3],[d3.time.year,1]],Jo=[[d3.time.format("%Y"),function(e){return!0}],[d3.time.format("%B"),function(e){return e.getMonth()}],[d3.time.format("%b %d"),function(e){return e.getDate()!=1}],[d3.time.format("%a %d"),function(e){return e.getDay()&&e.getDate()!=1}],[d3.time.format("%I %p"),function(e){return e.getHours()}],[d3.time.format("%I:%M"),function(e){return e.getMinutes()}],[d3.time.format(":%S"),function(e){return e.getSeconds()}],[d3.time.format(".%L"),function(e){return e.getMilliseconds()}]],Ko=d3.scale.linear(),Qo=qi(Jo);$o.year=function(e,t){return Ko.domain(e.map(Ui)).ticks(t).map(Ri)},d3.time.scale=function(){return ji(d3.scale.linear(),$o,Qo)};var Go=$o.map(function(e){return[e[0].utc,e[1]]}),Yo=[[d3.time.format.utc("%Y"),function(e){return!0}],[d3.time.format.utc("%B"),function(e){return e.getUTCMonth()}],[d3.time.format.utc("%b %d"),function(e){return e.getUTCDate()!=1}],[d3.time.format.utc("%a %d"),function(e){return e.getUTCDay()&&e.getUTCDate()!=1}],[d3.time.format.utc("%I %p"),function(e){return e.getUTCHours()}],[d3.time.format.utc("%I:%M"),function(e){return e.getUTCMinutes()}],[d3.time.format.utc(":%S"),function(e){return e.getUTCSeconds()}],[d3.time.format.utc(".%L"),function(e){return e.getUTCMilliseconds()}]],Zo=qi(Yo);Go.year=function(e,t){return Ko.domain(e.map(Wi)).ticks(t).map(zi)},d3.time.scale.utc=function(){return ji(d3.scale.linear(),Go,Zo)}})();
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_flat_0_aaaaaa_40x100.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100755
index 0000000000000000000000000000000000000000..5b5dab2ab7b1c50dea9cfe73dc5a269a92d2d4b4
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_flat_0_aaaaaa_40x100.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_flat_75_ffffff_40x100.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100755
index 0000000000000000000000000000000000000000..ac8b229af950c29356abf64a6c4aa894575445f0
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_flat_75_ffffff_40x100.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_55_fbf9ee_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_55_fbf9ee_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..ad3d6346e00f246102f72f2e026ed0491988b394
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_55_fbf9ee_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_65_ffffff_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..42ccba269b6e91bef12ad0fa18be651b5ef0ee68
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_75_dadada_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_75_dadada_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..5a46b47cb16631068aee9e0bd61269fc4e95e5cd
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_75_dadada_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_75_e6e6e6_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_75_e6e6e6_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..86c2baa655eac8539db34f8d9adb69ec1226201c
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_75_e6e6e6_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_95_fef1ec_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_95_fef1ec_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..4443fdc1a156babad4336f004eaf5ca5dfa0f9ab
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_glass_95_fef1ec_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png
new file mode 100755
index 0000000000000000000000000000000000000000..7c9fa6c6edcfcdd3e5b77e6f547b719e6fc66e30
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_222222_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_222222_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..ee039dc096a38a3753f92519546eee94bcfbeffa
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_222222_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_2e83ff_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_2e83ff_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..45e8928e5284adacea3f9ec07b9b50667d2ac65f
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_2e83ff_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_454545_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_454545_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..7ec70d11bfb2f77374dfd00ef61ba0c3647b5a0c
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_454545_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_888888_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_888888_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..5ba708c39172a69e069136bd1309c4322c61f571
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_888888_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_cd0a0a_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_cd0a0a_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..7930a558099bc8d92b4264eb67a0f040460f4a4f
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/images/ui-icons_cd0a0a_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/jquery-ui-1.7.2.custom.css b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/jquery-ui-1.7.2.custom.css
new file mode 100755
index 0000000000000000000000000000000000000000..15b7a663a55abe93d518d1b0ff9553ae16fb8b9f
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/custom-theme/jquery-ui-1.7.2.custom.css
@@ -0,0 +1,406 @@
+/*
+* jQuery UI CSS Framework
+* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
+*/
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { position: absolute; left: -99999999px; }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
+.ui-helper-clearfix { display: inline-block; }
+/* required comment for clearfix to work in Opera \*/
+* html .ui-helper-clearfix { height:1%; }
+.ui-helper-clearfix { display:block; }
+/* end clearfix */
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
+
+
+
+/*
+* jQuery UI CSS Framework
+* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=0.9em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=02_glass.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
+*/
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 0.9em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; }
+.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; }
+.ui-widget-content a { color: #222222; }
+.ui-widget-header { border: 1px solid #aaaaaa; background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }
+.ui-widget-header a { color: #222222; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default { border: 1px solid #d3d3d3; background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #555555; outline: none; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555; text-decoration: none; outline: none; }
+.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus { border: 1px solid #999999; background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; outline: none; }
+.ui-state-hover a, .ui-state-hover a:hover { color: #212121; text-decoration: none; outline: none; }
+.ui-state-active, .ui-widget-content .ui-state-active { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; outline: none; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121; outline: none; text-decoration: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight {border: 1px solid #fcefa1; background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; color: #363636; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a { color: #363636; }
+.ui-state-error, .ui-widget-content .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; color: #cd0a0a; }
+.ui-state-error a, .ui-widget-content .ui-state-error a { color: #cd0a0a; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text { color: #cd0a0a; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png); }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-off { background-position: -96px -144px; }
+.ui-icon-radio-on { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; }
+.ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; }
+.ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; }
+.ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; }
+.ui-corner-top { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; }
+.ui-corner-bottom { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; }
+.ui-corner-right {  -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; }
+.ui-corner-left { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; }
+.ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; }
+
+/* Overlays */
+.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
+.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; }/* Accordion
+----------------------------------*/
+.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
+.ui-accordion .ui-accordion-li-fix { display: inline; }
+.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
+.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em 2.2em; }
+.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; }
+.ui-accordion .ui-accordion-content-active { display: block; }/* Datepicker
+----------------------------------*/
+.ui-datepicker { width: 17em; padding: .2em .2em 0; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px;  }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { float:left; font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month, 
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker .ui-datepicker-title select.ui-datepicker-year { float: right; }
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0;  }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+    display: none; /*sorry for IE5*/
+    display/**/: block; /*sorry for IE5*/
+    position: absolute; /*must have*/
+    z-index: -1; /*must have*/
+    filter: mask(); /*must have*/
+    top: -4px; /*must have*/
+    left: -4px; /*must have*/
+    width: 200px; /*must have*/
+    height: 200px; /*must have*/
+}/* Dialog
+----------------------------------*/
+.ui-dialog { position: relative; padding: .2em; width: 300px; }
+.ui-dialog .ui-dialog-titlebar { padding: .5em .3em .3em 1em; position: relative;  }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 0 .2em; } 
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
+.ui-dialog .ui-dialog-buttonpane button { float: right; margin: .5em .4em .5em 0; cursor: pointer; padding: .2em .6em .3em .6em; line-height: 1.4em; width:auto; overflow:visible; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
+/* Progressbar
+----------------------------------*/
+.ui-progressbar { height:2em; text-align: left; }
+.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }/* Resizable
+----------------------------------*/
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;}
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0px; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0px; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0px; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0px; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* Slider
+----------------------------------*/
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }/* Tabs
+----------------------------------*/
+.ui-tabs { padding: .2em; zoom: 1; }
+.ui-tabs .ui-tabs-nav { list-style: none; position: relative; padding: .2em .2em 0; }
+.ui-tabs .ui-tabs-nav li { position: relative; float: left; border-bottom-width: 0 !important; margin: 0 .2em -1px 0; padding: 0; }
+.ui-tabs .ui-tabs-nav li a { float: left; text-decoration: none; padding: .5em 1em; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected { padding-bottom: 1px; border-bottom-width: 0; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
+.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
+.ui-tabs .ui-tabs-panel { padding: 1em 1.4em; display: block; border-width: 0; background: none; }
+.ui-tabs .ui-tabs-hide { display: none !important; }
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100755
index 0000000000000000000000000000000000000000..5b5dab2ab7b1c50dea9cfe73dc5a269a92d2d4b4
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100755
index 0000000000000000000000000000000000000000..ac8b229af950c29356abf64a6c4aa894575445f0
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..ad3d6346e00f246102f72f2e026ed0491988b394
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..42ccba269b6e91bef12ad0fa18be651b5ef0ee68
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..5a46b47cb16631068aee9e0bd61269fc4e95e5cd
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..86c2baa655eac8539db34f8d9adb69ec1226201c
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..4443fdc1a156babad4336f004eaf5ca5dfa0f9ab
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png
new file mode 100755
index 0000000000000000000000000000000000000000..7c9fa6c6edcfcdd3e5b77e6f547b719e6fc66e30
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_222222_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_222222_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..ee039dc096a38a3753f92519546eee94bcfbeffa
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_222222_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_2e83ff_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_2e83ff_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..45e8928e5284adacea3f9ec07b9b50667d2ac65f
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_2e83ff_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_454545_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_454545_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..7ec70d11bfb2f77374dfd00ef61ba0c3647b5a0c
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_454545_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_888888_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_888888_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..5ba708c39172a69e069136bd1309c4322c61f571
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_888888_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_cd0a0a_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_cd0a0a_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..7930a558099bc8d92b4264eb67a0f040460f4a4f
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/images/ui-icons_cd0a0a_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/jquery-ui-1.7.2.custom.css b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/jquery-ui-1.7.2.custom.css
new file mode 100755
index 0000000000000000000000000000000000000000..727c0f5cc8b1f3d9b3ef5ca1f0f19fc197c4712d
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/smoothness/jquery-ui-1.7.2.custom.css
@@ -0,0 +1,406 @@
+/*
+* jQuery UI CSS Framework
+* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
+*/
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { position: absolute; left: -99999999px; }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
+.ui-helper-clearfix { display: inline-block; }
+/* required comment for clearfix to work in Opera \*/
+* html .ui-helper-clearfix { height:1%; }
+.ui-helper-clearfix { display:block; }
+/* end clearfix */
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
+
+
+
+/*
+* jQuery UI CSS Framework
+* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=02_glass.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
+*/
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; }
+.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; }
+.ui-widget-content a { color: #222222; }
+.ui-widget-header { border: 1px solid #aaaaaa; background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }
+.ui-widget-header a { color: #222222; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default { border: 1px solid #d3d3d3; background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #555555; outline: none; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555; text-decoration: none; outline: none; }
+.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus { border: 1px solid #999999; background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; outline: none; }
+.ui-state-hover a, .ui-state-hover a:hover { color: #212121; text-decoration: none; outline: none; }
+.ui-state-active, .ui-widget-content .ui-state-active { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; outline: none; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121; outline: none; text-decoration: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight {border: 1px solid #fcefa1; background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; color: #363636; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a { color: #363636; }
+.ui-state-error, .ui-widget-content .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; color: #cd0a0a; }
+.ui-state-error a, .ui-widget-content .ui-state-error a { color: #cd0a0a; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text { color: #cd0a0a; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png); }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-off { background-position: -96px -144px; }
+.ui-icon-radio-on { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; }
+.ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; }
+.ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; }
+.ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; }
+.ui-corner-top { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; }
+.ui-corner-bottom { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; }
+.ui-corner-right {  -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; }
+.ui-corner-left { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; }
+.ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; }
+
+/* Overlays */
+.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
+.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; }/* Accordion
+----------------------------------*/
+.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
+.ui-accordion .ui-accordion-li-fix { display: inline; }
+.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
+.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em 2.2em; }
+.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; }
+.ui-accordion .ui-accordion-content-active { display: block; }/* Datepicker
+----------------------------------*/
+.ui-datepicker { width: 17em; padding: .2em .2em 0; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px;  }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { float:left; font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month, 
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker .ui-datepicker-title select.ui-datepicker-year { float: right; }
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0;  }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+    display: none; /*sorry for IE5*/
+    display/**/: block; /*sorry for IE5*/
+    position: absolute; /*must have*/
+    z-index: -1; /*must have*/
+    filter: mask(); /*must have*/
+    top: -4px; /*must have*/
+    left: -4px; /*must have*/
+    width: 200px; /*must have*/
+    height: 200px; /*must have*/
+}/* Dialog
+----------------------------------*/
+.ui-dialog { position: relative; padding: .2em; width: 300px; }
+.ui-dialog .ui-dialog-titlebar { padding: .5em .3em .3em 1em; position: relative;  }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 0 .2em; } 
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
+.ui-dialog .ui-dialog-buttonpane button { float: right; margin: .5em .4em .5em 0; cursor: pointer; padding: .2em .6em .3em .6em; line-height: 1.4em; width:auto; overflow:visible; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
+/* Progressbar
+----------------------------------*/
+.ui-progressbar { height:2em; text-align: left; }
+.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }/* Resizable
+----------------------------------*/
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;}
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0px; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0px; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0px; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0px; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* Slider
+----------------------------------*/
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }/* Tabs
+----------------------------------*/
+.ui-tabs { padding: .2em; zoom: 1; }
+.ui-tabs .ui-tabs-nav { list-style: none; position: relative; padding: .2em .2em 0; }
+.ui-tabs .ui-tabs-nav li { position: relative; float: left; border-bottom-width: 0 !important; margin: 0 .2em -1px 0; padding: 0; }
+.ui-tabs .ui-tabs-nav li a { float: left; text-decoration: none; padding: .5em 1em; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected { padding-bottom: 1px; border-bottom-width: 0; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
+.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
+.ui-tabs .ui-tabs-panel { padding: 1em 1.4em; display: block; border-width: 0; background: none; }
+.ui-tabs .ui-tabs-hide { display: none !important; }
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png
new file mode 100755
index 0000000000000000000000000000000000000000..954e22dbd99e8c6dd7091335599abf2d10bf8003
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png
new file mode 100755
index 0000000000000000000000000000000000000000..64ece5707d91a6edf9fad4bfcce0c4dbcafcf58d
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png
new file mode 100755
index 0000000000000000000000000000000000000000..abdc01082bf3534eafecc5819d28c9574d44ea89
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..9b383f4d2eab09c0f2a739d6b232c32934bc620b
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..a23baad25b1d1ff36e17361eab24271f2e9b7326
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100755
index 0000000000000000000000000000000000000000..42ccba269b6e91bef12ad0fa18be651b5ef0ee68
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png
new file mode 100755
index 0000000000000000000000000000000000000000..39d5824d6af5456f1e89fc7847ea3599ea5fd815
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png
new file mode 100755
index 0000000000000000000000000000000000000000..f1273672d253263b7564e9e21d69d7d9d0b337d9
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png
new file mode 100755
index 0000000000000000000000000000000000000000..359397acffdd84bd102f0e8a951c9d744f278db5
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_222222_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_222222_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..ee039dc096a38a3753f92519546eee94bcfbeffa
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_222222_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_228ef1_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_228ef1_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..10e3631dcab364a0a1ab81862d678fd032908dd7
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_228ef1_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_ef8c08_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_ef8c08_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..35bb8efa9c87eed9615e64354e12d3578c604823
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_ef8c08_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_ffd27a_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_ffd27a_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..baebb63e3fc8fbba697afca858abb3f55bd0f08e
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_ffd27a_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_ffffff_256x240.png b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_ffffff_256x240.png
new file mode 100755
index 0000000000000000000000000000000000000000..bef5178a9054c16582876bac57017f783272e750
Binary files /dev/null and b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/images/ui-icons_ffffff_256x240.png differ
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/jquery-ui-1.7.2.custom.css b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/jquery-ui-1.7.2.custom.css
new file mode 100755
index 0000000000000000000000000000000000000000..33979e0b30000a9d2f6bc83db64aed68f6283ee9
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/css/ui-lightness/jquery-ui-1.7.2.custom.css
@@ -0,0 +1,406 @@
+/*
+* jQuery UI CSS Framework
+* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
+*/
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { position: absolute; left: -99999999px; }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
+.ui-helper-clearfix { display: inline-block; }
+/* required comment for clearfix to work in Opera \*/
+* html .ui-helper-clearfix { height:1%; }
+.ui-helper-clearfix { display:block; }
+/* end clearfix */
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
+
+
+
+/*
+* jQuery UI CSS Framework
+* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS,%20Tahoma,%20Verdana,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=12_gloss_wave.png&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=08_diagonals_thick.png&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px
+*/
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; font-size: 1.1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; font-size: 1em; }
+.ui-widget-content { border: 1px solid #dddddd; background: #eeeeee url(images/ui-bg_highlight-soft_100_eeeeee_1x100.png) 50% top repeat-x; color: #333333; }
+.ui-widget-content a { color: #333333; }
+.ui-widget-header { border: 1px solid #e78f08; background: #f6a828 url(images/ui-bg_gloss-wave_35_f6a828_500x100.png) 50% 50% repeat-x; color: #ffffff; font-weight: bold; }
+.ui-widget-header a { color: #ffffff; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default { border: 1px solid #cccccc; background: #f6f6f6 url(images/ui-bg_glass_100_f6f6f6_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #1c94c4; outline: none; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #1c94c4; text-decoration: none; outline: none; }
+.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus { border: 1px solid #fbcb09; background: #fdf5ce url(images/ui-bg_glass_100_fdf5ce_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #c77405; outline: none; }
+.ui-state-hover a, .ui-state-hover a:hover { color: #c77405; text-decoration: none; outline: none; }
+.ui-state-active, .ui-widget-content .ui-state-active { border: 1px solid #fbd850; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #eb8f00; outline: none; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #eb8f00; outline: none; text-decoration: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight {border: 1px solid #fed22f; background: #ffe45c url(images/ui-bg_highlight-soft_75_ffe45c_1x100.png) 50% top repeat-x; color: #363636; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a { color: #363636; }
+.ui-state-error, .ui-widget-content .ui-state-error {border: 1px solid #cd0a0a; background: #b81900 url(images/ui-bg_diagonals-thick_18_b81900_40x40.png) 50% 50% repeat; color: #ffffff; }
+.ui-state-error a, .ui-widget-content .ui-state-error a { color: #ffffff; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text { color: #ffffff; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_ef8c08_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_ef8c08_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_ef8c08_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_228ef1_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_ffd27a_256x240.png); }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-off { background-position: -96px -144px; }
+.ui-icon-radio-on { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; }
+.ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; }
+.ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; }
+.ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; }
+.ui-corner-top { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; }
+.ui-corner-bottom { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; }
+.ui-corner-right {  -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; }
+.ui-corner-left { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; }
+.ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; }
+
+/* Overlays */
+.ui-widget-overlay { background: #666666 url(images/ui-bg_diagonals-thick_20_666666_40x40.png) 50% 50% repeat; opacity: .50;filter:Alpha(Opacity=50); }
+.ui-widget-shadow { margin: -5px 0 0 -5px; padding: 5px; background: #000000 url(images/ui-bg_flat_10_000000_40x100.png) 50% 50% repeat-x; opacity: .20;filter:Alpha(Opacity=20); -moz-border-radius: 5px; -webkit-border-radius: 5px; }/* Accordion
+----------------------------------*/
+.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
+.ui-accordion .ui-accordion-li-fix { display: inline; }
+.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
+.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em 2.2em; }
+.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; }
+.ui-accordion .ui-accordion-content-active { display: block; }/* Datepicker
+----------------------------------*/
+.ui-datepicker { width: 17em; padding: .2em .2em 0; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px;  }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { float:left; font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month, 
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker .ui-datepicker-title select.ui-datepicker-year { float: right; }
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0;  }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+    display: none; /*sorry for IE5*/
+    display/**/: block; /*sorry for IE5*/
+    position: absolute; /*must have*/
+    z-index: -1; /*must have*/
+    filter: mask(); /*must have*/
+    top: -4px; /*must have*/
+    left: -4px; /*must have*/
+    width: 200px; /*must have*/
+    height: 200px; /*must have*/
+}/* Dialog
+----------------------------------*/
+.ui-dialog { position: relative; padding: .2em; width: 300px; }
+.ui-dialog .ui-dialog-titlebar { padding: .5em .3em .3em 1em; position: relative;  }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 0 .2em; } 
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
+.ui-dialog .ui-dialog-buttonpane button { float: right; margin: .5em .4em .5em 0; cursor: pointer; padding: .2em .6em .3em .6em; line-height: 1.4em; width:auto; overflow:visible; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
+/* Progressbar
+----------------------------------*/
+.ui-progressbar { height:2em; text-align: left; }
+.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }/* Resizable
+----------------------------------*/
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;}
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0px; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0px; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0px; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0px; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* Slider
+----------------------------------*/
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }/* Tabs
+----------------------------------*/
+.ui-tabs { padding: .2em; zoom: 1; }
+.ui-tabs .ui-tabs-nav { list-style: none; position: relative; padding: .2em .2em 0; }
+.ui-tabs .ui-tabs-nav li { position: relative; float: left; border-bottom-width: 0 !important; margin: 0 .2em -1px 0; padding: 0; }
+.ui-tabs .ui-tabs-nav li a { float: left; text-decoration: none; padding: .5em 1em; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected { padding-bottom: 1px; border-bottom-width: 0; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
+.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
+.ui-tabs .ui-tabs-panel { padding: 1em 1.4em; display: block; border-width: 0; background: none; }
+.ui-tabs .ui-tabs-hide { display: none !important; }
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/js/jquery-ui-1.7.2.custom.min.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/js/jquery-ui-1.7.2.custom.min.js
new file mode 100755
index 0000000000000000000000000000000000000000..7db3697e42b1e5bc407e5b2aafda79b8671dd04a
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/js/jquery-ui-1.7.2.custom.min.js
@@ -0,0 +1,298 @@
+/*
+ * jQuery UI 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI
+ */
+jQuery.ui||(function(c){var i=c.fn.remove,d=c.browser.mozilla&&(parseFloat(c.browser.version)<1.9);c.ui={version:"1.7.2",plugin:{add:function(k,l,n){var m=c.ui[k].prototype;for(var j in n){m.plugins[j]=m.plugins[j]||[];m.plugins[j].push([l,n[j]])}},call:function(j,l,k){var n=j.plugins[l];if(!n||!j.element[0].parentNode){return}for(var m=0;m<n.length;m++){if(j.options[n[m][0]]){n[m][1].apply(j.element,k)}}}},contains:function(k,j){return document.compareDocumentPosition?k.compareDocumentPosition(j)&16:k!==j&&k.contains(j)},hasScroll:function(m,k){if(c(m).css("overflow")=="hidden"){return false}var j=(k&&k=="left")?"scrollLeft":"scrollTop",l=false;if(m[j]>0){return true}m[j]=1;l=(m[j]>0);m[j]=0;return l},isOverAxis:function(k,j,l){return(k>j)&&(k<(j+l))},isOver:function(o,k,n,m,j,l){return c.ui.isOverAxis(o,n,j)&&c.ui.isOverAxis(k,m,l)},keyCode:{BACKSPACE:8,CAPS_LOCK:20,COMMA:188,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38}};if(d){var f=c.attr,e=c.fn.removeAttr,h="http://www.w3.org/2005/07/aaa",a=/^aria-/,b=/^wairole:/;c.attr=function(k,j,l){var m=l!==undefined;return(j=="role"?(m?f.call(this,k,j,"wairole:"+l):(f.apply(this,arguments)||"").replace(b,"")):(a.test(j)?(m?k.setAttributeNS(h,j.replace(a,"aaa:"),l):f.call(this,k,j.replace(a,"aaa:"))):f.apply(this,arguments)))};c.fn.removeAttr=function(j){return(a.test(j)?this.each(function(){this.removeAttributeNS(h,j.replace(a,""))}):e.call(this,j))}}c.fn.extend({remove:function(){c("*",this).add(this).each(function(){c(this).triggerHandler("remove")});return i.apply(this,arguments)},enableSelection:function(){return this.attr("unselectable","off").css("MozUserSelect","").unbind("selectstart.ui")},disableSelection:function(){return this.attr("unselectable","on").css("MozUserSelect","none").bind("selectstart.ui",function(){return false})},scrollParent:function(){var j;if((c.browser.msie&&(/(static|relative)/).test(this.css("position")))||(/absolute/).test(this.css("position"))){j=this.parents().filter(function(){return(/(relative|absolute|fixed)/).test(c.curCSS(this,"position",1))&&(/(auto|scroll)/).test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0)}else{j=this.parents().filter(function(){return(/(auto|scroll)/).test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0)}return(/fixed/).test(this.css("position"))||!j.length?c(document):j}});c.extend(c.expr[":"],{data:function(l,k,j){return !!c.data(l,j[3])},focusable:function(k){var l=k.nodeName.toLowerCase(),j=c.attr(k,"tabindex");return(/input|select|textarea|button|object/.test(l)?!k.disabled:"a"==l||"area"==l?k.href||!isNaN(j):!isNaN(j))&&!c(k)["area"==l?"parents":"closest"](":hidden").length},tabbable:function(k){var j=c.attr(k,"tabindex");return(isNaN(j)||j>=0)&&c(k).is(":focusable")}});function g(m,n,o,l){function k(q){var p=c[m][n][q]||[];return(typeof p=="string"?p.split(/,?\s+/):p)}var j=k("getter");if(l.length==1&&typeof l[0]=="string"){j=j.concat(k("getterSetter"))}return(c.inArray(o,j)!=-1)}c.widget=function(k,j){var l=k.split(".")[0];k=k.split(".")[1];c.fn[k]=function(p){var n=(typeof p=="string"),o=Array.prototype.slice.call(arguments,1);if(n&&p.substring(0,1)=="_"){return this}if(n&&g(l,k,p,o)){var m=c.data(this[0],k);return(m?m[p].apply(m,o):undefined)}return this.each(function(){var q=c.data(this,k);(!q&&!n&&c.data(this,k,new c[l][k](this,p))._init());(q&&n&&c.isFunction(q[p])&&q[p].apply(q,o))})};c[l]=c[l]||{};c[l][k]=function(o,n){var m=this;this.namespace=l;this.widgetName=k;this.widgetEventPrefix=c[l][k].eventPrefix||k;this.widgetBaseClass=l+"-"+k;this.options=c.extend({},c.widget.defaults,c[l][k].defaults,c.metadata&&c.metadata.get(o)[k],n);this.element=c(o).bind("setData."+k,function(q,p,r){if(q.target==o){return m._setData(p,r)}}).bind("getData."+k,function(q,p){if(q.target==o){return m._getData(p)}}).bind("remove",function(){return m.destroy()})};c[l][k].prototype=c.extend({},c.widget.prototype,j);c[l][k].getterSetter="option"};c.widget.prototype={_init:function(){},destroy:function(){this.element.removeData(this.widgetName).removeClass(this.widgetBaseClass+"-disabled "+this.namespace+"-state-disabled").removeAttr("aria-disabled")},option:function(l,m){var k=l,j=this;if(typeof l=="string"){if(m===undefined){return this._getData(l)}k={};k[l]=m}c.each(k,function(n,o){j._setData(n,o)})},_getData:function(j){return this.options[j]},_setData:function(j,k){this.options[j]=k;if(j=="disabled"){this.element[k?"addClass":"removeClass"](this.widgetBaseClass+"-disabled "+this.namespace+"-state-disabled").attr("aria-disabled",k)}},enable:function(){this._setData("disabled",false)},disable:function(){this._setData("disabled",true)},_trigger:function(l,m,n){var p=this.options[l],j=(l==this.widgetEventPrefix?l:this.widgetEventPrefix+l);m=c.Event(m);m.type=j;if(m.originalEvent){for(var k=c.event.props.length,o;k;){o=c.event.props[--k];m[o]=m.originalEvent[o]}}this.element.trigger(m,n);return !(c.isFunction(p)&&p.call(this.element[0],m,n)===false||m.isDefaultPrevented())}};c.widget.defaults={disabled:false};c.ui.mouse={_mouseInit:function(){var j=this;this.element.bind("mousedown."+this.widgetName,function(k){return j._mouseDown(k)}).bind("click."+this.widgetName,function(k){if(j._preventClickEvent){j._preventClickEvent=false;k.stopImmediatePropagation();return false}});if(c.browser.msie){this._mouseUnselectable=this.element.attr("unselectable");this.element.attr("unselectable","on")}this.started=false},_mouseDestroy:function(){this.element.unbind("."+this.widgetName);(c.browser.msie&&this.element.attr("unselectable",this._mouseUnselectable))},_mouseDown:function(l){l.originalEvent=l.originalEvent||{};if(l.originalEvent.mouseHandled){return}(this._mouseStarted&&this._mouseUp(l));this._mouseDownEvent=l;var k=this,m=(l.which==1),j=(typeof this.options.cancel=="string"?c(l.target).parents().add(l.target).filter(this.options.cancel).length:false);if(!m||j||!this._mouseCapture(l)){return true}this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet){this._mouseDelayTimer=setTimeout(function(){k.mouseDelayMet=true},this.options.delay)}if(this._mouseDistanceMet(l)&&this._mouseDelayMet(l)){this._mouseStarted=(this._mouseStart(l)!==false);if(!this._mouseStarted){l.preventDefault();return true}}this._mouseMoveDelegate=function(n){return k._mouseMove(n)};this._mouseUpDelegate=function(n){return k._mouseUp(n)};c(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);(c.browser.safari||l.preventDefault());l.originalEvent.mouseHandled=true;return true},_mouseMove:function(j){if(c.browser.msie&&!j.button){return this._mouseUp(j)}if(this._mouseStarted){this._mouseDrag(j);return j.preventDefault()}if(this._mouseDistanceMet(j)&&this._mouseDelayMet(j)){this._mouseStarted=(this._mouseStart(this._mouseDownEvent,j)!==false);(this._mouseStarted?this._mouseDrag(j):this._mouseUp(j))}return !this._mouseStarted},_mouseUp:function(j){c(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=false;this._preventClickEvent=(j.target==this._mouseDownEvent.target);this._mouseStop(j)}return false},_mouseDistanceMet:function(j){return(Math.max(Math.abs(this._mouseDownEvent.pageX-j.pageX),Math.abs(this._mouseDownEvent.pageY-j.pageY))>=this.options.distance)},_mouseDelayMet:function(j){return this.mouseDelayMet},_mouseStart:function(j){},_mouseDrag:function(j){},_mouseStop:function(j){},_mouseCapture:function(j){return true}};c.ui.mouse.defaults={cancel:null,distance:1,delay:0}})(jQuery);;/*
+ * jQuery UI Draggable 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Draggables
+ *
+ * Depends:
+ *	ui.core.js
+ */
+(function(a){a.widget("ui.draggable",a.extend({},a.ui.mouse,{_init:function(){if(this.options.helper=="original"&&!(/^(?:r|a|f)/).test(this.element.css("position"))){this.element[0].style.position="relative"}(this.options.addClasses&&this.element.addClass("ui-draggable"));(this.options.disabled&&this.element.addClass("ui-draggable-disabled"));this._mouseInit()},destroy:function(){if(!this.element.data("draggable")){return}this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy()},_mouseCapture:function(b){var c=this.options;if(this.helper||c.disabled||a(b.target).is(".ui-resizable-handle")){return false}this.handle=this._getHandle(b);if(!this.handle){return false}return true},_mouseStart:function(b){var c=this.options;this.helper=this._createHelper(b);this._cacheHelperProportions();if(a.ui.ddmanager){a.ui.ddmanager.current=this}this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.element.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(b);this.originalPageX=b.pageX;this.originalPageY=b.pageY;if(c.cursorAt){this._adjustOffsetFromHelper(c.cursorAt)}if(c.containment){this._setContainment()}this._trigger("start",b);this._cacheHelperProportions();if(a.ui.ddmanager&&!c.dropBehaviour){a.ui.ddmanager.prepareOffsets(this,b)}this.helper.addClass("ui-draggable-dragging");this._mouseDrag(b,true);return true},_mouseDrag:function(b,d){this.position=this._generatePosition(b);this.positionAbs=this._convertPositionTo("absolute");if(!d){var c=this._uiHash();this._trigger("drag",b,c);this.position=c.position}if(!this.options.axis||this.options.axis!="y"){this.helper[0].style.left=this.position.left+"px"}if(!this.options.axis||this.options.axis!="x"){this.helper[0].style.top=this.position.top+"px"}if(a.ui.ddmanager){a.ui.ddmanager.drag(this,b)}return false},_mouseStop:function(c){var d=false;if(a.ui.ddmanager&&!this.options.dropBehaviour){d=a.ui.ddmanager.drop(this,c)}if(this.dropped){d=this.dropped;this.dropped=false}if((this.options.revert=="invalid"&&!d)||(this.options.revert=="valid"&&d)||this.options.revert===true||(a.isFunction(this.options.revert)&&this.options.revert.call(this.element,d))){var b=this;a(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){b._trigger("stop",c);b._clear()})}else{this._trigger("stop",c);this._clear()}return false},_getHandle:function(b){var c=!this.options.handle||!a(this.options.handle,this.element).length?true:false;a(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==b.target){c=true}});return c},_createHelper:function(c){var d=this.options;var b=a.isFunction(d.helper)?a(d.helper.apply(this.element[0],[c])):(d.helper=="clone"?this.element.clone():this.element);if(!b.parents("body").length){b.appendTo((d.appendTo=="parent"?this.element[0].parentNode:d.appendTo))}if(b[0]!=this.element[0]&&!(/(fixed|absolute)/).test(b.css("position"))){b.css("position","absolute")}return b},_adjustOffsetFromHelper:function(b){if(b.left!=undefined){this.offset.click.left=b.left+this.margins.left}if(b.right!=undefined){this.offset.click.left=this.helperProportions.width-b.right+this.margins.left}if(b.top!=undefined){this.offset.click.top=b.top+this.margins.top}if(b.bottom!=undefined){this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top}},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])){b.left+=this.scrollParent.scrollLeft();b.top+=this.scrollParent.scrollTop()}if((this.offsetParent[0]==document.body)||(this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)){b={top:0,left:0}}return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var b=this.element.position();return{top:b.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:b.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else{return{top:0,left:0}}},_cacheMargins:function(){this.margins={left:(parseInt(this.element.css("marginLeft"),10)||0),top:(parseInt(this.element.css("marginTop"),10)||0)}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e=this.options;if(e.containment=="parent"){e.containment=this.helper[0].parentNode}if(e.containment=="document"||e.containment=="window"){this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,a(e.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a(e.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]}if(!(/^(document|window|parent)$/).test(e.containment)&&e.containment.constructor!=Array){var c=a(e.containment)[0];if(!c){return}var d=a(e.containment).offset();var b=(a(c).css("overflow")!="hidden");this.containment=[d.left+(parseInt(a(c).css("borderLeftWidth"),10)||0)+(parseInt(a(c).css("paddingLeft"),10)||0)-this.margins.left,d.top+(parseInt(a(c).css("borderTopWidth"),10)||0)+(parseInt(a(c).css("paddingTop"),10)||0)-this.margins.top,d.left+(b?Math.max(c.scrollWidth,c.offsetWidth):c.offsetWidth)-(parseInt(a(c).css("borderLeftWidth"),10)||0)-(parseInt(a(c).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,d.top+(b?Math.max(c.scrollHeight,c.offsetHeight):c.offsetHeight)-(parseInt(a(c).css("borderTopWidth"),10)||0)-(parseInt(a(c).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}else{if(e.containment.constructor==Array){this.containment=e.containment}}},_convertPositionTo:function(f,h){if(!h){h=this.position}var c=f=="absolute"?1:-1;var e=this.options,b=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=(/(html|body)/i).test(b[0].tagName);return{top:(h.top+this.offset.relative.top*c+this.offset.parent.top*c-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():(g?0:b.scrollTop()))*c)),left:(h.left+this.offset.relative.left*c+this.offset.parent.left*c-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:b.scrollLeft())*c))}},_generatePosition:function(e){var h=this.options,b=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,i=(/(html|body)/i).test(b[0].tagName);if(this.cssPosition=="relative"&&!(this.scrollParent[0]!=document&&this.scrollParent[0]!=this.offsetParent[0])){this.offset.relative=this._getRelativeOffset()}var d=e.pageX;var c=e.pageY;if(this.originalPosition){if(this.containment){if(e.pageX-this.offset.click.left<this.containment[0]){d=this.containment[0]+this.offset.click.left}if(e.pageY-this.offset.click.top<this.containment[1]){c=this.containment[1]+this.offset.click.top}if(e.pageX-this.offset.click.left>this.containment[2]){d=this.containment[2]+this.offset.click.left}if(e.pageY-this.offset.click.top>this.containment[3]){c=this.containment[3]+this.offset.click.top}}if(h.grid){var g=this.originalPageY+Math.round((c-this.originalPageY)/h.grid[1])*h.grid[1];c=this.containment?(!(g-this.offset.click.top<this.containment[1]||g-this.offset.click.top>this.containment[3])?g:(!(g-this.offset.click.top<this.containment[1])?g-h.grid[1]:g+h.grid[1])):g;var f=this.originalPageX+Math.round((d-this.originalPageX)/h.grid[0])*h.grid[0];d=this.containment?(!(f-this.offset.click.left<this.containment[0]||f-this.offset.click.left>this.containment[2])?f:(!(f-this.offset.click.left<this.containment[0])?f-h.grid[0]:f+h.grid[0])):f}}return{top:(c-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():(i?0:b.scrollTop())))),left:(d-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():i?0:b.scrollLeft())))}},_clear:function(){this.helper.removeClass("ui-draggable-dragging");if(this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval){this.helper.remove()}this.helper=null;this.cancelHelperRemoval=false},_trigger:function(b,c,d){d=d||this._uiHash();a.ui.plugin.call(this,b,[c,d]);if(b=="drag"){this.positionAbs=this._convertPositionTo("absolute")}return a.widget.prototype._trigger.call(this,b,c,d)},plugins:{},_uiHash:function(b){return{helper:this.helper,position:this.position,absolutePosition:this.positionAbs,offset:this.positionAbs}}}));a.extend(a.ui.draggable,{version:"1.7.2",eventPrefix:"drag",defaults:{addClasses:true,appendTo:"parent",axis:false,cancel:":input,option",connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,delay:0,distance:1,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false}});a.ui.plugin.add("draggable","connectToSortable",{start:function(c,e){var d=a(this).data("draggable"),f=d.options,b=a.extend({},e,{item:d.element});d.sortables=[];a(f.connectToSortable).each(function(){var g=a.data(this,"sortable");if(g&&!g.options.disabled){d.sortables.push({instance:g,shouldRevert:g.options.revert});g._refreshItems();g._trigger("activate",c,b)}})},stop:function(c,e){var d=a(this).data("draggable"),b=a.extend({},e,{item:d.element});a.each(d.sortables,function(){if(this.instance.isOver){this.instance.isOver=0;d.cancelHelperRemoval=true;this.instance.cancelHelperRemoval=false;if(this.shouldRevert){this.instance.options.revert=true}this.instance._mouseStop(c);this.instance.options.helper=this.instance.options._helper;if(d.options.helper=="original"){this.instance.currentItem.css({top:"auto",left:"auto"})}}else{this.instance.cancelHelperRemoval=false;this.instance._trigger("deactivate",c,b)}})},drag:function(c,f){var e=a(this).data("draggable"),b=this;var d=function(i){var n=this.offset.click.top,m=this.offset.click.left;var g=this.positionAbs.top,k=this.positionAbs.left;var j=i.height,l=i.width;var p=i.top,h=i.left;return a.ui.isOver(g+n,k+m,p,h,j,l)};a.each(e.sortables,function(g){this.instance.positionAbs=e.positionAbs;this.instance.helperProportions=e.helperProportions;this.instance.offset.click=e.offset.click;if(this.instance._intersectsWith(this.instance.containerCache)){if(!this.instance.isOver){this.instance.isOver=1;this.instance.currentItem=a(b).clone().appendTo(this.instance.element).data("sortable-item",true);this.instance.options._helper=this.instance.options.helper;this.instance.options.helper=function(){return f.helper[0]};c.target=this.instance.currentItem[0];this.instance._mouseCapture(c,true);this.instance._mouseStart(c,true,true);this.instance.offset.click.top=e.offset.click.top;this.instance.offset.click.left=e.offset.click.left;this.instance.offset.parent.left-=e.offset.parent.left-this.instance.offset.parent.left;this.instance.offset.parent.top-=e.offset.parent.top-this.instance.offset.parent.top;e._trigger("toSortable",c);e.dropped=this.instance.element;e.currentItem=e.element;this.instance.fromOutside=e}if(this.instance.currentItem){this.instance._mouseDrag(c)}}else{if(this.instance.isOver){this.instance.isOver=0;this.instance.cancelHelperRemoval=true;this.instance.options.revert=false;this.instance._trigger("out",c,this.instance._uiHash(this.instance));this.instance._mouseStop(c,true);this.instance.options.helper=this.instance.options._helper;this.instance.currentItem.remove();if(this.instance.placeholder){this.instance.placeholder.remove()}e._trigger("fromSortable",c);e.dropped=false}}})}});a.ui.plugin.add("draggable","cursor",{start:function(c,d){var b=a("body"),e=a(this).data("draggable").options;if(b.css("cursor")){e._cursor=b.css("cursor")}b.css("cursor",e.cursor)},stop:function(b,c){var d=a(this).data("draggable").options;if(d._cursor){a("body").css("cursor",d._cursor)}}});a.ui.plugin.add("draggable","iframeFix",{start:function(b,c){var d=a(this).data("draggable").options;a(d.iframeFix===true?"iframe":d.iframeFix).each(function(){a('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1000}).css(a(this).offset()).appendTo("body")})},stop:function(b,c){a("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)})}});a.ui.plugin.add("draggable","opacity",{start:function(c,d){var b=a(d.helper),e=a(this).data("draggable").options;if(b.css("opacity")){e._opacity=b.css("opacity")}b.css("opacity",e.opacity)},stop:function(b,c){var d=a(this).data("draggable").options;if(d._opacity){a(c.helper).css("opacity",d._opacity)}}});a.ui.plugin.add("draggable","scroll",{start:function(c,d){var b=a(this).data("draggable");if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!="HTML"){b.overflowOffset=b.scrollParent.offset()}},drag:function(d,e){var c=a(this).data("draggable"),f=c.options,b=false;if(c.scrollParent[0]!=document&&c.scrollParent[0].tagName!="HTML"){if(!f.axis||f.axis!="x"){if((c.overflowOffset.top+c.scrollParent[0].offsetHeight)-d.pageY<f.scrollSensitivity){c.scrollParent[0].scrollTop=b=c.scrollParent[0].scrollTop+f.scrollSpeed}else{if(d.pageY-c.overflowOffset.top<f.scrollSensitivity){c.scrollParent[0].scrollTop=b=c.scrollParent[0].scrollTop-f.scrollSpeed}}}if(!f.axis||f.axis!="y"){if((c.overflowOffset.left+c.scrollParent[0].offsetWidth)-d.pageX<f.scrollSensitivity){c.scrollParent[0].scrollLeft=b=c.scrollParent[0].scrollLeft+f.scrollSpeed}else{if(d.pageX-c.overflowOffset.left<f.scrollSensitivity){c.scrollParent[0].scrollLeft=b=c.scrollParent[0].scrollLeft-f.scrollSpeed}}}}else{if(!f.axis||f.axis!="x"){if(d.pageY-a(document).scrollTop()<f.scrollSensitivity){b=a(document).scrollTop(a(document).scrollTop()-f.scrollSpeed)}else{if(a(window).height()-(d.pageY-a(document).scrollTop())<f.scrollSensitivity){b=a(document).scrollTop(a(document).scrollTop()+f.scrollSpeed)}}}if(!f.axis||f.axis!="y"){if(d.pageX-a(document).scrollLeft()<f.scrollSensitivity){b=a(document).scrollLeft(a(document).scrollLeft()-f.scrollSpeed)}else{if(a(window).width()-(d.pageX-a(document).scrollLeft())<f.scrollSensitivity){b=a(document).scrollLeft(a(document).scrollLeft()+f.scrollSpeed)}}}}if(b!==false&&a.ui.ddmanager&&!f.dropBehaviour){a.ui.ddmanager.prepareOffsets(c,d)}}});a.ui.plugin.add("draggable","snap",{start:function(c,d){var b=a(this).data("draggable"),e=b.options;b.snapElements=[];a(e.snap.constructor!=String?(e.snap.items||":data(draggable)"):e.snap).each(function(){var g=a(this);var f=g.offset();if(this!=b.element[0]){b.snapElements.push({item:this,width:g.outerWidth(),height:g.outerHeight(),top:f.top,left:f.left})}})},drag:function(u,p){var g=a(this).data("draggable"),q=g.options;var y=q.snapTolerance;var x=p.offset.left,w=x+g.helperProportions.width,f=p.offset.top,e=f+g.helperProportions.height;for(var v=g.snapElements.length-1;v>=0;v--){var s=g.snapElements[v].left,n=s+g.snapElements[v].width,m=g.snapElements[v].top,A=m+g.snapElements[v].height;if(!((s-y<x&&x<n+y&&m-y<f&&f<A+y)||(s-y<x&&x<n+y&&m-y<e&&e<A+y)||(s-y<w&&w<n+y&&m-y<f&&f<A+y)||(s-y<w&&w<n+y&&m-y<e&&e<A+y))){if(g.snapElements[v].snapping){(g.options.snap.release&&g.options.snap.release.call(g.element,u,a.extend(g._uiHash(),{snapItem:g.snapElements[v].item})))}g.snapElements[v].snapping=false;continue}if(q.snapMode!="inner"){var c=Math.abs(m-e)<=y;var z=Math.abs(A-f)<=y;var j=Math.abs(s-w)<=y;var k=Math.abs(n-x)<=y;if(c){p.position.top=g._convertPositionTo("relative",{top:m-g.helperProportions.height,left:0}).top-g.margins.top}if(z){p.position.top=g._convertPositionTo("relative",{top:A,left:0}).top-g.margins.top}if(j){p.position.left=g._convertPositionTo("relative",{top:0,left:s-g.helperProportions.width}).left-g.margins.left}if(k){p.position.left=g._convertPositionTo("relative",{top:0,left:n}).left-g.margins.left}}var h=(c||z||j||k);if(q.snapMode!="outer"){var c=Math.abs(m-f)<=y;var z=Math.abs(A-e)<=y;var j=Math.abs(s-x)<=y;var k=Math.abs(n-w)<=y;if(c){p.position.top=g._convertPositionTo("relative",{top:m,left:0}).top-g.margins.top}if(z){p.position.top=g._convertPositionTo("relative",{top:A-g.helperProportions.height,left:0}).top-g.margins.top}if(j){p.position.left=g._convertPositionTo("relative",{top:0,left:s}).left-g.margins.left}if(k){p.position.left=g._convertPositionTo("relative",{top:0,left:n-g.helperProportions.width}).left-g.margins.left}}if(!g.snapElements[v].snapping&&(c||z||j||k||h)){(g.options.snap.snap&&g.options.snap.snap.call(g.element,u,a.extend(g._uiHash(),{snapItem:g.snapElements[v].item})))}g.snapElements[v].snapping=(c||z||j||k||h)}}});a.ui.plugin.add("draggable","stack",{start:function(b,c){var e=a(this).data("draggable").options;var d=a.makeArray(a(e.stack.group)).sort(function(g,f){return(parseInt(a(g).css("zIndex"),10)||e.stack.min)-(parseInt(a(f).css("zIndex"),10)||e.stack.min)});a(d).each(function(f){this.style.zIndex=e.stack.min+f});this[0].style.zIndex=e.stack.min+d.length}});a.ui.plugin.add("draggable","zIndex",{start:function(c,d){var b=a(d.helper),e=a(this).data("draggable").options;if(b.css("zIndex")){e._zIndex=b.css("zIndex")}b.css("zIndex",e.zIndex)},stop:function(b,c){var d=a(this).data("draggable").options;if(d._zIndex){a(c.helper).css("zIndex",d._zIndex)}}})})(jQuery);;/*
+ * jQuery UI Droppable 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Droppables
+ *
+ * Depends:
+ *	ui.core.js
+ *	ui.draggable.js
+ */
+(function(a){a.widget("ui.droppable",{_init:function(){var c=this.options,b=c.accept;this.isover=0;this.isout=1;this.options.accept=this.options.accept&&a.isFunction(this.options.accept)?this.options.accept:function(e){return e.is(b)};this.proportions={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight};a.ui.ddmanager.droppables[this.options.scope]=a.ui.ddmanager.droppables[this.options.scope]||[];a.ui.ddmanager.droppables[this.options.scope].push(this);(this.options.addClasses&&this.element.addClass("ui-droppable"))},destroy:function(){var b=a.ui.ddmanager.droppables[this.options.scope];for(var c=0;c<b.length;c++){if(b[c]==this){b.splice(c,1)}}this.element.removeClass("ui-droppable ui-droppable-disabled").removeData("droppable").unbind(".droppable")},_setData:function(b,c){if(b=="accept"){this.options.accept=c&&a.isFunction(c)?c:function(e){return e.is(c)}}else{a.widget.prototype._setData.apply(this,arguments)}},_activate:function(c){var b=a.ui.ddmanager.current;if(this.options.activeClass){this.element.addClass(this.options.activeClass)}(b&&this._trigger("activate",c,this.ui(b)))},_deactivate:function(c){var b=a.ui.ddmanager.current;if(this.options.activeClass){this.element.removeClass(this.options.activeClass)}(b&&this._trigger("deactivate",c,this.ui(b)))},_over:function(c){var b=a.ui.ddmanager.current;if(!b||(b.currentItem||b.element)[0]==this.element[0]){return}if(this.options.accept.call(this.element[0],(b.currentItem||b.element))){if(this.options.hoverClass){this.element.addClass(this.options.hoverClass)}this._trigger("over",c,this.ui(b))}},_out:function(c){var b=a.ui.ddmanager.current;if(!b||(b.currentItem||b.element)[0]==this.element[0]){return}if(this.options.accept.call(this.element[0],(b.currentItem||b.element))){if(this.options.hoverClass){this.element.removeClass(this.options.hoverClass)}this._trigger("out",c,this.ui(b))}},_drop:function(c,d){var b=d||a.ui.ddmanager.current;if(!b||(b.currentItem||b.element)[0]==this.element[0]){return false}var e=false;this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function(){var f=a.data(this,"droppable");if(f.options.greedy&&a.ui.intersect(b,a.extend(f,{offset:f.element.offset()}),f.options.tolerance)){e=true;return false}});if(e){return false}if(this.options.accept.call(this.element[0],(b.currentItem||b.element))){if(this.options.activeClass){this.element.removeClass(this.options.activeClass)}if(this.options.hoverClass){this.element.removeClass(this.options.hoverClass)}this._trigger("drop",c,this.ui(b));return this.element}return false},ui:function(b){return{draggable:(b.currentItem||b.element),helper:b.helper,position:b.position,absolutePosition:b.positionAbs,offset:b.positionAbs}}});a.extend(a.ui.droppable,{version:"1.7.2",eventPrefix:"drop",defaults:{accept:"*",activeClass:false,addClasses:true,greedy:false,hoverClass:false,scope:"default",tolerance:"intersect"}});a.ui.intersect=function(q,j,o){if(!j.offset){return false}var e=(q.positionAbs||q.position.absolute).left,d=e+q.helperProportions.width,n=(q.positionAbs||q.position.absolute).top,m=n+q.helperProportions.height;var g=j.offset.left,c=g+j.proportions.width,p=j.offset.top,k=p+j.proportions.height;switch(o){case"fit":return(g<e&&d<c&&p<n&&m<k);break;case"intersect":return(g<e+(q.helperProportions.width/2)&&d-(q.helperProportions.width/2)<c&&p<n+(q.helperProportions.height/2)&&m-(q.helperProportions.height/2)<k);break;case"pointer":var h=((q.positionAbs||q.position.absolute).left+(q.clickOffset||q.offset.click).left),i=((q.positionAbs||q.position.absolute).top+(q.clickOffset||q.offset.click).top),f=a.ui.isOver(i,h,p,g,j.proportions.height,j.proportions.width);return f;break;case"touch":return((n>=p&&n<=k)||(m>=p&&m<=k)||(n<p&&m>k))&&((e>=g&&e<=c)||(d>=g&&d<=c)||(e<g&&d>c));break;default:return false;break}};a.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,g){var b=a.ui.ddmanager.droppables[e.options.scope];var f=g?g.type:null;var h=(e.currentItem||e.element).find(":data(droppable)").andSelf();droppablesLoop:for(var d=0;d<b.length;d++){if(b[d].options.disabled||(e&&!b[d].options.accept.call(b[d].element[0],(e.currentItem||e.element)))){continue}for(var c=0;c<h.length;c++){if(h[c]==b[d].element[0]){b[d].proportions.height=0;continue droppablesLoop}}b[d].visible=b[d].element.css("display")!="none";if(!b[d].visible){continue}b[d].offset=b[d].element.offset();b[d].proportions={width:b[d].element[0].offsetWidth,height:b[d].element[0].offsetHeight};if(f=="mousedown"){b[d]._activate.call(b[d],g)}}},drop:function(b,c){var d=false;a.each(a.ui.ddmanager.droppables[b.options.scope],function(){if(!this.options){return}if(!this.options.disabled&&this.visible&&a.ui.intersect(b,this,this.options.tolerance)){d=this._drop.call(this,c)}if(!this.options.disabled&&this.visible&&this.options.accept.call(this.element[0],(b.currentItem||b.element))){this.isout=1;this.isover=0;this._deactivate.call(this,c)}});return d},drag:function(b,c){if(b.options.refreshPositions){a.ui.ddmanager.prepareOffsets(b,c)}a.each(a.ui.ddmanager.droppables[b.options.scope],function(){if(this.options.disabled||this.greedyChild||!this.visible){return}var e=a.ui.intersect(b,this,this.options.tolerance);var g=!e&&this.isover==1?"isout":(e&&this.isover==0?"isover":null);if(!g){return}var f;if(this.options.greedy){var d=this.element.parents(":data(droppable):eq(0)");if(d.length){f=a.data(d[0],"droppable");f.greedyChild=(g=="isover"?1:0)}}if(f&&g=="isover"){f.isover=0;f.isout=1;f._out.call(f,c)}this[g]=1;this[g=="isout"?"isover":"isout"]=0;this[g=="isover"?"_over":"_out"].call(this,c);if(f&&g=="isout"){f.isout=0;f.isover=1;f._over.call(f,c)}})}}})(jQuery);;/*
+ * jQuery UI Resizable 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Resizables
+ *
+ * Depends:
+ *	ui.core.js
+ */
+(function(c){c.widget("ui.resizable",c.extend({},c.ui.mouse,{_init:function(){var e=this,j=this.options;this.element.addClass("ui-resizable");c.extend(this,{_aspectRatio:!!(j.aspectRatio),aspectRatio:j.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:j.helper||j.ghost||j.animate?j.helper||"ui-resizable-helper":null});if(this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)){if(/relative/.test(this.element.css("position"))&&c.browser.opera){this.element.css({position:"relative",top:"auto",left:"auto"})}this.element.wrap(c('<div class="ui-wrapper" style="overflow: hidden;"></div>').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")}));this.element=this.element.parent().data("resizable",this.element.data("resizable"));this.elementIsWrapper=true;this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")});this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0});this.originalResizeStyle=this.originalElement.css("resize");this.originalElement.css("resize","none");this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"}));this.originalElement.css({margin:this.originalElement.css("margin")});this._proportionallyResize()}this.handles=j.handles||(!c(".ui-resizable-handle",this.element).length?"e,s,se":{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"});if(this.handles.constructor==String){if(this.handles=="all"){this.handles="n,e,s,w,se,sw,ne,nw"}var k=this.handles.split(",");this.handles={};for(var f=0;f<k.length;f++){var h=c.trim(k[f]),d="ui-resizable-"+h;var g=c('<div class="ui-resizable-handle '+d+'"></div>');if(/sw|se|ne|nw/.test(h)){g.css({zIndex:++j.zIndex})}if("se"==h){g.addClass("ui-icon ui-icon-gripsmall-diagonal-se")}this.handles[h]=".ui-resizable-"+h;this.element.append(g)}}this._renderAxis=function(p){p=p||this.element;for(var m in this.handles){if(this.handles[m].constructor==String){this.handles[m]=c(this.handles[m],this.element).show()}if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var n=c(this.handles[m],this.element),o=0;o=/sw|ne|nw|se|n|s/.test(m)?n.outerHeight():n.outerWidth();var l=["padding",/ne|nw|n/.test(m)?"Top":/se|sw|s/.test(m)?"Bottom":/^e$/.test(m)?"Right":"Left"].join("");p.css(l,o);this._proportionallyResize()}if(!c(this.handles[m]).length){continue}}};this._renderAxis(this.element);this._handles=c(".ui-resizable-handle",this.element).disableSelection();this._handles.mouseover(function(){if(!e.resizing){if(this.className){var i=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)}e.axis=i&&i[1]?i[1]:"se"}});if(j.autoHide){this._handles.hide();c(this.element).addClass("ui-resizable-autohide").hover(function(){c(this).removeClass("ui-resizable-autohide");e._handles.show()},function(){if(!e.resizing){c(this).addClass("ui-resizable-autohide");e._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy();var d=function(f){c(f).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){d(this.element);var e=this.element;e.parent().append(this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")})).end().remove()}this.originalElement.css("resize",this.originalResizeStyle);d(this.originalElement)},_mouseCapture:function(e){var f=false;for(var d in this.handles){if(c(this.handles[d])[0]==e.target){f=true}}return this.options.disabled||!!f},_mouseStart:function(f){var i=this.options,e=this.element.position(),d=this.element;this.resizing=true;this.documentScroll={top:c(document).scrollTop(),left:c(document).scrollLeft()};if(d.is(".ui-draggable")||(/absolute/).test(d.css("position"))){d.css({position:"absolute",top:e.top,left:e.left})}if(c.browser.opera&&(/relative/).test(d.css("position"))){d.css({position:"relative",top:"auto",left:"auto"})}this._renderProxy();var j=b(this.helper.css("left")),g=b(this.helper.css("top"));if(i.containment){j+=c(i.containment).scrollLeft()||0;g+=c(i.containment).scrollTop()||0}this.offset=this.helper.offset();this.position={left:j,top:g};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:j,top:g};this.sizeDiff={width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:f.pageX,top:f.pageY};this.aspectRatio=(typeof i.aspectRatio=="number")?i.aspectRatio:((this.originalSize.width/this.originalSize.height)||1);var h=c(".ui-resizable-"+this.axis).css("cursor");c("body").css("cursor",h=="auto"?this.axis+"-resize":h);d.addClass("ui-resizable-resizing");this._propagate("start",f);return true},_mouseDrag:function(d){var g=this.helper,f=this.options,l={},p=this,i=this.originalMousePosition,m=this.axis;var q=(d.pageX-i.left)||0,n=(d.pageY-i.top)||0;var h=this._change[m];if(!h){return false}var k=h.apply(this,[d,q,n]),j=c.browser.msie&&c.browser.version<7,e=this.sizeDiff;if(this._aspectRatio||d.shiftKey){k=this._updateRatio(k,d)}k=this._respectSize(k,d);this._propagate("resize",d);g.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});if(!this._helper&&this._proportionallyResizeElements.length){this._proportionallyResize()}this._updateCache(k);this._trigger("resize",d,this.ui());return false},_mouseStop:function(g){this.resizing=false;var h=this.options,l=this;if(this._helper){var f=this._proportionallyResizeElements,d=f.length&&(/textarea/i).test(f[0].nodeName),e=d&&c.ui.hasScroll(f[0],"left")?0:l.sizeDiff.height,j=d?0:l.sizeDiff.width;var m={width:(l.size.width-j),height:(l.size.height-e)},i=(parseInt(l.element.css("left"),10)+(l.position.left-l.originalPosition.left))||null,k=(parseInt(l.element.css("top"),10)+(l.position.top-l.originalPosition.top))||null;if(!h.animate){this.element.css(c.extend(m,{top:k,left:i}))}l.helper.height(l.size.height);l.helper.width(l.size.width);if(this._helper&&!h.animate){this._proportionallyResize()}}c("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop",g);if(this._helper){this.helper.remove()}return false},_updateCache:function(d){var e=this.options;this.offset=this.helper.offset();if(a(d.left)){this.position.left=d.left}if(a(d.top)){this.position.top=d.top}if(a(d.height)){this.size.height=d.height}if(a(d.width)){this.size.width=d.width}},_updateRatio:function(g,f){var h=this.options,i=this.position,e=this.size,d=this.axis;if(g.height){g.width=(e.height*this.aspectRatio)}else{if(g.width){g.height=(e.width/this.aspectRatio)}}if(d=="sw"){g.left=i.left+(e.width-g.width);g.top=null}if(d=="nw"){g.top=i.top+(e.height-g.height);g.left=i.left+(e.width-g.width)}return g},_respectSize:function(k,f){var i=this.helper,h=this.options,q=this._aspectRatio||f.shiftKey,p=this.axis,s=a(k.width)&&h.maxWidth&&(h.maxWidth<k.width),l=a(k.height)&&h.maxHeight&&(h.maxHeight<k.height),g=a(k.width)&&h.minWidth&&(h.minWidth>k.width),r=a(k.height)&&h.minHeight&&(h.minHeight>k.height);if(g){k.width=h.minWidth}if(r){k.height=h.minHeight}if(s){k.width=h.maxWidth}if(l){k.height=h.maxHeight}var e=this.originalPosition.left+this.originalSize.width,n=this.position.top+this.size.height;var j=/sw|nw|w/.test(p),d=/nw|ne|n/.test(p);if(g&&j){k.left=e-h.minWidth}if(s&&j){k.left=e-h.maxWidth}if(r&&d){k.top=n-h.minHeight}if(l&&d){k.top=n-h.maxHeight}var m=!k.width&&!k.height;if(m&&!k.left&&k.top){k.top=null}else{if(m&&!k.top&&k.left){k.left=null}}return k},_proportionallyResize:function(){var j=this.options;if(!this._proportionallyResizeElements.length){return}var f=this.helper||this.element;for(var e=0;e<this._proportionallyResizeElements.length;e++){var g=this._proportionallyResizeElements[e];if(!this.borderDif){var d=[g.css("borderTopWidth"),g.css("borderRightWidth"),g.css("borderBottomWidth"),g.css("borderLeftWidth")],h=[g.css("paddingTop"),g.css("paddingRight"),g.css("paddingBottom"),g.css("paddingLeft")];this.borderDif=c.map(d,function(k,m){var l=parseInt(k,10)||0,n=parseInt(h[m],10)||0;return l+n})}if(c.browser.msie&&!(!(c(f).is(":hidden")||c(f).parents(":hidden").length))){continue}g.css({height:(f.height()-this.borderDif[0]-this.borderDif[2])||0,width:(f.width()-this.borderDif[1]-this.borderDif[3])||0})}},_renderProxy:function(){var e=this.element,h=this.options;this.elementOffset=e.offset();if(this._helper){this.helper=this.helper||c('<div style="overflow:hidden;"></div>');var d=c.browser.msie&&c.browser.version<7,f=(d?1:0),g=(d?2:-1);this.helper.addClass(this._helper).css({width:this.element.outerWidth()+g,height:this.element.outerHeight()+g,position:"absolute",left:this.elementOffset.left-f+"px",top:this.elementOffset.top-f+"px",zIndex:++h.zIndex});this.helper.appendTo("body").disableSelection()}else{this.helper=this.element}},_change:{e:function(f,e,d){return{width:this.originalSize.width+e}},w:function(g,e,d){var i=this.options,f=this.originalSize,h=this.originalPosition;return{left:h.left+e,width:f.width-e}},n:function(g,e,d){var i=this.options,f=this.originalSize,h=this.originalPosition;return{top:h.top+d,height:f.height-d}},s:function(f,e,d){return{height:this.originalSize.height+d}},se:function(f,e,d){return c.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[f,e,d]))},sw:function(f,e,d){return c.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[f,e,d]))},ne:function(f,e,d){return c.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[f,e,d]))},nw:function(f,e,d){return c.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[f,e,d]))}},_propagate:function(e,d){c.ui.plugin.call(this,e,[d,this.ui()]);(e!="resize"&&this._trigger(e,d,this.ui()))},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}));c.extend(c.ui.resizable,{version:"1.7.2",eventPrefix:"resize",defaults:{alsoResize:false,animate:false,animateDuration:"slow",animateEasing:"swing",aspectRatio:false,autoHide:false,cancel:":input,option",containment:false,delay:0,distance:1,ghost:false,grid:false,handles:"e,s,se",helper:false,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:1000}});c.ui.plugin.add("resizable","alsoResize",{start:function(e,f){var d=c(this).data("resizable"),g=d.options;_store=function(h){c(h).each(function(){c(this).data("resizable-alsoresize",{width:parseInt(c(this).width(),10),height:parseInt(c(this).height(),10),left:parseInt(c(this).css("left"),10),top:parseInt(c(this).css("top"),10)})})};if(typeof(g.alsoResize)=="object"&&!g.alsoResize.parentNode){if(g.alsoResize.length){g.alsoResize=g.alsoResize[0];_store(g.alsoResize)}else{c.each(g.alsoResize,function(h,i){_store(h)})}}else{_store(g.alsoResize)}},resize:function(f,h){var e=c(this).data("resizable"),i=e.options,g=e.originalSize,k=e.originalPosition;var j={height:(e.size.height-g.height)||0,width:(e.size.width-g.width)||0,top:(e.position.top-k.top)||0,left:(e.position.left-k.left)||0},d=function(l,m){c(l).each(function(){var p=c(this),q=c(this).data("resizable-alsoresize"),o={},n=m&&m.length?m:["width","height","top","left"];c.each(n||["width","height","top","left"],function(r,t){var s=(q[t]||0)+(j[t]||0);if(s&&s>=0){o[t]=s||null}});if(/relative/.test(p.css("position"))&&c.browser.opera){e._revertToRelativePosition=true;p.css({position:"absolute",top:"auto",left:"auto"})}p.css(o)})};if(typeof(i.alsoResize)=="object"&&!i.alsoResize.nodeType){c.each(i.alsoResize,function(l,m){d(l,m)})}else{d(i.alsoResize)}},stop:function(e,f){var d=c(this).data("resizable");if(d._revertToRelativePosition&&c.browser.opera){d._revertToRelativePosition=false;el.css({position:"relative"})}c(this).removeData("resizable-alsoresize-start")}});c.ui.plugin.add("resizable","animate",{stop:function(h,m){var n=c(this).data("resizable"),i=n.options;var g=n._proportionallyResizeElements,d=g.length&&(/textarea/i).test(g[0].nodeName),e=d&&c.ui.hasScroll(g[0],"left")?0:n.sizeDiff.height,k=d?0:n.sizeDiff.width;var f={width:(n.size.width-k),height:(n.size.height-e)},j=(parseInt(n.element.css("left"),10)+(n.position.left-n.originalPosition.left))||null,l=(parseInt(n.element.css("top"),10)+(n.position.top-n.originalPosition.top))||null;n.element.animate(c.extend(f,l&&j?{top:l,left:j}:{}),{duration:i.animateDuration,easing:i.animateEasing,step:function(){var o={width:parseInt(n.element.css("width"),10),height:parseInt(n.element.css("height"),10),top:parseInt(n.element.css("top"),10),left:parseInt(n.element.css("left"),10)};if(g&&g.length){c(g[0]).css({width:o.width,height:o.height})}n._updateCache(o);n._propagate("resize",h)}})}});c.ui.plugin.add("resizable","containment",{start:function(e,q){var s=c(this).data("resizable"),i=s.options,k=s.element;var f=i.containment,j=(f instanceof c)?f.get(0):(/parent/.test(f))?k.parent().get(0):f;if(!j){return}s.containerElement=c(j);if(/document/.test(f)||f==document){s.containerOffset={left:0,top:0};s.containerPosition={left:0,top:0};s.parentData={element:c(document),left:0,top:0,width:c(document).width(),height:c(document).height()||document.body.parentNode.scrollHeight}}else{var m=c(j),h=[];c(["Top","Right","Left","Bottom"]).each(function(p,o){h[p]=b(m.css("padding"+o))});s.containerOffset=m.offset();s.containerPosition=m.position();s.containerSize={height:(m.innerHeight()-h[3]),width:(m.innerWidth()-h[1])};var n=s.containerOffset,d=s.containerSize.height,l=s.containerSize.width,g=(c.ui.hasScroll(j,"left")?j.scrollWidth:l),r=(c.ui.hasScroll(j)?j.scrollHeight:d);s.parentData={element:j,left:n.left,top:n.top,width:g,height:r}}},resize:function(f,p){var s=c(this).data("resizable"),h=s.options,e=s.containerSize,n=s.containerOffset,l=s.size,m=s.position,q=s._aspectRatio||f.shiftKey,d={top:0,left:0},g=s.containerElement;if(g[0]!=document&&(/static/).test(g.css("position"))){d=n}if(m.left<(s._helper?n.left:0)){s.size.width=s.size.width+(s._helper?(s.position.left-n.left):(s.position.left-d.left));if(q){s.size.height=s.size.width/h.aspectRatio}s.position.left=h.helper?n.left:0}if(m.top<(s._helper?n.top:0)){s.size.height=s.size.height+(s._helper?(s.position.top-n.top):s.position.top);if(q){s.size.width=s.size.height*h.aspectRatio}s.position.top=s._helper?n.top:0}s.offset.left=s.parentData.left+s.position.left;s.offset.top=s.parentData.top+s.position.top;var k=Math.abs((s._helper?s.offset.left-d.left:(s.offset.left-d.left))+s.sizeDiff.width),r=Math.abs((s._helper?s.offset.top-d.top:(s.offset.top-n.top))+s.sizeDiff.height);var j=s.containerElement.get(0)==s.element.parent().get(0),i=/relative|absolute/.test(s.containerElement.css("position"));if(j&&i){k-=s.parentData.left}if(k+s.size.width>=s.parentData.width){s.size.width=s.parentData.width-k;if(q){s.size.height=s.size.width/s.aspectRatio}}if(r+s.size.height>=s.parentData.height){s.size.height=s.parentData.height-r;if(q){s.size.width=s.size.height*s.aspectRatio}}},stop:function(e,m){var p=c(this).data("resizable"),f=p.options,k=p.position,l=p.containerOffset,d=p.containerPosition,g=p.containerElement;var i=c(p.helper),q=i.offset(),n=i.outerWidth()-p.sizeDiff.width,j=i.outerHeight()-p.sizeDiff.height;if(p._helper&&!f.animate&&(/relative/).test(g.css("position"))){c(this).css({left:q.left-d.left-l.left,width:n,height:j})}if(p._helper&&!f.animate&&(/static/).test(g.css("position"))){c(this).css({left:q.left-d.left-l.left,width:n,height:j})}}});c.ui.plugin.add("resizable","ghost",{start:function(f,g){var d=c(this).data("resizable"),h=d.options,e=d.size;d.ghost=d.originalElement.clone();d.ghost.css({opacity:0.25,display:"block",position:"relative",height:e.height,width:e.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof h.ghost=="string"?h.ghost:"");d.ghost.appendTo(d.helper)},resize:function(e,f){var d=c(this).data("resizable"),g=d.options;if(d.ghost){d.ghost.css({position:"relative",height:d.size.height,width:d.size.width})}},stop:function(e,f){var d=c(this).data("resizable"),g=d.options;if(d.ghost&&d.helper){d.helper.get(0).removeChild(d.ghost.get(0))}}});c.ui.plugin.add("resizable","grid",{resize:function(d,l){var n=c(this).data("resizable"),g=n.options,j=n.size,h=n.originalSize,i=n.originalPosition,m=n.axis,k=g._aspectRatio||d.shiftKey;g.grid=typeof g.grid=="number"?[g.grid,g.grid]:g.grid;var f=Math.round((j.width-h.width)/(g.grid[0]||1))*(g.grid[0]||1),e=Math.round((j.height-h.height)/(g.grid[1]||1))*(g.grid[1]||1);if(/^(se|s|e)$/.test(m)){n.size.width=h.width+f;n.size.height=h.height+e}else{if(/^(ne)$/.test(m)){n.size.width=h.width+f;n.size.height=h.height+e;n.position.top=i.top-e}else{if(/^(sw)$/.test(m)){n.size.width=h.width+f;n.size.height=h.height+e;n.position.left=i.left-f}else{n.size.width=h.width+f;n.size.height=h.height+e;n.position.top=i.top-e;n.position.left=i.left-f}}}}});var b=function(d){return parseInt(d,10)||0};var a=function(d){return !isNaN(parseInt(d,10))}})(jQuery);;/*
+ * jQuery UI Selectable 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Selectables
+ *
+ * Depends:
+ *	ui.core.js
+ */
+(function(a){a.widget("ui.selectable",a.extend({},a.ui.mouse,{_init:function(){var b=this;this.element.addClass("ui-selectable");this.dragged=false;var c;this.refresh=function(){c=a(b.options.filter,b.element[0]);c.each(function(){var d=a(this);var e=d.offset();a.data(this,"selectable-item",{element:this,$element:d,left:e.left,top:e.top,right:e.left+d.outerWidth(),bottom:e.top+d.outerHeight(),startselected:false,selected:d.hasClass("ui-selected"),selecting:d.hasClass("ui-selecting"),unselecting:d.hasClass("ui-unselecting")})})};this.refresh();this.selectees=c.addClass("ui-selectee");this._mouseInit();this.helper=a(document.createElement("div")).css({border:"1px dotted black"}).addClass("ui-selectable-helper")},destroy:function(){this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable");this._mouseDestroy()},_mouseStart:function(d){var b=this;this.opos=[d.pageX,d.pageY];if(this.options.disabled){return}var c=this.options;this.selectees=a(c.filter,this.element[0]);this._trigger("start",d);a(c.appendTo).append(this.helper);this.helper.css({"z-index":100,position:"absolute",left:d.clientX,top:d.clientY,width:0,height:0});if(c.autoRefresh){this.refresh()}this.selectees.filter(".ui-selected").each(function(){var e=a.data(this,"selectable-item");e.startselected=true;if(!d.metaKey){e.$element.removeClass("ui-selected");e.selected=false;e.$element.addClass("ui-unselecting");e.unselecting=true;b._trigger("unselecting",d,{unselecting:e.element})}});a(d.target).parents().andSelf().each(function(){var e=a.data(this,"selectable-item");if(e){e.$element.removeClass("ui-unselecting").addClass("ui-selecting");e.unselecting=false;e.selecting=true;e.selected=true;b._trigger("selecting",d,{selecting:e.element});return false}})},_mouseDrag:function(i){var c=this;this.dragged=true;if(this.options.disabled){return}var e=this.options;var d=this.opos[0],h=this.opos[1],b=i.pageX,g=i.pageY;if(d>b){var f=b;b=d;d=f}if(h>g){var f=g;g=h;h=f}this.helper.css({left:d,top:h,width:b-d,height:g-h});this.selectees.each(function(){var j=a.data(this,"selectable-item");if(!j||j.element==c.element[0]){return}var k=false;if(e.tolerance=="touch"){k=(!(j.left>b||j.right<d||j.top>g||j.bottom<h))}else{if(e.tolerance=="fit"){k=(j.left>d&&j.right<b&&j.top>h&&j.bottom<g)}}if(k){if(j.selected){j.$element.removeClass("ui-selected");j.selected=false}if(j.unselecting){j.$element.removeClass("ui-unselecting");j.unselecting=false}if(!j.selecting){j.$element.addClass("ui-selecting");j.selecting=true;c._trigger("selecting",i,{selecting:j.element})}}else{if(j.selecting){if(i.metaKey&&j.startselected){j.$element.removeClass("ui-selecting");j.selecting=false;j.$element.addClass("ui-selected");j.selected=true}else{j.$element.removeClass("ui-selecting");j.selecting=false;if(j.startselected){j.$element.addClass("ui-unselecting");j.unselecting=true}c._trigger("unselecting",i,{unselecting:j.element})}}if(j.selected){if(!i.metaKey&&!j.startselected){j.$element.removeClass("ui-selected");j.selected=false;j.$element.addClass("ui-unselecting");j.unselecting=true;c._trigger("unselecting",i,{unselecting:j.element})}}}});return false},_mouseStop:function(d){var b=this;this.dragged=false;var c=this.options;a(".ui-unselecting",this.element[0]).each(function(){var e=a.data(this,"selectable-item");e.$element.removeClass("ui-unselecting");e.unselecting=false;e.startselected=false;b._trigger("unselected",d,{unselected:e.element})});a(".ui-selecting",this.element[0]).each(function(){var e=a.data(this,"selectable-item");e.$element.removeClass("ui-selecting").addClass("ui-selected");e.selecting=false;e.selected=true;e.startselected=true;b._trigger("selected",d,{selected:e.element})});this._trigger("stop",d);this.helper.remove();return false}}));a.extend(a.ui.selectable,{version:"1.7.2",defaults:{appendTo:"body",autoRefresh:true,cancel:":input,option",delay:0,distance:0,filter:"*",tolerance:"touch"}})})(jQuery);;/*
+ * jQuery UI Sortable 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Sortables
+ *
+ * Depends:
+ *	ui.core.js
+ */
+(function(a){a.widget("ui.sortable",a.extend({},a.ui.mouse,{_init:function(){var b=this.options;this.containerCache={};this.element.addClass("ui-sortable");this.refresh();this.floating=this.items.length?(/left|right/).test(this.items[0].item.css("float")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var b=this.items.length-1;b>=0;b--){this.items[b].item.removeData("sortable-item")}},_mouseCapture:function(e,f){if(this.reverting){return false}if(this.options.disabled||this.options.type=="static"){return false}this._refreshItems(e);var d=null,c=this,b=a(e.target).parents().each(function(){if(a.data(this,"sortable-item")==c){d=a(this);return false}});if(a.data(e.target,"sortable-item")==c){d=a(e.target)}if(!d){return false}if(this.options.handle&&!f){var g=false;a(this.options.handle,d).find("*").andSelf().each(function(){if(this==e.target){g=true}});if(!g){return false}}this.currentItem=d;this._removeCurrentsFromItems();return true},_mouseStart:function(e,f,b){var g=this.options,c=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(e);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");a.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(e);this.originalPageX=e.pageX;this.originalPageY=e.pageY;if(g.cursorAt){this._adjustOffsetFromHelper(g.cursorAt)}this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]};if(this.helper[0]!=this.currentItem[0]){this.currentItem.hide()}this._createPlaceholder();if(g.containment){this._setContainment()}if(g.cursor){if(a("body").css("cursor")){this._storedCursor=a("body").css("cursor")}a("body").css("cursor",g.cursor)}if(g.opacity){if(this.helper.css("opacity")){this._storedOpacity=this.helper.css("opacity")}this.helper.css("opacity",g.opacity)}if(g.zIndex){if(this.helper.css("zIndex")){this._storedZIndex=this.helper.css("zIndex")}this.helper.css("zIndex",g.zIndex)}if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){this.overflowOffset=this.scrollParent.offset()}this._trigger("start",e,this._uiHash());if(!this._preserveHelperProportions){this._cacheHelperProportions()}if(!b){for(var d=this.containers.length-1;d>=0;d--){this.containers[d]._trigger("activate",e,c._uiHash(this))}}if(a.ui.ddmanager){a.ui.ddmanager.current=this}if(a.ui.ddmanager&&!g.dropBehaviour){a.ui.ddmanager.prepareOffsets(this,e)}this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(e);return true},_mouseDrag:function(f){this.position=this._generatePosition(f);this.positionAbs=this._convertPositionTo("absolute");if(!this.lastPositionAbs){this.lastPositionAbs=this.positionAbs}if(this.options.scroll){var g=this.options,b=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if((this.overflowOffset.top+this.scrollParent[0].offsetHeight)-f.pageY<g.scrollSensitivity){this.scrollParent[0].scrollTop=b=this.scrollParent[0].scrollTop+g.scrollSpeed}else{if(f.pageY-this.overflowOffset.top<g.scrollSensitivity){this.scrollParent[0].scrollTop=b=this.scrollParent[0].scrollTop-g.scrollSpeed}}if((this.overflowOffset.left+this.scrollParent[0].offsetWidth)-f.pageX<g.scrollSensitivity){this.scrollParent[0].scrollLeft=b=this.scrollParent[0].scrollLeft+g.scrollSpeed}else{if(f.pageX-this.overflowOffset.left<g.scrollSensitivity){this.scrollParent[0].scrollLeft=b=this.scrollParent[0].scrollLeft-g.scrollSpeed}}}else{if(f.pageY-a(document).scrollTop()<g.scrollSensitivity){b=a(document).scrollTop(a(document).scrollTop()-g.scrollSpeed)}else{if(a(window).height()-(f.pageY-a(document).scrollTop())<g.scrollSensitivity){b=a(document).scrollTop(a(document).scrollTop()+g.scrollSpeed)}}if(f.pageX-a(document).scrollLeft()<g.scrollSensitivity){b=a(document).scrollLeft(a(document).scrollLeft()-g.scrollSpeed)}else{if(a(window).width()-(f.pageX-a(document).scrollLeft())<g.scrollSensitivity){b=a(document).scrollLeft(a(document).scrollLeft()+g.scrollSpeed)}}}if(b!==false&&a.ui.ddmanager&&!g.dropBehaviour){a.ui.ddmanager.prepareOffsets(this,f)}}this.positionAbs=this._convertPositionTo("absolute");if(!this.options.axis||this.options.axis!="y"){this.helper[0].style.left=this.position.left+"px"}if(!this.options.axis||this.options.axis!="x"){this.helper[0].style.top=this.position.top+"px"}for(var d=this.items.length-1;d>=0;d--){var e=this.items[d],c=e.item[0],h=this._intersectsWithPointer(e);if(!h){continue}if(c!=this.currentItem[0]&&this.placeholder[h==1?"next":"prev"]()[0]!=c&&!a.ui.contains(this.placeholder[0],c)&&(this.options.type=="semi-dynamic"?!a.ui.contains(this.element[0],c):true)){this.direction=h==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(e)){this._rearrange(f,e)}else{break}this._trigger("change",f,this._uiHash());break}}this._contactContainers(f);if(a.ui.ddmanager){a.ui.ddmanager.drag(this,f)}this._trigger("sort",f,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(c,d){if(!c){return}if(a.ui.ddmanager&&!this.options.dropBehaviour){a.ui.ddmanager.drop(this,c)}if(this.options.revert){var b=this;var e=b.placeholder.offset();b.reverting=true;a(this.helper).animate({left:e.left-this.offset.parent.left-b.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:e.top-this.offset.parent.top-b.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){b._clear(c)})}else{this._clear(c,d)}return false},cancel:function(){var b=this;if(this.dragging){this._mouseUp();if(this.options.helper=="original"){this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else{this.currentItem.show()}for(var c=this.containers.length-1;c>=0;c--){this.containers[c]._trigger("deactivate",null,b._uiHash(this));if(this.containers[c].containerCache.over){this.containers[c]._trigger("out",null,b._uiHash(this));this.containers[c].containerCache.over=0}}}if(this.placeholder[0].parentNode){this.placeholder[0].parentNode.removeChild(this.placeholder[0])}if(this.options.helper!="original"&&this.helper&&this.helper[0].parentNode){this.helper.remove()}a.extend(this,{helper:null,dragging:false,reverting:false,_noFinalSort:null});if(this.domPosition.prev){a(this.domPosition.prev).after(this.currentItem)}else{a(this.domPosition.parent).prepend(this.currentItem)}return true},serialize:function(d){var b=this._getItemsAsjQuery(d&&d.connected);var c=[];d=d||{};a(b).each(function(){var e=(a(d.item||this).attr(d.attribute||"id")||"").match(d.expression||(/(.+)[-=_](.+)/));if(e){c.push((d.key||e[1]+"[]")+"="+(d.key&&d.expression?e[1]:e[2]))}});return c.join("&")},toArray:function(d){var b=this._getItemsAsjQuery(d&&d.connected);var c=[];d=d||{};b.each(function(){c.push(a(d.item||this).attr(d.attribute||"id")||"")});return c},_intersectsWith:function(m){var e=this.positionAbs.left,d=e+this.helperProportions.width,k=this.positionAbs.top,j=k+this.helperProportions.height;var f=m.left,c=f+m.width,n=m.top,i=n+m.height;var o=this.offset.click.top,h=this.offset.click.left;var g=(k+o)>n&&(k+o)<i&&(e+h)>f&&(e+h)<c;if(this.options.tolerance=="pointer"||this.options.forcePointerForContainers||(this.options.tolerance!="pointer"&&this.helperProportions[this.floating?"width":"height"]>m[this.floating?"width":"height"])){return g}else{return(f<e+(this.helperProportions.width/2)&&d-(this.helperProportions.width/2)<c&&n<k+(this.helperProportions.height/2)&&j-(this.helperProportions.height/2)<i)}},_intersectsWithPointer:function(d){var e=a.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,d.top,d.height),c=a.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,d.left,d.width),g=e&&c,b=this._getDragVerticalDirection(),f=this._getDragHorizontalDirection();if(!g){return false}return this.floating?(((f&&f=="right")||b=="down")?2:1):(b&&(b=="down"?2:1))},_intersectsWithSides:function(e){var c=a.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,e.top+(e.height/2),e.height),d=a.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,e.left+(e.width/2),e.width),b=this._getDragVerticalDirection(),f=this._getDragHorizontalDirection();if(this.floating&&f){return((f=="right"&&d)||(f=="left"&&!d))}else{return b&&((b=="down"&&c)||(b=="up"&&!c))}},_getDragVerticalDirection:function(){var b=this.positionAbs.top-this.lastPositionAbs.top;return b!=0&&(b>0?"down":"up")},_getDragHorizontalDirection:function(){var b=this.positionAbs.left-this.lastPositionAbs.left;return b!=0&&(b>0?"right":"left")},refresh:function(b){this._refreshItems(b);this.refreshPositions()},_connectWith:function(){var b=this.options;return b.connectWith.constructor==String?[b.connectWith]:b.connectWith},_getItemsAsjQuery:function(b){var l=this;var g=[];var e=[];var h=this._connectWith();if(h&&b){for(var d=h.length-1;d>=0;d--){var k=a(h[d]);for(var c=k.length-1;c>=0;c--){var f=a.data(k[c],"sortable");if(f&&f!=this&&!f.options.disabled){e.push([a.isFunction(f.options.items)?f.options.items.call(f.element):a(f.options.items,f.element).not(".ui-sortable-helper"),f])}}}}e.push([a.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):a(this.options.items,this.element).not(".ui-sortable-helper"),this]);for(var d=e.length-1;d>=0;d--){e[d][0].each(function(){g.push(this)})}return a(g)},_removeCurrentsFromItems:function(){var d=this.currentItem.find(":data(sortable-item)");for(var c=0;c<this.items.length;c++){for(var b=0;b<d.length;b++){if(d[b]==this.items[c].item[0]){this.items.splice(c,1)}}}},_refreshItems:function(b){this.items=[];this.containers=[this];var h=this.items;var p=this;var f=[[a.isFunction(this.options.items)?this.options.items.call(this.element[0],b,{item:this.currentItem}):a(this.options.items,this.element),this]];var l=this._connectWith();if(l){for(var e=l.length-1;e>=0;e--){var m=a(l[e]);for(var d=m.length-1;d>=0;d--){var g=a.data(m[d],"sortable");if(g&&g!=this&&!g.options.disabled){f.push([a.isFunction(g.options.items)?g.options.items.call(g.element[0],b,{item:this.currentItem}):a(g.options.items,g.element),g]);this.containers.push(g)}}}}for(var e=f.length-1;e>=0;e--){var k=f[e][1];var c=f[e][0];for(var d=0,n=c.length;d<n;d++){var o=a(c[d]);o.data("sortable-item",k);h.push({item:o,instance:k,width:0,height:0,left:0,top:0})}}},refreshPositions:function(b){if(this.offsetParent&&this.helper){this.offset.parent=this._getParentOffset()}for(var d=this.items.length-1;d>=0;d--){var e=this.items[d];if(e.instance!=this.currentContainer&&this.currentContainer&&e.item[0]!=this.currentItem[0]){continue}var c=this.options.toleranceElement?a(this.options.toleranceElement,e.item):e.item;if(!b){e.width=c.outerWidth();e.height=c.outerHeight()}var f=c.offset();e.left=f.left;e.top=f.top}if(this.options.custom&&this.options.custom.refreshContainers){this.options.custom.refreshContainers.call(this)}else{for(var d=this.containers.length-1;d>=0;d--){var f=this.containers[d].element.offset();this.containers[d].containerCache.left=f.left;this.containers[d].containerCache.top=f.top;this.containers[d].containerCache.width=this.containers[d].element.outerWidth();this.containers[d].containerCache.height=this.containers[d].element.outerHeight()}}},_createPlaceholder:function(d){var b=d||this,e=b.options;if(!e.placeholder||e.placeholder.constructor==String){var c=e.placeholder;e.placeholder={element:function(){var f=a(document.createElement(b.currentItem[0].nodeName)).addClass(c||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!c){f.style.visibility="hidden"}return f},update:function(f,g){if(c&&!e.forcePlaceholderSize){return}if(!g.height()){g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10))}if(!g.width()){g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")||0,10))}}}}b.placeholder=a(e.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);e.placeholder.update(b,b.placeholder)},_contactContainers:function(d){for(var c=this.containers.length-1;c>=0;c--){if(this._intersectsWith(this.containers[c].containerCache)){if(!this.containers[c].containerCache.over){if(this.currentContainer!=this.containers[c]){var h=10000;var g=null;var e=this.positionAbs[this.containers[c].floating?"left":"top"];for(var b=this.items.length-1;b>=0;b--){if(!a.ui.contains(this.containers[c].element[0],this.items[b].item[0])){continue}var f=this.items[b][this.containers[c].floating?"left":"top"];if(Math.abs(f-e)<h){h=Math.abs(f-e);g=this.items[b]}}if(!g&&!this.options.dropOnEmpty){continue}this.currentContainer=this.containers[c];g?this._rearrange(d,g,null,true):this._rearrange(d,null,this.containers[c].element,true);this._trigger("change",d,this._uiHash());this.containers[c]._trigger("change",d,this._uiHash(this));this.options.placeholder.update(this.currentContainer,this.placeholder)}this.containers[c]._trigger("over",d,this._uiHash(this));this.containers[c].containerCache.over=1}}else{if(this.containers[c].containerCache.over){this.containers[c]._trigger("out",d,this._uiHash(this));this.containers[c].containerCache.over=0}}}},_createHelper:function(c){var d=this.options;var b=a.isFunction(d.helper)?a(d.helper.apply(this.element[0],[c,this.currentItem])):(d.helper=="clone"?this.currentItem.clone():this.currentItem);if(!b.parents("body").length){a(d.appendTo!="parent"?d.appendTo:this.currentItem[0].parentNode)[0].appendChild(b[0])}if(b[0]==this.currentItem[0]){this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}}if(b[0].style.width==""||d.forceHelperSize){b.width(this.currentItem.width())}if(b[0].style.height==""||d.forceHelperSize){b.height(this.currentItem.height())}return b},_adjustOffsetFromHelper:function(b){if(b.left!=undefined){this.offset.click.left=b.left+this.margins.left}if(b.right!=undefined){this.offset.click.left=this.helperProportions.width-b.right+this.margins.left}if(b.top!=undefined){this.offset.click.top=b.top+this.margins.top}if(b.bottom!=undefined){this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top}},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])){b.left+=this.scrollParent.scrollLeft();b.top+=this.scrollParent.scrollTop()}if((this.offsetParent[0]==document.body)||(this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)){b={top:0,left:0}}return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var b=this.currentItem.position();return{top:b.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:b.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else{return{top:0,left:0}}},_cacheMargins:function(){this.margins={left:(parseInt(this.currentItem.css("marginLeft"),10)||0),top:(parseInt(this.currentItem.css("marginTop"),10)||0)}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e=this.options;if(e.containment=="parent"){e.containment=this.helper[0].parentNode}if(e.containment=="document"||e.containment=="window"){this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,a(e.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a(e.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]}if(!(/^(document|window|parent)$/).test(e.containment)){var c=a(e.containment)[0];var d=a(e.containment).offset();var b=(a(c).css("overflow")!="hidden");this.containment=[d.left+(parseInt(a(c).css("borderLeftWidth"),10)||0)+(parseInt(a(c).css("paddingLeft"),10)||0)-this.margins.left,d.top+(parseInt(a(c).css("borderTopWidth"),10)||0)+(parseInt(a(c).css("paddingTop"),10)||0)-this.margins.top,d.left+(b?Math.max(c.scrollWidth,c.offsetWidth):c.offsetWidth)-(parseInt(a(c).css("borderLeftWidth"),10)||0)-(parseInt(a(c).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,d.top+(b?Math.max(c.scrollHeight,c.offsetHeight):c.offsetHeight)-(parseInt(a(c).css("borderTopWidth"),10)||0)-(parseInt(a(c).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}},_convertPositionTo:function(f,h){if(!h){h=this.position}var c=f=="absolute"?1:-1;var e=this.options,b=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=(/(html|body)/i).test(b[0].tagName);return{top:(h.top+this.offset.relative.top*c+this.offset.parent.top*c-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():(g?0:b.scrollTop()))*c)),left:(h.left+this.offset.relative.left*c+this.offset.parent.left*c-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:b.scrollLeft())*c))}},_generatePosition:function(e){var h=this.options,b=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,i=(/(html|body)/i).test(b[0].tagName);if(this.cssPosition=="relative"&&!(this.scrollParent[0]!=document&&this.scrollParent[0]!=this.offsetParent[0])){this.offset.relative=this._getRelativeOffset()}var d=e.pageX;var c=e.pageY;if(this.originalPosition){if(this.containment){if(e.pageX-this.offset.click.left<this.containment[0]){d=this.containment[0]+this.offset.click.left}if(e.pageY-this.offset.click.top<this.containment[1]){c=this.containment[1]+this.offset.click.top}if(e.pageX-this.offset.click.left>this.containment[2]){d=this.containment[2]+this.offset.click.left}if(e.pageY-this.offset.click.top>this.containment[3]){c=this.containment[3]+this.offset.click.top}}if(h.grid){var g=this.originalPageY+Math.round((c-this.originalPageY)/h.grid[1])*h.grid[1];c=this.containment?(!(g-this.offset.click.top<this.containment[1]||g-this.offset.click.top>this.containment[3])?g:(!(g-this.offset.click.top<this.containment[1])?g-h.grid[1]:g+h.grid[1])):g;var f=this.originalPageX+Math.round((d-this.originalPageX)/h.grid[0])*h.grid[0];d=this.containment?(!(f-this.offset.click.left<this.containment[0]||f-this.offset.click.left>this.containment[2])?f:(!(f-this.offset.click.left<this.containment[0])?f-h.grid[0]:f+h.grid[0])):f}}return{top:(c-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():(i?0:b.scrollTop())))),left:(d-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():i?0:b.scrollLeft())))}},_rearrange:function(g,f,c,e){c?c[0].appendChild(this.placeholder[0]):f.item[0].parentNode.insertBefore(this.placeholder[0],(this.direction=="down"?f.item[0]:f.item[0].nextSibling));this.counter=this.counter?++this.counter:1;var d=this,b=this.counter;window.setTimeout(function(){if(b==d.counter){d.refreshPositions(!e)}},0)},_clear:function(d,e){this.reverting=false;var f=[],b=this;if(!this._noFinalSort&&this.currentItem[0].parentNode){this.placeholder.before(this.currentItem)}this._noFinalSort=null;if(this.helper[0]==this.currentItem[0]){for(var c in this._storedCSS){if(this._storedCSS[c]=="auto"||this._storedCSS[c]=="static"){this._storedCSS[c]=""}}this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else{this.currentItem.show()}if(this.fromOutside&&!e){f.push(function(g){this._trigger("receive",g,this._uiHash(this.fromOutside))})}if((this.fromOutside||this.domPosition.prev!=this.currentItem.prev().not(".ui-sortable-helper")[0]||this.domPosition.parent!=this.currentItem.parent()[0])&&!e){f.push(function(g){this._trigger("update",g,this._uiHash())})}if(!a.ui.contains(this.element[0],this.currentItem[0])){if(!e){f.push(function(g){this._trigger("remove",g,this._uiHash())})}for(var c=this.containers.length-1;c>=0;c--){if(a.ui.contains(this.containers[c].element[0],this.currentItem[0])&&!e){f.push((function(g){return function(h){g._trigger("receive",h,this._uiHash(this))}}).call(this,this.containers[c]));f.push((function(g){return function(h){g._trigger("update",h,this._uiHash(this))}}).call(this,this.containers[c]))}}}for(var c=this.containers.length-1;c>=0;c--){if(!e){f.push((function(g){return function(h){g._trigger("deactivate",h,this._uiHash(this))}}).call(this,this.containers[c]))}if(this.containers[c].containerCache.over){f.push((function(g){return function(h){g._trigger("out",h,this._uiHash(this))}}).call(this,this.containers[c]));this.containers[c].containerCache.over=0}}if(this._storedCursor){a("body").css("cursor",this._storedCursor)}if(this._storedOpacity){this.helper.css("opacity",this._storedOpacity)}if(this._storedZIndex){this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex)}this.dragging=false;if(this.cancelHelperRemoval){if(!e){this._trigger("beforeStop",d,this._uiHash());for(var c=0;c<f.length;c++){f[c].call(this,d)}this._trigger("stop",d,this._uiHash())}return false}if(!e){this._trigger("beforeStop",d,this._uiHash())}this.placeholder[0].parentNode.removeChild(this.placeholder[0]);if(this.helper[0]!=this.currentItem[0]){this.helper.remove()}this.helper=null;if(!e){for(var c=0;c<f.length;c++){f[c].call(this,d)}this._trigger("stop",d,this._uiHash())}this.fromOutside=false;return true},_trigger:function(){if(a.widget.prototype._trigger.apply(this,arguments)===false){this.cancel()}},_uiHash:function(c){var b=c||this;return{helper:b.helper,placeholder:b.placeholder||a([]),position:b.position,absolutePosition:b.positionAbs,offset:b.positionAbs,item:b.currentItem,sender:c?c.element:null}}}));a.extend(a.ui.sortable,{getter:"serialize toArray",version:"1.7.2",eventPrefix:"sort",defaults:{appendTo:"parent",axis:false,cancel:":input,option",connectWith:false,containment:false,cursor:"auto",cursorAt:false,delay:0,distance:1,dropOnEmpty:true,forcePlaceholderSize:false,forceHelperSize:false,grid:false,handle:false,helper:"original",items:"> *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1000}})})(jQuery);;/*
+ * jQuery UI Accordion 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Accordion
+ *
+ * Depends:
+ *	ui.core.js
+ */
+(function(a){a.widget("ui.accordion",{_init:function(){var d=this.options,b=this;this.running=0;if(d.collapsible==a.ui.accordion.defaults.collapsible&&d.alwaysOpen!=a.ui.accordion.defaults.alwaysOpen){d.collapsible=!d.alwaysOpen}if(d.navigation){var c=this.element.find("a").filter(d.navigationFilter);if(c.length){if(c.filter(d.header).length){this.active=c}else{this.active=c.parent().parent().prev();c.addClass("ui-accordion-content-active")}}}this.element.addClass("ui-accordion ui-widget ui-helper-reset");if(this.element[0].nodeName=="UL"){this.element.children("li").addClass("ui-accordion-li-fix")}this.headers=this.element.find(d.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){a(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){a(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){a(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){a(this).removeClass("ui-state-focus")});this.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom");this.active=this._findActive(this.active||d.active).toggleClass("ui-state-default").toggleClass("ui-state-active").toggleClass("ui-corner-all").toggleClass("ui-corner-top");this.active.next().addClass("ui-accordion-content-active");a("<span/>").addClass("ui-icon "+d.icons.header).prependTo(this.headers);this.active.find(".ui-icon").toggleClass(d.icons.header).toggleClass(d.icons.headerSelected);if(a.browser.msie){this.element.find("a").css("zoom","1")}this.resize();this.element.attr("role","tablist");this.headers.attr("role","tab").bind("keydown",function(e){return b._keydown(e)}).next().attr("role","tabpanel");this.headers.not(this.active||"").attr("aria-expanded","false").attr("tabIndex","-1").next().hide();if(!this.active.length){this.headers.eq(0).attr("tabIndex","0")}else{this.active.attr("aria-expanded","true").attr("tabIndex","0")}if(!a.browser.safari){this.headers.find("a").attr("tabIndex","-1")}if(d.event){this.headers.bind((d.event)+".accordion",function(e){return b._clickHandler.call(b,e,this)})}},destroy:function(){var c=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role").unbind(".accordion").removeData("accordion");this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("tabindex");this.headers.find("a").removeAttr("tabindex");this.headers.children(".ui-icon").remove();var b=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active");if(c.autoHeight||c.fillHeight){b.css("height","")}},_setData:function(b,c){if(b=="alwaysOpen"){b="collapsible";c=!c}a.widget.prototype._setData.apply(this,arguments)},_keydown:function(e){var g=this.options,f=a.ui.keyCode;if(g.disabled||e.altKey||e.ctrlKey){return}var d=this.headers.length;var b=this.headers.index(e.target);var c=false;switch(e.keyCode){case f.RIGHT:case f.DOWN:c=this.headers[(b+1)%d];break;case f.LEFT:case f.UP:c=this.headers[(b-1+d)%d];break;case f.SPACE:case f.ENTER:return this._clickHandler({target:e.target},e.target)}if(c){a(e.target).attr("tabIndex","-1");a(c).attr("tabIndex","0");c.focus();return false}return true},resize:function(){var e=this.options,d;if(e.fillSpace){if(a.browser.msie){var b=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}d=this.element.parent().height();if(a.browser.msie){this.element.parent().css("overflow",b)}this.headers.each(function(){d-=a(this).outerHeight()});var c=0;this.headers.next().each(function(){c=Math.max(c,a(this).innerHeight()-a(this).height())}).height(Math.max(0,d-c)).css("overflow","auto")}else{if(e.autoHeight){d=0;this.headers.next().each(function(){d=Math.max(d,a(this).outerHeight())}).height(d)}}},activate:function(b){var c=this._findActive(b)[0];this._clickHandler({target:c},c)},_findActive:function(b){return b?typeof b=="number"?this.headers.filter(":eq("+b+")"):this.headers.not(this.headers.not(b)):b===false?a([]):this.headers.filter(":eq(0)")},_clickHandler:function(b,f){var d=this.options;if(d.disabled){return false}if(!b.target&&d.collapsible){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").find(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);this.active.next().addClass("ui-accordion-content-active");var h=this.active.next(),e={options:d,newHeader:a([]),oldHeader:d.active,newContent:a([]),oldContent:h},c=(this.active=a([]));this._toggle(c,h,e);return false}var g=a(b.currentTarget||f);var i=g[0]==this.active[0];if(this.running||(!d.collapsible&&i)){return false}this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").find(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);this.active.next().addClass("ui-accordion-content-active");if(!i){g.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").find(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected);g.next().addClass("ui-accordion-content-active")}var c=g.next(),h=this.active.next(),e={options:d,newHeader:i&&d.collapsible?a([]):g,oldHeader:this.active,newContent:i&&d.collapsible?a([]):c.find("> *"),oldContent:h.find("> *")},j=this.headers.index(this.active[0])>this.headers.index(g[0]);this.active=i?a([]):g;this._toggle(c,h,e,i,j);return false},_toggle:function(b,i,g,j,k){var d=this.options,m=this;this.toShow=b;this.toHide=i;this.data=g;var c=function(){if(!m){return}return m._completed.apply(m,arguments)};this._trigger("changestart",null,this.data);this.running=i.size()===0?b.size():i.size();if(d.animated){var f={};if(d.collapsible&&j){f={toShow:a([]),toHide:i,complete:c,down:k,autoHeight:d.autoHeight||d.fillSpace}}else{f={toShow:b,toHide:i,complete:c,down:k,autoHeight:d.autoHeight||d.fillSpace}}if(!d.proxied){d.proxied=d.animated}if(!d.proxiedDuration){d.proxiedDuration=d.duration}d.animated=a.isFunction(d.proxied)?d.proxied(f):d.proxied;d.duration=a.isFunction(d.proxiedDuration)?d.proxiedDuration(f):d.proxiedDuration;var l=a.ui.accordion.animations,e=d.duration,h=d.animated;if(!l[h]){l[h]=function(n){this.slide(n,{easing:h,duration:e||700})}}l[h](f)}else{if(d.collapsible&&j){b.toggle()}else{i.hide();b.show()}c(true)}i.prev().attr("aria-expanded","false").attr("tabIndex","-1").blur();b.prev().attr("aria-expanded","true").attr("tabIndex","0").focus()},_completed:function(b){var c=this.options;this.running=b?0:--this.running;if(this.running){return}if(c.clearStyle){this.toShow.add(this.toHide).css({height:"",overflow:""})}this._trigger("change",null,this.data)}});a.extend(a.ui.accordion,{version:"1.7.2",defaults:{active:null,alwaysOpen:true,animated:"slide",autoHeight:true,clearStyle:false,collapsible:false,event:"click",fillSpace:false,header:"> li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:false,navigationFilter:function(){return this.href.toLowerCase()==location.href.toLowerCase()}},animations:{slide:function(j,h){j=a.extend({easing:"swing",duration:300},j,h);if(!j.toHide.size()){j.toShow.animate({height:"show"},j);return}if(!j.toShow.size()){j.toHide.animate({height:"hide"},j);return}var c=j.toShow.css("overflow"),g,d={},f={},e=["height","paddingTop","paddingBottom"],b;var i=j.toShow;b=i[0].style.width;i.width(parseInt(i.parent().width(),10)-parseInt(i.css("paddingLeft"),10)-parseInt(i.css("paddingRight"),10)-(parseInt(i.css("borderLeftWidth"),10)||0)-(parseInt(i.css("borderRightWidth"),10)||0));a.each(e,function(k,m){f[m]="hide";var l=(""+a.css(j.toShow[0],m)).match(/^([\d+-.]+)(.*)$/);d[m]={value:l[1],unit:l[2]||"px"}});j.toShow.css({height:0,overflow:"hidden"}).show();j.toHide.filter(":hidden").each(j.complete).end().filter(":visible").animate(f,{step:function(k,l){if(l.prop=="height"){g=(l.now-l.start)/(l.end-l.start)}j.toShow[0].style[l.prop]=(g*d[l.prop].value)+d[l.prop].unit},duration:j.duration,easing:j.easing,complete:function(){if(!j.autoHeight){j.toShow.css("height","")}j.toShow.css("width",b);j.toShow.css({overflow:c});j.complete()}})},bounceslide:function(b){this.slide(b,{easing:b.down?"easeOutBounce":"swing",duration:b.down?1000:200})},easeslide:function(b){this.slide(b,{easing:"easeinout",duration:700})}}})})(jQuery);;/*
+ * jQuery UI Dialog 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Dialog
+ *
+ * Depends:
+ *	ui.core.js
+ *	ui.draggable.js
+ *	ui.resizable.js
+ */
+(function(c){var b={dragStart:"start.draggable",drag:"drag.draggable",dragStop:"stop.draggable",maxHeight:"maxHeight.resizable",minHeight:"minHeight.resizable",maxWidth:"maxWidth.resizable",minWidth:"minWidth.resizable",resizeStart:"start.resizable",resize:"drag.resizable",resizeStop:"stop.resizable"},a="ui-dialog ui-widget ui-widget-content ui-corner-all ";c.widget("ui.dialog",{_init:function(){this.originalTitle=this.element.attr("title");var l=this,m=this.options,j=m.title||this.originalTitle||"&nbsp;",e=c.ui.dialog.getTitleId(this.element),k=(this.uiDialog=c("<div/>")).appendTo(document.body).hide().addClass(a+m.dialogClass).css({position:"absolute",overflow:"hidden",zIndex:m.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(n){(m.closeOnEscape&&n.keyCode&&n.keyCode==c.ui.keyCode.ESCAPE&&l.close(n))}).attr({role:"dialog","aria-labelledby":e}).mousedown(function(n){l.moveToTop(false,n)}),g=this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(k),f=(this.uiDialogTitlebar=c("<div></div>")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(k),i=c('<a href="#"/>').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){i.addClass("ui-state-hover")},function(){i.removeClass("ui-state-hover")}).focus(function(){i.addClass("ui-state-focus")}).blur(function(){i.removeClass("ui-state-focus")}).mousedown(function(n){n.stopPropagation()}).click(function(n){l.close(n);return false}).appendTo(f),h=(this.uiDialogTitlebarCloseText=c("<span/>")).addClass("ui-icon ui-icon-closethick").text(m.closeText).appendTo(i),d=c("<span/>").addClass("ui-dialog-title").attr("id",e).html(j).prependTo(f);f.find("*").add(f).disableSelection();(m.draggable&&c.fn.draggable&&this._makeDraggable());(m.resizable&&c.fn.resizable&&this._makeResizable());this._createButtons(m.buttons);this._isOpen=false;(m.bgiframe&&c.fn.bgiframe&&k.bgiframe());(m.autoOpen&&this.open())},destroy:function(){(this.overlay&&this.overlay.destroy());this.uiDialog.hide();this.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body");this.uiDialog.remove();(this.originalTitle&&this.element.attr("title",this.originalTitle))},close:function(f){var d=this;if(false===d._trigger("beforeclose",f)){return}(d.overlay&&d.overlay.destroy());d.uiDialog.unbind("keypress.ui-dialog");(d.options.hide?d.uiDialog.hide(d.options.hide,function(){d._trigger("close",f)}):d.uiDialog.hide()&&d._trigger("close",f));c.ui.dialog.overlay.resize();d._isOpen=false;if(d.options.modal){var e=0;c(".ui-dialog").each(function(){if(this!=d.uiDialog[0]){e=Math.max(e,c(this).css("z-index"))}});c.ui.dialog.maxZ=e}},isOpen:function(){return this._isOpen},moveToTop:function(f,e){if((this.options.modal&&!f)||(!this.options.stack&&!this.options.modal)){return this._trigger("focus",e)}if(this.options.zIndex>c.ui.dialog.maxZ){c.ui.dialog.maxZ=this.options.zIndex}(this.overlay&&this.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=++c.ui.dialog.maxZ));var d={scrollTop:this.element.attr("scrollTop"),scrollLeft:this.element.attr("scrollLeft")};this.uiDialog.css("z-index",++c.ui.dialog.maxZ);this.element.attr(d);this._trigger("focus",e)},open:function(){if(this._isOpen){return}var e=this.options,d=this.uiDialog;this.overlay=e.modal?new c.ui.dialog.overlay(this):null;(d.next().length&&d.appendTo("body"));this._size();this._position(e.position);d.show(e.show);this.moveToTop(true);(e.modal&&d.bind("keypress.ui-dialog",function(h){if(h.keyCode!=c.ui.keyCode.TAB){return}var g=c(":tabbable",this),i=g.filter(":first")[0],f=g.filter(":last")[0];if(h.target==f&&!h.shiftKey){setTimeout(function(){i.focus()},1)}else{if(h.target==i&&h.shiftKey){setTimeout(function(){f.focus()},1)}}}));c([]).add(d.find(".ui-dialog-content :tabbable:first")).add(d.find(".ui-dialog-buttonpane :tabbable:first")).add(d).filter(":first").focus();this._trigger("open");this._isOpen=true},_createButtons:function(g){var f=this,d=false,e=c("<div></div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix");this.uiDialog.find(".ui-dialog-buttonpane").remove();(typeof g=="object"&&g!==null&&c.each(g,function(){return !(d=true)}));if(d){c.each(g,function(h,i){c('<button type="button"></button>').addClass("ui-state-default ui-corner-all").text(h).click(function(){i.apply(f.element[0],arguments)}).hover(function(){c(this).addClass("ui-state-hover")},function(){c(this).removeClass("ui-state-hover")}).focus(function(){c(this).addClass("ui-state-focus")}).blur(function(){c(this).removeClass("ui-state-focus")}).appendTo(e)});e.appendTo(this.uiDialog)}},_makeDraggable:function(){var d=this,f=this.options,e;this.uiDialog.draggable({cancel:".ui-dialog-content",handle:".ui-dialog-titlebar",containment:"document",start:function(){e=f.height;c(this).height(c(this).height()).addClass("ui-dialog-dragging");(f.dragStart&&f.dragStart.apply(d.element[0],arguments))},drag:function(){(f.drag&&f.drag.apply(d.element[0],arguments))},stop:function(){c(this).removeClass("ui-dialog-dragging").height(e);(f.dragStop&&f.dragStop.apply(d.element[0],arguments));c.ui.dialog.overlay.resize()}})},_makeResizable:function(g){g=(g===undefined?this.options.resizable:g);var d=this,f=this.options,e=typeof g=="string"?g:"n,e,s,w,se,sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",alsoResize:this.element,maxWidth:f.maxWidth,maxHeight:f.maxHeight,minWidth:f.minWidth,minHeight:f.minHeight,start:function(){c(this).addClass("ui-dialog-resizing");(f.resizeStart&&f.resizeStart.apply(d.element[0],arguments))},resize:function(){(f.resize&&f.resize.apply(d.element[0],arguments))},handles:e,stop:function(){c(this).removeClass("ui-dialog-resizing");f.height=c(this).height();f.width=c(this).width();(f.resizeStop&&f.resizeStop.apply(d.element[0],arguments));c.ui.dialog.overlay.resize()}}).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_position:function(i){var e=c(window),f=c(document),g=f.scrollTop(),d=f.scrollLeft(),h=g;if(c.inArray(i,["center","top","right","bottom","left"])>=0){i=[i=="right"||i=="left"?i:"center",i=="top"||i=="bottom"?i:"middle"]}if(i.constructor!=Array){i=["center","middle"]}if(i[0].constructor==Number){d+=i[0]}else{switch(i[0]){case"left":d+=0;break;case"right":d+=e.width()-this.uiDialog.outerWidth();break;default:case"center":d+=(e.width()-this.uiDialog.outerWidth())/2}}if(i[1].constructor==Number){g+=i[1]}else{switch(i[1]){case"top":g+=0;break;case"bottom":g+=e.height()-this.uiDialog.outerHeight();break;default:case"middle":g+=(e.height()-this.uiDialog.outerHeight())/2}}g=Math.max(g,h);this.uiDialog.css({top:g,left:d})},_setData:function(e,f){(b[e]&&this.uiDialog.data(b[e],f));switch(e){case"buttons":this._createButtons(f);break;case"closeText":this.uiDialogTitlebarCloseText.text(f);break;case"dialogClass":this.uiDialog.removeClass(this.options.dialogClass).addClass(a+f);break;case"draggable":(f?this._makeDraggable():this.uiDialog.draggable("destroy"));break;case"height":this.uiDialog.height(f);break;case"position":this._position(f);break;case"resizable":var d=this.uiDialog,g=this.uiDialog.is(":data(resizable)");(g&&!f&&d.resizable("destroy"));(g&&typeof f=="string"&&d.resizable("option","handles",f));(g||this._makeResizable(f));break;case"title":c(".ui-dialog-title",this.uiDialogTitlebar).html(f||"&nbsp;");break;case"width":this.uiDialog.width(f);break}c.widget.prototype._setData.apply(this,arguments)},_size:function(){var e=this.options;this.element.css({height:0,minHeight:0,width:"auto"});var d=this.uiDialog.css({height:"auto",width:e.width}).height();this.element.css({minHeight:Math.max(e.minHeight-d,0),height:e.height=="auto"?"auto":Math.max(e.height-d,0)})}});c.extend(c.ui.dialog,{version:"1.7.2",defaults:{autoOpen:true,bgiframe:false,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false,position:"center",resizable:true,show:null,stack:true,title:"",width:300,zIndex:1000},getter:"isOpen",uuid:0,maxZ:0,getTitleId:function(d){return"ui-dialog-title-"+(d.attr("id")||++this.uuid)},overlay:function(d){this.$el=c.ui.dialog.overlay.create(d)}});c.extend(c.ui.dialog.overlay,{instances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(d){return d+".dialog-overlay"}).join(" "),create:function(e){if(this.instances.length===0){setTimeout(function(){if(c.ui.dialog.overlay.instances.length){c(document).bind(c.ui.dialog.overlay.events,function(f){var g=c(f.target).parents(".ui-dialog").css("zIndex")||0;return(g>c.ui.dialog.overlay.maxZ)})}},1);c(document).bind("keydown.dialog-overlay",function(f){(e.options.closeOnEscape&&f.keyCode&&f.keyCode==c.ui.keyCode.ESCAPE&&e.close(f))});c(window).bind("resize.dialog-overlay",c.ui.dialog.overlay.resize)}var d=c("<div></div>").appendTo(document.body).addClass("ui-widget-overlay").css({width:this.width(),height:this.height()});(e.options.bgiframe&&c.fn.bgiframe&&d.bgiframe());this.instances.push(d);return d},destroy:function(d){this.instances.splice(c.inArray(this.instances,d),1);if(this.instances.length===0){c([document,window]).unbind(".dialog-overlay")}d.remove();var e=0;c.each(this.instances,function(){e=Math.max(e,this.css("z-index"))});this.maxZ=e},height:function(){if(c.browser.msie&&c.browser.version<7){var e=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);var d=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);if(e<d){return c(window).height()+"px"}else{return e+"px"}}else{return c(document).height()+"px"}},width:function(){if(c.browser.msie&&c.browser.version<7){var d=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth);var e=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth);if(d<e){return c(window).width()+"px"}else{return d+"px"}}else{return c(document).width()+"px"}},resize:function(){var d=c([]);c.each(c.ui.dialog.overlay.instances,function(){d=d.add(this)});d.css({width:0,height:0}).css({width:c.ui.dialog.overlay.width(),height:c.ui.dialog.overlay.height()})}});c.extend(c.ui.dialog.overlay.prototype,{destroy:function(){c.ui.dialog.overlay.destroy(this.$el)}})})(jQuery);;/*
+ * jQuery UI Slider 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Slider
+ *
+ * Depends:
+ *	ui.core.js
+ */
+(function(a){a.widget("ui.slider",a.extend({},a.ui.mouse,{_init:function(){var b=this,c=this.options;this._keySliding=false;this._handleIndex=null;this._detectOrientation();this._mouseInit();this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget ui-widget-content ui-corner-all");this.range=a([]);if(c.range){if(c.range===true){this.range=a("<div></div>");if(!c.values){c.values=[this._valueMin(),this._valueMin()]}if(c.values.length&&c.values.length!=2){c.values=[c.values[0],c.values[0]]}}else{this.range=a("<div></div>")}this.range.appendTo(this.element).addClass("ui-slider-range");if(c.range=="min"||c.range=="max"){this.range.addClass("ui-slider-range-"+c.range)}this.range.addClass("ui-widget-header")}if(a(".ui-slider-handle",this.element).length==0){a('<a href="#"></a>').appendTo(this.element).addClass("ui-slider-handle")}if(c.values&&c.values.length){while(a(".ui-slider-handle",this.element).length<c.values.length){a('<a href="#"></a>').appendTo(this.element).addClass("ui-slider-handle")}}this.handles=a(".ui-slider-handle",this.element).addClass("ui-state-default ui-corner-all");this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(d){d.preventDefault()}).hover(function(){if(!c.disabled){a(this).addClass("ui-state-hover")}},function(){a(this).removeClass("ui-state-hover")}).focus(function(){if(!c.disabled){a(".ui-slider .ui-state-focus").removeClass("ui-state-focus");a(this).addClass("ui-state-focus")}else{a(this).blur()}}).blur(function(){a(this).removeClass("ui-state-focus")});this.handles.each(function(d){a(this).data("index.ui-slider-handle",d)});this.handles.keydown(function(i){var f=true;var e=a(this).data("index.ui-slider-handle");if(b.options.disabled){return}switch(i.keyCode){case a.ui.keyCode.HOME:case a.ui.keyCode.END:case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:f=false;if(!b._keySliding){b._keySliding=true;a(this).addClass("ui-state-active");b._start(i,e)}break}var g,d,h=b._step();if(b.options.values&&b.options.values.length){g=d=b.values(e)}else{g=d=b.value()}switch(i.keyCode){case a.ui.keyCode.HOME:d=b._valueMin();break;case a.ui.keyCode.END:d=b._valueMax();break;case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:if(g==b._valueMax()){return}d=g+h;break;case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:if(g==b._valueMin()){return}d=g-h;break}b._slide(i,e,d);return f}).keyup(function(e){var d=a(this).data("index.ui-slider-handle");if(b._keySliding){b._stop(e,d);b._change(e,d);b._keySliding=false;a(this).removeClass("ui-state-active")}});this._refreshValue()},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider");this._mouseDestroy()},_mouseCapture:function(d){var e=this.options;if(e.disabled){return false}this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();var h={x:d.pageX,y:d.pageY};var j=this._normValueFromMouse(h);var c=this._valueMax()-this._valueMin()+1,f;var k=this,i;this.handles.each(function(l){var m=Math.abs(j-k.values(l));if(c>m){c=m;f=a(this);i=l}});if(e.range==true&&this.values(1)==e.min){f=a(this.handles[++i])}this._start(d,i);k._handleIndex=i;f.addClass("ui-state-active").focus();var g=f.offset();var b=!a(d.target).parents().andSelf().is(".ui-slider-handle");this._clickOffset=b?{left:0,top:0}:{left:d.pageX-g.left-(f.width()/2),top:d.pageY-g.top-(f.height()/2)-(parseInt(f.css("borderTopWidth"),10)||0)-(parseInt(f.css("borderBottomWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0)};j=this._normValueFromMouse(h);this._slide(d,i,j);return true},_mouseStart:function(b){return true},_mouseDrag:function(d){var b={x:d.pageX,y:d.pageY};var c=this._normValueFromMouse(b);this._slide(d,this._handleIndex,c);return false},_mouseStop:function(b){this.handles.removeClass("ui-state-active");this._stop(b,this._handleIndex);this._change(b,this._handleIndex);this._handleIndex=null;this._clickOffset=null;return false},_detectOrientation:function(){this.orientation=this.options.orientation=="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(d){var c,h;if("horizontal"==this.orientation){c=this.elementSize.width;h=d.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{c=this.elementSize.height;h=d.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}var f=(h/c);if(f>1){f=1}if(f<0){f=0}if("vertical"==this.orientation){f=1-f}var e=this._valueMax()-this._valueMin(),i=f*e,b=i%this.options.step,g=this._valueMin()+i-b;if(b>(this.options.step/2)){g+=this.options.step}return parseFloat(g.toFixed(5))},_start:function(d,c){var b={handle:this.handles[c],value:this.value()};if(this.options.values&&this.options.values.length){b.value=this.values(c);b.values=this.values()}this._trigger("start",d,b)},_slide:function(f,e,d){var g=this.handles[e];if(this.options.values&&this.options.values.length){var b=this.values(e?0:1);if((this.options.values.length==2&&this.options.range===true)&&((e==0&&d>b)||(e==1&&d<b))){d=b}if(d!=this.values(e)){var c=this.values();c[e]=d;var h=this._trigger("slide",f,{handle:this.handles[e],value:d,values:c});var b=this.values(e?0:1);if(h!==false){this.values(e,d,(f.type=="mousedown"&&this.options.animate),true)}}}else{if(d!=this.value()){var h=this._trigger("slide",f,{handle:this.handles[e],value:d});if(h!==false){this._setData("value",d,(f.type=="mousedown"&&this.options.animate))}}}},_stop:function(d,c){var b={handle:this.handles[c],value:this.value()};if(this.options.values&&this.options.values.length){b.value=this.values(c);b.values=this.values()}this._trigger("stop",d,b)},_change:function(d,c){var b={handle:this.handles[c],value:this.value()};if(this.options.values&&this.options.values.length){b.value=this.values(c);b.values=this.values()}this._trigger("change",d,b)},value:function(b){if(arguments.length){this._setData("value",b);this._change(null,0)}return this._value()},values:function(b,e,c,d){if(arguments.length>1){this.options.values[b]=e;this._refreshValue(c);if(!d){this._change(null,b)}}if(arguments.length){if(this.options.values&&this.options.values.length){return this._values(b)}else{return this.value()}}else{return this._values()}},_setData:function(b,d,c){a.widget.prototype._setData.apply(this,arguments);switch(b){case"disabled":if(d){this.handles.filter(".ui-state-focus").blur();this.handles.removeClass("ui-state-hover");this.handles.attr("disabled","disabled")}else{this.handles.removeAttr("disabled")}case"orientation":this._detectOrientation();this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation);this._refreshValue(c);break;case"value":this._refreshValue(c);break}},_step:function(){var b=this.options.step;return b},_value:function(){var b=this.options.value;if(b<this._valueMin()){b=this._valueMin()}if(b>this._valueMax()){b=this._valueMax()}return b},_values:function(b){if(arguments.length){var c=this.options.values[b];if(c<this._valueMin()){c=this._valueMin()}if(c>this._valueMax()){c=this._valueMax()}return c}else{return this.options.values}},_valueMin:function(){var b=this.options.min;return b},_valueMax:function(){var b=this.options.max;return b},_refreshValue:function(c){var f=this.options.range,d=this.options,l=this;if(this.options.values&&this.options.values.length){var i,h;this.handles.each(function(p,n){var o=(l.values(p)-l._valueMin())/(l._valueMax()-l._valueMin())*100;var m={};m[l.orientation=="horizontal"?"left":"bottom"]=o+"%";a(this).stop(1,1)[c?"animate":"css"](m,d.animate);if(l.options.range===true){if(l.orientation=="horizontal"){(p==0)&&l.range.stop(1,1)[c?"animate":"css"]({left:o+"%"},d.animate);(p==1)&&l.range[c?"animate":"css"]({width:(o-lastValPercent)+"%"},{queue:false,duration:d.animate})}else{(p==0)&&l.range.stop(1,1)[c?"animate":"css"]({bottom:(o)+"%"},d.animate);(p==1)&&l.range[c?"animate":"css"]({height:(o-lastValPercent)+"%"},{queue:false,duration:d.animate})}}lastValPercent=o})}else{var j=this.value(),g=this._valueMin(),k=this._valueMax(),e=k!=g?(j-g)/(k-g)*100:0;var b={};b[l.orientation=="horizontal"?"left":"bottom"]=e+"%";this.handle.stop(1,1)[c?"animate":"css"](b,d.animate);(f=="min")&&(this.orientation=="horizontal")&&this.range.stop(1,1)[c?"animate":"css"]({width:e+"%"},d.animate);(f=="max")&&(this.orientation=="horizontal")&&this.range[c?"animate":"css"]({width:(100-e)+"%"},{queue:false,duration:d.animate});(f=="min")&&(this.orientation=="vertical")&&this.range.stop(1,1)[c?"animate":"css"]({height:e+"%"},d.animate);(f=="max")&&(this.orientation=="vertical")&&this.range[c?"animate":"css"]({height:(100-e)+"%"},{queue:false,duration:d.animate})}}}));a.extend(a.ui.slider,{getter:"value values",version:"1.7.2",eventPrefix:"slide",defaults:{animate:false,delay:0,distance:0,max:100,min:0,orientation:"horizontal",range:false,step:1,value:0,values:null}})})(jQuery);;/*
+ * jQuery UI Tabs 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Tabs
+ *
+ * Depends:
+ *	ui.core.js
+ */
+(function(a){a.widget("ui.tabs",{_init:function(){if(this.options.deselectable!==undefined){this.options.collapsible=this.options.deselectable}this._tabify(true)},_setData:function(b,c){if(b=="selected"){if(this.options.collapsible&&c==this.options.selected){return}this.select(c)}else{this.options[b]=c;if(b=="deselectable"){this.options.collapsible=c}this._tabify()}},_tabId:function(b){return b.title&&b.title.replace(/\s/g,"_").replace(/[^A-Za-z0-9\-_:\.]/g,"")||this.options.idPrefix+a.data(b)},_sanitizeSelector:function(b){return b.replace(/:/g,"\\:")},_cookie:function(){var b=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+a.data(this.list[0]));return a.cookie.apply(null,[b].concat(a.makeArray(arguments)))},_ui:function(c,b){return{tab:c,panel:b,index:this.anchors.index(c)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var b=a(this);b.html(b.data("label.tabs")).removeData("label.tabs")})},_tabify:function(n){this.list=this.element.children("ul:first");this.lis=a("li:has(a[href])",this.list);this.anchors=this.lis.map(function(){return a("a",this)[0]});this.panels=a([]);var p=this,d=this.options;var c=/^#.+/;this.anchors.each(function(r,o){var q=a(o).attr("href");var s=q.split("#")[0],u;if(s&&(s===location.toString().split("#")[0]||(u=a("base")[0])&&s===u.href)){q=o.hash;o.href=q}if(c.test(q)){p.panels=p.panels.add(p._sanitizeSelector(q))}else{if(q!="#"){a.data(o,"href.tabs",q);a.data(o,"load.tabs",q.replace(/#.*$/,""));var w=p._tabId(o);o.href="#"+w;var v=a("#"+w);if(!v.length){v=a(d.panelTemplate).attr("id",w).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(p.panels[r-1]||p.list);v.data("destroy.tabs",true)}p.panels=p.panels.add(v)}else{d.disabled.push(r)}}});if(n){this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all");this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.lis.addClass("ui-state-default ui-corner-top");this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom");if(d.selected===undefined){if(location.hash){this.anchors.each(function(q,o){if(o.hash==location.hash){d.selected=q;return false}})}if(typeof d.selected!="number"&&d.cookie){d.selected=parseInt(p._cookie(),10)}if(typeof d.selected!="number"&&this.lis.filter(".ui-tabs-selected").length){d.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"))}d.selected=d.selected||0}else{if(d.selected===null){d.selected=-1}}d.selected=((d.selected>=0&&this.anchors[d.selected])||d.selected<0)?d.selected:0;d.disabled=a.unique(d.disabled.concat(a.map(this.lis.filter(".ui-state-disabled"),function(q,o){return p.lis.index(q)}))).sort();if(a.inArray(d.selected,d.disabled)!=-1){d.disabled.splice(a.inArray(d.selected,d.disabled),1)}this.panels.addClass("ui-tabs-hide");this.lis.removeClass("ui-tabs-selected ui-state-active");if(d.selected>=0&&this.anchors.length){this.panels.eq(d.selected).removeClass("ui-tabs-hide");this.lis.eq(d.selected).addClass("ui-tabs-selected ui-state-active");p.element.queue("tabs",function(){p._trigger("show",null,p._ui(p.anchors[d.selected],p.panels[d.selected]))});this.load(d.selected)}a(window).bind("unload",function(){p.lis.add(p.anchors).unbind(".tabs");p.lis=p.anchors=p.panels=null})}else{d.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"))}this.element[d.collapsible?"addClass":"removeClass"]("ui-tabs-collapsible");if(d.cookie){this._cookie(d.selected,d.cookie)}for(var g=0,m;(m=this.lis[g]);g++){a(m)[a.inArray(g,d.disabled)!=-1&&!a(m).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled")}if(d.cache===false){this.anchors.removeData("cache.tabs")}this.lis.add(this.anchors).unbind(".tabs");if(d.event!="mouseover"){var f=function(o,i){if(i.is(":not(.ui-state-disabled)")){i.addClass("ui-state-"+o)}};var j=function(o,i){i.removeClass("ui-state-"+o)};this.lis.bind("mouseover.tabs",function(){f("hover",a(this))});this.lis.bind("mouseout.tabs",function(){j("hover",a(this))});this.anchors.bind("focus.tabs",function(){f("focus",a(this).closest("li"))});this.anchors.bind("blur.tabs",function(){j("focus",a(this).closest("li"))})}var b,h;if(d.fx){if(a.isArray(d.fx)){b=d.fx[0];h=d.fx[1]}else{b=h=d.fx}}function e(i,o){i.css({display:""});if(a.browser.msie&&o.opacity){i[0].style.removeAttribute("filter")}}var k=h?function(i,o){a(i).closest("li").removeClass("ui-state-default").addClass("ui-tabs-selected ui-state-active");o.hide().removeClass("ui-tabs-hide").animate(h,h.duration||"normal",function(){e(o,h);p._trigger("show",null,p._ui(i,o[0]))})}:function(i,o){a(i).closest("li").removeClass("ui-state-default").addClass("ui-tabs-selected ui-state-active");o.removeClass("ui-tabs-hide");p._trigger("show",null,p._ui(i,o[0]))};var l=b?function(o,i){i.animate(b,b.duration||"normal",function(){p.lis.removeClass("ui-tabs-selected ui-state-active").addClass("ui-state-default");i.addClass("ui-tabs-hide");e(i,b);p.element.dequeue("tabs")})}:function(o,i,q){p.lis.removeClass("ui-tabs-selected ui-state-active").addClass("ui-state-default");i.addClass("ui-tabs-hide");p.element.dequeue("tabs")};this.anchors.bind(d.event+".tabs",function(){var o=this,r=a(this).closest("li"),i=p.panels.filter(":not(.ui-tabs-hide)"),q=a(p._sanitizeSelector(this.hash));if((r.hasClass("ui-tabs-selected")&&!d.collapsible)||r.hasClass("ui-state-disabled")||r.hasClass("ui-state-processing")||p._trigger("select",null,p._ui(this,q[0]))===false){this.blur();return false}d.selected=p.anchors.index(this);p.abort();if(d.collapsible){if(r.hasClass("ui-tabs-selected")){d.selected=-1;if(d.cookie){p._cookie(d.selected,d.cookie)}p.element.queue("tabs",function(){l(o,i)}).dequeue("tabs");this.blur();return false}else{if(!i.length){if(d.cookie){p._cookie(d.selected,d.cookie)}p.element.queue("tabs",function(){k(o,q)});p.load(p.anchors.index(this));this.blur();return false}}}if(d.cookie){p._cookie(d.selected,d.cookie)}if(q.length){if(i.length){p.element.queue("tabs",function(){l(o,i)})}p.element.queue("tabs",function(){k(o,q)});p.load(p.anchors.index(this))}else{throw"jQuery UI Tabs: Mismatching fragment identifier."}if(a.browser.msie){this.blur()}});this.anchors.bind("click.tabs",function(){return false})},destroy:function(){var b=this.options;this.abort();this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs");this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.anchors.each(function(){var c=a.data(this,"href.tabs");if(c){this.href=c}var d=a(this).unbind(".tabs");a.each(["href","load","cache"],function(e,f){d.removeData(f+".tabs")})});this.lis.unbind(".tabs").add(this.panels).each(function(){if(a.data(this,"destroy.tabs")){a(this).remove()}else{a(this).removeClass(["ui-state-default","ui-corner-top","ui-tabs-selected","ui-state-active","ui-state-hover","ui-state-focus","ui-state-disabled","ui-tabs-panel","ui-widget-content","ui-corner-bottom","ui-tabs-hide"].join(" "))}});if(b.cookie){this._cookie(null,b.cookie)}},add:function(e,d,c){if(c===undefined){c=this.anchors.length}var b=this,g=this.options,i=a(g.tabTemplate.replace(/#\{href\}/g,e).replace(/#\{label\}/g,d)),h=!e.indexOf("#")?e.replace("#",""):this._tabId(a("a",i)[0]);i.addClass("ui-state-default ui-corner-top").data("destroy.tabs",true);var f=a("#"+h);if(!f.length){f=a(g.panelTemplate).attr("id",h).data("destroy.tabs",true)}f.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide");if(c>=this.lis.length){i.appendTo(this.list);f.appendTo(this.list[0].parentNode)}else{i.insertBefore(this.lis[c]);f.insertBefore(this.panels[c])}g.disabled=a.map(g.disabled,function(k,j){return k>=c?++k:k});this._tabify();if(this.anchors.length==1){i.addClass("ui-tabs-selected ui-state-active");f.removeClass("ui-tabs-hide");this.element.queue("tabs",function(){b._trigger("show",null,b._ui(b.anchors[0],b.panels[0]))});this.load(0)}this._trigger("add",null,this._ui(this.anchors[c],this.panels[c]))},remove:function(b){var d=this.options,e=this.lis.eq(b).remove(),c=this.panels.eq(b).remove();if(e.hasClass("ui-tabs-selected")&&this.anchors.length>1){this.select(b+(b+1<this.anchors.length?1:-1))}d.disabled=a.map(a.grep(d.disabled,function(g,f){return g!=b}),function(g,f){return g>=b?--g:g});this._tabify();this._trigger("remove",null,this._ui(e.find("a")[0],c[0]))},enable:function(b){var c=this.options;if(a.inArray(b,c.disabled)==-1){return}this.lis.eq(b).removeClass("ui-state-disabled");c.disabled=a.grep(c.disabled,function(e,d){return e!=b});this._trigger("enable",null,this._ui(this.anchors[b],this.panels[b]))},disable:function(c){var b=this,d=this.options;if(c!=d.selected){this.lis.eq(c).addClass("ui-state-disabled");d.disabled.push(c);d.disabled.sort();this._trigger("disable",null,this._ui(this.anchors[c],this.panels[c]))}},select:function(b){if(typeof b=="string"){b=this.anchors.index(this.anchors.filter("[href$="+b+"]"))}else{if(b===null){b=-1}}if(b==-1&&this.options.collapsible){b=this.options.selected}this.anchors.eq(b).trigger(this.options.event+".tabs")},load:function(e){var c=this,g=this.options,b=this.anchors.eq(e)[0],d=a.data(b,"load.tabs");this.abort();if(!d||this.element.queue("tabs").length!==0&&a.data(b,"cache.tabs")){this.element.dequeue("tabs");return}this.lis.eq(e).addClass("ui-state-processing");if(g.spinner){var f=a("span",b);f.data("label.tabs",f.html()).html(g.spinner)}this.xhr=a.ajax(a.extend({},g.ajaxOptions,{url:d,success:function(i,h){a(c._sanitizeSelector(b.hash)).html(i);c._cleanup();if(g.cache){a.data(b,"cache.tabs",true)}c._trigger("load",null,c._ui(c.anchors[e],c.panels[e]));try{g.ajaxOptions.success(i,h)}catch(j){}c.element.dequeue("tabs")}}))},abort:function(){this.element.queue([]);this.panels.stop(false,true);if(this.xhr){this.xhr.abort();delete this.xhr}this._cleanup()},url:function(c,b){this.anchors.eq(c).removeData("cache.tabs").data("load.tabs",b)},length:function(){return this.anchors.length}});a.extend(a.ui.tabs,{version:"1.7.2",getter:"length",defaults:{ajaxOptions:null,cache:false,cookie:null,collapsible:false,disabled:[],event:"click",fx:null,idPrefix:"ui-tabs-",panelTemplate:"<div></div>",spinner:"<em>Loading&#8230;</em>",tabTemplate:'<li><a href="#{href}"><span>#{label}</span></a></li>'}});a.extend(a.ui.tabs.prototype,{rotation:null,rotate:function(d,f){var b=this,g=this.options;var c=b._rotate||(b._rotate=function(h){clearTimeout(b.rotation);b.rotation=setTimeout(function(){var i=g.selected;b.select(++i<b.anchors.length?i:0)},d);if(h){h.stopPropagation()}});var e=b._unrotate||(b._unrotate=!f?function(h){if(h.clientX){b.rotate(null)}}:function(h){t=g.selected;c()});if(d){this.element.bind("tabsshow",c);this.anchors.bind(g.event+".tabs",e);c()}else{clearTimeout(b.rotation);this.element.unbind("tabsshow",c);this.anchors.unbind(g.event+".tabs",e);delete this._rotate;delete this._unrotate}}})})(jQuery);;/*
+ * jQuery UI Datepicker 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Datepicker
+ *
+ * Depends:
+ *	ui.core.js
+ */
+(function($){$.extend($.ui,{datepicker:{version:"1.7.2"}});var PROP_NAME="datepicker";function Datepicker(){this.debug=false;this._curInst=null;this._keyEvent=false;this._disabledInputs=[];this._datepickerShowing=false;this._inDialog=false;this._mainDivId="ui-datepicker-div";this._inlineClass="ui-datepicker-inline";this._appendClass="ui-datepicker-append";this._triggerClass="ui-datepicker-trigger";this._dialogClass="ui-datepicker-dialog";this._disableClass="ui-datepicker-disabled";this._unselectableClass="ui-datepicker-unselectable";this._currentClass="ui-datepicker-current-day";this._dayOverClass="ui-datepicker-days-cell-over";this.regional=[];this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],dateFormat:"mm/dd/yy",firstDay:0,isRTL:false};this._defaults={showOn:"focus",showAnim:"show",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:false,hideIfNoPrevNext:false,navigationAsDateFormat:false,gotoCurrent:false,changeMonth:false,changeYear:false,showMonthAfterYear:false,yearRange:"-10:+10",showOtherMonths:false,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"normal",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:true,showButtonPanel:false};$.extend(this._defaults,this.regional[""]);this.dpDiv=$('<div id="'+this._mainDivId+'" class="ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all ui-helper-hidden-accessible"></div>')}$.extend(Datepicker.prototype,{markerClassName:"hasDatepicker",log:function(){if(this.debug){console.log.apply("",arguments)}},setDefaults:function(settings){extendRemove(this._defaults,settings||{});return this},_attachDatepicker:function(target,settings){var inlineSettings=null;for(var attrName in this._defaults){var attrValue=target.getAttribute("date:"+attrName);if(attrValue){inlineSettings=inlineSettings||{};try{inlineSettings[attrName]=eval(attrValue)}catch(err){inlineSettings[attrName]=attrValue}}}var nodeName=target.nodeName.toLowerCase();var inline=(nodeName=="div"||nodeName=="span");if(!target.id){target.id="dp"+(++this.uuid)}var inst=this._newInst($(target),inline);inst.settings=$.extend({},settings||{},inlineSettings||{});if(nodeName=="input"){this._connectDatepicker(target,inst)}else{if(inline){this._inlineDatepicker(target,inst)}}},_newInst:function(target,inline){var id=target[0].id.replace(/([:\[\]\.])/g,"\\\\$1");return{id:id,input:target,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:inline,dpDiv:(!inline?this.dpDiv:$('<div class="'+this._inlineClass+' ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>'))}},_connectDatepicker:function(target,inst){var input=$(target);inst.append=$([]);inst.trigger=$([]);if(input.hasClass(this.markerClassName)){return}var appendText=this._get(inst,"appendText");var isRTL=this._get(inst,"isRTL");if(appendText){inst.append=$('<span class="'+this._appendClass+'">'+appendText+"</span>");input[isRTL?"before":"after"](inst.append)}var showOn=this._get(inst,"showOn");if(showOn=="focus"||showOn=="both"){input.focus(this._showDatepicker)}if(showOn=="button"||showOn=="both"){var buttonText=this._get(inst,"buttonText");var buttonImage=this._get(inst,"buttonImage");inst.trigger=$(this._get(inst,"buttonImageOnly")?$("<img/>").addClass(this._triggerClass).attr({src:buttonImage,alt:buttonText,title:buttonText}):$('<button type="button"></button>').addClass(this._triggerClass).html(buttonImage==""?buttonText:$("<img/>").attr({src:buttonImage,alt:buttonText,title:buttonText})));input[isRTL?"before":"after"](inst.trigger);inst.trigger.click(function(){if($.datepicker._datepickerShowing&&$.datepicker._lastInput==target){$.datepicker._hideDatepicker()}else{$.datepicker._showDatepicker(target)}return false})}input.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).bind("setData.datepicker",function(event,key,value){inst.settings[key]=value}).bind("getData.datepicker",function(event,key){return this._get(inst,key)});$.data(target,PROP_NAME,inst)},_inlineDatepicker:function(target,inst){var divSpan=$(target);if(divSpan.hasClass(this.markerClassName)){return}divSpan.addClass(this.markerClassName).append(inst.dpDiv).bind("setData.datepicker",function(event,key,value){inst.settings[key]=value}).bind("getData.datepicker",function(event,key){return this._get(inst,key)});$.data(target,PROP_NAME,inst);this._setDate(inst,this._getDefaultDate(inst));this._updateDatepicker(inst);this._updateAlternate(inst)},_dialogDatepicker:function(input,dateText,onSelect,settings,pos){var inst=this._dialogInst;if(!inst){var id="dp"+(++this.uuid);this._dialogInput=$('<input type="text" id="'+id+'" size="1" style="position: absolute; top: -100px;"/>');this._dialogInput.keydown(this._doKeyDown);$("body").append(this._dialogInput);inst=this._dialogInst=this._newInst(this._dialogInput,false);inst.settings={};$.data(this._dialogInput[0],PROP_NAME,inst)}extendRemove(inst.settings,settings||{});this._dialogInput.val(dateText);this._pos=(pos?(pos.length?pos:[pos.pageX,pos.pageY]):null);if(!this._pos){var browserWidth=window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth;var browserHeight=window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight;var scrollX=document.documentElement.scrollLeft||document.body.scrollLeft;var scrollY=document.documentElement.scrollTop||document.body.scrollTop;this._pos=[(browserWidth/2)-100+scrollX,(browserHeight/2)-150+scrollY]}this._dialogInput.css("left",this._pos[0]+"px").css("top",this._pos[1]+"px");inst.settings.onSelect=onSelect;this._inDialog=true;this.dpDiv.addClass(this._dialogClass);this._showDatepicker(this._dialogInput[0]);if($.blockUI){$.blockUI(this.dpDiv)}$.data(this._dialogInput[0],PROP_NAME,inst);return this},_destroyDatepicker:function(target){var $target=$(target);var inst=$.data(target,PROP_NAME);if(!$target.hasClass(this.markerClassName)){return}var nodeName=target.nodeName.toLowerCase();$.removeData(target,PROP_NAME);if(nodeName=="input"){inst.append.remove();inst.trigger.remove();$target.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress)}else{if(nodeName=="div"||nodeName=="span"){$target.removeClass(this.markerClassName).empty()}}},_enableDatepicker:function(target){var $target=$(target);var inst=$.data(target,PROP_NAME);if(!$target.hasClass(this.markerClassName)){return}var nodeName=target.nodeName.toLowerCase();if(nodeName=="input"){target.disabled=false;inst.trigger.filter("button").each(function(){this.disabled=false}).end().filter("img").css({opacity:"1.0",cursor:""})}else{if(nodeName=="div"||nodeName=="span"){var inline=$target.children("."+this._inlineClass);inline.children().removeClass("ui-state-disabled")}}this._disabledInputs=$.map(this._disabledInputs,function(value){return(value==target?null:value)})},_disableDatepicker:function(target){var $target=$(target);var inst=$.data(target,PROP_NAME);if(!$target.hasClass(this.markerClassName)){return}var nodeName=target.nodeName.toLowerCase();if(nodeName=="input"){target.disabled=true;inst.trigger.filter("button").each(function(){this.disabled=true}).end().filter("img").css({opacity:"0.5",cursor:"default"})}else{if(nodeName=="div"||nodeName=="span"){var inline=$target.children("."+this._inlineClass);inline.children().addClass("ui-state-disabled")}}this._disabledInputs=$.map(this._disabledInputs,function(value){return(value==target?null:value)});this._disabledInputs[this._disabledInputs.length]=target},_isDisabledDatepicker:function(target){if(!target){return false}for(var i=0;i<this._disabledInputs.length;i++){if(this._disabledInputs[i]==target){return true}}return false},_getInst:function(target){try{return $.data(target,PROP_NAME)}catch(err){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(target,name,value){var inst=this._getInst(target);if(arguments.length==2&&typeof name=="string"){return(name=="defaults"?$.extend({},$.datepicker._defaults):(inst?(name=="all"?$.extend({},inst.settings):this._get(inst,name)):null))}var settings=name||{};if(typeof name=="string"){settings={};settings[name]=value}if(inst){if(this._curInst==inst){this._hideDatepicker(null)}var date=this._getDateDatepicker(target);extendRemove(inst.settings,settings);this._setDateDatepicker(target,date);this._updateDatepicker(inst)}},_changeDatepicker:function(target,name,value){this._optionDatepicker(target,name,value)},_refreshDatepicker:function(target){var inst=this._getInst(target);if(inst){this._updateDatepicker(inst)}},_setDateDatepicker:function(target,date,endDate){var inst=this._getInst(target);if(inst){this._setDate(inst,date,endDate);this._updateDatepicker(inst);this._updateAlternate(inst)}},_getDateDatepicker:function(target){var inst=this._getInst(target);if(inst&&!inst.inline){this._setDateFromField(inst)}return(inst?this._getDate(inst):null)},_doKeyDown:function(event){var inst=$.datepicker._getInst(event.target);var handled=true;var isRTL=inst.dpDiv.is(".ui-datepicker-rtl");inst._keyEvent=true;if($.datepicker._datepickerShowing){switch(event.keyCode){case 9:$.datepicker._hideDatepicker(null,"");break;case 13:var sel=$("td."+$.datepicker._dayOverClass+", td."+$.datepicker._currentClass,inst.dpDiv);if(sel[0]){$.datepicker._selectDay(event.target,inst.selectedMonth,inst.selectedYear,sel[0])}else{$.datepicker._hideDatepicker(null,$.datepicker._get(inst,"duration"))}return false;break;case 27:$.datepicker._hideDatepicker(null,$.datepicker._get(inst,"duration"));break;case 33:$.datepicker._adjustDate(event.target,(event.ctrlKey?-$.datepicker._get(inst,"stepBigMonths"):-$.datepicker._get(inst,"stepMonths")),"M");break;case 34:$.datepicker._adjustDate(event.target,(event.ctrlKey?+$.datepicker._get(inst,"stepBigMonths"):+$.datepicker._get(inst,"stepMonths")),"M");break;case 35:if(event.ctrlKey||event.metaKey){$.datepicker._clearDate(event.target)}handled=event.ctrlKey||event.metaKey;break;case 36:if(event.ctrlKey||event.metaKey){$.datepicker._gotoToday(event.target)}handled=event.ctrlKey||event.metaKey;break;case 37:if(event.ctrlKey||event.metaKey){$.datepicker._adjustDate(event.target,(isRTL?+1:-1),"D")}handled=event.ctrlKey||event.metaKey;if(event.originalEvent.altKey){$.datepicker._adjustDate(event.target,(event.ctrlKey?-$.datepicker._get(inst,"stepBigMonths"):-$.datepicker._get(inst,"stepMonths")),"M")}break;case 38:if(event.ctrlKey||event.metaKey){$.datepicker._adjustDate(event.target,-7,"D")}handled=event.ctrlKey||event.metaKey;break;case 39:if(event.ctrlKey||event.metaKey){$.datepicker._adjustDate(event.target,(isRTL?-1:+1),"D")}handled=event.ctrlKey||event.metaKey;if(event.originalEvent.altKey){$.datepicker._adjustDate(event.target,(event.ctrlKey?+$.datepicker._get(inst,"stepBigMonths"):+$.datepicker._get(inst,"stepMonths")),"M")}break;case 40:if(event.ctrlKey||event.metaKey){$.datepicker._adjustDate(event.target,+7,"D")}handled=event.ctrlKey||event.metaKey;break;default:handled=false}}else{if(event.keyCode==36&&event.ctrlKey){$.datepicker._showDatepicker(this)}else{handled=false}}if(handled){event.preventDefault();event.stopPropagation()}},_doKeyPress:function(event){var inst=$.datepicker._getInst(event.target);if($.datepicker._get(inst,"constrainInput")){var chars=$.datepicker._possibleChars($.datepicker._get(inst,"dateFormat"));var chr=String.fromCharCode(event.charCode==undefined?event.keyCode:event.charCode);return event.ctrlKey||(chr<" "||!chars||chars.indexOf(chr)>-1)}},_showDatepicker:function(input){input=input.target||input;if(input.nodeName.toLowerCase()!="input"){input=$("input",input.parentNode)[0]}if($.datepicker._isDisabledDatepicker(input)||$.datepicker._lastInput==input){return}var inst=$.datepicker._getInst(input);var beforeShow=$.datepicker._get(inst,"beforeShow");extendRemove(inst.settings,(beforeShow?beforeShow.apply(input,[input,inst]):{}));$.datepicker._hideDatepicker(null,"");$.datepicker._lastInput=input;$.datepicker._setDateFromField(inst);if($.datepicker._inDialog){input.value=""}if(!$.datepicker._pos){$.datepicker._pos=$.datepicker._findPos(input);$.datepicker._pos[1]+=input.offsetHeight}var isFixed=false;$(input).parents().each(function(){isFixed|=$(this).css("position")=="fixed";return !isFixed});if(isFixed&&$.browser.opera){$.datepicker._pos[0]-=document.documentElement.scrollLeft;$.datepicker._pos[1]-=document.documentElement.scrollTop}var offset={left:$.datepicker._pos[0],top:$.datepicker._pos[1]};$.datepicker._pos=null;inst.rangeStart=null;inst.dpDiv.css({position:"absolute",display:"block",top:"-1000px"});$.datepicker._updateDatepicker(inst);offset=$.datepicker._checkOffset(inst,offset,isFixed);inst.dpDiv.css({position:($.datepicker._inDialog&&$.blockUI?"static":(isFixed?"fixed":"absolute")),display:"none",left:offset.left+"px",top:offset.top+"px"});if(!inst.inline){var showAnim=$.datepicker._get(inst,"showAnim")||"show";var duration=$.datepicker._get(inst,"duration");var postProcess=function(){$.datepicker._datepickerShowing=true;if($.browser.msie&&parseInt($.browser.version,10)<7){$("iframe.ui-datepicker-cover").css({width:inst.dpDiv.width()+4,height:inst.dpDiv.height()+4})}};if($.effects&&$.effects[showAnim]){inst.dpDiv.show(showAnim,$.datepicker._get(inst,"showOptions"),duration,postProcess)}else{inst.dpDiv[showAnim](duration,postProcess)}if(duration==""){postProcess()}if(inst.input[0].type!="hidden"){inst.input[0].focus()}$.datepicker._curInst=inst}},_updateDatepicker:function(inst){var dims={width:inst.dpDiv.width()+4,height:inst.dpDiv.height()+4};var self=this;inst.dpDiv.empty().append(this._generateHTML(inst)).find("iframe.ui-datepicker-cover").css({width:dims.width,height:dims.height}).end().find("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a").bind("mouseout",function(){$(this).removeClass("ui-state-hover");if(this.className.indexOf("ui-datepicker-prev")!=-1){$(this).removeClass("ui-datepicker-prev-hover")}if(this.className.indexOf("ui-datepicker-next")!=-1){$(this).removeClass("ui-datepicker-next-hover")}}).bind("mouseover",function(){if(!self._isDisabledDatepicker(inst.inline?inst.dpDiv.parent()[0]:inst.input[0])){$(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover");$(this).addClass("ui-state-hover");if(this.className.indexOf("ui-datepicker-prev")!=-1){$(this).addClass("ui-datepicker-prev-hover")}if(this.className.indexOf("ui-datepicker-next")!=-1){$(this).addClass("ui-datepicker-next-hover")}}}).end().find("."+this._dayOverClass+" a").trigger("mouseover").end();var numMonths=this._getNumberOfMonths(inst);var cols=numMonths[1];var width=17;if(cols>1){inst.dpDiv.addClass("ui-datepicker-multi-"+cols).css("width",(width*cols)+"em")}else{inst.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("")}inst.dpDiv[(numMonths[0]!=1||numMonths[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi");inst.dpDiv[(this._get(inst,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl");if(inst.input&&inst.input[0].type!="hidden"&&inst==$.datepicker._curInst){$(inst.input[0]).focus()}},_checkOffset:function(inst,offset,isFixed){var dpWidth=inst.dpDiv.outerWidth();var dpHeight=inst.dpDiv.outerHeight();var inputWidth=inst.input?inst.input.outerWidth():0;var inputHeight=inst.input?inst.input.outerHeight():0;var viewWidth=(window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth)+$(document).scrollLeft();var viewHeight=(window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight)+$(document).scrollTop();offset.left-=(this._get(inst,"isRTL")?(dpWidth-inputWidth):0);offset.left-=(isFixed&&offset.left==inst.input.offset().left)?$(document).scrollLeft():0;offset.top-=(isFixed&&offset.top==(inst.input.offset().top+inputHeight))?$(document).scrollTop():0;offset.left-=(offset.left+dpWidth>viewWidth&&viewWidth>dpWidth)?Math.abs(offset.left+dpWidth-viewWidth):0;offset.top-=(offset.top+dpHeight>viewHeight&&viewHeight>dpHeight)?Math.abs(offset.top+dpHeight+inputHeight*2-viewHeight):0;return offset},_findPos:function(obj){while(obj&&(obj.type=="hidden"||obj.nodeType!=1)){obj=obj.nextSibling}var position=$(obj).offset();return[position.left,position.top]},_hideDatepicker:function(input,duration){var inst=this._curInst;if(!inst||(input&&inst!=$.data(input,PROP_NAME))){return}if(inst.stayOpen){this._selectDate("#"+inst.id,this._formatDate(inst,inst.currentDay,inst.currentMonth,inst.currentYear))}inst.stayOpen=false;if(this._datepickerShowing){duration=(duration!=null?duration:this._get(inst,"duration"));var showAnim=this._get(inst,"showAnim");var postProcess=function(){$.datepicker._tidyDialog(inst)};if(duration!=""&&$.effects&&$.effects[showAnim]){inst.dpDiv.hide(showAnim,$.datepicker._get(inst,"showOptions"),duration,postProcess)}else{inst.dpDiv[(duration==""?"hide":(showAnim=="slideDown"?"slideUp":(showAnim=="fadeIn"?"fadeOut":"hide")))](duration,postProcess)}if(duration==""){this._tidyDialog(inst)}var onClose=this._get(inst,"onClose");if(onClose){onClose.apply((inst.input?inst.input[0]:null),[(inst.input?inst.input.val():""),inst])}this._datepickerShowing=false;this._lastInput=null;if(this._inDialog){this._dialogInput.css({position:"absolute",left:"0",top:"-100px"});if($.blockUI){$.unblockUI();$("body").append(this.dpDiv)}}this._inDialog=false}this._curInst=null},_tidyDialog:function(inst){inst.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(event){if(!$.datepicker._curInst){return}var $target=$(event.target);if(($target.parents("#"+$.datepicker._mainDivId).length==0)&&!$target.hasClass($.datepicker.markerClassName)&&!$target.hasClass($.datepicker._triggerClass)&&$.datepicker._datepickerShowing&&!($.datepicker._inDialog&&$.blockUI)){$.datepicker._hideDatepicker(null,"")}},_adjustDate:function(id,offset,period){var target=$(id);var inst=this._getInst(target[0]);if(this._isDisabledDatepicker(target[0])){return}this._adjustInstDate(inst,offset+(period=="M"?this._get(inst,"showCurrentAtPos"):0),period);this._updateDatepicker(inst)},_gotoToday:function(id){var target=$(id);var inst=this._getInst(target[0]);if(this._get(inst,"gotoCurrent")&&inst.currentDay){inst.selectedDay=inst.currentDay;inst.drawMonth=inst.selectedMonth=inst.currentMonth;inst.drawYear=inst.selectedYear=inst.currentYear}else{var date=new Date();inst.selectedDay=date.getDate();inst.drawMonth=inst.selectedMonth=date.getMonth();inst.drawYear=inst.selectedYear=date.getFullYear()}this._notifyChange(inst);this._adjustDate(target)},_selectMonthYear:function(id,select,period){var target=$(id);var inst=this._getInst(target[0]);inst._selectingMonthYear=false;inst["selected"+(period=="M"?"Month":"Year")]=inst["draw"+(period=="M"?"Month":"Year")]=parseInt(select.options[select.selectedIndex].value,10);this._notifyChange(inst);this._adjustDate(target)},_clickMonthYear:function(id){var target=$(id);var inst=this._getInst(target[0]);if(inst.input&&inst._selectingMonthYear&&!$.browser.msie){inst.input[0].focus()}inst._selectingMonthYear=!inst._selectingMonthYear},_selectDay:function(id,month,year,td){var target=$(id);if($(td).hasClass(this._unselectableClass)||this._isDisabledDatepicker(target[0])){return}var inst=this._getInst(target[0]);inst.selectedDay=inst.currentDay=$("a",td).html();inst.selectedMonth=inst.currentMonth=month;inst.selectedYear=inst.currentYear=year;if(inst.stayOpen){inst.endDay=inst.endMonth=inst.endYear=null}this._selectDate(id,this._formatDate(inst,inst.currentDay,inst.currentMonth,inst.currentYear));if(inst.stayOpen){inst.rangeStart=this._daylightSavingAdjust(new Date(inst.currentYear,inst.currentMonth,inst.currentDay));this._updateDatepicker(inst)}},_clearDate:function(id){var target=$(id);var inst=this._getInst(target[0]);inst.stayOpen=false;inst.endDay=inst.endMonth=inst.endYear=inst.rangeStart=null;this._selectDate(target,"")},_selectDate:function(id,dateStr){var target=$(id);var inst=this._getInst(target[0]);dateStr=(dateStr!=null?dateStr:this._formatDate(inst));if(inst.input){inst.input.val(dateStr)}this._updateAlternate(inst);var onSelect=this._get(inst,"onSelect");if(onSelect){onSelect.apply((inst.input?inst.input[0]:null),[dateStr,inst])}else{if(inst.input){inst.input.trigger("change")}}if(inst.inline){this._updateDatepicker(inst)}else{if(!inst.stayOpen){this._hideDatepicker(null,this._get(inst,"duration"));this._lastInput=inst.input[0];if(typeof(inst.input[0])!="object"){inst.input[0].focus()}this._lastInput=null}}},_updateAlternate:function(inst){var altField=this._get(inst,"altField");if(altField){var altFormat=this._get(inst,"altFormat")||this._get(inst,"dateFormat");var date=this._getDate(inst);dateStr=this.formatDate(altFormat,date,this._getFormatConfig(inst));$(altField).each(function(){$(this).val(dateStr)})}},noWeekends:function(date){var day=date.getDay();return[(day>0&&day<6),""]},iso8601Week:function(date){var checkDate=new Date(date.getFullYear(),date.getMonth(),date.getDate());var firstMon=new Date(checkDate.getFullYear(),1-1,4);var firstDay=firstMon.getDay()||7;firstMon.setDate(firstMon.getDate()+1-firstDay);if(firstDay<4&&checkDate<firstMon){checkDate.setDate(checkDate.getDate()-3);return $.datepicker.iso8601Week(checkDate)}else{if(checkDate>new Date(checkDate.getFullYear(),12-1,28)){firstDay=new Date(checkDate.getFullYear()+1,1-1,4).getDay()||7;if(firstDay>4&&(checkDate.getDay()||7)<firstDay-3){return 1}}}return Math.floor(((checkDate-firstMon)/86400000)/7)+1},parseDate:function(format,value,settings){if(format==null||value==null){throw"Invalid arguments"}value=(typeof value=="object"?value.toString():value+"");if(value==""){return null}var shortYearCutoff=(settings?settings.shortYearCutoff:null)||this._defaults.shortYearCutoff;var dayNamesShort=(settings?settings.dayNamesShort:null)||this._defaults.dayNamesShort;var dayNames=(settings?settings.dayNames:null)||this._defaults.dayNames;var monthNamesShort=(settings?settings.monthNamesShort:null)||this._defaults.monthNamesShort;var monthNames=(settings?settings.monthNames:null)||this._defaults.monthNames;var year=-1;var month=-1;var day=-1;var doy=-1;var literal=false;var lookAhead=function(match){var matches=(iFormat+1<format.length&&format.charAt(iFormat+1)==match);if(matches){iFormat++}return matches};var getNumber=function(match){lookAhead(match);var origSize=(match=="@"?14:(match=="y"?4:(match=="o"?3:2)));var size=origSize;var num=0;while(size>0&&iValue<value.length&&value.charAt(iValue)>="0"&&value.charAt(iValue)<="9"){num=num*10+parseInt(value.charAt(iValue++),10);size--}if(size==origSize){throw"Missing number at position "+iValue}return num};var getName=function(match,shortNames,longNames){var names=(lookAhead(match)?longNames:shortNames);var size=0;for(var j=0;j<names.length;j++){size=Math.max(size,names[j].length)}var name="";var iInit=iValue;while(size>0&&iValue<value.length){name+=value.charAt(iValue++);for(var i=0;i<names.length;i++){if(name==names[i]){return i+1}}size--}throw"Unknown name at position "+iInit};var checkLiteral=function(){if(value.charAt(iValue)!=format.charAt(iFormat)){throw"Unexpected literal at position "+iValue}iValue++};var iValue=0;for(var iFormat=0;iFormat<format.length;iFormat++){if(literal){if(format.charAt(iFormat)=="'"&&!lookAhead("'")){literal=false}else{checkLiteral()}}else{switch(format.charAt(iFormat)){case"d":day=getNumber("d");break;case"D":getName("D",dayNamesShort,dayNames);break;case"o":doy=getNumber("o");break;case"m":month=getNumber("m");break;case"M":month=getName("M",monthNamesShort,monthNames);break;case"y":year=getNumber("y");break;case"@":var date=new Date(getNumber("@"));year=date.getFullYear();month=date.getMonth()+1;day=date.getDate();break;case"'":if(lookAhead("'")){checkLiteral()}else{literal=true}break;default:checkLiteral()}}}if(year==-1){year=new Date().getFullYear()}else{if(year<100){year+=new Date().getFullYear()-new Date().getFullYear()%100+(year<=shortYearCutoff?0:-100)}}if(doy>-1){month=1;day=doy;do{var dim=this._getDaysInMonth(year,month-1);if(day<=dim){break}month++;day-=dim}while(true)}var date=this._daylightSavingAdjust(new Date(year,month-1,day));if(date.getFullYear()!=year||date.getMonth()+1!=month||date.getDate()!=day){throw"Invalid date"}return date},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TIMESTAMP:"@",W3C:"yy-mm-dd",formatDate:function(format,date,settings){if(!date){return""}var dayNamesShort=(settings?settings.dayNamesShort:null)||this._defaults.dayNamesShort;var dayNames=(settings?settings.dayNames:null)||this._defaults.dayNames;var monthNamesShort=(settings?settings.monthNamesShort:null)||this._defaults.monthNamesShort;var monthNames=(settings?settings.monthNames:null)||this._defaults.monthNames;var lookAhead=function(match){var matches=(iFormat+1<format.length&&format.charAt(iFormat+1)==match);if(matches){iFormat++}return matches};var formatNumber=function(match,value,len){var num=""+value;if(lookAhead(match)){while(num.length<len){num="0"+num}}return num};var formatName=function(match,value,shortNames,longNames){return(lookAhead(match)?longNames[value]:shortNames[value])};var output="";var literal=false;if(date){for(var iFormat=0;iFormat<format.length;iFormat++){if(literal){if(format.charAt(iFormat)=="'"&&!lookAhead("'")){literal=false}else{output+=format.charAt(iFormat)}}else{switch(format.charAt(iFormat)){case"d":output+=formatNumber("d",date.getDate(),2);break;case"D":output+=formatName("D",date.getDay(),dayNamesShort,dayNames);break;case"o":var doy=date.getDate();for(var m=date.getMonth()-1;m>=0;m--){doy+=this._getDaysInMonth(date.getFullYear(),m)}output+=formatNumber("o",doy,3);break;case"m":output+=formatNumber("m",date.getMonth()+1,2);break;case"M":output+=formatName("M",date.getMonth(),monthNamesShort,monthNames);break;case"y":output+=(lookAhead("y")?date.getFullYear():(date.getYear()%100<10?"0":"")+date.getYear()%100);break;case"@":output+=date.getTime();break;case"'":if(lookAhead("'")){output+="'"}else{literal=true}break;default:output+=format.charAt(iFormat)}}}}return output},_possibleChars:function(format){var chars="";var literal=false;for(var iFormat=0;iFormat<format.length;iFormat++){if(literal){if(format.charAt(iFormat)=="'"&&!lookAhead("'")){literal=false}else{chars+=format.charAt(iFormat)}}else{switch(format.charAt(iFormat)){case"d":case"m":case"y":case"@":chars+="0123456789";break;case"D":case"M":return null;case"'":if(lookAhead("'")){chars+="'"}else{literal=true}break;default:chars+=format.charAt(iFormat)}}}return chars},_get:function(inst,name){return inst.settings[name]!==undefined?inst.settings[name]:this._defaults[name]},_setDateFromField:function(inst){var dateFormat=this._get(inst,"dateFormat");var dates=inst.input?inst.input.val():null;inst.endDay=inst.endMonth=inst.endYear=null;var date=defaultDate=this._getDefaultDate(inst);var settings=this._getFormatConfig(inst);try{date=this.parseDate(dateFormat,dates,settings)||defaultDate}catch(event){this.log(event);date=defaultDate}inst.selectedDay=date.getDate();inst.drawMonth=inst.selectedMonth=date.getMonth();inst.drawYear=inst.selectedYear=date.getFullYear();inst.currentDay=(dates?date.getDate():0);inst.currentMonth=(dates?date.getMonth():0);inst.currentYear=(dates?date.getFullYear():0);this._adjustInstDate(inst)},_getDefaultDate:function(inst){var date=this._determineDate(this._get(inst,"defaultDate"),new Date());var minDate=this._getMinMaxDate(inst,"min",true);var maxDate=this._getMinMaxDate(inst,"max");date=(minDate&&date<minDate?minDate:date);date=(maxDate&&date>maxDate?maxDate:date);return date},_determineDate:function(date,defaultDate){var offsetNumeric=function(offset){var date=new Date();date.setDate(date.getDate()+offset);return date};var offsetString=function(offset,getDaysInMonth){var date=new Date();var year=date.getFullYear();var month=date.getMonth();var day=date.getDate();var pattern=/([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g;var matches=pattern.exec(offset);while(matches){switch(matches[2]||"d"){case"d":case"D":day+=parseInt(matches[1],10);break;case"w":case"W":day+=parseInt(matches[1],10)*7;break;case"m":case"M":month+=parseInt(matches[1],10);day=Math.min(day,getDaysInMonth(year,month));break;case"y":case"Y":year+=parseInt(matches[1],10);day=Math.min(day,getDaysInMonth(year,month));break}matches=pattern.exec(offset)}return new Date(year,month,day)};date=(date==null?defaultDate:(typeof date=="string"?offsetString(date,this._getDaysInMonth):(typeof date=="number"?(isNaN(date)?defaultDate:offsetNumeric(date)):date)));date=(date&&date.toString()=="Invalid Date"?defaultDate:date);if(date){date.setHours(0);date.setMinutes(0);date.setSeconds(0);date.setMilliseconds(0)}return this._daylightSavingAdjust(date)},_daylightSavingAdjust:function(date){if(!date){return null}date.setHours(date.getHours()>12?date.getHours()+2:0);return date},_setDate:function(inst,date,endDate){var clear=!(date);var origMonth=inst.selectedMonth;var origYear=inst.selectedYear;date=this._determineDate(date,new Date());inst.selectedDay=inst.currentDay=date.getDate();inst.drawMonth=inst.selectedMonth=inst.currentMonth=date.getMonth();inst.drawYear=inst.selectedYear=inst.currentYear=date.getFullYear();if(origMonth!=inst.selectedMonth||origYear!=inst.selectedYear){this._notifyChange(inst)}this._adjustInstDate(inst);if(inst.input){inst.input.val(clear?"":this._formatDate(inst))}},_getDate:function(inst){var startDate=(!inst.currentYear||(inst.input&&inst.input.val()=="")?null:this._daylightSavingAdjust(new Date(inst.currentYear,inst.currentMonth,inst.currentDay)));return startDate},_generateHTML:function(inst){var today=new Date();today=this._daylightSavingAdjust(new Date(today.getFullYear(),today.getMonth(),today.getDate()));var isRTL=this._get(inst,"isRTL");var showButtonPanel=this._get(inst,"showButtonPanel");var hideIfNoPrevNext=this._get(inst,"hideIfNoPrevNext");var navigationAsDateFormat=this._get(inst,"navigationAsDateFormat");var numMonths=this._getNumberOfMonths(inst);var showCurrentAtPos=this._get(inst,"showCurrentAtPos");var stepMonths=this._get(inst,"stepMonths");var stepBigMonths=this._get(inst,"stepBigMonths");var isMultiMonth=(numMonths[0]!=1||numMonths[1]!=1);var currentDate=this._daylightSavingAdjust((!inst.currentDay?new Date(9999,9,9):new Date(inst.currentYear,inst.currentMonth,inst.currentDay)));var minDate=this._getMinMaxDate(inst,"min",true);var maxDate=this._getMinMaxDate(inst,"max");var drawMonth=inst.drawMonth-showCurrentAtPos;var drawYear=inst.drawYear;if(drawMonth<0){drawMonth+=12;drawYear--}if(maxDate){var maxDraw=this._daylightSavingAdjust(new Date(maxDate.getFullYear(),maxDate.getMonth()-numMonths[1]+1,maxDate.getDate()));maxDraw=(minDate&&maxDraw<minDate?minDate:maxDraw);while(this._daylightSavingAdjust(new Date(drawYear,drawMonth,1))>maxDraw){drawMonth--;if(drawMonth<0){drawMonth=11;drawYear--}}}inst.drawMonth=drawMonth;inst.drawYear=drawYear;var prevText=this._get(inst,"prevText");prevText=(!navigationAsDateFormat?prevText:this.formatDate(prevText,this._daylightSavingAdjust(new Date(drawYear,drawMonth-stepMonths,1)),this._getFormatConfig(inst)));var prev=(this._canAdjustMonth(inst,-1,drawYear,drawMonth)?'<a class="ui-datepicker-prev ui-corner-all" onclick="DP_jQuery.datepicker._adjustDate(\'#'+inst.id+"', -"+stepMonths+", 'M');\" title=\""+prevText+'"><span class="ui-icon ui-icon-circle-triangle-'+(isRTL?"e":"w")+'">'+prevText+"</span></a>":(hideIfNoPrevNext?"":'<a class="ui-datepicker-prev ui-corner-all ui-state-disabled" title="'+prevText+'"><span class="ui-icon ui-icon-circle-triangle-'+(isRTL?"e":"w")+'">'+prevText+"</span></a>"));var nextText=this._get(inst,"nextText");nextText=(!navigationAsDateFormat?nextText:this.formatDate(nextText,this._daylightSavingAdjust(new Date(drawYear,drawMonth+stepMonths,1)),this._getFormatConfig(inst)));var next=(this._canAdjustMonth(inst,+1,drawYear,drawMonth)?'<a class="ui-datepicker-next ui-corner-all" onclick="DP_jQuery.datepicker._adjustDate(\'#'+inst.id+"', +"+stepMonths+", 'M');\" title=\""+nextText+'"><span class="ui-icon ui-icon-circle-triangle-'+(isRTL?"w":"e")+'">'+nextText+"</span></a>":(hideIfNoPrevNext?"":'<a class="ui-datepicker-next ui-corner-all ui-state-disabled" title="'+nextText+'"><span class="ui-icon ui-icon-circle-triangle-'+(isRTL?"w":"e")+'">'+nextText+"</span></a>"));var currentText=this._get(inst,"currentText");var gotoDate=(this._get(inst,"gotoCurrent")&&inst.currentDay?currentDate:today);currentText=(!navigationAsDateFormat?currentText:this.formatDate(currentText,gotoDate,this._getFormatConfig(inst)));var controls=(!inst.inline?'<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" onclick="DP_jQuery.datepicker._hideDatepicker();">'+this._get(inst,"closeText")+"</button>":"");var buttonPanel=(showButtonPanel)?'<div class="ui-datepicker-buttonpane ui-widget-content">'+(isRTL?controls:"")+(this._isInRange(inst,gotoDate)?'<button type="button" class="ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all" onclick="DP_jQuery.datepicker._gotoToday(\'#'+inst.id+"');\">"+currentText+"</button>":"")+(isRTL?"":controls)+"</div>":"";var firstDay=parseInt(this._get(inst,"firstDay"),10);firstDay=(isNaN(firstDay)?0:firstDay);var dayNames=this._get(inst,"dayNames");var dayNamesShort=this._get(inst,"dayNamesShort");var dayNamesMin=this._get(inst,"dayNamesMin");var monthNames=this._get(inst,"monthNames");var monthNamesShort=this._get(inst,"monthNamesShort");var beforeShowDay=this._get(inst,"beforeShowDay");var showOtherMonths=this._get(inst,"showOtherMonths");var calculateWeek=this._get(inst,"calculateWeek")||this.iso8601Week;var endDate=inst.endDay?this._daylightSavingAdjust(new Date(inst.endYear,inst.endMonth,inst.endDay)):currentDate;var defaultDate=this._getDefaultDate(inst);var html="";for(var row=0;row<numMonths[0];row++){var group="";for(var col=0;col<numMonths[1];col++){var selectedDate=this._daylightSavingAdjust(new Date(drawYear,drawMonth,inst.selectedDay));var cornerClass=" ui-corner-all";var calender="";if(isMultiMonth){calender+='<div class="ui-datepicker-group ui-datepicker-group-';switch(col){case 0:calender+="first";cornerClass=" ui-corner-"+(isRTL?"right":"left");break;case numMonths[1]-1:calender+="last";cornerClass=" ui-corner-"+(isRTL?"left":"right");break;default:calender+="middle";cornerClass="";break}calender+='">'}calender+='<div class="ui-datepicker-header ui-widget-header ui-helper-clearfix'+cornerClass+'">'+(/all|left/.test(cornerClass)&&row==0?(isRTL?next:prev):"")+(/all|right/.test(cornerClass)&&row==0?(isRTL?prev:next):"")+this._generateMonthYearHeader(inst,drawMonth,drawYear,minDate,maxDate,selectedDate,row>0||col>0,monthNames,monthNamesShort)+'</div><table class="ui-datepicker-calendar"><thead><tr>';var thead="";for(var dow=0;dow<7;dow++){var day=(dow+firstDay)%7;thead+="<th"+((dow+firstDay+6)%7>=5?' class="ui-datepicker-week-end"':"")+'><span title="'+dayNames[day]+'">'+dayNamesMin[day]+"</span></th>"}calender+=thead+"</tr></thead><tbody>";var daysInMonth=this._getDaysInMonth(drawYear,drawMonth);if(drawYear==inst.selectedYear&&drawMonth==inst.selectedMonth){inst.selectedDay=Math.min(inst.selectedDay,daysInMonth)}var leadDays=(this._getFirstDayOfMonth(drawYear,drawMonth)-firstDay+7)%7;var numRows=(isMultiMonth?6:Math.ceil((leadDays+daysInMonth)/7));var printDate=this._daylightSavingAdjust(new Date(drawYear,drawMonth,1-leadDays));for(var dRow=0;dRow<numRows;dRow++){calender+="<tr>";var tbody="";for(var dow=0;dow<7;dow++){var daySettings=(beforeShowDay?beforeShowDay.apply((inst.input?inst.input[0]:null),[printDate]):[true,""]);var otherMonth=(printDate.getMonth()!=drawMonth);var unselectable=otherMonth||!daySettings[0]||(minDate&&printDate<minDate)||(maxDate&&printDate>maxDate);tbody+='<td class="'+((dow+firstDay+6)%7>=5?" ui-datepicker-week-end":"")+(otherMonth?" ui-datepicker-other-month":"")+((printDate.getTime()==selectedDate.getTime()&&drawMonth==inst.selectedMonth&&inst._keyEvent)||(defaultDate.getTime()==printDate.getTime()&&defaultDate.getTime()==selectedDate.getTime())?" "+this._dayOverClass:"")+(unselectable?" "+this._unselectableClass+" ui-state-disabled":"")+(otherMonth&&!showOtherMonths?"":" "+daySettings[1]+(printDate.getTime()>=currentDate.getTime()&&printDate.getTime()<=endDate.getTime()?" "+this._currentClass:"")+(printDate.getTime()==today.getTime()?" ui-datepicker-today":""))+'"'+((!otherMonth||showOtherMonths)&&daySettings[2]?' title="'+daySettings[2]+'"':"")+(unselectable?"":" onclick=\"DP_jQuery.datepicker._selectDay('#"+inst.id+"',"+drawMonth+","+drawYear+', this);return false;"')+">"+(otherMonth?(showOtherMonths?printDate.getDate():"&#xa0;"):(unselectable?'<span class="ui-state-default">'+printDate.getDate()+"</span>":'<a class="ui-state-default'+(printDate.getTime()==today.getTime()?" ui-state-highlight":"")+(printDate.getTime()>=currentDate.getTime()&&printDate.getTime()<=endDate.getTime()?" ui-state-active":"")+'" href="#">'+printDate.getDate()+"</a>"))+"</td>";printDate.setDate(printDate.getDate()+1);printDate=this._daylightSavingAdjust(printDate)}calender+=tbody+"</tr>"}drawMonth++;if(drawMonth>11){drawMonth=0;drawYear++}calender+="</tbody></table>"+(isMultiMonth?"</div>"+((numMonths[0]>0&&col==numMonths[1]-1)?'<div class="ui-datepicker-row-break"></div>':""):"");group+=calender}html+=group}html+=buttonPanel+($.browser.msie&&parseInt($.browser.version,10)<7&&!inst.inline?'<iframe src="javascript:false;" class="ui-datepicker-cover" frameborder="0"></iframe>':"");inst._keyEvent=false;return html},_generateMonthYearHeader:function(inst,drawMonth,drawYear,minDate,maxDate,selectedDate,secondary,monthNames,monthNamesShort){minDate=(inst.rangeStart&&minDate&&selectedDate<minDate?selectedDate:minDate);var changeMonth=this._get(inst,"changeMonth");var changeYear=this._get(inst,"changeYear");var showMonthAfterYear=this._get(inst,"showMonthAfterYear");var html='<div class="ui-datepicker-title">';var monthHtml="";if(secondary||!changeMonth){monthHtml+='<span class="ui-datepicker-month">'+monthNames[drawMonth]+"</span> "}else{var inMinYear=(minDate&&minDate.getFullYear()==drawYear);var inMaxYear=(maxDate&&maxDate.getFullYear()==drawYear);monthHtml+='<select class="ui-datepicker-month" onchange="DP_jQuery.datepicker._selectMonthYear(\'#'+inst.id+"', this, 'M');\" onclick=\"DP_jQuery.datepicker._clickMonthYear('#"+inst.id+"');\">";for(var month=0;month<12;month++){if((!inMinYear||month>=minDate.getMonth())&&(!inMaxYear||month<=maxDate.getMonth())){monthHtml+='<option value="'+month+'"'+(month==drawMonth?' selected="selected"':"")+">"+monthNamesShort[month]+"</option>"}}monthHtml+="</select>"}if(!showMonthAfterYear){html+=monthHtml+((secondary||changeMonth||changeYear)&&(!(changeMonth&&changeYear))?"&#xa0;":"")}if(secondary||!changeYear){html+='<span class="ui-datepicker-year">'+drawYear+"</span>"}else{var years=this._get(inst,"yearRange").split(":");var year=0;var endYear=0;if(years.length!=2){year=drawYear-10;endYear=drawYear+10}else{if(years[0].charAt(0)=="+"||years[0].charAt(0)=="-"){year=drawYear+parseInt(years[0],10);endYear=drawYear+parseInt(years[1],10)}else{year=parseInt(years[0],10);endYear=parseInt(years[1],10)}}year=(minDate?Math.max(year,minDate.getFullYear()):year);endYear=(maxDate?Math.min(endYear,maxDate.getFullYear()):endYear);html+='<select class="ui-datepicker-year" onchange="DP_jQuery.datepicker._selectMonthYear(\'#'+inst.id+"', this, 'Y');\" onclick=\"DP_jQuery.datepicker._clickMonthYear('#"+inst.id+"');\">";for(;year<=endYear;year++){html+='<option value="'+year+'"'+(year==drawYear?' selected="selected"':"")+">"+year+"</option>"}html+="</select>"}if(showMonthAfterYear){html+=(secondary||changeMonth||changeYear?"&#xa0;":"")+monthHtml}html+="</div>";return html},_adjustInstDate:function(inst,offset,period){var year=inst.drawYear+(period=="Y"?offset:0);var month=inst.drawMonth+(period=="M"?offset:0);var day=Math.min(inst.selectedDay,this._getDaysInMonth(year,month))+(period=="D"?offset:0);var date=this._daylightSavingAdjust(new Date(year,month,day));var minDate=this._getMinMaxDate(inst,"min",true);var maxDate=this._getMinMaxDate(inst,"max");date=(minDate&&date<minDate?minDate:date);date=(maxDate&&date>maxDate?maxDate:date);inst.selectedDay=date.getDate();inst.drawMonth=inst.selectedMonth=date.getMonth();inst.drawYear=inst.selectedYear=date.getFullYear();if(period=="M"||period=="Y"){this._notifyChange(inst)}},_notifyChange:function(inst){var onChange=this._get(inst,"onChangeMonthYear");if(onChange){onChange.apply((inst.input?inst.input[0]:null),[inst.selectedYear,inst.selectedMonth+1,inst])}},_getNumberOfMonths:function(inst){var numMonths=this._get(inst,"numberOfMonths");return(numMonths==null?[1,1]:(typeof numMonths=="number"?[1,numMonths]:numMonths))},_getMinMaxDate:function(inst,minMax,checkRange){var date=this._determineDate(this._get(inst,minMax+"Date"),null);return(!checkRange||!inst.rangeStart?date:(!date||inst.rangeStart>date?inst.rangeStart:date))},_getDaysInMonth:function(year,month){return 32-new Date(year,month,32).getDate()},_getFirstDayOfMonth:function(year,month){return new Date(year,month,1).getDay()},_canAdjustMonth:function(inst,offset,curYear,curMonth){var numMonths=this._getNumberOfMonths(inst);var date=this._daylightSavingAdjust(new Date(curYear,curMonth+(offset<0?offset:numMonths[1]),1));if(offset<0){date.setDate(this._getDaysInMonth(date.getFullYear(),date.getMonth()))}return this._isInRange(inst,date)},_isInRange:function(inst,date){var newMinDate=(!inst.rangeStart?null:this._daylightSavingAdjust(new Date(inst.selectedYear,inst.selectedMonth,inst.selectedDay)));newMinDate=(newMinDate&&inst.rangeStart<newMinDate?inst.rangeStart:newMinDate);var minDate=newMinDate||this._getMinMaxDate(inst,"min");var maxDate=this._getMinMaxDate(inst,"max");return((!minDate||date>=minDate)&&(!maxDate||date<=maxDate))},_getFormatConfig:function(inst){var shortYearCutoff=this._get(inst,"shortYearCutoff");shortYearCutoff=(typeof shortYearCutoff!="string"?shortYearCutoff:new Date().getFullYear()%100+parseInt(shortYearCutoff,10));return{shortYearCutoff:shortYearCutoff,dayNamesShort:this._get(inst,"dayNamesShort"),dayNames:this._get(inst,"dayNames"),monthNamesShort:this._get(inst,"monthNamesShort"),monthNames:this._get(inst,"monthNames")}},_formatDate:function(inst,day,month,year){if(!day){inst.currentDay=inst.selectedDay;inst.currentMonth=inst.selectedMonth;inst.currentYear=inst.selectedYear}var date=(day?(typeof day=="object"?day:this._daylightSavingAdjust(new Date(year,month,day))):this._daylightSavingAdjust(new Date(inst.currentYear,inst.currentMonth,inst.currentDay)));return this.formatDate(this._get(inst,"dateFormat"),date,this._getFormatConfig(inst))}});function extendRemove(target,props){$.extend(target,props);for(var name in props){if(props[name]==null||props[name]==undefined){target[name]=props[name]}}return target}function isArray(a){return(a&&(($.browser.safari&&typeof a=="object"&&a.length)||(a.constructor&&a.constructor.toString().match(/\Array\(\)/))))}$.fn.datepicker=function(options){if(!$.datepicker.initialized){$(document).mousedown($.datepicker._checkExternalClick).find("body").append($.datepicker.dpDiv);$.datepicker.initialized=true}var otherArgs=Array.prototype.slice.call(arguments,1);if(typeof options=="string"&&(options=="isDisabled"||options=="getDate")){return $.datepicker["_"+options+"Datepicker"].apply($.datepicker,[this[0]].concat(otherArgs))}if(options=="option"&&arguments.length==2&&typeof arguments[1]=="string"){return $.datepicker["_"+options+"Datepicker"].apply($.datepicker,[this[0]].concat(otherArgs))}return this.each(function(){typeof options=="string"?$.datepicker["_"+options+"Datepicker"].apply($.datepicker,[this].concat(otherArgs)):$.datepicker._attachDatepicker(this,options)})};$.datepicker=new Datepicker();$.datepicker.initialized=false;$.datepicker.uuid=new Date().getTime();$.datepicker.version="1.7.2";window.DP_jQuery=$})(jQuery);;/*
+ * jQuery UI Progressbar 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Progressbar
+ *
+ * Depends:
+ *   ui.core.js
+ */
+(function(a){a.widget("ui.progressbar",{_init:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this._valueMin(),"aria-valuemax":this._valueMax(),"aria-valuenow":this._value()});this.valueDiv=a('<div class="ui-progressbar-value ui-widget-header ui-corner-left"></div>').appendTo(this.element);this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow").removeData("progressbar").unbind(".progressbar");this.valueDiv.remove();a.widget.prototype.destroy.apply(this,arguments)},value:function(b){if(b===undefined){return this._value()}this._setData("value",b);return this},_setData:function(b,c){switch(b){case"value":this.options.value=c;this._refreshValue();this._trigger("change",null,{});break}a.widget.prototype._setData.apply(this,arguments)},_value:function(){var b=this.options.value;if(b<this._valueMin()){b=this._valueMin()}if(b>this._valueMax()){b=this._valueMax()}return b},_valueMin:function(){var b=0;return b},_valueMax:function(){var b=100;return b},_refreshValue:function(){var b=this.value();this.valueDiv[b==this._valueMax()?"addClass":"removeClass"]("ui-corner-right");this.valueDiv.width(b+"%");this.element.attr("aria-valuenow",b)}});a.extend(a.ui.progressbar,{version:"1.7.2",defaults:{value:0}})})(jQuery);;/*
+ * jQuery UI Effects 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/
+ */
+jQuery.effects||(function(d){d.effects={version:"1.7.2",save:function(g,h){for(var f=0;f<h.length;f++){if(h[f]!==null){g.data("ec.storage."+h[f],g[0].style[h[f]])}}},restore:function(g,h){for(var f=0;f<h.length;f++){if(h[f]!==null){g.css(h[f],g.data("ec.storage."+h[f]))}}},setMode:function(f,g){if(g=="toggle"){g=f.is(":hidden")?"show":"hide"}return g},getBaseline:function(g,h){var i,f;switch(g[0]){case"top":i=0;break;case"middle":i=0.5;break;case"bottom":i=1;break;default:i=g[0]/h.height}switch(g[1]){case"left":f=0;break;case"center":f=0.5;break;case"right":f=1;break;default:f=g[1]/h.width}return{x:f,y:i}},createWrapper:function(f){if(f.parent().is(".ui-effects-wrapper")){return f.parent()}var g={width:f.outerWidth(true),height:f.outerHeight(true),"float":f.css("float")};f.wrap('<div class="ui-effects-wrapper" style="font-size:100%;background:transparent;border:none;margin:0;padding:0"></div>');var j=f.parent();if(f.css("position")=="static"){j.css({position:"relative"});f.css({position:"relative"})}else{var i=f.css("top");if(isNaN(parseInt(i,10))){i="auto"}var h=f.css("left");if(isNaN(parseInt(h,10))){h="auto"}j.css({position:f.css("position"),top:i,left:h,zIndex:f.css("z-index")}).show();f.css({position:"relative",top:0,left:0})}j.css(g);return j},removeWrapper:function(f){if(f.parent().is(".ui-effects-wrapper")){return f.parent().replaceWith(f)}return f},setTransition:function(g,i,f,h){h=h||{};d.each(i,function(k,j){unit=g.cssUnit(j);if(unit[0]>0){h[j]=unit[0]*f+unit[1]}});return h},animateClass:function(h,i,k,j){var f=(typeof k=="function"?k:(j?j:null));var g=(typeof k=="string"?k:null);return this.each(function(){var q={};var o=d(this);var p=o.attr("style")||"";if(typeof p=="object"){p=p.cssText}if(h.toggle){o.hasClass(h.toggle)?h.remove=h.toggle:h.add=h.toggle}var l=d.extend({},(document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle));if(h.add){o.addClass(h.add)}if(h.remove){o.removeClass(h.remove)}var m=d.extend({},(document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle));if(h.add){o.removeClass(h.add)}if(h.remove){o.addClass(h.remove)}for(var r in m){if(typeof m[r]!="function"&&m[r]&&r.indexOf("Moz")==-1&&r.indexOf("length")==-1&&m[r]!=l[r]&&(r.match(/color/i)||(!r.match(/color/i)&&!isNaN(parseInt(m[r],10))))&&(l.position!="static"||(l.position=="static"&&!r.match(/left|top|bottom|right/)))){q[r]=m[r]}}o.animate(q,i,g,function(){if(typeof d(this).attr("style")=="object"){d(this).attr("style")["cssText"]="";d(this).attr("style")["cssText"]=p}else{d(this).attr("style",p)}if(h.add){d(this).addClass(h.add)}if(h.remove){d(this).removeClass(h.remove)}if(f){f.apply(this,arguments)}})})}};function c(g,f){var i=g[1]&&g[1].constructor==Object?g[1]:{};if(f){i.mode=f}var h=g[1]&&g[1].constructor!=Object?g[1]:(i.duration?i.duration:g[2]);h=d.fx.off?0:typeof h==="number"?h:d.fx.speeds[h]||d.fx.speeds._default;var j=i.callback||(d.isFunction(g[1])&&g[1])||(d.isFunction(g[2])&&g[2])||(d.isFunction(g[3])&&g[3]);return[g[0],i,h,j]}d.fn.extend({_show:d.fn.show,_hide:d.fn.hide,__toggle:d.fn.toggle,_addClass:d.fn.addClass,_removeClass:d.fn.removeClass,_toggleClass:d.fn.toggleClass,effect:function(g,f,h,i){return d.effects[g]?d.effects[g].call(this,{method:g,options:f||{},duration:h,callback:i}):null},show:function(){if(!arguments[0]||(arguments[0].constructor==Number||(/(slow|normal|fast)/).test(arguments[0]))){return this._show.apply(this,arguments)}else{return this.effect.apply(this,c(arguments,"show"))}},hide:function(){if(!arguments[0]||(arguments[0].constructor==Number||(/(slow|normal|fast)/).test(arguments[0]))){return this._hide.apply(this,arguments)}else{return this.effect.apply(this,c(arguments,"hide"))}},toggle:function(){if(!arguments[0]||(arguments[0].constructor==Number||(/(slow|normal|fast)/).test(arguments[0]))||(d.isFunction(arguments[0])||typeof arguments[0]=="boolean")){return this.__toggle.apply(this,arguments)}else{return this.effect.apply(this,c(arguments,"toggle"))}},addClass:function(g,f,i,h){return f?d.effects.animateClass.apply(this,[{add:g},f,i,h]):this._addClass(g)},removeClass:function(g,f,i,h){return f?d.effects.animateClass.apply(this,[{remove:g},f,i,h]):this._removeClass(g)},toggleClass:function(g,f,i,h){return((typeof f!=="boolean")&&f)?d.effects.animateClass.apply(this,[{toggle:g},f,i,h]):this._toggleClass(g,f)},morph:function(f,h,g,j,i){return d.effects.animateClass.apply(this,[{add:h,remove:f},g,j,i])},switchClass:function(){return this.morph.apply(this,arguments)},cssUnit:function(f){var g=this.css(f),h=[];d.each(["em","px","%","pt"],function(j,k){if(g.indexOf(k)>0){h=[parseFloat(g),k]}});return h}});d.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","color","outlineColor"],function(g,f){d.fx.step[f]=function(h){if(h.state==0){h.start=e(h.elem,f);h.end=b(h.end)}h.elem.style[f]="rgb("+[Math.max(Math.min(parseInt((h.pos*(h.end[0]-h.start[0]))+h.start[0],10),255),0),Math.max(Math.min(parseInt((h.pos*(h.end[1]-h.start[1]))+h.start[1],10),255),0),Math.max(Math.min(parseInt((h.pos*(h.end[2]-h.start[2]))+h.start[2],10),255),0)].join(",")+")"}});function b(g){var f;if(g&&g.constructor==Array&&g.length==3){return g}if(f=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(g)){return[parseInt(f[1],10),parseInt(f[2],10),parseInt(f[3],10)]}if(f=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(g)){return[parseFloat(f[1])*2.55,parseFloat(f[2])*2.55,parseFloat(f[3])*2.55]}if(f=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(g)){return[parseInt(f[1],16),parseInt(f[2],16),parseInt(f[3],16)]}if(f=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(g)){return[parseInt(f[1]+f[1],16),parseInt(f[2]+f[2],16),parseInt(f[3]+f[3],16)]}if(f=/rgba\(0, 0, 0, 0\)/.exec(g)){return a.transparent}return a[d.trim(g).toLowerCase()]}function e(h,f){var g;do{g=d.curCSS(h,f);if(g!=""&&g!="transparent"||d.nodeName(h,"body")){break}f="backgroundColor"}while(h=h.parentNode);return b(g)}var a={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]};d.easing.jswing=d.easing.swing;d.extend(d.easing,{def:"easeOutQuad",swing:function(g,h,f,j,i){return d.easing[d.easing.def](g,h,f,j,i)},easeInQuad:function(g,h,f,j,i){return j*(h/=i)*h+f},easeOutQuad:function(g,h,f,j,i){return -j*(h/=i)*(h-2)+f},easeInOutQuad:function(g,h,f,j,i){if((h/=i/2)<1){return j/2*h*h+f}return -j/2*((--h)*(h-2)-1)+f},easeInCubic:function(g,h,f,j,i){return j*(h/=i)*h*h+f},easeOutCubic:function(g,h,f,j,i){return j*((h=h/i-1)*h*h+1)+f},easeInOutCubic:function(g,h,f,j,i){if((h/=i/2)<1){return j/2*h*h*h+f}return j/2*((h-=2)*h*h+2)+f},easeInQuart:function(g,h,f,j,i){return j*(h/=i)*h*h*h+f},easeOutQuart:function(g,h,f,j,i){return -j*((h=h/i-1)*h*h*h-1)+f},easeInOutQuart:function(g,h,f,j,i){if((h/=i/2)<1){return j/2*h*h*h*h+f}return -j/2*((h-=2)*h*h*h-2)+f},easeInQuint:function(g,h,f,j,i){return j*(h/=i)*h*h*h*h+f},easeOutQuint:function(g,h,f,j,i){return j*((h=h/i-1)*h*h*h*h+1)+f},easeInOutQuint:function(g,h,f,j,i){if((h/=i/2)<1){return j/2*h*h*h*h*h+f}return j/2*((h-=2)*h*h*h*h+2)+f},easeInSine:function(g,h,f,j,i){return -j*Math.cos(h/i*(Math.PI/2))+j+f},easeOutSine:function(g,h,f,j,i){return j*Math.sin(h/i*(Math.PI/2))+f},easeInOutSine:function(g,h,f,j,i){return -j/2*(Math.cos(Math.PI*h/i)-1)+f},easeInExpo:function(g,h,f,j,i){return(h==0)?f:j*Math.pow(2,10*(h/i-1))+f},easeOutExpo:function(g,h,f,j,i){return(h==i)?f+j:j*(-Math.pow(2,-10*h/i)+1)+f},easeInOutExpo:function(g,h,f,j,i){if(h==0){return f}if(h==i){return f+j}if((h/=i/2)<1){return j/2*Math.pow(2,10*(h-1))+f}return j/2*(-Math.pow(2,-10*--h)+2)+f},easeInCirc:function(g,h,f,j,i){return -j*(Math.sqrt(1-(h/=i)*h)-1)+f},easeOutCirc:function(g,h,f,j,i){return j*Math.sqrt(1-(h=h/i-1)*h)+f},easeInOutCirc:function(g,h,f,j,i){if((h/=i/2)<1){return -j/2*(Math.sqrt(1-h*h)-1)+f}return j/2*(Math.sqrt(1-(h-=2)*h)+1)+f},easeInElastic:function(g,i,f,m,l){var j=1.70158;var k=0;var h=m;if(i==0){return f}if((i/=l)==1){return f+m}if(!k){k=l*0.3}if(h<Math.abs(m)){h=m;var j=k/4}else{var j=k/(2*Math.PI)*Math.asin(m/h)}return -(h*Math.pow(2,10*(i-=1))*Math.sin((i*l-j)*(2*Math.PI)/k))+f},easeOutElastic:function(g,i,f,m,l){var j=1.70158;var k=0;var h=m;if(i==0){return f}if((i/=l)==1){return f+m}if(!k){k=l*0.3}if(h<Math.abs(m)){h=m;var j=k/4}else{var j=k/(2*Math.PI)*Math.asin(m/h)}return h*Math.pow(2,-10*i)*Math.sin((i*l-j)*(2*Math.PI)/k)+m+f},easeInOutElastic:function(g,i,f,m,l){var j=1.70158;var k=0;var h=m;if(i==0){return f}if((i/=l/2)==2){return f+m}if(!k){k=l*(0.3*1.5)}if(h<Math.abs(m)){h=m;var j=k/4}else{var j=k/(2*Math.PI)*Math.asin(m/h)}if(i<1){return -0.5*(h*Math.pow(2,10*(i-=1))*Math.sin((i*l-j)*(2*Math.PI)/k))+f}return h*Math.pow(2,-10*(i-=1))*Math.sin((i*l-j)*(2*Math.PI)/k)*0.5+m+f},easeInBack:function(g,h,f,k,j,i){if(i==undefined){i=1.70158}return k*(h/=j)*h*((i+1)*h-i)+f},easeOutBack:function(g,h,f,k,j,i){if(i==undefined){i=1.70158}return k*((h=h/j-1)*h*((i+1)*h+i)+1)+f},easeInOutBack:function(g,h,f,k,j,i){if(i==undefined){i=1.70158}if((h/=j/2)<1){return k/2*(h*h*(((i*=(1.525))+1)*h-i))+f}return k/2*((h-=2)*h*(((i*=(1.525))+1)*h+i)+2)+f},easeInBounce:function(g,h,f,j,i){return j-d.easing.easeOutBounce(g,i-h,0,j,i)+f},easeOutBounce:function(g,h,f,j,i){if((h/=i)<(1/2.75)){return j*(7.5625*h*h)+f}else{if(h<(2/2.75)){return j*(7.5625*(h-=(1.5/2.75))*h+0.75)+f}else{if(h<(2.5/2.75)){return j*(7.5625*(h-=(2.25/2.75))*h+0.9375)+f}else{return j*(7.5625*(h-=(2.625/2.75))*h+0.984375)+f}}}},easeInOutBounce:function(g,h,f,j,i){if(h<i/2){return d.easing.easeInBounce(g,h*2,0,j,i)*0.5+f}return d.easing.easeOutBounce(g,h*2-i,0,j,i)*0.5+j*0.5+f}})})(jQuery);;/*
+ * jQuery UI Effects Blind 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Blind
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.blind=function(b){return this.queue(function(){var d=a(this),c=["position","top","left"];var h=a.effects.setMode(d,b.options.mode||"hide");var g=b.options.direction||"vertical";a.effects.save(d,c);d.show();var j=a.effects.createWrapper(d).css({overflow:"hidden"});var e=(g=="vertical")?"height":"width";var i=(g=="vertical")?j.height():j.width();if(h=="show"){j.css(e,0)}var f={};f[e]=h=="show"?i:0;j.animate(f,b.duration,b.options.easing,function(){if(h=="hide"){d.hide()}a.effects.restore(d,c);a.effects.removeWrapper(d);if(b.callback){b.callback.apply(d[0],arguments)}d.dequeue()})})}})(jQuery);;/*
+ * jQuery UI Effects Bounce 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Bounce
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.bounce=function(b){return this.queue(function(){var e=a(this),l=["position","top","left"];var k=a.effects.setMode(e,b.options.mode||"effect");var n=b.options.direction||"up";var c=b.options.distance||20;var d=b.options.times||5;var g=b.duration||250;if(/show|hide/.test(k)){l.push("opacity")}a.effects.save(e,l);e.show();a.effects.createWrapper(e);var f=(n=="up"||n=="down")?"top":"left";var p=(n=="up"||n=="left")?"pos":"neg";var c=b.options.distance||(f=="top"?e.outerHeight({margin:true})/3:e.outerWidth({margin:true})/3);if(k=="show"){e.css("opacity",0).css(f,p=="pos"?-c:c)}if(k=="hide"){c=c/(d*2)}if(k!="hide"){d--}if(k=="show"){var h={opacity:1};h[f]=(p=="pos"?"+=":"-=")+c;e.animate(h,g/2,b.options.easing);c=c/2;d--}for(var j=0;j<d;j++){var o={},m={};o[f]=(p=="pos"?"-=":"+=")+c;m[f]=(p=="pos"?"+=":"-=")+c;e.animate(o,g/2,b.options.easing).animate(m,g/2,b.options.easing);c=(k=="hide")?c*2:c/2}if(k=="hide"){var h={opacity:0};h[f]=(p=="pos"?"-=":"+=")+c;e.animate(h,g/2,b.options.easing,function(){e.hide();a.effects.restore(e,l);a.effects.removeWrapper(e);if(b.callback){b.callback.apply(this,arguments)}})}else{var o={},m={};o[f]=(p=="pos"?"-=":"+=")+c;m[f]=(p=="pos"?"+=":"-=")+c;e.animate(o,g/2,b.options.easing).animate(m,g/2,b.options.easing,function(){a.effects.restore(e,l);a.effects.removeWrapper(e);if(b.callback){b.callback.apply(this,arguments)}})}e.queue("fx",function(){e.dequeue()});e.dequeue()})}})(jQuery);;/*
+ * jQuery UI Effects Clip 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Clip
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.clip=function(b){return this.queue(function(){var f=a(this),j=["position","top","left","height","width"];var i=a.effects.setMode(f,b.options.mode||"hide");var k=b.options.direction||"vertical";a.effects.save(f,j);f.show();var c=a.effects.createWrapper(f).css({overflow:"hidden"});var e=f[0].tagName=="IMG"?c:f;var g={size:(k=="vertical")?"height":"width",position:(k=="vertical")?"top":"left"};var d=(k=="vertical")?e.height():e.width();if(i=="show"){e.css(g.size,0);e.css(g.position,d/2)}var h={};h[g.size]=i=="show"?d:0;h[g.position]=i=="show"?0:d/2;e.animate(h,{queue:false,duration:b.duration,easing:b.options.easing,complete:function(){if(i=="hide"){f.hide()}a.effects.restore(f,j);a.effects.removeWrapper(f);if(b.callback){b.callback.apply(f[0],arguments)}f.dequeue()}})})}})(jQuery);;/*
+ * jQuery UI Effects Drop 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Drop
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.drop=function(b){return this.queue(function(){var e=a(this),d=["position","top","left","opacity"];var i=a.effects.setMode(e,b.options.mode||"hide");var h=b.options.direction||"left";a.effects.save(e,d);e.show();a.effects.createWrapper(e);var f=(h=="up"||h=="down")?"top":"left";var c=(h=="up"||h=="left")?"pos":"neg";var j=b.options.distance||(f=="top"?e.outerHeight({margin:true})/2:e.outerWidth({margin:true})/2);if(i=="show"){e.css("opacity",0).css(f,c=="pos"?-j:j)}var g={opacity:i=="show"?1:0};g[f]=(i=="show"?(c=="pos"?"+=":"-="):(c=="pos"?"-=":"+="))+j;e.animate(g,{queue:false,duration:b.duration,easing:b.options.easing,complete:function(){if(i=="hide"){e.hide()}a.effects.restore(e,d);a.effects.removeWrapper(e);if(b.callback){b.callback.apply(this,arguments)}e.dequeue()}})})}})(jQuery);;/*
+ * jQuery UI Effects Explode 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Explode
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.explode=function(b){return this.queue(function(){var k=b.options.pieces?Math.round(Math.sqrt(b.options.pieces)):3;var e=b.options.pieces?Math.round(Math.sqrt(b.options.pieces)):3;b.options.mode=b.options.mode=="toggle"?(a(this).is(":visible")?"hide":"show"):b.options.mode;var h=a(this).show().css("visibility","hidden");var l=h.offset();l.top-=parseInt(h.css("marginTop"),10)||0;l.left-=parseInt(h.css("marginLeft"),10)||0;var g=h.outerWidth(true);var c=h.outerHeight(true);for(var f=0;f<k;f++){for(var d=0;d<e;d++){h.clone().appendTo("body").wrap("<div></div>").css({position:"absolute",visibility:"visible",left:-d*(g/e),top:-f*(c/k)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:g/e,height:c/k,left:l.left+d*(g/e)+(b.options.mode=="show"?(d-Math.floor(e/2))*(g/e):0),top:l.top+f*(c/k)+(b.options.mode=="show"?(f-Math.floor(k/2))*(c/k):0),opacity:b.options.mode=="show"?0:1}).animate({left:l.left+d*(g/e)+(b.options.mode=="show"?0:(d-Math.floor(e/2))*(g/e)),top:l.top+f*(c/k)+(b.options.mode=="show"?0:(f-Math.floor(k/2))*(c/k)),opacity:b.options.mode=="show"?1:0},b.duration||500)}}setTimeout(function(){b.options.mode=="show"?h.css({visibility:"visible"}):h.css({visibility:"visible"}).hide();if(b.callback){b.callback.apply(h[0])}h.dequeue();a("div.ui-effects-explode").remove()},b.duration||500)})}})(jQuery);;/*
+ * jQuery UI Effects Fold 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Fold
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.fold=function(b){return this.queue(function(){var e=a(this),k=["position","top","left"];var h=a.effects.setMode(e,b.options.mode||"hide");var o=b.options.size||15;var n=!(!b.options.horizFirst);var g=b.duration?b.duration/2:a.fx.speeds._default/2;a.effects.save(e,k);e.show();var d=a.effects.createWrapper(e).css({overflow:"hidden"});var i=((h=="show")!=n);var f=i?["width","height"]:["height","width"];var c=i?[d.width(),d.height()]:[d.height(),d.width()];var j=/([0-9]+)%/.exec(o);if(j){o=parseInt(j[1],10)/100*c[h=="hide"?0:1]}if(h=="show"){d.css(n?{height:0,width:o}:{height:o,width:0})}var m={},l={};m[f[0]]=h=="show"?c[0]:o;l[f[1]]=h=="show"?c[1]:0;d.animate(m,g,b.options.easing).animate(l,g,b.options.easing,function(){if(h=="hide"){e.hide()}a.effects.restore(e,k);a.effects.removeWrapper(e);if(b.callback){b.callback.apply(e[0],arguments)}e.dequeue()})})}})(jQuery);;/*
+ * jQuery UI Effects Highlight 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Highlight
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.highlight=function(b){return this.queue(function(){var e=a(this),d=["backgroundImage","backgroundColor","opacity"];var h=a.effects.setMode(e,b.options.mode||"show");var c=b.options.color||"#ffff99";var g=e.css("backgroundColor");a.effects.save(e,d);e.show();e.css({backgroundImage:"none",backgroundColor:c});var f={backgroundColor:g};if(h=="hide"){f.opacity=0}e.animate(f,{queue:false,duration:b.duration,easing:b.options.easing,complete:function(){if(h=="hide"){e.hide()}a.effects.restore(e,d);if(h=="show"&&a.browser.msie){this.style.removeAttribute("filter")}if(b.callback){b.callback.apply(this,arguments)}e.dequeue()}})})}})(jQuery);;/*
+ * jQuery UI Effects Pulsate 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Pulsate
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.pulsate=function(b){return this.queue(function(){var d=a(this);var g=a.effects.setMode(d,b.options.mode||"show");var f=b.options.times||5;var e=b.duration?b.duration/2:a.fx.speeds._default/2;if(g=="hide"){f--}if(d.is(":hidden")){d.css("opacity",0);d.show();d.animate({opacity:1},e,b.options.easing);f=f-2}for(var c=0;c<f;c++){d.animate({opacity:0},e,b.options.easing).animate({opacity:1},e,b.options.easing)}if(g=="hide"){d.animate({opacity:0},e,b.options.easing,function(){d.hide();if(b.callback){b.callback.apply(this,arguments)}})}else{d.animate({opacity:0},e,b.options.easing).animate({opacity:1},e,b.options.easing,function(){if(b.callback){b.callback.apply(this,arguments)}})}d.queue("fx",function(){d.dequeue()});d.dequeue()})}})(jQuery);;/*
+ * jQuery UI Effects Scale 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Scale
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.puff=function(b){return this.queue(function(){var f=a(this);var c=a.extend(true,{},b.options);var h=a.effects.setMode(f,b.options.mode||"hide");var g=parseInt(b.options.percent,10)||150;c.fade=true;var e={height:f.height(),width:f.width()};var d=g/100;f.from=(h=="hide")?e:{height:e.height*d,width:e.width*d};c.from=f.from;c.percent=(h=="hide")?g:100;c.mode=h;f.effect("scale",c,b.duration,b.callback);f.dequeue()})};a.effects.scale=function(b){return this.queue(function(){var g=a(this);var d=a.extend(true,{},b.options);var j=a.effects.setMode(g,b.options.mode||"effect");var h=parseInt(b.options.percent,10)||(parseInt(b.options.percent,10)==0?0:(j=="hide"?0:100));var i=b.options.direction||"both";var c=b.options.origin;if(j!="effect"){d.origin=c||["middle","center"];d.restore=true}var f={height:g.height(),width:g.width()};g.from=b.options.from||(j=="show"?{height:0,width:0}:f);var e={y:i!="horizontal"?(h/100):1,x:i!="vertical"?(h/100):1};g.to={height:f.height*e.y,width:f.width*e.x};if(b.options.fade){if(j=="show"){g.from.opacity=0;g.to.opacity=1}if(j=="hide"){g.from.opacity=1;g.to.opacity=0}}d.from=g.from;d.to=g.to;d.mode=j;g.effect("size",d,b.duration,b.callback);g.dequeue()})};a.effects.size=function(b){return this.queue(function(){var c=a(this),n=["position","top","left","width","height","overflow","opacity"];var m=["position","top","left","overflow","opacity"];var j=["width","height","overflow"];var p=["fontSize"];var k=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"];var f=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"];var g=a.effects.setMode(c,b.options.mode||"effect");var i=b.options.restore||false;var e=b.options.scale||"both";var o=b.options.origin;var d={height:c.height(),width:c.width()};c.from=b.options.from||d;c.to=b.options.to||d;if(o){var h=a.effects.getBaseline(o,d);c.from.top=(d.height-c.from.height)*h.y;c.from.left=(d.width-c.from.width)*h.x;c.to.top=(d.height-c.to.height)*h.y;c.to.left=(d.width-c.to.width)*h.x}var l={from:{y:c.from.height/d.height,x:c.from.width/d.width},to:{y:c.to.height/d.height,x:c.to.width/d.width}};if(e=="box"||e=="both"){if(l.from.y!=l.to.y){n=n.concat(k);c.from=a.effects.setTransition(c,k,l.from.y,c.from);c.to=a.effects.setTransition(c,k,l.to.y,c.to)}if(l.from.x!=l.to.x){n=n.concat(f);c.from=a.effects.setTransition(c,f,l.from.x,c.from);c.to=a.effects.setTransition(c,f,l.to.x,c.to)}}if(e=="content"||e=="both"){if(l.from.y!=l.to.y){n=n.concat(p);c.from=a.effects.setTransition(c,p,l.from.y,c.from);c.to=a.effects.setTransition(c,p,l.to.y,c.to)}}a.effects.save(c,i?n:m);c.show();a.effects.createWrapper(c);c.css("overflow","hidden").css(c.from);if(e=="content"||e=="both"){k=k.concat(["marginTop","marginBottom"]).concat(p);f=f.concat(["marginLeft","marginRight"]);j=n.concat(k).concat(f);c.find("*[width]").each(function(){child=a(this);if(i){a.effects.save(child,j)}var q={height:child.height(),width:child.width()};child.from={height:q.height*l.from.y,width:q.width*l.from.x};child.to={height:q.height*l.to.y,width:q.width*l.to.x};if(l.from.y!=l.to.y){child.from=a.effects.setTransition(child,k,l.from.y,child.from);child.to=a.effects.setTransition(child,k,l.to.y,child.to)}if(l.from.x!=l.to.x){child.from=a.effects.setTransition(child,f,l.from.x,child.from);child.to=a.effects.setTransition(child,f,l.to.x,child.to)}child.css(child.from);child.animate(child.to,b.duration,b.options.easing,function(){if(i){a.effects.restore(child,j)}})})}c.animate(c.to,{queue:false,duration:b.duration,easing:b.options.easing,complete:function(){if(g=="hide"){c.hide()}a.effects.restore(c,i?n:m);a.effects.removeWrapper(c);if(b.callback){b.callback.apply(this,arguments)}c.dequeue()}})})}})(jQuery);;/*
+ * jQuery UI Effects Shake 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Shake
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.shake=function(b){return this.queue(function(){var e=a(this),l=["position","top","left"];var k=a.effects.setMode(e,b.options.mode||"effect");var n=b.options.direction||"left";var c=b.options.distance||20;var d=b.options.times||3;var g=b.duration||b.options.duration||140;a.effects.save(e,l);e.show();a.effects.createWrapper(e);var f=(n=="up"||n=="down")?"top":"left";var p=(n=="up"||n=="left")?"pos":"neg";var h={},o={},m={};h[f]=(p=="pos"?"-=":"+=")+c;o[f]=(p=="pos"?"+=":"-=")+c*2;m[f]=(p=="pos"?"-=":"+=")+c*2;e.animate(h,g,b.options.easing);for(var j=1;j<d;j++){e.animate(o,g,b.options.easing).animate(m,g,b.options.easing)}e.animate(o,g,b.options.easing).animate(h,g/2,b.options.easing,function(){a.effects.restore(e,l);a.effects.removeWrapper(e);if(b.callback){b.callback.apply(this,arguments)}});e.queue("fx",function(){e.dequeue()});e.dequeue()})}})(jQuery);;/*
+ * jQuery UI Effects Slide 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Slide
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.slide=function(b){return this.queue(function(){var e=a(this),d=["position","top","left"];var i=a.effects.setMode(e,b.options.mode||"show");var h=b.options.direction||"left";a.effects.save(e,d);e.show();a.effects.createWrapper(e).css({overflow:"hidden"});var f=(h=="up"||h=="down")?"top":"left";var c=(h=="up"||h=="left")?"pos":"neg";var j=b.options.distance||(f=="top"?e.outerHeight({margin:true}):e.outerWidth({margin:true}));if(i=="show"){e.css(f,c=="pos"?-j:j)}var g={};g[f]=(i=="show"?(c=="pos"?"+=":"-="):(c=="pos"?"-=":"+="))+j;e.animate(g,{queue:false,duration:b.duration,easing:b.options.easing,complete:function(){if(i=="hide"){e.hide()}a.effects.restore(e,d);a.effects.removeWrapper(e);if(b.callback){b.callback.apply(this,arguments)}e.dequeue()}})})}})(jQuery);;/*
+ * jQuery UI Effects Transfer 1.7.2
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Effects/Transfer
+ *
+ * Depends:
+ *	effects.core.js
+ */
+(function(a){a.effects.transfer=function(b){return this.queue(function(){var f=a(this),h=a(b.options.to),e=h.offset(),g={top:e.top,left:e.left,height:h.innerHeight(),width:h.innerWidth()},d=f.offset(),c=a('<div class="ui-effects-transfer"></div>').appendTo(document.body).addClass(b.options.className).css({top:d.top,left:d.left,height:f.innerHeight(),width:f.innerWidth(),position:"absolute"}).animate(g,b.duration,b.options.easing,function(){c.remove();(b.callback&&b.callback.apply(f[0],arguments));f.dequeue()})})}})(jQuery);;
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/js/jquery-ui-i18n.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/js/jquery-ui-i18n.js
new file mode 100755
index 0000000000000000000000000000000000000000..61c769e6b8ce2b9ae28622640cb7e63e4cf2c9db
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery-ui/js/jquery-ui-i18n.js
@@ -0,0 +1,771 @@
+/* Arabic Translation for jQuery UI date picker plugin. */
+/* Khaled Al Horani -- koko.dw@gmail.com */
+/* خالد الحوراني -- koko.dw@gmail.com */
+/* NOTE: monthNames are the original months names and they are the Arabic names, not the new months name فبراير - يناير and there isn't any Arabic roots for these months */
+jQuery(function($){
+	$.datepicker.regional['ar'] = {
+		closeText: 'إغلاق',
+		prevText: '&#x3c;السابق',
+		nextText: 'التالي&#x3e;',
+		currentText: 'اليوم',
+		monthNames: ['كانون الثاني', 'شباط', 'آذار', 'نيسان', 'آذار', 'حزيران',
+		'تموز', 'آب', 'أيلول',	'تشرين الأول', 'تشرين الثاني', 'كانون الأول'],
+		monthNamesShort: ['1','2','3','4','5','6','7','8','9','10','11','12'],
+		dayNames: ['السبت', 'الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة'],
+		dayNamesShort: ['سبت', 'أحد', 'اثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة'],
+		dayNamesMin: ['سبت', 'أحد', 'اثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة'],
+		dateFormat: 'dd/mm/yy', firstDay: 0,
+  isRTL: true};
+	$.datepicker.setDefaults($.datepicker.regional['ar']);
+});/* Bulgarian initialisation for the jQuery UI date picker plugin. */
+/* Written by Stoyan Kyosev (http://svest.org). */
+jQuery(function($){
+    $.datepicker.regional['bg'] = {
+        closeText: 'затвори',
+        prevText: '&#x3c;назад',
+        nextText: 'напред&#x3e;',
+		nextBigText: '&#x3e;&#x3e;',
+        currentText: 'днес',
+        monthNames: ['Януари','Февруари','Март','Април','Май','Юни',
+        'Юли','Август','Септември','Октомври','Ноември','Декември'],
+        monthNamesShort: ['Яну','Фев','Мар','Апр','Май','Юни',
+        'Юли','Авг','Сеп','Окт','Нов','Дек'],
+        dayNames: ['Неделя','Понеделник','Вторник','Сряда','Четвъртък','Петък','Събота'],
+        dayNamesShort: ['Нед','Пон','Вто','Сря','Чет','Пет','Съб'],
+        dayNamesMin: ['Не','По','Вт','Ср','Че','Пе','Съ'],
+        dateFormat: 'dd.mm.yy', firstDay: 1,
+        isRTL: false};
+    $.datepicker.setDefaults($.datepicker.regional['bg']);
+});
+/* Inicialitzaci� en catal� per a l'extenci� 'calendar' per jQuery. */
+/* Writers: (joan.leon@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['ca'] = {
+		closeText: 'Tancar',
+		prevText: '&#x3c;Ant',
+		nextText: 'Seg&#x3e;',
+		currentText: 'Avui',
+		monthNames: ['Gener','Febrer','Mar&ccedil;','Abril','Maig','Juny',
+		'Juliol','Agost','Setembre','Octubre','Novembre','Desembre'],
+		monthNamesShort: ['Gen','Feb','Mar','Abr','Mai','Jun',
+		'Jul','Ago','Set','Oct','Nov','Des'],
+		dayNames: ['Diumenge','Dilluns','Dimarts','Dimecres','Dijous','Divendres','Dissabte'],
+		dayNamesShort: ['Dug','Dln','Dmt','Dmc','Djs','Dvn','Dsb'],
+		dayNamesMin: ['Dg','Dl','Dt','Dc','Dj','Dv','Ds'],
+		dateFormat: 'mm/dd/yy', firstDay: 0,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['ca']);
+});/* Czech initialisation for the jQuery UI date picker plugin. */
+/* Written by Tomas Muller (tomas@tomas-muller.net). */
+jQuery(function($){
+	$.datepicker.regional['cs'] = {
+		closeText: 'Zavřít',
+		prevText: '&#x3c;Dříve',
+		nextText: 'Později&#x3e;',
+		currentText: 'Nyní',
+		monthNames: ['leden','únor','březen','duben','květen','červen',
+        'červenec','srpen','září','říjen','listopad','prosinec'],
+		monthNamesShort: ['led','úno','bře','dub','kvě','čer',
+		'čvc','srp','zář','říj','lis','pro'],
+		dayNames: ['neděle', 'pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota'],
+		dayNamesShort: ['ne', 'po', 'út', 'st', 'čt', 'pá', 'so'],
+		dayNamesMin: ['ne','po','út','st','čt','pá','so'],
+		dateFormat: 'dd.mm.yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['cs']);
+});
+/* Danish initialisation for the jQuery UI date picker plugin. */
+/* Written by Jan Christensen ( deletestuff@gmail.com). */
+jQuery(function($){
+    $.datepicker.regional['da'] = {
+		closeText: 'Luk',
+        prevText: '&#x3c;Forrige',
+		nextText: 'Næste&#x3e;',
+		currentText: 'Idag',
+        monthNames: ['Januar','Februar','Marts','April','Maj','Juni',
+        'Juli','August','September','Oktober','November','December'],
+        monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+        'Jul','Aug','Sep','Okt','Nov','Dec'],
+		dayNames: ['Søndag','Mandag','Tirsdag','Onsdag','Torsdag','Fredag','Lørdag'],
+		dayNamesShort: ['Søn','Man','Tir','Ons','Tor','Fre','Lør'],
+		dayNamesMin: ['Sø','Ma','Ti','On','To','Fr','Lø'],
+        dateFormat: 'dd-mm-yy', firstDay: 0,
+		isRTL: false};
+    $.datepicker.setDefaults($.datepicker.regional['da']);
+});
+/* German initialisation for the jQuery UI date picker plugin. */
+/* Written by Milian Wolff (mail@milianw.de). */
+jQuery(function($){
+	$.datepicker.regional['de'] = {
+		closeText: 'schließen',
+		prevText: '&#x3c;zurück',
+		nextText: 'Vor&#x3e;',
+		currentText: 'heute',
+		monthNames: ['Januar','Februar','März','April','Mai','Juni',
+		'Juli','August','September','Oktober','November','Dezember'],
+		monthNamesShort: ['Jan','Feb','Mär','Apr','Mai','Jun',
+		'Jul','Aug','Sep','Okt','Nov','Dez'],
+		dayNames: ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'],
+		dayNamesShort: ['So','Mo','Di','Mi','Do','Fr','Sa'],
+		dayNamesMin: ['So','Mo','Di','Mi','Do','Fr','Sa'],
+		dateFormat: 'dd.mm.yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['de']);
+});
+/* Greek (el) initialisation for the jQuery UI date picker plugin. */
+/* Written by Alex Cicovic (http://www.alexcicovic.com) */
+jQuery(function($){
+	$.datepicker.regional['el'] = {
+		closeText: 'Κλείσιμο',
+		prevText: 'Προηγούμενος',
+		nextText: 'Επόμενος',
+		currentText: 'Τρέχων Μήνας',
+		monthNames: ['Ιανουάριος','Φεβρουάριος','Μάρτιος','Απρίλιος','Μάιος','Ιούνιος',
+		'Ιούλιος','Αύγουστος','Σεπτέμβριος','Οκτώβριος','Νοέμβριος','Δεκέμβριος'],
+		monthNamesShort: ['Ιαν','Φεβ','Μαρ','Απρ','Μαι','Ιουν',
+		'Ιουλ','Αυγ','Σεπ','Οκτ','Νοε','Δεκ'],
+		dayNames: ['Κυριακή','Δευτέρα','Τρίτη','Τετάρτη','Πέμπτη','Παρασκευή','Σάββατο'],
+		dayNamesShort: ['Κυρ','Δευ','Τρι','Τετ','Πεμ','Παρ','Σαβ'],
+		dayNamesMin: ['Κυ','Δε','Τρ','Τε','Πε','Πα','Σα'],
+		dateFormat: 'dd/mm/yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['el']);
+});/* Esperanto initialisation for the jQuery UI date picker plugin. */
+/* Written by Olivier M. (olivierweb@ifrance.com). */
+jQuery(function($){
+	$.datepicker.regional['eo'] = {
+		closeText: 'Fermi',
+		prevText: '&lt;Anta',
+		nextText: 'Sekv&gt;',
+		currentText: 'Nuna',
+		monthNames: ['Januaro','Februaro','Marto','Aprilo','Majo','Junio',
+		'Julio','Aŭgusto','Septembro','Oktobro','Novembro','Decembro'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+		'Jul','Aŭg','Sep','Okt','Nov','Dec'],
+		dayNames: ['Dimanĉo','Lundo','Mardo','Merkredo','Ĵaŭdo','Vendredo','Sabato'],
+		dayNamesShort: ['Dim','Lun','Mar','Mer','Ĵaŭ','Ven','Sab'],
+		dayNamesMin: ['Di','Lu','Ma','Me','Ĵa','Ve','Sa'],
+		dateFormat: 'dd/mm/yy', firstDay: 0,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['eo']);
+});
+/* Inicializaci�n en espa�ol para la extensi�n 'UI date picker' para jQuery. */
+/* Traducido por Vester (xvester@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['es'] = {
+		closeText: 'Cerrar',
+		prevText: '&#x3c;Ant',
+		nextText: 'Sig&#x3e;',
+		currentText: 'Hoy',
+		monthNames: ['Enero','Febrero','Marzo','Abril','Mayo','Junio',
+		'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'],
+		monthNamesShort: ['Ene','Feb','Mar','Abr','May','Jun',
+		'Jul','Ago','Sep','Oct','Nov','Dic'],
+		dayNames: ['Domingo','Lunes','Martes','Mi&eacute;rcoles','Jueves','Viernes','S&aacute;bado'],
+		dayNamesShort: ['Dom','Lun','Mar','Mi&eacute;','Juv','Vie','S&aacute;b'],
+		dayNamesMin: ['Do','Lu','Ma','Mi','Ju','Vi','S&aacute;'],
+		dateFormat: 'dd/mm/yy', firstDay: 0,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['es']);
+});/* Persian (Farsi) Translation for the jQuery UI date picker plugin. */
+/* Javad Mowlanezhad -- jmowla@gmail.com */
+/* Jalali calendar should supported soon! (Its implemented but I have to test it) */
+jQuery(function($) {
+	$.datepicker.regional['fa'] = {
+		closeText: 'بستن',
+		prevText: '&#x3c;قبلي',
+		nextText: 'بعدي&#x3e;',
+		currentText: 'امروز',
+		monthNames: ['فروردين','ارديبهشت','خرداد','تير','مرداد','شهريور',
+		'مهر','آبان','آذر','دي','بهمن','اسفند'],
+		monthNamesShort: ['1','2','3','4','5','6','7','8','9','10','11','12'],
+		dayNames: ['يکشنبه','دوشنبه','سه‌شنبه','چهارشنبه','پنجشنبه','جمعه','شنبه'],
+		dayNamesShort: ['ي','د','س','چ','پ','ج', 'ش'],
+		dayNamesMin: ['ي','د','س','چ','پ','ج', 'ش'],
+		dateFormat: 'yy/mm/dd', firstDay: 6,
+  isRTL: true};
+	$.datepicker.setDefaults($.datepicker.regional['fa']);
+});/* Finnish initialisation for the jQuery UI date picker plugin. */
+/* Written by Harri Kilpi� (harrikilpio@gmail.com). */
+jQuery(function($){
+    $.datepicker.regional['fi'] = {
+		closeText: 'Sulje',
+		prevText: '&laquo;Edellinen',
+		nextText: 'Seuraava&raquo;',
+		currentText: 'T&auml;n&auml;&auml;n',
+        monthNames: ['Tammikuu','Helmikuu','Maaliskuu','Huhtikuu','Toukokuu','Kes&auml;kuu',
+        'Hein&auml;kuu','Elokuu','Syyskuu','Lokakuu','Marraskuu','Joulukuu'],
+        monthNamesShort: ['Tammi','Helmi','Maalis','Huhti','Touko','Kes&auml;',
+        'Hein&auml;','Elo','Syys','Loka','Marras','Joulu'],
+		dayNamesShort: ['Su','Ma','Ti','Ke','To','Pe','Su'],
+		dayNames: ['Sunnuntai','Maanantai','Tiistai','Keskiviikko','Torstai','Perjantai','Lauantai'],
+		dayNamesMin: ['Su','Ma','Ti','Ke','To','Pe','La'],
+        dateFormat: 'dd.mm.yy', firstDay: 1,
+		isRTL: false};
+    $.datepicker.setDefaults($.datepicker.regional['fi']);
+});
+/* French initialisation for the jQuery UI date picker plugin. */
+/* Written by Keith Wood (kbwood@virginbroadband.com.au) and Stéphane Nahmani (sholby@sholby.net). */
+jQuery(function($){
+	$.datepicker.regional['fr'] = {
+		closeText: 'Fermer',
+		prevText: '&#x3c;Préc',
+		nextText: 'Suiv&#x3e;',
+		currentText: 'Courant',
+		monthNames: ['Janvier','Février','Mars','Avril','Mai','Juin',
+		'Juillet','Août','Septembre','Octobre','Novembre','Décembre'],
+		monthNamesShort: ['Jan','Fév','Mar','Avr','Mai','Jun',
+		'Jul','Aoû','Sep','Oct','Nov','Déc'],
+		dayNames: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'],
+		dayNamesShort: ['Dim','Lun','Mar','Mer','Jeu','Ven','Sam'],
+		dayNamesMin: ['Di','Lu','Ma','Me','Je','Ve','Sa'],
+		dateFormat: 'dd/mm/yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['fr']);
+});/* Hebrew initialisation for the UI Datepicker extension. */
+/* Written by Amir Hardon (ahardon at gmail dot com). */
+jQuery(function($){
+	$.datepicker.regional['he'] = {
+		closeText: 'סגור',
+		prevText: '&#x3c;הקודם',
+		nextText: 'הבא&#x3e;',
+		currentText: 'היום',
+		monthNames: ['ינואר','פברואר','מרץ','אפריל','מאי','יוני',
+		'יולי','אוגוסט','ספטמבר','אוקטובר','נובמבר','דצמבר'],
+		monthNamesShort: ['1','2','3','4','5','6',
+		'7','8','9','10','11','12'],
+		dayNames: ['ראשון','שני','שלישי','רביעי','חמישי','שישי','שבת'],
+		dayNamesShort: ['א\'','ב\'','ג\'','ד\'','ה\'','ו\'','שבת'],
+		dayNamesMin: ['א\'','ב\'','ג\'','ד\'','ה\'','ו\'','שבת'],
+		dateFormat: 'dd/mm/yy', firstDay: 0,
+		isRTL: true};
+	$.datepicker.setDefaults($.datepicker.regional['he']);
+});
+/* Croatian i18n for the jQuery UI date picker plugin. */
+/* Written by Vjekoslav Nesek. */
+jQuery(function($){
+	$.datepicker.regional['hr'] = {
+		closeText: 'Zatvori',
+		prevText: '&#x3c;',
+		nextText: '&#x3e;',
+		currentText: 'Danas',
+		monthNames: ['Siječanj','Veljača','Ožujak','Travanj','Svibanj','Lipani',
+		'Srpanj','Kolovoz','Rujan','Listopad','Studeni','Prosinac'],
+		monthNamesShort: ['Sij','Velj','Ožu','Tra','Svi','Lip',
+		'Srp','Kol','Ruj','Lis','Stu','Pro'],
+		dayNames: ['Nedjalja','Ponedjeljak','Utorak','Srijeda','Četvrtak','Petak','Subota'],
+		dayNamesShort: ['Ned','Pon','Uto','Sri','Čet','Pet','Sub'],
+		dayNamesMin: ['Ne','Po','Ut','Sr','Če','Pe','Su'],
+		dateFormat: 'dd.mm.yy.', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['hr']);
+});/* Hungarian initialisation for the jQuery UI date picker plugin. */
+/* Written by Istvan Karaszi (jquerycalendar@spam.raszi.hu). */
+jQuery(function($){
+	$.datepicker.regional['hu'] = {
+		closeText: 'bezárás',
+		prevText: '&laquo;&nbsp;vissza',
+		nextText: 'előre&nbsp;&raquo;',
+		currentText: 'ma',
+		monthNames: ['Január', 'Február', 'Március', 'Április', 'Május', 'Június',
+		'Július', 'Augusztus', 'Szeptember', 'Október', 'November', 'December'],
+		monthNamesShort: ['Jan', 'Feb', 'Már', 'Ápr', 'Máj', 'Jún',
+		'Júl', 'Aug', 'Szep', 'Okt', 'Nov', 'Dec'],
+		dayNames: ['Vasámap', 'Hétfö', 'Kedd', 'Szerda', 'Csütörtök', 'Péntek', 'Szombat'],
+		dayNamesShort: ['Vas', 'Hét', 'Ked', 'Sze', 'Csü', 'Pén', 'Szo'],
+		dayNamesMin: ['V', 'H', 'K', 'Sze', 'Cs', 'P', 'Szo'],
+		dateFormat: 'yy-mm-dd', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['hu']);
+});
+/* Armenian(UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Levon Zakaryan (levon.zakaryan@gmail.com)*/
+jQuery(function($){
+	$.datepicker.regional['hy'] = {
+		closeText: 'Փակել',
+		prevText: '&#x3c;Նախ.',
+		nextText: 'Հաջ.&#x3e;',
+		currentText: 'Այսօր',
+		monthNames: ['Հունվար','Փետրվար','Մարտ','Ապրիլ','Մայիս','Հունիս',
+		'Հուլիս','Օգոստոս','Սեպտեմբեր','Հոկտեմբեր','Նոյեմբեր','Դեկտեմբեր'],
+		monthNamesShort: ['Հունվ','Փետր','Մարտ','Ապր','Մայիս','Հունիս',
+		'Հուլ','Օգս','Սեպ','Հոկ','Նոյ','Դեկ'],
+		dayNames: ['կիրակի','եկուշաբթի','երեքշաբթի','չորեքշաբթի','հինգշաբթի','ուրբաթ','շաբաթ'],
+		dayNamesShort: ['կիր','երկ','երք','չրք','հնգ','ուրբ','շբթ'],
+		dayNamesMin: ['կիր','երկ','երք','չրք','հնգ','ուրբ','շբթ'],
+		dateFormat: 'dd.mm.yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['hy']);
+});/* Indonesian initialisation for the jQuery UI date picker plugin. */
+/* Written by Deden Fathurahman (dedenf@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['id'] = {
+		closeText: 'Tutup',
+		prevText: '&#x3c;mundur',
+		nextText: 'maju&#x3e;',
+		currentText: 'hari ini',
+		monthNames: ['Januari','Februari','Maret','April','Mei','Juni',
+		'Juli','Agustus','September','Oktober','Nopember','Desember'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Mei','Jun',
+		'Jul','Agus','Sep','Okt','Nop','Des'],
+		dayNames: ['Minggu','Senin','Selasa','Rabu','Kamis','Jumat','Sabtu'],
+		dayNamesShort: ['Min','Sen','Sel','Rab','kam','Jum','Sab'],
+		dayNamesMin: ['Mg','Sn','Sl','Rb','Km','jm','Sb'],
+		dateFormat: 'dd/mm/yy', firstDay: 0,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['id']);
+});/* Icelandic initialisation for the jQuery UI date picker plugin. */
+/* Written by Haukur H. Thorsson (haukur@eskill.is). */
+jQuery(function($){
+	$.datepicker.regional['is'] = {
+		closeText: 'Loka',
+		prevText: '&#x3c; Fyrri',
+		nextText: 'N&aelig;sti &#x3e;',
+		currentText: '&Iacute; dag',
+		monthNames: ['Jan&uacute;ar','Febr&uacute;ar','Mars','Apr&iacute;l','Ma&iacute','J&uacute;n&iacute;',
+		'J&uacute;l&iacute;','&Aacute;g&uacute;st','September','Okt&oacute;ber','N&oacute;vember','Desember'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Ma&iacute;','J&uacute;n',
+		'J&uacute;l','&Aacute;g&uacute;','Sep','Okt','N&oacute;v','Des'],
+		dayNames: ['Sunnudagur','M&aacute;nudagur','&THORN;ri&eth;judagur','Mi&eth;vikudagur','Fimmtudagur','F&ouml;studagur','Laugardagur'],
+		dayNamesShort: ['Sun','M&aacute;n','&THORN;ri','Mi&eth;','Fim','F&ouml;s','Lau'],
+		dayNamesMin: ['Su','M&aacute;','&THORN;r','Mi','Fi','F&ouml;','La'],
+		dateFormat: 'dd/mm/yy', firstDay: 0,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['is']);
+});/* Italian initialisation for the jQuery UI date picker plugin. */
+/* Written by Apaella (apaella@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['it'] = {
+		closeText: 'Chiudi',
+		prevText: '&#x3c;Prec',
+		nextText: 'Succ&#x3e;',
+		currentText: 'Oggi',
+		monthNames: ['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno',
+		'Luglio','Agosto','Settembre','Ottobre','Novembre','Dicembre'],
+		monthNamesShort: ['Gen','Feb','Mar','Apr','Mag','Giu',
+		'Lug','Ago','Set','Ott','Nov','Dic'],
+		dayNames: ['Domenica','Luned&#236','Marted&#236','Mercoled&#236','Gioved&#236','Venerd&#236','Sabato'],
+		dayNamesShort: ['Dom','Lun','Mar','Mer','Gio','Ven','Sab'],
+		dayNamesMin: ['Do','Lu','Ma','Me','Gio','Ve','Sa'],
+		dateFormat: 'dd/mm/yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['it']);
+});
+/* Japanese initialisation for the jQuery UI date picker plugin. */
+/* Written by Kentaro SATO (kentaro@ranvis.com). */
+jQuery(function($){
+	$.datepicker.regional['ja'] = {
+		closeText: '閉じる',
+		prevText: '&#x3c;前',
+		nextText: '次&#x3e;',
+		currentText: '今日',
+		monthNames: ['1月','2月','3月','4月','5月','6月',
+		'7月','8月','9月','10月','11月','12月'],
+		monthNamesShort: ['1月','2月','3月','4月','5月','6月',
+		'7月','8月','9月','10月','11月','12月'],
+		dayNames: ['日曜日','月曜日','火曜日','水曜日','木曜日','金曜日','土曜日'],
+		dayNamesShort: ['日','月','火','水','木','金','土'],
+		dayNamesMin: ['日','月','火','水','木','金','土'],
+		dateFormat: 'yy/mm/dd', firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: true};
+	$.datepicker.setDefaults($.datepicker.regional['ja']);
+});/* Korean initialisation for the jQuery calendar extension. */
+/* Written by DaeKwon Kang (ncrash.dk@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['ko'] = {
+		closeText: '닫기',
+		prevText: '이전달',
+		nextText: '다음달',
+		currentText: '오늘',
+		monthNames: ['1월(JAN)','2월(FEB)','3월(MAR)','4월(APR)','5월(MAY)','6월(JUN)',
+		'7월(JUL)','8월(AUG)','9월(SEP)','10월(OCT)','11월(NOV)','12월(DEC)'],
+		monthNamesShort: ['1월(JAN)','2월(FEB)','3월(MAR)','4월(APR)','5월(MAY)','6월(JUN)',
+		'7월(JUL)','8월(AUG)','9월(SEP)','10월(OCT)','11월(NOV)','12월(DEC)'],
+		dayNames: ['일','월','화','수','목','금','토'],
+		dayNamesShort: ['일','월','화','수','목','금','토'],
+		dayNamesMin: ['일','월','화','수','목','금','토'],
+		dateFormat: 'yy-mm-dd', firstDay: 0,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['ko']);
+});/* Lithuanian (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* @author Arturas Paleicikas <arturas@avalon.lt> */
+jQuery(function($){
+	$.datepicker.regional['lt'] = {
+		closeText: 'Uždaryti',
+		prevText: '&#x3c;Atgal',
+		nextText: 'Pirmyn&#x3e;',
+		currentText: 'Šiandien',
+		monthNames: ['Sausis','Vasaris','Kovas','Balandis','Gegužė','Birželis',
+		'Liepa','Rugpjūtis','Rugsėjis','Spalis','Lapkritis','Gruodis'],
+		monthNamesShort: ['Sau','Vas','Kov','Bal','Geg','Bir',
+		'Lie','Rugp','Rugs','Spa','Lap','Gru'],
+		dayNames: ['sekmadienis','pirmadienis','antradienis','trečiadienis','ketvirtadienis','penktadienis','šeštadienis'],
+		dayNamesShort: ['sek','pir','ant','tre','ket','pen','šeš'],
+		dayNamesMin: ['Se','Pr','An','Tr','Ke','Pe','Še'],
+		dateFormat: 'yy-mm-dd', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['lt']);
+});/* Latvian (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* @author Arturas Paleicikas <arturas.paleicikas@metasite.net> */
+jQuery(function($){
+	$.datepicker.regional['lv'] = {
+		closeText: 'Aizvērt',
+		prevText: 'Iepr',
+		nextText: 'Nāka',
+		currentText: 'Šodien',
+		monthNames: ['Janvāris','Februāris','Marts','Aprīlis','Maijs','Jūnijs',
+		'Jūlijs','Augusts','Septembris','Oktobris','Novembris','Decembris'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Mai','Jūn',
+		'Jūl','Aug','Sep','Okt','Nov','Dec'],
+		dayNames: ['svētdiena','pirmdiena','otrdiena','trešdiena','ceturtdiena','piektdiena','sestdiena'],
+		dayNamesShort: ['svt','prm','otr','tre','ctr','pkt','sst'],
+		dayNamesMin: ['Sv','Pr','Ot','Tr','Ct','Pk','Ss'],
+		dateFormat: 'dd-mm-yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['lv']);
+});/* Malaysian initialisation for the jQuery UI date picker plugin. */
+/* Written by Mohd Nawawi Mohamad Jamili (nawawi@ronggeng.net). */
+jQuery(function($){
+	$.datepicker.regional['ms'] = {
+		closeText: 'Tutup',
+		prevText: '&#x3c;Sebelum',
+		nextText: 'Selepas&#x3e;',
+		currentText: 'hari ini',
+		monthNames: ['Januari','Februari','Mac','April','Mei','Jun',
+		'Julai','Ogos','September','Oktober','November','Disember'],
+		monthNamesShort: ['Jan','Feb','Mac','Apr','Mei','Jun',
+		'Jul','Ogo','Sep','Okt','Nov','Dis'],
+		dayNames: ['Ahad','Isnin','Selasa','Rabu','Khamis','Jumaat','Sabtu'],
+		dayNamesShort: ['Aha','Isn','Sel','Rab','kha','Jum','Sab'],
+		dayNamesMin: ['Ah','Is','Se','Ra','Kh','Ju','Sa'],
+		dateFormat: 'dd/mm/yy', firstDay: 0,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['ms']);
+});/* Dutch (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Mathias Bynens <http://mathiasbynens.be/> */
+jQuery(function($){
+	$.datepicker.regional.nl = {
+		closeText: 'Sluiten',
+		prevText: '←',
+		nextText: '→',
+		currentText: 'Vandaag',
+		monthNames: ['januari', 'februari', 'maart', 'april', 'mei', 'juni',
+		'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
+		monthNamesShort: ['jan', 'feb', 'maa', 'apr', 'mei', 'jun',
+		'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+		dayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
+		dayNamesShort: ['zon', 'maa', 'din', 'woe', 'don', 'vri', 'zat'],
+		dayNamesMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
+		dateFormat: 'dd/mm/yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional.nl);
+});/* Norwegian initialisation for the jQuery UI date picker plugin. */
+/* Written by Naimdjon Takhirov (naimdjon@gmail.com). */
+jQuery(function($){
+    $.datepicker.regional['no'] = {
+		closeText: 'Lukk',
+        prevText: '&laquo;Forrige',
+		nextText: 'Neste&raquo;',
+		currentText: 'I dag',
+        monthNames: ['Januar','Februar','Mars','April','Mai','Juni',
+        'Juli','August','September','Oktober','November','Desember'],
+        monthNamesShort: ['Jan','Feb','Mar','Apr','Mai','Jun',
+        'Jul','Aug','Sep','Okt','Nov','Des'],
+		dayNamesShort: ['Søn','Man','Tir','Ons','Tor','Fre','Lør'],
+		dayNames: ['Søndag','Mandag','Tirsdag','Onsdag','Torsdag','Fredag','Lørdag'],
+		dayNamesMin: ['Sø','Ma','Ti','On','To','Fr','Lø'],
+        dateFormat: 'yy-mm-dd', firstDay: 0,
+		isRTL: false};
+    $.datepicker.setDefaults($.datepicker.regional['no']);
+});
+/* Polish initialisation for the jQuery UI date picker plugin. */
+/* Written by Jacek Wysocki (jacek.wysocki@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['pl'] = {
+		closeText: 'Zamknij',
+		prevText: '&#x3c;Poprzedni',
+		nextText: 'Następny&#x3e;',
+		currentText: 'Dziś',
+		monthNames: ['Styczeń','Luty','Marzec','Kwiecień','Maj','Czerwiec',
+		'Lipiec','Sierpień','Wrzesień','Październik','Listopad','Grudzień'],
+		monthNamesShort: ['Sty','Lu','Mar','Kw','Maj','Cze',
+		'Lip','Sie','Wrz','Pa','Lis','Gru'],
+		dayNames: ['Niedziela','Poniedzialek','Wtorek','Środa','Czwartek','Piątek','Sobota'],
+		dayNamesShort: ['Nie','Pn','Wt','Śr','Czw','Pt','So'],
+		dayNamesMin: ['N','Pn','Wt','Śr','Cz','Pt','So'],
+		dateFormat: 'yy-mm-dd', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['pl']);
+});
+/* Brazilian initialisation for the jQuery UI date picker plugin. */
+/* Written by Leonildo Costa Silva (leocsilva@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['pt-BR'] = {
+		closeText: 'Fechar',
+		prevText: '&#x3c;Anterior',
+		nextText: 'Pr&oacute;ximo&#x3e;',
+		currentText: 'Hoje',
+		monthNames: ['Janeiro','Fevereiro','Mar&ccedil;o','Abril','Maio','Junho',
+		'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],
+		monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun',
+		'Jul','Ago','Set','Out','Nov','Dez'],
+		dayNames: ['Domingo','Segunda-feira','Ter&ccedil;a-feira','Quarta-feira','Quinta-feira','Sexta-feira','Sabado'],
+		dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','Sab'],
+		dayNamesMin: ['Dom','Seg','Ter','Qua','Qui','Sex','Sab'],
+		dateFormat: 'dd/mm/yy', firstDay: 0,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['pt-BR']);
+});/* Romanian initialisation for the jQuery UI date picker plugin.
+ *
+ * Written by Edmond L. (ll_edmond@walla.com)
+ * and Ionut G. Stan (ionut.g.stan@gmail.com)
+ */
+jQuery(function($){
+	$.datepicker.regional['ro'] = {
+		closeText: 'Închide',
+		prevText: '&laquo; Luna precedentă',
+		nextText: 'Luna următoare &raquo;',
+		currentText: 'Azi',
+		monthNames: ['Ianuarie','Februarie','Martie','Aprilie','Mai','Iunie',
+		'Iulie','August','Septembrie','Octombrie','Noiembrie','Decembrie'],
+		monthNamesShort: ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun',
+		'Iul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+		dayNames: ['Duminică', 'Luni', 'Marţi', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă'],
+		dayNamesShort: ['Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm'],
+		dayNamesMin: ['Du','Lu','Ma','Mi','Jo','Vi','Sâ'],
+		dateFormat: 'dd MM yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['ro']);
+});
+/* Russian (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Andrew Stromnov (stromnov@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['ru'] = {
+		closeText: 'Закрыть',
+		prevText: '&#x3c;Пред',
+		nextText: 'След&#x3e;',
+		currentText: 'Сегодня',
+		monthNames: ['Январь','Февраль','Март','Апрель','Май','Июнь',
+		'Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'],
+		monthNamesShort: ['Янв','Фев','Мар','Апр','Май','Июн',
+		'Июл','Авг','Сен','Окт','Ноя','Дек'],
+		dayNames: ['воскресенье','понедельник','вторник','среда','четверг','пятница','суббота'],
+		dayNamesShort: ['вск','пнд','втр','срд','чтв','птн','сбт'],
+		dayNamesMin: ['Вс','Пн','Вт','Ср','Чт','Пт','Сб'],
+		dateFormat: 'dd.mm.yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['ru']);
+});/* Slovak initialisation for the jQuery UI date picker plugin. */
+/* Written by Vojtech Rinik (vojto@hmm.sk). */
+jQuery(function($){
+	$.datepicker.regional['sk'] = {
+		closeText: 'Zavrieť',
+		prevText: '&#x3c;Predchádzajúci',
+		nextText: 'Nasledujúci&#x3e;',
+		currentText: 'Dnes',
+		monthNames: ['Január','Február','Marec','Apríl','Máj','Jún',
+		'Júl','August','September','Október','November','December'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Máj','Jún',
+		'Júl','Aug','Sep','Okt','Nov','Dec'],
+		dayNames: ['Nedel\'a','Pondelok','Utorok','Streda','Štvrtok','Piatok','Sobota'],
+		dayNamesShort: ['Ned','Pon','Uto','Str','Štv','Pia','Sob'],
+		dayNamesMin: ['Ne','Po','Ut','St','Št','Pia','So'],
+		dateFormat: 'dd.mm.yy', firstDay: 0,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['sk']);
+});
+/* Slovenian initialisation for the jQuery UI date picker plugin. */
+/* Written by Jaka Jancar (jaka@kubje.org). */
+/* c = &#x10D;, s = &#x161; z = &#x17E; C = &#x10C; S = &#x160; Z = &#x17D; */
+jQuery(function($){
+	$.datepicker.regional['sl'] = {
+		closeText: 'Zapri',
+		prevText: '&lt;Prej&#x161;nji',
+		nextText: 'Naslednji&gt;',
+		currentText: 'Trenutni',
+		monthNames: ['Januar','Februar','Marec','April','Maj','Junij',
+		'Julij','Avgust','September','Oktober','November','December'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+		'Jul','Avg','Sep','Okt','Nov','Dec'],
+		dayNames: ['Nedelja','Ponedeljek','Torek','Sreda','&#x10C;etrtek','Petek','Sobota'],
+		dayNamesShort: ['Ned','Pon','Tor','Sre','&#x10C;et','Pet','Sob'],
+		dayNamesMin: ['Ne','Po','To','Sr','&#x10C;e','Pe','So'],
+		dateFormat: 'dd.mm.yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['sl']);
+});
+/* Albanian initialisation for the jQuery UI date picker plugin. */
+/* Written by Flakron Bytyqi (flakron@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['sq'] = {
+		closeText: 'mbylle',
+		prevText: '&#x3c;mbrapa',
+		nextText: 'Përpara&#x3e;',
+		currentText: 'sot',
+		monthNames: ['Janar','Shkurt','Mars','Prill','Maj','Qershor',
+		'Korrik','Gusht','Shtator','Tetor','Nëntor','Dhjetor'],
+		monthNamesShort: ['Jan','Shk','Mar','Pri','Maj','Qer',
+		'Kor','Gus','Sht','Tet','Nën','Dhj'],
+		dayNames: ['E Diel','E Hënë','E Martë','E Mërkurë','E Enjte','E Premte','E Shtune'],
+		dayNamesShort: ['Di','Hë','Ma','Më','En','Pr','Sh'],
+		dayNamesMin: ['Di','Hë','Ma','Më','En','Pr','Sh'],
+		dateFormat: 'dd.mm.yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['sq']);
+});
+/* Serbian i18n for the jQuery UI date picker plugin. */
+/* Written by Dejan Dimić. */
+jQuery(function($){
+	$.datepicker.regional['sr-SR'] = {
+		closeText: 'Zatvori',
+		prevText: '&#x3c;',
+		nextText: '&#x3e;',
+		currentText: 'Danas',
+		monthNames: ['Januar','Februar','Mart','April','Maj','Jun',
+		'Jul','Avgust','Septembar','Oktobar','Novembar','Decembar'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+		'Jul','Avg','Sep','Okt','Nov','Dec'],
+		dayNames: ['Nedelja','Ponedeljak','Utorak','Sreda','Četvrtak','Petak','Subota'],
+		dayNamesShort: ['Ned','Pon','Uto','Sre','Čet','Pet','Sub'],
+		dayNamesMin: ['Ne','Po','Ut','Sr','Če','Pe','Su'],
+		dateFormat: 'dd/mm/yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['sr-SR']);
+});
+/* Serbian i18n for the jQuery UI date picker plugin. */
+/* Written by Dejan Dimić. */
+jQuery(function($){
+	$.datepicker.regional['sr'] = {
+		closeText: 'Затвори',
+		prevText: '&#x3c;',
+		nextText: '&#x3e;',
+		currentText: 'Данас',
+		monthNames: ['Јануар','Фебруар','Март','Април','Мај','Јун',
+		'Јул','Август','Септембар','Октобар','Новембар','Децембар'],
+		monthNamesShort: ['Јан','Феб','Мар','Апр','Мај','Јун',
+		'Јул','Авг','Сеп','Окт','Нов','Дец'],
+		dayNames: ['Недеља','Понедељак','Уторак','Среда','Четвртак','Петак','Субота'],
+		dayNamesShort: ['Нед','Пон','Уто','Сре','Чет','Пет','Суб'],
+		dayNamesMin: ['Не','По','Ут','Ср','Че','Пе','Су'],
+		dateFormat: 'dd/mm/yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['sr']);
+});
+/* Swedish initialisation for the jQuery UI date picker plugin. */
+/* Written by Anders Ekdahl ( anders@nomadiz.se). */
+jQuery(function($){
+    $.datepicker.regional['sv'] = {
+		closeText: 'Stäng',
+        prevText: '&laquo;Förra',
+		nextText: 'Nästa&raquo;',
+		currentText: 'Idag',
+        monthNames: ['Januari','Februari','Mars','April','Maj','Juni',
+        'Juli','Augusti','September','Oktober','November','December'],
+        monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+        'Jul','Aug','Sep','Okt','Nov','Dec'],
+		dayNamesShort: ['Sön','Mån','Tis','Ons','Tor','Fre','Lör'],
+		dayNames: ['Söndag','Måndag','Tisdag','Onsdag','Torsdag','Fredag','Lördag'],
+		dayNamesMin: ['Sö','Må','Ti','On','To','Fr','Lö'],
+        dateFormat: 'yy-mm-dd', firstDay: 1,
+		isRTL: false};
+    $.datepicker.setDefaults($.datepicker.regional['sv']);
+});
+/* Thai initialisation for the jQuery UI date picker plugin. */
+/* Written by pipo (pipo@sixhead.com). */
+jQuery(function($){
+	$.datepicker.regional['th'] = {
+		closeText: 'ปิด',
+		prevText: '&laquo;&nbsp;ย้อน',
+		nextText: 'ถัดไป&nbsp;&raquo;',
+		currentText: 'วันนี้',
+		monthNames: ['มกราคม','กุมภาพันธ์','มีนาคม','เมษายน','พฤษภาคม','มิถุนายน',
+		'กรกฏาคม','สิงหาคม','กันยายน','ตุลาคม','พฤศจิกายน','ธันวาคม'],
+		monthNamesShort: ['ม.ค.','ก.พ.','มี.ค.','เม.ย.','พ.ค.','มิ.ย.',
+		'ก.ค.','ส.ค.','ก.ย.','ต.ค.','พ.ย.','ธ.ค.'],
+		dayNames: ['อาทิตย์','จันทร์','อังคาร','พุธ','พฤหัสบดี','ศุกร์','เสาร์'],
+		dayNamesShort: ['อา.','จ.','อ.','พ.','พฤ.','ศ.','ส.'],
+		dayNamesMin: ['อา.','จ.','อ.','พ.','พฤ.','ศ.','ส.'],
+		dateFormat: 'dd/mm/yy', firstDay: 0,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['th']);
+});/* Turkish initialisation for the jQuery UI date picker plugin. */
+/* Written by Izzet Emre Erkan (kara@karalamalar.net). */
+jQuery(function($){
+	$.datepicker.regional['tr'] = {
+		closeText: 'kapat',
+		prevText: '&#x3c;geri',
+		nextText: 'ileri&#x3e',
+		currentText: 'bugün',
+		monthNames: ['Ocak','Şubat','Mart','Nisan','Mayıs','Haziran',
+		'Temmuz','Ağustos','Eylül','Ekim','Kasım','Aralık'],
+		monthNamesShort: ['Oca','Şub','Mar','Nis','May','Haz',
+		'Tem','Ağu','Eyl','Eki','Kas','Ara'],
+		dayNames: ['Pazar','Pazartesi','Salı','Çarşamba','Perşembe','Cuma','Cumartesi'],
+		dayNamesShort: ['Pz','Pt','Sa','Ça','Pe','Cu','Ct'],
+		dayNamesMin: ['Pz','Pt','Sa','Ça','Pe','Cu','Ct'],
+		dateFormat: 'dd.mm.yy', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['tr']);
+});/* Ukrainian (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Maxim Drogobitskiy (maxdao@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['uk'] = {
+		clearText: 'Очистити', clearStatus: '',
+		closeText: 'Закрити', closeStatus: '',
+		prevText: '&#x3c;',  prevStatus: '',
+		prevBigText: '&#x3c;&#x3c;', prevBigStatus: '',
+		nextText: '&#x3e;', nextStatus: '',
+		nextBigText: '&#x3e;&#x3e;', nextBigStatus: '',
+		currentText: 'Сьогодні', currentStatus: '',
+		monthNames: ['Січень','Лютий','Березень','Квітень','Травень','Червень',
+		'Липень','Серпень','Вересень','Жовтень','Листопад','Грудень'],
+		monthNamesShort: ['Січ','Лют','Бер','Кві','Тра','Чер',
+		'Лип','Сер','Вер','Жов','Лис','Гру'],
+		monthStatus: '', yearStatus: '',
+		weekHeader: 'Не', weekStatus: '',
+		dayNames: ['неділя','понеділок','вівторок','середа','четвер','п’ятниця','субота'],
+		dayNamesShort: ['нед','пнд','вів','срд','чтв','птн','сбт'],
+		dayNamesMin: ['Нд','Пн','Вт','Ср','Чт','Пт','Сб'],
+		dayStatus: 'DD', dateStatus: 'D, M d',
+		dateFormat: 'dd/mm/yy', firstDay: 1,
+		initStatus: '', isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['uk']);
+});/* Chinese initialisation for the jQuery UI date picker plugin. */
+/* Written by Cloudream (cloudream@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['zh-CN'] = {
+		closeText: '关闭',
+		prevText: '&#x3c;上月',
+		nextText: '下月&#x3e;',
+		currentText: '今天',
+		monthNames: ['一月','二月','三月','四月','五月','六月',
+		'七月','八月','九月','十月','十一月','十二月'],
+		monthNamesShort: ['一','二','三','四','五','六',
+		'七','八','九','十','十一','十二'],
+		dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
+		dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'],
+		dayNamesMin: ['日','一','二','三','四','五','六'],
+		dateFormat: 'yy-mm-dd', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['zh-CN']);
+});
+/* Chinese initialisation for the jQuery UI date picker plugin. */
+/* Written by Ressol (ressol@gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['zh-TW'] = {
+		closeText: '關閉',
+		prevText: '&#x3c;上月',
+		nextText: '下月&#x3e;',
+		currentText: '今天',
+		monthNames: ['一月','二月','三月','四月','五月','六月',
+		'七月','八月','九月','十月','十一月','十二月'],
+		monthNamesShort: ['一','二','三','四','五','六',
+		'七','八','九','十','十一','十二'],
+		dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
+		dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'],
+		dayNamesMin: ['日','一','二','三','四','五','六'],
+		dateFormat: 'yy/mm/dd', firstDay: 1,
+		isRTL: false};
+	$.datepicker.setDefaults($.datepicker.regional['zh-TW']);
+});
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery.ui.map.full.min.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery.ui.map.full.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..0c8962736aeb9a6cb2ce20824a6064f82c277e75
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/jquery.ui.map.full.min.js
@@ -0,0 +1,2 @@
+/*! http://code.google.com/p/jquery-ui-map/ | Johan S�ll Larsson */
+eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('(3(d){d.a=3(a,b){k c=a.u(".")[0],a=a.u(".")[1];d[c]=d[c]||{};d[c][a]=3(a,b){J.N&&2.13(a,b)};d[c][a].G=d.p({1w:c,1F:a},b);d.1h[a]=3(b){k g="1E"===1G b,f=Q.G.14.15(J,1),i=2;l(g&&"1I"===b.1H(0,1))4 i;2.1A(3(){k h=d.R(2,a);h||(h=d.R(2,a,j d[c][a](b,2)));g&&(i=h[b].18(h,f))});4 i}};d.a("1z.1B",{m:{1D:"1C",1P:5},1O:3(a,b){b&&(2.m[a]=b,2.6("9").C(2.m));4 2.m[a]},13:3(a,b){2.D=b;o.p(2.m,a);2.m.U=2.E(2.m.U);2.Y();2.V&&2.V()},Y:3(){k a=2;a.q={9:j 8.7.1Q(a.D,a.m),H:[],s:[],r:[],S:j 8.7.1S};8.7.x.1R(a.q.9,"1K",3(){d(a.D).1p("1J",a.q.9)});a.y(a.m.1N,a.q.9)},16:3(a){2.6("M",j 8.7.1M).p(2.E(a));2.6("9").1q(2.6("M"));4 2},1r:3(a){k b=2.6("9").1s();4 b?b.1t(a.17()):!1},1x:3(a,b){2.6("9").1y[b].L(2.z(a));4 2},1u:3(a,b,c){a.9=2.6("9");a.W=2.E(a.W);k c=j(c||8.7.1v)(a),e=2.6("H");c.T?e[c.T]=c:e.L(c);c.M&&2.16(c.17());2.y(b,a.9,c);4 d(c)},w:3(a){2.A(2.6(a));2.O(a,[]);4 2},A:3(a){P(k b 10 a)a.Z(b)&&(a[b]t 8.7.1c?(8.7.x.2c(a[b]),a[b].F&&a[b].F(v)):a[b]t Q&&2.A(a[b]),a[b]=v)},2b:3(a,b,c){k a=2.6(a),e;P(e 10 a)a.Z(e)&&c(a[e],b.11&&a[e][b.I]?-1<d.2e(b.12,a[e][b.I].u(b.11)):a[e][b.I]===b.12);4 2},6:3(a,b){k c=2.q;l(!c[a]){l(-1<a.2d(">")){P(k e=a.X(/ /g,"").u(">"),d=0;d<e.N;d++){l(!c[e[d]])l(b)c[e[d]]=d+1<e.N?[]:b;1l 4 v;c=c[e[d]]}4 c}b&&!c[a]&&2.O(a,b)}4 c[a]},27:3(a,b,c){k d=2.6("S");d.C(a);d.2a(2.6("9"),2.z(b));2.y(c,d);4 2},O:3(a,b){2.q[a]=b;4 2},2f:3(){k a=2.6("9"),b=a.2j();d(a).1m("2g");a.2h(b);4 2},2i:3(){2.w("H");2.w("r");2.w("s");2.A(2.q);o.1Z(2.D,2.1T)},y:3(a){a&&d.1V(a)&&a.18(2,Q.G.14.15(J,1))},E:3(a){l(!a)4 j 8.7.K(0,0);l(a t 8.7.K)4 a;a=a.X(/ /g,"").u(",");4 j 8.7.K(a[0],a[1])},z:3(a){l(a){l(a t o)4 a[0];l(a t 1W)4 a}1l 4 v;4 d("#"+a)[0]},22:3(a,b){4 d(2.6("s > "+a,[]).L(j 8.7[a](o.p({9:2.6("9")},b))))},21:3(a,b){(!b?2.6("s > B",j 8.7.B):2.6("s > B",j 8.7.B(b,a))).C(o.p({9:2.6("9")},a))},20:3(a,b,c){2.6("s > "+a,j 8.7.23(b,o.p({9:2.6("9")},c)))},26:3(a,b,c){k d=2,g=2.6("r > 1n",j 8.7.1n),f=2.6("r > 1i",j 8.7.1i);b&&f.C(b);g.25(a,3(a,b){"24"===b?(f.1U(a),f.F(d.6("9"))):f.F(v);c(a,b)})},1Y:3(a,b){2.6("9").1X(2.6("r > 1e",j 8.7.1e(2.z(a),b)))},29:3(a,b){2.6("r > 1f",j 8.7.1f).28(a,b)}});o.1h.p({1d:3(a,b){4 2.n("1d",a,b)},1a:3(a){4 2.n("1a",a)},19:3(a,b){4 2.n("19",a,b)},1b:3(a,b){4 2.n("1b",a,b)},1o:3(a,b){4 2.n("1o",a,b)},1k:3(a){4 2.n("1k",a)},1j:3(a){4 2.n("1j",a)},1m:3(a){8.7.x.1p(2[0],a)},n:3(a,b,c){8.7&&2[0]t 8.7.1c?8.7.x.1L(2[0],a,b):c?2.1g(a,b,c):2.1g(a,b);4 2}})})(o);',62,144,'||this|function|return||get|maps|google|map||||||||||new|var|if|options|addEventListener|jQuery|extend|instance|services|overlays|instanceof|split|null|clear|event|_call|_unwrap|_c|FusionTablesLayer|setOptions|el|_latLng|setMap|prototype|markers|property|arguments|LatLng|push|bounds|length|set|for|Array|data|iw|id|center|_init|position|replace|_create|hasOwnProperty|in|delimiter|value|_setup|slice|call|addBounds|getPosition|apply|dblclick|rightclick|mouseover|MVCObject|click|StreetViewPanorama|Geocoder|bind|fn|DirectionsRenderer|dragend|drag|else|triggerEvent|DirectionsService|mouseout|trigger|fitBounds|inViewport|getBounds|contains|addMarker|Marker|namespace|addControl|controls|ui|each|gmap|roadmap|mapTypeId|string|pluginName|typeof|substring|_|init|bounds_changed|addListener|LatLngBounds|callback|option|zoom|Map|addListenerOnce|InfoWindow|name|setDirections|isFunction|Object|setStreetView|displayStreetView|removeData|loadKML|loadFusion|addShape|KmlLayer|OK|route|displayDirections|openInfoWindow|geocode|search|open|find|clearInstanceListeners|indexOf|inArray|refresh|resize|setCenter|destroy|getCenter'.split('|'),0,{}))
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/menu.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/menu.js
new file mode 100644
index 0000000000000000000000000000000000000000..dacad592121852771412af281ba189e81e5bd660
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/menu.js
@@ -0,0 +1,63 @@
+/* -*- mode: javascript -*-
+ */
+
+function getMouseXY(e) // works on IE6,FF,Moz,Opera7
+{ 
+  if (!e) e = window.event; // works on IE, but not NS (we rely on NS passing us the event)
+
+  if (e)
+  { 
+    if (e.pageY)
+    { // this doesn't work on IE6!! (works on FF,Moz,Opera7)
+      mousey = e.pageY;
+      algor = '[e.pageX]';
+      if (e.clientX || e.clientY) algor += ' [e.clientX] '
+    }
+    else if (e.clientY)
+    { // works on IE6,FF,Moz,Opera7
+if ( document.documentElement && document.documentElement.scrollTop )	
+	{
+      mousey = e.clientY + document.documentElement.scrollTop;
+	}
+	
+	else
+	{
+      mousey = e.clientY + document.body.scrollTop;
+	}
+      algor = '[e.clientX]';
+      if (e.pageX || e.pageY) algor += ' [e.pageX] '
+    }
+  }
+}
+
+var menu_firefox_flicker = false ;
+
+
+var mousey = 0
+
+function MenuDisplay(l_element)
+	{
+	getMouseXY()
+	if ( ! menu_firefox_flicker )
+		{
+		l_element.childNodes[1].style.display = 'block' ;
+		if ( mousey > 600 )
+			{
+			l_element.childNodes[1].style.left = '0px' ;
+			l_element.childNodes[1].style.display = 'block' ;
+			l_element.childNodes[1].style.top = - l_element.childNodes[1].offsetHeight + 'px' ;
+			}
+		}
+	else if ( mousey > 600 )
+		{
+		l_element.childNodes[1].style.top = - l_element.childNodes[1].offsetHeight + 'px' ;
+		}
+	}
+	
+function MenuHide(l_element)
+	{
+	if ( ! menu_firefox_flicker )
+		{
+		l_element.childNodes[1].style.display = 'none'
+		}
+	}
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/INSTALL b/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/INSTALL
new file mode 100755
index 0000000000000000000000000000000000000000..686431f8d297729fc10f29727b5c346227cbfd22
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/INSTALL
@@ -0,0 +1,8 @@
+Installing qTip is relatively straight forward. Simply include the both the jQuery and qTip library files e.g. jquery-qtip-1.0.0.min.js using script html element(s), like so: 
+
+<script type="text/javascript" src="/projects/qtip/js/jquery.1.3.2.min.js"></script>
+<script type="text/javascript" src="/projects/qtip/js/jquery.qtip-1.0.0.min.js"></script> 
+
+Notice the jQuery library is included before qTip. This is _absolutely essential_ for correct functioning of the library and its accompanying methods.
+
+It is HIGHLY RECOMMENDED that all JavaScript includes like the one above be placed after all your content at the footer of the page, just before the end </body> tag. This ensures that all content is loaded before manipulation of the DOM occurs.
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/LICENSE b/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/LICENSE
new file mode 100755
index 0000000000000000000000000000000000000000..e9cd5ecb5b66da2d53420906c4b3d6635900c41b
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/LICENSE
@@ -0,0 +1,7 @@
+Copyright © 2009 Craig Thompson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/REQUIREMENTS b/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/REQUIREMENTS
new file mode 100755
index 0000000000000000000000000000000000000000..3c7918fd9054d536118c6a7bce95a2f54b77ef3e
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/REQUIREMENTS
@@ -0,0 +1,5 @@
+JQUERY IS REQUIRED FOR USE OF THIS PLUGIN
+
+You can grab the latest version here: http://jquery.com
+
+We recommend version 1.3 due to its significant speed increases in several areas, but qTip supports the use of jQuery version 1.2.6 and above for all you who have decided not to upgrade.
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/jquery.qtip-1.0.0-rc3.min.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/jquery.qtip-1.0.0-rc3.min.js
new file mode 100755
index 0000000000000000000000000000000000000000..173885b75f5e586e418bb02127d8dd68f4029060
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/jquery.qtip-1.0.0-rc3.min.js
@@ -0,0 +1,15 @@
+/*
+ * jquery.qtip. The jQuery tooltip plugin
+ *
+ * Copyright (c) 2009 Craig Thompson
+ * http://craigsworks.com
+ *
+ * Licensed under MIT
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * Launch  : February 2009
+ * Version : 1.0.0-rc3
+ * Released: Tuesday 12th May, 2009 - 00:00
+ * Debug: jquery.qtip.debug.js
+ */
+(function(f){f.fn.qtip=function(B,u){var y,t,A,s,x,w,v,z;if(typeof B=="string"){if(typeof f(this).data("qtip")!=="object"){f.fn.qtip.log.error.call(self,1,f.fn.qtip.constants.NO_TOOLTIP_PRESENT,false)}if(B=="api"){return f(this).data("qtip").interfaces[f(this).data("qtip").current]}else{if(B=="interfaces"){return f(this).data("qtip").interfaces}}}else{if(!B){B={}}if(typeof B.content!=="object"||(B.content.jquery&&B.content.length>0)){B.content={text:B.content}}if(typeof B.content.title!=="object"){B.content.title={text:B.content.title}}if(typeof B.position!=="object"){B.position={corner:B.position}}if(typeof B.position.corner!=="object"){B.position.corner={target:B.position.corner,tooltip:B.position.corner}}if(typeof B.show!=="object"){B.show={when:B.show}}if(typeof B.show.when!=="object"){B.show.when={event:B.show.when}}if(typeof B.show.effect!=="object"){B.show.effect={type:B.show.effect}}if(typeof B.hide!=="object"){B.hide={when:B.hide}}if(typeof B.hide.when!=="object"){B.hide.when={event:B.hide.when}}if(typeof B.hide.effect!=="object"){B.hide.effect={type:B.hide.effect}}if(typeof B.style!=="object"){B.style={name:B.style}}B.style=c(B.style);s=f.extend(true,{},f.fn.qtip.defaults,B);s.style=a.call({options:s},s.style);s.user=f.extend(true,{},B)}return f(this).each(function(){if(typeof B=="string"){w=B.toLowerCase();A=f(this).qtip("interfaces");if(typeof A=="object"){if(u===true&&w=="destroy"){while(A.length>0){A[A.length-1].destroy()}}else{if(u!==true){A=[f(this).qtip("api")]}for(y=0;y<A.length;y++){if(w=="destroy"){A[y].destroy()}else{if(A[y].status.rendered===true){if(w=="show"){A[y].show()}else{if(w=="hide"){A[y].hide()}else{if(w=="focus"){A[y].focus()}else{if(w=="disable"){A[y].disable(true)}else{if(w=="enable"){A[y].disable(false)}}}}}}}}}}}else{v=f.extend(true,{},s);v.hide.effect.length=s.hide.effect.length;v.show.effect.length=s.show.effect.length;if(v.position.container===false){v.position.container=f(document.body)}if(v.position.target===false){v.position.target=f(this)}if(v.show.when.target===false){v.show.when.target=f(this)}if(v.hide.when.target===false){v.hide.when.target=f(this)}t=f.fn.qtip.interfaces.length;for(y=0;y<t;y++){if(typeof f.fn.qtip.interfaces[y]=="undefined"){t=y;break}}x=new d(f(this),v,t);f.fn.qtip.interfaces[t]=x;if(typeof f(this).data("qtip")=="object"){if(typeof f(this).attr("qtip")==="undefined"){f(this).data("qtip").current=f(this).data("qtip").interfaces.length}f(this).data("qtip").interfaces.push(x)}else{f(this).data("qtip",{current:0,interfaces:[x]})}if(v.content.prerender===false&&v.show.when.event!==false&&v.show.ready!==true){v.show.when.target.bind(v.show.when.event+".qtip-"+t+"-create",{qtip:t},function(C){z=f.fn.qtip.interfaces[C.data.qtip];z.options.show.when.target.unbind(z.options.show.when.event+".qtip-"+C.data.qtip+"-create");z.cache.mouse={x:C.pageX,y:C.pageY};p.call(z);z.options.show.when.target.trigger(z.options.show.when.event)})}else{x.cache.mouse={x:v.show.when.target.offset().left,y:v.show.when.target.offset().top};p.call(x)}}})};function d(u,t,v){var s=this;s.id=v;s.options=t;s.status={animated:false,rendered:false,disabled:false,focused:false};s.elements={target:u.addClass(s.options.style.classes.target),tooltip:null,wrapper:null,content:null,contentWrapper:null,title:null,button:null,tip:null,bgiframe:null};s.cache={mouse:{},position:{},toggle:0};s.timers={};f.extend(s,s.options.api,{show:function(y){var x,z;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"show")}if(s.elements.tooltip.css("display")!=="none"){return s}s.elements.tooltip.stop(true,false);x=s.beforeShow.call(s,y);if(x===false){return s}function w(){if(s.options.position.type!=="static"){s.focus()}s.onShow.call(s,y);if(f.browser.msie){s.elements.tooltip.get(0).style.removeAttribute("filter")}}s.cache.toggle=1;if(s.options.position.type!=="static"){s.updatePosition(y,(s.options.show.effect.length>0))}if(typeof s.options.show.solo=="object"){z=f(s.options.show.solo)}else{if(s.options.show.solo===true){z=f("div.qtip").not(s.elements.tooltip)}}if(z){z.each(function(){if(f(this).qtip("api").status.rendered===true){f(this).qtip("api").hide()}})}if(typeof s.options.show.effect.type=="function"){s.options.show.effect.type.call(s.elements.tooltip,s.options.show.effect.length);s.elements.tooltip.queue(function(){w();f(this).dequeue()})}else{switch(s.options.show.effect.type.toLowerCase()){case"fade":s.elements.tooltip.fadeIn(s.options.show.effect.length,w);break;case"slide":s.elements.tooltip.slideDown(s.options.show.effect.length,function(){w();if(s.options.position.type!=="static"){s.updatePosition(y,true)}});break;case"grow":s.elements.tooltip.show(s.options.show.effect.length,w);break;default:s.elements.tooltip.show(null,w);break}s.elements.tooltip.addClass(s.options.style.classes.active)}return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_SHOWN,"show")},hide:function(y){var x;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"hide")}else{if(s.elements.tooltip.css("display")==="none"){return s}}clearTimeout(s.timers.show);s.elements.tooltip.stop(true,false);x=s.beforeHide.call(s,y);if(x===false){return s}function w(){s.onHide.call(s,y)}s.cache.toggle=0;if(typeof s.options.hide.effect.type=="function"){s.options.hide.effect.type.call(s.elements.tooltip,s.options.hide.effect.length);s.elements.tooltip.queue(function(){w();f(this).dequeue()})}else{switch(s.options.hide.effect.type.toLowerCase()){case"fade":s.elements.tooltip.fadeOut(s.options.hide.effect.length,w);break;case"slide":s.elements.tooltip.slideUp(s.options.hide.effect.length,w);break;case"grow":s.elements.tooltip.hide(s.options.hide.effect.length,w);break;default:s.elements.tooltip.hide(null,w);break}s.elements.tooltip.removeClass(s.options.style.classes.active)}return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_HIDDEN,"hide")},updatePosition:function(w,x){var C,G,L,J,H,E,y,I,B,D,K,A,F,z;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"updatePosition")}else{if(s.options.position.type=="static"){return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.CANNOT_POSITION_STATIC,"updatePosition")}}G={position:{left:0,top:0},dimensions:{height:0,width:0},corner:s.options.position.corner.target};L={position:s.getPosition(),dimensions:s.getDimensions(),corner:s.options.position.corner.tooltip};if(s.options.position.target!=="mouse"){if(s.options.position.target.get(0).nodeName.toLowerCase()=="area"){J=s.options.position.target.attr("coords").split(",");for(C=0;C<J.length;C++){J[C]=parseInt(J[C])}H=s.options.position.target.parent("map").attr("name");E=f('img[usemap="#'+H+'"]:first').offset();G.position={left:Math.floor(E.left+J[0]),top:Math.floor(E.top+J[1])};switch(s.options.position.target.attr("shape").toLowerCase()){case"rect":G.dimensions={width:Math.ceil(Math.abs(J[2]-J[0])),height:Math.ceil(Math.abs(J[3]-J[1]))};break;case"circle":G.dimensions={width:J[2]+1,height:J[2]+1};break;case"poly":G.dimensions={width:J[0],height:J[1]};for(C=0;C<J.length;C++){if(C%2==0){if(J[C]>G.dimensions.width){G.dimensions.width=J[C]}if(J[C]<J[0]){G.position.left=Math.floor(E.left+J[C])}}else{if(J[C]>G.dimensions.height){G.dimensions.height=J[C]}if(J[C]<J[1]){G.position.top=Math.floor(E.top+J[C])}}}G.dimensions.width=G.dimensions.width-(G.position.left-E.left);G.dimensions.height=G.dimensions.height-(G.position.top-E.top);break;default:return f.fn.qtip.log.error.call(s,4,f.fn.qtip.constants.INVALID_AREA_SHAPE,"updatePosition");break}G.dimensions.width-=2;G.dimensions.height-=2}else{if(s.options.position.target.add(document.body).length===1){G.position={left:f(document).scrollLeft(),top:f(document).scrollTop()};G.dimensions={height:f(window).height(),width:f(window).width()}}else{if(typeof s.options.position.target.attr("qtip")!=="undefined"){G.position=s.options.position.target.qtip("api").cache.position}else{G.position=s.options.position.target.offset()}G.dimensions={height:s.options.position.target.outerHeight(),width:s.options.position.target.outerWidth()}}}y=f.extend({},G.position);if(G.corner.search(/right/i)!==-1){y.left+=G.dimensions.width}if(G.corner.search(/bottom/i)!==-1){y.top+=G.dimensions.height}if(G.corner.search(/((top|bottom)Middle)|center/)!==-1){y.left+=(G.dimensions.width/2)}if(G.corner.search(/((left|right)Middle)|center/)!==-1){y.top+=(G.dimensions.height/2)}}else{G.position=y={left:s.cache.mouse.x,top:s.cache.mouse.y};G.dimensions={height:1,width:1}}if(L.corner.search(/right/i)!==-1){y.left-=L.dimensions.width}if(L.corner.search(/bottom/i)!==-1){y.top-=L.dimensions.height}if(L.corner.search(/((top|bottom)Middle)|center/)!==-1){y.left-=(L.dimensions.width/2)}if(L.corner.search(/((left|right)Middle)|center/)!==-1){y.top-=(L.dimensions.height/2)}I=(f.browser.msie)?1:0;B=(f.browser.msie&&parseInt(f.browser.version.charAt(0))===6)?1:0;if(s.options.style.border.radius>0){if(L.corner.search(/Left/)!==-1){y.left-=s.options.style.border.radius}else{if(L.corner.search(/Right/)!==-1){y.left+=s.options.style.border.radius}}if(L.corner.search(/Top/)!==-1){y.top-=s.options.style.border.radius}else{if(L.corner.search(/Bottom/)!==-1){y.top+=s.options.style.border.radius}}}if(I){if(L.corner.search(/top/)!==-1){y.top-=I}else{if(L.corner.search(/bottom/)!==-1){y.top+=I}}if(L.corner.search(/left/)!==-1){y.left-=I}else{if(L.corner.search(/right/)!==-1){y.left+=I}}if(L.corner.search(/leftMiddle|rightMiddle/)!==-1){y.top-=1}}if(s.options.position.adjust.screen===true){y=o.call(s,y,G,L)}if(s.options.position.target==="mouse"&&s.options.position.adjust.mouse===true){if(s.options.position.adjust.screen===true&&s.elements.tip){K=s.elements.tip.attr("rel")}else{K=s.options.position.corner.tooltip}y.left+=(K.search(/right/i)!==-1)?-6:6;y.top+=(K.search(/bottom/i)!==-1)?-6:6}if(!s.elements.bgiframe&&f.browser.msie&&parseInt(f.browser.version.charAt(0))==6){f("select, object").each(function(){A=f(this).offset();A.bottom=A.top+f(this).height();A.right=A.left+f(this).width();if(y.top+L.dimensions.height>=A.top&&y.left+L.dimensions.width>=A.left){k.call(s)}})}y.left+=s.options.position.adjust.x;y.top+=s.options.position.adjust.y;F=s.getPosition();if(y.left!=F.left||y.top!=F.top){z=s.beforePositionUpdate.call(s,w);if(z===false){return s}s.cache.position=y;if(x===true){s.status.animated=true;s.elements.tooltip.animate(y,200,"swing",function(){s.status.animated=false})}else{s.elements.tooltip.css(y)}s.onPositionUpdate.call(s,w);if(typeof w!=="undefined"&&w.type&&w.type!=="mousemove"){f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_POSITION_UPDATED,"updatePosition")}}return s},updateWidth:function(w){var x;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"updateWidth")}else{if(w&&typeof w!=="number"){return f.fn.qtip.log.error.call(s,2,"newWidth must be of type number","updateWidth")}}x=s.elements.contentWrapper.siblings().add(s.elements.tip).add(s.elements.button);if(!w){if(typeof s.options.style.width.value=="number"){w=s.options.style.width.value}else{s.elements.tooltip.css({width:"auto"});x.hide();if(f.browser.msie){s.elements.wrapper.add(s.elements.contentWrapper.children()).css({zoom:"normal"})}w=s.getDimensions().width+1;if(!s.options.style.width.value){if(w>s.options.style.width.max){w=s.options.style.width.max}if(w<s.options.style.width.min){w=s.options.style.width.min}}}}if(w%2!==0){w-=1}s.elements.tooltip.width(w);x.show();if(s.options.style.border.radius){s.elements.tooltip.find(".qtip-betweenCorners").each(function(y){f(this).width(w-(s.options.style.border.radius*2))})}if(f.browser.msie){s.elements.wrapper.add(s.elements.contentWrapper.children()).css({zoom:"1"});s.elements.wrapper.width(w);if(s.elements.bgiframe){s.elements.bgiframe.width(w).height(s.getDimensions.height)}}return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_WIDTH_UPDATED,"updateWidth")},updateStyle:function(w){var z,A,x,y,B;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"updateStyle")}else{if(typeof w!=="string"||!f.fn.qtip.styles[w]){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.STYLE_NOT_DEFINED,"updateStyle")}}s.options.style=a.call(s,f.fn.qtip.styles[w],s.options.user.style);s.elements.content.css(q(s.options.style));if(s.options.content.title.text!==false){s.elements.title.css(q(s.options.style.title,true))}s.elements.contentWrapper.css({borderColor:s.options.style.border.color});if(s.options.style.tip.corner!==false){if(f("<canvas>").get(0).getContext){z=s.elements.tooltip.find(".qtip-tip canvas:first");x=z.get(0).getContext("2d");x.clearRect(0,0,300,300);y=z.parent("div[rel]:first").attr("rel");B=b(y,s.options.style.tip.size.width,s.options.style.tip.size.height);h.call(s,z,B,s.options.style.tip.color||s.options.style.border.color)}else{if(f.browser.msie){z=s.elements.tooltip.find('.qtip-tip [nodeName="shape"]');z.attr("fillcolor",s.options.style.tip.color||s.options.style.border.color)}}}if(s.options.style.border.radius>0){s.elements.tooltip.find(".qtip-betweenCorners").css({backgroundColor:s.options.style.border.color});if(f("<canvas>").get(0).getContext){A=g(s.options.style.border.radius);s.elements.tooltip.find(".qtip-wrapper canvas").each(function(){x=f(this).get(0).getContext("2d");x.clearRect(0,0,300,300);y=f(this).parent("div[rel]:first").attr("rel");r.call(s,f(this),A[y],s.options.style.border.radius,s.options.style.border.color)})}else{if(f.browser.msie){s.elements.tooltip.find('.qtip-wrapper [nodeName="arc"]').each(function(){f(this).attr("fillcolor",s.options.style.border.color)})}}}return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_STYLE_UPDATED,"updateStyle")},updateContent:function(A,y){var z,x,w;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"updateContent")}else{if(!A){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.NO_CONTENT_PROVIDED,"updateContent")}}z=s.beforeContentUpdate.call(s,A);if(typeof z=="string"){A=z}else{if(z===false){return}}if(f.browser.msie){s.elements.contentWrapper.children().css({zoom:"normal"})}if(A.jquery&&A.length>0){A.clone(true).appendTo(s.elements.content).show()}else{s.elements.content.html(A)}x=s.elements.content.find("img[complete=false]");if(x.length>0){w=0;x.each(function(C){f('<img src="'+f(this).attr("src")+'" />').load(function(){if(++w==x.length){B()}})})}else{B()}function B(){s.updateWidth();if(y!==false){if(s.options.position.type!=="static"){s.updatePosition(s.elements.tooltip.is(":visible"),true)}if(s.options.style.tip.corner!==false){n.call(s)}}}s.onContentUpdate.call(s);return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_CONTENT_UPDATED,"loadContent")},loadContent:function(w,z,A){var y;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"loadContent")}y=s.beforeContentLoad.call(s);if(y===false){return s}if(A=="post"){f.post(w,z,x)}else{f.get(w,z,x)}function x(B){s.onContentLoad.call(s);f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_CONTENT_LOADED,"loadContent");s.updateContent(B)}return s},updateTitle:function(w){if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"updateTitle")}else{if(!w){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.NO_CONTENT_PROVIDED,"updateTitle")}}returned=s.beforeTitleUpdate.call(s);if(returned===false){return s}if(s.elements.button){s.elements.button=s.elements.button.clone(true)}s.elements.title.html(w);if(s.elements.button){s.elements.title.prepend(s.elements.button)}s.onTitleUpdate.call(s);return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_TITLE_UPDATED,"updateTitle")},focus:function(A){var y,x,w,z;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"focus")}else{if(s.options.position.type=="static"){return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.CANNOT_FOCUS_STATIC,"focus")}}y=parseInt(s.elements.tooltip.css("z-index"));x=6000+f("div.qtip[qtip]").length-1;if(!s.status.focused&&y!==x){z=s.beforeFocus.call(s,A);if(z===false){return s}f("div.qtip[qtip]").not(s.elements.tooltip).each(function(){if(f(this).qtip("api").status.rendered===true){w=parseInt(f(this).css("z-index"));if(typeof w=="number"&&w>-1){f(this).css({zIndex:parseInt(f(this).css("z-index"))-1})}f(this).qtip("api").status.focused=false}});s.elements.tooltip.css({zIndex:x});s.status.focused=true;s.onFocus.call(s,A);f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_FOCUSED,"focus")}return s},disable:function(w){if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"disable")}if(w){if(!s.status.disabled){s.status.disabled=true;f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_DISABLED,"disable")}else{f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.TOOLTIP_ALREADY_DISABLED,"disable")}}else{if(s.status.disabled){s.status.disabled=false;f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_ENABLED,"disable")}else{f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.TOOLTIP_ALREADY_ENABLED,"disable")}}return s},destroy:function(){var w,x,y;x=s.beforeDestroy.call(s);if(x===false){return s}if(s.status.rendered){s.options.show.when.target.unbind("mousemove.qtip",s.updatePosition);s.options.show.when.target.unbind("mouseout.qtip",s.hide);s.options.show.when.target.unbind(s.options.show.when.event+".qtip");s.options.hide.when.target.unbind(s.options.hide.when.event+".qtip");s.elements.tooltip.unbind(s.options.hide.when.event+".qtip");s.elements.tooltip.unbind("mouseover.qtip",s.focus);s.elements.tooltip.remove()}else{s.options.show.when.target.unbind(s.options.show.when.event+".qtip-create")}if(typeof s.elements.target.data("qtip")=="object"){y=s.elements.target.data("qtip").interfaces;if(typeof y=="object"&&y.length>0){for(w=0;w<y.length-1;w++){if(y[w].id==s.id){y.splice(w,1)}}}}delete f.fn.qtip.interfaces[s.id];if(typeof y=="object"&&y.length>0){s.elements.target.data("qtip").current=y.length-1}else{s.elements.target.removeData("qtip")}s.onDestroy.call(s);f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_DESTROYED,"destroy");return s.elements.target},getPosition:function(){var w,x;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"getPosition")}w=(s.elements.tooltip.css("display")!=="none")?false:true;if(w){s.elements.tooltip.css({visiblity:"hidden"}).show()}x=s.elements.tooltip.offset();if(w){s.elements.tooltip.css({visiblity:"visible"}).hide()}return x},getDimensions:function(){var w,x;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"getDimensions")}w=(!s.elements.tooltip.is(":visible"))?true:false;if(w){s.elements.tooltip.css({visiblity:"hidden"}).show()}x={height:s.elements.tooltip.outerHeight(),width:s.elements.tooltip.outerWidth()};if(w){s.elements.tooltip.css({visiblity:"visible"}).hide()}return x}})}function p(){var s,w,u,t,v,y,x;s=this;s.beforeRender.call(s);s.status.rendered=true;s.elements.tooltip='<div qtip="'+s.id+'" class="qtip '+(s.options.style.classes.tooltip||s.options.style)+'"style="display:none; -moz-border-radius:0; -webkit-border-radius:0; border-radius:0;position:'+s.options.position.type+';">  <div class="qtip-wrapper" style="position:relative; overflow:hidden; text-align:left;">    <div class="qtip-contentWrapper" style="overflow:hidden;">       <div class="qtip-content '+s.options.style.classes.content+'"></div></div></div></div>';s.elements.tooltip=f(s.elements.tooltip);s.elements.tooltip.appendTo(s.options.position.container);s.elements.tooltip.data("qtip",{current:0,interfaces:[s]});s.elements.wrapper=s.elements.tooltip.children("div:first");s.elements.contentWrapper=s.elements.wrapper.children("div:first").css({background:s.options.style.background});s.elements.content=s.elements.contentWrapper.children("div:first").css(q(s.options.style));if(f.browser.msie){s.elements.wrapper.add(s.elements.content).css({zoom:1})}if(s.options.hide.when.event=="unfocus"){s.elements.tooltip.attr("unfocus",true)}if(typeof s.options.style.width.value=="number"){s.updateWidth()}if(f("<canvas>").get(0).getContext||f.browser.msie){if(s.options.style.border.radius>0){m.call(s)}else{s.elements.contentWrapper.css({border:s.options.style.border.width+"px solid "+s.options.style.border.color})}if(s.options.style.tip.corner!==false){e.call(s)}}else{s.elements.contentWrapper.css({border:s.options.style.border.width+"px solid "+s.options.style.border.color});s.options.style.border.radius=0;s.options.style.tip.corner=false;f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.CANVAS_VML_NOT_SUPPORTED,"render")}if((typeof s.options.content.text=="string"&&s.options.content.text.length>0)||(s.options.content.text.jquery&&s.options.content.text.length>0)){u=s.options.content.text}else{if(typeof s.elements.target.attr("title")=="string"&&s.elements.target.attr("title").length>0){u=s.elements.target.attr("title").replace("\\n","<br />");s.elements.target.attr("title","")}else{if(typeof s.elements.target.attr("alt")=="string"&&s.elements.target.attr("alt").length>0){u=s.elements.target.attr("alt").replace("\\n","<br />");s.elements.target.attr("alt","")}else{u=" ";f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.NO_VALID_CONTENT,"render")}}}if(s.options.content.title.text!==false){j.call(s)}s.updateContent(u);l.call(s);if(s.options.show.ready===true){s.show()}if(s.options.content.url!==false){t=s.options.content.url;v=s.options.content.data;y=s.options.content.method||"get";s.loadContent(t,v,y)}s.onRender.call(s);f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_RENDERED,"render")}function m(){var F,z,t,B,x,E,u,G,D,y,w,C,A,s,v;F=this;F.elements.wrapper.find(".qtip-borderBottom, .qtip-borderTop").remove();t=F.options.style.border.width;B=F.options.style.border.radius;x=F.options.style.border.color||F.options.style.tip.color;E=g(B);u={};for(z in E){u[z]='<div rel="'+z+'" style="'+((z.search(/Left/)!==-1)?"left":"right")+":0; position:absolute; height:"+B+"px; width:"+B+'px; overflow:hidden; line-height:0.1px; font-size:1px">';if(f("<canvas>").get(0).getContext){u[z]+='<canvas height="'+B+'" width="'+B+'" style="vertical-align: top"></canvas>'}else{if(f.browser.msie){G=B*2+3;u[z]+='<v:arc stroked="false" fillcolor="'+x+'" startangle="'+E[z][0]+'" endangle="'+E[z][1]+'" style="width:'+G+"px; height:"+G+"px; margin-top:"+((z.search(/bottom/)!==-1)?-2:-1)+"px; margin-left:"+((z.search(/Right/)!==-1)?E[z][2]-3.5:-1)+'px; vertical-align:top; display:inline-block; behavior:url(#default#VML)"></v:arc>'}}u[z]+="</div>"}D=F.getDimensions().width-(Math.max(t,B)*2);y='<div class="qtip-betweenCorners" style="height:'+B+"px; width:"+D+"px; overflow:hidden; background-color:"+x+'; line-height:0.1px; font-size:1px;">';w='<div class="qtip-borderTop" dir="ltr" style="height:'+B+"px; margin-left:"+B+'px; line-height:0.1px; font-size:1px; padding:0;">'+u.topLeft+u.topRight+y;F.elements.wrapper.prepend(w);C='<div class="qtip-borderBottom" dir="ltr" style="height:'+B+"px; margin-left:"+B+'px; line-height:0.1px; font-size:1px; padding:0;">'+u.bottomLeft+u.bottomRight+y;F.elements.wrapper.append(C);if(f("<canvas>").get(0).getContext){F.elements.wrapper.find("canvas").each(function(){A=E[f(this).parent("[rel]:first").attr("rel")];r.call(F,f(this),A,B,x)})}else{if(f.browser.msie){F.elements.tooltip.append('<v:image style="behavior:url(#default#VML);"></v:image>')}}s=Math.max(B,(B+(t-B)));v=Math.max(t-B,0);F.elements.contentWrapper.css({border:"0px solid "+x,borderWidth:v+"px "+s+"px"})}function r(u,w,s,t){var v=u.get(0).getContext("2d");v.fillStyle=t;v.beginPath();v.arc(w[0],w[1],s,0,Math.PI*2,false);v.fill()}function e(v){var t,s,x,u,w;t=this;if(t.elements.tip!==null){t.elements.tip.remove()}s=t.options.style.tip.color||t.options.style.border.color;if(t.options.style.tip.corner===false){return}else{if(!v){v=t.options.style.tip.corner}}x=b(v,t.options.style.tip.size.width,t.options.style.tip.size.height);t.elements.tip='<div class="'+t.options.style.classes.tip+'" dir="ltr" rel="'+v+'" style="position:absolute; height:'+t.options.style.tip.size.height+"px; width:"+t.options.style.tip.size.width+'px; margin:0 auto; line-height:0.1px; font-size:1px;">';if(f("<canvas>").get(0).getContext){t.elements.tip+='<canvas height="'+t.options.style.tip.size.height+'" width="'+t.options.style.tip.size.width+'"></canvas>'}else{if(f.browser.msie){u=t.options.style.tip.size.width+","+t.options.style.tip.size.height;w="m"+x[0][0]+","+x[0][1];w+=" l"+x[1][0]+","+x[1][1];w+=" "+x[2][0]+","+x[2][1];w+=" xe";t.elements.tip+='<v:shape fillcolor="'+s+'" stroked="false" filled="true" path="'+w+'" coordsize="'+u+'" style="width:'+t.options.style.tip.size.width+"px; height:"+t.options.style.tip.size.height+"px; line-height:0.1px; display:inline-block; behavior:url(#default#VML); vertical-align:"+((v.search(/top/)!==-1)?"bottom":"top")+'"></v:shape>';t.elements.tip+='<v:image style="behavior:url(#default#VML);"></v:image>';t.elements.contentWrapper.css("position","relative")}}t.elements.tooltip.prepend(t.elements.tip+"</div>");t.elements.tip=t.elements.tooltip.find("."+t.options.style.classes.tip).eq(0);if(f("<canvas>").get(0).getContext){h.call(t,t.elements.tip.find("canvas:first"),x,s)}if(v.search(/top/)!==-1&&f.browser.msie&&parseInt(f.browser.version.charAt(0))===6){t.elements.tip.css({marginTop:-4})}n.call(t,v)}function h(t,v,s){var u=t.get(0).getContext("2d");u.fillStyle=s;u.beginPath();u.moveTo(v[0][0],v[0][1]);u.lineTo(v[1][0],v[1][1]);u.lineTo(v[2][0],v[2][1]);u.fill()}function n(u){var t,w,s,x,v;t=this;if(t.options.style.tip.corner===false||!t.elements.tip){return}if(!u){u=t.elements.tip.attr("rel")}w=positionAdjust=(f.browser.msie)?1:0;t.elements.tip.css(u.match(/left|right|top|bottom/)[0],0);if(u.search(/top|bottom/)!==-1){if(f.browser.msie){if(parseInt(f.browser.version.charAt(0))===6){positionAdjust=(u.search(/top/)!==-1)?-3:1}else{positionAdjust=(u.search(/top/)!==-1)?1:2}}if(u.search(/Middle/)!==-1){t.elements.tip.css({left:"50%",marginLeft:-(t.options.style.tip.size.width/2)})}else{if(u.search(/Left/)!==-1){t.elements.tip.css({left:t.options.style.border.radius-w})}else{if(u.search(/Right/)!==-1){t.elements.tip.css({right:t.options.style.border.radius+w})}}}if(u.search(/top/)!==-1){t.elements.tip.css({top:-positionAdjust})}else{t.elements.tip.css({bottom:positionAdjust})}}else{if(u.search(/left|right/)!==-1){if(f.browser.msie){positionAdjust=(parseInt(f.browser.version.charAt(0))===6)?1:((u.search(/left/)!==-1)?1:2)}if(u.search(/Middle/)!==-1){t.elements.tip.css({top:"50%",marginTop:-(t.options.style.tip.size.height/2)})}else{if(u.search(/Top/)!==-1){t.elements.tip.css({top:t.options.style.border.radius-w})}else{if(u.search(/Bottom/)!==-1){t.elements.tip.css({bottom:t.options.style.border.radius+w})}}}if(u.search(/left/)!==-1){t.elements.tip.css({left:-positionAdjust})}else{t.elements.tip.css({right:positionAdjust})}}}s="padding-"+u.match(/left|right|top|bottom/)[0];x=t.options.style.tip.size[(s.search(/left|right/)!==-1)?"width":"height"];t.elements.tooltip.css("padding",0);t.elements.tooltip.css(s,x);if(f.browser.msie&&parseInt(f.browser.version.charAt(0))==6){v=parseInt(t.elements.tip.css("margin-top"))||0;v+=parseInt(t.elements.content.css("margin-top"))||0;t.elements.tip.css({marginTop:v})}}function j(){var s=this;if(s.elements.title!==null){s.elements.title.remove()}s.elements.title=f('<div class="'+s.options.style.classes.title+'">').css(q(s.options.style.title,true)).css({zoom:(f.browser.msie)?1:0}).prependTo(s.elements.contentWrapper);if(s.options.content.title.text){s.updateTitle.call(s,s.options.content.title.text)}if(s.options.content.title.button!==false&&typeof s.options.content.title.button=="string"){s.elements.button=f('<a class="'+s.options.style.classes.button+'" style="float:right; position: relative"></a>').css(q(s.options.style.button,true)).html(s.options.content.title.button).prependTo(s.elements.title).click(function(t){if(!s.status.disabled){s.hide(t)}})}}function l(){var t,v,u,s;t=this;v=t.options.show.when.target;u=t.options.hide.when.target;if(t.options.hide.fixed){u=u.add(t.elements.tooltip)}if(t.options.hide.when.event=="inactive"){s=["click","dblclick","mousedown","mouseup","mousemove","mouseout","mouseenter","mouseleave","mouseover"];function y(z){if(t.status.disabled===true){return}clearTimeout(t.timers.inactive);t.timers.inactive=setTimeout(function(){f(s).each(function(){u.unbind(this+".qtip-inactive");t.elements.content.unbind(this+".qtip-inactive")});t.hide(z)},t.options.hide.delay)}}else{if(t.options.hide.fixed===true){t.elements.tooltip.bind("mouseover.qtip",function(){if(t.status.disabled===true){return}clearTimeout(t.timers.hide)})}}function x(z){if(t.status.disabled===true){return}if(t.options.hide.when.event=="inactive"){f(s).each(function(){u.bind(this+".qtip-inactive",y);t.elements.content.bind(this+".qtip-inactive",y)});y()}clearTimeout(t.timers.show);clearTimeout(t.timers.hide);t.timers.show=setTimeout(function(){t.show(z)},t.options.show.delay)}function w(z){if(t.status.disabled===true){return}if(t.options.hide.fixed===true&&t.options.hide.when.event.search(/mouse(out|leave)/i)!==-1&&f(z.relatedTarget).parents("div.qtip[qtip]").length>0){z.stopPropagation();z.preventDefault();clearTimeout(t.timers.hide);return false}clearTimeout(t.timers.show);clearTimeout(t.timers.hide);t.elements.tooltip.stop(true,true);t.timers.hide=setTimeout(function(){t.hide(z)},t.options.hide.delay)}if((t.options.show.when.target.add(t.options.hide.when.target).length===1&&t.options.show.when.event==t.options.hide.when.event&&t.options.hide.when.event!=="inactive")||t.options.hide.when.event=="unfocus"){t.cache.toggle=0;v.bind(t.options.show.when.event+".qtip",function(z){if(t.cache.toggle==0){x(z)}else{w(z)}})}else{v.bind(t.options.show.when.event+".qtip",x);if(t.options.hide.when.event!=="inactive"){u.bind(t.options.hide.when.event+".qtip",w)}}if(t.options.position.type.search(/(fixed|absolute)/)!==-1){t.elements.tooltip.bind("mouseover.qtip",t.focus)}if(t.options.position.target==="mouse"&&t.options.position.type!=="static"){v.bind("mousemove.qtip",function(z){t.cache.mouse={x:z.pageX,y:z.pageY};if(t.status.disabled===false&&t.options.position.adjust.mouse===true&&t.options.position.type!=="static"&&t.elements.tooltip.css("display")!=="none"){t.updatePosition(z)}})}}function o(u,v,A){var z,s,x,y,t,w;z=this;if(A.corner=="center"){return v.position}s=f.extend({},u);y={x:false,y:false};t={left:(s.left<f.fn.qtip.cache.screen.scroll.left),right:(s.left+A.dimensions.width+2>=f.fn.qtip.cache.screen.width+f.fn.qtip.cache.screen.scroll.left),top:(s.top<f.fn.qtip.cache.screen.scroll.top),bottom:(s.top+A.dimensions.height+2>=f.fn.qtip.cache.screen.height+f.fn.qtip.cache.screen.scroll.top)};x={left:(t.left&&(A.corner.search(/right/i)!=-1||(A.corner.search(/right/i)==-1&&!t.right))),right:(t.right&&(A.corner.search(/left/i)!=-1||(A.corner.search(/left/i)==-1&&!t.left))),top:(t.top&&A.corner.search(/top/i)==-1),bottom:(t.bottom&&A.corner.search(/bottom/i)==-1)};if(x.left){if(z.options.position.target!=="mouse"){s.left=v.position.left+v.dimensions.width}else{s.left=z.cache.mouse.x}y.x="Left"}else{if(x.right){if(z.options.position.target!=="mouse"){s.left=v.position.left-A.dimensions.width}else{s.left=z.cache.mouse.x-A.dimensions.width}y.x="Right"}}if(x.top){if(z.options.position.target!=="mouse"){s.top=v.position.top+v.dimensions.height}else{s.top=z.cache.mouse.y}y.y="top"}else{if(x.bottom){if(z.options.position.target!=="mouse"){s.top=v.position.top-A.dimensions.height}else{s.top=z.cache.mouse.y-A.dimensions.height}y.y="bottom"}}if(s.left<0){s.left=u.left;y.x=false}if(s.top<0){s.top=u.top;y.y=false}if(z.options.style.tip.corner!==false){s.corner=new String(A.corner);if(y.x!==false){s.corner=s.corner.replace(/Left|Right|Middle/,y.x)}if(y.y!==false){s.corner=s.corner.replace(/top|bottom/,y.y)}if(s.corner!==z.elements.tip.attr("rel")){e.call(z,s.corner)}}return s}function q(u,t){var v,s;v=f.extend(true,{},u);for(s in v){if(t===true&&s.search(/(tip|classes)/i)!==-1){delete v[s]}else{if(!t&&s.search(/(width|border|tip|title|classes|user)/i)!==-1){delete v[s]}}}return v}function c(s){if(typeof s.tip!=="object"){s.tip={corner:s.tip}}if(typeof s.tip.size!=="object"){s.tip.size={width:s.tip.size,height:s.tip.size}}if(typeof s.border!=="object"){s.border={width:s.border}}if(typeof s.width!=="object"){s.width={value:s.width}}if(typeof s.width.max=="string"){s.width.max=parseInt(s.width.max.replace(/([0-9]+)/i,"$1"))}if(typeof s.width.min=="string"){s.width.min=parseInt(s.width.min.replace(/([0-9]+)/i,"$1"))}if(typeof s.tip.size.x=="number"){s.tip.size.width=s.tip.size.x;delete s.tip.size.x}if(typeof s.tip.size.y=="number"){s.tip.size.height=s.tip.size.y;delete s.tip.size.y}return s}function a(){var s,t,u,x,v,w;s=this;u=[true,{}];for(t=0;t<arguments.length;t++){u.push(arguments[t])}x=[f.extend.apply(f,u)];while(typeof x[0].name=="string"){x.unshift(c(f.fn.qtip.styles[x[0].name]))}x.unshift(true,{classes:{tooltip:"qtip-"+(arguments[0].name||"defaults")}},f.fn.qtip.styles.defaults);v=f.extend.apply(f,x);w=(f.browser.msie)?1:0;v.tip.size.width+=w;v.tip.size.height+=w;if(v.tip.size.width%2>0){v.tip.size.width+=1}if(v.tip.size.height%2>0){v.tip.size.height+=1}if(v.tip.corner===true){v.tip.corner=(s.options.position.corner.tooltip==="center")?false:s.options.position.corner.tooltip}return v}function b(v,u,t){var s={bottomRight:[[0,0],[u,t],[u,0]],bottomLeft:[[0,0],[u,0],[0,t]],topRight:[[0,t],[u,0],[u,t]],topLeft:[[0,0],[0,t],[u,t]],topMiddle:[[0,t],[u/2,0],[u,t]],bottomMiddle:[[0,0],[u,0],[u/2,t]],rightMiddle:[[0,0],[u,t/2],[0,t]],leftMiddle:[[u,0],[u,t],[0,t/2]]};s.leftTop=s.bottomRight;s.rightTop=s.bottomLeft;s.leftBottom=s.topRight;s.rightBottom=s.topLeft;return s[v]}function g(s){var t;if(f("<canvas>").get(0).getContext){t={topLeft:[s,s],topRight:[0,s],bottomLeft:[s,0],bottomRight:[0,0]}}else{if(f.browser.msie){t={topLeft:[-90,90,0],topRight:[-90,90,-s],bottomLeft:[90,270,0],bottomRight:[90,270,-s]}}}return t}function k(){var s,t,u;s=this;u=s.getDimensions();t='<iframe class="qtip-bgiframe" frameborder="0" tabindex="-1" src="javascript:false" style="display:block; position:absolute; z-index:-1; filter:alpha(opacity=\'0\'); border: 1px solid red; height:'+u.height+"px; width:"+u.width+'px" />';s.elements.bgiframe=s.elements.wrapper.prepend(t).children(".qtip-bgiframe:first")}f(document).ready(function(){f.fn.qtip.cache={screen:{scroll:{left:f(window).scrollLeft(),top:f(window).scrollTop()},width:f(window).width(),height:f(window).height()}};var s;f(window).bind("resize scroll",function(t){clearTimeout(s);s=setTimeout(function(){if(t.type==="scroll"){f.fn.qtip.cache.screen.scroll={left:f(window).scrollLeft(),top:f(window).scrollTop()}}else{f.fn.qtip.cache.screen.width=f(window).width();f.fn.qtip.cache.screen.height=f(window).height()}for(i=0;i<f.fn.qtip.interfaces.length;i++){var u=f.fn.qtip.interfaces[i];if(u.status.rendered===true&&(u.options.position.type!=="static"||u.options.position.adjust.scroll&&t.type==="scroll"||u.options.position.adjust.resize&&t.type==="resize")){u.updatePosition(t,true)}}},100)});f(document).bind("mousedown.qtip",function(t){if(f(t.target).parents("div.qtip").length===0){f(".qtip[unfocus]").each(function(){var u=f(this).qtip("api");if(f(this).is(":visible")&&!u.status.disabled&&f(t.target).add(u.elements.target).length>1){u.hide(t)}})}})});f.fn.qtip.interfaces=[];f.fn.qtip.log={error:function(){return this}};f.fn.qtip.constants={};f.fn.qtip.defaults={content:{prerender:false,text:false,url:false,data:null,title:{text:false,button:false}},position:{target:false,corner:{target:"bottomRight",tooltip:"topLeft"},adjust:{x:0,y:0,mouse:true,screen:false,scroll:true,resize:true},type:"absolute",container:false},show:{when:{target:false,event:"mouseover"},effect:{type:"fade",length:100},delay:140,solo:false,ready:false},hide:{when:{target:false,event:"mouseout"},effect:{type:"fade",length:100},delay:0,fixed:false},api:{beforeRender:function(){},onRender:function(){},beforePositionUpdate:function(){},onPositionUpdate:function(){},beforeShow:function(){},onShow:function(){},beforeHide:function(){},onHide:function(){},beforeContentUpdate:function(){},onContentUpdate:function(){},beforeContentLoad:function(){},onContentLoad:function(){},beforeTitleUpdate:function(){},onTitleUpdate:function(){},beforeDestroy:function(){},onDestroy:function(){},beforeFocus:function(){},onFocus:function(){}}};f.fn.qtip.styles={defaults:{background:"white",color:"#111",overflow:"hidden",textAlign:"left",width:{min:0,max:250},padding:"5px 9px",border:{width:1,radius:0,color:"#d3d3d3"},tip:{corner:false,color:false,size:{width:13,height:13},opacity:1},title:{background:"#e1e1e1",fontWeight:"bold",padding:"7px 12px"},button:{cursor:"pointer"},classes:{target:"",tip:"qtip-tip",title:"qtip-title",button:"qtip-button",content:"qtip-content",active:"qtip-active"}},cream:{border:{width:3,radius:0,color:"#F9E98E"},title:{background:"#F0DE7D",color:"#A27D35"},background:"#FBF7AA",color:"#A27D35",classes:{tooltip:"qtip-cream"}},light:{border:{width:3,radius:0,color:"#E2E2E2"},title:{background:"#f1f1f1",color:"#454545"},background:"white",color:"#454545",classes:{tooltip:"qtip-light"}},dark:{border:{width:3,radius:0,color:"#303030"},title:{background:"#404040",color:"#f3f3f3"},background:"#505050",color:"#f3f3f3",classes:{tooltip:"qtip-dark"}},red:{border:{width:3,radius:0,color:"#CE6F6F"},title:{background:"#f28279",color:"#9C2F2F"},background:"#F79992",color:"#9C2F2F",classes:{tooltip:"qtip-red"}},green:{border:{width:3,radius:0,color:"#A9DB66"},title:{background:"#b9db8c",color:"#58792E"},background:"#CDE6AC",color:"#58792E",classes:{tooltip:"qtip-green"}},blue:{border:{width:3,radius:0,color:"#ADD9ED"},title:{background:"#D0E9F5",color:"#5E99BD"},background:"#E5F6FE",color:"#4D9FBF",classes:{tooltip:"qtip-blue"}}}})(jQuery);
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/jquery.qtip.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/jquery.qtip.js
new file mode 100755
index 0000000000000000000000000000000000000000..173885b75f5e586e418bb02127d8dd68f4029060
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/qtip/jquery.qtip.js
@@ -0,0 +1,15 @@
+/*
+ * jquery.qtip. The jQuery tooltip plugin
+ *
+ * Copyright (c) 2009 Craig Thompson
+ * http://craigsworks.com
+ *
+ * Licensed under MIT
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * Launch  : February 2009
+ * Version : 1.0.0-rc3
+ * Released: Tuesday 12th May, 2009 - 00:00
+ * Debug: jquery.qtip.debug.js
+ */
+(function(f){f.fn.qtip=function(B,u){var y,t,A,s,x,w,v,z;if(typeof B=="string"){if(typeof f(this).data("qtip")!=="object"){f.fn.qtip.log.error.call(self,1,f.fn.qtip.constants.NO_TOOLTIP_PRESENT,false)}if(B=="api"){return f(this).data("qtip").interfaces[f(this).data("qtip").current]}else{if(B=="interfaces"){return f(this).data("qtip").interfaces}}}else{if(!B){B={}}if(typeof B.content!=="object"||(B.content.jquery&&B.content.length>0)){B.content={text:B.content}}if(typeof B.content.title!=="object"){B.content.title={text:B.content.title}}if(typeof B.position!=="object"){B.position={corner:B.position}}if(typeof B.position.corner!=="object"){B.position.corner={target:B.position.corner,tooltip:B.position.corner}}if(typeof B.show!=="object"){B.show={when:B.show}}if(typeof B.show.when!=="object"){B.show.when={event:B.show.when}}if(typeof B.show.effect!=="object"){B.show.effect={type:B.show.effect}}if(typeof B.hide!=="object"){B.hide={when:B.hide}}if(typeof B.hide.when!=="object"){B.hide.when={event:B.hide.when}}if(typeof B.hide.effect!=="object"){B.hide.effect={type:B.hide.effect}}if(typeof B.style!=="object"){B.style={name:B.style}}B.style=c(B.style);s=f.extend(true,{},f.fn.qtip.defaults,B);s.style=a.call({options:s},s.style);s.user=f.extend(true,{},B)}return f(this).each(function(){if(typeof B=="string"){w=B.toLowerCase();A=f(this).qtip("interfaces");if(typeof A=="object"){if(u===true&&w=="destroy"){while(A.length>0){A[A.length-1].destroy()}}else{if(u!==true){A=[f(this).qtip("api")]}for(y=0;y<A.length;y++){if(w=="destroy"){A[y].destroy()}else{if(A[y].status.rendered===true){if(w=="show"){A[y].show()}else{if(w=="hide"){A[y].hide()}else{if(w=="focus"){A[y].focus()}else{if(w=="disable"){A[y].disable(true)}else{if(w=="enable"){A[y].disable(false)}}}}}}}}}}}else{v=f.extend(true,{},s);v.hide.effect.length=s.hide.effect.length;v.show.effect.length=s.show.effect.length;if(v.position.container===false){v.position.container=f(document.body)}if(v.position.target===false){v.position.target=f(this)}if(v.show.when.target===false){v.show.when.target=f(this)}if(v.hide.when.target===false){v.hide.when.target=f(this)}t=f.fn.qtip.interfaces.length;for(y=0;y<t;y++){if(typeof f.fn.qtip.interfaces[y]=="undefined"){t=y;break}}x=new d(f(this),v,t);f.fn.qtip.interfaces[t]=x;if(typeof f(this).data("qtip")=="object"){if(typeof f(this).attr("qtip")==="undefined"){f(this).data("qtip").current=f(this).data("qtip").interfaces.length}f(this).data("qtip").interfaces.push(x)}else{f(this).data("qtip",{current:0,interfaces:[x]})}if(v.content.prerender===false&&v.show.when.event!==false&&v.show.ready!==true){v.show.when.target.bind(v.show.when.event+".qtip-"+t+"-create",{qtip:t},function(C){z=f.fn.qtip.interfaces[C.data.qtip];z.options.show.when.target.unbind(z.options.show.when.event+".qtip-"+C.data.qtip+"-create");z.cache.mouse={x:C.pageX,y:C.pageY};p.call(z);z.options.show.when.target.trigger(z.options.show.when.event)})}else{x.cache.mouse={x:v.show.when.target.offset().left,y:v.show.when.target.offset().top};p.call(x)}}})};function d(u,t,v){var s=this;s.id=v;s.options=t;s.status={animated:false,rendered:false,disabled:false,focused:false};s.elements={target:u.addClass(s.options.style.classes.target),tooltip:null,wrapper:null,content:null,contentWrapper:null,title:null,button:null,tip:null,bgiframe:null};s.cache={mouse:{},position:{},toggle:0};s.timers={};f.extend(s,s.options.api,{show:function(y){var x,z;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"show")}if(s.elements.tooltip.css("display")!=="none"){return s}s.elements.tooltip.stop(true,false);x=s.beforeShow.call(s,y);if(x===false){return s}function w(){if(s.options.position.type!=="static"){s.focus()}s.onShow.call(s,y);if(f.browser.msie){s.elements.tooltip.get(0).style.removeAttribute("filter")}}s.cache.toggle=1;if(s.options.position.type!=="static"){s.updatePosition(y,(s.options.show.effect.length>0))}if(typeof s.options.show.solo=="object"){z=f(s.options.show.solo)}else{if(s.options.show.solo===true){z=f("div.qtip").not(s.elements.tooltip)}}if(z){z.each(function(){if(f(this).qtip("api").status.rendered===true){f(this).qtip("api").hide()}})}if(typeof s.options.show.effect.type=="function"){s.options.show.effect.type.call(s.elements.tooltip,s.options.show.effect.length);s.elements.tooltip.queue(function(){w();f(this).dequeue()})}else{switch(s.options.show.effect.type.toLowerCase()){case"fade":s.elements.tooltip.fadeIn(s.options.show.effect.length,w);break;case"slide":s.elements.tooltip.slideDown(s.options.show.effect.length,function(){w();if(s.options.position.type!=="static"){s.updatePosition(y,true)}});break;case"grow":s.elements.tooltip.show(s.options.show.effect.length,w);break;default:s.elements.tooltip.show(null,w);break}s.elements.tooltip.addClass(s.options.style.classes.active)}return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_SHOWN,"show")},hide:function(y){var x;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"hide")}else{if(s.elements.tooltip.css("display")==="none"){return s}}clearTimeout(s.timers.show);s.elements.tooltip.stop(true,false);x=s.beforeHide.call(s,y);if(x===false){return s}function w(){s.onHide.call(s,y)}s.cache.toggle=0;if(typeof s.options.hide.effect.type=="function"){s.options.hide.effect.type.call(s.elements.tooltip,s.options.hide.effect.length);s.elements.tooltip.queue(function(){w();f(this).dequeue()})}else{switch(s.options.hide.effect.type.toLowerCase()){case"fade":s.elements.tooltip.fadeOut(s.options.hide.effect.length,w);break;case"slide":s.elements.tooltip.slideUp(s.options.hide.effect.length,w);break;case"grow":s.elements.tooltip.hide(s.options.hide.effect.length,w);break;default:s.elements.tooltip.hide(null,w);break}s.elements.tooltip.removeClass(s.options.style.classes.active)}return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_HIDDEN,"hide")},updatePosition:function(w,x){var C,G,L,J,H,E,y,I,B,D,K,A,F,z;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"updatePosition")}else{if(s.options.position.type=="static"){return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.CANNOT_POSITION_STATIC,"updatePosition")}}G={position:{left:0,top:0},dimensions:{height:0,width:0},corner:s.options.position.corner.target};L={position:s.getPosition(),dimensions:s.getDimensions(),corner:s.options.position.corner.tooltip};if(s.options.position.target!=="mouse"){if(s.options.position.target.get(0).nodeName.toLowerCase()=="area"){J=s.options.position.target.attr("coords").split(",");for(C=0;C<J.length;C++){J[C]=parseInt(J[C])}H=s.options.position.target.parent("map").attr("name");E=f('img[usemap="#'+H+'"]:first').offset();G.position={left:Math.floor(E.left+J[0]),top:Math.floor(E.top+J[1])};switch(s.options.position.target.attr("shape").toLowerCase()){case"rect":G.dimensions={width:Math.ceil(Math.abs(J[2]-J[0])),height:Math.ceil(Math.abs(J[3]-J[1]))};break;case"circle":G.dimensions={width:J[2]+1,height:J[2]+1};break;case"poly":G.dimensions={width:J[0],height:J[1]};for(C=0;C<J.length;C++){if(C%2==0){if(J[C]>G.dimensions.width){G.dimensions.width=J[C]}if(J[C]<J[0]){G.position.left=Math.floor(E.left+J[C])}}else{if(J[C]>G.dimensions.height){G.dimensions.height=J[C]}if(J[C]<J[1]){G.position.top=Math.floor(E.top+J[C])}}}G.dimensions.width=G.dimensions.width-(G.position.left-E.left);G.dimensions.height=G.dimensions.height-(G.position.top-E.top);break;default:return f.fn.qtip.log.error.call(s,4,f.fn.qtip.constants.INVALID_AREA_SHAPE,"updatePosition");break}G.dimensions.width-=2;G.dimensions.height-=2}else{if(s.options.position.target.add(document.body).length===1){G.position={left:f(document).scrollLeft(),top:f(document).scrollTop()};G.dimensions={height:f(window).height(),width:f(window).width()}}else{if(typeof s.options.position.target.attr("qtip")!=="undefined"){G.position=s.options.position.target.qtip("api").cache.position}else{G.position=s.options.position.target.offset()}G.dimensions={height:s.options.position.target.outerHeight(),width:s.options.position.target.outerWidth()}}}y=f.extend({},G.position);if(G.corner.search(/right/i)!==-1){y.left+=G.dimensions.width}if(G.corner.search(/bottom/i)!==-1){y.top+=G.dimensions.height}if(G.corner.search(/((top|bottom)Middle)|center/)!==-1){y.left+=(G.dimensions.width/2)}if(G.corner.search(/((left|right)Middle)|center/)!==-1){y.top+=(G.dimensions.height/2)}}else{G.position=y={left:s.cache.mouse.x,top:s.cache.mouse.y};G.dimensions={height:1,width:1}}if(L.corner.search(/right/i)!==-1){y.left-=L.dimensions.width}if(L.corner.search(/bottom/i)!==-1){y.top-=L.dimensions.height}if(L.corner.search(/((top|bottom)Middle)|center/)!==-1){y.left-=(L.dimensions.width/2)}if(L.corner.search(/((left|right)Middle)|center/)!==-1){y.top-=(L.dimensions.height/2)}I=(f.browser.msie)?1:0;B=(f.browser.msie&&parseInt(f.browser.version.charAt(0))===6)?1:0;if(s.options.style.border.radius>0){if(L.corner.search(/Left/)!==-1){y.left-=s.options.style.border.radius}else{if(L.corner.search(/Right/)!==-1){y.left+=s.options.style.border.radius}}if(L.corner.search(/Top/)!==-1){y.top-=s.options.style.border.radius}else{if(L.corner.search(/Bottom/)!==-1){y.top+=s.options.style.border.radius}}}if(I){if(L.corner.search(/top/)!==-1){y.top-=I}else{if(L.corner.search(/bottom/)!==-1){y.top+=I}}if(L.corner.search(/left/)!==-1){y.left-=I}else{if(L.corner.search(/right/)!==-1){y.left+=I}}if(L.corner.search(/leftMiddle|rightMiddle/)!==-1){y.top-=1}}if(s.options.position.adjust.screen===true){y=o.call(s,y,G,L)}if(s.options.position.target==="mouse"&&s.options.position.adjust.mouse===true){if(s.options.position.adjust.screen===true&&s.elements.tip){K=s.elements.tip.attr("rel")}else{K=s.options.position.corner.tooltip}y.left+=(K.search(/right/i)!==-1)?-6:6;y.top+=(K.search(/bottom/i)!==-1)?-6:6}if(!s.elements.bgiframe&&f.browser.msie&&parseInt(f.browser.version.charAt(0))==6){f("select, object").each(function(){A=f(this).offset();A.bottom=A.top+f(this).height();A.right=A.left+f(this).width();if(y.top+L.dimensions.height>=A.top&&y.left+L.dimensions.width>=A.left){k.call(s)}})}y.left+=s.options.position.adjust.x;y.top+=s.options.position.adjust.y;F=s.getPosition();if(y.left!=F.left||y.top!=F.top){z=s.beforePositionUpdate.call(s,w);if(z===false){return s}s.cache.position=y;if(x===true){s.status.animated=true;s.elements.tooltip.animate(y,200,"swing",function(){s.status.animated=false})}else{s.elements.tooltip.css(y)}s.onPositionUpdate.call(s,w);if(typeof w!=="undefined"&&w.type&&w.type!=="mousemove"){f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_POSITION_UPDATED,"updatePosition")}}return s},updateWidth:function(w){var x;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"updateWidth")}else{if(w&&typeof w!=="number"){return f.fn.qtip.log.error.call(s,2,"newWidth must be of type number","updateWidth")}}x=s.elements.contentWrapper.siblings().add(s.elements.tip).add(s.elements.button);if(!w){if(typeof s.options.style.width.value=="number"){w=s.options.style.width.value}else{s.elements.tooltip.css({width:"auto"});x.hide();if(f.browser.msie){s.elements.wrapper.add(s.elements.contentWrapper.children()).css({zoom:"normal"})}w=s.getDimensions().width+1;if(!s.options.style.width.value){if(w>s.options.style.width.max){w=s.options.style.width.max}if(w<s.options.style.width.min){w=s.options.style.width.min}}}}if(w%2!==0){w-=1}s.elements.tooltip.width(w);x.show();if(s.options.style.border.radius){s.elements.tooltip.find(".qtip-betweenCorners").each(function(y){f(this).width(w-(s.options.style.border.radius*2))})}if(f.browser.msie){s.elements.wrapper.add(s.elements.contentWrapper.children()).css({zoom:"1"});s.elements.wrapper.width(w);if(s.elements.bgiframe){s.elements.bgiframe.width(w).height(s.getDimensions.height)}}return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_WIDTH_UPDATED,"updateWidth")},updateStyle:function(w){var z,A,x,y,B;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"updateStyle")}else{if(typeof w!=="string"||!f.fn.qtip.styles[w]){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.STYLE_NOT_DEFINED,"updateStyle")}}s.options.style=a.call(s,f.fn.qtip.styles[w],s.options.user.style);s.elements.content.css(q(s.options.style));if(s.options.content.title.text!==false){s.elements.title.css(q(s.options.style.title,true))}s.elements.contentWrapper.css({borderColor:s.options.style.border.color});if(s.options.style.tip.corner!==false){if(f("<canvas>").get(0).getContext){z=s.elements.tooltip.find(".qtip-tip canvas:first");x=z.get(0).getContext("2d");x.clearRect(0,0,300,300);y=z.parent("div[rel]:first").attr("rel");B=b(y,s.options.style.tip.size.width,s.options.style.tip.size.height);h.call(s,z,B,s.options.style.tip.color||s.options.style.border.color)}else{if(f.browser.msie){z=s.elements.tooltip.find('.qtip-tip [nodeName="shape"]');z.attr("fillcolor",s.options.style.tip.color||s.options.style.border.color)}}}if(s.options.style.border.radius>0){s.elements.tooltip.find(".qtip-betweenCorners").css({backgroundColor:s.options.style.border.color});if(f("<canvas>").get(0).getContext){A=g(s.options.style.border.radius);s.elements.tooltip.find(".qtip-wrapper canvas").each(function(){x=f(this).get(0).getContext("2d");x.clearRect(0,0,300,300);y=f(this).parent("div[rel]:first").attr("rel");r.call(s,f(this),A[y],s.options.style.border.radius,s.options.style.border.color)})}else{if(f.browser.msie){s.elements.tooltip.find('.qtip-wrapper [nodeName="arc"]').each(function(){f(this).attr("fillcolor",s.options.style.border.color)})}}}return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_STYLE_UPDATED,"updateStyle")},updateContent:function(A,y){var z,x,w;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"updateContent")}else{if(!A){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.NO_CONTENT_PROVIDED,"updateContent")}}z=s.beforeContentUpdate.call(s,A);if(typeof z=="string"){A=z}else{if(z===false){return}}if(f.browser.msie){s.elements.contentWrapper.children().css({zoom:"normal"})}if(A.jquery&&A.length>0){A.clone(true).appendTo(s.elements.content).show()}else{s.elements.content.html(A)}x=s.elements.content.find("img[complete=false]");if(x.length>0){w=0;x.each(function(C){f('<img src="'+f(this).attr("src")+'" />').load(function(){if(++w==x.length){B()}})})}else{B()}function B(){s.updateWidth();if(y!==false){if(s.options.position.type!=="static"){s.updatePosition(s.elements.tooltip.is(":visible"),true)}if(s.options.style.tip.corner!==false){n.call(s)}}}s.onContentUpdate.call(s);return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_CONTENT_UPDATED,"loadContent")},loadContent:function(w,z,A){var y;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"loadContent")}y=s.beforeContentLoad.call(s);if(y===false){return s}if(A=="post"){f.post(w,z,x)}else{f.get(w,z,x)}function x(B){s.onContentLoad.call(s);f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_CONTENT_LOADED,"loadContent");s.updateContent(B)}return s},updateTitle:function(w){if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"updateTitle")}else{if(!w){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.NO_CONTENT_PROVIDED,"updateTitle")}}returned=s.beforeTitleUpdate.call(s);if(returned===false){return s}if(s.elements.button){s.elements.button=s.elements.button.clone(true)}s.elements.title.html(w);if(s.elements.button){s.elements.title.prepend(s.elements.button)}s.onTitleUpdate.call(s);return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_TITLE_UPDATED,"updateTitle")},focus:function(A){var y,x,w,z;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"focus")}else{if(s.options.position.type=="static"){return f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.CANNOT_FOCUS_STATIC,"focus")}}y=parseInt(s.elements.tooltip.css("z-index"));x=6000+f("div.qtip[qtip]").length-1;if(!s.status.focused&&y!==x){z=s.beforeFocus.call(s,A);if(z===false){return s}f("div.qtip[qtip]").not(s.elements.tooltip).each(function(){if(f(this).qtip("api").status.rendered===true){w=parseInt(f(this).css("z-index"));if(typeof w=="number"&&w>-1){f(this).css({zIndex:parseInt(f(this).css("z-index"))-1})}f(this).qtip("api").status.focused=false}});s.elements.tooltip.css({zIndex:x});s.status.focused=true;s.onFocus.call(s,A);f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_FOCUSED,"focus")}return s},disable:function(w){if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"disable")}if(w){if(!s.status.disabled){s.status.disabled=true;f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_DISABLED,"disable")}else{f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.TOOLTIP_ALREADY_DISABLED,"disable")}}else{if(s.status.disabled){s.status.disabled=false;f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_ENABLED,"disable")}else{f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.TOOLTIP_ALREADY_ENABLED,"disable")}}return s},destroy:function(){var w,x,y;x=s.beforeDestroy.call(s);if(x===false){return s}if(s.status.rendered){s.options.show.when.target.unbind("mousemove.qtip",s.updatePosition);s.options.show.when.target.unbind("mouseout.qtip",s.hide);s.options.show.when.target.unbind(s.options.show.when.event+".qtip");s.options.hide.when.target.unbind(s.options.hide.when.event+".qtip");s.elements.tooltip.unbind(s.options.hide.when.event+".qtip");s.elements.tooltip.unbind("mouseover.qtip",s.focus);s.elements.tooltip.remove()}else{s.options.show.when.target.unbind(s.options.show.when.event+".qtip-create")}if(typeof s.elements.target.data("qtip")=="object"){y=s.elements.target.data("qtip").interfaces;if(typeof y=="object"&&y.length>0){for(w=0;w<y.length-1;w++){if(y[w].id==s.id){y.splice(w,1)}}}}delete f.fn.qtip.interfaces[s.id];if(typeof y=="object"&&y.length>0){s.elements.target.data("qtip").current=y.length-1}else{s.elements.target.removeData("qtip")}s.onDestroy.call(s);f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_DESTROYED,"destroy");return s.elements.target},getPosition:function(){var w,x;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"getPosition")}w=(s.elements.tooltip.css("display")!=="none")?false:true;if(w){s.elements.tooltip.css({visiblity:"hidden"}).show()}x=s.elements.tooltip.offset();if(w){s.elements.tooltip.css({visiblity:"visible"}).hide()}return x},getDimensions:function(){var w,x;if(!s.status.rendered){return f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.TOOLTIP_NOT_RENDERED,"getDimensions")}w=(!s.elements.tooltip.is(":visible"))?true:false;if(w){s.elements.tooltip.css({visiblity:"hidden"}).show()}x={height:s.elements.tooltip.outerHeight(),width:s.elements.tooltip.outerWidth()};if(w){s.elements.tooltip.css({visiblity:"visible"}).hide()}return x}})}function p(){var s,w,u,t,v,y,x;s=this;s.beforeRender.call(s);s.status.rendered=true;s.elements.tooltip='<div qtip="'+s.id+'" class="qtip '+(s.options.style.classes.tooltip||s.options.style)+'"style="display:none; -moz-border-radius:0; -webkit-border-radius:0; border-radius:0;position:'+s.options.position.type+';">  <div class="qtip-wrapper" style="position:relative; overflow:hidden; text-align:left;">    <div class="qtip-contentWrapper" style="overflow:hidden;">       <div class="qtip-content '+s.options.style.classes.content+'"></div></div></div></div>';s.elements.tooltip=f(s.elements.tooltip);s.elements.tooltip.appendTo(s.options.position.container);s.elements.tooltip.data("qtip",{current:0,interfaces:[s]});s.elements.wrapper=s.elements.tooltip.children("div:first");s.elements.contentWrapper=s.elements.wrapper.children("div:first").css({background:s.options.style.background});s.elements.content=s.elements.contentWrapper.children("div:first").css(q(s.options.style));if(f.browser.msie){s.elements.wrapper.add(s.elements.content).css({zoom:1})}if(s.options.hide.when.event=="unfocus"){s.elements.tooltip.attr("unfocus",true)}if(typeof s.options.style.width.value=="number"){s.updateWidth()}if(f("<canvas>").get(0).getContext||f.browser.msie){if(s.options.style.border.radius>0){m.call(s)}else{s.elements.contentWrapper.css({border:s.options.style.border.width+"px solid "+s.options.style.border.color})}if(s.options.style.tip.corner!==false){e.call(s)}}else{s.elements.contentWrapper.css({border:s.options.style.border.width+"px solid "+s.options.style.border.color});s.options.style.border.radius=0;s.options.style.tip.corner=false;f.fn.qtip.log.error.call(s,2,f.fn.qtip.constants.CANVAS_VML_NOT_SUPPORTED,"render")}if((typeof s.options.content.text=="string"&&s.options.content.text.length>0)||(s.options.content.text.jquery&&s.options.content.text.length>0)){u=s.options.content.text}else{if(typeof s.elements.target.attr("title")=="string"&&s.elements.target.attr("title").length>0){u=s.elements.target.attr("title").replace("\\n","<br />");s.elements.target.attr("title","")}else{if(typeof s.elements.target.attr("alt")=="string"&&s.elements.target.attr("alt").length>0){u=s.elements.target.attr("alt").replace("\\n","<br />");s.elements.target.attr("alt","")}else{u=" ";f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.NO_VALID_CONTENT,"render")}}}if(s.options.content.title.text!==false){j.call(s)}s.updateContent(u);l.call(s);if(s.options.show.ready===true){s.show()}if(s.options.content.url!==false){t=s.options.content.url;v=s.options.content.data;y=s.options.content.method||"get";s.loadContent(t,v,y)}s.onRender.call(s);f.fn.qtip.log.error.call(s,1,f.fn.qtip.constants.EVENT_RENDERED,"render")}function m(){var F,z,t,B,x,E,u,G,D,y,w,C,A,s,v;F=this;F.elements.wrapper.find(".qtip-borderBottom, .qtip-borderTop").remove();t=F.options.style.border.width;B=F.options.style.border.radius;x=F.options.style.border.color||F.options.style.tip.color;E=g(B);u={};for(z in E){u[z]='<div rel="'+z+'" style="'+((z.search(/Left/)!==-1)?"left":"right")+":0; position:absolute; height:"+B+"px; width:"+B+'px; overflow:hidden; line-height:0.1px; font-size:1px">';if(f("<canvas>").get(0).getContext){u[z]+='<canvas height="'+B+'" width="'+B+'" style="vertical-align: top"></canvas>'}else{if(f.browser.msie){G=B*2+3;u[z]+='<v:arc stroked="false" fillcolor="'+x+'" startangle="'+E[z][0]+'" endangle="'+E[z][1]+'" style="width:'+G+"px; height:"+G+"px; margin-top:"+((z.search(/bottom/)!==-1)?-2:-1)+"px; margin-left:"+((z.search(/Right/)!==-1)?E[z][2]-3.5:-1)+'px; vertical-align:top; display:inline-block; behavior:url(#default#VML)"></v:arc>'}}u[z]+="</div>"}D=F.getDimensions().width-(Math.max(t,B)*2);y='<div class="qtip-betweenCorners" style="height:'+B+"px; width:"+D+"px; overflow:hidden; background-color:"+x+'; line-height:0.1px; font-size:1px;">';w='<div class="qtip-borderTop" dir="ltr" style="height:'+B+"px; margin-left:"+B+'px; line-height:0.1px; font-size:1px; padding:0;">'+u.topLeft+u.topRight+y;F.elements.wrapper.prepend(w);C='<div class="qtip-borderBottom" dir="ltr" style="height:'+B+"px; margin-left:"+B+'px; line-height:0.1px; font-size:1px; padding:0;">'+u.bottomLeft+u.bottomRight+y;F.elements.wrapper.append(C);if(f("<canvas>").get(0).getContext){F.elements.wrapper.find("canvas").each(function(){A=E[f(this).parent("[rel]:first").attr("rel")];r.call(F,f(this),A,B,x)})}else{if(f.browser.msie){F.elements.tooltip.append('<v:image style="behavior:url(#default#VML);"></v:image>')}}s=Math.max(B,(B+(t-B)));v=Math.max(t-B,0);F.elements.contentWrapper.css({border:"0px solid "+x,borderWidth:v+"px "+s+"px"})}function r(u,w,s,t){var v=u.get(0).getContext("2d");v.fillStyle=t;v.beginPath();v.arc(w[0],w[1],s,0,Math.PI*2,false);v.fill()}function e(v){var t,s,x,u,w;t=this;if(t.elements.tip!==null){t.elements.tip.remove()}s=t.options.style.tip.color||t.options.style.border.color;if(t.options.style.tip.corner===false){return}else{if(!v){v=t.options.style.tip.corner}}x=b(v,t.options.style.tip.size.width,t.options.style.tip.size.height);t.elements.tip='<div class="'+t.options.style.classes.tip+'" dir="ltr" rel="'+v+'" style="position:absolute; height:'+t.options.style.tip.size.height+"px; width:"+t.options.style.tip.size.width+'px; margin:0 auto; line-height:0.1px; font-size:1px;">';if(f("<canvas>").get(0).getContext){t.elements.tip+='<canvas height="'+t.options.style.tip.size.height+'" width="'+t.options.style.tip.size.width+'"></canvas>'}else{if(f.browser.msie){u=t.options.style.tip.size.width+","+t.options.style.tip.size.height;w="m"+x[0][0]+","+x[0][1];w+=" l"+x[1][0]+","+x[1][1];w+=" "+x[2][0]+","+x[2][1];w+=" xe";t.elements.tip+='<v:shape fillcolor="'+s+'" stroked="false" filled="true" path="'+w+'" coordsize="'+u+'" style="width:'+t.options.style.tip.size.width+"px; height:"+t.options.style.tip.size.height+"px; line-height:0.1px; display:inline-block; behavior:url(#default#VML); vertical-align:"+((v.search(/top/)!==-1)?"bottom":"top")+'"></v:shape>';t.elements.tip+='<v:image style="behavior:url(#default#VML);"></v:image>';t.elements.contentWrapper.css("position","relative")}}t.elements.tooltip.prepend(t.elements.tip+"</div>");t.elements.tip=t.elements.tooltip.find("."+t.options.style.classes.tip).eq(0);if(f("<canvas>").get(0).getContext){h.call(t,t.elements.tip.find("canvas:first"),x,s)}if(v.search(/top/)!==-1&&f.browser.msie&&parseInt(f.browser.version.charAt(0))===6){t.elements.tip.css({marginTop:-4})}n.call(t,v)}function h(t,v,s){var u=t.get(0).getContext("2d");u.fillStyle=s;u.beginPath();u.moveTo(v[0][0],v[0][1]);u.lineTo(v[1][0],v[1][1]);u.lineTo(v[2][0],v[2][1]);u.fill()}function n(u){var t,w,s,x,v;t=this;if(t.options.style.tip.corner===false||!t.elements.tip){return}if(!u){u=t.elements.tip.attr("rel")}w=positionAdjust=(f.browser.msie)?1:0;t.elements.tip.css(u.match(/left|right|top|bottom/)[0],0);if(u.search(/top|bottom/)!==-1){if(f.browser.msie){if(parseInt(f.browser.version.charAt(0))===6){positionAdjust=(u.search(/top/)!==-1)?-3:1}else{positionAdjust=(u.search(/top/)!==-1)?1:2}}if(u.search(/Middle/)!==-1){t.elements.tip.css({left:"50%",marginLeft:-(t.options.style.tip.size.width/2)})}else{if(u.search(/Left/)!==-1){t.elements.tip.css({left:t.options.style.border.radius-w})}else{if(u.search(/Right/)!==-1){t.elements.tip.css({right:t.options.style.border.radius+w})}}}if(u.search(/top/)!==-1){t.elements.tip.css({top:-positionAdjust})}else{t.elements.tip.css({bottom:positionAdjust})}}else{if(u.search(/left|right/)!==-1){if(f.browser.msie){positionAdjust=(parseInt(f.browser.version.charAt(0))===6)?1:((u.search(/left/)!==-1)?1:2)}if(u.search(/Middle/)!==-1){t.elements.tip.css({top:"50%",marginTop:-(t.options.style.tip.size.height/2)})}else{if(u.search(/Top/)!==-1){t.elements.tip.css({top:t.options.style.border.radius-w})}else{if(u.search(/Bottom/)!==-1){t.elements.tip.css({bottom:t.options.style.border.radius+w})}}}if(u.search(/left/)!==-1){t.elements.tip.css({left:-positionAdjust})}else{t.elements.tip.css({right:positionAdjust})}}}s="padding-"+u.match(/left|right|top|bottom/)[0];x=t.options.style.tip.size[(s.search(/left|right/)!==-1)?"width":"height"];t.elements.tooltip.css("padding",0);t.elements.tooltip.css(s,x);if(f.browser.msie&&parseInt(f.browser.version.charAt(0))==6){v=parseInt(t.elements.tip.css("margin-top"))||0;v+=parseInt(t.elements.content.css("margin-top"))||0;t.elements.tip.css({marginTop:v})}}function j(){var s=this;if(s.elements.title!==null){s.elements.title.remove()}s.elements.title=f('<div class="'+s.options.style.classes.title+'">').css(q(s.options.style.title,true)).css({zoom:(f.browser.msie)?1:0}).prependTo(s.elements.contentWrapper);if(s.options.content.title.text){s.updateTitle.call(s,s.options.content.title.text)}if(s.options.content.title.button!==false&&typeof s.options.content.title.button=="string"){s.elements.button=f('<a class="'+s.options.style.classes.button+'" style="float:right; position: relative"></a>').css(q(s.options.style.button,true)).html(s.options.content.title.button).prependTo(s.elements.title).click(function(t){if(!s.status.disabled){s.hide(t)}})}}function l(){var t,v,u,s;t=this;v=t.options.show.when.target;u=t.options.hide.when.target;if(t.options.hide.fixed){u=u.add(t.elements.tooltip)}if(t.options.hide.when.event=="inactive"){s=["click","dblclick","mousedown","mouseup","mousemove","mouseout","mouseenter","mouseleave","mouseover"];function y(z){if(t.status.disabled===true){return}clearTimeout(t.timers.inactive);t.timers.inactive=setTimeout(function(){f(s).each(function(){u.unbind(this+".qtip-inactive");t.elements.content.unbind(this+".qtip-inactive")});t.hide(z)},t.options.hide.delay)}}else{if(t.options.hide.fixed===true){t.elements.tooltip.bind("mouseover.qtip",function(){if(t.status.disabled===true){return}clearTimeout(t.timers.hide)})}}function x(z){if(t.status.disabled===true){return}if(t.options.hide.when.event=="inactive"){f(s).each(function(){u.bind(this+".qtip-inactive",y);t.elements.content.bind(this+".qtip-inactive",y)});y()}clearTimeout(t.timers.show);clearTimeout(t.timers.hide);t.timers.show=setTimeout(function(){t.show(z)},t.options.show.delay)}function w(z){if(t.status.disabled===true){return}if(t.options.hide.fixed===true&&t.options.hide.when.event.search(/mouse(out|leave)/i)!==-1&&f(z.relatedTarget).parents("div.qtip[qtip]").length>0){z.stopPropagation();z.preventDefault();clearTimeout(t.timers.hide);return false}clearTimeout(t.timers.show);clearTimeout(t.timers.hide);t.elements.tooltip.stop(true,true);t.timers.hide=setTimeout(function(){t.hide(z)},t.options.hide.delay)}if((t.options.show.when.target.add(t.options.hide.when.target).length===1&&t.options.show.when.event==t.options.hide.when.event&&t.options.hide.when.event!=="inactive")||t.options.hide.when.event=="unfocus"){t.cache.toggle=0;v.bind(t.options.show.when.event+".qtip",function(z){if(t.cache.toggle==0){x(z)}else{w(z)}})}else{v.bind(t.options.show.when.event+".qtip",x);if(t.options.hide.when.event!=="inactive"){u.bind(t.options.hide.when.event+".qtip",w)}}if(t.options.position.type.search(/(fixed|absolute)/)!==-1){t.elements.tooltip.bind("mouseover.qtip",t.focus)}if(t.options.position.target==="mouse"&&t.options.position.type!=="static"){v.bind("mousemove.qtip",function(z){t.cache.mouse={x:z.pageX,y:z.pageY};if(t.status.disabled===false&&t.options.position.adjust.mouse===true&&t.options.position.type!=="static"&&t.elements.tooltip.css("display")!=="none"){t.updatePosition(z)}})}}function o(u,v,A){var z,s,x,y,t,w;z=this;if(A.corner=="center"){return v.position}s=f.extend({},u);y={x:false,y:false};t={left:(s.left<f.fn.qtip.cache.screen.scroll.left),right:(s.left+A.dimensions.width+2>=f.fn.qtip.cache.screen.width+f.fn.qtip.cache.screen.scroll.left),top:(s.top<f.fn.qtip.cache.screen.scroll.top),bottom:(s.top+A.dimensions.height+2>=f.fn.qtip.cache.screen.height+f.fn.qtip.cache.screen.scroll.top)};x={left:(t.left&&(A.corner.search(/right/i)!=-1||(A.corner.search(/right/i)==-1&&!t.right))),right:(t.right&&(A.corner.search(/left/i)!=-1||(A.corner.search(/left/i)==-1&&!t.left))),top:(t.top&&A.corner.search(/top/i)==-1),bottom:(t.bottom&&A.corner.search(/bottom/i)==-1)};if(x.left){if(z.options.position.target!=="mouse"){s.left=v.position.left+v.dimensions.width}else{s.left=z.cache.mouse.x}y.x="Left"}else{if(x.right){if(z.options.position.target!=="mouse"){s.left=v.position.left-A.dimensions.width}else{s.left=z.cache.mouse.x-A.dimensions.width}y.x="Right"}}if(x.top){if(z.options.position.target!=="mouse"){s.top=v.position.top+v.dimensions.height}else{s.top=z.cache.mouse.y}y.y="top"}else{if(x.bottom){if(z.options.position.target!=="mouse"){s.top=v.position.top-A.dimensions.height}else{s.top=z.cache.mouse.y-A.dimensions.height}y.y="bottom"}}if(s.left<0){s.left=u.left;y.x=false}if(s.top<0){s.top=u.top;y.y=false}if(z.options.style.tip.corner!==false){s.corner=new String(A.corner);if(y.x!==false){s.corner=s.corner.replace(/Left|Right|Middle/,y.x)}if(y.y!==false){s.corner=s.corner.replace(/top|bottom/,y.y)}if(s.corner!==z.elements.tip.attr("rel")){e.call(z,s.corner)}}return s}function q(u,t){var v,s;v=f.extend(true,{},u);for(s in v){if(t===true&&s.search(/(tip|classes)/i)!==-1){delete v[s]}else{if(!t&&s.search(/(width|border|tip|title|classes|user)/i)!==-1){delete v[s]}}}return v}function c(s){if(typeof s.tip!=="object"){s.tip={corner:s.tip}}if(typeof s.tip.size!=="object"){s.tip.size={width:s.tip.size,height:s.tip.size}}if(typeof s.border!=="object"){s.border={width:s.border}}if(typeof s.width!=="object"){s.width={value:s.width}}if(typeof s.width.max=="string"){s.width.max=parseInt(s.width.max.replace(/([0-9]+)/i,"$1"))}if(typeof s.width.min=="string"){s.width.min=parseInt(s.width.min.replace(/([0-9]+)/i,"$1"))}if(typeof s.tip.size.x=="number"){s.tip.size.width=s.tip.size.x;delete s.tip.size.x}if(typeof s.tip.size.y=="number"){s.tip.size.height=s.tip.size.y;delete s.tip.size.y}return s}function a(){var s,t,u,x,v,w;s=this;u=[true,{}];for(t=0;t<arguments.length;t++){u.push(arguments[t])}x=[f.extend.apply(f,u)];while(typeof x[0].name=="string"){x.unshift(c(f.fn.qtip.styles[x[0].name]))}x.unshift(true,{classes:{tooltip:"qtip-"+(arguments[0].name||"defaults")}},f.fn.qtip.styles.defaults);v=f.extend.apply(f,x);w=(f.browser.msie)?1:0;v.tip.size.width+=w;v.tip.size.height+=w;if(v.tip.size.width%2>0){v.tip.size.width+=1}if(v.tip.size.height%2>0){v.tip.size.height+=1}if(v.tip.corner===true){v.tip.corner=(s.options.position.corner.tooltip==="center")?false:s.options.position.corner.tooltip}return v}function b(v,u,t){var s={bottomRight:[[0,0],[u,t],[u,0]],bottomLeft:[[0,0],[u,0],[0,t]],topRight:[[0,t],[u,0],[u,t]],topLeft:[[0,0],[0,t],[u,t]],topMiddle:[[0,t],[u/2,0],[u,t]],bottomMiddle:[[0,0],[u,0],[u/2,t]],rightMiddle:[[0,0],[u,t/2],[0,t]],leftMiddle:[[u,0],[u,t],[0,t/2]]};s.leftTop=s.bottomRight;s.rightTop=s.bottomLeft;s.leftBottom=s.topRight;s.rightBottom=s.topLeft;return s[v]}function g(s){var t;if(f("<canvas>").get(0).getContext){t={topLeft:[s,s],topRight:[0,s],bottomLeft:[s,0],bottomRight:[0,0]}}else{if(f.browser.msie){t={topLeft:[-90,90,0],topRight:[-90,90,-s],bottomLeft:[90,270,0],bottomRight:[90,270,-s]}}}return t}function k(){var s,t,u;s=this;u=s.getDimensions();t='<iframe class="qtip-bgiframe" frameborder="0" tabindex="-1" src="javascript:false" style="display:block; position:absolute; z-index:-1; filter:alpha(opacity=\'0\'); border: 1px solid red; height:'+u.height+"px; width:"+u.width+'px" />';s.elements.bgiframe=s.elements.wrapper.prepend(t).children(".qtip-bgiframe:first")}f(document).ready(function(){f.fn.qtip.cache={screen:{scroll:{left:f(window).scrollLeft(),top:f(window).scrollTop()},width:f(window).width(),height:f(window).height()}};var s;f(window).bind("resize scroll",function(t){clearTimeout(s);s=setTimeout(function(){if(t.type==="scroll"){f.fn.qtip.cache.screen.scroll={left:f(window).scrollLeft(),top:f(window).scrollTop()}}else{f.fn.qtip.cache.screen.width=f(window).width();f.fn.qtip.cache.screen.height=f(window).height()}for(i=0;i<f.fn.qtip.interfaces.length;i++){var u=f.fn.qtip.interfaces[i];if(u.status.rendered===true&&(u.options.position.type!=="static"||u.options.position.adjust.scroll&&t.type==="scroll"||u.options.position.adjust.resize&&t.type==="resize")){u.updatePosition(t,true)}}},100)});f(document).bind("mousedown.qtip",function(t){if(f(t.target).parents("div.qtip").length===0){f(".qtip[unfocus]").each(function(){var u=f(this).qtip("api");if(f(this).is(":visible")&&!u.status.disabled&&f(t.target).add(u.elements.target).length>1){u.hide(t)}})}})});f.fn.qtip.interfaces=[];f.fn.qtip.log={error:function(){return this}};f.fn.qtip.constants={};f.fn.qtip.defaults={content:{prerender:false,text:false,url:false,data:null,title:{text:false,button:false}},position:{target:false,corner:{target:"bottomRight",tooltip:"topLeft"},adjust:{x:0,y:0,mouse:true,screen:false,scroll:true,resize:true},type:"absolute",container:false},show:{when:{target:false,event:"mouseover"},effect:{type:"fade",length:100},delay:140,solo:false,ready:false},hide:{when:{target:false,event:"mouseout"},effect:{type:"fade",length:100},delay:0,fixed:false},api:{beforeRender:function(){},onRender:function(){},beforePositionUpdate:function(){},onPositionUpdate:function(){},beforeShow:function(){},onShow:function(){},beforeHide:function(){},onHide:function(){},beforeContentUpdate:function(){},onContentUpdate:function(){},beforeContentLoad:function(){},onContentLoad:function(){},beforeTitleUpdate:function(){},onTitleUpdate:function(){},beforeDestroy:function(){},onDestroy:function(){},beforeFocus:function(){},onFocus:function(){}}};f.fn.qtip.styles={defaults:{background:"white",color:"#111",overflow:"hidden",textAlign:"left",width:{min:0,max:250},padding:"5px 9px",border:{width:1,radius:0,color:"#d3d3d3"},tip:{corner:false,color:false,size:{width:13,height:13},opacity:1},title:{background:"#e1e1e1",fontWeight:"bold",padding:"7px 12px"},button:{cursor:"pointer"},classes:{target:"",tip:"qtip-tip",title:"qtip-title",button:"qtip-button",content:"qtip-content",active:"qtip-active"}},cream:{border:{width:3,radius:0,color:"#F9E98E"},title:{background:"#F0DE7D",color:"#A27D35"},background:"#FBF7AA",color:"#A27D35",classes:{tooltip:"qtip-cream"}},light:{border:{width:3,radius:0,color:"#E2E2E2"},title:{background:"#f1f1f1",color:"#454545"},background:"white",color:"#454545",classes:{tooltip:"qtip-light"}},dark:{border:{width:3,radius:0,color:"#303030"},title:{background:"#404040",color:"#f3f3f3"},background:"#505050",color:"#f3f3f3",classes:{tooltip:"qtip-dark"}},red:{border:{width:3,radius:0,color:"#CE6F6F"},title:{background:"#f28279",color:"#9C2F2F"},background:"#F79992",color:"#9C2F2F",classes:{tooltip:"qtip-red"}},green:{border:{width:3,radius:0,color:"#A9DB66"},title:{background:"#b9db8c",color:"#58792E"},background:"#CDE6AC",color:"#58792E",classes:{tooltip:"qtip-green"}},blue:{border:{width:3,radius:0,color:"#ADD9ED"},title:{background:"#D0E9F5",color:"#5E99BD"},background:"#E5F6FE",color:"#4D9FBF",classes:{tooltip:"qtip-blue"}}}})(jQuery);
\ No newline at end of file
diff --git a/misc/PublicationBulletins/Portail-LeHavre/libjs/sorttable.js b/misc/PublicationBulletins/Portail-LeHavre/libjs/sorttable.js
new file mode 100644
index 0000000000000000000000000000000000000000..bcf2df6bde870160bd93107c4ecf4d55545ce224
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/libjs/sorttable.js
@@ -0,0 +1,244 @@
+/* -*- mode: javascript -*-
+ */
+
+addEvent(window, "load", sortables_init);
+
+var SORT_COLUMN_INDEX;
+
+function sortables_init() {
+    // Find all tables with class sortable and make them sortable
+    if (!document.getElementsByTagName) return;
+    var tbls = document.getElementsByTagName("table");
+    for (var ti=0;ti<tbls.length;ti++) {
+        var thisTbl = tbls[ti];
+        if (((' '+thisTbl.className+' ').indexOf("sortable") != -1) && (thisTbl.id)) {
+            //initTable(thisTbl.id);
+            ts_makeSortable(thisTbl);
+        }
+    }
+}
+
+function ts_makeSortable(table) {
+    if (table.rows && table.rows.length > 0) {
+        var firstRow = table.rows[0];
+    }
+    if (!firstRow) return;
+    
+    // We have a first row: assume it's the header, and make its contents clickable links
+    for (var i=0;i<firstRow.cells.length;i++) {
+        var cell = firstRow.cells[i];
+        var txt = ts_getInnerText(cell);
+        cell.innerHTML = '<a href="#" class="sortheader" '+ 
+        'onclick="ts_resortTable(this, '+i+');return false;">' + 
+        txt+'<span class="sortarrow">&nbsp;&nbsp;&nbsp;</span></a>';
+    }
+}
+
+function ts_getInnerText(el) {
+	if (typeof el == "string") return el;
+	if (typeof el == "undefined") { return el };
+	if (el.innerText) return el.innerText;	//Not needed but it is faster
+	var str = "";
+	
+	var cs = el.childNodes;
+	var l = cs.length;
+	for (var i = 0; i < l; i++) {
+		switch (cs[i].nodeType) {
+			case 1: //ELEMENT_NODE
+				str += ts_getInnerText(cs[i]);
+				break;
+			case 3:	//TEXT_NODE
+				str += cs[i].nodeValue;
+				break;
+		}
+	}
+	return str;
+}
+
+// Check ifan element contains a css class
+// Uses classList  Introduced in Gecko 1.9.2 (FF 3.6) or regexp
+var containsClass = function (elm, className) {
+    if (document.documentElement.classList) {
+        containsClass = function (elm, className) {
+            return elm.classList.contains(className);
+        }
+    } else {
+        containsClass = function (elm, className) {
+            if (!elm || !elm.className) {
+                return false;
+            }
+            var re = new RegExp('(^|\\s)' + className + '(\\s|$)');
+            return elm.className.match(re);
+        }
+    }
+    return containsClass(elm, className);
+}
+
+
+function ts_resortTable(lnk,clid) {
+    // get the span
+    var span;
+    for (var ci=0;ci<lnk.childNodes.length;ci++) {
+        if (lnk.childNodes[ci].tagName && lnk.childNodes[ci].tagName.toLowerCase() == 'span') span = lnk.childNodes[ci];
+    }
+    var spantext = ts_getInnerText(span);
+    var td = lnk.parentNode;
+    var column = clid || td.cellIndex;
+    var table = getParent(td,'TABLE');
+    
+    // Work out a type for the column
+    if (table.rows.length <= 1) return;
+    
+    isNumeric = false;
+    if (containsClass(table.rows[0].cells[column], 'sortnumeric'))  {
+	isNumeric = true;
+    }
+
+    var itm = ts_getInnerText(table.rows[1].cells[column]);
+    sortfn = ts_sort_caseinsensitive;
+    if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) sortfn = ts_sort_date;
+    if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) sortfn = ts_sort_date;
+    if (itm.match(/^[�$]/)) sortfn = ts_sort_currency;
+    if (itm.match(/^[\d\.]+$/) || isNumeric) sortfn = ts_sort_numeric;
+    SORT_COLUMN_INDEX = column;
+    var firstRow = new Array();
+    var newRows = new Array();
+    var botRows = new Array();
+    var topRows = new Array();
+    var ir = 0;
+    var ib = 0;
+    var it = 0;
+    for (i=0;i<table.rows[0].length;i++) { firstRow[i] = table.rows[0][i]; }
+    for (j=1;j<table.rows.length;j++) { 
+      if (!table.rows[j].className) {
+	newRows[ir] = table.rows[j];
+	ir += 1;
+      } else {
+	if (table.rows[j].className.indexOf('sortbottom') != -1) {
+	  botRows[ib] = table.rows[j];
+	  ib += 1;
+	} else {
+	  if (table.rows[j].className.indexOf('sorttop') != -1) {
+	    topRows[it] = table.rows[j];
+	    it += 1;
+	  } else {
+	    newRows[ir] = table.rows[j];
+	    ir += 1;
+	  }
+	}
+      }
+    }
+
+    newRows.sort(sortfn);
+
+    if (span.getAttribute("sortdir") == 'down') {
+        ARROW = '&nbsp;&nbsp;&uarr;';
+        newRows.reverse();
+        span.setAttribute('sortdir','up');
+    } else {
+        ARROW = '&nbsp;&nbsp;&darr;';
+        span.setAttribute('sortdir','down');
+    }
+    
+    // We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
+    // place sorttop rows at first:
+    for (i=0; i < topRows.length; i++) {
+      table.tBodies[0].appendChild(topRows[i]);
+    }
+    // standard (sorted) rows:
+    for (i=0; i < newRows.length;i++) { 
+      table.tBodies[0].appendChild(newRows[i]);
+    }
+    // do sortbottom rows only
+    for (i=0; i < botRows.length;i++) {
+      table.tBodies[0].appendChild(botRows[i]);
+    }
+    // Delete any other arrows there may be showing
+    var allspans = document.getElementsByTagName("span");
+    for (var ci=0;ci<allspans.length;ci++) {
+        if (allspans[ci].className == 'sortarrow') {
+            if (getParent(allspans[ci],"table") == getParent(lnk,"table")) { // in the same table as us?
+                allspans[ci].innerHTML = '&nbsp;&nbsp;&nbsp;';
+            }
+        }
+    }
+        
+    span.innerHTML = ARROW;
+}
+
+function getParent(el, pTagName) {
+	if (el == null) return null;
+	else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase())	// Gecko bug, supposed to be uppercase
+		return el;
+	else
+		return getParent(el.parentNode, pTagName);
+}
+function ts_sort_date(a,b) {
+    // y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
+    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
+    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
+    if (aa.length == 10) {
+        dt1 = aa.substr(6,4)+aa.substr(3,2)+aa.substr(0,2);
+    } else {
+        yr = aa.substr(6,2);
+        if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
+        dt1 = yr+aa.substr(3,2)+aa.substr(0,2);
+    }
+    if (bb.length == 10) {
+        dt2 = bb.substr(6,4)+bb.substr(3,2)+bb.substr(0,2);
+    } else {
+        yr = bb.substr(6,2);
+        if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
+        dt2 = yr+bb.substr(3,2)+bb.substr(0,2);
+    }
+    if (dt1==dt2) return 0;
+    if (dt1<dt2) return -1;
+    return 1;
+}
+
+function ts_sort_currency(a,b) { 
+    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
+    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
+    return parseFloat(aa) - parseFloat(bb);
+}
+
+function ts_sort_numeric(a,b) { 
+    aa = parseFloat(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
+    if (isNaN(aa)) aa = 0;
+    bb = parseFloat(ts_getInnerText(b.cells[SORT_COLUMN_INDEX])); 
+    if (isNaN(bb)) bb = 0;
+    return aa-bb;
+}
+
+function ts_sort_caseinsensitive(a,b) {
+    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
+    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
+    if (aa==bb) return 0;
+    if (aa<bb) return -1;
+    return 1;
+}
+
+function ts_sort_default(a,b) {
+    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
+    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
+    if (aa==bb) return 0;
+    if (aa<bb) return -1;
+    return 1;
+}
+
+
+function addEvent(elm, evType, fn, useCapture)
+// addEvent and removeEvent
+// cross-browser event handling for IE5+,  NS6 and Mozilla
+// By Scott Andrew
+{
+  if (elm.addEventListener){
+    elm.addEventListener(evType, fn, useCapture);
+    return true;
+  } else if (elm.attachEvent){
+    var r = elm.attachEvent("on"+evType, fn);
+    return r;
+  } else {
+    alert("Handler could not be removed");
+  }
+} 
diff --git a/misc/PublicationBulletins/Portail-LeHavre/phpToPDF.php b/misc/PublicationBulletins/Portail-LeHavre/phpToPDF.php
new file mode 100755
index 0000000000000000000000000000000000000000..becd37c20c0802d11a40f33c155f0436f315c1ab
--- /dev/null
+++ b/misc/PublicationBulletins/Portail-LeHavre/phpToPDF.php
@@ -0,0 +1,1394 @@
+<?php
+require('fpdf.php');
+
+$red = array(255,0,0);
+$green = array(0,255,0);
+$blue = array(0,0,255);
+$black = array(0,0,0);
+$formatA4 = array(595.28,841.89);
+
+function isInteger($val)
+{
+	if ($val - round($val) == 0) return true;
+	else return false;
+}
+function plus10pourcentArrondi($valeur)
+{
+	if ($valeur > 10000) 		$ratio=1000;
+	else if ($valeur > 1000) 	$ratio=100;
+	else if ($valeur > 100) 	$ratio=10;
+	else 				$ratio=1;
+
+	$res = $valeur + (0.1*$valeur);
+	$res = round($res/$ratio) * $ratio;
+
+	return $res;
+}
+
+function moins10pourcentArrondi($valeur)
+{
+	if ($valeur > 10000) 		$ratio=1000;
+	else if ($valeur > 1000) 	$ratio=100;
+	else if ($valeur > 100) 	$ratio=10;
+	else 				$ratio=1;
+
+	if ($valeur >0)
+		$res = $valeur - (0.1*$valeur);
+	else $res = $valeur + (0.1*$valeur);
+	
+	$res = round($res/$ratio) * $ratio;
+	return $res;
+}
+
+class phpToPDF extends FPDF
+{
+	var $legends;
+	var $wLegend;
+	var $sum;
+	var $NbVal;
+
+	var $_toc=array();
+	var $_numbering=false;
+	var $_numberingFooter=false;
+	var $_numPageNum=1;
+
+	var $tb_columns; 		//number of columns of the table
+	var $tb_header_type; 	//array which contains the header characteristics and texts
+	var $tb_header_draw;	//TRUE or FALSE, the header is drawed or not
+	var $tb_border_draw;	//TRUE or FALSE, the table border is drawed or not
+	var $tb_data_type; 		//array which contains the data characteristics (only the characteristics)
+	var $tb_table_type; 	//array which contains the table charactersitics
+	var $table_startx, $table_starty;	//the X and Y position where the table starts
+
+	var $Draw_Header_Command;	//command which determines in the DrawData first the header draw
+	var $New_Page_Commit;	// = true/false if a new page has been comited
+	var $Data_On_Current_Page; // = true/false ... if on current page was some data written	
+	
+
+	function AddPage($orientation='') {
+		parent::AddPage($orientation);
+		if($this->_numbering)
+			$this->_numPageNum++;
+	}
+
+	function startPageNums() {
+		$this->_numbering=true;
+		$this->_numberingFooter=true;
+	}
+
+	function stopPageNums() {
+		$this->_numbering=false;
+	}
+
+	function numPageNo() {
+		return $this->_numPageNum;
+	}
+
+	function TOC_Entry($txt,$level=0) {
+		$this->_toc[]=array('t'=>$txt,'l'=>$level,'p'=>$this->numPageNo());
+	}
+
+	function insertTOC( $location=1,
+						$labelSize=20,
+						$entrySize=10,
+						$tocfont='Times',
+						$label='Table des mati�res'
+						) {
+		//make toc at end
+		$this->stopPageNums();
+		$this->AddPage();
+		$tocstart=$this->page;
+
+		$this->SetFont($tocfont,'B',$labelSize);
+		$this->Cell(0,5,$label,0,1,'C');
+		$this->Ln(20);
+
+
+		$this->SetLeftMargin(20);
+
+
+
+		foreach($this->_toc as $t) {
+
+			//Offset
+			$level=$t['l'];
+			if($level>0)
+				$this->Cell($level*8);
+			$weight='';
+			if($level==0)
+				$weight='B';
+			$str=$t['t'];
+			$this->SetFont($tocfont,$weight,$entrySize);
+			$strsize=$this->GetStringWidth($str);
+			$this->Cell($strsize+2,$this->FontSize+2,$str);
+
+			//Filling dots
+			$this->SetFont($tocfont,'',$entrySize);
+			$PageCellSize=$this->GetStringWidth($t['p'])+2;
+			$w=$this->w-$this->lMargin-$this->rMargin-$PageCellSize-($level*8)-($strsize+2);
+			$nb=$w/$this->GetStringWidth('.');
+			$dots=str_repeat('.',$nb);
+			$this->Cell($w,$this->FontSize+2,$dots,0,0,'R');
+
+			//Page number
+			$this->Cell($PageCellSize,$this->FontSize+2,$t['p'],0,1,'R');
+
+			$this->Ln(2);
+		}
+
+		//grab it and move to selected location
+		$n=$this->page;
+		$n_toc = $n - $tocstart + 1;
+		$last = array();
+
+		//store toc pages
+		for($i = $tocstart;$i <= $n;$i++)
+			$last[]=$this->pages[$i];
+
+		//move pages
+		for($i=$tocstart - 1;$i>=$location-1;$i--)
+			$this->pages[$i+$n_toc]=$this->pages[$i];
+
+		//Put toc pages at insert point
+		for($i = 0;$i < $n_toc;$i++)
+			$this->pages[$location + $i]=$last[$i];
+	}
+
+	function Footer() {
+		if($this->_numberingFooter==false)
+			return;
+		//Go to 1.5 cm from bottom
+		$this->SetY(-15);
+		//Select Arial italic 8
+		$this->SetFont('Arial','I',8);
+		$this->Cell(0,7,$this->numPageNo(),0,0,'C'); 
+		if($this->_numbering==false)
+			$this->_numberingFooter=false;
+	}
+
+	function SetDash($black=false,$white=false)
+	{
+		if($black and $white)
+			$s=sprintf('[%.3f %.3f] 0 d',$black*$this->k,$white*$this->k);
+		else
+			$s='[] 0 d';
+		$this->_out($s);
+	}
+	
+	function SetLegends($data, $format)
+	{
+		$this->legends=array();
+		$this->wLegend=0;
+		$this->sum=array_sum($data);
+		$this->NbVal=count($data);
+		foreach($data as $l=>$val)
+		{
+			$p=sprintf('%.2f',$val/$this->sum*100).'%';
+			$legend=str_replace(array('%l','%v','%p'),array($l,$val,$p),$format);
+			$this->legends[]=$legend;
+			$this->wLegend=max($this->GetStringWidth($legend),$this->wLegend);
+		}
+	}
+
+	function DiagCirculaire($largeur, $hauteur, $data, $format, $couleurs=null, $legend=1)
+	{
+		$this->SetFont('Courier', '', 10);
+		$this->SetLegends($data,$format);
+
+		$XPage = $this->GetX();
+		$YPage = $this->GetY();
+		$marge = 2;
+		$hLegende = 5;
+		$rayon = min($largeur - $marge * 4 - $hLegende - $this->wLegend, $hauteur - $marge * 2);
+		$rayon = floor($rayon / 2);
+		$XDiag = $XPage + $marge + $rayon;
+		$YDiag = $YPage + $marge + $rayon;
+		if($couleurs == null) {
+			for($i = 0;$i < $this->NbVal; $i++) {
+				$gray = $i * intval(255 / $this->NbVal);
+				$couleurs[$i] = array($gray,$gray,$gray);
+			}
+		}
+
+		//Secteurs
+		$this->SetLineWidth(0.2);
+		$angleDebut = 0;
+		$angleFin = 0;
+		$i = 0;
+		foreach($data as $val) {
+			$angle = floor(($val * 360) / doubleval($this->sum));
+			if ($angle != 0) {
+				$angleFin = $angleDebut + $angle;
+				$this->SetFillColor($couleurs[$i][0],$couleurs[$i][1],$couleurs[$i][2]);
+				$this->Sector($XDiag, $YDiag, $rayon, $angleDebut, $angleFin);
+				$angleDebut += $angle;
+			}
+			$i++;
+		}
+		if ($angleFin != 360) {
+			$this->Sector($XDiag, $YDiag, $rayon, $angleDebut - $angle, 360);
+		}
+
+		//L&eacute;gendes
+		if ($legend == 1)
+		{
+			$this->SetFont('Courier', '', 10);
+			$x1 = $XPage + 2 * $rayon + 4 * $marge;
+			$x2 = $x1 + $hLegende + $marge;
+			$y1 = $YDiag - $rayon + (2 * $rayon - $this->NbVal*($hLegende + $marge)) / 2;
+			for($i=0; $i<$this->NbVal; $i++) {
+				$this->SetFillColor($couleurs[$i][0],$couleurs[$i][1],$couleurs[$i][2]);
+				$this->Rect($x1, $y1, $hLegende, $hLegende, 'DF');
+				$this->SetXY($x2,$y1);
+				$this->Cell(0,$hLegende,$this->legends[$i]);
+				$y1+=$hLegende + $marge;
+			}
+		}
+	}
+
+
+	function DiagBatons($largeur, $hauteur, $data, $format, $couleur=null, $maxValRepere=0, $nbIndRepere=4)
+	{
+		$this->SetFont('Courier', '', 10);
+		$this->SetLegends($data,$format);
+
+		$XPage = $this->GetX();
+		$YPage = $this->GetY();
+		$marge = 2;
+		$YDiag = $YPage + $marge;
+		$hDiag = floor($hauteur - $marge * 2);
+		$XDiag = $XPage + $marge * 2 + $this->wLegend;
+		$lDiag = floor($largeur - $marge * 3 - $this->wLegend);
+		if($couleur == null)
+			$couleur=array(155,155,155);
+		if ($maxValRepere == 0) {
+			$maxValRepere = max($data);
+		}
+		$valIndRepere = ceil($maxValRepere / $nbIndRepere);
+		$maxValRepere = $valIndRepere * $nbIndRepere;
+		$lRepere = floor($lDiag / $nbIndRepere);
+		$lDiag = $lRepere * $nbIndRepere;
+		$unite = $lDiag / $maxValRepere;
+		$hBaton = floor($hDiag / ($this->NbVal + 1));
+		$hDiag = $hBaton * ($this->NbVal + 1);
+		$eBaton = floor($hBaton * 80 / 100);
+
+		$this->SetLineWidth(0.2);
+		$this->Rect($XDiag, $YDiag, $lDiag, $hDiag);
+
+		$this->SetFont('Courier', '', 10);
+		$this->SetFillColor($couleur[0],$couleur[1],$couleur[2]);
+		$i=0;
+		foreach($data as $val) {
+			//Barre
+			$xval = $XDiag;
+			$lval = (int)($val * $unite);
+			$yval = $YDiag + ($i + 1) * $hBaton - $eBaton / 2;
+			$hval = $eBaton;
+			$this->Rect($xval, $yval, $lval, $hval, 'DF');
+			//L&eacute;gende
+			$this->SetXY(0, $yval);
+			$this->Cell($xval - $marge, $hval, $this->legends[$i],0,0,'R');
+			$i++;
+		}
+
+		//Echelles
+		for ($i = 0; $i <= $nbIndRepere; $i++) {
+			$xpos = $XDiag + $lRepere * $i;
+			$this->Line($xpos, $YDiag, $xpos, $YDiag + $hDiag);
+			$val = $i * $valIndRepere;
+			$xpos = $XDiag + $lRepere * $i - $this->GetStringWidth($val) / 2;
+			$ypos = $YDiag + $hDiag - $marge;
+			$this->Text($xpos, $ypos, $val);
+		}
+	}	
+
+	function Sector($xc, $yc, $r, $a, $b, $style='FD', $cw=true, $o=90)
+	{
+		if($cw){
+			$d = $b;
+			$b = $o - $a;
+			$a = $o - $d;
+		}else{
+			$b += $o;
+			$a += $o;
+		}
+		$a = ($a%360)+360;
+		$b = ($b%360)+360;
+		if ($a > $b)
+			$b +=360;
+		$b = $b/360*2*M_PI;
+		$a = $a/360*2*M_PI;
+		$d = $b-$a;
+		if ($d == 0 )
+			$d =2*M_PI;
+		$k = $this->k;
+		$hp = $this->h;
+		if($style=='F')
+			$op='f';
+		elseif($style=='FD' or $style=='DF')
+			$op='b';
+		else
+			$op='s';
+		if (sin($d/2))
+			$MyArc = 4/3*(1-cos($d/2))/sin($d/2)*$r;
+		//first put the center
+		$this->_out(sprintf('%.2f %.2f m',($xc)*$k,($hp-$yc)*$k));
+		//put the first point
+		$this->_out(sprintf('%.2f %.2f l',($xc+$r*cos($a))*$k,(($hp-($yc-$r*sin($a)))*$k)));
+		//draw the arc
+		if ($d < M_PI/2){
+			$this->_Arc($xc+$r*cos($a)+$MyArc*cos(M_PI/2+$a),
+						$yc-$r*sin($a)-$MyArc*sin(M_PI/2+$a),
+						$xc+$r*cos($b)+$MyArc*cos($b-M_PI/2),
+						$yc-$r*sin($b)-$MyArc*sin($b-M_PI/2),
+						$xc+$r*cos($b),
+						$yc-$r*sin($b)
+						);
+		}else{
+			$b = $a + $d/4;
+			$MyArc = 4/3*(1-cos($d/8))/sin($d/8)*$r;
+			$this->_Arc($xc+$r*cos($a)+$MyArc*cos(M_PI/2+$a),
+						$yc-$r*sin($a)-$MyArc*sin(M_PI/2+$a),
+						$xc+$r*cos($b)+$MyArc*cos($b-M_PI/2),
+						$yc-$r*sin($b)-$MyArc*sin($b-M_PI/2),
+						$xc+$r*cos($b),
+						$yc-$r*sin($b)
+						);
+			$a = $b;
+			$b = $a + $d/4;
+			$this->_Arc($xc+$r*cos($a)+$MyArc*cos(M_PI/2+$a),
+						$yc-$r*sin($a)-$MyArc*sin(M_PI/2+$a),
+						$xc+$r*cos($b)+$MyArc*cos($b-M_PI/2),
+						$yc-$r*sin($b)-$MyArc*sin($b-M_PI/2),
+						$xc+$r*cos($b),
+						$yc-$r*sin($b)
+						);
+			$a = $b;
+			$b = $a + $d/4;
+			$this->_Arc($xc+$r*cos($a)+$MyArc*cos(M_PI/2+$a),
+						$yc-$r*sin($a)-$MyArc*sin(M_PI/2+$a),
+						$xc+$r*cos($b)+$MyArc*cos($b-M_PI/2),
+						$yc-$r*sin($b)-$MyArc*sin($b-M_PI/2),
+						$xc+$r*cos($b),
+						$yc-$r*sin($b)
+						);
+			$a = $b;
+			$b = $a + $d/4;
+			$this->_Arc($xc+$r*cos($a)+$MyArc*cos(M_PI/2+$a),
+						$yc-$r*sin($a)-$MyArc*sin(M_PI/2+$a),
+						$xc+$r*cos($b)+$MyArc*cos($b-M_PI/2),
+						$yc-$r*sin($b)-$MyArc*sin($b-M_PI/2),
+						$xc+$r*cos($b),
+						$yc-$r*sin($b)
+						);
+		}
+		//terminate drawing
+		$this->_out($op);
+	}
+
+	function _Arc($x1, $y1, $x2, $y2, $x3, $y3 )
+	{
+		$h = $this->h;
+		$this->_out(sprintf('%.2f %.2f %.2f %.2f %.2f %.2f c',
+			$x1*$this->k,
+			($h-$y1)*$this->k,
+			$x2*$this->k,
+			($h-$y2)*$this->k,
+			$x3*$this->k,
+			($h-$y3)*$this->k));
+	}	
+	
+	//returns the width of the page in user units
+	function PageWidth(){
+		return (int) $this->w-$this->rMargin-$this->lMargin;
+	}
+
+	//constructor(not a real one, but have to call it first)
+	//we initialize all the variables that we use
+	function Table_Init($col_no = 0, $header_draw = true, $border_draw = true){
+		$this->tb_columns = $col_no;
+		$this->tb_header_type = Array();
+		$this->tb_header_draw = $header_draw;
+		$this->tb_border_draw = $border_draw;
+		$this->tb_data_type = Array();
+		$this->tb_type = Array();
+		$this->table_startx = $this->GetX();
+		$this->table_starty = $this->GetY();
+
+		$this->Draw_Header_Command = false; //by default we don't draw the header
+		$this->New_Page_Commit = false;		//NO we do not consider first time a new page
+		$this->Data_On_Current_Page = false;
+	}
+
+	//Sets the number of columns of the table
+	function Set_Table_Columns($nr){
+		$this->tb_columns = $nr;
+	}
+
+	/*
+	Characteristics constants for Header Type:
+	EVERY CELL FROM THE TABLE IS A MULTICELL
+
+		WIDTH - this is the cell width. This value must be sent only to the HEADER!!!!!!!!
+		T_COLOR - text color = array(r,g,b);
+		T_SIZE - text size
+		T_FONT - text font - font type = "Arial", "Times"
+		T_ALIGN - text align - "RLCJ"
+		V_ALIGN - text vertical alignment - "TMB"
+		T_TYPE - text type (Bold Italic etc)
+		LN_SPACE - space between lines
+		BG_COLOR - background color = array(r,g,b);
+		BRD_COLOR - border color = array(r,g,b);
+		BRD_SIZE - border size --
+		BRD_TYPE - border size -- up down, with border without!!! etc
+		BRD_TYPE_NEW_PAGE - border type on new page - this is user only if specified(<>'')
+		TEXT - header text -- THIS ALSO BELONGS ONLY TO THE HEADER!!!!
+
+		all these setting conform to the settings from the multicell functions!!!!
+	*/
+
+	/*
+	Function: Set_Header_Type($type_arr) -- sets the array for the header type
+
+	type array =
+		 array(
+			0=>array(
+					"WIDTH" => 10,
+					"T_COLOR" => array(120,120,120),
+					"T_SIZE" => 5,
+					...
+					"TEXT" => "Header text 1"
+				  ),
+			1=>array(
+					...
+				  ),
+		 );
+	where 0,1... are the column number
+	*/
+
+	function Set_Header_Type($type_arr){
+		$this->tb_header_type = $type_arr;
+	}
+
+
+	/*
+	Characteristics constants for Data Type:
+	EVERY CELL FROM THE TABLE IS A MULTICELL
+		T_COLOR - text color = array(r,g,b);
+		T_SIZE - text size
+		T_FONT - text font - font type = "Arial", "Times"
+		T_ALIGN - text align - "RLCJ"
+		V_ALIGN - text vertical alignment - "TMB"
+		T_TYPE - text type (Bold Italic etc)
+		LN_SPACE - space between lines
+		BG_COLOR - background color = array(r,g,b);
+		BRD_COLOR - border color = array(r,g,b);
+		BRD_SIZE - border size --
+		BRD_TYPE - border size -- up down, with border without!!! etc
+		BRD_TYPE_NEW_PAGE - border type on new page - this is user only if specified(<>'')
+
+		all these settings conform to the settings from the multicell functions!!!!
+	*/
+
+	/*
+	Function: Set_data_Type($type_arr) -- sets the array for the header type
+
+	type array =
+		 array(
+			0=>array(
+					"T_COLOR" => array(120,120,120),
+					"T_SIZE" => 5,
+					...
+					"BRD_TYPE" => 1
+				  ),
+			1=>array(
+					...
+				  ),
+		 );
+	where 0,1... are the column number
+	*/
+
+	function Set_Data_Type($type_arr){
+		$this->tb_data_type = $type_arr;
+	}
+
+
+
+	/*
+	Function Set_Table_Type
+
+	$type_arr = array(
+					"BRD_COLOR"=> array (120,120,120), //border color
+					"BRD_SIZE"=>5), //border line width
+					"TB_COLUMNS"=>5), //the number of columns
+					"TB_ALIGN"=>"L"), //the align of the table, possible values = L, R, C equivalent to Left, Right, Center
+					'L_MARGIN' => 0// left margin... reference from this->lmargin values
+					)
+	*/
+	function Set_Table_Type($type_arr){
+
+		if (isset($type_arr['TB_COLUMNS'])) $this->tb_columns = $type_arr['TB_COLUMNS'];
+		if (!isset($type_arr['L_MARGIN'])) $type_arr['L_MARGIN']=0;//default values
+
+		$this->tb_table_type = $type_arr;
+
+	}
+
+	//this functiondraws the exterior table border!!!!
+	function Draw_Table_Border(){
+	/*				"BRD_COLOR"=> array (120,120,120), //border color
+					"BRD_SIZE"=>5), //border line width
+					"TB_COLUMNS"=>5), //the number of columns
+					"TB_ALIGN"=>"L"), //the align of the table, possible values = L, R, C equivalent to Left, Right, Center
+	*/
+
+		if ( ! $this->tb_border_draw ) return;
+
+		if ( ! $this->Data_On_Current_Page) return; //there was no data on the current page
+
+		//set the colors
+		list($r, $g, $b) = $this->tb_table_type['BRD_COLOR'];
+		$this->SetDrawColor($r, $g, $b);
+
+		//set the line width
+		$this->SetLineWidth($this->tb_table_type['BRD_SIZE']);
+
+		//draw the border
+		$this->Rect(
+			$this->table_startx,
+			$this->table_starty,
+			$this->Get_Table_Width(),
+			$this->GetY()-$this->table_starty);
+
+	}
+
+	function End_Page_Border(){
+		if (isset($this->tb_table_type['BRD_TYPE_END_PAGE'])){
+
+			if (strpos($this->tb_table_type['BRD_TYPE_END_PAGE'], 'B') >= 0){
+
+				//set the colors
+				list($r, $g, $b) = $this->tb_table_type['BRD_COLOR'];
+				$this->SetDrawColor($r, $g, $b);
+
+				//set the line width
+				$this->SetLineWidth($this->tb_table_type['BRD_SIZE']);
+
+				//draw the line
+				$this->Line($this->table_startx, $this->GetY(), $this->table_startx + $this->Get_Table_Width(), $this->GetY());
+			}
+		}
+	}
+
+	//returns the table width in user units
+	function Get_Table_Width()
+	{
+		//calculate the table width
+		$tb_width = 0;
+		for ($i=0; $i < $this->tb_columns; $i++){
+			$tb_width += $this->tb_header_type[$i]['WIDTH'];
+		}
+		return $tb_width;
+	}
+
+	//alignes the table to C, L or R(default is L)
+	function Table_Align(){
+		//check if the table is aligned
+		if (isset($this->tb_table_type['TB_ALIGN'])) $tb_align = $this->tb_table_type['TB_ALIGN']; else $tb_align='';
+
+		//set the table align
+		switch($tb_align){
+			case 'C':
+				$this->SetX($this->lMargin + $this->tb_table_type['L_MARGIN'] + ($this->PageWidth() - $this->Get_Table_Width())/2);
+				break;
+			case 'R':
+				$this->SetX($this->lMargin + $this->tb_table_type['L_MARGIN'] + ($this->PageWidth() - $this->Get_Table_Width()));
+				break;
+			default:
+				$this->SetX($this->lMargin + $this->tb_table_type['L_MARGIN']);
+				break;
+		}//if (isset($this->tb_table_type['TB_ALIGN'])){
+	}
+
+	//Draws the Header
+	function Draw_Header(){
+		$this->Draw_Header_Command = true;
+	}
+
+	//Draws the Header
+	function Draw_Header_( $next_line_height = 0 ){
+
+		$this->Table_Align();
+
+		$this->table_startx = $this->GetX();
+		$this->table_starty = $this->GetY();
+
+		//if the header will be showed
+		if ( ! $this->tb_header_draw ) return;
+
+		$h = 0;
+
+		//calculate the maximum height of the cells
+		for($i=0;$i<$this->tb_columns;$i++)
+		{
+
+			$this->SetFont(	$this->tb_header_type[$i]['T_FONT'],
+							$this->tb_header_type[$i]['T_TYPE'],
+							$this->tb_header_type[$i]['T_SIZE']);
+
+			$this->tb_header_type[$i]['CELL_WIDTH'] = $this->tb_header_type[$i]['WIDTH'];
+
+			if (isset($this->tb_header_type[$i]['COLSPAN'])){
+
+				$colspan = (int) $this->tb_header_type[$i]['COLSPAN'];//convert to integer
+
+				for ($j = 1; $j < $colspan; $j++){
+					//if there is a colspan, then calculate the number of lines also with the with of the next cell
+					if (($i + $j) < $this->tb_columns)
+						$this->tb_header_type[$i]['CELL_WIDTH'] += $this->tb_header_type[$i + $j]['WIDTH'];
+				}
+			}
+
+			$this->tb_header_type[$i]['CELL_LINES'] =
+				$this->NbLines($this->tb_header_type[$i]['CELL_WIDTH'],$this->tb_header_type[$i]['TEXT']);
+
+			//this is the maximum cell height
+			$h = max($h, $this->tb_header_type[$i]['LN_SIZE'] * $this->tb_header_type[$i]['CELL_LINES']);
+
+//			if (isset($data[$i]['COLSPAN'])){
+				//just skip the other cells
+//				$i = $i + $colspan - 1;
+//			}
+
+		}
+
+		//Issue a page break first if needed
+		//calculate the header hight and the next data line hight
+		$this->CheckPageBreak($h + $next_line_height, false);
+
+		//Draw the cells of the row
+		for($i=0; $i<$this->tb_columns; $i++)
+		{
+			//border size BRD_SIZE
+			$this->SetLineWidth($this->tb_header_type[$i]['BRD_SIZE']);
+
+			//fill color = BG_COLOR
+			list($r, $g, $b) = $this->tb_header_type[$i]['BG_COLOR'];
+			$this->SetFillColor($r, $g, $b);
+
+			//Draw Color = BRD_COLOR
+			list($r, $g, $b) = $this->tb_header_type[$i]['BRD_COLOR'];
+			$this->SetDrawColor($r, $g, $b);
+
+			//Text Color = T_COLOR
+			list($r, $g, $b) = $this->tb_header_type[$i]['T_COLOR'];
+			$this->SetTextColor($r, $g, $b);
+
+			//Set the font, font type and size
+			$this->SetFont(	$this->tb_header_type[$i]['T_FONT'],
+							$this->tb_header_type[$i]['T_TYPE'],
+							$this->tb_header_type[$i]['T_SIZE']);
+
+			//Save the current position
+			$x=$this->GetX();
+			$y=$this->GetY();
+
+			if ($this->New_Page_Commit){
+				if (isset($this->tb_header_type[$i]['BRD_TYPE_NEW_PAGE'])){
+					$this->tb_header_type[$i]['BRD_TYPE'] .= $this->tb_header_type[$i]['BRD_TYPE_NEW_PAGE'];
+				}
+			}
+
+			//Print the text
+			$this->MultiCellTable(
+					$this->tb_header_type[$i]['CELL_WIDTH'],
+					$this->tb_header_type[$i]['LN_SIZE'],
+					$this->tb_header_type[$i]['TEXT'],
+					$this->tb_header_type[$i]['BRD_TYPE'],
+					$this->tb_header_type[$i]['T_ALIGN'],
+					$this->tb_header_type[$i]['V_ALIGN'],
+					1,
+					$h - $this->tb_header_type[$i]['LN_SIZE'] * $this->tb_header_type[$i]['CELL_LINES']
+					);
+
+			//Put the position to the right of the cell
+			$this->SetXY($x+$this->tb_header_type[$i]['CELL_WIDTH'],$y);
+
+			if (isset($this->tb_header_type[$i]['COLSPAN'])){
+				$i = $i + (int)$this->tb_header_type[$i]['COLSPAN'] - 1;
+			}
+
+
+		}
+
+		//Go to the next line
+		$this->Ln($h);
+
+		$this->Draw_Header_Command = false;
+		$this->New_Page_Commit = false;
+		$this->Data_On_Current_Page = true;
+	}
+
+	//this function Draws the data's from the table
+	//have to call this function after the table initialization, after the table, header and data types are set
+	//and after the header is drawed
+	/*
+	$header = true -> on new page draws the header
+			= false - > the header is not drawed
+	*/
+
+	function Draw_Data($data, $header = true){
+
+		$h = 0;
+
+		//calculate the maximum height of the cells
+		for($i=0; $i < $this->tb_columns; $i++)
+		{
+
+			if (!isset($data[$i]['T_FONT'])) $data[$i]['T_FONT'] = $this->tb_data_type[$i]['T_FONT'];
+			if (!isset($data[$i]['T_TYPE'])) $data[$i]['T_TYPE'] = $this->tb_data_type[$i]['T_TYPE'];
+			if (!isset($data[$i]['T_SIZE'])) $data[$i]['T_SIZE'] = $this->tb_data_type[$i]['T_SIZE'];
+			if (!isset($data[$i]['T_COLOR'])) $data[$i]['T_COLOR'] = $this->tb_data_type[$i]['T_COLOR'];
+			if (!isset($data[$i]['T_ALIGN'])) $data[$i]['T_ALIGN'] = $this->tb_data_type[$i]['T_ALIGN'];
+			if (!isset($data[$i]['V_ALIGN'])) $data[$i]['V_ALIGN'] = $this->tb_data_type[$i]['V_ALIGN'];
+			if (!isset($data[$i]['LN_SIZE'])) $data[$i]['LN_SIZE'] = $this->tb_data_type[$i]['LN_SIZE'];
+			if (!isset($data[$i]['BRD_SIZE'])) $data[$i]['BRD_SIZE'] = $this->tb_data_type[$i]['BRD_SIZE'];
+			if (!isset($data[$i]['BRD_COLOR'])) $data[$i]['BRD_COLOR'] = $this->tb_data_type[$i]['BRD_COLOR'];
+			if (!isset($data[$i]['BRD_TYPE'])) $data[$i]['BRD_TYPE'] = $this->tb_data_type[$i]['BRD_TYPE'];
+			if (!isset($data[$i]['BG_COLOR'])) $data[$i]['BG_COLOR'] = $this->tb_data_type[$i]['BG_COLOR'];
+
+			$this->SetFont(	$data[$i]['T_FONT'],
+							$data[$i]['T_TYPE'],
+							$data[$i]['T_SIZE']);
+
+			$data[$i]['CELL_WIDTH'] = $this->tb_header_type[$i]['WIDTH'];
+
+			if (isset($data[$i]['COLSPAN'])){
+
+				$colspan = (int) $data[$i]['COLSPAN'];//convert to integer
+
+				for ($j = 1; $j < $colspan; $j++){
+					//if there is a colspan, then calculate the number of lines also with the with of the next cell
+					if (($i + $j) < $this->tb_columns)
+						$data[$i]['CELL_WIDTH'] += $this->tb_header_type[$i + $j]['WIDTH'];
+				}
+			}
+
+			$data[$i]['CELL_LINES'] = $this->NbLines($data[$i]['CELL_WIDTH'], $data[$i]['TEXT']);
+
+			//this is the maximum cell height
+			$h = max($h, $data[$i]['LN_SIZE'] * $data[$i]['CELL_LINES']);
+
+			if (isset($data[$i]['COLSPAN'])){
+				//just skip the other cells
+				$i = $i + $colspan - 1;
+			}
+
+		}
+
+
+		$this->CheckPageBreak($h, $header);
+
+		if ($this->Draw_Header_Command){//draw the header
+			$this->Draw_Header_($h);
+		}
+
+		$this->Table_Align();
+
+		//Draw the cells of the row
+		for($i=0;$i<$this->tb_columns;$i++)
+		{
+
+			//border size BRD_SIZE
+			$this->SetLineWidth($data[$i]['BRD_SIZE']);
+
+			//fill color = BG_COLOR
+			list($r, $g, $b) = $data[$i]['BG_COLOR'];
+			$this->SetFillColor($r, $g, $b);
+
+			//Draw Color = BRD_COLOR
+			list($r, $g, $b) = $data[$i]['BRD_COLOR'];
+			$this->SetDrawColor($r, $g, $b);
+
+			//Text Color = T_COLOR
+			list($r, $g, $b) = $data[$i]['T_COLOR'];
+			$this->SetTextColor($r, $g, $b);
+
+			//Set the font, font type and size
+			$this->SetFont(	$data[$i]['T_FONT'],
+							$data[$i]['T_TYPE'],
+							$data[$i]['T_SIZE']);
+
+			//Save the current position
+			$x=$this->GetX();
+			$y=$this->GetY();
+
+			//print the text
+			$this->MultiCellTable(
+					$data[$i]['CELL_WIDTH'],
+					$data[$i]['LN_SIZE'],
+					$data[$i]['TEXT'],
+					$data[$i]['BRD_TYPE'],
+					$data[$i]['T_ALIGN'],
+					$data[$i]['V_ALIGN'],
+					1,
+					$h - $data[$i]['LN_SIZE'] * $data[$i]['CELL_LINES']
+					);
+
+			//Put the position to the right of the cell
+			$this->SetXY($x + $data[$i]['CELL_WIDTH'],$y);
+
+			//if we have colspan, just ignore the next cells
+			if (isset($data[$i]['COLSPAN'])){
+				$i = $i + (int)$data[$i]['COLSPAN'] - 1;
+			}
+
+		}
+
+		$this->Data_On_Current_Page = true;
+
+		//Go to the next line
+		$this->Ln($h);
+	}
+
+	//if the table is bigger than a page then it jumps to next page and draws the header
+	/*
+	$h = is the height that if is overriden than the document jumps to a new page
+	$header = true/false = this specifies at a new page we write again the header or not. This variable
+	is used at the moment when the header draw makes the new page jump
+	*/
+
+	function CheckPageBreak($h, $header = true)
+	{
+		//If the height h would cause an overflow, add a new page immediately
+		if($this->GetY()+$h > $this->PageBreakTrigger){
+
+			$this->Draw_Table_Border();//draw the table border
+
+			$this->End_Page_Border();//if there is a special handling for end page??? this is specific for me
+
+			$this->AddPage($this->CurOrientation);//add a new page
+
+			$this->Data_On_Current_Page = false;
+
+			$this->New_Page_Commit = true;//new page commit
+
+			$this->table_startx = $this->GetX();
+			$this->table_starty = $this->GetY();
+			if ($header) $this ->Draw_Header();//if we have to draw the header!!!
+		}
+
+		//align the table
+		$this->Table_Align();
+	}
+
+	/**   This method returns the number of lines that will a text ocupy on the specified width
+	      Call:
+	      @param
+	                        $w - width
+	                        $txt - text
+	      @return           number
+	*/
+	function NbLines($w,$txt)
+	{
+		//Computes the number of lines a MultiCell of width w will take
+		$cw=&$this->CurrentFont['cw'];
+		if($w==0)
+			$w=$this->w-$this->rMargin-$this->x;
+		$wmax=($w-2*$this->cMargin)*1000/$this->FontSize;
+		$s=str_replace("\r",'',$txt);
+		$nb=strlen($s);
+		if($nb>0 and $s[$nb-1]=="\n")
+			$nb--;
+		$sep=-1;
+		$i=0;
+		$j=0;
+		$l=0;
+		$nl=1;
+		while($i<$nb)
+		{
+			$c=$s[$i];
+			if($c=="\n")
+			{
+				$i++;
+				$sep=-1;
+				$j=$i;
+				$l=0;
+				$nl++;
+				continue;
+			}
+			if($c==' ')
+				$sep=$i;
+			$l+=$cw[$c];
+			if($l>$wmax)
+			{
+				if($sep==-1)
+				{
+					if($i==$j)
+						$i++;
+				}
+				else
+					$i=$sep+1;
+				$sep=-1;
+				$j=$i;
+				$l=0;
+				$nl++;
+			}
+			else
+				$i++;
+		}
+		return $nl;
+	}
+
+
+	/**   This method allows printing text with line breaks.
+	      It works like a modified MultiCell
+	      Call:
+	      @param
+	                        $w - width
+	                        $h - line height
+	                        $txt - the outputed text
+	                        $border - border(LRTB 0 or 1)
+	                        $align - horizontal align 'JLR'
+	                        $fill - fill (1/0)
+	                        $vh - vertical adjustment - the Multicell Height will be with this VH Higher!!!!
+	                        $valign - Vertical Alignment - Top, Middle, Bottom
+	      @return           nothing
+	*/
+	function MultiCellTable($w, $h, $txt, $border=0, $align='J', $valign='T', $fill=0, $vh=0)
+	{
+
+		$b1 = '';//border for top cell
+		$b2 = '';//border for middle cell
+		$b3 = '';//border for bottom cell
+
+		if($border)
+		{
+			if($border==1)
+			{
+				$border = 'LTRB';
+				$b1 = 'LRT';//without the bottom
+				$b2 = 'LR';//without the top and bottom
+				$b3 = 'LRB';//without the top
+			}
+			else
+			{
+				$b2='';
+				if(is_int(strpos($border,'L')))
+					$b2.='L';
+				if(is_int(strpos($border,'R')))
+					$b2.='R';
+				$b1=is_int(strpos($border,'T')) ? $b2.'T' : $b2;
+				$b3=is_int(strpos($border,'B')) ? $b2.'B' : $b2;
+
+			}
+		}
+
+		switch ($valign){
+			case 'T':
+				$wh_T = 0;//Top width
+				$wh_B = $vh - $wh_T;//Bottom width
+				break;
+			case 'M':
+				$wh_T = $vh/2;
+				$wh_B = $vh/2;
+				break;
+			case 'B':
+				$wh_T = $vh;
+				$wh_B = 0;
+				break;
+			default://default is TOP ALIGN
+				$wh_T = 0;//Top width
+				$wh_B = $vh - $wh_T;//Bottom width
+		}
+
+		//save the X position
+		$x = $this->x;
+		/*
+			if $wh_T == 0 that means that we have no vertical adjustments so I will skip the cells that
+			draws the top and bottom borders
+		*/
+
+		if ($wh_T != 0)//only when there is a difference
+		{
+			//draw the top borders!!!
+			$this->Cell($w,$wh_T,'',$b1,2,$align,$fill);
+		}
+
+		$b2 = is_int(strpos($border,'T')) && ($wh_T == 0) ? $b2.'T' : $b2;
+		$b2 = is_int(strpos($border,'B')) && ($wh_B == 0) ? $b2.'B' : $b2;
+
+		$this->MultiCell($w,$h,$txt,$b2,$align,$fill);
+
+		if ($wh_B != 0){//only when there is a difference
+
+			//go to the saved X position
+			//a multicell always runs to the begin of line
+			$this->x = $x;
+
+			$this->Cell($w, $wh_B, '', $b3, 2, $align,$fill);
+
+			$this->x=$this->lMargin;
+		}
+
+	}	
+	
+	
+	
+	
+	
+	
+	
+	
+	//***************************************************************************************************************
+	//  LES FONCTIONS AJOUTEES PAR JC CORNIC
+	//***************************************************************************************************************
+
+	function setRepere($titre, $posX, $posY, $sizeX, $sizeY, $datasX, $datasY, $droites)
+	{
+
+		$espaceX=25;
+		$espaceY=30;
+
+		// Si le min=max alors on change le nombre de d&eacute;coupage d'ordonn&eacute;e
+		if ($datasY[0] == $datasY[1])
+			if ($datasY[0] == 0) $datasY[2] = 0;
+			else $datasY[2] = 1;
+
+		// Le titre
+		$this->SetXY($posX+$espaceX, $posY - 10);
+		$this->Cell($sizeX , 10, $titre, 0, 2, "C");
+
+		if (($datasY[1]-$datasY[0]) != 0)
+			$ratioY = $sizeY/($datasY[1]-$datasY[0]);
+		else $ratioY = abs($sizeY/(2*$datasY[0]));
+		
+		if ($datasY[0] < 0)
+			$decalageYNeg = $datasY[0]*$ratioY;
+		else $decalageYNeg = 0;
+
+
+		$this->SetDrawColor(0, 0, 0);
+		$this->Line($posX+$espaceX, $posY+$sizeY+$decalageYNeg, $posX+$espaceX+$sizeX, $posY+$sizeY+$decalageYNeg); // X
+		$this->Line($posX+$espaceX, $posY+$sizeY, $posX+$espaceX, $posY); // Y
+
+		$this->SetTextColor(0,0,0);
+		// Pour l'axe des X
+		switch (count($datasX))
+		{
+			case 1:
+				// Mettre la valeur au milieu de l'axe	
+				$this->SetXY($posX+$espaceX, $posY + $sizeY);
+				$this->Cell($sizeX, 10, $datasX[0], 0, 1, "C");
+			
+			break;
+			case 2:
+				// Mettre les deux valeurs en d&eacute;but et fin d'axe	
+				$this->Text($posX+$espaceX, $posY + $sizeY + 10, $datasX[0]);	
+				$this->Text($posX+$espaceX + $sizeX, $posY + $sizeY + 10, $datasX[1]);	
+			break;
+			default:
+			break;
+		}
+
+		// Pour l'axe des Y
+		$yPos = $posY + $sizeY;
+		$xPos = $posX+$espaceX - 12;
+		$value = $datasY[0];
+		$yInter = $sizeY / $datasY[2];
+		$valueInter = ($datasY[1] - $datasY[0]) / $datasY[2];
+
+		if ($datasY[2] == 5) //**** minY et maxY diff&eacute;rents ****//
+			for ($i=0 ; $i <= $datasY[2] ; $i++)
+			{
+				// Mettre les $i valeurs entre le d&eacute;but et la fin de l'axe
+				$this->Text($xPos, $yPos, $value);
+		
+				// Mettre les petites barres correspondantes...
+				$this->Line($posX+$espaceX-2, $yPos, $posX+$espaceX+2, $yPos);
+			
+				$yPos -= $yInter;
+				
+				if ($i==4) $value=$datasY[1];
+				else $value += $valueInter;
+			}
+		else //**** minY et maxY &eacute;gaux --> 1 ou 2 intervalles au lieu de 5
+		{
+			//**** Droite horizontale y=0
+			if ($datasY[0] == 0)
+			{
+				$this->Text($xPos, $yPos, $value);
+				$this->Line($posX-2, $yPos, $posX+2, $yPos);		
+			}
+			else //**** Droite horizontale y=$datasY[0]
+			{
+				if ($datasY[0] <0)
+				{
+					//**** Y=$datasY[0] < 0
+					$this->Text($xPos, $yPos, $value);
+					$this->Line($posX-2, $yPos, $posX+2, $yPos);		
+					
+					$yPos -= $yInter/2;
+					$value = 0;
+					
+					//**** Y=0
+					$this->Text($xPos, $yPos, $value);
+					$this->Line($posX-2, $yPos, $posX+2, $yPos);		
+				}
+				else	
+				{
+					//**** Y=0
+					$this->Text($xPos, $yPos, $value);
+					$this->Line($posX-2, $yPos, $posX+2, $yPos);		
+		
+					//**** Y=$datasY[0] > 0
+					$this->Text($xPos, $yPos, $value);
+					$this->Line($posX-2, $yPos, $posX+2, $yPos);		
+				}		
+			}
+		}
+
+		// Et on y met les droites...	
+		$legendX = $posX+$espaceX + $sizeX/2;
+		$legendY = $posY + $sizeY + 20;
+		for ($i=0 ; $i<count($droites) ; $i++)
+		{
+			
+//			$j=4*$i+1;
+//			$k=4*$i+2;
+//			$col=4*$i+3;
+//			$l=4*$i+4;
+
+			if ($datasY[0] != $datasY[1])
+			{
+				$y1 = $posY+$sizeY - ( ($droites[$i][0]-$datasY[0])*$sizeY/($datasY[1]-$datasY[0]));
+				$y2 = $posY+$sizeY - ( ($droites[$i][1]-$datasY[0])*$sizeY/($datasY[1]-$datasY[0]));
+			}
+			else
+			{
+				$y1 = $posY+$sizeY;
+				$y2 = $posY+$sizeY;
+			}
+
+
+			$this->SetDrawColor($droites[$i][2][0], $droites[$i][2][1], $droites[$i][2][2]);
+			$this->Line($posX+$espaceX, $y1, $posX+$sizeX, $y2);
+			
+			// ajouter la l&eacute;gende si elle doit �tre
+			if ($droites[$i][3] != "")
+			{
+				$this->Line($legendX - 20, $legendY, $legendX - 3, $legendY);
+				
+				$this->SetTextColor($droites[$i][2][0], $droites[$i][2][1], $droites[$i][2][2]);
+				$this->Text($legendX, $legendY, $droites[$i][3]);
+				$legendY += 5;
+			}
+		}	
+
+		// Et on encadre le repere...
+		$this->SetDrawColor(0,0,0);
+		$espace_Y = 15;
+		$this->Line($posX, $posY - $espace_Y, $posX+$espaceX + $sizeX + $espaceX, $posY - $espace_Y); // -Y
+		$this->Line($posX+$espaceX + $sizeX + $espaceX, $posY - $espace_Y, $posX+$espaceX + $sizeX + $espaceX, $posY + $sizeY + $espaceY); // +X
+		$this->Line($posX+$espaceX + $sizeX + $espaceX, $posY + $sizeY + $espaceY, $posX, $posY + $sizeY + $espaceY); // +Y
+		$this->Line($posX, $posY + $sizeY + $espaceY, $posX, $posY - $espace_Y); // -X
+	}
+
+	//***********************************************************************************************************
+	// Pour &eacute;crire un texte dans ue case... [BUI] pour le style de la police et [[LCR]] pour le centrage &eacute;ventuel
+	// Par d&eacute;fault, le texte sera normal et � gauche...
+	// Fonction destin&eacute;e � dessiner un tableau dans un file.pdf
+	function drawTableau(&$pdf, $tableType, $headerType, $headerDatas, $datasType, $datas)
+	{
+		$nbCol = count($headerDatas)/2;
+
+		//we initialize the table class
+		$pdf->Table_Init($nbCol, true, true);
+		
+		//***************************************************************************
+		//TABLE HEADER SETTINGS
+		//***************************************************************************
+		$table_subtype = $tableType;
+		$pdf->Set_Table_Type($table_subtype);
+
+		for($i=0; $i<$nbCol; $i++) 
+		{
+			$header_type[$i] = $headerType;
+			$header_type[$i]['WIDTH'] = $headerDatas[$i];
+
+			// Les contenus
+			$j = $nbCol+$i;
+			$header_type[$i]['TEXT'] = $headerDatas[$j];
+
+			// Si une donn&eacute;e == 0 alors on affiche rien...
+			if ($header_type[$i]['TEXT'] != "0") ;
+			else $header_type[$i]['TEXT'] = "";
+			
+			// par d&eacute;faut, le texte est centr&eacute; � gauche, non italic, non soulign&eacute; et non gras.
+			// par d&eacute;faut, les cellules ne sont pas fusionn&eacute;es.
+			$header_type[$i]['T_TYPE'] = '';
+			$header_type[$i]['T_ALIGN'] = '';		
+			$header_type[$i]['COLSPAN'] = "1";
+		}
+
+		// Si l'utilisateur veut un alignement sp&eacute;cifique pour la premi�re colonne. Sinon, T_ALIGN  prend le dessus...
+		if (isset($headerType['T_ALIGN_COL0']))
+			$header_type[0]['T_ALIGN'] = $headerType['T_ALIGN_COL0'];
+
+		// Si l'utilisateur veut un fond color&eacute; sp&eacute;cifique  pour la premi�re colonne. Sinon, BG_COLOR  prend le dessus...
+		if (isset($headerType['BG_COLOR_COL0']))
+			$header_type[0]['BG_COLOR'] = $headerType['BG_COLOR_COL0'];
+				
+		// Si l'utilisateur pr&eacute;cise un type ou un alignement pour une cellule pr&eacute;cise du tableau, on l'applique ici
+		// Il faut utiliser les balises [I], [B], [U] pour Italic, Bold et Underline
+		// Il faut utiliser les balises [L], [C], [R] pour left, centered et rigth
+		for($i=0; $i<$nbCol; $i++) 
+		{
+			if (sscanf($header_type[$i]['TEXT'], "[%[a-zA-Z]]%s", $balise, $reste) != 0)
+			{
+				//echo "balise = " . $balise;
+				if ( (strpos($balise, "I")===FALSE) && (strpos($balise, "B")===FALSE) && (strpos($balise, "U")===FALSE)
+				  && (strpos($balise, "L")===FALSE) && (strpos($balise, "C")===FALSE) && (strpos($balise, "R")===FALSE) )
+					; // Mauvaise balise ou l'utilisateur veut mettre des crochets dans son tableau, c'est son droit...
+				else
+				{
+					//echo "balise = " . $balise . "<br>";
+					// On teste les diff&eacute;rentes balises pour ajuster la cellule.
+					if (strpos($balise, "I") === FALSE) ;
+					else $header_type[$i]['T_TYPE'] .= 'I';
+					if (strpos($balise, "B") === FALSE) ;
+					else $header_type[$i]['T_TYPE'] .= 'B';
+					if (strpos($balise, "U") === FALSE) ;
+					else $header_type[$i]['T_TYPE'] .= 'U';
+					if (strpos($balise, "L") === FALSE) ;
+					else $header_type[$i]['T_ALIGN'] .= 'L';
+					if (strpos($balise, "C") === FALSE) ;
+					else $header_type[$i]['T_ALIGN'] .= 'C';
+					if (strpos($balise, "R") === FALSE) ;
+					else $header_type[$i]['T_ALIGN'] .= 'R';
+				}
+				
+				// On supprime la balise du texte de la cellule...
+				$header_type[$i]['TEXT'] = str_replace("[".$balise."]", "", $header_type[$i]['TEXT']);
+			}
+		}
+		// Si l'utilsateur ne veut pas de header pour son tableau, il met NULL dans la premiere cellule...
+		if ($header_type[0]['TEXT'] == NULL)
+		{
+			for($i=0; $i<$nbCol; $i++)
+			{
+				$header_type[$i]['LN_SIZE'] = 0;
+				$header_type[$i]['TEXT'] = "";
+			}
+		}
+		
+
+		// Test si l'utilisateur veut fusionner DEUX cellules dans le header de son tableau. Il doit mettre "COLSPAN2" dans la premi�re cellule � fusionner.
+		for($i=0 ; $i<$nbCol ; $i++)
+		{
+			$k=$nbCol+$i;
+			$i_1 = $i-1;
+			if ( ($k<count($headerDatas)) && ($headerDatas[$k] === "COLSPAN2") )
+			{
+				$header_type[$i_1]['COLSPAN'] = "2";
+				$header_type[$i]['TEXT']= "";
+			}
+		}
+
+		//set the header type
+		$pdf->Set_Header_Type($header_type);
+		$pdf->Draw_Header();
+		
+		//***************************************************************************
+		//TABLE DATA SETTINGS
+		//***************************************************************************		
+		$data_type = Array();//reset the array
+		for ($i=0; $i<$nbCol; $i++) $data_type[$i] = $datasType;
+		$pdf->Set_Data_Type($data_type);
+		
+		//*********************************************************************
+		// Ce qui suit est valable pour toutes les cellules du tableau (hors header bien entendu).
+		//*********************************************************************
+		$data = Array();
+		for ($i=0 ; $i<count($datas) ; $i+=$nbCol)
+		{
+			//*********************************************************************
+			// Ce qui suit est valable pour la premi�re colonne du tableau
+			//*********************************************************************
+			// si l'utilisateur a pr&eacute;cis&eacute; un alignement pour la premi�re colonne, on l'applique ici
+			if (isset($datasType['T_ALIGN_COL0']))
+				$data[0]['T_ALIGN'] = $datasType['T_ALIGN_COL0'];
+				
+			// Si l'utilisateur a pr&eacute;cis&eacute; une couleur de fond pour la premi�re colonne, on l'applique ici.
+			if (isset($datasType['BG_COLOR_COL0']))
+				$data[0]['BG_COLOR'] = $datasType['BG_COLOR_COL0'];
+				
+			for ($j=$i ; $j<$i+$nbCol ; $j++)
+			{
+				$k = $j-$i;
+				$data[$k]['TEXT'] = $datas[$j];
+				$data[$k]['T_SIZE'] = $datasType['T_SIZE'];
+				$data[$k]['LN_SIZE'] = $datasType['LN_SIZE'];
+				
+				// par d&eacute;faut, le texte est centr&eacute; � gauche, non italic, non soulign&eacute; et non gras.
+				// par d&eacute;faut, les cellules ne sont pas fusionn&eacute;es.
+				$data[$k]['T_TYPE'] = '';
+				$data[$k]['T_ALIGN'] = '';		
+				$data[$k]['COLSPAN'] = "1";
+					
+				// Si l'utilisateur a pr&eacute;cis&eacute; une couleur de fond pour les autres colonnes, on l'applique ici.
+				if ( (isset($datasType['BG_COLOR'])) && ($k!=0) )
+					$data[$k]['BG_COLOR'] = $datasType['BG_COLOR'];
+				
+				// Si l'utilisateur pr&eacute;cise un type ou un alignement pour une cellule pr&eacute;cise du tableau, on l'applique ici
+				// Il faut utiliser les balises [I], [B], [U] pour Italic, Bold et Underline
+				// Il faut utiliser les balises [L], [C], [R] pour left, centered et rigth
+				if (sscanf($data[$k]['TEXT'], "[%[a-zA-Z]]%s", $balise, $reste) != 0)
+				{
+					//echo "balise = " . $balise;
+					if ( (strpos($balise, "I")===FALSE) && (strpos($balise, "B")===FALSE) && (strpos($balise, "U")===FALSE)
+					  && (strpos($balise, "L")===FALSE) && (strpos($balise, "C")===FALSE) && (strpos($balise, "R")===FALSE) )
+						; // Mauvaise balise ou l'utilisateur veut mettre des crochets dans son tableau, c'est son droit...
+					else
+					{
+						//echo "balise = " . $balise . "<br>";
+						// On teste les diff&eacute;rentes balises pour ajuster la cellule.
+						if (strpos($balise, "I") === FALSE) ;
+						else $data[$k]['T_TYPE'] .= 'I';
+						if (strpos($balise, "B") === FALSE) ;
+						else $data[$k]['T_TYPE'] .= 'B';
+						if (strpos($balise, "U") === FALSE) ;
+						else $data[$k]['T_TYPE'] .= 'U';
+						if (strpos($balise, "L") === FALSE) ;
+						else $data[$k]['T_ALIGN'] .= 'L';
+						if (strpos($balise, "C") === FALSE) ;
+						else $data[$k]['T_ALIGN'] .= 'C';
+						if (strpos($balise, "R") === FALSE) ;
+						else $data[$k]['T_ALIGN'] .= 'R';
+					}
+					
+					// On supprime la balise du texte de la cellule...
+					$data[$k]['TEXT'] = str_replace("[".$balise."]", "", $data[$k]['TEXT']);
+				}
+
+				// Si la valeur de la cellule est 0, le choix a &eacute;t&eacute; fait ICI de ne rien mettre dans la cellule.
+				if ($data[$k]['TEXT'] == "0")
+					$data[$k]['TEXT'] ="";
+					
+				// Test si l'utilisateur veut fusionner deux cellules dans le header de son tableau. Il doit mettre le contenu
+				// de la cellule fusionn&eacute;e dans la premi�re cellule et "COLSPAN2" dans la deuxi�me cellule.
+				if ( ($k<$nbCol) && ($data[$k]['TEXT'] === "COLSPAN2") )
+				{
+					$k_1 = $k-1;
+					$data[$k_1]['COLSPAN'] = "2";
+					$data[$k]['TEXT']= "";
+				}				
+			}
+			$pdf->Draw_Data($data);
+		}
+		
+		$pdf->Draw_Table_Border();
+	}
+}
+?>
\ No newline at end of file
diff --git a/misc/PublicationBulletins/README.txt b/misc/PublicationBulletins/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4008fd147920166dd3e1cb801c9f854645ff5b68
--- /dev/null
+++ b/misc/PublicationBulletins/README.txt
@@ -0,0 +1,33 @@
+
+Exemples de code pour publier les bulletins de notes des étudiants
+
+
+Attention: ces codes ne font PAS partie de ScoDoc et ne sont pas du
+tout supportés par l'équipe de ScoDoc. Aucune garantie de
+fonctionnement ou de sécurité.
+
+Pour toute question, contacter les auteurs ou la liste de diffusion.
+
+
+1) Dans ExemplePHP, deux exemples de code récupérant depuis ScoDoc et affichant les
+bulletins des étudiants.
+
+La version index-abs.php affiche en sus les absences.
+
+Auteur: Yann Leboulanger (Université Paris 10)
+
+
+2) Portail-LeHavre reprend le même code en l'améliorant et avec de la documentation.
+
+Auteurs: Code contribué par Yann Leboulanger (Université Paris 10), Juin 2013
+Modifié par D. SOUDIERE avec le concours de Catherine Hatinguais
+
+
+3) Bulletins-Orleans est une autre amélioration
+permettant la publication des notes et absences (si elle sont saisies) des étudiants.
+Possibilité de choisir l'affichage des notes ou des moyennes.
+Modification de la feuille de style permettant une meilleure
+visibilité.
+Auteur: Pascal Legrand <pascal.legrand@univ-orleans.fr>
+
+
diff --git a/misc/Restore-database.txt b/misc/Restore-database.txt
new file mode 100644
index 0000000000000000000000000000000000000000..89fcfcc7f974c69f3238648fb337865711e59740
--- /dev/null
+++ b/misc/Restore-database.txt
@@ -0,0 +1,69 @@
+
+Notes sur la restauration de la base SQL complete
+(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
+
+On obtient un fichier texte assez volumineux (on peut utiliser gzip pour le compresser avant transfert).
+
+Le copier sur la machine destination.
+
+2) Sur la machine destination:
+
+ Avant toute chose, stopper scodoc:
+ /etc/init.d/scodoc stop
+
+ 1.1) Supprimer toutes les bases ScoDoc existantes s'il y en a:
+     su postgres
+     psql -l
+    liste les bases: celles de ScoDoc sont SCO* 
+
+   Pour chaque base SCO*, faire dropdb
+    dropdb SCOUSERS
+    dropdb SCOGEII
+    ...
+
+ Pour les feignants, 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.3) Recopier les fichiers (photos, config, archives): copier le repertoire complet
+   /opt/scodoc/instance/var
+  de la machine origine vers la nouvelle
+
+
+Puis redemarrer ScoDoc:
+  en tant que root: /etc/init.d/scodoc start
+
+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
+
+
+----
+Cas d'une seule base à copier: (eg un seul département, mais faire
+attention aux utilisateurs definis dans la base SCOUSERS):
+
+En tant qu'utilisateur "postgres":
+Dump: (script avec commande de creation de la base)
+pg_dump --create SCOINFO > /tmp/scoinfo.dump
+
+Restore: (si necessaire, utiliser dropdb avant)
+psql -f /tmp/scoinfo.dump postgres
+
+---
+Cas d'un dump via sco_dump_db (assistance):
+createdb -E UTF-8 SCOXXX
+zcat xxx | psql SCOXXX
+
diff --git a/misc/SuppressAccents.py b/misc/SuppressAccents.py
new file mode 100644
index 0000000000000000000000000000000000000000..3b119e593e97f504b7185894bd44a0cc71d266df
--- /dev/null
+++ b/misc/SuppressAccents.py
@@ -0,0 +1,78 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Suppression des accents d'une chaine
+
+Source: http://wikipython.flibuste.net/moin.py/JouerAvecUnicode#head-1213938516c633958921591439c33d202244e2f4
+"""
+
+_reptable = {}
+def _fill_reptable():
+    _corresp = [
+        (u"A",  [0x00C0,0x00C1,0x00C2,0x00C3,0x00C4,0x00C5,0x0100,0x0102,0x0104]),
+        (u"AE", [0x00C6]),
+        (u"a",  [0x00E0,0x00E1,0x00E2,0x00E3,0x00E4,0x00E5,0x0101,0x0103,0x0105]),
+        (u"ae", [0x00E6]),
+        (u"C",  [0x00C7,0x0106,0x0108,0x010A,0x010C]),
+        (u"c",  [0x00E7,0x0107,0x0109,0x010B,0x010D]),
+        (u"D",  [0x00D0,0x010E,0x0110]),
+        (u"d",  [0x00F0,0x010F,0x0111]),
+        (u"E",  [0x00C8,0x00C9,0x00CA,0x00CB,0x0112,0x0114,0x0116,0x0118,0x011A]),
+        (u"e",  [0x00E8,0x00E9,0x00EA,0x00EB,0x0113,0x0115,0x0117,0x0119,0x011B]),
+        (u"G",  [0x011C,0x011E,0x0120,0x0122]),
+        (u"g",  [0x011D,0x011F,0x0121,0x0123]),
+        (u"H",  [0x0124,0x0126]),
+        (u"h",  [0x0125,0x0127]),
+        (u"I",  [0x00CC,0x00CD,0x00CE,0x00CF,0x0128,0x012A,0x012C,0x012E,0x0130]),
+        (u"i",  [0x00EC,0x00ED,0x00EE,0x00EF,0x0129,0x012B,0x012D,0x012F,0x0131]),
+        (u"IJ", [0x0132]),
+        (u"ij", [0x0133]),
+        (u"J",  [0x0134]),
+        (u"j",  [0x0135]),
+        (u"K",  [0x0136]),
+        (u"k",  [0x0137,0x0138]),
+        (u"L",  [0x0139,0x013B,0x013D,0x013F,0x0141]),
+        (u"l",  [0x013A,0x013C,0x013E,0x0140,0x0142]),
+        (u"N",  [0x00D1,0x0143,0x0145,0x0147,0x014A]),
+        (u"n",  [0x00F1,0x0144,0x0146,0x0148,0x0149,0x014B]),
+        (u"O",  [0x00D2,0x00D3,0x00D4,0x00D5,0x00D6,0x00D8,0x014C,0x014E,0x0150]),
+        (u"o",  [0x00F2,0x00F3,0x00F4,0x00F5,0x00F6,0x00F8,0x014D,0x014F,0x0151]),
+        (u"OE", [0x0152]),
+        (u"oe", [0x0153]),
+        (u"R",  [0x0154,0x0156,0x0158]),
+        (u"r",  [0x0155,0x0157,0x0159]),
+        (u"S",  [0x015A,0x015C,0x015E,0x0160]),
+        (u"s",  [0x015B,0x015D,0x015F,0x01610,0x017F]),
+        (u"T",  [0x0162,0x0164,0x0166]),
+        (u"t",  [0x0163,0x0165,0x0167]),
+        (u"U",  [0x00D9,0x00DA,0x00DB,0x00DC,0x0168,0x016A,0x016C,0x016E,0x0170,0x172]),
+        (u"u",  [0x00F9,0x00FA,0x00FB,0x00FC,0x0169,0x016B,0x016D,0x016F,0x0171]),
+        (u"W",  [0x0174]),
+        (u"w",  [0x0175]),
+        (u"Y",  [0x00DD,0x0176,0x0178]),
+        (u"y",  [0x00FD,0x00FF,0x0177]),
+        (u"Z",  [0x0179,0x017B,0x017D]),
+        (u"z",  [0x017A,0x017C,0x017E]),
+        (u"2",  [0x00B2]), # deux exposant
+        (u" ",  [0x00A0]), # &nbsp
+        (u"",  [0xB0]), # degre
+        (u"",  [0xA9]), # copyright
+        (u"1/2",  [0xBD]), # 1/2
+        ]
+    global _reptable
+    for repchar,codes in _corresp :
+        for code in codes :
+            _reptable[code] = repchar
+
+_fill_reptable()
+def suppression_diacritics(s) :
+    """Suppression des accents et autres marques.
+
+    @param s: le texte à nettoyer.
+    @type s: str ou unicode
+    @return: le texte nettoyé de ses marques diacritiques.
+    @rtype: unicode
+    """
+    if isinstance(s,str) :
+        s = unicode(s,"utf8","replace")
+    return s.translate(_reptable)
diff --git a/misc/backup_db b/misc/backup_db
new file mode 100755
index 0000000000000000000000000000000000000000..c40759c6bb7904274c6786471f55cf9c336ecb23
--- /dev/null
+++ b/misc/backup_db
@@ -0,0 +1,77 @@
+#!/bin/bash
+
+# usage: backup_db dbname
+# Dump une base postgresql, et garde plusieurs dumps dans le passe
+# Les dumps sont compresses (gzip).
+#
+# E. Viennet pour ScoDoc, 2005-2011
+
+# Note: pour restaurer un backup (en supprimant la base existante !!!)
+# 1- supprimer la base existante si elle existe (dropdb)
+# 2- recreer la base, vide: createdb -E UTF-8 NOMDELABASE
+# 3- pg_restore  --create -d SCOINFO SCOINFO_pgdump
+
+
+# Nombre de copies a conserver dans le passe:
+NUMBACK=500
+
+
+DBNAME=$1
+DUMPBASE=BACKUP-"$DBNAME"
+DUMPFILE="$DUMPBASE"/"$DBNAME"_pgdump
+
+function remove_leadings_zeros {
+   n=$1
+   if [ $n = "0000" ]; then
+      echo 0
+   else
+     while [ "${n:0:1}" = '0' ]; do
+        n=${n:1}
+     done
+     echo $n 
+   fi
+}
+
+# Check if base pathname for backups exist
+if [ ! -e ${DUMPBASE} ]; then
+    mkdir ${DUMPBASE}
+fi
+
+
+# 1- rotate previous dumps
+if [ -e ${DUMPFILE}.gz ]
+then
+  LIST=$(ls -r1 ${DUMPFILE}*)
+fi
+
+
+for i in $LIST; do
+   x=${i#*.}
+   num=${x%.*} # just the number
+   num=$(remove_leadings_zeros $num)
+   if [ "$i" != "$DUMPFILE".gz ]; then
+      if [ $num -lt $NUMBACK ]; then
+        # rename file
+        base=${i%%.*}	
+	ni=$(($num+1))
+	mv $i "$base".$(printf "%04d" $ni).gz
+      else
+        # remove file
+        rm $i
+      fi
+   fi
+done
+
+# rename last dump to .0000
+if [ -e "$DUMPFILE".gz ]
+then
+   mv "$DUMPFILE".gz "$DUMPFILE.0000.gz"
+fi
+
+# 2- dump
+#pg_dump "$DBNAME" > $DUMPFILE
+
+pg_dump --format=t "$DBNAME" -f $DUMPFILE
+
+gzip $DUMPFILE
+
diff --git a/misc/backup_db2 b/misc/backup_db2
new file mode 100755
index 0000000000000000000000000000000000000000..f34c19f5ef47213b0ad4285c8fbb37e9f5b86499
--- /dev/null
+++ b/misc/backup_db2
@@ -0,0 +1,53 @@
+#!/bin/bash
+
+# usage: backup_db2 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
+# (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
+#
+# Puis en tant qu'utilisateur postgres:  su postgres
+# 1- supprimer la base existante si elle existe: dropdb SCOXXX
+#
+# 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
+#
+# Revenir a l'utilisateur root: exit
+# 4- Relancer scodoc: /etc/init.d/scodoc start
+
+DBNAME=$1
+DUMPBASE="$DBNAME"-BACKUPS
+DUMPFILE="$DUMPBASE"/incoming/"$DBNAME"_pgdump
+
+
+# 1-Check if base pathname for backups exist
+if [ ! -e ${DUMPBASE} ]; then
+    mkdir ${DUMPBASE}
+fi
+
+# Initialize subdirs if needed
+for s in incoming backup.hourly backup.daily backup.weekly backup.monthly
+do
+    if [ ! -e ${DUMPBASE}/"$s" ]; then
+        mkdir ${DUMPBASE}/"$s"
+    fi
+done
+
+
+# 2- Dump postgresql data
+pg_dump --format=t "$DBNAME" -f $DUMPFILE
+
+gzip $DUMPFILE
+
+# 3- Rotate backups: remove unneeded copies
+/opt/scodoc/Products/ScoDoc/misc/backup_rotation.sh "$DUMPBASE"
diff --git a/misc/backup_rotation.sh b/misc/backup_rotation.sh
new file mode 100755
index 0000000000000000000000000000000000000000..351a1e365fe7dc2844c47d4d648303a7d392f5ff
--- /dev/null
+++ b/misc/backup_rotation.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+# Backup rotation
+# Usage example: backup_rotation.sh /var/lib/postgresql/BACKUP-SCOGEII
+#
+# This script is designed to run each hour
+#
+# E. Viennet 2014, loosely inspired by  Julius Zaromskis, 
+#    http://nicaw.wordpress.com/2013/04/18/bash-backup-rotation-script/
+
+# Storage folder where to move backup files
+# Must contain backup.monthly backup.weekly backup.daily backup.hourly folders
+storage="$1"
+
+NB_HOURLY=48   # nb de sauvegardes horaires a conserver (1 par heure)
+NB_DAILY=40    # nb de sauvegardes quotidiennes a conserver
+NB_WEEKLY=30   # nombre de sauvegardes hebdomadaires a conserver
+NB_MONTHLY=200 # nombre de sauvegardes mensuelles a conserver
+
+# Work in backup directory:
+cd $storage
+
+# Source folder where files are backed
+source="incoming"
+
+# Destination file names
+date_daily=$(date -u +"%Y-%m-%d")
+date_hourly=$(date -u +"%Y-%m-%dT%H:%M")
+
+# Get current month and week day number
+month_day=$(date -u +"%d")
+week_day=$(date -u +"%u")
+hour=$(date -u +"%H")
+
+# Optional check if source files exist. Email if failed.
+#if [ ! -f $source/archive.tgz ]; then
+#ls -l $source/ | mail your@email.com -s "[backup script] Daily backup failed! Please check for missing files."
+#fi
+
+# We take files from source folder and move them to
+# the appropriate destination folder:
+
+# On first month day do (once)
+if [ "$month_day" -eq 1 ] && [ ! -e backup.monthly/$date_daily ]; then
+  destination=backup.monthly/$date_daily
+else
+  # On sunday do (once)
+  if [ "$week_day" -eq 7 ] && [ ! -e backup.weekly/$date_daily ]; then
+    destination=backup.weekly/$date_daily
+  else
+    if [ "$hour" -eq 0 ] ; then
+      # On any regular day just after midnight do
+      destination=backup.daily/$date_daily
+     else
+      # Each hour do:
+      destination=backup.hourly/$date_hourly
+     fi
+  fi
+fi
+
+# Move the files
+mkdir $destination
+mv $source/* $destination
+
+# hourly - keep NB_HOURLY 
+m=$(($NB_HOURLY * 60))
+find ./backup.hourly  -maxdepth 1 -mmin +"$m" -type d -exec /bin/rm -r {} \;
+
+# daily - keep for NB_DAILY days
+find ./backup.daily/ -maxdepth 1 -mtime +"$NB_DAILY" -type d -exec /bin/rm -r {} \;
+
+# weekly - keep for NB_WEEKLY days
+d=$(($NB_WEEKLY * 7))
+find ./backup.weekly/ -maxdepth 1 -mtime +"$d" -type d -exec /bin/rm -r {} \;
+
+# monthly - keep for NB_MONTHLY days (approx: 30 days/month)
+d=$(($NB_MONTHLY * 30))
+find ./backup.monthly/ -maxdepth 1 -mtime +"$d" -type d -exec /bin/rm -r {} \;
diff --git a/misc/backup_to_remote_server.sh b/misc/backup_to_remote_server.sh
new file mode 100644
index 0000000000000000000000000000000000000000..2509ab6aff921021a963ab1d56d79e0d6266047f
--- /dev/null
+++ b/misc/backup_to_remote_server.sh
@@ -0,0 +1,95 @@
+#!/bin/bash
+
+# Script effectuant un backup des principaux repertoires
+# du serveur ScoDoc vers une machine distante.
+# utilise rsync (pour ne cpier que les fichiers modifies)
+# et ssh (pour se connecter de faon securisee).
+#
+# L'utilisateur root du serveur scodoc (qui execute ce script)
+# doit pouvoir se conencter directement (sans mot de passe) sur
+# la machine distante (installer les cles ssh necessaires).
+#
+# A adapter a vos besoins. Utilisation a vos risques et perils.
+#
+# E. Viennet, 2002
+
+# Installation:
+# 1- Installer rsync:
+#     apt-get install rsync
+# 2- mettre en place un job cron:
+#     par exemple copier ce script dans /etc/cron.daily/
+#    (et le rendre executable avec chmod +x ...)
+
+# -------------------- CONFIGURATION A ADAPTER
+
+remotehost=XXXX # nom ou IP du serveur de sauvegarde
+destdir=/home/SAU-SCODOC # repertoire sur serveur de sauvegarde
+
+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`
+
+# -----------------------------------------------------
+
+# ----------------------------------
+# Subroutine to terminate abnormally
+# ----------------------------------
+terminate()
+{
+ dateTest=`date`
+
+ mail -s "Attention: Probleme sauvegarde ScoDoc" $SUPERVISORMAIL <<EOF
+The execution of script $CALLER was not successful on $MACHINE.
+
+Look at logfile $logfile"
+
+$CALLER terminated, exiting now with rc=1."
+
+EOF
+ 
+# repeat message for logs...
+ echo "The execution of script $CALLER was not successful on $MACHINE."
+ echo
+ echo  "Look at logfile $logfile"
+ echo
+ echo "$CALLER terminated, exiting now with rc=1."
+ dateTest=`date`
+ echo "End of script at: $dateTest"
+ echo ""
+
+ exit 1
+}
+
+# --------------------------------------
+# Subroutine to mirror a dir using rsync (mirror on REMOTE HOST)
+# Variables:
+#   remotehost : hostname on which is the mirror
+#   srcdir     : directory to mirror on local host 
+#   destdir    : directory on remotehost where to put the copy
+#   logfile    : filename to log actions
+# --------------------------------------
+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
+  if [ $? -ne 0 ]
+  then
+    echo Error in rsync: code=$?
+    terminate
+  fi
+
+  echo "ending at" `date` >> $logfile 2>&1
+  echo "---------------"  >> $logfile 2>&1
+}
+
+
+
+# ----------- REPERTOIRES A SAUVEGARDER:
+for srcdir in /etc /home /root /opt /usr/local /var; do
+    rsync_mirror_to_remote
+done
diff --git a/misc/change_enseignant.py b/misc/change_enseignant.py
new file mode 100755
index 0000000000000000000000000000000000000000..d8d3bc0cf201a79b0b87fba14ce8cb3416711f10
--- /dev/null
+++ b/misc/change_enseignant.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""Change un identifiant d'enseignant (pour corriger une erreur, typiquement un doublon)
+
+(à lancer en tant qu'utilisateur postgres)
+Emmanuel Viennet, 2007 - 2014
+"""
+
+import pdb, os, sys
+import psycopg2
+
+
+if len(sys.argv) != 4:
+    print 'Usage: %s database ancien_utilisateur nouvel_utilisateur' % sys.argv[0]
+    print 'Exemple: change_enseignant.py SCOGEII toto tata'
+    sys.exit(1)
+
+dbname = sys.argv[1]
+OLD_ID = sys.argv[2]
+NEW_ID = sys.argv[3]
+
+DBCNXSTRING = 'dbname=%s' % dbname
+
+# Confirmation
+ans = raw_input("Remplacer le l'utilisateur %s par %s dans toute la base du departement %s ?"
+                % (OLD_ID, NEW_ID, dbname)).strip()
+if not ans or ans[0].lower() not in 'oOyY':
+    print 'annulation'
+    sys.exit(-1)
+
+
+cnx = psycopg2.connect( DBCNXSTRING )
+
+cursor = cnx.cursor()
+req = "update %s set %s=%%(new_id)s where %s=%%(old_id)s"
+args = { 'old_id' : OLD_ID, 'new_id' : NEW_ID }
+
+tables_attr = {
+    'notes_formsemestre' : 'responsable_id',
+    'entreprise_contact' : 'enseignant',
+    'admissions' : 'rapporteur',
+    'notes_moduleimpl' : 'responsable_id',
+    'notes_modules_enseignants' : 'ens_id',
+    'notes_notes' : 'uid',
+    'notes_notes_log' : 'uid',
+    'notes_appreciations' : 'author',           
+    }
+
+for (table, attr) in tables_attr.items():
+    cursor.execute(req % (table, attr, attr), args)
+    print 'table %s:  %s' % (table, cursor.statusmessage)
+
+cnx.commit()
+
+
+
diff --git a/misc/change_etudid.py b/misc/change_etudid.py
new file mode 100644
index 0000000000000000000000000000000000000000..eef410195b4e8bb96cd6866ea40c31f3be0a6916
--- /dev/null
+++ b/misc/change_etudid.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""Change un etudid
+
+Suite a de fausses manips, il arrive que l'on est des "doublons":
+le même étudiant est enregistré sous deux "etudid" différents !
+
+Normalement, l'utilisation d'imports basés sur le code NIP (Apogée)
+évite le problème (qui ne se pose qu'en cas d'inscriptions manuelles
+mal gérées).
+
+Ce script permet de changer un etudid, typiquement pour associer à un
+etudiant le code d'un autre étudiant (son doublon).
+
+Ne traite que les inscriptions, les notes, absences, annotations, mais
+évidemment pas les tables uniques (identité, adresse, admission).
+
+Attention: script a lancer en tant que "www-data", avec ScoDoc arrete
+et postgresql lance
+
+Emmanuel Viennet, 2007
+"""
+
+import pdb,os,sys,psycopg2
+
+
+DBCNXSTRING = 'dbname=SCOXXX' 
+
+# exemple:
+OLD_ETUDID = 'EID1512' # etudid qui est en double (que l'on supprime)
+NEW_ETUDID = '10500686' # etudid destination (celui d'origine)
+
+cnx = psycopg2.connect( DBCNXSTRING )
+
+cursor = cnx.cursor()
+req = "update %s set etudid=%%(new_etudid)s where etudid=%%(old_etudid)s"
+args = { 'old_etudid' : OLD_ETUDID, 'new_etudid' : NEW_ETUDID }
+
+tables = ( 'absences', 
+           'absences_notifications',
+           'billet_absence',
+           'scolog',
+           'etud_annotations',
+           'entreprise_contact',
+           'notes_formsemestre_inscription',
+           'notes_moduleimpl_inscription',
+           'notes_notes', 'notes_notes_log',
+           'scolar_events',
+           'scolar_formsemestre_validation',
+           'scolar_autorisation_inscription',
+           'notes_appreciations',
+           # nouvelles absences
+           #'abs_absences',
+           #'abs_presences',
+           #'abs_justifs',
+           )
+
+for table in tables:
+    cursor.execute(req % table, args)
+    print 'table %s:  %s' % (table, cursor.statusmessage)
+
+cnx.commit()
+
+
+
diff --git a/misc/count_inscriptions.py b/misc/count_inscriptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..98afe99d41e81a991993de74d3b05129b150e4c1
--- /dev/null
+++ b/misc/count_inscriptions.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""Affiche nombre d'inscriptions aux semestres pour chaque etudiant
+
+   et supprime les etudiants jamais inscrits ayant un homonyme exact
+   (erreur passage GEA, fev 2007)
+"""
+
+import pdb,os,sys,psycopg
+import csv
+
+DBCNXSTRING = 'host=localhost user=scogea dbname=SCOXXXX password=XXXXX'
+
+SCO_ENCODING = 'utf-8'
+
+cnx = psycopg.connect( DBCNXSTRING )
+
+cursor = cnx.cursor()
+cursor.execute("select * from identite i order by nom")
+R = cursor.dictfetchall()
+
+nzero = 0
+nhomonoins = 0
+print 'etudid, nom, prenom, nb_inscriptions'
+for e in R:
+    cursor.execute("select count(*) from notes_formsemestre_inscription where etudid=%(etudid)s", { 'etudid' : e['etudid'] } )
+    nbins = cursor.fetchone()[0]
+    if nbins == 0:
+        nzero += 1
+        # recherche homonyme
+        cursor.execute("select * from identite i where nom=%(nom)s and prenom=%(prenom)s", e )
+        H = cursor.dictfetchall()
+        if len(H) == 2:
+            nhomonoins += 1            
+            print e['etudid'], e['nom'], e['prenom'], nbins
+            # etudiant non inscrit ayant un homonyme exact:
+            #  il doit etre supprimé !!!            
+            #cursor.execute("delete from admissions where etudid=%(etudid)s", e)
+            #cursor.execute("delete from identite where etudid=%(etudid)s", e)
+
+cnx.commit()
+
+print '= %d etudiants, %d jamais inscrits, %d avec homo' % (len(R), nzero, nhomonoins)
diff --git a/misc/create_user_table.sql b/misc/create_user_table.sql
new file mode 100644
index 0000000000000000000000000000000000000000..363a69436e268a85e99cd9a4f0dccdd0738ca354
--- /dev/null
+++ b/misc/create_user_table.sql
@@ -0,0 +1,27 @@
+
+CREATE SEQUENCE sco_users_idgen;
+
+CREATE FUNCTION sco_users_newid( text ) returns text as '
+	select $1 || to_char(  nextval(''sco_users_idgen''), ''FM999999999'' ) 
+	as result;
+	' language SQL;
+
+
+-- Source pour Zope User Folder
+
+CREATE TABLE sco_users (
+	user_id text default sco_users_newid('U') PRIMARY KEY,
+	user_name text unique,
+	passwd text not null,
+	roles text,
+	date_modif_passwd date default now(),
+	nom text,
+	prenom text,
+        email text,
+	dept text, -- departement d'appartenance
+	passwd_temp int default 0, -- 0 ok, 1 mot de passe temporaire
+	status text default NULL, -- NULL actif, 'old' ancien (pas de login possible)
+	date_expiration date, -- date limite, NULL => sans limite
+    login_edt text default NULL -- login logiciel emplois du temps (pour decodage ics)
+) with oids;
+
diff --git a/misc/createtables.sql b/misc/createtables.sql
new file mode 100644
index 0000000000000000000000000000000000000000..8eeea15417257403fb263c355676e74f129fa7ca
--- /dev/null
+++ b/misc/createtables.sql
@@ -0,0 +1,657 @@
+
+-- Creation des tables pour gestion notes
+-- E. Viennet, Sep 2005
+
+
+
+-- creation de la base: utiliser le script config/create_dept.sh 
+--
+--  ou pour tester: en tant qu'utilisateur postgres
+--     createuser --pwprompt scogea
+--     createdb -E UTF-8 -O scogea SCOGEA "scolarite GEA"
+--
+--
+
+-- generation des id
+CREATE SEQUENCE serial;
+CREATE SEQUENCE notes_idgen;
+
+CREATE FUNCTION notes_newid( text ) returns text as '
+	select $1 || to_char(  nextval(''notes_idgen''), ''FM999999999'' ) 
+	as result;
+	' language SQL;
+
+CREATE SEQUENCE notes_idgen2;
+
+CREATE FUNCTION notes_newid2( text ) returns text as '
+	select $1 || to_char(  nextval(''notes_idgen2''), ''FM999999999'' ) 
+	as result;
+	' language SQL;
+
+CREATE SEQUENCE notes_idgen_etud;
+
+CREATE FUNCTION notes_newid_etud( text ) returns text as '
+	select $1 || to_char(  nextval(''notes_idgen_etud''), ''FM999999999'' ) 
+	as result;
+	' language SQL;
+
+-- Preferences
+CREATE TABLE sco_prefs (
+    pref_id text DEFAULT notes_newid('PREF'::text) UNIQUE NOT NULL,
+    name text NOT NULL,
+    value text,
+    formsemestre_id text default NULL,
+    UNIQUE(name,formsemestre_id)
+) WITH OIDS;
+
+
+CREATE TABLE identite (
+    etudid text DEFAULT notes_newid_etud('EID'::text) UNIQUE NOT NULL,
+    nom text,
+    prenom text,
+    sexe text,
+    date_naissance date, -- new: date en texte
+    lieu_naissance text,
+    dept_naissance text,
+    nationalite text,   
+    statut text, -- NULL ou 'SALARIE' 
+    foto text, -- deprecated
+    photo_filename text,
+    code_nip text UNIQUE, -- code NIP Apogee (may be null)
+    code_ine text UNIQUE,  -- code INE Apogee (may be null)   
+    nom_usuel text, -- optionnel (si present, affiché à la place du nom)
+    boursier text -- 'O' (capital o) si boursier
+)  WITH OIDS;
+
+CREATE TABLE adresse (
+    adresse_id text DEFAULT notes_newid_etud('ADR'::text) NOT NULL,
+    etudid text NOT NULL,
+    email text, -- email institutionnel
+    emailperso text, -- email personnel (exterieur)    
+    domicile text,
+    codepostaldomicile text,
+    villedomicile text,
+    paysdomicile text,
+    telephone text,
+    telephonemobile text,
+    fax text,
+    typeadresse text DEFAULT 'domicile'::text NOT NULL,
+    entreprise_id integer,
+    description text
+) WITH OIDS;
+
+CREATE TABLE admissions (
+    adm_id text DEFAULT notes_newid_etud('ADM'::text) NOT NULL,
+    etudid text NOT NULL,
+    annee integer,
+    bac text,
+    specialite text,
+    annee_bac integer,
+    math real,
+    physique real,
+    anglais real,
+    francais real,
+    rang integer, -- dans les voeux du candidat (inconnu avec APB)
+    qualite real,
+    rapporteur text,
+    decision text,
+    score real,
+    commentaire text,
+    nomlycee text,
+    villelycee text,
+    codepostallycee text,
+    codelycee text,
+    debouche text, -- OBSOLETE UNUSED situation APRES etre passe par chez nous (texte libre)
+    type_admission text, -- 'APB', 'APC-PC', 'CEF', 'Direct', '?' (autre)
+    boursier_prec integer default NULL, -- etait boursier dans le cycle precedent (lycee) ?
+    classement integer default NULL, -- classement par le jury d'admission (1 à N), global (pas celui d'APB si il y a des groupes)
+    apb_groupe text, -- code du groupe APB
+    apb_classement_gr integer default NULL -- classement (1..Ngr) par le jury dans le groupe APB
+) WITH OIDS;
+
+
+CREATE TABLE itemsuivi (
+    itemsuivi_id text DEFAULT notes_newid('SUI'::text) PRIMARY KEY,
+    etudid text NOT NULL,
+    item_date date DEFAULT now(), -- date de l'observation
+    situation text  -- situation à cette date (champ libre)
+) WITH OIDS;
+
+CREATE TABLE itemsuivi_tags (
+    tag_id text DEFAULT notes_newid('TG') PRIMARY KEY,
+    title text UNIQUE NOT NULL
+) WITH OIDS;
+
+CREATE TABLE itemsuivi_tags_assoc (
+    tag_id text REFERENCES itemsuivi_tags(tag_id) ON DELETE CASCADE,
+    itemsuivi_id text REFERENCES itemsuivi(itemsuivi_id) ON DELETE CASCADE,
+    PRIMARY KEY (tag_id, itemsuivi_id)
+) WITH OIDS;
+
+
+CREATE TABLE absences (
+    etudid text NOT NULL,
+    jour date, -- jour de l'absence
+    estabs boolean, -- vrai si absent
+    estjust boolean, -- vrai si justifie
+    matin boolean, -- vrai si concerne le matin, faux si apres midi
+    description text,  -- "raison" de l'absence
+    entry_date timestamp with time zone DEFAULT now(),
+    moduleimpl_id text -- moduleimpid concerne (optionnel)
+) WITH OIDS;
+
+CREATE TABLE absences_notifications (    
+    etudid text NOT NULL,
+    notification_date timestamp with time zone DEFAULT now(),
+    email text NOT NULL,
+    nbabs integer,
+    nbabsjust integer,
+    formsemestre_id text -- semestre concerne par cette notification    
+) WITH OIDS;
+
+CREATE SEQUENCE notes_idgen_billets;
+CREATE FUNCTION notes_newid_billet( text ) returns text as '
+	select $1 || to_char(  nextval(''notes_idgen_billets''), ''FM999999999'' ) 
+	as result;
+	' language SQL;
+
+CREATE TABLE billet_absence (
+    billet_id text DEFAULT notes_newid_billet('B'::text) NOT NULL,
+    etudid text NOT NULL,
+    abs_begin timestamp with time zone,
+    abs_end  timestamp with time zone,
+    description text, -- "raison" de l'absence
+    etat integer default 0, -- 0 new, 1 processed    
+    entry_date timestamp with time zone DEFAULT now(),
+    justified integer default 0 -- 1 si l'absence pourrait etre justifiée
+) WITH OIDS;
+
+
+-- --- Log des actions (journal modif etudiants)
+CREATE TABLE scolog (
+    date timestamp without time zone DEFAULT now(),
+    authenticated_user text,
+    remote_addr text,
+    remote_host text,
+    method text,
+    etudid character(32),
+    msg text
+) WITH OIDS;
+
+
+CREATE TABLE etud_annotations (
+    id integer DEFAULT nextval('serial'::text) NOT NULL,
+    date timestamp without time zone DEFAULT now(),
+    etudid character(32),
+    author text, -- now unused
+    comment text,
+    zope_authenticated_user text, -- should be author
+    zope_remote_addr text
+) WITH OIDS;
+
+--  ------------ Nouvelle gestion des absences ------------
+CREATE SEQUENCE abs_idgen;
+CREATE FUNCTION abs_newid( text ) returns text as '
+	select $1 || to_char(  nextval(''abs_idgen''), ''FM999999999'' ) 
+	as result;
+	' language SQL;
+
+CREATE TABLE abs_absences (
+    absid text default abs_newid('AB') PRIMARY KEY,
+    etudid character(32),
+    abs_begin timestamp with time zone,
+    abs_end  timestamp with time zone
+) WITH OIDS;
+
+CREATE TABLE abs_presences (
+    absid text default abs_newid('PR') PRIMARY KEY,
+    etudid character(32),
+    abs_begin timestamp with time zone,
+    abs_end  timestamp with time zone
+) WITH OIDS;
+
+CREATE TABLE abs_justifs (
+    absid text default abs_newid('JU') PRIMARY KEY,
+    etudid character(32),
+    abs_begin timestamp with time zone,
+    abs_end  timestamp with time zone,
+    category text,
+    description text
+) WITH OIDS;
+
+
+
+--  ------------ ENTREPRISES ------------
+
+CREATE TABLE entreprises (
+    entreprise_id serial NOT NULL,
+    nom text,
+    adresse text,
+    ville text,
+    codepostal text,
+    pays text,
+    contact_origine text,
+    secteur text,
+    note text,
+    privee text,
+    localisation text,
+    qualite_relation integer, -- -1 inconnue, 0, 25, 50, 75, 100
+    plus10salaries integer,
+    date_creation timestamp without time zone DEFAULT now()
+) WITH OIDS;
+
+
+CREATE TABLE entreprise_correspondant (
+    entreprise_corresp_id serial NOT NULL,
+    nom text,
+    prenom text,
+    fonction text,
+    phone1 text,
+    phone2 text,
+    mobile text,
+    mail1 text,
+    mail2 text,
+    note text,
+    entreprise_id integer,
+    civilite text,
+    fax text
+) WITH OIDS;
+
+
+--
+--
+
+CREATE TABLE entreprise_contact (
+    entreprise_contact_id serial NOT NULL,
+    date date,
+    type_contact text,
+    entreprise_id integer,
+    entreprise_corresp_id integer,
+    etudid text,
+    description text,
+    enseignant text
+) WITH OIDS;
+
+
+--  ------------ NOTES ------------
+
+
+-- Description generique d'un module (eg infos du PPN)
+CREATE SEQUENCE notes_idgen_fcod;
+CREATE FUNCTION notes_newid_fcod( text ) returns text as '
+	select $1 || to_char(  nextval(''notes_idgen_fcod''), ''FM999999999'' ) 
+	as result;
+	' language SQL;
+
+CREATE TABLE notes_formations (
+	formation_id text default notes_newid('FORM') PRIMARY KEY,
+	acronyme text NOT NULL, -- 'DUT R&T', 'LPSQRT', ...	
+	titre text NOT NULL,     -- titre complet
+	titre_officiel text NOT NULL, -- "DUT Gestion des Entreprises et Admininistration"
+	version integer default 1, -- version de la formation
+	formation_code text default notes_newid_fcod('FCOD') NOT NULL,
+	type_parcours  int DEFAULT 0, -- 0 DUT, 100 Lic Pro
+	code_specialite text default NULL,
+	UNIQUE(acronyme,titre,version)
+) WITH OIDS;
+
+CREATE TABLE notes_ue (
+	ue_id text default notes_newid('UE') PRIMARY KEY,
+	formation_id text REFERENCES notes_formations(formation_id),
+	acronyme text NOT NULL,
+	numero int, -- ordre de presentation
+	titre text,
+	type  int DEFAULT 0, -- 0 normal ("fondamentale"), 1 "sport", 2 "projet et stage (LP)", 4 "élective"
+	ue_code text default notes_newid_fcod('UCOD') NOT NULL,
+	ects real, -- nombre de credits ECTS
+	is_external integer default 0, -- si UE effectuee dans le cursus d'un autre etablissement
+	code_apogee text,  -- id de l'element pedagogique Apogee correspondant
+    coefficient real -- coef UE, utilise seulement si l'option use_ue_coefs est activée
+) WITH OIDS;
+
+CREATE TABLE notes_matieres (
+	matiere_id text default notes_newid('MAT') PRIMARY KEY,
+	ue_id text REFERENCES notes_ue(ue_id),
+	titre text,
+	numero int, -- ordre de presentation
+	UNIQUE(ue_id,titre)
+) WITH OIDS;
+
+CREATE TABLE notes_semestres (
+	-- une bete table 1,2,3,...,8 pour l'instant fera l'affaire...
+	semestre_id int PRIMARY KEY
+) WITH OIDS;
+INSERT INTO notes_semestres (semestre_id) VALUES (-1); -- denote qu'il n'y a pas de semestres dans ce diplome
+INSERT INTO notes_semestres (semestre_id) VALUES (1);
+INSERT INTO notes_semestres (semestre_id) VALUES (2);
+INSERT INTO notes_semestres (semestre_id) VALUES (3);
+INSERT INTO notes_semestres (semestre_id) VALUES (4);
+INSERT INTO notes_semestres (semestre_id) VALUES (5);
+INSERT INTO notes_semestres (semestre_id) VALUES (6);
+INSERT INTO notes_semestres (semestre_id) VALUES (7);
+INSERT INTO notes_semestres (semestre_id) VALUES (8);
+
+CREATE TABLE notes_modules (
+	module_id text default notes_newid('MOD') PRIMARY KEY,
+	titre text,
+	code  text NOT NULL,
+	heures_cours real, 
+	heures_td real, 
+	heures_tp real,
+	coefficient real, -- coef PPN
+	ue_id text REFERENCES notes_ue(ue_id),
+	formation_id text REFERENCES notes_formations(formation_id),
+	matiere_id text  REFERENCES notes_matieres(matiere_id),
+	semestre_id integer REFERENCES notes_semestres(semestre_id),
+	numero int, -- ordre de presentation
+	abbrev text, -- nom court
+	ects real, -- nombre de credits ECTS (NON UTILISES)
+	code_apogee text,  -- id de l'element pedagogique Apogee correspondant
+	module_type int -- NULL ou 0:defaut, 1: malus (NOTES_MALUS)
+) WITH OIDS;
+
+CREATE TABLE notes_tags (
+	tag_id text default notes_newid('TAG') PRIMARY KEY,
+	title text UNIQUE NOT NULL
+) WITH OIDS;
+
+CREATE TABLE notes_modules_tags (
+	tag_id text REFERENCES notes_tags(tag_id) ON DELETE CASCADE,
+	module_id text REFERENCES notes_modules(module_id) ON DELETE CASCADE,
+	PRIMARY KEY (tag_id, module_id)
+) WITH OIDS;
+    
+
+-- Mise en oeuvre d'un semestre de formation
+CREATE TABLE notes_formsemestre (
+	formsemestre_id text default notes_newid('SEM') PRIMARY KEY,
+	formation_id text REFERENCES notes_formations(formation_id),
+	semestre_id int REFERENCES notes_semestres(semestre_id),
+	titre text,
+	date_debut date,
+	date_fin   date,
+	-- responsable_id text,
+	-- gestion_absence integer default 1,   -- XXX obsolete
+	-- bul_show_decision integer default 1, -- XXX obsolete
+	-- bul_show_uevalid integer default 1,  -- XXX obsolete
+	etat integer default 1, -- 1 ouvert, 0 ferme (verrouille)
+	-- nomgroupetd text default 'TD',  -- XXX obsolete
+	-- nomgroupetp text default 'TP',  -- XXX obsolete 
+	-- nomgroupeta text default 'langues', -- XXX obsolete
+	-- bul_show_codemodules integer default 1, -- XXX obsolete
+	-- bul_show_rangs integer default 1,  -- XXX obsolete
+	-- bul_show_ue_rangs integer default 1, -- XXX obsolete
+	-- bul_show_mod_rangs integer default 1, -- XXX obsolete
+	gestion_compensation integer default 0, -- gestion compensation sem DUT
+	bul_hide_xml integer default 0, --  ne publie pas le bulletin XML
+	gestion_semestrielle integer default 0, -- semestres decales (pour gestion jurys)
+	bul_bgcolor text default 'white', -- couleur fond bulletins HTML
+	modalite text,   -- FI, FC, APP, ''
+	resp_can_edit integer default 0, -- autorise resp. a modifier semestre
+	resp_can_change_ens integer default 1, -- autorise resp. a modifier slt les enseignants
+	ens_can_edit_eval int default 0, -- autorise les ens a creer des evals
+	elt_sem_apo text, -- code element semestre Apogee, eg VRTW1 ou V2INCS4,V2INLS4
+	elt_annee_apo text -- code element annee Apogee, eg VRT1A ou V2INLA,V2INCA
+) WITH OIDS;
+
+-- id des utilsateurs responsables (aka directeurs des etudes) du semestre:
+CREATE TABLE notes_formsemestre_responsables (
+    formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id) ON DELETE CASCADE,
+    responsable_id text NOT NULL,
+    UNIQUE(formsemestre_id, responsable_id)
+) WITH OIDS;
+
+-- Etape Apogee associes au semestre:
+CREATE TABLE notes_formsemestre_etapes (
+    formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id) ON DELETE CASCADE,
+    etape_apo text NOT NULL
+) WITH OIDS;
+
+CREATE TABLE notes_form_modalites (
+    form_modalite_id text default notes_newid('Md') PRIMARY KEY,
+    modalite text, -- la clef dans notes_formsemestre
+    titre text, -- le nom complet de la modalite pour les documents scodoc
+    numero SERIAL -- integer, ordre de presentation
+);
+INSERT INTO notes_form_modalites (modalite, titre) VALUES ('', 'Autres formations');
+INSERT INTO notes_form_modalites (modalite, titre) VALUES ('FI', 'Formation Initiale');
+INSERT INTO notes_form_modalites (modalite, titre) VALUES ('FC', 'Formation Continue');
+INSERT INTO notes_form_modalites (modalite, titre) VALUES ('FAP', 'Apprentissage');
+INSERT INTO notes_form_modalites (modalite, titre) VALUES ('DEC', 'Formation Décalées');
+INSERT INTO notes_form_modalites (modalite, titre) VALUES ('LIC', 'Licence');
+INSERT INTO notes_form_modalites (modalite, titre) VALUES ('CP', 'Contrats de Professionnalisation');
+INSERT INTO notes_form_modalites (modalite, titre) VALUES ('EXT', 'Extérieur');
+
+-- semsets
+CREATE TABLE notes_semset (
+   semset_id text default notes_newid('NSS') PRIMARY KEY,
+   title text,
+   annee_scolaire int default NULL, -- 2016
+   sem_id int default NULL -- periode: 0 (année), 1 (Simpair), 2 (Spair)
+) WITH OIDS;
+
+CREATE TABLE notes_semset_formsemestre (
+   formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id) ON DELETE CASCADE,
+   semset_id text REFERENCES notes_semset (semset_id) ON DELETE CASCADE,
+   PRIMARY KEY (formsemestre_id, semset_id)
+) WITH OIDS;
+
+-- Coef des UE capitalisees arrivant dans ce semestre:
+CREATE TABLE notes_formsemestre_uecoef (
+	formsemestre_uecoef_id text default notes_newid('SEM') PRIMARY KEY,
+	formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id),
+	ue_id  text REFERENCES notes_ue(ue_id),
+	coefficient real NOT NULL,
+	UNIQUE(formsemestre_id, ue_id)
+) WITH OIDS;
+
+
+-- Formules utilisateurs pour calcul moyenne UE
+CREATE TABLE notes_formsemestre_ue_computation_expr (
+	notes_formsemestre_ue_computation_expr_id text default notes_newid('UEXPR') PRIMARY KEY,
+	formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id),
+	ue_id  text REFERENCES notes_ue(ue_id),
+	computation_expr text, -- formule de calcul moyenne
+	UNIQUE(formsemestre_id, ue_id)
+) WITH OIDS;
+
+-- Menu custom associe au semestre
+CREATE TABLE notes_formsemestre_custommenu (
+	custommenu_id text default notes_newid('CMENU') PRIMARY KEY,
+	formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id),
+	title text,
+	url text,
+	idx integer default 0 -- rang dans le menu	
+) WITH OIDS;
+
+-- Mise en oeuvre d'un module pour une annee/semestre
+CREATE TABLE notes_moduleimpl (
+	moduleimpl_id  text default notes_newid('MIP') PRIMARY KEY,
+	module_id text REFERENCES notes_modules(module_id),
+	formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id),
+	responsable_id text,
+	computation_expr text, -- formule de calcul moyenne
+	UNIQUE(module_id,formsemestre_id) -- ajoute
+) WITH OIDS;
+
+-- Enseignants (chargés de TD ou TP) d'un moduleimpl
+CREATE TABLE notes_modules_enseignants (
+	modules_enseignants_id text default notes_newid('ENS') PRIMARY KEY,
+	moduleimpl_id text REFERENCES notes_moduleimpl(moduleimpl_id),
+	ens_id text -- est le user_name de sco_users (de la base SCOUSERS)
+) WITH OIDS;
+
+-- Inscription a un semestre de formation
+CREATE TABLE notes_formsemestre_inscription (
+	formsemestre_inscription_id text default notes_newid2('SI') PRIMARY KEY,
+	etudid text REFERENCES identite(etudid),
+	formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id),
+	etat text, -- I inscrit, D demission en cours de semestre, DEF si "defaillant"
+	UNIQUE(formsemestre_id, etudid)
+) WITH OIDS;
+
+-- Inscription a un module  (etudiants,moduleimpl)
+CREATE TABLE notes_moduleimpl_inscription (
+	moduleimpl_inscription_id text default notes_newid2('MI') PRIMARY KEY,
+	moduleimpl_id text REFERENCES notes_moduleimpl(moduleimpl_id),
+	etudid text REFERENCES identite(etudid),
+	UNIQUE( moduleimpl_id, etudid)
+) WITH OIDS;
+
+
+CREATE TABLE partition(
+       partition_id text default notes_newid2('P') PRIMARY KEY,
+       formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id),
+       partition_name text, -- "TD", "TP", ... (NULL for 'all')
+       compute_ranks integer default 1, -- calcul rang etudiants dans les groupes (currently unused)
+       numero SERIAL, -- ordre de presentation
+       bul_show_rank integer default 0,
+       show_in_lists integer default 1, -- montre dans les noms de groupes
+       UNIQUE(formsemestre_id,partition_name)
+) WITH OIDS;
+
+CREATE TABLE group_descr (
+       group_id text default notes_newid2('G') PRIMARY KEY,
+       partition_id text REFERENCES partition(partition_id),
+       group_name text, -- "A", "C2", ...  (NULL for 'all')
+       UNIQUE(partition_id, group_name)     
+) WITH OIDS;
+
+CREATE TABLE group_membership(
+       group_membership_id text default notes_newid2('GM') PRIMARY KEY,
+       etudid text REFERENCES identite(etudid),       
+       group_id text REFERENCES group_descr(group_id),
+       UNIQUE(etudid, group_id)
+) WITH OIDS;
+
+-- Evaluations (controles, examens, ...)
+CREATE TABLE notes_evaluation (
+	evaluation_id text default notes_newid('EVAL') PRIMARY KEY,
+	moduleimpl_id text REFERENCES notes_moduleimpl(moduleimpl_id),
+	jour date,      
+	heure_debut time,
+	heure_fin time,
+	description text,
+	note_max real,
+	coefficient real,
+	visibulletin integer default 1,
+	publish_incomplete integer default 0, -- prise en compte meme si incomplete
+	evaluation_type integer default 0, -- type d'evaluation: 0 normale, 1 rattrapage
+	numero int -- ordre de presentation (le plus petit numero est normalement la plus ancienne eval)
+) WITH OIDS;
+
+-- Les notes...
+CREATE TABLE notes_notes (
+	etudid text REFERENCES identite(etudid),
+	evaluation_id text REFERENCES notes_evaluation(evaluation_id),
+	value real,	-- null si absent, voir valeurs speciales dans notes_table.py
+	UNIQUE(etudid,evaluation_id),
+	-- infos sur saisie de cette note:
+	comment text,
+	date timestamp default now(),
+	uid text
+) WITH OIDS;
+CREATE INDEX notes_notes_evaluation_id_idx ON notes_notes (evaluation_id);
+
+-- Historique des modifs sur notes (anciennes entrees de notes_notes)
+CREATE TABLE notes_notes_log (
+	id 	SERIAL PRIMARY KEY,
+	etudid text REFERENCES identite(etudid), 
+	evaluation_id text,  -- REFERENCES notes_evaluation(evaluation_id),
+	value real,
+	comment text,
+	date timestamp,
+	uid text
+	-- pas de foreign key, sinon bug lors supression notes (et on 
+	-- veut garder le log)
+	-- FOREIGN KEY (etudid,evaluation_id) REFERENCES notes_notes(etudid,evaluation_id)
+) WITH OIDS;
+
+
+---------------------------------------------------------------------
+-- Parcours d'un etudiant
+--
+-- etat: INSCRIPTION inscr. de l'etud dans ce semestre
+--       DEM         l'etud demissionne EN COURS DE SEMESTRE
+--       DIPLOME     en fin semestre, attribution du diplome correspondant
+--                          (ou plutot, validation du semestre)
+--       AUT_RED     en fin semestre, autorise a redoubler ce semestre
+--       EXCLUS      exclus (== non autorise a redoubler)
+--       VALID_SEM   obtention semestre après jury terminal
+--       VALID_UE    obtention UE après jury terminal
+--       ECHEC_SEM   echec a ce semestre
+--       UTIL_COMPENSATION utilise formsemestre_id pour compenser et valider
+--                         comp_formsemestre_id
+CREATE TABLE scolar_events (
+	event_id     text default notes_newid('EVT') PRIMARY KEY,
+	etudid text,
+	event_date timestamp default now(),
+	formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id),
+        ue_id text REFERENCES notes_ue(ue_id),
+	event_type text, -- 'CREATION', 'INSCRIPTION', 'DEMISSION', 
+                         -- 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
+                         -- 'ECHEC_SEM'
+	                 -- 'UTIL_COMPENSATION'
+        comp_formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id)
+                         -- semestre compense par formsemestre_id
+) WITH OIDS;
+
+-- Stockage des codes d'etat apres jury
+CREATE SEQUENCE notes_idgen_svalid;
+
+CREATE FUNCTION notes_newidsvalid( text ) returns text as '
+	select $1 || to_char(  nextval(''notes_idgen_svalid''), ''FM999999999'' ) 
+	as result;
+	' language SQL;
+
+CREATE TABLE scolar_formsemestre_validation (
+	formsemestre_validation_id text default notes_newidsvalid('VAL') PRIMARY KEY,
+	etudid text NOT NULL,
+	formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id), -- anciennement (<2015-03-17) NULL si external
+	ue_id text REFERENCES notes_ue(ue_id), -- NULL si validation de semestre
+	code text NOT NULL,
+	assidu integer, -- NULL pour les UE, 0|1 pour les semestres
+	event_date timestamp default now(),
+	compense_formsemestre_id text, -- null sauf si compense un semestre
+	moy_ue real, -- moyenne UE capitalisee (/20, NULL si non calculee)
+	semestre_id int, -- (normalement NULL) indice du semestre, utile seulement pour UE "antérieures" et si la formation définit des UE utilisées dans plusieurs semestres (cas R&T IUTV v2)
+	is_external integer default 0, -- si UE validée dans le cursus d'un autre etablissement
+	UNIQUE(etudid,formsemestre_id,ue_id) -- une seule decision
+) WITH OIDS;
+
+CREATE TABLE scolar_autorisation_inscription (
+	autorisation_inscription_id text default notes_newidsvalid('AUT') PRIMARY KEY,
+	etudid text NOT NULL,
+	formation_code text NOT NULL,
+	semestre_id int REFERENCES notes_semestres(semestre_id), -- semestre ou on peut s'inscrire
+	date timestamp default now(),
+	origin_formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id)
+) WITH OIDS;
+
+---------------------------------------------------------------------
+-- NOUVELLES (pour page d'accueil et flux rss associe)
+--
+CREATE TABLE scolar_news (
+	news_id text default notes_newid('NEWS') PRIMARY KEY,
+	date timestamp default now(),
+	authenticated_user text, 
+	type text, -- 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC'
+	object text, -- moduleimpl_id, formation_id, formsemestre_id, 
+	text text, -- free text
+	url text -- optional URL
+) WITH OIDS;
+
+-- Appreciations sur bulletins
+CREATE TABLE notes_appreciations (
+    id integer DEFAULT nextval('serial'::text) NOT NULL,
+    date timestamp without time zone DEFAULT now(),
+    etudid text REFERENCES identite(etudid),
+    formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id),
+    author text,
+    comment text,
+    zope_authenticated_user text,
+    zope_remote_addr text
+) WITH OIDS;
+
+
+
diff --git a/misc/example-api-1.py b/misc/example-api-1.py
new file mode 100644
index 0000000000000000000000000000000000000000..06b070cff31f89131a42a4d8fc11e3886a28ed30
--- /dev/null
+++ b/misc/example-api-1.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Exemple connexion sur ScoDoc et utilisation de l'API
+
+- Ouverture session
+- Liste semestres
+- Liste modules
+- Creation d'une évaluation
+- Saisie d'une note
+
+Attention: cet exemple est en Python 3 (>= 3.6)
+"""
+
+import requests
+import urllib3
+import pdb
+from pprint import pprint as pp
+
+# A modifier pour votre serveur:
+CHECK_CERTIFICATE = False  # set to True in production
+BASEURL = "https://scodoc.xxx.net/ScoDoc/RT/Scolarite"
+USER = "XXX"
+PASSWORD = "XXX"
+
+# --- 
+if not CHECK_CERTIFICATE:
+    urllib3.disable_warnings()
+
+class ScoError(Exception):
+    pass
+
+def GET(s, path, errmsg=None):
+    """Get and returns as JSON"""
+    r = s.get(BASEURL + "/" + path)
+    if r.status_code != 200:
+        raise ScoError(errmsg or "erreur !")
+    return r.json()  # decode la reponse JSON
+
+
+def POST(s, path, data, errmsg=None):
+    """Post"""
+    r = s.post(BASEURL + "/" + path, data=data)
+    if r.status_code != 200:
+        raise ScoError(errmsg or "erreur !")
+    return r.text
+
+
+# --- Ouverture session (login)
+s = requests.Session()
+r = s.get(BASEURL, auth=(USER, PASSWORD), verify=CHECK_CERTIFICATE)
+if r.status_code != 200:
+    raise ScoError("erreur de connection: vérifier adresse et identifiants")
+
+# --- Recupere la liste de tous les semestres:
+sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !")
+
+# sems est une liste de semestres (dictionnaires)
+for sem in sems:
+    if sem["etat"] == "1":
+        break
+
+if sem["etat"] == "0":
+    raise ScoError("Aucun semestre non verrouillé !")
+
+# Affiche le  semestre trouvé:
+pp(sem)
+
+# ---- Récupère la description de ce semestre:
+# semdescr = GET(s, f"Notes/formsemestre_description?formsemestre_id={sem['formsemestre_id']}&with_evals=0&format=json" )
+
+# ---- Liste les modules et prend le premier
+mods = GET(s, f"/Notes/do_moduleimpl_list?formsemestre_id={sem['formsemestre_id']}")
+print(f"{len(mods)} modules dans le semestre {sem['titre']}")
+
+mod = mods[0]
+
+# ---- Etudiants inscrits dans ce module
+inscrits = GET(
+    s, f"Notes/do_moduleimpl_inscription_list?moduleimpl_id={mod['moduleimpl_id']}"
+)
+print(f"{len(inscrits)} inscrits dans ce module")
+# prend le premier inscrit, au hasard:
+etudid = inscrits[0]["etudid"]
+
+# ---- Création d'une evaluation le dernier jour du semestre
+jour = sem["date_fin"]
+evaluation_id = POST(
+    s,
+    "/Notes/do_evaluation_create",
+    data={
+        "moduleimpl_id": mod["moduleimpl_id"],
+        "coefficient": 1,
+        "jour": jour,  # "5/9/2019",
+        "heure_debut": "9h00",
+        "heure_fin": "10h00",
+        "note_max": 20,  # notes sur 20
+        "description": "essai",
+    },
+    errmsg="échec création évaluation",
+)
+
+print(
+    f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}"
+)
+print(
+    "Pour vérifier, aller sur: ",
+    BASEURL + "/Notes/moduleimpl_status?moduleimpl_id=" + mod["moduleimpl_id"],
+)
+
+# ---- Saisie d'une note
+junk = POST(
+    s,
+    "/Notes/save_note",
+    data={
+        "etudid": etudid,
+        "evaluation_id": evaluation_id,
+        "value": 16.66,  # la note !
+        "comment": "test API",
+    },
+)
diff --git a/misc/example-api-python2.py b/misc/example-api-python2.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea5a2536eda95fd47bbd98df25a8bb374e16f39c
--- /dev/null
+++ b/misc/example-api-python2.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Exemple connexion sur ScoDoc et utilisation de l'API
+
+Attention: cet exemple est en Python 2.
+Voir example-api-1.py pour une version en Python3 plus moderne.
+"""
+
+import urllib, urllib2
+
+# A modifier pour votre serveur:
+BASEURL = "https://scodoc.xxx.net/ScoDoc/RT/Scolarite"
+USER = "XXX"
+PASSWORD = "XXX"
+
+values = {'__ac_name' : USER,
+          '__ac_password' : PASSWORD,
+          }
+
+# Configure memorisation des cookies:
+opener = urllib2.build_opener(urllib2.HTTPCookieProcessor())
+urllib2.install_opener(opener)
+
+data = urllib.urlencode(values)
+
+req = urllib2.Request(BASEURL, data) # this is a POST http request
+response = urllib2.urlopen(req)
+
+# --- Use API
+
+# Affiche la liste des formations en format XML
+req = urllib2.Request(BASEURL+'/Notes/formation_list?format=xml' )
+response = urllib2.urlopen(req)
+print response.read()[:100] # limite aux 100 premiers caracteres...
+
+# Recupere la liste de tous les semestres:
+req = urllib2.Request(BASEURL+'/Notes/formsemestre_list?format=json') # format json
+response = urllib2.urlopen(req)
+js_data = response.read()
+
+# Plus amusant: va retrouver le bulletin de notes du premier etudiant (au hasard donc) du premier semestre (au hasard aussi)
+try:
+    import json # Attention: ceci demande Python >= 2.6
+except:
+    import simplejson as json # python2.4 with simplejson installed
+
+data = json.loads(js_data) # decode la reponse JSON
+if not data:
+    print "Aucun semestre !"
+else:
+    formsemestre_id = str(data[0]['formsemestre_id'])
+    # Obtient la liste des groupes:
+    req = urllib2.Request(BASEURL+'/Notes/formsemestre_partition_list?format=json&formsemestre_id='+formsemestre_id) # format json
+    response = urllib2.urlopen(req) 
+    js_data = response.read()
+    data = json.loads(js_data)
+    group_id = data[0]['group'][0]['group_id'] # premier groupe (normalement existe toujours)
+    # Liste les étudiants de ce groupe:
+    req = urllib2.Request(BASEURL+'/Notes/group_list?format=json&with_codes=1&group_id='+group_id) # format json
+    response = urllib2.urlopen(req) 
+    js_data = response.read()
+    data = json.loads(js_data)
+    # Le code du premier étudiant:
+    if not data:
+        print "pas d'etudiants dans ce semestre !"
+    else:
+        etudid = data[0]['etudid']
+        # Récupère bulletin de notes:
+        req = urllib2.Request(BASEURL+'/Notes/formsemestre_bulletinetud?formsemestre_id='+formsemestre_id+'&etudid=' + etudid + '&format=xml') # format XML ici !
+        response = urllib2.urlopen(req)
+        xml_bulletin = response.read()
+        print '----- Bulletin de notes en XML:'
+        print xml_bulletin
+        # Récupère la moyenne générale:
+        import xml.dom.minidom
+        doc = xml.dom.minidom.parseString(xml_bulletin)
+        moy = doc.getElementsByTagName('note')[0].getAttribute('value') # une chaine unicode
+        print '\nMoyenne generale: ', moy
+    
diff --git a/misc/format_import_etudiants.txt b/misc/format_import_etudiants.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f4e1dd77d890bb59628ca25d5904dea7f097a563
--- /dev/null
+++ b/misc/format_import_etudiants.txt
@@ -0,0 +1,55 @@
+# Format fichiers CSV pour import etudiants
+# E.V., Sept 2005
+# (';' separated file)
+# Attribut     Type     Table       AllowNulls  Description Aliases
+Code_NIP;     text;     identite;   1; code etudiant (NIP Apogee);NIP
+Code_INE;     text;     identite;   1; code INE;INE
+#
+nom;          text;     identite;   0;  nom de l'etudiant;
+nom_usuel; text;    identite;   1;  nom usuel (si different);
+prenom;       text;     identite;   0;  prenom de l'etudiant
+sexe (H ou F);         text;     identite;   0;  sexe ('H' ou 'F');sexe;genre
+date_naissance;text;identite;   1;  date de naissance (jj/mm/aaaa)
+lieu_naissance;text;identite; 1; lieu de naissance
+nationalite;  text;     identite;   1;  nationalite
+statut; text;         identite; 1; ("SALARIE", ...)
+photo_filename; text; identite; 1; nom fichier image
+#
+# Informations pour inscription:
+codesemestre; text;     INS;        0;  code semestre inscription
+groupes;     text;     INS;        1;  groupe(s), séparés par des point-virgules, doivent exister avant. On peut spécifier la partition sous la forme partition:groupe.
+# 
+bac;          text;     admissions; 1;  type de bac (S, STI, ...)
+specialite;   text;     admissions; 1;  specialite du bac (SVT, ...)
+annee_bac;    integer;  admissions; 1;  annee d'obtention du bac
+math;         real;     admissions; 1;  note de math en terminale
+physique;     real;     admissions; 1;  note de physique en terminale
+anglais;      real;     admissions; 1;  note de anglais en terminale
+francais;     real;     admissions; 1;  note de francais au bac
+type_admission; text; admissions; 1; voie d'admission (APB, APB-PC, CEF, ...) 
+boursier_prec; integer; admissions; 1; 0/1  etait boursier dans le cycle precedent (lycee) ?
+qualite;      real;     admissions; 1;  note de qualite du dossier
+rapporteur;   text;     admissions; 1;  identite du rapporteur (enseignant IUT)
+decision;     text;     admissions; 1;  decision (admis, attente, ...)
+score;        real;     admissions; 1;  score calcule lors de l'admission
+classement;         integer;      admissions;1; rang classement lors de l'admission;classement global;
+apb_groupe; text;  admissions;1; intitulé ou code du groupe APB;Code groupe;Groupe;
+apb_classement_gr;         integer;      admissions;1; rang classement dans le groupe APB;classement gr;
+commentaire;  text;     admissions; 1;  commentaire du rapporteur;comment;remarque
+nomlycee;     text;     admissions; 1;  nom du lycee;Libellé établissement;
+villelycee;   text;     admissions; 1;  ville du lycee;Commune établissement;
+codepostallycee; text;  admissions; 1;  code postal du lycee;Département établissement;
+codelycee;    text;     admissions; 1;  code national etablissement;UAI établissement;
+#
+email;        text;     adresse;    1;  adresse e-mail;mail
+emailperso; text;     adresse;    1;  adresse e-mail;mailperso;mail personnel;mail externe
+domicile;     text;     adresse;    1;  adresse domicile
+codepostaldomicile; text; adresse;  1;  code postal domicile
+villedomicile; text;    adresse;    1;  ville domicile
+paysdomicile; text;     adresse;    1;  pays domicile
+telephone;    text;     adresse;    1;  num. telephone (fixe)
+telephonemobile; text;  adresse;    1;  num. telephone (mobile)
+#
+# Pas tout à fait admission:
+debouche;text; admissions;1;situation APRES être passé par chez nous;
+
diff --git a/misc/geolocalize_lycees.py b/misc/geolocalize_lycees.py
new file mode 100644
index 0000000000000000000000000000000000000000..818ec8c36fcbfc8c7d0553c1a69480b71532df7c
--- /dev/null
+++ b/misc/geolocalize_lycees.py
@@ -0,0 +1,65 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 2001 - 2012 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
+#
+##############################################################################
+
+"""Géolocalisation du fichiers des lycees (etablissements.csv)
+Ajoute deux colonnes: LAT, LONG
+
+Utilise geopy http://www.geopy.org/ et l'API Google
+
+"""
+
+from geopy import geocoders
+import time
+
+SOURCE = '../config/etablissements-orig.csv'
+DEST = '../config/etablissements-geocode.csv'
+
+
+g = geocoders.Google(domain="maps.google.fr") #, api_key='XXX')
+
+inf = open(SOURCE)
+out = open(DEST, 'w')
+
+head = inf.readline()
+out.write(head.strip() + ';LAT;LNG'+'\n')
+for line in inf:
+    address = ' '.join(line.split(';')[2:]).strip()
+    print address
+    try:
+        place, (lat, lng) = g.geocode(address)
+    except: # multiple possible locations ?
+        time.sleep(0.11)
+        try:
+            place, (lat, lng) = g.geocode(address + ' France', exactly_one=False)[0]
+        except:
+            place, (lat, lng) = 'NOT FOUND', (0.,0.)
+    print "%s: %.5f, %.5f" % (address, lat, lng)  
+    out.write( line.strip() + ';%s;%s\n' % (lat,lng) )
+    time.sleep(0.11) # Google API Rate limit of 10 requests per second.
+
+inf.close()
+out.close()
diff --git a/misc/get_codes_from_names.py b/misc/get_codes_from_names.py
new file mode 100644
index 0000000000000000000000000000000000000000..6cc8bc49af22b657235707e0c01cdc8c1f8fbb00
--- /dev/null
+++ b/misc/get_codes_from_names.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+
+"""Pour un semestre, Affiche colonnes code_nip, code_ine
+   etant donnes les noms/prenoms dans un CSV
+   (ne change pas la BD)
+
+   XXX TODO: OBSOLETE, a moderniser (psycopg2, python 3, encoding)
+"""
+
+import pdb,os,sys,psycopg
+import csv
+
+
+CSVFILENAME = '/tmp/aaa.csv'
+formsemestre_id = 'SEM229' 
+DBCNXSTRING = 'host=localhost user=scoinfo dbname=SCOINFO password=XXX'
+
+idx_prenom = 1
+idx_nom = 0
+
+
+
+
+# en general, pas d'accents dans le CSV
+SCO_ENCODING = 'iso8859-15'
+from SuppressAccents import suppression_diacritics
+def suppr_acc_and_ponct(s):
+    s = s.replace( ' ', '' )
+    s = s.replace('-', ' ')    
+    return str(suppression_diacritics( unicode(s, SCO_ENCODING) ))
+
+def make_key(nom, prenom):
+    nom = suppr_acc_and_ponct(nom).upper()    
+    prenom = suppr_acc_and_ponct(prenom).upper()
+    return nom + ' ' + prenom[:4]
+
+reader = csv.reader(open( CSVFILENAME, "rb"))
+noms = {}
+for row in reader:
+    if row[0][0] != '#':
+        key = make_key( row[idx_nom], row[idx_prenom])
+        if noms.has_key(key):
+            raise ValueError, 'duplicate key: %s' % key
+        noms[key] = row
+
+cnx = psycopg.connect( DBCNXSTRING )
+
+cursor = cnx.cursor()
+cursor.execute("select * from identite i, notes_formsemestre_inscription ins where i.etudid = ins.etudid and ins.formsemestre_id = '%s'" %formsemestre_id )
+R = cursor.dictfetchall()
+
+nok=0
+print 'nom,prenom,ine,nip'
+for e in R:
+    key = make_key(e['nom'], e['prenom'])
+    if not noms.has_key(key):
+        print '** no match for %s (%s)' % (key, e['etudid'])
+    else:
+        info = noms[key]
+        print '%s,%s,%s,%s' % (e['nom'],e['prenom'], e['code_ine'], e['code_nip'])
+        nok+=1
+
+cnx.commit()
+
+print '%d etudiants, %d ok' % (len(R), nok)
diff --git a/misc/imp_exp.xls b/misc/imp_exp.xls
new file mode 100644
index 0000000000000000000000000000000000000000..604f0850e6a6a07f42a4de0e6075de59f0f6c799
Binary files /dev/null and b/misc/imp_exp.xls differ
diff --git a/misc/iscid_create_formation_from_xls.py b/misc/iscid_create_formation_from_xls.py
new file mode 100644
index 0000000000000000000000000000000000000000..b41a9d9a16e9a57feafd6be842e7455ad6b3589b
--- /dev/null
+++ b/misc/iscid_create_formation_from_xls.py
@@ -0,0 +1,128 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+# Creation d'une formation ISCID à partir d'un xls listant les modules
+
+# XXX TODO : a tester et moderniser (ects, verifier champs, python 3, importer codes depuis ScoDoc ?)
+
+import os, sys, pdb, pprint
+from openpyxl import load_workbook # apt-get install python-openpyxl
+import jaxml
+SCO_ENCODING = 'utf-8'
+
+INPUT_FILENAME = "/tmp/Bachelor.xlsx"
+OUTPUT_FILENAME= os.path.splitext(INPUT_FILENAME)[0] + '.xml' 
+
+FIRST_SHEET_IDX=1 # saute première feuille du classeur
+
+
+# Code de ScoDoc (sco_utils.py)
+UE_STANDARD = 0 # UE "fondamentale"
+UE_SPORT = 1    # bonus "sport"
+UE_STAGE_LP = 2 # ue "projet tuteuré et stage" dans les Lic. Pro.
+UE_ELECTIVE = 4 # UE "élective" dans certains parcours (UCAC?, ISCID)
+UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
+
+# Code du fichier Excel:
+UE_TYPE2CODE = { u'UE F' : UE_STANDARD, u'UE E' : UE_ELECTIVE }
+
+# Lecture du fichier Excel
+UE = []
+wb = load_workbook(filename=INPUT_FILENAME)
+#print wb.get_sheet_names()
+
+for sheet_name in wb.get_sheet_names()[FIRST_SHEET_IDX:]:
+    print 'Importing sheet %s' % sheet_name
+    sheet = wb.get_sheet_by_name(sheet_name)
+    # Avance jusqu'à trouver le titre 'CODE' en premiere colonne
+    i=0
+    while i < len(sheet.rows) and sheet.rows[i][0].value != 'CODE':
+        i = i + 1
+
+    i = i + 1
+    ue = None
+    while i < len(sheet.rows):
+        code = sheet.rows[i][0].value
+        type_ue = sheet.rows[i][2].value
+        if type_ue in UE_TYPE2CODE:
+            if ue:
+                UE.append(ue)
+            # creation UE
+            acronyme = code # ici l'acronyme d'UE est le code du module
+            if not acronyme and (i < len(sheet.rows)-1):
+                acronyme = sheet.rows[i+1][0].value # code module sur ligne suivante
+                #print acronyme
+                if acronyme: # tres specifique: deduit l'acronyme d'UE du code module
+                    parts = acronyme.split(u'-')
+                    parts[-1] = parts[-1][-1] # ne garde que le dernier chiffre
+                    acronyme = u'-'.join(parts) # B1-LV1-EN1 -> B1-LV1-1
+                #print '->', acronyme
+            if not acronyme:
+                acronyme = sheet.rows[i][3].value # fallback: titre
+            ue = { 'acronyme' : acronyme,
+                   'titre' : sheet.rows[i][3].value,
+                   'ects' : sheet.rows[i][5].value or u"",
+                   'type' : UE_TYPE2CODE[type_ue],
+                   'numero' : (sheet.rows[i][1].value or 0)*1000 + i*10,
+                   'modules' : []
+                   }
+            i_ue = i
+        if code:
+            ue['modules'].append( {
+                'code' : code,
+                'heures_td' : sheet.rows[i_ue][4].value or u"",
+                'titre' : sheet.rows[i][3].value,
+                'semestre_id' : sheet.rows[i][1].value,
+                'numero' : i*10
+                } )
+
+        i += 1 # next line
+
+    if ue:
+        UE.append(ue)
+
+
+def sstr(s):
+    if type(s) is type(u''):
+        return s.encode(SCO_ENCODING)
+    else:
+        return str(s)
+
+# ----- Write to XML    
+doc = jaxml.XML_document( encoding=SCO_ENCODING )
+
+doc._push()
+doc.formation( acronyme="Bachelor ISCID",
+               code_specialite="",
+               type_parcours="1001",
+               titre_officiel="Bachelor ISCID",
+               formation_code="FCOD4",
+               version="1",
+               titre="Bachelor ISCID",
+               formation_id="FORM115"
+               )
+
+for ue in UE:
+    doc._push()
+    doc.ue( acronyme=sstr(ue['acronyme']), ects=sstr(ue['ects']), titre=sstr(ue['titre']), numero=sstr(ue['numero']), type=sstr(ue['type']) )
+    doc._push()
+    doc.matiere( titre=sstr(ue['titre']) ) # useless but necessary
+    for m in ue['modules']:
+        doc._push()
+        doc.module( coefficient="1.0", code=sstr(m['code']), 
+                    heures_td=sstr(m['heures_td']), 
+                    titre=sstr(m['titre']), abbrev=sstr(m['titre']),
+                    semestre_id=sstr(m['semestre_id']),
+                    numero=sstr(m['numero']) 
+            )
+        doc._pop() # /module
+    doc._pop() # /matiere
+    doc._pop() # /ue
+    
+doc._pop() # /formation
+
+#---
+print 'Writing XML file: ', OUTPUT_FILENAME
+f = open(OUTPUT_FILENAME, 'w')
+f.write(str(doc))
+f.close()
diff --git a/misc/parcoursDUT.csv b/misc/parcoursDUT.csv
new file mode 100644
index 0000000000000000000000000000000000000000..73f1ffed369764204a70f6053d4caa2544d1430f
--- /dev/null
+++ b/misc/parcoursDUT.csv
@@ -0,0 +1,61 @@
+# Id	Prev.	Assiduité	Moy Gen	Barres UE	Comp prev/cur	Suivant	Code SEM	Codes UE	Code prev. (si modifié)	Devenir	Action	Explication
+# Semestre prec. validé:												
+10	None, ADM, ADC, ADJ	ok	ok	ok	*	*	ADM	ADM		NEXT		Passage normal
+20	None, ADM, ADC, ADJ	ok	no	ok	*	oui	ATT	ADM		NEXT		Pas moy: attente suivant pour compenser
+30	None, ADM, ADC, ADJ	ok	*	no	*	*	ATB	ADM, AJ		NEXT		Pas barre UE
+40	None, ADM, ADC, ADJ	no	*	*	*	oui	ATJ	AJ		NEXT		Pb assiduité, passe sans valider pour l'instant
+50	ADM, ADJ, ADC	ok	no	*	ok	*	ADC	ADM, CMP		NEXT		Compense avec semestre précédent
+												
+# Semestre prec. ATJ (pb assiduité):												
+60	ATJ	no	*	*	*	*	NAR	AJ	AJ	REO		Pb assiduité persistant: réorientation
+70	ATJ	no	*	*	*	*	AJ	AJ	AJ	REDOANNEE		Pb assiduité persistant: redoublement année
+80	*	no	*	*	*	*	AJ		ADM	REO		Pb assiduité, étudiant en échec.
+												
+												
+												
+# Semestre prec. ATT (pb moy gen):												
+90	ATT	ok	ok	ok	ok	*	ADM	ADM	ADC	NEXT		Passage, et compense précédent
+100	ATT	ok	ok	ok	*	*	ADM	ADJ	ADJ	NEXT		Passage, le jury valide le précédent
+110	ATT	no	ok	ok	*	oui	ATJ	AJ	ADJ	NEXT		Passage, le jury valide le précédent, pb assiduité
+120	ATT	ok	no	*	*	*	AJ	AJ	AJ	REDOANNEE		Redoublement année
+130	ATT	*	ok	ok	no	*	AJ	AJ	AJ	REDOANNEE		Pas de compensation ni validation du précédent
+140	ATT	ok	no	ok	*	*	ATT		ADJ	NEXT		Pas moy, le jury valide le précédent, semestre en attente pour compenser
+												
+# Semestre prec. ATB (pb barre UE):												
+200	ATB	*	*	*	*	*	AJ	ADM, AJ	AJ	REDOANNEE		Le précédent ne peut pas être validé, redoublement année
+210	ATB	*	*	*	*	*	NAR	ADM, AJ	NAR	REO		Le précédent ne peut pas être validé, réorientation
+220	ATB	ok	ok	ok	*	*	ADM	ADM	ADJ	NEXT		Le jury valide le précédent
+230	ATB	ok	no	ok	*	oui	ATT	ADM, AJ	ADJ	NEXT		Le jury valide le précédent, pas moyenne gen., attente suivant
+240	ATB	ok	*	no	*	oui	ATB	ADM, AJ	ADJ	NEXT		Le jury valide le précédent, pb barre UE, attente
+250	ATB	no	*	*	*	oui	ATJ	AJ	ADJ	NEXT		Le jury valide le précédent, mais probleme assiduité.
+260	ATB,ATT	*	ok	ok	*	*	ADJ		AJ	REDOANNEE		Le jury valide ce semestre, et fait recommencer le précédent.
+												
+# Semestre prec. AJ (ajourné):												
+300	AJ	ok	no	*	*	*	AJ		AJ	REDOANNEE		Echec de 2 semestres, redouble année
+310	AJ	ok	ok	no	*	*	AJ		AJ	REDOANNEE		Echec de 2 semestres, redouble année
+320	AJ	no	*	*	*	*	NAR			REO		Echec, pas assidu: réorientation
+330	AJ	ok	ok	ok	*	*	ATT			REDOANNEE		Ne valide pas car mais manque le précédent: redouble ( modif 2017)
+												
+# Décisions du jury:												
+400	*	ok	no	*	*	*	ADJ	ADM,CMP		NEXT		Le jury décide de valider
+410	ATT,ATB	ok	no	*	*	*	ADJ	ADM,CMP	ADJ	NEXT		Le jury décide de valider ce semestre et le précédent
+420	*	ok	ok	no	*	*	ADJ	ADM,CMP		NEXT		Le jury décide de valider
+430	ATT,ATB	ok	ok	no	*	*	ADJ	ADM,CMP	ADJ	NEXT		Le jury décide de valider ce semestre et le précédent
+												
+												
+450	ATT,ATB	no	no	ok	*	oui	ATT	ADM, AJ	ADJ	NEXT		Pb moy: attente, mais le jury valide le précédent
+												
+# Semestres “décales” (REDOSEM)												
+500	None, ADM, ADC, ADJ,ATT,ATB	ok	no	*	no	*	AJ			REDOSEM		Pas moy: redouble ce semestre
+510	None, ADM, ADC, ADJ,ATT,ATB	ok	ok	no	no	*	AJ			REDOSEM		Pas barre UE: redouble ce semestre
+520	None, ADM, ADC, ADJ,ATB,ATT	no	*	*	*	*	AJ			REDOSEM		Pb assiduité: redouble ce semestre
+# Nouvelles regles avec plusieurs devenirs en semestres decales:												
+550	ATT,ATB	*	no	*	no	*	AJ			RA_OR_RS		Deux semestres ratés, choix de recommencer le premier ou le second
+560	ATT,ATB	*	ok	no	no	*	AJ			RA_OR_RS		Deux semestres ratés, choix de recommencer le premier ou le second
+570	None,ADM,ADJ,ADC	*	no	ok	no	*	ATT			RS_OR_NEXT		Semestre raté, choix de redoubler le semestre ou de continuer pour éventuellement compenser.
+580	None,ADM,ADJ,ADC	*	*	no	no	*	ATB			RS_OR_NEXT		Semestre raté, choix de redoubler ou de s'en remettre au jury du semestre suivant.
+												
+# Exclusion (art. 22): si precedent non valide et pas les barres dans le courant, on peut ne pas autoriser a redoubler:												
+# (le cas ATB est couvert plus haut)												
+600	AJ,ATT,NAR	ok	no	*	*	*	NAR		NAR	REO		Non autorisé à redoubler
+610	AJ,ATT,NAR	ok	ok	no	*	*	NAR		NAR	REO		Non autorisé à redoubler
diff --git a/misc/parcoursDUT.xls b/misc/parcoursDUT.xls
new file mode 100644
index 0000000000000000000000000000000000000000..32fed11ad591b72c316b6893cd1ec7decb54fe72
Binary files /dev/null and b/misc/parcoursDUT.xls differ
diff --git a/misc/reset_sem_ens.py b/misc/reset_sem_ens.py
new file mode 100755
index 0000000000000000000000000000000000000000..9420a0ff9d5a80143b95cad3d297ad1b5fe10788
--- /dev/null
+++ b/misc/reset_sem_ens.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""Affecte tous les modules d'un semestre au responsable de ce semestre
+Utile uniquement pour certains tests.
+
+(à lancer en tant qu'utilisateur postgres)
+Emmanuel Viennet, 2020
+"""
+from __future__ import print_function
+
+import pdb, os, sys
+import psycopg2
+
+
+if len(sys.argv) != 4:
+    print( 'Usage: %s database formsemestre_id user_name' % sys.argv[0])
+    print( 'Exemple: reset_sem_ens.py SCOGEII SEM34534 toto')
+    sys.exit(1)
+
+dbname = sys.argv[1]
+formsemestre_id = sys.argv[2]
+user_name = sys.argv[3]
+
+DBCNXSTRING = 'dbname=%s' % dbname
+
+cnx = psycopg2.connect( DBCNXSTRING )
+
+cursor = cnx.cursor()
+
+print('affecting all modules of semestre %s to "%s"' % (formsemestre_id, user_name))
+
+req = "update notes_moduleimpl set responsable_id=%(responsable_id)s where formsemestre_id=%(formsemestre_id)s"
+cursor.execute(req,  {'formsemestre_id':formsemestre_id, 'responsable_id': user_name})
+cnx.commit()
+
+
+
+
+
diff --git a/misc/testpydot.py b/misc/testpydot.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f2a3f6f732cd702cf1b13ce4ce72b076b6a2171
--- /dev/null
+++ b/misc/testpydot.py
@@ -0,0 +1,37 @@
+# essai pydot (bug ?)
+# EV, sept 2011
+
+import pydot
+
+print 'pydot version:', pydot.__version__
+
+g = pydot.Dot('graphname')
+g.add_node(pydot.Node('a'))
+g.add_node(pydot.Node('b'))
+
+
+n = g.get_node('a')
+
+print n
+print 'nodes names = %s' % [ x.get_name() for x in g.get_node_list() ]
+
+edges = [ ('a','b'), ('b','c'), ('c','d') ] 
+g = pydot.graph_from_edges(edges)
+print 'nodes names = %s' % [ x.get_name() for x in g.get_node_list() ]
+
+if not len(g.get_node_list()):
+    print 'bug: empty node list !' # incompatibility versions python / pydot 
+
+# Les fleches ?
+for (src_id, dst_id) in edges:
+    e = g.get_edge(src_id, dst_id)
+    e.set('arrowhead', 'normal')
+    e.set( 'arrowsize', 2 )
+    e.set_label( str( (src_id, dst_id) ) )
+    e.set_fontname('Helvetica')
+    e.set_fontsize(8.0)
+
+g.write_jpeg('/tmp/graph_from_edges_dot.jpg', prog='dot') # ok sur ScoDoc / Debian 5, pas de fleches en Debian 6
+# cf https://www-lipn.univ-paris13.fr/projects/scodoc/ticket/190
+
+
diff --git a/misc/zopelistmethods.py b/misc/zopelistmethods.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e1c1e9ab80382b1e626ea124561f406e28f463d
--- /dev/null
+++ b/misc/zopelistmethods.py
@@ -0,0 +1,66 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""List Zope published methods (helps redesign ScoDoc's API).
+
+Launch ScoDoc as follows: (as root)
+
+ /opt/scodoc/bin/zopectl debug 
+
+Then run this file
+
+E. Viennet 2020-01-26
+"""
+from __future__ import print_function
+from types import FunctionType, MethodType
+from pprint import pprint as pp
+import inspect
+
+from ZScoDoc import ZScoDoc
+from ZScolar import ZScolar
+from ZNotes import ZNotes
+from ZAbsences import ZAbsences
+from ZScoUsers import ZScoUsers
+from ZEntreprises import ZEntreprises
+
+
+def get_methods_description(klass):
+    D = klass.__dict__
+    M = []
+    for method_name in D:
+        o = D[method_name]
+        if method_name[0] != "_" and type(o) == FunctionType and o.__doc__:
+            argspec = inspect.getargspec(D[method_name])
+            defaults = argspec.defaults or []
+            if defaults:
+                args = argspec.args[: -len(defaults)]
+            else:
+                args = argspec.args
+            for a in defaults:
+                args.append("%s=%s" % (argspec.args[len(args)], repr(a)))
+            M.append((method_name, ", ".join(args)))
+    M.sort()
+    return M
+
+
+published_by_module = {}
+lines = []
+for klass in ZScoDoc, ZScolar, ZNotes, ZAbsences, ZEntreprises, ZScoUsers:
+    module_name = klass.__name__
+    published_by_module[module_name] = []
+    M = get_methods_description(klass)
+    for m in M:
+        published_by_module[module_name].append(m)
+        lines.append((module_name, m[0], m[1]))
+    print("%s = %d published methods" % (module_name, len(M)))
+
+print("---\nModule \t #methods")
+N = 0
+for module_name in published_by_module:
+    n = len(published_by_module[module_name])
+    print(module_name, "\t", n)
+    N += n
+
+print("Total: \t ", N)
+
+open("publishedmethods.csv", "w").write("\n".join(["\t".join(l) for l in lines]))
diff --git a/notes_cache.py b/notes_cache.py
new file mode 100644
index 0000000000000000000000000000000000000000..63d84e87bffef3e408686c0ce102a57dad958157
--- /dev/null
+++ b/notes_cache.py
@@ -0,0 +1,65 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Gestion rudimentaire de cache pour Notes
+
+
+  NOTA: inutilisable dans une instance Zope (can't pickle functions)
+"""
+
+from notes_log import log
+
+
+class CacheFunc:
+    """gestion rudimentaire de cache pour une fonction
+    func doit etre sans effet de bord, et sans arguments nommés
+    """
+
+    def __init__(self, func):
+        log("new CacheFunc for %s" % str(func))
+        self.func = func
+        self.cache = {}  # { arguments : function result }
+
+    def __call__(self, *args):
+        if self.cache.has_key(args):
+            # log('cache hit %s' % str(args))
+            return self.cache[args]
+        else:
+            val = self.func(*args)
+            self.cache[args] = val
+            log("caching %s(%s)" % (str(self.func), str(args)))
+            return val
+
+    def inval_cache_entry(self, *args):  # >
+        "expire cache for these args"
+        log("inval_cache_entry %s(%s)" % (str(self.func), str(args)))  # >
+        del self.cache[args]
+
+    def inval_cache(self):  # >
+        "clear whole cache"
+        log("inval_cache %s(%s)" % (str(self.func), str(args)))  # >
+        self.cache = {}
diff --git a/notes_log.py b/notes_log.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2b29a9efe6a844f51d0bba018c19c6847ef066c
--- /dev/null
+++ b/notes_log.py
@@ -0,0 +1,114 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+import pdb, os, sys, time, re, inspect
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email.Header import Header
+import traceback
+
+# Simple & stupid file logguer, used only to debug
+# (logging to SQL is done in scolog)
+
+
+LOG_FILENAME = "notes.log"  # empty to disable logging
+DEFAULT_LOG_DIR = "/tmp"  # clients should call set_log_directory to change this
+
+ALARM_DESTINATION = "emmanuel.viennet@univ-paris13.fr"  # XXX a mettre en preference
+
+
+class _logguer:
+    def __init__(self):
+        self.file = None
+        self.directory = None
+        self.set_log_directory(DEFAULT_LOG_DIR)
+
+    def set_log_directory(self, directory):
+        if self.directory != directory and self.file:
+            # changing directory when a log is already open: close it
+            self.file.close()
+            self.file = None
+        self.directory = directory
+
+    def _open(self):
+        if LOG_FILENAME:
+            path = os.path.join(self.directory, LOG_FILENAME)
+            self.file = open(path, "a")
+            self("new _logguer")
+        else:
+            self.file = None  # logging disabled
+
+    def __call__(self, msg):
+        if not self.file:
+            self._open()
+        if self.file:
+            dept = retreive_dept()
+            if dept:
+                dept = " (%s)" % dept
+            self.file.write(
+                "[%s]%s %s\n" % (time.strftime("%a %b %d %H:%M:%S %Y"), dept, msg)
+            )
+            # if not dept:
+            #    import traceback
+            #    traceback.print_stack(file=self.file) # hunt missing REQUESTS
+
+            self.file.flush()
+
+
+log = _logguer()
+
+
+def retreive_request(skip=0):
+    """Try to retreive a REQUEST variable in caller stack.
+    This is a hack, used only in log functions.
+    """
+
+    def search(frame):
+        if frame.f_locals.has_key("REQUEST"):
+            return frame.f_locals["REQUEST"]
+        if frame.f_back:
+            return search(frame.f_back)
+        else:
+            return None
+
+    frame = inspect.currentframe()
+    if frame:  # not supported by all pythons
+        startframe = frame
+        while skip and startframe.f_back:
+            startframe = startframe.f_back
+        return search(startframe)
+    else:
+        return None
+
+
+def retreive_dept():
+    """Try to retreive departement (from REQUEST URL)"""
+    REQUEST = retreive_request()
+    if not REQUEST:
+        return ""
+    try:
+        url = REQUEST.URL
+        m = re.match("^.*ScoDoc/(\w+).*$", url)
+        return m.group(1)
+    except:
+        return ""
+
+
+# Alarms by email:
+def sendAlarm(context, subj, txt):
+    import sco_utils
+
+    msg = MIMEMultipart()
+    subj = Header(subj, sco_utils.SCO_ENCODING)
+    msg["Subject"] = subj
+    msg["From"] = context.get_preference("email_from_addr")
+    msg["To"] = ALARM_DESTINATION
+    msg.epilogue = ""
+    txt = MIMEText(txt, "plain", sco_utils.SCO_ENCODING)
+    msg.attach(txt)
+    context.sendEmail(msg)
+
+
+# Debug: log call stack
+def logCallStack():
+    log("Call stack:\n" + "\n".join(x.strip() for x in traceback.format_stack()[:-1]))
diff --git a/notes_table.py b/notes_table.py
new file mode 100644
index 0000000000000000000000000000000000000000..28b1de96bbde9299cfa1f88a54bfd693c8891821
--- /dev/null
+++ b/notes_table.py
@@ -0,0 +1,1496 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Calculs sur les notes et cache des resultats
+"""
+from types import StringType
+import pdb
+import inspect
+
+import scolars
+import sco_groups
+from notes_log import log, logCallStack
+from sco_utils import *
+from notesdb import *
+import sco_codes_parcours
+from sco_parcours_dut import formsemestre_get_etud_capitalisation
+from sco_parcours_dut import list_formsemestre_utilisateurs_uecap
+import sco_parcours_dut
+import sco_formsemestre
+from sco_formsemestre_edit import formsemestre_uecoef_list, formsemestre_uecoef_create
+import sco_evaluations
+import sco_compute_moy
+from sco_formulas import NoteVector
+
+# Support for old user-written "bonus" functions with 2 args:
+BONUS_TWO_ARGS = len(inspect.getargspec(CONFIG.compute_bonus)[0]) == 2
+
+
+def comp_ranks(T):
+    """Calcul rangs à partir d'une liste ordonnée de tuples [ (valeur, ..., etudid) ] 
+    (valeur est une note numérique), en tenant compte des ex-aequos
+    Le resultat est: { etudid : rang } où rang est une chaine decrivant le rang
+    """
+    rangs = {}  # { etudid : rang } (rang est une chaine)
+    nb_ex = 0  # nb d'ex-aequo consécutifs en cours
+    for i in range(len(T)):
+        # test ex-aequo
+        if i < len(T) - 1:
+            next = T[i + 1][0]
+        else:
+            next = None
+        moy = T[i][0]
+        if nb_ex:
+            srang = "%d ex" % (i + 1 - nb_ex)
+            if moy == next:
+                nb_ex += 1
+            else:
+                nb_ex = 0
+        else:
+            if moy == next:
+                srang = "%d ex" % (i + 1 - nb_ex)
+                nb_ex = 1
+            else:
+                srang = "%d" % (i + 1)
+        rangs[T[i][-1]] = srang  # str(i+1)
+    return rangs
+
+
+def get_sem_ues_modimpls(context, formsemestre_id, modimpls=None):
+    """Get liste des UE du semestre (à partir des moduleimpls)
+    (utilisé quand on ne peut pas construire nt et faire nt.get_ues())
+    """
+    if modimpls is None:
+        modimpls = context.do_moduleimpl_list(formsemestre_id=formsemestre_id)
+    uedict = {}
+    for modimpl in modimpls:
+        mod = context.do_module_list(args={"module_id": modimpl["module_id"]})[0]
+        modimpl["module"] = mod
+        if not mod["ue_id"] in uedict:
+            ue = context.do_ue_list(args={"ue_id": mod["ue_id"]})[0]
+            uedict[ue["ue_id"]] = ue
+    ues = uedict.values()
+    ues.sort(key=lambda u: u["numero"])
+    return ues, modimpls
+
+
+def comp_etud_sum_coef_modules_ue(context, formsemestre_id, etudid, ue_id):
+    """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
+    ou None s'il n'y a aucun module.
+
+    (nécessaire pour éviter appels récursifs de nt, qui peuvent boucler)
+    """
+    infos = SimpleDictFetch(
+        context,
+        """SELECT mod.coefficient
+    FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins
+    WHERE mod.module_id = mi.module_id
+    and ins.etudid = %(etudid)s
+    and ins.moduleimpl_id = mi.moduleimpl_id
+    and mi.formsemestre_id = %(formsemestre_id)s
+    and mod.ue_id = %(ue_id)s
+    """,
+        {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
+    )
+
+    if not infos:
+        return None
+    else:
+        s = sum(x["coefficient"] for x in infos)
+        return s
+
+
+class NotesTable:
+    """Une NotesTable représente un tableau de notes pour un semestre de formation.
+    Les colonnes sont des modules.
+    Les lignes des étudiants.
+    On peut calculer les moyennes par étudiant (pondérées par les coefs)
+    ou les moyennes par module.
+
+    Attributs publics (en lecture):
+    - inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions)
+    - identdict: { etudid : ident }
+    - sem : le formsemestre
+    get_table_moyennes_triees: [ (moy_gen, moy_ue1, moy_ue2, ... moy_ues, moy_mod1, ..., moy_modn, etudid) ] 
+    (où toutes les valeurs sont soit des nombrs soit des chaines spéciales comme 'NA', 'NI'), 
+    incluant les UE de sport
+
+    - bonus[etudid] : valeur du bonus "sport".
+
+    Attributs privés:
+    - _modmoys : { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
+    - _ues : liste des UE de ce semestre (hors capitalisees)
+    - _matmoys : { matiere_id : { etudid: note moyenne dans cette matiere } }
+    
+    """
+
+    def __init__(self, context, formsemestre_id):
+        log("NotesTable( formsemestre_id=%s )" % formsemestre_id)
+        # open('/tmp/cache.log','a').write('NotesTables(%s)\n' % formsemestre_id) # XXX DEBUG
+        if not formsemestre_id:
+            logCallStack()
+            raise ScoValueError("invalid formsemestre_id (%s)" % formsemestre_id)
+        self.context = context
+        self.formsemestre_id = formsemestre_id
+        cnx = context.GetDBConnexion()
+        self.sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        self.moduleimpl_stats = {}  # { moduleimpl_id : {stats} }
+        self._uecoef = {}  # { ue_id : coef } cache coef manuels ue cap
+        self._evaluations_etats = None  # liste des evaluations avec état
+        self.use_ue_coefs = context.get_preference("use_ue_coefs", formsemestre_id)
+        # Infos sur les etudiants
+        self.inscrlist = context.do_formsemestre_inscription_list(
+            args={"formsemestre_id": formsemestre_id}
+        )
+        # infos identite etudiant
+        # xxx sous-optimal: 1/select par etudiant -> 0.17" pour identdict sur GTR1 !
+        self.identdict = {}  # { etudid : ident }
+        self.inscrdict = {}  # { etudid : inscription }
+        for x in self.inscrlist:
+            i = scolars.etudident_list(cnx, {"etudid": x["etudid"]})[0]
+            self.identdict[x["etudid"]] = i
+            self.inscrdict[x["etudid"]] = x
+            x["nomp"] = (i["nom_usuel"] or i["nom"]) + i["prenom"]  # pour tri
+
+        # Tri les etudids par NOM
+        self.inscrlist.sort(lambda x, y: cmp(x["nomp"], y["nomp"]))
+
+        # { etudid : rang dans l'ordre alphabetique }
+        rangalpha = {}
+        for i in range(len(self.inscrlist)):
+            rangalpha[self.inscrlist[i]["etudid"]] = i
+
+        self.bonus = DictDefault(defaultvalue=0)
+        # Notes dans les modules  { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
+        (
+            self._modmoys,
+            self._modimpls,
+            self._valid_evals_per_mod,
+            valid_evals,
+            mods_att,
+            self.expr_diagnostics,
+        ) = sco_compute_moy.do_formsemestre_moyennes(context, self, formsemestre_id)
+        self._mods_att = mods_att  # liste des modules avec des notes en attente
+        self._matmoys = {}  # moyennes par matieres
+        self._valid_evals = {}  # { evaluation_id : eval }
+        for e in valid_evals:
+            self._valid_evals[e["evaluation_id"]] = e  # Liste des modules et UE
+        uedict = {}  # public member: { ue_id : ue }
+        self.uedict = uedict
+        for modimpl in self._modimpls:
+            mod = modimpl["module"]  # has been added here by do_formsemestre_moyennes
+            if not mod["ue_id"] in uedict:
+                ue = context.do_ue_list(args={"ue_id": mod["ue_id"]})[0]
+                uedict[ue["ue_id"]] = ue
+            else:
+                ue = uedict[mod["ue_id"]]
+            modimpl["ue"] = ue  # add ue dict to moduleimpl
+            self._matmoys[mod["matiere_id"]] = {}
+            mat = context.do_matiere_list(args={"matiere_id": mod["matiere_id"]})[0]
+            modimpl["mat"] = mat  # add matiere dict to moduleimpl
+            # calcul moyennes du module et stocke dans le module
+            # nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif=
+
+        self.formation = context.formation_list(
+            args={"formation_id": self.sem["formation_id"]}
+        )[0]
+        self.parcours = sco_codes_parcours.get_parcours_from_code(
+            self.formation["type_parcours"]
+        )
+
+        # Decisions jury et UE capitalisées
+        self.comp_decisions_jury()
+        self.comp_ue_capitalisees()
+
+        # Liste des moyennes de tous, en chaines de car., triées
+        self._ues = uedict.values()
+        self._ues.sort(key=lambda u: u["numero"])
+
+        T = []
+        # XXX self.comp_ue_coefs(cnx)
+        self.moy_gen = {}  # etudid : moy gen (avec UE capitalisées)
+        self.moy_ue = {}  # ue_id : { etudid : moy ue } (valeur numerique)
+        self.etud_moy_infos = {}  # etudid : resultats de comp_etud_moy_gen()
+        valid_moy = []  # liste des valeurs valides de moyenne generale (pour min/max)
+        for ue in self._ues:
+            self.moy_ue[ue["ue_id"]] = {}
+        self._etud_moy_ues = {}  # { etudid : { ue_id : {'moy', 'sum_coefs', ... } }
+
+        for etudid in self.get_etudids():
+            etud_moy_gen = self.comp_etud_moy_gen(etudid, cnx)
+            self.etud_moy_infos[etudid] = etud_moy_gen
+            ue_status = etud_moy_gen["moy_ues"]
+            self._etud_moy_ues[etudid] = ue_status
+
+            moy_gen = etud_moy_gen["moy"]
+            self.moy_gen[etudid] = moy_gen
+            if etud_moy_gen["sum_coefs"] > 0:
+                valid_moy.append(moy_gen)
+
+            moy_ues = []
+            for ue in self._ues:
+                moy_ue = ue_status[ue["ue_id"]]["moy"]
+                moy_ues.append(moy_ue)
+                self.moy_ue[ue["ue_id"]][etudid] = moy_ue
+
+            t = [moy_gen] + moy_ues
+            #
+            is_cap = {}  # ue_id : is_capitalized
+            for ue in self._ues:
+                is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"]
+
+            for modimpl in self.get_modimpls():
+                val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
+                if is_cap[modimpl["module"]["ue_id"]]:
+                    t.append("-c-")
+                else:
+                    t.append(val)
+            #
+            t.append(etudid)
+            T.append(tuple(t))
+        # tri par moyennes décroissantes,
+        # en laissant les demissionnaires a la fin, par ordre alphabetique
+        def cmprows(x, y):
+            try:
+                return cmp(float(y[0]), float(x[0]))  # moy. gen.
+            except:
+                vx, vy = x[0], y[0]
+                try:
+                    vx = float(vx)
+                except:
+                    pass
+                try:
+                    vy = float(vy)
+                except:
+                    pass
+
+                if type(vx) == type(vy):  # and type(vx) == StringType:
+                    # rang alphabetique par nom
+                    return rangalpha[x[-1]] - rangalpha[y[-1]]
+                else:
+                    # Laisse les chaines a la fin de la liste
+                    return cmp(str(type(vx)), str(type(vy)))  # A revoir !
+                    # fallback *** should not occur ***
+                    # txt = '\nkey missing in cmprows !!!\nx=%s\ny=%s\n' % (str(x),str(y))
+                    # txt += '\nrangalpha=%s' % str(rangalpha) + '\n\nT=%s' % str(T)
+                    # context.send_debug_alert(txt, REQUEST=None)
+                    # return cmp(x,y)
+
+        T.sort(cmprows)
+        self.T = T
+
+        if len(valid_moy):
+            self.moy_min = min(valid_moy)
+            self.moy_max = max(valid_moy)
+        else:
+            self.moy_min = self.moy_max = "NA"
+
+        # calcul rangs (/ moyenne generale)
+        self.rangs = comp_ranks(T)
+
+        self.rangs_groupes = (
+            {}
+        )  # { group_id : { etudid : rang } }  (lazy, see get_etud_rang_group)
+        self.group_etuds = (
+            {}
+        )  # { group_id : set of etudids } (lazy, see get_etud_rang_group)
+
+        # calcul rangs dans chaque UE
+        ue_rangs = (
+            {}
+        )  # ue_rangs[ue_id] = ({ etudid : rang }, nb_inscrits) (rang est une chaine)
+        for ue in self._ues:
+            ue_id = ue["ue_id"]
+            val_ids = [
+                (self.moy_ue[ue_id][etudid], etudid) for etudid in self.moy_ue[ue_id]
+            ]
+            ue_eff = len(
+                [x for x in val_ids if type(x[0]) == FloatType]
+            )  # nombre d'étudiants avec une note dans l'UE
+            val_ids.sort(cmprows)
+            ue_rangs[ue_id] = (
+                comp_ranks(val_ids),
+                ue_eff,
+            )  # et non: len(self.moy_ue[ue_id]) qui est l'effectif de la promo
+        self.ue_rangs = ue_rangs
+        # ---- calcul rangs dans les modules
+        self.mod_rangs = {}
+        for modimpl in self._modimpls:
+            vals = self._modmoys[modimpl["moduleimpl_id"]]
+            val_ids = [(vals[etudid], etudid) for etudid in vals.keys()]
+            val_ids.sort(cmprows)
+            self.mod_rangs[modimpl["moduleimpl_id"]] = (comp_ranks(val_ids), len(vals))
+        #
+        self.compute_moy_moy()
+        #
+        log("NotesTable( formsemestre_id=%s ) done." % formsemestre_id)
+
+    def get_etudids(self, sorted=False):
+        if sorted:
+            # Tri par moy. generale décroissante
+            return [x[-1] for x in self.T]
+        else:
+            # Tri par ordre alphabetique de NOM
+            return [x["etudid"] for x in self.inscrlist]
+
+    def get_sexnom(self, etudid):
+        "M. DUPONT"
+        etud = self.identdict[etudid]
+        return etud["sexe"] + " " + strupper(etud["nom_usuel"] or etud["nom"])
+
+    def get_nom_short(self, etudid):
+        "formatte nom d'un etud (pour table recap)"
+        etud = self.identdict[etudid]
+        # Attention aux caracteres multibytes pour decouper les 2 premiers:
+        return (
+            strupper(etud["nom_usuel"] or etud["nom"])
+            + " "
+            + etud["prenom"].decode(SCO_ENCODING).capitalize()[:2].encode(SCO_ENCODING)
+            + "."
+        )
+
+    def get_nom_long(self, etudid):
+        "formatte nom d'un etud:  M. Pierre DUPONT"
+        etud = self.identdict[etudid]
+        return " ".join(
+            [
+                scolars.format_sexe(etud["sexe"]),
+                scolars.format_prenom(etud["prenom"]),
+                scolars.format_nom(etud["nom_usuel"] or etud["nom"]),
+            ]
+        )
+
+    def get_displayed_etud_code(self, etudid):
+        'code à afficher sur les listings "anonymes"'
+        return self.identdict[etudid]["code_nip"] or self.identdict[etudid]["etudid"]
+
+    def get_etud_etat(self, etudid):
+        "Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
+        if self.inscrdict.has_key(etudid):
+            return self.inscrdict[etudid]["etat"]
+        else:
+            return ""
+
+    def get_etud_etat_html(self, etudid):
+        etat = self.inscrdict[etudid]["etat"]
+        if etat == "I":
+            return ""
+        elif etat == "D":
+            return ' <font color="red">(DEMISSIONNAIRE)</font> '
+        elif etat == DEF:
+            return ' <font color="red">(DEFAILLANT)</font> '
+        else:
+            return ' <font color="red">(%s)</font> ' % etat
+
+    def get_ues(self, filter_sport=False, filter_non_inscrit=False, etudid=None):
+        """liste des ue, ordonnée par numero.
+        Si filter_non_inscrit, retire les UE dans lesquelles l'etudiant n'est 
+        inscrit à aucun module.
+        Si filter_sport, retire les UE de type SPORT
+        """
+        if not filter_sport and not filter_non_inscrit:
+            return self._ues
+
+        if filter_sport:
+            ues_src = [ue for ue in self._ues if ue["type"] != UE_SPORT]
+        else:
+            ues_src = self._ues
+        if not filter_non_inscrit:
+            return ues_src
+        ues = []
+        for ue in ues_src:
+            if self.get_etud_ue_status(etudid, ue["ue_id"])["is_capitalized"]:
+                # garde toujours les UE capitalisees
+                has_note = True
+            else:
+                has_note = False
+                # verifie que l'etud. est inscrit a au moins un module de l'UE
+                # (en fait verifie qu'il a une note)
+                modimpls = self.get_modimpls(ue["ue_id"])
+
+                for modi in modimpls:
+                    moy = self.get_etud_mod_moy(modi["moduleimpl_id"], etudid)
+                    try:
+                        float(moy)
+                        has_note = True
+                        break
+                    except:
+                        pass
+            if has_note:
+                ues.append(ue)
+        return ues
+
+    def get_modimpls(self, ue_id=None):
+        "liste des modules pour une UE (ou toutes si ue_id==None), triés par matières."
+        if ue_id is None:
+            r = self._modimpls
+        else:
+            r = [m for m in self._modimpls if m["ue"]["ue_id"] == ue_id]
+        # trie la liste par ue.numero puis mat.numero puis mod.numero
+        r.sort(
+            lambda x, y: cmp(
+                x["ue"]["numero"] * 1000000
+                + x["mat"]["numero"] * 1000
+                + x["module"]["numero"],
+                y["ue"]["numero"] * 1000000
+                + y["mat"]["numero"] * 1000
+                + y["module"]["numero"],
+            )
+        )
+        return r
+
+    def get_etud_eval_note(self, etudid, evaluation_id):
+        "note d'un etudiant a une evaluation"
+        return self._valid_evals[evaluation_id]["notes"][etudid]
+
+    def get_evals_in_mod(self, moduleimpl_id):
+        "liste des evaluations valides dans un module"
+        return [
+            e for e in self._valid_evals.values() if e["moduleimpl_id"] == moduleimpl_id
+        ]
+
+    def get_mod_stats(self, moduleimpl_id):
+        """moyenne generale, min, max pour un module
+        Ne prend en compte que les evaluations où toutes les notes sont entrées
+        Cache le resultat.
+        """
+        if moduleimpl_id in self.moduleimpl_stats:
+            return self.moduleimpl_stats[moduleimpl_id]
+        nb_notes = 0
+        sum_notes = 0.0
+        nb_missing = 0
+        moys = self._modmoys[moduleimpl_id]
+        vals = []
+        for etudid in self.get_etudids():
+            # saute les demissionnaires et les défaillants:
+            if self.inscrdict[etudid]["etat"] != "I":
+                continue
+            val = moys.get(etudid, None)  # None si non inscrit
+            try:
+                vals.append(float(val))
+            except:
+                nb_missing = nb_missing + 1
+        sum_notes = sum(vals)
+        nb_notes = len(vals)
+        if nb_notes > 0:
+            moy = sum_notes / nb_notes
+            max_note, min_note = max(vals), min(vals)
+        else:
+            moy, min_note, max_note = "NA", "-", "-"
+        s = {
+            "moy": moy,
+            "max": max_note,
+            "min": min_note,
+            "nb_notes": nb_notes,
+            "nb_missing": nb_missing,
+            "nb_valid_evals": len(self._valid_evals_per_mod[moduleimpl_id]),
+        }
+        self.moduleimpl_stats[moduleimpl_id] = s
+        return s
+
+    def compute_moy_moy(self):
+        """precalcule les moyennes d'UE et generale (moyennes sur tous
+        les etudiants), et les stocke dans self.moy_moy, self.ue['moy']
+
+        Les moyennes d'UE ne tiennent pas compte des capitalisations.
+        """
+        ues = self.get_ues()
+        sum_moy = 0  # la somme des moyennes générales valides
+        nb_moy = 0  # le nombre de moyennes générales valides
+        for ue in ues:
+            ue["_notes"] = []  # liste tmp des valeurs de notes valides dans l'ue
+        nb_dem = 0  # nb d'étudiants démissionnaires dans le semestre
+        nb_def = 0  # nb d'étudiants défaillants dans le semestre
+        T = self.get_table_moyennes_triees()
+        for t in T:
+            etudid = t[-1]
+            # saute les demissionnaires et les défaillants:
+            if self.inscrdict[etudid]["etat"] != "I":
+                if self.inscrdict[etudid]["etat"] == "D":
+                    nb_dem += 1
+                if self.inscrdict[etudid]["etat"] == DEF:
+                    nb_def += 1
+                continue
+            try:
+                sum_moy += float(t[0])
+                nb_moy += 1
+            except:
+                pass
+            i = 0
+            for ue in ues:
+                i += 1
+                try:
+                    ue["_notes"].append(float(t[i]))
+                except:
+                    pass
+        self.nb_demissions = nb_dem
+        self.nb_defaillants = nb_def
+        if nb_moy > 0:
+            self.moy_moy = sum_moy / nb_moy
+        else:
+            self.moy_moy = "-"
+
+        i = 0
+        for ue in ues:
+            i += 1
+            ue["nb_moy"] = len(ue["_notes"])
+            if ue["nb_moy"] > 0:
+                ue["moy"] = sum(ue["_notes"]) / ue["nb_moy"]
+                ue["max"] = max(ue["_notes"])
+                ue["min"] = min(ue["_notes"])
+            else:
+                ue["moy"], ue["max"], ue["min"] = "", "", ""
+            del ue["_notes"]
+
+    def get_etud_mod_moy(self, moduleimpl_id, etudid):
+        """moyenne d'un etudiant dans un module (ou NI si non inscrit)"""
+        return self._modmoys[moduleimpl_id].get(etudid, "NI")
+
+    def get_etud_mat_moy(self, matiere_id, etudid):
+        """moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
+        matmoy = self._matmoys.get(matiere_id, None)
+        if not matmoy:
+            return "NM"  # non inscrit
+            # log('*** oups: get_etud_mat_moy(%s, %s)' % (matiere_id, etudid))
+            # raise ValueError('matiere invalide !') # should not occur
+        return matmoy.get(etudid, "NA")
+
+    def comp_etud_moy_ue(self, etudid, ue_id=None, cnx=None):
+        """Calcule moyenne gen. pour un etudiant dans une UE 
+        Ne prend en compte que les evaluations où toutes les notes sont entrées
+        Return a dict(moy, nb_notes, nb_missing, sum_coefs)
+        Si pas de notes, moy == 'NA' et sum_coefs==0
+        Si non inscrit, moy == 'NI' et sum_coefs==0            
+        """
+        assert ue_id
+        modimpls = self.get_modimpls(ue_id)
+        nb_notes = 0  # dans cette UE
+        sum_notes = 0.0
+        sum_coefs = 0.0
+        nb_missing = 0  # nb de modules sans note dans cette UE
+
+        notes_bonus_gen = []  # liste des notes de sport et culture
+        coefs_bonus_gen = []
+
+        ue_malus = 0.0  # malus à appliquer à cette moyenne d'UE
+
+        notes = NoteVector()
+        coefs = NoteVector()
+        coefs_mask = NoteVector()  # 0/1, 0 si coef a ete annulé
+
+        matiere_id_last = None
+        matiere_sum_notes = matiere_sum_coefs = 0.0
+
+        est_inscrit = False  # inscrit à l'un des modules de cette UE ?
+
+        for modimpl in modimpls:
+            mod_ue_id = modimpl["ue"]["ue_id"]
+            # module ne faisant pas partie d'une UE capitalisee
+            val = self._modmoys[modimpl["moduleimpl_id"]].get(etudid, "NI")
+            # si 'NI', etudiant non inscrit a ce module
+            if val != "NI":
+                est_inscrit = True
+            if modimpl["module"]["module_type"] == MODULE_STANDARD:
+                coef = modimpl["module"]["coefficient"]
+                if modimpl["ue"]["type"] != UE_SPORT:
+                    notes.append(val, name=modimpl["module"]["code"])
+                    try:
+                        sum_notes += val * coef
+                        sum_coefs += coef
+                        nb_notes = nb_notes + 1
+                        coefs.append(coef)
+                        coefs_mask.append(1)
+                        matiere_id = modimpl["module"]["matiere_id"]
+                        if (
+                            matiere_id_last
+                            and matiere_id != matiere_id_last
+                            and matiere_sum_coefs
+                        ):
+                            self._matmoys[matiere_id_last][etudid] = (
+                                matiere_sum_notes / matiere_sum_coefs
+                            )
+                            matiere_sum_notes = matiere_sum_coefs = 0.0
+                        matiere_sum_notes += val * coef
+                        matiere_sum_coefs += coef
+                        matiere_id_last = matiere_id
+                    except:
+                        nb_missing = nb_missing + 1
+                        coefs.append(0)
+                        coefs_mask.append(0)
+
+                else:  # UE_SPORT:
+                    # la note du module de sport agit directement sur la moyenne gen.
+                    try:
+                        notes_bonus_gen.append(float(val))
+                        coefs_bonus_gen.append(coef)
+                    except:
+                        # log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef))
+                        pass
+            elif modimpl["module"]["module_type"] == MODULE_MALUS:
+                try:
+                    ue_malus += val
+                except:
+                    pass  # si non inscrit ou manquant, ignore
+            else:
+                raise ValueError(
+                    "invalid module type (%s)" % modimpl["module"]["module_type"]
+                )
+
+        if matiere_id_last and matiere_sum_coefs:
+            self._matmoys[matiere_id_last][etudid] = (
+                matiere_sum_notes / matiere_sum_coefs
+            )
+
+        # Calcul moyenne:
+        if sum_coefs > 0:
+            moy = sum_notes / sum_coefs
+            if ue_malus:
+                moy -= ue_malus
+                moy = max(NOTES_MIN, min(moy, 20.0))
+            moy_valid = True
+        else:
+            moy = "NA"
+            moy_valid = False
+
+        # Recalcule la moyenne en utilisant une formule utilisateur
+        expr_diag = {}
+        formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id, cnx)
+        if formula:
+            moy = sco_compute_moy.compute_user_formula(
+                self.context,
+                self.sem,
+                etudid,
+                moy,
+                moy_valid,
+                notes,
+                coefs,
+                coefs_mask,
+                formula,
+                diag_info=expr_diag,
+            )
+            if expr_diag:
+                expr_diag["ue_id"] = ue_id
+                self.expr_diagnostics.append(expr_diag)
+
+        return dict(
+            moy=moy,
+            nb_notes=nb_notes,
+            nb_missing=nb_missing,
+            sum_coefs=sum_coefs,
+            notes_bonus_gen=notes_bonus_gen,
+            coefs_bonus_gen=coefs_bonus_gen,
+            expr_diag=expr_diag,
+            ue_malus=ue_malus,
+            est_inscrit=est_inscrit,
+        )
+
+    def comp_etud_moy_gen(self, etudid, cnx):
+        """Calcule moyenne gen. pour un etudiant
+        Return a dict:
+         moy  : moyenne générale         
+         nb_notes, nb_missing, sum_coefs
+         ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
+         ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
+         ects_pot_pro: (float) nb d'ECTS issus d'UE pro
+         moy_ues : { ue_id : ue_status }
+        où ue_status = {
+             'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE
+             'moy' :  moyenne, avec capitalisation eventuelle
+             'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale
+                         (la somme des coefs des modules, ou le coef d'UE capitalisée, 
+                         ou encore le coef d'UE si l'option use_ue_coefs est active)
+             'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation)
+             'cur_coef_ue': coefficient de l'UE courante
+             'is_capitalized' : True|False,
+             'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
+             'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon, 
+             'ects_pot_pro' : 0 si UE non pro, = ects_pot sinon, 
+             'formsemestre_id' : (si capitalisee),
+             'event_date' : (si capitalisee)
+             }
+        Si pas de notes, moy == 'NA' et sum_coefs==0
+
+        Prend toujours en compte les UE capitalisées.
+        """
+        # log('comp_etud_moy_gen(etudid=%s)' % etudid)
+
+        # Si l'étudiant a Demissionné ou est DEFaillant, on n'enregistre pas ses moyennes
+        block_computation = (
+            self.inscrdict[etudid]["etat"] == "D"
+            or self.inscrdict[etudid]["etat"] == DEF
+        )
+
+        moy_ues = {}
+        notes_bonus_gen = (
+            []
+        )  # liste des notes de sport et culture (s'appliquant à la MG)
+        coefs_bonus_gen = []
+        nb_notes = 0  # nb de notes d'UE (non capitalisees)
+        sum_notes = 0.0  # somme des notes d'UE
+        # somme des coefs d'UE (eux-même somme des coefs de modules avec notes):
+        sum_coefs = 0.0
+
+        nb_missing = 0  # nombre d'UE sans notes
+        sem_ects_pot = 0.0
+        sem_ects_pot_fond = 0.0
+        sem_ects_pot_pro = 0.0
+
+        for ue in self.get_ues():
+            ue_id = ue["ue_id"]
+            # - On calcule la moyenne d'UE courante:
+            if not block_computation:
+                mu = self.comp_etud_moy_ue(etudid, ue_id=ue["ue_id"], cnx=cnx)
+            else:
+                mu = dict(
+                    moy="NA",
+                    nb_notes=0,
+                    nb_missing=0,
+                    sum_coefs=0,
+                    notes_bonus_gen=0,
+                    coefs_bonus_gen=0,
+                    expr_diag="",
+                    est_inscrit=False,
+                )
+            # infos supplementaires pouvant servir au calcul du bonus sport
+            mu["ue"] = ue
+            moy_ues[ue["ue_id"]] = mu
+
+            # - Faut-il prendre une UE capitalisée ?
+            if mu["moy"] != "NA" and mu["est_inscrit"]:
+                max_moy_ue = mu["moy"]
+            else:
+                # pas de notes dans l'UE courante, ou pas inscrit
+                max_moy_ue = 0.0
+            if not mu["est_inscrit"]:
+                coef_ue = 0.0
+            else:
+                if self.use_ue_coefs:
+                    coef_ue = mu["ue"]["coefficient"]
+                else:
+                    # coef UE = sum des coefs modules
+                    coef_ue = mu["sum_coefs"]
+
+            # is_capitalized si l'UE prise en compte est une UE capitalisée
+            mu["is_capitalized"] = False
+            # was_capitalized s'il y a precedemment une UE capitalisée (pas forcement meilleure)
+            mu["was_capitalized"] = False
+
+            is_external = 0
+            event_date = None
+            if not block_computation:
+                for ue_cap in self.ue_capitalisees[etudid]:
+                    if ue_cap["ue_code"] == ue["ue_code"]:
+                        moy_ue_cap = ue_cap["moy"]
+                        mu["was_capitalized"] = True
+                        event_date = event_date or ue_cap["event_date"]
+                        if (moy_ue_cap != "NA") and (moy_ue_cap > max_moy_ue):
+                            # meilleure UE capitalisée
+                            event_date = ue_cap["event_date"]
+                            max_moy_ue = moy_ue_cap
+                            mu["is_capitalized"] = True
+                            capitalized_ue_id = ue_cap["ue_id"]
+                            formsemestre_id = ue_cap["formsemestre_id"]
+                            coef_ue = self.get_etud_ue_cap_coef(
+                                etudid, ue, ue_cap, cnx=cnx
+                            )
+                            is_external = ue_cap["is_external"]
+
+            mu["cur_moy_ue"] = mu["moy"]  # la moyenne dans le sem. courant
+            if mu["est_inscrit"]:
+                mu["cur_coef_ue"] = mu["sum_coefs"]
+            else:
+                mu["cur_coef_ue"] = 0.0
+            mu["moy"] = max_moy_ue  # la moyenne d'UE a prendre en compte
+            mu["is_external"] = is_external  # validation externe (dite "antérieure")
+            mu["coef_ue"] = coef_ue  # coef reel ou coef de l'ue si capitalisee
+
+            if mu["is_capitalized"]:
+                mu["formsemestre_id"] = formsemestre_id
+                mu["capitalized_ue_id"] = capitalized_ue_id
+            if mu["was_capitalized"]:
+                mu["event_date"] = event_date
+            # - ECTS ? ("pot" pour "potentiels" car les ECTS ne seront acquises qu'apres validation du jury
+            if (
+                type(mu["moy"]) == FloatType
+                and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE
+            ):
+                mu["ects_pot"] = ue["ects"] or 0.0
+                if UE_is_fondamentale(ue["type"]):
+                    mu["ects_pot_fond"] = mu["ects_pot"]
+                else:
+                    mu["ects_pot_fond"] = 0.0
+                if UE_is_professionnelle(ue["type"]):
+                    mu["ects_pot_pro"] = mu["ects_pot"]
+                else:
+                    mu["ects_pot_pro"] = 0.0
+            else:
+                mu["ects_pot"] = 0.0
+                mu["ects_pot_fond"] = 0.0
+                mu["ects_pot_pro"] = 0.0
+            sem_ects_pot += mu["ects_pot"]
+            sem_ects_pot_fond += mu["ects_pot_fond"]
+            sem_ects_pot_pro += mu["ects_pot_pro"]
+
+            # - Calcul moyenne générale dans le semestre:
+            if mu["is_capitalized"]:
+                try:
+                    sum_notes += mu["moy"] * mu["coef_ue"]
+                    sum_coefs += mu["coef_ue"]
+                except:  # pas de note dans cette UE
+                    pass
+            else:
+                if mu["coefs_bonus_gen"]:
+                    notes_bonus_gen.extend(mu["notes_bonus_gen"])
+                    coefs_bonus_gen.extend(mu["coefs_bonus_gen"])
+                #
+                try:
+                    sum_notes += mu["moy"] * mu["sum_coefs"]
+                    sum_coefs += mu["sum_coefs"]
+                    nb_notes = nb_notes + 1
+                except TypeError:
+                    nb_missing = nb_missing + 1
+        # Le resultat:
+        infos = dict(
+            nb_notes=nb_notes,
+            nb_missing=nb_missing,
+            sum_coefs=sum_coefs,
+            moy_ues=moy_ues,
+            ects_pot=sem_ects_pot,
+            ects_pot_fond=sem_ects_pot_fond,
+            ects_pot_pro=sem_ects_pot_pro,
+            sem=self.sem,
+        )
+        # ---- Calcul moyenne (avec bonus sport&culture)
+        if sum_coefs <= 0 or block_computation:
+            infos["moy"] = "NA"
+        else:
+            if self.use_ue_coefs:
+                # Calcul optionnel (mai 2020)
+                # moyenne pondére par leurs coefficients des moyennes d'UE
+                sum_moy_ue = 0
+                sum_coefs_ue = 0
+                for mu in moy_ues.values():
+                    # mu["moy"] can be a number, or "NA", or "ERR" (user-defined UE formulas)
+                    if isnumber(mu["moy"]) and (
+                        mu["est_inscrit"] or mu["is_capitalized"]
+                    ):
+                        coef_ue = mu["ue"]["coefficient"]
+                        sum_moy_ue += mu["moy"] * coef_ue
+                        sum_coefs_ue += coef_ue
+                if sum_coefs_ue != 0:
+                    infos["moy"] = sum_moy_ue / sum_coefs_ue
+                else:
+                    infos["moy"] = "NA"
+            else:
+                # Calcul standard ScoDoc: moyenne pondérée des notes de modules
+                infos["moy"] = sum_notes / sum_coefs
+
+            if notes_bonus_gen and infos["moy"] != "NA":
+                # regle de calcul maison (configurable, voir bonus_sport.py)
+                if sum(coefs_bonus_gen) <= 0 and len(coefs_bonus_gen) != 1:
+                    log(
+                        "comp_etud_moy_gen: invalid or null coefficient (%s) for notes_bonus_gen=%s (etudid=%s, formsemestre_id=%s)"
+                        % (
+                            coefs_bonus_gen,
+                            notes_bonus_gen,
+                            etudid,
+                            self.formsemestre_id,
+                        )
+                    )
+                    bonus = 0
+                else:
+                    if len(coefs_bonus_gen) == 1:
+                        coefs_bonus_gen = [1.0]  # irrelevant, may be zero
+
+                    if BONUS_TWO_ARGS:
+                        # backward compat: compute_bonus took only 2 args
+                        bonus = CONFIG.compute_bonus(notes_bonus_gen, coefs_bonus_gen)
+                    else:
+                        bonus = CONFIG.compute_bonus(
+                            notes_bonus_gen, coefs_bonus_gen, infos=infos
+                        )
+                self.bonus[etudid] = bonus
+                infos["moy"] += bonus
+                infos["moy"] = min(infos["moy"], 20.0)  # clip bogus bonus
+
+        return infos
+
+    def get_etud_moy_gen(self, etudid):
+        """Moyenne generale de cet etudiant dans ce semestre.
+        Prend en compte les UE capitalisées.
+        Si pas de notes: 'NA'
+        """
+        return self.moy_gen[etudid]
+
+    def get_etud_moy_infos(self, etudid):
+        """Infos sur moyennes"""
+        return self.etud_moy_infos[etudid]
+
+    # was etud_has_all_ue_over_threshold:
+    def etud_check_conditions_ues(self, etudid):
+        """Vrai si les conditions sur les UE sont remplies.
+        Ne considère que les UE ayant des notes (moyenne calculée).
+        (les UE sans notes ne sont pas comptées comme sous la barre)
+        Prend en compte les éventuelles UE capitalisées.
+        
+        Pour les parcours habituels, cela revient à vérifier que
+        les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes)
+        
+        Pour les parcours non standards (LP2014), cela peut être plus compliqué.
+
+        Return: True|False, message explicatif
+        """
+        return self.parcours.check_barre_ues(
+            [self.get_etud_ue_status(etudid, ue["ue_id"]) for ue in self._ues]
+        )
+
+    def get_table_moyennes_triees(self):
+        return self.T
+
+    def get_etud_rang(self, etudid):
+        return self.rangs[etudid]
+
+    def get_etud_rang_group(self, etudid, group_id):
+        """Returns rank of etud in this group and number of etuds in group.
+        If etud not in group, returns None.
+        """
+        if not group_id in self.rangs_groupes:
+            # lazy: fill rangs_groupes on demand
+            # { groupe : { etudid : rang } }
+            if not group_id in self.group_etuds:
+                # lazy fill: list of etud in group_id
+                etuds = sco_groups.get_group_members(self.context, group_id)
+                self.group_etuds[group_id] = set([x["etudid"] for x in etuds])
+            # 1- build T restricted to group
+            Tr = []
+            for t in self.get_table_moyennes_triees():
+                t_etudid = t[-1]
+                if t_etudid in self.group_etuds[group_id]:
+                    Tr.append(t)
+            #
+            self.rangs_groupes[group_id] = comp_ranks(Tr)
+
+        return (
+            self.rangs_groupes[group_id].get(etudid, None),
+            len(self.rangs_groupes[group_id]),
+        )
+
+    def get_table_moyennes_dict(self):
+        """{ etudid : (liste des moyennes) } comme get_table_moyennes_triees
+        """
+        D = {}
+        for t in self.T:
+            D[t[-1]] = t
+        return D
+
+    def get_moduleimpls_attente(self):
+        "Liste des moduleimpls avec des notes en attente"
+        return self._mods_att
+
+    # Decisions existantes du jury
+    def comp_decisions_jury(self):
+        """Cherche les decisions du jury pour le semestre (pas les UE).
+        Calcule l'attribut:
+        decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
+        decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
+        Si la decision n'a pas été prise, la clé etudid n'est pas présente.
+        Si l'étudiant est défaillant, met un code DEF sur toutes les UE
+        """
+        cnx = self.context.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "select etudid, code, assidu, compense_formsemestre_id, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is NULL;",
+            {"formsemestre_id": self.formsemestre_id},
+        )
+        decisions_jury = {}
+        for (
+            etudid,
+            code,
+            assidu,
+            compense_formsemestre_id,
+            event_date,
+        ) in cursor.fetchall():
+            decisions_jury[etudid] = {
+                "code": code,
+                "assidu": assidu,
+                "compense_formsemestre_id": compense_formsemestre_id,
+                "event_date": DateISOtoDMY(event_date),
+            }
+
+        self.decisions_jury = decisions_jury
+        # UEs:
+        cursor.execute(
+            "select etudid, ue_id, code, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is not NULL;",
+            {"formsemestre_id": self.formsemestre_id},
+        )
+        decisions_jury_ues = {}
+        for (etudid, ue_id, code, event_date) in cursor.fetchall():
+            if not decisions_jury_ues.has_key(etudid):
+                decisions_jury_ues[etudid] = {}
+            # Calcul des ECTS associes a cette UE:
+            ects = 0.0
+            if sco_codes_parcours.code_ue_validant(code):
+                ue = self.uedict.get(ue_id, None)
+                if ue is None:  # not in list for this sem ??? (probably an error)
+                    log(
+                        "Warning: %s capitalized an UE %s which is not part of current sem %s"
+                        % (etudid, ue_id, self.formsemestre_id)
+                    )
+                    ue = self.context.do_ue_list(args={"ue_id": ue_id})[0]
+                    self.uedict[ue_id] = ue  # record this UE
+                    if ue_id not in self._uecoef:
+                        cl = formsemestre_uecoef_list(
+                            cnx,
+                            args={
+                                "formsemestre_id": self.formsemestre_id,
+                                "ue_id": ue_id,
+                            },
+                        )
+                        if not cl:
+                            # cas anormal: UE capitalisee, pas dans ce semestre, et sans coef
+                            log("Warning: setting UE coef to zero")
+                            formsemestre_uecoef_create(
+                                cnx,
+                                args={
+                                    "formsemestre_id": self.formsemestre_id,
+                                    "ue_id": ue_id,
+                                    "coefficient": 0,
+                                },
+                            )
+
+                ects = ue["ects"] or 0.0  # 0 if None
+
+            decisions_jury_ues[etudid][ue_id] = {
+                "code": code,
+                "ects": ects,  # 0. si non UE validée ou si mode de calcul different (?)
+                "event_date": DateISOtoDMY(event_date),
+            }
+
+        self.decisions_jury_ues = decisions_jury_ues
+
+    def get_etud_decision_sem(self, etudid):
+        """Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
+        { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
+        Si état défaillant, force le code a DEF
+        """
+        if self.get_etud_etat(etudid) == DEF:
+            return {
+                "code": DEF,
+                "assidu": 0,
+                "event_date": "",
+                "compense_formsemestre_id": None,
+            }
+        else:
+            return self.decisions_jury.get(etudid, None)
+
+    def get_etud_decision_ues(self, etudid):
+        """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
+        Ne tient pas compte des UE capitalisées.
+        { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
+        Ne renvoie aucune decision d'UE pour les défaillants
+        """
+        if self.get_etud_etat(etudid) == DEF:
+            return {}
+        else:
+            return self.decisions_jury_ues.get(etudid, None)
+
+    def sem_has_decisions(self):
+        """True si au moins une decision de jury dans ce semestre"""
+        return len([x for x in self.decisions_jury_ues.values() if x]) > 0
+
+    def etud_has_decision(self, etudid):
+        """True s'il y a une décision de jury pour cet étudiant"""
+        return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)
+
+    def all_etuds_have_sem_decisions(self):
+        """True si tous les étudiants du semestre ont une décision de jury.
+        ne regarde pas les décisions d'UE (todo: à voir ?)
+        """
+        for etudid in self.get_etudids():
+            if self.inscrdict[etudid]["etat"] == "D":
+                continue  # skip demissionnaires
+            if self.get_etud_decision_sem(etudid) is None:
+                return False
+        return True
+
+    # Capitalisation des UEs
+    def comp_ue_capitalisees(self):
+        """Cherche pour chaque etudiant ses UE capitalisées dans ce semestre.
+        Calcule l'attribut:
+        ue_capitalisees = { etudid :
+                             [{ 'moy':, 'event_date' : ,'formsemestre_id' : }, ...] }
+        """
+        self.ue_capitalisees = DictDefault(defaultvalue=[])
+        cnx = None
+        for etudid in self.get_etudids():
+            capital = formsemestre_get_etud_capitalisation(
+                self.context, self.sem, etudid
+            )
+            for ue_cap in capital:
+                # Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc)
+                # il faut la calculer ici et l'enregistrer
+                if ue_cap["moy_ue"] is None:
+                    log(
+                        "comp_ue_capitalisees: recomputing UE moy (etudid=%s, ue_id=%s formsemestre_id=%s)"
+                        % (etudid, ue_cap["ue_id"], ue_cap["formsemestre_id"])
+                    )
+                    nt_cap = self.context._getNotesCache().get_NotesTable(
+                        self.context, ue_cap["formsemestre_id"]
+                    )  # > UE capitalisees par un etud
+                    moy_ue_cap = nt_cap.get_etud_ue_status(etudid, ue_cap["ue_id"])[
+                        "moy"
+                    ]
+                    ue_cap["moy_ue"] = moy_ue_cap
+                    if (
+                        type(moy_ue_cap) == FloatType
+                        and moy_ue_cap >= self.parcours.NOTES_BARRE_VALID_UE
+                    ):
+                        if not cnx:
+                            cnx = self.context.GetDBConnexion(autocommit=False)
+                        sco_parcours_dut.do_formsemestre_validate_ue(
+                            cnx,
+                            nt_cap,
+                            ue_cap["formsemestre_id"],
+                            etudid,
+                            ue_cap["ue_id"],
+                            ue_cap["code"],
+                        )
+                    else:
+                        log(
+                            "*** valid inconsistency: moy_ue_cap=%s (etudid=%s, ue_id=%s formsemestre_id=%s)"
+                            % (
+                                moy_ue_cap,
+                                etudid,
+                                ue_cap["ue_id"],
+                                ue_cap["formsemestre_id"],
+                            )
+                        )
+                ue_cap["moy"] = ue_cap["moy_ue"]  # backward compat (needs refactoring)
+                self.ue_capitalisees[etudid].append(ue_cap)
+        if cnx:
+            cnx.commit()
+        # log('comp_ue_capitalisees=\n%s' % pprint.pformat(self.ue_capitalisees) )
+
+    # def comp_etud_sum_coef_modules_ue(self, etudid, ue_id):
+    #     """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
+    #     ou None s'il n'y a aucun module
+    #     """
+    #     c_list = [ mod['module']['coefficient']
+    #                for mod in self._modimpls
+    #                if (( mod['module']['ue_id'] == ue_id)
+    #                    and self._modmoys[mod['moduleimpl_id']].get(etudid, False) is not False)
+    #     ]
+    #     if not c_list:
+    #         return None
+    #     return sum(c_list)
+
+    def get_etud_ue_cap_coef(self, etudid, ue, ue_cap, cnx=None):
+        """Calcule le coefficient d'une UE capitalisée, pour cet étudiant, 
+        injectée dans le semestre courant.
+
+        ue : ue du semestre courant
+        
+        ue_cap = resultat de formsemestre_get_etud_capitalisation
+        { 'ue_id' (dans le semestre source), 
+          'ue_code', 'moy', 'event_date','formsemestre_id' }
+        """
+        # log("get_etud_ue_cap_coef\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s\n" % (self.formsemestre_id, etudid, ue, ue_cap))
+        # 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ?
+        if ue["ue_id"] not in self._uecoef:
+            self._uecoef[ue["ue_id"]] = formsemestre_uecoef_list(
+                cnx,
+                args={"formsemestre_id": self.formsemestre_id, "ue_id": ue["ue_id"]},
+            )
+
+        if len(self._uecoef[ue["ue_id"]]):
+            # utilisation du coef manuel
+            return self._uecoef[ue["ue_id"]][0]["coefficient"]
+
+        # 2- Mode automatique: calcul du coefficient
+        # Capitalisation depuis un autre semestre ScoDoc ?
+        coef = None
+        if ue_cap["formsemestre_id"]:
+            # Somme des coefs dans l'UE du semestre d'origine (nouveau: 23/01/2016)
+            coef = comp_etud_sum_coef_modules_ue(
+                self.context, ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"]
+            )
+        if coef != None:
+            return coef
+        else:
+            # Capitalisation UE externe: quel coef appliquer ?
+            # Si l'étudiant est inscrit dans le semestre courant,
+            # somme des coefs des modules de l'UE auxquels il est inscrit
+            c = comp_etud_sum_coef_modules_ue(
+                self.context, self.formsemestre_id, etudid, ue["ue_id"]
+            )
+            if c is not None:  # inscrit à au moins un module de cette UE
+                return c
+            # arfff: aucun moyen de déterminer le coefficient de façon sûre
+            log(
+                "* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s"
+                % (self.formsemestre_id, etudid, ue, ue_cap)
+            )
+            raise ScoValueError(
+                """<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer pour l'étudiant <a href="ficheEtud?etudid=%s" class="discretelink">%s</a></p>
+        <p>Il faut <a href="formsemestre_edit_uecoefs?formsemestre_id=%s&err_ue_id=%s">saisir le coefficient de cette UE avant de continuer</a></p>
+        </div>
+        """
+                % (
+                    ue["acronyme"],
+                    etudid,
+                    self.get_nom_long(etudid),
+                    self.formsemestre_id,
+                    ue["ue_id"],
+                )
+            )
+
+        return 0.0  # ?
+
+    def get_etud_ue_status(self, etudid, ue_id):
+        "Etat de cette UE (note, coef, capitalisation, ...)"
+        return self._etud_moy_ues[etudid][ue_id]
+
+    def etud_has_notes_attente(self, etudid):
+        """Vrai si cet etudiant a au moins une note en attente dans ce semestre.
+        (ne compte que les notes en attente dans des évaluation avec coef. non nul).
+        """
+        cnx = self.context.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "select n.* from notes_notes n, notes_evaluation e, notes_moduleimpl m, notes_moduleimpl_inscription i where n.etudid = %(etudid)s and n.value = %(code_attente)s and n.evaluation_id=e.evaluation_id and e.moduleimpl_id=m.moduleimpl_id and m.formsemestre_id=%(formsemestre_id)s and e.coefficient != 0 and m.moduleimpl_id=i.moduleimpl_id and i.etudid=%(etudid)s",
+            {
+                "formsemestre_id": self.formsemestre_id,
+                "etudid": etudid,
+                "code_attente": NOTES_ATTENTE,
+            },
+        )
+        return len(cursor.fetchall()) > 0
+
+    def get_evaluations_etats(self):  # evaluation_list_in_sem
+        """[ {...evaluation et son etat...} ]
+        """
+        if self._evaluations_etats is None:
+            self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
+                self.context, self.formsemestre_id
+            )
+
+        return self._evaluations_etats
+
+    def get_sem_evaluation_etat_list(self):
+        """Liste des evaluations de ce semestre, avec leur etat
+        """
+        return self.get_evaluations_etats()
+
+    def get_mod_evaluation_etat_list(self, moduleimpl_id):
+        """Liste des évaluations de ce module
+        """
+        return [
+            e
+            for e in self.get_evaluations_etats()
+            if e["moduleimpl_id"] == moduleimpl_id
+        ]
+
+
+import thread
+
+
+class CacheNotesTable:
+    """gestion rudimentaire de cache pour les NotesTables"""
+
+    def __init__(self):
+        log("new CacheTable (id=%s)" % id(self))
+        #
+        self.lock = thread.allocate_lock()
+        self.owner_thread = None  # thread owning this cache
+        self.nref = 0
+        # Cache des NotesTables
+        self.cache = {}  # { formsemestre_id : NoteTable instance }
+        # Cache des classeur PDF (bulletins)
+        self.pdfcache = {}  # { formsemestre_id : (filename, pdfdoc) }
+        # Listeners:
+        self.listeners = DictDefault(
+            defaultvalue={}
+        )  # {formsemestre_id : {listener_id : callback }}
+
+    def acquire(self):
+        "If this thread does not own the cache, acquire the lock"
+        if thread.get_ident() != self.owner_thread:
+            if self.lock.locked():
+                log(
+                    "acquire: ident=%s waiting for lock" % thread.get_ident()
+                )  # XXX debug
+            self.lock.acquire()
+            self.owner_thread = thread.get_ident()
+            if self.owner_thread is None:  # bug catching
+                log("WARNING: None thread id !")
+        self.nref += 1
+        # log('nref=%d' % self.nref)
+
+    def release(self):
+        "Release the lock"
+        cur_owner_thread = self.owner_thread
+        # log('release: ident=%s (nref=%d)' % (thread.get_ident(), self.nref))
+        self.nref -= 1
+        if self.nref == 0:
+            self.lock.release()
+            self.owner_thread = None
+        # Debug:
+        if thread.get_ident() != cur_owner_thread:
+            log(
+                "WARNING: release: ident=%s != owner=%s nref=%d"
+                % (thread.get_ident(), cur_owner_thread, self.nref)
+            )
+            raise NoteProcessError("problem with notes cache")
+
+    def get_NotesTable(self, context, formsemestre_id):  # >
+        try:
+            self.acquire()
+            if self.cache.has_key(formsemestre_id):
+                # log('cache hit %s (id=%s, thread=%s)'
+                #    % (formsemestre_id, id(self), thread.get_ident()))
+                return self.cache[formsemestre_id]
+            else:
+                t0 = time.time()
+                nt = NotesTable(context, formsemestre_id)
+                dt = time.time() - t0
+                self.cache[formsemestre_id] = nt
+                log(
+                    "caching formsemestre_id=%s (id=%s) (%gs)"
+                    % (formsemestre_id, id(self), dt)
+                )
+                return nt
+        finally:
+            self.release()
+
+    def get_cached_formsemestre_ids(self):
+        "List of currently cached formsemestre_id"
+        return self.cache.keys()
+
+    def inval_cache(self, context, formsemestre_id=None, pdfonly=False):  # >
+        "expire cache pour un semestre (ou tous si pas d'argument)"
+        log(
+            "inval_cache, formsemestre_id=%s pdfonly=%s (id=%s)"
+            % (formsemestre_id, pdfonly, id(self))  # >
+        )
+        try:
+            self.acquire()
+            if not hasattr(self, "pdfcache"):
+                self.pdfcache = {}  # fix for old zope instances...
+            if formsemestre_id is None:
+                # clear all caches
+                log("----- inval_cache: clearing all caches -----")
+                # log('cache was containing ' + str(self.cache.keys()))
+                # logCallStack() # >>> DEBUG <<<
+                if not pdfonly:
+                    self.cache = {}
+                self.pdfcache = {}
+                self._call_all_listeners()
+                context.get_evaluations_cache().inval_cache()
+            else:
+                # formsemestre_id modifié:
+                # on doit virer formsemestre_id et tous les semestres
+                # susceptibles d'utiliser des UE capitalisées de ce semestre.
+                to_trash = [formsemestre_id] + list_formsemestre_utilisateurs_uecap(
+                    context, formsemestre_id
+                )
+                if not pdfonly:
+                    for formsemestre_id in to_trash:
+                        if self.cache.has_key(formsemestre_id):
+                            log(
+                                "delete %s from cache (id=%s)"
+                                % (formsemestre_id, id(self))
+                            )
+                            del self.cache[formsemestre_id]
+                            self._call_listeners(formsemestre_id)
+                    context.get_evaluations_cache().inval_cache()
+
+                for formsemestre_id in to_trash:
+                    for (
+                        cached_formsemestre_id,
+                        cached_version,
+                    ) in self.pdfcache.keys():
+                        if cached_formsemestre_id == formsemestre_id:
+                            log(
+                                "delete pdfcache[(%s,%s)]"
+                                % (formsemestre_id, cached_version)
+                            )
+                            del self.pdfcache[(formsemestre_id, cached_version)]
+        finally:
+            self.release()
+
+    def store_bulletins_pdf(self, formsemestre_id, version, filename, pdfdoc):
+        "cache pdf data"
+        log(
+            "caching PDF formsemestre_id=%s version=%s (id=%s)"
+            % (formsemestre_id, version, id(self))
+        )
+        try:
+            self.acquire()
+            self.pdfcache[(formsemestre_id, version)] = (filename, pdfdoc)
+        finally:
+            self.release()
+
+    def get_bulletins_pdf(self, formsemestre_id, version):
+        "returns cached PDF, or None if not in the cache"
+        try:
+            self.acquire()
+            if not hasattr(self, "pdfcache"):
+                self.pdfcache = {}  # fix for old zope instances...
+            r = self.pdfcache.get((formsemestre_id, version), None)
+            if r:
+                log(
+                    "get_bulletins_pdf(%s): cache hit %s (id=%s, thread=%s)"
+                    % (version, formsemestre_id, id(self), thread.get_ident())
+                )
+            return r
+        finally:
+            self.release()
+
+    def add_listener(self, callback, formsemestre_id, listener_id):
+        """Add a "listener": a function called each time a formsemestre is modified"""
+        self.listeners[formsemestre_id][listener_id] = callback
+
+    def remove_listener(self, formsemestre_id, listener_id):
+        """Remove a listener.
+        May raise exception if does not exists.
+        """
+        del self.listeners[formsemestre_id][listener_id]
+
+    def _call_listeners(self, formsemestre_id):
+        for listener_id, callback in self.listeners[formsemestre_id].items():
+            callback(listener_id)
+
+    def _call_all_listeners(self):
+        for formsemestre_id in self.listeners:
+            self._call_listeners(formsemestre_id)
+
+
+#
+# Cache global: chaque instance, repérée par sa connexion a la DB, a un cache
+# qui est recréé à la demande (voir ZNotes._getNotesCache() )
+#
+NOTES_CACHE_INST = {}  # { db_cnx_string : CacheNotesTable instance }
diff --git a/notes_users.py b/notes_users.py
new file mode 100644
index 0000000000000000000000000000000000000000..6917ab3f69116966f3ae23c7a82d904fb0fd65c3
--- /dev/null
+++ b/notes_users.py
@@ -0,0 +1,31 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Basic User management
+"""
+
+raise NotImplementedError()
diff --git a/notesdb.py b/notesdb.py
new file mode 100644
index 0000000000000000000000000000000000000000..409f0bf344b0afb9b0f10b2e22c99e206afd01ba
--- /dev/null
+++ b/notesdb.py
@@ -0,0 +1,630 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+import pdb, os, sys, string
+import traceback
+import psycopg2
+import psycopg2.pool
+import thread
+from notes_log import log
+from sco_exceptions import *
+from types import *
+from cgi import escape
+import datetime
+
+quote_html = escape
+
+
+def quote_dict(d):
+    "html quote all values in dict"
+    for k in d.keys():
+        v = d[k]
+        if type(v) == StringType:
+            d[k] = quote_html(v, quote=True)
+
+
+def unquote(s):
+    "inverse of quote"
+    # pas d'inverse de cgi.escape
+    # ne traite que &
+    # XX voir aussi sco_utils.unescape_html
+    return s.replace("&amp;", "&")
+
+
+# Ramene une connexion a la base de donnees scolarite
+# pour l'instance donnee par context
+# La connexion est unique (réutilisée) pour chaque thread
+# et est par défaut en autocommit
+_pools = {}
+
+
+def GetDBConnexion(context, autocommit=True):
+    pool = _pools.get(context._db_cnx_string, None)
+    if not pool:
+        pool = psycopg2.pool.ThreadedConnectionPool(2, 8, dsn=context._db_cnx_string)
+        _pools[context._db_cnx_string] = pool
+        # log('GetDBConnexion: created pool for "%s"' % context._db_cnx_string)
+    cnx = pool.getconn(key=(thread.get_ident(), autocommit))
+    # log('GetDBConnexion: autocommit=%s cnx=%s' % (autocommit,cnx))
+    if cnx.autocommit != autocommit:
+        cnx.autocommit = autocommit
+    return cnx
+
+
+# Same for users:
+_users_pools = {}
+
+
+def GetUsersDBConnexion(context, autocommit=True):
+    pool = _users_pools.get(context._db_cnx_string, None)
+    if not pool:
+        pool = psycopg2.pool.ThreadedConnectionPool(2, 8, dsn=context._db_cnx_string)
+        _users_pools[context._db_cnx_string] = pool
+        log('GetUsersDBConnexion: created pool for "%s"' % context._db_cnx_string)
+    cnx = pool.getconn(key=(thread.get_ident(), autocommit))
+    if cnx.autocommit != autocommit:
+        cnx.autocommit = autocommit
+    return cnx
+
+
+class ScoDocCursor(psycopg2.extensions.cursor):
+    """A database cursor emulating some methods of psycopg v1 cursors"""
+
+    def dictfetchall(cursor):
+        col_names = [d[0] for d in cursor.description]
+        return [dict(zip(col_names, row)) for row in cursor.fetchall()]
+
+    def dictfetchone(cursor):
+        col_names = [d[0] for d in cursor.description]
+        row = cursor.fetchone()
+        if row:
+            return dict(zip(col_names, row))
+        else:
+            return {}
+
+
+def SimpleQuery(context, query, args, cursor=None):
+    if not cursor:
+        cnx = context.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    # log( 'SimpleQuery(%s)' % (query % args) )
+    cursor.execute(query, args)
+    return cursor
+
+
+def SimpleDictFetch(context, query, args, cursor=None):
+    cursor = SimpleQuery(context, query, args, cursor=cursor)
+    return cursor.dictfetchall()
+
+
+def DBInsertDict(cnx, table, vals, commit=0, convert_empty_to_nulls=1):
+    "insert into table values in dict 'vals'"
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    if convert_empty_to_nulls:
+        for col in vals.keys():
+            if vals[col] == "":
+                vals[col] = None
+    # open('/tmp/vals','a').write( str(vals) + '\n' )
+    cols = vals.keys()
+    colnames = ",".join(cols)
+    fmt = ",".join(["%%(%s)s" % col for col in cols])
+    # print 'insert into %s (%s) values (%s)' % (table,colnames,fmt)
+    oid = None
+    try:
+        if vals:
+            cursor.execute(
+                "insert into %s (%s) values (%s)" % (table, colnames, fmt), vals
+            )
+        else:
+            cursor.execute("insert into %s default values" % table)
+        oid = cursor.lastrowid
+    except:
+        log("DBInsertDict: EXCEPTION !")
+        log("DBInsertDict: table=%s, vals=%s" % (str(table), str(vals)))
+        log("DBInsertDict: commit (exception)")
+        cnx.commit()  # get rid of this transaction
+        raise  # and re-raise exception
+    if commit:
+        log("DBInsertDict: commit (requested)")
+        cnx.commit()
+    return oid
+
+
+def DBSelectArgs(
+    cnx,
+    table,
+    vals,
+    what=["*"],
+    sortkey=None,
+    test="=",
+    operator="and",
+    distinct=True,
+    aux_tables=[],
+    id_name=None,
+):
+    """Select * from table where values match dict vals.
+    Returns cnx, columns_names, list of tuples
+    aux_tables = ( tablename, id_name )
+    """
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    if sortkey:
+        orderby = " order by " + sortkey
+    else:
+        orderby = ""
+    if distinct:
+        distinct = " distinct "
+    else:
+        distinct = ""
+    operator = " " + operator + " "
+    # liste des tables (apres "from")
+    tables = [table] + [x[0] for x in aux_tables]
+    for i in range(len(tables)):
+        tables[i] = "%s T%d" % (tables[i], i)
+    tables = ", ".join(tables)
+    # condition (apres "where")
+    cond = ""
+    i = 1
+    cl = []
+    for (aux_tab, aux_id) in aux_tables:
+        cl.append("T0.%s = T%d.%s" % (id_name, i, aux_id))
+        i = i + 1
+    cond += " and ".join(cl)
+    # Traitement des expressions régulières:
+    #  n'autorise pas d'expressions
+    if test == "~":
+        for k in vals.keys():
+            vals[k] = vals[k].translate(string.maketrans("", ""), '%*()+=&|[]"`')
+
+    if vals:
+        if aux_tables:  # paren
+            cond += " AND ( "
+        cond += operator.join(
+            ["T0.%s%s%%(%s)s" % (x, test, x) for x in vals.keys() if vals[x] != None]
+        )
+        cnuls = " and ".join(
+            ["T0.%s is NULL" % x for x in vals.keys() if vals[x] is None]
+        )
+        if cnuls:
+            if cond:
+                cond = cond + " and " + cnuls
+            else:
+                cond = cnuls
+        # close paren
+        if aux_tables:
+            cond += ") "
+    if cond:
+        cond = " where " + cond
+    #
+    req = "select " + distinct + ", ".join(what) + " from " + tables + cond + orderby
+    # open('/tmp/select.log','a').write( req % vals + '\n' )
+    try:
+        cursor.execute(req, vals)
+    except:
+        log('Exception in DBSelectArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals))
+        log(traceback.format_exc())
+        raise ScoException()
+    return cursor.dictfetchall()
+
+
+def DBUpdateArgs(cnx, table, vals, where=None, commit=False, convert_empty_to_nulls=1):
+    if not vals or where is None:
+        return
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    if convert_empty_to_nulls:
+        for col in vals.keys():
+            if vals[col] == "":
+                vals[col] = None
+    s = ", ".join(["%s=%%(%s)s" % (x, x) for x in vals.keys()])
+    try:
+        req = "update " + table + " set " + s + " where " + where
+        cursor.execute(req, vals)
+        # log('req=%s\n'%req)
+        # log('vals=%s\n'%vals)
+    except:
+        cnx.commit()  # get rid of this transaction
+        log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals))
+        raise  # and re-raise exception
+    if commit:
+        cnx.commit()
+
+
+def DBDelete(cnx, table, colid, val, commit=False):
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    try:
+        cursor.execute(
+            "delete from " + table + " where %s=%%(%s)s" % (colid, colid), {colid: val}
+        )
+    except:
+        cnx.commit()  # get rid of this transaction
+        raise  # and re-raise exception
+    if commit:
+        cnx.commit()
+
+
+# --------------------------------------------------------------------
+
+# REQLOG = open('/tmp/req.log', 'w') # DEBUG
+# REQN = 0
+
+
+class EditableTable:
+    """ --- generic class: SQL table with create/edit/list/delete
+    """
+
+    def __init__(
+        self,
+        table_name,
+        id_name,
+        dbfields,
+        sortkey=None,
+        output_formators={},
+        input_formators={},
+        aux_tables=[],
+        convert_null_outputs_to_empty=True,
+        callback_on_write=None,
+        allow_set_id=False,
+        html_quote=True,
+        fields_creators={},  # { field : [ sql_command_to_create_it ] }
+        filter_nulls=True,  # dont allow to set fields to null
+    ):
+        self.table_name = table_name
+        self.id_name = id_name
+        self.aux_tables = aux_tables
+        self.dbfields = dbfields
+        self.sortkey = sortkey
+        self.output_formators = output_formators
+        self.input_formators = input_formators
+        self.convert_null_outputs_to_empty = convert_null_outputs_to_empty
+        self.callback_on_write = (
+            callback_on_write  # called after each modification (USELESS and unused)
+        )
+        self.allow_set_id = allow_set_id
+        self.html_quote = html_quote
+        self.fields_creators = fields_creators
+        self.filter_nulls = filter_nulls
+        self.sql_default_values = None
+
+    def create(self, cnx, args, has_uniq_values=False):
+        "create object in table"
+        vals = dictfilter(args, self.dbfields, self.filter_nulls)
+        if vals.has_key(self.id_name) and not self.allow_set_id:
+            del vals[self.id_name]
+        if self.html_quote:
+            quote_dict(vals)  # quote all HTML markup
+        # format value
+        for title in vals.keys():
+            if self.input_formators.has_key(title):
+                vals[title] = self.input_formators[title](vals[title])
+        # insert
+        oid = DBInsertDict(cnx, self.table_name, vals, commit=True)
+        # get back new object id
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        cursor.execute(
+            "select %(id_name)s from %(table_name)s where oid=%(oid)s"
+            % {"id_name": self.id_name, "table_name": self.table_name, "oid": oid}
+        )
+        new_id = cursor.fetchone()[0]
+        if has_uniq_values:  # XXX probably obsolete
+            # check  all tuples (without id_name) are UNIQUE !
+            res = DBSelectArgs(cnx, self.table_name, vals, what=[self.id_name])
+            if len(res) != 1:
+                # BUG !
+                log("create: BUG table_name=%s args=%s" % (self.table_name, str(args)))
+                assert len(res) == 1, "len(res) = %d != 1 !" % len(res)
+        if self.callback_on_write:
+            self.callback_on_write()
+
+        return new_id
+
+    def delete(self, cnx, oid, commit=True):
+        "delete tuple"
+        DBDelete(cnx, self.table_name, self.id_name, oid, commit=commit)
+        if self.callback_on_write:
+            self.callback_on_write()
+
+    def list(
+        self,
+        cnx,
+        args={},
+        operator="and",
+        test="=",
+        sortkey=None,
+        disable_formatting=False,
+    ):
+        "returns list of dicts"
+        # REQLOG.write('%s: %s by %s (%s) %d\n'%(self.table_name,args,sys._getframe(1).f_code.co_name, sys._getframe(2).f_code.co_name, REQN))
+        # REQLOG.flush()
+        # global REQN
+        # REQN = REQN + 1
+        vals = dictfilter(args, self.dbfields, self.filter_nulls)
+        if not sortkey:
+            sortkey = self.sortkey
+        res = DBSelectArgs(
+            cnx,
+            self.table_name,
+            vals,
+            sortkey=sortkey,
+            test=test,
+            operator=operator,
+            aux_tables=self.aux_tables,
+            id_name=self.id_name,
+        )
+        for r in res:
+            self.format_output(r, disable_formatting=disable_formatting)
+        return res
+
+    def format_output(self, r, disable_formatting=False):
+        "Format dict using provided output_formators"
+        for (k, v) in r.items():
+            if v is None and self.convert_null_outputs_to_empty:
+                v = ""
+            # format value
+            if not disable_formatting and self.output_formators.has_key(k):
+                try:  # XXX debug "isodate"
+                    v = self.output_formators[k](v)
+                except:
+                    log("*** list: vars=%s" % str(vars()))
+                    log("*** list: r=%s" % str(r))
+                    raise
+            r[k] = v
+
+    def edit(self, cnx, args, html_quote=None):
+        """Change fields"""
+        vals = dictfilter(args, self.dbfields, self.filter_nulls)
+        html_quote = html_quote or self.html_quote
+        if html_quote:
+            quote_dict(vals)  # quote HTML
+        # format value
+        for title in vals.keys():
+            if self.input_formators.has_key(title):
+                vals[title] = self.input_formators[title](vals[title])
+        DBUpdateArgs(
+            cnx,
+            self.table_name,
+            vals,
+            where="%s=%%(%s)s" % (self.id_name, self.id_name),
+            commit=True,
+        )
+        if self.callback_on_write:
+            self.callback_on_write()
+
+    def get_sql_default_values(self, cnx):
+        "return dict with SQL default values for each field"
+        if self.sql_default_values is None:  # not cached
+            # We insert a new tuple, get the values and delete it
+            # XXX non, car certaines tables ne peuvent creer de tuples
+            # a default, a cause des references ou contraintes d'intégrité.
+            # oid = self.create(cnx, {})
+            # vals = self.list(cnx, args= {self.id_name : oid})[0]
+            # self.delete(cnx, oid)
+            # self.sql_default_values = vals
+            #
+            # Méthode spécifique à postgresql (>= 7.4)
+            cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+            cursor.execute(
+                "SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_name = '%s'"
+                % self.table_name
+            )
+            d = {}
+            for info in cursor.dictfetchall():
+                v = info["column_default"]
+                # strip type information if present (eg 'hello'::text)
+                if v:
+                    v = v.split("::")[0]
+                # convert type to Python type
+                if v:
+                    if info["data_type"] == "text":
+                        log('get: v="%s"' % v)
+                        if v[0] == v[-1] == "'":
+                            v = v[1:-1]  # strip quotes
+                        elif v[:2] == "E'" and v[-1] == "'":
+                            v = v[2:-1]  # postgresql string with escapes
+                        v = v.replace(
+                            "\\012", "\n"
+                        )  # fix (je ne comprend pas bien pourquoi les valeurs sont ici quotées, ce n'est pas le cas dans les tables ordinaires)
+                        v = v.replace("''", "'")  # idem
+                        log('-->  v="%s"' % v)
+                    elif info["data_type"] == "real":
+                        v = float(v)
+                    elif info["data_type"] == "integer":
+                        v = int(v)
+                    # elif info['data_type'] == 'date':
+                    #    pass # XXX todo
+                    else:
+                        log("Warning: unhandled SQL type in get_sql_default_values")
+                d[info["column_name"]] = v
+            self.sql_default_values = d
+        return self.sql_default_values
+
+
+def dictfilter(d, fields, filter_nulls=True):
+    # returns a copy of d with only keys listed in "fields" and non null values
+    r = {}
+    for f in fields:
+        if d.has_key(f) and (d[f] != None or not filter_nulls):
+            try:
+                val = d[f].strip()
+            except:
+                val = d[f]
+            # if val != '': not a good idea: how to suppress a field ?
+            r[f] = val
+    return r
+
+
+# --------------------------------------------------------------------
+# --- Misc Tools
+
+
+def DateDMYtoISO(dmy, null_is_empty=False):
+    "convert date string from french format to ISO"
+    if not dmy:
+        if null_is_empty:
+            return ""
+        else:
+            return None
+    if type(dmy) != StringType:
+        return dmy.strftime("%Y-%m-%d")
+
+    t = dmy.split("/")
+
+    if len(t) != 3:
+        raise ScoValueError('Format de date (j/m/a) invalide: "%s"' % str(dmy))
+    day, month, year = t
+    year = int(year)
+    month = int(month)
+    day = int(day)
+    # accept years YYYY or YY, uses 1970 as pivot
+    if year < 100:
+        if year > 70:
+            year += 1900
+        else:
+            year += 2000
+
+    if month < 1 or month > 12:
+        raise ScoValueError("mois de la date invalide ! (%s)" % month)
+    # compute nb of day in month:
+    mo = month
+    if mo > 7:
+        mo = mo + 1
+    if mo % 2:
+        MonthNbDays = 31
+    elif mo == 2:
+        if year % 4 == 0 and ((year % 100 != 0) or (year % 400 == 0)):
+            MonthNbDays = 29  # leap
+        else:
+            MonthNbDays = 28
+    else:
+        MonthNbDays = 30
+    if day < 1 or day > MonthNbDays:
+        raise ScoValueError("jour de la date invalide ! (%s)" % day)
+    return "%04d-%02d-%02d" % (year, month, day)
+
+
+def DateISOtoDMY(isodate):
+    if not isodate:
+        return ""
+    arg = isodate  # debug
+    # si isodate est une instance de DateTime !
+    try:
+        isodate = "%s-%s-%s" % (isodate.year(), isodate.month(), isodate.day())
+        # log('DateISOtoDMY: converted isodate to iso !')
+    except:
+        pass
+    # drop time from isodate and split
+    t = str(isodate).split()[0].split("-")
+    if len(t) != 3:
+        # XXX recherche bug intermittent assez etrange
+        log('*** DateISOtoDMY: invalid isodate "%s" (arg="%s")' % (str(isodate), arg))
+        raise NoteProcessError(
+            'invalid isodate: "%s" (arg="%s" type=%s)' % (str(isodate), arg, type(arg))
+        )
+    year, month, day = t
+    year = int(year)
+    month = int(month)
+    day = int(day)
+    if month < 1 or month > 12:
+        raise ValueError("invalid month")
+    if day < 1 or day > 31:
+        raise ValueError("invalid day")
+    return "%02d/%02d/%04d" % (day, month, year)
+
+
+def TimetoISO8601(t, null_is_empty=False):
+    "convert time string to ISO 8601 (allow 16:03, 16h03, 16)"
+    if isinstance(t, datetime.time):
+        return t.isoformat()
+    if not t and null_is_empty:
+        return ""
+    t = t.strip().upper().replace("H", ":")
+    if t and t.count(":") == 0 and len(t) < 3:
+        t = t + ":00"
+    return t
+
+
+def TimefromISO8601(t):
+    "convert time string from ISO 8601 to our display format"
+    if not t:
+        return t
+    # XXX strange bug turnaround...
+    try:
+        t = "%s:%s" % (t.hour(), t.minute())
+        # log('TimefromISO8601: converted isotime to iso !')
+    except:
+        pass
+    fs = str(t).split(":")
+    return fs[0] + "h" + fs[1]  # discard seconds
+
+
+def TimeDuration(heure_debut, heure_fin):
+    """duree (nb entier de minutes) entre deux heures a notre format
+    ie 12h23
+    """
+    if heure_debut and heure_fin:
+        h0, m0 = [int(x) for x in heure_debut.split("h")]
+        h1, m1 = [int(x) for x in heure_fin.split("h")]
+        d = (h1 - h0) * 60 + (m1 - m0)
+        return d
+    else:
+        return None
+
+
+def float_null_is_zero(x):
+    if x is None or x == "":
+        return 0.0
+    else:
+        return float(x)
+
+
+def int_null_is_zero(x):
+    if x is None or x == "":
+        return 0
+    else:
+        return int(x)
+
+
+def int_null_is_null(x):
+    if x is None or x == "":
+        return None
+    else:
+        return int(x)
+
+
+def float_null_is_null(x):
+    if x is None or x == "":
+        return None
+    else:
+        return float(x)
+
+
+# post filtering
+#
+def UniqListofDicts(L, key):
+    """L is a list of dicts.
+    Remove from L all items which share the same key/value
+    """
+    # well, code is simpler than documentation:
+    d = {}
+    for item in L:
+        d[item[key]] = item
+    return d.values()
+
+
+#
+def copy_tuples_changing_attribute(
+    cnx, table, column, old_value, new_value, to_exclude=[]
+):
+    """Duplicate tuples in DB table, replacing column old_value by new_value
+
+    Will raise exception if violation of integerity constraint !
+    """
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        "select * from %s where %s=%%(old_value)s" % (table, column),
+        {"old_value": old_value},
+    )
+    res = cursor.dictfetchall()
+    for t in res:
+        t[column] = new_value
+        for c in to_exclude:
+            del t[c]
+        DBInsertDict(cnx, table, t, convert_empty_to_nulls=False)
diff --git a/pe_avislatex.py b/pe_avislatex.py
new file mode 100644
index 0000000000000000000000000000000000000000..06fda7362150b77cba98b891f049554e2d85939e
--- /dev/null
+++ b/pe_avislatex.py
@@ -0,0 +1,549 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+##############################################################################
+#  Module "Avis de poursuite d'étude"
+#  conçu et développé par Cléo Baras (IUT de Grenoble)
+##############################################################################
+
+import codecs
+import re
+import scolars
+import pe_jurype, pe_tagtable, pe_tools
+
+from sco_utils import *
+import scolars
+
+import pe_jurype, pe_tagtable, pe_tools
+from gen_tables import GenTable, SeqGenTable
+
+DEBUG = False  # Pour debug et repérage des prints à changer en Log
+
+DONNEE_MANQUANTE = (
+    u""  # Caractère de remplacement des données manquantes dans un avis PE
+)
+
+# ----------------------------------------------------------------------------------------
+def get_code_latex_from_modele(fichier):
+    """Lit le code latex à partir d'un modèle. Renvoie une chaine unicode.
+    
+    Le fichier doit contenir le chemin relatif
+    vers le modele : attention pas de vérification du format d'encodage
+    Le fichier doit donc etre enregistré avec le même codage que ScoDoc (utf-8)
+    """
+    fid_latex = codecs.open(fichier, "r", encoding=SCO_ENCODING)
+    un_avis_latex = fid_latex.read()
+    fid_latex.close()
+    return un_avis_latex
+
+
+# ----------------------------------------------------------------------------------------
+def get_code_latex_from_scodoc_preference(
+    context, formsemestre_id, champ="pe_avis_latex_tmpl"
+):
+    """
+    Extrait le template (ou le tag d'annotation au regard du champ fourni) des préférences LaTeX
+    et s'assure qu'il est renvoyé au format unicode
+    """
+    template_latex = context.get_preference(champ, formsemestre_id)
+    # Conversion du template en unicode:
+    if template_latex:
+        template_latex = template_latex.decode(SCO_ENCODING)
+    else:
+        template_latex = u""  # EV: preference non définie (None)
+
+    return template_latex
+
+
+# ----------------------------------------------------------------------------------------
+def get_tags_latex(code_latex):
+    """Recherche tous les tags présents dans un code latex (ce code étant obtenu
+    à la lecture d'un modèle d'avis pe).
+    Ces tags sont répérés par les balises **, débutant et finissant le tag
+    et sont renvoyés sous la forme d'une liste.
+    
+    result: liste de chaines unicode
+    """
+    if code_latex:
+        # changé par EV: était r"([\*]{2}[a-zA-Z0-9:éèàâêëïôöù]+[\*]{2})"
+        res = re.findall(r"([\*]{2}[^ \t\n\r\f\v\*]+[\*]{2})", code_latex)
+        return [tag[2:-2] for tag in res]
+    else:
+        return []
+
+
+def comp_latex_parcourstimeline(etudiant, promo, taille=17):
+    """Interprète un tag dans un avis latex **parcourstimeline**
+    et génère le code latex permettant de retracer le parcours d'un étudiant
+    sous la forme d'une frise temporelle.
+    Nota: modeles/parcourstimeline.tex doit avoir été inclu dans le préambule
+
+    result: chaine unicode (EV:)
+    """
+    codelatexDebut = (
+        u"""
+    \\begin{parcourstimeline}{**debut**}{**fin**}{**nbreSemestres**}{%d}
+    """
+        % taille
+    )
+
+    modeleEvent = u"""
+    \\parcoursevent{**nosem**}{**nomsem**}{**descr**}
+    """
+
+    codelatexFin = u"""
+    \\end{parcourstimeline}
+    """
+    reslatex = codelatexDebut
+    reslatex = reslatex.replace("**debut**", etudiant["entree"].decode(SCO_ENCODING))
+    reslatex = reslatex.replace("**fin**", str(etudiant["promo"]).decode(SCO_ENCODING))
+    reslatex = reslatex.replace(
+        "**nbreSemestres**", str(etudiant["nbSemestres"]).decode(SCO_ENCODING)
+    )
+    # Tri du parcours par ordre croissant : de la forme descr, nom sem date-date
+    parcours = etudiant["parcours"][::-1]  # EV: XXX je ne comprend pas ce commentaire ?
+
+    for no_sem in range(etudiant["nbSemestres"]):
+        descr = modeleEvent
+        nom_semestre_dans_parcours = parcours[no_sem]["nom_semestre_dans_parcours"]
+        descr = descr.replace(u"**nosem**", str(no_sem + 1))
+        if no_sem % 2 == 0:
+            descr = descr.replace(u"**nomsem**", nom_semestre_dans_parcours)
+            descr = descr.replace(u"**descr**", u"")
+        else:
+            descr = descr.replace(u"**nomsem**", u"")
+            descr = descr.replace(u"**descr**", nom_semestre_dans_parcours)
+        reslatex += descr
+    reslatex += codelatexFin
+    return reslatex
+
+
+# ----------------------------------------------------------------------------------------
+def interprete_tag_latex(tag):
+    """Découpe les tags latex de la forme S1:groupe:dut:min et renvoie si possible 
+    le résultat sous la forme d'un quadruplet.
+    """
+    infotag = tag.split(":")
+    if len(infotag) == 4:
+        return (
+            infotag[0].upper(),
+            infotag[1].lower(),
+            infotag[2].lower(),
+            infotag[3].lower(),
+        )
+    else:
+        return (None, None, None, None)
+
+
+# ----------------------------------------------------------------------------------------
+def get_code_latex_avis_etudiant(
+    donnees_etudiant, un_avis_latex, annotationPE, footer_latex, prefs
+):
+    """
+    Renvoie le code latex permettant de générer l'avis d'un étudiant en utilisant ses 
+    donnees_etudiant contenu dans le dictionnaire de synthèse du jury PE et en suivant un
+    fichier modele donné
+
+    result: chaine unicode
+    """
+    if not donnees_etudiant or not un_avis_latex:  # Cas d'un template vide
+        return annotationPE if annotationPE else u""
+
+    # Le template latex (corps + footer)
+    code = un_avis_latex + "\n\n" + footer_latex
+
+    # Recherche des tags dans le fichier
+    tags_latex = get_tags_latex(code)
+    if DEBUG:
+        print("Les tags" + str(tags_latex))
+
+    # Interprète et remplace chaque tags latex par les données numériques de l'étudiant (y compris les
+    # tags "macros" tels que parcourstimeline
+    for tag_latex in tags_latex:
+        # les tags numériques
+        valeur = DONNEE_MANQUANTE
+
+        if ":" in tag_latex:
+            (aggregat, groupe, tag_scodoc, champ) = interprete_tag_latex(tag_latex)
+            valeur = str_from_syntheseJury(
+                donnees_etudiant, aggregat, groupe, tag_scodoc, champ
+            )
+
+        # La macro parcourstimeline
+        elif tag_latex == u"parcourstimeline":
+            valeur = comp_latex_parcourstimeline(
+                donnees_etudiant, donnees_etudiant["promo"]
+            )
+
+        # Le tag annotationPE
+        elif tag_latex == u"annotation":
+            valeur = annotationPE
+
+        # Le tag bilanParTag
+        elif tag_latex == u"bilanParTag":
+            valeur = get_bilanParTag(donnees_etudiant)
+
+        # Les tags "simples": par ex. nom, prenom, sexe, ...
+        else:
+            if tag_latex in donnees_etudiant:
+                valeur = donnees_etudiant[tag_latex].decode(SCO_ENCODING)
+            elif tag_latex in prefs:  # les champs **NomResponsablePE**, ...
+                valeur = pe_tools.escape_for_latex(prefs[tag_latex]).decode(
+                    SCO_ENCODING
+                )
+
+        # Gestion des pb d'encodage XXX debug
+        # print(tag_latex, valeur)
+        assert isinstance(tag_latex, unicode)
+        assert isinstance(valeur, unicode)
+
+        # Substitution
+        code = code.replace("**" + tag_latex + "**", valeur)
+    return code
+
+
+# ----------------------------------------------------------------------------------------
+def get_annotation_PE(context, etudid, tag_annotation_pe):
+    """Renvoie l'annotation PE dans la liste de ces annotations ; 
+    Cette annotation est reconnue par la présence d'un tag **PE** 
+    (cf. context.get_preferences -> pe_tag_annotation_avis_latex).
+
+    Result: chaine unicode
+    """
+    if tag_annotation_pe:
+        cnx = context.GetDBConnexion()
+        annotations = scolars.etud_annotations_list(
+            cnx, args={"etudid": etudid}
+        )  # Les annotations de l'étudiant
+        annotationsPE = []
+
+        exp = re.compile(r"^" + tag_annotation_pe)
+
+        for a in annotations:
+            commentaire = unescape_html(a["comment"]).decode(SCO_ENCODING)
+            if exp.match(commentaire):  # tag en début de commentaire ?
+                a["comment_u"] = commentaire  # unicode, HTML non quoté
+                annotationsPE.append(
+                    a
+                )  # sauvegarde l'annotation si elle contient le tag
+
+        if annotationsPE:  # Si des annotations existent, prend la plus récente
+            annotationPE = sorted(annotationsPE, key=lambda a: a["date"], reverse=True)[
+                0
+            ]["comment_u"]
+
+            annotationPE = exp.sub(
+                u"", annotationPE
+            )  # Suppression du tag d'annotation PE
+            annotationPE = annotationPE.replace(u"\r", u"")  # Suppression des \r
+            annotationPE = annotationPE.replace(
+                u"<br/>", u"\n\n"
+            )  # Interprète les retours chariots html
+            return annotationPE
+    return u""  # pas d'annotations
+
+
+# ----------------------------------------------------------------------------------------
+def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ):
+    """Extrait du dictionnaire de synthèse du juryPE pour un étudiant donnée, 
+    une valeur indiquée par un champ ; 
+    si champ est une liste, renvoie la liste des valeurs extraites.
+
+    Result: chaine unicode ou liste de chaines unicode
+    """
+
+    if isinstance(champ, list):
+        return [
+            str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, chp)
+            for chp in champ
+        ]
+    else:  # champ = str à priori
+        # print(champ)
+        valeur = DONNEE_MANQUANTE
+        if (
+            (aggregat in donnees_etudiant)
+            and (groupe in donnees_etudiant[aggregat])
+            and (tag_scodoc in donnees_etudiant[aggregat][groupe])
+        ):
+            donnees_numeriques = donnees_etudiant[aggregat][groupe][tag_scodoc]
+            if champ == "rang":
+                valeur = u"%s/%d" % (
+                    donnees_numeriques[
+                        pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index("rang")
+                    ],
+                    donnees_numeriques[
+                        pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index(
+                            "nbinscrits"
+                        )
+                    ],
+                )
+            elif champ in pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS:
+                indice_champ = pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index(
+                    champ
+                )
+                if (
+                    len(donnees_numeriques) > indice_champ
+                    and donnees_numeriques[indice_champ] != None
+                ):
+                    if isinstance(
+                        donnees_numeriques[indice_champ], float
+                    ):  # valeur numérique avec formattage unicode
+                        valeur = u"%2.2f" % donnees_numeriques[indice_champ]
+                    else:
+                        valeur = u"%s" % donnees_numeriques[indice_champ]
+
+        # print(valeur)
+        return valeur
+
+
+# ----------------------------------------------------------------------------------------
+def get_bilanParTag(donnees_etudiant, groupe="groupe"):
+    """Renvoie le code latex d'un tableau récapitulant, pour tous les tags trouvés dans 
+    les données étudiants, ses résultats.
+    result: chaine unicode
+    """
+
+    entete = [
+        (
+            agg,
+            pe_jurype.JuryPE.PARCOURS[agg]["affichage_court"],
+            pe_jurype.JuryPE.PARCOURS[agg]["ordre"],
+        )
+        for agg in pe_jurype.JuryPE.PARCOURS
+    ]
+    entete = sorted(entete, key=lambda t: t[2])
+
+    lignes = []
+    valeurs = {"note": [], "rang": []}
+    for (indice_aggregat, (aggregat, intitule, ordre)) in enumerate(entete):
+        # print("> " + aggregat)
+        # listeTags = jury.get_allTagForAggregat(aggregat)  # les tags de l'aggrégat
+        listeTags = [
+            tag for tag in donnees_etudiant[aggregat][groupe].keys() if tag != "dut"
+        ]  #
+        for tag in listeTags:
+
+            if tag not in lignes:
+                lignes.append(tag)
+                valeurs["note"].append(
+                    [""] * len(entete)
+                )  # Ajout d'une ligne de données
+                valeurs["rang"].append(
+                    [""] * len(entete)
+                )  # Ajout d'une ligne de données
+            indice_tag = lignes.index(tag)  # l'indice de ligne du tag
+
+            # print(" --- " + tag + "(" + str(indice_tag) + "," + str(indice_aggregat) + ")")
+            [note, rang] = str_from_syntheseJury(
+                donnees_etudiant, aggregat, groupe, tag, ["note", "rang"]
+            )
+            valeurs["note"][indice_tag][indice_aggregat] = "" + note + ""
+            valeurs["rang"][indice_tag][indice_aggregat] = (
+                ("\\textit{" + rang + "}") if note else ""
+            )  # rang masqué si pas de notes
+
+    # pprint.pprint(valeurs)
+    # print(len(entete))
+    code_latex = u"\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n"
+    code_latex += u"\\hline \n"
+    code_latex += (
+        u" & "
+        + " & ".join(
+            ["\\textbf{" + intitule + "}" for (agg, intitule, ordre) in entete]
+        )
+        + " \\\\ \n"
+    )
+    code_latex += u"\\hline"
+    code_latex += u"\\hline \n"
+    for (i, ligne_val) in enumerate(valeurs["note"]):
+        titre = lignes[i]  # règle le pb d'encodage
+        # print titre, ligne_val
+        code_latex += (
+            u"\\textbf{"
+            + titre.decode(SCO_ENCODING)
+            + u"} & "
+            + " & ".join(ligne_val)
+            + u"\\\\ \n"
+        )
+        code_latex += (
+            u" & "
+            + u" & ".join(
+                [u"{\\scriptsize " + clsmt + u"}" for clsmt in valeurs["rang"][i]]
+            )
+            + u"\\\\ \n"
+        )
+        code_latex += u"\\hline \n"
+    code_latex += u"\\end{tabular}"
+
+    return code_latex
+
+
+# ----------------------------------------------------------------------------------------
+def get_avis_poursuite_par_etudiant(
+    context, jury, etudid, template_latex, tag_annotation_pe, footer_latex, prefs
+):
+    """Renvoie un nom de fichier et le contenu de l'avis latex d'un étudiant dont l'etudid est fourni.
+    result: [ chaine unicode, chaine unicode ]
+    """
+    if pe_tools.PE_DEBUG:
+        pe_tools.pe_print(jury.syntheseJury[etudid]["nom"] + " " + etudid)
+
+    sexe = jury.syntheseJury[etudid]["sexe"].decode(SCO_ENCODING)
+    nom = jury.syntheseJury[etudid]["nom"].replace(" ", "-").decode(SCO_ENCODING)
+    prenom = jury.syntheseJury[etudid]["prenom"].replace(" ", "-").decode(SCO_ENCODING)
+
+    nom_fichier = (
+        u"avis_poursuite_"
+        + pe_tools.remove_accents(nom)
+        + "_"
+        + pe_tools.remove_accents(prenom)
+        + "_"
+        + etudid
+    )
+    if pe_tools.PE_DEBUG:
+        pe_tools.pe_print("fichier latex =" + nom_fichier, type(nom_fichier))
+
+    # Entete (commentaire)
+
+    contenu_latex = u"%% ---- Etudiant: " + sexe + " " + nom + " " + prenom + u"\n"
+
+    # les annnotations
+    annotationPE = get_annotation_PE(
+        context, etudid, tag_annotation_pe=tag_annotation_pe
+    )
+    if pe_tools.PE_DEBUG:
+        pe_tools.pe_print(annotationPE, type(annotationPE))
+
+    # le LaTeX
+    avis = get_code_latex_avis_etudiant(
+        jury.syntheseJury[etudid], template_latex, annotationPE, footer_latex, prefs
+    )
+    # if pe_tools.PE_DEBUG: pe_tools.pe_print(avis, type(avis))
+    contenu_latex += avis + "\n"
+
+    return [nom_fichier, contenu_latex]
+
+
+def get_templates_from_distrib(template="avis"):
+    """Récupère le template (soit un_avis.tex soit le footer.tex) à partir des fichiers mémorisés dans la distrib des avis pe (distrib local
+    ou par défaut et le renvoie"""
+    if template == "avis":
+        pe_local_tmpl = pe_tools.PE_LOCAL_AVIS_LATEX_TMPL
+        pe_default_tmpl = pe_tools.PE_DEFAULT_AVIS_LATEX_TMPL
+    elif template == "footer":
+        pe_local_tmpl = pe_tools.PE_LOCAL_FOOTER_TMPL
+        pe_default_tmpl = pe_tools.PE_DEFAULT_FOOTER_TMPL
+
+    if template in ["avis", "footer"]:
+        # pas de preference pour le template: utilise fichier du serveur
+        p = os.path.join(SCO_SRCDIR, pe_local_tmpl)
+        if os.path.exists(p):
+            template_latex = get_code_latex_from_modele(p)
+        else:
+            p = os.path.join(SCO_SRCDIR, pe_default_tmpl)
+            if os.path.exists(p):
+                template_latex = get_code_latex_from_modele(p)
+            else:
+                template_latex = ""  # fallback: avis vides
+        return template_latex
+
+
+# ----------------------------------------------------------------------------------------
+def table_syntheseAnnotationPE(context, syntheseJury, tag_annotation_pe):
+    """Génère un fichier excel synthétisant les annotations PE telles qu'inscrites dans les fiches de chaque étudiant
+    """
+    sT = SeqGenTable()  # le fichier excel à générer
+
+    # Les etudids des étudiants à afficher, triés par ordre alphabétiques de nom+prénom
+    donnees_tries = sorted(
+        [
+            (etudid, syntheseJury[etudid]["nom"] + " " + syntheseJury[etudid]["prenom"])
+            for etudid in syntheseJury.keys()
+        ],
+        key=lambda c: c[1],
+    )
+    etudids = [e[0] for e in donnees_tries]
+    if not etudids:  # Si pas d'étudiants
+        T = GenTable(
+            columns_ids=["pas d'étudiants"],
+            rows=[],
+            titles={"pas d'étudiants": "pas d'étudiants"},
+            html_sortable=True,
+            xls_sheet_name="dut",
+        )
+        sT.add_genTable("Annotation PE", T)
+        return sT
+
+    # Si des étudiants
+    maxParcours = max(
+        [syntheseJury[etudid]["nbSemestres"] for etudid in etudids]
+    )  # le nombre de semestre le + grand
+
+    infos = ["sexe", "nom", "prenom", "age", "nbSemestres"]
+    entete = ["etudid"]
+    entete.extend(infos)
+    entete.extend(["P%d" % i for i in range(1, maxParcours + 1)])  # ajout du parcours
+    entete.append("Annotation PE")
+    columns_ids = entete  # les id et les titres de colonnes sont ici identiques
+    titles = {i: i for i in columns_ids}
+
+    rows = []
+    for (
+        etudid
+    ) in etudids:  # parcours des étudiants par ordre alphabétique des nom+prénom
+        e = syntheseJury[etudid]
+        # Les info générales:
+        row = {
+            "etudid": etudid,
+            "sexe": e["sexe"],
+            "nom": e["nom"],
+            "prenom": e["prenom"],
+            "age": e["age"],
+            "nbSemestres": e["nbSemestres"],
+        }
+        # Les parcours: P1, P2, ...
+        n = 1
+        for p in e["parcours"]:
+            row["P%d" % n] = p["titreannee"]
+            n += 1
+
+        # L'annotation PE
+        annotationPE = get_annotation_PE(
+            context, etudid, tag_annotation_pe=tag_annotation_pe
+        )
+        row["Annotation PE"] = annotationPE.encode(SCO_ENCODING) if annotationPE else ""
+        rows.append(row)
+
+    T = GenTable(
+        columns_ids=columns_ids,
+        rows=rows,
+        titles=titles,
+        html_sortable=True,
+        xls_sheet_name="Annotation PE",
+    )
+    sT.add_genTable("Annotation PE", T)
+    return sT
diff --git a/pe_jurype.py b/pe_jurype.py
new file mode 100644
index 0000000000000000000000000000000000000000..b6d998c93b091711ba7f8ece33a3afa4fbe5ace2
--- /dev/null
+++ b/pe_jurype.py
@@ -0,0 +1,1262 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+##############################################################################
+#  Module "Avis de poursuite d'étude"
+#  conçu et développé par Cléo Baras (IUT de Grenoble)
+##############################################################################
+
+"""
+Created on Fri Sep  9 09:15:05 2016
+
+@author: barasc
+"""
+
+# ----------------------------------------------------------
+# Ensemble des fonctions et des classes
+# permettant les calculs preliminaires (hors affichage)
+# a l'edition d'un jury de poursuites d'etudes
+# ----------------------------------------------------------
+
+
+try:
+    from cStringIO import StringIO
+except:
+    try:
+        from StringIO import StringIO
+    except:
+        from io import StringIO
+from zipfile import ZipFile, BadZipfile
+import pprint
+
+from gen_tables import GenTable, SeqGenTable
+
+import sco_codes_parcours  # sco_codes_parcours.NEXT -> sem suivant
+import sco_report
+from sco_utils import *
+
+import pe_tagtable, pe_tools, pe_avislatex, pe_semestretag, pe_settag
+
+# ----------------------------------------------------------------------------------------
+def comp_nom_semestre_dans_parcours(context, sem):
+    """Le nom a afficher pour titrer un semestre
+    par exemple: "semestre 2 FI 2015"
+    """
+    F = context.Notes.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
+    return "%s %s %s  %s" % (
+        parcours.SESSION_NAME,  # eg "semestre"
+        sem["semestre_id"],  # eg 2
+        sem.get("modalite", ""),  # eg FI ou FC
+        sem["annee_debut"],  # eg 2015
+    )
+
+
+# ----------------------------------------------------------------------------------------
+class JuryPE:
+    """Classe memorisant toutes les informations necessaires pour etablir un jury de PE. Modele
+    base sur NotesTable
+
+    Attributs : - diplome : l'annee d'obtention du diplome DUT et du jury de PE (generalement fevrier XXXX)
+                - context : le contexte Zope
+                - juryEtudDict : dictionnaire récapitulant les étudiants participant au jury PE (données administratives +
+                                celles des semestres valides à prendre en compte permettant le calcul des moyennes  ...
+                                {'etudid : { 'nom', 'prenom', 'sexe', 'diplome', '',  }}
+                                Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue
+                                et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif
+    
+
+    Note (EV:): les attributs sont des chaines encodées (utf8), comme dans ScoDoc (pas des unicodes)
+    """
+
+    # Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et
+    # leur affichage dans les avis latex
+    PARCOURS = {
+        "S1": {
+            "aggregat": ["S1"],
+            "ordre": 1,
+            "affichage_court": "S1",
+            "affichage_long": "Semestre 1",
+        },
+        "S2": {
+            "aggregat": ["S2"],
+            "ordre": 2,
+            "affichage_court": "S2",
+            "affichage_long": "Semestre 2",
+        },
+        "S3": {
+            "aggregat": ["S3"],
+            "ordre": 4,
+            "affichage_court": "S3",
+            "affichage_long": "Semestre 3",
+        },
+        "S4": {
+            "aggregat": ["S4"],
+            "ordre": 5,
+            "affichage_court": "S4",
+            "affichage_long": "Semestre 4",
+        },
+        "1A": {
+            "aggregat": ["S1", "S2"],
+            "ordre": 3,
+            "affichage_court": "1A",
+            "affichage_long": "1ère année",
+        },
+        "2A": {
+            "aggregat": ["S3", "S4"],
+            "ordre": 6,
+            "affichage_court": "2A",
+            "affichage_long": "2ème année",
+        },
+        "3S": {
+            "aggregat": ["S1", "S2", "S3"],
+            "ordre": 7,
+            "affichage_court": "S1+S2+S3",
+            "affichage_long": "DUT du semestre 1 au semestre 3",
+        },
+        "4S": {
+            "aggregat": ["S1", "S2", "S3", "S4"],
+            "ordre": 8,
+            "affichage_court": "DUT",
+            "affichage_long": "DUT (tout semestre inclus)",
+        },
+    }
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def __init__(self, context, semBase):
+        """
+        Création d'une table PE sur la base d'un semestre selectionné. De ce semestre est déduit :
+        1. l'année d'obtention du DUT,
+        2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés.
+
+        Args:
+            context:
+            semBase: le dictionnaire sem donnant la base du jury
+            meme_programme: si True, impose un même programme pour tous les étudiants participant au jury,
+                            si False, permet des programmes differents
+        """
+        self.context = context
+        self.semTagDict = (
+            {}
+        )  # Les semestres taggués à la base des calculs de moyenne par tag
+        self.setTagDict = (
+            {}
+        )  # dictionnaire récapitulant les semTag impliqués dans le jury de la forme { 'formsemestre_id' : object Semestre_tag
+        self.promoTagDict = {}
+
+        # L'année du diplome
+        self.diplome = get_annee_diplome_semestre(semBase)
+
+        # Un zip où ranger les fichiers générés:
+        self.NOM_EXPORT_ZIP = "Jury_PE_%s" % self.diplome
+        self.zipdata = StringIO()
+        self.zipfile = ZipFile(self.zipdata, "w")
+
+        #
+        self.ETUDINFO_DICT = {}  # Les infos sur les étudiants
+        self.PARCOURSINFO_DICT = {}  # Les parcours des étudiants
+        self.syntheseJury = {}  # Le jury de synthèse
+
+        # Calcul du jury PE
+        self.exe_calculs_juryPE(semBase)
+        self.synthetise_juryPE()
+
+        # Export des données => mode 1 seule feuille -> supprimé
+        # filename = self.NOM_EXPORT_ZIP + "jurySyntheseDict_" + str(self.diplome) + '.xls'
+        # self.xls = self.table_syntheseJury(mode="singlesheet")
+        # self.add_file_to_zip(filename, self.xls.excel())
+
+        # Fabrique 1 fichier excel résultat avec 1 seule feuille => trop gros
+        filename = self.NOM_EXPORT_ZIP + "_jurySyntheseDict" + ".xls"
+        self.xlsV2 = self.table_syntheseJury(mode="multiplesheet")
+        if self.xlsV2:
+            self.add_file_to_zip(filename, self.xlsV2.excel())
+
+        # Pour debug
+        # self.syntheseJury = pe_tools.JURY_SYNTHESE_POUR_DEBUG #Un dictionnaire fictif pour debug
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def add_file_to_zip(self, filename, data, path=""):
+        """Add a file to our zip
+        All files under NOM_EXPORT_ZIP/
+        path may specify a subdirectory
+        """
+        path_in_zip = os.path.join(self.NOM_EXPORT_ZIP, path, filename)
+        self.zipfile.writestr(path_in_zip, data)
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def get_zipped_data(self):
+        """returns zipped data with all generated (CSV) files
+        """
+        if self.zipfile:
+            self.zipfile.close()
+            self.zipfile = None
+        return self.zipdata.getvalue()
+
+    # **************************************************************************************************************** #
+    # Lancement des différentes actions permettant le calcul du jury PE
+    # **************************************************************************************************************** #
+    def exe_calculs_juryPE(self, semBase):
+        # Liste des étudiants à traiter pour identifier ceux qui seront diplômés
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print(
+                "*** Recherche et chargement des étudiants diplômés en %d"
+                % (self.diplome)
+            )
+        self.get_etudiants_in_jury(
+            semBase, avec_meme_formation=False
+        )  # calcul des coSemestres
+
+        # Les semestres impliqués (ceux valides pour les étudiants à traiter)
+        # -------------------------------------------------------------------
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print("*** Création des semestres taggués")
+        self.get_semtags_in_jury()
+        if pe_tools.PE_DEBUG:
+            for semtag in self.semTagDict.values():  # Export
+                filename = self.NOM_EXPORT_ZIP + semtag.nom + ".csv"
+                self.zipfile.writestr(filename, semtag.str_tagtable())
+        # self.export_juryPEDict()
+
+        # Les moyennes sur toute la scolarité
+        # -----------------------------------
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print(
+                "*** Création des moyennes sur différentes combinaisons de semestres et différents groupes d'étudiant"
+            )
+        self.get_settags_in_jury()
+        if pe_tools.PE_DEBUG:
+            for settagdict in self.setTagDict.values():  # Export
+                for settag in settagdict.values():
+                    filename = self.NOM_EXPORT_ZIP + semtag.nom + ".csv"
+                    self.zipfile.writestr(filename, semtag.str_tagtable())
+        # self.export_juryPEDict()
+
+        # Les interclassements
+        # --------------------
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print(
+                "*** Création des interclassements au sein de la promo sur différentes combinaisons de semestres"
+            )
+        self.get_promotags_in_jury()
+
+    # **************************************************************************************************************** #
+    # Fonctions relatives à la liste des étudiants à prendre en compte dans le jury
+    # **************************************************************************************************************** #
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def get_etudiants_in_jury(self, semBase, avec_meme_formation=False):
+        """
+        Calcule la liste des étudiants à prendre en compte dans le jury et la renvoie sous la forme
+        """
+        # Les cosemestres donnant lieu à meme année de diplome
+        coSems = get_cosemestres_diplomants(
+            self.context, semBase, avec_meme_formation=avec_meme_formation
+        )  # calcul des coSemestres
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print(
+                "1) Recherche des coSemestres -> %d trouvés" % len(coSems)
+            )
+
+        # Les étudiants inscrits dans les cosemestres
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print("2) Liste des étudiants dans les différents co-semestres")
+        listEtudId = self.get_etudiants_dans_semestres(
+            coSems
+        )  #  étudiants faisant parti des cosemestres
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print(" => %d étudiants trouvés" % len(listEtudId))
+
+        # L'analyse des parcours étudiants pour déterminer leur année effective de diplome avec prise en compte des redoublements, des abandons, ....
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print("3) Analyse des parcours individuels des étudiants")
+
+        for (no_etud, etudid) in enumerate(listEtudId):
+            self.add_etudiants(etudid)
+            if pe_tools.PE_DEBUG:
+                if (no_etud + 1) % 10 == 0:
+                    pe_tools.pe_print((no_etud + 1), " ", end="")
+        pe_tools.pe_print()
+
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print(
+                "  => %d étudiants à diplômer en %d"
+                % (len(self.get_etudids_du_jury()), self.diplome)
+            )
+            pe_tools.pe_print(
+                "  => %d étudiants éliminer pour abandon"
+                % (len(listEtudId) - len(self.get_etudids_du_jury()))
+            )
+
+    # ------------------------------------------------------------------------------------------------------------------
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def get_etudiants_dans_semestres(self, semsListe):
+        """Renvoie la liste des etudid des etudiants inscrits à l'un des semestres de la liste fournie en paramètre
+        en supprimant les doublons (i.e. un même étudiant qui apparaîtra 2 fois)"""
+
+        etudiants = []
+        for sem in semsListe:  # pour chacun des semestres de la liste
+
+            # nt = self.get_notes_d_un_semestre( sem['formsemestre_id'] )
+            nt = self.get_cache_notes_d_un_semestre(
+                self.context, sem["formsemestre_id"]
+            )
+            # context.Notes._getNotesCache().get_NotesTable(context.Notes, sem['formsemestre_id'])
+            etudiantsDuSemestre = (
+                nt.get_etudids()
+            )  # nt.identdict.keys() # identification des etudiants du semestre
+
+            if pe_tools.PE_DEBUG:
+                pe_tools.pe_print(
+                    "  --> chargement du semestre %s : %d etudiants "
+                    % (sem["formsemestre_id"], len(etudiantsDuSemestre))
+                )
+            etudiants.extend(etudiantsDuSemestre)
+
+        return list(set(etudiants))  # suppression des doublons
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def get_etudids_du_jury(self, ordre="aucun"):
+        """Renvoie la liste de tous les étudiants (concrètement leur etudid)
+        participant au jury c'est à dire, ceux dont la date du 'jury' est self.diplome
+        et n'ayant pas abandonné.
+        Si l'ordre est précisé, donne une liste etudid dont le nom, prenom trié par ordre alphabétique
+        """
+        etudids = [
+            etudid
+            for (etudid, donnees) in self.PARCOURSINFO_DICT.items()
+            if donnees["diplome"] == self.diplome and donnees["abandon"] == False
+        ]
+        if ordre == "alphabetique":  # Tri alphabétique
+            etudidsAvecNom = [
+                (etudid, etud["nom"] + "/" + etud["prenom"])
+                for (etudid, etud) in self.PARCOURSINFO_DICT.items()
+                if etudid in etudids
+            ]
+            etudidsAvecNomTrie = sorted(etudidsAvecNom, key=lambda col: col[1])
+            etudids = [etud[0] for etud in etudidsAvecNomTrie]
+        return etudids
+
+    # ------------------------------------------------------------------------------------------------------------------
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def add_etudiants(self, etudid):
+        """ Ajoute un étudiant (via son etudid) au dictionnaire de synthèse jurydict.
+        L'ajout consiste à :
+        > insérer une entrée pour l'étudiant en mémorisant ses infos (get_etudInfo),
+        avec son nom, prénom, etc...
+        > à analyser son parcours, pour vérifier s'il n'a pas abandonné l'IUT en cours de route => clé abandon
+        > à chercher ses semestres valides (formsemestre_id) et ses années valides (formannee_id),
+        c'est à dire ceux pour lesquels il faudra prendre en compte ses notes dans les calculs de moyenne (type 1A=S1+S2/2)
+        """
+
+        if not self.PARCOURSINFO_DICT.has_key(etudid):
+            etud = self.get_cache_etudInfo_d_un_etudiant(
+                self.context, etudid
+            )  # On charge les données de l'étudiant
+            if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
+                pe_tools.pe_print(etud["nom"] + " " + etud["prenom"], end="")
+
+            self.PARCOURSINFO_DICT[etudid] = {
+                "etudid": etudid,  # les infos sur l'étudiant
+                "nom": etud["nom"],  # Ajout à la table jury
+            }
+
+            # Analyse du parcours de l'étudiant
+
+            # Sa date prévisionnelle de diplome
+            self.PARCOURSINFO_DICT[etudid][
+                "diplome"
+            ] = self.calcul_anneePromoDUT_d_un_etudiant(etudid)
+            if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
+                pe_tools.pe_print(
+                    "promo=" + str(self.PARCOURSINFO_DICT[etudid]["diplome"]), end=""
+                )
+
+            # Est-il réorienté ou démissionnaire ?
+            self.PARCOURSINFO_DICT[etudid][
+                "abandon"
+            ] = self.est_un_etudiant_reoriente_ou_demissionnaire(etudid)
+
+            # A-t-il arrêté de lui-même sa formation avant la fin ?
+            etatD = self.est_un_etudiant_disparu(etudid)
+            if etatD == True:
+                self.PARCOURSINFO_DICT[etudid]["abandon"] = True
+            # dans le jury ne seront traités que les étudiants ayant la date attendue de diplome et n'ayant pas abandonné
+
+            # Quels sont ses semestres validant (i.e ceux dont les notes doivent être prises en compte pour le jury)
+            # et s'ils existent quelles sont ses notes utiles ?
+            sesFormsemestre_idValidants = [
+                self.get_Fid_d_un_Si_valide_d_un_etudiant(etudid, nom_sem)
+                for nom_sem in JuryPE.PARCOURS["4S"][
+                    "aggregat"
+                ]  # Recherche du formsemestre_id de son Si valide (ou a défaut en cours)
+            ]
+            for (i, nom_sem) in enumerate(JuryPE.PARCOURS["4S"]["aggregat"]):
+                fid = sesFormsemestre_idValidants[i]
+                self.PARCOURSINFO_DICT[etudid][nom_sem] = fid  # ['formsemestre_id']
+                if fid != None and pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
+                    pe_tools.pe_print(nom_sem + "=" + fid, end="")
+                    # self.get_moyennesEtClassements_par_semestre_d_un_etudiant( etudid, fid )
+
+            # Quelles sont ses années validantes ('1A', '2A') et ses parcours (3S, 4S) validants ?
+            for parcours in ["1A", "2A", "3S", "4S"]:
+                lesSemsDuParcours = JuryPE.PARCOURS[parcours][
+                    "aggregat"
+                ]  # les semestres du parcours : par ex. ['S1', 'S2', 'S3']
+                lesFidsValidantDuParcours = [
+                    sesFormsemestre_idValidants[
+                        JuryPE.PARCOURS["4S"]["aggregat"].index(nom_sem)
+                    ]
+                    for nom_sem in lesSemsDuParcours  # par ex. ['SEM4532', 'SEM567', ...]
+                ]
+                parcours_incomplet = (
+                    sum([fid == None for fid in lesFidsValidantDuParcours]) > 0
+                )
+
+                if not parcours_incomplet:
+                    self.PARCOURSINFO_DICT[etudid][
+                        parcours
+                    ] = lesFidsValidantDuParcours[-1]
+                else:
+                    self.PARCOURSINFO_DICT[etudid][parcours] = None
+                if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
+                    pe_tools.pe_print(
+                        parcours + "=" + str(self.PARCOURSINFO_DICT[etudid][parcours]),
+                        end="",
+                    )
+
+            if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
+                print
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def est_un_etudiant_reoriente_ou_demissionnaire(self, etudid):
+        """Renvoie True si l'étudiant est réorienté (NAR) ou démissionnaire (DEM)"""
+        reponse = False
+        etud = self.get_cache_etudInfo_d_un_etudiant(self.context, etudid)
+        (code, parcours) = sco_report.get_codeparcoursetud(self.context.Notes, etud)
+        if (
+            len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values()))
+            > 0
+        ):  # Eliminé car NAR apparait dans le parcours
+            reponse = True
+            if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
+                pe_tools.pe_print("  -> à éliminer car réorienté (NAR)")
+        if "DEM" in parcours.values():  # Eliminé car DEM
+            reponse = True
+            if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
+                pe_tools.pe_print("  -> à éliminer car DEM")
+        return reponse
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def est_un_etudiant_disparu(self, etudid):
+        """Renvoie True si l'étudiant n'a pas achevé la formation à l'IUT et a disparu des listes, sans
+        pour autant avoir été indiqué NAR ou DEM ; recherche son dernier semestre validé et regarde s'il
+        n'existe pas parmi les semestres existants dans scodoc un semestre postérieur (en terme de date de
+        début) de n° au moins égal à celui de son dernier semestre valide dans lequel il aurait pu
+        s'inscrire mais ne l'a pas fait. """
+        sessems = self.get_semestresDUT_d_un_etudiant(
+            etudid
+        )  # les semestres de l'étudiant
+        sonDernierSidValide = self.get_dernier_semestre_id_valide_d_un_etudiant(etudid)
+
+        sesdates = [
+            pe_tagtable.conversionDate_StrToDate(sem["date_fin"]) for sem in sessems
+        ]  # association 1 date -> 1 semestrePE pour les semestres de l'étudiant
+        lastdate = max(sesdates)  # date de fin de l'inscription la plus récente
+
+        # if PETable.AFFICHAGE_DEBUG_PE == True : pe_tools.pe_print("     derniere inscription = ", lastDateSem)
+        semestresDeScoDoc = self.context.Notes.formsemestre_list()
+        semestresSuperieurs = [
+            sem for sem in semestresDeScoDoc if sem["semestre_id"] > sonDernierSidValide
+        ]  # Semestre de rang plus élevé que son dernier sem valide
+        datesDesSemestresSuperieurs = [
+            pe_tagtable.conversionDate_StrToDate(sem["date_debut"])
+            for sem in semestresSuperieurs
+        ]
+        datesDesSemestresPossibles = [
+            date_deb for date_deb in datesDesSemestresSuperieurs if date_deb >= lastdate
+        ]  # date de debut des semestres possibles postérieur au dernier semestre de l'étudiant et de niveau plus élevé que le dernier semestre valide de l'étudiant
+        if (
+            len(datesDesSemestresPossibles) > 0
+        ):  # etudiant ayant disparu de la circulation
+            #            if PETable.AFFICHAGE_DEBUG_PE == True :
+            #                pe_tools.pe_print("  -> à éliminer car des semestres où il aurait pu s'inscrire existent ")
+            #                pe_tools.pe_print(pe_tools.print_semestres_description( datesDesSemestresPossibles.values() ))
+            return True
+        else:
+            return False
+
+    # ------------------------------------------------------------------------------------------------------------------
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def get_dernier_semestre_id_valide_d_un_etudiant(self, etudid):
+        """Renvoie le n° (semestre_id) du dernier semestre validé par un étudiant fourni par son etudid
+        et None si aucun semestre n'a été validé
+        """
+        etud = self.get_cache_etudInfo_d_un_etudiant(self.context, etudid)
+        (code, parcours) = sco_report.get_codeparcoursetud(
+            self.context.Notes, etud
+        )  # description = '1234:A', parcours = {1:ADM, 2:NAR, ...}
+        sonDernierSemestreValide = max(
+            [
+                int(cle)
+                for (cle, code) in parcours.items()
+                if code in sco_codes_parcours.CODES_SEM_VALIDES
+            ]
+            + [0]
+        )  # n° du dernier semestre valide, 0 sinon
+        return sonDernierSemestreValide if sonDernierSemestreValide > 0 else None
+
+    # ------------------------------------------------------------------------------------------------------------------
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def get_Fid_d_un_Si_valide_d_un_etudiant(self, etudid, nom_semestre):
+        """Récupère le formsemestre_id valide d'un étudiant fourni son etudid à un semestre DUT de n° semestre_id
+        donné. Si le semestre est en cours (pas encore de jury), renvoie le formsemestre_id actuel."""
+        semestre_id = JuryPE.PARCOURS["4S"]["aggregat"].index(nom_semestre) + 1
+        sesSi = self.get_semestresDUT_d_un_etudiant(
+            etudid, semestre_id
+        )  # extrait uniquement les Si par ordre temporel décroissant
+
+        if len(sesSi) > 0:  # S'il a obtenu au moins une note
+            # mT = sesMoyennes[0]
+            leFid = sesSi[0]["formsemestre_id"]
+            for (i, sem) in enumerate(
+                sesSi
+            ):  # Parcours des éventuels semestres précédents
+                nt = self.get_cache_notes_d_un_semestre(
+                    self.context, sem["formsemestre_id"]
+                )
+                dec = nt.get_etud_decision_sem(
+                    etudid
+                )  # quelle est la décision du jury ?
+                if (
+                    dec and dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES.keys()
+                ):  # isinstance( sesMoyennes[i+1], float) and
+                    # mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
+                    leFid = sem["formsemestre_id"]
+        else:
+            leFid = None
+        return leFid
+
+    # **************************************************************************************************************** #
+    # Traitements des semestres impliqués dans le jury
+    # **************************************************************************************************************** #
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def get_semtags_in_jury(self):
+        """
+        Créé les semestres tagués relatifs aux résultats des étudiants à prendre en compte dans le jury.
+        Calcule les moyennes et les classements de chaque semestre par tag et les statistiques de ces semestres.
+        """
+        lesFids = self.get_formsemestreids_du_jury(
+            self.get_etudids_du_jury(), liste_semestres=["S1", "S2", "S3", "S4"]
+        )
+        for (i, fid) in enumerate(lesFids):
+            if pe_tools.PE_DEBUG:
+                pe_tools.pe_print(
+                    u"%d) Semestre taggué %s (avec classement dans groupe)"
+                    % (i + 1, fid)
+                )
+            self.add_semtags_in_jury(fid)
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def add_semtags_in_jury(self, fid):
+        """Crée si nécessaire un semtag et le mémorise dans self.semTag ;
+        charge également les données des nouveaux étudiants qui en font partis.
+        """
+        # Semestre taggué avec classement dans le groupe
+        if not self.semTagDict.has_key(fid):
+            nt = self.get_cache_notes_d_un_semestre(self.context, fid)
+
+            # Création du semestres
+            self.semTagDict[fid] = pe_semestretag.SemestreTag(
+                self.context, nt, nt.sem
+            )  # Création du pesemestre associé
+            self.semTagDict[fid].comp_data_semtag()
+            lesEtudids = self.semTagDict[fid].get_etudids()
+
+            lesEtudidsManquants = []
+            for etudid in lesEtudids:
+                if (
+                    etudid not in self.PARCOURSINFO_DICT
+                ):  # Si l'étudiant n'a pas été pris en compte dans le jury car déjà diplômé ou redoublant
+                    lesEtudidsManquants.append(etudid)
+                    # self.get_cache_etudInfo_d_un_etudiant(self.context, etudid)
+                    self.add_etudiants(
+                        etudid
+                    )  # Ajoute les élements de parcours de l'étudiant
+
+            nbinscrit = self.semTagDict[fid].get_nbinscrits()
+            if pe_tools.PE_DEBUG:
+                pe_tools.pe_print(
+                    u"   - %d étudiants classés " % (nbinscrit)
+                    + ": "
+                    + ",".join(
+                        [etudid for etudid in self.semTagDict[fid].get_etudids()]
+                    )
+                )
+                if lesEtudidsManquants:
+                    pe_tools.pe_print(
+                        u"   - dont %d étudiants manquants ajoutés aux données du jury"
+                        % (len(lesEtudidsManquants))
+                        + ": "
+                        + ", ".join(lesEtudidsManquants)
+                    )
+                pe_tools.pe_print(u"    - Export csv")
+                filename = self.NOM_EXPORT_ZIP + self.semTagDict[fid].nom + ".csv"
+                self.zipfile.writestr(filename, self.semTagDict[fid].str_tagtable())
+
+    # ----------------------------------------------------------------------------------------------------------------
+    def get_formsemestreids_du_jury(self, etudids, liste_semestres="4S"):
+        """Renvoie la liste des formsemestre_id validants des étudiants en parcourant les semestres valides des étudiants mémorisés dans
+        self.PARCOURSINFO_DICT.
+        Les étudiants sont identifiés par leur etudic donnés dans la liste etudids (généralement self.get_etudids_in_jury() ).
+        La liste_semestres peut être une liste ou une chaine de caractères parmi :
+            * None => tous les Fids validant
+            * 'Si' => le ième 1 semestre
+            * 'iA' => l'année i = ['S1, 'S2'] ou ['S3', 'S4']
+            * '3S', '4S' => fusion des semestres
+            * [ 'Si', 'iA' , ... ] => une liste combinant les formats précédents
+        """
+        champs_possibles = JuryPE.PARCOURS.keys()
+        if (
+            not isinstance(liste_semestres, list)
+            and not isinstance(liste_semestres, str)
+            and liste_semestres not in champs_possibles
+        ):
+            raise ValueError(
+                "Probleme de paramètres d'appel dans pe_jurype.JuryPE.get_formsemestreids_du_jury"
+            )
+
+        if isinstance(liste_semestres, list):
+            res = []
+            for elmt in liste_semestres:
+                res.extend(self.get_formsemestreids_du_jury(etudids, elmt))
+            return list(set(res))
+
+        # si liste_sem est un nom de parcours
+        nom_sem = liste_semestres
+        # if nom_sem in ['1A', '2A', '3S', '4S'] :
+        #     return self.get_formsemestreids_du_jury(etudids, JuryPE.PARCOURS[nom_sem] )
+        # else :
+        fids = {
+            self.PARCOURSINFO_DICT[etudid][nom_sem]
+            for etudid in etudids
+            if self.PARCOURSINFO_DICT[etudid][nom_sem] != None
+        }
+
+        return list(fids)
+
+    # **************************************************************************************************************** #
+    # Traitements des parcours impliquées dans le jury
+    # **************************************************************************************************************** #
+
+    # # ----------------------------------------------------------------------------------------------------------------
+    # def get_antags_in_jury(self, avec_affichage_debug=True ):
+    #     """Construit les settag associés aux années 1A et 2A du jury"""
+    #     lesAnnees = {'1A' : ['S1', 'S2'], '2A' : ['S3', 'S4'] }
+    #     for nom_annee in lesAnnees:
+    #         lesAidDesAnnees = self.get_anneeids_du_jury(annee= nom_annee) # les annee_ids des étudiants du jury
+    #         for aid in lesAidDesAnnees:
+    #             fidSemTagFinal = JuryPE.convert_aid_en_fid( aid )
+    #             lesEtudisDelAnnee = self.semTagDict[ fidSemTagFinal ].get_etudids() # les etudiants sont ceux inscrits dans le semestre final de l'année
+    #             parcoursDesEtudiants = { etudid : self.PARCOURSINFO_DICT[etudid] for etudid in lesEtudisDelAnnee } # les parcours des etudid aka quels semestres sont à prendre en compte
+    #
+    #             lesFidsDesEtudiants = self.get_formsemestreids_du_jury(lesEtudisDelAnnee, nom_annee) # les formsemestres_id à prendre en compte pour les moyennes
+    #             # Manque-t-il des semtag associés ; si oui, les créé
+    #             pe_tools.pe_print(aid, lesFidsDesEtudiants)
+    #             for fid in lesFidsDesEtudiants:
+    #                 self.add_semtags_in_jury(fid, avec_affichage_debug=avec_affichage_debug)
+    #             lesSemTagDesEtudiants = { fid: self.semTagDict[fid] for fid in lesFidsDesEtudiants }
+    #
+    #             # Tous les semtag nécessaires pour ses étudiants avec ajout éventuel s'ils n'ont pas été chargés
+    #             pe_tools.pe_print(" -> Création de l'année tagguée " + str( aid ))
+    #             #settag_id, short_name, listeEtudId, groupe, listeSemAAggreger, ParcoursEtudDict, SemTagDict, with_comp_moy=True)
+    #             self.anTagDict[ aid ] = pe_settag.SetTag( aid, "Annee " + self.semTagDict[fidSemTagFinal].short_name, \
+    #                                                         lesEtudisDelAnnee, 'groupe', lesAnnees[ nom_annee ], parcoursDesEtudiants, lesSemTagDesEtudiants )
+    #             self.anTagDict[ aid ].comp_data_settag() # calcul les moyennes
+
+    # **************************************************************************************************************** #
+    # Traitements des moyennes sur différentes combinaisons de parcours 1A, 2A, 3S et 4S,
+    # impliquées dans le jury
+    # **************************************************************************************************************** #
+
+    def get_settags_in_jury(self):
+        """Calcule les moyennes sur la totalité du parcours (S1 jusqu'à S3 ou S4)
+        en classant les étudiants au sein du semestre final du parcours (même S3, même S4, ...)"""
+
+        # Par groupe :
+        # combinaisons = { 'S1' : ['S1'], 'S2' : ['S2'], 'S3' : ['S3'], 'S4' : ['S4'], \
+        #                  '1A' : ['S1', 'S2'], '2A' : ['S3', 'S4'],
+        #                  '3S' : ['S1', 'S2', 'S3'], '4S' : ['S1', 'S2', 'S3', 'S4'] }
+
+        # ---> sur 2 parcours DUT (cas S3 fini, cas S4 fini)
+        combinaisons = ["1A", "2A", "3S", "4S"]
+        for (i, nom) in enumerate(combinaisons):
+            parcours = JuryPE.PARCOURS[nom][
+                "aggregat"
+            ]  # La liste des noms de semestres (S1, S2, ...) impliqués dans l'aggrégat
+
+            # Recherche des parcours possibles par le biais de leur Fid final
+            fids_finaux = self.get_formsemestreids_du_jury(
+                self.get_etudids_du_jury(), nom
+            )  # les formsemestre_ids validant finaux des étudiants du jury
+
+            if len(fids_finaux) > 0:  # S'il existe des parcours validant
+                if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1:
+                    pe_tools.pe_print("%d) Fusion %s avec" % (i + 1, nom))
+
+                if nom not in self.setTagDict:
+                    self.setTagDict[nom] = {}
+
+                for fid in fids_finaux:
+                    if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1:
+                        pe_tools.pe_print(u"   - semestre final %s" % (fid))
+                    settag = pe_settag.SetTag(
+                        nom, parcours=parcours
+                    )  # Le set tag fusionnant les données
+                    etudiants = self.semTagDict[
+                        fid
+                    ].get_etudids()  # Les étudiants du sem final
+
+                    # ajoute les étudiants au semestre
+                    settag.set_Etudiants(
+                        etudiants,
+                        self.PARCOURSINFO_DICT,
+                        self.ETUDINFO_DICT,
+                        nom_sem_final=self.semTagDict[fid].nom,
+                    )
+
+                    # manque-t-il des semestres ? Si oui, les ajoute au jurype puis au settag
+                    for ffid in settag.get_Fids_in_settag():
+                        if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1:
+                            pe_tools.pe_print(
+                                u"      -> ajout du semestre tagué %s" % (ffid)
+                            )
+                        self.add_semtags_in_jury(ffid)
+                    settag.set_SemTagDict(
+                        self.semTagDict
+                    )  # ajoute les semestres au settag
+
+                    settag.comp_data_settag()  # Calcul les moyennes, les rangs, ..
+
+                    self.setTagDict[nom][fid] = settag  # Mémorise le résultat
+
+            else:
+                if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1:
+                    pe_tools.pe_print("%d) Pas de fusion %s possible" % (i + 1, nom))
+
+    def get_promotags_in_jury(self):
+        """Calcule les aggrégats en interclassant les étudiants du jury (les moyennes ont déjà été calculées en amont)"""
+
+        lesEtudids = self.get_etudids_du_jury()
+
+        for (i, nom) in enumerate(JuryPE.PARCOURS.keys()):
+
+            settag = pe_settag.SetTagInterClasse(nom, diplome=self.diplome)
+            nbreEtudInscrits = settag.set_Etudiants(
+                lesEtudids, self.PARCOURSINFO_DICT, self.ETUDINFO_DICT
+            )
+            if nbreEtudInscrits > 0:
+                if pe_tools.PE_DEBUG:
+                    pe_tools.pe_print(
+                        u"%d) %s avec interclassement sur la promo" % (i + 1, nom)
+                    )
+                if nom in ["S1", "S2", "S3", "S4"]:
+                    settag.set_SetTagDict(self.semTagDict)
+                else:  # cas des aggrégats
+                    settag.set_SetTagDict(self.setTagDict[nom])
+                settag.comp_data_settag()
+                self.promoTagDict[nom] = settag
+            else:
+                if pe_tools.PE_DEBUG:
+                    pe_tools.pe_print(
+                        u"%d) Pas d'interclassement %s sur la promo faute de notes"
+                        % (i + 1, nom)
+                    )
+
+    # **************************************************************************************************************** #
+    # Méthodes pour la synthèse du juryPE
+    # *****************************************************************************************************************
+    def synthetise_juryPE(self):
+        """Synthétise tous les résultats du jury PE dans un dictionnaire"""
+        self.syntheseJury = {}
+        for etudid in self.get_etudids_du_jury():
+            etudinfo = self.ETUDINFO_DICT[etudid]
+            self.syntheseJury[etudid] = {
+                "nom": etudinfo["nom"],
+                "prenom": etudinfo["prenom"],
+                "sexe": etudinfo["sexe"],
+                "age": str(pe_tools.calcul_age(etudinfo["date_naissance"])),
+                "lycee": etudinfo["nomlycee"]
+                + (
+                    " (" + etudinfo["villelycee"] + ")"
+                    if etudinfo["villelycee"] != ""
+                    else ""
+                ),
+                "bac": etudinfo["bac"],
+                "nip": etudinfo["code_nip"],  # pour la photo
+                "entree": self.get_dateEntree(etudid),
+                "promo": self.diplome,
+            }
+            # Le parcours
+            self.syntheseJury[etudid]["parcours"] = self.get_parcoursIUT(
+                etudid
+            )  # liste des semestres
+            self.syntheseJury[etudid]["nbSemestres"] = len(
+                self.syntheseJury[etudid]["parcours"]
+            )  # nombre de semestres
+
+            # Ses résultats
+            for nom in JuryPE.PARCOURS:  # S1, puis S2, puis 1A
+                # dans le groupe : la table tagguée dans les semtag ou les settag si aggrégat
+                self.syntheseJury[etudid][nom] = {"groupe": {}, "promo": {}}
+                if (
+                    self.PARCOURSINFO_DICT[etudid][nom] != None
+                ):  # Un parcours valide existe
+                    if nom in ["S1", "S2", "S3", "S4"]:
+                        tagtable = self.semTagDict[self.PARCOURSINFO_DICT[etudid][nom]]
+                    else:
+                        tagtable = self.setTagDict[nom][
+                            self.PARCOURSINFO_DICT[etudid][nom]
+                        ]
+                    for tag in tagtable.get_all_tags():
+                        self.syntheseJury[etudid][nom]["groupe"][
+                            tag
+                        ] = tagtable.get_resultatsEtud(
+                            tag, etudid
+                        )  # Le tuple des résultats
+
+                    # interclassé dans la promo
+                    tagtable = self.promoTagDict[nom]
+                    for tag in tagtable.get_all_tags():
+                        self.syntheseJury[etudid][nom]["promo"][
+                            tag
+                        ] = tagtable.get_resultatsEtud(tag, etudid)
+
+    def get_dateEntree(self, etudid):
+        """Renvoie l'année d'entrée de l'étudiant à l'IUT"""
+        etudinfo = self.ETUDINFO_DICT[etudid]
+        semDeb = self.get_semestresDUT_d_un_etudiant(etudid)[-1]  # le 1er sem à l'IUT
+        return semDeb["annee_debut"]
+
+    def get_parcoursIUT(self, etudid):
+        """Renvoie une liste d'infos sur les semestres du parcours d'un étudiant 
+        
+        """
+        etudinfo = self.ETUDINFO_DICT[etudid]
+        sems = self.get_semestresDUT_d_un_etudiant(etudid)
+
+        infos = []
+        for sem in sems:
+            nomsem = comp_nom_semestre_dans_parcours(self.context, sem)
+            infos.append(
+                {
+                    "nom_semestre_dans_parcours": nomsem,
+                    "titreannee": sem["titreannee"],
+                    "formsemestre_id": sem["formsemestre_id"],  # utile dans le futur ?
+                }
+            )
+        return infos
+
+    # **************************************************************************************************************** #
+    # Méthodes d'affichage pour debug
+    # **************************************************************************************************************** #
+    def str_etudiants_in_jury(self, delim=";"):
+
+        # En tete:
+        entete = ["Id", "Nom", "Abandon", "Diplome"]
+        for nom_sem in ["S1", "S2", "S3", "S4", "1A", "2A", "3S", "4S"]:
+            entete += [nom_sem, "descr"]
+        chaine = delim.join(entete) + "\n"
+
+        for etudid in self.PARCOURSINFO_DICT:
+
+            donnees = self.PARCOURSINFO_DICT[etudid]
+            # pe_tools.pe_print(etudid, donnees)
+            # les infos générales
+            descr = [
+                etudid,
+                donnees["nom"],
+                str(donnees["abandon"]),
+                str(donnees["diplome"]),
+            ]
+
+            # les semestres
+            for nom_sem in ["S1", "S2", "S3", "S4", "1A", "2A", "3S", "4S"]:
+                table = (
+                    self.semTagDict[donnees[nom_sem]].nom
+                    if donnees[nom_sem] in self.semTagDict
+                    else "manquant"
+                )
+                descr += [
+                    donnees[nom_sem] if donnees[nom_sem] != None else "manquant",
+                    table,
+                ]
+
+            chaine += delim.join(descr) + "\n"
+        return chaine
+
+    #
+    def export_juryPEDict(self):
+        """Export csv de self.PARCOURSINFO_DICT"""
+        fichier = "juryParcoursDict_" + str(self.diplome)
+        pe_tools.pe_print(" -> Export de " + fichier)
+        filename = self.NOM_EXPORT_ZIP + fichier + ".csv"
+        self.zipfile.writestr(filename, self.str_etudiants_in_jury())
+
+    def get_allTagForAggregat(self, nom_aggregat):
+        """Extrait du dictionnaire syntheseJury la liste des tags d'un semestre ou
+         d'un aggrégat donné par son nom (S1, S2, S3 ou S4, 1A, ...). Renvoie [] si aucun tag."""
+        taglist = set()
+        for etudid in self.get_etudids_du_jury():
+            taglist = taglist.union(
+                set(self.syntheseJury[etudid][nom_aggregat]["groupe"].keys())
+            )
+            taglist = taglist.union(
+                set(self.syntheseJury[etudid][nom_aggregat]["promo"].keys())
+            )
+        return list(taglist)
+
+    def get_allTagInSyntheseJury(self):
+        """Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag """
+        allTags = set()
+        for nom in JuryPE.PARCOURS.keys():
+            allTags = allTags.union(set(self.get_allTagForAggregat(nom)))
+        return sorted(list(allTags)) if len(allTags) > 0 else []
+
+    def table_syntheseJury(self, mode="singlesheet"):  # XXX was str_syntheseJury
+        """Table(s) du jury
+        mode: singlesheet ou multiplesheet pour export excel
+        """
+        sT = SeqGenTable()  # le fichier excel à générer
+
+        # Les etudids des étudiants à afficher, triés par ordre alphabétiques de nom+prénom
+        donnees_tries = sorted(
+            [
+                (
+                    etudid,
+                    self.syntheseJury[etudid]["nom"]
+                    + " "
+                    + self.syntheseJury[etudid]["prenom"],
+                )
+                for etudid in self.syntheseJury.keys()
+            ],
+            key=lambda c: c[1],
+        )
+        etudids = [e[0] for e in donnees_tries]
+        if not etudids:  # Si pas d'étudiants
+            T = GenTable(
+                columns_ids=["pas d'étudiants"],
+                rows=[],
+                titles={"pas d'étudiants": "pas d'étudiants"},
+                html_sortable=True,
+                xls_sheet_name="dut",
+            )
+            sT.add_genTable("dut", T)
+            return sT
+
+        # Si des étudiants
+        maxParcours = max(
+            [self.syntheseJury[etudid]["nbSemestres"] for etudid in etudids]
+        )
+
+        infos = ["sexe", "nom", "prenom", "age", "nbSemestres"]
+        entete = ["etudid"]
+        entete.extend(infos)
+        entete.extend(["P%d" % i for i in range(1, maxParcours + 1)])
+        champs = [
+            "note",
+            "class groupe",
+            "class promo",
+            "min/moy/max groupe",
+            "min/moy/max promo",
+        ]
+
+        # Les aggrégats à afficher par ordre tel que indiqué dans le dictionnaire parcours
+        aggregats = JuryPE.PARCOURS.keys()  # ['S1', 'S2', ..., '1A', '4S']
+        aggregats = sorted(
+            aggregats, key=lambda t: JuryPE.PARCOURS[t]["ordre"]
+        )  # Tri des aggrégats
+
+        if mode == "multiplesheet":
+            allSheets = (
+                self.get_allTagInSyntheseJury()
+            )  # tous les tags de syntheseJuryDict
+            allSheets = sorted(allSheets)  # Tri des tags par ordre alphabétique
+            for (
+                sem
+            ) in aggregats:  # JuryPE.PARCOURS.keys() -> ['S1', 'S2', ..., '1A', '4S']
+                entete.extend(["%s %s" % (sem, champ) for champ in champs])
+        else:  # "singlesheet"
+            allSheets = ["singlesheet"]
+            for (
+                sem
+            ) in aggregats:  # JuryPE.PARCOURS.keys() -> ['S1', 'S2', ..., '1A', '4S']
+                tags = self.get_allTagForAggregat(sem)
+                entete.extend(
+                    ["%s %s %s" % (sem, tag, champ) for tag in tags for champ in champs]
+                )
+
+        columns_ids = entete  # les id et les titres de colonnes sont ici identiques
+        titles = {i: i for i in columns_ids}
+
+        for (
+            sheet
+        ) in (
+            allSheets
+        ):  # Pour tous les sheets à générer (1 si singlesheet, autant que de tags si multiplesheet)
+            rows = []
+            for etudid in etudids:
+                e = self.syntheseJury[etudid]
+                # Les info générales:
+                row = {
+                    "etudid": etudid,
+                    "sexe": e["sexe"],
+                    "nom": e["nom"],
+                    "prenom": e["prenom"],
+                    "age": e["age"],
+                    "nbSemestres": e["nbSemestres"],
+                }
+                # Les parcours: P1, P2, ...
+                n = 1
+                for p in e["parcours"]:
+                    row["P%d" % n] = p["titreannee"]
+                    n += 1
+                # if self.syntheseJury[etudid]['nbSemestres'] < maxParcours:
+                #    descr += delim.join( ['']*( maxParcours -self.syntheseJury[etudid]['nbSemestres']) ) + delim
+                for sem in aggregats:  # JuryPE.PARCOURS.keys():
+                    listeTags = (
+                        self.get_allTagForAggregat(sem)
+                        if mode == "singlesheet"
+                        else [sheet]
+                    )
+                    for tag in listeTags:
+                        if tag in self.syntheseJury[etudid][sem]["groupe"]:
+                            resgroupe = self.syntheseJury[etudid][sem]["groupe"][
+                                tag
+                            ]  # tuple
+                        else:
+                            resgroupe = (None, None, None, None, None, None, None)
+                        if tag in self.syntheseJury[etudid][sem]["promo"]:
+                            respromo = self.syntheseJury[etudid][sem]["promo"][tag]
+                        else:
+                            respromo = (None, None, None, None, None, None, None)
+
+                        # note = "%2.2f" % resgroupe[0] if isinstance(resgroupe[0], float) else str(resgroupe[0])
+                        champ = (
+                            "%s %s " % (sem, tag)
+                            if mode == "singlesheet"
+                            else "%s " % (sem)
+                        )
+                        row[champ + "note"] = fmt_note(resgroupe[0])
+                        row[champ + "class groupe"] = "%s / %s" % (
+                            resgroupe[2],
+                            resgroupe[3],
+                        )
+                        row[champ + "class promo"] = "%s / %s" % (
+                            respromo[2],
+                            respromo[3],
+                        )
+                        row[champ + "min/moy/max groupe"] = "%s / %s / %s" % tuple(
+                            fmt_note(x)
+                            for x in (resgroupe[6], resgroupe[4], resgroupe[5])
+                        )
+                        row[champ + "min/moy/max promo"] = "%s / %s / %s" % tuple(
+                            fmt_note(x) for x in (respromo[6], respromo[4], respromo[5])
+                        )
+                rows.append(row)
+
+            T = GenTable(
+                columns_ids=columns_ids,
+                rows=rows,
+                titles=titles,
+                html_sortable=True,
+                xls_sheet_name=sheet,
+            )
+            sT.add_genTable(sheet, T)
+
+        if mode == "singlesheet":
+            return sT.get_genTable("singlesheet")
+        else:
+            return sT
+
+    # **************************************************************************************************************** #
+    # Méthodes de classe pour gestion d'un cache de données accélérant les calculs / intérêt à débattre
+    # **************************************************************************************************************** #
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def get_cache_etudInfo_d_un_etudiant(self, context, etudid):
+        """Renvoie les informations sur le parcours d'un étudiant soit en les relisant depuis
+        ETUDINFO_DICT si mémorisée soit en les chargeant et en les mémorisant
+        """
+        if etudid not in self.ETUDINFO_DICT:
+            self.ETUDINFO_DICT[etudid] = context.getEtudInfo(
+                etudid=etudid, filled=True
+            )[0]
+        return self.ETUDINFO_DICT[etudid]
+
+    # ------------------------------------------------------------------------------------------------------------------
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def get_cache_notes_d_un_semestre(
+        cls, context, formsemestre_id
+    ):  # inutile en realité !
+        """Charge la table des notes d'un formsemestre
+        """
+        return context.Notes._getNotesCache().get_NotesTable(
+            context.Notes, formsemestre_id
+        )
+
+    # ------------------------------------------------------------------------------------------------------------------
+
+    # ------------------------------------------------------------------------------------------------------------------
+    def get_semestresDUT_d_un_etudiant(self, etudid, semestre_id=None):
+        """Renvoie la liste des semestres DUT d'un étudiant
+        pour un semestre_id (parmi 1,2,3,4) donné
+        en fonction de ses infos d'etud (cf. context.getEtudInfo(etudid=etudid, filled=True)[0]),
+        les semestres étant triés par ordre décroissant.
+        Si semestre_id == None renvoie tous les semestres"""
+        etud = self.get_cache_etudInfo_d_un_etudiant(self.context, etudid)
+        if semestre_id == None:
+            sesSems = [sem for sem in etud["sems"] if 1 <= sem["semestre_id"] <= 4]
+        else:
+            sesSems = [sem for sem in etud["sems"] if sem["semestre_id"] == semestre_id]
+        return sesSems
+
+    # **********************************************
+    def calcul_anneePromoDUT_d_un_etudiant(self, etudid):
+        """Calcule et renvoie la date de diplome prévue pour un étudiant fourni avec son etudid
+        en fonction de sesSemestres de scolarisation"""
+        sesSemestres = self.get_semestresDUT_d_un_etudiant(etudid)
+        return max([get_annee_diplome_semestre(sem) for sem in sesSemestres])
+
+    # *********************************************
+    # Fonctions d'affichage pour debug
+    def get_resultat_d_un_etudiant(self, etudid):
+        chaine = ""
+        for nom_sem in ["S1", "S2", "S3", "S4"]:
+            semtagid = self.PARCOURSINFO_DICT[etudid][
+                nom_sem
+            ]  # le formsemestre_id du semestre taggué de l'étudiant
+            semtag = self.semTagDict[semtagid]
+            chaine += "Semestre " + nom_sem + semtagid + "\n"
+            # le détail du calcul tag par tag
+            # chaine += "Détail du calcul du tag\n"
+            # chaine += "-----------------------\n"
+            # for tag in semtag.taglist:
+            #     chaine += "Tag=" + tag + "\n"
+            #     chaine += semtag.str_detail_resultat_d_un_tag(tag, etudid=etudid) + "\n"
+            # le bilan des tags
+            chaine += "Bilan des tags\n"
+            chaine += "--------------\n"
+            for tag in semtag.taglist:
+                chaine += (
+                    tag + ";" + semtag.str_resTag_d_un_etudiant(tag, etudid) + "\n"
+                )
+            chaine += "\n"
+        return chaine
+
+    def get_date_entree_etudiant(self, etudid):
+        """Renvoie la date d'entree d'un étudiant"""
+        return str(
+            min([int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"]])
+        )
+
+
+# ----------------------------------------------------------------------------------------
+# Fonctions
+
+# ----------------------------------------------------------------------------------------
+def get_annee_diplome_semestre(sem):
+    """ Pour un semestre donne, décrit par le biais du dictionnaire sem usuel :
+    sem = {'formestre_id': ..., 'semestre_id': ..., 'annee_debut': ...},
+    à condition qu'il soit un semestre de formation DUT,
+    predit l'annee à laquelle sera remis le diplome DUT des etudiants scolarisés dans le semestre
+    (en supposant qu'il n'y ait plus de redoublement) et la renvoie sous la forme d'un int.
+    Hypothese : les semestres de 1ere partie d'annee universitaire (comme des S1 ou des S3) s'etalent
+    sur deux annees civiles - contrairement au semestre de seconde partie d'annee universitaire (comme
+    des S2 ou des S4).
+    Par exemple :
+        > S4 debutant en 2016 finissant en 2016 => diplome en 2016
+        > S3 debutant en 2015 et finissant en 2016 => diplome en 2016
+        > S3 (decale) debutant en 2015 et finissant en 2015 => diplome en 2016
+    La regle de calcul utilise l'annee_fin du semestre sur le principe suivant :
+    nbreSemRestant = nombre de semestres restant avant diplome
+    nbreAnneeRestant = nombre d'annees restant avant diplome
+    1 - delta = 0 si semestre de 1ere partie d'annee / 1 sinon
+    decalage = active ou desactive un increment a prendre en compte en cas de semestre decale
+    """
+    if (
+        1 <= sem["semestre_id"] <= 4
+    ):  # Si le semestre est un semestre DUT => problème si formation DUT en 1 an ??
+        nbreSemRestant = 4 - sem["semestre_id"]
+        nbreAnRestant = nbreSemRestant // 2
+        delta = int(sem["annee_fin"]) - int(sem["annee_debut"])
+        decalage = nbreSemRestant % 2  # 0 si S4, 1 si S3, 0 si S2, 1 si S1
+        increment = decalage * (1 - delta)
+        return int(sem["annee_fin"]) + nbreAnRestant + increment
+
+
+# ----------------------------------------------------------------------------------------
+
+
+# ----------------------------------------------------------------------------------
+def get_cosemestres_diplomants(context, semBase, avec_meme_formation=False):
+    """ Partant d'un semestre de Base = {'formsemestre_id': ..., 'semestre_id': ..., 'annee_debut': ...},
+        renvoie la liste de tous ses co-semestres (lui-meme inclus)
+        Par co-semestre, s'entend les semestres :
+        > dont l'annee predite pour la remise du diplome DUT est la meme
+        > dont la formation est la même (optionnel)
+        > ne prenant en compte que les etudiants sans redoublement
+    """
+    tousLesSems = (
+        context.Notes.formsemestre_list()
+    )  # tous les semestres memorises dans scodoc
+    diplome = get_annee_diplome_semestre(semBase)
+
+    if avec_meme_formation:  # si une formation est imposee
+        nom_formation = semBase["formation_id"]
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print("   - avec formation imposée : ", nom_formation)
+        coSems = [
+            sem
+            for sem in tousLesSems
+            if get_annee_diplome_semestre(sem) == diplome
+            and sem["formation_id"] == semBase["formation_id"]
+        ]
+    else:
+        if pe_tools.PE_DEBUG:
+            pe_tools.pe_print("   - toutes formations confondues")
+        coSems = [
+            sem for sem in tousLesSems if get_annee_diplome_semestre(sem) == diplome
+        ]
+
+    return coSems
diff --git a/pe_semestretag.py b/pe_semestretag.py
new file mode 100644
index 0000000000000000000000000000000000000000..c7a906aa5f81ee069a51a7cc05bfb4a1d3c88ed5
--- /dev/null
+++ b/pe_semestretag.py
@@ -0,0 +1,504 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+##############################################################################
+#  Module "Avis de poursuite d'étude"
+#  conçu et développé par Cléo Baras (IUT de Grenoble)
+##############################################################################
+
+"""
+Created on Fri Sep  9 09:15:05 2016
+
+@author: barasc
+"""
+import notes_table
+import datetime
+import sco_codes_parcours
+import sco_tag_module
+import sco_utils
+import pe_tagtable
+import pe_jurype
+
+
+class SemestreTag(pe_tagtable.TableTag):
+    """Un SemestreTag représente un tableau de notes (basé sur notesTable)
+    modélisant les résultats des étudiants sous forme de moyennes par tag.
+
+    Attributs récupérés via des NotesTables :
+    - nt: le tableau de notes du semestre considéré
+    - nt.inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions)
+    - nt.identdict: { etudid : ident }
+    - nt._modimpls : liste des moduleimpl { ... 'module_id', ...}
+
+    Attributs supplémentaires :
+    - inscrlist/identdict: étudiants inscrits hors démissionnaires ou défaillants
+    - _tagdict : Dictionnaire résumant les tags et les modules du semestre auxquels ils sont liés
+    - _sum_coeff_semestre : la somme des coeffs du semestre
+
+    Attributs hérités de TableTag :
+    - nom :
+    - resultats: {tag: { etudid: (note_moy, somme_coff), ...} , ...}
+    - rang
+    - statistiques
+    
+    Redéfinition :
+    - get_etudids() : les etudids des étudiants non défaillants ni démissionnaires
+    """
+
+    DEBUG = True
+
+    # -----------------------------------------------------------------------------
+    # Fonctions d'initialisation
+    # -----------------------------------------------------------------------------
+    def __init__(
+        self, context, notetable, sem
+    ):  # Initialisation sur la base d'une notetable
+        """Instantiation d'un objet SemestreTag à partir d'un tableau de note
+        et des informations sur le semestre pour le dater
+        """
+        pe_tagtable.TableTag.__init__(
+            self,
+            nom="S%d %s %s-%s"
+            % (
+                sem["semestre_id"],
+                "ENEPS"
+                if "ENEPS" in sem["titre"]
+                else "UFA"
+                if "UFA" in sem["titre"]
+                else "FI",
+                sem["annee_debut"],
+                sem["annee_fin"],
+            ),
+        )
+
+        # Les attributs spécifiques
+        self.context = context
+        self.nt = notetable
+
+        # Les attributs hérités : la liste des étudiants
+        self.inscrlist = [etud for etud in self.nt.inscrlist if etud["etat"] == "I"]
+        self.identdict = {
+            etudid: ident
+            for (etudid, ident) in self.nt.identdict.items()
+            if etudid in self.get_etudids()
+        }  # Liste des étudiants non démissionnaires et non défaillants
+
+        # Les modules pris en compte dans le calcul des moyennes par tag => ceux des UE standards
+        self.modimpls = [
+            modimpl
+            for modimpl in self.nt._modimpls
+            if modimpl["ue"]["type"] == sco_utils.UE_STANDARD
+        ]  # la liste des modules (objet modimpl)
+        # self._modimpl_ids = [modimpl['moduleimpl_id'] for modimpl in self._modimpls] # la liste de id des modules (modimpl_id)
+        self.somme_coeffs = sum(
+            [modimpl["module"]["coefficient"] for modimpl in self.modimpls]
+        )
+
+    # -----------------------------------------------------------------------------
+    def comp_data_semtag(self):
+        """Calcule tous les données numériques associées au semtag"""
+        # Attributs relatifs aux tag pour les modules pris en compte
+        self.tagdict = (
+            self.do_tagdict()
+        )  # Dictionnaire résumant les tags et les données (normalisées) des modules du semestre auxquels ils sont liés
+
+        # Calcul des moyennes de chaque étudiant puis ajoute la moyenne au sens "DUT"
+        for tag in self.tagdict:
+            self.add_moyennesTag(tag, self.comp_MoyennesTag(tag, force=True))
+        self.add_moyennesTag("dut", self.get_moyennes_DUT())
+        self.taglist = sorted(
+            self.tagdict.keys() + ["dut"]
+        )  # actualise la liste des tags
+
+    # -----------------------------------------------------------------------------
+    def get_etudids(self):
+        """Renvoie la liste des etud_id des étudiants inscrits au semestre
+        """
+        return [etud["etudid"] for etud in self.inscrlist]
+
+    # -----------------------------------------------------------------------------
+    def do_tagdict(self):
+        """Parcourt les modimpl du semestre (instance des modules d'un programme) et synthétise leurs données sous la
+        forme d'un dictionnaire reliant les tags saisis dans le programme aux
+        données des modules qui les concernent, à savoir les modimpl_id, les module_id, le code du module, le coeff,
+        la pondération fournie avec le tag (par défaut 1 si non indiquée).
+        { tagname1 : { modimpl_id1 : { 'module_id' : ..., 'coeff' : ..., 'coeff_norm' : ..., 'ponderation' : ..., 'module_code' : ..., 'ue_xxx' : ...},
+                       modimpl_id2 : ....
+                     },
+          tagname2 : ...
+        }
+        Renvoie le dictionnaire ainsi construit.
+
+        Rq: choix fait de repérer les modules par rapport à leur modimpl_id (valable uniquement pour un semestre), car
+        correspond à la majorité des calculs de moyennes pour les étudiants 
+        (seuls ceux qui ont capitalisé des ue auront un régime de calcul différent).
+        """
+        tagdict = {}
+
+        for modimpl in self.modimpls:
+            modimpl_id = modimpl["moduleimpl_id"]
+            tags = sco_tag_module.module_tag_list(
+                self.context, modimpl["module_id"]
+            )  # liste des tags pour le modimpl concerné
+
+            for (
+                tag
+            ) in tags:  # tag de la forme "mathématiques", "théorie", "pe:0", "maths:2"
+                [tagname, ponderation] = sco_tag_module.split_tagname_coeff(
+                    tag
+                )  # extrait un tagname et un éventuel coefficient de pondération (par defaut: 1)
+                # tagname = tagname
+                if not tagdict.has_key(tagname):  # Ajout d'une clé pour le tag
+                    tagdict[tagname] = {}
+
+                # Ajout du modimpl au tagname considéré
+                tagdict[tagname][modimpl_id] = {
+                    "module_id": modimpl["module_id"],  # les données sur le module
+                    "coeff": modimpl["module"][
+                        "coefficient"
+                    ],  # le coeff du module dans le semestre
+                    "ponderation": ponderation,  # la pondération demandée pour le tag sur le module
+                    "module_code": modimpl["module"][
+                        "code"
+                    ],  # le code qui doit se retrouver à l'identique dans des ue capitalisee
+                    "ue_id": modimpl["ue"]["ue_id"],  # les données sur l'ue
+                    "ue_code": modimpl["ue"]["ue_code"],
+                    "ue_acronyme": modimpl["ue"]["acronyme"],
+                }
+        return tagdict
+
+    # -----------------------------------------------------------------------------
+    def comp_MoyennesTag(self, tag, force=False):
+        """Calcule et renvoie les "moyennes" de tous les étudiants du SemTag (non défaillants)
+        à un tag donné, en prenant en compte
+        tous les modimpl_id concerné par le tag, leur coeff et leur pondération.
+        Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
+        Renvoie les informations sous la forme d'une liste  [ (moy, somme_coeff_normalise, etudid), ...]
+        """
+        lesMoyennes = []
+        for etudid in self.get_etudids():
+            (
+                notes,
+                coeffs_norm,
+                ponderations,
+            ) = self.get_listesNotesEtCoeffsTagEtudiant(
+                tag, etudid
+            )  # les notes associées au tag
+            coeffs = comp_coeff_pond(
+                coeffs_norm, ponderations
+            )  # les coeff pondérés par les tags
+            (moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme(
+                notes, coeffs, force=force
+            )
+            lesMoyennes += [
+                (moyenne, somme_coeffs, etudid)
+            ]  # Un tuple (pour classement résumant les données)
+        return lesMoyennes
+
+    # -----------------------------------------------------------------------------
+    def get_moyennes_DUT(self):
+        """Lit les moyennes DUT du semestre pour tous les étudiants
+        et les renvoie au même format que comp_MoyennesTag"""
+        return [(self.nt.moy_gen[etudid], 1.0, etudid) for etudid in self.get_etudids()]
+
+    # -----------------------------------------------------------------------------
+    def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2):
+        """ Renvoie un couple donnant la note et le coeff normalisé d'un étudiant à un module d'id modimpl_id.
+        La note et le coeff sont extraits :
+        1) soit des données du semestre en normalisant le coefficient par rapport à la somme des coefficients des modules du semestre,
+        2) soit des données des UE précédemment capitalisées, en recherchant un module de même CODE que le modimpl_id proposé,
+        le coefficient normalisé l'étant alors par rapport au total des coefficients du semestre auquel appartient l'ue capitalisée
+        """
+        (note, coeff_norm) = (None, None)
+
+        modimpl = get_moduleimpl(self.nt, modimpl_id)  # Le module considéré
+        if modimpl == None or profondeur < 0:
+            return (None, None)
+
+        # Y-a-t-il eu capitalisation d'UE ?
+        ue_capitalisees = self.get_ue_capitalisees(
+            etudid
+        )  # les ue capitalisées des étudiants
+        ue_capitalisees_id = [
+            ue["ue_id"] for ue in ue_capitalisees
+        ]  # les id des ue capitalisées
+
+        # Si le module ne fait pas partie des UE capitalisées
+        if modimpl["module"]["ue_id"] not in ue_capitalisees_id:
+            note = self.nt.get_etud_mod_moy(modimpl_id, etudid)  # lecture de la note
+            coeff = modimpl["module"]["coefficient"]  # le coeff
+            coeff_norm = (
+                coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
+            )  # le coeff normalisé
+
+        # Si le module fait partie d'une UE capitalisée
+        elif len(ue_capitalisees) > 0:
+            moy_ue_actuelle = get_moy_ue_from_nt(
+                self.nt, etudid, modimpl_id
+            )  # la moyenne actuelle
+            # A quel semestre correspond l'ue capitalisée et quelles sont ses notes ?
+            # fid_prec = [ ue['formsemestre_id'] for ue in ue_capitalisees if ue['ue_id'] == modimpl['module']['ue_id'] ][0]
+            # semestre_id = modimpl['module']['semestre_id']
+            fids_prec = [
+                ue["formsemestre_id"]
+                for ue in ue_capitalisees
+                if ue["ue_code"] == modimpl["ue"]["ue_code"]
+            ]  # and ue['semestre_id'] == semestre_id]
+            if len(fids_prec) > 0:
+                # => le formsemestre_id du semestre dont vient la capitalisation
+                fid_prec = fids_prec[0]
+                # Lecture des notes de ce semestre
+                nt_prec = self.context.Notes._getNotesCache().get_NotesTable(
+                    self.context.Notes, fid_prec
+                )  # le tableau de note du semestre considéré
+
+                # Y-a-t-il un module équivalent c'est à dire correspondant au même code (le module_id n'étant pas significatif en cas de changement de PPN)
+                modimpl_prec = [
+                    module
+                    for module in nt_prec._modimpls
+                    if module["module"]["code"] == modimpl["module"]["code"]
+                ]
+                if len(modimpl_prec) > 0:  # si une correspondance est trouvée
+                    modprec_id = modimpl_prec[0]["moduleimpl_id"]
+                    moy_ue_capitalisee = get_moy_ue_from_nt(nt_prec, etudid, modprec_id)
+                    if (
+                        moy_ue_actuelle >= moy_ue_capitalisee
+                    ):  # on prend la meilleure ue
+                        note = self.nt.get_etud_mod_moy(
+                            modimpl_id, etudid
+                        )  # lecture de la note
+                        coeff = modimpl["module"]["coefficient"]  # le coeff
+                        coeff_norm = (
+                            coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
+                        )  # le coeff normalisé
+                    else:
+                        semtag_prec = SemestreTag(self.context, nt_prec, nt_prec.sem)
+                        (note, coeff_norm) = semtag_prec.get_noteEtCoeff_modimpl(
+                            modprec_id, etudid, profondeur=profondeur - 1
+                        )  # lecture de la note via le semtag associé au modimpl capitalisé
+
+                # Sinon - pas de notes à prendre en compte
+        return (note, coeff_norm)
+
+    # -----------------------------------------------------------------------------
+    def get_ue_capitalisees(self, etudid):
+        """Renvoie la liste des ue_id effectivement capitalisées par un étudiant"""
+        # return [ ue for ue in self.nt.ue_capitalisees[etudid] if self.nt.get_etud_ue_status(etudid,ue['ue_id'])['is_capitalized'] ]
+        return self.nt.ue_capitalisees[etudid]
+
+    # -----------------------------------------------------------------------------
+    def get_listesNotesEtCoeffsTagEtudiant(self, tag, etudid):
+        """Renvoie un triplet (notes, coeffs_norm, ponderations) où notes, coeff_norm et ponderation désignent trois listes
+         donnant -pour un tag donné- les note, coeff et ponderation de chaque modimpl à prendre en compte dans
+         le calcul de la moyenne du tag.
+         Les notes et coeff_norm sont extraits grâce à SemestreTag.get_noteEtCoeff_modimpl (donc dans semestre courant ou UE capitalisée).
+         Les pondérations sont celles déclarées avec le tag (cf. _tagdict). """
+
+        notes = []
+        coeffs_norm = []
+        ponderations = []
+        for (moduleimpl_id, modimpl) in self.tagdict[
+            tag
+        ].items():  # pour chaque module du semestre relatif au tag
+            (note, coeff_norm) = self.get_noteEtCoeff_modimpl(moduleimpl_id, etudid)
+            if note != None:
+                notes.append(note)
+                coeffs_norm.append(coeff_norm)
+                ponderations.append(modimpl["ponderation"])
+        return (notes, coeffs_norm, ponderations)
+
+    # -----------------------------------------------------------------------------
+    # Fonctions d'affichage (et d'export csv) des données du semestre en mode debug
+    # -----------------------------------------------------------------------------
+    def str_detail_resultat_d_un_tag(self, tag, etudid=None, delim=";"):
+        """Renvoie une chaine de caractère décrivant les résultats d'étudiants à un tag :
+        rappelle les notes obtenues dans les modules à prendre en compte, les moyennes et les rangs calculés.
+        Si etudid=None, tous les étudiants inscrits dans le semestre sont pris en compte. Sinon seuls les étudiants indiqués sont affichés."""
+        # Entete
+        chaine = delim.join(["%15s" % "nom", "etudid"]) + delim
+        taglist = self.get_all_tags()
+        if tag in taglist:
+            for mod in self.tagdict[tag].values():
+                chaine += mod["module_code"] + delim
+                chaine += ("%1.1f" % mod["ponderation"]) + delim
+                chaine += "coeff" + delim
+            chaine += delim.join(
+                ["moyenne", "rang", "nbinscrit", "somme_coeff", "somme_coeff"]
+            )  # ligne 1
+        chaine += "\n"
+
+        # Différents cas de boucles sur les étudiants (de 1 à plusieurs)
+        if etudid == None:
+            lesEtuds = self.get_etudids()
+        elif isinstance(etudid, str) and etudid in self.get_etudids():
+            lesEtuds = [etudid]
+        elif isinstance(etudid, list):
+            lesEtuds = [eid for eid in self.get_etudids() if eid in etudid]
+        else:
+            lesEtuds = []
+
+        for etudid in lesEtuds:
+            descr = "%15s" % self.nt.get_nom_short(etudid)[:15] + delim + etudid + delim
+            if tag in taglist:
+                for modimpl_id in self.tagdict[tag]:
+                    (note, coeff) = self.get_noteEtCoeff_modimpl(modimpl_id, etudid)
+                    descr += (
+                        (
+                            "%2.2f" % note
+                            if note != None and isinstance(note, float)
+                            else str(note)
+                        )
+                        + delim
+                        + (
+                            "%1.5f" % coeff
+                            if coeff != None and isinstance(coeff, float)
+                            else str(coeff)
+                        )
+                        + delim
+                        + (
+                            "%1.5f" % (coeff * self.somme_coeffs)
+                            if coeff != None and isinstance(coeff, float)
+                            else str(coeff * self._sum_coeff_semestre)
+                        )
+                        + delim
+                    )
+                moy = self.get_moy_from_resultats(tag, etudid)
+                rang = self.get_rang_from_resultats(tag, etudid)
+                coeff = self.get_coeff_from_resultats(tag, etudid)
+                tot = (
+                    coeff * self.somme_coeffs
+                    if coeff != None
+                    and self.somme_coeffs != None
+                    and isinstance(coeff, float)
+                    else None
+                )
+                descr += (
+                    pe_tagtable.TableTag.str_moytag(
+                        moy, rang, len(self.get_etudids()), delim=delim
+                    )
+                    + delim
+                )
+                descr += (
+                    (
+                        "%1.5f" % coeff
+                        if coeff != None and isinstance(coeff, float)
+                        else str(coeff)
+                    )
+                    + delim
+                    + (
+                        "%.2f" % (tot)
+                        if tot != None
+                        else str(coeff) + "*" + str(self.somme_coeffs)
+                    )
+                )
+            chaine += descr
+            chaine += "\n"
+        return chaine
+
+    def str_tagsModulesEtCoeffs(self):
+        """Renvoie une chaine affichant la liste des tags associés au semestre, les modules qui les concernent et les coeffs de pondération.
+        Plus concrêtement permet d'afficher le contenu de self._tagdict"""
+        chaine = "Semestre %s d'id %d" % (self.nom, id(self)) + "\n"
+        chaine += " -> somme de coeffs: " + str(self.somme_coeffs) + "\n"
+        taglist = self.get_all_tags()
+        for tag in taglist:
+            chaine += " > " + tag + ": "
+            for (modid, mod) in self.tagdict[tag].items():
+                chaine += (
+                    mod["module_code"]
+                    + " ("
+                    + str(mod["coeff"])
+                    + "*"
+                    + str(mod["ponderation"])
+                    + ") "
+                    + modid
+                    + ", "
+                )
+            chaine += "\n"
+        return chaine
+
+
+# ************************************************************************
+# Fonctions diverses
+# ************************************************************************
+
+# *********************************************
+def comp_coeff_pond(coeffs, ponderations):
+    """
+    Applique une ponderation (indiquée dans la liste ponderations) à une liste de coefficients :
+    ex: coeff = [2, 3, 1, None], ponderation = [1, 2, 0, 1] => [2*1, 3*2, 1*0, None]
+    Les coeff peuvent éventuellement être None auquel cas None est conservé ;
+    Les pondérations sont des floattants
+    """
+    if (
+        coeffs == None
+        or ponderations == None
+        or not isinstance(coeffs, list)
+        or not isinstance(ponderations, list)
+        or len(coeffs) != len(ponderations)
+    ):
+        raise ValueError("Erreur de paramètres dans comp_coeff_pond")
+    return [
+        (None if coeffs[i] == None else coeffs[i] * ponderations[i])
+        for i in range(len(coeffs))
+    ]
+
+
+# -----------------------------------------------------------------------------
+def get_moduleimpl(nt, modimpl_id):
+    """Renvoie l'objet modimpl dont l'id est modimpl_id fourni dans la note table nt,
+       en utilisant l'attribut nt._modimpls"""
+    modimplids = [
+        modimpl["moduleimpl_id"] for modimpl in nt._modimpls
+    ]  # la liste de id des modules (modimpl_id)
+    if modimpl_id not in modimplids:
+        if SemestreTag.DEBUG:
+            print "SemestreTag.get_moduleimpl( %s ) : le modimpl recherche n'existe pas" % (
+                modimpl_id
+            )
+        return None
+    return nt._modimpls[modimplids.index(modimpl_id)]
+
+
+# **********************************************
+def get_moy_ue_from_nt(nt, etudid, modimpl_id):
+    """Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve le module de modimpl_id
+    en partant du note table nt"""
+    mod = get_moduleimpl(nt, modimpl_id)  # le module
+    indice = 0
+    while indice < len(nt._ues):
+        if (
+            nt._ues[indice]["ue_id"] == mod["module"]["ue_id"]
+        ):  # si les ue_id correspond
+            data = [
+                ligne for ligne in nt.T if ligne[-1] == etudid
+            ]  # les notes de l'étudiant
+            if data:
+                return data[0][indice + 1]  # la moyenne à l'ue
+            else:
+                indice += 1
+        return None  # si non trouvé
diff --git a/pe_settag.py b/pe_settag.py
new file mode 100644
index 0000000000000000000000000000000000000000..08af55c74c8b28e77097d626beaaa3246c0228a5
--- /dev/null
+++ b/pe_settag.py
@@ -0,0 +1,329 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+##############################################################################
+#  Module "Avis de poursuite d'étude"
+#  conçu et développé par Cléo Baras (IUT de Grenoble)
+##############################################################################
+
+"""
+Created on Fri Sep  9 09:15:05 2016
+
+@author: barasc
+"""
+
+from pe_tools import *
+
+import pe_tagtable
+import pe_semestretag
+import notes_table
+import pe_jurype
+
+
+class SetTag(pe_tagtable.TableTag):
+    """Agrège plusieurs semestres (ou settag) taggués (SemestreTag/Settag de 1 à 4) pour extraire des moyennes
+    et des classements par tag pour un groupe d'étudiants donnés.
+    par. exemple fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S'
+    Le settag est identifié sur la base du dernier semestre (ici le 'S3') ;
+    les étudiants considérés sont donc ceux inscrits dans ce S3
+    à condition qu'ils disposent d'un parcours sur tous les semestres fusionnés valides (par. ex
+    un etudiant non inscrit dans un S1 mais dans un S2 et un S3 n'est pas pris en compte).
+    """
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def __init__(self, nom_combinaison, parcours):
+
+        pe_tagtable.TableTag.__init__(self, nom=nom_combinaison)
+        self.combinaison = nom_combinaison
+        self.parcours = parcours  # Le groupe de semestres/parcours à aggréger
+
+    # -------------------------------------------------------------------------------------------
+    def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None):
+        """Détermine la liste des étudiants à prendre en compte, en partant de
+        la liste en paramètre et en vérifiant qu'ils ont tous un parcours valide."""
+        if nom_sem_final:
+            self.nom += "_" + nom_sem_final
+        for etudid in etudiants:
+            parcours_incomplet = (
+                sum([juryPEDict[etudid][nom_sem] == None for nom_sem in self.parcours])
+                > 0
+            )  # manque-t-il des formsemestre_id validant aka l'étudiant n'a pas été inscrit dans tous les semestres de l'aggrégat
+            if not parcours_incomplet:
+                self.inscrlist.append(etudInfoDict[etudid])
+                self.identdict[etudid] = etudInfoDict[etudid]
+
+        delta = len(etudiants) - len(self.inscrlist)
+        if delta > 0:
+            pe_print(self.nom + " -> " + str(delta) + " étudiants supprimés")
+
+        # Le sous-ensemble des parcours
+        self.parcoursDict = {etudid: juryPEDict[etudid] for etudid in self.identdict}
+
+    # -------------------------------------------------------------------------------------------
+    def get_Fids_in_settag(self):
+        """Renvoie la liste des semestres (leur formsemestre_id) à prendre en compte
+        pour le calcul des moyennes, en considérant tous les étudiants inscrits et
+        tous les semestres de leur parcours"""
+        return list(
+            {
+                self.parcoursDict[etudid][nom_sem]
+                for etudid in self.identdict
+                for nom_sem in self.parcours
+            }
+        )
+
+    # ---------------------------------------------------------------------------------------------
+    def set_SemTagDict(self, SemTagDict):
+        """Mémorise les semtag nécessaires au jury."""
+        self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()}
+        if PE_DEBUG >= 1:
+            pe_print(u"    => %d semestres fusionnés" % len(self.SemTagDict))
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def comp_data_settag(self):
+        """Calcule tous les données numériques relatives au settag"""
+        # Attributs relatifs aux tag pour les modules pris en compte
+        self.taglist = self.do_taglist()  # la liste des tags
+        self.do_tagdict()  # le dico descriptif des tags
+        # if PE_DEBUG >= 1: pe_print("   => Tags = " + ", ".join( self.taglist ))
+
+        # Calcul des moyennes de chaque étudiant par tag
+        reussiteAjoutTag = {"OK": [], "KO": []}
+        for tag in self.taglist:
+            moyennes = self.comp_MoyennesSetTag(tag, force=False)
+            res = self.add_moyennesTag(tag, moyennes)  # pas de notes => pas de moyenne
+            reussiteAjoutTag["OK" if res else "KO"].append(tag)
+        if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG:
+            pe_print(
+                "     => Fusion de %d tags : " % (len(reussiteAjoutTag["OK"]))
+                + ", ".join(reussiteAjoutTag["OK"])
+            )
+        if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG:
+            pe_print(
+                "     => %d tags manquants : " % (len(reussiteAjoutTag["KO"]))
+                + ", ".join(reussiteAjoutTag["KO"])
+            )
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def get_etudids(self):
+        return self.identdict.keys()
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def do_taglist(self):
+        """Parcourt les tags des semestres taggués et les synthétise sous la forme
+        d'une liste en supprimant les doublons
+        """
+        ensemble = []
+        for semtag in self.SemTagDict.values():
+            ensemble.extend(semtag.get_all_tags())
+        return sorted(list(set(ensemble)))
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def do_tagdict(self):
+        """Synthétise la liste des modules pris en compte dans le calcul d'un tag (pour analyse des résultats)
+        """
+        self.tagdict = {}
+        for semtag in self.SemTagDict.values():
+            for tag in semtag.get_all_tags():
+                if tag != "dut":
+                    if tag not in self.tagdict:
+                        self.tagdict[tag] = {}
+                    for mod in semtag.tagdict[tag]:
+                        self.tagdict[tag][mod] = semtag.tagdict[tag][mod]
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid):
+        """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs)
+        avec notes et coeffs deux listes"""
+        lesSemsDeLEtudiant = [
+            self.parcoursDict[etudid][nom_sem] for nom_sem in self.parcours
+        ]  # peuvent être None
+
+        notes = [
+            self.SemTagDict[fid].get_moy_from_resultats(tag, etudid)
+            for fid in lesSemsDeLEtudiant
+            if tag in self.SemTagDict[fid].taglist
+        ]  # eventuellement None
+        coeffs = [
+            self.SemTagDict[fid].get_coeff_from_resultats(tag, etudid)
+            for fid in lesSemsDeLEtudiant
+            if tag in self.SemTagDict[fid].taglist
+        ]
+        return (notes, coeffs)
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def comp_MoyennesSetTag(self, tag, force=False):
+        """Calcule et renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les semestres taggués
+         de l'aggrégat, et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération
+        appliqué dans cette moyenne.
+
+        Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
+
+        Renvoie les informations sous la forme d'une liste  [etudid: (moy, somme_coeff_normalisée, rang), ...}
+        """
+        # if tag not in self.get_all_tags() : return None
+
+        # Calcule les moyennes
+        lesMoyennes = []
+        for (
+            etudid
+        ) in (
+            self.get_etudids()
+        ):  # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag
+            (notes, coeffs_norm) = self.get_NotesEtCoeffsSetTagEtudiant(
+                tag, etudid
+            )  # lecture des notes associées au tag
+            (moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme(
+                notes, coeffs_norm, force=force
+            )
+            lesMoyennes += [
+                (moyenne, somme_coeffs, etudid)
+            ]  # Un tuple (pour classement résumant les données)
+        return lesMoyennes
+
+
+class SetTagInterClasse(pe_tagtable.TableTag):
+    """Récupère les moyennes de SetTag aggrégant un même parcours (par ex un ['S1', 'S2'] n'ayant pas fini au même S2
+     pour fournir un interclassement sur un groupe d'étudiant => seul compte alors la promo
+     nom_combinaison = 'S1' ou '1A'
+    """
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def __init__(self, nom_combinaison, diplome):
+
+        pe_tagtable.TableTag.__init__(self, nom=nom_combinaison + "_%d" % diplome)
+        self.combinaison = nom_combinaison
+        self.parcoursDict = {}
+
+    # -------------------------------------------------------------------------------------------
+    def set_Etudiants(self, etudiants, juryPEDict, etudInfoDict, nom_sem_final=None):
+        """Détermine la liste des étudiants à prendre en compte, en partant de
+        la liste fournie en paramètre et en vérifiant que l'étudiant dispose bien d'un parcours valide pour la combinaison demandée.
+        Renvoie le nombre d'étudiants effectivement inscrits."""
+        if nom_sem_final:
+            self.nom += "_" + nom_sem_final
+        for etudid in etudiants:
+            if juryPEDict[etudid][self.combinaison] != None:
+                self.inscrlist.append(etudInfoDict[etudid])
+                self.identdict[etudid] = etudInfoDict[etudid]
+                self.parcoursDict[etudid] = juryPEDict[etudid]
+        return len(self.inscrlist)
+
+    # -------------------------------------------------------------------------------------------
+    def get_Fids_in_settag(self):
+        """Renvoie la liste des semestres (les formsemestre_id finissant la combinaison par ex. '3S' dont les fid des S3) à prendre en compte
+        pour les moyennes, en considérant tous les étudiants inscrits"""
+        return list(
+            {self.parcoursDict[etudid][self.combinaison] for etudid in self.identdict}
+        )
+
+    # ---------------------------------------------------------------------------------------------
+    def set_SetTagDict(self, SetTagDict):
+        """Mémorise les settag nécessaires au jury."""
+        self.SetTagDict = {
+            fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None
+        }
+        if PE_DEBUG >= 1:
+            pe_print(u"    => %d semestres utilisés" % len(self.SetTagDict))
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def comp_data_settag(self):
+        """Calcule tous les données numériques relatives au settag"""
+        # Attributs relatifs aux tag pour les modules pris en compte
+        self.taglist = self.do_taglist()
+
+        # if PE_DEBUG >= 1: pe_print("   => Tags = " + ", ".join( self.taglist ))
+
+        # Calcul des moyennes de chaque étudiant par tag
+        reussiteAjoutTag = {"OK": [], "KO": []}
+        for tag in self.taglist:
+            moyennes = self.get_MoyennesSetTag(tag, force=False)
+            res = self.add_moyennesTag(tag, moyennes)  # pas de notes => pas de moyenne
+            reussiteAjoutTag["OK" if res else "KO"].append(tag)
+        if len(reussiteAjoutTag["OK"]) > 0 and PE_DEBUG:
+            pe_print(
+                "     => Interclassement de %d tags : " % (len(reussiteAjoutTag["OK"]))
+                + ", ".join(reussiteAjoutTag["OK"])
+            )
+        if len(reussiteAjoutTag["KO"]) > 0 and PE_DEBUG:
+            pe_print(
+                "     => %d tags manquants : " % (len(reussiteAjoutTag["KO"]))
+                + ", ".join(reussiteAjoutTag["KO"])
+            )
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def get_etudids(self):
+        return self.identdict.keys()
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def do_taglist(self):
+        """Parcourt les tags des semestres taggués et les synthétise sous la forme
+        d'une liste en supprimant les doublons
+        """
+        ensemble = []
+        for settag in self.SetTagDict.values():
+            ensemble.extend(settag.get_all_tags())
+        return sorted(list(set(ensemble)))
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def get_NotesEtCoeffsSetTagEtudiant(self, tag, etudid):
+        """Récupère tous les notes et les coeffs d'un étudiant relatives à un tag dans ses semestres valides et les renvoie dans un tuple (notes, coeffs)
+        avec notes et coeffs deux listes"""
+        leSetTagDeLetudiant = self.parcoursDict[etudid][self.combinaison]
+
+        note = self.SetTagDict[leSetTagDeLetudiant].get_moy_from_resultats(tag, etudid)
+        coeff = self.SetTagDict[leSetTagDeLetudiant].get_coeff_from_resultats(
+            tag, etudid
+        )
+        return (note, coeff)
+
+    # -------------------------------------------------------------------------------------------------------------------
+    def get_MoyennesSetTag(self, tag, force=False):
+        """Renvoie les "moyennes" des étudiants à un tag donné, en prenant en compte tous les settag de l'aggrégat,
+        et leur coeff Par moyenne, s'entend une note moyenne, la somme des coefficients de pondération
+        appliqué dans cette moyenne.
+
+        Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
+
+        Renvoie les informations sous la forme d'une liste  [etudid: (moy, somme_coeff_normalisée, rang), ...}
+        """
+        # if tag not in self.get_all_tags() : return None
+
+        # Calcule les moyennes
+        lesMoyennes = []
+        for (
+            etudid
+        ) in (
+            self.get_etudids()
+        ):  # Pour tous les étudiants non défaillants du semestre inscrits dans des modules relatifs au tag
+            (moyenne, somme_coeffs) = self.get_NotesEtCoeffsSetTagEtudiant(
+                tag, etudid
+            )  # lecture des notes associées au tag
+            lesMoyennes += [
+                (moyenne, somme_coeffs, etudid)
+            ]  # Un tuple (pour classement résumant les données)
+        return lesMoyennes
diff --git a/pe_tagtable.py b/pe_tagtable.py
new file mode 100644
index 0000000000000000000000000000000000000000..07602cb38ff6b7cf593bd0e9845c57c129878ecb
--- /dev/null
+++ b/pe_tagtable.py
@@ -0,0 +1,338 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+##############################################################################
+#  Module "Avis de poursuite d'étude"
+#  conçu et développé par Cléo Baras (IUT de Grenoble)
+##############################################################################
+
+
+"""
+Created on Thu Sep  8 09:36:33 2016
+
+@author: barasc
+"""
+
+import notes_table
+import datetime
+import codecs
+
+
+class TableTag:
+    """
+    Classe mémorisant les moyennes des étudiants à différents tag et permettant de calculer les rangs et les statistiques :
+    - nom : Nom représentatif des données de la Table
+    - inscrlist : Les étudiants inscrits dans le TagTag avec leur information de la forme :
+        { etudid : dictionnaire d'info extrait de Scodoc, ...}
+    - taglist : Liste triée des noms des tags
+    - resultats : Dictionnaire donnant les notes-moyennes de chaque étudiant par tag et la somme commulée
+    des coeff utilisées dans le calcul de la moyenne pondérée, sous la forme :
+        { tag : { etudid: (note_moy, somme_coeff_norm),
+                                        ...}
+    - rangs : Dictionnaire donnant les rang par tag de chaque étudiant de la forme :
+        { tag : {etudid: rang, ...} }
+    - nbinscrits : Nombre d'inscrits dans le semestre (pas de distinction entre les tags)
+    - statistiques : Dictionnaire donnant les stastitiques (moyenne, min, max) des résultats par tag de la forme :
+        { tag : (moy, min, max), ...}
+
+    """
+
+    def __init__(self, nom=""):
+        self.nom = nom
+        self.inscrlist = []
+        self.identdict = {}
+        self.taglist = []
+
+        self.resultats = {}
+        self.rangs = {}
+        self.statistiques = {}
+
+    # *****************************************************************************************************************
+    # Accesseurs
+    # *****************************************************************************************************************
+
+    # -----------------------------------------------------------------------------------------------------------
+    def get_moy_from_resultats(self, tag, etudid):
+        """Renvoie la moyenne obtenue par un étudiant à un tag donné au regard du format de self.resultats"""
+        return (
+            self.resultats[tag][etudid][0]
+            if tag in self.resultats and etudid in self.resultats[tag]
+            else None
+        )
+
+    # -----------------------------------------------------------------------------------------------------------
+    def get_rang_from_resultats(self, tag, etudid):
+        """Renvoie le rang à un tag d'un étudiant au regard du format de self.resultats"""
+        return (
+            self.rangs[tag][etudid]
+            if tag in self.resultats and etudid in self.resultats[tag]
+            else None
+        )
+
+    # -----------------------------------------------------------------------------------------------------------
+    def get_coeff_from_resultats(self, tag, etudid):
+        """Renvoie la somme des coeffs de pondération normalisée utilisés dans le calcul de la moyenne à un tag d'un étudiant 
+        au regard du format de self.resultats.
+        """
+        return (
+            self.resultats[tag][etudid][1]
+            if tag in self.resultats and etudid in self.resultats[tag]
+            else None
+        )
+
+    # -----------------------------------------------------------------------------------------------------------
+    def get_all_tags(self):
+        """Renvoie la liste des tags du semestre triée par ordre alphabétique"""
+        # return self.taglist
+        return sorted(self.resultats.keys())
+
+    # -----------------------------------------------------------------------------------------------------------
+    def get_nbinscrits(self):
+        """Renvoie le nombre d'inscrits"""
+        return len(self.inscrlist)
+
+    # -----------------------------------------------------------------------------------------------------------
+    def get_moy_from_stats(self, tag):
+        """ Renvoie la moyenne des notes calculées pour d'un tag donné"""
+        return self.statistiques[tag][0] if tag in self.statistiques else None
+
+    def get_min_from_stats(self, tag):
+        """ Renvoie la plus basse des notes calculées pour d'un tag donné"""
+        return self.statistiques[tag][1] if tag in self.statistiques else None
+
+    def get_max_from_stats(self, tag):
+        """ Renvoie la plus haute des notes calculées pour d'un tag donné"""
+        return self.statistiques[tag][2] if tag in self.statistiques else None
+
+    # -----------------------------------------------------------------------------------------------------------
+    # La structure des données mémorisées pour chaque tag dans le dictionnaire de synthèse
+    # d'un jury PE
+    FORMAT_DONNEES_ETUDIANTS = (
+        "note",
+        "coeff",
+        "rang",
+        "nbinscrits",
+        "moy",
+        "max",
+        "min",
+    )
+
+    def get_resultatsEtud(self, tag, etudid):
+        """Renvoie un tuple (note, coeff, rang, nb_inscrit, moy, min, max) synthétisant les résultats d'un étudiant
+        à un tag donné. None sinon"""
+        return (
+            self.get_moy_from_resultats(tag, etudid),
+            self.get_coeff_from_resultats(tag, etudid),
+            self.get_rang_from_resultats(tag, etudid),
+            self.get_nbinscrits(),
+            self.get_moy_from_stats(tag),
+            self.get_min_from_stats(tag),
+            self.get_max_from_stats(tag),
+        )
+
+    #        return self.tag_stats[tag]
+    #    else :
+    #        return self.pe_stats
+
+    # *****************************************************************************************************************
+    # Ajout des notes
+    # *****************************************************************************************************************
+
+    # -----------------------------------------------------------------------------------------------------------
+    def add_moyennesTag(self, tag, listMoyEtCoeff):
+        """
+        Mémorise les moyennes, les coeffs de pondération et les etudid dans resultats
+        avec calcul du rang
+        :param tag: Un tag
+        :param listMoyEtCoeff: Une liste donnant [ (moy, coeff, etudid) ]
+        """
+        # ajout des moyennes au dictionnaire résultat
+        if listMoyEtCoeff:
+            self.resultats[tag] = {
+                etudid: (moyenne, somme_coeffs)
+                for (moyenne, somme_coeffs, etudid) in listMoyEtCoeff
+            }
+
+            # Calcule les rangs
+            lesMoyennesTriees = sorted(
+                listMoyEtCoeff, reverse=True, key=lambda col: col[0]
+            )  # triées
+            self.rangs[tag] = notes_table.comp_ranks(lesMoyennesTriees)  # les rangs
+
+            # calcul des stats
+            self.comp_stats_d_un_tag(tag)
+            return True
+        return False
+
+    # *****************************************************************************************************************
+    # Méthodes dévolues aux calculs de statistiques (min, max, moy) sur chaque moyenne taguée
+    # *****************************************************************************************************************
+
+    def comp_stats_d_un_tag(self, tag):
+        """
+        Calcule la moyenne generale, le min, le max pour un tag donné,
+        en ne prenant en compte que les moyennes significatives. Mémorise le resultat dans
+        self.statistiques
+        """
+        stats = ("-NA-", "-", "-")
+        if tag not in self.resultats:
+            return stats
+
+        notes = [
+            self.get_moy_from_resultats(tag, etudid) for etudid in self.resultats[tag]
+        ]  # les notes du tag
+        notes_valides = [
+            note for note in notes if isinstance(note, float) and note != None
+        ]
+        nb_notes_valides = len(notes_valides)
+        if nb_notes_valides > 0:
+            (moy, coeff) = moyenne_ponderee_terme_a_terme(notes_valides, force=True)
+            self.statistiques[tag] = (moy, max(notes_valides), min(notes_valides))
+
+    # ************************************************************************
+    # Méthodes dévolues aux affichages -> a revoir
+    # ************************************************************************
+    def str_resTag_d_un_etudiant(self, tag, etudid, delim=";"):
+        """Renvoie une chaine de caractères (valable pour un csv)
+        décrivant la moyenne et le rang d'un étudiant, pour un tag donné ;
+        """
+        if tag not in self.get_all_tags() or etudid not in self.resultats[tag]:
+            return ""
+
+        moystr = TableTag.str_moytag(
+            self.get_moy_from_resultats(tag, etudid),
+            self.get_rang_from_resultats(tag, etudid),
+            self.get_nbinscrits(),
+            delim=delim,
+        )
+        return moystr
+
+    def str_res_d_un_etudiant(self, etudid, delim=";"):
+        """Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique). """
+        return delim.join(
+            [self.str_resTag_d_un_etudiant(tag, etudid) for tag in self.get_all_tags()]
+        )
+
+    # -----------------------------------------------------------------------
+    def str_moytag(cls, moyenne, rang, nbinscrit, delim=";"):
+        """Renvoie une chaine de caractères représentant une moyenne (float ou string) et un rang
+        pour différents formats d'affichage : HTML, debug ligne de commande, csv"""
+        moystr = (
+            "%2.2f%s%s%s%d" % (moyenne, delim, rang, delim, nbinscrit)
+            if isinstance(moyenne, float)
+            else str(moyenne) + delim + str(rang) + delim + str(nbinscrit)
+        )
+        return moystr
+
+    str_moytag = classmethod(str_moytag)
+    # -----------------------------------------------------------------------
+
+    def str_tagtable(self, delim=";", decimal_sep=","):
+        """Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags. """
+        entete = ["etudid", "nom", "prenom"]
+        for tag in self.get_all_tags():
+            entete += [titre + "_" + tag for titre in ["note", "rang", "nb_inscrit"]]
+        chaine = delim.join(entete) + "\n"
+
+        for etudid in self.identdict:
+            descr = delim.join(
+                [
+                    etudid,
+                    self.identdict[etudid]["nom"],
+                    self.identdict[etudid]["prenom"],
+                ]
+            )
+            descr += delim + self.str_res_d_un_etudiant(etudid, delim)
+            chaine += descr + "\n"
+
+        # Ajout des stats ... à faire
+
+        if decimal_sep != ".":
+            return chaine.replace(".", decimal_sep)
+        else:
+            return chaine
+
+
+# ************************************************************************
+# Fonctions diverses
+# ************************************************************************
+
+
+# *********************************************
+def moyenne_ponderee_terme_a_terme(notes, coeffs=None, force=False):
+    """
+    Calcule la moyenne pondérée d'une liste de notes avec d'éventuels coeffs de pondération.
+    Renvoie le résultat sous forme d'un tuple (moy, somme_coeff)
+
+    La liste de notes contient soit : 1) des valeurs numériques 2) des strings "-NA-" (pas de notes) ou "-NI-" (pas inscrit)
+    ou "-c-" ue capitalisée, 3) None.
+    Le paramètre force indique si le calcul de la moyenne doit être forcée ou non, c'est à
+    dire s'il y a ou non omission des notes non numériques (auquel cas la moyenne est calculée sur les
+    notes disponibles) ; sinon renvoie (None, None).
+    """
+    # Vérification des paramètres d'entrée
+    if not isinstance(notes, list) or (
+        coeffs != None and not isinstance(coeffs, list) and len(coeffs) != len(notes)
+    ):
+        raise ValueError("Erreur de paramètres dans moyenne_ponderee_terme_a_terme")
+
+    # Récupération des valeurs des paramètres d'entrée
+    coeffs = [1] * len(notes) if coeffs == None else coeffs
+
+    # S'il n'y a pas de notes
+    if not notes:  # Si notes = []
+        return (None, None)
+
+    notesValides = [
+        (1 if isinstance(note, float) or isinstance(note, int) else 0) for note in notes
+    ]  # Liste indiquant les notes valides
+    if force == True or (
+        force == False and sum(notesValides) == len(notes)
+    ):  # Si on force le calcul de la moyenne ou qu'on ne le force pas et qu'on a le bon nombre de notes
+        (moyenne, ponderation) = (0.0, 0.0)
+        for i in range(len(notes)):
+            if notesValides[i]:
+                moyenne += coeffs[i] * notes[i]
+                ponderation += coeffs[i]
+        return (
+            (moyenne / (ponderation * 1.0), ponderation)
+            if ponderation != 0
+            else (None, 0)
+        )
+    else:  # Si on ne force pas le calcul de la moyenne
+        return (None, None)
+
+
+# -------------------------------------------------------------------------------------------
+def conversionDate_StrToDate(date_fin):
+    """ Conversion d'une date fournie sous la forme d'une chaine de caractère de
+    type 'jj/mm/aaaa' en un objet date du package datetime.
+    Fonction servant au tri des semestres par date
+    """
+    (d, m, y) = [int(x) for x in date_fin.split("/")]
+    date_fin_dst = datetime.date(y, m, d)
+    return date_fin_dst
diff --git a/pe_tools.py b/pe_tools.py
new file mode 100644
index 0000000000000000000000000000000000000000..d501cc48d3a06899cdc20c3a9adb8ceac8cc11ad
--- /dev/null
+++ b/pe_tools.py
@@ -0,0 +1,958 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+##############################################################################
+#  Module "Avis de poursuite d'étude"
+#  conçu et développé par Cléo Baras (IUT de Grenoble)
+##############################################################################
+
+"""
+Created on Thu Sep  8 09:36:33 2016
+
+@author: barasc
+"""
+from __future__ import print_function
+
+import datetime
+import operator
+import re
+import unicodedata
+
+from sco_utils import *
+import notes_table
+
+PE_DEBUG = 0
+
+if not PE_DEBUG:
+    # log to notes.log
+    def pe_print(*a, **kw):
+        # kw is ignored. log always add a newline
+        log(" ".join(a))
+
+
+else:
+    pe_print = print  # print function
+
+
+# Generated LaTeX files are encoded as:
+PE_LATEX_ENCODING = "utf-8"
+
+REP_DEFAULT_AVIS = "config/doc_poursuites_etudes/"
+PE_DEFAULT_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_avis.tex"
+PE_LOCAL_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "local/modeles/un_avis.tex"
+PE_DEFAULT_FOOTER_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_footer.tex"
+PE_LOCAL_FOOTER_TMPL = REP_DEFAULT_AVIS + "local/modeles/un_footer.tex"
+
+# ----------------------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------------------
+def print_semestres_description(sems, avec_affichage_debug=False):
+    """Dediee a l'affichage d'un semestre pour debug du module"""
+
+    def chaine_semestre(sem):
+        desc = (
+            "S"
+            + str(sem["semestre_id"])
+            + " "
+            + sem["modalite"]
+            + " "
+            + sem["anneescolaire"]
+        )
+        desc += " (" + sem["annee_debut"] + "/" + sem["annee_fin"] + ") "
+        desc += sem["formation_id"] + " / " + sem["formsemestre_id"]
+        desc += " - " + sem["titre_num"]
+        return desc
+
+    if avec_affichage_debug == True:
+        if isinstance(sems, list):
+            for sem in sems:
+                pe_print(chaine_semestre(sem))
+        else:
+            pe_print(chaine_semestre(sems))
+
+
+# ----------------------------------------------------------------------------------------
+def calcul_age(born):
+    """Calcule l'age à partir de la date de naissance sous forme d'une chaine de caractère 'jj/mm/aaaa'. 
+    Aucun test de validité sur le format de la date n'est fait.
+    """
+    if not isinstance(born, str) or born == "":
+        return ""
+
+    donnees = born.split("/")
+    naissance = datetime.datetime(int(donnees[2]), int(donnees[1]), int(donnees[0]))
+    today = datetime.date.today()
+    return (
+        today.year
+        - naissance.year
+        - ((today.month, today.day) < (naissance.month, naissance.day))
+    )
+
+
+# ----------------------------------------------------------------------------------------
+def remove_accents(input_unicode_str):
+    """Supprime les accents d'une chaine unicode"""
+    nfkd_form = unicodedata.normalize("NFKD", input_unicode_str)
+    only_ascii = nfkd_form.encode("ASCII", "ignore")
+    return only_ascii
+
+
+def escape_for_latex(s):
+    """Protège les caractères pour inclusion dans du source LaTeX    
+    """
+    if not s:
+        return ""
+    conv = {
+        "&": r"\&",
+        "%": r"\%",
+        "$": r"\$",
+        "#": r"\#",
+        "_": r"\_",
+        "{": r"\{",
+        "}": r"\}",
+        "~": r"\textasciitilde{}",
+        "^": r"\^{}",
+        "\\": r"\textbackslash{}",
+        "<": r"\textless ",
+        ">": r"\textgreater ",
+    }
+    exp = re.compile(
+        "|".join(
+            re.escape(unicode(key))
+            for key in sorted(conv.keys(), key=lambda item: -len(item))
+        )
+    )
+    return exp.sub(lambda match: conv[match.group()], s)
+
+
+# ----------------------------------------------------------------------------------------
+def list_directory_filenames(path):
+    """List of regular filenames in a directory (recursive)
+    Excludes files and directories begining with .
+    """
+    R = []
+    for root, dirs, files in os.walk(path, topdown=True):
+        dirs[:] = [d for d in dirs if d[0] != "."]
+        R += [os.path.join(root, fn) for fn in files if fn[0] != "."]
+    return R
+
+
+def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
+    """Read pathname server file and add content to zip under path_in_zip
+    """
+    rooted_path_in_zip = os.path.join(ziproot, path_in_zip)
+    data = open(pathname).read()
+    zipfile.writestr(rooted_path_in_zip, data)
+
+
+def add_pe_stuff_to_zip(context, zipfile, ziproot):
+    """Add auxiliary files to (already opened) zip
+    Put all local files found under config/doc_poursuites_etudes/local
+    and config/doc_poursuites_etudes/distrib
+    If a file is present in both subtrees, take the one in local.
+
+    Also copy logos
+    """
+    PE_AUX_DIR = os.path.join(SCO_SRCDIR, "config/doc_poursuites_etudes")
+    distrib_dir = os.path.join(PE_AUX_DIR, "distrib")
+    distrib_pathnames = list_directory_filenames(
+        distrib_dir
+    )  # eg /opt/scodoc/Products/ScoDoc/config/doc_poursuites_etudes/distrib/modeles/toto.tex
+    l = len(distrib_dir)
+    distrib_filenames = {x[l + 1 :] for x in distrib_pathnames}  # eg modeles/toto.tex
+
+    local_dir = os.path.join(PE_AUX_DIR, "local")
+    local_pathnames = list_directory_filenames(local_dir)
+    l = len(local_dir)
+    local_filenames = {x[l + 1 :] for x in local_pathnames}
+
+    for filename in distrib_filenames | local_filenames:
+        if filename in local_filenames:
+            add_local_file_to_zip(
+                zipfile, ziproot, os.path.join(local_dir, filename), "avis/" + filename
+            )
+        else:
+            add_local_file_to_zip(
+                zipfile,
+                ziproot,
+                os.path.join(distrib_dir, filename),
+                "avis/" + filename,
+            )
+
+    # Logos: (add to logos/ directory in zip)
+    logos_names = ["logo_header.jpg", "logo_footer.jpg"]
+    for f in logos_names:
+        logo = os.path.join(SCODOC_LOGOS_DIR, f)
+        if os.path.isfile(logo):
+            add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + f)
+
+
+# ----------------------------------------------------------------------------------------
+# Variable pour le debug des avislatex (en squeezant le calcul du jury souvent long)
+JURY_SYNTHESE_POUR_DEBUG = {
+    "EID1810": {
+        "nom": "ROUX",
+        "entree": "2016",
+        "sexe": "M.",
+        "promo": 2016,
+        "S2": {
+            "groupe": {
+                "informatique": (
+                    13.184230769230767,
+                    0.21666666666666667,
+                    "18",
+                    78,
+                    9.731491508491509,
+                    18.46846153846154,
+                    18.46846153846154,
+                ),
+                "technique": (
+                    12.975409073359078,
+                    0.6166666666666666,
+                    "16",
+                    78,
+                    9.948540264387688,
+                    18.29285714285714,
+                    18.29285714285714,
+                ),
+                "pe": (
+                    12.016584900684544,
+                    1.116666666666667,
+                    "20",
+                    78,
+                    9.83147528118408,
+                    17.691755169172936,
+                    17.691755169172936,
+                ),
+                "mathematiques": (
+                    12.25,
+                    0.1,
+                    "15 ex",
+                    78,
+                    8.45153073717949,
+                    19.0625,
+                    19.0625,
+                ),
+                "dut": (
+                    12.43750128724589,
+                    1.0,
+                    "19",
+                    78,
+                    10.151630181286441,
+                    17.881104750512645,
+                    17.881104750512645,
+                ),
+            },
+            "promo": {
+                "informatique": (
+                    13.184230769230767,
+                    0.21666666666666667,
+                    "25",
+                    73,
+                    11.696187214611871,
+                    18.51346153846154,
+                    18.51346153846154,
+                ),
+                "technique": (
+                    12.975409073359078,
+                    0.6166666666666666,
+                    "23",
+                    73,
+                    11.862307379173147,
+                    17.616047267953675,
+                    17.616047267953675,
+                ),
+                "pe": (
+                    12.016584900684544,
+                    1.116666666666667,
+                    "28",
+                    73,
+                    11.571004424603757,
+                    16.706338951857248,
+                    16.706338951857248,
+                ),
+                "mathematiques": (
+                    12.25,
+                    0.1,
+                    "18 ex",
+                    73,
+                    10.00886454908676,
+                    19.0625,
+                    19.0625,
+                ),
+                "dut": (
+                    12.43750128724589,
+                    1.0,
+                    "25",
+                    73,
+                    11.88798432763965,
+                    17.397627309377608,
+                    17.397627309377608,
+                ),
+            },
+        },
+        "S1": {
+            "groupe": {
+                "informatique": (
+                    16.064999999999998,
+                    0.16666666666666669,
+                    "11",
+                    82,
+                    11.020296296296294,
+                    19.325999999999997,
+                    19.325999999999997,
+                ),
+                "technique": (
+                    14.513007894736845,
+                    0.6333333333333333,
+                    "11",
+                    82,
+                    11.195082967479676,
+                    18.309764912280702,
+                    18.309764912280702,
+                ),
+                "pe": (
+                    13.260301515151516,
+                    1.1,
+                    "19",
+                    82,
+                    10.976036277232245,
+                    17.7460505050505,
+                    17.7460505050505,
+                ),
+                "mathematiques": (
+                    11.142850000000001,
+                    0.13333333333333333,
+                    "34",
+                    82,
+                    10.314605121951217,
+                    19.75,
+                    19.75,
+                ),
+                "dut": (
+                    13.54367375,
+                    1.0,
+                    "19",
+                    82,
+                    11.22193801880508,
+                    18.226902529333334,
+                    18.226902529333334,
+                ),
+            },
+            "promo": {
+                "informatique": (
+                    16.064999999999998,
+                    0.16666666666666669,
+                    "15",
+                    73,
+                    13.265276712328768,
+                    19.325999999999997,
+                    19.325999999999997,
+                ),
+                "technique": (
+                    14.513007894736845,
+                    0.6333333333333333,
+                    "16",
+                    73,
+                    12.996048795361693,
+                    18.309764912280702,
+                    18.309764912280702,
+                ),
+                "pe": (
+                    13.260301515151516,
+                    1.1,
+                    "25",
+                    73,
+                    12.4107195879539,
+                    17.7460505050505,
+                    17.7460505050505,
+                ),
+                "mathematiques": (
+                    11.142850000000001,
+                    0.13333333333333333,
+                    "39",
+                    73,
+                    11.320606952054794,
+                    19.75,
+                    19.75,
+                ),
+                "dut": (
+                    13.54367375,
+                    1.0,
+                    "25",
+                    73,
+                    12.730581289342638,
+                    18.226902529333334,
+                    18.226902529333334,
+                ),
+            },
+        },
+        "4S": {
+            "groupe": {
+                "informatique": (
+                    14.84359375,
+                    0.5333333333333333,
+                    "2",
+                    19,
+                    10.69933552631579,
+                    18.28646875,
+                    18.28646875,
+                ),
+                "pe": (
+                    12.93828572598162,
+                    3.75,
+                    "4",
+                    19,
+                    11.861967145815218,
+                    15.737718967605682,
+                    15.737718967605682,
+                ),
+                "mathematiques": (None, None, "1 ex", 19, None, None, None),
+                "ptut": (None, None, "1 ex", 19, None, None, None),
+                "dut": (
+                    13.511767410105122,
+                    4.0,
+                    "4",
+                    19,
+                    12.573349864933606,
+                    15.781651391587998,
+                    15.781651391587998,
+                ),
+            },
+            "promo": {
+                "informatique": (
+                    16.075,
+                    0.1,
+                    "4",
+                    73,
+                    10.316541095890413,
+                    19.333333333333336,
+                    19.333333333333336,
+                ),
+                "pe": (
+                    13.52416666666667,
+                    0.49999999999999994,
+                    "13",
+                    73,
+                    11.657102668465479,
+                    16.853208080808084,
+                    16.853208080808084,
+                ),
+                "mathematiques": (
+                    None,
+                    None,
+                    "55 ex",
+                    73,
+                    7.705091805555555,
+                    19.8,
+                    19.8,
+                ),
+                "dut": (
+                    14.425416666666665,
+                    1.0,
+                    "12",
+                    73,
+                    13.188168241098825,
+                    16.612613522048612,
+                    16.612613522048612,
+                ),
+            },
+        },
+        "S4": {
+            "groupe": {
+                "informatique": (
+                    16.075,
+                    0.1,
+                    "1",
+                    19,
+                    8.799078947368422,
+                    16.075,
+                    16.075,
+                ),
+                "technique": (
+                    13.835576923076923,
+                    0.4333333333333333,
+                    "4",
+                    19,
+                    12.238304655870447,
+                    16.521153846153847,
+                    16.521153846153847,
+                ),
+                "pe": (
+                    13.52416666666667,
+                    0.49999999999999994,
+                    "4",
+                    19,
+                    12.292846491228072,
+                    16.25833333333334,
+                    16.25833333333334,
+                ),
+                "dut": (
+                    14.425416666666665,
+                    1.0,
+                    "6",
+                    19,
+                    13.628367861842106,
+                    15.267566666666665,
+                    15.267566666666665,
+                ),
+            },
+            "promo": {
+                "informatique": (
+                    16.075,
+                    0.1,
+                    "4",
+                    73,
+                    10.316541095890413,
+                    19.333333333333336,
+                    19.333333333333336,
+                ),
+                "pe": (
+                    13.52416666666667,
+                    0.49999999999999994,
+                    "13",
+                    73,
+                    11.657102668465479,
+                    16.853208080808084,
+                    16.853208080808084,
+                ),
+                "technique": (
+                    13.835576923076923,
+                    0.4333333333333333,
+                    "11",
+                    73,
+                    12.086685508009952,
+                    17.25909420289855,
+                    17.25909420289855,
+                ),
+                "mathematiques": (
+                    None,
+                    None,
+                    "55 ex",
+                    73,
+                    7.705091805555555,
+                    19.8,
+                    19.8,
+                ),
+                "ptut": (
+                    13.5,
+                    0.13333333333333333,
+                    "50",
+                    73,
+                    13.898173515981734,
+                    17.083333333333332,
+                    17.083333333333332,
+                ),
+                "dut": (
+                    14.425416666666665,
+                    1.0,
+                    "12",
+                    73,
+                    13.188168241098825,
+                    16.612613522048612,
+                    16.612613522048612,
+                ),
+            },
+        },
+        "1A": {
+            "groupe": {
+                "informatique": (
+                    14.43673913043478,
+                    0.38333333333333336,
+                    "16",
+                    78,
+                    11.046040002787066,
+                    18.85992173913043,
+                    18.85992173913043,
+                ),
+                "technique": (
+                    13.754459142857144,
+                    1.25,
+                    "14",
+                    78,
+                    11.179785631638866,
+                    18.493250340136054,
+                    18.493250340136054,
+                ),
+                "pe": (
+                    12.633767581547854,
+                    2.216666666666667,
+                    "18",
+                    78,
+                    10.912253971396854,
+                    18.39547581699347,
+                    18.39547581699347,
+                ),
+                "mathematiques": (
+                    11.617342857142857,
+                    0.23333333333333334,
+                    "24",
+                    78,
+                    9.921286855287565,
+                    19.375000000000004,
+                    19.375000000000004,
+                ),
+                "dut": (
+                    12.990587518622945,
+                    2.0,
+                    "18",
+                    78,
+                    11.2117147027821,
+                    18.391345156695156,
+                    18.391345156695156,
+                ),
+            },
+            "promo": {
+                "informatique": (
+                    13.184230769230767,
+                    0.21666666666666667,
+                    "25",
+                    73,
+                    11.696187214611871,
+                    18.51346153846154,
+                    18.51346153846154,
+                ),
+                "technique": (
+                    12.975409073359078,
+                    0.6166666666666666,
+                    "23",
+                    73,
+                    11.862307379173147,
+                    17.616047267953675,
+                    17.616047267953675,
+                ),
+                "pe": (
+                    12.016584900684544,
+                    1.116666666666667,
+                    "28",
+                    73,
+                    11.571004424603757,
+                    16.706338951857248,
+                    16.706338951857248,
+                ),
+                "mathematiques": (
+                    12.25,
+                    0.1,
+                    "18 ex",
+                    73,
+                    10.00886454908676,
+                    19.0625,
+                    19.0625,
+                ),
+                "dut": (
+                    12.43750128724589,
+                    1.0,
+                    "25",
+                    73,
+                    11.88798432763965,
+                    17.397627309377608,
+                    17.397627309377608,
+                ),
+            },
+        },
+        "2A": {
+            "groupe": {
+                "informatique": (
+                    15.88333333333333,
+                    0.15000000000000002,
+                    "2",
+                    19,
+                    9.805818713450288,
+                    17.346666666666668,
+                    17.346666666666668,
+                ),
+                "pe": (
+                    13.378513043478259,
+                    1.5333333333333334,
+                    "6",
+                    19,
+                    12.099566454042717,
+                    16.06209927536232,
+                    16.06209927536232,
+                ),
+                "technique": (
+                    13.965093333333336,
+                    1.1666666666666665,
+                    "5",
+                    19,
+                    12.51068332957394,
+                    16.472092380952386,
+                    16.472092380952386,
+                ),
+                "mathematiques": (None, None, "1 ex", 19, None, None, None),
+                "dut": (
+                    14.032947301587301,
+                    2.0,
+                    "4",
+                    19,
+                    13.043386086541773,
+                    15.574706269841268,
+                    15.574706269841268,
+                ),
+            },
+            "promo": {
+                "informatique": (
+                    16.075,
+                    0.1,
+                    "4",
+                    73,
+                    10.316541095890413,
+                    19.333333333333336,
+                    19.333333333333336,
+                ),
+                "pe": (
+                    13.52416666666667,
+                    0.49999999999999994,
+                    "13",
+                    73,
+                    11.657102668465479,
+                    16.853208080808084,
+                    16.853208080808084,
+                ),
+                "technique": (
+                    13.835576923076923,
+                    0.4333333333333333,
+                    "11",
+                    73,
+                    12.086685508009952,
+                    17.25909420289855,
+                    17.25909420289855,
+                ),
+                "mathematiques": (
+                    None,
+                    None,
+                    "55 ex",
+                    73,
+                    7.705091805555555,
+                    19.8,
+                    19.8,
+                ),
+                "dut": (
+                    14.425416666666665,
+                    1.0,
+                    "12",
+                    73,
+                    13.188168241098825,
+                    16.612613522048612,
+                    16.612613522048612,
+                ),
+            },
+        },
+        "nbSemestres": 4,
+        "nip": "21414563",
+        "prenom": "Baptiste",
+        "age": "21",
+        "lycee": "PONCET",
+        "3S": {
+            "groupe": {
+                "informatique": (
+                    14.559423076923077,
+                    0.43333333333333335,
+                    "3",
+                    19,
+                    11.137856275303646,
+                    18.8095,
+                    18.8095,
+                ),
+                "pe": (
+                    12.84815019664546,
+                    3.25,
+                    "4",
+                    19,
+                    11.795678015751701,
+                    15.657624449801428,
+                    15.657624449801428,
+                ),
+                "technique": (
+                    13.860638395358142,
+                    1.9833333333333334,
+                    "3",
+                    19,
+                    12.395950358235925,
+                    17.340302131732695,
+                    17.340302131732695,
+                ),
+                "mathematiques": (
+                    11.494044444444445,
+                    0.3,
+                    "6",
+                    19,
+                    9.771571754385965,
+                    14.405358333333334,
+                    14.405358333333334,
+                ),
+                "dut": (
+                    13.207217657917942,
+                    3.0,
+                    "4",
+                    19,
+                    12.221677199297439,
+                    15.953012966561774,
+                    15.953012966561774,
+                ),
+            },
+            "promo": {
+                "informatique": (15.5, 0.05, "13", 73, 10.52222222222222, 20.0, 20.0),
+                "pe": (
+                    13.308035483870967,
+                    1.0333333333333334,
+                    "17",
+                    73,
+                    11.854843423685786,
+                    16.191317607526884,
+                    16.191317607526884,
+                ),
+                "technique": (
+                    14.041625757575758,
+                    0.7333333333333333,
+                    "10",
+                    73,
+                    11.929466899200335,
+                    16.6400384469697,
+                    16.6400384469697,
+                ),
+                "mathematiques": (
+                    11.0625,
+                    0.06666666666666667,
+                    "40",
+                    73,
+                    11.418430205479451,
+                    19.53,
+                    19.53,
+                ),
+                "dut": (
+                    13.640477936507937,
+                    1.0,
+                    "14",
+                    73,
+                    12.097377866597594,
+                    16.97088994741667,
+                    16.97088994741667,
+                ),
+            },
+        },
+        "bac": "STI2D",
+        "S3": {
+            "groupe": {
+                "informatique": (15.5, 0.05, "5", 19, 12.842105263157896, 20.0, 20.0),
+                "pe": (
+                    13.308035483870967,
+                    1.0333333333333334,
+                    "8",
+                    19,
+                    12.339608902093943,
+                    15.967147311827956,
+                    15.967147311827956,
+                ),
+                "technique": (
+                    14.041625757575758,
+                    0.7333333333333333,
+                    "7",
+                    19,
+                    13.128539816586922,
+                    16.44310151515152,
+                    16.44310151515152,
+                ),
+                "mathematiques": (
+                    11.0625,
+                    0.06666666666666667,
+                    "6",
+                    19,
+                    9.280921052631578,
+                    16.125,
+                    16.125,
+                ),
+                "dut": (
+                    13.640477936507937,
+                    1.0,
+                    "8",
+                    19,
+                    12.83638061385213,
+                    15.881845873015871,
+                    15.881845873015871,
+                ),
+            },
+            "promo": {
+                "informatique": (15.5, 0.05, "13", 73, 10.52222222222222, 20.0, 20.0),
+                "pe": (
+                    13.308035483870967,
+                    1.0333333333333334,
+                    "17",
+                    73,
+                    11.854843423685786,
+                    16.191317607526884,
+                    16.191317607526884,
+                ),
+                "technique": (
+                    14.041625757575758,
+                    0.7333333333333333,
+                    "10",
+                    73,
+                    11.929466899200335,
+                    16.6400384469697,
+                    16.6400384469697,
+                ),
+                "mathematiques": (
+                    11.0625,
+                    0.06666666666666667,
+                    "40",
+                    73,
+                    11.418430205479451,
+                    19.53,
+                    19.53,
+                ),
+                "dut": (
+                    13.640477936507937,
+                    1.0,
+                    "14",
+                    73,
+                    12.097377866597594,
+                    16.97088994741667,
+                    16.97088994741667,
+                ),
+            },
+        },
+        "parcours": [
+            {
+                "nom_semestre_dans_parcours": "semestre 4 FAP  2016",
+                "titreannee": "DUT RT UFA (PPN 2013), semestre 4 FAP  2016",
+            },
+            {
+                "nom_semestre_dans_parcours": "semestre 3 FAP  2015-2016",
+                "titreannee": "DUT RT UFA (PPN 2013), semestre 3 FAP  2015-2016",
+            },
+            {
+                "nom_semestre_dans_parcours": "semestre 2 FI  2015",
+                "titreannee": "DUT RT, semestre 2 FI  2015",
+            },
+            {
+                "nom_semestre_dans_parcours": "semestre 1 FI  2014-2015",
+                "titreannee": "DUT RT, semestre 1 FI  2014-2015",
+            },
+        ],
+    }
+}
diff --git a/pe_view.py b/pe_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c3cc695e662cc0a98d12e3d50975d44e1f336f8
--- /dev/null
+++ b/pe_view.py
@@ -0,0 +1,190 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+##############################################################################
+#  Module "Avis de poursuite d'étude"
+#  conçu et développé par Cléo Baras (IUT de Grenoble)
+##############################################################################
+
+
+"""ScoDoc : interface des fonctions de gestion des avis de poursuites d'étude
+
+"""
+
+from sco_utils import *
+from notes_log import log
+import sco_formsemestre
+import sco_formsemestre_status
+import notes_table
+from gen_tables import GenTable
+import sco_codes_parcours
+
+import pe_tools
+from pe_tools import *
+import pe_tagtable
+import pe_semestretag
+import pe_settag
+import pe_jurype
+import pe_avislatex
+
+
+def _pe_view_sem_recap_form(context, formsemestre_id, REQUEST=None):
+    H = [
+        context.sco_header(REQUEST, page_title="Avis de poursuite d'études"),
+        """<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
+        <p class="help">
+        Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de poursuites d'études.
+        <br/>
+        De nombreux aspects sont paramétrables: 
+        <a href="https://scodoc.org/AvisPoursuiteEtudes">
+        voir la documentation</a>.
+        </p>
+        <form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form" enctype="multipart/form-data">
+        <div class="pe_template_up">
+        Les templates sont généralement installés sur le serveur ou dans le paramétrage de ScoDoc.<br/> 
+        Au besoin, vous pouvez spécifier ici votre propre fichier de template (<tt>un_avis.tex</tt>):
+        <div class="pe_template_upb">Template: <input type="file" size="30" name="avis_tmpl_file"/></div>
+        <div class="pe_template_upb">Pied de page: <input type="file" size="30" name="footer_tmpl_file"/></div>
+        </div>
+        <input type="submit" value="Générer les documents"/>
+        <input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
+        </form>
+        """.format(
+            formsemestre_id=formsemestre_id
+        ),
+    ]
+    return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+def pe_view_sem_recap(
+    context,
+    formsemestre_id,
+    avis_tmpl_file=None,
+    footer_tmpl_file=None,
+    mode_debug=False,
+    REQUEST=None,
+):
+    """Génération des avis de poursuite d'étude
+    
+    mode_debug = Pour "squeezer" le calcul du jury pe (long)
+    et debugger uniquement la partie avis latex
+    """
+    if REQUEST and REQUEST.method == "GET":
+        return _pe_view_sem_recap_form(context, formsemestre_id, REQUEST=REQUEST)
+    prefs = context.get_preferences(formsemestre_id=formsemestre_id)
+
+    semBase = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+
+    jury = pe_jurype.JuryPE(context, semBase)
+
+    # Ajout avis LaTeX au même zip:
+    etudids = jury.syntheseJury.keys()
+
+    # Récupération du template latex, du footer latex et du tag identifiant les annotations relatives aux PE
+    # (chaines unicodes, html non quoté)
+    template_latex = ""
+    # template fourni via le formulaire Web
+    if avis_tmpl_file:
+        template_latex = avis_tmpl_file.read()
+        template_latex = template_latex.decode(SCO_ENCODING)
+    else:
+        # template indiqué dans préférences ScoDoc ?
+        template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
+            context, formsemestre_id, champ="pe_avis_latex_tmpl"
+        )
+
+    template_latex = template_latex.strip()
+    if not template_latex:
+        # pas de preference pour le template: utilise fichier du serveur
+        template_latex = pe_avislatex.get_templates_from_distrib("avis")
+
+    # Footer:
+    footer_latex = ""
+    # template fourni via le formulaire Web
+    if footer_tmpl_file:
+        footer_latex = footer_tmpl_file.read()
+        footer_latex = footer_latex.decode(SCO_ENCODING)
+    else:
+        footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
+            context, formsemestre_id, champ="pe_avis_latex_footer"
+        )
+    footer_latex = footer_latex.strip()
+    if not footer_latex:
+        # pas de preference pour le footer: utilise fichier du serveur
+        footer_latex = pe_avislatex.get_templates_from_distrib(
+            "footer"
+        )  # fallback: footer vides
+
+    tag_annotation_pe = pe_avislatex.get_code_latex_from_scodoc_preference(
+        context, formsemestre_id, champ="pe_tag_annotation_avis_latex"
+    )
+
+    # Ajout des annotations PE dans un fichier excel
+    sT = pe_avislatex.table_syntheseAnnotationPE(
+        context, jury.syntheseJury, tag_annotation_pe
+    )
+    if sT:
+        jury.add_file_to_zip(jury.NOM_EXPORT_ZIP + "_annotationsPE.xls", sT.excel())
+
+    latex_pages = {}  # Dictionnaire de la forme nom_fichier => contenu_latex
+    for etudid in etudids:
+        [nom_fichier, contenu_latex] = pe_avislatex.get_avis_poursuite_par_etudiant(
+            context,
+            jury,
+            etudid,
+            template_latex,
+            tag_annotation_pe,
+            footer_latex,
+            prefs,
+        )
+
+        jury.add_file_to_zip(
+            ("avis/" + nom_fichier + ".tex").encode(PE_LATEX_ENCODING),
+            contenu_latex.encode(PE_LATEX_ENCODING),
+        )
+        latex_pages[nom_fichier] = contenu_latex  # Sauvegarde dans un dico
+
+    # Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous
+    doc_latex = "\n% -----\n".join(
+        ["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())]
+    )
+    jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex.encode(PE_LATEX_ENCODING))
+
+    # Ajoute image, LaTeX class file(s) and modeles
+    pe_tools.add_pe_stuff_to_zip(context, jury.zipfile, jury.NOM_EXPORT_ZIP)
+    data = jury.get_zipped_data()
+    size = len(data)
+
+    content_type = "application/zip"
+    if REQUEST != None:
+        REQUEST.RESPONSE.setHeader(
+            "content-disposition",
+            'attachement; filename="%s.zip"' % jury.NOM_EXPORT_ZIP,
+        )
+        REQUEST.RESPONSE.setHeader("content-type", content_type)
+        REQUEST.RESPONSE.setHeader("content-length", size)
+    return data
diff --git a/safehtml.py b/safehtml.py
new file mode 100644
index 0000000000000000000000000000000000000000..461719755e40546b31bde6c258e7f4f602002d82
--- /dev/null
+++ b/safehtml.py
@@ -0,0 +1,13 @@
+from stripogram import html2text, html2safehtml
+
+# permet de conserver quelques tags html
+def HTML2SafeHTML(text, convert_br=True):
+    text = html2safehtml(text, valid_tags=("b", "a", "i", "br", "p"))
+    if convert_br:
+        return newline_to_br(text)
+    else:
+        return text
+
+
+def newline_to_br(text):
+    return text.replace("\n", "<br/>")
diff --git a/sco_abs_notification.py b/sco_abs_notification.py
new file mode 100644
index 0000000000000000000000000000000000000000..7af6e57886815fc399c406a43d0610d64804944b
--- /dev/null
+++ b/sco_abs_notification.py
@@ -0,0 +1,291 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Système de notification par mail des excès d'absences
+(see ticket #147)
+
+
+Il suffit d'appeler abs_notify() après chaque ajout d'absence.
+"""
+
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email.Header import Header
+from email import Encoders
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from scolog import logdb
+import sco_bulletins
+import sco_formsemestre
+
+
+def abs_notify(context, etudid, date):
+    """Check if notifications are requested and send them
+    Considère le nombre d'absence dans le semestre courant 
+    (s'il n'y a pas de semestre courant, ne fait rien,
+    car l'etudiant n'est pas inscrit au moment de l'absence!).
+    """
+    sem = retreive_current_formsemestre(context, etudid, date)
+    if not sem:
+        return  # non inscrit a la date, pas de notification
+
+    debut_sem = DateDMYtoISO(sem["date_debut"])
+    fin_sem = DateDMYtoISO(sem["date_fin"])
+    nbabs = context.CountAbs(etudid, debut=debut_sem, fin=fin_sem)
+    nbabsjust = context.CountAbsJust(etudid, debut=debut_sem, fin=fin_sem)
+
+    do_abs_notify(context, sem, etudid, date, nbabs, nbabsjust)
+
+
+def do_abs_notify(context, sem, etudid, date, nbabs, nbabsjust):
+    """Given new counts of absences, check if notifications are requested and send them.
+    """
+    # prefs fallback to global pref if sem is None:
+    if sem:
+        formsemestre_id = sem["formsemestre_id"]
+    else:
+        formsemestre_id = None
+    prefs = context.get_preferences(formsemestre_id=sem["formsemestre_id"])
+
+    destinations = abs_notify_get_destinations(
+        context, sem, prefs, etudid, date, nbabs, nbabsjust
+    )
+    msg = abs_notification_message(context, sem, prefs, etudid, nbabs, nbabsjust)
+    if not msg:
+        return  # abort
+
+    # Vérification fréquence (pour ne pas envoyer de mails trop souvent)
+    abs_notify_max_freq = context.get_preference("abs_notify_max_freq")
+    destinations_filtered = []
+    for email_addr in destinations:
+        nbdays_since_last_notif = user_nbdays_since_last_notif(
+            context, email_addr, etudid
+        )
+        if (nbdays_since_last_notif is None) or (
+            nbdays_since_last_notif >= abs_notify_max_freq
+        ):
+            destinations_filtered.append(email_addr)
+
+    if destinations_filtered:
+        abs_notify_send(
+            context,
+            destinations_filtered,
+            etudid,
+            msg,
+            nbabs,
+            nbabsjust,
+            formsemestre_id,
+        )
+
+
+def abs_notify_send(
+    context, destinations, etudid, msg, nbabs, nbabsjust, formsemestre_id
+):
+    """Actually send the notification by email, and register it in database"""
+    cnx = context.GetDBConnexion()
+    log("abs_notify: sending notification to %s" % destinations)
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    for email in destinations:
+        del msg["To"]
+        msg["To"] = email
+        context.sendEmail(msg)
+        SimpleQuery(
+            context,
+            """insert into absences_notifications (etudid, email, nbabs, nbabsjust, formsemestre_id) values (%(etudid)s, %(email)s, %(nbabs)s, %(nbabsjust)s, %(formsemestre_id)s)""",
+            vars(),
+            cursor=cursor,
+        )
+
+    logdb(
+        cnx=cnx,
+        method="abs_notify",
+        etudid=etudid,
+        msg="sent to %s (nbabs=%d)" % (destinations, nbabs),
+    )
+
+
+def abs_notify_get_destinations(context, sem, prefs, etudid, date, nbabs, nbabsjust):
+    """Returns set of destination emails to be notified
+    """
+    formsemestre_id = sem["formsemestre_id"]
+
+    destinations = []  # list of email address to notify
+
+    if abs_notify_is_above_threshold(
+        context, etudid, nbabs, nbabsjust, formsemestre_id
+    ):
+        if sem and prefs["abs_notify_respsem"]:
+            # notifie chaque responsable du semestre
+            for responsable_id in sem["responsables"]:
+                u = context.Users.user_info(responsable_id)
+                if u["email"]:
+                    destinations.append(u["email"])
+        if prefs["abs_notify_chief"] and prefs["email_chefdpt"]:
+            destinations.append(prefs["email_chefdpt"])
+        if prefs["abs_notify_email"]:
+            destinations.append(prefs["abs_notify_email"])
+        if prefs["abs_notify_etud"]:
+            etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+            if etud["email_default"]:
+                destinations.append(etud["email_default"])
+
+    # Notification (à chaque fois) des resp. de modules ayant des évaluations
+    # à cette date
+    # nb: on pourrait prevoir d'utiliser un autre format de message pour ce cas
+    if sem and prefs["abs_notify_respeval"]:
+        mods = mod_with_evals_at_date(context, date, etudid)
+        for mod in mods:
+            u = context.Users.user_info(mod["responsable_id"])
+            if u["email"]:
+                destinations.append(u["email"])
+
+    # uniq
+    destinations = set(destinations)
+
+    return destinations
+
+
+def abs_notify_is_above_threshold(context, etudid, nbabs, nbabsjust, formsemestre_id):
+    """True si il faut notifier les absences (indépendemment du destinataire)
+
+    nbabs: nombre d'absence (de tous types, unité de compte = demi-journée)
+    nbabsjust: nombre d'absences justifiées
+    
+    (nbabs > abs_notify_abs_threshold) 
+    (nbabs - nbabs_last_notified) > abs_notify_abs_increment
+    """
+    abs_notify_abs_threshold = context.get_preference(
+        "abs_notify_abs_threshold", formsemestre_id
+    )
+    abs_notify_abs_increment = context.get_preference(
+        "abs_notify_abs_increment", formsemestre_id
+    )
+    nbabs_last_notified = etud_nbabs_last_notified(context, etudid, formsemestre_id)
+
+    if nbabs_last_notified == 0:
+        if nbabs > abs_notify_abs_threshold:
+            return True  # first notification
+        else:
+            return False
+    else:
+        if (nbabs - nbabs_last_notified) >= abs_notify_abs_increment:
+            return True
+    return False
+
+
+def etud_nbabs_last_notified(context, etudid, formsemestre_id=None):
+    """nbabs lors de la dernière notification envoyée pour cet étudiant dans ce semestre
+    ou sans semestre (ce dernier cas est nécessaire pour la transition au nouveau code)"""
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        """select * from absences_notifications where etudid = %(etudid)s and (formsemestre_id = %(formsemestre_id)s or formsemestre_id is NULL) order by notification_date desc""",
+        vars(),
+    )
+    res = cursor.dictfetchone()
+    if res:
+        return res["nbabs"]
+    else:
+        return 0
+
+
+def user_nbdays_since_last_notif(context, email_addr, etudid):
+    """nb days since last notification to this email, or None if no previous notification"""
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        """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()
+    if res:
+        mxd = res["notification_date"]  # mx.DateTime instance
+        lastdate = datetime.datetime(mxd.year, mxd.month, mxd.day)
+        now = datetime.datetime.now()
+        return (now - lastdate).days
+    else:
+        return None
+
+
+def abs_notification_message(context, sem, prefs, etudid, nbabs, nbabsjust):
+    """Mime notification message based on template.
+    returns None if sending should be canceled (emplty template).
+    """
+    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+
+    # Variables accessibles dans les balises du template: %(nom_variable)s :
+    values = sco_bulletins.make_context_dict(context, sem, etud)
+
+    values["nbabs"] = nbabs
+    values["nbabsjust"] = nbabsjust
+    values["nbabsnonjust"] = nbabs - nbabsjust
+    values["url_ficheetud"] = context.ScoURL() + "/ficheEtud?etudid=" + etudid
+
+    template = prefs["abs_notification_mail_tmpl"]
+    if template:
+        txt = prefs["abs_notification_mail_tmpl"] % values
+    else:
+        log("abs_notification_message: empty template, not sending message")
+        return None
+
+    subject = """Trop d'absences pour %(nomprenom)s""" % etud
+    #
+    msg = MIMEMultipart()
+    subj = Header("[ScoDoc] " + subject, SCO_ENCODING)
+    msg["Subject"] = subj
+    msg["From"] = prefs["email_from_addr"]
+    txt = MIMEText(txt, "plain", SCO_ENCODING)
+    msg.attach(txt)
+    return msg
+
+
+def retreive_current_formsemestre(context, etudid, cur_date):
+    """Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée
+    date est une chaine au format ISO (yyyy-mm-dd)
+    """
+    req = """SELECT i.formsemestre_id FROM notes_formsemestre_inscription i, notes_formsemestre sem
+    WHERE sem.formsemestre_id = i.formsemestre_id AND i.etudid=%(etudid)s
+    AND (%(cur_date)s >= sem.date_debut) AND (%(cur_date)s <= sem.date_fin)"""
+
+    r = SimpleDictFetch(context, req, {"etudid": etudid, "cur_date": cur_date})
+    if not r:
+        return None
+    # s'il y a plusieurs semestres, prend le premier (rarissime et non significatif):
+    sem = sco_formsemestre.get_formsemestre(context, r[0]["formsemestre_id"])
+    return sem
+
+
+def mod_with_evals_at_date(context, date_abs, etudid):
+    """Liste des moduleimpls avec des evaluations a la date indiquée
+    """
+    req = """SELECT m.* FROM notes_moduleimpl m, notes_evaluation e, notes_moduleimpl_inscription i
+    WHERE m.moduleimpl_id = e.moduleimpl_id AND e.moduleimpl_id = i.moduleimpl_id
+    AND i.etudid = %(etudid)s AND e.jour = %(date_abs)s"""
+    r = SimpleDictFetch(context, req, {"etudid": etudid, "date_abs": date_abs})
+    return r
diff --git a/sco_abs_views.py b/sco_abs_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b98cbc4e6d97d9ff9122824c3f59d66095bedda
--- /dev/null
+++ b/sco_abs_views.py
@@ -0,0 +1,806 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Pages HTML gestion absences 
+   (la plupart portées du DTML)
+"""
+
+from stripogram import html2text, html2safehtml
+from gen_tables import GenTable
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+import sco_groups
+import sco_find_etud
+import sco_formsemestre
+import sco_photos
+
+import ZAbsences
+
+
+def doSignaleAbsence(
+    context,
+    datedebut,
+    datefin,
+    moduleimpl_id=None,
+    demijournee=2,
+    estjust=False,
+    description=None,
+    REQUEST=None,
+):  # etudid implied
+    """Signalement d'une absence
+    """
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    etudid = etud["etudid"]
+
+    description_abs = description
+    dates = context.DateRangeISO(datedebut, datefin)
+    nbadded = 0
+    for jour in dates:
+        if demijournee == "2":
+            context._AddAbsence(
+                etudid, jour, False, estjust, REQUEST, description_abs, moduleimpl_id
+            )
+            context._AddAbsence(
+                etudid, jour, True, estjust, REQUEST, description_abs, moduleimpl_id
+            )
+            nbadded += 2
+        else:
+            matin = int(demijournee)
+            context._AddAbsence(
+                etudid, jour, matin, estjust, REQUEST, description_abs, moduleimpl_id
+            )
+            nbadded += 1
+    #
+    if estjust:
+        J = ""
+    else:
+        J = "NON "
+    M = ""
+    if moduleimpl_id and moduleimpl_id != "NULL":
+        mod = context.Notes.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+        formsemestre_id = mod["formsemestre_id"]
+        nt = context.Notes._getNotesCache().get_NotesTable(
+            context.Notes, formsemestre_id
+        )
+        ues = nt.get_ues(etudid=etudid)
+        for ue in ues:
+            modimpls = nt.get_modimpls(ue_id=ue["ue_id"])
+            for modimpl in modimpls:
+                if modimpl["moduleimpl_id"] == moduleimpl_id:
+                    M = "dans le module %s" % modimpl["module"]["code"]
+    H = [
+        context.sco_header(
+            REQUEST, page_title="Signalement d'une absence pour %(nomprenom)s" % etud
+        ),
+        """<h2>Signalement d'absences</h2>""",
+    ]
+    if dates:
+        H.append(
+            """<p>Ajout de %d absences <b>%sjustifiées</b> du %s au %s %s</p>"""
+            % (nbadded, J, datedebut, datefin, M)
+        )
+    else:
+        H.append(
+            """<p class="warning">Aucune date ouvrable entre le %s et le %s !</p>"""
+            % (datedebut, datefin)
+        )
+
+    H.append(
+        """<ul><li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Autre absence pour <b>%(nomprenom)s</b></a></li>
+                    <li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses absences</a></li>
+                </ul>
+              <hr>"""
+        % etud
+    )
+    H.append(sco_find_etud.form_search_etud(context, REQUEST))
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def SignaleAbsenceEtud(context, REQUEST=None):  # etudid implied
+    """Formulaire individuel simple de signalement d'une absence
+    """
+    # brute-force portage from very old dtml code ...
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    etudid = etud["etudid"]
+    if not etud["cursem"]:
+        menu_module = ""
+    else:
+        formsemestre_id = etud["cursem"]["formsemestre_id"]
+        nt = context.Notes._getNotesCache().get_NotesTable(
+            context.Notes, formsemestre_id
+        )
+        ues = nt.get_ues(etudid=etudid)
+        menu_module = """<p><select name="moduleimpl_id">
+        <option value="NULL" selected>(Module)</option>"""
+        for ue in ues:
+            modimpls = nt.get_modimpls(ue_id=ue["ue_id"])
+            for modimpl in modimpls:
+                menu_module += (
+                    """<option value="%(modimpl_id)s">%(modname)s</option>\n"""
+                    % {
+                        "modimpl_id": modimpl["moduleimpl_id"],
+                        "modname": modimpl["module"]["code"],
+                    }
+                )
+        menu_module += """</select></p>"""
+
+    H = [
+        context.sco_header(
+            REQUEST, page_title="Signalement d'une absence pour %(nomprenom)s" % etud
+        ),
+        """<table><tr><td>
+          <h2>Signalement d'une absence pour %(nomprenom)s</h2>
+          </td><td>
+          """
+        % etud,
+        """<a href="%s/ficheEtud?etudid=%s">""" % (context.ScoURL(), etud["etudid"]),
+        sco_photos.etud_photo_html(
+            context, etudid=etudid, title="fiche de " + etud["nomprenom"], REQUEST=REQUEST
+        ),
+        """</a></td></tr></table>""",
+        """
+<form action="doSignaleAbsence" method="get">
+<input type="hidden" name="etudid" value="%(etudid)s">
+<p>
+<table><tr>
+<td>Date d&eacute;but :  </td>
+<td><input type="text" name="datedebut" size="10" class="datepicker"/> <em>j/m/a</em></td>
+<td>&nbsp;&nbsp;&nbsp;Date Fin (optionnel):</td>
+<td><input type="text" name="datefin" size="10" class="datepicker"/> <em>j/m/a</em></td>
+</tr>
+</table>
+<br/>
+<input type="radio" name="demijournee" value="2" checked>journ&eacute;e(s)
+&nbsp;<input type="radio" name="demijournee" value="1">Matin(s)
+&nbsp;<input type="radio" name="demijournee" value="0">Apr&egrave;s midi
+
+%(menu_module)s
+
+<p>
+<input type="checkbox" name="estjust"/>Absence justifi&eacute;e.
+<br/>
+Raison: <input type="text" name="description" size="42"/> (optionnel)
+</p>
+
+<p>
+<input type="submit" value="Envoyer"/> 
+<em>
+ <p>Seuls les modules du semestre en cours apparaissent.</p><p> Evitez de saisir une absence pour un module qui n'est pas en place à cette date.</p>
+<p>Toutes les dates sont au format jour/mois/annee</p>
+</em>
+
+</form> 
+          """
+        % {"etudid": etud["etudid"], "menu_module": menu_module},
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def doJustifAbsence(
+    context, datedebut, datefin, demijournee, description=None, REQUEST=None
+):  # etudid implied
+    """Justification d'une absence
+    """
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    etudid = etud["etudid"]
+    description_abs = description
+    dates = context.DateRangeISO(datedebut, datefin)
+    nbadded = 0
+    for jour in dates:
+        if demijournee == "2":
+            context._AddJustif(
+                etudid=etudid,
+                jour=jour,
+                matin=False,
+                REQUEST=REQUEST,
+                description=description_abs,
+            )
+            context._AddJustif(
+                etudid=etudid,
+                jour=jour,
+                matin=True,
+                REQUEST=REQUEST,
+                description=description_abs,
+            )
+            nbadded += 2
+        else:
+            matin = int(demijournee)
+            context._AddJustif(
+                etudid=etudid,
+                jour=jour,
+                matin=matin,
+                REQUEST=REQUEST,
+                description=description_abs,
+            )
+            nbadded += 1
+    #
+    H = [
+        context.sco_header(
+            REQUEST, page_title="Justification d'une absence pour %(nomprenom)s" % etud
+        ),
+        """<h2>Justification d'absences</h2>""",
+    ]
+    if dates:
+        H.append(
+            """<p>Ajout de %d <b>justifications</b> du %s au %s</p>"""
+            % (nbadded, datedebut, datefin)
+        )
+    else:
+        H.append(
+            """<p class="warning">Aucune date ouvrable entre le %s et le %s !</p>"""
+            % (datedebut, datefin)
+        )
+
+    H.append(
+        """<ul><li><a href="JustifAbsenceEtud?etudid=%(etudid)s">Autre justification pour <b>%(nomprenom)s</b></a></li>
+<li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Signaler une absence</a></li>
+<li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses absences</a></li>
+<li><a href="ListeAbsEtud?etudid=%(etudid)s">Liste de ses absences</a></li>
+</ul>
+<hr>"""
+        % etud
+    )
+    H.append(sco_find_etud.form_search_etud(context, REQUEST))
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def JustifAbsenceEtud(context, REQUEST=None):  # etudid implied
+    """Formulaire individuel simple de justification d'une absence
+    """
+    # brute-force portage from very old dtml code ...
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    etudid = etud["etudid"]
+    H = [
+        context.sco_header(
+            REQUEST, page_title="Justification d'une absence pour %(nomprenom)s" % etud
+        ),
+        """<table><tr><td>
+          <h2>Justification d'une absence pour %(nomprenom)s</h2>
+          </td><td>
+          """
+        % etud,
+        """<a href="%s/ficheEtud?etudid=%s">""" % (context.ScoURL(), etud["etudid"]),
+        sco_photos.etud_photo_html(
+            context, etudid=etudid, title="fiche de " + etud["nomprenom"], REQUEST=REQUEST
+        ),
+        """</a></td></tr></table>""",
+        """
+<form action="doJustifAbsence" method="get"> 
+<input type="hidden" name="etudid" value="%(etudid)s">
+
+<p>
+<table><tr>
+<td>Date d&eacute;but :  </td>
+<td>
+<input type="text" name="datedebut" size="10" class="datepicker"/>
+</td>
+<td>&nbsp;&nbsp;&nbsp;Date Fin (optionnel):</td>
+<td><input type="text" name="datefin" size="10" class="datepicker"/></td>
+</tr>
+</table>
+<br/>
+
+<input type="radio" name="demijournee" value="2" checked>journ&eacute;e(s)
+&nbsp;<input type="radio" name="demijournee" value="1">Matin(s)
+&nbsp;<input type="radio" name="demijournee" value="0">Apr&egrave;s midi
+
+<br/><br/>
+Raison: <input type="text" name="description" size="42"/> (optionnel)
+
+<p>
+<input type="submit" value="Envoyer"> 
+
+</form> """
+        % etud,
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def doAnnuleAbsence(
+    context, datedebut, datefin, demijournee, REQUEST=None
+):  # etudid implied
+    """Annulation des absences pour une demi journée
+    """
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    etudid = etud["etudid"]
+
+    dates = context.DateRangeISO(datedebut, datefin)
+    nbadded = 0
+    for jour in dates:
+        if demijournee == "2":
+            context._AnnuleAbsence(etudid, jour, False, REQUEST=REQUEST)
+            context._AnnuleAbsence(etudid, jour, True, REQUEST=REQUEST)
+            nbadded += 2
+        else:
+            matin = int(demijournee)
+            context._AnnuleAbsence(etudid, jour, matin, REQUEST=REQUEST)
+            nbadded += 1
+    #
+    H = [
+        context.sco_header(
+            REQUEST, page_title="Annulation d'une absence pour %(nomprenom)s" % etud
+        ),
+        """<h2>Annulation d'absences pour %(nomprenom)s</h2>""" % etud,
+    ]
+    if dates:
+        H.append(
+            "<p>Annulation sur %d demi-journées du %s au %s"
+            % (nbadded, datedebut, datefin)
+        )
+    else:
+        H.append(
+            """<p class="warning">Aucune date ouvrable entre le %s et le %s !</p>"""
+            % (datedebut, datefin)
+        )
+
+    H.append(
+        """<ul><li><a href="AnnuleAbsenceEtud?etudid=%(etudid)s">Annulation d'une
+autre absence pour <b>%(nomprenom)s</b></a></li>
+                    <li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Ajout d'une absence</a></li>
+                    <li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses absences</a></li>
+                </ul>
+              <hr>"""
+        % etud
+    )
+    H.append(sco_find_etud.form_search_etud(context, REQUEST))
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def AnnuleAbsenceEtud(context, REQUEST=None):  # etudid implied
+    """Formulaire individuel simple d'annulation d'une absence
+    """
+    # brute-force portage from very old dtml code ...
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    etudid = etud["etudid"]
+
+    H = [
+        context.sco_header(
+            REQUEST, page_title="Annulation d'une absence pour %(nomprenom)s" % etud
+        ),
+        """<table><tr><td>
+          <h2><font color="#FF0000">Annulation</font> d'une absence pour %(nomprenom)s</h2>
+          </td><td>
+          """
+        % etud,  #  "
+        """<a href="%s/ficheEtud?etudid=%s">""" % (context.ScoURL(), etud["etudid"]),
+        sco_photos.etud_photo_html(
+            context, etudid=etudid, title="fiche de " + etud["nomprenom"], REQUEST=REQUEST
+        ),
+        """</a></td></tr></table>""",
+        """<p>A n'utiliser que suite à une erreur de saisie ou lorsqu'il s'avère que l'étudiant était en fait présent. </p>
+          <p><font color="#FF0000">Si plusieurs modules sont affectés, les absences seront toutes effacées. </font></p>
+          """
+        % etud,
+        """<table frame="border" border="1"><tr><td>
+<form action="doAnnuleAbsence" method="get"> 
+<input type="hidden" name="etudid" value="%(etudid)s">
+<p>
+<table><tr>
+<td>Date d&eacute;but :  </td>
+<td>
+<input type="text" name="datedebut" size="10" class="datepicker"/> <em>j/m/a</em>
+</td>
+<td>&nbsp;&nbsp;&nbsp;Date Fin (optionnel):</td>
+<td>
+<input type="text" name="datefin" size="10" class="datepicker"/> <em>j/m/a</em>
+</td>
+</tr>
+</table>
+
+<input type="radio" name="demijournee" value="2" checked>journ&eacute;e(s)
+&nbsp;<input type="radio" name="demijournee" value="1">Matin(s)
+&nbsp;<input type="radio" name="demijournee" value="0">Apr&egrave;s midi
+
+
+<p>
+<input type="submit" value="Supprimer les absences"> 
+</form> 
+</td></tr>
+
+<tr><td>
+<form action="doAnnuleJustif" method="get"> 
+<input type="hidden" name="etudid" value="%(etudid)s">
+<p>
+<table><tr>
+<td>Date d&eacute;but :  </td>
+<td>
+<input type="text" name="datedebut0" size="10" class="datepicker"/> <em>j/m/a</em>
+</td>
+<td>&nbsp;&nbsp;&nbsp;Date Fin (optionnel):</td>
+<td>
+<input type="text" name="datefin0" size="10" class="datepicker"/> <em>j/m/a</em>
+</td>
+</tr>
+</table>
+<p>
+
+<input type="radio" name="demijournee" value="2" checked>journ&eacute;e(s)
+&nbsp;<input type="radio" name="demijournee" value="1">Matin(s)
+&nbsp;<input type="radio" name="demijournee" value="0">Apr&egrave;s midi
+
+
+<p>
+<input type="submit" value="Supprimer les justificatifs"> 
+<i>(utiliser ceci en cas de justificatif erron&eacute; saisi ind&eacute;pendemment d'une absence)</i>
+</form> 
+</td></tr></table>"""
+        % etud,
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def doAnnuleJustif(
+    context, datedebut0, datefin0, demijournee, REQUEST=None
+):  # etudid implied
+    """Annulation d'une justification 
+    """
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    etudid = etud["etudid"]
+    dates = context.DateRangeISO(datedebut0, datefin0)
+    nbadded = 0
+    for jour in dates:
+        # Attention: supprime matin et après midi
+        if demijournee == "2":
+            context._AnnuleJustif(etudid, jour, False, REQUEST=REQUEST)
+            context._AnnuleJustif(etudid, jour, True, REQUEST=REQUEST)
+            nbadded += 2
+        else:
+            matin = int(demijournee)
+            context._AnnuleJustif(etudid, jour, matin, REQUEST=REQUEST)
+            nbadded += 1
+    #
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Annulation d'une justification pour %(nomprenom)s" % etud,
+        ),
+        """<h2>Annulation de justifications pour %(nomprenom)s</h2>""" % etud,
+    ]
+
+    if dates:
+        H.append(
+            "<p>Annulation sur %d demi-journées du %s au %s"
+            % (nbadded, datedebut0, datefin0)
+        )
+    else:
+        H.append(
+            """<p class="warning">Aucune date ouvrable entre le %s et le %s !</p>"""
+            % (datedebut0, datefin0)
+        )
+    H.append(
+        """<ul><li><a href="AnnuleAbsenceEtud?etudid=%(etudid)s">Annulation d'une
+autre absence pour <b>%(nomprenom)s</b></a></li>
+                    <li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Ajout d'une absence</a></li>
+                    <li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses absences</a></li>
+                </ul>
+              <hr>"""
+        % etud
+    )
+    H.append(sco_find_etud.form_search_etud(context, REQUEST))
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def EtatAbsences(context, REQUEST=None):
+    """Etat des absences: choix du groupe"""
+    # crude portage from 1999 DTML
+    H = [
+        context.sco_header(REQUEST, page_title="Etat des absences"),
+        """<h2>Etat des absences pour un groupe</h2>
+<form action="EtatAbsencesGr" method="GET">""",
+        formChoixSemestreGroupe(context),
+        """<input type="submit" name="" value=" OK " width=100>
+
+<table><tr><td>Date de début (j/m/a) : </td><td>
+
+<input type="text" name="debut" size="10" value="01/09/%s" class="datepicker"/>
+
+</td></tr><tr><td>Date de fin : </td><td>
+
+<input type="text" name="fin" size="10" value="%s" class="datepicker"/>
+
+</td></tr></table>
+</form>"""
+        % (AnneeScolaire(REQUEST), datetime.datetime.now().strftime("%d/%m/%Y")),
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def formChoixSemestreGroupe(context, all=False):
+    """partie de formulaire pour le choix d'un semestre et d'un groupe.
+    Si all, donne tous les semestres (même ceux verrouillés).
+    """
+    # XXX assez primitif, à ameliorer TOTALEMENT OBSOLETE !
+    if all:
+        sems = sco_formsemestre.do_formsemestre_list(context)
+    else:
+        sems = sco_formsemestre.do_formsemestre_list(context, args={"etat": "1"})
+    if not sems:
+        raise ScoValueError("aucun semestre !")
+    H = ['<select  name="group_ids">']
+    nbgroups = 0
+    for sem in sems:
+        for p in sco_groups.get_partitions_list(context, sem["formsemestre_id"]):
+            for group in sco_groups.get_partition_groups(context, p):
+                if group["group_name"]:
+                    group_tit = "%s %s" % (p["partition_name"], group["group_name"])
+                else:
+                    group_tit = "tous"
+                H.append(
+                    '<option value="%s">%s: %s</option>'
+                    % (group["group_id"], sem["titremois"], group_tit)
+                )
+
+    H.append("</select>")
+    return "\n".join(H)
+
+
+def CalAbs(context, REQUEST=None):  # etud implied
+    """Calendrier des absences d un etudiant
+    """
+    # crude portage from 1999 DTML
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    etudid = etud["etudid"]
+    anneescolaire = int(AnneeScolaire(REQUEST))
+    datedebut = str(anneescolaire) + "-08-31"
+    datefin = str(anneescolaire + 1) + "-07-31"
+    nbabs = context.CountAbs(etudid=etudid, debut=datedebut, fin=datefin)
+    nbabsjust = context.CountAbsJust(etudid=etudid, debut=datedebut, fin=datefin)
+    events = []
+    for a in context.ListeAbsJust(etudid=etudid, datedebut=datedebut):
+        events.append(
+            (str(a["jour"]), "a", "#F8B7B0", "", a["matin"], a["description"])
+        )
+    for a in context.ListeAbsNonJust(etudid=etudid, datedebut=datedebut):
+        events.append(
+            (str(a["jour"]), "A", "#EE0000", "", a["matin"], a["description"])
+        )
+    justifs_noabs = context.ListeJustifs(
+        etudid=etudid, datedebut=datedebut, only_no_abs=True
+    )
+    for a in justifs_noabs:
+        events.append(
+            (str(a["jour"]), "X", "#8EA2C6", "", a["matin"], a["description"])
+        )
+    CalHTML = ZAbsences.YearTable(context, anneescolaire, events=events, halfday=1)
+
+    #
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Calendrier des absences de %(nomprenom)s" % etud,
+            cssstyles=["css/calabs.css"],
+        ),
+        """<table><tr><td><h2>Absences de %(nomprenom)s (%(inscription)s)</h2><p>"""
+        % etud,
+        """<b><font color="#EE0000">A : absence NON justifiée</font><br/>
+             <font color="#F8B7B0">a : absence justifiée</font><br/>
+             <font color="#8EA2C6">X : justification sans absence</font><br/>
+             %d absences sur l'année, dont %d justifiées (soit %d non justifiées)</b> <em>(%d justificatifs inutilisés)</em>
+          </p>
+           """
+        % (nbabs, nbabsjust, nbabs - nbabsjust, len(justifs_noabs)),
+        """</td>
+<td><a href="%s/ficheEtud?etudid=%s">%s</a></td>
+</tr>
+</table>"""
+        % (
+            context.ScoURL(),
+            etudid,
+            sco_photos.etud_photo_html(
+                context, etudid=etudid, title="fiche de " + etud["nomprenom"], REQUEST=REQUEST
+            ),
+        ),
+        CalHTML,
+        """<form method="GET" action="CalAbs" name="f">""",
+        """<input type="hidden" name="etudid" value="%s"/>""" % etudid,
+        """Année scolaire %s-%s""" % (anneescolaire, anneescolaire + 1),
+        """&nbsp;&nbsp;Changer année: <select name="sco_year" onchange="document.f.submit()">""",
+    ]
+    for y in range(anneescolaire, anneescolaire - 10, -1):
+        H.append("""<option value="%s" """ % y)
+        if y == anneescolaire:
+            H.append("selected")
+        H.append(""">%s</option>""" % y)
+    H.append("""</select></form>""")
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def ListeAbsEtud(
+    context,
+    etudid,
+    with_evals=True,  # indique les evaluations aux dates d'absences
+    format="html",
+    absjust_only=0,  # si vrai, renvoie table absences justifiées
+    REQUEST=None,
+):
+    """Liste des absences d'un étudiant sur l'année en cours
+    En format 'html': page avec deux tableaux (non justifiées et justifiées).
+    En format xls ou pdf: l'un ou l'autre des table, suivant absjust_only.
+    En format 'text': texte avec liste d'absences (pour mails).
+    """
+    absjust_only = int(absjust_only)  # si vrai, table absjust seule (export xls ou pdf)
+    datedebut = "%s-08-31" % AnneeScolaire(REQUEST)
+
+    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+
+    # Liste des absences et titres colonnes tables:
+    titles, columns_ids, absnonjust, absjust = context.Absences._TablesAbsEtud(
+        etudid, datedebut, with_evals=with_evals, format=format
+    )
+
+    if REQUEST:
+        base_url_nj = "%s?etudid=%s&amp;absjust_only=0" % (REQUEST.URL0, etudid)
+        base_url_j = "%s?etudid=%s&amp;absjust_only=1" % (REQUEST.URL0, etudid)
+    else:
+        base_url_nj = base_url_j = ""
+    tab_absnonjust = GenTable(
+        titles=titles,
+        columns_ids=columns_ids,
+        rows=absnonjust,
+        html_class="table_leftalign",
+        table_id="tab_absnonjust",
+        base_url=base_url_nj,
+        filename="abs_" + make_filename(etud["nomprenom"]),
+        caption="Absences non justifiées de %(nomprenom)s" % etud,
+        preferences=context.get_preferences(),
+    )
+    tab_absjust = GenTable(
+        titles=titles,
+        columns_ids=columns_ids,
+        rows=absjust,
+        html_class="table_leftalign",
+        table_id="tab_absjust",
+        base_url=base_url_j,
+        filename="absjust_" + make_filename(etud["nomprenom"]),
+        caption="Absences justifiées de %(nomprenom)s" % etud,
+        preferences=context.get_preferences(),
+    )
+
+    # Formats non HTML et demande d'une seule table:
+    if format != "html" and format != "text":
+        if absjust_only == 1:
+            return tab_absjust.make_page(context, format=format, REQUEST=REQUEST)
+        else:
+            return tab_absnonjust.make_page(context, format=format, REQUEST=REQUEST)
+
+    if format == "html":
+        # Mise en forme HTML:
+        H = []
+        H.append(
+            context.sco_header(REQUEST, page_title="Absences de %s" % etud["nomprenom"])
+        )
+        H.append(
+            """<h2>Absences de %s (à partir du %s)</h2>"""
+            % (etud["nomprenom"], DateISOtoDMY(datedebut))
+        )
+
+        if len(absnonjust):
+            H.append("<h3>%d absences non justifiées:</h3>" % len(absnonjust))
+            H.append(tab_absnonjust.html())
+        else:
+            H.append("""<h3>Pas d'absences non justifiées</h3>""")
+
+        if len(absjust):
+            H.append("""<h3>%d absences justifiées:</h3>""" % len(absjust))
+            H.append(tab_absjust.html())
+        else:
+            H.append("""<h3>Pas d'absences justifiées</h3>""")
+        return "\n".join(H) + context.sco_footer(REQUEST)
+
+    elif format == "text":
+        T = []
+        if not len(absnonjust) and not len(absjust):
+            T.append(
+                """--- Pas d'absences enregistrées depuis le %s"""
+                % DateISOtoDMY(datedebut)
+            )
+        else:
+            T.append(
+                """--- Absences enregistrées à partir du %s:"""
+                % DateISOtoDMY(datedebut)
+            )
+            T.append("\n")
+        if len(absnonjust):
+            T.append("* %d absences non justifiées:" % len(absnonjust))
+            T.append(tab_absnonjust.text())
+        if len(absjust):
+            T.append("* %d absences justifiées:" % len(absjust))
+            T.append(tab_absjust.text())
+        return "\n".join(T)
+    else:
+        raise ValueError("Invalid format !")
+
+
+def absences_index_html(context, REQUEST=None):
+    """Gestionnaire absences, page principale"""
+    # crude portage from 1999 DTML
+    sems = sco_formsemestre.do_formsemestre_list(context)
+    authuser = REQUEST.AUTHENTICATED_USER
+
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Gestion des absences",
+            cssstyles=["css/calabs.css"],
+            javascripts=["js/calabs.js"],
+        ),
+        """<h2>Gestion des Absences</h2>""",
+    ]
+    if not sems:
+        H.append(
+            """<p class="warning">Aucun semestre défini (ou aucun groupe d'étudiant)</p>"""
+        )
+    else:
+        H.append(
+            """<ul><li><a href="EtatAbsences">Afficher l'état des absences (pour tout un groupe)</a></li>"""
+        )
+        if context.get_preference("handle_billets_abs"):
+            H.append(
+                """<li><a href="listeBillets">Traitement des billets d'absence en attente</a></li>"""
+            )
+        H.append(
+            """<p>Pour signaler, annuler ou justifier une absence, choisissez d'abord l'étudiant concerné:</p>"""
+        )
+        H.append(sco_find_etud.form_search_etud(context, REQUEST))
+        if authuser.has_permission(ScoAbsChange, context):
+            H.extend(
+                (
+                    """<hr/>
+<form action="SignaleAbsenceGrHebdo" id="formw">
+<input type="hidden" name="destination" value="%s"/>
+<p>
+<span  style="font-weight: bold; font-size:120%%;">
+ Saisie par semaine </span> - Choix du groupe:
+ <input name="datelundi" type="hidden" value="x"/>
+            """
+                    % REQUEST.URL0,
+                    formChoixSemestreGroupe(context),
+                    "</p>",
+                    context.CalSelectWeek(REQUEST=REQUEST),
+                    """<p class="help">Sélectionner le groupe d'étudiants, puis cliquez sur une semaine pour
+saisir les absences de toute cette semaine.</p>
+                      </form>""",
+                )
+            )
+        else:
+            H.append(
+                """<p class="scoinfo">Vous n'avez pas l'autorisation d'ajouter, justifier ou supprimer des absences.</p>"""
+            )
+
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
diff --git a/sco_apogee_compare.py b/sco_apogee_compare.py
new file mode 100644
index 0000000000000000000000000000000000000000..2277bfba8a98484580d98a3dd869e511350fbd69
--- /dev/null
+++ b/sco_apogee_compare.py
@@ -0,0 +1,345 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Comparaison de deux fichiers Apogée (maquettes)
+
+1) Vérifier:
+etape_apogee, vdi_apogee, cod_dip_apogee, annee_scolaire
+structure: col_ids (la comparaison portera sur les colonnes communes)
+
+
+2) Comparer listes d'étudiants
+ Présents dans A mais pas dans B
+ Présents dans B mais pas dans A
+ nombre communs
+
+3) Comparer résultats
+Pour chaque étudiant commun:
+ Pour chaque colonne commune:
+    comparer les résultats
+
+"""
+
+from collections import OrderedDict
+
+from sco_utils import *
+from notes_log import log
+import sco_apogee_csv
+from gen_tables import GenTable
+
+_help_txt = """
+<div class="help">
+<p>Outil de comparaison de fichiers (maquettes CSV) Apogée.
+</p>
+<p>Cet outil compare deux fichiers fournis. Aucune donnée stockée dans ScoDoc n'est utilisée.
+</p>
+</div>
+"""
+
+
+def apo_compare_csv_form(context, REQUEST=None):
+    """Form: submit 2 CSV files to compare them.
+    """
+    H = [
+        context.sco_header(REQUEST, page_title="Comparaison de fichiers Apogée"),
+        """<h2>Comparaison de fichiers Apogée</h2>
+        <form id="apo_csv_add" action="apo_compare_csv" method="post" enctype="multipart/form-data">
+        """,
+        _help_txt,
+        """
+        <div class="apo_compare_csv_form_but">
+        Fichier Apogée A: 
+        <input type="file" size="30" name="A_file"/>
+        </div>
+        <div class="apo_compare_csv_form_but">
+        Fichier Apogée B: 
+        <input type="file" size="30" name="B_file"/>
+        </div>
+        <input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
+        <div class="apo_compare_csv_form_submit">
+        <input type="submit" value="Comparer ces fichiers"/>
+        </div>
+        </form>""",
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def apo_compare_csv(context, A_file, B_file, autodetect=True, REQUEST=None):
+    """Page comparing 2 Apogee CSV files
+    """
+    A = _load_apo_data(A_file, autodetect=autodetect)
+    B = _load_apo_data(B_file, autodetect=autodetect)
+
+    H = [
+        context.sco_header(REQUEST, page_title="Comparaison de fichiers Apogée"),
+        "<h2>Comparaison de fichiers Apogée</h2>",
+        _help_txt,
+        '<div class="apo_compare_csv">',
+        _apo_compare_csv(context, A, B, REQUEST=None),
+        "</div>",
+        """<p><a href="apo_compare_csv_form" class="stdlink">Autre comparaison</a></p>""",
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def _load_apo_data(csvfile, autodetect=True):
+    "Read data from request variable and build ApoData"
+    data = csvfile.read()
+    if autodetect:
+        data, message = sco_apogee_csv.fix_data_encoding(data)
+        if message:
+            log("apo_compare_csv: %s" % message)
+        if not data:
+            raise ScoValueError("apo_compare_csv: no data")
+    apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
+    return apo_data
+
+
+def _apo_compare_csv(context, A, B, REQUEST=None):
+    """Generate html report comparing A and B, two instances of ApoData
+    representing Apogee CSV maquettes.
+    """
+    L = []
+    # 1-- Check etape and codes
+    L.append('<div class="section"><div class="tit">En-tête</div>')
+    L.append('<div><span class="key">Nom fichier A:</span><span class="val_ok">')
+    L.append(A.orig_filename)
+    L.append("</span></div>")
+    L.append('<div><span class="key">Nom fichier B:</span><span class="val_ok">')
+    L.append(B.orig_filename)
+    L.append("</span></div>")
+    L.append('<div><span class="key">Étape Apogée:</span>')
+    if A.etape_apogee != B.etape_apogee:
+        L.append(
+            '<span class="val_dif">%s != %s</span>' % (A.etape_apogee, B.etape_apogee)
+        )
+    else:
+        L.append('<span class="val_ok">%s</span>' % (A.etape_apogee,))
+    L.append("</div>")
+
+    L.append('<div><span class="key">VDI Apogée:</span>')
+    if A.vdi_apogee != B.vdi_apogee:
+        L.append('<span class="val_dif">%s != %s</span>' % (A.vdi_apogee, B.vdi_apogee))
+    else:
+        L.append('<span class="val_ok">%s</span>' % (A.vdi_apogee,))
+    L.append("</div>")
+
+    L.append('<div><span class="key">Code diplôme :</span>')
+    if A.cod_dip_apogee != B.cod_dip_apogee:
+        L.append(
+            '<span class="val_dif">%s != %s</span>'
+            % (A.cod_dip_apogee, B.cod_dip_apogee)
+        )
+    else:
+        L.append('<span class="val_ok">%s</span>' % (A.cod_dip_apogee,))
+    L.append("</div>")
+
+    L.append('<div><span class="key">Année scolaire :</span>')
+    if A.annee_scolaire != B.annee_scolaire:
+        L.append(
+            '<span class="val_dif">%s != %s</span>'
+            % (A.annee_scolaire, B.annee_scolaire)
+        )
+    else:
+        L.append('<span class="val_ok">%s</span>' % (A.annee_scolaire,))
+    L.append("</div>")
+
+    # Colonnes:
+    A_elts = set(A.apo_elts.keys())
+    B_elts = set(B.apo_elts.keys())
+    L.append('<div><span class="key">Éléments Apogée :</span>')
+    if A_elts == B_elts:
+        L.append('<span class="val_ok">%d</span>' % len(A_elts))
+    else:
+        elts_communs = A_elts.intersection(B_elts)
+        elts_only_A = A_elts - A_elts.intersection(B_elts)
+        elts_only_B = B_elts - A_elts.intersection(B_elts)
+        L.append(
+            '<span class="val_dif">différents (%d en commun, %d seulement dans A, %d seulement dans B)</span>'
+            % (len(elts_communs), len(elts_only_A), len(elts_only_B),)
+        )
+        if elts_only_A:
+            L.append(
+                '<div span class="key">Éléments seulement dans A : </span><span class="val_dif">%s</span></div>'
+                % ", ".join(sorted(elts_only_A))
+            )
+        if elts_only_B:
+            L.append(
+                '<div span class="key">Éléments seulement dans B : </span><span class="val_dif">%s</span></div>'
+                % ", ".join(sorted(elts_only_B))
+            )
+    L.append("</div>")
+    L.append("</div>")  # /section
+
+    # 2--
+    L.append('<div class="section"><div class="tit">Étudiants</div>')
+
+    A_nips = set(A.etud_by_nip)
+    B_nips = set(B.etud_by_nip)
+    nb_etuds_communs = len(A_nips.intersection(B_nips))
+    nb_etuds_dif = len(A_nips.union(B_nips) - A_nips.intersection(B_nips))
+    L.append("""<div><span class="key">Liste d'étudiants :</span>""")
+    if A_nips == B_nips:
+        L.append(
+            """<span class="s_ok">
+        %d étudiants (tous présents dans chaque fichier)</span>
+        """
+            % len(A_nips)
+        )
+    else:
+        L.append(
+            '<span class="val_dif">différents (%d en commun, %d différents)</span>'
+            % (nb_etuds_communs, nb_etuds_dif)
+        )
+    L.append("</div>")
+    L.append("</div>")  # /section
+
+    # 3-- Résultats de chaque étudiant:
+    if nb_etuds_communs > 0:
+        L.append(
+            """<div class="section sec_table">
+        <div class="tit">Différences de résultats des étudiants présents dans les deux fichiers</div>
+        <p>
+        """
+        )
+        T = apo_table_compare_etud_results(context, A, B, REQUEST=REQUEST)
+        if T.get_nb_rows() > 0:
+            L.append(T.html())
+        else:
+            L.append(
+                """<p class="p_ok">aucune différence de résultats 
+            sur les %d étudiants communs (<em>les éléments Apogée n'apparaissant pas dans les deux fichiers sont omis</em>)</p>
+            """
+                % nb_etuds_communs
+            )
+        L.append("</div>")  # /section
+
+    return "\n".join(L)
+
+
+def apo_table_compare_etud_results(context, A, B, REQUEST=None):
+    """
+    """
+    D = compare_etuds_res(A, B)
+    T = GenTable(
+        rows=D,
+        titles={
+            "nip": "NIP",
+            "nom": "Nom",
+            "prenom": "Prénom",
+            "elt_code": "Element",
+            "type_res": "Type",
+            "val_A": "A: %s" % A.orig_filename or "",
+            "val_B": "B: %s" % B.orig_filename or "",
+        },
+        columns_ids=("nip", "nom", "prenom", "elt_code", "type_res", "val_A", "val_B"),
+        html_class="table_leftalign",
+        html_with_td_classes=True,
+        preferences=context.get_preferences(),
+    )
+    return T
+
+
+def _build_etud_res(e, apo_data):
+    r = {}
+    for elt_code in apo_data.apo_elts:
+        elt = apo_data.apo_elts[elt_code]
+        col_ids_type = [
+            (ec["apoL_a01_code"], ec["Type R\xc3\xa9s."]) for ec in elt.cols
+        ]  # les colonnes de cet élément
+        r[elt_code] = {}
+        for (col_id, type_res) in col_ids_type:
+            r[elt_code][type_res] = e.cols[col_id]
+    return r
+
+
+def compare_etud_res(r_A, r_B, remove_missing=True):
+    """Pour chaque valeur difference dans les resultats d'un etudiant
+    elt_code   type_res   val_A     val_B
+    """
+    diffs = []
+    elt_codes = set(r_A).union(set(r_B))
+    for elt_code in elt_codes:
+        for type_res in r_A.get(elt_code, r_B.get(elt_code)):
+            if elt_code not in r_A:
+                if remove_missing:
+                    continue
+                else:
+                    val_A = None  # element absent
+            else:
+                val_A = r_A[elt_code][type_res]
+            if elt_code not in r_B:
+                if remove_missing:
+                    continue
+                else:
+                    val_B = None  # element absent
+            else:
+                val_B = r_B[elt_code][type_res]
+            if type_res == "N":
+                # Cas particulier pour les notes: compare les nombres
+                try:
+                    val_A_num = float(val_A.replace(",", "."))
+                    val_B_num = float(val_B.replace(",", "."))
+                except ValueError:
+                    val_A_num, val_B_num = val_A, val_B
+                val_A, val_B = val_A_num, val_B_num
+            if val_A != val_B:
+                diffs.append(
+                    {
+                        "elt_code": elt_code,
+                        "type_res": type_res,
+                        "val_A": val_A,
+                        "val_B": val_B,
+                    }
+                )
+    return diffs
+
+
+def compare_etuds_res(A, B):
+    """
+    nip, nom, prenom, elt_code, type_res, val_A, val_B
+    """
+    A_nips = set(A.etud_by_nip)
+    B_nips = set(B.etud_by_nip)
+    common_nips = A_nips.intersection(B_nips)
+    # A_not_B_nips = A_nips - B_nips
+    # B_not_A_nips = B_nips - A_nips
+    D = []
+    for nip in common_nips:
+        etu_A = A.etud_by_nip[nip]
+        etu_B = B.etud_by_nip[nip]
+        r_A = _build_etud_res(etu_A, A)
+        r_B = _build_etud_res(etu_B, B)
+        diffs = compare_etud_res(r_A, r_B)
+        for d in diffs:
+            d.update(
+                {"nip": etu_A["nip"], "nom": etu_A["nom"], "prenom": etu_A["prenom"]}
+            )
+            D.append(d)
+    return D
diff --git a/sco_apogee_csv.py b/sco_apogee_csv.py
new file mode 100644
index 0000000000000000000000000000000000000000..18d5d9f804a6361b8b2246a836f064e91f00023b
--- /dev/null
+++ b/sco_apogee_csv.py
@@ -0,0 +1,1311 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Exportation des résultats des étudiants vers Apogée.
+
+EXPERIMENTAL / PRECAUTIONS !
+
+Code inspiré par les travaux de Damien Mascré, scodoc2apogee (en Java).
+
+A utiliser en fin de semestre, après les jury.
+
+On communique avec Apogée via des fichiers CSV.
+
+Le fichier CSV, champs séparés par des tabulations, a la structure suivante:
+
+ <pre>
+ XX-APO_TITRES-XX
+ apoC_annee	2007/2008
+ apoC_cod_dip	VDTCJ
+ apoC_Cod_Exp	1
+ apoC_cod_vdi	111
+ apoC_Fichier_Exp	VDTCJ_V1CJ.txt
+ apoC_lib_dip	DUT CJ
+ apoC_Titre1	Export Apogée du 13/06/2008 à 14:29
+ apoC_Titre2
+
+ XX-APO_COLONNES-XX
+ apoL_a01_code	Type Objet	Code	Version	Année	Session	Admission/Admissibilité	Type Rés.			Etudiant	Numéro
+ apoL_a02_nom										1	Nom
+ apoL_a03_prenom										1	Prénom
+ apoL_a04_naissance									Session	Admissibilité	Naissance
+ APO_COL_VAL_DEB
+ apoL_c0001	VET	V1CJ	111	2007	0	1	N	V1CJ - DUT CJ an1	0	1	Note
+ apoL_c0002	VET	V1CJ	111	2007	0	1	B		0	1	Barème
+ apoL_c0003	VET	V1CJ	111	2007	0	1	R		0	1	Résultat
+ APO_COL_VAL_FIN
+ apoL_c0030	APO_COL_VAL_FIN
+
+ XX-APO_VALEURS-XX
+ apoL_a01_code	apoL_a02_nom	apoL_a03_prenom	apoL_a04_naissance	apoL_c0001	apoL_c0002	apoL_c0003	apoL_c0004	apoL_c0005	apoL_c0006	apoL_c0007	apoL_c0008	apoL_c0009	apoL_c0010	apoL_c0011	apoL_c0012	apoL_c0013	apoL_c0014	apoL_c0015	apoL_c0016	apoL_c0017	apoL_c0018	apoL_c0019	apoL_c0020	apoL_c0021	apoL_c0022	apoL_c0023	apoL_c0024	apoL_c0025	apoL_c0026	apoL_c0027	apoL_c0028	apoL_c0029
+ 10601232	AARIF	MALIKA	 22/09/1986	18	20	ADM	18	20	ADM	18	20	ADM	18	20	ADM	18	20	ADM	18	20	18	20	ADM	18	20	ADM	18	20	ADM	18	20	ADM
+ </pre>
+
+ 
+ On récupère nos éléments pédagogiques dans la section XX-APO-COLONNES-XX et
+ notre liste d'étudiants dans la section XX-APO_VALEURS-XX. Les champs de la
+ section XX-APO_VALEURS-XX sont décrits par les lignes successives de la
+ section XX-APO_COLONNES-XX.
+
+ Le fichier CSV correspond à une étape, qui est récupérée sur la ligne
+ <pre>
+ apoL_c0001	VET	V1CJ ...
+ </pre>
+
+
+XXX A vérifier:
+ AJAC car 1 sem. validé et pas de NAR 
+ 
+"""
+from cStringIO import StringIO
+from zipfile import ZipFile
+import pprint
+
+try:
+    from chardet import detect as chardet_detect
+except:
+    chardet_detect = None
+
+import sco_formsemestre
+from sco_formsemestre import ApoEtapeVDI
+import sco_formsemestre_status
+import sco_parcours_dut
+import sco_codes_parcours
+from sco_codes_parcours import code_semestre_validant
+from sco_codes_parcours import ATT, ATB, ADM, ADC, ADJ, ATJ, ATB, AJ, CMP, NAR, RAT, DEF
+from gen_tables import GenTable
+from notesdb import *
+from sco_utils import *
+
+APO_PORTAL_ENCODING = (
+    "utf8"  # encodage du fichier CSV Apogée (était 'ISO-8859-1' avant jul. 2016)
+)
+APO_INPUT_ENCODING = "ISO-8859-1"  #
+APO_OUTPUT_ENCODING = APO_INPUT_ENCODING  # encodage des fichiers Apogee générés
+APO_DECIMAL_SEP = ","  # separateur décimal: virgule
+APO_SEP = "\t"
+APO_NEWLINE = "\r\n"
+
+
+def code_scodoc_to_apo(code):
+    """Conversion code jury ScoDoc en code Apogée
+    """
+    return {
+        ATT: "AJAC",
+        ATB: "AJAC",
+        ATJ: "AJAC",
+        ADM: "ADM",
+        ADJ: "ADM",
+        ADC: "ADMC",
+        AJ: "AJ",
+        CMP: "COMP",
+        "DEM": "NAR",
+        DEF: "NAR",
+        NAR: "NAR",
+        RAT: "ATT",
+    }.get(code, "DEF")
+
+
+def _apo_fmt_note(note):
+    "Formatte une note pour Apogée (séparateur décimal: ',')"
+    if not note and type(note) != FloatType:
+        return ""
+    try:
+        val = float(note)
+    except ValueError:
+        return ""
+    return ("%3.2f" % val).replace(".", APO_DECIMAL_SEP)
+
+
+def guess_data_encoding(text, threshold=0.6):
+    """Guess string encoding, using chardet heuristics.
+    Returns encoding, or None if detection failed (confidence below threshold,
+    or chardet not installed)
+    """
+    if not chardet_detect:
+        return None  # package not installed
+    r = chardet_detect(text)
+    if r["confidence"] < threshold:
+        return None
+    else:
+        return r["encoding"]
+
+
+def fix_data_encoding(
+    text, default_source_encoding=APO_INPUT_ENCODING, dest_encoding=APO_INPUT_ENCODING
+):
+    """Try to ensure that text is using dest_encoding
+    returns converted text, and a message describing the conversion. 
+    """
+    message = ""
+    detected_encoding = guess_data_encoding(text)
+    if not detected_encoding:
+        if default_source_encoding != dest_encoding:
+            message = "converting from %s to %s" % (
+                default_source_encoding,
+                dest_encoding,
+            )
+            text = text.decode(default_source_encoding).encode(dest_encoding)
+    else:
+        if detected_encoding != dest_encoding:
+            message = "converting from detected %s to %s" % (
+                detected_encoding,
+                dest_encoding,
+            )
+            text = text.decode(detected_encoding).encode(dest_encoding)
+    return text, message
+
+
+class StringIOFileLineWrapper(object):
+    def __init__(self, data):
+        self.f = StringIO(data)
+        self.lineno = 0
+
+    def close(self):
+        return self.f.close()
+
+    def readline(self):
+        self.lineno += 1
+        return self.f.readline()
+
+
+class DictCol(dict):
+    "A dict, where we can add attributes"
+    pass
+
+
+class ApoElt:
+    """Definition d'un Element Apogee
+    sur plusieurs colonnes du fichier CSV
+    """
+
+    def __init__(self, cols):
+        assert len(cols) > 0
+        assert len(set([c["Code"] for c in cols])) == 1  # colonnes de meme code
+        assert len(set([c["Type Objet"] for c in cols])) == 1  # colonnes de meme type
+        self.cols = cols
+        self.code = cols[0]["Code"]
+        self.version = cols[0]["Version"]
+        self.type_objet = cols[0]["Type Objet"]
+
+    def append(self, col):
+        assert col["Code"] == self.code
+        if col["Type Objet"] != self.type_objet:
+            log(
+                "Warning: ApoElt: duplicate id %s (%s and %s)"
+                % (self.code, self.type_objet, col["Type Objet"])
+            )
+            self.type_objet = col["Type Objet"]
+        self.cols.append(col)
+
+    def __repr__(self):
+        return "ApoElt(code='%s', cols=%s)" % (self.code, pprint.pformat(self.cols))
+
+
+class EtuCol:
+    """Valeurs colonnes d'un element pour un etudiant"""
+
+    def __init__(self, nip, apo_elt, init_vals):
+        pass  # XXX
+
+
+ETUD_OK = "ok"
+ETUD_ORPHELIN = "orphelin"
+ETUD_NON_INSCRIT = "non_inscrit"
+
+VOID_APO_RES = dict(N="", B="", J="", R="", M="")
+
+
+class ApoEtud(dict):
+    """Etudiant Apogee: 
+    """
+
+    def __init__(
+        self,
+        nip="",
+        nom="",
+        prenom="",
+        naissance="",
+        cols={},
+        export_res_etape=True,
+        export_res_sem=True,
+        export_res_ues=True,
+        export_res_modules=True,
+        export_res_sdj=True,
+        export_res_rat=True,
+    ):
+        self["nip"] = nip
+        self["nom"] = nom
+        self["prenom"] = prenom
+        self["naissance"] = naissance
+        self.cols = cols  # { col_id : value }  colid = 'apoL_c0001'
+        self.new_cols = {}  # { col_id : value to record in csv }
+        self.etud = None  # etud ScoDoc
+        self.etat = None  # ETUD_OK, ...
+        self.is_NAR = False  # set to True si NARé dans un semestre
+        self.log = []
+        self.has_logged_no_decision = False
+        self.export_res_etape = export_res_etape  # VET, ...
+        self.export_res_sem = export_res_sem  # elt_sem_apo
+        self.export_res_ues = export_res_ues
+        self.export_res_modules = export_res_modules
+        self.export_res_sdj = export_res_sdj  # export meme si pas de decision de jury
+        self.export_res_rat = export_res_rat
+
+    def __repr__(self):
+        return "ApoEtud( nom='%s', nip='%s' )" % (self["nom"], self["nip"])
+
+    def lookup_scodoc(self, context, etape_formsemestre_ids):
+        etuds = context.getEtudInfo(code_nip=self["nip"], filled=True)
+        if not etuds:
+            # pas dans ScoDoc
+            self.etud = None
+            self.log.append("non inscrit dans ScoDoc")
+            self.etat = ETUD_ORPHELIN
+        else:
+            self.etud = etuds[0]
+            # cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape:
+            formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]}
+            self.in_formsemestre_ids = formsemestre_ids.intersection(
+                etape_formsemestre_ids
+            )
+            if not self.in_formsemestre_ids:
+                self.log.append(
+                    "connu dans ScoDoc, mais pas inscrit dans un semestre de cette étape"
+                )
+                self.etat = ETUD_NON_INSCRIT
+            else:
+                self.etat = ETUD_OK
+
+    def associate_sco(self, context, apo_data):
+        """Recherche les valeurs des éléments Apogée pour cet étudiant
+        Set .new_cols
+        """
+        self.col_elts = {}  # {'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}
+        if self.etat is None:
+            self.lookup_scodoc(context, apo_data.etape_formsemestre_ids)
+        if self.etat != ETUD_OK:
+            self.new_cols = (
+                self.cols
+            )  # etudiant inconnu, recopie les valeurs existantes dans Apo
+        else:
+            sco_elts = {}  # valeurs trouvées dans ScoDoc   code : { N, B, J, R }
+            for col_id in apo_data.col_ids[4:]:
+                code = apo_data.cols[col_id]["Code"]  # 'V1RT'
+                el = sco_elts.get(
+                    code, None
+                )  # {'R': ADM, 'J': '', 'B': 20, 'N': '12.14'}
+                if el is None:  # pas déjà trouvé
+                    cur_sem, autre_sem = self.etud_semestres_de_etape(context, apo_data)
+                    for sem in apo_data.sems_etape:
+                        el = self.search_elt_in_sem(
+                            context, code, sem, cur_sem, autre_sem
+                        )
+                        if el != None:
+                            sco_elts[code] = el
+                            break
+                self.col_elts[code] = el
+                if el is None:
+                    self.new_cols[col_id] = self.cols[col_id]
+                else:
+                    try:
+                        self.new_cols[col_id] = sco_elts[code][
+                            apo_data.cols[col_id]["Type Rés."]
+                        ]
+                    except KeyError:
+                        log(
+                            "associate_sco: missing key, etud=%s\ncode='%s'\netape='%s'"
+                            % (self, code, apo_data.etape_apogee)
+                        )
+                        raise ScoValueError(
+                            """L'élément %s n'a pas de résultat: peut-être une erreur dans les codes sur le programme pédagogique (vérifier qu'il est bien associé à une UE ou semestre)?"""
+                            % code
+                        )
+            # recopie les 4 premieres colonnes (nom, ..., naissance):
+            for col_id in apo_data.col_ids[:4]:
+                self.new_cols[col_id] = self.cols[col_id]
+
+    def unassociated_codes(self, apo_data):
+        "list of apo elements for this student without a value in ScoDoc"
+        codes = set([apo_data.cols[col_id].code for col_id in apo_data.col_ids])
+        return codes - set(sco_elts)
+
+    def search_elt_in_sem(self, context, code, sem, cur_sem, autre_sem):
+        """
+        VET code jury etape
+        ELP élément pédagogique: UE, module
+        Autres éléments: résultats du semestre ou de l'année scolaire:
+        => VRTW1: code additionnel au semestre ("code élement semestre", elt_sem_apo)
+        => VRT1A: le même que le VET: ("code élement annuel", elt_annee_apo)
+        Attention, si le semestre couvre plusieurs étapes, indiquer les codes des éléments, 
+        séparés par des virgules.
+
+        Args:
+           code (str): code apo de l'element cherché
+           sem (dict): semestre dans lequel on cherche l'élément
+           cur_sem (dict): semestre "courant" pour résultats annuels (VET)
+           autre_sem (dict): autre semestre utilisé pour calculé les résultats annuels (VET)
+        
+        Returns:
+           dict: with N, B, J, R keys, ou None si elt non trouvé
+        """
+        etudid = self.etud["etudid"]
+        nt = context._getNotesCache().get_NotesTable(context, sem["formsemestre_id"])
+        if etudid not in nt.identdict:
+            return None  # etudiant non inscrit dans ce semestre
+
+        decision = nt.get_etud_decision_sem(etudid)
+        if not self.export_res_sdj and not decision:
+            # pas de decision de jury, on n'enregistre rien
+            # (meme si démissionnaire)
+            if not self.has_logged_no_decision:
+                self.log.append("Pas de decision")
+                self.has_logged_no_decision = True
+            return VOID_APO_RES
+
+        if decision and decision["code"] == NAR:
+            self.is_NAR = True
+
+        # Element etape (annuel ou non):
+        if sco_formsemestre.sem_has_etape(sem, code) or (
+            code in sem["elt_annee_apo"].split(",")
+        ):
+            export_res_etape = self.export_res_etape
+            if not export_res_etape:
+                # exporte toujours le résultat de l'étape si l'étudiant est diplômé
+                Se = sco_parcours_dut.SituationEtudParcours(
+                    context, self.etud, cur_sem["formsemestre_id"]
+                )
+                export_res_etape = Se.all_other_validated()
+
+            if export_res_etape:
+                return self.comp_elt_annuel(context, etudid, cur_sem, autre_sem)
+            else:
+                return VOID_APO_RES
+
+        # Element semestre:
+        if code in sem["elt_sem_apo"].split(","):
+            if self.export_res_sem:
+                return self.comp_elt_semestre(context, nt, decision, etudid)
+            else:
+                return VOID_APO_RES
+
+        # Elements UE
+        decisions_ue = nt.get_etud_decision_ues(etudid)
+        for ue in nt.get_ues():
+            if ue["code_apogee"] == code:
+                if self.export_res_ues:
+                    if decisions_ue and ue["ue_id"] in decisions_ue:
+                        ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
+                        code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
+                        return dict(
+                            N=_apo_fmt_note(ue_status["moy"]),
+                            B=20,
+                            J="",
+                            R=code_scodoc_to_apo(code_decision_ue),
+                            M="",
+                        )
+                    else:
+                        return VOID_APO_RES
+                else:
+                    return VOID_APO_RES
+
+        # Elements Modules
+        modimpls = nt.get_modimpls()
+        module_code_found = False
+        for modimpl in modimpls:
+            if modimpl["module"]["code_apogee"] == code:
+                n = nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
+                if n != "NI" and self.export_res_modules:
+                    return dict(N=_apo_fmt_note(n), B=20, J="", R="")
+                else:
+                    module_code_found = True
+        if module_code_found:
+            return VOID_APO_RES
+        #
+        return None  # element Apogee non trouvé dans ce semestre
+
+    def comp_elt_semestre(self, context, nt, decision, etudid):
+        """Calcul résultat apo semestre"""
+        # resultat du semestre
+        decision_apo = code_scodoc_to_apo(decision["code"])
+        note = nt.get_etud_moy_gen(etudid)
+        if (
+            decision_apo == "DEF"
+            or decision["code"] == "DEM"
+            or decision["code"] == DEF
+        ):
+            note_str = "0,01"  # note non nulle pour les démissionnaires
+        else:
+            note_str = _apo_fmt_note(note)
+        return dict(N=note_str, B=20, J="", R=decision_apo, M="")
+
+    def comp_elt_annuel(self, context, etudid, cur_sem, autre_sem):
+        """Calcul resultat annuel (VET) à partir du semestre courant
+        et de l'autre (le suivant ou le précédent complétant l'année scolaire)
+        """
+        # Code annuel:
+        #  - Note: moyenne des moyennes générales des deux semestres (pas vraiment de sens, mais faute de mieux)
+        #    on pourrait aussi bien prendre seulement la note du dernier semestre (S2 ou S4). Paramétrable ?
+        #  - Résultat jury:
+        #      si l'autre est validé, code du semestre courant (ex: S1 (ADM), S2 (AJ) => année AJ)
+        #      si l'autre n'est pas validé ou est DEF ou DEM, code de l'autre
+        #
+        #    XXX cette règle est discutable, à valider
+
+        # print 'comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id'])
+        if not cur_sem:
+            # l'étudiant n'a pas de semestre courant ?!
+            log("comp_elt_annuel: %s no cur_sem" % etudid)
+            return VOID_APO_RES
+        cur_nt = context._getNotesCache().get_NotesTable(
+            context, cur_sem["formsemestre_id"]
+        )
+        cur_decision = cur_nt.get_etud_decision_sem(etudid)
+        if not cur_decision:
+            # pas de decision => pas de résultat annuel
+            return VOID_APO_RES
+
+        if (cur_decision["code"] == RAT) and not self.export_res_rat:
+            # ne touche pas aux RATs
+            return VOID_APO_RES
+
+        if not autre_sem:
+            # formations monosemestre, ou code VET semestriel,
+            # ou jury intermediaire et etudiant non redoublant...
+            return self.comp_elt_semestre(context, cur_nt, cur_decision, etudid)
+
+        decision_apo = code_scodoc_to_apo(cur_decision["code"])
+
+        autre_nt = context._getNotesCache().get_NotesTable(
+            context, autre_sem["formsemestre_id"]
+        )
+        autre_decision = autre_nt.get_etud_decision_sem(etudid)
+        if not autre_decision:
+            # pas de decision dans l'autre => pas de résultat annuel
+            return VOID_APO_RES
+        autre_decision_apo = code_scodoc_to_apo(autre_decision["code"])
+        if (
+            autre_decision_apo == "DEF"
+            or autre_decision["code"] == "DEM"
+            or autre_decision["code"] == DEF
+        ) or (
+            decision_apo == "DEF"
+            or cur_decision["code"] == "DEM"
+            or cur_decision["code"] == DEF
+        ):
+            note_str = "0,01"  # note non nulle pour les démissionnaires
+        else:
+            note = cur_nt.get_etud_moy_gen(etudid)
+            autre_note = autre_nt.get_etud_moy_gen(etudid)
+            # print 'note=%s autre_note=%s' % (note, autre_note)
+            try:
+                moy_annuelle = (note + autre_note) / 2
+            except:
+                moy_annuelle = ""
+            note_str = _apo_fmt_note(moy_annuelle)
+
+        if code_semestre_validant(autre_decision["code"]):
+            decision_apo_annuelle = decision_apo
+        else:
+            decision_apo_annuelle = autre_decision_apo
+
+        return dict(N=note_str, B=20, J="", R=decision_apo_annuelle, M="")
+
+    def etud_semestres_de_etape(self, context, apo_data):
+        """
+        Lorsqu'on a une formation semestrialisée mais avec un code étape annuel,
+        il faut considérer les deux semestres ((S1,S2) ou (S3,S4)) pour calculer
+        le code annuel (VET ou VRT1A (voir elt_annee_apo)).
+
+        Pour les jurys intermediaires (janvier, S1 ou S3):  (S2 ou S4) de la même étape lors d'une année précédente ?
+
+        Renvoie le semestre "courant" et l'autre semestre, ou None s'il n'y en a pas.
+        """
+        # Cherche le semestre "courant":
+        cur_sems = [
+            sem
+            for sem in self.etud["sems"]
+            if (
+                (sem["semestre_id"] == apo_data.cur_semestre_id)
+                and (apo_data.etape in sem["etapes"])
+                and (
+                    sco_formsemestre.sem_in_annee_scolaire(
+                        context, sem, apo_data.annee_scolaire
+                    )
+                )
+            )
+        ]
+        if not cur_sems:
+            cur_sem = None
+        else:
+            # prend le plus recent avec decision
+            cur_sem = None
+            for sem in cur_sems:
+                nt = context._getNotesCache().get_NotesTable(
+                    context, sem["formsemestre_id"]
+                )
+                decision = nt.get_etud_decision_sem(self.etud["etudid"])
+                if decision:
+                    cur_sem = sem
+                    break
+            if cur_sem is None:
+                cur_sem = cur_sems[0]  # aucun avec decison, prend le plus recent
+
+        if apo_data.cur_semestre_id <= 0:
+            return (
+                cur_sem,
+                None,
+            )  # "autre_sem" non pertinent pour sessions sans semestres
+
+        if apo_data.jury_intermediaire:  # jury de janvier
+            # Le semestre suivant: exemple 2 si on est en jury de S1
+            autre_semestre_id = apo_data.cur_semestre_id + 1
+        else:
+            # Le précédent (S1 si on est en S2)
+            autre_semestre_id = apo_data.cur_semestre_id - 1
+
+        # L'autre semestre DOIT être antérieur au courant indiqué par apo_data
+        if apo_data.periode is not None:
+            if apo_data.periode == 1:
+                courant_annee_debut = apo_data.annee_scolaire
+                courant_mois_debut = 9  # periode = 1 (sept-jan)
+            elif apo_data.periode == 2:
+                courant_annee_debut = apo_data.annee_scolaire + 1
+                courant_mois_debut = 1  # ou 2 (fev-jul)
+            else:
+                raise ValueError("invalid pediode value !")  # bug ?
+            courant_date_debut = "%d-%02d-01" % (
+                courant_annee_debut,
+                courant_mois_debut,
+            )
+        else:
+            courant_date_debut = "9999-99-99"
+
+        # etud['sems'] est la liste des semestres de l'étudiant, triés par date,
+        # le plus récemment effectué en tête.
+        # Cherche les semestres (antérieurs) de l'indice autre de la même étape apogée
+        # s'il y en a plusieurs, choisit le plus récent ayant une décision
+
+        autres_sems = []
+        for sem in self.etud["sems"]:
+            if (
+                sem["semestre_id"] == autre_semestre_id
+                and apo_data.etape_apogee in sem["etapes"]
+            ):
+                if (
+                    sem["date_debut_iso"] < courant_date_debut
+                ):  # on demande juste qu'il ait démarré avant
+                    autres_sems.append(sem)
+        if not autres_sems:
+            autre_sem = None
+        elif len(autres_sems) == 1:
+            autre_sem = autres_sems[0]
+        else:
+            autre_sem = None
+            for sem in autres_sems:
+                nt = context._getNotesCache().get_NotesTable(
+                    context, sem["formsemestre_id"]
+                )
+                decision = nt.get_etud_decision_sem(self.etud["etudid"])
+                if decision:
+                    autre_sem = sem
+                    break
+            if autre_sem is None:
+                autre_sem = autres_sems[0]  # aucun avec decision, prend le plus recent
+
+        return cur_sem, autre_sem
+
+
+class ApoData:
+    def __init__(
+        self,
+        data,
+        periode=None,
+        export_res_etape=True,
+        export_res_sem=True,
+        export_res_ues=True,
+        export_res_modules=True,
+        export_res_sdj=True,
+        export_res_rat=True,
+        orig_filename=None,
+    ):
+        """Lecture du fichier CSV Apogée
+        Regroupe les élements importants d'un fichier CSV Apogée
+        periode = 1 (sept-jan) ou 2 (fev-jul), mais cette info n'est pas
+         (toujours) présente dans les CSV Apogée et doit être indiquée par l'utilisateur
+        Laisser periode à None si etape en 1 semestre (LP, décalés, ...)
+        """
+        self.export_res_etape = export_res_etape  # VET, ...
+        self.export_res_sem = export_res_sem  # elt_sem_apo
+        self.export_res_ues = export_res_ues
+        self.export_res_modules = export_res_modules
+        self.export_res_sdj = export_res_sdj
+        self.export_res_rat = export_res_rat
+        self.orig_filename = orig_filename
+        self.periode = periode  #
+        try:
+            self.read_csv(data)
+        except FormatError as e:
+            # essaie de retrouver le nom du fichier pour enrichir le message d'erreur
+            filename = ""
+            if self.orig_filename is None:
+                if hasattr(self, "titles"):
+                    filename = self.titles.get("apoC_Fichier_Exp", filename)
+            else:
+                filename = self.orig_filename
+            raise FormatError(
+                "<h3>Erreur lecture du fichier Apogée <tt>%s</tt></h3><p>" % filename
+                + e.args[0]
+                + "</p>"
+            )
+        self.etape_apogee = self.get_etape_apogee()  #  'V1RT'
+        self.vdi_apogee = self.get_vdi_apogee()  # '111'
+        self.etape = ApoEtapeVDI(etape=self.etape_apogee, vdi=self.vdi_apogee)
+        self.cod_dip_apogee = self.get_cod_dip_apogee()
+        self.annee_scolaire = self.get_annee_scolaire()
+        self.jury_intermediaire = (
+            False  # True si jury à mi-étape, eg jury de S1 dans l'étape (S1, S2)
+        )
+
+        log(
+            "ApoData( periode=%s, annee_scolaire=%s )"
+            % (self.periode, self.annee_scolaire)
+        )
+
+    def set_periode(self, periode):  # currently unused
+        self.periode = periode
+
+    def setup(self, context):
+        """Recherche semestres ScoDoc concernés
+        """
+        self.context = context
+        self.sems_etape = comp_apo_sems(context, self.etape_apogee, self.annee_scolaire)
+        self.etape_formsemestre_ids = {s["formsemestre_id"] for s in self.sems_etape}
+        if self.periode != None:
+            self.sems_periode = [
+                s
+                for s in self.sems_etape
+                if (s["periode"] == self.periode) or s["semestre_id"] < 0
+            ]
+            if not self.sems_periode:
+                log("** Warning: ApoData.setup: sems_periode is empty")
+                log(
+                    "**  (periode=%s, sems_etape [periode]=%s)"
+                    % (self.periode, [s["periode"] for s in self.sems_etape])
+                )
+                self.sems_periode = None
+                self.cur_semestre_id = -1  # ?
+            else:
+                self.cur_semestre_id = self.sems_periode[0]["semestre_id"]
+                # Les semestres de la période ont le même indice, n'est-ce pas ?
+                if not all(
+                    self.cur_semestre_id == s["semestre_id"] for s in self.sems_periode
+                ):
+                    # debugging information
+                    import pprint
+
+                    log("*** ApoData.set() error !")
+                    log(
+                        "ApoData( periode=%s, annee_scolaire=%s, cur_semestre_id=%s )"
+                        % (self.periode, self.annee_scolaire, self.cur_semestre_id)
+                    )
+                    log("%d semestres dans la periode: " % len(self.sems_periode))
+                    for s in self.sems_periode:
+                        log(pprint.pformat(s))
+
+                    raise ValueError(
+                        "incohérence détectée (contacter les développeurs)"
+                    )
+            # Cette condition sera inadaptée si semestres décalés
+            # (mais ils n'ont pas d'étape annuelle, espérons!)
+            if self.cur_semestre_id >= 0:  # non pertinent pour sessions sans semestres
+                self.jury_intermediaire = (self.cur_semestre_id % 2) != 0
+        else:
+            self.sems_periode = None
+
+    def read_csv(self, data):
+        if not data:
+            raise FormatError("Fichier Apogée vide !")
+
+        data_utf8 = data.decode(APO_INPUT_ENCODING).encode(SCO_ENCODING)
+        f = StringIOFileLineWrapper(data_utf8)  # pour traiter comme un fichier
+        # check that we are at the begining of Apogee CSV
+        line = f.readline().strip()
+        if line != "XX-APO_TITRES-XX":
+            raise FormatError("format incorrect: pas de XX-APO_TITRES-XX")
+
+        # 1-- En-tête: du début jusqu'à la balise XX-APO_VALEURS-XX
+        idx = data_utf8.index("XX-APO_VALEURS-XX")
+        self.header = data_utf8[:idx]  # en codage ScoDoc (utf8)
+
+        # 2-- Titres:
+        #   on va y chercher apoC_Fichier_Exp qui donnera le nom du fichier
+        #   ainsi que l'année scolaire et le code diplôme.
+        self.titles = _apo_read_TITRES(f)
+
+        # 3-- La section XX-APO_TYP_RES-XX est ignorée:
+        line = f.readline().strip()
+        if line != "XX-APO_TYP_RES-XX":
+            raise FormatError("format incorrect: pas de XX-APO_TYP_RES-XX")
+        _apo_skip_section(f)
+
+        # 4-- Définition de colonnes: (on y trouve aussi l'étape)
+        line = f.readline().strip()
+        if line != "XX-APO_COLONNES-XX":
+            raise FormatError("format incorrect: pas de XX-APO_COLONNES-XX")
+        self.cols = _apo_read_cols(f)
+        self.apo_elts = self._group_elt_cols(self.cols)
+
+        # 5-- Section XX-APO_VALEURS-XX
+        # Lecture des étudiants et de leurs résultats
+        while True:  # skip
+            line = f.readline()
+            if not line:
+                raise FormatError("format incorrect: pas de XX-APO_VALEURS-XX")
+            if line.strip() == "XX-APO_VALEURS-XX":
+                break
+        self.column_titles = f.readline()
+        self.col_ids = self.column_titles.strip().split()
+        self.etuds = self.apo_read_etuds(f)
+        self.etud_by_nip = {e["nip"]: e for e in self.etuds}
+
+    def get_etud_by_nip(self, nip):
+        "returns ApoEtud with a given NIP code"
+        return self.etud_by_nip[nip]
+
+    def _group_elt_cols(self, cols):
+        """Return ordered dict of ApoElt from list of ApoCols.
+        Clé: id apogée, eg 'V1RT', 'V1GE2201', ...
+        Valeur: ApoElt, avec les attributs code, type_objet
+
+        Si les id Apogée ne sont pas uniques (ce n'est pas garanti), garde le premier 
+        """
+        elts = collections.OrderedDict()
+        for col_id in sorted(cols.keys(), reverse=True):
+            col = cols[col_id]
+            if col["Code"] in elts:
+                elts[col["Code"]].append(col)
+            else:
+                elts[col["Code"]] = ApoElt([col])
+        return elts  # { code apo : ApoElt }
+
+    def apo_read_etuds(self, f):
+        """Lecture des etudiants (et resultats) du fichier CSV Apogée
+        -> liste de dicts
+        """
+        L = []
+        while True:
+            line = f.readline()
+            if not line:
+                break
+            if not line.strip():
+                continue  # silently ignore blank lines
+            line = line.strip(APO_NEWLINE)
+            fs = line.split(APO_SEP)
+            cols = {}  # { col_id : value }
+            for i in range(len(fs)):
+                cols[self.col_ids[i]] = fs[i]
+            L.append(
+                ApoEtud(
+                    nip=fs[0],  # id etudiant
+                    nom=fs[1],
+                    prenom=fs[2],
+                    naissance=fs[3],
+                    cols=cols,
+                    export_res_etape=self.export_res_etape,
+                    export_res_sem=self.export_res_sem,
+                    export_res_ues=self.export_res_ues,
+                    export_res_modules=self.export_res_modules,
+                    export_res_sdj=self.export_res_sdj,
+                    export_res_rat=self.export_res_rat,
+                )
+            )
+
+        return L
+
+    def get_etape_apogee(self):
+        """Le code etape: 'V1RT', donné par le code de l'élément VET
+        """
+        for elt in self.apo_elts.values():
+            if elt.type_objet == "VET":
+                return elt.code
+        raise ScoValueError("Pas de code etape Apogee (manque élément VET)")
+
+    def get_vdi_apogee(self):
+        """le VDI (version de diplôme), stocké dans l'élément VET
+        (note: on pourrait peut-être aussi bien le récupérer dans l'en-tête XX-APO_TITRES-XX apoC_cod_vdi)
+        """
+        for elt in self.apo_elts.values():
+            if elt.type_objet == "VET":
+                return elt.version
+        raise ScoValueError("Pas de VDI Apogee (manque élément VET)")
+
+    def get_cod_dip_apogee(self):
+        """Le code diplôme, indiqué dans l'en-tête de la maquette
+        exemple: VDTRT
+        Retourne '' si absent.
+        """
+        return self.titles.get("apoC_cod_dip", "")
+
+    def get_annee_scolaire(self):
+        """Annee scolaire du fichier Apogee: un integer
+        = annee du mois de septembre de début
+        """
+        m = re.match("[12][0-9]{3}", self.titles["apoC_annee"])
+        if not m:
+            raise FormatError(
+                'Annee scolaire (apoC_annee) invalide: "%s"' % self.titles["apoC_annee"]
+            )
+        return int(m.group(0))
+
+    def write_header(self, f):
+        """write apo CSV header on f
+        (beginning of CSV until columns titles just after XX-APO_VALEURS-XX line)
+        """
+        f.write(self.header)
+        f.write(APO_NEWLINE)
+        f.write("XX-APO_VALEURS-XX" + APO_NEWLINE)
+        f.write(self.column_titles)
+
+    def write_etuds(self, f):
+        """write apo CSV etuds on f
+        """
+        for e in self.etuds:
+            fs = []  #  e['nip'], e['nom'], e['prenom'], e['naissance'] ]
+            for col_id in self.col_ids:
+                try:
+                    fs.append(str(e.new_cols[col_id]))
+                except KeyError:
+                    log(
+                        "Error: %s %s missing column key %s"
+                        % (e["nip"], e["nom"], col_id)
+                    )
+                    log("Details:\ne = %s" % pprint.pformat(e))
+                    log("col_ids=%s" % pprint.pformat(self.col_ids))
+                    log("etudiant ignore.\n")
+
+            f.write(APO_SEP.join(fs) + APO_NEWLINE)
+
+    def list_unknown_elements(self):
+        """Liste des codes des elements Apogee non trouvés dans ScoDoc
+        (après traitement de tous les étudiants)
+        """
+        s = set()
+        for e in self.etuds:
+            ul = [code for code in e.col_elts if e.col_elts[code] is None]
+            s.update(ul)
+        L = list(s)
+        L.sort()
+        return L
+
+    def list_elements(self):
+        """Liste les codes des elements Apogée de la maquette
+        et ceux des semestres ScoDoc associés
+        Retourne deux ensembles
+        """
+        try:
+            maq_elems = {self.cols[col_id]["Code"] for col_id in self.col_ids[4:]}
+        except KeyError:
+            # une colonne déclarée dans l'en-tête n'est pas présente
+            declared = self.col_ids[4:]  # id des colones dans l'en-tête
+            present = sorted(self.cols.keys())  # colones presentes
+            log("Fichier Apogee invalide:")
+            log("Colonnes declarees: %s" % declared)
+            log("Colonnes presentes: %s" % present)
+            raise FormatError(
+                """Fichier Apogee invalide<br/>Colonnes declarees: <tt>%s</tt>
+            <br/>Colonnes presentes: <tt>%s</tt>"""
+                % (declared, present)
+            )
+        # l'ensemble de tous les codes des elements apo des semestres:
+        sem_elems = reduce(set.union, self.get_codes_by_sem().values(), set())
+
+        return maq_elems, sem_elems
+
+    def get_codes_by_sem(self):
+        """Pour chaque semestre associé, donne l'ensemble des codes Apogée qui s'y trouvent
+        (dans le semestre, les UE et les modules)
+        """
+        codes_by_sem = {}
+        for sem in self.sems_etape:
+            s = set()
+            codes_by_sem[sem["formsemestre_id"]] = s
+            for col_id in self.col_ids[4:]:
+                code = self.cols[col_id]["Code"]  # 'V1RT'
+                # associé à l'étape, l'année ou les semestre:
+                if (
+                    sco_formsemestre.sem_has_etape(sem, code)
+                    or (code in sem["elt_sem_apo"].split(","))
+                    or (code in sem["elt_annee_apo"].split(","))
+                ):
+                    s.add(code)
+                    continue
+                # associé à une UE:
+                nt = self.context._getNotesCache().get_NotesTable(
+                    self.context, sem["formsemestre_id"]
+                )
+                for ue in nt.get_ues():
+                    if ue["code_apogee"] == code:
+                        s.add(code)
+                        continue
+                # associé à un module:
+                modimpls = nt.get_modimpls()
+                for modimpl in modimpls:
+                    if modimpl["module"]["code_apogee"] == code:
+                        s.add(code)
+                        continue
+        # log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
+        return codes_by_sem
+
+    def build_cr_table(self):
+        """Table compte rendu des décisions
+        """
+        CR = []  # tableau compte rendu des decisions
+        for e in self.etuds:
+            cr = {
+                "NIP": e["nip"],
+                "nom": e["nom"],
+                "prenom": e["prenom"],
+                "est_NAR": e.is_NAR,
+                "commentaire": "; ".join(e.log),
+            }
+            if e.col_elts and e.col_elts[self.etape_apogee] != None:
+                cr["etape"] = e.col_elts[self.etape_apogee].get("R", "")
+                cr["etape_note"] = e.col_elts[self.etape_apogee].get("N", "")
+            else:
+                cr["etape"] = ""
+                cr["etape_note"] = ""
+            CR.append(cr)
+
+        columns_ids = ["NIP", "nom", "prenom"]
+        columns_ids.extend(("etape", "etape_note", "est_NAR", "commentaire"))
+
+        T = GenTable(
+            columns_ids=columns_ids,
+            titles=dict(zip(columns_ids, columns_ids)),
+            rows=CR,
+            xls_sheet_name="Decisions ScoDoc",
+        )
+        return T
+
+
+def _apo_read_cols(f):
+    """Lecture colonnes apo : 
+    Démarre après la balise XX-APO_COLONNES-XX
+    et s'arrête après la balise APO_COL_VAL_FIN
+
+    Colonne Apogee: les champs sont données par la ligne
+    apoL_a01_code de la section XX-APO_COLONNES-XX    
+    col_id est apoL_c0001, apoL_c0002, ...
+    
+    :return: { col_id : { title : value } }
+    Example: { 'apoL_c0001' : { 'Type Objet' : 'VET', 'Code' : 'V1IN', ... }, ... }
+    """
+    line = f.readline().strip(" " + APO_NEWLINE)
+    fs = line.split(APO_SEP)
+    if fs[0] != "apoL_a01_code":
+        raise FormatError("invalid line: %s (expecting apoL_a01_code)"(line, i))
+    col_keys = fs
+
+    while True:  # skip premiere partie (apoL_a02_nom, ...)
+        line = f.readline().strip(" " + APO_NEWLINE)
+        if line == "APO_COL_VAL_DEB":
+            break
+    # après APO_COL_VAL_DEB
+    cols = {}
+    i = 0
+    while True:
+        line = f.readline().strip(" " + APO_NEWLINE)
+        if line == "APO_COL_VAL_FIN":
+            break
+        i += 1
+        fs = line.split(APO_SEP)
+        # print fs[0], len(fs)
+        # sanity check
+        col_id = fs[0]  # apoL_c0001, ...
+        if col_id in cols:
+            raise FormatError("duplicate column definition: %s" % col_id)
+        m = re.match(r"^apoL_c([0-9]{4})$", col_id)
+        if not m:
+            raise FormatError("invalid column id: %s (expecting apoL_c%04d)"(line, i))
+        if int(m.group(1)) != i:
+            raise FormatError("invalid column id: %s for index %s" % (col_id, i))
+
+        cols[col_id] = DictCol(zip(col_keys, fs))
+        cols[col_id].lineno = f.lineno  # for debuging purpose
+
+    return cols
+
+
+def _apo_read_TITRES(f):
+    "Lecture section TITRES du fichier Apogée, renvoie dict"
+    d = {}
+    while True:
+        line = f.readline().strip(
+            " " + APO_NEWLINE
+        )  # ne retire pas le \t (pour les clés vides)
+        if not line.strip():  # stoppe sur ligne  pleines de \t
+            break
+
+        fields = line.split(APO_SEP)
+        if len(fields) == 2:
+            k, v = fields
+        else:
+            log("Error read CSV: \nline=%s\nfields=%s" % (line, fields))
+            log(dir(f))
+            raise FormatError(
+                "Fichier Apogee incorrect (section titres, %d champs au lieu de 2)"
+                % len(fields)
+            )
+        d[k] = v
+    #
+    if not d.get("apoC_Fichier_Exp", None):
+        raise FormatError("Fichier Apogee incorrect: pas de titre apoC_Fichier_Exp")
+    # keep only basename: may be a windows or unix pathname
+    s = d["apoC_Fichier_Exp"].split("/")[-1]
+    s = s.split("\\")[-1]  # for DOS paths, eg C:\TEMP\VL4RT_V3ASR.TXT
+    d["apoC_Fichier_Exp"] = s
+    return d
+
+
+def _apo_skip_section(f):
+    "Saute section Apo: s'arrete apres ligne vide"
+    while True:
+        line = f.readline().strip()
+        if not line:
+            break
+
+
+# -------------------------------------
+
+
+def comp_apo_sems(context, etape_apogee, annee_scolaire):
+    """
+    :param etape_apogee: etape (string or ApoEtapeVDI)
+    :param annee_scolaire: annee (int)
+    :return: list of sems for etape_apogee in annee_scolaire
+    """
+    return sco_formsemestre.list_formsemestre_by_etape(
+        context, etape_apo=str(etape_apogee), annee_scolaire=annee_scolaire
+    )
+
+
+def nar_etuds_table(context, apo_data, NAR_Etuds):
+    """Liste les NAR -> excel table
+    """
+    code_etape = apo_data.etape_apogee
+    today = datetime.datetime.today().strftime("%d/%m/%y")
+    L = []
+    NAR_Etuds.sort(key=lambda k: k["nom"])
+    for e in NAR_Etuds:
+        L.append(
+            {
+                "nom": e["nom"],
+                "prenom": e["prenom"],
+                "c0": "",
+                "c1": "AD",
+                "etape": code_etape,
+                "c3": "",
+                "c4": "",
+                "c5": "",
+                "c6": "N",
+                "c7": "",
+                "c8": "",
+                "NIP": e["nip"],
+                "c10": "",
+                "c11": "",
+                "c12": "",
+                "c13": "NAR - Jury",
+                "date": today,
+            }
+        )
+
+    columns_ids = (
+        "NIP",
+        "nom",
+        "prenom",
+        "etape",
+        "c0",
+        "c1",
+        "c3",
+        "c4",
+        "c5",
+        "c6",
+        "c7",
+        "c8",
+        "c10",
+        "c11",
+        "c12",
+        "c13",
+        "date",
+    )
+    T = GenTable(
+        columns_ids=columns_ids,
+        titles=dict(zip(columns_ids, columns_ids)),
+        rows=L,
+        xls_sheet_name="NAR ScoDoc",
+    )
+    return T.excel()
+
+
+def export_csv_to_apogee(
+    context,
+    apo_csv_data,
+    periode=None,
+    dest_zip=None,
+    export_res_etape=True,
+    export_res_sem=True,
+    export_res_ues=True,
+    export_res_modules=True,
+    export_res_sdj=True,
+    export_res_rat=True,
+    REQUEST=None,
+):
+    """Genere un fichier CSV Apogée 
+    à partir d'un fichier CSV Apogée vide (ou partiellement rempli)
+    et des résultats ScoDoc.
+    Si dest_zip, ajoute les fichiers générés à ce zip
+    sinon crée un zip et le publie
+    """
+    apo_data = ApoData(
+        apo_csv_data,
+        periode=periode,
+        export_res_etape=export_res_etape,
+        export_res_sem=export_res_sem,
+        export_res_ues=export_res_ues,
+        export_res_modules=export_res_modules,
+        export_res_sdj=export_res_sdj,
+        export_res_rat=export_res_rat,
+    )
+    apo_data.setup(context)  # -> .sems_etape
+
+    for e in apo_data.etuds:
+        e.lookup_scodoc(context, apo_data.etape_formsemestre_ids)
+        e.associate_sco(context, apo_data)
+
+    # Ré-écrit le fichier Apogée
+    f = StringIO()
+    apo_data.write_header(f)
+    apo_data.write_etuds(f)
+
+    # Table des NAR:
+    NAR_Etuds = [e for e in apo_data.etuds if e.is_NAR]
+    if NAR_Etuds:
+        nar_xls = nar_etuds_table(context, apo_data, NAR_Etuds)
+    else:
+        nar_xls = None
+
+    # Journaux & Comptes-rendus
+    # Orphelins: etudiants dans fichier Apogée mais pas dans ScoDoc
+    Apo_Non_ScoDoc = [e for e in apo_data.etuds if e.etat == ETUD_ORPHELIN]
+    # Non inscrits: connus de ScoDoc mais pas inscrit dans l'étape cette année
+    Apo_Non_ScoDoc_Inscrits = [e for e in apo_data.etuds if e.etat == ETUD_NON_INSCRIT]
+    # CR table
+    cr_table = apo_data.build_cr_table()
+    cr_xls = cr_table.excel()
+
+    # Create ZIP
+    if not dest_zip:
+        data = StringIO()
+        dest_zip = ZipFile(data, "w")
+        my_zip = True
+    else:
+        my_zip = False
+    # Ensure unique filenames
+    filename = apo_data.titles["apoC_Fichier_Exp"]
+    basename, ext = os.path.splitext(filename)
+    csv_filename = filename
+
+    if csv_filename in dest_zip.namelist():
+        basename = filename + "-" + apo_data.vdi_apogee
+        csv_filename = basename + ext
+    nf = 1
+    tmplname = basename
+    while csv_filename in dest_zip.namelist():
+        basename = tmplname + "-%d" % nf
+        csv_filename = basename + ext
+        nf += 1
+
+    log_filename = "scodoc-" + basename + ".log.txt"
+    nar_filename = basename + "-nar.xls"
+    cr_filename = basename + "-decisions.xls"
+
+    logf = StringIO()
+    logf.write("export_to_apogee du %s\n\n" % time.ctime())
+    logf.write("Semestres ScoDoc sources:\n")
+    for sem in apo_data.sems_etape:
+        logf.write("\t%(titremois)s\n" % sem)
+    logf.write("Periode: %s\n" % periode)
+    logf.write("export_res_etape: %s\n" % int(export_res_etape))
+    logf.write("export_res_sem: %s\n" % int(export_res_sem))
+    logf.write("export_res_ues: %s\n" % int(export_res_ues))
+    logf.write("export_res_modules: %s\n" % int(export_res_modules))
+    logf.write("export_res_sdj: %s\n" % int(export_res_sdj))
+    logf.write(
+        "\nEtudiants Apogee non trouves dans ScoDoc:\n"
+        + "\n".join(
+            ["%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"]) for e in Apo_Non_ScoDoc]
+        )
+    )
+    logf.write(
+        "\nEtudiants Apogee non inscrits sur ScoDoc dans cette étape:\n"
+        + "\n".join(
+            [
+                "%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"])
+                for e in Apo_Non_ScoDoc_Inscrits
+            ]
+        )
+    )
+
+    logf.write(
+        "\n\nElements Apogee inconnus dans ces semestres ScoDoc:\n"
+        + "\n".join(apo_data.list_unknown_elements())
+    )
+    log(logf.getvalue())  # sortie aussi sur le log ScoDoc
+
+    csv_data = f.getvalue().decode(SCO_ENCODING).encode(APO_OUTPUT_ENCODING)
+
+    # Write data to ZIP
+    dest_zip.writestr(csv_filename, csv_data)
+    dest_zip.writestr(log_filename, logf.getvalue())
+    if nar_xls:
+        dest_zip.writestr(nar_filename, nar_xls)
+    dest_zip.writestr(cr_filename, cr_xls)
+
+    if my_zip:
+        dest_zip.close()
+        size = data.tell()
+        content_type = "application/zip"
+        REQUEST.RESPONSE.setHeader(
+            "content-disposition", 'attachement; filename="%s-scodoc.zip"' % basename
+        )
+        REQUEST.RESPONSE.setHeader("content-type", content_type)
+        REQUEST.RESPONSE.setHeader("content-length", size)
+        return data.getvalue()
+    else:
+        return None  # zip modified in place
diff --git a/sco_archives.py b/sco_archives.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6f31c6327b52b3a03a3dd51bbf1d7c4d035e275
--- /dev/null
+++ b/sco_archives.py
@@ -0,0 +1,566 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""ScoDoc : gestion des archives des PV et bulletins, et des dossiers etudiants (admission)
+
+
+ Archives are plain files, stored in 
+    <INSTANCE_HOME>/var/scodoc/archives/<deptid>
+ (where <INSTANCE_HOME> is usually /opt/scodoc/instance, and <deptid> a departement id)
+
+ 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>
+
+ Les documents liés à l'étudiant sont dans
+    <archivedir>/docetuds/<dept>/<etudid>/<YYYY-MM-DD-HH-MM-SS>
+
+ 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
+    
+ Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt
+ qui est une description (humaine, format libre) de l'archive.
+
+"""
+
+from mx.DateTime import DateTime as mxDateTime
+import mx.DateTime
+import shutil
+import glob
+
+from sco_utils import *
+from notesdb import *
+
+import sco_formsemestre
+import sco_pvjury
+import sco_excel
+import sco_pvpdf
+import sco_groups
+import sco_groups_view
+from sco_recapcomplet import make_formsemestre_recapcomplet
+import sco_bulletins_pdf
+
+
+class BaseArchiver:
+    def __init__(self, archive_type=""):
+        dirs = [os.environ["INSTANCE_HOME"], "var", "scodoc", "archives"]
+        if archive_type:
+            dirs.append(archive_type)
+        self.root = os.path.join(*dirs)
+        log("initialized archiver, path=" + self.root)
+        path = dirs[0]
+        for dir in dirs[1:]:
+            path = os.path.join(path, dir)
+            try:
+                GSL.acquire()
+                if not os.path.isdir(path):
+                    log("creating directory %s" % path)
+                    os.mkdir(path)
+            finally:
+                GSL.release()
+
+    def get_obj_dir(self, context, oid):
+        """
+        :return: path to directory of archives for this object (eg formsemestre_id or etudid).
+        If directory does not yet exist, create it.
+        """
+        dept_dir = os.path.join(self.root, context.DeptId())
+        try:
+            GSL.acquire()
+            if not os.path.isdir(dept_dir):
+                log("creating directory %s" % dept_dir)
+                os.mkdir(dept_dir)
+            obj_dir = os.path.join(dept_dir, oid)
+            if not os.path.isdir(obj_dir):
+                log("creating directory %s" % obj_dir)
+                os.mkdir(obj_dir)
+        finally:
+            GSL.release()
+        return obj_dir
+
+    def list_oids(self, context):
+        """
+        :return: list of archive oids
+        """
+        base = os.path.join(self.root, context.DeptId()) + os.path.sep
+        dirs = glob.glob(base + "*")
+        return [os.path.split(x)[1] for x in dirs]
+
+    def list_obj_archives(self, context, oid):
+        """Returns
+        :return: list of archive identifiers for this object (paths to non empty dirs)
+        """
+        base = self.get_obj_dir(context, oid) + os.path.sep
+        dirs = glob.glob(
+            base
+            + "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]"
+        )
+        dirs = [os.path.join(base, d) for d in dirs]
+        dirs = [d for d in dirs if os.path.isdir(d) and os.listdir(d)]  # non empty dirs
+        dirs.sort()
+        return dirs
+
+    def delete_archive(self, archive_id):
+        """Delete (forever) this archive"""
+        try:
+            GSL.acquire()
+            shutil.rmtree(archive_id, ignore_errors=True)
+        finally:
+            GSL.release()
+
+    def get_archive_date(self, archive_id):
+        """Returns date (as a DateTime object) of an archive"""
+        dt = [int(x) for x in os.path.split(archive_id)[1].split("-")]
+        return mxDateTime(*dt)
+
+    def list_archive(self, archive_id):
+        """Return list of filenames (without path) in archive"""
+        try:
+            GSL.acquire()
+            files = os.listdir(archive_id)
+        finally:
+            GSL.release()
+        files.sort()
+        return [f for f in files if f and f[0] != "_"]
+
+    def get_archive_name(self, archive_id):
+        """name identifying archive, to be used in web URLs"""
+        return os.path.split(archive_id)[1]
+
+    def is_valid_archive_name(self, archive_name):
+        """check if name is valid."""
+        return re.match(
+            "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name
+        )
+
+    def get_id_from_name(self, context, oid, archive_name):
+        """returns archive id (check that name is valid)"""
+        if not self.is_valid_archive_name(archive_name):
+            raise ValueError("invalid archive name")
+        archive_id = os.path.join(self.get_obj_dir(context, oid), archive_name)
+        if not os.path.isdir(archive_id):
+            log(
+                "invalid archive name: %s, oid=%s, archive_id=%s"
+                % (archive_name, oid, archive_id)
+            )
+            raise ValueError("invalid archive name")
+        return archive_id
+
+    def get_archive_description(self, archive_id):
+        """Return description of archive"""
+        return open(os.path.join(archive_id, "_description.txt")).read()
+
+    def create_obj_archive(self, context, oid, description):
+        """Creates a new archive for this object and returns its id."""
+        archive_id = (
+            self.get_obj_dir(context, oid)
+            + os.path.sep
+            + "-".join(["%02d" % x for x in time.localtime()[:6]])
+        )
+        log("creating archive: %s" % archive_id)
+        try:
+            GSL.acquire()
+            os.mkdir(archive_id)  # if exists, raises an OSError
+        finally:
+            GSL.release()
+        self.store(archive_id, "_description.txt", description)
+        return archive_id
+
+    def store(self, archive_id, filename, data):
+        """Store data in archive, under given filename. 
+        Filename may be modified (sanitized): return used filename
+        The file is created or replaced.
+        """
+        filename = sanitize_filename(filename)
+        log("storing %s (%d bytes) in %s" % (filename, len(data), archive_id))
+        try:
+            GSL.acquire()
+            fname = os.path.join(archive_id, filename)
+            f = open(fname, "w")
+            f.write(data)
+            f.close()
+        finally:
+            GSL.release()
+        return filename
+
+    def get(self, archive_id, filename):
+        """Retreive data"""
+        if not is_valid_filename(filename):
+            log('Archiver.get: invalid filename "%s"' % filename)
+            raise ValueError("invalid filename")
+        fname = os.path.join(archive_id, filename)
+        log("reading archive file %s" % fname)
+        return open(fname).read()
+
+    def get_archived_file(self, context, REQUEST, 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(context, oid, archive_name)
+        data = self.get(archive_id, filename)
+        ext = os.path.splitext(strlower(filename))[1]
+        if ext == ".html" or ext == ".htm":
+            return data
+        elif ext == ".xml":
+            REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+            return data
+        elif ext == ".xls":
+            return sco_excel.sendExcelFile(REQUEST, data, filename)
+        elif ext == ".csv":
+            return sendCSVFile(REQUEST, data, filename)
+        elif ext == ".pdf":
+            return sendPDFFile(REQUEST, data, filename)
+
+        return data  # should set mimetype...
+
+
+class SemsArchiver(BaseArchiver):
+    def __init__(self):
+        BaseArchiver.__init__(self, archive_type="")
+
+
+PVArchive = SemsArchiver()
+
+
+# ----------------------------------------------------------------------------
+
+
+def do_formsemestre_archive(
+    context,
+    REQUEST,
+    formsemestre_id,
+    group_ids=[],  # si indiqué, ne prend que ces groupes
+    description="",
+    date_jury="",
+    signature=None,  # pour lettres indiv
+    date_commission=None,
+    numeroArrete=None,
+    VDICode=None,
+    showTitle=False,
+    anonymous=False,
+    bulVersion="long",
+):
+    """Make and store new archive for this formsemestre.
+    Store:
+    - tableau recap (xls), pv jury (xls et pdf), bulletins (xml et pdf), lettres individuelles (pdf)
+    """
+    archive_id = PVArchive.create_obj_archive(context, formsemestre_id, description)
+    date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
+
+    if not group_ids:
+        # tous les inscrits du semestre
+        group_ids = [sco_groups.get_default_group(context, formsemestre_id)]
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST
+    )
+    groups_filename = "-" + groups_infos.groups_filename
+    etudids = [m["etudid"] for m in groups_infos.members]
+
+    # Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes)
+    data, filename, format = make_formsemestre_recapcomplet(
+        context, REQUEST, formsemestre_id, format="xls"
+    )
+    if data:
+        PVArchive.store(archive_id, "Tableau_moyennes.xls", data)
+    # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
+    data, filename, format = make_formsemestre_recapcomplet(
+        context, REQUEST, formsemestre_id, format="html", disable_etudlink=True
+    )
+    if data:
+        data = "\n".join(
+            [
+                context.sco_header(
+                    REQUEST,
+                    page_title="Moyennes archivées le %s" % date,
+                    head_message="Moyennes archivées le %s" % date,
+                    no_side_bar=True,
+                ),
+                '<h2 class="fontorange">Valeurs archivées le %s</h2>' % date,
+                '<style type="text/css">table.notes_recapcomplet tr {  color: rgb(185,70,0); }</style>',
+                data,
+                context.sco_footer(REQUEST),
+            ]
+        )
+        PVArchive.store(archive_id, "Tableau_moyennes.html", data)
+
+    # Bulletins en XML (pour tous les etudiants, n'utilise pas les groupes)
+    data, filename, format = make_formsemestre_recapcomplet(
+        context, REQUEST, formsemestre_id, format="xml", xml_with_decisions=True
+    )
+    if data:
+        PVArchive.store(archive_id, "Bulletins.xml", data)
+    # Decisions de jury, en XLS
+    data = sco_pvjury.formsemestre_pvjury(
+        context, formsemestre_id, format="xls", REQUEST=REQUEST, publish=False
+    )
+    if data:
+        PVArchive.store(archive_id, "Decisions_Jury.xls", data)
+    # Classeur bulletins (PDF)
+    data, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
+        context, formsemestre_id, REQUEST, version=bulVersion
+    )
+    if data:
+        PVArchive.store(archive_id, "Bulletins.pdf", data)
+    # Lettres individuelles (PDF):
+    data = sco_pvpdf.pdf_lettres_individuelles(
+        context,
+        formsemestre_id,
+        etudids=etudids,
+        date_jury=date_jury,
+        date_commission=date_commission,
+        signature=signature,
+    )
+    if data:
+        PVArchive.store(archive_id, "CourriersDecisions%s.pdf" % groups_filename, data)
+    # PV de jury (PDF):
+    dpv = sco_pvjury.dict_pvjury(
+        context, formsemestre_id, etudids=etudids, with_prev=True
+    )
+    data = sco_pvpdf.pvjury_pdf(
+        context,
+        dpv,
+        REQUEST,
+        date_commission=date_commission,
+        date_jury=date_jury,
+        numeroArrete=numeroArrete,
+        VDICode=VDICode,
+        showTitle=showTitle,
+        anonymous=anonymous,
+    )
+    if data:
+        PVArchive.store(archive_id, "PV_Jury%s.pdf" % groups_filename, data)
+
+
+def formsemestre_archive(context, REQUEST, formsemestre_id, group_ids=[]):
+    """Make and store new archive for this formsemestre.
+    (all students or only selected groups)
+    """
+    if not context._can_edit_pv(REQUEST, formsemestre_id):
+        raise AccessDenied(
+            "opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
+        )
+
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    if not group_ids:
+        # tous les inscrits du semestre
+        group_ids = [sco_groups.get_default_group(context, formsemestre_id)]
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST
+    )
+
+    H = [
+        context.html_sem_header(
+            REQUEST,
+            "Archiver les PV et résultats du semestre",
+            sem=sem,
+            javascripts=sco_groups_view.JAVASCRIPTS,
+            cssstyles=sco_groups_view.CSSSTYLES,
+            init_qtip=True,
+        ),
+        """<p class="help">Cette page permet de générer et d'archiver tous
+les documents résultant de ce semestre: PV de jury, lettres individuelles, 
+tableaux récapitulatifs.</p><p class="help">Les documents archivés sont 
+enregistrés et non modifiables, on peut les retrouver ultérieurement.
+</p><p class="help">On peut archiver plusieurs versions des documents (avant et après le jury par exemple).
+</p>
+        """,
+    ]
+    F = [
+        """<p><em>Note: les documents sont aussi affectés par les réglages sur la page "<a href="edit_preferences">Paramétrage</a>" (accessible à l'administrateur du département).</em>
+        </p>""",
+        context.sco_footer(REQUEST),
+    ]
+
+    descr = [
+        (
+            "description",
+            {"input_type": "textarea", "rows": 4, "cols": 77, "title": "Description"},
+        ),
+        ("sep", {"input_type": "separator", "title": "Informations sur PV de jury"}),
+    ]
+    descr += sco_pvjury.descrform_pvjury(sem)
+    descr += [
+        (
+            "signature",
+            {
+                "input_type": "file",
+                "size": 30,
+                "explanation": "optionnel: image scannée de la signature pour les lettres individuelles",
+            },
+        ),
+        (
+            "bulVersion",
+            {
+                "input_type": "menu",
+                "title": "Version des bulletins archivés",
+                "labels": [
+                    "Version courte",
+                    "Version intermédiaire",
+                    "Version complète",
+                ],
+                "allowed_values": ["short", "selectedevals", "long"],
+                "default": "long",
+            },
+        ),
+    ]
+    menu_choix_groupe = (
+        """<div class="group_ids_sel_menu">Groupes d'étudiants à lister: """
+        + sco_groups_view.menu_groups_choice(context, groups_infos)
+        + """(pour les PV et lettres)</div>"""
+    )
+
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        cancelbutton="Annuler",
+        method="POST",
+        submitlabel="Générer et archiver les documents",
+        name="tf",
+        formid="group_selector",
+        html_foot_markup=menu_choix_groupe,
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + "\n" + tf[1] + "\n".join(F)
+    elif tf[0] == -1:
+        msg = "Opération%20annulée"
+    else:
+        # submit
+        sf = tf[2]["signature"]
+        signature = sf.read()  # image of signature
+        if tf[2]["anonymous"]:
+            tf[2]["anonymous"] = True
+        else:
+            tf[2]["anonymous"] = False
+        do_formsemestre_archive(
+            context,
+            REQUEST,
+            formsemestre_id,
+            group_ids=group_ids,
+            description=tf[2]["description"],
+            date_jury=tf[2]["date_jury"],
+            date_commission=tf[2]["date_commission"],
+            signature=signature,
+            numeroArrete=tf[2]["numeroArrete"],
+            VDICode=tf[2]["VDICode"],
+            showTitle=tf[2]["showTitle"],
+            anonymous=tf[2]["anonymous"],
+            bulVersion=tf[2]["bulVersion"],
+        )
+        msg = "Nouvelle%20archive%20créée"
+
+    # submitted or cancelled:
+    return REQUEST.RESPONSE.redirect(
+        "formsemestre_list_archives?formsemestre_id=%s&amp;head_message=%s"
+        % (formsemestre_id, msg)
+    )
+
+
+def formsemestre_list_archives(context, REQUEST, formsemestre_id):
+    """Page listing archives
+    """
+    L = []
+    for archive_id in PVArchive.list_obj_archives(context, formsemestre_id):
+        a = {
+            "archive_id": archive_id,
+            "description": PVArchive.get_archive_description(archive_id),
+            "date": PVArchive.get_archive_date(archive_id),
+            "content": PVArchive.list_archive(archive_id),
+        }
+        L.append(a)
+
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    H = [context.html_sem_header(REQUEST, "Archive des PV et résultats ", sem)]
+    if not L:
+        H.append("<p>aucune archive enregistrée</p>")
+    else:
+        H.append("<ul>")
+        for a in L:
+            archive_name = PVArchive.get_archive_name(a["archive_id"])
+            H.append(
+                '<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&amp;archive_name=%s">supprimer</a>)<ul>'
+                % (
+                    a["date"].strftime("%d/%m/%Y %H:%M"),
+                    a["description"],
+                    formsemestre_id,
+                    archive_name,
+                )
+            )
+            for filename in a["content"]:
+                H.append(
+                    '<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&amp;archive_name=%s&amp;filename=%s">%s</a></li>'
+                    % (formsemestre_id, archive_name, filename, filename)
+                )
+            if not a["content"]:
+                H.append("<li><em>aucun fichier !</em></li>")
+            H.append("</ul></li>")
+        H.append("</ul>")
+
+    return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+def formsemestre_get_archived_file(
+    context, REQUEST, formsemestre_id, archive_name, filename
+):
+    """Send file to client.
+    """
+    return PVArchive.get_archived_file(
+        context, REQUEST, formsemestre_id, archive_name, filename
+    )
+
+
+def formsemestre_delete_archive(
+    context, REQUEST, formsemestre_id, archive_name, dialog_confirmed=False
+):
+    """Delete an archive
+    """
+    if not context._can_edit_pv(REQUEST, formsemestre_id):
+        raise AccessDenied(
+            "opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
+        )
+    sem = sco_formsemestre.get_formsemestre(
+        context, formsemestre_id
+    )  # check formsemestre_id
+    archive_id = PVArchive.get_id_from_name(context, formsemestre_id, archive_name)
+
+    dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id)
+
+    if not dialog_confirmed:
+        return context.confirmDialog(
+            """<h2>Confirmer la suppression de l'archive du %s ?</h2>
+               <p>La suppression sera définitive.</p>"""
+            % PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"),
+            dest_url="",
+            REQUEST=REQUEST,
+            cancel_url=dest_url,
+            parameters={
+                "formsemestre_id": formsemestre_id,
+                "archive_name": archive_name,
+            },
+        )
+
+    PVArchive.delete_archive(archive_id)
+    return REQUEST.RESPONSE.redirect(dest_url + "&amp;head_message=Archive%20supprimée")
diff --git a/sco_archives_etud.py b/sco_archives_etud.py
new file mode 100644
index 0000000000000000000000000000000000000000..86a19b4c0d8b45065b900adfff6ff5bec3de0775
--- /dev/null
+++ b/sco_archives_etud.py
@@ -0,0 +1,332 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""ScoDoc : gestion des fichiers archivés associés aux étudiants
+     Il s'agit de fichiers quelconques, généralement utilisés pour conserver
+     les dossiers d'admission et autres pièces utiles. 
+"""
+
+from sco_utils import *
+from notes_log import log
+import ImportScolars
+import sco_formsemestre
+import sco_groups
+import sco_trombino
+import sco_excel
+import sco_archives
+
+
+class EtudsArchiver(sco_archives.BaseArchiver):
+    def __init__(self):
+        sco_archives.BaseArchiver.__init__(self, archive_type="docetuds")
+
+
+EtudsArchive = EtudsArchiver()
+
+
+def can_edit_etud_archive(context, authuser):
+    """True si l'utilisateur peut modifier les archives etudiantes
+    """
+    return authuser.has_permission(ScoEtudAddAnnotations, context)
+
+
+def etud_list_archives_html(context, REQUEST, etudid):
+    """HTML snippet listing archives
+    """
+    can_edit = can_edit_etud_archive(context, REQUEST.AUTHENTICATED_USER)
+    L = []
+    for archive_id in EtudsArchive.list_obj_archives(context, etudid):
+        a = {
+            "archive_id": archive_id,
+            "description": EtudsArchive.get_archive_description(archive_id),
+            "date": EtudsArchive.get_archive_date(archive_id),
+            "content": EtudsArchive.list_archive(archive_id),
+        }
+        L.append(a)
+    delete_icon = icontag(
+        "delete_small_img", title="Supprimer fichier", alt="supprimer"
+    )
+    delete_disabled_icon = icontag(
+        "delete_small_dis_img", title="Suppression non autorisée"
+    )
+    H = ['<div class="etudarchive"><ul>']
+    for a in L:
+        archive_name = EtudsArchive.get_archive_name(a["archive_id"])
+        H.append(
+            """<li><span class ="etudarchive_descr" title="%s">%s</span>"""
+            % (a["date"].strftime("%d/%m/%Y %H:%M"), a["description"])
+        )
+        for filename in a["content"]:
+            H.append(
+                """<a class="stdlink etudarchive_link" href="etud_get_archived_file?etudid=%s&amp;archive_name=%s&amp;filename=%s">%s</a>"""
+                % (etudid, archive_name, filename, filename)
+            )
+        if not a["content"]:
+            H.append("<em>aucun fichier !</em>")
+        if can_edit:
+            H.append(
+                '<span class="deletudarchive"><a class="smallbutton" href="etud_delete_archive?etudid=%s&amp;archive_name=%s">%s</a></span>'
+                % (etudid, archive_name, delete_icon)
+            )
+        else:
+            H.append('<span class="deletudarchive">' + delete_disabled_icon + "</span>")
+        H.append("</li>")
+    if can_edit:
+        H.append(
+            '<li class="addetudarchive"><a class="stdlink" href="etud_upload_file_form?etudid=%s">ajouter un fichier</a></li>'
+            % etudid
+        )
+    H.append("</ul></div>")
+    return "".join(H)
+
+
+def add_archives_info_to_etud_list(context, etuds):
+    """Add key 'etudarchive' describing archive of etuds
+    (used to list all archives of a group)
+    """
+    for etud in etuds:
+        l = []
+        for archive_id in EtudsArchive.list_obj_archives(context, etud["etudid"]):
+            l.append(
+                "%s (%s)"
+                % (
+                    EtudsArchive.get_archive_description(archive_id),
+                    EtudsArchive.list_archive(archive_id)[0],
+                )
+            )
+            etud["etudarchive"] = ", ".join(l)
+
+
+def etud_upload_file_form(context, REQUEST, etudid):
+    """Page with a form to choose and upload a file, with a description.
+    """
+    # check permission
+    if not can_edit_etud_archive(context, REQUEST.AUTHENTICATED_USER):
+        raise AccessDenied(
+            "opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
+        )
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Chargement d'un document associé à %(nomprenom)s" % etud,
+        ),
+        """<h2>Chargement d'un document associé à %(nomprenom)s</h2>                     
+         """
+        % etud,
+        """<p>Le fichier ne doit pas dépasser %sMo.</p>             
+         """
+        % (CONFIG.ETUD_MAX_FILE_SIZE / (1024 * 1024)),
+    ]
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("etudid", {"default": etudid, "input_type": "hidden"}),
+            ("datafile", {"input_type": "file", "title": "Fichier", "size": 30}),
+            (
+                "description",
+                {
+                    "input_type": "textarea",
+                    "rows": 4,
+                    "cols": 77,
+                    "title": "Description",
+                },
+            ),
+        ),
+        submitlabel="Valider",
+        cancelbutton="Annuler",
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(REQUEST.URL1 + "/ficheEtud?etudid=" + etudid)
+    else:
+        data = tf[2]["datafile"].read()
+        descr = tf[2]["description"]
+        filename = tf[2]["datafile"].filename
+        _store_etud_file_to_new_archive(
+            context, REQUEST, etudid, data, filename, description=descr
+        )
+        return REQUEST.RESPONSE.redirect(REQUEST.URL1 + "/ficheEtud?etudid=" + etudid)
+
+
+def _store_etud_file_to_new_archive(
+    context, REQUEST, etudid, data, filename, description=""
+):
+    """Store data to new archive.
+    """
+    filesize = len(data)
+    if filesize < 10 or filesize > CONFIG.ETUD_MAX_FILE_SIZE:
+        return 0, "Fichier image de taille invalide ! (%d)" % filesize
+    archive_id = EtudsArchive.create_obj_archive(context, etudid, description)
+    EtudsArchive.store(archive_id, filename, data)
+
+
+def etud_delete_archive(context, REQUEST, etudid, archive_name, dialog_confirmed=False):
+    """Delete an archive
+    """
+    # check permission
+    if not can_edit_etud_archive(context, REQUEST.AUTHENTICATED_USER):
+        raise AccessDenied(
+            "opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
+        )
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    archive_id = EtudsArchive.get_id_from_name(context, etudid, archive_name)
+    dest_url = "ficheEtud?etudid=%s" % etudid
+    if not dialog_confirmed:
+        return context.confirmDialog(
+            """<h2>Confirmer la suppression des fichiers ?</h2>
+            <p>Fichier associé le %s à l'étudiant %s</p>
+               <p>La suppression sera définitive.</p>"""
+            % (
+                EtudsArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"),
+                etud["nomprenom"],
+            ),
+            dest_url="",
+            REQUEST=REQUEST,
+            cancel_url=dest_url,
+            parameters={"etudid": etudid, "archive_name": archive_name},
+        )
+
+    EtudsArchive.delete_archive(archive_id)
+    return REQUEST.RESPONSE.redirect(dest_url + "&amp;head_message=Archive%20supprimée")
+
+
+def etud_get_archived_file(context, REQUEST, etudid, archive_name, filename):
+    """Send file to client.
+    """
+    return EtudsArchive.get_archived_file(
+        context, REQUEST, etudid, archive_name, filename
+    )
+
+
+# --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants)
+def etudarchive_generate_excel_sample(context, group_id=None, REQUEST=None):
+    """Feuille excel pour import fichiers etudiants (utilisé pour admissions)
+    """
+    fmt = ImportScolars.sco_import_format()
+    data = ImportScolars.sco_import_generate_excel_sample(
+        fmt,
+        context=context,
+        group_ids=[group_id],
+        only_tables=["identite"],
+        exclude_cols=[
+            "date_naissance",
+            "lieu_naissance",
+            "nationalite",
+            "statut",
+            "photo_filename",
+        ],
+        extra_cols=["fichier_a_charger"],
+        REQUEST=REQUEST,
+    )
+    return sco_excel.sendExcelFile(REQUEST, data, "ImportFichiersEtudiants.xls")
+
+
+def etudarchive_import_files_form(context, group_id, REQUEST=None):
+    """Formualaire pour importation fichiers d'un groupe
+    """
+    H = [
+        context.sco_header(
+            REQUEST, page_title="Import de fichiers associés aux étudiants"
+        ),
+        """<h2 class="formsemestre">Téléchargement de fichier associés aux étudiants</h2>
+        <p>Les fichiers associés (dossiers d'admission, certificats, ...), de types quelconques (pdf, doc, images)
+        sont accessibles aux utilisateurs via la fiche individuelle de l'étudiant.
+        </p>
+        <p class="warning">Ne pas confondre avec les photos des étudiants, qui se chargent via l'onglet "Photos".</p>
+         <p><b>Vous pouvez aussi charger à tout moment de nouveaux fichiers, ou en supprimer, via la fiche de chaque étudiant.</b></p>
+         <p class="help">Cette page permet de charger en une seule fois les fichiers de plusieurs étudiants.<br/>
+          Il faut d'abord remplir une feuille excel donnant les noms 
+          des fichiers (un fichier par étudiant).
+         </p>
+         <p class="help">Ensuite, réunir vos fichiers dans un fichier zip, puis télécharger 
+         simultanément le fichier excel et le fichier zip.
+         </p>
+        <ol>
+        <li><a class="stdlink" href="etudarchive_generate_excel_sample?group_id=%s">
+        Obtenir la feuille excel à remplir</a>
+        </li>
+        <li style="padding-top: 2em;">
+         """
+        % group_id,
+    ]
+    F = context.sco_footer(REQUEST)
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("xlsfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}),
+            ("zipfile", {"title": "Fichier zip:", "input_type": "file", "size": 40}),
+            (
+                "description",
+                {
+                    "input_type": "textarea",
+                    "rows": 4,
+                    "cols": 77,
+                    "title": "Description",
+                },
+            ),
+            ("group_id", {"input_type": "hidden"}),
+        ),
+    )
+
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + "</li></ol>" + F
+    elif tf[0] == -1:
+        # retrouve le semestre à partir du groupe:
+        g = sco_groups.get_group(context, group_id)
+        return REQUEST.RESPONSE.redirect(
+            "formsemestre_status?formsemestre_id=" + g["formsemestre_id"]
+        )
+    else:
+        return etudarchive_import_files(
+            context,
+            group_id=tf[2]["group_id"],
+            xlsfile=tf[2]["xlsfile"],
+            zipfile=tf[2]["zipfile"],
+            REQUEST=REQUEST,
+            description=tf[2]["description"],
+        )
+
+
+def etudarchive_import_files(
+    context, group_id=None, xlsfile=None, zipfile=None, REQUEST=None, description=""
+):
+    def callback(context, etud, data, filename, REQUEST):
+        _store_etud_file_to_new_archive(
+            context, REQUEST, etud["etudid"], data, filename, description
+        )
+
+    filename_title = "fichier_a_charger"
+    page_title = "Téléchargement de fichiers associés aux étudiants"
+    # Utilise la fontion au depart developpee pour les photos
+    r = sco_trombino.zip_excel_import_files(
+        context, xlsfile, zipfile, REQUEST, callback, filename_title, page_title
+    )
+    return r + context.sco_footer(REQUEST)
diff --git a/sco_bac.py b/sco_bac.py
new file mode 100644
index 0000000000000000000000000000000000000000..12ca3e5216c2f55ef9226c74823dbb5b1040df8a
--- /dev/null
+++ b/sco_bac.py
@@ -0,0 +1,165 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Bacs: noms de séries et spécialités, nomenclatures, abbréviations
+
+Types prédéfinis:
+G bacs généraux (S, L, ES, ...)
+T bacs technologiques (STI2D, STG, ...)
+P bacs professionnels
+E diplômes étrangers (équivalences)
+X divers
+"""
+
+_BACS = (  # tuples (bac, specialite, abbrev, type
+    # --- Reçus d'APB de 2013 à 2015 (tel qu'observé à IUTV)
+    #  merci d'envoyer vos mises à jour
+    ("A1 LETTRES-SCIENCES", "", "A1", "G"),
+    ("B ECONOMIQUE ET SOCIAL", "", "B", "G"),
+    ("B", "ÉCONOMIQUE ET SOCIAL", "B", "G"),
+    ("C MATHÉMATIQUES ET SCIENCES PHYSIQUES", "", "C", "G"),
+    ("C MATHEMATIQUES ET SCIENCES PHYSIQUES", "", "C", "G"),
+    ("D", "", "D", "G"),
+    ("E", "", "E", "G"),
+    ("L LITTERATURE", "", "L", "G"),
+    ("L LITTÉRATURE", "", "L", "G"),
+    ("L LITTÉRATURE", "LITTÉRATURE", "L", "G"),
+    ("L", "LITTÉRATURE", "L", "G"),
+    ("L", "", "L", "G"),
+    ("S", "SCIENTIFIQUE", "S", "G"),
+    ("S SCIENTIFIQUE", "", "S", "G"),
+    ("S SCIENTIFIQUE", "SCIENTIFIQUE", "S", "G"),
+    ("ES ECONOMIQUE ET SOCIAL", "", "ES", "G"),
+    ("ES ECONOMIQUE ET SOCIAL", "ECONOMIQUE ET SOCIAL", "ES", "G"),
+    ("ES", "ECONOMIQUE ET SOCIAL", "ES", "G"),
+    ("0000 SANS BAC", "", "SANS", "X"),
+    ("0001 BAC INTERNATIONAL", "", "Int.", "X"),
+    ("0021 BACS PROFESSIONNELS INDUSTRIELS", "", "Pro I", "P"),
+    ("0021", "BACS PROFESSIONNELS INDUSTRIELS", "Pro I", "P"),
+    ("0022 BACS PROFESSIONNELS TERTIAIRES", "", "Pro T", "P"),
+    ("0022", "BACS PROFESSIONNELS TERTIAIRES", "Pro T", "P"),
+    ("0030 CAPACITE DE DROIT", "", "C.D.", "X"),
+    ("0030 CAPACITÉ DE DROIT", "", "C.D.", "X"),
+    ("0031 TITRE ÉTRANGER ADMIS EN ÉQUIVALENCE", "", "Etr.", "E"),  # accentué
+    ("0031 TITRE ETRANGER ADMIS EN EQUIVALENCE", "", "Etr.", "E"),  # non acc
+    (
+        "0031 TITRE ETRANGER ADMIS EN EQUIVALENCE",
+        "TITRE ETRANGER ADMIS EN EQUIVALENCE",
+        "Etr.",
+        "E",
+    ),
+    ("0031", "TITRE ÉTRANGER ADMIS EN ÉQUIVALENCE", "Etr.", "E"),
+    ("31", "", "Etr.", "E"),
+    ("0032 TITRE FRANCAIS ADMIS EN DISPENSE", "", "Disp.", "X"),
+    ("0032", "TITRE FRANCAIS ADMIS EN DISPENSE", "Disp.", "X"),
+    ("0033 DAEU A OU ESEU A", "", "DAEU", "X"),
+    ("0034 DAEU B OU ESEU B", "", "DAEU", "X"),
+    ("0036 VALIDATION ETUDES EXPERIENCES PROF.", "", "VAE", "X"),
+    ("0036", "VALIDATION ÉTUDES EXPÉRIENCES PROF.", "VAE", "X"),
+    ("0037 AUTRES CAS DE NON BACHELIERS", "", "Non", "X"),
+    ("ST DE L'AGRONOMIE ET DU VIVANT", "", "STAV", "T"),
+    ("ST DE L'INDUSTRIE ET DU DEVT DURABLE", "", "STI2D", "T"),
+    ("ST DU MANAGEMENT ET DE LA GESTION", "", "STMG", "T"),
+    ("ST2S SCIENCES ET TECHNO SANTE ET SOCIAL", "", "ST2S", "T"),
+    ("ST2S", "SCIENCES ET TECHNO SANTÉ ET SOCIAL", "ST2S", "T"),
+    ("STI SCIENCES ET TECHNIQUES INDUSTRIELLES", "", "STI", "T"),
+    (
+        "STI SCIENCES ET TECHNIQUES INDUSTRIELLES",
+        "SCIENCES ET TECHNIQUES INDUSTRIELLES",
+        "STI",
+        "T",
+    ),
+    ("STI", "SCIENCES ET TECHNIQUES INDUSTRIELLES", "STI", "T"),
+    ("STG SCIENCES ET TECHNOLOGIES DE GESTION", "", "STG", "T"),
+    (
+        "STG SCIENCES ET TECHNOLOGIES DE GESTION",
+        "SCIENCES ET TECHNOLOGIES DE GESTION",
+        "STG",
+        "T",
+    ),
+    ("STG", "SCIENCES ET TECHNOLOGIES DE GESTION", "STG", "T"),
+    ("STL", "SCIENCES ET TECHNO. DE LABORATOIRE", "STL", "T"),
+    ("STL SCIENCES ET TECHNO. DE LABORATOIRE", "", "STL", "T"),
+    ("STT SCIENCES ET TECHNOLOGIES TERTIAIRES", "", "STT", "T"),
+    ("STT", "SCIENCES ET TECHNOLOGIES TERTIAIRES", "STT", "T"),
+    ("STT", "", "STT", "T"),
+    ("F3", "", "F3", "T"),
+    ("SMS SCIENCES MEDICO-SOCIALES", "", "SMS", "T"),
+    ("SMS", "SCIENCES MÉDICO-SOCIALES", "SMS", "T"),
+    ("G1 TECHNIQUES ADMINISTRATIVES", "", "G1", "T"),
+    ("G2", "", "G2", "T"),
+    ("G2 TECHNIQUES QUANTITATIVES DE GESTION", "", "G2", "T"),
+    (
+        "G2 TECHNIQUES QUANTITATIVES DE GESTION",
+        "TECHNIQUES QUANTITATIVES DE GESTION",
+        "G2",
+        "T",
+    ),
+    ("G3", "TECHNIQUES COMMERCIALES", "G3", "T"),
+    ("HOT", "HÔTELLERIE", "HOT", "P"),
+)
+
+# { (bac, specialite) : (abbrev, type) }
+BACS_SSP = {(t[0], t[1]): t[2:] for t in _BACS}
+
+# bac :  (abbrev, type) (retient la derniere)
+BACS_S = {t[0]: t[2:] for t in _BACS}
+
+
+class Baccalaureat:
+    def __init__(self, bac, specialite=""):
+        self.bac = bac
+        self.specialite = specialite
+        self._abbrev, self._type = BACS_SSP.get((bac, specialite), (None, None))
+        # Parfois, la specialite commence par la serie: essaye
+        if self._type is None and specialite and specialite.startswith(bac):
+            specialite = specialite[len(bac) :].strip(" -")
+            self._abbrev, self._type = BACS_SSP.get((bac, specialite), (None, None))
+        # Cherche la forme serie specialite
+        if self._type is None and specialite:
+            self._abbrev, self._type = BACS_S.get(bac + " " + specialite, (None, None))
+        # Cherche avec juste le bac, sans specialite
+        if self._type is None:
+            self._abbrev, self._type = BACS_S.get(bac, (None, None))
+
+    def abbrev(self):
+        "abbreviation for this bac"
+        if self._abbrev is None:
+            return (
+                self.bac
+            )  # could try to build an abbrev, either from bac/specialite or using a user-supplied lookup table (not implemented)
+        return self._abbrev
+
+    def type(self):
+        "type de bac (une lettre: G, T, P, E, X)"
+        return self._type or "X"
+
+    def is_general(self):
+        return self.type() == "G"
+
+    def is_techno(self):
+        return selt.type() == "T"
diff --git a/sco_bulletins.py b/sco_bulletins.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed4a19d565ef3941865a41ba314f9c28b5431240
--- /dev/null
+++ b/sco_bulletins.py
@@ -0,0 +1,1264 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Génération des bulletins de notes
+
+"""
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email.MIMEBase import MIMEBase
+from email.Header import Header
+from email import Encoders
+
+import htmlutils, time
+from reportlab.lib.colors import Color
+
+from sco_utils import *
+from notes_table import *
+import sco_formsemestre
+import sco_groups
+import sco_pvjury
+import sco_formsemestre_status
+import sco_photos
+import ZAbsences
+import sco_abs_views
+import sco_preferences
+import sco_codes_parcours
+import sco_bulletins_generator
+import sco_bulletins_xml
+import sco_bulletins_json
+
+
+def make_context_dict(context, sem, etud):
+    """Construit dictionnaire avec valeurs pour substitution des textes
+    (preferences bul_pdf_*)
+    """
+    C = sem.copy()
+    C["responsable"] = " ,".join(
+        [
+            context.Users.user_info(user_name=responsable_id)["prenomnom"]
+            for responsable_id in sem["responsables"]
+        ]
+    )
+
+    annee_debut = sem["date_debut"].split("/")[2]
+    annee_fin = sem["date_fin"].split("/")[2]
+    if annee_debut != annee_fin:
+        annee = "%s - %s" % (annee_debut, annee_fin)
+    else:
+        annee = annee_debut
+    C["anneesem"] = annee
+    C.update(etud)
+    # copie preferences
+    for name in sco_preferences.PREFS_NAMES:
+        C[name] = context.get_preference(name, sem["formsemestre_id"])
+
+    # ajoute groupes et group_0, group_1, ...
+    sco_groups.etud_add_group_infos(context, etud, sem)
+    C["groupes"] = etud["groupes"]
+    n = 0
+    for partition_id in etud["partitions"]:
+        C["group_%d" % n] = etud["partitions"][partition_id]["group_name"]
+        n += 1
+
+    # ajoute date courante
+    t = time.localtime()
+    C["date_dmy"] = time.strftime("%d/%m/%Y", t)
+    C["date_iso"] = time.strftime("%Y-%m-%d", t)
+
+    return C
+
+
+def formsemestre_bulletinetud_dict(
+    context, formsemestre_id, etudid, version="long", REQUEST=None
+):
+    """Collecte informations pour bulletin de notes
+    Retourne un dictionnaire (avec valeur par défaut chaine vide).
+    Le contenu du dictionnaire dépend des options (rangs, ...) 
+    et de la version choisie (short, long, selectedevals).
+
+    Cette fonction est utilisée pour les bulletins HTML et PDF, mais pas ceux en XML.
+    """
+    if not version in ("short", "long", "selectedevals"):
+        raise ValueError("invalid version code !")
+
+    prefs = context.get_preferences(formsemestre_id)
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > toutes notes
+
+    I = DictDefault(defaultvalue="")
+    I["etudid"] = etudid
+    I["formsemestre_id"] = formsemestre_id
+    I["sem"] = nt.sem
+    if REQUEST:
+        I["server_name"] = REQUEST.BASE0
+    else:
+        I["server_name"] = ""
+
+    # Formation et parcours
+    I["formation"] = context.formation_list(
+        args={"formation_id": I["sem"]["formation_id"]}
+    )[0]
+    I["parcours"] = sco_codes_parcours.get_parcours_from_code(
+        I["formation"]["type_parcours"]
+    )
+    # Infos sur l'etudiant
+    I["etud"] = context.getEtudInfo(etudid=etudid, filled=1)[0]
+    I["descr_situation"] = I["etud"]["inscriptionstr"]
+    if I["etud"]["inscription_formsemestre_id"]:
+        I["descr_situation_html"] = (
+            """<a href="formsemestre_status?formsemestre_id=%s">%s</a>"""
+            % (I["etud"]["inscription_formsemestre_id"], I["descr_situation"])
+        )
+    else:
+        I["descr_situation_html"] = I["descr_situation"]
+    # Groupes:
+    partitions = sco_groups.get_partitions_list(
+        context, formsemestre_id, with_default=False
+    )
+    partitions_etud_groups = {}  # { partition_id : { etudid : group } }
+    for partition in partitions:
+        pid = partition["partition_id"]
+        partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(
+            context, pid
+        )
+    # --- Absences
+    AbsSemEtud = ZAbsences.getAbsSemEtud(context, nt.sem, etudid)
+    I["nbabs"] = AbsSemEtud.CountAbs()
+    I["nbabsjust"] = AbsSemEtud.CountAbsJust()
+
+    # --- Decision Jury
+    infos, dpv = etud_descr_situation_semestre(
+        context,
+        etudid,
+        formsemestre_id,
+        format="html",
+        show_date_inscr=prefs["bul_show_date_inscr"],
+        show_decisions=prefs["bul_show_decision"],
+        show_uevalid=prefs["bul_show_uevalid"],
+        show_mention=prefs["bul_show_mention"],
+    )
+
+    if dpv:
+        I["decision_sem"] = dpv["decisions"][0]["decision_sem"]
+    else:
+        I["decision_sem"] = ""
+    I.update(infos)
+
+    I["etud_etat_html"] = nt.get_etud_etat_html(etudid)
+    I["etud_etat"] = nt.get_etud_etat(etudid)
+    I["filigranne"] = ""
+    I["demission"] = ""
+    if I["etud_etat"] == "D":
+        I["demission"] = "(Démission)"
+        I["filigranne"] = "Démission"
+    elif I["etud_etat"] == DEF:
+        I["demission"] = "(Défaillant)"
+        I["filigranne"] = "Défaillant"
+    elif (prefs["bul_show_temporary"] and not I["decision_sem"]) or prefs[
+        "bul_show_temporary_forced"
+    ]:
+        I["filigranne"] = prefs["bul_temporary_txt"]
+
+    # --- Appreciations
+    cnx = context.GetDBConnexion()
+    apprecs = scolars.appreciations_list(
+        cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
+    )
+    I["appreciations_list"] = apprecs
+    I["appreciations_txt"] = [x["date"] + ": " + x["comment"] for x in apprecs]
+    I["appreciations"] = I[
+        "appreciations_txt"
+    ]  # deprecated / keep it for backward compat in templates
+
+    # --- Notes
+    ues = nt.get_ues()
+    modimpls = nt.get_modimpls()
+    moy_gen = nt.get_etud_moy_gen(etudid)
+    I["nb_inscrits"] = len(nt.rangs)
+    I["moy_gen"] = fmt_note(moy_gen)
+    I["moy_min"] = fmt_note(nt.moy_min)
+    I["moy_max"] = fmt_note(nt.moy_max)
+    I["mention"] = ""
+    if dpv:
+        decision_sem = dpv["decisions"][0]["decision_sem"]
+        if decision_sem and sco_codes_parcours.code_semestre_validant(
+            decision_sem["code"]
+        ):
+            I["mention"] = get_mention(moy_gen)
+
+    if dpv and dpv["decisions"][0]:
+        I["sum_ects"] = dpv["decisions"][0]["sum_ects"]
+        I["sum_ects_capitalises"] = dpv["decisions"][0]["sum_ects_capitalises"]
+    else:
+        I["sum_ects"] = 0
+        I["sum_ects_capitalises"] = 0
+    I["moy_moy"] = fmt_note(nt.moy_moy)  # moyenne des moyennes generales
+    if type(moy_gen) != StringType and type(nt.moy_moy) != StringType:
+        I["moy_gen_bargraph_html"] = "&nbsp;" + htmlutils.horizontal_bargraph(
+            moy_gen * 5, nt.moy_moy * 5
+        )
+    else:
+        I["moy_gen_bargraph_html"] = ""
+
+    if prefs["bul_show_rangs"]:
+        rang = str(nt.get_etud_rang(etudid))
+    else:
+        rang = ""
+
+    rang_gr, ninscrits_gr, gr_name = get_etud_rangs_groups(
+        context, etudid, formsemestre_id, partitions, partitions_etud_groups, nt
+    )
+
+    if nt.get_moduleimpls_attente():
+        # n'affiche pas le rang sur le bulletin s'il y a des
+        # notes en attente dans ce semestre
+        rang = RANG_ATTENTE_STR
+        rang_gr = DictDefault(defaultvalue=RANG_ATTENTE_STR)
+    I["rang"] = rang
+    I["rang_gr"] = rang_gr
+    I["gr_name"] = gr_name
+    I["ninscrits_gr"] = ninscrits_gr
+    I["nbetuds"] = len(nt.rangs)
+    I["nb_demissions"] = nt.nb_demissions
+    I["nb_defaillants"] = nt.nb_defaillants
+    if prefs["bul_show_rangs"]:
+        I["rang_nt"] = "%s / %d" % (
+            rang,
+            I["nbetuds"] - nt.nb_demissions - nt.nb_defaillants,
+        )
+        I["rang_txt"] = "Rang " + I["rang_nt"]
+    else:
+        I["rang_nt"], I["rang_txt"] = "", ""
+    I["note_max"] = 20.0  # notes toujours sur 20
+    I["bonus_sport_culture"] = nt.bonus[etudid]
+    # Liste les UE / modules /evals
+    I["ues"] = []
+    I["matieres_modules"] = {}
+    I["matieres_modules_capitalized"] = {}
+    for ue in ues:
+        u = ue.copy()
+        ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
+        u["ue_status"] = ue_status  # { 'moy', 'coef_ue', ...}
+        if ue["type"] != UE_SPORT:
+            u["cur_moy_ue_txt"] = fmt_note(ue_status["cur_moy_ue"])
+        else:
+            x = fmt_note(nt.bonus[etudid], keep_numeric=True)
+            if type(x) == StringType:
+                u["cur_moy_ue_txt"] = "pas de bonus"
+            else:
+                u["cur_moy_ue_txt"] = "bonus de %.3g points" % x
+        u["moy_ue_txt"] = fmt_note(ue_status["moy"])
+        if ue_status["coef_ue"] != None:
+            u["coef_ue_txt"] = fmt_coef(ue_status["coef_ue"])
+        else:
+            # C'est un bug:
+            log("u=" + pprint.pformat(u))
+            raise Exception("invalid None coef for ue")
+
+        if (
+            dpv
+            and dpv["decisions"][0]["decisions_ue"]
+            and ue["ue_id"] in dpv["decisions"][0]["decisions_ue"]
+        ):
+            u["ects"] = dpv["decisions"][0]["decisions_ue"][ue["ue_id"]]["ects"]
+            if ue["type"] == UE_ELECTIVE:
+                u["ects"] = (
+                    "%g+" % u["ects"]
+                )  # ajoute un "+" pour indiquer ECTS d'une UE élective
+        else:
+            if ue_status["is_capitalized"]:
+                u["ects"] = ue_status["ue"].get("ects", "-")
+            else:
+                u["ects"] = "-"
+        modules, ue_attente = _ue_mod_bulletin(
+            context, etudid, formsemestre_id, ue["ue_id"], modimpls, nt, version
+        )
+        #
+        u["modules"] = modules  # detail des modules de l'UE (dans le semestre courant)
+        # auparavant on filtrait les modules sans notes
+        #   si ue_status['cur_moy_ue'] != 'NA' alors u['modules'] = [] (pas de moyenne => pas de modules)
+
+        u[
+            "modules_capitalized"
+        ] = []  # modules de l'UE capitalisée (liste vide si pas capitalisée)
+        if ue_status["is_capitalized"]:
+            sem_origin = sco_formsemestre.get_formsemestre(
+                context, ue_status["formsemestre_id"]
+            )
+            u["ue_descr_txt"] = "Capitalisée le %s" % DateISOtoDMY(
+                ue_status["event_date"]
+            )
+            u["ue_descr_html"] = (
+                '<a href="formsemestre_bulletinetud?formsemestre_id=%s&amp;etudid=%s" title="%s" class="bull_link">%s</a>'
+                % (
+                    sem_origin["formsemestre_id"],
+                    etudid,
+                    sem_origin["titreannee"],
+                    u["ue_descr_txt"],
+                )
+            )
+            # log('cap details   %s' % ue_status['moy'])
+            if ue_status["moy"] != "NA" and ue_status["formsemestre_id"]:
+                # detail des modules de l'UE capitalisee
+                nt_cap = context._getNotesCache().get_NotesTable(
+                    context, ue_status["formsemestre_id"]
+                )  # > toutes notes
+
+                u["modules_capitalized"], junk = _ue_mod_bulletin(
+                    context,
+                    etudid,
+                    formsemestre_id,
+                    ue_status["capitalized_ue_id"],
+                    nt_cap.get_modimpls(),
+                    nt_cap,
+                    version,
+                )
+                I["matieres_modules_capitalized"].update(
+                    _sort_mod_by_matiere(u["modules_capitalized"], nt_cap, etudid)
+                )
+        else:
+            if prefs["bul_show_ue_rangs"] and ue["type"] != UE_SPORT:
+                if ue_attente:  # nt.get_moduleimpls_attente():
+                    u["ue_descr_txt"] = "%s/%s" % (
+                        RANG_ATTENTE_STR,
+                        nt.ue_rangs[ue["ue_id"]][1],
+                    )
+                else:
+                    u["ue_descr_txt"] = "%s/%s" % (
+                        nt.ue_rangs[ue["ue_id"]][0][etudid],
+                        nt.ue_rangs[ue["ue_id"]][1],
+                    )
+                u["ue_descr_html"] = u["ue_descr_txt"]
+            else:
+                u["ue_descr_txt"] = u["ue_descr_html"] = ""
+
+        if ue_status["is_capitalized"] or modules:
+            I["ues"].append(u)  # ne montre pas les UE si non inscrit
+
+        # Accès par matieres
+        I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))
+
+    #
+    C = make_context_dict(context, I["sem"], I["etud"])
+    C.update(I)
+    #
+    # log( 'C = \n%s\n' % pprint.pformat(C) ) # tres pratique pour voir toutes les infos dispo
+    return C
+
+
+def _sort_mod_by_matiere(modlist, nt, etudid):
+    matmod = {}  # { matiere_id : [] }
+    for mod in modlist:
+        matiere_id = mod["module"]["matiere_id"]
+        if matiere_id not in matmod:
+            moy = nt.get_etud_mat_moy(matiere_id, etudid)
+            matmod[matiere_id] = {
+                "titre": mod["mat"]["titre"],
+                "modules": mod,
+                "moy": moy,
+                "moy_txt": fmt_note(moy),
+            }
+    return matmod
+
+
+def _ue_mod_bulletin(context, etudid, formsemestre_id, ue_id, modimpls, nt, version):
+    """Infos sur les modules (et évaluations) dans une UE
+    (ajoute les informations aux modimpls)
+    Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit).
+    """
+    bul_show_mod_rangs = context.get_preference("bul_show_mod_rangs", formsemestre_id)
+    bul_show_abs_modules = context.get_preference(
+        "bul_show_abs_modules", formsemestre_id
+    )
+    if bul_show_abs_modules:
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        debut_sem = DateDMYtoISO(sem["date_debut"])
+        fin_sem = DateDMYtoISO(sem["date_fin"])
+
+    ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue_id]
+    mods = []  # result
+    ue_attente = False  # true si une eval en attente dans cette UE
+    for modimpl in ue_modimpls:
+        mod_attente = False
+        mod = modimpl.copy()
+        mod_moy = nt.get_etud_mod_moy(
+            modimpl["moduleimpl_id"], etudid
+        )  # peut etre 'NI'
+        is_malus = mod["module"]["module_type"] == MODULE_MALUS
+        if bul_show_abs_modules:
+            mod_abs = [
+                context.Absences.CountAbs(
+                    etudid=etudid,
+                    debut=debut_sem,
+                    fin=fin_sem,
+                    moduleimpl_id=modimpl["moduleimpl_id"],
+                ),
+                context.Absences.CountAbsJust(
+                    etudid=etudid,
+                    debut=debut_sem,
+                    fin=fin_sem,
+                    moduleimpl_id=modimpl["moduleimpl_id"],
+                ),
+            ]
+            mod["mod_abs_txt"] = fmt_abs(mod_abs)
+        else:
+            mod["mod_abs_txt"] = ""
+
+        mod["mod_moy_txt"] = fmt_note(mod_moy)
+        if mod["mod_moy_txt"][:2] == "NA":
+            mod["mod_moy_txt"] = "-"
+        if is_malus:
+            mod["mod_moy_txt"] = ""
+            mod["mod_coef_txt"] = ""
+        else:
+            mod["mod_coef_txt"] = fmt_coef(modimpl["module"]["coefficient"])
+        if mod["mod_moy_txt"] != "NI":  # ne montre pas les modules 'non inscrit'
+            mods.append(mod)
+            if is_malus:  # n'affiche pas les statistiques sur les modules malus
+                mod["stats"] = {
+                    "moy": "",
+                    "max": "",
+                    "min": "",
+                    "nb_notes": "",
+                    "nb_missing": "",
+                    "nb_valid_evals": "",
+                }
+            else:
+                mod["stats"] = nt.get_mod_stats(modimpl["moduleimpl_id"])
+            mod["mod_descr_txt"] = "Module %s, coef. %s (%s)" % (
+                modimpl["module"]["titre"],
+                fmt_coef(modimpl["module"]["coefficient"]),
+                context.Users.user_info(modimpl["responsable_id"])["nomcomplet"],
+            )
+            link_mod = (
+                '<a class="bull_link" href="moduleimpl_status?moduleimpl_id=%s" title="%s">'
+                % (modimpl["moduleimpl_id"], mod["mod_descr_txt"])
+            )
+            if context.get_preference("bul_show_codemodules", formsemestre_id):
+                mod["code"] = modimpl["module"]["code"]
+                mod["code_html"] = link_mod + mod["code"] + "</a>"
+            else:
+                mod["code"] = mod["code_html"] = ""
+            mod["name"] = (
+                modimpl["module"]["abbrev"] or modimpl["module"]["titre"] or ""
+            )
+            mod["name_html"] = link_mod + mod["name"] + "</a>"
+
+            mod_descr = "Module %s, coef. %s (%s)" % (
+                modimpl["module"]["titre"],
+                fmt_coef(modimpl["module"]["coefficient"]),
+                context.Users.user_info(modimpl["responsable_id"])["nomcomplet"],
+            )
+            link_mod = (
+                '<a class="bull_link" href="moduleimpl_status?moduleimpl_id=%s" title="%s">'
+                % (modimpl["moduleimpl_id"], mod_descr)
+            )
+            if context.get_preference("bul_show_codemodules", formsemestre_id):
+                mod["code_txt"] = modimpl["module"]["code"]
+                mod["code_html"] = link_mod + mod["code_txt"] + "</a>"
+            else:
+                mod["code_txt"] = ""
+                mod["code_html"] = ""
+            # Evaluations: notes de chaque eval
+            evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"])
+            mod["evaluations"] = []
+            for e in evals:
+                e = e.copy()
+                if int(e["visibulletin"]) == 1 or version == "long":
+                    # affiche "bonus" quand les points de malus sont négatifs
+                    if is_malus:
+                        val = e["notes"].get(etudid, {"value": "NP"})[
+                            "value"
+                        ]  # NA si etud demissionnaire
+                        if val > 0 or val == "NP":
+                            e["name"] = "Points de malus sur cette UE"
+                        else:
+                            e["name"] = "Points de bonus sur cette UE"
+                    else:
+                        e["name"] = e["description"] or "le %s" % e["jour"]
+                e["target_html"] = (
+                    "evaluation_listenotes?evaluation_id=%s&amp;format=html&amp;tf-submitted=1"
+                    % e["evaluation_id"]
+                )
+                e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
+                    e["target_html"],
+                    e["name"],
+                )
+                val = e["notes"].get(etudid, {"value": "NP"})[
+                    "value"
+                ]  # NA si etud demissionnaire
+                if val == "NP":
+                    e["note_txt"] = "nd"
+                    e["note_html"] = '<span class="note_nd">nd</span>'
+                    e["coef_txt"] = ""
+                else:
+                    # (-0.15) s'affiche "bonus de 0.15"
+                    if is_malus:
+                        val = abs(val)
+                    e["note_txt"] = fmt_note(val, note_max=e["note_max"])
+                    e["note_html"] = e["note_txt"]
+                    if is_malus:
+                        e["coef_txt"] = ""
+                    else:
+                        e["coef_txt"] = fmt_coef(e["coefficient"])
+                if e["evaluation_type"] == EVALUATION_RATTRAPAGE:
+                    e["coef_txt"] = "rat."
+                if e["etat"]["evalattente"]:
+                    mod_attente = True  # une eval en attente dans ce module
+                if (not is_malus) or (val != "NP"):
+                    mod["evaluations"].append(
+                        e
+                    )  # ne liste pas les eval malus sans notes
+
+            # Evaluations incomplètes ou futures:
+            mod["evaluations_incompletes"] = []
+            if context.get_preference("bul_show_all_evals", formsemestre_id):
+                complete_eval_ids = Set([e["evaluation_id"] for e in evals])
+                all_evals = context.do_evaluation_list(
+                    args={"moduleimpl_id": modimpl["moduleimpl_id"]}
+                )
+                all_evals.reverse()  # plus ancienne d'abord
+                for e in all_evals:
+                    if e["evaluation_id"] not in complete_eval_ids:
+                        e = e.copy()
+                        mod["evaluations_incompletes"].append(e)
+                        e["name"] = (e["description"] or "") + " (%s)" % e["jour"]
+                        e["target_html"] = (
+                            "evaluation_listenotes?evaluation_id=%s&amp;format=html&amp;tf-submitted=1"
+                            % e["evaluation_id"]
+                        )
+                        e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % (
+                            e["target_html"],
+                            e["name"],
+                        )
+                        e["note_txt"] = e["note_html"] = ""
+                        e["coef_txt"] = fmt_coef(e["coefficient"])
+            # Classement
+            if bul_show_mod_rangs and mod["mod_moy_txt"] != "-" and not is_malus:
+                rg = nt.mod_rangs[modimpl["moduleimpl_id"]]
+                if mod_attente:  # nt.get_moduleimpls_attente():
+                    mod["mod_rang"] = RANG_ATTENTE_STR
+                else:
+                    mod["mod_rang"] = rg[0][etudid]
+                mod["mod_eff"] = rg[1]  # effectif dans ce module
+                mod["mod_rang_txt"] = "%s/%s" % (mod["mod_rang"], mod["mod_eff"])
+            else:
+                mod["mod_rang_txt"] = ""
+        if mod_attente:
+            ue_attente = True
+    return mods, ue_attente
+
+
+def get_etud_rangs_groups(
+    context, etudid, formsemestre_id, partitions, partitions_etud_groups, nt
+):
+    """Ramene rang et nb inscrits dans chaque partition
+    """
+    rang_gr, ninscrits_gr, gr_name = {}, {}, {}
+    for partition in partitions:
+        if partition["partition_name"] != None:
+            partition_id = partition["partition_id"]
+
+            if etudid in partitions_etud_groups[partition_id]:
+                group = partitions_etud_groups[partition_id][etudid]
+
+                (
+                    rang_gr[partition_id],
+                    ninscrits_gr[partition_id],
+                ) = nt.get_etud_rang_group(etudid, group["group_id"])
+                gr_name[partition_id] = group["group_name"]
+            else:  # etudiant non present dans cette partition
+                rang_gr[partition_id], ninscrits_gr[partition_id] = "", ""
+                gr_name[partition_id] = ""
+
+    return rang_gr, ninscrits_gr, gr_name
+
+
+def etud_descr_situation_semestre(
+    context,
+    etudid,
+    formsemestre_id,
+    ne="",
+    format="html",  # currently unused
+    show_decisions=True,
+    show_uevalid=True,
+    show_date_inscr=True,
+    show_mention=False,
+):
+    """Dict décrivant la situation de l'étudiant dans ce semestre.
+    Si format == 'html', peut inclure du balisage html (actuellement inutilisé)
+
+    situation : chaine résumant en français la situation de l'étudiant.
+                Par ex. "Inscrit le 31/12/1999. Décision jury: Validé. ..."
+    
+    date_inscription : (vide si show_date_inscr est faux)
+    date_demission   : (vide si pas demission ou si show_date_inscr est faux)
+    descr_inscription : "Inscrit" ou "Pas inscrit[e]"
+    descr_demission   : "Démission le 01/02/2000" ou vide si pas de démission
+    descr_defaillance  : "Défaillant" ou vide si non défaillant.
+    decision_jury     :  "Validé", "Ajourné", ... (code semestre)
+    descr_decision_jury : "Décision jury: Validé" (une phrase)
+    decisions_ue        : noms (acronymes) des UE validées, séparées par des virgules.
+    descr_decisions_ue  : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid
+    descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention
+    """
+    cnx = context.GetDBConnexion()
+    infos = DictDefault(defaultvalue="")
+
+    # --- Situation et décisions jury
+
+    # demission/inscription ?
+    events = scolars.scolar_events_list(
+        cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
+    )
+    date_inscr = None
+    date_dem = None
+    date_def = None
+    date_echec = None
+    for event in events:
+        event_type = event["event_type"]
+        if event_type == "INSCRIPTION":
+            if date_inscr:
+                # plusieurs inscriptions ???
+                # date_inscr += ', ' +   event['event_date'] + ' (!)'
+                # il y a eu une erreur qui a laissé un event 'inscription'
+                # on l'efface:
+                log(
+                    "etud_descr_situation_semestre: removing duplicate INSCRIPTION event for etudid=%s !"
+                    % etudid
+                )
+                scolars.scolar_events_delete(cnx, event["event_id"])
+            else:
+                date_inscr = event["event_date"]
+        elif event_type == "DEMISSION":
+            # assert date_dem == None, 'plusieurs démissions !'
+            if date_dem:  # cela ne peut pas arriver sauf bug (signale a Evry 2013?)
+                log(
+                    "etud_descr_situation_semestre: removing duplicate DEMISSION event for etudid=%s !"
+                    % etudid
+                )
+                scolars.scolar_events_delete(cnx, event["event_id"])
+            else:
+                date_dem = event["event_date"]
+        elif event_type == "DEFAILLANCE":
+            if date_def:
+                log(
+                    "etud_descr_situation_semestre: removing duplicate DEFAILLANCE event for etudid=%s !"
+                    % etudid
+                )
+                scolars.scolar_events_delete(cnx, event["event_id"])
+            else:
+                date_def = event["event_date"]
+    if show_date_inscr:
+        if not date_inscr:
+            infos["date_inscription"] = ""
+            infos["descr_inscription"] = "Pas inscrit%s." % ne
+        else:
+            infos["date_inscription"] = date_inscr
+            infos["descr_inscription"] = "Inscrit%s le %s." % (ne, date_inscr)
+    else:
+        infos["date_inscription"] = ""
+        infos["descr_inscription"] = ""
+
+    infos["situation"] = infos["descr_inscription"]
+
+    if date_dem:
+        infos["descr_demission"] = "Démission le %s." % date_dem
+        infos["date_demission"] = date_dem
+        infos["descr_decision_jury"] = "Démission"
+        infos["situation"] += " " + infos["descr_demission"]
+        return infos, None  # ne donne pas les dec. de jury pour les demissionnaires
+    if date_def:
+        infos["descr_defaillance"] = "Défaillant%s" % ne
+        infos["date_defaillance"] = date_def
+        infos["descr_decision_jury"] = "Défaillant%s" % ne
+        infos["situation"] += " " + infos["descr_defaillance"]
+
+    dpv = sco_pvjury.dict_pvjury(context, formsemestre_id, etudids=[etudid])
+
+    if not show_decisions:
+        return infos, dpv
+
+    # Decisions de jury:
+    pv = dpv["decisions"][0]
+    dec = ""
+    if pv["decision_sem_descr"]:
+        infos["decision_jury"] = pv["decision_sem_descr"]
+        infos["descr_decision_jury"] = (
+            "Décision jury: " + pv["decision_sem_descr"] + ". "
+        )
+        dec = infos["descr_decision_jury"]
+    else:
+        infos["descr_decision_jury"] = ""
+
+    if pv["decisions_ue_descr"] and show_uevalid:
+        infos["decisions_ue"] = pv["decisions_ue_descr"]
+        infos["descr_decisions_ue"] = " UE acquises: " + pv["decisions_ue_descr"] + ". "
+        dec += infos["descr_decisions_ue"]
+    else:
+        # infos['decisions_ue'] = None
+        infos["descr_decisions_ue"] = ""
+
+    infos["mention"] = pv["mention"]
+    if pv["mention"] and show_mention:
+        dec += "Mention " + pv["mention"] + ". "
+
+    infos["situation"] += " " + dec
+    if not pv["validation_parcours"]:  # parcours non terminé
+        if pv["autorisations_descr"]:
+            infos["situation"] += (
+                " Autorisé à s'inscrire en %s." % pv["autorisations_descr"]
+            )
+    else:
+        infos["situation"] += " Diplôme obtenu."
+    return infos, dpv
+
+
+# ------ Page bulletin
+def formsemestre_bulletinetud(
+    context,
+    etudid=None,
+    formsemestre_id=None,
+    format="html",
+    version="long",
+    xml_with_decisions=False,
+    force_publishing=False,  # force publication meme si semestre non publie sur "portail"
+    prefer_mail_perso=False,
+    REQUEST=None,
+):
+    "page bulletin de notes"
+    try:
+        etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+        etudid = etud["etudid"]
+    except:
+        return log_unknown_etud(context, REQUEST, format=format)
+
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+
+    R = []
+    if format == "html" or format == "pdfmail":
+        R.append(
+            _formsemestre_bulletinetud_header_html(
+                context, etud, etudid, sem, formsemestre_id, format, version, REQUEST
+            )
+        )
+
+    R.append(
+        do_formsemestre_bulletinetud(
+            context,
+            formsemestre_id,
+            etudid,
+            format=format,
+            version=version,
+            xml_with_decisions=xml_with_decisions,
+            force_publishing=force_publishing,
+            prefer_mail_perso=prefer_mail_perso,
+            REQUEST=REQUEST,
+        )[0]
+    )
+
+    if format == "html" or format == "pdfmail":
+        R.append("""<p>Situation actuelle: """)
+        if etud["inscription_formsemestre_id"]:
+            R.append(
+                """<a class="stdlink" href="formsemestre_status?formsemestre_id=%s">"""
+                % etud["inscription_formsemestre_id"]
+            )
+        R.append(etud["inscriptionstr"])
+        if etud["inscription_formsemestre_id"]:
+            R.append("""</a>""")
+        R.append("""</p>""")
+        if sem["modalite"] == "EXT":
+            R.append(
+                """<p><a 
+            href="formsemestre_ext_edit_ue_validations?formsemestre_id=%s&amp;etudid=%s" 
+            class="stdlink">
+            Editer les validations d'UE dans ce semestre extérieur
+            </a></p>"""
+                % (formsemestre_id, etudid)
+            )
+        # Place du diagramme radar
+        R.append(
+            """<form id="params">
+        <input type="hidden" name="etudid" id="etudid" value="%s"/>
+        <input type="hidden" name="formsemestre_id" id="formsemestre_id" value="%s"/>
+        </form>"""
+            % (etudid, formsemestre_id)
+        )
+        R.append('<div id="radar_bulletin"></div>')
+
+        # --- Pied de page
+        R.append(context.sco_footer(REQUEST))
+
+    return "".join(R)
+
+
+def can_send_bulletin_by_mail(context, formsemestre_id, REQUEST):
+    """True if current user is allowed to send a bulletin by mail
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    return (
+        context.get_preference("bul_mail_allowed_for_all", formsemestre_id)
+        or authuser.has_permission(ScoImplement, context)
+        or str(authuser) in sem["responsables"]
+    )
+
+
+def do_formsemestre_bulletinetud(
+    context,
+    formsemestre_id,
+    etudid,
+    version="long",  # short, long, selectedevals
+    format="html",
+    REQUEST=None,
+    nohtml=False,
+    xml_with_decisions=False,  # force decisions dans XML
+    force_publishing=False,  # force publication meme si semestre non publie sur "portail"
+    prefer_mail_perso=False,  # mails envoyes sur adresse perso si non vide
+):
+    """Génère le bulletin au format demandé.
+    Retourne: (bul, filigranne)
+    où bul est au format demandé (html, pdf, pdfmail, pdfpart, xml)
+    et filigranne est un message à placer en "filigranne" (eg "Provisoire").
+    """
+    if format == "xml":
+        bul = repr(
+            sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
+                context,
+                formsemestre_id,
+                etudid,
+                REQUEST=REQUEST,
+                xml_with_decisions=xml_with_decisions,
+                force_publishing=force_publishing,
+                version=version,
+            )
+        )
+        return bul, ""
+
+    elif format == "json":
+        bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
+            context,
+            formsemestre_id,
+            etudid,
+            REQUEST=REQUEST,
+            xml_with_decisions=xml_with_decisions,
+            force_publishing=force_publishing,
+            version=version,
+        )
+        return bul, ""
+
+    I = formsemestre_bulletinetud_dict(
+        context, formsemestre_id, etudid, REQUEST=REQUEST
+    )
+    etud = I["etud"]
+
+    if format == "html":
+        htm, junk = sco_bulletins_generator.make_formsemestre_bulletinetud(
+            context, I, version=version, format="html", REQUEST=REQUEST
+        )
+        return htm, I["filigranne"]
+
+    elif format == "pdf" or format == "pdfpart":
+        bul, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
+            context,
+            I,
+            version=version,
+            format="pdf",
+            stand_alone=(format != "pdfpart"),
+            REQUEST=REQUEST,
+        )
+        if format == "pdf":
+            return (
+                sendPDFFile(REQUEST, bul, filename),
+                I["filigranne"],
+            )  # unused ret. value
+        else:
+            return bul, I["filigranne"]
+
+    elif format == "pdfmail":
+        # format pdfmail: envoie le pdf par mail a l'etud, et affiche le html
+        # check permission
+        if not can_send_bulletin_by_mail(context, formsemestre_id, REQUEST):
+            raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+
+        if nohtml:
+            htm = ""  # speed up if html version not needed
+        else:
+            htm, junk = sco_bulletins_generator.make_formsemestre_bulletinetud(
+                context, I, version=version, format="html", REQUEST=REQUEST
+            )
+
+        pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
+            context, I, version=version, format="pdf", REQUEST=REQUEST
+        )
+
+        if prefer_mail_perso:
+            recipient_addr = etud.get("emailperso", "") or etud.get("email", "")
+        else:
+            recipient_addr = etud["email_default"]
+
+        if not recipient_addr:
+            if nohtml:
+                h = ""  # permet de compter les non-envois
+            else:
+                h = (
+                    "<div class=\"boldredmsg\">%s n'a pas d'adresse e-mail !</div>"
+                    % etud["nomprenom"]
+                ) + htm
+            return h, I["filigranne"]
+        #
+        mail_bulletin(context, formsemestre_id, I, pdfdata, filename, recipient_addr)
+        emaillink = '<a class="stdlink" href="mailto:%s">%s</a>' % (
+            recipient_addr,
+            recipient_addr,
+        )
+        return (
+            ('<div class="head_message">Message mail envoyé à %s</div>' % (emaillink))
+            + htm,
+            I["filigranne"],
+        )
+
+    else:
+        raise ValueError("do_formsemestre_bulletinetud: invalid format (%s)" % format)
+
+
+def mail_bulletin(context, formsemestre_id, I, pdfdata, filename, recipient_addr):
+    """Send bulletin by email to etud
+    If bul_mail_list_abs pref is true, put list of absences in mail body (text).
+    """
+    etud = I["etud"]
+    webmaster = context.get_preference("bul_mail_contact_addr", formsemestre_id)
+    dept = unescape_html(context.get_preference("DeptName", formsemestre_id))
+    copy_addr = context.get_preference("email_copy_bulletins", formsemestre_id)
+    intro_mail = context.get_preference("bul_intro_mail", formsemestre_id)
+
+    if intro_mail:
+        hea = intro_mail % {
+            "nomprenom": etud["nomprenom"],
+            "dept": dept,
+            "webmaster": webmaster,
+        }
+    else:
+        hea = ""
+
+    if context.get_preference("bul_mail_list_abs"):
+        hea += "\n\n" + sco_abs_views.ListeAbsEtud(
+            context, etud["etudid"], with_evals=False, format="text"
+        )
+
+    msg = MIMEMultipart()
+    subj = Header("Relevé de notes de %s" % etud["nomprenom"], SCO_ENCODING)
+    recipients = [recipient_addr]
+    msg["Subject"] = subj
+    msg["From"] = context.get_preference("email_from_addr", formsemestre_id)
+    msg["To"] = " ,".join(recipients)
+    if copy_addr:
+        msg["Bcc"] = copy_addr.strip()
+    # Guarantees the message ends in a newline
+    msg.epilogue = ""
+    # Text
+    txt = MIMEText(hea, "plain", SCO_ENCODING)
+    # log('hea:\n' + hea)
+    msg.attach(txt)
+    # Attach pdf
+    att = MIMEBase("application", "pdf")
+    att.add_header("Content-Disposition", "attachment", filename=filename)
+    att.set_payload(pdfdata)
+    Encoders.encode_base64(att)
+    msg.attach(att)
+    log("mail bulletin a %s" % msg["To"])
+    context.sendEmail(msg)
+
+
+def _formsemestre_bulletinetud_header_html(
+    context,
+    etud,
+    etudid,
+    sem,
+    formsemestre_id=None,
+    format=None,
+    version=None,
+    REQUEST=None,
+):
+    authuser = REQUEST.AUTHENTICATED_USER
+    uid = str(authuser)
+    H = [
+        context.sco_header(
+            page_title="Bulletin de %(nomprenom)s" % etud,
+            REQUEST=REQUEST,
+            javascripts=[
+                "js/bulletin.js",
+                "libjs/d3.v3.min.js",
+                "js/radar_bulletin.js",
+            ],
+            cssstyles=["css/radar_bulletin.css"],
+        ),
+        """<table class="bull_head"><tr><td>
+          <h2><a class="discretelink" href="ficheEtud?etudid=%(etudid)s">%(nomprenom)s</a></h2>
+          """
+        % etud,
+        """
+          <form name="f" method="GET" action="%s">"""
+        % REQUEST.URL0,
+        """Bulletin <span class="bull_liensemestre"><a href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">
+          %(titremois)s</a></span> 
+          <br/>"""
+        % sem,
+        """<table><tr>""",
+        """<td>établi le %s (notes sur 20)</td>""" % time.strftime("%d/%m/%Y à %Hh%M"),
+        """<td><span class="rightjust">
+             <input type="hidden" name="formsemestre_id" value="%s"></input>"""
+        % formsemestre_id,
+        """<input type="hidden" name="etudid" value="%s"></input>""" % etudid,
+        """<input type="hidden" name="format" value="%s"></input>""" % format,
+        """<select name="version" onchange="document.f.submit()" class="noprint">""",
+    ]
+    for (v, e) in (
+        ("short", "Version courte"),
+        ("selectedevals", "Version intermédiaire"),
+        ("long", "Version complète"),
+    ):
+        if v == version:
+            selected = " selected"
+        else:
+            selected = ""
+        H.append('<option value="%s"%s>%s</option>' % (v, selected, e))
+    H.append("""</select></td>""")
+    # Menu
+    url = REQUEST.URL0
+    qurl = urllib.quote_plus(url + "?" + REQUEST.QUERY_STRING)
+
+    menuBul = [
+        {
+            "title": "Réglages bulletins",
+            "url": "formsemestre_edit_options?formsemestre_id=%s&amp;target_url=%s"
+            % (formsemestre_id, qurl),
+            "enabled": (uid in sem["responsables"])
+            or authuser.has_permission(ScoImplement, context),
+        },
+        {
+            "title": 'Version papier (pdf, format "%s")'
+            % sco_bulletins_generator.bulletin_get_class_name_displayed(
+                context, formsemestre_id
+            ),
+            "url": url
+            + "?formsemestre_id=%s&amp;etudid=%s&amp;format=pdf&amp;version=%s"
+            % (formsemestre_id, etudid, version),
+        },
+        {
+            "title": "Envoi par mail à %s" % etud["email"],
+            "url": url
+            + "?formsemestre_id=%s&amp;etudid=%s&amp;format=pdfmail&amp;version=%s"
+            % (formsemestre_id, etudid, version),
+            "enabled": etud["email"]
+            and can_send_bulletin_by_mail(
+                context, formsemestre_id, REQUEST
+            ),  # possible slt si on a un mail...
+        },
+        {
+            "title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"],
+            "url": url
+            + "?formsemestre_id=%s&amp;etudid=%s&amp;format=pdfmail&amp;version=%s&amp;prefer_mail_perso=1"
+            % (formsemestre_id, etudid, version),
+            "enabled": etud["emailperso"]
+            and can_send_bulletin_by_mail(
+                context, formsemestre_id, REQUEST
+            ),  # possible slt si on a un mail...
+        },
+        {
+            "title": "Version XML",
+            "url": url
+            + "?formsemestre_id=%s&amp;etudid=%s&amp;format=xml&amp;version=%s"
+            % (formsemestre_id, etudid, version),
+        },
+        {
+            "title": "Ajouter une appréciation",
+            "url": "appreciation_add_form?etudid=%s&amp;formsemestre_id=%s"
+            % (etudid, formsemestre_id),
+            "enabled": (
+                (authuser in sem["responsables"])
+                or (authuser.has_permission(ScoEtudInscrit, context))
+            ),
+        },
+        {
+            "title": "Enregistrer un semestre effectué ailleurs",
+            "url": "formsemestre_ext_create_form?etudid=%s&amp;formsemestre_id=%s"
+            % (etudid, formsemestre_id),
+            "enabled": authuser.has_permission(ScoImplement, context),
+        },
+        {
+            "title": "Enregistrer une validation d'UE antérieure",
+            "url": "formsemestre_validate_previous_ue?etudid=%s&amp;formsemestre_id=%s"
+            % (etudid, formsemestre_id),
+            "enabled": context._can_validate_sem(REQUEST, formsemestre_id),
+        },
+        {
+            "title": "Enregistrer note d'une UE externe",
+            "url": "external_ue_create_form?etudid=%s&amp;formsemestre_id=%s"
+            % (etudid, formsemestre_id),
+            "enabled": context._can_validate_sem(REQUEST, formsemestre_id),
+        },
+        {
+            "title": "Entrer décisions jury",
+            "url": "formsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s"
+            % (formsemestre_id, etudid),
+            "enabled": context._can_validate_sem(REQUEST, formsemestre_id),
+        },
+        {
+            "title": "Editer PV jury",
+            "url": "formsemestre_pvjury_pdf?formsemestre_id=%s&amp;etudid=%s"
+            % (formsemestre_id, etudid),
+            "enabled": True,
+        },
+    ]
+
+    H.append("""<td class="bulletin_menubar"><div class="bulletin_menubar">""")
+    H.append(sco_formsemestre_status.makeMenu("Autres opérations", menuBul, alone=True))
+    H.append("""</div></td>""")
+    H.append(
+        '<td> <a href="%s">%s</a></td>'
+        % (
+            url
+            + "?formsemestre_id=%s&amp;etudid=%s&amp;format=pdf&amp;version=%s"
+            % (formsemestre_id, etudid, version),
+            ICON_PDF,
+        )
+    )
+    H.append("""</tr></table>""")
+    #
+    H.append(
+        """</form></span></td><td class="bull_photo">
+    <a href="%s/ficheEtud?etudid=%s">%s</a>
+    """
+        % (
+            context.ScoURL(),
+            etudid,
+            sco_photos.etud_photo_html(
+                context, etud, title="fiche de " + etud["nom"], REQUEST=REQUEST
+            ),
+        )
+    )
+    H.append(
+        """</td></tr>
+    </table>
+    """
+    )
+
+    return "".join(H)
+
+
+def formsemestre_bulletins_choice(
+    context, REQUEST, formsemestre_id, title="", explanation="", choose_mail=False
+):
+    """Choix d'une version de bulletin
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    H = [
+        context.html_sem_header(REQUEST, title, sem),
+        """
+      <form name="f" method="GET" action="%s">
+      <input type="hidden" name="formsemestre_id" value="%s"></input>
+      """
+        % (REQUEST.URL0, formsemestre_id),
+    ]
+    H.append("""<select name="version" class="noprint">""")
+    for (v, e) in (
+        ("short", "Version courte"),
+        ("selectedevals", "Version intermédiaire"),
+        ("long", "Version complète"),
+    ):
+        H.append('<option value="%s">%s</option>' % (v, e))
+
+    H.append("""</select>&nbsp;&nbsp;<input type="submit" value="Générer"/>""")
+    if choose_mail:
+        H.append(
+            """<div><input type="checkbox" name="prefer_mail_perso" value="1">Utiliser si possible les adresses personnelles</div>"""
+        )
+
+    H.append("""<p class="help">""" + explanation + """</p>""")
+
+    return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+expl_bull = """Versions des bulletins:<ul><li><bf>courte</bf>: moyennes des modules</li><li><bf>intermédiaire</bf>: moyennes des modules et notes des évaluations sélectionnées</li><li><bf>complète</bf>: toutes les notes</li><ul>"""
+
+
+def formsemestre_bulletins_pdf_choice(context, REQUEST, formsemestre_id, version=None):
+    """Choix version puis envois classeur bulletins pdf"""
+    if version:
+        return context.formsemestre_bulletins_pdf(
+            formsemestre_id, REQUEST, version=version
+        )
+    return formsemestre_bulletins_choice(
+        context,
+        REQUEST,
+        formsemestre_id,
+        title="Choisir la version des bulletins à générer",
+        explanation=expl_bull,
+    )
+
+
+def formsemestre_bulletins_mailetuds_choice(
+    context,
+    REQUEST,
+    formsemestre_id,
+    version=None,
+    dialog_confirmed=False,
+    prefer_mail_perso=0,
+):
+    """Choix version puis envois classeur bulletins pdf"""
+    if version:
+        return context.formsemestre_bulletins_mailetuds(
+            formsemestre_id,
+            REQUEST,
+            version=version,
+            dialog_confirmed=dialog_confirmed,
+            prefer_mail_perso=prefer_mail_perso,
+        )
+    return formsemestre_bulletins_choice(
+        context,
+        REQUEST,
+        formsemestre_id,
+        title="Choisir la version des bulletins à envoyer par mail",
+        explanation="Chaque étudiant ayant une adresse mail connue de ScoDoc recevra une copie PDF de son bulletin de notes, dans la version choisie.</p><p>"
+        + expl_bull,
+        choose_mail=True,
+    )
+
+
+"""
+from debug import *
+from sco_bulletins import *
+context = go_dept(app, 'RT')
+etudid='EID25013'
+etudid='EID27760' # bonh
+formsemestre_id = 'SEM27425'
+I = formsemestre_bulletinetud_dict(context.Notes, formsemestre_id, etudid, REQUEST=REQUEST)
+"""
diff --git a/sco_bulletins_example.py b/sco_bulletins_example.py
new file mode 100644
index 0000000000000000000000000000000000000000..c064a5462efef9bd777a6b731f9b2f4bde140786
--- /dev/null
+++ b/sco_bulletins_example.py
@@ -0,0 +1,72 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""Generation bulletins de notes: exemple minimal pour les programmeurs
+"""
+
+# Quelques modules ScoDoc utiles:
+from sco_pdf import *
+import sco_preferences
+from notes_log import log
+import sco_bulletins_generator
+import sco_bulletins_standard
+
+
+class BulletinGeneratorExample(sco_bulletins_standard.BulletinGeneratorStandard):
+    """Un exemple simple de bulletin de notes en version PDF seulement.
+    Part du bulletin standard et redéfini la partie centrale.
+    """
+
+    description = "exemple (ne pas utiliser)"  # la description doit être courte: elle apparait dans le menu de paramètrage
+    supported_formats = [
+        "pdf"
+    ]  # indique que ce générateur ne peut produire que du PDF (la version web sera donc celle standard de ScoDoc)
+
+    # En général, on veut définir un format de table spécial, sans changer le reste (titre, pied de page).
+    # Si on veut changer le reste, surcharger les méthodes:
+    #  .bul_title_pdf(self)  : partie haute du bulletin
+    #  .bul_part_below(self, format='') : infos sous la table
+    #  .bul_signatures_pdf(self) : signatures
+
+    def bul_table(self, format=""):
+        """Défini la partie centrale de notre bulletin PDF.
+        Doit renvoyer une liste d'objets PLATYPUS
+        """
+        assert format == "pdf"  # garde fou
+        return [
+            Paragraph(
+                SU(
+                    "L'étudiant %(nomprenom)s a une moyenne générale de %(moy_gen)s"
+                    % self.infos
+                ),
+                self.CellStyle,  # un style pdf standard
+            )
+        ]
+
+
+# Déclarer votre classe à ScoDoc:
+sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample)
diff --git a/sco_bulletins_generator.py b/sco_bulletins_generator.py
new file mode 100644
index 0000000000000000000000000000000000000000..a9b598d6ba10e45d14d1a1ddbc4c0b03f01d437b
--- /dev/null
+++ b/sco_bulletins_generator.py
@@ -0,0 +1,316 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""Génération des bulletins de note: super-classe pour les générateurs (HTML et PDF)
+
+class BulletinGenerator:
+ description
+ supported_formats = [ 'pdf', 'html' ]
+ .bul_title_pdf()
+ .bul_table(format)
+ .bul_part_below(format)
+ .bul_signatures_pdf()
+
+ .__init__ et .generate(format) methodes appelees par le client (sco_bulletin)
+
+La préférence 'bul_class_name' donne le nom de la classe generateur.
+La préférence 'bul_pdf_class_name' est obsolete (inutilisée).
+
+
+"""
+import collections
+
+import sco_preferences
+from notes_log import log
+import sco_formsemestre
+from sco_pdf import *
+
+BULLETIN_CLASSES = (
+    collections.OrderedDict()
+)  # liste des types des classes de générateurs de bulletins PDF
+
+
+def register_bulletin_class(klass):
+    log("registering bulletin class '%s'" % klass.__name__)
+    BULLETIN_CLASSES[klass.__name__] = klass
+
+
+def bulletin_class_descriptions():
+    return [x.description for x in BULLETIN_CLASSES.values()]
+
+
+def bulletin_class_names():
+    return BULLETIN_CLASSES.keys()
+
+
+def bulletin_default_class_name():
+    return bulletin_class_names()[0]
+
+
+def bulletin_get_class(class_name):
+    return BULLETIN_CLASSES[class_name]
+
+
+def bulletin_get_class_name_displayed(context, formsemestre_id):
+    """Le nom du générateur utilisé, en clair"""
+    bul_class_name = context.get_preference("bul_class_name", formsemestre_id)
+    try:
+        gen_class = bulletin_get_class(bul_class_name)
+        return gen_class.description
+    except:
+        return "invalide ! (voir paramètres)"
+
+
+class BulletinGenerator:
+    "Virtual superclass for PDF bulletin generators" ""
+    # Here some helper methods
+    # see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
+    supported_formats = []  # should list supported formats, eg [ 'html', 'pdf' ]
+    description = "superclass for bulletins"  # description for user interface
+
+    def __init__(
+        self,
+        context,
+        infos,
+        authuser=None,
+        version="long",
+        filigranne=None,
+        server_name=None,
+    ):
+        if not version in ("short", "long", "selectedevals"):
+            raise ValueError("invalid version code !")
+        self.context = context
+        self.infos = infos
+        self.authuser = authuser  # nécessaire pour version HTML qui contient liens dépendant de l'utilisateur
+        self.version = version
+        self.filigranne = filigranne
+        self.server_name = server_name
+        # Store preferences for convenience:
+        formsemestre_id = self.infos["formsemestre_id"]
+        self.preferences = context.get_preferences(formsemestre_id)
+        self.diagnostic = None  # error message if any problem
+        # Common PDF styles:
+        #  - Pour tous les champs du bulletin sauf les cellules de table:
+        self.FieldStyle = reportlab.lib.styles.ParagraphStyle({})
+        self.FieldStyle.fontName = self.preferences["SCOLAR_FONT_BUL_FIELDS"]
+        self.FieldStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
+        self.FieldStyle.firstLineIndent = 0
+        #  - Pour les cellules de table:
+        self.CellStyle = reportlab.lib.styles.ParagraphStyle({})
+        self.CellStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
+        self.CellStyle.fontName = self.preferences["SCOLAR_FONT"]
+        self.CellStyle.leading = (
+            1.0 * self.preferences["SCOLAR_FONT_SIZE"]
+        )  # vertical space
+        # Marges du document PDF
+        self.margins = (
+            self.preferences["left_margin"],
+            self.preferences["top_margin"],
+            self.preferences["right_margin"],
+            self.preferences["bottom_margin"],
+        )
+
+    def get_filename(self):
+        """Build a filename to be proposed to the web client"""
+        sem = sco_formsemestre.get_formsemestre(
+            self.context, self.infos["formsemestre_id"]
+        )
+        dt = time.strftime("%Y-%m-%d")
+        filename = "bul-%s-%s-%s.pdf" % (
+            sem["titre_num"],
+            dt,
+            self.infos["etud"]["nom"],
+        )
+        filename = unescape_html(filename).replace(" ", "_").replace("&", "")
+        return filename
+
+    def generate(self, format="", stand_alone=True):
+        """Return bulletin in specified format
+        """
+        if not format in self.supported_formats:
+            raise ValueError("unsupported bulletin format (%s)" % format)
+        try:
+            PDFLOCK.acquire()  # this lock is necessary since reportlab is not re-entrant
+            if format == "html":
+                return self.generate_html()
+            elif format == "pdf":
+                return self.generate_pdf(stand_alone=stand_alone)
+            else:
+                raise ValueError("invalid bulletin format (%s)" % format)
+        finally:
+            PDFLOCK.release()
+
+    def generate_html(self):
+        """Return bulletin as an HTML string
+        """
+        H = ['<div class="notes_bulletin">']
+        H.append(self.bul_table(format="html"))  # table des notes
+        H.append(self.bul_part_below(format="html"))  # infos sous la table
+        H.append("</div>")
+        return "\n".join(H)
+
+    def generate_pdf(self, stand_alone=True):
+        """Build PDF bulletin from distinct parts
+        Si stand_alone, génère un doc PDF complet et renvoie une string
+        Sinon, renvoie juste une liste d'objets PLATYPUS pour intégration
+        dans un autre document.
+        """
+        formsemestre_id = self.infos["formsemestre_id"]
+
+        objects = self.bul_title_pdf()  # partie haute du bulletin
+        objects += self.bul_table(format="pdf")  # table des notes
+        objects += self.bul_part_below(format="pdf")  # infos sous la table
+        objects += self.bul_signatures_pdf()  # signatures
+
+        # Réduit sur une page
+        objects = [KeepInFrame(0, 0, objects, mode="shrink")]
+        #
+        if not stand_alone:
+            objects.append(PageBreak())  # insert page break at end
+            return objects
+        else:
+            # Generation du document PDF
+            sem = sco_formsemestre.get_formsemestre(self.context, formsemestre_id)
+            report = cStringIO.StringIO()  # in-memory document, no disk file
+            document = BaseDocTemplate(report)
+            document.addPageTemplates(
+                ScolarsPageTemplate(
+                    document,
+                    author="%s %s (E. Viennet) [%s]"
+                    % (SCONAME, SCOVERSION, self.description),
+                    title="Bulletin %s de %s"
+                    % (sem["titremois"], self.infos["etud"]["nomprenom"]),
+                    subject="Bulletin de note",
+                    margins=self.margins,
+                    server_name=self.server_name,
+                    filigranne=self.filigranne,
+                    preferences=self.context.get_preferences(formsemestre_id),
+                )
+            )
+            document.build(objects)
+            data = report.getvalue()
+        return data
+
+    def buildTableObject(self, P, pdfTableStyle, colWidths):
+        """Utility used by some old-style generators.
+        Build a platypus Table instance from a nested list of cells, style and widths.
+        P: table, as a list of lists
+        PdfTableStyle: commandes de style pour la table (reportlab)
+        """
+        try:
+            # put each table cell in a Paragraph
+            Pt = [[Paragraph(SU(x), self.CellStyle) for x in line] for line in P]
+        except:
+            # enquête sur exception intermittente...
+            log("*** bug in PDF buildTableObject:")
+            log("P=%s" % P)
+            # compris: reportlab is not thread safe !
+            #   see http://two.pairlist.net/pipermail/reportlab-users/2006-June/005037.html
+            # (donc maintenant protégé dans ScoDoc par un Lock global)
+            self.diagnostic = "erreur lors de la génération du PDF<br/>"
+            self.diagnostic += "<pre>" + traceback.format_exc() + "</pre>"
+            return []
+        return Table(Pt, colWidths=colWidths, style=pdfTableStyle)
+
+
+# ---------------------------------------------------------------------------
+def make_formsemestre_bulletinetud(
+    context,
+    infos,
+    version="long",  # short, long, selectedevals
+    format="pdf",  # html, pdf
+    stand_alone=True,
+    REQUEST=None,
+):
+    """Bulletin de notes
+
+    Appelle une fonction générant le bulletin au format spécifié à partir des informations infos,
+    selon les préférences du semestre.
+
+    """
+    if not version in ("short", "long", "selectedevals"):
+        raise ValueError("invalid version code !")
+
+    formsemestre_id = infos["formsemestre_id"]
+    bul_class_name = context.get_preference("bul_class_name", formsemestre_id)
+    try:
+        gen_class = bulletin_get_class(bul_class_name)
+    except:
+        raise ValueError(
+            "Type de bulletin PDF invalide (paramètre: %s)" % bul_pdf_class_name
+        )
+
+    try:
+        PDFLOCK.acquire()
+        bul_generator = gen_class(
+            context,
+            infos,
+            authuser=REQUEST.AUTHENTICATED_USER,
+            version=version,
+            filigranne=infos["filigranne"],
+            server_name=REQUEST.BASE0,
+        )
+        if format not in bul_generator.supported_formats:
+            # use standard generator
+            log(
+                "Bulletin format %s not supported by %s, using %s"
+                % (format, bul_class_name, bulletin_default_class_name())
+            )
+            bul_class_name = bulletin_default_class_name()
+            gen_class = bulletin_get_class(bul_class_name)
+            bul_generator = gen_class(
+                context,
+                infos,
+                authuser=REQUEST.AUTHENTICATED_USER,
+                version=version,
+                filigranne=infos["filigranne"],
+                server_name=REQUEST.BASE0,
+            )
+
+        data = bul_generator.generate(format=format, stand_alone=stand_alone)
+    finally:
+        PDFLOCK.release()
+
+    if bul_generator.diagnostic:
+        log("bul_error: %s" % bul_generator.diagnostic)
+        raise NoteProcessError(bul_generator.diagnostic)
+
+    filename = bul_generator.get_filename()
+
+    return data, filename
+
+
+# ---------------------------------------------------------------------------
+
+# Classes de bulletins:
+import sco_bulletins_standard
+import sco_bulletins_legacy
+
+# import sco_bulletins_example # format exemple (à désactiver en production)
+
+# ... ajouter ici vos modules ...
+import sco_bulletins_ucac  # format expérimental UCAC Cameroun
diff --git a/sco_bulletins_json.py b/sco_bulletins_json.py
new file mode 100644
index 0000000000000000000000000000000000000000..80b273e1859c8058838cf23df0cab851087adda8
--- /dev/null
+++ b/sco_bulletins_json.py
@@ -0,0 +1,394 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Génération du bulletin en format JSON (beta, non completement testé)
+
+"""
+import json
+
+from notes_table import *
+import sco_formsemestre
+import sco_groups
+import sco_photos
+import ZAbsences
+import sco_bulletins
+
+# -------- Bulletin en JSON
+
+
+def make_json_formsemestre_bulletinetud(
+    context,
+    formsemestre_id,
+    etudid,
+    REQUEST=None,
+    xml_with_decisions=False,
+    version="long",
+    force_publishing=False,  # force publication meme si semestre non publie sur "portail"
+):
+    """Renvoie bulletin en chaine JSON"""
+
+    d = formsemestre_bulletinetud_published_dict(
+        context,
+        formsemestre_id,
+        etudid,
+        force_publishing=force_publishing,
+        REQUEST=REQUEST,
+        xml_with_decisions=xml_with_decisions,
+        version=version,
+    )
+
+    if REQUEST:
+        REQUEST.RESPONSE.setHeader("content-type", JSON_MIMETYPE)
+
+    return json.dumps(d, cls=ScoDocJSONEncoder, encoding=SCO_ENCODING)
+
+
+# (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict()
+#   pour simplifier le code, mais attention a la maintenance !)
+#
+def formsemestre_bulletinetud_published_dict(
+    context,
+    formsemestre_id,
+    etudid,
+    force_publishing=False,
+    xml_nodate=False,
+    REQUEST=None,
+    xml_with_decisions=False,  # inclue les decisions même si non publiées
+    version="long",
+):
+    """Dictionnaire representant les informations _publiees_ du bulletin de notes
+    Utilisé pour JSON, devrait l'être aussi pour XML. (todo)
+    """
+
+    d = {}
+
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    if sem["bul_hide_xml"] == "0" or force_publishing:
+        published = 1
+    else:
+        published = 0
+    if xml_nodate:
+        docdate = ""
+    else:
+        docdate = datetime.datetime.now().isoformat()
+
+    el = {
+        "etudid": etudid,
+        "formsemestre_id": formsemestre_id,
+        "date": docdate,
+        "publie": published,
+        "etapes": sem["etapes"],
+    }
+    # backward compat:
+    if sem["etapes"]:
+        el["etape_apo"] = sem["etapes"][0] or ""
+        n = 2
+        for et in sem["etapes"][1:]:
+            el["etape_apo" + str(n)] = et or ""
+            n += 1
+    d.update(**el)
+
+    # Infos sur l'etudiant
+    etudinfo = context.getEtudInfo(etudid=etudid, filled=1)[0]
+
+    d["etudiant"] = dict(
+        etudid=etudid,
+        code_nip=etudinfo["code_nip"],
+        code_ine=etudinfo["code_ine"],
+        nom=quote_xml_attr(etudinfo["nom"]),
+        prenom=quote_xml_attr(etudinfo["prenom"]),
+        sexe=quote_xml_attr(etudinfo["sexe"]),
+        photo_url=quote_xml_attr(sco_photos.etud_photo_url(context, etudinfo)),
+        email=quote_xml_attr(etudinfo["email"]),
+        emailperso=quote_xml_attr(etudinfo["emailperso"]),
+    )
+
+    # Disponible pour publication ?
+    if not published:
+        return d  # stop !
+
+    # Groupes:
+    partitions = sco_groups.get_partitions_list(
+        context, formsemestre_id, with_default=False
+    )
+    partitions_etud_groups = {}  # { partition_id : { etudid : group } }
+    for partition in partitions:
+        pid = partition["partition_id"]
+        partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(
+            context, pid
+        )
+
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > toutes notes
+    ues = nt.get_ues()
+    modimpls = nt.get_modimpls()
+    nbetuds = len(nt.rangs)
+    mg = fmt_note(nt.get_etud_moy_gen(etudid))
+    if (
+        nt.get_moduleimpls_attente()
+        or context.get_preference("bul_show_rangs", formsemestre_id) == 0
+    ):
+        # n'affiche pas le rang sur le bulletin s'il y a des
+        # notes en attente dans ce semestre
+        rang = ""
+        rang_gr = {}
+        ninscrits_gr = {}
+    else:
+        rang = str(nt.get_etud_rang(etudid))
+        rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups(
+            context, etudid, formsemestre_id, partitions, partitions_etud_groups, nt
+        )
+
+    d["note"] = dict(
+        value=mg,
+        min=fmt_note(nt.moy_min),
+        max=fmt_note(nt.moy_max),
+        moy=fmt_note(nt.moy_moy),
+    )
+    d["rang"] = dict(value=rang, ninscrits=nbetuds)
+    d["rang_group"] = []
+    if rang_gr:
+        for partition in partitions:
+            d["rang_group"].append(
+                dict(
+                    group_type=partition["partition_name"],
+                    group_name=gr_name[partition["partition_id"]],
+                    value=rang_gr[partition["partition_id"]],
+                    ninscrits=ninscrits_gr[partition["partition_id"]],
+                )
+            )
+
+    d["note_max"] = dict(value=20)  # notes toujours sur 20
+    d["bonus_sport_culture"] = dict(value=nt.bonus[etudid])
+
+    # Liste les UE / modules /evals
+    d["ue"] = []
+    d["ue_capitalisee"] = []
+    for ue in ues:
+        ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
+        try:
+            ects_txt = str(int(ue["ects"]))
+        except:
+            ects_txt = ""
+        u = dict(
+            id=ue["ue_id"],
+            numero=quote_xml_attr(ue["numero"]),
+            acronyme=quote_xml_attr(ue["acronyme"]),
+            titre=quote_xml_attr(ue["titre"]),
+            note=dict(
+                value=fmt_note(ue_status["cur_moy_ue"]),
+                min=fmt_note(ue["min"]),
+                max=fmt_note(ue["max"]),
+            ),
+            rang=str(nt.ue_rangs[ue["ue_id"]][0][etudid]),
+            effectif=str(nt.ue_rangs[ue["ue_id"]][1]),
+            ects=ects_txt,
+            code_apogee=quote_xml_attr(ue["code_apogee"]),
+        )
+        d["ue"].append(u)
+        u["module"] = []
+        # Liste les modules de l'UE
+        ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue["ue_id"]]
+        for modimpl in ue_modimpls:
+            mod_moy = fmt_note(nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid))
+            if mod_moy == "NI":  # ne mentionne pas les modules ou n'est pas inscrit
+                continue
+            mod = modimpl["module"]
+            # if mod['ects'] is None:
+            #    ects = ''
+            # else:
+            #    ects = str(mod['ects'])
+            modstat = nt.get_mod_stats(modimpl["moduleimpl_id"])
+
+            m = dict(
+                id=modimpl["moduleimpl_id"],
+                code=mod["code"],
+                coefficient=mod["coefficient"],
+                numero=mod["numero"],
+                titre=quote_xml_attr(mod["titre"]),
+                abbrev=quote_xml_attr(mod["abbrev"]),
+                # ects=ects, ects des modules maintenant inutilisés
+                note=dict(value=mod_moy),
+                code_apogee=quote_xml_attr(mod["code_apogee"]),
+            )
+            m["note"].update(modstat)
+            for k in ("min", "max", "moy"):  # formatte toutes les notes
+                m["note"][k] = fmt_note(m["note"][k])
+
+            u["module"].append(m)
+            if context.get_preference("bul_show_mod_rangs", formsemestre_id):
+                m["rang"] = dict(
+                    value=nt.mod_rangs[modimpl["moduleimpl_id"]][0][etudid]
+                )
+                m["effectif"] = dict(value=nt.mod_rangs[modimpl["moduleimpl_id"]][1])
+
+            # --- notes de chaque eval:
+            evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"])
+            m["evaluation"] = []
+            if version != "short":
+                for e in evals:
+                    if int(e["visibulletin"]) == 1 or version == "long":
+                        val = e["notes"].get(etudid, {"value": "NP"})[
+                            "value"
+                        ]  # NA si etud demissionnaire
+                        val = fmt_note(val, note_max=e["note_max"])
+                        m["evaluation"].append(
+                            dict(
+                                jour=DateDMYtoISO(e["jour"], null_is_empty=True),
+                                heure_debut=TimetoISO8601(
+                                    e["heure_debut"], null_is_empty=True
+                                ),
+                                heure_fin=TimetoISO8601(
+                                    e["heure_fin"], null_is_empty=True
+                                ),
+                                coefficient=e["coefficient"],
+                                evaluation_type=e["evaluation_type"],
+                                description=quote_xml_attr(e["description"]),
+                                note=val,
+                            )
+                        )
+                # Evaluations incomplètes ou futures:
+                complete_eval_ids = Set([e["evaluation_id"] for e in evals])
+                if context.get_preference("bul_show_all_evals", formsemestre_id):
+                    all_evals = context.do_evaluation_list(
+                        args={"moduleimpl_id": modimpl["moduleimpl_id"]}
+                    )
+                    all_evals.reverse()  # plus ancienne d'abord
+                    for e in all_evals:
+                        if e["evaluation_id"] not in complete_eval_ids:
+                            m["evaluation"].append(
+                                dict(
+                                    jour=DateDMYtoISO(e["jour"], null_is_empty=True),
+                                    heure_debut=TimetoISO8601(
+                                        e["heure_debut"], null_is_empty=True
+                                    ),
+                                    heure_fin=TimetoISO8601(
+                                        e["heure_fin"], null_is_empty=True
+                                    ),
+                                    coefficient=e["coefficient"],
+                                    description=quote_xml_attr(e["description"]),
+                                    incomplete="1",
+                                )
+                            )
+
+        # UE capitalisee (listee seulement si meilleure que l'UE courante)
+        if ue_status["is_capitalized"]:
+            try:
+                ects_txt = str(int(ue_status["ue"].get("ects", "")))
+            except:
+                ects_txt = ""
+            d["ue_capitalisee"].append(
+                dict(
+                    id=ue["ue_id"],
+                    numero=quote_xml_attr(ue["numero"]),
+                    acronyme=quote_xml_attr(ue["acronyme"]),
+                    titre=quote_xml_attr(ue["titre"]),
+                    note=fmt_note(ue_status["moy"]),
+                    coefficient_ue=fmt_note(ue_status["coef_ue"]),
+                    date_capitalisation=DateDMYtoISO(ue_status["event_date"]),
+                    ects=ects_txt,
+                )
+            )
+
+    # --- Absences
+    if context.get_preference("bul_show_abs", formsemestre_id):
+        debut_sem = DateDMYtoISO(sem["date_debut"])
+        fin_sem = DateDMYtoISO(sem["date_fin"])
+        AbsEtudSem = ZAbsences.getAbsSemEtud(context, sem, etudid)
+        nbabs = AbsEtudSem.CountAbs()
+        nbabsjust = AbsEtudSem.CountAbsJust()
+
+        d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
+
+    # --- Decision Jury
+    if (
+        context.get_preference("bul_show_decision", formsemestre_id)
+        or xml_with_decisions
+    ):
+        infos, dpv = sco_bulletins.etud_descr_situation_semestre(
+            context,
+            etudid,
+            formsemestre_id,
+            format="xml",
+            show_uevalid=context.get_preference("bul_show_uevalid", formsemestre_id),
+        )
+        d["situation"] = quote_xml_attr(infos["situation"])
+        if dpv:
+            decision = dpv["decisions"][0]
+            etat = decision["etat"]
+            if decision["decision_sem"]:
+                code = decision["decision_sem"]["code"]
+            else:
+                code = ""
+
+            d["decision"] = dict(code=code, etat=etat)
+            if (
+                decision["decision_sem"]
+                and "compense_formsemestre_id" in decision["decision_sem"]
+            ):
+                d["decision"]["compense_formsemestre_id"] = decision["decision_sem"][
+                    "compense_formsemestre_id"
+                ]
+
+            d["decision_ue"] = []
+            if decision[
+                "decisions_ue"
+            ]:  # and context.get_preference('bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
+                for ue_id in decision["decisions_ue"].keys():
+                    ue = context.do_ue_list({"ue_id": ue_id})[0]
+                    d["decision_ue"].append(
+                        dict(
+                            ue_id=ue["ue_id"],
+                            numero=quote_xml_attr(ue["numero"]),
+                            acronyme=quote_xml_attr(ue["acronyme"]),
+                            titre=quote_xml_attr(ue["titre"]),
+                            code=decision["decisions_ue"][ue_id]["code"],
+                            ects=quote_xml_attr(ue["ects"] or ""),
+                        )
+                    )
+            d["autorisation_inscription"] = []
+            for aut in decision["autorisations"]:
+                d["autorisation_inscription"].append(
+                    dict(semestre_id=aut["semestre_id"])
+                )
+        else:
+            d["decision"] = dict(code="", etat="DEM")
+
+    # --- Appreciations
+    cnx = context.GetDBConnexion()
+    apprecs = scolars.appreciations_list(
+        cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
+    )
+    d["appreciation"] = []
+    for app in apprecs:
+        d["appreciation"].append(
+            dict(comment=quote_xml_attr(app["comment"]), date=DateDMYtoISO(app["date"]))
+        )
+
+    #
+    return d
diff --git a/sco_bulletins_legacy.py b/sco_bulletins_legacy.py
new file mode 100644
index 0000000000000000000000000000000000000000..a39661837f242a45aba7813cc04bd43f36f3678d
--- /dev/null
+++ b/sco_bulletins_legacy.py
@@ -0,0 +1,540 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""Generation bulletins de notes dans l'ancien format de ScoDoc (avant juillet 2011).
+
+ Code partiellement redondant, copié de l'ancien système de gestion des bulletins.
+
+ Voir sco_bulletins_standard pour une version plus récente.
+
+ CE FORMAT N'EVOLUERA PLUS ET EST CONSIDERE COMME OBSOLETE.
+ 
+"""
+
+import traceback, re
+
+import sco_formsemestre
+from sco_pdf import *
+import sco_preferences
+from notes_log import log
+import sco_bulletins_generator
+import sco_bulletins_pdf
+
+# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences)
+class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
+    description = "Ancien format ScoDoc"  # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc
+    supported_formats = ["html", "pdf"]
+
+    def bul_title_pdf(self):
+        """Génère la partie "titre" du bulletin de notes.
+        Renvoie une liste d'objets platypus
+        """
+        objects = sco_bulletins_pdf.process_field(
+            self.context, self.preferences["bul_pdf_title"], self.infos, self.FieldStyle
+        )
+        objects.append(
+            Spacer(1, 5 * mm)
+        )  # impose un espace vertical entre le titre et la table qui suit
+        return objects
+
+    def bul_table(self, format="html"):
+        """Table bulletin"""
+        if format == "pdf":
+            return self.bul_table_pdf()
+        elif format == "html":
+            return self.bul_table_html()
+        else:
+            raise ValueError("invalid bulletin format (%s)" % format)
+
+    def bul_table_pdf(self):
+        """Génère la table centrale du bulletin de notes
+        Renvoie une liste d'objets PLATYPUS (eg instance de Table).
+        """
+        P, pdfTableStyle, colWidths = _bulletin_pdf_table_legacy(
+            self.context, self.infos, version=self.version
+        )
+        return [self.buildTableObject(P, pdfTableStyle, colWidths)]
+
+    def bul_table_html(self):
+        """Génère la table centrale du bulletin de notes: chaine HTML
+        """
+        format = "html"
+        I = self.infos
+        authuser = self.authuser
+        formsemestre_id = self.infos["formsemestre_id"]
+        context = self.context
+
+        bul_show_abs_modules = context.get_preference(
+            "bul_show_abs_modules", formsemestre_id
+        )
+
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        if sem["bul_bgcolor"]:
+            bgcolor = sem["bul_bgcolor"]
+        else:
+            bgcolor = "background-color: rgb(255,255,240)"
+
+        linktmpl = '<span onclick="toggle_vis_ue(this);" class="toggle_ue">%s</span>'
+        minuslink = linktmpl % icontag("minus_img", border="0", alt="-")
+        pluslink = linktmpl % icontag("plus_img", border="0", alt="+")
+
+        H = ['<table class="notes_bulletin" style="background-color: %s;">' % bgcolor]
+
+        if context.get_preference("bul_show_minmax", formsemestre_id):
+            minmax = (
+                '<span class="bul_minmax" title="[min, max] promo">[%s, %s]</span>'
+                % (I["moy_min"], I["moy_max"])
+            )
+            bargraph = ""
+        else:
+            minmax = ""
+            bargraph = I["moy_gen_bargraph_html"]
+        # 1ere ligne: titres
+        H.append(
+            '<tr><td class="note_bold">Moyenne</td><td class="note_bold cell_graph">%s%s%s%s</td>'
+            % (I["moy_gen"], I["etud_etat_html"], minmax, bargraph)
+        )
+        H.append('<td class="note_bold">%s</td>' % I["rang_txt"])
+        H.append('<td class="note_bold">Note/20</td><td class="note_bold">Coef</td>')
+        if bul_show_abs_modules:
+            H.append('<td class="note_bold">Abs (J. / N.J.)</td>')
+        H.append("</tr>")
+
+        def list_modules(ue_modules, rowstyle):
+            for mod in ue_modules:
+                if mod["mod_moy_txt"] == "NI":
+                    continue  # saute les modules où on n'est pas inscrit
+                H.append('<tr class="notes_bulletin_row_mod%s">' % rowstyle)
+                if context.get_preference("bul_show_minmax_mod", formsemestre_id):
+                    rang_minmax = (
+                        '%s <span class="bul_minmax" title="[min, max] UE">[%s, %s]</span>'
+                        % (
+                            mod["mod_rang_txt"],
+                            fmt_note(mod["stats"]["min"]),
+                            fmt_note(mod["stats"]["max"]),
+                        )
+                    )
+                else:
+                    rang_minmax = mod["mod_rang_txt"]  # vide si pas option rang
+                H.append(
+                    '<td>%s</td><td>%s</td><td>%s</td><td class="note">%s</td><td>%s</td>'
+                    % (
+                        mod["code_html"],
+                        mod["name_html"],
+                        rang_minmax,
+                        mod["mod_moy_txt"],
+                        mod["mod_coef_txt"],
+                    )
+                )
+                if bul_show_abs_modules:
+                    H.append("<td>%s</td>" % mod["mod_abs_txt"])
+                H.append("</tr>")
+
+                if self.version != "short":
+                    # --- notes de chaque eval:
+                    for e in mod["evaluations"]:
+                        if int(e["visibulletin"]) == 1 or self.version == "long":
+                            H.append(
+                                '<tr class="notes_bulletin_row_eval%s">' % rowstyle
+                            )
+                            H.append(
+                                '<td>%s</td><td>%s</td><td class="bull_nom_eval">%s</td><td class="note">%s</td><td class="bull_coef_eval">%s</td></tr>'
+                                % (
+                                    "",
+                                    "",
+                                    e["name_html"],
+                                    e["note_html"],
+                                    e["coef_txt"],
+                                )
+                            )
+
+        # Contenu table: UE apres UE
+        for ue in I["ues"]:
+            ue_descr = ue["ue_descr_html"]
+            coef_ue = ue["coef_ue_txt"]
+            rowstyle = ""
+            plusminus = minuslink  #
+            if ue["ue_status"]["is_capitalized"]:
+                if context.get_preference("bul_show_ue_cap_details", formsemestre_id):
+                    plusminus = minuslink
+                    hide = ""
+                else:
+                    plusminus = pluslink
+                    hide = "sco_hide"
+                H.append('<tr class="notes_bulletin_row_ue">')
+                H.append(
+                    '<td class="note_bold">%s%s</td><td class="note_bold">%s</td><td>%s</td><td>%s</td><td>%s</td>'
+                    % (
+                        plusminus,
+                        ue["acronyme"],
+                        ue["moy_ue_txt"],
+                        ue_descr,
+                        "",
+                        coef_ue,
+                    )
+                )
+                if bul_show_abs_modules:
+                    H.append("<td></td>")
+                H.append("</tr>")
+                list_modules(ue["modules_capitalized"], " bul_row_ue_cap %s" % hide)
+
+                coef_ue = ""
+                ue_descr = "(en cours, non prise en compte)"
+                rowstyle = (
+                    " bul_row_ue_cur"  # style css pour indiquer UE non prise en compte
+                )
+
+            H.append('<tr class="notes_bulletin_row_ue">')
+            if context.get_preference("bul_show_minmax", formsemestre_id):
+                moy_txt = (
+                    '%s <span class="bul_minmax" title="[min, max] UE">[%s, %s]</span>'
+                    % (
+                        fmt_note(ue["cur_moy_ue_txt"]),
+                        fmt_note(ue["min"]),
+                        fmt_note(ue["max"]),
+                    )
+                )
+            else:
+                moy_txt = ue["cur_moy_ue_txt"]
+
+            H.append(
+                '<td class="note_bold">%s%s</td><td class="note_bold">%s</td><td>%s</td><td>%s</td><td>%s</td>'
+                % (minuslink, ue["acronyme"], moy_txt, ue_descr, "", coef_ue)
+            )
+            if bul_show_abs_modules:
+                H.append("<td></td>")
+            H.append("</tr>")
+            list_modules(ue["modules"], rowstyle)
+
+        H.append("</table>")
+
+        # ---------------
+        return "\n".join(H)
+
+    def bul_part_below(self, format="html"):
+        """Génère les informations placées sous la table de notes
+        (absences, appréciations, décisions de jury...)
+        """
+        if format == "pdf":
+            return self.bul_part_below_pdf()
+        elif format == "html":
+            return self.bul_part_below_html()
+        else:
+            raise ValueError("invalid bulletin format (%s)" % format)
+
+    def bul_part_below_pdf(self):
+        """
+        Renvoie une liste d'objets platypus
+        """
+        objects = []
+
+        # ----- ABSENCES
+        if self.preferences["bul_show_abs"]:
+            nbabs = self.infos["nbabs"]
+            nbabsjust = self.infos["nbabsjust"]
+            objects.append(Spacer(1, 2 * mm))
+            if nbabs:
+                objects.append(
+                    Paragraph(
+                        SU(
+                            "%d absences (1/2 journées), dont %d justifiées."
+                            % (nbabs, nbabsjust)
+                        ),
+                        self.CellStyle,
+                    )
+                )
+            else:
+                objects.append(
+                    Paragraph(SU("Pas d'absences signalées."), self.CellStyle)
+                )
+
+        # ----- APPRECIATIONS
+        if self.infos.get("appreciations_list", False):
+            objects.append(Spacer(1, 3 * mm))
+            objects.append(
+                Paragraph(
+                    SU("Appréciation : " + "\n".join(self.infos["appreciations_txt"])),
+                    self.CellStyle,
+                )
+            )
+
+        # ----- DECISION JURY
+        if self.preferences["bul_show_decision"]:
+            objects += sco_bulletins_pdf.process_field(
+                self.context,
+                self.preferences["bul_pdf_caption"],
+                self.infos,
+                self.FieldStyle,
+            )
+
+        return objects
+
+    def bul_part_below_html(self):
+        """
+        Renvoie chaine HTML
+        """
+        I = self.infos
+        authuser = self.authuser
+        H = []
+        # --- Absences
+        H.append(
+            """<p>
+        <a href="../Absences/CalAbs?etudid=%(etudid)s" class="bull_link">
+        <b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées
+        (pendant ce semestre).
+        </a></p>
+            """
+            % I
+        )
+        # --- Decision Jury
+        if I["situation"]:
+            H.append("""<p class="bull_situation">%(situation)s</p>""" % I)
+        # --- Appreciations
+        # le dir. des etud peut ajouter des appreciations,
+        # mais aussi le chef (perm. ScoEtudInscrit)
+        can_edit_app = (str(authuser) in self.infos["responsables"]) or (
+            authuser.has_permission(ScoEtudInscrit, self.context)
+        )
+        H.append('<div class="bull_appreciations">')
+        if I["appreciations_list"]:
+            H.append("<p><b>Appréciations</b></p>")
+        for app in I["appreciations_list"]:
+            if can_edit_app:
+                mlink = (
+                    '<a class="stdlink" href="appreciation_add_form?id=%s">modifier</a> <a class="stdlink" href="appreciation_add_form?id=%s&amp;suppress=1">supprimer</a>'
+                    % (app["id"], app["id"])
+                )
+            else:
+                mlink = ""
+            H.append(
+                '<p><span class="bull_appreciations_date">%s</span>%s<span class="bull_appreciations_link">%s</span></p>'
+                % (app["date"], app["comment"], mlink)
+            )
+        if can_edit_app:
+            H.append(
+                '<p><a class="stdlink" href="appreciation_add_form?etudid=%(etudid)s&amp;formsemestre_id=%(formsemestre_id)s">Ajouter une appréciation</a></p>'
+                % self.infos
+            )
+        H.append("</div>")
+        # ---------------
+        return "\n".join(H)
+
+    def bul_signatures_pdf(self):
+        """Génère les signatures placées en bas du bulletin
+        Renvoie une liste d'objets platypus
+        """
+        show_left = self.preferences["bul_show_sig_left"]
+        show_right = self.preferences["bul_show_sig_right"]
+        if show_left or show_right:
+            if show_left:
+                L = [
+                    [
+                        sco_bulletins_pdf.process_field(
+                            self.context,
+                            self.preferences["bul_pdf_sig_left"],
+                            self.infos,
+                            self.FieldStyle,
+                        )
+                    ]
+                ]
+            else:
+                L = [[""]]
+            if show_right:
+                L[0].append(
+                    sco_bulletins_pdf.process_field(
+                        self.context,
+                        self.preferences["bul_pdf_sig_right"],
+                        self.infos,
+                        self.FieldStyle,
+                    )
+                )
+            else:
+                L[0].append("")
+            t = Table(L)
+            t._argW[0] = 10 * cm  # fixe largeur colonne gauche
+
+            return [Spacer(1, 1.5 * cm), t]  # espace vertical avant signatures
+        else:
+            return []
+
+
+sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
+
+
+class BulTableStyle:
+    """Construction du style de tables reportlab platypus pour les bulletins "classiques"
+    """
+
+    LINEWIDTH = 0.5
+    LINECOLOR = Color(0, 0, 0)
+    UEBGCOLOR = Color(
+        170 / 255.0, 187 / 255.0, 204 / 255.0
+    )  # couleur fond lignes titres UE
+    MODSEPCOLOR = Color(
+        170 / 255.0, 170 / 255.0, 170 / 255.0
+    )  # lignes séparant les modules
+
+    def __init__(self):
+        self.pdfTableStyle = [
+            ("LINEBELOW", (0, 0), (-1, 0), self.LINEWIDTH, self.LINECOLOR)
+        ]
+        self.tabline = 0
+
+    def get_style(self):
+        "get resulting style (a list of platypus table commands)"
+        # ajoute cadre extérieur bleu:
+        self.pdfTableStyle.append(("BOX", (0, 0), (-1, -1), 0.4, blue))
+
+        return self.pdfTableStyle
+
+    def newline(self, ue_type=None):
+        self.tabline += 1
+        if ue_type == "cur":  # UE courante non prise en compte (car capitalisee)
+            self.pdfTableStyle.append(
+                (
+                    "BACKGROUND",
+                    (0, self.tabline),
+                    (-1, self.tabline),
+                    Color(210 / 255.0, 210 / 255.0, 210 / 255.0),
+                )
+            )
+
+    def ueline(self):  # met la ligne courante du tableau pdf en style 'UE'
+        self.newline()
+        i = self.tabline
+        self.pdfTableStyle.append(("BACKGROUND", (0, i), (-1, i), self.UEBGCOLOR))
+
+    def modline(
+        self, ue_type=None
+    ):  # met la ligne courante du tableau pdf en style 'Module'
+        self.newline(ue_type=ue_type)
+        i = self.tabline
+        self.pdfTableStyle.append(("LINEABOVE", (0, i), (-1, i), 1, self.MODSEPCOLOR))
+
+
+def _bulletin_pdf_table_legacy(context, I, version="long"):
+    """Génère la table centrale du bulletin de notes
+    Renvoie un triplet:
+    - table (liste de listes de chaines de caracteres)
+    - style (commandes table Platypus)
+    - largeurs de colonnes
+    """
+    S = BulTableStyle()
+    P = []  # elems pour gen. pdf
+    formsemestre_id = I["formsemestre_id"]
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    bul_show_abs_modules = context.get_preference(
+        "bul_show_abs_modules", formsemestre_id
+    )
+
+    if context.get_preference("bul_show_minmax", formsemestre_id):
+        minmax = ' <font size="8">[%s, %s]</font>' % (I["moy_min"], I["moy_max"])
+    else:
+        minmax = ""
+
+    t = [
+        "Moyenne",
+        "%s%s%s" % (I["moy_gen"], I["etud_etat_html"], minmax),
+        I["rang_txt"],
+        "Note/20",
+        "Coef",
+    ]
+    if bul_show_abs_modules:
+        t.append("Abs (J. / N.J.)")
+    P.append(bold_paras(t))
+
+    def list_modules(ue_modules, ue_type=None):
+        "ajoute les lignes decrivant les modules d'une UE, avec eventuellement les évaluations de chacun"
+        for mod in ue_modules:
+            if mod["mod_moy_txt"] == "NI":
+                continue  # saute les modules où on n'est pas inscrit
+            S.modline(ue_type=ue_type)
+            if context.get_preference("bul_show_minmax_mod", formsemestre_id):
+                rang_minmax = '%s <font size="8">[%s, %s]</font>' % (
+                    mod["mod_rang_txt"],
+                    fmt_note(mod["stats"]["min"]),
+                    fmt_note(mod["stats"]["max"]),
+                )
+            else:
+                rang_minmax = mod["mod_rang_txt"]  # vide si pas option rang
+            t = [
+                mod["code"],
+                mod["name"],
+                rang_minmax,
+                mod["mod_moy_txt"],
+                mod["mod_coef_txt"],
+            ]
+            if bul_show_abs_modules:
+                t.append(mod["mod_abs_txt"])
+            P.append(t)
+            if version != "short":
+                # --- notes de chaque eval:
+                for e in mod["evaluations"]:
+                    if int(e["visibulletin"]) == 1 or version == "long":
+                        S.newline(ue_type=ue_type)
+                        t = ["", "", e["name"], e["note_txt"], e["coef_txt"]]
+                        if bul_show_abs_modules:
+                            t.append("")
+                        P.append(t)
+
+    for ue in I["ues"]:
+        ue_descr = ue["ue_descr_txt"]
+        coef_ue = ue["coef_ue_txt"]
+        ue_type = None
+        if ue["ue_status"]["is_capitalized"]:
+            t = [ue["acronyme"], ue["moy_ue_txt"], ue_descr, "", coef_ue]
+            if bul_show_abs_modules:
+                t.append("")
+            P.append(bold_paras(t))
+            coef_ue = ""
+            ue_descr = "(en cours, non prise en compte)"
+            S.ueline()
+            if context.get_preference("bul_show_ue_cap_details", formsemestre_id):
+                list_modules(ue["modules_capitalized"])
+            ue_type = "cur"
+
+        if context.get_preference("bul_show_minmax", formsemestre_id):
+            moy_txt = '%s <font size="8">[%s, %s]</font>' % (
+                ue["cur_moy_ue_txt"],
+                ue["min"],
+                ue["max"],
+            )
+        else:
+            moy_txt = ue["cur_moy_ue_txt"]
+        t = [ue["acronyme"], moy_txt, ue_descr, "", coef_ue]
+        if bul_show_abs_modules:
+            t.append("")
+        P.append(bold_paras(t))
+        S.ueline()
+        list_modules(ue["modules"], ue_type=ue_type)
+
+    # Largeur colonnes:
+    colWidths = [None, 5 * cm, 6 * cm, 2 * cm, 1.2 * cm]
+    if len(P[0]) > 5:
+        colWidths.append(1.5 * cm)  # absences/modules
+
+    return P, S.get_style(), colWidths
diff --git a/sco_bulletins_pdf.py b/sco_bulletins_pdf.py
new file mode 100644
index 0000000000000000000000000000000000000000..7e6750ebe41055f1d545a29c4ecc9a9ac8a9e36a
--- /dev/null
+++ b/sco_bulletins_pdf.py
@@ -0,0 +1,261 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Génération des bulletins de notes en format PDF
+
+On peut installer plusieurs classes générant des bulletins de formats différents.
+La préférence (par semestre) 'bul_pdf_class_name' conserve le nom de la classe Python
+utilisée pour générer les bulletins en PDF. Elle doit être une sous-classe de PDFBulletinGenerator
+et définir les méthodes fabriquant les éléments PDF:
+ gen_part_title
+ gen_table
+ gen_part_below
+ gen_signatures
+
+Les éléments PDF sont des objets PLATYPUS de la bibliothèque Reportlab.
+Voir la documentation (Reportlab's User Guide), chapitre 5 et suivants.
+
+Pour définir un nouveau type de bulletin:
+ - créer un fichier source sco_bulletins_pdf_xxxx.py où xxxx est le nom (court) de votre type;
+ - dans ce fichier, sous-classer PDFBulletinGenerator ou PDFBulletinGeneratorDefault
+    (s'inspirer de sco_bulletins_pdf_default);
+ - en fin du fichier sco_bulletins_pdf.py, ajouter la ligne
+    import sco_bulletins_pdf_xxxx
+ - votre type sera alors (après redémarrage de ScoDoc) proposé dans le formulaire de paramètrage ScoDoc.
+
+Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
+
+"""
+import htmlutils, time
+import pprint, traceback
+
+from notes_table import *
+import sco_bulletins
+
+from sco_pdf import *
+import os
+
+
+def pdfassemblebulletins(
+    formsemestre_id,
+    objects,
+    bul_title,
+    infos,
+    pagesbookmarks,
+    filigranne=None,
+    server_name="",
+    context=None,
+):
+    "generate PDF document from a list of PLATYPUS objects"
+    if not objects:
+        return ""
+    # Paramètres de mise en page
+    margins = (
+        context.get_preference("left_margin", formsemestre_id),
+        context.get_preference("top_margin", formsemestre_id),
+        context.get_preference("right_margin", formsemestre_id),
+        context.get_preference("bottom_margin", formsemestre_id),
+    )
+
+    report = cStringIO.StringIO()  # in-memory document, no disk file
+    document = BaseDocTemplate(report)
+    document.addPageTemplates(
+        ScolarsPageTemplate(
+            document,
+            author="%s %s (E. Viennet)" % (SCONAME, SCOVERSION),
+            title="Bulletin %s" % bul_title,
+            subject="Bulletin de note",
+            server_name=server_name,
+            margins=margins,
+            pagesbookmarks=pagesbookmarks,
+            filigranne=filigranne,
+            preferences=context.get_preferences(formsemestre_id),
+        )
+    )
+    document.build(objects)
+    data = report.getvalue()
+    return data
+
+
+def process_field(
+    context, field, cdict, style, suppress_empty_pars=False, format="pdf"
+):
+    """Process a field given in preferences, returns
+    - if format = 'pdf': a list of Platypus objects
+    - if format = 'html' : a string
+    
+    Substitutes all %()s markup    
+    Remove potentialy harmful <img> tags
+    Replaces <logo name="header" width="xxx" height="xxx">
+    by <img src=".../logos/logo_header" width="xxx" height="xxx">
+
+    If format = 'html', replaces <para> by <p>. HTML does not allow logos. 
+    """
+    try:
+        text = (field or "") % WrapDict(
+            cdict
+        )  # note that None values are mapped to empty strings
+    except:
+        log("process_field: invalid format=%s" % field)
+        text = (
+            "<para><i>format invalide !<i></para><para>"
+            + traceback.format_exc()
+            + "</para>"
+        )
+    # remove unhandled or dangerous tags:
+    text = re.sub(r"<\s*img", "", text)
+    if format == "html":
+        # convert <para>
+        text = re.sub(r"<\s*para(\s*)(.*?)>", r"<p>", text)
+        return text
+    # --- PDF format:
+    # handle logos:
+    image_dir = SCODOC_LOGOS_DIR + "/logos_" + context.DeptId() + "/"
+    if not os.path.exists(image_dir):
+        image_dir = SCODOC_LOGOS_DIR + "/" # use global logos
+    text = re.sub(
+        r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text
+    )  # remove forbidden src attribute
+    text = re.sub(
+        r'<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>',
+        r'<img\1src="%s/logo_\2.jpg"\3/>' % image_dir,
+        text,
+    )
+    # nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
+    # tentatives d'acceder à d'autres fichiers !
+
+    # log('field: %s' % (text))
+    return makeParas(text, style, suppress_empty=suppress_empty_pars)
+
+
+def get_formsemestre_bulletins_pdf(
+    context, formsemestre_id, REQUEST, version="selectedevals"
+):
+    "document pdf et filename"
+    cached = context._getNotesCache().get_bulletins_pdf(formsemestre_id, version)
+    if cached:
+        return cached[1], cached[0]
+    fragments = []
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    # Make each bulletin
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etudids, get_sexnom
+    bookmarks = {}
+    filigrannes = {}
+    i = 1
+    for etudid in nt.get_etudids():
+        frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
+            context,
+            formsemestre_id,
+            etudid,
+            format="pdfpart",
+            version=version,
+            REQUEST=REQUEST,
+        )
+        fragments += frag
+        filigrannes[i] = filigranne
+        bookmarks[i] = suppress_accents(nt.get_sexnom(etudid))
+        i = i + 1
+    #
+    infos = {"DeptName": context.get_preference("DeptName", formsemestre_id)}
+    if REQUEST:
+        server_name = REQUEST.BASE0
+    else:
+        server_name = ""
+    try:
+        PDFLOCK.acquire()
+        pdfdoc = pdfassemblebulletins(
+            formsemestre_id,
+            fragments,
+            sem["titremois"],
+            infos,
+            bookmarks,
+            filigranne=filigrannes,
+            server_name=server_name,
+            context=context,
+        )
+    finally:
+        PDFLOCK.release()
+    #
+    dt = time.strftime("%Y-%m-%d")
+    filename = "bul-%s-%s.pdf" % (sem["titre_num"], dt)
+    filename = unescape_html(filename).replace(" ", "_").replace("&", "")
+    # fill cache
+    context._getNotesCache().store_bulletins_pdf(
+        formsemestre_id, version, filename, pdfdoc
+    )
+    return pdfdoc, filename
+
+
+def get_etud_bulletins_pdf(context, etudid, REQUEST, version="selectedevals"):
+    "Bulletins pdf de tous les semestres de l'étudiant, et filename"
+    etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+    fragments = []
+    bookmarks = {}
+    filigrannes = {}
+    i = 1
+    for sem in etud["sems"]:
+        frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
+            context,
+            sem["formsemestre_id"],
+            etudid,
+            format="pdfpart",
+            version=version,
+            REQUEST=REQUEST,
+        )
+        fragments += frag
+        filigrannes[i] = filigranne
+        bookmarks[i] = sem["session_id"]  # eg RT-DUT-FI-S1-2015
+        i = i + 1
+    infos = {"DeptName": context.get_preference("DeptName")}
+    if REQUEST:
+        server_name = REQUEST.BASE0
+    else:
+        server_name = ""
+    try:
+        PDFLOCK.acquire()
+        pdfdoc = pdfassemblebulletins(
+            None,
+            fragments,
+            etud["nomprenom"],
+            infos,
+            bookmarks,
+            filigranne=filigrannes,
+            server_name=server_name,
+            context=context,
+        )
+    finally:
+        PDFLOCK.release()
+    #
+    filename = "bul-%s" % (etud["nomprenom"])
+    filename = (
+        unescape_html(filename).replace(" ", "_").replace("&", "").replace(".", "")
+        + ".pdf"
+    )
+
+    return pdfdoc, filename
diff --git a/sco_bulletins_signature.py b/sco_bulletins_signature.py
new file mode 100644
index 0000000000000000000000000000000000000000..ecf2e29c3530ba49fe64b958ddaa4d268758605b
--- /dev/null
+++ b/sco_bulletins_signature.py
@@ -0,0 +1,106 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Signature des bulletin pdf
+
+
+XXX en projet, non finalisé: on peut utiliser en attendant les paramétrages des bulletins (malcommodes).
+
+
+Il ne s'agit pas d'une "signature" électronique, mais simplement d'une image 
+que l'on intègre dans les documents pdf générés (bulletins, classeur des bulletins,
+envois par mail).
+
+La signature est controlée par:
+- la présence d'un fichier .../ScoDoc/static/signatures/<DeptId>/<formsemestre_id>/bul_sig_{left|right}
+   ou, à défaut, .../ScoDoc/signatures/<DeptId>/bul_sig_{left|right}
+- les préférences booléennes bul_sig_left et bul_sig_right 
+   (ne pas confondre avec bul_show_sig_left...)
+- les préférences bul_sig_left_image_height et bul_sig_right_image_height 
+    (hauteur de l'image, float, en mm)
+
+API:
+- form_change_bul_sig( side = "left" | "right", [formsemestre_id])
+    affiche signature courante, soit globale soit pour le semestre
+    upload nouvelle
+- set_bul_sig( side = "left", [formsemestre_id], image )
+
+Lien sur Scolarite/edit_preferences (sans formsemestre_id)
+et sur page "réglages bulletin" (avec formsemestre_id)
+
+"""
+import os
+
+
+def form_change_bul_sig(context, side, formsemestre_id=None, REQUEST=None):
+    """Change pdf signature
+    """
+    filename = _get_sig_existing_filename(
+        context, side, formsemestre_id=formsemestre_id
+    )
+    if side == "left":
+        sidetxt = "gauche"
+    elif side == "right":
+        sidetxt = "droite"
+    else:
+        raise ValueError("invalid value for 'side' parameter")
+    signatureloc = get_bul_sig_img()
+    H = [
+        self.sco_header(REQUEST, page_title="Changement de signature"),
+        """<h2>Changement de la signature bulletin de %(sidetxt)s</h2>       
+            """
+        % (sidetxt,),
+    ]
+    "<p>Photo actuelle (%(signatureloc)s):      "
+
+
+def get_bul_sig_img(context, side, formsemestre_id=None):
+    "send back signature image data"
+    # slow, not cached, used for unfrequent access (do not bypass python)
+
+
+def _sig_filename(context, side, formsemestre_id=None):
+    if not side in ("left", "right"):
+        raise ValueError("side must be left or right")
+    dirs = [SCODOC_LOGOS_DIR, context.DeptId()]
+    if formsemestre_id:
+        dirs.append(formsemestre_id)
+    dirs.append("bul_sig_{}".format(side))
+    return os.path.join(*dirs)
+
+
+def _get_sig_existing_filename(context, side, formsemestre_id=None):
+    "full path to signature to use, or None if no signature available"
+    if formsemestre_id:
+        filename = _sig_filename(context, side, formsemestre_id=formsemestre_id)
+        if os.path.exists(filename):
+            return filename
+    filename = _sig_filename(context, side)
+    if os.path.exists(filename):
+        return filename
+    else:
+        return None
diff --git a/sco_bulletins_standard.py b/sco_bulletins_standard.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc5af80f86874fd868eab248914f56514341b985
--- /dev/null
+++ b/sco_bulletins_standard.py
@@ -0,0 +1,674 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""Generation du bulletin note au format standard
+
+Nouvelle version juillet 2011: changement de la présentation de la table.
+
+
+
+
+Note sur le PDF:
+Les templates utilisent les XML markup tags de ReportLab
+ (voir ReportLab user guide, page 70 et suivantes), dans lesquels les balises
+de la forme %(XXX)s sont remplacées par la valeur de XXX, pour XXX dans:
+
+- preferences du semestre (ou globales) (voir sco_preferences.py)
+- champs de formsemestre: titre, date_debut, date_fin, responsable, anneesem
+- champs de l'etudiant s(etud, décoré par getEtudInfo)
+- demission ("DEMISSION" ou vide)
+- situation ("Inscrit le XXX")
+
+Balises img: actuellement interdites.
+
+"""
+import traceback, re
+
+import sco_formsemestre
+from sco_pdf import *
+import sco_preferences
+from notes_log import log
+import sco_bulletins_generator
+import sco_bulletins_pdf
+import sco_groups
+import sco_evaluations
+import gen_tables
+import sco_codes_parcours
+
+# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences)
+class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
+    description = "standard ScoDoc (version 2011)"  # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc
+    supported_formats = ["html", "pdf"]
+
+    def bul_title_pdf(self):
+        """Génère la partie "titre" du bulletin de notes.
+        Renvoie une liste d'objets platypus
+        """
+        objects = sco_bulletins_pdf.process_field(
+            self.context, self.preferences["bul_pdf_title"], self.infos, self.FieldStyle
+        )
+        objects.append(
+            Spacer(1, 5 * mm)
+        )  # impose un espace vertical entre le titre et la table qui suit
+        return objects
+
+    def bul_table(self, format="html"):
+        """Génère la table centrale du bulletin de notes
+        Renvoie:
+        - en HTML: une chaine
+        - en PDF: une liste d'objets PLATYPUS (eg instance de Table).
+        """
+        formsemestre_id = self.infos["formsemestre_id"]
+        colkeys, P, pdf_style, colWidths = self.build_bulletin_table()
+
+        T = gen_tables.GenTable(
+            rows=P,
+            columns_ids=colkeys,
+            pdf_table_style=pdf_style,
+            pdf_col_widths=[colWidths[k] for k in colkeys],
+            preferences=self.context.get_preferences(formsemestre_id),
+            html_class="notes_bulletin",
+            html_class_ignore_default=True,
+            html_with_td_classes=True,
+        )
+
+        return T.gen(format=format)
+
+    def bul_part_below(self, format="html"):
+        """Génère les informations placées sous la table de notes
+        (absences, appréciations, décisions de jury...)
+        Renvoie:
+        - en HTML: une chaine
+        - en PDF: une liste d'objets platypus
+        """
+        H = []  # html
+        Op = []  # objets platypus
+        # ----- ABSENCES
+        if self.preferences["bul_show_abs"]:
+            nbabs = self.infos["nbabs"]
+            Op.append(Spacer(1, 2 * mm))
+            if nbabs:
+                H.append(
+                    """<p class="bul_abs">
+                <a href="../Absences/CalAbs?etudid=%(etudid)s" class="bull_link">
+                <b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées
+                (pendant ce semestre).
+                </a></p>
+                """
+                    % self.infos
+                )
+                Op.append(
+                    Paragraph(
+                        SU(
+                            "%(nbabs)s absences (1/2 journées), dont %(nbabsjust)s justifiées."
+                            % self.infos
+                        ),
+                        self.CellStyle,
+                    )
+                )
+            else:
+                H.append("""<p class="bul_abs">Pas d'absences signalées.</p>""")
+                Op.append(Paragraph(SU("Pas d'absences signalées."), self.CellStyle))
+
+        # ---- APPRECIATIONS
+        # le dir. des etud peut ajouter des appreciations,
+        # mais aussi le chef (perm. ScoEtudInscrit)
+        can_edit_app = (str(self.authuser) in self.infos["responsables"]) or (
+            self.authuser.has_permission(ScoEtudInscrit, self.context)
+        )
+        H.append('<div class="bull_appreciations">')
+        for app in self.infos["appreciations_list"]:
+            if can_edit_app:
+                mlink = (
+                    '<a class="stdlink" href="appreciation_add_form?id=%s">modifier</a> <a class="stdlink" href="appreciation_add_form?id=%s&amp;suppress=1">supprimer</a>'
+                    % (app["id"], app["id"])
+                )
+            else:
+                mlink = ""
+            H.append(
+                '<p><span class="bull_appreciations_date">%s</span>%s<span class="bull_appreciations_link">%s</span></p>'
+                % (app["date"], app["comment"], mlink)
+            )
+        if can_edit_app:
+            H.append(
+                '<p><a class="stdlink" href="appreciation_add_form?etudid=%(etudid)s&amp;formsemestre_id=%(formsemestre_id)s">Ajouter une appréciation</a></p>'
+                % self.infos
+            )
+        H.append("</div>")
+        # Appreciations sur PDF:
+        if self.infos.get("appreciations_list", False):
+            Op.append(Spacer(1, 3 * mm))
+            Op.append(
+                Paragraph(
+                    SU("Appréciation : " + "\n".join(self.infos["appreciations_txt"])),
+                    self.CellStyle,
+                )
+            )
+
+        # ----- DECISION JURY
+        if self.preferences["bul_show_decision"]:
+            Op += sco_bulletins_pdf.process_field(
+                self.context,
+                self.preferences["bul_pdf_caption"],
+                self.infos,
+                self.FieldStyle,
+                format="pdf",
+            )
+            field = sco_bulletins_pdf.process_field(
+                self.context,
+                self.preferences["bul_pdf_caption"],
+                self.infos,
+                self.FieldStyle,
+                format="html",
+            )
+            H.append('<div class="bul_decision">' + field + "</div>")
+
+        # -----
+        if format == "pdf":
+            return Op
+        elif format == "html":
+            return "\n".join(H)
+
+    def bul_signatures_pdf(self):
+        """Génère les signatures placées en bas du bulletin PDF
+        Renvoie une liste d'objets platypus
+        """
+        show_left = self.preferences["bul_show_sig_left"]
+        show_right = self.preferences["bul_show_sig_right"]
+        if show_left or show_right:
+            if show_left:
+                L = [
+                    [
+                        sco_bulletins_pdf.process_field(
+                            self.context,
+                            self.preferences["bul_pdf_sig_left"],
+                            self.infos,
+                            self.FieldStyle,
+                        )
+                    ]
+                ]
+            else:
+                L = [[""]]
+            if show_right:
+                L[0].append(
+                    sco_bulletins_pdf.process_field(
+                        self.context,
+                        self.preferences["bul_pdf_sig_right"],
+                        self.infos,
+                        self.FieldStyle,
+                    )
+                )
+            else:
+                L[0].append("")
+            t = Table(L)
+            t._argW[0] = 10 * cm  # fixe largeur colonne gauche
+
+            return [Spacer(1, 1.5 * cm), t]  # espace vertical avant signatures
+        else:
+            return []
+
+    PDF_LINEWIDTH = 0.5
+    PDF_LINECOLOR = Color(0, 0, 0)
+    PDF_MODSEPCOLOR = Color(
+        170 / 255.0, 170 / 255.0, 170 / 255.0
+    )  # lignes séparant les modules
+    PDF_UE_CUR_BG = Color(
+        210 / 255.0, 210 / 255.0, 210 / 255.0
+    )  # fond UE courantes non prises en compte
+    PDF_LIGHT_GRAY = Color(0.75, 0.75, 0.75)
+
+    PDF_COLOR_CACHE = {}  # (r,g,b) : pdf Color instance
+
+    def ue_color(self, ue_type=UE_STANDARD):
+        rgb_color = UE_COLORS.get(ue_type, UE_DEFAULT_COLOR)
+        color = self.PDF_COLOR_CACHE.get(rgb_color, None)
+        if not color:
+            color = Color(*rgb_color)
+            self.PDF_COLOR_CACHE[rgb_color] = color
+        return color
+
+    def ue_color_rgb(self, ue_type=UE_STANDARD):
+        rgb_color = UE_COLORS.get(ue_type, UE_DEFAULT_COLOR)
+        return "rgb(%d,%d,%d);" % (
+            rgb_color[0] * 255,
+            rgb_color[1] * 255,
+            rgb_color[2] * 255,
+        )
+
+    def build_bulletin_table(self):
+        """Génère la table centrale du bulletin de notes
+        Renvoie: colkeys, P, pdf_style, colWidths
+        - colkeys: nom des colonnes de la table (clés)
+        - table (liste de dicts de chaines de caracteres)
+        - style (commandes table Platypus)
+        - largeurs de colonnes pour PDF
+        """
+        I = self.infos
+        context = self.context
+        P = []  # elems pour générer table avec gen_table (liste de dicts)
+        formsemestre_id = I["formsemestre_id"]
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        prefs = context.get_preferences(formsemestre_id)
+
+        # Colonnes à afficher:
+        with_col_abs = prefs["bul_show_abs_modules"]
+        with_col_minmax = (
+            prefs["bul_show_minmax"]
+            or prefs["bul_show_minmax_mod"]
+            or prefs["bul_show_minmax_eval"]
+        )
+        with_col_moypromo = prefs["bul_show_moypromo"]
+        with_col_rang = prefs["bul_show_rangs"]
+        with_col_coef = prefs["bul_show_coef"]
+        with_col_ects = prefs["bul_show_ects"]
+
+        colkeys = ["titre", "module"]  # noms des colonnes à afficher
+        if with_col_rang:
+            colkeys += ["rang"]
+        if with_col_minmax:
+            colkeys += ["min"]
+        if with_col_moypromo:
+            colkeys += ["moy"]
+        if with_col_minmax:
+            colkeys += ["max"]
+        colkeys += ["note"]
+        if with_col_coef:
+            colkeys += ["coef"]
+        if with_col_ects:
+            colkeys += ["ects"]
+        if with_col_abs:
+            colkeys += ["abs"]
+        colidx = {}  # { nom_colonne : indice à partir de 0 } (pour styles platypus)
+        i = 0
+        for k in colkeys:
+            colidx[k] = i
+            i += 1
+
+        if prefs["bul_pdf_mod_colwidth"]:
+            bul_pdf_mod_colwidth = float(prefs["bul_pdf_mod_colwidth"]) * cm
+        else:
+            bul_pdf_mod_colwidth = None
+        colWidths = {
+            "titre": None,
+            "module": bul_pdf_mod_colwidth,
+            "min": 1.5 * cm,
+            "moy": 1.5 * cm,
+            "max": 1.5 * cm,
+            "rang": 2.2 * cm,
+            "note": 2 * cm,
+            "coef": 1.5 * cm,
+            "ects": 1.5 * cm,
+            "abs": 2.0 * cm,
+        }
+        # HTML specific
+        linktmpl = (
+            '<span onclick="toggle_vis_ue(this);" class="toggle_ue">%s</span>&nbsp;'
+        )
+        minuslink = linktmpl % icontag("minus_img", border="0", alt="-")
+        pluslink = linktmpl % icontag("plus_img", border="0", alt="+")
+
+        # 1er ligne titres
+        t = {
+            "min": "Promotion",
+            "moy": "",
+            "max": "",
+            "rang": "Rang",
+            "note": "Note/20",
+            "coef": "Coef.",
+            "ects": "ECTS",
+            "abs": "Abs.",
+            "_min_colspan": 2,
+            "_css_row_class": "note_bold",
+            "_pdf_row_markup": ["b"],
+            "_pdf_style": [],
+        }
+        if with_col_moypromo and not with_col_minmax:
+            t["moy"] = "Promotion"
+        P.append(t)
+        # 2eme ligne titres si nécessaire
+        if with_col_minmax or with_col_abs:
+            t = {
+                "min": "mini",
+                "moy": "moy.",
+                "max": "maxi",
+                "abs": "(Tot. / J.)",
+                "_css_row_class": "note_bold",
+                "_pdf_row_markup": ["b"],
+                "_pdf_style": [],
+            }
+            P.append(t)
+        P[-1]["_pdf_style"].append(
+            ("LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, self.PDF_LINECOLOR)
+        )
+
+        # Moyenne générale:
+        nbabs = I["nbabs"]
+        nbabsjust = I["nbabsjust"]
+        t = {
+            "titre": "Moyenne générale:",
+            "rang": I["rang_nt"],
+            "note": I["moy_gen"],
+            "min": I["moy_min"],
+            "max": I["moy_max"],
+            "moy": I["moy_moy"],
+            "abs": "%s / %s" % (nbabs, nbabsjust),
+            "_css_row_class": "notes_bulletin_row_gen",
+            "_titre_colspan": 2,
+            "_pdf_row_markup": ['font size="12"', "b"],  # bold, size 12
+            "_pdf_style": [("LINEABOVE", (0, 1), (-1, 1), 1, self.PDF_LINECOLOR)],
+        }
+        P.append(t)
+
+        # Rangs dans les partitions:
+        partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
+            context, formsemestre_id
+        )
+        for partition in partitions:
+            if partition["bul_show_rank"]:
+                partition_id = partition["partition_id"]
+                P.append(
+                    {
+                        "titre": "Rang dans %s %s:  %s / %s inscrits"
+                        % (
+                            partition["partition_name"],
+                            I["gr_name"][partition_id],
+                            I["rang_gr"][partition_id],
+                            I["ninscrits_gr"][partition_id],
+                        ),
+                        "_titre_colspan": 3,
+                        "_css_row_class": "notes_bulletin_row_rang",
+                    }
+                )
+
+        # Chaque UE:
+        for ue in I["ues"]:
+            ue_type = None
+            coef_ue = ue["coef_ue_txt"]
+            ue_descr = ue["ue_descr_txt"]
+            rowstyle = ""
+            plusminus = minuslink  #
+            if ue["ue_status"]["is_capitalized"]:
+                # UE capitalisée meilleure que UE courante:
+                if prefs["bul_show_ue_cap_details"]:
+                    hidden = False
+                    cssstyle = ""
+                    plusminus = minuslink
+                else:
+                    hidden = True
+                    cssstyle = "sco_hide"
+                    plusminus = pluslink
+                try:
+                    ects_txt = str(int(ue["ects"]))
+                except:
+                    ects_txt = "-"
+
+                t = {
+                    "titre": ue["acronyme"] + " " + ue["titre"],
+                    "_titre_html": plusminus + ue["acronyme"] + " " + ue["titre"],
+                    "_titre_help": ue["ue_descr_txt"],
+                    "_titre_colspan": 2,
+                    "module": ue_descr,
+                    "note": ue["moy_ue_txt"],
+                    "coef": coef_ue,
+                    "ects": ects_txt,
+                    "_css_row_class": "notes_bulletin_row_ue",
+                    "_tr_attrs": 'style="background-color: %s"' % self.ue_color_rgb(),
+                    "_pdf_row_markup": ["b"],
+                    "_pdf_style": [
+                        ("BACKGROUND", (0, 0), (-1, 0), self.ue_color()),
+                        ("LINEABOVE", (0, 0), (-1, 0), 1, self.PDF_LINECOLOR),
+                    ],
+                }
+                P.append(t)
+                # Notes de l'UE capitalisée obtenues antérieurement:
+                self._list_modules(
+                    ue["modules_capitalized"],
+                    matieres_modules=self.infos["matieres_modules_capitalized"],
+                    ue_type=ue_type,
+                    P=P,
+                    prefs=prefs,
+                    rowstyle=" bul_row_ue_cap %s" % cssstyle,
+                    hidden=hidden,
+                )
+                ue_type = "cur"
+                ue_descr = ""
+                rowstyle = (
+                    " bul_row_ue_cur"  # style css pour indiquer UE non prise en compte
+                )
+            try:
+                ects_txt = str(int(ue["ects"]))
+            except:
+                ects_txt = "-"
+            t = {
+                "titre": ue["acronyme"] + " " + ue["titre"],
+                "_titre_html": minuslink + ue["acronyme"] + " " + ue["titre"],
+                "_titre_colspan": 2,
+                "module": ue["titre"],
+                "rang": ue_descr,
+                "note": ue["cur_moy_ue_txt"],
+                "coef": coef_ue,
+                "ects": ects_txt,
+                "_css_row_class": "notes_bulletin_row_ue",
+                "_tr_attrs": 'style="background-color: %s"'
+                % self.ue_color_rgb(ue_type=ue["type"]),
+                "_pdf_row_markup": ["b"],
+                "_pdf_style": [
+                    ("BACKGROUND", (0, 0), (-1, 0), self.ue_color(ue_type=ue["type"])),
+                    ("LINEABOVE", (0, 0), (-1, 0), 1, self.PDF_LINECOLOR),
+                ],
+            }
+            if ue_type == "cur":
+                t["module"] = "(en cours, non prise en compte)"
+                t["_css_row_class"] += " notes_bulletin_row_ue_cur"
+                t["_titre_help"] = "(en cours, non prise en compte)"
+            if prefs["bul_show_minmax"]:
+                t["min"] = fmt_note(ue["min"])
+                t["max"] = fmt_note(ue["max"])
+            if prefs["bul_show_moypromo"]:
+                t["moy"] = fmt_note(ue["moy"]).replace("NA", "-")
+            # Cas particulier des UE sport (bonus)
+            if ue["type"] == UE_SPORT and not ue_descr:
+                del t["module"]
+                del t["coef"]
+                t["_pdf_style"].append(("SPAN", (colidx["note"], 0), (-1, 0)))
+                # t["_module_colspan"] = 3 # non car bug si aucune colonne additionnelle 
+            # UE électives
+            if ue["type"] == UE_ELECTIVE:
+                t["module"] += " <i>(élective)</i>"
+            if ue["modules"]:
+                if (
+                    ue_type != "cur" or prefs["bul_show_ue_cap_current"]
+                ):  # ne montre pas UE en cours et capitalisée, sauf si forcé
+                    P.append(t)
+                    self._list_modules(
+                        ue["modules"],
+                        matieres_modules=self.infos["matieres_modules"],
+                        ue_type=ue_type,
+                        P=P,
+                        prefs=prefs,
+                        rowstyle=rowstyle,
+                    )
+
+        # Ligne somme ECTS
+        if with_col_ects:
+            t = {
+                "titre": "Crédits ECTS acquis:",
+                "ects": str(int(I["sum_ects"])),  # incluant les UE capitalisees
+                "_css_row_class": "notes_bulletin_row_sum_ects",
+                "_pdf_row_markup": ["b"],  # bold
+                "_pdf_style": [
+                    ("BACKGROUND", (0, 0), (-1, 0), self.PDF_LIGHT_GRAY),
+                    ("LINEABOVE", (0, 0), (-1, 0), 1, self.PDF_LINECOLOR),
+                ],
+            }
+            P.append(t)
+        # Global pdf style commands:
+        pdf_style = [
+            ("VALIGN", (0, 0), (-1, -1), "TOP"),
+            ("BOX", (0, 0), (-1, -1), 0.4, blue),  # ajoute cadre extérieur bleu:
+        ]
+        #
+        return colkeys, P, pdf_style, colWidths
+
+    def _list_modules(
+        self,
+        ue_modules,
+        matieres_modules={},
+        ue_type=None,
+        P=None,
+        prefs=None,
+        rowstyle="",
+        hidden=False,
+    ):
+        """Liste dans la table les descriptions des modules et, si version != short, des évaluations.
+        """
+        if ue_type == "cur":  # UE courante non prise en compte (car capitalisee)
+            pdf_style_bg = [("BACKGROUND", (0, 0), (-1, 0), self.PDF_UE_CUR_BG)]
+        else:
+            pdf_style_bg = []
+        pdf_style = pdf_style_bg + [
+            ("LINEABOVE", (0, 0), (-1, 0), 1, self.PDF_MODSEPCOLOR),
+            ("SPAN", (0, 0), (1, 0)),
+        ]
+        if ue_type == "cur":  # UE courante non prise en compte (car capitalisee)
+            pdf_style.append(("BACKGROUND", (0, 0), (-1, 0), self.PDF_UE_CUR_BG))
+
+        last_matiere_id = None
+        for mod in ue_modules:
+            if mod["mod_moy_txt"] == "NI":
+                continue  # saute les modules où on n'est pas inscrit
+            # Matière:
+            matiere_id = mod["module"]["matiere_id"]
+            if prefs["bul_show_matieres"] and matiere_id != last_matiere_id:
+                mat = matieres_modules[matiere_id]
+                P.append(
+                    {
+                        "titre": mat["titre"],
+                        #'_titre_help' : matiere_id,
+                        "_titre_colspan": 2,
+                        "note": mat["moy_txt"],
+                        "_css_row_class": "notes_bulletin_row_mat%s" % rowstyle,
+                        "_pdf_style": pdf_style_bg
+                        + [("LINEABOVE", (0, 0), (-1, 0), 2, self.PDF_MODSEPCOLOR)],
+                        "_pdf_row_markup": ['font color="darkblue"'],
+                    }
+                )
+            last_matiere_id = matiere_id
+            #
+            t = {
+                "titre": mod["code_txt"] + " " + mod["name"],
+                "_titre_colspan": 2,
+                "rang": mod["mod_rang_txt"],  # vide si pas option rang
+                "note": mod["mod_moy_txt"],
+                "coef": mod["mod_coef_txt"],
+                "abs": mod.get(
+                    "mod_abs_txt", ""
+                ),  # absent si pas option show abs module
+                "_css_row_class": "notes_bulletin_row_mod%s" % rowstyle,
+                "_titre_target": "moduleimpl_status?moduleimpl_id=%s"
+                % mod["moduleimpl_id"],
+                "_titre_help": mod["mod_descr_txt"],
+                "_hidden": hidden,
+                "_pdf_style": pdf_style,
+            }
+            if prefs["bul_show_minmax_mod"]:
+                t["min"] = fmt_note(mod["stats"]["min"])
+                t["max"] = fmt_note(mod["stats"]["max"])
+            if prefs["bul_show_moypromo"]:
+                t["moy"] = fmt_note(mod["stats"]["moy"]).replace("NA", "-")
+            P.append(t)
+
+            if self.version != "short":
+                # --- notes de chaque eval:
+                nbeval = self._list_evals(
+                    mod["evaluations"], P, rowstyle, pdf_style_bg, hidden, prefs=prefs
+                )
+                # evals futures ou incomplètes:
+                nbeval += self._list_evals(
+                    mod["evaluations_incompletes"],
+                    P,
+                    rowstyle,
+                    pdf_style_bg,
+                    hidden,
+                    incomplete=True,
+                    prefs=prefs,
+                )
+
+                if nbeval:  # boite autour des evaluations (en pdf)
+                    P[-1]["_pdf_style"].append(
+                        ("BOX", (1, 1 - nbeval), (-1, 0), 0.2, self.PDF_LIGHT_GRAY)
+                    )
+
+    def _list_evals(
+        self,
+        evals,
+        P,
+        rowstyle="",
+        pdf_style_bg=[],
+        hidden=False,
+        incomplete=False,
+        prefs={},
+    ):
+        if incomplete:  # style special pour evaluations incompletes:
+            rowstyle += " notes_bulletin_row_eval_incomplete"
+            pdf_row_markup = ['font color="red"']
+        else:
+            pdf_row_markup = []
+        # --- notes de chaque eval:
+        nbeval = 0
+        for e in evals:
+            if int(e["visibulletin"]) == 1 or self.version == "long":
+                if nbeval == 0:
+                    eval_style = " b_eval_first"
+                else:
+                    eval_style = ""
+                t = {
+                    "module": '<bullet indent="2mm">&bull;</bullet>&nbsp;' + e["name"],
+                    "coef": "<i>" + e["coef_txt"] + "</i>",
+                    "_hidden": hidden,
+                    "_module_target": e["target_html"],
+                    # '_module_help' : ,
+                    "_css_row_class": "notes_bulletin_row_eval" + eval_style + rowstyle,
+                    "_pdf_style": pdf_style_bg[:],
+                    "_pdf_row_markup": pdf_row_markup,
+                }
+                if e["note_txt"]:
+                    t["note"] = "<i>" + e["note_txt"] + "</i>"
+                else:
+                    t["_module_colspan"] = 2
+                if prefs["bul_show_minmax_eval"]:
+                    etat = sco_evaluations.do_evaluation_etat(
+                        self.context, e["evaluation_id"]
+                    )
+                    t["min"] = fmt_note(etat["mini"])
+                    t["max"] = fmt_note(etat["maxi"])
+                P.append(t)
+                nbeval += 1
+        return nbeval
+
+
+sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
diff --git a/sco_bulletins_ucac.py b/sco_bulletins_ucac.py
new file mode 100644
index 0000000000000000000000000000000000000000..39fe016c8add7903a08b9a497bd4570b0c3d7b5c
--- /dev/null
+++ b/sco_bulletins_ucac.py
@@ -0,0 +1,309 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""Generation bulletins de notes 
+
+Format table "UCAC"
+On redéfini la table centrale du bulletin de note et hérite de tout le reste du bulletin standard.
+
+E. Viennet, juillet 2011
+"""
+import traceback
+
+import sco_formsemestre
+from sco_pdf import *
+import sco_preferences
+
+from notes_log import log
+import sco_bulletins_generator
+import sco_bulletins_standard
+import gen_tables
+
+
+class BulletinGeneratorUCAC(sco_bulletins_standard.BulletinGeneratorStandard):
+    description = "style UCAC"  # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc
+    supported_formats = ["html", "pdf"]
+
+    PDF_LINEWIDTH = 1.25
+    PDF_TITLEBGCOLOR = Color(
+        170 / 255.0, 187 / 255.0, 204 / 255.0
+    )  # couleur fond lignes titres UE
+    # Inherited constants:
+    # PDF_LINECOLOR = Color(0.,0,0)
+    # PDF_MODSEPCOLOR = Color(170/255.,170/255.,170/255.) # lignes séparant les modules
+    # PDF_UE_CUR_BG = Color(210/255.,210/255.,210/255.) # fond UE courantes non prises en compte
+    # PDF_LIGHT_GRAY = Color(0.75,0.75,0.75)
+
+    def build_bulletin_table(self):  # overload standard method
+        """Génère la table centrale du bulletin de notes UCAC
+
+        La version n'est ici pas utilisée (on ne montre jamais les notes des évaluations).
+
+        Renvoie: colkeys, P, pdf_style, colWidths
+        - colkeys: nom des colonnes de la table (clés)
+        - table (liste de dicts de chaines de caracteres)
+        - style (commandes table Platypus)
+        - largeurs de colonnes pour PDF
+        """
+        I = self.infos
+        context = self.context
+        formsemestre_id = I["formsemestre_id"]
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        prefs = context.get_preferences(formsemestre_id)
+
+        P = []  # elems pour générer table avec gen_table (liste de dicts)
+
+        # Noms des colonnes à afficher:
+        colkeys = [
+            "code_ue",
+            "titre_ue",
+            "module",
+            "note",
+            "moyenne_ue",
+            "coef",
+            "total",
+        ]
+        if prefs["bul_show_abs_modules"]:
+            colkeys.append("abs")
+
+        # Largeur colonnes (pour PDF seulement):
+        colWidths = {
+            "code_ue": 20 * mm,
+            "titre_ue": 40 * mm,
+            "module": 42 * mm,
+            "note": 22 * mm,
+            "moyenne_ue": 22 * mm,
+            "coef": 22 * mm,
+            "total": 22 * mm,
+            "abs": 22 * mm,
+        }
+
+        # 1ère ligne titres
+        P.append(
+            {
+                "code_ue": "Code UE",
+                "titre_ue": "Unités d'enseignement",
+                "module": "Modules",
+                "note": "Notes/20",
+                "moyenne_ue": "Moyenne UE/20",
+                "coef": "Coef.",
+                "total": "Total",
+                "abs": "Abs (J. / N.J.)",
+                "_css_row_class": "bul_ucac_row_tit",
+                "_pdf_row_markup": ["b"],
+                "_pdf_style": [
+                    ("BACKGROUND", (0, 0), (-1, 0), Color(0.75, 0.75, 0.75))
+                ],
+            }
+        )
+
+        # Quantités spécifiques à l'UCAC, calculées ici au vol:
+        sum_coef_ues = 0.0  # somme des coefs des UE
+        sum_pt_sem = 0.0  # somme des "points validés" (coef x moyenne UE)
+
+        def list_ue(ue, ue_descr, nb_modules=0):
+            # ligne décrivant UE
+            moy_txt = ue["cur_moy_ue_txt"]
+            # log("ue['ue_status']['cur_moy_ue'] = %s" % ue['ue_status']['cur_moy_ue'] )
+            # log("ue['ue_status']['coef_ue'] = %s" % ue['ue_status']['coef_ue'] )
+            try:
+                total_pt_ue_v = (
+                    ue["ue_status"]["cur_moy_ue"] * ue["ue_status"]["coef_ue"]
+                )
+                total_pt_ue = fmt_note(total_pt_ue_v)
+                # log('total_pt_ue = %s' % total_pt_ue)
+            except:
+                # log("ue['ue_status']['cur_moy_ue'] = %s" % ue['ue_status']['cur_moy_ue'] )
+                # log("ue['ue_status']['coef_ue'] = %s" % ue['ue_status']['coef_ue'] )
+                total_pt_ue_v = 0
+                total_pt_ue = ""
+
+            t = {
+                "code_ue": ue["acronyme"],
+                "titre_ue": "%s %s" % (ue["titre"], ue_descr or ""),
+                "_titre_ue_colspan": 3,
+                "moyenne_ue": moy_txt,  # moyenne de l'UE
+                "coef": coef_ue,  # Attention: on affiche le coefficient de l'UE, et non le total des crédits
+                "total": total_pt_ue,  # points (et non crédits),
+                "_css_row_class": "bul_ucac_row_ue",
+                "_pdf_style": [
+                    ("BACKGROUND", (0, 0), (-1, 0), self.ue_color(ue_type=ue["type"]))
+                ],
+            }
+            if (
+                nb_modules > 0
+            ):  # cases inutilisées par les lignes modules: span vertical
+                t["_pdf_style"] += [
+                    ("SPAN", (0, 0), (0, nb_modules)),
+                    ("SPAN", (1, 0), (1, nb_modules)),
+                    ("SPAN", (4, 0), (-1, nb_modules)),
+                ]
+            P.append(t)
+            return total_pt_ue_v
+
+        def list_modules(ue_modules, ue_type=None, rowstyle="", hidden=False):
+            "Ajoute les lignes décrivant les modules d'une UE"
+            pdf_style = [("LINEABOVE", (0, 2), (-1, 3), 1, self.PDF_MODSEPCOLOR)]
+            if ue_type == "cur":  # UE courante non prise en compte (car capitalisee)
+                pdf_style.append(("BACKGROUND", (0, 0), (-1, 0), self.PDF_UE_CUR_BG))
+
+            for mod in ue_modules:
+                if mod["mod_moy_txt"] == "NI":
+                    continue  # saute les modules où on n'est pas inscrit
+                P.append(
+                    {
+                        "module": mod["name"],
+                        "note": mod["mod_moy_txt"],
+                        "abs": mod["mod_abs_txt"],
+                        "_pdf_style": pdf_style,
+                        "_css_row_class": "bul_ucac_row_mod%s" % rowstyle,
+                        "_hidden": hidden,
+                    }
+                )
+
+        for ue in I["ues"]:
+            # log('** ue %s' % ue['titre'])
+            ue_descr = ue["ue_descr_txt"]
+            coef_ue = ue["coef_ue_txt"]
+            ue_type = None
+            # --- UE capitalisée:
+            if ue["ue_status"]["is_capitalized"]:
+                if context.get_preference("bul_show_ue_cap_details", formsemestre_id):
+                    nb_modules = len(ue["modules_capitalized"])
+                    hidden = False
+                    cssstyle = ""
+                else:
+                    nb_modules = 0
+                    hidden = True
+                    cssstyle = "sco_hide"
+                pt = list_ue(ue, ue_descr, nb_modules=nb_modules)
+                sum_pt_sem += pt
+                coef_ue = ""
+                # Notes des modules de l'UE capitalisée antérieurement:
+                list_modules(
+                    ue["modules_capitalized"],
+                    hidden=hidden,
+                    rowstyle=" bul_ucac_row_cap %s" % cssstyle,
+                )
+
+                ue_descr = "(en cours, non prise en compte)"
+                ue_type = "cur"
+                rowstyle = " bul_ucac_row_ue_cur"
+
+            # --- UE ordinaire
+            pt = list_ue(ue, ue_descr)
+            if not ue["ue_status"]["is_capitalized"]:
+                sum_pt_sem += pt
+                sum_coef_ues += ue["ue_status"]["coef_ue"]
+
+            if len(ue["modules"]) > 1:  # liste les autres modules
+                list_modules(ue["modules"][1:], ue_type=ue_type)
+
+        # Ligne "Total"
+        P.append(
+            {
+                "code_ue": "Total",
+                "moyenne_ue": I["moy_gen"],
+                "coef": fmt_note(sum_coef_ues),
+                "total": fmt_note(sum_pt_sem),
+                "_code_ue_colspan": 4,
+                "_css_row_class": "bul_ucac_row_total",
+                "_pdf_row_markup": ["b"],
+                "_pdf_style": [("BACKGROUND", (0, 0), (-1, 0), self.PDF_TITLEBGCOLOR)],
+            }
+        )
+
+        # Ligne décision jury (toujours présente, ignore le paramètre)
+        P.append(
+            {
+                "code_ue": "Décision",
+                "_code_ue_colspan": 4,
+                "moyenne_ue": I.get("decision_jury", "") or "",
+                "_moyenne_ue_colspan": 3,
+                "_css_row_class": "bul_ucac_row_decision",
+                "_pdf_row_markup": ["b"],
+                "_pdf_style": [("BACKGROUND", (0, 0), (-1, 0), self.PDF_TITLEBGCOLOR)],
+            }
+        )
+
+        # Ligne "Mention" (figure toujours: le paramètre 'bul_show_mention' est ignoré)
+        P.append(
+            {
+                "code_ue": "Mention",
+                "_code_ue_colspan": 4,
+                "moyenne_ue": I["mention"] or "",
+                "_moyenne_ue_colspan": 3,
+                "_css_row_class": "bul_ucac_row_mention",
+                "_pdf_row_markup": ["b"],
+                "_pdf_style": [
+                    ("BACKGROUND", (0, 0), (-1, 0), self.PDF_TITLEBGCOLOR),
+                    ("SPAN", (0, 0), (3, 0)),
+                    ("SPAN", (4, 0), (-1, 0)),
+                ],
+            }
+        )
+
+        # Global pdf style comands:
+        pdf_style = [
+            ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
+            ("ALIGN", (0, 0), (-1, -1), "CENTER"),
+            (
+                "INNERGRID",
+                (0, 0),
+                (-1, -1),
+                self.PDF_LINEWIDTH,
+                self.PDF_LINECOLOR,
+            ),  # grille interne
+            (
+                "BOX",
+                (0, 0),
+                (-1, -1),
+                self.PDF_LINEWIDTH,
+                self.PDF_LINECOLOR,
+            ),  # bordure extérieure
+            (
+                "BACKGROUND",
+                (0, 0),
+                (-1, 0),
+                self.PDF_TITLEBGCOLOR,
+            ),  # couleur fond ligne titre
+        ]
+
+        #    if len(P[0]) > 5:
+        #    colWidths.append( 1.5*cm ) # absences/modules
+        # log('len(P) = %s' % len(P) )
+        # log( 'lens P=%s' % [ len(x) for x in P ] )
+        # log('P=\n%s' % pprint.pformat(P))
+        return colkeys, P, pdf_style, colWidths
+
+
+sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC)
+
+
+def bulletin_table_ucac(context, I, version=None):
+    """
+    """
diff --git a/sco_bulletins_xml.py b/sco_bulletins_xml.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e78eb9f03386a5a4cfe6a44bd52248b609fac9b
--- /dev/null
+++ b/sco_bulletins_xml.py
@@ -0,0 +1,399 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Génération du bulletin en format XML
+
+
+Note: la structure de ce XML est issue de (mauvais) choix historiques
+et ne peut pas être modifiée car d'autres logiciels l'utilisent (portail publication bulletins etudiants).
+
+Je recommande d'utiliser la version JSON.
+Malheureusement, le code de génération JSON et XML sont séparés, ce qui est absurde et complique la maintenance (si on ajoute des informations aux bulletins).
+
+Je propose de considérer le XMl comme "deprecated" et de ne plus le modifier, sauf nécessité.
+"""
+
+from notes_table import *
+import sco_formsemestre
+import sco_groups
+import sco_photos
+import ZAbsences
+import sco_bulletins
+
+# -------- Bulletin en XML
+# (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict()
+#   pour simplifier le code, mais attention a la maintenance !)
+#
+def make_xml_formsemestre_bulletinetud(
+    context,
+    formsemestre_id,
+    etudid,
+    doc=None,  # XML document
+    force_publishing=False,
+    xml_nodate=False,
+    REQUEST=None,
+    xml_with_decisions=False,  # inclue les decisions même si non publiées
+    version="long",
+):
+    "bulletin au format XML"
+    log("xml_bulletin( formsemestre_id=%s, etudid=%s )" % (formsemestre_id, etudid))
+    if REQUEST:
+        REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+    if not doc:
+        doc = jaxml.XML_document(encoding=SCO_ENCODING)
+
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    if sem["bul_hide_xml"] == "0" or force_publishing:
+        published = 1
+    else:
+        published = 0
+    if xml_nodate:
+        docdate = ""
+    else:
+        docdate = datetime.datetime.now().isoformat()
+
+    el = {
+        "etudid": etudid,
+        "formsemestre_id": formsemestre_id,
+        "date": docdate,
+        "publie": published,
+    }
+    if sem["etapes"]:
+        el["etape_apo"] = sem["etapes"][0] or ""
+        n = 2
+        for et in sem["etapes"][1:]:
+            el["etape_apo" + str(n)] = et or ""
+            n += 1
+
+    doc.bulletinetud(**el)
+
+    # Infos sur l'etudiant
+    etudinfo = context.getEtudInfo(etudid=etudid, filled=1)[0]
+    doc._push()
+    doc.etudiant(
+        etudid=etudid,
+        code_nip=etudinfo["code_nip"],
+        code_ine=etudinfo["code_ine"],
+        nom=quote_xml_attr(etudinfo["nom"]),
+        prenom=quote_xml_attr(etudinfo["prenom"]),
+        sexe=quote_xml_attr(etudinfo["sexe"]),
+        photo_url=quote_xml_attr(sco_photos.etud_photo_url(context, etudinfo)),
+        email=quote_xml_attr(etudinfo["email"]),
+        emailperso=quote_xml_attr(etudinfo["emailperso"]),
+    )
+    doc._pop()
+
+    # Disponible pour publication ?
+    if not published:
+        return doc  # stop !
+
+    # Groupes:
+    partitions = sco_groups.get_partitions_list(
+        context, formsemestre_id, with_default=False
+    )
+    partitions_etud_groups = {}  # { partition_id : { etudid : group } }
+    for partition in partitions:
+        pid = partition["partition_id"]
+        partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(
+            context, pid
+        )
+
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > toutes notes
+    ues = nt.get_ues()
+    modimpls = nt.get_modimpls()
+    nbetuds = len(nt.rangs)
+    mg = fmt_note(nt.get_etud_moy_gen(etudid))
+    if (
+        nt.get_moduleimpls_attente()
+        or context.get_preference("bul_show_rangs", formsemestre_id) == 0
+    ):
+        # n'affiche pas le rang sur le bulletin s'il y a des
+        # notes en attente dans ce semestre
+        rang = ""
+        rang_gr = {}
+        ninscrits_gr = {}
+    else:
+        rang = str(nt.get_etud_rang(etudid))
+        rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups(
+            context, etudid, formsemestre_id, partitions, partitions_etud_groups, nt
+        )
+
+    doc._push()
+    doc.note(
+        value=mg,
+        min=fmt_note(nt.moy_min),
+        max=fmt_note(nt.moy_max),
+        moy=fmt_note(nt.moy_moy),
+    )
+    doc._pop()
+    doc._push()
+    doc.rang(value=rang, ninscrits=nbetuds)
+    doc._pop()
+    if rang_gr:
+        for partition in partitions:
+            doc._push()
+            doc.rang_group(
+                group_type=partition["partition_name"],
+                group_name=gr_name[partition["partition_id"]],
+                value=rang_gr[partition["partition_id"]],
+                ninscrits=ninscrits_gr[partition["partition_id"]],
+            )
+            doc._pop()
+    doc._push()
+    doc.note_max(value=20)  # notes toujours sur 20
+    doc._pop()
+    doc._push()
+    doc.bonus_sport_culture(value=nt.bonus[etudid])
+    doc._pop()
+    # Liste les UE / modules /evals
+    for ue in ues:
+        ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
+        doc._push()
+        doc.ue(
+            id=ue["ue_id"],
+            numero=quote_xml_attr(ue["numero"]),
+            acronyme=quote_xml_attr(ue["acronyme"]),
+            titre=quote_xml_attr(ue["titre"]),
+            code_apogee=quote_xml_attr(ue["code_apogee"]),
+        )
+        doc._push()
+        if ue["type"] != UE_SPORT:
+            v = ue_status["cur_moy_ue"]
+        else:
+            v = nt.bonus[etudid]
+        doc.note(value=fmt_note(v), min=fmt_note(ue["min"]), max=fmt_note(ue["max"]))
+        doc._pop()
+        try:
+            ects_txt = str(int(ue["ects"]))
+        except:
+            ects_txt = ""
+        doc._push()
+        doc.ects(value=ects_txt)
+        doc._pop()
+        doc._push()
+        doc.rang(value=str(nt.ue_rangs[ue["ue_id"]][0][etudid]))
+        doc._pop()
+        doc._push()
+        doc.effectif(value=str(nt.ue_rangs[ue["ue_id"]][1]))
+        doc._pop()
+        # Liste les modules de l'UE
+        ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue["ue_id"]]
+        for modimpl in ue_modimpls:
+            mod_moy = fmt_note(nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid))
+            if mod_moy == "NI":  # ne mentionne pas les modules ou n'est pas inscrit
+                continue
+            mod = modimpl["module"]
+            doc._push()
+            # if mod['ects'] is None:
+            #    ects = ''
+            # else:
+            #    ects = str(mod['ects'])
+            doc.module(
+                id=modimpl["moduleimpl_id"],
+                code=mod["code"],
+                coefficient=mod["coefficient"],
+                numero=mod["numero"],
+                titre=quote_xml_attr(mod["titre"]),
+                abbrev=quote_xml_attr(mod["abbrev"]),
+                code_apogee=quote_xml_attr(mod["code_apogee"])
+                # ects=ects ects des modules maintenant inutilisés
+            )
+            doc._push()
+            modstat = nt.get_mod_stats(modimpl["moduleimpl_id"])
+            doc.note(
+                value=mod_moy,
+                min=fmt_note(modstat["min"]),
+                max=fmt_note(modstat["max"]),
+            )
+            doc._pop()
+            if context.get_preference("bul_show_mod_rangs", formsemestre_id):
+                doc._push()
+                doc.rang(value=nt.mod_rangs[modimpl["moduleimpl_id"]][0][etudid])
+                doc._pop()
+                doc._push()
+                doc.effectif(value=nt.mod_rangs[modimpl["moduleimpl_id"]][1])
+                doc._pop()
+            # --- notes de chaque eval:
+            evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"])
+            if version != "short":
+                for e in evals:
+                    if int(e["visibulletin"]) == 1 or version == "long":
+                        doc._push()
+                        doc.evaluation(
+                            jour=DateDMYtoISO(e["jour"], null_is_empty=True),
+                            heure_debut=TimetoISO8601(
+                                e["heure_debut"], null_is_empty=True
+                            ),
+                            heure_fin=TimetoISO8601(e["heure_fin"], null_is_empty=True),
+                            coefficient=e["coefficient"],
+                            evaluation_type=e["evaluation_type"],
+                            description=quote_xml_attr(e["description"]),
+                            note_max_origin=e[
+                                "note_max"
+                            ],  # notes envoyées sur 20, ceci juste pour garder trace
+                        )
+                        val = e["notes"].get(etudid, {"value": "NP"})[
+                            "value"
+                        ]  # NA si etud demissionnaire
+                        val = fmt_note(val, note_max=e["note_max"])
+                        doc.note(value=val)
+                        doc._pop()
+                # Evaluations incomplètes ou futures:
+                complete_eval_ids = Set([e["evaluation_id"] for e in evals])
+                if context.get_preference("bul_show_all_evals", formsemestre_id):
+                    all_evals = context.do_evaluation_list(
+                        args={"moduleimpl_id": modimpl["moduleimpl_id"]}
+                    )
+                    all_evals.reverse()  # plus ancienne d'abord
+                    for e in all_evals:
+                        if e["evaluation_id"] not in complete_eval_ids:
+                            doc._push()
+                            doc.evaluation(
+                                jour=DateDMYtoISO(e["jour"], null_is_empty=True),
+                                heure_debut=TimetoISO8601(
+                                    e["heure_debut"], null_is_empty=True
+                                ),
+                                heure_fin=TimetoISO8601(
+                                    e["heure_fin"], null_is_empty=True
+                                ),
+                                coefficient=e["coefficient"],
+                                description=quote_xml_attr(e["description"]),
+                                incomplete="1",
+                                note_max_origin=e[
+                                    "note_max"
+                                ],  # notes envoyées sur 20, ceci juste pour garder trace
+                            )
+                            doc._pop()
+            doc._pop()
+        doc._pop()
+        # UE capitalisee (listee seulement si meilleure que l'UE courante)
+        if ue_status["is_capitalized"]:
+            try:
+                ects_txt = str(int(ue_status["ue"].get("ects", "")))
+            except:
+                ects_txt = ""
+            doc._push()
+            doc.ue_capitalisee(
+                id=ue["ue_id"],
+                numero=quote_xml_attr(ue["numero"]),
+                acronyme=quote_xml_attr(ue["acronyme"]),
+                titre=quote_xml_attr(ue["titre"]),
+            )
+            doc._push()
+            doc.note(value=fmt_note(ue_status["moy"]))
+            doc._pop()
+            doc._push()
+            doc.ects(value=ects_txt)
+            doc._pop()
+            doc._push()
+            doc.coefficient_ue(value=fmt_note(ue_status["coef_ue"]))
+            doc._pop()
+            doc._push()
+            doc.date_capitalisation(value=DateDMYtoISO(ue_status["event_date"]))
+            doc._pop()
+            doc._pop()
+    # --- Absences
+    if context.get_preference("bul_show_abs", formsemestre_id):
+        debut_sem = DateDMYtoISO(sem["date_debut"])
+        fin_sem = DateDMYtoISO(sem["date_fin"])
+        AbsEtudSem = ZAbsences.getAbsSemEtud(context, sem, etudid)
+        nbabs = AbsEtudSem.CountAbs()
+        nbabsjust = AbsEtudSem.CountAbsJust()
+        doc._push()
+        doc.absences(nbabs=nbabs, nbabsjust=nbabsjust)
+        doc._pop()
+    # --- Decision Jury
+    if (
+        context.get_preference("bul_show_decision", formsemestre_id)
+        or xml_with_decisions
+    ):
+        infos, dpv = sco_bulletins.etud_descr_situation_semestre(
+            context,
+            etudid,
+            formsemestre_id,
+            format="xml",
+            show_uevalid=context.get_preference("bul_show_uevalid", formsemestre_id),
+        )
+        doc.situation(quote_xml_attr(infos["situation"]))
+        if dpv:
+            decision = dpv["decisions"][0]
+            etat = decision["etat"]
+            if decision["decision_sem"]:
+                code = decision["decision_sem"]["code"]
+            else:
+                code = ""
+            doc._push()
+
+            if (
+                decision["decision_sem"]
+                and "compense_formsemestre_id" in decision["decision_sem"]
+            ):
+                doc.decision(
+                    code=code,
+                    etat=etat,
+                    compense_formsemestre_id=decision["decision_sem"][
+                        "compense_formsemestre_id"
+                    ],
+                )
+            else:
+                doc.decision(code=code, etat=etat)
+
+            doc._pop()
+
+            if decision[
+                "decisions_ue"
+            ]:  # and context.get_preference('bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
+                for ue_id in decision["decisions_ue"].keys():
+                    ue = context.do_ue_list({"ue_id": ue_id})[0]
+                    doc._push()
+                    doc.decision_ue(
+                        ue_id=ue["ue_id"],
+                        numero=quote_xml_attr(ue["numero"]),
+                        acronyme=quote_xml_attr(ue["acronyme"]),
+                        titre=quote_xml_attr(ue["titre"]),
+                        code=decision["decisions_ue"][ue_id]["code"],
+                    )
+                    doc._pop()
+
+            for aut in decision["autorisations"]:
+                doc._push()
+                doc.autorisation_inscription(semestre_id=aut["semestre_id"])
+                doc._pop()
+        else:
+            doc._push()
+            doc.decision(code="", etat="DEM")
+            doc._pop()
+    # --- Appreciations
+    cnx = context.GetDBConnexion()
+    apprecs = scolars.appreciations_list(
+        cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
+    )
+    for app in apprecs:
+        doc.appreciation(quote_xml_attr(app["comment"]), date=DateDMYtoISO(app["date"]))
+    return doc
diff --git a/sco_cache.py b/sco_cache.py
new file mode 100644
index 0000000000000000000000000000000000000000..d2a603eec1670546d4a123a9bbdd98fb660c1f2f
--- /dev/null
+++ b/sco_cache.py
@@ -0,0 +1,81 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Cache simple par etudiant
+
+"""
+
+from notes_log import log
+import thread, time
+
+# Cache data
+class simpleCache:
+    def __init__(self):
+        self.inval_cache()  # >
+
+    def inval_cache(self, key=None):  # >
+        if key:
+            if key in self.cache:
+                del self.cache[key]
+        else:
+            # clear all entries
+            self.cache = {}  # key : data
+
+    def set(self, key, data):
+        self.cache[key] = data
+
+    def get(self, key):
+        """returns None if not in cache"""
+        return self.cache.get(key, None)
+
+
+class expiringCache(simpleCache):
+    """A simple cache wich cache data for a most "duration" seconds.
+
+    This is used for users (which may be updated from external 
+    information systems)
+    """
+
+    def __init__(self, max_validity=60):
+        simpleCache.__init__(self)
+        self.max_validity = max_validity
+
+    def set(self, key, data):
+        simpleCache.set(self, key, (data, time.time()))
+
+    def get(self, key):
+        info = simpleCache.get(self, key)
+        if info:
+            data, t = info
+            if time.time() - t < self.max_validity:
+                return data
+            else:
+                # expired
+                self.inval_cache(key)  # >
+                return None
+        else:
+            return None  # not in cache
diff --git a/sco_codes_parcours.py b/sco_codes_parcours.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d7f3d329a8a018ae51eadfda4645cb798fafc09
--- /dev/null
+++ b/sco_codes_parcours.py
@@ -0,0 +1,673 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@univ-paris13.fr
+#
+##############################################################################
+
+"""Semestres: Codes gestion parcours (constantes)
+"""
+from types import ListType, TupleType, FloatType
+import collections
+
+NOTES_TOLERANCE = 0.00499999999999  # si note >= (BARRE-TOLERANCE), considere ok
+# (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999)
+
+# Barre sur moyenne générale utilisée pour compensations semestres:
+NOTES_BARRE_GEN_COMPENSATION = 10.0 - NOTES_TOLERANCE
+
+# ----------------------------------------------------------------
+#  Types d'UE:
+UE_STANDARD = 0  # UE "fondamentale"
+UE_SPORT = 1  # bonus "sport"
+UE_STAGE_LP = 2  # ue "projet tuteuré et stage" dans les Lic. Pro.
+UE_STAGE_10 = 3  # ue "stage" avec moyenne requise > 10
+UE_ELECTIVE = 4  # UE "élective" dans certains parcours (UCAC?, ISCID)
+UE_PROFESSIONNELLE = 5  # UE "professionnelle" (ISCID, ...)
+
+
+def UE_is_fondamentale(ue_type):
+    return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE)
+
+
+def UE_is_professionnelle(ue_type):
+    return (
+        ue_type == UE_PROFESSIONNELLE
+    )  # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro
+
+
+UE_TYPE_NAME = {
+    UE_STANDARD: "Standard",
+    UE_SPORT: "Sport/Culture (points bonus)",
+    UE_STAGE_LP: "Projet tuteuré et stage (Lic. Pro.)",
+    UE_STAGE_10: "Stage (moyenne min. 10/20)",
+    UE_ELECTIVE: "Elective (ISCID)",
+    UE_PROFESSIONNELLE: "Professionnelle (ISCID)"
+    #                 UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)',
+    #                 UE_OPTIONNELLE : '"Optionnelle" (UCAC)'
+}
+
+# Couleurs RGB (dans [0.,1.]) des UE pour les bulletins:
+UE_DEFAULT_COLOR = (150 / 255.0, 200 / 255.0, 180 / 255.0)
+UE_COLORS = {
+    UE_STANDARD: UE_DEFAULT_COLOR,
+    UE_SPORT: (0.40, 0.90, 0.50),
+    UE_STAGE_LP: (0.80, 0.90, 0.90),
+}
+UE_SEM_DEFAULT = 1000000  # indice semestre des UE sans modules
+
+# ------------------------------------------------------------------
+# Codes proposés par ADIUT / Apogee
+ADM = "ADM"  # moyenne gen., barres UE, assiduité: sem. validé
+ADC = "ADC"  # admis par compensation (eg moy(S1, S2) > 10)
+ADJ = "ADJ"  # admis par le jury
+ATT = "ATT"  #
+ATJ = "ATJ"  # pb assiduité: décision repoussée au semestre suivant
+ATB = "ATB"
+AJ = "AJ"
+CMP = "CMP"  # utile pour UE seulement (indique UE acquise car semestre acquis)
+NAR = "NAR"
+RAT = "RAT"  # en attente rattrapage, sera ATT dans Apogée
+DEF = "DEF"  # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission)
+
+# codes actions
+REDOANNEE = "REDOANNEE"  # redouble annee (va en Sn-1)
+REDOSEM = "REDOSEM"  # redouble semestre (va en Sn)
+RA_OR_NEXT = "RA_OR_NEXT"  # redouble annee ou passe en Sn+1
+RA_OR_RS = "RA_OR_RS"  # redouble annee ou semestre
+RS_OR_NEXT = "RS_OR_NEXT"  # redouble semestre ou passe en Sn+1
+NEXT_OR_NEXT2 = "NEXT_OR_NEXT2"  # passe en suivant (Sn+1) ou sur-suivant (Sn+2)
+NEXT = "NEXT"
+NEXT2 = "NEXT2"  # passe au sur-suivant (Sn+2)
+REO = "REO"
+BUG = "BUG"
+
+ALL = "ALL"
+
+CODES_EXPL = {
+    ADM: "Validé",
+    ADC: "Validé par compensation",
+    ADJ: "Validé par le Jury",
+    ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)",
+    ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
+    ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
+    AJ: "Ajourné",
+    NAR: "Echec, non autorisé à redoubler",
+    RAT: "En attente d'un rattrapage",
+    DEF: "Défaillant",
+}
+# Nota: ces explications sont personnalisables via le fichier de config scodoc_config.py
+#  variable: CONFIG.CODES_EXP
+
+CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True}  # semestre validé
+CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True}  # semestre en attente
+
+CODES_SEM_REO = {NAR: 1}  # reorientation
+
+CODES_UE_VALIDES = {ADM: True, CMP: True}  # UE validée
+
+
+def code_semestre_validant(code):
+    "Vrai si ce CODE entraine la validation du semestre"
+    return CODES_SEM_VALIDES.get(code, False)
+
+
+def code_semestre_attente(code):
+    "Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)"
+    return CODES_SEM_ATTENTES.get(code, False)
+
+
+def code_ue_validant(code):
+    "Vrai si ce code entraine la validation de l'UE"
+    return CODES_UE_VALIDES.get(code, False)
+
+
+DEVENIR_EXPL = {
+    NEXT: "Passage au semestre suivant",
+    REDOANNEE: "Redoublement année",
+    REDOSEM: "Redoublement semestre",
+    RA_OR_NEXT: "Passage, ou redoublement année",
+    RA_OR_RS: "Redoublement année, ou redoublement semestre",  # slt si sems decales
+    RS_OR_NEXT: "Passage, ou redoublement semestre",
+    NEXT_OR_NEXT2: "Passage en semestre suivant ou à celui d'après",
+    NEXT2: "Passage au sur-suivant",
+    REO: "Réorientation",
+}
+
+# Devenirs autorises dans les cursus sans semestres décalés:
+DEVENIRS_STD = {NEXT: 1, REDOANNEE: 1, RA_OR_NEXT: 1, REO: 1}
+
+# Devenirs autorises dans les cursus en un seul semestre, semestre_id==-1 (licences ?)
+DEVENIRS_MONO = {REDOANNEE: 1, REO: 1}
+
+# Devenirs supplementaires (en mode manuel) pour les cursus avec semestres decales
+DEVENIRS_DEC = {REDOSEM: 1, RS_OR_NEXT: 1}
+
+# Devenirs en n+2 (sautant un semestre)  (si semestres décalés et s'il ne manque qu'un semestre avant le n+2)
+DEVENIRS_NEXT2 = {NEXT_OR_NEXT2: 1, NEXT2: 1}
+
+NO_SEMESTRE_ID = -1  # code semestre si pas de semestres
+
+# Regles gestion parcours
+class DUTRule:
+    def __init__(self, rule_id, premise, conclusion):
+        self.rule_id = rule_id
+        self.premise = premise
+        self.conclusion = conclusion
+        # self.code, self.codes_ue, self.devenir, self.action, self.explication = conclusion
+
+    def match(self, state):
+        "True if state match rule premise"
+        assert len(state) == len(self.premise)
+        for i in range(len(state)):
+            prem = self.premise[i]
+            if type(prem) == ListType or type(prem) == TupleType:
+                if not state[i] in prem:
+                    return False
+            else:
+                if prem != ALL and prem != state[i]:
+                    return False
+        return True
+
+
+# Types de parcours
+DEFAULT_TYPE_PARCOURS = 100  # pour le menu de creation nouvelle formation
+
+
+class TypeParcours:
+    TYPE_PARCOURS = None  # id, utilisé par notes_formation.type_parcours
+    NAME = None  # required
+    NB_SEM = 1  # Nombre de semestres
+    COMPENSATION_UE = True  # inutilisé
+    BARRE_MOY = 10.0
+    BARRE_UE_DEFAULT = 8.0
+    BARRE_UE = {}
+    NOTES_BARRE_VALID_UE_TH = 10.0  # seuil pour valider UE
+    NOTES_BARRE_VALID_UE = NOTES_BARRE_VALID_UE_TH - NOTES_TOLERANCE  # barre sur UE
+    ALLOW_SEM_SKIP = False  # Passage: autorise-t-on les sauts de semestres ?
+    SESSION_NAME = "semestre"
+    SESSION_NAME_A = "du "
+    SESSION_ABBRV = "S"  # S1, S2, ...
+    UNUSED_CODES = set()  # Ensemble des codes jury non autorisés dans ce parcours
+    UE_IS_MODULE = False  # 1 seul module par UE (si plusieurs modules, etudiants censéments inscrits à un seul d'entre eux)
+    ECTS_ONLY = False  # Parcours avec progression basée uniquement sur les ECTS
+    ALLOWED_UE_TYPES = UE_TYPE_NAME.keys()  # par defaut, autorise tous les types d'UE
+
+    def check(self, formation=None):
+        return True, ""  # status, diagnostic_message
+
+    def get_barre_ue(self, ue_type, tolerance=True):
+        """Barre pour cette UE (la valeur peut dépendre du type d'UE).
+        Si tolerance, diminue de epsilon pour éviter les effets d'arrondis.
+        """
+        if tolerance:
+            t = NOTES_TOLERANCE
+        else:
+            t = 0.0
+        return self.BARRE_UE.get(ue_type, self.BARRE_UE_DEFAULT) - t
+
+    def ues_sous_barre(self, ues_status):
+        """Filtre les ues: liste celles ayant une moyenne sous la barre
+
+        ues_status est une liste de dict ayant des entrées 'moy' et 'coef_ue'
+        """
+        return [
+            ue_status
+            for ue_status in ues_status
+            if ue_status["coef_ue"] > 0
+            and type(ue_status["moy"]) == FloatType
+            and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
+        ]
+
+    def check_barre_ues(self, ues_status):
+        """True si la ou les conditions sur les UE sont valides
+        Par defaut, vrai si les moyennes d'UE sont au dessus de la barre.
+        Le cas des LP2014 est plus compliqué.
+        """
+        n = len(self.ues_sous_barre(ues_status))
+        if n == 0:
+            return True, "les UEs sont au dessus des barres"
+        else:
+            return False, """<b>%d UE sous la barre</b>""" % n
+
+
+TYPES_PARCOURS = (
+    collections.OrderedDict()
+)  # liste des parcours définis (instances de sous-classes de TypeParcours)
+
+
+def register_parcours(Parcours):
+    TYPES_PARCOURS[Parcours.TYPE_PARCOURS] = Parcours
+
+
+class ParcoursDUT(TypeParcours):
+    """DUT selon l'arrêté d'août 2005"""
+
+    TYPE_PARCOURS = 100
+    NAME = "DUT"
+    NB_SEM = 4
+    COMPENSATION_UE = True
+    ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
+
+
+register_parcours(ParcoursDUT())
+
+
+class ParcoursDUT4(ParcoursDUT):
+    """DUT (en 4 semestres sans compensations)"""
+
+    TYPE_PARCOURS = 110
+    NAME = "DUT4"
+    COMPENSATION_UE = False
+
+
+register_parcours(ParcoursDUT4())
+
+
+class ParcoursDUTMono(TypeParcours):
+    """DUT en un an (FC, Années spéciales)"""
+
+    TYPE_PARCOURS = 120
+    NAME = "DUT"
+    NB_SEM = 1
+    COMPENSATION_UE = False
+    UNUSED_CODES = set((ADC, ATT, ATB))
+
+
+register_parcours(ParcoursDUTMono())
+
+
+class ParcoursDUT2(ParcoursDUT):
+    """DUT en deux semestres (par ex.: années spéciales semestrialisées)"""
+
+    TYPE_PARCOURS = 130
+    NAME = "DUT2"
+    NB_SEM = 2
+
+
+register_parcours(ParcoursDUT2())
+
+
+class ParcoursLP(TypeParcours):
+    """Licence Pro (en un "semestre")
+    (pour anciennes LP. Après 2014, préférer ParcoursLP2014)
+    """
+
+    TYPE_PARCOURS = 200
+    NAME = "LP"
+    NB_SEM = 1
+    COMPENSATION_UE = False
+    ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_LP]
+    BARRE_UE_DEFAULT = 0.0  # pas de barre sur les UE "normales"
+    BARRE_UE = {UE_STAGE_LP: 10.0}
+    # pas de codes ATT en LP
+    UNUSED_CODES = set((ADC, ATT, ATB))
+
+
+register_parcours(ParcoursLP())
+
+
+class ParcoursLP2sem(ParcoursLP):
+    """Licence Pro (en deux "semestres")"""
+
+    TYPE_PARCOURS = 210
+    NAME = "LP2sem"
+    NB_SEM = 2
+    COMPENSATION_UE = True
+    UNUSED_CODES = set((ADC,))  # autorise les codes ATT et ATB, mais pas ADC.
+
+
+register_parcours(ParcoursLP2sem())
+
+
+class ParcoursLP2semEvry(ParcoursLP):
+    """Licence Pro (en deux "semestres", U. Evry)"""
+
+    TYPE_PARCOURS = 220
+    NAME = "LP2semEvry"
+    NB_SEM = 2
+    COMPENSATION_UE = True
+
+
+register_parcours(ParcoursLP2semEvry())
+
+
+class ParcoursLP2014(TypeParcours):
+    """Licence Pro (en un "semestre"), selon arrêté du 22/01/2014"""
+
+    # Note: texte de référence
+    # https://www.legifrance.gouv.fr/affichTexte.do?cidTexte=JORFTEXT000000397481
+
+    # Article 7: Le stage et le projet tutoré constituent chacun une unité d'enseignement.
+    # Article 10:
+    # La licence professionnelle est décernée aux étudiants qui ont obtenu à la fois une moyenne
+    # générale égale ou supérieure à 10 sur 20 à l'ensemble des unités d'enseignement, y compris le
+    # projet tutoré et le stage, et une moyenne égale ou supérieure à 10 sur 20 à l'ensemble constitué
+    # du projet tutoré et du stage.
+
+    # Actuellement, les points suivants de l'article 7 ("Les unités d'enseignement sont affectées par
+    # l'établissement d'un coefficient qui peut varier dans un rapport de 1 à 3. ", etc ne sont _pas_
+    # vérifiés par ScoDoc)
+
+    TYPE_PARCOURS = 230
+    NAME = "LP2014"
+    NB_SEM = 1
+    ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_LP]
+    BARRE_UE_DEFAULT = 0.0  # pas de barre sur les UE "normales"
+    # pas de codes ATT en LP
+    UNUSED_CODES = set((ADC, ATT, ATB))
+    # Specifique aux LP
+    BARRE_MOY_UE_STAGE_PROJET = 10.0
+
+    def check_barre_ues(self, ues_status):
+        """True si la ou les conditions sur les UE sont valides
+        Article 10: "une moyenne égale ou supérieure à 10 sur 20 à l'ensemble constitué 
+                     du projet tutoré et du stage."
+        """
+        # Les UE de type "projet ou stage" ayant des notes
+        mc_stages_proj = [
+            (ue_status["moy"], ue_status["coef_ue"])
+            for ue_status in ues_status
+            if ue_status["ue"]["type"] == UE_STAGE_LP
+            and type(ue_status["moy"]) == FloatType
+        ]
+        # Moyenne des moyennes:
+        sum_coef = sum(x[1] for x in mc_stages_proj)
+        if sum_coef > 0.0:
+            moy = sum([x[0] * x[1] for x in mc_stages_proj]) / sum_coef
+            ok = moy > (self.BARRE_MOY_UE_STAGE_PROJET - NOTES_TOLERANCE)
+            if ok:
+                return True, "moyenne des UE de stages et projets au dessus de 10"
+            else:
+                return (
+                    False,
+                    "<b>moyenne des UE de stages et projets inférieure à 10</b>",
+                )
+        else:
+            return True, ""  # pas de coef, condition ok
+
+
+register_parcours(ParcoursLP2014())
+
+
+class ParcoursLP2sem(ParcoursLP):
+    """Licence Pro (en deux "semestres", selon arrêté du 22/01/2014)"""
+
+    TYPE_PARCOURS = 240
+    NAME = "LP2014_2sem"
+    NB_SEM = 2
+
+
+register_parcours(ParcoursLP2sem())
+
+
+# Masters: M2 en deux semestres
+class ParcoursM2(TypeParcours):
+    """Master 2 (en deux "semestres")"""
+
+    TYPE_PARCOURS = 250
+    NAME = "M2sem"
+    NB_SEM = 2
+    COMPENSATION_UE = True
+    UNUSED_CODES = set((ATT, ATB))
+
+
+register_parcours(ParcoursM2())
+
+
+class ParcoursM2noncomp(ParcoursM2):
+    """Master 2 (en deux "semestres") sans compensation"""
+
+    TYPE_PARCOURS = 251
+    NAME = "M2noncomp"
+    COMPENSATION_UE = False
+    UNUSED_CODES = set((ADC, ATT, ATB))
+
+
+register_parcours(ParcoursM2noncomp())
+
+
+class ParcoursMono(TypeParcours):
+    """Formation générique en une session"""
+
+    TYPE_PARCOURS = 300
+    NAME = "Mono"
+    NB_SEM = 1
+    COMPENSATION_UE = False
+    UNUSED_CODES = set((ADC, ATT, ATB))
+
+
+register_parcours(ParcoursMono())
+
+
+class ParcoursLegacy(TypeParcours):
+    """DUT (ancien ScoDoc, ne plus utiliser)"""
+
+    TYPE_PARCOURS = 0
+    NAME = "DUT"
+    NB_SEM = 4
+    COMPENSATION_UE = None  # backward compat: defini dans formsemestre
+    ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
+
+
+register_parcours(ParcoursLegacy())
+
+
+class ParcoursISCID(TypeParcours):
+    """Superclasse pour les parcours de l'ISCID"""
+
+    # SESSION_NAME = "année"
+    # SESSION_NAME_A = "de l'"
+    # SESSION_ABBRV = 'A' # A1, A2, ...
+    COMPENSATION_UE = False
+    UNUSED_CODES = set((ADC, ATT, ATB, ATJ))
+    UE_IS_MODULE = True  # pas de matieres et modules
+    ECTS_ONLY = True  # jury basés sur les ECTS (pas moyenne generales, pas de barres, pas de compensations)
+    ALLOWED_UE_TYPES = [UE_STANDARD, UE_ELECTIVE, UE_PROFESSIONNELLE]
+    NOTES_BARRE_VALID_MODULE_TH = 10.0
+    NOTES_BARRE_VALID_MODULE = (
+        NOTES_BARRE_VALID_MODULE_TH - NOTES_TOLERANCE
+    )  # barre sur module
+    ECTS_BARRE_VALID_YEAR = 60
+    ECTS_FONDAMENTAUX_PER_YEAR = 42  # mini pour valider l'annee
+    ECTS_PROF_DIPL = 0  # crédits professionnels requis pour obtenir le diplôme
+
+
+class ParcoursBachelorISCID6(ParcoursISCID):
+    """ISCID: Bachelor en 3 ans (6 sem.)"""
+
+    NAME = "ParcoursBachelorISCID6"
+    TYPE_PARCOURS = 1001
+    NAME = ""
+    NB_SEM = 6
+    ECTS_PROF_DIPL = 8  # crédits professionnels requis pour obtenir le diplôme
+
+
+register_parcours(ParcoursBachelorISCID6())
+
+
+class ParcoursMasterISCID4(ParcoursISCID):
+    "ISCID: Master en 2 ans (4 sem.)"
+    TYPE_PARCOURS = 1002
+    NAME = "ParcoursMasterISCID4"
+    NB_SEM = 4
+    ECTS_PROF_DIPL = 15  # crédits professionnels requis pour obtenir le diplôme
+
+
+register_parcours(ParcoursMasterISCID4())
+
+
+class ParcoursUCAC(TypeParcours):
+    """Règles de validation UCAC"""
+
+    SESSION_NAME = "année"
+    SESSION_NAME_A = "de l'"
+    COMPENSATION_UE = False
+    BARRE_MOY = 12.0
+    NOTES_BARRE_VALID_UE_TH = 12.0  # seuil pour valider UE
+    NOTES_BARRE_VALID_UE = NOTES_BARRE_VALID_UE_TH - NOTES_TOLERANCE  # barre sur UE
+    BARRE_UE_DEFAULT = (
+        NOTES_BARRE_VALID_UE_TH  # il faut valider tt les UE pour valider l'année
+    )
+
+
+class ParcoursLicenceUCAC3(ParcoursUCAC):
+    """UCAC: Licence en 3 sessions d'un an"""
+
+    TYPE_PARCOURS = 501
+    NAME = "Licence UCAC en 3 sessions d'un an"
+    NB_SEM = 3
+
+
+register_parcours(ParcoursLicenceUCAC3())
+
+
+class ParcoursMasterUCAC2(ParcoursUCAC):
+    """UCAC: Master en 2 sessions d'un an"""
+
+    TYPE_PARCOURS = 502
+    NAME = "Master UCAC en 2 sessions d'un an"
+    NB_SEM = 2
+
+
+register_parcours(ParcoursMasterUCAC2())
+
+
+class ParcoursMonoUCAC(ParcoursUCAC):
+    """UCAC: Formation en 1 session de durée variable"""
+
+    TYPE_PARCOURS = 503
+    NAME = "Formation UCAC en 1 session de durée variable"
+    NB_SEM = 1
+    UNUSED_CODES = set((ADC, ATT, ATB))
+
+
+register_parcours(ParcoursMonoUCAC())
+
+
+class Parcours6Sem(TypeParcours):
+    """Parcours générique en 6 semestres"""
+
+    TYPE_PARCOURS = 600
+    NAME = "Formation en 6 semestres"
+    NB_SEM = 6
+    COMPENSATION_UE = True
+
+
+register_parcours(Parcours6Sem())
+
+# # En cours d'implémentation:
+# class ParcoursLicenceLMD(TypeParcours):
+#     """Licence standard en 6 semestres dans le LMD"""
+#     TYPE_PARCOURS = 401
+#     NAME = "Licence LMD"
+#     NB_SEM = 6
+#     COMPENSATION_UE = True
+
+# register_parcours(ParcoursLicenceLMD())
+
+
+class ParcoursMasterLMD(TypeParcours):
+    """Master générique en 4 semestres dans le LMD"""
+
+    TYPE_PARCOURS = 402
+    NAME = "Master LMD"
+    NB_SEM = 4
+    COMPENSATION_UE = True  # variabale inutilisée
+    UNUSED_CODES = set((ADC, ATT, ATB))
+
+
+register_parcours(ParcoursMasterLMD())
+
+
+class ParcoursMasterIG(ParcoursMasterLMD):
+    """Master de l'Institut Galilée (U. Paris 13) en 4 semestres (LMD)"""
+
+    TYPE_PARCOURS = 403
+    NAME = "Master IG P13"
+    BARRE_MOY = 10.0
+    NOTES_BARRE_VALID_UE_TH = 10.0  # seuil pour valider UE
+    NOTES_BARRE_VALID_UE = NOTES_BARRE_VALID_UE_TH - NOTES_TOLERANCE  # barre sur UE
+    BARRE_UE_DEFAULT = 7.0  # Les UE normales avec moins de 7/20 sont éliminatoires
+    # Specifique à l'UE de stage
+    BARRE_MOY_UE_STAGE = 10.0
+    ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_10]
+
+    def check_barre_ues(self, ues_status):  # inspire de la fonction de ParcoursLP2014
+        """True si la ou les conditions sur les UE sont valides
+        moyenne d'UE > 7, ou > 10 si UE de stage
+        """
+        # Il y a-t-il une UE standard sous la barre ?
+        ue_sb = [
+            ue_status
+            for ue_status in ues_status
+            if ue_status["ue"]["type"] == UE_STANDARD
+            and ue_status["coef_ue"] > 0
+            and type(ue_status["moy"]) == FloatType
+            and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
+        ]
+        if len(ue_sb):
+            return (
+                False,
+                "<b>%d UE sous la barre (%s/20)</b>"
+                % (len(ue_sb), self.BARRE_UE_DEFAULT),
+            )
+        # Les UE de type "stage" ayant des notes
+        mc_stages = [
+            (ue_status["moy"], ue_status["coef_ue"])
+            for ue_status in ues_status
+            if ue_status["ue"]["type"] == UE_STAGE_10
+            and type(ue_status["moy"]) == FloatType
+        ]
+        # Moyenne des moyennes:
+        sum_coef = sum(x[1] for x in mc_stages)
+        if sum_coef > 0.0:
+            moy = sum([x[0] * x[1] for x in mc_stages]) / sum_coef
+            ok = moy > (self.BARRE_MOY_UE_STAGE - NOTES_TOLERANCE)
+            if ok:
+                return True, "moyenne des UE de stages au dessus de 10"
+            else:
+                return False, "<b>moyenne des UE de stages inférieure à 10</b>"
+        else:
+            return True, ""  # pas de coef, condition ok
+
+
+register_parcours(ParcoursMasterIG())
+
+
+# Ajouter ici vos parcours, le TYPE_PARCOURS devant être unique au monde
+# (avisez sur la liste de diffusion)
+
+
+# ...
+
+
+# -------------------------
+_tp = TYPES_PARCOURS.items()
+_tp.sort(key=lambda x: x[1].__doc__)  # sort by intitulé
+FORMATION_PARCOURS_DESCRS = [p[1].__doc__ for p in _tp]  # intitulés (eg pour menu)
+FORMATION_PARCOURS_TYPES = [p[0] for p in _tp]  # codes numeriques (TYPE_PARCOURS)
+
+
+def get_parcours_from_code(code_parcours):
+    return TYPES_PARCOURS[code_parcours]
diff --git a/sco_compute_moy.py b/sco_compute_moy.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa7d05a3fbb12d360d61d61a6ba4910153f4e8ed
--- /dev/null
+++ b/sco_compute_moy.py
@@ -0,0 +1,385 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Calcul des moyennes de module
+"""
+
+from sets import Set
+import traceback
+
+from sco_utils import *
+from notesdb import *
+from notes_log import log, sendAlarm
+import sco_formsemestre
+import sco_groups
+import sco_evaluations
+from sco_formulas import *
+import ZAbsences
+
+
+def moduleimpl_has_expression(context, mod):
+    "True if we should use a user-defined expression"
+    expr = mod["computation_expr"]
+    if not expr:
+        return False
+    expr = expr.strip()
+    if not expr or expr[0] == "#":
+        return False
+    return True
+
+
+def formsemestre_expressions_use_abscounts(context, formsemestre_id):
+    """True si les notes de ce semestre dépendent des compteurs d'absences.
+    Cela n'est normalement pas le cas, sauf si des formules utilisateur utilisent ces compteurs.
+    """
+    # check presence of 'nbabs' in expressions
+    ab = "nb_abs"  # chaine recherchée
+    cnx = context.GetDBConnexion()
+    # 1- moyennes d'UE:
+    elist = formsemestre_ue_computation_expr_list(
+        cnx, {"formsemestre_id": formsemestre_id}
+    )
+    for e in elist:
+        expr = e["computation_expr"].strip()
+        if expr and expr[0] != "#" and ab in expr:
+            return True
+    # 2- moyennes de modules
+    for mod in context.Notes.do_moduleimpl_list(formsemestre_id=formsemestre_id):
+        if moduleimpl_has_expression(context, mod) and ab in mod["computation_expr"]:
+            return True
+    return False
+
+
+_formsemestre_ue_computation_exprEditor = EditableTable(
+    "notes_formsemestre_ue_computation_expr",
+    "notes_formsemestre_ue_computation_expr_id",
+    (
+        "notes_formsemestre_ue_computation_expr_id",
+        "formsemestre_id",
+        "ue_id",
+        "computation_expr",
+    ),
+    html_quote=False,  # does nt automatically quote
+)
+formsemestre_ue_computation_expr_create = _formsemestre_ue_computation_exprEditor.create
+formsemestre_ue_computation_expr_delete = _formsemestre_ue_computation_exprEditor.delete
+formsemestre_ue_computation_expr_list = _formsemestre_ue_computation_exprEditor.list
+formsemestre_ue_computation_expr_edit = _formsemestre_ue_computation_exprEditor.edit
+
+
+def get_ue_expression(formsemestre_id, ue_id, cnx, html_quote=False):
+    """Returns UE expression (formula), or None if no expression has been defined
+    """
+    el = formsemestre_ue_computation_expr_list(
+        cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
+    )
+    if not el:
+        return None
+    else:
+        expr = el[0]["computation_expr"].strip()
+        if expr and expr[0] != "#":
+            if html_quote:
+                expr = quote_html(expr)
+            return expr
+        else:
+            return None
+
+
+def compute_user_formula(
+    context,
+    sem,
+    etudid,
+    moy,
+    moy_valid,
+    notes,
+    coefs,
+    coefs_mask,
+    formula,
+    diag_info={},  # infos supplementaires a placer ds messages d'erreur
+    use_abs=True,
+):
+    """Calcul moyenne a partir des notes et coefs, en utilisant la formule utilisateur (une chaine).
+    Retourne moy, et en cas d'erreur met à jour diag_info (msg)
+    """
+    if use_abs:
+        AbsSemEtud = ZAbsences.getAbsSemEtud(context, sem, etudid)
+        nbabs = AbsSemEtud.CountAbs()
+        nbabs_just = AbsSemEtud.CountAbsJust()
+    else:
+        nbabs, nbabs_just = 0, 0
+    try:
+        moy_val = float(moy)
+    except:
+        moy_val = 0.0  # 0. when no valid value
+    variables = {
+        "cmask": coefs_mask,  # NoteVector(v=coefs_mask),
+        "notes": notes,  # NoteVector(v=notes),
+        "coefs": coefs,  # NoteVector(v=coefs),
+        "moy": moy,
+        "moy_valid": moy_valid,  # deprecated, use moy_is_valid
+        "moy_is_valid": moy_valid,  # True si moyenne numerique
+        "moy_val": moy_val,
+        "nb_abs": float(nbabs),
+        "nb_abs_just": float(nbabs_just),
+        "nb_abs_nojust": float(nbabs - nbabs_just),
+    }
+    try:
+        formula = formula.replace("\n", "").replace("\r", "")
+        # log('expression : %s\nvariables=%s\n' % (formula, variables)) # XXX debug
+        user_moy = eval_user_expression(context, formula, variables)
+        # log('user_moy=%s' % user_moy)
+        if user_moy != "NA0" and user_moy != "NA":
+            user_moy = float(user_moy)
+            if (user_moy > 20) or (user_moy < 0):
+                etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+
+                raise ScoException(
+                    """valeur moyenne %s hors limite pour <a href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s">%s</a>"""
+                    % (user_moy, sem["formsemestre_id"], etudid, etud["nomprenom"])
+                )
+    except:
+        log(
+            "invalid expression : %s\nvariables=%s\n"
+            % (formula, pprint.pformat(variables))
+        )
+        tb = traceback.format_exc()
+        log("Exception during evaluation:\n%s\n" % tb)
+        diag_info.update({"msg": tb.splitlines()[-1]})
+        user_moy = "ERR"
+
+    # log('formula=%s\nvariables=%s\nmoy=%s\nuser_moy=%s' % (formula, variables, moy, user_moy))
+
+    return user_moy
+
+
+def do_moduleimpl_moyennes(context, nt, mod):
+    """Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits
+    au moduleimpl mod, la liste des evaluations "valides" (toutes notes entrées
+    ou en attente), et att (vrai s'il y a des notes en attente dans ce module).
+    La moyenne est calculée en utilisant les coefs des évaluations.
+    Les notes NEUTRES (abs. excuses) ne sont pas prises en compte.
+    Les notes ABS sont remplacées par des zéros.
+    S'il manque des notes et que le coef n'est pas nul,
+    la moyenne n'est pas calculée: NA
+    Ne prend en compte que les evaluations où toutes les notes sont entrées.
+    Le résultat est une note sur 20.
+    """
+    diag_info = {}  # message d'erreur formule
+    moduleimpl_id = mod["moduleimpl_id"]
+    is_malus = mod["module"]["module_type"] == MODULE_MALUS
+    sem = sco_formsemestre.get_formsemestre(context, mod["formsemestre_id"])
+    etudids = context.do_moduleimpl_listeetuds(
+        moduleimpl_id
+    )  # tous, y compris demissions
+    # Inscrits au semestre (pour traiter les demissions):
+    inssem_set = Set(
+        [
+            x["etudid"]
+            for x in context.do_formsemestre_inscription_listinscrits(
+                mod["formsemestre_id"]
+            )
+        ]
+    )
+    insmod_set = inssem_set.intersection(etudids)  # inscrits au semestre et au module
+
+    evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
+    evals.sort(
+        key=lambda x: (x["numero"], x["jour"], x["heure_debut"])
+    )  # la plus ancienne en tête
+
+    user_expr = moduleimpl_has_expression(context, mod)
+    attente = False
+    # recupere les notes de toutes les evaluations
+    eval_rattr = None
+    for e in evals:
+        e["nb_inscrits"] = e["etat"]["nb_inscrits"]
+        NotesDB = context._notes_getall(
+            e["evaluation_id"]
+        )  # toutes, y compris demissions
+        # restreint aux étudiants encore inscrits à ce module
+        notes = [
+            NotesDB[etudid]["value"] for etudid in NotesDB if (etudid in insmod_set)
+        ]
+        e["nb_notes"] = len(notes)
+        e["nb_abs"] = len([x for x in notes if x is None])
+        e["nb_neutre"] = len([x for x in notes if x == NOTES_NEUTRALISE])
+        e["nb_att"] = len([x for x in notes if x == NOTES_ATTENTE])
+        e["notes"] = NotesDB
+
+        if e["etat"]["evalattente"]:
+            attente = True
+        if e["evaluation_type"] == EVALUATION_RATTRAPAGE:
+            if eval_rattr:
+                # !!! plusieurs rattrapages !
+                diag_info.update(
+                    {
+                        "msg": "plusieurs évaluations de rattrapage !",
+                        "moduleimpl_id": moduleimpl_id,
+                    }
+                )
+            eval_rattr = e
+
+    # Les modules MALUS ne sont jamais considérés en attente
+    if is_malus:
+        attente = False
+
+    # filtre les evals valides (toutes les notes entrées)
+    valid_evals = [
+        e
+        for e in evals
+        if (
+            (e["etat"]["evalcomplete"] or e["etat"]["evalattente"])
+            and (e["note_max"] > 0)
+        )
+    ]
+    #
+    R = {}
+    formula = unescape_html(mod["computation_expr"])
+    formula_use_abs = "abs" in formula
+
+    for etudid in insmod_set:  # inscrits au semestre et au module
+        sum_notes = 0.0
+        sum_coefs = 0.0
+        nb_missing = 0
+        for e in valid_evals:
+            if e["evaluation_type"] != EVALUATION_NORMALE:
+                continue
+            if e["notes"].has_key(etudid):
+                note = e["notes"][etudid]["value"]
+                if note is None:  # ABSENT
+                    note = 0
+                if note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
+                    sum_notes += (note * 20.0 / e["note_max"]) * e["coefficient"]
+                    sum_coefs += e["coefficient"]
+            else:
+                # il manque une note ! (si publish_incomplete, cela peut arriver, on ignore)
+                if e["coefficient"] > 0 and e["publish_incomplete"] == "0":
+                    nb_missing += 1
+        if nb_missing == 0 and sum_coefs > 0:
+            if sum_coefs > 0:
+                R[etudid] = sum_notes / sum_coefs
+                moy_valid = True
+            else:
+                R[etudid] = "na"
+                moy_valid = False
+        else:
+            R[etudid] = "NA%d" % nb_missing
+            moy_valid = False
+
+        if user_expr:
+            # recalcule la moyenne en utilisant la formule utilisateur
+            notes = []
+            coefs = []
+            coefs_mask = []  # 0/1, 0 si coef a ete annulé
+            nb_notes = 0  # nombre de notes valides
+            for e in evals:
+                if (
+                    (e["etat"]["evalcomplete"] or e["etat"]["evalattente"])
+                    and e["notes"].has_key(etudid)
+                ) and (e["note_max"] > 0):
+                    note = e["notes"][etudid]["value"]
+                    if note is None:
+                        note = 0
+                    if note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
+                        notes.append(note * 20.0 / e["note_max"])
+                        coefs.append(e["coefficient"])
+                        coefs_mask.append(1)
+                        nb_notes += 1
+                    else:
+                        notes.append(0.0)
+                        coefs.append(0.0)
+                        coefs_mask.append(0)
+                else:
+                    notes.append(0.0)
+                    coefs.append(0.0)
+                    coefs_mask.append(0)
+            if nb_notes > 0:
+                user_moy = compute_user_formula(
+                    context,
+                    sem,
+                    etudid,
+                    R[etudid],
+                    moy_valid,
+                    notes,
+                    coefs,
+                    coefs_mask,
+                    formula,
+                    diag_info=diag_info,
+                    use_abs=formula_use_abs,
+                )
+                if diag_info:
+                    diag_info["moduleimpl_id"] = moduleimpl_id
+                R[etudid] = user_moy
+        # Note de rattrapage ?
+        if eval_rattr:
+            if eval_rattr["notes"].has_key(etudid):
+                note = eval_rattr["notes"][etudid]["value"]
+                if note != None and note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
+                    if type(R[etudid]) != FloatType:
+                        R[etudid] = note
+                    else:
+                        note_sur_20 = note * 20.0 / eval_rattr["note_max"]
+                        if note_sur_20 > R[etudid]:
+                            # log('note_sur_20=%s' % note_sur_20)
+                            R[etudid] = note_sur_20
+
+    return R, valid_evals, attente, diag_info
+
+
+def do_formsemestre_moyennes(context, nt, formsemestre_id):
+    """retourne dict { moduleimpl_id : { etudid, note_moyenne_dans_ce_module } },
+    la liste des moduleimpls, la liste des evaluations valides,
+    liste des moduleimpls  avec notes en attente.
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    inscr = context.do_formsemestre_inscription_list(
+        args={"formsemestre_id": formsemestre_id}
+    )
+    etudids = [x["etudid"] for x in inscr]
+    modimpls = context.do_moduleimpl_list(formsemestre_id=formsemestre_id)
+    # recupere les moyennes des etudiants de tous les modules
+    D = {}
+    valid_evals = []
+    valid_evals_per_mod = {}  # { moduleimpl_id : eval }
+    mods_att = []
+    expr_diags = []
+    for modimpl in modimpls:
+        mod = context.do_module_list(args={"module_id": modimpl["module_id"]})[0]
+        modimpl["module"] = mod  # add module dict to moduleimpl (used by nt)
+        moduleimpl_id = modimpl["moduleimpl_id"]
+        assert not D.has_key(moduleimpl_id)
+        D[moduleimpl_id], valid_evals_mod, attente, expr_diag = do_moduleimpl_moyennes(
+            context, nt, modimpl
+        )
+        valid_evals_per_mod[moduleimpl_id] = valid_evals_mod
+        valid_evals += valid_evals_mod
+        if attente:
+            mods_att.append(modimpl)
+        if expr_diag:
+            expr_diags.append(expr_diag)
+    #
+    return D, modimpls, valid_evals_per_mod, valid_evals, mods_att, expr_diags
diff --git a/sco_cost_formation.py b/sco_cost_formation.py
new file mode 100644
index 0000000000000000000000000000000000000000..07ac063d92cd9b8ca45ffef5c9ff9eab6b0f1da8
--- /dev/null
+++ b/sco_cost_formation.py
@@ -0,0 +1,197 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Rapports estimation coût de formation basé sur le programme pédagogique
+   et les nombres de groupes.
+
+   (coût théorique en heures équivalent TD)
+"""
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from gen_tables import GenTable
+import sco_excel, sco_pdf
+from sco_pdf import SU
+import sco_formsemestre
+import sco_formsemestre_status
+
+
+def formsemestre_table_estim_cost(
+    context,
+    formsemestre_id,
+    n_group_td=1,
+    n_group_tp=1,
+    coef_tp=1,
+    coef_cours=1.5,
+    REQUEST=None,
+):
+    """
+    Rapports estimation coût de formation basé sur le programme pédagogique
+    et les nombres de groupes.
+    Coût théorique en heures équivalent TD.
+    Attention: ne prend en compte que les modules utilisés dans ce semestre.
+    Attention: prend en compte _tous_ les modules utilisés dans ce semestre, ce qui
+    peut conduire à une sur-estimation du coût s'il y a des modules optionnels
+    (dans ce cas, retoucher le tableau excel exporté).
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    sco_formsemestre_status.fill_formsemestre(context, sem, REQUEST=REQUEST)
+    Mlist = context.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+    T = []
+    for M in Mlist:
+        Mod = M["module"]
+        T.append(
+            {
+                "code": Mod["code"],
+                "titre": Mod["titre"],
+                "heures_cours": Mod["heures_cours"],
+                "heures_td": Mod["heures_td"] * n_group_td,
+                "heures_tp": Mod["heures_tp"] * n_group_tp,
+            }
+        )
+
+    # calcul des heures:
+    for t in T:
+        t["HeqTD"] = (
+            t["heures_td"] + coef_cours * t["heures_cours"] + coef_tp * t["heures_tp"]
+        )
+    sum_cours = sum([t["heures_cours"] for t in T])
+    sum_td = sum([t["heures_td"] for t in T])
+    sum_tp = sum([t["heures_tp"] for t in T])
+    sum_heqtd = sum_td + coef_cours * sum_cours + coef_tp * sum_tp
+    assert abs(sum([t["HeqTD"] for t in T]) - sum_heqtd) < 0.01, "%s != %s" % (
+        sum([t["HeqTD"] for t in T]),
+        sum_heqtd,
+    )
+
+    T.append(
+        {
+            "code": "TOTAL SEMESTRE",
+            "heures_cours": sum_cours,
+            "heures_td": sum_td,
+            "heures_tp": sum_tp,
+            "HeqTD": sum_heqtd,
+            "_table_part": "foot",
+        }
+    )
+
+    titles = {
+        "code": "Code",
+        "titre": "Titre",
+        "heures_cours": "Cours",
+        "heures_td": "TD",
+        "heures_tp": "TP",
+        "HeqTD": "HeqTD",
+    }
+
+    tab = GenTable(
+        titles=titles,
+        columns_ids=(
+            "code",
+            "titre",
+            "heures_cours",
+            "heures_td",
+            "heures_tp",
+            "HeqTD",
+        ),
+        rows=T,
+        html_sortable=True,
+        preferences=context.get_preferences(formsemestre_id),
+        html_class="table_leftalign table_listegroupe",
+        xls_before_table=[
+            ["%(titre)s %(num_sem)s %(modalitestr)s" % sem],
+            ["Formation %(titre)s version %(version)s" % sem["formation"]],
+            [],
+            ["", "TD", "TP"],
+            ["Nombre de groupes", n_group_td, n_group_tp],
+            [],
+            [],
+        ],
+        html_caption="""<div class="help">
+                    Estimation du coût de formation basé sur le programme pédagogique
+    et les nombres de groupes.<br/>
+    Coût théorique en heures équivalent TD.<br/>
+    Attention: ne prend en compte que les modules utilisés dans ce semestre.<br/>
+    Attention: prend en compte <em>tous les modules</em> utilisés dans ce semestre, ce qui
+    peut conduire à une sur-estimation du coût s'il y a des modules optionnels
+    (dans ce cas, retoucher le tableau excel exporté).
+    </div>
+                    """,
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        filename="EstimCout-S%s" % sem["semestre_id"],
+    )
+    return tab
+
+
+def formsemestre_estim_cost(
+    context,
+    formsemestre_id,
+    n_group_td=1,
+    n_group_tp=1,
+    coef_tp=1,
+    coef_cours=1.5,
+    format="html",
+    REQUEST=None,
+):
+    """Page (formulaire) estimation coûts"""
+
+    n_group_td = int(n_group_td)
+    n_group_tp = int(n_group_tp)
+    coef_tp = float(coef_tp)
+    coef_cours = float(coef_cours)
+
+    tab = formsemestre_table_estim_cost(
+        context,
+        formsemestre_id,
+        n_group_td=n_group_td,
+        n_group_tp=n_group_tp,
+        coef_tp=coef_tp,
+        coef_cours=coef_cours,
+        REQUEST=REQUEST,
+    )
+    h = """
+    <form name="f" method="get" action="%s">
+    <input type="hidden" name="formsemestre_id" value="%s"></input>
+    Nombre de groupes de TD: <input type="text" name="n_group_td" value="%s" onchange="document.f.submit()"/><br/>
+    Nombre de groupes de TP: <input type="text" name="n_group_tp" value="%s" onchange="document.f.submit()"/>
+    &nbsp;Coefficient heures TP: <input type="text" name="coef_tp" value="%s" onchange="document.f.submit()"/>
+    <br/>
+    </form>
+    """ % (
+        REQUEST.URL0,
+        formsemestre_id,
+        n_group_td,
+        n_group_tp,
+        coef_tp,
+    )
+    tab.html_before_table = h
+    tab.base_url = (
+        "%s?formsemestre_id=%s&amp;n_group_td=%s&amp;n_group_tp=%s&amp;coef_tp=%s"
+        % (REQUEST.URL0, formsemestre_id, n_group_td, n_group_tp, coef_tp)
+    )
+
+    return tab.make_page(context, format=format, REQUEST=REQUEST)
diff --git a/sco_debouche.py b/sco_debouche.py
new file mode 100644
index 0000000000000000000000000000000000000000..360818d8518c936b4e6073ce8d0d6df13ec92a04
--- /dev/null
+++ b/sco_debouche.py
@@ -0,0 +1,387 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""
+Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant
+"""
+
+import safehtml
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from scolog import logdb
+from gen_tables import GenTable
+import sco_formsemestre
+import sco_groups
+import sco_tag_module
+
+
+def report_debouche_date(context, start_year=None, format="html", REQUEST=None):
+    """Rapport (table) pour les débouchés des étudiants sortis à partir de l'année indiquée.
+    """
+    if not start_year:
+        return report_debouche_ask_date(context, REQUEST=REQUEST)
+    if format == "xls":
+        keep_numeric = True  # pas de conversion des notes en strings
+    else:
+        keep_numeric = False
+
+    etudids = get_etudids_with_debouche(context, start_year)
+    tab = table_debouche_etudids(context, etudids, keep_numeric=keep_numeric)
+
+    tab.filename = make_filename("debouche_scodoc_%s" % start_year)
+    tab.origin = "Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + ""
+    tab.caption = "Récapitulatif débouchés à partir du 1/1/%s." % start_year
+    tab.base_url = "%s?start_year=%s" % (REQUEST.URL0, start_year)
+    return tab.make_page(
+        context,
+        title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
+        init_qtip=True,
+        javascripts=["js/etud_info.js"],
+        format=format,
+        REQUEST=REQUEST,
+        with_html_headers=True,
+    )
+
+
+def get_etudids_with_debouche(context, start_year):
+    """Liste des etudids de tous les semestres terminant
+    à partir du 1er janvier de start_year
+    et ayant un 'debouche' renseigné.
+    """
+    start_date = str(start_year) + "-01-01"
+    # Recupere tous les etudid avec un debouché renseigné et une inscription dans un semestre
+    # posterieur à la date de depart:
+    # r = SimpleDictFetch(context,
+    #                    """SELECT DISTINCT i.etudid
+    #                    FROM notes_formsemestre_inscription i, admissions adm, notes_formsemestre s
+    #                    WHERE adm.debouche is not NULL
+    #                    AND i.etudid = adm.etudid AND i.formsemestre_id = s.formsemestre_id
+    #                    AND s.date_fin >= %(start_date)s
+    #                    """,
+    #                    {'start_date' : start_date })
+
+    r = SimpleDictFetch(
+        context,
+        """SELECT DISTINCT i.etudid
+                        FROM notes_formsemestre_inscription i, notes_formsemestre s, itemsuivi it
+                        WHERE i.etudid = it.etudid
+                        AND i.formsemestre_id = s.formsemestre_id AND s.date_fin >= %(start_date)s
+                        """,
+        {"start_date": start_date},
+    )
+
+    return [x["etudid"] for x in r]
+
+
+def table_debouche_etudids(context, etudids, keep_numeric=True):
+    """Rapport pour ces etudiants
+    """
+    L = []
+    for etudid in etudids:
+        etud = context.getEtudInfo(filled=1, etudid=etudid)[0]
+        # retrouve le "dernier" semestre (au sens de la date de fin)
+        sems = etud["sems"]
+        es = [(sems[i]["date_fin_iso"], i) for i in range(len(sems))]
+        imax = max(es)[1]
+        last_sem = sems[imax]
+        nt = context._getNotesCache().get_NotesTable(
+            context, last_sem["formsemestre_id"]
+        )
+        row = {
+            "etudid": etudid,
+            "sexe": etud["sexe"],
+            "nom": etud["nom"],
+            "prenom": etud["prenom"],
+            "_nom_target": "ficheEtud?etudid=" + etud["etudid"],
+            "_prenom_target": "ficheEtud?etudid=" + etud["etudid"],
+            "_nom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]),
+            # 'debouche' : etud['debouche'],
+            "moy": fmt_note(nt.get_etud_moy_gen(etudid), keep_numeric=keep_numeric),
+            "rang": nt.get_etud_rang(etudid),
+            "effectif": len(nt.T),
+            "semestre_id": last_sem["semestre_id"],
+            "semestre": last_sem["titre"],
+            "date_debut": last_sem["date_debut"],
+            "date_fin": last_sem["date_fin"],
+            "periode": "%s - %s" % (last_sem["mois_debut"], last_sem["mois_fin"]),
+            "sem_ident": "%s %s"
+            % (last_sem["date_debut_iso"], last_sem["titre"]),  # utile pour tris
+        }
+
+        # recherche des débouchés
+        debouche = itemsuivi_list_etud(context, etudid)  # liste de plusieurs items
+        if debouche:
+            row["debouche"] = "<br>".join(
+                [
+                    str(it["item_date"])
+                    + " : "
+                    + it["situation"]
+                    + " <i>"
+                    + it["tags"]
+                    + "</i>"
+                    for it in debouche
+                ]
+            )  #
+        else:
+            row["debouche"] = "non renseigné"
+        L.append(row)
+    L.sort(key=lambda x: x["sem_ident"])
+
+    titles = {
+        "sexe": "",
+        "nom": "Nom",
+        "prenom": "Prénom",
+        "semestre": "Dernier semestre",
+        "semestre_id": "S",
+        "periode": "Dates",
+        "moy": "Moyenne",
+        "rang": "Rang",
+        "effectif": "Eff.",
+        "debouche": "Débouché",
+    }
+    tab = GenTable(
+        columns_ids=(
+            "semestre",
+            "semestre_id",
+            "periode",
+            "sexe",
+            "nom",
+            "prenom",
+            "moy",
+            "rang",
+            "effectif",
+            "debouche",
+        ),
+        titles=titles,
+        rows=L,
+        # html_col_width='4em',
+        html_sortable=True,
+        html_class="table_leftalign table_listegroupe",
+        preferences=context.get_preferences(),
+    )
+    return tab
+
+
+def report_debouche_ask_date(context, REQUEST=None):
+    """Formulaire demande date départ
+    """
+    return (
+        context.sco_header(REQUEST)
+        + """<form method="GET">
+            Date de départ de la recherche: <input type="text" name="start_year" value="" size=10/>
+            </form>"""
+        + context.sco_footer(REQUEST)
+    )
+
+
+# ----------------------------------------------------------------------------
+#
+# Nouveau suivi des etudiants (nov 2017)
+#
+# ----------------------------------------------------------------------------
+
+# OBSOLETE (this field has been copied to itemsuivi)
+# def debouche_set(context, object, value, REQUEST=None):
+#     """Set debouche (field in admission table, may be deprecated ?)
+#     """
+#     if not context.can_edit_suivi(REQUEST):
+#         raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+#     adm_id = object
+#     debouche = value.strip('-_ \t')
+#     cnx = context.GetDBConnexion()
+#     adms = scolars.admission_list(cnx, {'etudid' : etudid})
+#     if not adms:
+#         raise ValueError('no admission info for %s !' % etudid)
+#     adm = adms[0]
+#     adm['debouche'] = debouche
+#     admission_edit(cnx, adm)
+
+
+_itemsuiviEditor = EditableTable(
+    "itemsuivi",
+    "itemsuivi_id",
+    ("itemsuivi_id", "etudid", "item_date", "situation"),
+    sortkey="item_date desc",
+    convert_null_outputs_to_empty=True,
+    output_formators={"situation": safehtml.HTML2SafeHTML, "item_date": DateISOtoDMY},
+    input_formators={"item_date": DateDMYtoISO},
+)
+
+_itemsuivi_create = _itemsuiviEditor.create
+_itemsuivi_delete = _itemsuiviEditor.delete
+_itemsuivi_list = _itemsuiviEditor.list
+_itemsuivi_edit = _itemsuiviEditor.edit
+
+
+class ItemSuiviTag(sco_tag_module.ScoTag):
+    """Les tags sur les items
+    """
+
+    tag_table = "itemsuivi_tags"  # table (tag_id, title)
+    assoc_table = "itemsuivi_tags_assoc"  # table (tag_id, object_id)
+    obj_colname = "itemsuivi_id"  # column name for object_id in assoc_table
+
+
+def itemsuivi_get(cnx, itemsuivi_id, ignore_errors=False):
+    """get an item"""
+    items = _itemsuivi_list(cnx, {"itemsuivi_id": itemsuivi_id})
+    if items:
+        return items[0]
+    elif not ignore_errors:
+        raise ValueError("invalid itemsuivi_id")
+    return None
+
+
+def itemsuivi_suppress(context, itemsuivi_id, REQUEST=None):
+    """Suppression d'un item
+    """
+    if not context.can_edit_suivi(REQUEST):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    cnx = context.GetDBConnexion()
+    item = itemsuivi_get(cnx, itemsuivi_id, ignore_errors=True)
+    if item:
+        _itemsuivi_delete(cnx, itemsuivi_id)
+        logdb(REQUEST, cnx, method="itemsuivi_suppress", etudid=item["etudid"])
+        log("suppressed itemsuivi %s" % (itemsuivi_id,))
+
+
+def itemsuivi_create(
+    context, etudid, item_date=None, situation="", REQUEST=None, format=None
+):
+    """Creation d'un item"""
+    if not context.can_edit_suivi(REQUEST):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    cnx = context.GetDBConnexion()
+    itemsuivi_id = _itemsuivi_create(
+        cnx, args={"etudid": etudid, "item_date": item_date, "situation": situation}
+    )
+    logdb(REQUEST, cnx, method="itemsuivi_create", etudid=etudid)
+    log("created itemsuivi %s for %s" % (itemsuivi_id, etudid))
+    item = itemsuivi_get(cnx, itemsuivi_id)
+    if format == "json":
+        return sendJSON(REQUEST, item)
+    return item
+
+
+def itemsuivi_set_date(context, itemsuivi_id, item_date, REQUEST=None):
+    """set item date
+    item_date is a string dd/mm/yyyy
+    """
+    if not context.can_edit_suivi(REQUEST):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    # log('itemsuivi_set_date %s : %s' % (itemsuivi_id, item_date))
+    cnx = context.GetDBConnexion()
+    item = itemsuivi_get(cnx, itemsuivi_id)
+    item["item_date"] = item_date
+    _itemsuivi_edit(cnx, item)
+
+
+def itemsuivi_set_situation(context, object, value, REQUEST=None):
+    """set situation"""
+    if not context.can_edit_suivi(REQUEST):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    itemsuivi_id = object
+    situation = value.strip("-_ \t")
+    # log('itemsuivi_set_situation %s : %s' % (itemsuivi_id, situation))
+    cnx = context.GetDBConnexion()
+    item = itemsuivi_get(cnx, itemsuivi_id)
+    item["situation"] = situation
+    _itemsuivi_edit(cnx, item)
+    return situation or IT_SITUATION_MISSING_STR
+
+
+def itemsuivi_list_etud(context, etudid, format=None, REQUEST=None):
+    """Liste des items pour cet étudiant, avec tags"""
+    cnx = context.GetDBConnexion()
+    items = _itemsuivi_list(cnx, {"etudid": etudid})
+    for it in items:
+        it["tags"] = ", ".join(itemsuivi_tag_list(context, it["itemsuivi_id"]))
+    if format == "json":
+        return sendJSON(REQUEST, items)
+    return items
+
+
+def itemsuivi_tag_list(context, itemsuivi_id):
+    """les noms de tags associés à cet item"""
+    r = SimpleDictFetch(
+        context,
+        """SELECT t.title
+          FROM itemsuivi_tags_assoc a, itemsuivi_tags t
+          WHERE a.tag_id = t.tag_id
+          AND a.itemsuivi_id = %(itemsuivi_id)s
+          """,
+        {"itemsuivi_id": itemsuivi_id},
+    )
+    return [x["title"] for x in r]
+
+
+def itemsuivi_tag_search(context, term, REQUEST=None):
+    """List all used tag names (for auto-completion)"""
+    # restrict charset to avoid injections
+    if not ALPHANUM_EXP.match(term.decode(SCO_ENCODING)):
+        data = []
+    else:
+        r = SimpleDictFetch(
+            context,
+            "SELECT title FROM itemsuivi_tags WHERE title LIKE %(term)s",
+            {"term": term + "%"},
+        )
+        data = [x["title"] for x in r]
+
+    return sendJSON(REQUEST, data)
+
+
+def itemsuivi_tag_set(context, itemsuivi_id="", taglist=[], REQUEST=None):
+    """taglist may either be:
+    a string with tag names separated by commas ("un;deux")
+    or a list of strings (["un", "deux"])
+    """
+    if not context.can_edit_suivi(REQUEST):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    if not taglist:
+        taglist = []
+    elif type(taglist) == StringType:
+        taglist = taglist.split(",")
+    taglist = [t.strip() for t in taglist]
+    # log('itemsuivi_tag_set: itemsuivi_id=%s taglist=%s' % (itemsuivi_id, taglist))
+    # Sanity check:
+    cnx = context.GetDBConnexion()
+    item = itemsuivi_get(cnx, itemsuivi_id)
+
+    newtags = set(taglist)
+    oldtags = set(itemsuivi_tag_list(context, itemsuivi_id))
+    to_del = oldtags - newtags
+    to_add = newtags - oldtags
+
+    # should be atomic, but it's not.
+    for tagname in to_add:
+        t = ItemSuiviTag(context, tagname, object_id=itemsuivi_id)
+    for tagname in to_del:
+        t = ItemSuiviTag(context, tagname)
+        t.remove_tag_from_object(itemsuivi_id)
diff --git a/sco_dept.py b/sco_dept.py
new file mode 100644
index 0000000000000000000000000000000000000000..5714020299e2e47f51501ef988ffc3c607c7c76e
--- /dev/null
+++ b/sco_dept.py
@@ -0,0 +1,267 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Page accueil département (liste des semestres, etc)
+"""
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+import sco_modalites
+import sco_news
+import sco_up_to_date
+import sco_formsemestre
+from gen_tables import GenTable
+
+
+def index_html(context, REQUEST=None, showcodes=0, showsemtable=0):
+    "Page accueil département (liste des semestres)"
+    showsemtable = int(showsemtable)
+    H = []
+
+    # News:
+    rssicon = icontag("rssscodoc_img", title="Flux RSS", border="0")
+    H.append(sco_news.scolar_news_summary_html(context, rssicon=rssicon))
+
+    # Avertissement de mise à jour:
+    H.append(sco_up_to_date.html_up_to_date_box(context))
+
+    # Liste de toutes les sessions:
+    sems = sco_formsemestre.do_formsemestre_list(context)
+    cursems = []  # semestres "courants"
+    othersems = []  # autres (verrouillés)
+    # icon image:
+    groupicon = icontag("groupicon_img", title="Inscrits", border="0")
+    emptygroupicon = icontag("emptygroupicon_img", title="Pas d'inscrits", border="0")
+    lockicon = icontag("lock32_img", title="verrouillé", border="0")
+    # Sélection sur l'etat du semestre
+    for sem in sems:
+        if sem["etat"] == "1" and sem["modalite"] != "EXT":
+            sem["lockimg"] = ""
+            cursems.append(sem)
+        else:
+            sem["lockimg"] = lockicon
+            othersems.append(sem)
+        # Responsable de formation:
+        sco_formsemestre.sem_set_responsable_name(context, sem)
+
+        if showcodes == "1":
+            sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"]
+        else:
+            sem["tmpcode"] = ""
+        # Nombre d'inscrits:
+        args = {"formsemestre_id": sem["formsemestre_id"]}
+        ins = context.Notes.do_formsemestre_inscription_list(args=args)
+        nb = len(ins)  # nb etudiants
+        sem["nb_inscrits"] = nb
+        if nb > 0:
+            sem["groupicon"] = groupicon
+        else:
+            sem["groupicon"] = emptygroupicon
+
+    # S'il n'y a pas d'utilisateurs dans la base, affiche message
+    if not context.Users.get_userlist():
+        H.append(
+            """<h2>Aucun utilisateur défini !</h2><p>Pour définir des utilisateurs
+        <a href="Users">passez par la page Utilisateurs</a>.
+        <br/>
+        Définissez au moins un utilisateur avec le rôle AdminXXX (le responsable du département XXX).
+        </p>
+        """
+        )
+
+    # Liste des formsemestres "courants"
+    if cursems:
+        H.append('<h2 class="listesems">Sessions en cours</h2>')
+        H.append(_sem_table(context, cursems))
+    else:
+        # aucun semestre courant: affiche aide
+        H.append(
+            """<h2 class="listesems">Aucune session en cours !</h2>
+        <p>Pour ajouter une session, aller dans <a href="Notes">Programmes</a>,
+        choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
+        </p><p>
+        Là, en bas de page, suivez le lien
+        "<em>Mettre en place un nouveau semestre de formation...</em>"
+        </p>"""
+        )
+
+    if showsemtable:
+        H.append(
+            """<hr/>
+        <h2>Semestres de %s</h2>
+        """
+            % context.get_preference("DeptName")
+        )
+        H.append(_sem_table_gt(context, sems).html())
+        H.append("</table>")
+    if not showsemtable:
+        H.append(
+            '<hr/><p><a href="%s?showsemtable=1">Voir tous les semestres</a></p>'
+            % REQUEST.URL0
+        )
+
+    H.append(
+        """<p><form action="Notes/view_formsemestre_by_etape">
+Chercher étape courante: <input name="etape_apo" type="text" size="8"></input>    
+    </form
+    </p>
+    """
+    )
+    #
+    authuser = REQUEST.AUTHENTICATED_USER
+    if authuser.has_permission(ScoEtudInscrit, context):
+        H.append(
+            """<hr>
+        <h3>Gestion des étudiants</h3>
+        <ul>
+        <li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a></li>
+        <li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a> (ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
+        le tableau de bord semestre si vous souhaitez inscrire les
+        étudiants importés à un semestre)</li>
+        </ul>
+        """
+        )
+    #
+    if authuser.has_permission(ScoEditApo, context):
+        H.append(
+            """<hr>
+        <h3>Exports Apogée</h3>
+        <ul>
+        <li><a class="stdlink" href="Notes/semset_page">Années scolaires / exports Apogée</a></li>
+        </ul>
+        """
+        )
+    #
+    H.append(
+        """<hr>
+        <h3>Assistance</h3>
+        <ul>
+        <li><a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a></li>
+        </ul>
+    """
+    )
+    #
+    return context.sco_header(REQUEST) + "\n".join(H) + context.sco_footer(REQUEST)
+
+
+def _sem_table(context, sems):
+    """Affiche liste des semestres, utilisée pour semestres en cours
+    """
+    tmpl = """<tr class="%(trclass)s">%(tmpcode)s
+    <td class="semicon">%(lockimg)s <a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>        
+    <td class="datesem">%(mois_debut)s</td><td class="datesem"><a title="%(session_id)s">-</a> %(mois_fin)s</td>
+    <td><a class="stdlink" href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
+    <span class="respsem">(%(responsable_name)s)</span>
+    </td>
+    </tr>
+    """
+
+    # Liste des semestres, groupés par modalités
+    sems_by_mod, modalites = sco_modalites.group_sems_by_modalite(context, sems)
+
+    H = ['<table class="listesems">']
+    for modalite in modalites:
+        if len(modalites) > 1:
+            H.append('<tr><th colspan="4">%s</th></tr>' % modalite["titre"])
+
+        if sems_by_mod[modalite["modalite"]]:
+            cur_idx = sems_by_mod[modalite["modalite"]][0]["semestre_id"]
+            for sem in sems_by_mod[modalite["modalite"]]:
+                if cur_idx != sem["semestre_id"]:
+                    sem["trclass"] = "firstsem"  # separe les groupes de semestres
+                    cur_idx = sem["semestre_id"]
+                else:
+                    sem["trclass"] = ""
+                H.append(tmpl % sem)
+    H.append("</table>")
+    return "\n".join(H)
+
+
+def _sem_table_gt(context, sems, showcodes=False):
+    """Nouvelle version de la table des semestres
+    """
+    _style_sems(context, sems)
+    columns_ids = (
+        "lockimg",
+        "semestre_id_n",
+        "modalite",
+        #'mois_debut',
+        "dash_mois_fin",
+        "titre_resp",
+        "nb_inscrits",
+        "etapes_apo_str",
+    )
+    if showcodes:
+        columns_ids = ("formsemestre_id",) + columns_ids
+
+    tab = GenTable(
+        titles={
+            "formsemestre_id": "id",
+            "semestre_id_n": "S#",
+            "modalite": "",
+            "mois_debut": "Début",
+            "dash_mois_fin": "Année",
+            "titre_resp": "Semestre",
+            "nb_inscrits": "N",  # groupicon,
+        },
+        columns_ids=columns_ids,
+        rows=sems,
+        html_class="table_leftalign semlist",
+        html_sortable=True,
+        # base_url = '%s?formsemestre_id=%s' % (REQUEST.URL0, formsemestre_id),
+        # caption='Maquettes enregistrées',
+        preferences=context.get_preferences(),
+    )
+
+    return tab
+
+
+def _style_sems(context, sems):
+    """ajoute quelques attributs de présentation pour la table
+    """
+    for sem in sems:
+        sem["_groupicon_target"] = (
+            "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % sem
+        )
+        sem["_formsemestre_id_class"] = "blacktt"
+        sem["dash_mois_fin"] = '<a title="%(session_id)s"></a> %(anneescolaire)s' % sem
+        sem["_dash_mois_fin_class"] = "datesem"
+        sem["titre_resp"] = (
+            """<a class="stdlink" href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
+    <span class="respsem">(%(responsable_name)s)</span>"""
+            % sem
+        )
+        sem["_css_row_class"] = "css_S%d css_M%s" % (
+            sem["semestre_id"],
+            sem["modalite"],
+        )
+        sem["_semestre_id_class"] = "semestre_id"
+        sem["_modalite_class"] = "modalite"
+        if sem["semestre_id"] == -1:
+            sem["semestre_id_n"] = ""
+        else:
+            sem["semestre_id_n"] = sem["semestre_id"]
diff --git a/sco_dump_db.py b/sco_dump_db.py
new file mode 100644
index 0000000000000000000000000000000000000000..53d007b047f3c480ac1a0da648a35c166de35485
--- /dev/null
+++ b/sco_dump_db.py
@@ -0,0 +1,221 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Dump base de données pour debug et support technique
+
+Le principe est le suivant:
+ 1- S'il existe une base en cours d'anonymisation, s'arrête et affiche un msg d'erreur l'utilisateur,
+    qui peut décider de la supprimer.
+
+ 2- ScoDoc lance un script qui duplique la base (la copie de SCORT devient ANORT)
+     -  (si elle existe deja, s'arrête)
+createdb -E UTF-8 ANORT
+pg_dump SCORT | psql ANORT
+
+
+ 3- ScoDoc lance le script d'anonymisation config/anonymize_db.py qui:
+     - vide ou anonymise certaines colonnes
+     - dump cette base modifiée
+     - supprime cette base.
+
+ 4- La copie dump anonymisé est uploadée.
+
+
+"""
+
+import fcntl
+import subprocess
+import requests
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email.MIMEBase import MIMEBase
+from email.Header import Header
+from email import Encoders
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+
+SCO_DUMP_LOCK = "/tmp/scodump.lock"
+
+
+def sco_dump_and_send_db(context, REQUEST=None):
+    """Dump base de données du département courant et l'envoie anonymisée pour debug
+    """
+    H = [context.sco_header(REQUEST, page_title="Assistance technique")]
+    # get currect (dept) DB name:
+    cursor = SimpleQuery(context, "SELECT current_database()", {})
+    db_name = cursor.fetchone()[0]
+    ano_db_name = "ANO" + db_name
+    # Lock
+    try:
+        x = open(SCO_DUMP_LOCK, "w+")
+        fcntl.flock(x, fcntl.LOCK_EX | fcntl.LOCK_NB)
+    except fcntl.BlockingIOError:
+        raise ScoValueError(
+            "Un envoi de la base "
+            + db_name
+            + " est déjà en cours, re-essayer plus tard"
+        )
+
+    try:
+        # Drop if exists
+        _drop_ano_db(ano_db_name)
+
+        # Duplicate database
+        _duplicate_db(db_name, ano_db_name)
+
+        # Anonymisation
+        _anonymize_db(ano_db_name)
+
+        # Send
+        r = _send_db(context, REQUEST, ano_db_name)
+        if r.status_code == requests.codes.INSUFFICIENT_STORAGE:
+            H.append(
+                """<p class="warning">
+            Erreur: espace serveur trop plein.
+            Merci de contacter <a href="mailto:{0}">{0}</a></p>""".format(
+                    SCO_DEV_MAIL
+                )
+            )
+        elif r.status_code == requests.codes.OK:
+            H.append("""<p>Opération effectuée.</p>""")
+        else:
+            H.append(
+                """<p class="warning">
+            Erreur: code <tt>{0} {1}</tt>
+            Merci de contacter <a href="mailto:{2}">{2}</a></p>""".format(
+                    r.status_code, r.reason, SCO_DEV_MAIL
+                )
+            )
+
+    finally:
+        # Drop anonymized database
+        _drop_ano_db(ano_db_name)
+
+        # Remove lock
+        fcntl.flock(x, fcntl.LOCK_UN)
+
+    log("sco_dump_and_send_db: done.")
+    return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+def _duplicate_db(db_name, ano_db_name):
+    """Create new database, and copy old one into"""
+    cmd = ["createdb", "-E", "UTF-8", ano_db_name]
+    log("sco_dump_and_send_db/_duplicate_db: {}".format(cmd))
+    try:
+        out = subprocess.check_output(cmd)
+    except subprocess.CalledProcessError as e:
+        log("sco_dump_and_send_db: exception createdb {}".format(e))
+        raise ScoValueError(
+            "erreur lors de la creation de la base {}".format(ano_db_name)
+        )
+
+    cmd = "pg_dump {} | psql {}".format(db_name, ano_db_name)
+    log("sco_dump_and_send_db/_duplicate_db: {}".format(cmd))
+    try:
+        out = subprocess.check_output(cmd, shell=1)
+    except subprocess.CalledProcessError as e:
+        log("sco_dump_and_send_db: exception {}".format(e))
+        raise ScoValueError(
+            "erreur lors de la duplication de la base {} vers {}".format(
+                db_name, ano_db_name
+            )
+        )
+
+
+def _anonymize_db(ano_db_name):
+    """Anonymize a departement database
+    """
+    cmd = os.path.join(SCO_CONFIG_DIR, "anonymize_db.py")
+    log("_anonymize_db: {}".format(cmd))
+    try:
+        out = subprocess.check_output([cmd, ano_db_name])
+    except subprocess.CalledProcessError as e:
+        log("sco_dump_and_send_db: exception in anonymisation: {}".format(e))
+        raise ScoValueError(
+            "erreur lors de l'anonymisation de la base {}".format(ano_db_name)
+        )
+
+
+def _get_scodoc_serial(context):
+    try:
+        return int(open(os.path.join(SCODOC_VERSION_DIR, "scodoc.sn")).read())
+    except:
+        return 0
+
+
+def _send_db(context, REQUEST, ano_db_name):
+    """Dump this (anonymized) database and send it to tech support
+    """
+    log("dumping anonymized database {}".format(ano_db_name))
+    try:
+        data = subprocess.check_output("pg_dump {} | gzip".format(ano_db_name), shell=1)
+    except subprocess.CalledProcessError as e:
+        log("sco_dump_and_send_db: exception in anonymisation: {}".format(e))
+        raise ScoValueError(
+            "erreur lors de l'anonymisation de la base {}".format(ano_db_name)
+        )
+
+    log("uploading anonymized dump...")
+    files = {"file": (ano_db_name + ".gz", data)}
+    r = requests.post(
+        SCO_DUMP_UP_URL,
+        files=files,
+        data={
+            "dept_name": context.get_preference("DeptName"),
+            "serial": _get_scodoc_serial(context),
+            "sco_user": str(REQUEST.AUTHENTICATED_USER),
+            "sent_by": context.Users.user_info(str(REQUEST.AUTHENTICATED_USER))[
+                "nomcomplet"
+            ],
+            "sco_version": SCOVERSION,
+            "sco_subversion": get_svn_version(SCO_CONFIG_DIR),
+        },
+    )
+    return r
+
+
+def _drop_ano_db(ano_db_name):
+    """drop temp database if it exists"""
+    existing_databases = [
+        s.split("|")[0].strip()
+        for s in subprocess.check_output(["psql", "-l"]).split("\n")[3:]
+    ]
+    if ano_db_name not in existing_databases:
+        log("_drop_ano_db: no temp db, nothing to drop")
+        return
+    cmd = ["dropdb", ano_db_name]
+    log("sco_dump_and_send_db: {}".format(cmd))
+    try:
+        out = subprocess.check_output(cmd)
+    except subprocess.CalledProcessError as e:
+        log("sco_dump_and_send_db: exception dropdb {}".format(e))
+        raise ScoValueError(
+            "erreur lors de la suppression de la base {}".format(ano_db_name)
+        )
diff --git a/sco_edit_formation.py b/sco_edit_formation.py
new file mode 100644
index 0000000000000000000000000000000000000000..27d8d355bb5a4173ca9a678f37c7c68930e4b912
--- /dev/null
+++ b/sco_edit_formation.py
@@ -0,0 +1,250 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Ajout/Modification/Supression formations
+(portage from DTML)
+"""
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+import sco_codes_parcours
+import sco_formsemestre
+
+
+def formation_delete(context, formation_id=None, dialog_confirmed=False, REQUEST=None):
+    """Delete a formation
+    """
+    F = context.formation_list(args={"formation_id": formation_id})
+    if not F:
+        raise ScoValueError("formation inexistante !")
+    F = F[0]
+
+    H = [
+        context.sco_header(REQUEST, page_title="Suppression d'une formation"),
+        """<h2>Suppression de la formation %(titre)s (%(acronyme)s)</h2>""" % F,
+    ]
+
+    sems = sco_formsemestre.do_formsemestre_list(
+        context, {"formation_id": formation_id}
+    )
+    if sems:
+        H.append(
+            """<p class="warning">Impossible de supprimer cette formation, car les sessions suivantes l'utilisent:</p>
+<ul>"""
+        )
+        for sem in sems:
+            H.append(
+                '<li><a href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>'
+                % sem
+            )
+        H.append('</ul><p><a href="%s">Revenir</a></p>' % REQUEST.URL1)
+    else:
+        if not dialog_confirmed:
+            return context.confirmDialog(
+                """<h2>Confirmer la suppression de la formation %(titre)s (%(acronyme)s) ?</h2>
+    <p><b>Attention:</b> la suppression d'une formation est <b>irréversible</b> et implique la supression de toutes les UE, matières et modules de la formation !
+</p>
+                """
+                % F,
+                REQUEST=REQUEST,
+                OK="Supprimer cette formation",
+                cancel_url=REQUEST.URL1,
+                parameters={"formation_id": formation_id},
+            )
+        else:
+            context.do_formation_delete(F["formation_id"], REQUEST)
+            H.append(
+                """<p>OK, formation supprimée.</p>
+    <p><a class="stdlink" href="%s">continuer</a></p>"""
+                % REQUEST.URL1
+            )
+
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def formation_create(context, REQUEST=None):
+    """Creation d'une formation
+    """
+    return formation_edit(context, create=True, REQUEST=REQUEST)
+
+
+def formation_edit(context, formation_id=None, create=False, REQUEST=None):
+    """Edit or create a formation
+    """
+    if create:
+        H = [
+            context.sco_header(REQUEST, page_title="Création d'une formation"),
+            """<h2>Création d'une formation</h2>
+
+<p class="help">Une "formation" décrit une filière, comme un DUT ou une Licence. La formation se subdivise en unités pédagogiques (UE, matières, modules). Elle peut se diviser en plusieurs semestres (ou sessions), qui seront mis en place séparément.
+</p>
+
+<p>Le <tt>titre</tt> est le nom complet, parfois adapté pour mieux distinguer les modalités ou versions de programme pédagogique. Le <tt>titre_officiel</tt> est le nom complet du diplôme, qui apparaitra sur certains PV de jury de délivrance du diplôme.
+</p>
+""",
+        ]
+        submitlabel = "Créer cette formation"
+        initvalues = {"type_parcours": sco_codes_parcours.DEFAULT_TYPE_PARCOURS}
+        is_locked = False
+    else:
+        # edit an existing formation
+        F = context.formation_list(args={"formation_id": formation_id})
+        if not F:
+            raise ScoValueError("formation inexistante !")
+        initvalues = F[0]
+        is_locked = context.formation_has_locked_sems(formation_id)
+        submitlabel = "Modifier les valeurs"
+        H = [
+            context.sco_header(REQUEST, page_title="Modification d'une formation"),
+            """<h2>Modification de la formation %(acronyme)s</h2>""" % initvalues,
+        ]
+        if is_locked:
+            H.append(
+                '<p class="warning">Attention: Formation verrouillée, le type de parcours ne peut être modifié.</p>'
+            )
+
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("formation_id", {"default": formation_id, "input_type": "hidden"}),
+            (
+                "acronyme",
+                {
+                    "size": 12,
+                    "explanation": "identifiant de la formation (par ex. DUT R&T)",
+                    "allow_null": False,
+                },
+            ),
+            (
+                "titre",
+                {
+                    "size": 80,
+                    "explanation": "nom complet de la formation (ex: DUT Réseaux et Télécommunications",
+                    "allow_null": False,
+                },
+            ),
+            (
+                "titre_officiel",
+                {
+                    "size": 80,
+                    "explanation": "nom officiel (pour les PV de jury)",
+                    "allow_null": False,
+                },
+            ),
+            (
+                "type_parcours",
+                {
+                    "input_type": "menu",
+                    "title": "Type de parcours",
+                    "type": "int",
+                    "allowed_values": sco_codes_parcours.FORMATION_PARCOURS_TYPES,
+                    "labels": sco_codes_parcours.FORMATION_PARCOURS_DESCRS,
+                    "explanation": "détermine notamment le nombre de semestres et les règles de validation d'UE et de semestres (barres)",
+                    "readonly": is_locked,
+                },
+            ),
+            (
+                "formation_code",
+                {
+                    "size": 12,
+                    "title": "Code formation",
+                    "explanation": "code interne. Toutes les formations partageant le même code sont compatibles (compensation de semestres, capitalisation d'UE).  Laisser vide si vous ne savez pas, ou entrer le code d'une formation existante.",
+                },
+            ),
+            (
+                "code_specialite",
+                {
+                    "size": 12,
+                    "title": "Code spécialité",
+                    "explanation": "optionel: code utilisé pour échanger avec d'autres logiciels et identifiant la filière ou spécialité (exemple: ASUR). N'est utilisé que s'il n'y a pas de numéro de semestre.",
+                },
+            ),
+        ),
+        initvalues=initvalues,
+        submitlabel=submitlabel,
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+    else:
+        # check unicity : constraint UNIQUE(acronyme,titre,version)
+        if create:
+            version = 1
+        else:
+            version = initvalues["version"]
+        args = {
+            "acronyme": tf[2]["acronyme"],
+            "titre": tf[2]["titre"],
+            "version": version,
+        }
+        quote_dict(args)
+        others = context.formation_list(args=args)
+        if others and ((len(others) > 1) or others[0]["formation_id"] != formation_id):
+            return (
+                "\n".join(H)
+                + tf_error_message(
+                    "Valeurs incorrectes: il existe déjà une formation avec même titre, acronyme et version."
+                )
+                + tf[1]
+                + context.sco_footer(REQUEST)
+            )
+        #
+        if create:
+            formation_id = context.do_formation_create(tf[2], REQUEST)
+        else:
+            do_formation_edit(context, tf[2])
+        return REQUEST.RESPONSE.redirect("ue_list?formation_id=%s" % formation_id)
+
+
+def do_formation_edit(context, args):
+    "edit a formation"
+    # log('do_formation_edit( args=%s )'%args)
+
+    # On autorise  la modif de la formation meme si elle est verrouillee
+    # car cela ne change que du cosmetique, (sauf eventuellement le code formation ?)
+    # mais si verrouillée on ne peut changer le type de parcours
+    if context.formation_has_locked_sems(args["formation_id"]):
+        if args.has_key("type_parcours"):
+            del args["type_parcours"]
+    # On ne peut jamais supprimer le code formation:
+    if args.has_key("formation_code") and not args["formation_code"]:
+        del args["formation_code"]
+
+    cnx = context.GetDBConnexion()
+    context._formationEditor.edit(cnx, args)
+
+    # Invalide les semestres utilisant cette formation:
+    for sem in sco_formsemestre.do_formsemestre_list(
+        context, args={"formation_id": args["formation_id"]}
+    ):
+        context._inval_cache(
+            formsemestre_id=sem["formsemestre_id"]
+        )  # > formation modif.
diff --git a/sco_edit_matiere.py b/sco_edit_matiere.py
new file mode 100644
index 0000000000000000000000000000000000000000..8af7cd20289b5f5d0f8a560b5e77a69e72faefca
--- /dev/null
+++ b/sco_edit_matiere.py
@@ -0,0 +1,214 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Ajout/Modification/Supression matieres
+(portage from DTML)
+"""
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+import sco_formsemestre
+
+
+def matiere_create(context, ue_id=None, REQUEST=None):
+    """Creation d'une matiere
+    """
+    UE = context.do_ue_list(args={"ue_id": ue_id})[0]
+    H = [
+        context.sco_header(REQUEST, page_title="Création d'une matière"),
+        """<h2>Création d'une matière dans l'UE %(titre)s (%(acronyme)s)</h2>""" % UE,
+        """<p class="help">Les matières sont des groupes de modules dans une UE
+d'une formation donnée. Les matières servent surtout pour la
+présentation (bulletins, etc) mais <em>n'ont pas de rôle dans le calcul
+des notes.</em>
+</p> 
+
+<p class="help">Si votre formation n'utilise pas la notion de
+"matières", créez une matière par UE, et donnez lui le même nom que l'UE
+(en effet, tout module doit appartenir à une matière).
+</p>
+
+<p class="help">Comme les UE, les matières n'ont pas de coefficient
+associé.
+</p>""",
+    ]
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("ue_id", {"input_type": "hidden", "default": ue_id}),
+            ("titre", {"size": 30, "explanation": "nom de la matière."}),
+            (
+                "numero",
+                {
+                    "size": 2,
+                    "explanation": "numéro (1,2,3,4...) pour affichage",
+                    "type": "int",
+                },
+            ),
+        ),
+        submitlabel="Créer cette matière",
+    )
+
+    dest_url = REQUEST.URL1 + "/ue_list?formation_id=" + UE["formation_id"]
+
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(dest_url)
+    else:
+        # check unicity
+        mats = context.do_matiere_list(args={"ue_id": ue_id, "titre": tf[2]["titre"]})
+        if mats:
+            return (
+                "\n".join(H)
+                + tf_error_message("Titre de matière déjà existant dans cette UE")
+                + tf[1]
+                + context.sco_footer(REQUEST)
+            )
+        matiere_id = context.do_matiere_create(tf[2], REQUEST)
+        return REQUEST.RESPONSE.redirect(dest_url)
+
+
+def matiere_delete(context, matiere_id=None, REQUEST=None):
+    """Delete an UE"""
+    M = context.do_matiere_list(args={"matiere_id": matiere_id})[0]
+    UE = context.do_ue_list(args={"ue_id": M["ue_id"]})[0]
+    H = [
+        context.sco_header(REQUEST, page_title="Suppression d'une matière"),
+        "<h2>Suppression de la matière %(titre)s" % M,
+        " dans l'UE (%(acronyme)s))</h2>" % UE,
+    ]
+
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (("matiere_id", {"input_type": "hidden"}),),
+        initvalues=M,
+        submitlabel="Confirmer la suppression",
+        cancelbutton="Annuler",
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+    else:
+        context.do_matiere_delete(matiere_id, REQUEST)
+        return REQUEST.RESPONSE.redirect(
+            REQUEST.URL1 + "/ue_list?formation_id=" + str(UE["formation_id"])
+        )
+
+
+def matiere_edit(context, matiere_id=None, REQUEST=None):
+    """Edit matiere"""
+    F = context.do_matiere_list(args={"matiere_id": matiere_id})
+    if not F:
+        raise ScoValueError("Matière inexistante !")
+    F = F[0]
+    U = context.do_ue_list(args={"ue_id": F["ue_id"]})
+    if not F:
+        raise ScoValueError("UE inexistante !")
+    U = U[0]
+    Fo = context.formation_list(args={"formation_id": U["formation_id"]})[0]
+
+    ues = context.do_ue_list(args={"formation_id": U["formation_id"]})
+    ue_names = ["%(acronyme)s (%(titre)s)" % u for u in ues]
+    ue_ids = [u["ue_id"] for u in ues]
+    H = [
+        context.sco_header(REQUEST, page_title="Modification d'une matière"),
+        """<h2>Modification de la matière %(titre)s""" % F,
+        """(formation %(acronyme)s, version %(version)s)</h2>""" % Fo,
+    ]
+    help = """<p class="help">Les matières sont des groupes de modules dans une UE
+d'une formation donnée. Les matières servent surtout pour la
+présentation (bulletins, etc) mais <em>n'ont pas de rôle dans le calcul
+des notes.</em>
+</p> 
+
+<p class="help">Si votre formation n'utilise pas la notion de
+"matières", créez une matière par UE, et donnez lui le même nom que l'UE
+(en effet, tout module doit appartenir à une matière).
+</p>
+
+<p class="help">Comme les UE, les matières n'ont pas de coefficient
+associé.
+</p>"""
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("matiere_id", {"input_type": "hidden"}),
+            (
+                "ue_id",
+                {"input_type": "menu", "allowed_values": ue_ids, "labels": ue_names},
+            ),
+            ("titre", {"size": 30, "explanation": "nom de cette matière"}),
+            (
+                "numero",
+                {
+                    "size": 2,
+                    "explanation": "numéro (1,2,3,4...) pour affichage",
+                    "type": "int",
+                },
+            ),
+        ),
+        initvalues=F,
+        submitlabel="Modifier les valeurs",
+    )
+
+    dest_url = REQUEST.URL1 + "/ue_list?formation_id=" + U["formation_id"]
+
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + help + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(dest_url)
+    else:
+        # check unicity
+        mats = context.do_matiere_list(
+            args={"ue_id": tf[2]["ue_id"], "titre": tf[2]["titre"]}
+        )
+        if len(mats) > 1 or (len(mats) == 1 and mats[0]["matiere_id"] != matiere_id):
+            return (
+                "\n".join(H)
+                + tf_error_message("Titre de matière déjà existant dans cette UE")
+                + tf[1]
+                + context.sco_footer(REQUEST)
+            )
+
+        # changement d'UE ?
+        if tf[2]["ue_id"] != F["ue_id"]:
+            log("attaching mat %s to new UE %s" % (matiere_id, tf[2]["ue_id"]))
+            SimpleQuery(
+                context,
+                "UPDATE notes_modules SET ue_id = %(ue_id)s WHERE matiere_id=%(matiere_id)s",
+                {"ue_id": tf[2]["ue_id"], "matiere_id": matiere_id},
+            )
+
+        context.do_matiere_edit(tf[2])
+
+        return REQUEST.RESPONSE.redirect(dest_url)
diff --git a/sco_edit_module.py b/sco_edit_module.py
new file mode 100644
index 0000000000000000000000000000000000000000..d22e5accb10caa2b5a9d2890a8eb892ba79987b5
--- /dev/null
+++ b/sco_edit_module.py
@@ -0,0 +1,501 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Ajout/Modification/Supression UE
+(portage from DTML)
+"""
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+import sco_codes_parcours
+from TrivialFormulator import TrivialFormulator, TF
+import sco_formsemestre
+import sco_edit_ue
+import sco_tag_module
+
+_MODULE_HELP = """<p class="help">
+Les modules sont décrits dans le programme pédagogique. Un module est pour ce 
+logiciel l'unité pédagogique élémentaire. On va lui associer une note 
+à travers des <em>évaluations</em>. <br/>
+Cette note (moyenne de module) sera utilisée pour calculer la moyenne
+générale (et la moyenne de l'UE à laquelle appartient le module). Pour
+cela, on utilisera le <em>coefficient</em> associé au module.
+</p>
+
+<p class="help">Un module possède un enseignant responsable
+(typiquement celui qui dispense le cours magistral). On peut associer
+au module une liste d'enseignants (typiquement les chargés de TD).
+Tous ces enseignants, plus le responsable du semestre, pourront
+saisir et modifier les notes de ce module.
+</p> """
+
+
+def module_create(context, matiere_id=None, REQUEST=None):
+    """Creation d'un module
+    """
+    if not matiere_id:
+        raise ScoValueError("invalid matiere !")
+    M = context.do_matiere_list(args={"matiere_id": matiere_id})[0]
+    UE = context.do_ue_list(args={"ue_id": M["ue_id"]})[0]
+    Fo = context.formation_list(args={"formation_id": UE["formation_id"]})[0]
+    parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
+    semestres_indices = range(1, parcours.NB_SEM + 1)
+    H = [
+        context.sco_header(REQUEST, page_title="Création d'un module"),
+        """<h2>Création d'un module dans la matière %(titre)s""" % M,
+        """ (UE %(acronyme)s)</h2>""" % UE,
+        _MODULE_HELP,
+    ]
+    # cherche le numero adequat (pour placer le module en fin de liste)
+    Mods = context.do_module_list(args={"matiere_id": matiere_id})
+    if Mods:
+        default_num = max([m["numero"] for m in Mods]) + 10
+    else:
+        default_num = 10
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            (
+                "code",
+                {
+                    "size": 10,
+                    "explanation": "code du module (doit être unique dans la formation)",
+                    "allow_null": False,
+                    "validator": lambda val, field, formation_id=Fo[
+                        "formation_id"
+                    ]: check_module_code_unicity(val, field, formation_id, context),
+                },
+            ),
+            ("titre", {"size": 30, "explanation": "nom du module"}),
+            ("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
+            (
+                "module_type",
+                {
+                    "input_type": "menu",
+                    "title": "Type",
+                    "explanation": "",
+                    "labels": ("Standard", "Malus"),
+                    "allowed_values": (str(MODULE_STANDARD), str(MODULE_MALUS)),
+                },
+            ),
+            (
+                "heures_cours",
+                {"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
+            ),
+            (
+                "heures_td",
+                {
+                    "size": 4,
+                    "type": "float",
+                    "explanation": "nombre d'heures de Travaux Dirigés",
+                },
+            ),
+            (
+                "heures_tp",
+                {
+                    "size": 4,
+                    "type": "float",
+                    "explanation": "nombre d'heures de Travaux Pratiques",
+                },
+            ),
+            (
+                "coefficient",
+                {
+                    "size": 4,
+                    "type": "float",
+                    "explanation": "coefficient dans la formation (PPN)",
+                    "allow_null": False,
+                },
+            ),
+            # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
+            ("formation_id", {"default": UE["formation_id"], "input_type": "hidden"}),
+            ("ue_id", {"default": M["ue_id"], "input_type": "hidden"}),
+            ("matiere_id", {"default": M["matiere_id"], "input_type": "hidden"}),
+            (
+                "semestre_id",
+                {
+                    "input_type": "menu",
+                    "type": "int",
+                    "title": strcapitalize(parcours.SESSION_NAME),
+                    "explanation": "%s de début du module dans la formation standard"
+                    % parcours.SESSION_NAME,
+                    "labels": [str(x) for x in semestres_indices],
+                    "allowed_values": semestres_indices,
+                },
+            ),
+            (
+                "code_apogee",
+                {
+                    "title": "Code Apogée",
+                    "size": 15,
+                    "explanation": "code élément pédagogique Apogée (optionnel)",
+                },
+            ),
+            (
+                "numero",
+                {
+                    "size": 2,
+                    "explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
+                    "type": "int",
+                    "default": default_num,
+                },
+            ),
+        ),
+        submitlabel="Créer ce module",
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + context.sco_footer(REQUEST)
+    else:
+        moduleid = context.do_module_create(tf[2], REQUEST)
+        return REQUEST.RESPONSE.redirect(
+            REQUEST.URL1 + "/ue_list?formation_id=" + UE["formation_id"]
+        )
+
+
+def module_delete(context, module_id=None, REQUEST=None):
+    """Delete a module"""
+    if not module_id:
+        raise ScoValueError("invalid module !")
+    Mods = context.do_module_list(args={"module_id": module_id})
+    if not Mods:
+        raise ScoValueError("Module inexistant !")
+    Mod = Mods[0]
+    H = [
+        context.sco_header(REQUEST, page_title="Suppression d'un module"),
+        """<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % Mod,
+    ]
+
+    dest_url = REQUEST.URL1 + "/ue_list?formation_id=" + Mod["formation_id"]
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (("module_id", {"input_type": "hidden"}),),
+        initvalues=Mod,
+        submitlabel="Confirmer la suppression",
+        cancelbutton="Annuler",
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(dest_url)
+    else:
+        context.do_module_delete(module_id, REQUEST)
+        return REQUEST.RESPONSE.redirect(dest_url)
+
+
+def check_module_code_unicity(code, field, formation_id, context, module_id=None):
+    "true si code module unique dans la formation"
+    Mods = context.do_module_list(args={"code": code, "formation_id": formation_id})
+    if module_id:  # edition: supprime le module en cours
+        Mods = [m for m in Mods if m["module_id"] != module_id]
+
+    return len(Mods) == 0
+
+
+def module_edit(context, module_id=None, REQUEST=None):
+    """Edit a module"""
+    if not module_id:
+        raise ScoValueError("invalid module !")
+    Mod = context.do_module_list(args={"module_id": module_id})
+    if not Mod:
+        raise ScoValueError("invalid module !")
+    Mod = Mod[0]
+    unlocked = not context.module_is_locked(module_id)
+    Fo = context.formation_list(args={"formation_id": Mod["formation_id"]})[0]
+    parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
+    M = SimpleDictFetch(
+        context,
+        "SELECT ue.acronyme, mat.* FROM notes_matieres mat, notes_ue ue WHERE mat.ue_id = ue.ue_id AND ue.formation_id = %(formation_id)s ORDER BY ue.numero, mat.numero",
+        {"formation_id": Mod["formation_id"]},
+    )
+    Mnames = ["%s / %s" % (x["acronyme"], x["titre"]) for x in M]
+    Mids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in M]
+    Mod["ue_matiere_id"] = "%s!%s" % (Mod["ue_id"], Mod["matiere_id"])
+
+    semestres_indices = range(1, parcours.NB_SEM + 1)
+
+    dest_url = REQUEST.URL1 + "/ue_list?formation_id=" + Mod["formation_id"]
+
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Modification du module %(titre)s" % Mod,
+            cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
+            javascripts=[
+                "libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
+                "libjs/jQuery-tagEditor/jquery.caret.min.js",
+                "js/module_tag_editor.js",
+            ],
+        ),
+        """<h2>Modification du module %(titre)s""" % Mod,
+        """ (formation %(acronyme)s, version %(version)s)</h2>""" % Fo,
+        _MODULE_HELP,
+    ]
+    if not unlocked:
+        H.append(
+            """<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>"""
+        )
+
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            (
+                "code",
+                {
+                    "size": 10,
+                    "explanation": "code du module (doit être unique dans la formation)",
+                    "allow_null": False,
+                    "validator": lambda val, field, formation_id=Mod[
+                        "formation_id"
+                    ]: check_module_code_unicity(
+                        val, field, formation_id, context, module_id=module_id
+                    ),
+                },
+            ),
+            ("titre", {"size": 30, "explanation": "nom du module"}),
+            ("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
+            (
+                "module_type",
+                {
+                    "input_type": "menu",
+                    "title": "Type",
+                    "explanation": "",
+                    "labels": ("Standard", "Malus"),
+                    "allowed_values": (str(MODULE_STANDARD), str(MODULE_MALUS)),
+                    "enabled": unlocked,
+                },
+            ),
+            (
+                "heures_cours",
+                {"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
+            ),
+            (
+                "heures_td",
+                {
+                    "size": 4,
+                    "type": "float",
+                    "explanation": "nombre d'heures de Travaux Dirigés",
+                },
+            ),
+            (
+                "heures_tp",
+                {
+                    "size": 4,
+                    "type": "float",
+                    "explanation": "nombre d'heures de Travaux Pratiques",
+                },
+            ),
+            (
+                "coefficient",
+                {
+                    "size": 4,
+                    "type": "float",
+                    "explanation": "coefficient dans la formation (PPN)",
+                    "allow_null": False,
+                    "enabled": unlocked,
+                },
+            ),
+            # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS',  'enabled' : unlocked }),
+            ("formation_id", {"input_type": "hidden"}),
+            ("ue_id", {"input_type": "hidden"}),
+            ("module_id", {"input_type": "hidden"}),
+            (
+                "ue_matiere_id",
+                {
+                    "input_type": "menu",
+                    "title": "Matière",
+                    "explanation": "un module appartient à une seule matière.",
+                    "labels": Mnames,
+                    "allowed_values": Mids,
+                    "enabled": unlocked,
+                },
+            ),
+            (
+                "semestre_id",
+                {
+                    "input_type": "menu",
+                    "type": "int",
+                    "title": parcours.SESSION_NAME.capitalize(),
+                    "explanation": "%s de début du module dans la formation standard"
+                    % parcours.SESSION_NAME,
+                    "labels": [str(x) for x in semestres_indices],
+                    "allowed_values": semestres_indices,
+                    "enabled": unlocked,
+                },
+            ),
+            (
+                "code_apogee",
+                {
+                    "title": "Code Apogée",
+                    "size": 15,
+                    "explanation": "code élément pédagogique Apogée (optionnel)",
+                },
+            ),
+            (
+                "numero",
+                {
+                    "size": 2,
+                    "explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
+                    "type": "int",
+                },
+            ),
+        ),
+        html_foot_markup="""<div style="width: 90%;"><span class="sco_tag_edit"><textarea data-module_id="{}" class="module_tag_editor">{}</textarea></span></div>""".format(
+            module_id, ",".join(sco_tag_module.module_tag_list(context, module_id))
+        ),
+        initvalues=Mod,
+        submitlabel="Modifier ce module",
+    )
+
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(dest_url)
+    else:
+        # l'UE peut changer
+        tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
+        # Check unicité code module dans la formation
+
+        context.do_module_edit(tf[2])
+        return REQUEST.RESPONSE.redirect(dest_url)
+
+
+# Edition en ligne du code Apogee
+def edit_module_set_code_apogee(context, id=None, value=None, REQUEST=None):
+    "set UE code apogee"
+    module_id = id
+    value = value.strip("-_ \t")
+    log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value))
+
+    modules = context.do_module_list(args={"module_id": module_id})
+    if not modules:
+        return "module invalide"  # shoud not occur
+    module = modules[0]
+
+    context.do_module_edit({"module_id": module_id, "code_apogee": value})
+    if not value:
+        value = APO_MISSING_CODE_STR
+    return value
+
+
+def module_list(context, formation_id, REQUEST=None):
+    """Liste des modules de la formation
+    (XXX inutile ou a revoir)
+    """
+    if not formation_id:
+        raise ScoValueError("invalid formation !")
+    F = context.formation_list(args={"formation_id": formation_id})[0]
+    H = [
+        context.sco_header(REQUEST, page_title="Liste des modules de %(titre)s" % F),
+        """<h2>Listes des modules dans la formation %(titre)s (%(acronyme)s)</h2>"""
+        % F,
+        '<ul class="notes_module_list">',
+    ]
+    editable = REQUEST.AUTHENTICATED_USER.has_permission(ScoChangeFormation, context)
+
+    for Mod in context.do_module_list(args={"formation_id": formation_id}):
+        H.append('<li class="notes_module_list">%s' % Mod)
+        if editable:
+            H.append('<a href="module_edit?module_id=%(module_id)s">modifier</a>' % Mod)
+            H.append(
+                '<a href="module_delete?module_id=%(module_id)s">supprimer</a>' % Mod
+            )
+        H.append("</li>")
+    H.append("</ul>")
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+#
+
+
+def formation_add_malus_modules(context, formation_id, titre=None, REQUEST=None):
+    """Création d'un module de "malus" dans chaque UE d'une formation
+    """
+    ue_list = context.do_ue_list(args={"formation_id": formation_id})
+
+    for ue in ue_list:
+        # Un seul module de malus par UE:
+        nb_mod_malus = len(
+            [
+                mod
+                for mod in context.do_module_list(args={"ue_id": ue["ue_id"]})
+                if mod["module_type"] == MODULE_MALUS
+            ]
+        )
+        if nb_mod_malus == 0:
+            ue_add_malus_module(context, ue["ue_id"], titre=titre, REQUEST=REQUEST)
+
+    if REQUEST:
+        REQUEST.RESPONSE.redirect("ue_list?formation_id=" + formation_id)
+
+
+def ue_add_malus_module(context, ue_id, titre=None, code=None, REQUEST=None):
+    """Add a malus module in this ue
+    """
+    ue = context.do_ue_list(args={"ue_id": ue_id})[0]
+
+    if titre is None:
+        titre = ""
+    if code is None:
+        code = "MALUS%d" % ue["numero"]
+
+    # Tout module doit avoir un semestre_id (indice 1, 2, ...)
+    semestre_ids = sco_edit_ue.ue_list_semestre_ids(context, ue)
+    if semestre_ids:
+        semestre_id = semestre_ids[0]
+    else:
+        # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement
+        # le semestre ? ou affecter le malus au semestre 1 ???
+        raise ScoValueError(
+            "Impossible d'ajouter un malus s'il n'y a pas d'autres modules"
+        )
+
+    # Matiere pour placer le module malus
+    Matlist = context.do_matiere_list(args={"ue_id": ue_id})
+    numero = max([mat["numero"] for mat in Matlist]) + 10
+    matiere_id = context.do_matiere_create(
+        {"ue_id": ue_id, "titre": "Malus", "numero": numero}, REQUEST
+    )
+
+    module_id = context.do_module_create(
+        {
+            "titre": titre,
+            "code": code,
+            "coefficient": 0.0,  # unused
+            "ue_id": ue_id,
+            "matiere_id": matiere_id,
+            "formation_id": ue["formation_id"],
+            "semestre_id": semestre_id,
+            "module_type": MODULE_MALUS,
+        },
+        REQUEST,
+    )
+
+    return module_id
diff --git a/sco_edit_ue.py b/sco_edit_ue.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3986e254b14cda5304f525d0be15855b0ee00ba
--- /dev/null
+++ b/sco_edit_ue.py
@@ -0,0 +1,871 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Ajout/Modification/Supression UE
+
+"""
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+from gen_tables import GenTable
+import sco_groups
+import sco_formsemestre
+import sco_formsemestre_validation
+import sco_codes_parcours
+import sco_tag_module
+
+
+def ue_create(context, formation_id=None, REQUEST=None):
+    """Creation d'une UE
+    """
+    return ue_edit(context, create=True, formation_id=formation_id, REQUEST=REQUEST)
+
+
+def ue_edit(context, ue_id=None, create=False, formation_id=None, REQUEST=None):
+    """Modification ou creation d'une UE    
+    """
+    create = int(create)
+    if not create:
+        U = context.do_ue_list(args={"ue_id": ue_id})
+        if not U:
+            raise ScoValueError("UE inexistante !")
+        U = U[0]
+        formation_id = U["formation_id"]
+        title = "Modification de l'UE %(titre)s" % U
+        initvalues = U
+        submitlabel = "Modifier les valeurs"
+    else:
+        title = "Création d'une UE"
+        initvalues = {}
+        submitlabel = "Créer cette UE"
+    Fol = context.formation_list(args={"formation_id": formation_id})
+    if not Fol:
+        raise ScoValueError(
+            "Formation %s inexistante ! (si vous avez suivi un lien valide, merci de signaler le problème)"
+            % formation_id
+        )
+    Fo = Fol[0]
+    parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
+
+    H = [
+        context.sco_header(REQUEST, page_title=title, javascripts=["js/edit_ue.js"]),
+        "<h2>" + title,
+        " (formation %(acronyme)s, version %(version)s)</h2>" % Fo,
+        """
+<p class="help">Les UE sont des groupes de modules dans une formation donnée, utilisés pour l'évaluation (on calcule des moyennes par UE et applique des seuils ("barres")). 
+</p>
+
+<p class="help">Note: L'UE n'a pas de coefficient associé. Seuls les <em>modules</em> ont des coefficients.
+</p>""",
+    ]
+
+    ue_types = parcours.ALLOWED_UE_TYPES
+    ue_types.sort()
+    ue_types_names = [UE_TYPE_NAME[k] for k in ue_types]
+    ue_types = [str(x) for x in ue_types]
+
+    fw = [
+        ("ue_id", {"input_type": "hidden"}),
+        ("create", {"input_type": "hidden", "default": create}),
+        ("formation_id", {"input_type": "hidden", "default": formation_id}),
+        ("titre", {"size": 30, "explanation": "nom de l'UE"}),
+        ("acronyme", {"size": 8, "explanation": "abbréviation", "allow_null": False}),
+        (
+            "numero",
+            {
+                "size": 2,
+                "explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage",
+                "type": "int",
+            },
+        ),
+        (
+            "type",
+            {
+                "explanation": "type d'UE",
+                "input_type": "menu",
+                "allowed_values": ue_types,
+                "labels": ue_types_names,
+            },
+        ),
+        (
+            "ects",
+            {
+                "size": 4,
+                "type": "float",
+                "title": "ECTS",
+                "explanation": "nombre de crédits ECTS",
+            },
+        ),
+        (
+            "coefficient",
+            {
+                "size": 4,
+                "type": "float",
+                "title": "Coefficient",
+                "explanation": """les coefficients d'UE ne sont utilisés que lorsque 
+                l'option <em>Utiliser les coefficients d'UE pour calculer la moyenne générale</em>
+                est activée. Par défaut, le coefficient d'une UE est simplement la somme des 
+                coefficients des modules dans lesquels l'étudiant a des notes.
+                """,
+            },
+        ),
+        (
+            "ue_code",
+            {
+                "size": 12,
+                "title": "Code UE",
+                "explanation": "code interne (optionnel). Toutes les UE partageant le même code (et le même code de formation) sont compatibles (compensation de semestres, capitalisation d'UE). Voir liste ci-dessous.",
+            },
+        ),
+        (
+            "code_apogee",
+            {
+                "title": "Code Apogée",
+                "size": 15,
+                "explanation": "code élément pédagogique Apogée (optionnel)",
+            },
+        ),
+    ]
+    if parcours.UE_IS_MODULE:
+        # demande le semestre pour creer le module immediatement:
+        semestres_indices = range(1, parcours.NB_SEM + 1)
+        fw.append(
+            (
+                "semestre_id",
+                {
+                    "input_type": "menu",
+                    "type": "int",
+                    "title": strcapitalize(parcours.SESSION_NAME),
+                    "explanation": "%s de début du module dans la formation"
+                    % parcours.SESSION_NAME,
+                    "labels": [str(x) for x in semestres_indices],
+                    "allowed_values": semestres_indices,
+                },
+            )
+        )
+    if create and not parcours.UE_IS_MODULE:
+        fw.append(
+            (
+                "create_matiere",
+                {
+                    "input_type": "boolcheckbox",
+                    "default": False,
+                    "title": "Créer matière identique",
+                    "explanation": "créer immédiatement une matière dans cette UE (utile si on n'utilise pas de matières)",
+                },
+            )
+        )
+    tf = TrivialFormulator(
+        REQUEST.URL0, REQUEST.form, fw, initvalues=initvalues, submitlabel=submitlabel
+    )
+    if tf[0] == 0:
+        X = """<div id="ue_list_code"></div>
+        """
+        return "\n".join(H) + tf[1] + X + context.sco_footer(REQUEST)
+    else:
+        if create:
+            if not tf[2]["ue_code"]:
+                del tf[2]["ue_code"]
+            if not tf[2]["numero"]:
+                if not "semestre_id" in tf[2]:
+                    tf[2]["semestre_id"] = 0
+                # numero regroupant par semestre ou année:
+                tf[2]["numero"] = next_ue_numero(
+                    context, formation_id, int(tf[2]["semestre_id"] or 0)
+                )
+
+            ue_id = context.do_ue_create(tf[2], REQUEST)
+            if parcours.UE_IS_MODULE or tf[2]["create_matiere"]:
+                matiere_id = context.do_matiere_create(
+                    {"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1}, REQUEST
+                )
+            if parcours.UE_IS_MODULE:
+                # dans ce mode, crée un (unique) module dans l'UE:
+                module_id = context.do_module_create(
+                    {
+                        "titre": tf[2]["titre"],
+                        "code": tf[2]["acronyme"],
+                        "coefficient": 1.0,  # tous les modules auront coef 1, et on utilisera les ECTS
+                        "ue_id": ue_id,
+                        "matiere_id": matiere_id,
+                        "formation_id": formation_id,
+                        "semestre_id": tf[2]["semestre_id"],
+                    },
+                    REQUEST,
+                )
+        else:
+            ue_id = do_ue_edit(context, tf[2])
+        return REQUEST.RESPONSE.redirect(
+            REQUEST.URL1 + "/ue_list?formation_id=" + formation_id
+        )
+
+
+def _add_ue_semestre_id(context, ue_list):
+    """ajoute semestre_id dans les ue, en regardant le premier module de chacune.
+    Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000), 
+    qui les place à la fin de la liste.
+    """
+    for ue in ue_list:
+        Modlist = context.do_module_list(args={"ue_id": ue["ue_id"]})
+        if Modlist:
+            ue["semestre_id"] = Modlist[0]["semestre_id"]
+        else:
+            ue["semestre_id"] = 1000000
+
+
+def next_ue_numero(context, formation_id, semestre_id=None):
+    """Numero d'une nouvelle UE dans cette formation.
+    Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
+    """
+    ue_list = context.do_ue_list(args={"formation_id": formation_id})
+    if not ue_list:
+        return 0
+    if semestre_id is None:
+        return ue_list[-1]["numero"] + 1000
+    else:
+        # Avec semestre: (prend le semestre du 1er module de l'UE)
+        _add_ue_semestre_id(context, ue_list)
+        ue_list_semestre = [ue for ue in ue_list if ue["semestre_id"] == semestre_id]
+        if ue_list_semestre:
+            return ue_list_semestre[-1]["numero"] + 10
+        else:
+            return ue_list[-1]["numero"] + 1000
+
+
+def ue_delete(
+    context, ue_id=None, delete_validations=False, dialog_confirmed=False, REQUEST=None
+):
+    """Delete an UE"""
+    ue = context.do_ue_list(args={"ue_id": ue_id})
+    if not ue:
+        raise ScoValueError("UE inexistante !")
+    ue = ue[0]
+
+    if not dialog_confirmed:
+        return context.confirmDialog(
+            "<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue,
+            dest_url="",
+            REQUEST=REQUEST,
+            parameters={"ue_id": ue_id},
+            cancel_url="ue_list?formation_id=%s" % ue["formation_id"],
+        )
+
+    return context._do_ue_delete(
+        ue_id, delete_validations=delete_validations, REQUEST=REQUEST
+    )
+
+
+def ue_list(context, formation_id=None, msg="", REQUEST=None):
+    """Liste des matières et modules d'une formation, avec liens pour 
+    editer (si non verrouillée).
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+
+    F = context.formation_list(args={"formation_id": formation_id})
+    if not F:
+        raise ScoValueError("invalid formation_id")
+    F = F[0]
+    parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
+    locked = context.formation_has_locked_sems(formation_id)
+
+    ue_list = context.do_ue_list(args={"formation_id": formation_id})
+    # tri par semestre et numero:
+    _add_ue_semestre_id(context, ue_list)
+    ue_list.sort(key=lambda u: (u["semestre_id"], u["numero"]))
+    has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ue_list])) != len(ue_list)
+
+    perm_change = authuser.has_permission(ScoChangeFormation, context)
+    # editable = (not locked) and perm_change
+    # On autorise maintanant la modification des formations qui ont des semestres verrouillés,
+    # sauf si cela affect les notes passées (verrouillées):
+    #   - pas de modif des modules utilisés dans des semestres verrouillés
+    #   - pas de changement des codes d'UE utilisés dans des semestres verrouillés
+    editable = perm_change
+    tag_editable = authuser.has_permission(ScoEditFormationTags, context) or perm_change
+    if locked:
+        lockicon = icontag("lock32_img", title="verrouillé")
+    else:
+        lockicon = ""
+
+    arrow_up, arrow_down, arrow_none = sco_groups.getArrowIconsTags(context, REQUEST)
+    delete_icon = icontag(
+        "delete_small_img", title="Supprimer (module inutilisé)", alt="supprimer"
+    )
+    delete_disabled_icon = icontag(
+        "delete_small_dis_img", title="Suppression impossible (module utilisé)"
+    )
+    H = [
+        context.sco_header(
+            REQUEST,
+            cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
+            javascripts=[
+                "libjs/jinplace-1.2.1.min.js",
+                "js/ue_list.js",
+                "libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
+                "libjs/jQuery-tagEditor/jquery.caret.min.js",
+                "js/module_tag_editor.js",
+            ],
+            page_title="Programme %s" % F["acronyme"],
+        ),
+        """<h2>Formation %(titre)s (%(acronyme)s) [version %(version)s] code %(formation_code)s"""
+        % F,
+        lockicon,
+        "</h2>",
+    ]
+    if locked:
+        H.append(
+            """<p class="help">Cette formation est verrouillée car %d semestres verrouillés s'y réferent.
+Si vous souhaitez modifier cette formation (par exemple pour y ajouter un module), vous devez:
+</p>
+<ul class="help">
+<li>soit créer une nouvelle version de cette formation pour pouvoir l'éditer librement (vous pouvez passer par la fonction "Associer à une nouvelle version du programme" (menu "Semestre") si vous avez un semestre en cours);</li>
+<li>soit déverrouiller le ou les semestres qui s'y réfèrent (attention, en principe ces semestres sont archivés 
+    et ne devraient pas être modifiés).</li>
+</ul>"""
+            % len(locked)
+        )
+    if msg:
+        H.append('<p class="msg">' + msg + "</p>")
+
+    if has_duplicate_ue_codes:
+        H.append(
+            """<div class="ue_warning"><span>Attention: plusieurs UE de cette formation ont le même code. Il faut corriger cela ci-dessous, sinon les calculs d'ECTS seront erronés !</span></div>"""
+        )
+
+    # Description de la formation
+    H.append('<div class="formation_descr">')
+    H.append(
+        '<div class="fd_d"><span class="fd_t">Titre:</span><span class="fd_v">%(titre)s</span></div>'
+        % F
+    )
+    H.append(
+        '<div class="fd_d"><span class="fd_t">Titre officiel:</span><span class="fd_v">%(titre_officiel)s</span></div>'
+        % F
+    )
+    H.append(
+        '<div class="fd_d"><span class="fd_t">Acronyme:</span><span class="fd_v">%(acronyme)s</span></div>'
+        % F
+    )
+    H.append(
+        '<div class="fd_d"><span class="fd_t">Code:</span><span class="fd_v">%(formation_code)s</span></div>'
+        % F
+    )
+    H.append(
+        '<div class="fd_d"><span class="fd_t">Version:</span><span class="fd_v">%(version)s</span></div>'
+        % F
+    )
+    H.append(
+        '<div class="fd_d"><span class="fd_t">Type parcours:</span><span class="fd_v">%s</span></div>'
+        % parcours.__doc__
+    )
+    if parcours.UE_IS_MODULE:
+        H.append(
+            '<div class="fd_d"><span class="fd_t"> </span><span class="fd_n">(Chaque module est une UE)</span></div>'
+        )
+
+    if editable:
+        H.append(
+            '<div><a href="formation_edit?formation_id=%(formation_id)s" class="stdlink">modifier ces informations</a></div>'
+            % F
+        )
+
+    H.append("</div>")
+
+    # Description des UE/matières/modules
+    H.append('<div class="formation_ue_list">')
+    H.append('<div class="ue_list_tit">Programme pédagogique:</div>')
+
+    H.append(
+        '<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>'
+    )
+
+    cur_ue_semestre_id = None
+    iue = 0
+    for UE in ue_list:
+        if UE["ects"]:
+            UE["ects_str"] = ", %g ECTS" % UE["ects"]
+        else:
+            UE["ects_str"] = ""
+        if editable:
+            klass = "span_apo_edit"
+        else:
+            klass = ""
+        UE["code_apogee_str"] = (
+            """, Apo: <span class="%s" data-url="edit_ue_set_code_apogee" id="%s" data-placeholder="%s">"""
+            % (klass, UE["ue_id"], APO_MISSING_CODE_STR)
+            + (UE["code_apogee"] or "")
+            + "</span>"
+        )
+
+        if cur_ue_semestre_id != UE["semestre_id"]:
+            cur_ue_semestre_id = UE["semestre_id"]
+            if iue > 0:
+                H.append("</ul>")
+            if UE["semestre_id"] == UE_SEM_DEFAULT:
+                lab = "Pas d'indication de semestre:"
+            else:
+                lab = "Semestre %s:" % UE["semestre_id"]
+            H.append('<div class="ue_list_tit_sem">%s</div>' % lab)
+            H.append('<ul class="notes_ue_list">')
+        H.append('<li class="notes_ue_list">')
+        if iue != 0 and editable:
+            H.append(
+                '<a href="ue_move?ue_id=%s&amp;after=0" class="aud">%s</a>'
+                % (UE["ue_id"], arrow_up)
+            )
+        else:
+            H.append(arrow_none)
+        if iue < len(ue_list) - 1 and editable:
+            H.append(
+                '<a href="ue_move?ue_id=%s&amp;after=1" class="aud">%s</a>'
+                % (UE["ue_id"], arrow_down)
+            )
+        else:
+            H.append(arrow_none)
+        iue += 1
+        UE["acro_titre"] = str(UE["acronyme"])
+        if UE["titre"] != UE["acronyme"]:
+            UE["acro_titre"] += " " + str(UE["titre"])
+        H.append(
+            """%(acro_titre)s <span class="ue_code">(code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s)</span>
+            <span class="ue_coef"></span>
+            """
+            % UE
+        )
+
+        if UE["type"] != UE_STANDARD:
+            H.append('<span class="ue_type">%s</span>' % UE_TYPE_NAME[UE["type"]])
+        ue_editable = editable and not context.ue_is_locked(UE["ue_id"])
+        if ue_editable:
+            H.append(
+                '<a class="stdlink" href="ue_edit?ue_id=%(ue_id)s">modifier</a>' % UE
+            )
+        else:
+            H.append('<span class="locked">[verrouillé]</span>')
+        if not parcours.UE_IS_MODULE:
+            H.append('<ul class="notes_matiere_list">')
+        Matlist = context.do_matiere_list(args={"ue_id": UE["ue_id"]})
+        for Mat in Matlist:
+            if not parcours.UE_IS_MODULE:
+                H.append('<li class="notes_matiere_list">')
+                if editable and not context.matiere_is_locked(Mat["matiere_id"]):
+                    H.append(
+                        '<a class="stdlink" href="matiere_edit?matiere_id=%(matiere_id)s">'
+                        % Mat
+                    )
+                H.append("%(titre)s" % Mat)
+                if editable and not context.matiere_is_locked(Mat["matiere_id"]):
+                    H.append("</a>")
+
+            H.append('<ul class="notes_module_list">')
+            Modlist = context.do_module_list(args={"matiere_id": Mat["matiere_id"]})
+            im = 0
+            for Mod in Modlist:
+                Mod["nb_moduleimpls"] = context.module_count_moduleimpls(
+                    Mod["module_id"]
+                )
+                klass = "notes_module_list"
+                if Mod["module_type"] == MODULE_MALUS:
+                    klass += " module_malus"
+                H.append('<li class="%s">' % klass)
+
+                H.append('<span class="notes_module_list_buts">')
+                if im != 0 and editable:
+                    H.append(
+                        '<a href="module_move?module_id=%s&amp;after=0" class="aud">%s</a>'
+                        % (Mod["module_id"], arrow_up)
+                    )
+                else:
+                    H.append(arrow_none)
+                if im < len(Modlist) - 1 and editable:
+                    H.append(
+                        '<a href="module_move?module_id=%s&amp;after=1" class="aud">%s</a>'
+                        % (Mod["module_id"], arrow_down)
+                    )
+                else:
+                    H.append(arrow_none)
+                im += 1
+                if Mod["nb_moduleimpls"] == 0 and editable:
+                    H.append(
+                        '<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
+                        % (Mod["module_id"], delete_icon)
+                    )
+                else:
+                    H.append(delete_disabled_icon)
+                H.append("</span>")
+
+                mod_editable = (
+                    editable  # and not context.module_is_locked(Mod['module_id'])
+                )
+                if mod_editable:
+                    H.append(
+                        '<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">'
+                        % Mod
+                    )
+                H.append(
+                    '<span class="formation_module_tit">%s</span>'
+                    % join_words(Mod["code"], Mod["titre"])
+                )
+                if mod_editable:
+                    H.append("</a>")
+                heurescoef = (
+                    "%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s"
+                    % Mod
+                )
+                if mod_editable:
+                    klass = "span_apo_edit"
+                else:
+                    klass = ""
+                heurescoef += (
+                    ', Apo: <span class="%s" data-url="edit_module_set_code_apogee" id="%s" data-placeholder="%s">'
+                    % (klass, Mod["module_id"], APO_MISSING_CODE_STR)
+                    + (Mod["code_apogee"] or "")
+                    + "</span>"
+                )
+                if tag_editable:
+                    tag_cls = "module_tag_editor"
+                else:
+                    tag_cls = "module_tag_editor_ro"
+                tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="{}">{}</textarea></form></span>"""
+                tag_edit = tag_mk.format(
+                    Mod["module_id"],
+                    tag_cls,
+                    ",".join(sco_tag_module.module_tag_list(context, Mod["module_id"])),
+                )
+                H.append(
+                    " %s %s" % (parcours.SESSION_NAME, Mod["semestre_id"])
+                    + " (%s)" % heurescoef
+                    + tag_edit
+                )
+                H.append("</li>")
+            if not Modlist:
+                H.append("<li>Aucun module dans cette matière !")
+                if editable:
+                    H.append(
+                        '<a class="stdlink" href="matiere_delete?matiere_id=%(matiere_id)s">supprimer cette matière</a>'
+                        % Mat
+                    )
+                H.append("</li>")
+            if editable:  # and ((not parcours.UE_IS_MODULE) or len(Modlist) == 0):
+                H.append(
+                    '<li> <a class="stdlink" href="module_create?matiere_id=%(matiere_id)s">créer un module</a></li>'
+                    % Mat
+                )
+            H.append("</ul>")
+            H.append("</li>")
+        if not Matlist:
+            H.append("<li>Aucune matière dans cette UE ! ")
+            if editable:
+                H.append(
+                    """<a class="stdlink" href="ue_delete?ue_id=%(ue_id)s">supprimer l'UE</a>"""
+                    % UE
+                )
+            H.append("</li>")
+        if editable and not parcours.UE_IS_MODULE:
+            H.append(
+                '<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>'
+                % UE
+            )
+        if not parcours.UE_IS_MODULE:
+            H.append("</ul>")
+    H.append("</ul>")
+    if editable:
+        H.append(
+            '<ul><li><a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>'
+            % formation_id
+        )
+        H.append(
+            '<li><a href="formation_add_malus_modules?formation_id=%(formation_id)s" class="stdlink">Ajouter des modules de malus dans chaque UE</a></li></ul>'
+            % F
+        )
+    H.append("</div>")  # formation_ue_list
+
+    H.append("<p><ul>")
+    if editable:
+        H.append(
+            """
+<li><a class="stdlink" href="formation_create_new_version?formation_id=%(formation_id)s">Créer une nouvelle version (non verrouillée)</a></li>
+"""
+            % F
+        )
+    H.append(
+        """
+<li><a class="stdlink" href="formation_table_recap?formation_id=%(formation_id)s">Table récapitulative de la formation</a></li>
+    
+<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&amp;format=xml">Export XML de la formation</a> (permet de la sauvegarder pour l'échanger avec un autre site)</li>
+
+<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&amp;format=json">Export JSON de la formation</a></li>
+
+<li><a class="stdlink" href="module_list?formation_id=%(formation_id)s">Liste détaillée des modules de la formation</a> (debug) </li>
+</ul>
+</p>"""
+        % F
+    )
+    if perm_change:
+        H.append(
+            """
+        <h3> <a name="sems">Semestres ou sessions de cette formation</a></h3>
+        <p><ul>"""
+        )
+        for sem in sco_formsemestre.do_formsemestre_list(
+            context, args={"formation_id": formation_id}
+        ):
+            H.append(
+                '<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a>'
+                % sem
+            )
+            if sem["etat"] != "1":
+                H.append(" [verrouillé]")
+            else:
+                H.append(
+                    ' <a class="stdlink" href="formsemestre_editwithmodules?formation_id=%(formation_id)s&amp;formsemestre_id=%(formsemestre_id)s">Modifier</a>'
+                    % sem
+                )
+            H.append("</li>")
+        H.append("</ul>")
+
+    if authuser.has_permission(ScoImplement, context):
+        H.append(
+            """<ul>
+        <li><a class="stdlink" href="formsemestre_createwithmodules?formation_id=%(formation_id)s&amp;semestre_id=1">Mettre en place un nouveau semestre de formation %(acronyme)s</a>
+ </li>
+
+</ul>"""
+            % F
+        )
+    #   <li>(debug) <a class="stdlink" href="check_form_integrity?formation_id=%(formation_id)s">Vérifier cohérence</a></li>
+
+    warn, ue_multiples = sco_formsemestre_validation.check_formation_ues(
+        context, formation_id
+    )
+    H.append(warn)
+
+    H.append(context.sco_footer(REQUEST))
+    return "".join(H)
+
+
+def ue_sharing_code(context, ue_code=None, ue_id=None, hide_ue_id=None):
+    """HTML list of UE sharing this code
+    Either ue_code or ue_id may be specified.
+    """
+    if ue_id:
+        ue = context.do_ue_list(args={"ue_id": ue_id})[0]
+        if not ue_code:
+            ue_code = ue["ue_code"]
+        F = context.formation_list(args={"formation_id": ue["formation_id"]})[0]
+        formation_code = F["formation_code"]
+
+    ue_list_all = context.do_ue_list(args={"ue_code": ue_code})
+    if ue_id:
+        # retire les UE d'autres formations:
+        # log('checking ucode %s formation %s' % (ue_code, formation_code))
+        ue_list = []
+        for ue in ue_list_all:
+            F = context.formation_list(args={"formation_id": ue["formation_id"]})[0]
+            if formation_code == F["formation_code"]:
+                ue_list.append(ue)
+    else:
+        ue_list = ue_list_all
+
+    if hide_ue_id:  # enlève l'ue de depart
+        ue_list = [ue for ue in ue_list if ue["ue_id"] != hide_ue_id]
+
+    if not ue_list:
+        if ue_id:
+            return """<span class="ue_share">Seule UE avec code %s</span>""" % ue_code
+        else:
+            return """<span class="ue_share">Aucune UE avec code %s</span>""" % ue_code
+    H = []
+    if ue_id:
+        H.append('<span class="ue_share">Autres UE avec le code %s:</span>' % ue_code)
+    else:
+        H.append('<span class="ue_share">UE avec le code %s:</span>' % ue_code)
+    H.append("<ul>")
+    for ue in ue_list:
+        F = context.formation_list(args={"formation_id": ue["formation_id"]})[0]
+        H.append(
+            '<li>%s (%s) dans <a class="stdlink" href="ue_list?formation_id=%s">%s (%s)</a>, version %s</li>'
+            % (
+                ue["acronyme"],
+                ue["titre"],
+                F["formation_id"],
+                F["acronyme"],
+                F["titre"],
+                F["version"],
+            )
+        )
+    H.append("</ul>")
+    return "\n".join(H)
+
+
+def do_ue_edit(context, args, bypass_lock=False, dont_invalidate_cache=False):
+    "edit an UE"
+    # check
+    ue_id = args["ue_id"]
+    ue = context.do_ue_list({"ue_id": ue_id})[0]
+    if (not bypass_lock) and context.ue_is_locked(ue["ue_id"]):
+        raise ScoLockedFormError()
+    # check: acronyme unique dans cette formation
+    if args.has_key("acronyme"):
+        new_acro = args["acronyme"]
+        ues = context.do_ue_list(
+            {"formation_id": ue["formation_id"], "acronyme": new_acro}
+        )
+        if ues and ues[0]["ue_id"] != ue_id:
+            raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"])
+
+    # On ne peut pas supprimer le code UE:
+    if args.has_key("ue_code") and not args["ue_code"]:
+        del args["ue_code"]
+
+    cnx = context.GetDBConnexion()
+    context._ueEditor.edit(cnx, args)
+
+    if not dont_invalidate_cache:
+        # Invalide les semestres utilisant cette formation:
+        for sem in sco_formsemestre.do_formsemestre_list(
+            context, args={"formation_id": ue["formation_id"]}
+        ):
+            context._inval_cache(
+                formsemestre_id=sem["formsemestre_id"]
+            )  # > formation (ue) modif.
+
+
+# essai edition en ligne:
+def edit_ue_set_code_apogee(context, id=None, value=None, REQUEST=None):
+    "set UE code apogee"
+    ue_id = id
+    value = value.strip("-_ \t")
+    log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value))
+
+    ues = context.do_ue_list(args={"ue_id": ue_id})
+    if not ues:
+        return "ue invalide"
+    ue = ues[0]
+
+    do_ue_edit(
+        context,
+        {"ue_id": ue_id, "code_apogee": value},
+        bypass_lock=True,
+        dont_invalidate_cache=False,
+    )
+    if not value:
+        value = APO_MISSING_CODE_STR
+    return value
+
+
+# ---- Table recap formation
+def formation_table_recap(context, formation_id, format="html", REQUEST=None):
+    """
+    """
+    F = context.formation_list(args={"formation_id": formation_id})
+    if not F:
+        raise ScoValueError("invalid formation_id")
+    F = F[0]
+    T = []
+    ue_list = context.do_ue_list(args={"formation_id": formation_id})
+    for UE in ue_list:
+        Matlist = context.do_matiere_list(args={"ue_id": UE["ue_id"]})
+        for Mat in Matlist:
+            Modlist = context.do_module_list(args={"matiere_id": Mat["matiere_id"]})
+            for Mod in Modlist:
+                Mod["nb_moduleimpls"] = context.module_count_moduleimpls(
+                    Mod["module_id"]
+                )
+                #
+                T.append(
+                    {
+                        "UE_acro": UE["acronyme"],
+                        "Mat_tit": Mat["titre"],
+                        "Mod_tit": Mod["abbrev"] or Mod["titre"],
+                        "Mod_code": Mod["code"],
+                        "Mod_coef": Mod["coefficient"],
+                        "Mod_sem": Mod["semestre_id"],
+                        "nb_moduleimpls": Mod["nb_moduleimpls"],
+                        "heures_cours": Mod["heures_cours"],
+                        "heures_td": Mod["heures_td"],
+                        "heures_tp": Mod["heures_tp"],
+                        "ects": Mod["ects"],
+                    }
+                )
+    columns_ids = [
+        "UE_acro",
+        "Mat_tit",
+        "Mod_tit",
+        "Mod_code",
+        "Mod_coef",
+        "Mod_sem",
+        "nb_moduleimpls",
+        "heures_cours",
+        "heures_td",
+        "heures_tp",
+        "ects",
+    ]
+    titles = {
+        "UE_acro": "UE",
+        "Mat_tit": "Matière",
+        "Mod_tit": "Module",
+        "Mod_code": "Code",
+        "Mod_coef": "Coef.",
+        "Mod_sem": "Sem.",
+        "nb_moduleimpls": "Nb utilisé",
+        "heures_cours": "Cours (h)",
+        "heures_td": "TD (h)",
+        "heures_tp": "TP (h)",
+        "ects": "ECTS",
+    }
+
+    title = (
+        """Formation %(titre)s (%(acronyme)s) [version %(version)s] code %(formation_code)s"""
+        % F
+    )
+    tab = GenTable(
+        columns_ids=columns_ids,
+        rows=T,
+        titles=titles,
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        caption=title,
+        html_caption=title,
+        html_class="table_leftalign",
+        base_url="%s?formation_id=%s" % (REQUEST.URL0, formation_id),
+        page_title=title,
+        html_title="<h2>" + title + "</h2>",
+        pdf_title=title,
+        preferences=context.get_preferences(),
+    )
+    return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+
+def ue_list_semestre_ids(context, ue):
+    """Liste triée des numeros de semestres des modules dans cette UE
+    Il est recommandable que tous les modules d'une UE aient le même indice de semestre.
+    Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
+    aussi ScoDoc laisse le choix.
+    """
+    Modlist = context.do_module_list(args={"ue_id": ue["ue_id"]})
+    return sorted(list(set([mod["semestre_id"] for mod in Modlist])))
diff --git a/sco_edt_cal.py b/sco_edt_cal.py
new file mode 100644
index 0000000000000000000000000000000000000000..f058a08ef6595f10e125b94157ca6fbb6a02ae93
--- /dev/null
+++ b/sco_edt_cal.py
@@ -0,0 +1,223 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Accès aux emplois du temps
+
+XXX usage uniquement experimental pour tests implémentations
+
+XXX incompatible avec les ics HyperPlanning Paris 13 (était pour GPU).
+
+"""
+
+import urllib2
+import traceback
+import icalendar
+
+from sco_utils import *
+import sco_formsemestre
+import sco_groups
+import sco_groups_view
+
+
+def formsemestre_get_ics_url(context, sem):
+    """
+    edt_sem_ics_url est un template
+    utilisé avec .format(sem=sem)
+    Par exemple:
+    https://example.fr/agenda/{sem[etapes][0]}
+    """
+    ics_url_tmpl = context.get_preference("edt_sem_ics_url", sem["formsemestre_id"])
+    if not ics_url_tmpl:
+        return None
+    try:
+        ics_url = ics_url_tmpl.format(sem=sem)
+    except:
+        log(
+            "Exception in formsemestre_get_ics_url(formsemestre_id=%s)"
+            % sem["formsemestre_id"]
+        )
+        log("ics_url_tmpl='%s'" % ics_url_tmpl)
+        log(traceback.format_exc())
+        return None
+    return ics_url
+
+
+def formsemestre_load_ics(context, sem):
+    """Load ics data, from our cache or, when necessary, from external provider
+    """
+    # TODO: cacher le résultat
+    ics_url = formsemestre_get_ics_url(context, sem)
+    if not ics_url:
+        ics_data = ""
+    else:
+        log("Loading edt from %s" % ics_url)
+        f = urllib2.urlopen(
+            ics_url, timeout=5
+        )  # 5s TODO: add config parameter, eg for slow networks
+        ics_data = f.read()
+        f.close()
+
+    cal = icalendar.Calendar.from_ical(ics_data)
+    return cal
+
+
+def formsemestre_edt_groups_used(context, sem):
+    """L'ensemble des groupes EDT utilisés dans l'emplois du temps publié
+    """
+    cal = formsemestre_load_ics(context, sem)
+    return {e["X-GROUP-ID"].decode("utf8") for e in events}
+
+
+def get_edt_transcodage_groups(context, formsemestre_id):
+    """ -> { nom_groupe_edt : nom_groupe_scodoc }
+    """
+    # TODO: valider ces données au moment où on enregistre les préférences
+    edt2sco = {}
+    sco2edt = {}
+    msg = ""  # message erreur, '' si ok
+    txt = context.get_preference("edt_groups2scodoc", formsemestre_id)
+    if not txt:
+        return edt2sco, sco2edt, msg
+
+    line_num = 1
+    for line in txt.split("\n"):
+        fs = [s.strip() for s in line.split(";")]
+        if len(fs) == 1:  # groupe 'tous'
+            edt2sco[fs[0]] = None
+            sco2edt[None] = fs[0]
+        elif len(fs) == 2:
+            edt2sco[fs[0]] = fs[1]
+            sco2edt[fs[1]] = fs[0]
+        else:
+            msg = "ligne %s invalide" % line_num
+        line_num += 1
+
+    log("sco2edt=%s" % pprint.pformat(sco2edt))
+    return edt2sco, sco2edt, msg
+
+
+def group_edt_json(context, group_id, start="", end="", REQUEST=None):
+    """EDT complet du semestre, au format JSON
+    TODO: indiquer un groupe
+    TODO: utiliser start et end (2 dates au format ISO YYYY-MM-DD)
+    TODO: cacher
+    """
+    group = sco_groups.get_group(context, group_id)
+    sem = sco_formsemestre.get_formsemestre(context, group["formsemestre_id"])
+    edt2sco, sco2edt, msg = get_edt_transcodage_groups(
+        context, group["formsemestre_id"]
+    )
+
+    edt_group_name = sco2edt.get(group["group_name"], group["group_name"])
+    log("group scodoc=%s : edt=%s" % (group["group_name"], edt_group_name))
+
+    cal = formsemestre_load_ics(context, sem)
+    events = [e for e in cal.walk() if e.name == "VEVENT"]
+    J = []
+    for e in events:
+        # if e['X-GROUP-ID'].encode('utf-8').strip() == edt_group_name:
+        if "DESCRIPTION" in e:
+            d = {
+                "title": e.decoded(
+                    "DESCRIPTION"
+                ),  # + '/' + e['X-GROUP-ID'].encode('utf-8'),
+                "start": e.decoded("dtstart").isoformat(),
+                "end": e.decoded("dtend").isoformat(),
+            }
+            J.append(d)
+
+    return sendJSON(REQUEST, J)
+
+
+"""XXX
+for e in events:
+    if 'DESCRIPTION' in e:
+        print e.decoded('DESCRIPTION').encode('utf-8')
+"""
+
+
+def experimental_calendar(context, group_id=None, formsemestre_id=None, REQUEST=None):
+    """experimental page
+    """
+    return "\n".join(
+        [
+            context.sco_header(
+                REQUEST,
+                javascripts=[
+                    "libjs/purl.js",
+                    "libjs/moment.min.js",
+                    "libjs/fullcalendar/fullcalendar.min.js",
+                ],
+                cssstyles=[
+                    #                'libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css',
+                    #                'libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css',
+                    #                'libjs/bootstrap-multiselect/bootstrap-multiselect.css'
+                    "libjs/fullcalendar/fullcalendar.css",
+                    # media='print' 'libjs/fullcalendar/fullcalendar.print.css'
+                ],
+            ),
+            """<style>
+        #loading {
+        display: none;
+        position: absolute;
+        top: 10px;
+        right: 10px;
+        }
+        </style>
+        """,
+            """<form id="group_selector" method="get">
+        <span style="font-weight: bold; font-size:120%">Emplois du temps du groupe</span>""",
+            sco_groups_view.menu_group_choice(
+                context, group_id=group_id, formsemestre_id=formsemestre_id
+            ),
+            """</form><div id="loading">loading...</div>
+        <div id="calendar"></div>
+        """,
+            context.sco_footer(REQUEST),
+            """<script>
+$(document).ready(function() {
+
+var group_id = $.url().param()['group_id'];
+
+$('#calendar').fullCalendar({
+  events: {
+    url: 'group_edt_json?group_id=' + group_id,
+    error: function() {
+      $('#script-warning').show();
+    }
+   },
+  timeFormat: 'HH:mm',
+  timezone: 'local', // heure locale du client
+  loading: function(bool) {
+    $('#loading').toggle(bool);
+  }
+});
+});
+</script>
+        """,
+        ]
+    )
diff --git a/sco_etape_apogee.py b/sco_etape_apogee.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ef7dab6995a1e5e10ef3c213e4842c1b5a3d145
--- /dev/null
+++ b/sco_etape_apogee.py
@@ -0,0 +1,475 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""ScoDoc : stockage et vérifications des "maquettes" Apogée 
+   (fichiers CSV pour l'export vers Apogée)
+   associées aux années scolaires
+
+   Voir sco_apogee_csv.py pour la structure du fichier Apogée.
+
+   Stockage: utilise sco_archive.py
+   => /opt/scodoc/var/scodoc/archives/apo_csv/RT/2016-1/2016-07-03-16-12-19/V3ASR.csv 
+   pour une maquette de l'année scolaire 2016, semestre 1, etape V3ASR
+
+   ou bien (à partir de ScoDoc 1678) :
+   /opt/scodoc/var/scodoc/archives/apo_csv/RT/2016-1/2016-07-03-16-12-19/V3ASR!111.csv 
+   pour une maquette de l'étape V3ASR version VDI 111.
+
+   La version VDI sera ignorée sauf si elle est indiquée dans l'étape du semestre.
+   apo_csv_get() 
+   
+   API:
+   apo_csv_store(context, csv, annee_scolaire, sem_id)        
+      store maq file (archive)
+      
+   apo_csv_get(context, etape_apo, annee_scolaire, sem_id, vdi_apo=None)
+      get maq data (read stored file and returns string)
+      if vdi_apo, get maq for this etape/vdi, else returns the first matching etape.
+
+   apo_csv_delete(context, etape_apo, annee_scolaire, sem_id)
+
+   apo_csv_list_stored_etapes(context, annee_scolaire=None, sem_id=None, etapes=None) 
+       returns: liste des codes etapes et version vdi stockés (pour l'annee_scolaire et le sem_id indiqués)
+
+   apo_csv_semset_check(context, semset)
+      check students in stored maqs vs students in sem
+      Cas à détecter:
+      - etudiants ScoDoc sans code NIP
+      - etudiants dans sem (ScoDoc) mais dans aucun CSV
+      - etudiants dans un CSV mais pas dans sem ScoDoc
+      - etudiants dans plusieurs CSV (argh!)
+      detecte aussi si on a plusieurs années scolaires
+      
+      returns: etuds_ok (in ScoDoc and CSVs)
+               etuds_no_apo
+               unknown_apo : liste de { 'NIP', 'nom', 'prenom' }
+               dups_apo : liste de { 'NIP', 'nom', 'prenom', 'etapes_apo' }
+               etapes_missing_csv : liste des étapes du semestre sans maquette CSV
+
+   apo_csv_check_etape(context, semset, set_nips, etape_apo)
+      check une etape
+      
+"""
+
+from sco_utils import *
+from notes_log import log
+import sco_formsemestre
+import notes_table
+import sco_groups
+import sco_groups_view
+import sco_archives
+import sco_apogee_csv
+
+
+class ApoCSVArchiver(sco_archives.BaseArchiver):
+    def __init__(self):
+        sco_archives.BaseArchiver.__init__(self, archive_type="apo_csv")
+
+
+ApoCSVArchive = ApoCSVArchiver()
+
+
+# def get_sem_apo_archive(context, formsemestre_id):
+#     """Get, or create if necessary, the archive for apo CSV files"""
+
+#     archive_id
+
+#     return archive_id
+
+
+def apo_csv_store(context, csv_data, annee_scolaire, sem_id):
+    """
+    csv_data: maquette content, as a string, encoding given by APO_INPUT_ENCODING (latin-1, not utf8)
+    annee_scolaire: int (2016)
+    sem_id: 0 (année ?), 1 (premier semestre de l'année) ou 2 (deuxième semestre)
+    :return: etape_apo du fichier CSV stocké
+    """
+    # sanity check
+    filesize = len(csv_data)
+    if filesize < 10 or filesize > CONFIG.ETUD_MAX_FILE_SIZE:
+        raise ScoValueError("Fichier csv de taille invalide ! (%d)" % filesize)
+
+    if not annee_scolaire:
+        raise ScoValueError("Impossible de déterminer l'année scolaire !")
+
+    apo_data = sco_apogee_csv.ApoData(
+        csv_data, periode=sem_id
+    )  # parse le fichier -> exceptions
+
+    filename = str(apo_data.etape) + ".csv"  # will concatenate VDI to etape
+
+    if str(apo_data.etape) in apo_csv_list_stored_etapes(
+        context, annee_scolaire, sem_id=sem_id
+    ):
+        raise ScoValueError(
+            "Etape %s déjà stockée pour cette année scolaire !" % apo_data.etape
+        )
+
+    oid = "%d-%d" % (annee_scolaire, sem_id)
+    description = "%s;%s;%s" % (str(apo_data.etape), annee_scolaire, sem_id)
+    archive_id = ApoCSVArchive.create_obj_archive(context, oid, description)
+    ApoCSVArchive.store(archive_id, filename, csv_data)
+
+    return apo_data.etape
+
+
+def apo_csv_list_stored_archives(
+    context, annee_scolaire=None, sem_id=None, etapes=None
+):
+    """
+    :return: list of informations about stored CSV
+    [ { } ]
+    """
+    oids = ApoCSVArchive.list_oids(context)  # [ '2016-1', ... ]
+    # filter
+    if annee_scolaire:
+        e = re.compile(str(annee_scolaire) + "-.+")
+        oids = [x for x in oids if e.match(x)]
+    if sem_id:
+        e = re.compile(r"[0-9]{4}-" + str(sem_id))
+        oids = [x for x in oids if e.match(x)]
+
+    infos = []  # liste d'infos
+    for oid in oids:
+        archive_ids = ApoCSVArchive.list_obj_archives(context, oid)
+        for archive_id in archive_ids:
+            description = ApoCSVArchive.get_archive_description(archive_id)
+            fs = tuple(description.split(";"))
+            if len(fs) == 3:
+                arch_etape_apo, arch_annee_scolaire, arch_sem_id = fs
+            else:
+                raise ValueError("Archive invalide: " + archive_id)
+
+            if (etapes is None) or (arch_etape_apo in etapes):
+                infos.append(
+                    {
+                        "archive_id": archive_id,
+                        "annee_scolaire": int(arch_annee_scolaire),
+                        "sem_id": int(arch_sem_id),
+                        "etape_apo": arch_etape_apo,  # qui contient éventuellement le VDI
+                        "date": ApoCSVArchive.get_archive_date(archive_id),
+                    }
+                )
+    infos.sort(key=lambda x: x["etape_apo"])
+
+    return infos
+
+
+def apo_csv_list_stored_etapes(context, annee_scolaire, sem_id=None, etapes=None):
+    """
+    :return: list of stored etapes [ ApoEtapeVDI, ... ]
+    """
+    infos = apo_csv_list_stored_archives(
+        context, annee_scolaire=annee_scolaire, sem_id=sem_id, etapes=etapes
+    )
+    return [info["etape_apo"] for info in infos]
+
+
+def apo_csv_delete(context, archive_id):
+    """Delete archived CSV
+    """
+    ApoCSVArchive.delete_archive(archive_id)
+
+
+def apo_csv_get_archive(context, etape_apo, annee_scolaire="", sem_id=""):
+    """Get archive"""
+    stored_archives = apo_csv_list_stored_archives(
+        context, annee_scolaire=annee_scolaire, sem_id=sem_id
+    )
+    for info in stored_archives:
+        if info["etape_apo"] == etape_apo:
+            return info
+    return None
+
+
+def apo_csv_get(context, etape_apo="", annee_scolaire="", sem_id=""):
+    """Get CSV data for given etape_apo
+    :return: CSV, as a data string
+    """
+    info = apo_csv_get_archive(context, etape_apo, annee_scolaire, sem_id)
+    if not info:
+        raise ScoValueError(
+            "Etape %s non enregistree (%s, %s)" % (etape_apo, annee_scolaire, sem_id)
+        )
+    archive_id = info["archive_id"]
+    data = ApoCSVArchive.get(archive_id, etape_apo + ".csv")
+    return data
+
+
+# ------------------------------------------------------------------------
+
+
+def apo_get_sem_etapes(context, sem):
+    """Etapes de ce semestre: pour l'instant, celles déclarées 
+    Dans une future version, on pourrait aussi utiliser les étapes 
+    d'inscription des étudiants, recupérées via le portail, 
+    voir check_paiement_etuds().
+    
+    :return: list of etape_apo (ApoEtapeVDI instances)
+    """
+    return sem["etapes"]
+
+
+def apo_csv_check_etape(context, semset, set_nips, etape_apo):
+    """Check etape vs set of sems
+    """
+    # Etudiants dans la maquette CSV:
+    csv_data = apo_csv_get(
+        context, etape_apo, semset["annee_scolaire"], semset["sem_id"]
+    )
+    apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
+    apo_nips = {e["nip"] for e in apo_data.etuds}
+    #
+    nips_ok = set_nips.intersection(apo_nips)
+    nips_no_apo = set_nips - apo_nips  # dans ScoDoc mais pas dans cette maquette Apogée
+    nips_no_sco = apo_nips - set_nips  # dans Apogée mais pas dans ScoDoc
+
+    # Elements Apogee vs ScoDoc
+    apo_data.setup(context)
+    maq_elems, sem_elems = apo_data.list_elements()
+
+    return nips_ok, apo_nips, nips_no_apo, nips_no_sco, maq_elems, sem_elems
+
+
+def apo_csv_semset_check(context, semset, allow_missing_apo=False):  # was apo_csv_check
+    """
+    check students in stored maqs vs students in semset
+      Cas à détecter:
+      - étapes sans maquette CSV (etapes_missing_csv)
+      - etudiants ScoDoc sans code NIP (etuds_without_nip)
+      - etudiants dans semset (ScoDoc) mais dans aucun CSV (nips_no_apo)
+      - etudiants dans un CSV mais pas dans semset ScoDoc (nips_no_sco)
+      - etudiants dans plusieurs CSV (argh!)
+      + si plusieurs annees scolaires
+    """
+    # Etapes du semestre sans maquette CSV:
+    etapes_apo = apo_csv_list_stored_etapes(
+        context, semset["annee_scolaire"], semset["sem_id"], etapes=semset.list_etapes()
+    )
+    etapes_missing_csv = []
+    for e in semset.list_etapes():
+        if not e in etapes_apo:
+            etapes_missing_csv.append(e)
+
+    # Etudiants inscrits dans ce semset:
+    semset.load_etuds()
+
+    set_nips = set().union(*[s["nips"] for s in semset.sems])
+    #
+    nips_ok = set()  # codes nip des etudiants dans ScoDoc et Apogée
+    nips_no_apo = set_nips.copy()  # dans ScoDoc mais pas dans Apogée
+    nips_no_sco = set()  # dans Apogée mais pas dans ScoDoc
+    etapes_apo_nips = []  # liste des nip de chaque maquette
+    maq_elems = set()
+    sem_elems = set()
+    for etape_apo in etapes_apo:
+        (
+            et_nips_ok,
+            et_apo_nips,
+            et_nips_no_apo,
+            et_nips_no_sco,
+            et_maq_elems,
+            et_sem_elems,
+        ) = apo_csv_check_etape(context, semset, set_nips, etape_apo)
+        nips_ok |= et_nips_ok
+        nips_no_apo -= et_apo_nips
+        nips_no_sco |= et_nips_no_sco
+        etapes_apo_nips.append(et_apo_nips)
+        maq_elems |= et_maq_elems
+        sem_elems |= et_sem_elems
+
+    # doublons: etudiants mentionnés dans plusieurs maquettes Apogée:
+    apo_dups = set()
+    if len(etapes_apo_nips) > 1:
+        all_nips = etapes_apo_nips[0]
+        for etape_apo_nips in etapes_apo_nips[1:]:
+            apo_dups |= all_nips & etape_apo_nips
+            all_nips |= etape_apo_nips
+
+    # All ok ?
+    ok_for_export = (
+        (not etapes_missing_csv)
+        and (not semset["etuds_without_nip"])
+        and ((not nips_no_apo) or allow_missing_apo)
+        and (not apo_dups)
+        and len(semset.annees_scolaires()) <= 1
+    )
+
+    return (
+        ok_for_export,
+        etapes_missing_csv,
+        semset["etuds_without_nip"],
+        nips_ok,
+        nips_no_apo,
+        nips_no_sco,
+        apo_dups,
+        maq_elems,
+        sem_elems,
+    )
+
+
+def apo_csv_retreive_etuds_by_nip(context, semset, nips):
+    """
+    Search info about listed nips in stored CSV
+    :return: list [ { 'etape_apo', 'nip', 'nom', 'prenom' } ]
+    """
+    apo_etuds_by_nips = {}
+    etapes_apo = apo_csv_list_stored_etapes(
+        context, semset["annee_scolaire"], semset["sem_id"]
+    )
+    for etape_apo in etapes_apo:
+        csv_data = apo_csv_get(
+            context, etape_apo, semset["annee_scolaire"], semset["sem_id"]
+        )
+        apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
+        etape_apo = apo_data.etape_apogee
+        for e in apo_data.etuds:
+            e["etape_apo"] = etape_apo
+        apo_etuds_by_nips.update(dict([(e["nip"], e) for e in apo_data.etuds]))
+
+    etuds = {}  # { nip : etud or None }
+    for nip in nips:
+        etuds[nip] = apo_etuds_by_nips.get(nip, {"nip": nip, "etape_apo": "?"})
+
+    return etuds
+
+
+"""
+Tests:
+
+from debug import *
+import sco_groups
+import sco_groups_view
+import sco_formsemestre
+from sco_etape_apogee import *
+from sco_apogee_csv import *
+from sco_semset import *
+
+context = go_dept(app, 'RT').Notes
+csv_data = open('/opt/misc/VDTRT_V1RT.TXT').read()
+annee_scolaire=2015
+sem_id=1
+
+apo_data = sco_apogee_csv.ApoData(csv_data, periode=sem_id)
+print apo_data.etape_apogee
+
+apo_data.setup(context)
+e = apo_data.etuds[0]
+e.lookup_scodoc(context, apo_data.etape_formsemestre_ids)
+e.associate_sco(context, apo_data)
+
+print apo_csv_list_stored_archives(context)
+
+
+apo_csv_store(context, csv_data, annee_scolaire, sem_id)
+
+
+
+groups_infos = sco_groups_view.DisplayedGroupsInfos(context, [sco_groups.get_default_group(context, formsemestre_id)], formsemestre_id=formsemestre_id, REQUEST=REQUEST)
+
+nt = context.Notes._getNotesCache().get_NotesTable(context.Notes, formsemestre_id)
+
+#
+s = SemSet(context, 'NSS29902')
+apo_data = sco_apogee_csv.ApoData(open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2015-2/2016-07-10-11-26-15/V1RT.csv').read(), periode=1) 
+
+# cas Tiziri K. (inscrite en S1, démission en fin de S1, pas inscrite en S2)
+# => pas de décision, ce qui est voulu (?)
+#
+
+apo_data.setup(context)
+e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0]
+e.lookup_scodoc(context, apo_data.etape_formsemestre_ids)
+e.associate_sco(context, apo_data)
+
+self=e
+col_id='apoL_c0129'
+
+# --
+import sco_portal_apogee
+context = go_dept(app, 'GEA').Notes
+#csv_data = sco_portal_apogee.get_maquette_apogee(context, etape='V1GE', annee_scolaire=2015)
+csv_data = open('/tmp/V1GE.txt').read()
+apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
+
+
+# ------
+# les elements inconnus:
+
+from debug import *
+import sco_groups
+import sco_groups_view
+import sco_formsemestre
+from sco_etape_apogee import *
+from sco_apogee_csv import *
+from sco_semset import *
+
+context = go_dept(app, 'RT').Notes
+csv_data = open('/opt/misc/V2RT.csv').read()
+annee_scolaire=2015
+sem_id=1
+
+apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
+print apo_data.etape_apogee
+
+apo_data.setup(context)
+for e in apo_data.etuds:
+    e.lookup_scodoc(context, apo_data.etape_formsemestre_ids)
+    e.associate_sco(context, apo_data)
+
+# ------
+# test export jury intermediaire
+from debug import *
+import sco_groups
+import sco_groups_view
+import sco_formsemestre
+from sco_etape_apogee import *
+from sco_apogee_csv import *
+from sco_semset import *
+
+context = go_dept(app, 'CJ').Notes
+csv_data = open('/opt/scodoc/var/scodoc/archives/apo_csv/CJ/2016-1/2017-03-06-21-46-32/V1CJ.csv').read()
+annee_scolaire=2016
+sem_id=1
+
+apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
+print apo_data.etape_apogee
+
+apo_data.setup(context)
+e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0] #
+e.lookup_scodoc(context, apo_data.etape_formsemestre_ids)
+e.associate_sco(context, apo_data)
+
+self=e
+
+sco_elts = {}
+col_id='apoL_c0001'
+code = apo_data.cols[col_id]['Code'] # 'V1RT'
+
+sem = apo_data.sems_periode[0] # le S1
+
+"""
diff --git a/sco_etape_apogee_view.py b/sco_etape_apogee_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ba3b8a1e114b811bd3a01140af3ca08130896dc
--- /dev/null
+++ b/sco_etape_apogee_view.py
@@ -0,0 +1,848 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""ScoDoc : formulaires gestion maquettes Apogee / export resultats
+"""
+
+from cStringIO import StringIO
+from zipfile import ZipFile
+
+from sco_utils import *
+from notes_log import log
+import sco_formsemestre
+import sco_formsemestre_status
+import notes_table
+from gen_tables import GenTable
+import sco_semset
+import sco_etape_apogee
+import sco_apogee_csv
+import sco_portal_apogee
+from sco_apogee_csv import APO_PORTAL_ENCODING, APO_INPUT_ENCODING
+import sco_archives
+
+
+def apo_semset_maq_status(
+    context,
+    semset_id="",
+    allow_missing_apo=False,
+    allow_missing_decisions=False,
+    block_export_res_etape=False,
+    block_export_res_sem=False,
+    block_export_res_ues=False,
+    block_export_res_modules=False,
+    block_export_res_sdj=True,
+    REQUEST=None,
+):
+    """Page statut / tableau de bord
+    
+    """
+    if not semset_id:
+        raise ValueError("invalid null semset_id")
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    semset.fill_formsemestres(REQUEST)
+
+    allow_missing_apo = int(
+        allow_missing_apo
+    )  # autorise export meme si etudiants Apo manquants
+    allow_missing_decisions = int(
+        allow_missing_decisions
+    )  # autorise export meme s'il manque des décisions de jury
+    block_export_res_etape = int(block_export_res_etape)
+    block_export_res_sem = int(block_export_res_sem)
+    block_export_res_ues = int(block_export_res_ues)
+    block_export_res_modules = int(block_export_res_modules)
+    block_export_res_sdj = int(block_export_res_sdj)
+
+    prefs = context.get_preferences()
+
+    tab_archives = table_apo_csv_list(context, semset, REQUEST=REQUEST)
+
+    (
+        ok_for_export,
+        etapes_missing_csv,
+        etuds_without_nip,
+        nips_ok,
+        nips_no_apo,
+        nips_no_sco,
+        apo_dups,
+        maq_elems,
+        sem_elems,
+    ) = sco_etape_apogee.apo_csv_semset_check(context, semset, allow_missing_apo)
+
+    if not allow_missing_decisions:
+        ok_for_export &= semset["jury_ok"]
+
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Export Apogée",
+            javascripts=["js/apo_semset_maq_status.js"],
+        ),
+        """<h2>Export des résultats vers Apogée</h2>"""
+        """<div class="semset_descr">""",
+        semset.html_descr(),
+        semset.html_form_sems(),
+        """</div>""",
+    ]
+    # Bilans code apogée
+    H.append(semset.html_diagnostic())
+
+    # Maquettes enregistrées
+    H.append(
+        """<div class="apo_csv_list">
+    <span class="box_title">Maquettes Apogée</span>
+    """
+    )
+    if not tab_archives.is_empty():
+        H.append(tab_archives.html())
+    else:
+        H.append("""<p><em>Aucune maquette chargée</em></p>""")
+    # Upload fichier:
+    H.append(
+        """<form id="apo_csv_add" action="view_apo_csv_store" method="post" enctype="multipart/form-data">
+        Charger votre fichier maquette Apogée: 
+        <input type="file" size="30" name="csvfile"/>
+        <input type="hidden" name="semset_id" value="%s"/>
+        <input type="submit" value="Ajouter ce fichier"/>
+        <input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
+        </form>"""
+        % (semset_id,)
+    )
+    # Récupération sur portail:
+    maquette_url = sco_portal_apogee.get_maquette_url(context)
+    if maquette_url:  # portail configuré
+        menu_etapes = """<option value=""></option>"""
+        menu_etapes += "".join(
+            ['<option value="%s">%s</option>' % (et, et) for et in semset.list_etapes()]
+        )
+        H.append(
+            """<form id="apo_csv_download" action="view_apo_csv_download_and_store" method="post" enctype="multipart/form-data">
+        Ou récupérer maquette Apogée pour une étape:
+        <script type="text/javascript">
+        function change_etape(e) {
+           $('#apo_csv_download_submit_btn').attr('disabled', (e.value == ""));           
+        }
+        </script>
+        <select name="etape_apo" onchange="change_etape(this);">
+        %s
+        </select>
+        <input type="hidden" name="semset_id" value="%s"/>
+        <input id="apo_csv_download_submit_btn" type="submit" value="Télécharger" disabled="disabled"/>
+        </form>"""
+            % (menu_etapes, semset_id)
+        )
+    #
+    H.append("</div>")
+
+    # Tableau de bord
+    if ok_for_export:
+        class_ok = "apo_csv_status_ok"
+    else:
+        class_ok = "apo_csv_status_nok"
+
+    H.append('<div class="apo_csv_status %s">' % class_ok)
+    if ok_for_export:
+        H.append("""<span class="box_title">Exportation</span>""")
+    else:
+        H.append(
+            """<span class="box_title">Problèmes à résoudre avant export des résultats:</span>"""
+        )
+    H.append('<div class="apo_csv_problems"><ul>')
+    if len(semset.annees_scolaires()) > 1:
+        H.append("""<li>Il y a plusieurs années scolaires !</li>""")
+    if nips_no_sco:  # seulement un warning
+        url_list = (
+            "view_apo_etuds?semset_id=%s&amp;title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20les%%20semestres%%20ScoDoc:&amp;nips=%s"
+            % (semset_id, "&amp;nips=".join(nips_no_sco))
+        )
+        H.append(
+            '<li class="apo_csv_warning">Attention: il y a <a href="%s">%d étudiant(s)</a> dans les maquettes Apogée chargées non inscrit(s) dans ce semestre ScoDoc;</li>'
+            % (url_list, len(nips_no_sco))
+        )
+
+    if etapes_missing_csv:
+        H.append(
+            "<li>Etapes sans maquette: <tt>%s</tt></li>"
+            % sco_formsemestre.etapes_apo_str(sorted(etapes_missing_csv))
+        )
+
+    if etuds_without_nip:
+        H.append("<li>%d étudiants ScoDoc sans code NIP</li>" % len(etuds_without_nip))
+
+    if nips_no_apo:
+        url_list = (
+            "view_scodoc_etuds?semset_id=%s&amp;title=Etudiants%%20ScoDoc%%20non%%20listés%%20dans%%20les%%20maquettes%%20Apogée%%20chargées&amp;nips=%s"
+            % (semset_id, "&amp;nips=".join(nips_no_apo))
+        )
+        H.append(
+            '<li><a href="%s">%d étudiants</a> dans ce semestre non présents dans les maquettes Apogée chargées</li>'
+            % (url_list, len(nips_no_apo))
+        )
+
+    if nips_no_sco:  # seulement un warning
+        url_list = (
+            "view_apo_etuds?semset_id=%s&amp;title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20les%%20semestres%%20ScoDoc:&amp;nips=%s"
+            % (semset_id, "&amp;nips=".join(nips_no_sco))
+        )
+        H.append(
+            '<li class="apo_csv_warning">Attention: il reste <a href="%s">%d étudiants</a> dans les maquettes Apogée chargées mais pas inscrits dans ce semestre ScoDoc</li>'
+            % (url_list, len(nips_no_sco))
+        )
+
+    if apo_dups:
+        url_list = (
+            "view_apo_etuds?semset_id=%s&amp;title=Doublons%%20Apogee&amp;nips=%s"
+            % (semset_id, "&amp;nips=".join(apo_dups))
+        )
+        H.append(
+            '<li><a href="%s">%d étudiants</a> présents dans les <em>plusieurs</em> maquettes Apogée chargées</li>'
+            % (url_list, len(apo_dups))
+        )
+
+    H.append("</ul></div>")
+
+    # Decisions de jury
+    if semset["jury_ok"]:
+        class_ok = "apo_csv_jury_ok"
+    else:
+        class_ok = "apo_csv_jury_nok"
+
+    H.append('<div class="apo_csv_jury %s"><ul>' % class_ok)
+    if semset["jury_ok"]:
+        H.append("""<li>Décisions de jury saisies</li>""")
+    else:
+        H.append("""<li>Il manque des décisions de jury !</li>""")
+
+    if ok_for_export:
+        H.append("""<li>%d étudiants, prêt pour l'export.</li>""" % len(nips_ok))
+    H.append("</ul></div>")
+
+    H.append(
+        """<form name="f" method="get" action="%s">
+    <input type="hidden" name="semset_id" value="%s"></input>
+    <div><input type="checkbox" name="allow_missing_apo" value="1" onchange="document.f.submit()" """
+        % (REQUEST.URL0, semset_id)
+    )
+    if allow_missing_apo:
+        H.append("checked")
+    H.append(
+        """ >autoriser export même si étudiants manquants dans Apogée</input></div>"""
+    )
+    H.append(
+        """<div><input type="checkbox" name="allow_missing_decisions" value="1" onchange="document.f.submit()" """
+    )
+    if allow_missing_decisions:
+        H.append("checked")
+    H.append(
+        """ >autoriser export même si des décisions de jury n'ont pas été saisies</input></div>"""
+    )
+    H.append("""</form>""")
+
+    if semset and ok_for_export:
+        H.append(
+            """<form class="form_apo_export" action="apo_csv_export_results" method="get">        
+        <input type="submit" value="Export vers Apogée">
+        <input type="hidden" name="semset_id" value="%s"/>
+        """
+            % (semset_id,)
+        )
+        H.append('<div id="param_export_res">')
+
+        def checked(block, pname, msg):
+            if not prefs[pname]:
+                return (
+                    "disabled",
+                    "checked",
+                    "<em>export de " + msg + " désactivé dans les paramètres</em>",
+                )
+            if block:
+                return "", "checked", "ne pas exporter " + msg
+            else:
+                return "", "", "ne pas exporter " + msg
+
+        H.append(
+            """<div><label><input type="checkbox" name="block_export_res_etape" value="1" %s %s>%s</input></label></div>"""
+            % checked(
+                block_export_res_etape, "export_res_etape", "résultat de l'étape (VET), sauf si diplôme"
+            )
+        )
+        H.append(
+            """<div><label><input type="checkbox" name="block_export_res_sem" value="1" %s %s/>%s</label></div>"""
+            % checked(block_export_res_sem, "export_res_sem", "résultat du semestre")
+        )
+        H.append(
+            """<div><label><input type="checkbox" name="block_export_res_ues" value="1" %s %s/>%s</label></div>"""
+            % checked(block_export_res_ues, "export_res_ues", "résultats d'UE")
+        )
+        H.append(
+            """<div><label><input type="checkbox" name="block_export_res_modules" value="1" %s %s/>%s</label></div>"""
+            % checked(
+                block_export_res_modules, "export_res_modules", "résultats de module"
+            )
+        )
+        H.append(
+            """<div><label><input type="checkbox" name="block_export_res_sdj" value="1" %s %s/>%s</label></div>"""
+            % checked(
+                block_export_res_sdj,
+                "export_res_sdj",
+                "résultats sans décision de jury",
+            )
+        )
+        H.append("</div>")
+        H.append("</form>")
+
+    # Elements:
+    missing = maq_elems - sem_elems
+    H.append('<div id="apo_elements">')
+    H.append(
+        '<p>Elements Apogée: <span class="apo_elems">%s</span></p>'
+        % ", ".join(
+            [
+                e if not e in missing else '<span class="missing">' + e + "</span>"
+                for e in sorted(maq_elems)
+            ]
+        )
+    )
+
+    if missing:
+        formation_ids = {sem["formation_id"] for sem in semset.sems}
+        formations = [context.formation_list(formation_id=i)[0] for i in formation_ids]
+        # log('formations=%s' % formations)
+        H.append(
+            '<div class="apo_csv_status_missing_elems"><span class="fontred">Elements Apogée absents dans ScoDoc: </span><span class="apo_elems fontred">%s</span>'
+            % ", ".join(sorted(missing))
+        )
+        H.append(
+            '<div class="help">Ces éléments de la maquette Apogée ne sont pas déclarés dans ScoDoc et ne seront donc pas remplis.</div><div> Vous pouvez les déclarer dans les programmes pédagogiques: '
+        )
+        H.append(
+            ", ".join(
+                [
+                    '<a class="stdlink"  href="ue_list?formation_id=%(formation_id)s">%(acronyme)s v%(version)s</a>'
+                    % f
+                    for f in formations
+                ]
+            )
+        )
+        H.append("</div></div>")
+
+    H.append("</div>")
+    H.append("</div>")
+    # Aide:
+    H.append(
+        """
+    <p><a class="stdlink" href="semset_page">Retour aux ensembles de semestres</a></p>
+    
+    <div class="pas_help">
+    <h3>Explications</h3>
+    <p>Cette page permet de stocker les fichiers Apogée nécessaires pour 
+    l'export des résultats après les jurys, puis de remplir et exporter ces fichiers.
+    </p>
+    <p>
+    Les fichiers ("maquettes") Apogée sont de type CSV, du texte codé en %s.
+    </p>
+    <p>On a un fichier par étape Apogée. Pour les obtenir, soit on peut les télécharger directement (si votre ScoDoc est interfacé avec Apogée), soit se débrouiller pour exporter le fichier 
+    texte depuis Apogée. Son contenu ressemble à cela:</p>
+    <pre class="small_pre_acc">
+ XX-APO_TITRES-XX
+ apoC_annee	2007/2008
+ apoC_cod_dip	VDTCJ
+ apoC_Cod_Exp	1
+ apoC_cod_vdi	111
+ apoC_Fichier_Exp	VDTCJ_V1CJ.txt
+ apoC_lib_dip	DUT CJ
+ apoC_Titre1	Export Apogée du 13/06/2008 à 14:29
+ apoC_Titre2
+
+ XX-APO_COLONNES-XX
+ apoL_a01_code	Type Objet	Code	Version	Année	Session	Admission/Admissibilité	Type Rés.			Etudiant	Numéro
+ apoL_a02_nom										1	Nom
+ apoL_a03_prenom										1	Prénom
+ apoL_a04_naissance									Session	Admissibilité	Naissance
+ APO_COL_VAL_DEB
+ apoL_c0001	VET	V1CJ	111	2007	0	1	N	V1CJ - DUT CJ an1	0	1	Note
+ apoL_c0002	VET	V1CJ	111	2007	0	1	B		0	1	Barème
+ apoL_c0003	VET	V1CJ	111	2007	0	1	R		0	1	Résultat
+ APO_COL_VAL_FIN
+ apoL_c0030	APO_COL_VAL_FIN
+
+ XX-APO_VALEURS-XX
+ apoL_a01_code	apoL_a02_nom	apoL_a03_prenom	apoL_a04_naissance	apoL_c0001	apoL_c0002	apoL_c0003	apoL_c0004	apoL_c0005	apoL_c0006 (...)
+ 11681234	DUPONT	TOTO	 23/09/1986	18	20	ADM	18	20	ADM	(...)
+    </pre>
+    <p>Après avoir obtenu les fichier, stockez-les dans ScoDoc 
+    (bouton "Ajouter fichier" en haut de cette page. Après vérification, il va 
+    apparaitre dans une table. Vous pouvez supprimer ce fichier, ou en ajouter 
+    d'autres si votre semestre correspond à plusieurs étapes Apogée.
+    </p>
+    <p>ScoDoc vérifie que tous les étudiants du semestre sont mentionnés dans 
+    un fichier Apogée et que les étapes correspondent.</p>
+    <p>Lorsque c'est le cas, et que les décisions de jury sont saisies, 
+    un bouton "Export vers Apogée" apparait et vous pouvez exporter les résultats.
+    <p>
+    <p>Vous obtiendrez alors un fichier ZIP comprenant tous les fichiers nécessaires.
+    Certains de ces fichiers devront être importés dans Apogée.
+    </p>
+    </div>
+    """
+        % (APO_INPUT_ENCODING,)
+    )
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def table_apo_csv_list(context, semset, REQUEST=None):
+    """Table des archives (triée par date d'archivage)
+    """
+    annee_scolaire = semset["annee_scolaire"]
+    sem_id = semset["sem_id"]
+
+    T = sco_etape_apogee.apo_csv_list_stored_archives(
+        context, annee_scolaire, sem_id, etapes=semset.list_etapes()
+    )
+
+    for t in T:
+        # Ajoute qq infos pour affichage:
+        csv_data = sco_etape_apogee.apo_csv_get(
+            context, t["etape_apo"], annee_scolaire, sem_id
+        )
+        apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
+        t["filename"] = apo_data.titles["apoC_Fichier_Exp"]
+        t["nb_etuds"] = len(apo_data.etuds)
+        t["date_str"] = t["date"].strftime("%d/%m/%Y à %H:%M")
+        view_link = "view_apo_csv?etape_apo=%s&semset_id=%s" % (
+            t["etape_apo"],
+            semset["semset_id"],
+        )
+        t["_filename_target"] = view_link
+        t["_etape_apo_target"] = view_link
+        t["suppress"] = icontag(
+            "delete_small_img", border="0", alt="supprimer", title="Supprimer"
+        )
+        t["_suppress_target"] = "view_apo_csv_delete?etape_apo=%s&semset_id=%s" % (
+            t["etape_apo"],
+            semset["semset_id"],
+        )
+
+    columns_ids = ["filename", "etape_apo", "date_str", "nb_etuds"]
+    # if can_edit:
+    columns_ids = ["suppress"] + columns_ids
+
+    tab = GenTable(
+        titles={
+            "archive_id": "",
+            "filename": "Fichier",
+            "etape_apo": "Etape",
+            "nb_etuds": "Nb étudiants",
+            "date_str": "Enregistré le",
+        },
+        columns_ids=columns_ids,
+        rows=T,
+        html_class="table_leftalign apo_maq_list",
+        html_sortable=True,
+        # base_url = '%s?formsemestre_id=%s' % (REQUEST.URL0, formsemestre_id),
+        # caption='Maquettes enregistrées',
+        preferences=context.get_preferences(),
+    )
+
+    return tab
+
+
+def view_apo_etuds(context, semset_id, title="", nips=[], format="html", REQUEST=None):
+    """Table des étudiants Apogée par nips
+    """
+    if not semset_id:
+        raise ValueError("invalid null semset_id")
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    annee_scolaire = semset["annee_scolaire"]
+    sem_id = semset["sem_id"]
+
+    if nips and type(nips) != type([]):
+        nips = [nips]
+    etuds = sco_etape_apogee.apo_csv_retreive_etuds_by_nip(context, semset, nips)
+    # Ils sont parfois dans ScoDoc même si pas dans le semestre: essaie de les retrouver
+    for etud in etuds.values():
+        etud_sco = context.getEtudInfo(code_nip=etud["nip"], filled=True)
+        if etud_sco:
+            e = etud_sco[0]
+            etud["inscriptions_scodoc"] = ", ".join(
+                [
+                    '<a href="formsemestre_bulletinetud?formsemestre_id={s[formsemestre_id]}&etudid={e[etudid]}">{s[etapes_apo_str]} (S{s[semestre_id]})</a>'.format(
+                        s=sem, e=e
+                    )
+                    for sem in e["sems"]
+                ]
+            )
+
+    return _view_etuds_page(
+        context,
+        semset_id,
+        title=title,
+        etuds=etuds.values(),
+        keys=("nip", "etape_apo", "nom", "prenom", "inscriptions_scodoc"),
+        format=format,
+        REQUEST=REQUEST,
+    )
+
+
+def view_scodoc_etuds(
+    context, semset_id, title="", etudids=None, nips=None, format="html", REQUEST=None
+):
+    """Table des étudiants ScoDoc par nips ou etudids
+    """
+    if etudids is not None:
+        if type(etudids) != type([]):
+            etudids = [etudids]
+        etuds = [
+            context.getEtudInfo(etudid=etudid, filled=True)[0] for etudid in etudids
+        ]
+    elif nips is not None:
+        if type(nips) != type([]):
+            nips = [nips]
+        etuds = [context.getEtudInfo(code_nip=nip, filled=True)[0] for nip in nips]
+    else:
+        raise ValueError("etudid or NIP must be specified")
+
+    for e in etuds:
+        tgt = "ficheEtud?etudid=" + e["etudid"]
+        e["_nom_target"] = tgt
+        e["_prenom_target"] = tgt
+        e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],)
+        e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
+
+    return _view_etuds_page(
+        context,
+        semset_id,
+        title=title,
+        etuds=etuds,
+        keys=("code_nip", "nom", "prenom"),
+        format=format,
+        REQUEST=REQUEST,
+    )
+
+
+def _view_etuds_page(
+    context, semset_id, title="", etuds=[], keys=(), format="html", REQUEST=None
+):
+    # Tri les étudiants par nom:
+    if etuds:
+        etuds.sort(key=lambda x: (x["nom"], x["prenom"]))
+
+    H = [
+        context.sco_header(
+            REQUEST, page_title=title, init_qtip=True, javascripts=["js/etud_info.js"]
+        ),
+        "<h2>%s</h2>" % title,
+    ]
+
+    tab = GenTable(
+        titles={
+            "nip": "Code NIP",
+            "code_nip": "Code NIP",
+            "etape_apo": "Etape",
+            "nom": "Nom",
+            "prenom": "Prénom",
+            "inscriptions_scodoc": "Inscriptions ScoDoc",
+        },
+        columns_ids=keys,
+        rows=etuds,
+        html_sortable=True,
+        html_class="table_leftalign",
+        filename="students_apo",
+        preferences=context.get_preferences(),
+    )
+    if format != "html":
+        return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+    H.append(tab.html())
+
+    H.append(
+        """<p><a href="apo_semset_maq_status?semset_id=%s">Retour à la page d'export Apogée</a>"""
+        % semset_id
+    )
+
+    return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+def view_apo_csv_store(
+    context, semset_id="", csvfile=None, data="", autodetect=False, REQUEST=None
+):
+    """Store CSV data
+    Le semset identifie l'annee scolaire et le semestre
+    Si csvfile, lit depuis FILE, sinon utilise data
+    """
+    if not semset_id:
+        raise ValueError("invalid null semset_id")
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+
+    if csvfile:
+        data = csvfile.read()
+        if autodetect:
+            # check encoding (although documentation states that users SHOULD upload LATIN1)
+            data, message = sco_apogee_csv.fix_data_encoding(data)
+            if message:
+                log("view_apo_csv_store: %s" % message)
+        else:
+            log("view_apo_csv_store: autodetection of encoding disabled by user")
+    if not data:
+        raise ScoValueError("view_apo_csv_store: no data")
+
+    # check si etape maquette appartient bien au semset
+    apo_data = sco_apogee_csv.ApoData(
+        data, periode=semset["sem_id"]
+    )  # parse le fichier -> exceptions
+    if apo_data.etape not in semset["etapes"]:
+        raise ScoValueError(
+            "Le code étape de ce fichier ne correspond pas à ceux de cet ensemble"
+        )
+
+    sco_etape_apogee.apo_csv_store(
+        context, data, semset["annee_scolaire"], semset["sem_id"]
+    )
+
+    return REQUEST.RESPONSE.redirect("apo_semset_maq_status?semset_id=" + semset_id)
+
+
+def view_apo_csv_download_and_store(context, etape_apo="", semset_id="", REQUEST=None):
+    """Download maquette and store it
+    """
+    if not semset_id:
+        raise ValueError("invalid null semset_id")
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+
+    data = sco_portal_apogee.get_maquette_apogee(
+        context, etape=etape_apo, annee_scolaire=semset["annee_scolaire"]
+    )
+    # here, data is utf8
+    # but we store and generate latin1 files, to ease further import in Apogée
+    data = data.decode(APO_PORTAL_ENCODING).encode(APO_INPUT_ENCODING)
+    return view_apo_csv_store(
+        context, semset_id, data=data, autodetect=False, REQUEST=REQUEST
+    )
+
+
+def view_apo_csv_delete(
+    context, etape_apo="", semset_id="", dialog_confirmed=False, REQUEST=None
+):
+    """Delete CSV file
+    """
+    if not semset_id:
+        raise ValueError("invalid null semset_id")
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    dest_url = "apo_semset_maq_status?semset_id=" + semset_id
+    if not dialog_confirmed:
+        return context.confirmDialog(
+            """<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
+               <p>La suppression sera définitive.</p>"""
+            % (etape_apo,),
+            dest_url="",
+            REQUEST=REQUEST,
+            cancel_url=dest_url,
+            parameters={"semset_id": semset_id, "etape_apo": etape_apo},
+        )
+
+    info = sco_etape_apogee.apo_csv_get_archive(
+        context, etape_apo, semset["annee_scolaire"], semset["sem_id"]
+    )
+    sco_etape_apogee.apo_csv_delete(context, info["archive_id"])
+    return REQUEST.RESPONSE.redirect(dest_url + "&amp;head_message=Archive%20supprimée")
+
+
+def view_apo_csv(context, etape_apo="", semset_id="", format="html", REQUEST=None):
+    """Visualise une maquette stockée
+    Si format="raw", renvoie le fichier maquette tel quel
+    """
+    if not semset_id:
+        raise ValueError("invalid null semset_id")
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    annee_scolaire = semset["annee_scolaire"]
+    sem_id = semset["sem_id"]
+    csv_data = sco_etape_apogee.apo_csv_get(context, etape_apo, annee_scolaire, sem_id)
+    if format == "raw":
+        return sendCSVFile(REQUEST, csv_data, etape_apo + ".txt")
+    apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
+
+    (
+        ok_for_export,
+        etapes_missing_csv,
+        etuds_without_nip,
+        nips_ok,
+        nips_no_apo,
+        nips_no_sco,
+        apo_dups,
+        maq_elems,
+        sem_elems,
+    ) = sco_etape_apogee.apo_csv_semset_check(context, semset)
+
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Maquette Apogée enregistrée pour %s" % etape_apo,
+            init_qtip=True,
+            javascripts=["js/etud_info.js"],
+        ),
+        """<h2>Etudiants dans la maquette Apogée %s</h2>""" % etape_apo,
+        """<p>Pour l'ensemble <a class="stdlink" href="apo_semset_maq_status?semset_id=%(semset_id)s">%(title)s</a> (indice semestre: %(sem_id)s)</p>"""
+        % semset,
+    ]
+    # Infos générales
+    H.append(
+        """
+    <div class="apo_csv_infos">
+    <div class="apo_csv_etape"><span>Code étape:</span><span>{0.etape_apogee} VDI {0.vdi_apogee} (année {0.annee_scolaire})</span></div>
+    </div>
+    """.format(
+            apo_data
+        )
+    )
+
+    # Liste des étudiants (sans les résultats pour le moment): TODO
+    etuds = apo_data.etuds
+    if not etuds:
+        return "\n".join(H) + "<p>Aucun étudiant</p>" + context.sco_footer(REQUEST)
+
+    # Ajout infos sur ScoDoc vs Apogee
+    for e in etuds:
+        e["in_scodoc"] = e["nip"] not in nips_no_sco
+        e["in_scodoc_str"] = {True: "oui", False: "non"}[e["in_scodoc"]]
+        if e["in_scodoc"]:
+            e["_in_scodoc_str_target"] = "ficheEtud?code_nip=" + e["nip"]
+            e.update(context.getEtudInfo(code_nip=e["nip"], filled=True)[0])
+            e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],)
+            e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
+        else:
+            e["_css_row_class"] = "apo_not_scodoc"
+
+    # Construit la table:
+    tab = GenTable(
+        titles={
+            "nip": "Code NIP",
+            "nom": "Nom",
+            "prenom": "Prénom",
+            "naissance": "Naissance",
+            "in_scodoc_str": "Inscrit dans ces semestres ScoDoc",
+        },
+        columns_ids=("nip", "nom", "prenom", "naissance", "in_scodoc_str"),
+        rows=etuds,
+        html_sortable=True,
+        html_class="table_leftalign apo_maq_table",
+        base_url="%s?etape_apo=%s&semset_id=%s" % (REQUEST.URL0, etape_apo, semset_id),
+        filename="students_" + etape_apo,
+        caption="Etudiants Apogée en " + etape_apo,
+        preferences=context.get_preferences(),
+    )
+
+    if format != "html":
+        return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+    H += [
+        tab.html(),
+        """<p><a class="stdlink" href="view_apo_csv?etape_apo=%s&semset_id=%s&format=raw">fichier maquette CSV brut (non rempli par ScoDoc)</a></p>"""
+        % (etape_apo, semset_id),
+        """<div><a class="stdlink" href="apo_semset_maq_status?semset_id=%s">Retour</a>    
+        </div>"""
+        % semset_id,
+        context.sco_footer(REQUEST),
+    ]
+
+    return "\n".join(H)
+
+
+# called from Web
+def apo_csv_export_results(
+    context,
+    semset_id,
+    block_export_res_etape=False,
+    block_export_res_sem=False,
+    block_export_res_ues=False,
+    block_export_res_modules=False,
+    block_export_res_sdj=False,
+    REQUEST=None,
+):
+    """Remplit les fichiers CSV archivés
+    et donne un ZIP avec tous les résultats.
+    """
+    # nota: on peut éventuellement exporter même si tout n'est pas ok
+    # mais le lien via le tableau de bord n'est pas actif
+    # Les fichiers résultats ne sont pas stockés: pas besoin de permission particulière
+    prefs = context.get_preferences()
+    export_res_etape = prefs["export_res_etape"] and not int(block_export_res_etape)
+    export_res_sem = prefs["export_res_sem"] and not int(block_export_res_sem)
+    export_res_ues = prefs["export_res_ues"] and not int(block_export_res_ues)
+    export_res_modules = prefs["export_res_modules"] and not int(
+        block_export_res_modules
+    )
+    export_res_sdj = prefs["export_res_sdj"] and not int(block_export_res_sdj)
+    export_res_rat = prefs["export_res_rat"]
+
+    if not semset_id:
+        raise ValueError("invalid null semset_id")
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    annee_scolaire = semset["annee_scolaire"]
+    periode = semset["sem_id"]
+
+    data = StringIO()
+    dest_zip = ZipFile(data, "w")
+
+    etapes_apo = sco_etape_apogee.apo_csv_list_stored_etapes(
+        context, annee_scolaire, periode, etapes=semset.list_etapes()
+    )
+    for etape_apo in etapes_apo:
+        apo_csv = sco_etape_apogee.apo_csv_get(
+            context, etape_apo, annee_scolaire, periode
+        )
+        sco_apogee_csv.export_csv_to_apogee(
+            context,
+            apo_csv,
+            periode=periode,
+            export_res_etape=export_res_etape,
+            export_res_sem=export_res_sem,
+            export_res_ues=export_res_ues,
+            export_res_modules=export_res_modules,
+            export_res_sdj=export_res_sdj,
+            export_res_rat=export_res_rat,
+            dest_zip=dest_zip,
+            REQUEST=REQUEST,
+        )
+
+    basename = (
+        context.get_preference("DeptName")
+        + str(annee_scolaire)
+        + "-%s-" % periode
+        + "-".join(etapes_apo)
+    )
+    basename = sco_archives.sanitize_filename(unescape_html(basename))
+
+    dest_zip.close()
+    size = data.tell()
+    content_type = "application/zip"
+    REQUEST.RESPONSE.setHeader(
+        "content-disposition", 'attachement; filename="%s.zip"' % basename
+    )
+    REQUEST.RESPONSE.setHeader("content-type", content_type)
+    REQUEST.RESPONSE.setHeader("content-length", size)
+    return data.getvalue()
diff --git a/sco_etape_bilan.py b/sco_etape_bilan.py
new file mode 100644
index 0000000000000000000000000000000000000000..0957a3d35048f852b4aeb2a2330e71a0266da076
--- /dev/null
+++ b/sco_etape_bilan.py
@@ -0,0 +1,768 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2019 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
+#
+##############################################################################
+
+"""
+# Outil de comparaison Apogée/ScoDoc (J.-M. Place, Jan 2020)
+
+## fonctionalités
+
+Le menu 'synchronisation avec Apogée' ne permet pas de traiter facilement les cas
+où un même code étape est implementé dans des semestres (au sens ScoDoc) différents.
+
+La proposition est d'ajouter à la page de description des ensembles de semestres 
+une section permettant de faire le point sur les cas particuliers.
+
+Cette section est composée de deux parties:
+* Une partie effectif où figurent le nombre d'étudiants selon un répartition par  
+ semestre (en ligne) et par code étape (en colonne). On ajoute également des 
+ colonnes/lignes correspondant à des anomalies (étudiant sans code étape, sans
+ semestre, avec deux semestres, sans NIP, etc.).
+ 
+ * La seconde partie présente la liste des étudiants. Il est possible qu'un 
+  même nom figure deux fois dans la liste (si on a pas pu faire la correspondance
+  entre une inscription apogée et un étudiant d'un semestre, par exemple).
+ 
+ L'activation d'un des nombres du tableau 'effectifs' restreint l'affichage de 
+ la liste aux étudiants qui contribuent à ce nombre.
+
+## Réalisation
+
+Les modifications logicielles portent sur:
+
+### La création d'une classe sco_etape_bilan.py
+
+Cette classe compile la totalité des données:
+
+** Liste des semestres
+
+** Listes des étapes
+
+** Liste des étudiants
+
+** constitution des listes d'anomalies
+
+Cette classe explore la suite semestres du semset.
+Pour chaque semestre, elle recense les étudiants du semestre et 
+les codes étapes concernés.
+
+puis tous les codes étapes (toujours en important les étudiants de l'étape
+via le portail)
+
+enfin on dispatch chaque étudiant dans une case - soit ordinaire, soit 
+correspondant à une anomalie.
+
+### Modification de sco_etape_apogee_view.py
+
+Pour insertion de l'affichage ajouté
+
+### Modification de sco_semset.py
+
+Affichage proprement dit
+
+### Modification de scp_formsemestre.py
+
+Modification/ajout de la méthode sem_in_semestre_scolaire pour permettre 
+l'inscrition de semestres décalés (S1 en septembre, ...).
+Le filtrage s'effctue sur la date et non plus sur la parité du semestre (1-3/2-4).
+"""
+
+
+from sco_portal_apogee import get_inscrits_etape
+from notes_log import log
+from sco_utils import annee_scolaire_debut
+from gen_tables import GenTable
+
+COL_PREFIX = "COL_"
+
+# Les indicatifs sont des marqueurs de classe CSS insérés dans la table étudiant
+# et utilisés par le javascript pour permettre un filtrage de la liste étudiants
+#  sur un 'cas' considéré
+
+# indicatifs
+COL_CUMUL = "C9"
+ROW_CUMUL = "R9"
+
+# Constante d'anomalie
+PAS_DE_NIP = "C1"
+PAS_D_ETAPE = "C2"
+PLUSIEURS_ETAPES = "C3"
+PAS_DE_SEMESTRE = "R4"
+PLUSIEURS_SEMESTRES = "R5"
+NIP_NON_UNIQUE = "U"
+
+FLAG = {
+    PAS_DE_NIP: "A",
+    PAS_D_ETAPE: "B",
+    PLUSIEURS_ETAPES: "C",
+    PAS_DE_SEMESTRE: "D",
+    PLUSIEURS_SEMESTRES: "E",
+    NIP_NON_UNIQUE: "U",
+}
+
+
+class DataEtudiant:
+    """
+    Structure de donnée des informations pour un étudiant
+    """
+
+    def __init__(self, nip="", etudid=""):
+        self.nip = nip
+        self.etudid = etudid
+        self.data_apogee = None
+        self.data_scodoc = None
+        self.etapes = set()  # l'ensemble des étapes où il est inscrit
+        self.semestres = set()  # l'ensemble des semestres où il est inscrit
+        self.tags = set()  # les anomalies relevées
+        self.ind_row = "-"  # là où il compte dans les effectifs (ligne et colonne)
+        self.ind_col = "-"
+
+    def add_etape(self, etape):
+        self.etapes.add(etape)
+
+    def add_semestre(self, semestre):
+        self.semestres.add(semestre)
+
+    def set_apogee(self, data_apogee):
+        self.data_apogee = data_apogee
+
+    def set_scodoc(self, data_scodoc):
+        self.data_scodoc = data_scodoc
+
+    def add_tag(self, tag):
+        self.tags.add(tag)
+
+    def set_ind_row(self, indicatif):
+        self.ind_row = indicatif
+
+    def set_ind_col(self, indicatif):
+        self.ind_col = indicatif
+
+    def get_identity(self):
+        """
+        Calcul le nom/prénom de l'étudiant (données ScoDoc en priorité, sinon données Apogée)
+        :return: L'identité calculée
+        """
+        if self.data_scodoc is not None:
+            return self.data_scodoc["nom"] + self.data_scodoc["prenom"]
+        else:
+            return self.data_apogee["nom"] + self.data_apogee["prenom"]
+
+
+def help():
+    return """
+    <div id="export_help" class="pas_help"> <span>Explications sur les tableaux des effectifs et liste des 
+    étudiants</span> 
+        <div> <p>Le tableau des effectifs présente le nombre d'étudiants selon deux critères:</p> 
+        <ul> 
+        <li>En colonne le statut de l'étudiant par rapport à Apogée: 
+            <ul> 
+                <li><span class="libelle">Hors Apogée</span> 
+                    <span class="anomalie">(anomalie A</span>): Le NIP de l'étudiant n'est pas connu d'apogée ou 
+                l'étudiant n'a pas de NIP</li> 
+                <li><span class="libelle">Pas d'étape</span> <span class="anomalie">(anomalie B</span>): Le NIP de 
+                    l'étudiant ne correspond à aucune des étapes connues pour cet ensemble de semestre. Il est 
+                    possible qu'il soit inscrit ailleurs (dans une autre ensemble de semestres, un autre département, 
+                    une autre composante de l'université) ou en mobilité internationale.</li> 
+                <li><span class="libelle">Plusieurs étapes</span> <span class="anomalie">(anomalie C)</span>: 
+                    Les étudiants inscrits dans plusieurs étapes apogée de l'ensemble de semestres</li> 
+                <li>Un des codes étapes connus (la liste des codes étapes connus est l'union des codes étapes 
+                    déclarés pour chaque semestre particpant</li> 
+                <li><span class="libelle">Total semestre</span>: cumul des effectifs de la ligne</li> 
+            </ul> 
+            </li> 
+        <li>En ligne le statut de l'étudiant par rapport à ScoDoc: 
+            <ul> 
+                <li>Inscription dans un des semestres de l'ensemble</li> 
+                <li><span class="libelle">Hors semestre</span> <span class="anomalie">(anomalie D)</span>: 
+                    L'étudiant, bien qu'enregistré par apogée dans un des codes étapes connus, ne figure dans aucun 
+                    des semestres de l'ensemble. On y trouve par exemple les étudiants régulièrement inscrits 
+                    mais non présents à la rentrée (donc non enregistrés dans ScoDoc) <p>Note: On ne considère 
+                    ici que les semestres de l'ensemble (l'inscription de l'étudiant dans un semestre étranger à 
+                    l'ensemble actuel n'est pas vérifiée).</p> </li> 
+                <li><span class="libelle">Plusieurs semestres</span> <span class="anomalie">(anomalie E)</span>: 
+                    L'étudiant est enregistré dans plusieurs semestres de l'ensemble.</li> 
+                <li><span class="libelle">Total</span>: cumul des effectifs de la colonne</li> 
+            </ul> 
+            </li> 
+        <li>(<span class="anomalie">anomalie U</span>) On présente également les cas où un même NIP est affecté 
+            à deux dossiers différents (Un dossier d'apogée et un dossier de ScoDoc). Un tel cas compte pour 
+            deux unités dans le tableau des effcetifs et engendre 2 lignes distinctes dans la liste des étudiants</li> 
+        </ul> 
+        </div> 
+    </div> """
+
+
+def entete_liste_etudiant():
+    return """
+            <h4 id='effectifs'>Liste des étudiants <span id='compte'></span>
+                <ul>
+                <li id='sans_filtre'>Pas de filtrage: Cliquez sur un des nombres du tableau ci-dessus pour 
+                    n'afficher que les étudiants correspondants</li>
+                <li id='filtre_row' style='display:none'></li>
+                <li id='filtre_col' style='display:none'></li>
+                </ul>
+            </h4>
+    """
+
+
+class EtapeBilan:
+    """
+    Structure de donnée représentation l'état global de la comparaison ScoDoc/Apogée
+    """
+
+    def __init__(self, context):
+        self.context = context
+        self.semestres = (
+            {}
+        )  # Dictionnaire des formsemestres du semset (formsemestre_id -> semestre)
+        self.etapes = []  # Liste des étapes apogées du semset (clé_apogée)
+        # pour les descriptions qui suivents:
+        #   cle_etu = nip si non vide, sinon etudid
+        #   data_etu = { nip, etudid, data_apogee, data_scodoc }
+        self.etudiants = {}  # cle_etu -> data_etu
+        self.keys_etu = {}  # nip -> [ etudid* ]
+        self.etu_semestre = {}  # semestre -> { key_etu }
+        self.etu_etapes = {}  # etape -> { key_etu }
+        self.repartition = {}  # (ind_row, ind_col) -> nombre d étudiants
+        self.tag_count = {}  # nombre d'animalies détectées (par type d'anomalie)
+
+        # on collectionne les indicatifs trouvés pour n'afficher que les indicatifs 'utiles'
+        self.indicatifs = {}
+        self.top_row = 0
+        self.top_col = 0
+        self.all_rows_ind = [PAS_DE_SEMESTRE, PLUSIEURS_SEMESTRES]
+        self.all_cols_ind = [PAS_DE_NIP, PAS_D_ETAPE, PLUSIEURS_ETAPES]
+        self.all_rows_str = None
+        self.all_cols_str = None
+        self.titres = {
+            PAS_DE_NIP: "PAS_DE_NIP",
+            PAS_D_ETAPE: "PAS_D_ETAPE",
+            PLUSIEURS_ETAPES: "PLUSIEURS_ETAPES",
+            PAS_DE_SEMESTRE: "PAS_DE_SEMESTRE",
+            PLUSIEURS_SEMESTRES: "PLUSIEURS_SEMESTRES",
+            NIP_NON_UNIQUE: "NIP_NON_UNIQUE",
+        }
+
+    def inc_tag_count(self, tag):
+        if tag not in self.tag_count:
+            self.tag_count[tag] = 0
+        self.tag_count[tag] += 1
+
+    def set_indicatif(self, item, as_row):  # item = semestre ou key_etape
+        if as_row:
+            indicatif = "R" + chr(self.top_row + 97)
+            self.all_rows_ind.append(indicatif)
+            self.top_row += 1
+        else:
+            indicatif = "C" + chr(self.top_col + 97)
+            self.all_cols_ind.append(indicatif)
+            self.top_col += 1
+        self.indicatifs[item] = indicatif
+        if self.top_row > 26:
+            log("Dépassement (plus de 26 semestres dans la table diagnostic")
+        if self.top_col > 26:
+            log("Dépassement (plus de 26 étapes dans la table diagnostic")
+
+    def add_sem(self, semestre):
+        """
+        Prise en compte d'un semestre dans le bilan.
+        * ajoute le semestre et les étudiants du semestre
+        * ajoute les étapes du semestre et (via portail) les étudiants pour ces codes étapes
+        :param semestre: Le semestre à prendre en compte
+        :return: None
+        """
+        self.semestres[semestre["formsemestre_id"]] = semestre
+        # if anneeapogee == None:  # année d'inscription par défaut
+        anneeapogee = str(
+            annee_scolaire_debut(semestre["annee_debut"], semestre["mois_debut_ord"])
+        )
+        self.set_indicatif(semestre["formsemestre_id"], True)
+        for etape in semestre["etapes"]:
+            self.add_etape(etape.etape_vdi, anneeapogee)
+
+    def add_etape(self, etape_str, anneeapogee):
+        """
+        Prise en compte d'une étape apogée
+        :param etape_str: La clé de l'étape à prendre en compte
+        :param anneeapogee:  l'année de l'étape à prendre en compte
+        :return: None
+        """
+        if etape_str != "":
+            key_etape = etape_to_key(anneeapogee, etape_str)
+            if key_etape not in self.etapes:
+                self.etapes.append(key_etape)
+                self.set_indicatif(
+                    key_etape, False
+                )  # ajout de la colonne/indicatif supplémentaire
+
+    def compute_key_etu(self, nip, etudid):
+        """
+        Calcul de la clé étudiant:
+        * Le nip si il existe
+        * sinon l'identifiant ScoDoc
+        Tient à jour le dictionnaire key_etu (référentiel des étudiants)
+        La problèmatique est de gérer toutes les anomalies possibles:
+        - étudiant sans nip,
+        - plusieurs étudiants avec le même nip,
+        - etc.
+        :param nip: le nip de l'étudiant
+        :param etudid: l'identifiant ScoDoc
+        :return: L'identifiant unique de l'étudiant
+        """
+        if nip not in self.keys_etu:
+            self.keys_etu[nip] = []
+        if etudid not in self.keys_etu[nip]:
+            if etudid is None:
+                if len(self.keys_etu[nip]) == 1:
+                    etudid = self.keys_etu[nip][0]
+                else:  # nip non trouvé ou utilisé par plusieurs étudiants
+                    self.keys_etu[nip].append(None)
+            else:
+                self.keys_etu[nip].append(etudid)
+        return nip, etudid
+
+    def register_etud_apogee(self, etud, etape):
+        """
+        Enregistrement des données de l'étudiant par rapport à apogée.
+        L'étudiant peut avoir été déjà enregistré auparavant (par exemple connu par son semestre)
+        Dans ce cas, on ne met à jour que son association à l'étape apogée
+        :param etud: les données étudiant
+        :param etape:  l'étape apogée
+        :return:
+        """
+        nip = etud["nip"]
+        key_etu = self.compute_key_etu(nip, None)
+        if key_etu not in self.etudiants:
+            data = DataEtudiant(nip)
+            data.set_apogee(etud)
+            data.add_etape(etape)
+            self.etudiants[key_etu] = data
+        else:
+            self.etudiants[key_etu].set_apogee(etud)
+            self.etudiants[key_etu].add_etape(etape)
+        return key_etu
+
+    def register_etud_scodoc(self, etud, semestre):
+        """
+        Enregistrement de l'étudiant par rapport à son semestre
+        :param etud: Les données de l'étudiant
+        :param semestre:  Le semestre où il est à enregistrer
+        :return: la clé unique pour cet étudiant
+        """
+        nip = etud["code_nip"]
+        etudid = etud["etudid"]
+        key_etu = self.compute_key_etu(nip, etudid)
+        if key_etu not in self.etudiants:
+            data = DataEtudiant(nip, etudid)
+            data.set_scodoc(etud)
+            data.add_semestre(semestre)
+            self.etudiants[key_etu] = data
+        else:
+            self.etudiants[key_etu].add_semestre(semestre)
+        return key_etu
+
+    def load_listes(self):
+        """
+        Inventaire complet des étudiants:
+        * Pour tous les semestres d'abord
+        * Puis pour toutes les étapes
+        :return:  None
+        """
+        for semestre in self.semestres:
+            etuds = self.semestres[semestre]["etuds"]
+            self.etu_semestre[semestre] = set()
+            for etud in etuds:
+                key_etu = self.register_etud_scodoc(etud, semestre)
+                self.etu_semestre[semestre].add(key_etu)
+
+        for key_etape in self.etapes:
+            anneeapogee, etapestr = key_to_values(key_etape)
+            self.etu_etapes[key_etape] = set()
+            for etud in get_inscrits_etape(self.context, etapestr, anneeapogee):
+                key_etu = self.register_etud_apogee(etud, key_etape)
+                self.etu_etapes[key_etape].add(key_etu)
+
+    def dispatch(self):
+        """
+        Réparti l'ensemble des étudiants selon les lignes (semestres) et les colonnes (étapes).
+
+        :return:  None
+        """
+        # Initialisation des cumuls
+        self.repartition[ROW_CUMUL, COL_CUMUL] = 0
+        self.repartition[PAS_DE_SEMESTRE, COL_CUMUL] = 0
+        self.repartition[PLUSIEURS_SEMESTRES, COL_CUMUL] = 0
+        self.repartition[ROW_CUMUL, PAS_DE_NIP] = 0
+        self.repartition[ROW_CUMUL, PAS_D_ETAPE] = 0
+        self.repartition[ROW_CUMUL, PLUSIEURS_ETAPES] = 0
+        for semestre in self.semestres:
+            self.repartition[self.indicatifs[semestre], COL_CUMUL] = 0
+        for key_etape in self.etapes:
+            self.repartition[ROW_CUMUL, self.indicatifs[key_etape]] = 0
+
+        # recherche des nip identiques
+        for nip in self.keys_etu:
+            if nip != "":
+                nbnips = len(self.keys_etu[nip])
+                if nbnips > 1:
+                    for i, etudid in enumerate(self.keys_etu[nip]):
+                        data_etu = self.etudiants[nip, etudid]
+                        data_etu.add_tag(NIP_NON_UNIQUE)
+                        data_etu.nip = data_etu.nip + "&nbsp;(%d/%d)" % (i + 1, nbnips)
+                        self.inc_tag_count(NIP_NON_UNIQUE)
+        for nip in self.keys_etu:
+            for etudid in self.keys_etu[nip]:
+                key_etu = (nip, etudid)
+                data_etu = self.etudiants[key_etu]
+                ind_col = "-"
+                ind_row = "-"
+
+                # calcul de la colonne
+                if len(data_etu.etapes) == 1:
+                    ind_col = self.indicatifs[list(data_etu.etapes)[0]]
+                elif nip == "":
+                    data_etu.add_tag(FLAG[PAS_DE_NIP])
+                    ind_col = PAS_DE_NIP
+                elif len(data_etu.etapes) == 0:
+                    self.etudiants[key_etu].add_tag(FLAG[PAS_D_ETAPE])
+                    ind_col = PAS_D_ETAPE
+                if len(data_etu.etapes) > 1:
+                    data_etu.add_tag(FLAG[PLUSIEURS_ETAPES])
+                    ind_col = PLUSIEURS_ETAPES
+
+                if len(data_etu.semestres) == 1:
+                    ind_row = self.indicatifs[list(data_etu.semestres)[0]]
+                elif len(data_etu.semestres) > 1:
+                    data_etu.add_tag(FLAG[PLUSIEURS_SEMESTRES])
+                    ind_row = PLUSIEURS_SEMESTRES
+                elif len(data_etu.semestres) < 1:
+                    self.etudiants[key_etu].add_tag(FLAG[PAS_DE_SEMESTRE])
+                    ind_row = PAS_DE_SEMESTRE
+
+                data_etu.set_ind_col(ind_col)
+                data_etu.set_ind_row(ind_row)
+                self._inc_count(ind_row, ind_col)
+                self.inc_tag_count(ind_row)
+                self.inc_tag_count(ind_col)
+
+    def html_diagnostic(self):
+        """
+        affichage de l'html
+        :return: Le code html à afficher
+        """
+        self.load_listes()  # chargement des données
+        self.dispatch()  # analyse et répartition
+        # calcul de la liste des colonnes et des lignes de la table des effectifs
+        self.all_rows_str = "'" + ",".join(["." + r for r in self.all_rows_ind]) + "'"
+        self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'"
+
+        H = [
+            '<div id="synthese" class=u"semset_description"><h4>Tableau des effectifs</h4>',
+            self._diagtable(),
+            self.display_tags(),
+            entete_liste_etudiant(),
+            self.table_effectifs(),
+            help(),
+        ]
+
+        return "\n".join(H)
+
+    def _inc_count(self, ind_row, ind_col):
+        if (ind_row, ind_col) not in self.repartition:
+            self.repartition[ind_row, ind_col] = 0
+        self.repartition[ind_row, ind_col] += 1
+        self.repartition[ROW_CUMUL, ind_col] += 1
+        self.repartition[ind_row, COL_CUMUL] += 1
+        self.repartition[ROW_CUMUL, COL_CUMUL] += 1
+
+    def _get_count(self, ind_row, ind_col):
+        if (ind_row, ind_col) in self.repartition:
+            count = self.repartition[ind_row, ind_col]
+            if count > 1:
+                comptage = "(%d étudiants)" % count
+            else:
+                comptage = "(1 étudiant)"
+        else:
+            count = 0
+            return ""
+
+        # Ajoute l'appel à la routine javascript de filtrage (apo_semset_maq_status.js
+        # signature:
+        #   function show_css(elt, all_rows, all_cols, row, col, precision)
+        #      elt: le lien cliqué
+        #      all_rows: la liste de toutes les lignes existantes dans le tableau répartition
+        #           (exemple: ".Rb,.R1,.R2,.R3")
+        #      all_cols: la liste de toutes les colonnes existantes dans le tableau répartition
+        #           (exemple: ".Ca,.C1,.C2,.C3")
+        #      row: la ligne sélectionnée (sélecteur css) (expl: ".R1")
+        #            ; '*' si pas de sélection sur la ligne
+        #      col: la (les) colonnes sélectionnées (sélecteur css) (exple: ".C2")
+        #            ; '*' si pas de sélection sur colonne
+        #      precision: ajout sur le titre (en général, le nombre d'étudiant)
+        #      filtre_row: explicitation du filtre ligne éventuelle
+        #      filtre_col: explicitation du filtre colonne évnetuelle
+        if ind_row == ROW_CUMUL and ind_col == COL_CUMUL:
+            javascript = "doFiltrage(%s, %s, '*', '*', '%s', '%s', '%s');" % (
+                self.all_rows_str,
+                self.all_cols_str,
+                comptage,
+                "",
+                "",
+            )
+        elif ind_row == ROW_CUMUL:
+            javascript = "doFiltrage(%s, %s, '*', '.%s', '%s', '%s', '%s');" % (
+                self.all_rows_str,
+                self.all_cols_str,
+                ind_col,
+                comptage,
+                "",
+                self.titres[ind_col].replace("<br/>", " / "),
+            )
+        elif ind_col == COL_CUMUL:
+            javascript = "doFiltrage(%s, %s, '.%s', '*', '%s', '%s', '%s');" % (
+                self.all_rows_str,
+                self.all_cols_str,
+                ind_row,
+                " (%d étudiants)" % count,
+                self.titres[ind_row],
+                "",
+            )
+        else:
+            javascript = "doFiltrage(%s, %s, '.%s', '.%s', '%s', '%s', '%s');" % (
+                self.all_rows_str,
+                self.all_cols_str,
+                ind_row,
+                ind_col,
+                comptage,
+                self.titres[ind_row],
+                self.titres[ind_col].replace("<br/>", " / "),
+            )
+        return "<a href='#synthese' onclick=\"%s\">%d</a>" % (javascript, count)
+
+    def _diagtable(self):
+        H = []
+
+        liste_semestres = sorted(self.semestres.keys())
+        liste_etapes = []
+        for key_etape in self.etapes:
+            liste_etapes.append(key_etape)
+        liste_etapes.sort(key=lambda key: etape_to_col(key_etape))
+
+        col_ids = []
+        if PAS_DE_NIP in self.tag_count:
+            col_ids.append(PAS_DE_NIP)
+        if PAS_D_ETAPE in self.tag_count:
+            col_ids.append(PAS_D_ETAPE)
+        if PLUSIEURS_ETAPES in self.tag_count:
+            col_ids.append(PLUSIEURS_ETAPES)
+        self.titres["row_title"] = "Semestre"
+        self.titres[PAS_DE_NIP] = "Hors Apogée (" + FLAG[PAS_DE_NIP] + ")"
+        self.titres[PAS_D_ETAPE] = "Pas d'étape (" + FLAG[PAS_D_ETAPE] + ")"
+        self.titres[PLUSIEURS_ETAPES] = (
+            "Plusieurs etapes (" + FLAG[PLUSIEURS_ETAPES] + ")"
+        )
+        for key_etape in liste_etapes:
+            col_id = self.indicatifs[key_etape]
+            col_ids.append(col_id)
+            self.titres[col_id] = "%s<br/>%s" % key_to_values(key_etape)
+        col_ids.append(COL_CUMUL)
+        self.titres[COL_CUMUL] = "Total<br/>semestre"
+
+        rows = []
+        for semestre in liste_semestres:
+            ind_row = self.indicatifs[semestre]
+            self.titres[ind_row] = (
+                "%(titre_num)s (%(formsemestre_id)s)" % self.semestres[semestre]
+            )
+            row = {
+                "row_title": self.link_semestre(semestre),
+                PAS_DE_NIP: self._get_count(ind_row, PAS_DE_NIP),
+                PAS_D_ETAPE: self._get_count(ind_row, PAS_D_ETAPE),
+                PLUSIEURS_ETAPES: self._get_count(ind_row, PLUSIEURS_ETAPES),
+                COL_CUMUL: self._get_count(ind_row, COL_CUMUL),
+                "_css_row_class": ind_row,
+            }
+            for key_etape in liste_etapes:
+                ind_col = self.indicatifs[key_etape]
+                row[ind_col] = self._get_count(ind_row, ind_col)
+            rows.append(row)
+
+        if PAS_DE_SEMESTRE in self.tag_count:
+            row = {
+                "row_title": "Hors semestres (" + FLAG[PAS_DE_SEMESTRE] + ")",
+                PAS_DE_NIP: "",
+                PAS_D_ETAPE: "",
+                PLUSIEURS_ETAPES: "",
+                COL_CUMUL: self._get_count(PAS_DE_SEMESTRE, COL_CUMUL),
+                "_css_row_class": PAS_DE_SEMESTRE,
+            }
+            for key_etape in liste_etapes:
+                ind_col = self.indicatifs[key_etape]
+                row[ind_col] = self._get_count(PAS_DE_SEMESTRE, ind_col)
+            rows.append(row)
+
+        if PLUSIEURS_SEMESTRES in self.tag_count:
+            row = {
+                "row_title": "Plusieurs semestres (" + FLAG[PLUSIEURS_SEMESTRES] + ")",
+                PAS_DE_NIP: "",
+                PAS_D_ETAPE: "",
+                PLUSIEURS_ETAPES: "",
+                COL_CUMUL: self._get_count(PLUSIEURS_SEMESTRES, COL_CUMUL),
+                "_css_row_class": PLUSIEURS_SEMESTRES,
+            }
+            for key_etape in liste_etapes:
+                ind_col = self.indicatifs[key_etape]
+                row[ind_col] = self._get_count(PLUSIEURS_SEMESTRES, ind_col)
+            rows.append(row)
+
+        row = {
+            "row_title": "Total",
+            PAS_DE_NIP: self._get_count(ROW_CUMUL, PAS_DE_NIP),
+            PAS_D_ETAPE: self._get_count(ROW_CUMUL, PAS_D_ETAPE),
+            PLUSIEURS_ETAPES: self._get_count(ROW_CUMUL, PLUSIEURS_ETAPES),
+            COL_CUMUL: self._get_count(ROW_CUMUL, COL_CUMUL),
+            "_css_row_class": COL_CUMUL,
+        }
+        for key_etape in liste_etapes:
+            ind_col = self.indicatifs[key_etape]
+            row[ind_col] = self._get_count(ROW_CUMUL, ind_col)
+        rows.append(row)
+
+        H.append(
+            GenTable(
+                rows,
+                col_ids,
+                self.titres,
+                html_class="repartition",
+                html_with_td_classes=True,
+            ).gen(format="html")
+        )
+        return "\n".join(H)
+
+    def display_tags(self):
+        H = []
+        if NIP_NON_UNIQUE in self.tag_count:
+            H.append("<h4>Anomalies</h4>")
+            javascript = "show_tag(%s, %s, '%s');" % (
+                self.all_rows_str,
+                self.all_cols_str,
+                NIP_NON_UNIQUE,
+            )
+            H.append(
+                'Code(s) nip) partagé(s) par <a href="#synthèse" onclick="%s">%d</a> étudiants<br/>'
+                % (javascript, self.tag_count[NIP_NON_UNIQUE])
+            )
+        return "\n".join(H)
+
+    @staticmethod
+    def link_etu(etudid, nom):
+        return '<a class="stdlink" href="ficheEtud?etudid=%s">%s</a>' % (etudid, nom)
+
+    def link_semestre(self, semestre, short=False):
+        if short:
+            return (
+                '<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%('
+                "formsemestre_id)s</a> " % self.semestres[semestre]
+            )
+        else:
+            return (
+                '<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s'
+                " %(mois_debut)s - %(mois_fin)s)</a>" % self.semestres[semestre]
+            )
+
+    def table_effectifs(self):
+        H = []
+
+        col_ids = ["tag", "etudiant", "prenom", "nip", "semestre", "apogee", "annee"]
+        titles = {
+            "tag": "Etat",
+            "etudiant": "Nom",
+            "prenom": "Prenom",
+            "nip": "code nip",
+            "semestre": "semestre",
+            "annee": "année",
+            "apogee": "etape",
+        }
+        rows = []
+
+        for data_etu in sorted(
+            self.etudiants.values(), key=lambda etu: etu.get_identity()
+        ):
+            nip = data_etu.nip
+            etudid = data_etu.etudid
+            if data_etu.data_scodoc is None:
+                nom = data_etu.data_apogee["nom"]
+                prenom = data_etu.data_apogee["prenom"]
+                link = nom
+            else:
+                nom = data_etu.data_scodoc["nom"]
+                prenom = data_etu.data_scodoc["prenom"]
+                link = self.link_etu(etudid, nom)
+            tag = ", ".join([tag for tag in sorted(data_etu.tags)])
+            semestre = "<br/>".join(
+                [self.link_semestre(sem, True) for sem in data_etu.semestres]
+            )
+            annees = "<br/>".join([etape[0] for etape in data_etu.etapes])
+            etapes = "<br/>".join([etape[1] for etape in data_etu.etapes])
+            classe = data_etu.ind_row + data_etu.ind_col
+            if NIP_NON_UNIQUE in data_etu.tags:
+                classe += " " + NIP_NON_UNIQUE
+            row = {
+                "tag": tag,
+                "etudiant": link,
+                "prenom": prenom.capitalize(),
+                "nip": nip,
+                "semestre": semestre,
+                "annee": annees,
+                "apogee": etapes,
+                "_css_row_class": classe,
+            }
+            rows.append(row)
+
+        H.append(
+            GenTable(
+                rows,
+                col_ids,
+                titles,
+                table_id="detail",
+                html_class="table_leftalign",
+                html_sortable=True,
+            ).gen(format="html")
+        )
+        return "\n".join(H)
+
+
+def etape_to_key(anneeapogee, etapestr):
+    return anneeapogee, etapestr
+
+
+def key_to_values(key_etape):
+    return key_etape
+
+
+def etape_to_col(key_etape):
+    return "%s@%s" % key_etape
diff --git a/sco_evaluations.py b/sco_evaluations.py
new file mode 100644
index 0000000000000000000000000000000000000000..246ebcb65e1ac51d2d65e07b12e0b1dbe9f8138e
--- /dev/null
+++ b/sco_evaluations.py
@@ -0,0 +1,1047 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""Evaluations
+"""
+from sets import Set
+
+from notes_log import log, logCallStack
+from sco_utils import *
+from notesdb import *
+from gen_tables import GenTable
+import sco_news
+import sco_formsemestre
+import sco_groups
+import ZAbsences
+
+# --------------------------------------------------------------------
+#
+#    MISC AUXILIARY FUNCTIONS
+#
+# --------------------------------------------------------------------
+def notes_moyenne_median_mini_maxi(notes):
+    "calcule moyenne et mediane d'une liste de valeurs (floats)"
+    notes = [
+        x
+        for x in notes
+        if (x != None) and (x != NOTES_NEUTRALISE) and (x != NOTES_ATTENTE)
+    ]
+    n = len(notes)
+    if not n:
+        return None, None, None, None
+    moy = sum(notes) / n
+    median = ListMedian(notes)
+    mini = min(notes)
+    maxi = max(notes)
+    return moy, median, mini, maxi
+
+
+def ListMedian(L):
+    """Median of a list L"""
+    n = len(L)
+    if not n:
+        raise ValueError("empty list")
+    L.sort()
+    if n % 2:
+        return L[n / 2]
+    else:
+        return (L[n / 2] + L[n / 2 - 1]) / 2
+
+
+# --------------------------------------------------------------------
+
+
+def do_evaluation_delete(context, REQUEST, evaluation_id):
+    "delete evaluation"
+    the_evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
+    if not the_evals:
+        raise ValueError("evaluation inexistante !")
+
+    NotesDB = context._notes_getall(evaluation_id)  # { etudid : value }
+    notes = [x["value"] for x in NotesDB.values()]
+    if notes:
+        raise ScoValueError(
+            "Impossible de supprimer cette évaluation: il reste des notes"
+        )
+
+    moduleimpl_id = the_evals[0]["moduleimpl_id"]
+    context._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id)
+    cnx = context.GetDBConnexion()
+
+    context._evaluationEditor.delete(cnx, evaluation_id)
+    # inval cache pour ce semestre
+    M = context.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+    context._inval_cache(formsemestre_id=M["formsemestre_id"])  # > eval delete
+    # news
+    mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    mod["moduleimpl_id"] = M["moduleimpl_id"]
+    mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
+    sco_news.add(
+        context,
+        REQUEST,
+        typ=sco_news.NEWS_NOTE,
+        object=moduleimpl_id,
+        text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
+        url=mod["url"],
+    )
+
+
+_DEE_TOT = 0
+
+
+def do_evaluation_etat(
+    context, evaluation_id, partition_id=None, select_first_partition=False
+):
+    """donne infos sur l'etat du evaluation
+    { nb_inscrits, nb_notes, nb_abs, nb_neutre, nb_att, 
+    moyenne, mediane, mini, maxi,
+    date_last_modif, gr_complets, gr_incomplets, evalcomplete }
+    evalcomplete est vrai si l'eval est complete (tous les inscrits
+    à ce module ont des notes)
+    evalattente est vrai s'il ne manque que des notes en attente
+    """
+    # global _DEE_TOT
+    # t0=time.time()
+    # if evaluation_id == 'GEAEVAL82883':
+    #     log('do_evaluation_etat: evaluation_id=%s  partition_id=%s sfp=%s' % (evaluation_id, partition_id, select_first_partition))
+
+    nb_inscrits = len(
+        sco_groups.do_evaluation_listeetuds_groups(
+            context, evaluation_id, getallstudents=True
+        )
+    )
+    NotesDB = context._notes_getall(evaluation_id)  # { etudid : value }
+    notes = [x["value"] for x in NotesDB.values()]
+    nb_abs = len([x for x in notes if x is None])
+    nb_neutre = len([x for x in notes if x == NOTES_NEUTRALISE])
+    nb_att = len([x for x in notes if x == NOTES_ATTENTE])
+    moy_num, median_num, mini_num, maxi_num = notes_moyenne_median_mini_maxi(notes)
+    if moy_num is None:
+        median, moy = "", ""
+        median_num, moy_num = None, None
+        mini, maxi = "", ""
+        mini_num, maxi_num = None, None
+    else:
+        median = fmt_note(median_num)
+        moy = fmt_note(moy_num)
+        mini = fmt_note(mini_num)
+        maxi = fmt_note(maxi_num)
+    # cherche date derniere modif note
+    if len(NotesDB):
+        t = [x["date"] for x in NotesDB.values()]
+        last_modif = max(t)
+    else:
+        last_modif = None
+    # ---- Liste des groupes complets et incomplets
+    E = context.do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    is_malus = Mod["module_type"] == MODULE_MALUS  # True si module de malus
+    formsemestre_id = M["formsemestre_id"]
+    # Si partition_id is None, prend 'all' ou bien la premiere:
+    if partition_id is None:
+        if select_first_partition:
+            partitions = sco_groups.get_partitions_list(context, formsemestre_id)
+            partition = partitions[0]
+        else:
+            partition = sco_groups.get_default_partition(context, formsemestre_id)
+        partition_id = partition["partition_id"]
+
+    # Il faut considerer les inscriptions au semestre
+    # (pour avoir l'etat et le groupe) et aussi les inscriptions
+    # au module (pour gerer les modules optionnels correctement)
+    insem = context.do_formsemestre_inscription_listinscrits(formsemestre_id)
+    insmod = context.do_moduleimpl_inscription_list(moduleimpl_id=E["moduleimpl_id"])
+    insmodset = Set([x["etudid"] for x in insmod])
+    # retire de insem ceux qui ne sont pas inscrits au module
+    ins = [i for i in insem if i["etudid"] in insmodset]
+
+    # Nombre de notes valides d'étudiants inscrits au module
+    # (car il peut y avoir des notes d'étudiants désinscrits depuis l'évaluation)
+    nb_notes = len(insmodset.intersection(NotesDB))
+    nb_notes_total = len(NotesDB)
+
+    # On considere une note "manquante" lorsqu'elle n'existe pas
+    # ou qu'elle est en attente (ATT)
+    GrNbMissing = DictDefault()  # group_id : nb notes manquantes
+    GrNotes = DictDefault(defaultvalue=[])  # group_id: liste notes valides
+    TotalNbMissing = 0
+    TotalNbAtt = 0
+    groups = {}  # group_id : group
+    etud_groups = sco_groups.get_etud_groups_in_partition(context, partition_id)
+
+    for i in ins:
+        group = etud_groups.get(i["etudid"], None)
+        if group and not group["group_id"] in groups:
+            groups[group["group_id"]] = group
+        #
+        isMissing = False
+        if NotesDB.has_key(i["etudid"]):
+            val = NotesDB[i["etudid"]]["value"]
+            if val == NOTES_ATTENTE:
+                isMissing = True
+                TotalNbAtt += 1
+            if group:
+                GrNotes[group["group_id"]].append(val)
+        else:
+            if group:
+                junk = GrNotes[group["group_id"]]  # create group
+            isMissing = True
+        if isMissing:
+            TotalNbMissing += 1
+            if group:
+                GrNbMissing[group["group_id"]] += 1
+
+    gr_incomplets = [x for x in GrNbMissing.keys()]
+    gr_incomplets.sort()
+    if (
+        (TotalNbMissing > 0)
+        and (E["evaluation_type"] != EVALUATION_RATTRAPAGE)
+        and not is_malus
+    ):
+        complete = False
+    else:
+        complete = True
+    if (
+        TotalNbMissing > 0
+        and (TotalNbMissing == TotalNbAtt or E["publish_incomplete"] != "0")
+        and not is_malus
+    ):
+        evalattente = True
+    else:
+        evalattente = False
+
+    # Calcul moyenne dans chaque groupe de TD
+    gr_moyennes = []  # group : {moy,median, nb_notes}
+    for group_id in GrNotes.keys():
+        notes = GrNotes[group_id]
+        gr_moy, gr_median, gr_mini, gr_maxi = notes_moyenne_median_mini_maxi(notes)
+        gr_moyennes.append(
+            {
+                "group_id": group_id,
+                "group_name": groups[group_id]["group_name"],
+                "gr_moy_num": gr_moy,
+                "gr_moy": fmt_note(gr_moy),
+                "gr_median_num": gr_median,
+                "gr_median": fmt_note(gr_median),
+                "gr_mini": fmt_note(gr_mini),
+                "gr_maxi": fmt_note(gr_maxi),
+                "gr_mini": gr_mini,
+                "gr_maxi": gr_maxi,
+                "gr_nb_notes": len(notes),
+                "gr_nb_att": len([x for x in notes if x == NOTES_ATTENTE]),
+            }
+        )
+    gr_moyennes.sort(key=operator.itemgetter("group_name"))
+    # log('gr_moyennes=%s' % gr_moyennes)
+    # _DEE_TOT += (time.time() - t0)
+    # log('%s\t_DEE_TOT=%f' % (evaluation_id, _DEE_TOT))
+    # if evaluation_id == 'GEAEVAL82883':
+    #    logCallStack()
+
+    # retourne mapping
+    return {
+        "evaluation_id": evaluation_id,
+        "nb_inscrits": nb_inscrits,
+        "nb_notes": nb_notes,  # nb notes etudiants inscrits
+        "nb_notes_total": nb_notes_total,  # nb de notes (incluant desinscrits)
+        "nb_abs": nb_abs,
+        "nb_neutre": nb_neutre,
+        "nb_att": nb_att,
+        "moy": moy,
+        "moy_num": moy_num,
+        "median": median,
+        "mini": mini,
+        "mini_num": mini_num,
+        "maxi": maxi,
+        "maxi_num": maxi_num,
+        "median_num": median_num,
+        "last_modif": last_modif,
+        "gr_incomplets": gr_incomplets,
+        "gr_moyennes": gr_moyennes,
+        "groups": groups,
+        "evalcomplete": complete,
+        "evalattente": evalattente,
+        "is_malus": is_malus,
+    }
+
+
+def do_evaluation_list_in_sem(context, formsemestre_id):
+    """Liste les evaluations de tous les modules de ce semestre.
+    Donne pour chaque eval son état (voir do_evaluation_etat)
+    { evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... }
+
+    Exemple:
+    [ {
+    'coefficient': 1.0,
+    'description': 'QCM et cas pratiques',
+    'etat': {'evalattente': False,
+          'evalcomplete': True,
+          'evaluation_id': 'GEAEVAL82883',
+          'gr_incomplets': [],
+          'gr_moyennes': [{'gr_median': '12.00',
+                           'gr_median_num' : 12.,
+                           'gr_moy': '11.88',
+                           'gr_moy_num' : 11.88,
+                           'gr_nb_att': 0,
+                           'gr_nb_notes': 166,
+                           'group_id': 'GEAG266762',
+                           'group_name': None}],
+          'groups': {'GEAG266762': {'etudid': 'GEAEID80603',
+                                    'group_id': 'GEAG266762',
+                                    'group_name': None,
+                                    'partition_id': 'GEAP266761'}
+           },
+          'last_modif': datetime.datetime(2015, 12, 3, 15, 15, 16),
+          'median': '12.00',
+          'moy': '11.84',
+          'nb_abs': 2,
+          'nb_att': 0,
+          'nb_inscrits': 166,
+          'nb_neutre': 0,
+          'nb_notes': 168,
+          'nb_notes_total': 169
+  },
+ 'evaluation_id': 'GEAEVAL82883',
+ 'evaluation_type': 0,
+ 'heure_debut': datetime.time(8, 0),
+ 'heure_fin': datetime.time(9, 30),
+ 'jour': datetime.date(2015, 11, 3), // vide => 1/1/1
+ 'moduleimpl_id': 'GEAMIP80490',
+ 'note_max': 20.0,
+ 'numero': 0,
+ 'publish_incomplete': 0,
+ 'visibulletin': 1} ]
+
+    """
+    req = "select E.* from notes_evaluation E, notes_moduleimpl MI where MI.formsemestre_id = %(formsemestre_id)s and MI.moduleimpl_id = E.moduleimpl_id"
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    res = cursor.dictfetchall()
+    # etat de chaque evaluation:
+    for r in res:
+        r["jour"] = r["jour"] or datetime.date(1900, 1, 1)  # pour les comparaisons
+        r["etat"] = do_evaluation_etat(context, r["evaluation_id"])
+
+    return res
+
+
+# remplacé par nt.get_sem_evaluation_etat_list()
+#
+# def formsemestre_evaluations_list(context, formsemestre_id):
+#     """Liste (non triée) des evals pour ce semestre"""
+#     req = "select E.* from notes_evaluation E, notes_moduleimpl MI where MI.formsemestre_id = %(formsemestre_id)s and MI.moduleimpl_id = E.moduleimpl_id"
+#     cnx = context.GetDBConnexion()
+#     cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+#     cursor.execute( req, { 'formsemestre_id' : formsemestre_id } )
+#     return cursor.dictfetchall()
+
+
+def _eval_etat(evals):
+    """evals: list of mappings (etats)
+    -> nb_eval_completes, nb_evals_en_cours,
+    nb_evals_vides, date derniere modif
+
+    Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
+
+    """
+    nb_evals_completes, nb_evals_en_cours, nb_evals_vides = 0, 0, 0
+    dates = []
+    for e in evals:
+        if e["etat"]["evalcomplete"]:
+            nb_evals_completes += 1
+        elif e["etat"]["nb_notes"] == 0:
+            nb_evals_vides += 1
+        else:
+            nb_evals_en_cours += 1
+        dates.append(e["etat"]["last_modif"])
+
+    dates = sort_dates(dates)
+
+    if len(dates):
+        last_modif = dates[-1]  # date de derniere modif d'une note dans un module
+    else:
+        last_modif = ""
+
+    return {
+        "nb_evals_completes": nb_evals_completes,
+        "nb_evals_en_cours": nb_evals_en_cours,
+        "nb_evals_vides": nb_evals_vides,
+        "last_modif": last_modif,
+    }
+
+
+def do_evaluation_etat_in_sem(context, formsemestre_id, REQUEST=None):
+    """-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
+    date derniere modif, attente"""
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > liste evaluations et moduleimpl en attente
+    evals = nt.get_sem_evaluation_etat_list()
+    etat = _eval_etat(evals)
+    # Ajoute information sur notes en attente
+    etat["attente"] = len(nt.get_moduleimpls_attente()) > 0
+    return etat
+
+
+def do_evaluation_etat_in_mod(context, nt, moduleimpl_id):
+    """
+    """
+    evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
+    etat = _eval_etat(evals)
+    etat["attente"] = moduleimpl_id in [
+        m["moduleimpl_id"] for m in nt.get_moduleimpls_attente()
+    ]  # > liste moduleimpl en attente
+    return etat
+
+
+def formsemestre_evaluations_cal(context, formsemestre_id, REQUEST=None):
+    """Page avec calendrier de toutes les evaluations de ce semestre"""
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > liste evaluations
+
+    evals = nt.get_sem_evaluation_etat_list()
+    nb_evals = len(evals)
+
+    color_incomplete = "#FF6060"
+    color_complete = "#A0FFA0"
+    color_futur = "#70E0FF"
+
+    today = time.strftime("%Y-%m-%d")
+
+    year = int(sem["annee_debut"])
+    if sem["mois_debut_ord"] < 8:
+        year -= 1  # calendrier septembre a septembre
+    events = {}  # (day, halfday) : event
+    for e in evals:
+        etat = e["etat"]
+        if not e["jour"]:
+            continue
+        day = e["jour"].strftime("%Y-%m-%d")
+        mod = context.do_moduleimpl_withmodule_list(moduleimpl_id=e["moduleimpl_id"])[0]
+        txt = mod["module"]["code"] or mod["module"]["abbrev"] or "eval"
+        if e["heure_debut"]:
+            debut = e["heure_debut"].strftime("%Hh%M")
+        else:
+            debut = "?"
+        if e["heure_fin"]:
+            fin = e["heure_fin"].strftime("%Hh%M")
+        else:
+            fin = "?"
+        description = "%s, de %s à %s" % (mod["module"]["titre"], debut, fin)
+        if etat["evalcomplete"]:
+            color = color_complete
+        else:
+            color = color_incomplete
+        if day > today:
+            color = color_futur
+        href = "moduleimpl_status?moduleimpl_id=%s" % e["moduleimpl_id"]
+        # if e['heure_debut'].hour < 12:
+        #    halfday = True
+        # else:
+        #    halfday = False
+        if not day in events:
+            # events[(day,halfday)] = [day, txt, color, href, halfday, description, mod]
+            events[day] = [day, txt, color, href, description, mod]
+        else:
+            e = events[day]
+            if e[-1]["moduleimpl_id"] != mod["moduleimpl_id"]:
+                # plusieurs evals de modules differents a la meme date
+                e[1] += ", " + txt
+                e[4] += ", " + description
+                if not etat["evalcomplete"]:
+                    e[2] = color_incomplete
+                if day > today:
+                    e[2] = color_futur
+
+    CalHTML = ZAbsences.YearTable(
+        context.Absences, year, events=events.values(), halfday=False, pad_width=None
+    )
+
+    H = [
+        context.html_sem_header(
+            REQUEST, "Evaluations du semestre", sem, cssstyles=["css/calabs.css"]
+        ),
+        '<div class="cal_evaluations">',
+        CalHTML,
+        "</div>",
+        "<p>soit %s évaluations planifiées;" % nb_evals,
+        """<ul><li>en <span style="background-color: %s">rouge</span> les évaluations passées auxquelles il manque des notes</li>
+          <li>en <span style="background-color: %s">vert</span> les évaluations déjà notées</li>
+          <li>en <span style="background-color: %s">bleu</span> les évaluations futures</li></ul></p>"""
+        % (color_incomplete, color_complete, color_futur),
+        """<p><a href="formsemestre_evaluations_delai_correction?formsemestre_id=%s" class="stdlink">voir les délais de correction</a></p>
+          """
+        % (formsemestre_id,),
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def evaluation_date_first_completion(context, evaluation_id):
+    """Première date à laquelle l'évaluation a été complète
+    ou None si actuellement incomplète
+    """
+    etat = do_evaluation_etat(context, evaluation_id)
+    if not etat["evalcomplete"]:
+        return None
+
+    E = context.do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    formsemestre_id = M["formsemestre_id"]
+
+    # Il faut considerer les inscriptions au semestre
+    # (pour avoir l'etat et le groupe) et aussi les inscriptions
+    # au module (pour gerer les modules optionnels correctement)
+    insem = context.do_formsemestre_inscription_listinscrits(formsemestre_id)
+    insmod = context.do_moduleimpl_inscription_list(moduleimpl_id=E["moduleimpl_id"])
+    insmodset = Set([x["etudid"] for x in insmod])
+    # retire de insem ceux qui ne sont pas inscrits au module
+    ins = [i for i in insem if i["etudid"] in insmodset]
+
+    notes = context._notes_getall(evaluation_id, filter_suppressed=False).values()
+    notes_log = context._notes_getall(
+        evaluation_id, filter_suppressed=False, table="notes_notes_log"
+    ).values()
+    date_premiere_note = {}  # etudid : date
+    for note in notes + notes_log:
+        etudid = note["etudid"]
+        if etudid in date_premiere_note:
+            date_premiere_note[etudid] = min(note["date"], date_premiere_note[etudid])
+        else:
+            date_premiere_note[etudid] = note["date"]
+
+    if not date_premiere_note:
+        return None  # complete mais aucun etudiant non démissionnaires
+    # complet au moment du max (date la plus tardive) des premieres dates de saisie
+    return max(date_premiere_note.values())
+
+
+def formsemestre_evaluations_delai_correction(
+    context, formsemestre_id, format="html", REQUEST=None
+):
+    """Experimental: un tableau indiquant pour chaque évaluation
+    le nombre de jours avant la publication des notes.
+
+    N'indique pas les évaluations de ratrapage ni celles des modules de bonus/malus.
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > liste evaluations
+
+    evals = nt.get_sem_evaluation_etat_list()
+    T = []
+    for e in evals:
+        M = context.do_moduleimpl_list(moduleimpl_id=e["moduleimpl_id"])[0]
+        Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+        if (e["evaluation_type"] != EVALUATION_NORMALE) or (
+            Mod["module_type"] == MODULE_MALUS
+        ):
+            continue
+        e["date_first_complete"] = evaluation_date_first_completion(
+            context, e["evaluation_id"]
+        )
+        if e["date_first_complete"]:
+            e["delai_correction"] = (e["date_first_complete"].date() - e["jour"]).days
+        else:
+            e["delai_correction"] = None
+
+        e["module_code"] = Mod["code"]
+        e["_module_code_target"] = (
+            "moduleimpl_status?moduleimpl_id=" + M["moduleimpl_id"]
+        )
+        e["module_titre"] = Mod["titre"]
+        e["responsable_id"] = M["responsable_id"]
+        e["responsable_nomplogin"] = context.Users.user_info(M["responsable_id"])[
+            "nomplogin"
+        ]
+        e["_jour_target"] = "evaluation_listenotes?evaluation_id=" + e["evaluation_id"]
+        T.append(e)
+
+    columns_ids = (
+        "module_code",
+        "module_titre",
+        "responsable_nomplogin",
+        "jour",
+        "date_first_complete",
+        "delai_correction",
+        "description",
+    )
+    titles = {
+        "module_code": "Code",
+        "module_titre": "Module",
+        "responsable_nomplogin": "Responsable",
+        "jour": "Date",
+        "date_first_complete": "Fin saisie",
+        "delai_correction": "Délai",
+        "description": "Description",
+    }
+    tab = GenTable(
+        titles=titles,
+        columns_ids=columns_ids,
+        rows=T,
+        html_class="table_leftalign table_coldate",
+        html_sortable=True,
+        html_title="<h2>Correction des évaluations du semestre</h2>",
+        caption="Correction des évaluations du semestre",
+        preferences=context.get_preferences(formsemestre_id),
+        base_url="%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id),
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        filename=make_filename("evaluations_delais_" + sem["titreannee"]),
+    )
+    return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+
+def module_evaluation_insert_before(context, ModEvals, next_eval, REQUEST):
+    """Renumber evals such that an evaluation with can be inserted before next_eval
+    Returns numero suitable for the inserted evaluation
+    """
+    if next_eval:
+        n = next_eval["numero"]
+        if not n:
+            log("renumbering old evals")
+            module_evaluation_renumber(context, next_eval["moduleimpl_id"], REQUEST)
+            next_eval = context.do_evaluation_list(
+                args={"evaluation_id": next_eval["evaluation_id"]}
+            )[0]
+            n = next_eval["numero"]
+    else:
+        n = 1
+    # log('inserting at position numero %s' % n )
+    # all numeros >= n are incremented
+    for e in ModEvals:
+        if e["numero"] >= n:
+            e["numero"] += 1
+            # log('incrementing %s to %s' % (e['evaluation_id'], e['numero']))
+            context.do_evaluation_edit(REQUEST, e)
+
+    return n
+
+
+def module_evaluation_move(context, evaluation_id, after=0, REQUEST=None, redirect=1):
+    """Move before/after previous one (decrement/increment numero)
+    (published)
+    """
+    e = context.do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
+    redirect = int(redirect)
+
+    # access: can change eval ? (raises exception)
+    context._evaluation_check_write_access(REQUEST, moduleimpl_id=e["moduleimpl_id"])
+
+    module_evaluation_renumber(
+        context, e["moduleimpl_id"], REQUEST=REQUEST, only_if_unumbered=True
+    )
+    e = context.do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
+
+    after = int(after)  # 0: deplace avant, 1 deplace apres
+    if after not in (0, 1):
+        raise ValueError('invalid value for "after"')
+    ModEvals = context.do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]})
+    # log('ModEvals=%s' % [ x['evaluation_id'] for x in ModEvals] )
+    if len(ModEvals) > 1:
+        idx = [p["evaluation_id"] for p in ModEvals].index(evaluation_id)
+        neigh = None  # object to swap with
+        if after == 0 and idx > 0:
+            neigh = ModEvals[idx - 1]
+        elif after == 1 and idx < len(ModEvals) - 1:
+            neigh = ModEvals[idx + 1]
+        if neigh:  #
+            # swap numero with neighbor
+            e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
+            context.do_evaluation_edit(REQUEST, e)
+            context.do_evaluation_edit(REQUEST, neigh)
+    # redirect to moduleimpl page:
+    if redirect:
+        return REQUEST.RESPONSE.redirect(
+            "moduleimpl_status?moduleimpl_id=" + e["moduleimpl_id"]
+        )
+
+
+def module_evaluation_renumber(
+    context, moduleimpl_id, REQUEST=None, only_if_unumbered=False, redirect=0
+):
+    """Renumber evaluations in this module, according to their date. (numero=0: oldest one)
+    Needed because previous versions of ScoDoc did not have eval numeros
+    Note: existing numeros are ignored
+    """
+    redirect = int(redirect)
+    # log('module_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id )
+    # List sorted according to date/heure, ignoring numeros:
+    # (note that we place  evaluations with NULL date at the end)
+    ModEvals = context.do_evaluation_list(
+        args={"moduleimpl_id": moduleimpl_id}, sortkey="jour asc, heure_debut asc"
+    )
+
+    all_numbered = False not in [x["numero"] > 0 for x in ModEvals]
+    if all_numbered and only_if_unumbered:
+        return  # all ok
+
+    # log('module_evaluation_renumber')
+    # Reset all numeros:
+    i = 1
+    for e in ModEvals:
+        e["numero"] = i
+        context.do_evaluation_edit(REQUEST, e)
+        i += 1
+
+    # If requested, redirect to moduleimpl page:
+    if redirect:
+        return REQUEST.RESPONSE.redirect(
+            "moduleimpl_status?moduleimpl_id=" + moduleimpl_id
+        )
+
+
+#  -------------- VIEWS
+def evaluation_describe(context, evaluation_id="", edit_in_place=True, REQUEST=None):
+    """HTML description of evaluation, for page headers
+    edit_in_place: allow in-place editing when permitted (not implemented)
+    """
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+    moduleimpl_id = E["moduleimpl_id"]
+    M = context.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    formsemestre_id = M["formsemestre_id"]
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    u = context.Users.user_info(M["responsable_id"])
+    resp = u["prenomnom"]
+    nomcomplet = u["nomcomplet"]
+    can_edit = context.can_edit_notes(
+        REQUEST.AUTHENTICATED_USER, moduleimpl_id, allow_ens=False
+    )
+
+    link = (
+        '<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
+        % moduleimpl_id
+    )
+    mod_descr = (
+        '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s'
+        % (moduleimpl_id, Mod["code"], Mod["titre"], nomcomplet, resp, link)
+    )
+
+    etit = E["description"] or ""
+    if etit:
+        etit = ' "' + etit + '"'
+    if Mod["module_type"] == MODULE_MALUS:
+        etit += ' <span class="eval_malus">(points de malus)</span>'
+    H = [
+        '<span class="eval_title">Evaluation%s</span><p><b>Module : %s</b></p>'
+        % (etit, mod_descr)
+    ]
+    if Mod["module_type"] == MODULE_MALUS:
+        # Indique l'UE
+        ue = context.do_ue_list(args={"ue_id": Mod["ue_id"]})[0]
+        H.append("<p><b>UE : %(acronyme)s</b></p>" % ue)
+        # store min/max values used by JS client-side checks:
+        H.append(
+            '<span id="eval_note_min" class="sco-hidden">-20.</span><span id="eval_note_max" class="sco-hidden">20.</span>'
+        )
+    else:
+        # date et absences (pas pour evals de malus)
+        jour = E["jour"] or "<em>pas de date</em>"
+        H.append(
+            "<p>Réalisée le <b>%s</b> de %s à %s "
+            % (jour, E["heure_debut"], E["heure_fin"])
+        )
+        if E["jour"]:
+            group_id = sco_groups.get_default_group(context, formsemestre_id)
+            H.append(
+                '<span class="noprint"><a href="%s/Absences/EtatAbsencesDate?group_ids=%s&date=%s">(absences ce jour)</a></span>'
+                % (context.ScoURL(), group_id, urllib.quote(E["jour"], safe=""))
+            )
+        H.append(
+            '</p><p>Coefficient dans le module: <b>%s</b>, notes sur <span id="eval_note_max">%g</span> '
+            % (E["coefficient"], E["note_max"])
+        )
+        H.append('<span id="eval_note_min" class="sco-hidden">0.</span>')
+    if can_edit:
+        H.append(
+            '<a href="evaluation_edit?evaluation_id=%s">(modifier l\'évaluation)</a>'
+            % evaluation_id
+        )
+    H.append("</p>")
+
+    return '<div class="eval_description">' + "\n".join(H) + "</div>"
+
+
+def evaluation_create_form(
+    context,
+    moduleimpl_id=None,
+    evaluation_id=None,
+    REQUEST=None,
+    edit=False,
+    readonly=False,
+    page_title="Evaluation",
+):
+    "formulaire creation/edition des evaluations (pas des notes)"
+    if evaluation_id != None:
+        the_eval = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+        moduleimpl_id = the_eval["moduleimpl_id"]
+    #
+    M = context.do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
+    is_malus = M["module"]["module_type"] == MODULE_MALUS  # True si module de malus
+    formsemestre_id = M["formsemestre_id"]
+    min_note_max = NOTES_PRECISION  # le plus petit bareme possible
+    if not readonly:
+        try:
+            context._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id)
+        except AccessDenied as e:
+            return (
+                context.sco_header(REQUEST)
+                + "<h2>Opération non autorisée</h2><p>"
+                + str(e)
+                + "</p>"
+                + '<p><a href="%s">Revenir</a></p>' % (str(REQUEST.HTTP_REFERER),)
+                + context.sco_footer(REQUEST)
+            )
+    if readonly:
+        edit = True  # montre les donnees existantes
+    if not edit:
+        # creation nouvel
+        if moduleimpl_id is None:
+            raise ValueError("missing moduleimpl_id parameter")
+        initvalues = {
+            "note_max": 20,
+            "jour": time.strftime("%d/%m/%Y", time.localtime()),
+            "publish_incomplete": is_malus,
+        }
+        submitlabel = "Créer cette évaluation"
+        action = "Création d'une é"
+        link = ""
+    else:
+        # edition donnees existantes
+        # setup form init values
+        if evaluation_id is None:
+            raise ValueError("missing evaluation_id parameter")
+        initvalues = the_eval
+        moduleimpl_id = initvalues["moduleimpl_id"]
+        submitlabel = "Modifier les données"
+        if readonly:
+            action = "E"
+            link = (
+                '<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
+                % M["moduleimpl_id"]
+            )
+        else:
+            action = "Modification d'une é"
+            link = ""
+        # Note maximale actuelle dans cette eval ?
+        etat = do_evaluation_etat(context, evaluation_id)
+        if etat["maxi_num"] is not None:
+            min_note_max = max(NOTES_PRECISION, etat["maxi_num"])
+        else:
+            min_note_max = NOTES_PRECISION
+    #
+    if min_note_max > NOTES_PRECISION:
+        min_note_max_str = fmt_note(min_note_max)
+    else:
+        min_note_max_str = "0"
+    #
+    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"])
+    #
+    help = """<div class="help"><p class="help">
+    Le coefficient d'une évaluation n'est utilisé que pour pondérer les évaluations au sein d'un module.
+    Il est fixé librement par l'enseignant pour refléter l'importance de ses différentes notes
+    (examens, projets, travaux pratiques...). Ce coefficient est utilisé pour calculer la note
+    moyenne de chaque étudiant dans ce module.
+    </p><p class="help">
+    Ne pas confondre ce coefficient avec le coefficient du module, qui est lui fixé par le programme
+    pédagogique (le PPN pour les DUT) et pondère les moyennes de chaque module pour obtenir
+    les moyennes d'UE et la moyenne générale.
+    </p><p class="help">
+    L'option <em>Visible sur bulletins</em> indique que la note sera reportée sur les bulletins
+    en version dite "intermédiaire" (dans cette version, on peut ne faire apparaitre que certaines
+    notes, en sus des moyennes de modules. Attention, cette option n'empêche pas la publication sur
+    les bulletins en version "longue" (la note est donc visible par les étudiants sur le portail).
+    </p><p class="help">
+    La modalité "rattrapage" permet de définir une évaluation dont les notes remplaceront les moyennes du modules
+    si elles sont meilleures que celles calculées. Dans ce cas, le coefficient est ignoré, et toutes les notes n'ont
+    pas besoin d'être rentrées.
+    </p>
+    <p class="help">
+    Les évaluations des modules de type "malus" sont spéciales: le coefficient n'est pas utilisé. 
+    Les notes de malus sont toujours comprises entre -20 et 20. Les points sont soustraits à la moyenne
+    de l'UE à laquelle appartient le module malus (si la note est négative, la moyenne est donc augmentée).
+    </p>
+    """
+    mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (
+        moduleimpl_id,
+        Mod["code"],
+        Mod["titre"],
+        link,
+    )
+    if not readonly:
+        H = ["<h3>%svaluation en %s</h3>" % (action, mod_descr)]
+    else:
+        return sco_evaluations.evaluation_describe(
+            context, evaluation_id, REQUEST=REQUEST
+        )
+
+    heures = ["%02dh%02d" % (h, m) for h in range(8, 19) for m in (0, 30)]
+    #
+    initvalues["visibulletin"] = initvalues.get("visibulletin", "1")
+    if initvalues["visibulletin"] == "1":
+        initvalues["visibulletinlist"] = ["X"]
+    else:
+        initvalues["visibulletinlist"] = []
+    if REQUEST.form.get("tf-submitted", False) and not REQUEST.form.has_key(
+        "visibulletinlist"
+    ):
+        REQUEST.form["visibulletinlist"] = []
+    #
+    form = [
+        ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
+        ("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
+        ("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
+        # ('jour', { 'title' : 'Date (j/m/a)', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }),
+        (
+            "jour",
+            {
+                "input_type": "date",
+                "title": "Date",
+                "size": 12,
+                "explanation": "date de l'examen, devoir ou contrôle",
+            },
+        ),
+        (
+            "heure_debut",
+            {
+                "title": "Heure de début",
+                "explanation": "heure du début de l'épreuve",
+                "input_type": "menu",
+                "allowed_values": heures,
+                "labels": heures,
+            },
+        ),
+        (
+            "heure_fin",
+            {
+                "title": "Heure de fin",
+                "explanation": "heure de fin de l'épreuve",
+                "input_type": "menu",
+                "allowed_values": heures,
+                "labels": heures,
+            },
+        ),
+    ]
+    if is_malus:  # pas de coefficient
+        form.append(("coefficient", {"input_type": "hidden", "default": "1."}))
+    else:
+        form.append(
+            (
+                "coefficient",
+                {
+                    "size": 10,
+                    "type": "float",
+                    "explanation": "coef. dans le module (choisi librement par l'enseignant)",
+                    "allow_null": False,
+                },
+            )
+        )
+    form += [
+        (
+            "note_max",
+            {
+                "size": 4,
+                "type": "float",
+                "title": "Notes de 0 à",
+                "explanation": "barème (note max actuelle: %s)" % min_note_max_str,
+                "allow_null": False,
+                "max_value": NOTES_MAX,
+                "min_value": min_note_max,
+            },
+        ),
+        (
+            "description",
+            {
+                "size": 36,
+                "type": "text",
+                "explanation": 'type d\'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".',
+            },
+        ),
+        (
+            "visibulletinlist",
+            {
+                "input_type": "checkbox",
+                "allowed_values": ["X"],
+                "labels": [""],
+                "title": "Visible sur bulletins",
+                "explanation": "(pour les bulletins en version intermédiaire)",
+            },
+        ),
+        (
+            "publish_incomplete",
+            {
+                "input_type": "boolcheckbox",
+                "title": "Prise en compte immédiate",
+                "explanation": "notes utilisées même si incomplètes",
+            },
+        ),
+        (
+            "evaluation_type",
+            {
+                "input_type": "menu",
+                "title": "Modalité",
+                "allowed_values": (EVALUATION_NORMALE, EVALUATION_RATTRAPAGE),
+                "type": "int",
+                "labels": ("Normale", "Rattrapage"),
+            },
+        ),
+    ]
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        form,
+        cancelbutton="Annuler",
+        submitlabel=submitlabel,
+        initvalues=initvalues,
+        readonly=readonly,
+    )
+
+    dest_url = "moduleimpl_status?moduleimpl_id=%s" % M["moduleimpl_id"]
+    if tf[0] == 0:
+        head = context.sco_header(REQUEST, page_title=page_title)
+        return head + "\n".join(H) + "\n" + tf[1] + help + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(dest_url)
+    else:
+        # form submission
+        if tf[2]["visibulletinlist"]:
+            tf[2]["visibulletin"] = 1
+        else:
+            tf[2]["visibulletin"] = 0
+        if not edit:
+            # creation d'une evaluation
+            evaluation_id = context.do_evaluation_create(REQUEST=REQUEST, **tf[2])
+            return REQUEST.RESPONSE.redirect(dest_url)
+        else:
+            context.do_evaluation_edit(REQUEST, tf[2])
+            return REQUEST.RESPONSE.redirect(dest_url)
diff --git a/sco_excel.py b/sco_excel.py
new file mode 100644
index 0000000000000000000000000000000000000000..81538a378a810bd76fbf446bdb3e2116cbfc588a
--- /dev/null
+++ b/sco_excel.py
@@ -0,0 +1,663 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+
+""" Excel file handling
+"""
+
+from pyExcelerator import *
+
+from notes_log import log
+from scolog import logdb
+from sco_exceptions import *
+from sco_utils import *
+import notesdb
+
+import time, datetime
+from types import StringType, IntType, FloatType, LongType
+
+# colors, voir exemple format.py
+COLOR_CODES = {
+    "black": 0,
+    "red": 0x0A,
+    "mauve": 0x19,
+    "marron": 0x3C,
+    "blue": 0x4,
+    "orange": 0x34,
+    "lightyellow": 0x2B,
+}
+
+
+def sendExcelFile(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", XLS_MIMETYPE)
+    REQUEST.RESPONSE.setHeader(
+        "content-disposition", 'attachment; filename="%s"' % filename
+    )
+    return data
+
+
+##  (stolen from xlrd)
+# Convert an Excel number (presumed to represent a date, a datetime or a time) into
+# a Python datetime.datetime
+# @param xldate The Excel number
+# @param datemode 0: 1900-based, 1: 1904-based.
+# @return a datetime.datetime object, to the nearest_second.
+# <br>Special case: if 0.0 <= xldate < 1.0, it is assumed to represent a time;
+# a datetime.time object will be returned.
+# <br>Note: 1904-01-01 is not regarded as a valid date in the datemode 1 system; its "serial number"
+# is zero.
+
+_XLDAYS_TOO_LARGE = (2958466, 2958466 - 1462)  # This is equivalent to 10000-01-01
+
+
+def xldate_as_datetime(xldate, datemode=0):
+    if datemode not in (0, 1):
+        raise ValueError("invalid mode %s" % datemode)
+    if xldate == 0.00:
+        return datetime.time(0, 0, 0)
+    if xldate < 0.00:
+        raise ValueError("invalid date code %s" % xldate)
+    xldays = int(xldate)
+    frac = xldate - xldays
+    seconds = int(round(frac * 86400.0))
+    assert 0 <= seconds <= 86400
+    if seconds == 86400:
+        seconds = 0
+        xldays += 1
+    if xldays >= _XLDAYS_TOO_LARGE[datemode]:
+        raise ValueError("date too large %s" % xldate)
+
+    if xldays == 0:
+        # second = seconds % 60; minutes = seconds // 60
+        minutes, second = divmod(seconds, 60)
+        # minute = minutes % 60; hour    = minutes // 60
+        hour, minute = divmod(minutes, 60)
+        return datetime.time(hour, minute, second)
+
+    if xldays < 61 and datemode == 0:
+        raise ValueError("ambiguous date %s" % xldate)
+
+    return datetime.datetime.fromordinal(
+        xldays + 693594 + 1462 * datemode
+    ) + datetime.timedelta(seconds=seconds)
+
+
+# Sous-classes pour ajouter methode savetostr()
+# (generation de fichiers en memoire)
+# XXX ne marche pas car accès a methodes privees (__xxx)
+# -> on utilise version modifiee par nous meme de pyExcelerator
+#
+# class XlsDocWithSave(CompoundDoc.XlsDoc):
+#     def savetostr(self, stream):
+#         #added by Emmanuel: save method, but returns a string
+#         # 1. Align stream on 0x1000 boundary (and therefore on sector boundary)
+#         padding = '\x00' * (0x1000 - (len(stream) % 0x1000))
+#         self.book_stream_len = len(stream) + len(padding)
+
+#         self.__build_directory()
+#         self.__build_sat()
+#         self.__build_header()
+
+#         return self.header+self.packed_MSAT_1st+stream+padding+self.packed_MSAT_2nd+self.packed_SAT+self.dir_stream
+
+# class WorkbookWithSave(Workbook):
+#     def savetostr(self):
+#         doc = XlsDocWithSave()
+#         return doc.savetostr(self.get_biff_data())
+
+
+def Excel_MakeStyle(
+    bold=False, italic=False, color="black", bgcolor=None, halign=None, valign=None
+):
+    style = XFStyle()
+    font = Font()
+    if bold:
+        font.bold = bold
+    if italic:
+        font.italic = italic
+    font.name = "Arial"
+    colour_index = COLOR_CODES.get(color, None)
+    if colour_index:
+        font.colour_index = colour_index
+    if bgcolor:
+        style.pattern = Pattern()
+        style.pattern.pattern = Pattern.SOLID_PATTERN
+        style.pattern.pattern_fore_colour = COLOR_CODES.get(bgcolor, None)
+    al = None
+    if halign:
+        al = Alignment()
+        al.horz = {
+            "left": Alignment.HORZ_LEFT,
+            "right": Alignment.HORZ_RIGHT,
+            "center": Alignment.HORZ_CENTER,
+        }[halign]
+    if valign:
+        if not al:
+            al = Alignment()
+        al.vert = {
+            "top": Alignment.VERT_TOP,
+            "bottom": VERT_BOTTOM,
+            "center": VERT_CENTER,
+        }[valign]
+    if al:
+        style.alignment = al
+    style.font = font
+    return style
+
+
+class ScoExcelSheet:
+    def __init__(self, sheet_name="feuille", default_style=None):
+        self.sheet_name = sheet_name
+        self.cells = []  # list of list
+        self.cells_styles_lico = {}  # { (li,co) : style }
+        self.cells_styles_li = {}  # { li : style }
+        self.cells_styles_co = {}  # { co : style }
+        if not default_style:
+            default_style = Excel_MakeStyle()
+        self.default_style = default_style
+
+    def set_style(self, style=None, li=None, co=None):
+        if li != None and co != None:
+            self.cells_styles_lico[(li, co)] = style
+        elif li != None:
+            self.cells_styles_li[li] = style
+        elif co != None:
+            self.cells_styles_co[co] = style
+
+    def append(self, l):
+        """Append a line of cells"""
+        self.cells.append(l)
+
+    def get_cell_style(self, li, co):
+        """Get style for specified cell"""
+        return (
+            self.cells_styles_lico.get((li, co), None)
+            or self.cells_styles_li.get(li, None)
+            or self.cells_styles_co.get(co, None)
+            or self.default_style
+        )
+
+    def gen_workbook(self, wb=None):
+        """Generates and returns a workbook from stored data.
+        If wb, add a sheet (tab) to the existing workbook (in this case, returns None).
+        """
+        if wb == None:
+            wb = Workbook()  # Création du fichier
+            sauvegarde = True
+        else:
+            sauvegarde = False
+        ws0 = wb.add_sheet(self.sheet_name.decode(SCO_ENCODING))
+        li = 0
+        for l in self.cells:
+            co = 0
+            for c in l:
+                # safety net: allow only str, int and float
+                if type(c) == LongType:
+                    c = int(c)  # assume all ScoDoc longs fits in int !
+                elif type(c) not in (IntType, FloatType):
+                    c = str(c).decode(SCO_ENCODING)
+
+                ws0.write(li, co, c, self.get_cell_style(li, co))
+                co += 1
+            li += 1
+        if sauvegarde == True:
+            return wb.savetostr()
+        else:
+            return None
+
+
+def Excel_SimpleTable(titles=[], lines=[[]], SheetName="feuille", titlesStyles=[]):
+    """Export simple type 'CSV': 1ere ligne en gras, le reste tel quel
+    """
+    # XXX devrait maintenant utiliser ScoExcelSheet
+    wb = Workbook()
+    ws0 = wb.add_sheet(SheetName.decode(SCO_ENCODING))
+    if not titlesStyles:
+        style = Excel_MakeStyle(bold=True)
+        titlesStyles = [style] * len(titles)
+    # ligne de titres
+    col = 0
+    for it in titles:
+        ws0.write(0, col, it.decode(SCO_ENCODING), titlesStyles[col])
+        col += 1
+    # suite
+    default_style = Excel_MakeStyle()
+    text_style = Excel_MakeStyle()
+    text_style.num_format_str = "@"
+    li = 1
+    for l in lines:
+        col = 0
+        for it in l:
+            cell_style = default_style
+            # safety net: allow only str, int and float
+            if type(it) == LongType:
+                it = int(it)  # assume all ScoDoc longs fits in int !
+            elif type(it) not in (IntType, FloatType):
+                it = str(it).decode(SCO_ENCODING)
+                cell_style = text_style
+            ws0.write(li, col, it, cell_style)
+            col += 1
+        li += 1
+    #
+    return wb.savetostr()
+
+
+def Excel_feuille_saisie(E, titreannee, description, lines):
+    """Genere feuille excel pour saisie des notes.
+    E: evaluation (dict)
+    lines: liste de tuples
+               (etudid, nom, prenom, etat, groupe, val, explanation)
+    """
+    SheetName = "Saisie notes"
+    wb = Workbook()
+    ws0 = wb.add_sheet(SheetName.decode(SCO_ENCODING))
+    # ajuste largeurs colonnes (unite inconnue, empirique)
+    ws0.col(0).width = 400  # codes
+    ws0.col(1).width = 6000  # noms
+    ws0.col(2).width = 4000  # prenoms
+    ws0.col(3).width = 6000  # groupes
+    ws0.col(4).width = 3000  # notes
+    ws0.col(5).width = 13000  # remarques
+    # styles
+    style_titres = XFStyle()
+    font0 = Font()
+    font0.bold = True
+    font0.name = "Arial"
+    font0.bold = True
+    font0.height = 14 * 0x14
+    style_titres.font = font0
+
+    style_expl = XFStyle()
+    font_expl = Font()
+    font_expl.name = "Arial"
+    font_expl.italic = True
+    font0.height = 12 * 0x14
+    font_expl.colour_index = 0x0A  # rouge, voir exemple format.py
+    style_expl.font = font_expl
+
+    topborders = Borders()
+    topborders.top = 1
+    topleftborders = Borders()
+    topleftborders.top = 1
+    topleftborders.left = 1
+    rightborder = Borders()
+    rightborder.right = 1
+
+    style_ro = XFStyle()  # cells read-only
+    font_ro = Font()
+    font_ro.name = "Arial"
+    font_ro.colour_index = COLOR_CODES["mauve"]
+    style_ro.font = font_ro
+    style_ro.borders = rightborder
+
+    style_dem = XFStyle()  # cells read-only
+    font_dem = Font()
+    font_dem.name = "Arial"
+    font_dem.colour_index = COLOR_CODES["marron"]
+    style_dem.font = font_dem
+    style_dem.borders = topborders
+
+    style = XFStyle()
+    font1 = Font()
+    font1.name = "Arial"
+    font1.height = 12 * 0x14
+    style.font = font1
+
+    style_nom = XFStyle()  # style pour nom, prenom, groupe
+    style_nom.font = font1
+    style_nom.borders = topborders
+
+    style_notes = XFStyle()
+    font2 = Font()
+    font2.name = "Arial"
+    font2.bold = True
+    style_notes.font = font2
+    style_notes.num_format_str = "general"
+    style_notes.pattern = Pattern()  # fond jaune
+    style_notes.pattern.pattern = Pattern.SOLID_PATTERN
+    style_notes.pattern.pattern_fore_colour = COLOR_CODES["lightyellow"]
+    style_notes.borders = topborders
+
+    style_comment = XFStyle()
+    font_comment = Font()
+    font_comment.name = "Arial"
+    font_comment.height = 9 * 0x14
+    font_comment.colour_index = COLOR_CODES["blue"]
+    style_comment.font = font_comment
+    style_comment.borders = topborders
+
+    # ligne de titres
+    li = 0
+    ws0.write(
+        li, 0, u"Feuille saisie note (à enregistrer au format excel)", style_titres
+    )
+    li += 1
+    ws0.write(li, 0, u"Saisir les notes dans la colonne E (cases jaunes)", style_expl)
+    li += 1
+    ws0.write(li, 0, u"Ne pas modifier les cases en mauve !", style_expl)
+    li += 1
+    # Nom du semestre
+    ws0.write(li, 0, unescape_html(titreannee).decode(SCO_ENCODING), style_titres)
+    li += 1
+    # description evaluation
+    ws0.write(li, 0, unescape_html(description).decode(SCO_ENCODING), style_titres)
+    li += 1
+    ws0.write(
+        li, 0, u"Evaluation du %s (coef. %g)" % (E["jour"], E["coefficient"]), style
+    )
+    li += 1
+    li += 1  # ligne blanche
+    # code et titres colonnes
+    ws0.write(li, 0, u"!%s" % E["evaluation_id"], style_ro)
+    ws0.write(li, 1, u"Nom", style_titres)
+    ws0.write(li, 2, u"Prénom", style_titres)
+    ws0.write(li, 3, u"Groupe", style_titres)
+    ws0.write(li, 4, u"Note sur %g" % E["note_max"], style_titres)
+    ws0.write(li, 5, u"Remarque", style_titres)
+    # etudiants
+    for line in lines:
+        li += 1
+        st = style_nom
+        ws0.write(li, 0, ("!" + line[0]).decode(SCO_ENCODING), style_ro)  # code
+        if line[3] != "I":
+            st = style_dem
+            if line[3] == "D":  # demissionnaire
+                s = "DEM"
+            else:
+                s = line[3]  # etat autre
+        else:
+            s = line[4]  # groupes TD/TP/...
+        ws0.write(li, 1, line[1].decode(SCO_ENCODING), st)
+        ws0.write(li, 2, line[2].decode(SCO_ENCODING), st)
+        ws0.write(li, 3, s.decode(SCO_ENCODING), st)
+        try:
+            val = float(line[5])
+        except:
+            val = line[5].decode(SCO_ENCODING)
+        ws0.write(li, 4, val, style_notes)  # note
+        ws0.write(li, 5, line[6].decode(SCO_ENCODING), style_comment)  # comment
+    # explication en bas
+    li += 2
+    ws0.write(li, 1, u"Code notes", style_titres)
+    ws0.write(li + 1, 1, u"ABS", style_expl)
+    ws0.write(li + 1, 2, u"absent (0)", style_expl)
+    ws0.write(li + 2, 1, u"EXC", style_expl)
+    ws0.write(li + 2, 2, u"pas prise en compte", style_expl)
+    ws0.write(li + 3, 1, u"ATT", style_expl)
+    ws0.write(li + 3, 2, u"en attente", style_expl)
+    ws0.write(li + 4, 1, u"SUPR", style_expl)
+    ws0.write(li + 4, 2, u"pour supprimer note déjà entrée", style_expl)
+    ws0.write(li + 5, 1, u"", style_expl)
+    ws0.write(li + 5, 2, u"cellule vide -> note non modifiée", style_expl)
+    return wb.savetostr()
+
+
+def Excel_to_list(data, convert_to_string=str):  # we may need 'encoding' argument ?
+    """returns list of list
+    convert_to_string is a conversion function applied to all non-string values (ie numbers)
+    """
+    try:
+        P = parse_xls("", SCO_ENCODING, doc=data)
+    except:
+        log("Excel_to_list: failure to import document")
+        open("/tmp/last_scodoc_import_failure.xls", "w").write(data)
+        raise ScoValueError(
+            "Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !"
+        )
+
+    diag = []  # liste de chaines pour former message d'erreur
+    # n'utilise que la première feuille
+    if len(P) < 1:
+        diag.append("Aucune feuille trouvée dans le classeur !")
+        return diag, None
+    if len(P) > 1:
+        diag.append("Attention: n'utilise que la première feuille du classeur !")
+    # fill matrix
+    sheet_name, values = P[0]
+    sheet_name = sheet_name.encode(SCO_ENCODING, "backslashreplace")
+    if not values:
+        diag.append("Aucune valeur trouvée dans le classeur !")
+        return diag, None
+    indexes = values.keys()
+    # search numbers of rows and cols
+    rows = [x[0] for x in indexes]
+    cols = [x[1] for x in indexes]
+    nbcols = max(cols) + 1
+    nbrows = max(rows) + 1
+    M = []
+    for i in range(nbrows):
+        M.append([""] * nbcols)
+
+    for row_idx, col_idx in indexes:
+        v = values[(row_idx, col_idx)]
+        if isinstance(v, unicode):
+            v = v.encode(SCO_ENCODING, "backslashreplace")
+        elif convert_to_string:
+            v = convert_to_string(v)
+        M[row_idx][col_idx] = v
+    diag.append('Feuille "%s", %d lignes' % (sheet_name, len(M)))
+    # diag.append(str(M))
+    #
+    return diag, M
+
+
+#
+def Excel_feuille_listeappel(
+    context,
+    sem,
+    groupname,
+    lines,
+    partitions=[],  # partitions a montrer (colonnes)
+    with_codes=False,  # indique codes etuds
+    with_paiement=False,  # indique si etudiant a paye inscription
+    server_name=None,
+):
+    "generation feuille appel"
+    formsemestre_id = sem["formsemestre_id"]
+    SheetName = "Liste " + groupname
+    wb = Workbook()
+    ws0 = wb.add_sheet(SheetName.decode(SCO_ENCODING))
+
+    font1 = Font()
+    font1.name = "Arial"
+    font1.height = 10 * 0x14
+
+    font1i = Font()
+    font1i.name = "Arial"
+    font1i.height = 10 * 0x14
+    font1i.italic = True
+
+    style1i = XFStyle()
+    style1i.font = font1i
+
+    style1b = XFStyle()
+    style1b.font = font1
+    borders = Borders()
+    borders.left = 1
+    borders.top = 1
+    borders.bottom = 1
+    style1b.borders = borders
+
+    style2 = XFStyle()
+    font2 = Font()
+    font2.name = "Arial"
+    font2.height = 14 * 0x14
+    style2.font = font2
+
+    style2b = XFStyle()
+    style2b.font = font1i
+    borders = Borders()
+    borders.left = 1
+    borders.top = 1
+    borders.bottom = 1
+    borders.right = 1
+    style2b.borders = borders
+
+    style2tb = XFStyle()
+    borders = Borders()
+    borders.top = 1
+    borders.bottom = 1
+    style2tb.borders = borders
+    style2tb.font = Font()
+    style2tb.font.height = 16 * 0x14  # -> ligne hautes
+
+    style2t3 = XFStyle()
+    borders = Borders()
+    borders.top = 1
+    borders.bottom = 1
+    borders.left = 1
+    style2t3.borders = borders
+
+    style2t3bold = XFStyle()
+    borders = Borders()
+    borders.top = 1
+    borders.bottom = 1
+    borders.left = 1
+    style2t3bold.borders = borders
+    fontb = Font()
+    fontb.bold = True
+    style2t3bold.font = fontb
+
+    style3 = XFStyle()
+    font3 = Font()
+    font3.name = "Arial"
+    font3.bold = True
+    font3.height = 14 * 0x14
+    style3.font = font3
+
+    NbWeeks = 4  # nombre de colonnes pour remplir absences
+
+    # ligne 1
+    li = 0
+    ws0.write(
+        li,
+        1,
+        (
+            "%s %s (%s - %s)"
+            % (
+                context.get_preference("DeptName", formsemestre_id),
+                notesdb.unquote(sem["titre_num"]),
+                sem["date_debut"],
+                sem["date_fin"],
+            )
+        ).decode(SCO_ENCODING),
+        style2,
+    )
+    # ligne 2
+    li += 1
+    ws0.write(li, 1, u"Discipline :", style2)
+    # ligne 3
+    li += 1
+    ws0.write(li, 1, u"Enseignant :", style2)
+    ws0.write(li, 5, ("Groupe %s" % groupname).decode(SCO_ENCODING), style3)
+    # Avertissement pour ne pas confondre avec listes notes
+    ws0.write(
+        li + 1, 2, u"Ne pas utiliser cette feuille pour saisir les notes !", style1i
+    )
+    #
+    li += 2
+    li += 1
+    ws0.write(li, 1, u"Nom", style3)
+    co = 2
+    for partition in partitions:
+        if partition["partition_name"]:
+            ws0.write(li, co, partition["partition_name"].decode(SCO_ENCODING), style3)
+            co += 1
+    if with_codes:
+        coc = co
+        ws0.write(li, coc, u"etudid", style3)
+        ws0.write(li, coc + 1, u"code_nip", style3)
+        ws0.write(li, coc + 2, u"code_ine", style3)
+        co += 3
+
+    for i in range(NbWeeks):
+        ws0.write(li, co + i, "", style2b)
+    n = 0
+    for t in lines:
+        n += 1
+        li += 1
+        ws0.write(li, 0, n, style1b)
+        nomprenom = (
+            t["sexe"] + " " + t["nom"] + " " + strcapitalize(strlower(t["prenom"]))
+        )
+        style_nom = style2t3
+        if with_paiement:
+            paie = t.get("paiementinscription", None)
+            if paie is None:
+                nomprenom += " (inscription ?)"
+                style_nom = style2t3bold
+            elif not paie:
+                nomprenom += " (non paiement)"
+                style_nom = style2t3bold
+        ws0.write(li, 1, nomprenom.decode(SCO_ENCODING), style_nom)
+        co = 2
+        for partition in partitions:
+            if partition["partition_name"]:
+                ws0.write(
+                    li,
+                    co,
+                    t.get(partition["partition_id"], "").decode(SCO_ENCODING),
+                    style2t3,
+                )
+                co += 1
+        if with_codes:
+            ws0.write(li, coc, t["etudid"].decode(SCO_ENCODING), style2t3)
+            if t["code_nip"]:
+                code_nip = t["code_nip"].decode(SCO_ENCODING)
+            else:
+                code_nip = u""
+            ws0.write(li, coc + 1, code_nip, style2t3)
+            if t["code_ine"]:
+                code_ine = t["code_ine"].decode(SCO_ENCODING)
+            else:
+                code_ine = u""
+            ws0.write(li, coc + 2, code_ine, style2t3)
+        if t["etath"]:
+            etath = t["etath"].decode(SCO_ENCODING)
+        else:
+            etath = u""
+        ws0.write(li, co, etath, style2b)  # etat
+        for i in range(1, NbWeeks):
+            ws0.write(li, co + i, u"", style2b)  # cellules vides
+        ws0.row(li).height = 850  # sans effet ?
+    #
+    li += 2
+    dt = time.strftime("%d/%m/%Y à %Hh%M")
+    if server_name:
+        dt += " sur " + server_name
+    ws0.write(li, 1, ("Liste éditée le " + dt).decode(SCO_ENCODING), style1i)
+    #
+    ws0.col(0).width = 850
+    ws0.col(1).width = 9000
+
+    return wb.savetostr()
diff --git a/sco_exceptions.py b/sco_exceptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a3d6fda29802db7bb38cc06c48cc2916af39859
--- /dev/null
+++ b/sco_exceptions.py
@@ -0,0 +1,86 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Exception handling
+"""
+
+# --- Exceptions
+MSGPERMDENIED = "l'utilisateur %s n'a pas le droit d'effectuer cette operation"
+
+
+class ScoException(Exception):
+    pass
+
+
+class NoteProcessError(ScoException):
+    "misc errors in process"
+    pass
+
+
+class InvalidEtudId(NoteProcessError):
+    pass
+
+
+class AccessDenied(ScoException):
+    pass
+
+
+class InvalidNoteValue(ScoException):
+    pass
+
+
+# Exception qui stoque dest_url, utilisee dans Zope standard_error_message
+class ScoValueError(ScoException):
+    def __init__(self, msg, dest_url=None, REQUEST=None):
+        ScoException.__init__(self, msg)
+        self.dest_url = dest_url
+        if REQUEST and dest_url:
+            REQUEST.set("dest_url", dest_url)
+
+
+class FormatError(ScoValueError):
+    pass
+
+
+class ScoLockedFormError(ScoException):
+    def __init__(self, msg="", REQUEST=None):
+        msg = (
+            "Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). "
+            + str(msg)
+        )
+        ScoException.__init__(self, msg)
+
+
+class ScoGenError(ScoException):
+    "exception avec affichage d'une page explicative ad-hoc"
+
+    def __init__(self, msg="", REQUEST=None):
+        ScoException.__init__(self, msg)
+
+
+class ScoInvalidDateError(ScoValueError):
+    pass
diff --git a/sco_export_results.py b/sco_export_results.py
new file mode 100644
index 0000000000000000000000000000000000000000..b5abe6b37c01e33995c4d3376d87314559febc18
--- /dev/null
+++ b/sco_export_results.py
@@ -0,0 +1,350 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Export d'une table avec les résultats de tous les étudiants
+"""
+
+import scolars
+import sco_bac
+import sco_formsemestre
+import sco_parcours_dut
+import sco_codes_parcours
+from sco_codes_parcours import NO_SEMESTRE_ID
+import sco_excel
+from notesdb import *
+from sco_utils import *
+from gen_tables import GenTable
+import sco_pvjury
+import html_sco_header
+
+
+def _build_results_table(context, start_date=None, end_date=None, types_parcours=[]):
+    """Construit une table avec les résultats de jury de TOUS les étudiants
+    de TOUS les semestres ScoDoc de ce département entre les dates indiquées
+    (c'est à dire commençant APRES ou à start_date et terminant avant ou à end_date)
+    Les dates sont des chaines iso.
+    """
+    formsemestre_ids = get_set_formsemestre_id_dates(context, start_date, end_date)
+    # Décisions de jury de tous les semestres:
+    dpv_by_sem = {}
+    for formsemestre_id in formsemestre_ids:
+        dpv_by_sem[formsemestre_id] = sco_pvjury.dict_pvjury(
+            context, formsemestre_id, with_parcours_decisions=True
+        )
+
+    semlist = [dpv["formsemestre"] for dpv in dpv_by_sem.values() if dpv]
+    semlist_parcours = []
+    for sem in semlist:
+        sem["formation"] = context.Notes.formation_list(
+            args={"formation_id": sem["formation_id"]}
+        )[0]
+        sem["parcours"] = sco_codes_parcours.get_parcours_from_code(
+            sem["formation"]["type_parcours"]
+        )
+        if sem["parcours"].TYPE_PARCOURS in types_parcours:
+            semlist_parcours.append(sem)
+    formsemestre_ids_parcours = [sem["formsemestre_id"] for sem in semlist_parcours]
+
+    # Ensemble des étudiants
+    etuds_infos = (
+        {}
+    )  # etudid : { formsemestre_id d'inscription le plus recent dans les dates considérées, etud }
+    for formsemestre_id in formsemestre_ids_parcours:
+        nt = context._getNotesCache().get_NotesTable(
+            context, formsemestre_id
+        )  # > get_etudids
+        etudids = nt.get_etudids()
+        for etudid in etudids:
+            if etudid not in etuds_infos:  # pas encore traité ?
+                etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+                for sem in etud["sems"]:  # le plus récent d'abord
+                    if sem["formsemestre_id"] in formsemestre_ids_parcours:
+                        etuds_infos[etudid] = {
+                            "recent_formsemestre_id": sem["formsemestre_id"],
+                            "etud": etud,
+                        }
+                        break
+                # sanity check
+                assert etudid in etuds_infos
+    # Construit la table (semblable à pvjury_table)
+    rows, titles, columns_ids = _build_results_list(context, dpv_by_sem, etuds_infos)
+    tab = GenTable(
+        rows=rows,
+        titles=titles,
+        columns_ids=columns_ids,
+        filename=make_filename("scodoc-results-%s-%s" % (start_date, end_date)),
+        caption="Résultats ScoDoc de %s à %s" % (start_date, end_date),
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        html_class="table_leftalign",
+        html_sortable=True,
+        preferences=context.get_preferences(),
+    )
+    return tab, semlist
+
+
+def _build_results_list(context, dpv_by_sem, etuds_infos):
+    """Construit la table (semblable à pvjury_table)
+    Returns:
+        rows, titles, columns_ids
+    """
+    titles = {
+        "anneescolaire": "Année",
+        "periode": "Période",
+        "sid": "Semestre",
+        "etudid": "etudid",
+        "code_nip": "NIP",
+        "nom": "Nom",
+        "prenom": "Prénom",
+        "sexe": "Civ.",
+        "nom_usuel": "Nom usuel",
+        "bac": "Bac",
+        "parcours": "Parcours",
+        "devenir": "Devenir",
+    }
+    columns_ids = [
+        "anneescolaire",
+        "periode",
+        "sid",
+        "code_nip",
+        "sexe",
+        "nom",
+        # 'nom_usuel', # inutile ?
+        "prenom",
+        "bac",
+        "parcours",
+    ]
+
+    # Recherche la liste des indices de semestres à considérer
+    all_idx = set()
+    for etudid in etuds_infos:
+        # la décision de jury à considérer pour cet étudiant:
+        e = dpv_by_sem[etuds_infos[etudid]["recent_formsemestre_id"]]["decisions_dict"][
+            etudid
+        ]
+        all_idx |= set(e["parcours_decisions"].keys())
+    sem_ids = sorted(all_idx)
+    # ajoute les titres des colonnes résultats de semestres
+    for i in sem_ids:
+        if i != NO_SEMESTRE_ID:
+            titles[i] = "S%d" % i
+        else:
+            titles[i] = "S"  # pas très parlant ?
+        columns_ids += [i]
+    columns_ids += ["devenir"]
+    # Construit la liste:
+    rows = []
+    for etudid in etuds_infos:
+        etud = etuds_infos[etudid]["etud"]
+        bac = sco_bac.Baccalaureat(etud["bac"], etud["specialite"])
+        dpv = dpv_by_sem[etuds_infos[etudid]["recent_formsemestre_id"]]
+        dec = dpv["decisions_dict"][etudid]
+        l = {
+            "etudid": etudid,
+            "code_nip": etud["code_nip"],
+            "nom": etud["nom"],
+            "nom_usuel": etud["nom_usuel"],
+            "prenom": etud["prenom"],
+            "sexe": etud["sexe"],
+            "_nom_target": "%s/ficheEtud?etudid=%s" % (context.ScoURL(), etudid),
+            "_nom_td_attrs": 'id="%s" class="etudinfo"' % etudid,
+            "bac": bac.abbrev(),
+            "parcours": dec["parcours"],
+        }
+        for sem in reversed(etud["sems"]):
+            r = l.copy()
+            dpv = dpv_by_sem.get(sem["formsemestre_id"], None)
+            if dpv:  # semestre à inclure dans ce rapport
+                dec = dpv["decisions_dict"].get(etudid, None)
+                if dec and dec["decision_sem"]:
+                    code = dec["decision_sem"]["code"]
+                    if dec["validation_parcours"]:
+                        r["devenir"] = "Diplôme obtenu"
+                    else:
+                        r["devenir"] = dec["autorisations_descr"]
+                else:
+                    code = "-"
+                r[sem["semestre_id"]] = code
+                r["periode"] = sem["periode"]
+                r["anneescolaire"] = annee_scolaire_debut(
+                    int(sem["annee_debut"]), sem["mois_debut_ord"]
+                )
+                r["sid"] = "{} {} {}".format(
+                    sem["sem_id_txt"], context.DeptId(), sem["modalite"]
+                )
+                rows.append(r)
+
+    return rows, titles, columns_ids
+
+
+def get_set_formsemestre_id_dates(context, start_date, end_date):
+    """Ensemble des formsemestre_id entre ces dates
+    """
+    s = SimpleDictFetch(
+        context,
+        "SELECT formsemestre_id FROM notes_formsemestre WHERE date_debut >= %(start_date)s AND date_fin <= %(end_date)s",
+        {"start_date": start_date, "end_date": end_date},
+    )
+    return {x["formsemestre_id"] for x in s}
+
+
+def scodoc_table_results(
+    context, start_date="", end_date="", types_parcours=[], format="html", REQUEST=None
+):
+    """Page affichant la table des résultats
+    Les dates sont en dd/mm/yyyy (datepicker javascript)
+    types_parcours est la liste des types de parcours à afficher 
+    (liste de chaines, eg ['100', '210'] )
+    """
+    log("scodoc_table_results: start_date=%s" % (start_date,))  # XXX
+    if not types_parcours:
+        types_parcours = []
+    if not isinstance(types_parcours, ListType):
+        types_parcours = [types_parcours]
+    if start_date:
+        start_date_iso = DateDMYtoISO(start_date)
+    if end_date:
+        end_date_iso = DateDMYtoISO(end_date)
+    types_parcours = [int(x) for x in types_parcours if x]
+
+    if start_date and end_date:
+        tab, semlist = _build_results_table(
+            context, start_date_iso, end_date_iso, types_parcours
+        )
+        tab.base_url = "%s?start_date=%s&amp;end_date=%s&amp;types_parcours=%s" % (
+            REQUEST.URL0,
+            start_date,
+            end_date,
+            "&amp;types_parcours=".join([str(x) for s in types_parcours]),
+        )
+        if format != "html":
+            return tab.make_page(
+                context, format=format, with_html_headers=False, REQUEST=REQUEST
+            )
+        tab_html = tab.html()
+        nb_rows = tab.get_nb_rows()
+    else:
+        tab = None
+        nb_rows = 0
+        tab_html = ""
+        semlist = []
+
+    # affiche la liste des semestres utilisés:
+    info_sems = ["<ul>"]
+    menu_options = []
+    type_parcours_set = set()
+    for sem in sorted(semlist, key=lambda x: x["dateord"]):
+        if sem["parcours"].TYPE_PARCOURS in types_parcours:
+            selected = "selected"
+        else:
+            selected = ""
+        if sem["parcours"].TYPE_PARCOURS not in type_parcours_set:
+            type_parcours_set.add(sem["parcours"].TYPE_PARCOURS)
+            menu_options.append(
+                '<option value="%s" %s>%s</option>'
+                % (sem["parcours"].TYPE_PARCOURS, selected, sem["parcours"].__doc__)
+            )
+
+        if sem["parcours"].TYPE_PARCOURS in types_parcours:
+            info_sems.append(
+                '<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>'
+                % sem
+            )
+
+    info_sems.append("</ul>")
+
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Export résultats",
+            init_qtip=True,
+            javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
+            + ["js/etud_info.js", "js/export_results.js"],
+            cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS,
+        ),
+        # XXX
+        """
+        <h2>Table des résultats de tous les semestres</h2>
+        <p class="warning">Développement en cours / attention !</p>
+        """,
+        _DATE_FORM.format(
+            start_date=start_date,
+            end_date=end_date,
+            menu_options="\n".join(menu_options),
+        ),
+        """<div>
+        <h4>%d étudiants dans les %d semestres de cette période</h4>
+        </div>
+        """
+        % (nb_rows, len(semlist)),
+        tab_html,
+        """<div><h4>Semestres pris en compte:</h4>
+        """,
+        "\n".join(info_sems),
+        """</div>""",
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+# Formulaire pour saisie dates et sélection parcours
+_DATE_FORM = """
+<form method="get">
+<div><b>Choisir les dates :</b>
+<div>Début: <input type="text" name="start_date" size="10" value="{start_date}" class="datepicker"/> </div>
+<div>Fin: <input type="text" name="end_date" size="10" value="{end_date}" class="datepicker"/></div>
+<input type="submit" name="" value=" OK " width=100/>
+</div>
+<div>
+<b>Types de parcours :</b>
+<select name="types_parcours" id="parcours_sel" class="multiselect" multiple="multiple">
+{menu_options}
+</select>
+
+<input type="submit" name="" value=" charger " width=100/>
+</form>
+"""
+
+# ------- debug
+"""
+# /opt/scodoc/bin/zopectl debug 
+from debug import *
+from sco_export_results import *
+context = go_dept(app, 'RT').Notes
+etudid = 'EID27764'
+etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+
+start_date='2015-08-15'
+end_date='2017-08-31'
+
+formsemestre_ids = get_set_formsemestre_id_dates(context, start_date, end_date)
+dpv_by_sem = {}
+for formsemestre_id in formsemestre_ids:
+    dpv_by_sem[formsemestre_id] = sco_pvjury.dict_pvjury(context, formsemestre_id, with_parcours_decisions=True)
+
+semlist = [ dpv['formsemestre'] for dpv in dpv_by_sem.values() ]
+
+"""
diff --git a/sco_find_etud.py b/sco_find_etud.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f1d0e1a9914ad2cd4da3fa998d45a126be72805
--- /dev/null
+++ b/sco_find_etud.py
@@ -0,0 +1,428 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Recherche d'étudiants
+"""
+
+from sco_utils import *
+import xml.dom.minidom
+
+from notesdb import *
+from notes_log import log
+from gen_tables import GenTable
+
+import scolars
+import sco_formsemestre
+import sco_groups
+
+
+def form_search_etud(
+    context,
+    REQUEST=None,
+    dest_url=None,
+    parameters=None,
+    parameters_keys=None,
+    title="Rechercher un &eacute;tudiant par nom&nbsp;: ",
+    add_headers=False,  # complete page
+):
+    "form recherche par nom"
+    H = []
+    if title:
+        H.append("<h2>%s</h2>" % title)
+    H.append(
+        """<form action="search_etud_in_dept" method="POST">
+    <b>%s</b>
+    <input type="text" name="expnom" width=12 value="">
+    <input type="submit" value="Chercher">
+    <br/>(entrer une partie du nom)
+    """
+        % title
+    )
+    if dest_url:
+        H.append('<input type="hidden" name="dest_url" value="%s"/>' % dest_url)
+    if parameters:
+        for param in parameters.keys():
+            H.append(
+                '<input type="hidden" name="%s" value="%s"/>'
+                % (param, parameters[param])
+            )
+        H.append(
+            '<input type="hidden" name="parameters_keys" value="%s"/>'
+            % (",".join(parameters.keys()))
+        )
+    elif parameters_keys:
+        for key in parameters_keys.split(","):
+            v = REQUEST.form.get(key, False)
+            if v:
+                H.append('<input type="hidden" name="%s" value="%s"/>' % (key, v))
+        H.append(
+            '<input type="hidden" name="parameters_keys" value="%s"/>' % parameters_keys
+        )
+    H.append("</form>")
+
+    if add_headers:
+        return (
+            context.sco_header(REQUEST, page_title="Choix d'un étudiant")
+            + "\n".join(H)
+            + context.sco_footer(REQUEST)
+        )
+    else:
+        return "\n".join(H)
+
+
+# was chercheEtud()
+def search_etud_in_dept(
+    context,
+    expnom=None,
+    dest_url="ficheEtud",
+    parameters={},
+    parameters_keys="",
+    add_headers=True,  # complete page
+    title=None,
+    REQUEST=None,
+):
+    """Page recherche d'un etudiant
+    expnom est un regexp sur le nom ou un code_nip
+    dest_url est la page sur laquelle on sera redirigé après choix
+    parameters spécifie des arguments additionnels à passer à l'URL (en plus de etudid)
+    """
+    if type(expnom) == ListType:
+        expnom = expnom[0]
+    q = []
+    if parameters:
+        for param in parameters.keys():
+            q.append("%s=%s" % (param, parameters[param]))
+    elif parameters_keys:
+        for key in parameters_keys.split(","):
+            v = REQUEST.form.get(key, False)
+            if v:
+                q.append("%s=%s" % (key, v))
+    query_string = "&amp;".join(q)
+
+    no_side_bar = True
+    H = []
+    if title:
+        H.append("<h2>%s</h2>" % title)
+
+    if is_valid_code_nip(expnom):
+        etuds = search_etuds_infos(context, code_nip=expnom, REQUEST=REQUEST)
+    elif expnom:
+        etuds = search_etuds_infos(context, expnom=expnom, REQUEST=REQUEST)
+    else:
+        etuds = []
+    if len(etuds) == 1:
+        # va directement a la destination
+        return REQUEST.RESPONSE.redirect(
+            dest_url + "?etudid=%s&amp;" % etuds[0]["etudid"] + query_string
+        )
+
+    if len(etuds) > 0:
+        # Choix dans la liste des résultats:
+        H.append(
+            """<h2>%d résultats pour "%s": choisissez un étudiant:</h2>"""
+            % (len(etuds), expnom)
+        )
+        H.append(
+            form_search_etud(
+                context,
+                dest_url=dest_url,
+                parameters=parameters,
+                parameters_keys=parameters_keys,
+                REQUEST=REQUEST,
+                title="Autre recherche",
+            )
+        )
+
+        for e in etuds:
+            target = dest_url + "?etudid=%s&amp;" % e["etudid"] + query_string
+            e["_nomprenom_target"] = target
+            e["inscription_target"] = target
+            e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
+            sco_groups.etud_add_group_infos(context, e, e["cursem"])
+
+        tab = GenTable(
+            columns_ids=("nomprenom", "code_nip", "inscription", "groupes"),
+            titles={
+                "nomprenom": "Etudiant",
+                "code_nip": "NIP",
+                "inscription": "Inscription",
+                "groupes": "Groupes",
+            },
+            rows=etuds,
+            html_sortable=True,
+            html_class="table_leftalign",
+            preferences=context.get_preferences(),
+        )
+        H.append(tab.html())
+        if len(etuds) > 20:  # si la page est grande
+            H.append(
+                form_search_etud(
+                    context,
+                    dest_url=dest_url,
+                    parameters=parameters,
+                    parameters_keys=parameters_keys,
+                    REQUEST=REQUEST,
+                    title="Autre recherche",
+                )
+            )
+
+    else:
+        H.append('<h2 style="color: red;">Aucun résultat pour "%s".</h2>' % expnom)
+        add_headers = True
+        no_side_bar = False
+    H.append(
+        """<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant</p>"""
+    )
+    if add_headers:
+        return (
+            context.sco_header(
+                REQUEST,
+                page_title="Choix d'un étudiant",
+                init_qtip=True,
+                javascripts=["js/etud_info.js"],
+                no_side_bar=no_side_bar,
+            )
+            + "\n".join(H)
+            + context.sco_footer(REQUEST)
+        )
+    else:
+        return "\n".join(H)
+
+
+# Was chercheEtudsInfo()
+def search_etuds_infos(context, expnom=None, code_nip=None, REQUEST=None):
+    """recherche les étudiants correspondants à expnom ou au code_nip
+    et ramene liste de mappings utilisables en DTML.        
+    """
+    may_be_nip = is_valid_code_nip(expnom)
+    cnx = context.GetDBConnexion()
+    if expnom and not may_be_nip:
+        expnom = strupper(expnom)  # les noms dans la BD sont en uppercase
+        etuds = scolars.etudident_list(cnx, args={"nom": expnom}, test="~")
+    else:
+        code_nip = code_nip or expnom
+        if code_nip:
+            etuds = scolars.etudident_list(cnx, args={"code_nip": code_nip})
+        else:
+            etuds = []
+    context.fillEtudsInfo(etuds)
+    return etuds
+
+
+def search_etud_by_name(context, term, REQUEST=None):
+    """Recherche noms étudiants par début du nom, pour autocomplete
+    Accepte aussi un début de code NIP (au moins 6 caractères)
+    Renvoie une liste de nom en JSON
+    """
+    cnx = context.GetDBConnexion()
+    may_be_nip = is_valid_code_nip(term)
+    # term = strupper(term) # conserve les accents
+    term = term.upper()
+    if (
+        not ALPHANUM_EXP.match(
+            term.decode(SCO_ENCODING)
+        )  #  n'autorise pas les caractères spéciaux
+        and not may_be_nip
+    ):
+        data = []
+    else:
+        if may_be_nip:
+            r = SimpleDictFetch(
+                context,
+                "SELECT nom, prenom, code_nip FROM identite WHERE code_nip LIKE %(beginning)s ORDER BY nom",
+                {"beginning": term + "%"},
+            )
+            data = [
+                {
+                    "label": "%s %s %s"
+                    % (x["code_nip"], x["nom"], scolars.format_prenom(x["prenom"])),
+                    "value": x["code_nip"],
+                }
+                for x in r
+            ]
+        else:
+            r = SimpleDictFetch(
+                context,
+                "SELECT nom, prenom FROM identite WHERE nom LIKE %(beginning)s ORDER BY nom",
+                {"beginning": term + "%"},
+            )
+
+            data = [
+                {
+                    "label": "%s %s" % (x["nom"], scolars.format_prenom(x["prenom"])),
+                    "value": x["nom"],
+                }
+                for x in r
+            ]
+    # log(data)
+    return sendJSON(REQUEST, data)
+
+
+# ---------- Recherche sur plusieurs département
+
+
+def form_search_etud_in_accessible_depts(context, REQUEST):
+    """Form recherche etudiants pour page accueil ScoDoc
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    # present form only to authenticated users
+    if not authuser.has_role("Authenticated"):
+        return ""
+    return """<form action="table_etud_in_accessible_depts" method="POST">
+    <b>Chercher étudiant:</b>
+    <input type="text" name="expnom" width=12 value="">
+    <input type="submit" value="Chercher">
+    <br/>(entrer une partie du nom ou le code NIP, cherche dans tous les départements autorisés)
+    """
+
+
+def can_view_dept(context, REQUEST):
+    """True if auth user can access (View) this context"""
+    authuser = REQUEST.AUTHENTICATED_USER
+    return authuser.has_permission(ScoView, context)
+
+
+def search_etud_in_accessible_depts(context, expnom=None, code_nip=None, REQUEST=None):
+    """
+    context est le ZScoDoc
+    result is a list of (sorted) etuds, one list per dept.
+    """
+    result = []
+    accessible_depts = []
+    deptList = context.list_depts()  # definis dans Zope
+    for dept in deptList:
+        # log('%s searching %s' % (str(REQUEST.AUTHENTICATED_USER),dept))
+        if can_view_dept(dept, REQUEST):
+            if expnom or code_nip:
+                accessible_depts.append(dept.Scolarite.DeptId())
+                etuds = search_etuds_infos(
+                    dept.Scolarite, expnom=expnom, code_nip=code_nip, REQUEST=REQUEST
+                )
+            else:
+                etuds = []
+            result.append(etuds)
+    return result, accessible_depts
+
+
+def table_etud_in_accessible_depts(context, expnom=None, REQUEST=None):
+    """
+    Page avec table étudiants trouvés, dans tous les departements.
+    Attention: nous sommes ici au niveau de ScoDoc, pas dans un département
+    """
+    result, accessible_depts = search_etud_in_accessible_depts(
+        context, expnom=expnom, REQUEST=REQUEST
+    )
+    H = [
+        """<div class="table_etud_in_accessible_depts">""",
+        """<h3>Recherche multi-département de "<tt>%s</tt>"</h3>""" % expnom,
+    ]
+    for etuds in result:
+        if etuds:
+            DeptId = etuds[0]["dept"]
+            # H.append('<h3>Département %s</h3>' % DeptId)
+            dest_url = DeptId + "/Scolarite/ficheEtud"
+            for e in etuds:
+                target = dest_url + "?etudid=%s" % e["etudid"]
+                e["_nomprenom_target"] = target
+                e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
+
+            tab = GenTable(
+                titles={"nomprenom": "Etudiants en " + DeptId},
+                columns_ids=("nomprenom",),
+                rows=etuds,
+                html_sortable=True,
+                html_class="table_leftalign",
+            )
+
+            H.append('<div class="table_etud_in_dept">')
+            H.append(tab.html())
+            H.append("</div>")
+    if len(accessible_depts) > 1:
+        ss = "s"
+    else:
+        ss = ""
+    H.append(
+        """<p>(recherche menée dans le%s département%s: %s)</p><p>
+    <a href=".." class="stdlink">Retour à l'accueil</a></p>"""
+        % (ss, ss, ", ".join(accessible_depts))
+    )
+    H.append("</div>")
+
+    return (
+        context.scodoc_top_html_header(REQUEST, page_title="Choix d'un étudiant")
+        + "\n".join(H)
+        + context.standard_html_footer(REQUEST)
+    )
+
+
+def search_inscr_etud_by_nip(context, code_nip, REQUEST=None, format="json"):
+    """Recherche multi-departement d'un étudiant par son code NIP
+    Seuls les départements accessibles par l'utilisateur sont cherchés.
+
+    Renvoie une liste des inscriptions de l'étudiants dans tout ScoDoc:
+    code_nip, nom, prenom, sexe, dept, formsemestre_id, date_debut_sem, date_fin_sem
+    """
+    result, depts = search_etud_in_accessible_depts(
+        context, code_nip=code_nip, REQUEST=REQUEST
+    )
+
+    T = []
+    for etuds in result:
+        if etuds:
+            DeptId = etuds[0]["dept"]
+            for e in etuds:
+                for sem in e["sems"]:
+                    T.append(
+                        {
+                            "dept": DeptId,
+                            "etudid": e["etudid"],
+                            "code_nip": e["code_nip"],
+                            "sexe": e["sexe"],
+                            "nom": e["nom"],
+                            "prenom": e["prenom"],
+                            "formsemestre_id": sem["formsemestre_id"],
+                            "date_debut_iso": sem["date_debut_iso"],
+                            "date_fin_iso": sem["date_fin_iso"],
+                        }
+                    )
+
+    columns_ids = (
+        "dept",
+        "etudid",
+        "code_nip",
+        "sexe",
+        "nom",
+        "prenom",
+        "formsemestre_id",
+        "date_debut_iso",
+        "date_fin_iso",
+    )
+    tab = GenTable(columns_ids=columns_ids, rows=T)
+
+    return tab.make_page(
+        context, format=format, with_html_headers=False, REQUEST=REQUEST, publish=True
+    )
diff --git a/sco_formations.py b/sco_formations.py
new file mode 100644
index 0000000000000000000000000000000000000000..c6b03f9b3b4870bd91a5ca6725b43c4fe0b31f50
--- /dev/null
+++ b/sco_formations.py
@@ -0,0 +1,316 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Import / Export de formations
+"""
+from operator import itemgetter
+
+from sco_utils import *
+import xml.dom.minidom
+
+from notesdb import *
+from notes_log import log
+import sco_codes_parcours
+import sco_formsemestre
+import sco_tag_module
+from gen_tables import GenTable
+
+
+def formation_export(
+    context, formation_id, export_ids=False, export_tags=True, format=None, REQUEST=None
+):
+    """Get a formation, with UE, matieres, modules
+    in desired format
+    """
+    F = context.formation_list(args={"formation_id": formation_id})[0]
+    ues = context.do_ue_list({"formation_id": formation_id})
+    F["ue"] = ues
+    for ue in ues:
+        ue_id = ue["ue_id"]
+        if not export_ids:
+            del ue["ue_id"]
+            del ue["formation_id"]
+        if ue["ects"] is None:
+            del ue["ects"]
+        mats = context.do_matiere_list({"ue_id": ue_id})
+        ue["matiere"] = mats
+        for mat in mats:
+            matiere_id = mat["matiere_id"]
+            if not export_ids:
+                del mat["matiere_id"]
+                del mat["ue_id"]
+            mods = context.do_module_list({"matiere_id": matiere_id})
+            mat["module"] = mods
+            for mod in mods:
+                if export_tags:
+                    # mod['tags'] = sco_tag_module.module_tag_list(context, module_id=mod['module_id'])
+                    tags = sco_tag_module.module_tag_list(
+                        context, module_id=mod["module_id"]
+                    )
+                    if tags:
+                        mod["tags"] = [{"name": x} for x in tags]
+                if not export_ids:
+                    del mod["ue_id"]
+                    del mod["matiere_id"]
+                    del mod["module_id"]
+                    del mod["formation_id"]
+                if mod["ects"] is None:
+                    del mod["ects"]
+
+    return sendResult(
+        REQUEST, F, name="formation", format=format, force_outer_xml_tag=False
+    )
+
+
+ELEMENT_NODE = 1
+TEXT_NODE = 3
+
+
+def XMLToDicts(element, encoding):
+    """Represent dom element as a dict
+    Example:
+       <foo x="1" y="2"><bar z="2"/></foo>
+    will give us:
+       ('foo', {'y': '2', 'x': '1'}, [('bar', {'z': '2'}, [])])
+    """
+    d = {}
+    # attributes
+    if element.attributes:
+        for i in range(len(element.attributes)):
+            a = element.attributes.item(i).nodeName.encode(encoding)
+            v = element.getAttribute(element.attributes.item(i).nodeName)
+            d[a] = v.encode(encoding)
+    # descendants
+    childs = []
+    for child in element.childNodes:
+        if child.nodeType == ELEMENT_NODE:
+            childs.append(XMLToDicts(child, encoding))
+    return (element.nodeName.encode(encoding), d, childs)
+
+
+def formation_import_xml(
+    context, REQUEST, doc, import_tags=True, encoding=SCO_ENCODING
+):
+    """Create a formation from XML representation
+    (format dumped by formation_export( format='xml' ))
+    """
+    log("formation_import_xml: doc=%s" % doc)
+    try:
+        dom = xml.dom.minidom.parseString(doc)
+    except:
+        log("formation_import_xml: invalid XML data")
+        raise ScoValueError("Fichier XML invalide")
+
+    f = dom.getElementsByTagName("formation")[0]  # or dom.documentElement
+    D = XMLToDicts(f, encoding)
+    assert D[0] == "formation"
+    F = D[1]
+    F_quoted = F.copy()
+    log("F=%s" % F)
+    quote_dict(F_quoted)
+    log("F_quoted=%s" % F_quoted)
+    # find new version number
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    log(
+        "select max(version) from notes_formations where acronyme=%(acronyme)s and titre=%(titre)s"
+        % F_quoted
+    )
+    cursor.execute(
+        "select max(version) from notes_formations where acronyme=%(acronyme)s and titre=%(titre)s",
+        F_quoted,
+    )
+    res = cursor.fetchall()
+    try:
+        version = int(res[0][0]) + 1
+    except:
+        version = 1
+    F["version"] = version
+    # create formation
+    # F_unquoted = F.copy()
+    # unescape_html_dict(F_unquoted)
+    formation_id = context.do_formation_create(F, REQUEST)
+    log("formation %s created" % formation_id)
+    ues_old2new = {}  # xml ue_id : new ue_id
+    modules_old2new = {}  # xml module_id : new module_id
+    # (nb: mecanisme utilise pour cloner semestres seulement, pas pour I/O XML)
+    # -- create UEs
+    for ue_info in D[2]:
+        assert ue_info[0] == "ue"
+        ue_info[1]["formation_id"] = formation_id
+        if "ue_id" in ue_info[1]:
+            xml_ue_id = ue_info[1]["ue_id"]
+            del ue_info[1]["ue_id"]
+        else:
+            xml_ue_id = None
+        ue_id = context.do_ue_create(ue_info[1], REQUEST)
+        if xml_ue_id:
+            ues_old2new[xml_ue_id] = ue_id
+        # -- create matieres
+        for mat_info in ue_info[2]:
+            assert mat_info[0] == "matiere"
+            mat_info[1]["ue_id"] = ue_id
+            mat_id = context.do_matiere_create(mat_info[1], REQUEST)
+            # -- create modules
+            for mod_info in mat_info[2]:
+                assert mod_info[0] == "module"
+                if "module_id" in mod_info[1]:
+                    xml_module_id = mod_info[1]["module_id"]
+                    del mod_info[1]["module_id"]
+                else:
+                    xml_module_id = None
+                mod_info[1]["formation_id"] = formation_id
+                mod_info[1]["matiere_id"] = mat_id
+                mod_info[1]["ue_id"] = ue_id
+                mod_id = context.do_module_create(mod_info[1], REQUEST)
+                if xml_module_id:
+                    modules_old2new[xml_module_id] = mod_id
+                if import_tags:
+                    if len(mod_info) > 2:
+                        tag_names = [t[1]["name"] for t in mod_info[2]]
+                        sco_tag_module.module_tag_set(context, mod_id, tag_names)
+
+    return formation_id, modules_old2new, ues_old2new
+
+
+def formation_list_table(context, formation_id=None, args={}, REQUEST=None):
+    """List formation, grouped by titre and sorted by versions
+    and listing associated semestres
+    returns a table
+    """
+    formations = context.formation_list(formation_id=formation_id, args=args)
+    title = "Programmes pédagogiques"
+    lockicon = icontag(
+        "lock32_img", title="Comporte des semestres verrouillés", border="0"
+    )
+    suppricon = icontag(
+        "delete_small_img", border="0", alt="supprimer", title="Supprimer"
+    )
+    editicon = icontag(
+        "edit_img", border="0", alt="modifier", title="Modifier titres et code"
+    )
+
+    editable = REQUEST.AUTHENTICATED_USER.has_permission(ScoChangeFormation, context)
+
+    # Traduit/ajoute des champs à afficher:
+    for f in formations:
+        try:
+            f["parcours_name"] = sco_codes_parcours.get_parcours_from_code(
+                f["type_parcours"]
+            ).NAME
+        except:
+            f["parcours_name"] = ""
+        f["_titre_target"] = "ue_list?formation_id=%(formation_id)s" % f
+        f["_titre_link_class"] = "stdlink"
+        # Ajoute les semestres associés à chaque formation:
+        f["sems"] = sco_formsemestre.do_formsemestre_list(
+            context, args={"formation_id": f["formation_id"]}
+        )
+        f["sems_list_txt"] = ", ".join([s["session_id"] for s in f["sems"]])
+        f["_sems_list_txt_html"] = ", ".join(
+            [
+                '<a class="discretelink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(session_id)s<a>'
+                % s
+                for s in f["sems"]
+            ]
+            + [
+                '<a class="stdlink" href="formsemestre_createwithmodules?formation_id=%(formation_id)s&amp;semestre_id=1">ajouter</a>'
+                % f
+            ]
+        )
+        if f["sems"]:
+            f["date_fin_dernier_sem"] = max([s["date_fin_iso"] for s in f["sems"]])
+            f["annee_dernier_sem"] = f["date_fin_dernier_sem"].split("-")[0]
+        else:
+            f["date_fin_dernier_sem"] = ""
+            f["annee_dernier_sem"] = ""
+        locked = context.formation_has_locked_sems(f["formation_id"])
+        #
+        if locked:
+            but_locked = lockicon
+        else:
+            but_locked = '<span class="but_placeholder"></span>'
+        if editable and not locked:
+            but_suppr = (
+                '<a class="stdlink" href="formation_delete?formation_id=%s">%s</a>'
+                % (f["formation_id"], suppricon)
+            )
+        else:
+            but_suppr = '<span class="but_placeholder"></span>'
+        if editable:
+            but_edit = (
+                '<a class="stdlink" href="formation_edit?formation_id=%s">%s</a>'
+                % (f["formation_id"], editicon)
+            )
+        else:
+            but_edit = '<span class="but_placeholder"></span>'
+        f["buttons"] = ""
+        f["_buttons_html"] = but_locked + but_suppr + but_edit
+    # Tri par annee_denier_sem, type, acronyme, titre, version décroissante
+    formations.sort(key=itemgetter("version"), reverse=True)
+    formations.sort(key=itemgetter("titre"))
+    formations.sort(key=itemgetter("acronyme"))
+    formations.sort(key=itemgetter("parcours_name"))
+    formations.sort(
+        key=itemgetter("annee_dernier_sem"), reverse=True
+    )  # plus recemments utilises en tete
+
+    #
+    columns_ids = (
+        "buttons",
+        "acronyme",
+        "parcours_name",
+        "formation_code",
+        "version",
+        "titre",
+        "sems_list_txt",
+    )
+    titles = {
+        "buttons": "",
+        "acronyme": "Acro.",
+        "parcours_name": "Type",
+        "titre": "Titre",
+        "version": "Version",
+        "formation_code": "Code",
+        "sems_list_txt": "Semestres",
+    }
+    return GenTable(
+        columns_ids=columns_ids,
+        rows=formations,
+        titles=titles,
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        caption=title,
+        html_caption=title,
+        table_id="formation_list_table",
+        html_class="formation_list_table table_leftalign",
+        html_with_td_classes=True,
+        html_sortable=True,
+        base_url="%s?formation_id=%s" % (REQUEST.URL0, formation_id),
+        page_title=title,
+        pdf_title=title,
+        preferences=context.get_preferences(),
+    )
diff --git a/sco_formsemestre.py b/sco_formsemestre.py
new file mode 100644
index 0000000000000000000000000000000000000000..140530f810310e8fb7052cc81e5fcff2fbe94f14
--- /dev/null
+++ b/sco_formsemestre.py
@@ -0,0 +1,555 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Operations de base sur les formsemestres
+"""
+
+from sco_utils import *
+from notesdb import *
+from notes_log import log
+from gen_tables import GenTable
+
+import sco_codes_parcours
+from sco_codes_parcours import NO_SEMESTRE_ID
+
+_formsemestreEditor = EditableTable(
+    "notes_formsemestre",
+    "formsemestre_id",
+    (
+        "formsemestre_id",
+        "semestre_id",
+        "formation_id",
+        "titre",
+        "date_debut",
+        "date_fin",
+        "gestion_compensation",
+        "gestion_semestrielle",
+        "etat",
+        "bul_hide_xml",
+        "bul_bgcolor",
+        "modalite",
+        "resp_can_edit",
+        "resp_can_change_ens",
+        "ens_can_edit_eval",
+        "elt_sem_apo",
+        "elt_annee_apo",
+    ),
+    sortkey="date_debut",
+    output_formators={
+        "date_debut": DateISOtoDMY,
+        "date_fin": DateISOtoDMY,
+        "gestion_compensation": str,
+        "gestion_semestrielle": str,
+        "etat": str,
+        "bul_hide_xml": str,
+    },
+    input_formators={
+        "date_debut": DateDMYtoISO,
+        "date_fin": DateDMYtoISO,
+        "gestion_compensation": int,
+        "gestion_semestrielle": int,
+        "etat": int,
+        "bul_hide_xml": int,
+    },
+)
+
+
+def get_formsemestre(context, formsemestre_id):
+    "list ONE formsemestre"
+    try:
+        sem = do_formsemestre_list(context, args={"formsemestre_id": formsemestre_id})[
+            0
+        ]
+        return sem
+    except:
+        log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id)
+        raise
+
+
+def do_formsemestre_list(context, *a, **kw):
+    "list formsemestres"
+    # log('do_formsemestre_list: a=%s kw=%s' % (str(a),str(kw)))
+    cnx = context.GetDBConnexion()
+
+    sems = _formsemestreEditor.list(cnx, *a, **kw)
+
+    # Ajoute les étapes Apogee et les responsables:
+    for sem in sems:
+        sem["etapes"] = read_formsemestre_etapes(context, sem["formsemestre_id"])
+        sem["responsables"] = read_formsemestre_responsables(
+            context, sem["formsemestre_id"]
+        )
+
+    # Filtre sur code etape si indiqué:
+    if "args" in kw:
+        etape = kw["args"].get("etape_apo", None)
+        if etape:
+            sems = [sem for sem in sems if etape in sem["etapes"]]
+
+    for sem in sems:
+        formsemestre_enrich(context, sem)
+
+    # tri par date
+    sems.sort(
+        lambda x, y: cmp(
+            (y["dateord"], y["semestre_id"]), (x["dateord"], x["semestre_id"])
+        )
+    )
+
+    return sems
+
+
+def formsemestre_enrich(context, sem):
+    """Ajoute champs souvent utiles: titre + annee et dateord (pour tris)
+    """
+    # imports ici pour eviter refs circulaires
+    import sco_formsemestre_edit
+    import scolars
+
+    F = context.Notes.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
+    # 'S1', 'S2', ... ou '' pour les monosemestres
+    if sem["semestre_id"] != NO_SEMESTRE_ID:
+        sem["sem_id_txt"] = "S%s" % sem["semestre_id"]
+    else:
+        sem["sem_id_txt"] = ""
+    # Nom avec numero semestre:
+    sem["titre_num"] = sem["titre"]  # eg "DUT Informatique"
+    if sem["semestre_id"] != NO_SEMESTRE_ID:
+        sem["titre_num"] += " %s %s" % (
+            parcours.SESSION_NAME,
+            sem["semestre_id"],
+        )  # eg "DUT Informatique semestre 2"
+
+    sem["dateord"] = DateDMYtoISO(sem["date_debut"])
+    sem["date_debut_iso"] = DateDMYtoISO(sem["date_debut"])
+    sem["date_fin_iso"] = DateDMYtoISO(sem["date_fin"])
+    try:
+        mois_debut, annee_debut = sem["date_debut"].split("/")[1:]
+    except:
+        mois_debut, annee_debut = "", ""
+    try:
+        mois_fin, annee_fin = sem["date_fin"].split("/")[1:]
+    except:
+        mois_fin, annee_fin = "", ""
+    sem["annee_debut"] = annee_debut
+    sem["annee_fin"] = annee_fin
+    sem["mois_debut_ord"] = int(mois_debut)
+    sem["mois_fin_ord"] = int(mois_fin)
+
+    sem["annee"] = annee_debut
+    # 2007 ou 2007-2008:
+    sem["anneescolaire"] = annee_scolaire_repr(int(annee_debut), sem["mois_debut_ord"])
+    # La période: considère comme "S1" (ou S3) les débuts en aout-sept-octobre
+    # devrait sans doute pouvoir etre changé...
+    if sem["mois_debut_ord"] >= 8 and sem["mois_debut_ord"] <= 10:
+        sem["periode"] = 1  # typiquement, début en septembre: S1, S3...
+    else:
+        sem["periode"] = 2  # typiquement, début en février: S2, S4...
+
+    sem["titreannee"] = "%s %s  %s" % (
+        sem["titre_num"],
+        sem.get("modalite", ""),
+        annee_debut,
+    )
+    if annee_fin != annee_debut:
+        sem["titreannee"] += "-" + annee_fin
+        sem["annee"] += "-" + annee_fin
+    # et les dates sous la forme "oct 2007 - fev 2008"
+    months = scolars.abbrvmonthsnames
+    if mois_debut:
+        mois_debut = months[int(mois_debut) - 1]
+    if mois_fin:
+        mois_fin = months[int(mois_fin) - 1]
+    sem["mois_debut"] = mois_debut + " " + annee_debut
+    sem["mois_fin"] = mois_fin + " " + annee_fin
+    sem["titremois"] = "%s %s  (%s - %s)" % (
+        sem["titre_num"],
+        sem.get("modalite", ""),
+        sem["mois_debut"],
+        sem["mois_fin"],
+    )
+    sem["session_id"] = sco_formsemestre_edit.get_formsemestre_session_id(
+        context, sem, F, parcours
+    )
+    sem["etapes"] = read_formsemestre_etapes(context, sem["formsemestre_id"])
+    sem["etapes_apo_str"] = formsemestre_etape_apo_str(sem)
+    sem["responsables"] = read_formsemestre_responsables(
+        context, sem["formsemestre_id"]
+    )
+
+
+def formsemestre_etape_apo_str(sem):
+    "chaine décrivant le(s) codes étapes Apogée"
+    return etapes_apo_str(sem["etapes"])
+
+
+def etapes_apo_str(etapes):
+    "Chaine decrivant une liste d'instance de ApoEtapeVDI"
+    return ", ".join([str(x) for x in etapes])
+
+
+def do_formsemestre_edit(context, sem, cnx=None, **kw):
+    if not cnx:
+        cnx = context.GetDBConnexion()
+
+    _formsemestreEditor.edit(cnx, sem, **kw)
+    write_formsemestre_etapes(context, sem)
+    write_formsemestre_responsables(context, sem)
+
+    context._inval_cache(formsemestre_id=sem["formsemestre_id"])  # > modif formsemestre
+
+
+def read_formsemestre_responsables(context, formsemestre_id):
+    """recupere liste des responsables de ce semestre
+    :returns: liste de chaines
+    """
+    r = SimpleDictFetch(
+        context,
+        "SELECT responsable_id FROM notes_formsemestre_responsables WHERE formsemestre_id = %(formsemestre_id)s",
+        {"formsemestre_id": formsemestre_id},
+    )
+    return [x["responsable_id"] for x in r]
+
+
+def write_formsemestre_responsables(context, sem):
+    return _write_formsemestre_aux(context, sem, "responsables", "responsable_id")
+
+
+def read_formsemestre_etapes(context, formsemestre_id):
+    """recupere liste des codes etapes associés à ce semestre
+    :returns: liste d'instance de ApoEtapeVDI
+    """
+    r = SimpleDictFetch(
+        context,
+        "SELECT etape_apo FROM notes_formsemestre_etapes WHERE formsemestre_id = %(formsemestre_id)s",
+        {"formsemestre_id": formsemestre_id},
+    )
+    return [ApoEtapeVDI(x["etape_apo"]) for x in r if x["etape_apo"]]
+
+
+def write_formsemestre_etapes(context, sem):
+    return _write_formsemestre_aux(context, sem, "etapes", "etape_apo")
+
+
+def _write_formsemestre_aux(context, sem, fieldname, valuename):
+    """fieldname: 'etapes' ou 'responsables'
+    valuename: 'etape_apo' ou 'responsable_id'
+    """
+    if not "etapes" in sem:
+        return
+    cnx = context.GetDBConnexion(autocommit=False)
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    tablename = "notes_formsemestre_" + fieldname
+    try:
+        cursor.execute(
+            "DELETE from " + tablename + " where formsemestre_id = %(formsemestre_id)s",
+            {"formsemestre_id": sem["formsemestre_id"]},
+        )
+        for item in sem[fieldname]:
+            if item:
+                cursor.execute(
+                    "INSERT INTO "
+                    + tablename
+                    + " (formsemestre_id, "
+                    + valuename
+                    + ") VALUES (%(formsemestre_id)s, %("
+                    + valuename
+                    + ")s)",
+                    {"formsemestre_id": sem["formsemestre_id"], valuename: str(item)},
+                )
+    except:
+        log("Warning: exception in write_formsemestre_aux !")
+        cnx.rollback()
+        raise
+    cnx.commit()
+
+
+# ------ Utilisé pour stocker le VDI avec le code étape (noms de fichiers maquettes et code semestres)
+class ApoEtapeVDI:
+    _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, ...
+        """
+        if other is None:
+            return -1
+        if type(other) == str:
+            other = ApoEtapeVDI(other)
+
+        if self.vdi and other.vdi:
+            return cmp((self.etape, self.vdi), (other.etape, other.vdi))
+        else:
+            return cmp(self.etape, other.etape)
+
+    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(
+        [
+            context.Users.user_info(responsable_id)["nomprenom"]
+            for responsable_id in sem["responsables"]
+        ]
+    )
+
+
+def sem_in_semestre_scolaire(context, sem, year=False, saison=0, REQUEST=None):
+    """n'utilise que la date de debut, pivot au 1er aout
+    si annee non specifiée, année scolaire courante
+    Patch Jmp: ajout du parametre optionnel saison 
+    1 = sept, 0 = janvier, None = année complète
+    si saison non spécifiée: année complète
+    pivot de saison au 1er décembre
+    XXX TODO: la période (ici appelée "saison" devrait être éditable 
+    manuellement dans le formsemestre_edit afin de couvrir els cas particulier
+    comme un semestre S2 qui commecerait en décembre... voire novembre.
+    )
+    """
+    if not year:
+        year = AnneeScolaire(REQUEST)
+    # est-on dans la même année universitaire ?
+    if sem["mois_debut_ord"] > 7:
+        if sem["annee_debut"] != str(year):
+            return False
+    else:
+        if sem["annee_debut"] != str(year + 1):
+            return False
+    # rafinement éventuel sur le semestre
+    # saison is None => pas de rafinement => True
+    if saison == 0:
+        return True
+    elif saison == 1:  # calcul en fonction de la saison
+        return sem["mois_debut_ord"] > 7 and sem["mois_debut_ord"] < 12
+    else:  # saison == 0
+        return sem["mois_debut_ord"] <= 7 or sem["mois_debut_ord"] == 12
+
+
+def sem_in_annee_scolaire(context, sem, year=False, REQUEST=None):
+    """Test si sem appartient à l'année scolaire year (int).
+    N'utilise que la date de debut, pivot au 1er août.
+    Si annee non specifiée, année scolaire courante
+    """
+    if not year:
+        year = AnneeScolaire(REQUEST)
+    return ((sem["annee_debut"] == str(year)) and (sem["mois_debut_ord"] > 7)) or (
+        (sem["annee_debut"] == str(year + 1)) and (sem["mois_debut_ord"] <= 7)
+    )
+
+
+def sem_une_annee(context, sem):
+    """Test si sem est entièrement sur la même année scolaire.
+    (ce n'est pas obligatoire mais si ce n'est pas le cas les exports Apogée ne vont pas fonctionner)
+    pivot au 1er août.
+    """
+    if sem["date_debut_iso"] > sem["date_fin_iso"]:
+        log('Warning: semestre %(formsemestre_id)s begins after ending !' % sem)
+        return False
+    
+    debut = int(sem["annee_debut"])
+    if sem["mois_debut_ord"] < 8: # considere que debut sur l'anne scolaire precedente
+        debut -= 1
+    fin = int(sem["annee_fin"])
+    if sem["mois_fin_ord"] < 9: # 9 (sept) pour autoriser un début en sept et une fin en aout
+        fin -= 1
+    return debut == fin
+
+def scodoc_get_all_unlocked_sems(context):
+    """Liste de tous les semestres non verrouillés de tous les départements"""
+    depts = context.list_depts()
+    semdepts = []
+    for dept in depts:
+        semdepts += [
+            (sem, dept.Scolarite.Notes)
+            for sem in do_formsemestre_list(dept.Scolarite.Notes)
+            if sem["etat"] == "1"
+        ]
+    return semdepts
+
+
+def table_formsemestres(
+    context,
+    sems,
+    columns_ids=(),
+    sup_columns_ids=(),
+    html_title="<h2>Semestres</h2>",
+    html_next_section="",
+    REQUEST=None,
+):
+    """Une table presentant des semestres
+    """
+    for sem in sems:
+        sem_set_responsable_name(context, sem)
+        sem["_titre_num_target"] = (
+            "formsemestre_status?formsemestre_id=%s" % sem["formsemestre_id"]
+        )
+
+    if not columns_ids:
+        columns_ids = (
+            "etat",
+            "modalite",
+            "mois_debut",
+            "mois_fin",
+            "titre_num",
+            "responsable_name",
+            "etapes_apo_str",
+        )
+    columns_ids += sup_columns_ids
+
+    titles = {
+        "modalite": "",
+        "mois_debut": "Début",
+        "mois_fin": "Fin",
+        "titre_num": "Semestre",
+        "responsable_name": "Resp.",
+        "etapes_apo_str": "Apo.",
+    }
+    if sems:
+        preferences = context.get_preferences(sems[0]["formsemestre_id"])
+    else:
+        preferences = context.get_preferences()
+    tab = GenTable(
+        columns_ids=columns_ids,
+        rows=sems,
+        titles=titles,
+        html_class="table_leftalign",
+        html_sortable=True,
+        html_title=html_title,
+        html_next_section=html_next_section,
+        html_empty_element="<p><em>aucun résultat</em></p>",
+        page_title="Semestres",
+        preferences=preferences,
+    )
+    return tab
+
+
+def list_formsemestre_by_etape(
+    context, etape_apo=None, annee_scolaire=False, REQUEST=None
+):
+    """Liste des semestres de cette etape, pour l'annee scolaire indiquée (sinon, pour toutes)
+    """
+    ds = {}  # formsemestre_id : sem
+    if etape_apo:
+        sems = do_formsemestre_list(context, args={"etape_apo": etape_apo})
+        for sem in sems:
+            if annee_scolaire:  # restriction annee scolaire
+                if sem_in_annee_scolaire(
+                    context, sem, year=annee_scolaire, REQUEST=REQUEST
+                ):
+                    ds[sem["formsemestre_id"]] = sem
+        sems = ds.values()
+    else:
+        sems = do_formsemestre_list(context)
+        if annee_scolaire:
+            sems = [
+                sem
+                for sem in sems
+                if sem_in_annee_scolaire(
+                    context, sem, year=annee_scolaire, REQUEST=REQUEST
+                )
+            ]
+
+    sems.sort(key=lambda s: (s["modalite"], s["dateord"]))
+    return sems
+
+
+def view_formsemestre_by_etape(context, etape_apo=None, format="html", REQUEST=None):
+    """Affiche table des semestres correspondants à l'étape
+    """
+    if etape_apo:
+        html_title = (
+            """<h2>Semestres courants de l'étape <tt>%s</tt></h2>""" % etape_apo
+        )
+    else:
+        html_title = """<h2>Semestres courants</h2>"""
+    tab = table_formsemestres(
+        context,
+        list_formsemestre_by_etape(
+            context, etape_apo=etape_apo, annee_scolaire=AnneeScolaire(REQUEST)
+        ),
+        html_title=html_title,
+        html_next_section="""<form action="view_formsemestre_by_etape">
+    Etape: <input name="etape_apo" type="text" size="8"></input>    
+        </form>""",
+        REQUEST=REQUEST,
+    )
+    tab.base_url = "%s?etape_apo=%s" % (REQUEST.URL0, etape_apo or "")
+    return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+
+def sem_has_etape(sem, code_etape):
+    return code_etape in sem["etapes"]
diff --git a/sco_formsemestre_custommenu.py b/sco_formsemestre_custommenu.py
new file mode 100644
index 0000000000000000000000000000000000000000..840d6b2e2e1b5236a11c11106149b700139b7246
--- /dev/null
+++ b/sco_formsemestre_custommenu.py
@@ -0,0 +1,158 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Menu "custom" (défini par l'utilisateur) dans les semestres
+"""
+
+
+from sco_utils import *
+from notesdb import *
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+import sco_formsemestre
+import sco_formsemestre_status
+import sco_edt_cal
+
+_custommenuEditor = EditableTable(
+    "notes_formsemestre_custommenu",
+    "custommenu_id",
+    ("custommenu_id", "formsemestre_id", "title", "url", "idx"),
+    sortkey="idx",
+)
+
+
+notes_formsemestre_custommenu_create = _custommenuEditor.create
+notes_formsemestre_custommenu_list = _custommenuEditor.list
+notes_formsemestre_custommenu_edit = _custommenuEditor.edit
+
+
+def formsemestre_custommenu_get(context, formsemestre_id):
+    "returns dict [ { 'title' :  xxx, 'url' : xxx } ]"
+    cnx = context.GetDBConnexion()
+    vals = notes_formsemestre_custommenu_list(cnx, {"formsemestre_id": formsemestre_id})
+    return vals
+
+
+def formsemestre_custommenu_html(context, formsemestre_id, base_url=""):
+    "HTML code for custom menu"
+    menu = []
+    # Calendrier électronique ?
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    ics_url = sco_edt_cal.formsemestre_get_ics_url(context, sem)
+    if ics_url:
+        menu.append({"title": "Emploi du temps (ics)", "url": ics_url})
+    menu += formsemestre_custommenu_get(context, formsemestre_id)
+    menu.append(
+        {
+            "title": "Modifier ce menu...",
+            "url": base_url
+            + "formsemestre_custommenu_edit?formsemestre_id="
+            + formsemestre_id,
+        }
+    )
+    return sco_formsemestre_status.makeMenu("Liens", menu)
+
+
+def formsemestre_custommenu_edit(context, formsemestre_id, REQUEST=None):
+    """Dialog to edit the custom menu"""
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    H = [
+        context.html_sem_header(REQUEST, "Modification du menu du semestre ", sem),
+        """<p class="help">Ce menu, spécifique à chaque semestre, peut être utilisé pour placer des liens vers vos applications préférées.</p>
+          <p class="help">Procédez en plusieurs fois si vous voulez ajouter plusieurs items.</p>""",
+    ]
+    descr = [
+        ("formsemestre_id", {"input_type": "hidden"}),
+        (
+            "sep",
+            {
+                "input_type": "separator",
+                "template": "<tr><td><b>Titre</b></td><td><b>URL</b></td></tr>",
+            },
+        ),
+    ]
+    menu = formsemestre_custommenu_get(context, formsemestre_id)
+    menu.append({"custommenu_id": "new", "url": "", "title": ""})
+    initvalues = {}
+    for item in menu:
+        descr.append(
+            (
+                "title_" + item["custommenu_id"],
+                {"size": 40, "template": '<tr><td class="tf-field">%(elem)s</td>'},
+            )
+        )
+        descr.append(
+            (
+                "url_" + item["custommenu_id"],
+                {"size": 80, "template": '<td class="tf-field">%(elem)s</td></tr>'},
+            )
+        )
+        initvalues["title_" + item["custommenu_id"]] = item["title"]
+        initvalues["url_" + item["custommenu_id"]] = item["url"]
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        initvalues=initvalues,
+        cancelbutton="Annuler",
+        method="GET",
+        submitlabel="Enregistrer",
+        name="tf",
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + "\n" + tf[1] + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+    else:
+        # form submission
+        cnx = context.GetDBConnexion()
+        # add new
+        if tf[2]["title_new"]:
+            notes_formsemestre_custommenu_create(
+                cnx,
+                {
+                    "formsemestre_id": formsemestre_id,
+                    "title": tf[2]["title_new"],
+                    "url": tf[2]["url_new"],
+                },
+            )
+        # edit existings
+        s = "title_"
+        for x in tf[2].keys():
+            if x[: len(s)] == s and x != "title_new":
+                custommenu_id = x[len(s) :]
+                notes_formsemestre_custommenu_edit(
+                    cnx,
+                    {
+                        "custommenu_id": custommenu_id,
+                        "title": tf[2]["title_" + custommenu_id],
+                        "url": tf[2]["url_" + custommenu_id],
+                    },
+                )
+        REQUEST.RESPONSE.redirect(
+            "formsemestre_status?formsemestre_id=%s" % formsemestre_id
+        )
diff --git a/sco_formsemestre_edit.py b/sco_formsemestre_edit.py
new file mode 100644
index 0000000000000000000000000000000000000000..f1796cfb1855d1d8054278ce77a7946328c75f31
--- /dev/null
+++ b/sco_formsemestre_edit.py
@@ -0,0 +1,1703 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Form choix modules / responsables et creation formsemestre
+"""
+
+from notesdb import *
+from sco_utils import *
+import sco_groups
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+import notes_table
+import sco_portal_apogee
+import scolars
+import sco_parcours_dut
+import sco_codes_parcours
+import sco_compute_moy
+import sco_modalites
+import sco_formsemestre
+from sco_formsemestre import ApoEtapeVDI
+
+
+def _default_sem_title(F):
+    """Default title for a semestre in formation F"""
+    return F["titre"]
+
+
+def formsemestre_createwithmodules(context, REQUEST=None):
+    """Page création d'un semestre"""
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Création d'un semestre",
+            javascripts=["libjs/AutoSuggest.js"],
+            cssstyles=["css/autosuggest_inquisitor.css"],
+            bodyOnLoad="init_tf_form('')",
+        ),
+        """<h2>Mise en place d'un semestre de formation</h2>""",
+        do_formsemestre_createwithmodules(context, REQUEST=REQUEST),
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def formsemestre_editwithmodules(context, REQUEST, formsemestre_id):
+    """Page modification semestre"""
+    # portage from dtml
+    authuser = REQUEST.AUTHENTICATED_USER
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    H = [
+        context.html_sem_header(
+            REQUEST,
+            "Modification du semestre",
+            sem,
+            javascripts=["libjs/AutoSuggest.js"],
+            cssstyles=["css/autosuggest_inquisitor.css"],
+            bodyOnLoad="init_tf_form('')",
+        )
+    ]
+    if sem["etat"] != "1":
+        H.append(
+            """<p>%s<b>Ce semestre est verrouillé.</b></p>"""
+            % icontag("lock_img", border="0", title="Semestre verrouillé")
+        )
+    else:
+        H.append(do_formsemestre_createwithmodules(context, REQUEST=REQUEST, edit=1))
+        if not REQUEST.get("tf-submitted", False):
+            H.append(
+                """<p class="help">Seuls les modules cochés font partie de ce semestre. Pour les retirer, les décocher et appuyer sur le bouton "modifier".
+</p>
+<p class="help">Attention : s'il y a déjà des évaluations dans un module, il ne peut pas être supprimé !</p>
+<p class="help">Les modules ont toujours un responsable. Par défaut, c'est le directeur des études.</p>"""
+            )
+
+    return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+def can_edit_sem(context, REQUEST, formsemestre_id="", sem=None):
+    """Return sem if user can edit it, False otherwise
+    """
+    sem = sem or sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    authuser = REQUEST.AUTHENTICATED_USER
+    if not authuser.has_permission(ScoImplement, context):  # pas chef
+        if not sem["resp_can_edit"] or str(authuser) not in sem["responsables"]:
+            return False
+    return sem
+
+
+def do_formsemestre_createwithmodules(context, REQUEST=None, edit=False):
+    "Form choix modules / responsables et creation formsemestre"
+    # Fonction accessible à tous, controle acces à la main:
+    if edit:
+        formsemestre_id = REQUEST.form["formsemestre_id"]
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    authuser = REQUEST.AUTHENTICATED_USER
+    if not authuser.has_permission(ScoImplement, context):
+        if not edit:
+            # il faut ScoImplement pour creer un semestre
+            raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
+        else:
+            if not sem["resp_can_edit"] or str(authuser) not in sem["responsables"]:
+                raise AccessDenied(
+                    "vous n'avez pas le droit d'effectuer cette opération"
+                )
+
+    # Liste des enseignants avec forme pour affichage / saisie avec suggestion
+    userlist = context.Users.get_userlist()
+    login2display = {}  # user_name : forme pour affichage = "NOM Prenom (login)"
+    for u in userlist:
+        login2display[u["user_name"]] = u["nomplogin"]
+    allowed_user_names = login2display.values() + [""]
+    #
+    formation_id = REQUEST.form["formation_id"]
+    F = context.formation_list(args={"formation_id": formation_id})
+    if not F:
+        raise ScoValueError("Formation inexistante !")
+    F = F[0]
+    if not edit:
+        initvalues = {"titre": _default_sem_title(F)}
+        semestre_id = REQUEST.form["semestre_id"]
+        sem_module_ids = set()
+    else:
+        # setup form init values
+        initvalues = sem
+        semestre_id = initvalues["semestre_id"]
+        #        initvalues['inscrire_etuds'] = initvalues.get('inscrire_etuds','1')
+        #        if initvalues['inscrire_etuds'] == '1':
+        #            initvalues['inscrire_etudslist'] = ['X']
+        #        else:
+        #            initvalues['inscrire_etudslist'] = []
+        #        if REQUEST.form.get('tf-submitted',False) and not REQUEST.form.has_key('inscrire_etudslist'):
+        #            REQUEST.form['inscrire_etudslist'] = []
+        # add associated modules to tf-checked
+        ams = context.do_moduleimpl_list(formsemestre_id=formsemestre_id)
+        sem_module_ids = set([x["module_id"] for x in ams])
+        initvalues["tf-checked"] = [x["module_id"] for x in ams]
+        for x in ams:
+            initvalues[str(x["module_id"])] = login2display.get(
+                x["responsable_id"], x["responsable_id"]
+            )
+
+        initvalues["responsable_id"] = login2display.get(
+            sem["responsables"][0], sem["responsables"][0]
+        )
+        if len(sem["responsables"]) > 1:
+            initvalues["responsable_id2"] = login2display.get(
+                sem["responsables"][1], sem["responsables"][1]
+            )
+
+    # Liste des ID de semestres
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute("select semestre_id from notes_semestres")
+    semestre_id_list = [str(x[0]) for x in cursor.fetchall()]
+    semestre_id_labels = []
+    for sid in semestre_id_list:
+        if sid == "-1":
+            semestre_id_labels.append("pas de semestres")
+        else:
+            semestre_id_labels.append(sid)
+    # Liste des modules  dans ce semestre de cette formation
+    # on pourrait faire un simple context.module_list( )
+    # mais si on veut l'ordre du PPN (groupe par UE et matieres) il faut:
+    mods = []  # liste de dicts
+    uelist = context.do_ue_list({"formation_id": formation_id})
+    for ue in uelist:
+        matlist = context.do_matiere_list({"ue_id": ue["ue_id"]})
+        for mat in matlist:
+            modsmat = context.do_module_list({"matiere_id": mat["matiere_id"]})
+            # XXX debug checks
+            for m in modsmat:
+                if m["ue_id"] != ue["ue_id"]:
+                    log(
+                        "XXX createwithmodules: m.ue_id=%s != u.ue_id=%s !"
+                        % (m["ue_id"], ue["ue_id"])
+                    )
+                if m["formation_id"] != formation_id:
+                    log(
+                        "XXX createwithmodules: formation_id=%s\n\tm=%s"
+                        % (formation_id, str(m))
+                    )
+                if m["formation_id"] != ue["formation_id"]:
+                    log(
+                        "XXX createwithmodules: formation_id=%s\n\tue=%s\tm=%s"
+                        % (formation_id, str(ue), str(m))
+                    )
+            # /debug
+            mods = mods + modsmat
+    # Pour regroupement des modules par semestres:
+    semestre_ids = {}
+    for mod in mods:
+        semestre_ids[mod["semestre_id"]] = 1
+    semestre_ids = semestre_ids.keys()
+    semestre_ids.sort()
+
+    modalites = sco_modalites.do_modalite_list(context)
+    modalites_abbrv = [m["modalite"] for m in modalites]
+    modalites_titles = [m["titre"] for m in modalites]
+    #
+    modform = [
+        ("formsemestre_id", {"input_type": "hidden"}),
+        ("formation_id", {"input_type": "hidden", "default": formation_id}),
+        (
+            "date_debut",
+            {
+                "title": "Date de début",  # j/m/a
+                "input_type": "date",
+                "explanation": "j/m/a",
+                "size": 9,
+                "allow_null": False,
+            },
+        ),
+        (
+            "date_fin",
+            {
+                "title": "Date de fin",  # j/m/a
+                "input_type": "date",
+                "explanation": "j/m/a",
+                "size": 9,
+                "allow_null": False,
+            },
+        ),
+        (
+            "responsable_id",
+            {
+                "input_type": "text_suggest",
+                "size": 50,
+                "title": "Directeur des études",
+                "explanation": "taper le début du nom et choisir dans le menu",
+                "allowed_values": allowed_user_names,
+                "allow_null": False,  # il faut au moins un responsable de semestre
+                "text_suggest_options": {
+                    "script": "Users/get_userlist_xml?",
+                    "varname": "start",
+                    "json": False,
+                    "noresults": "Valeur invalide !",
+                    "timeout": 60000,
+                },
+            },
+        ),
+        (
+            "responsable_id2",
+            {
+                "input_type": "text_suggest",
+                "size": 50,
+                "title": "Co-directeur des études",
+                "explanation": "",
+                "allowed_values": allowed_user_names,
+                "allow_null": True,  # optionnel
+                "text_suggest_options": {
+                    "script": "Users/get_userlist_xml?",
+                    "varname": "start",
+                    "json": False,
+                    "noresults": "Valeur invalide !",
+                    "timeout": 60000,
+                },
+            },
+        ),
+        (
+            "titre",
+            {
+                "size": 40,
+                "title": "Nom de ce semestre",
+                "explanation": """n'indiquez pas les dates, ni le semestre, ni la modalité dans le titre: ils seront automatiquement ajoutés <input type="button" value="remettre titre par défaut" onClick="document.tf.titre.value='%s';"/>"""
+                % _default_sem_title(F),
+            },
+        ),
+        (
+            "modalite",
+            {
+                "input_type": "menu",
+                "title": "Modalité",
+                "allowed_values": modalites_abbrv,
+                "labels": modalites_titles,
+            },
+        ),
+        (
+            "semestre_id",
+            {
+                "input_type": "menu",
+                "title": "Semestre dans la formation",
+                "allowed_values": semestre_id_list,
+                "labels": semestre_id_labels,
+            },
+        ),
+    ]
+    etapes = sco_portal_apogee.get_etapes_apogee_dept(context)
+    # Propose les etapes renvoyées par le portail
+    # et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement)
+    etapes_set = {et[0] for et in etapes}
+    if edit:
+        for etape_vdi in sem["etapes"]:
+            if etape_vdi.etape not in etapes_set:
+                etapes.append((etape_vdi.etape, "inconnue"))
+    modform.append(
+        (
+            "elt_help_apo",
+            {
+                "title": "Codes Apogée nécessaires pour inscrire les étudiants et exporter les notes en fin de semestre:",
+                "input_type": "separator",
+            },
+        )
+    )
+
+    mf_manual = {
+        "size": 12,
+        "template": '<tr%(item_dom_attr)s><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s',
+    }
+    if etapes:
+        mf = {
+            "input_type": "menu",
+            "allowed_values": [""] + [e[0] for e in etapes],
+            "labels": ["(aucune)"] + ["%s (%s)" % (e[1], e[0]) for e in etapes],
+            "template": '<tr%(item_dom_attr)s><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s',
+        }
+    else:
+        # fallback: code etape libre
+        mf = mf_manual
+
+    for n in range(1, EDIT_NB_ETAPES + 1):
+        mf["title"] = "Etape Apogée (%d)" % n
+        modform.append(("etape_apo" + str(n), mf.copy()))
+        modform.append(
+            (
+                "vdi_apo" + str(n),
+                {
+                    "size": 7,
+                    "title": "Version (VDI): ",
+                    "template": '<span class="vdi_label">%(label)s</span><span class="tf-field">%(elem)s</span></td></tr>',
+                },
+            )
+        )
+    # Saisie manuelle de l'étape: (seulement si menus)
+    if etapes:
+        n = 0
+        mf = mf_manual
+        mf["title"] = "Etape Apogée (+)"
+        modform.append(("etape_apo" + str(n), mf.copy()))
+        modform.append(
+            (
+                "vdi_apo" + str(n),
+                {
+                    "size": 7,
+                    "title": "Version (VDI): ",
+                    "template": '<span class="vdi_label">%(label)s</span><span class="tf-field">%(elem)s</span></td></tr>',
+                    "explanation": "saisie manuelle si votre étape n'est pas dans le menu",
+                },
+            )
+        )
+
+    modform.append(
+        (
+            "elt_sem_apo",
+            {
+                "size": 32,
+                "title": "Element(s) Apogée:",
+                "explanation": "du semestre (ex: VRTW1). Séparés par des virgules.",
+                "allow_null": not context.get_preference(
+                    "always_require_apo_sem_codes"
+                ),
+            },
+        )
+    )
+    modform.append(
+        (
+            "elt_annee_apo",
+            {
+                "size": 32,
+                "title": "Element(s) Apogée:",
+                "explanation": "de l'année (ex: VRT1A). Séparés par des virgules.",
+                "allow_null": not context.get_preference(
+                    "always_require_apo_sem_codes"
+                ),
+            },
+        )
+    )
+    if edit:
+        formtit = (
+            """
+        <p><a href="formsemestre_edit_uecoefs?formsemestre_id=%s">Modifier les coefficients des UE capitalisées</a></p>
+        <h3>Sélectionner les modules, leurs responsables et les étudiants à inscrire:</h3>
+        """
+            % formsemestre_id
+        )
+    else:
+        formtit = """<h3>Sélectionner les modules et leurs responsables</h3><p class="help">Si vous avez des parcours (options), ne sélectionnez que les modules du tronc commun.</p>"""
+
+    modform += [
+        (
+            "gestion_compensation_lst",
+            {
+                "input_type": "checkbox",
+                "title": "Jurys",
+                "allowed_values": ["X"],
+                "explanation": "proposer compensations de semestres (parcours DUT)",
+                "labels": [""],
+            },
+        ),
+        (
+            "gestion_semestrielle_lst",
+            {
+                "input_type": "checkbox",
+                "title": "",
+                "allowed_values": ["X"],
+                "explanation": "formation semestrialisée (jurys avec semestres décalés)",
+                "labels": [""],
+            },
+        ),
+    ]
+    if authuser.has_permission(ScoImplement, context):
+        modform += [
+            (
+                "resp_can_edit",
+                {
+                    "input_type": "boolcheckbox",
+                    "title": "Autorisations",
+                    "explanation": "Autoriser le directeur des études à modifier ce semestre",
+                },
+            )
+        ]
+    modform += [
+        (
+            "resp_can_change_ens",
+            {
+                "input_type": "boolcheckbox",
+                "title": "",
+                "explanation": "Autoriser le directeur des études à modifier les enseignants",
+            },
+        ),
+        (
+            "ens_can_edit_eval",
+            {
+                "input_type": "boolcheckbox",
+                "title": "",
+                "explanation": "Autoriser tous les enseignants associés à un module à y créer des évaluations",
+            },
+        ),
+        (
+            "bul_bgcolor",
+            {
+                "size": 8,
+                "title": "Couleur fond des bulletins",
+                "explanation": "version web seulement (ex: #ffeeee)",
+            },
+        ),
+        (
+            "bul_publish_xml_lst",
+            {
+                "input_type": "checkbox",
+                "title": "Publication",
+                "allowed_values": ["X"],
+                "explanation": "publier le bulletin sur le portail étudiants",
+                "labels": [""],
+            },
+        ),
+        (
+            "sep",
+            {
+                "input_type": "separator",
+                "title": "",
+                "template": "</table>%s<table>" % formtit,
+            },
+        ),
+    ]
+
+    nbmod = 0
+    if edit:
+        templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"
+    else:
+        templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td></tr>"
+    for semestre_id in semestre_ids:
+        modform.append(
+            (
+                "sep",
+                {
+                    "input_type": "separator",
+                    "title": "<b>Semestre %s</b>" % semestre_id,
+                    "template": templ_sep,
+                },
+            )
+        )
+        for mod in mods:
+            if mod["semestre_id"] == semestre_id:
+                nbmod += 1
+                if edit:
+                    select_name = "%s!group_id" % mod["module_id"]
+
+                    def opt_selected(gid):
+                        if gid == REQUEST.form.get(select_name):
+                            return "selected"
+                        else:
+                            return ""
+
+                    if mod["module_id"] in sem_module_ids:
+                        disabled = "disabled"
+                    else:
+                        disabled = ""
+                    fcg = '<select name="%s" %s>' % (select_name, disabled)
+                    default_group_id = sco_groups.get_default_group(
+                        context, formsemestre_id
+                    )
+                    fcg += '<option value="%s" %s>Tous</option>' % (
+                        default_group_id,
+                        opt_selected(default_group_id),
+                    )
+                    fcg += '<option value="" %s>Aucun</option>' % opt_selected("")
+                    for p in sco_groups.get_partitions_list(context, formsemestre_id):
+                        if p["partition_name"] != None:
+                            for group in sco_groups.get_partition_groups(context, p):
+                                fcg += '<option value="%s" %s>%s %s</option>' % (
+                                    group["group_id"],
+                                    opt_selected(group["group_id"]),
+                                    p["partition_name"],
+                                    group["group_name"],
+                                )
+                    fcg += "</select>"
+                    itemtemplate = (
+                        """<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
+                        + fcg
+                        + "</td></tr>"
+                    )
+                else:
+                    itemtemplate = """<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
+                modform.append(
+                    (
+                        str(mod["module_id"]),
+                        {
+                            "input_type": "text_suggest",
+                            "size": 50,
+                            "withcheckbox": True,
+                            "title": "%s %s" % (mod["code"], mod["titre"]),
+                            "allowed_values": allowed_user_names,
+                            "template": itemtemplate,
+                            "text_suggest_options": {
+                                "script": "Users/get_userlist_xml?",
+                                "varname": "start",
+                                "json": False,
+                                "noresults": "Valeur invalide !",
+                                "timeout": 60000,
+                            },
+                        },
+                    )
+                )
+    if nbmod == 0:
+        modform.append(
+            (
+                "sep",
+                {
+                    "input_type": "separator",
+                    "title": "aucun module dans cette formation !!!",
+                },
+            )
+        )
+    if edit:
+        #         modform.append( ('inscrire_etudslist',
+        #                          { 'input_type' : 'checkbox',
+        #                            'allowed_values' : ['X'], 'labels' : [ '' ],
+        #                            'title' : '' ,
+        #                            'explanation' : 'inscrire tous les étudiants du semestre aux modules ajoutés'}) )
+        submitlabel = "Modifier ce semestre de formation"
+    else:
+        submitlabel = "Créer ce semestre de formation"
+    #
+    # Etapes:
+    if edit:
+        n = 1
+        for etape_vdi in sem["etapes"]:
+            initvalues["etape_apo" + str(n)] = etape_vdi.etape
+            initvalues["vdi_apo" + str(n)] = etape_vdi.vdi
+            n += 1
+    #
+    initvalues["gestion_compensation"] = initvalues.get("gestion_compensation", "0")
+    if initvalues["gestion_compensation"] == "1":
+        initvalues["gestion_compensation_lst"] = ["X"]
+    else:
+        initvalues["gestion_compensation_lst"] = []
+    if REQUEST.form.get("tf-submitted", False) and not REQUEST.form.has_key(
+        "gestion_compensation_lst"
+    ):
+        REQUEST.form["gestion_compensation_lst"] = []
+
+    initvalues["gestion_semestrielle"] = initvalues.get("gestion_semestrielle", "0")
+    if initvalues["gestion_semestrielle"] == "1":
+        initvalues["gestion_semestrielle_lst"] = ["X"]
+    else:
+        initvalues["gestion_semestrielle_lst"] = []
+    if REQUEST.form.get("tf-submitted", False) and not REQUEST.form.has_key(
+        "gestion_semestrielle_lst"
+    ):
+        REQUEST.form["gestion_semestrielle_lst"] = []
+
+    initvalues["bul_hide_xml"] = initvalues.get("bul_hide_xml", "0")
+    if initvalues["bul_hide_xml"] == "0":
+        initvalues["bul_publish_xml_lst"] = ["X"]
+    else:
+        initvalues["bul_publish_xml_lst"] = []
+    if REQUEST.form.get("tf-submitted", False) and not REQUEST.form.has_key(
+        "bul_publish_xml_lst"
+    ):
+        REQUEST.form["bul_publish_xml_lst"] = []
+
+    #
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        modform,
+        submitlabel=submitlabel,
+        cancelbutton="Annuler",
+        top_buttons=True,
+        initvalues=initvalues,
+    )
+    msg = ""
+    if tf[0] == 1:
+        # check dates
+        if DateDMYtoISO(tf[2]["date_debut"]) > DateDMYtoISO(tf[2]["date_fin"]):
+            msg = '<ul class="tf-msg"><li class="tf-msg">Dates de début et fin incompatibles !</li></ul>'
+        if context.get_preference("always_require_apo_sem_codes") and not any(
+            [tf[2]["etape_apo" + str(n)] for n in range(0, EDIT_NB_ETAPES + 1)]
+        ):
+            msg = '<ul class="tf-msg"><li class="tf-msg">Code étape Apogée manquant</li></ul>'
+    
+    if tf[0] == 0 or msg:
+        return (
+            '<p>Formation <a class="discretelink" href="ue_list?formation_id=%(formation_id)s"><em>%(titre)s</em> (%(acronyme)s), version %(version)d, code %(formation_code)s</a></p>'
+            % F
+            + msg
+            + tf[1]
+        )
+    elif tf[0] == -1:
+        return "<h4>annulation</h4>"
+    else:
+        if tf[2]["gestion_compensation_lst"]:
+            tf[2]["gestion_compensation"] = 1
+        else:
+            tf[2]["gestion_compensation"] = 0
+        if tf[2]["gestion_semestrielle_lst"]:
+            tf[2]["gestion_semestrielle"] = 1
+        else:
+            tf[2]["gestion_semestrielle"] = 0
+        if tf[2]["bul_publish_xml_lst"]:
+            tf[2]["bul_hide_xml"] = 0
+        else:
+            tf[2]["bul_hide_xml"] = 1
+
+        # remap les identifiants de responsables:
+        tf[2]["responsable_id"] = context.Users.get_user_name_from_nomplogin(
+            tf[2]["responsable_id"]
+        )
+        tf[2]["responsable_id2"] = context.Users.get_user_name_from_nomplogin(
+            tf[2]["responsable_id2"]
+        )
+        tf[2]["responsables"] = [tf[2]["responsable_id"]]
+        if tf[2]["responsable_id2"]:
+            tf[2]["responsables"].append(tf[2]["responsable_id2"])
+
+        for module_id in tf[2]["tf-checked"]:
+            mod_resp_id = context.Users.get_user_name_from_nomplogin(tf[2][module_id])
+            if mod_resp_id is None:
+                # Si un module n'a pas de responsable (ou inconnu), l'affecte au 1er directeur des etudes:
+                mod_resp_id = tf[2]["responsable_id"]
+            tf[2][module_id] = mod_resp_id
+
+        # etapes:
+        tf[2]["etapes"] = []
+        if etapes:  # menus => case supplementaire pour saisie manuelle, indicée 0
+            start_i = 0
+        else:
+            start_i = 1
+        for n in range(start_i, EDIT_NB_ETAPES + 1):
+            tf[2]["etapes"].append(
+                ApoEtapeVDI(
+                    etape=tf[2]["etape_apo" + str(n)], vdi=tf[2]["vdi_apo" + str(n)]
+                )
+            )
+        if not edit:
+            # creation du semestre
+            formsemestre_id = context.do_formsemestre_create(tf[2], REQUEST)
+            # creation des modules
+            for module_id in tf[2]["tf-checked"]:
+                modargs = {
+                    "module_id": module_id,
+                    "formsemestre_id": formsemestre_id,
+                    "responsable_id": tf[2][module_id],
+                }
+                mid = context.do_moduleimpl_create(modargs)
+            return REQUEST.RESPONSE.redirect(
+                "formsemestre_status?formsemestre_id=%s&amp;head_message=Nouveau%%20semestre%%20créé"
+                % formsemestre_id
+            )
+        else:
+            # modification du semestre:
+            # on doit creer les modules nouvellement selectionnés
+            # modifier ceux a modifier, et DETRUIRE ceux qui ne sont plus selectionnés.
+            # Note: la destruction echouera s'il y a des objets dependants
+            #       (eg des evaluations définies)
+            # nouveaux modules
+            checkedmods = tf[2]["tf-checked"]
+            sco_formsemestre.do_formsemestre_edit(context, tf[2])
+            ams = context.do_moduleimpl_list(formsemestre_id=formsemestre_id)
+            existingmods = [x["module_id"] for x in ams]
+            mods_tocreate = [x for x in checkedmods if not x in existingmods]
+            # modules a existants a modifier
+            mods_toedit = [x for x in checkedmods if x in existingmods]
+            # modules a detruire
+            mods_todelete = [x for x in existingmods if not x in checkedmods]
+            #
+            msg = []
+            for module_id in mods_tocreate:
+                modargs = {
+                    "module_id": module_id,
+                    "formsemestre_id": formsemestre_id,
+                    "responsable_id": tf[2][module_id],
+                }
+                moduleimpl_id = context.do_moduleimpl_create(modargs)
+                mod = context.do_module_list({"module_id": module_id})[0]
+                msg += ["création de %s (%s)" % (mod["code"], mod["titre"])]
+                # INSCRIPTIONS DES ETUDIANTS
+                log(
+                    'inscription module: %s = "%s"'
+                    % ("%s!group_id" % module_id, tf[2]["%s!group_id" % module_id])
+                )
+                group_id = tf[2]["%s!group_id" % module_id]
+                if group_id:
+                    etudids = [
+                        x["etudid"]
+                        for x in sco_groups.get_group_members(context, group_id)
+                    ]
+                    log(
+                        "inscription module:module_id=%s,moduleimpl_id=%s: %s"
+                        % (module_id, moduleimpl_id, etudids)
+                    )
+                    context.do_moduleimpl_inscrit_etuds(
+                        moduleimpl_id, formsemestre_id, etudids, REQUEST=REQUEST
+                    )
+                    msg += [
+                        "inscription de %d étudiants au module %s"
+                        % (len(etudids), mod["code"])
+                    ]
+                else:
+                    log(
+                        "inscription module:module_id=%s,moduleimpl_id=%s: aucun etudiant inscrit"
+                        % (module_id, moduleimpl_id)
+                    )
+            #
+            ok, diag = formsemestre_delete_moduleimpls(
+                context, formsemestre_id, mods_todelete
+            )
+            msg += diag
+            for module_id in mods_toedit:
+                moduleimpl_id = context.do_moduleimpl_list(
+                    formsemestre_id=formsemestre_id, module_id=module_id
+                )[0]["moduleimpl_id"]
+                modargs = {
+                    "moduleimpl_id": moduleimpl_id,
+                    "module_id": module_id,
+                    "formsemestre_id": formsemestre_id,
+                    "responsable_id": tf[2][module_id],
+                }
+                context.do_moduleimpl_edit(modargs, formsemestre_id=formsemestre_id)
+                mod = context.do_module_list({"module_id": module_id})[0]
+
+            if msg:
+                msg_html = (
+                    '<div class="ue_warning"><span>Attention !<ul><li>'
+                    + "</li><li>".join(msg)
+                    + "</li></ul></span></div>"
+                )
+                if ok:
+                    msg_html += "<p>Modification effectuée</p>"
+                else:
+                    msg_html += "<p>Modification effectuée (<b>mais modules cités non supprimés</b>)</p>"
+                msg_html += (
+                    '<a href="formsemestre_status?formsemestre_id=%s">retour au tableau de bord</a>'
+                    % formsemestre_id
+                )
+                return msg_html
+            else:
+                return REQUEST.RESPONSE.redirect(
+                    "formsemestre_status?formsemestre_id=%s&amp;head_message=Semestre modifié"
+                    % formsemestre_id
+                )
+
+
+def formsemestre_delete_moduleimpls(context, formsemestre_id, module_ids_to_del):
+    """Delete moduleimpls
+     module_ids_to_del: list of module_id (warning: not moduleimpl)
+     Moduleimpls must have no associated evaluations.
+     """
+    ok = True
+    msg = []
+    for module_id in module_ids_to_del:
+        # get id
+        moduleimpl_id = context.do_moduleimpl_list(
+            formsemestre_id=formsemestre_id, module_id=module_id
+        )[0]["moduleimpl_id"]
+        mod = context.do_module_list({"module_id": module_id})[0]
+        # Evaluations dans ce module ?
+        evals = context.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
+        if evals:
+            msg += [
+                '<b>impossible de supprimer %s (%s) car il y a %d évaluations définies (<a href="moduleimpl_status?moduleimpl_id=%s" class="stdlink">supprimer les d\'abord</a>)</b>'
+                % (mod["code"], mod["titre"], len(evals), moduleimpl_id)
+            ]
+            ok = False
+        else:
+            msg += ["suppression de %s (%s)" % (mod["code"], mod["titre"])]
+            context.do_moduleimpl_delete(moduleimpl_id, formsemestre_id=formsemestre_id)
+
+    return ok, msg
+
+
+def formsemestre_clone(context, formsemestre_id, REQUEST=None):
+    """
+    Formulaire clonage d'un semestre
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    # Liste des enseignants avec forme pour affichage / saisie avec suggestion
+    userlist = context.Users.get_userlist()
+    login2display = {}  # user_name : forme pour affichage = "NOM Prenom (login)"
+    for u in userlist:
+        login2display[u["user_name"]] = u["nomplogin"]
+    allowed_user_names = login2display.values() + [""]
+
+    initvalues = {
+        "formsemestre_id": sem["formsemestre_id"],
+        "responsable_id": login2display.get(
+            sem["responsables"][0], sem["responsables"][0]
+        ),
+    }
+
+    H = [
+        context.html_sem_header(
+            REQUEST,
+            "Copie du semestre",
+            sem,
+            javascripts=["libjs/AutoSuggest.js"],
+            cssstyles=["css/autosuggest_inquisitor.css"],
+            bodyOnLoad="init_tf_form('')",
+        ),
+        """<p class="help">Cette opération duplique un semestre: on reprend les mêmes modules et responsables. Aucun étudiant n'est inscrit.</p>""",
+    ]
+
+    descr = [
+        ("formsemestre_id", {"input_type": "hidden"}),
+        (
+            "date_debut",
+            {
+                "title": "Date de début",  # j/m/a
+                "input_type": "date",
+                "explanation": "j/m/a",
+                "size": 9,
+                "allow_null": False,
+            },
+        ),
+        (
+            "date_fin",
+            {
+                "title": "Date de fin",  # j/m/a
+                "input_type": "date",
+                "explanation": "j/m/a",
+                "size": 9,
+                "allow_null": False,
+            },
+        ),
+        (
+            "responsable_id",
+            {
+                "input_type": "text_suggest",
+                "size": 50,
+                "title": "Directeur des études",
+                "explanation": "taper le début du nom et choisir dans le menu",
+                "allowed_values": allowed_user_names,
+                "allow_null": False,
+                "text_suggest_options": {
+                    "script": "Users/get_userlist_xml?",
+                    "varname": "start",
+                    "json": False,
+                    "noresults": "Valeur invalide !",
+                    "timeout": 60000,
+                },
+            },
+        ),
+        (
+            "clone_evaluations",
+            {
+                "title": "Copier aussi les évaluations",
+                "input_type": "boolcheckbox",
+                "explanation": "copie toutes les évaluations, sans les dates (ni les notes!)",
+            },
+        ),
+        (
+            "clone_partitions",
+            {
+                "title": "Copier aussi les partitions",
+                "input_type": "boolcheckbox",
+                "explanation": "copie toutes les partitions (sans les étudiants!)",
+            },
+        ),
+    ]
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        submitlabel="Dupliquer ce semestre",
+        cancelbutton="Annuler",
+        initvalues=initvalues,
+    )
+    msg = ""
+    if tf[0] == 1:
+        # check dates
+        if DateDMYtoISO(tf[2]["date_debut"]) > DateDMYtoISO(tf[2]["date_fin"]):
+            msg = '<ul class="tf-msg"><li class="tf-msg">Dates de début et fin incompatibles !</li></ul>'
+    if tf[0] == 0 or msg:
+        return "".join(H) + msg + tf[1] + context.sco_footer(REQUEST)
+    elif tf[0] == -1:  # cancel
+        return REQUEST.RESPONSE.redirect(
+            "formsemestre_status?formsemestre_id=%s" % formsemestre_id
+        )
+    else:
+        new_formsemestre_id = do_formsemestre_clone(
+            context,
+            formsemestre_id,
+            context.Users.get_user_name_from_nomplogin(tf[2]["responsable_id"]),
+            tf[2]["date_debut"],
+            tf[2]["date_fin"],
+            clone_evaluations=tf[2]["clone_evaluations"],
+            clone_partitions=tf[2]["clone_partitions"],
+            REQUEST=REQUEST,
+        )
+        return REQUEST.RESPONSE.redirect(
+            "formsemestre_status?formsemestre_id=%s&amp;head_message=Nouveau%%20semestre%%20créé"
+            % new_formsemestre_id
+        )
+
+
+def do_formsemestre_clone(
+    context,
+    orig_formsemestre_id,
+    responsable_id,  # new resp.
+    date_debut,
+    date_fin,  # 'dd/mm/yyyy'
+    clone_evaluations=False,
+    clone_partitions=False,
+    REQUEST=None,
+):
+    """Clone a semestre: make copy, same modules, same options, same resps, same partitions.
+    New dates, responsable_id    
+    """
+    log("cloning %s" % orig_formsemestre_id)
+    orig_sem = sco_formsemestre.get_formsemestre(context, orig_formsemestre_id)
+    cnx = context.GetDBConnexion()
+    # 1- create sem
+    args = orig_sem.copy()
+    del args["formsemestre_id"]
+    args["responsables"] = [responsable_id]
+    args["date_debut"] = date_debut
+    args["date_fin"] = date_fin
+    args["etat"] = 1  # non verrouillé
+    formsemestre_id = context.do_formsemestre_create(args, REQUEST)
+    log("created formsemestre %s" % formsemestre_id)
+    # 2- create moduleimpls
+    mods_orig = context.do_moduleimpl_list(formsemestre_id=orig_formsemestre_id)
+    for mod_orig in mods_orig:
+        args = mod_orig.copy()
+        args["formsemestre_id"] = formsemestre_id
+        mid = context.do_moduleimpl_create(args)
+        # copy notes_modules_enseignants
+        ens = context.do_ens_list(args={"moduleimpl_id": mod_orig["moduleimpl_id"]})
+        for e in ens:
+            args = e.copy()
+            args["moduleimpl_id"] = mid
+            context.do_ens_create(args)
+        # optionally, copy evaluations
+        if clone_evaluations:
+            evals = context.do_evaluation_list(
+                args={"moduleimpl_id": mod_orig["moduleimpl_id"]}
+            )
+            for e in evals:
+                args = e.copy()
+                del args["jour"]  # erase date
+                args["moduleimpl_id"] = mid
+                evaluation_id = context.do_evaluation_create(REQUEST=REQUEST, **args)
+
+    # 3- copy uecoefs
+    objs = formsemestre_uecoef_list(cnx, args={"formsemestre_id": orig_formsemestre_id})
+    for obj in objs:
+        args = obj.copy()
+        args["formsemestre_id"] = formsemestre_id
+        c = formsemestre_uecoef_create(cnx, args)
+
+    # NB: don't copy notes_formsemestre_custommenu (usually specific)
+
+    # 4- Copy new style preferences
+    prefs = context.get_preferences(orig_formsemestre_id)
+
+    if orig_formsemestre_id in prefs.base_prefs.prefs:
+        for pname in prefs.base_prefs.prefs[orig_formsemestre_id]:
+            if not prefs.is_global(pname):
+                pvalue = prefs[pname]
+                prefs.base_prefs.set(formsemestre_id, pname, pvalue)
+
+    # 5- Copy formules utilisateur
+    objs = sco_compute_moy.formsemestre_ue_computation_expr_list(
+        cnx, args={"formsemestre_id": orig_formsemestre_id}
+    )
+    for obj in objs:
+        args = obj.copy()
+        args["formsemestre_id"] = formsemestre_id
+        c = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args)
+
+    # 5- Copy partitions
+    if clone_partitions:
+        listgroups = []
+        listnamegroups = []
+        # Création des partitions:
+        for part in sco_groups.get_partitions_list(context, orig_formsemestre_id):
+            if part["partition_name"] != None:
+                partname = part["partition_name"]
+                orig_partition_id = part["partition_id"]
+                new_partition_id = sco_groups.partition_create(
+                    context,
+                    formsemestre_id,
+                    partition_name=partname,
+                    REQUEST=REQUEST,
+                    redirect=0,
+                )
+                for g in sco_groups.get_partition_groups(context, part):
+                    if g["group_name"] != None:
+                        listnamegroups.append(g["group_name"])
+                listgroups.append([new_partition_id, listnamegroups])
+                listnamegroups = []
+
+        # Création des groupes dans les nouvelles partitions:
+        for newpart in sco_groups.get_partitions_list(context, formsemestre_id):
+            for g in listgroups:
+                if newpart["partition_id"] == g[0]:
+                    part_id = g[0]
+                    for group_name in g[1]:
+                        group_id = sco_groups.createGroup(
+                            context, part_id, group_name=group_name, REQUEST=REQUEST
+                        )
+
+    return formsemestre_id
+
+
+# ---------------------------------------------------------------------------------------
+
+
+def formsemestre_associate_new_version(
+    context,
+    formsemestre_id,
+    other_formsemestre_ids=[],
+    REQUEST=None,
+    dialog_confirmed=False,
+):
+    """Formulaire changement formation d'un semestre"""
+    if not dialog_confirmed:
+        # dresse le liste des semestres de la meme formation et version
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+        othersems = sco_formsemestre.do_formsemestre_list(
+            context,
+            args={
+                "formation_id": F["formation_id"],
+                "version": F["version"],
+                "etat": "1",
+            },
+        )
+        of = []
+        for s in othersems:
+            if (
+                s["formsemestre_id"] == formsemestre_id
+                or s["formsemestre_id"] in other_formsemestre_ids
+            ):
+                checked = 'checked="checked"'
+            else:
+                checked = ""
+            if s["formsemestre_id"] == formsemestre_id:
+                disabled = 'disabled="1"'
+            else:
+                disabled = ""
+            of.append(
+                '<div><input type="checkbox" name="other_formsemestre_ids:list" value="%s" %s %s>%s</input></div>'
+                % (s["formsemestre_id"], checked, disabled, s["titremois"])
+            )
+
+        return context.confirmDialog(
+            """<h2>Associer à une nouvelle version de formation non verrouillée ?</h2>
+                <p>Le programme pédagogique ("formation") va être dupliqué pour que vous puissiez le modifier sans affecter les autres semestres. Les autres paramètres (étudiants, notes...) du semestre seront inchangés.</p>
+                <p>Veillez à ne pas abuser de cette possibilité, car créer trop de versions de formations va vous compliquer la gestion (à vous de garder trace des différences et à ne pas vous tromper par la suite...).
+                </p>
+                <div class="othersemlist"><p>Si vous voulez associer aussi d'autres semestres à la nouvelle version, cochez-les:</p>"""
+            + "".join(of)
+            + "</div>",
+            OK="Associer ces semestres à une nouvelle version",
+            dest_url="",
+            REQUEST=REQUEST,
+            cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
+            parameters={"formsemestre_id": formsemestre_id},
+        )
+    else:
+        do_formsemestres_associate_new_version(
+            context, [formsemestre_id] + other_formsemestre_ids, REQUEST=REQUEST
+        )
+        return REQUEST.RESPONSE.redirect(
+            "formsemestre_status?formsemestre_id=%s&amp;head_message=Formation%%20dupliquée"
+            % formsemestre_id
+        )
+
+
+def do_formsemestres_associate_new_version(context, formsemestre_ids, REQUEST=None):
+    """Cree une nouvelle version de la formation du semestre, et y rattache les semestres.
+    Tous les moduleimpl sont ré-associés à la nouvelle formation, ainsi que les decisions de jury 
+    si elles existent (codes d'UE validées).
+    Les semestre doivent tous appartenir à la meme version de la formation
+    """
+    log("do_formsemestres_associate_new_version %s" % formsemestre_ids)
+    if not formsemestre_ids:
+        return
+    # Check: tous de la même formation
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_ids[0])
+    formation_id = sem["formation_id"]
+    for formsemestre_id in formsemestre_ids[1:]:
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        if formation_id != sem["formation_id"]:
+            raise ScoValueError("les semestres ne sont pas tous de la même formation !")
+
+    cnx = context.GetDBConnexion()
+    # New formation:
+    formation_id, modules_old2new, ues_old2new = context.formation_create_new_version(
+        formation_id, redirect=False, REQUEST=REQUEST
+    )
+
+    for formsemestre_id in formsemestre_ids:
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        sem["formation_id"] = formation_id
+        context.do_formsemestre_edit(sem, cnx=cnx, html_quote=False)
+        _reassociate_moduleimpls(
+            context, cnx, formsemestre_id, ues_old2new, modules_old2new
+        )
+
+    cnx.commit()
+
+
+def _reassociate_moduleimpls(
+    context, cnx, formsemestre_id, ues_old2new, modules_old2new
+):
+    """Associe les moduleimpls d'un semestre existant à un autre programme
+    et met à jour les décisions de jury (validations d'UE).
+    """
+    # re-associate moduleimpls to new modules:
+    modimpls = context.do_moduleimpl_list(formsemestre_id=formsemestre_id)
+    for mod in modimpls:
+        mod["module_id"] = modules_old2new[mod["module_id"]]
+        context.do_moduleimpl_edit(mod, formsemestre_id=formsemestre_id, cnx=cnx)
+    # update decisions:
+    events = scolars.scolar_events_list(cnx, args={"formsemestre_id": formsemestre_id})
+    for e in events:
+        if e["ue_id"]:
+            e["ue_id"] = ues_old2new[e["ue_id"]]
+        scolars.scolar_events_edit(cnx, e)
+    validations = sco_parcours_dut.scolar_formsemestre_validation_list(
+        cnx, args={"formsemestre_id": formsemestre_id}
+    )
+    for e in validations:
+        if e["ue_id"]:
+            e["ue_id"] = ues_old2new[e["ue_id"]]
+        # log('e=%s' % e )
+        sco_parcours_dut.scolar_formsemestre_validation_edit(cnx, e)
+
+
+def formsemestre_delete(context, formsemestre_id, REQUEST=None):
+    """Delete a formsemestre (affiche avertissements)"""
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    H = [
+        context.html_sem_header(REQUEST, "Suppression du semestre", sem),
+        """<div class="ue_warning"><span>Attention !</span>
+<p class="help">A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement,
+<b>un semestre ne doit jamais être supprimé</b> (on perd la mémoire des notes et de tous les événements liés à ce semestre !).</p>
+
+ <p class="help">Tous les modules de ce semestre seront supprimés. Ceci n'est possible que
+ si :</p>
+ <ol>
+  <li>aucune décision de jury n'a été entrée dans ce semestre;</li>
+  <li>et aucun étudiant de ce semestre ne le compense avec un autre semestre.</li>
+  </ol></div>""",
+    ]
+
+    evals = context.do_evaluation_list_in_formsemestre(formsemestre_id)
+    if evals:
+        H.append(
+            """<p class="warning">Attention: il y a %d évaluations dans ce semestre (sa suppression entrainera l'effacement définif des notes) !</p>"""
+            % len(evals)
+        )
+        submit_label = (
+            "Confirmer la suppression (du semestre et des %d évaluations !)"
+            % len(evals)
+        )
+    else:
+        submit_label = "Confirmer la suppression du semestre"
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (("formsemestre_id", {"input_type": "hidden"}),),
+        initvalues=F,
+        submitlabel=submit_label,
+        cancelbutton="Annuler",
+    )
+    if tf[0] == 0:
+        if formsemestre_has_decisions_or_compensations(context, formsemestre_id):
+            H.append(
+                """<p><b>Ce semestre ne peut pas être supprimé ! (il y a des décisions de jury ou des compensations par d'autres semestres)</b></p>"""
+            )
+        else:
+            H.append(tf[1])
+        return "\n".join(H) + context.sco_footer(REQUEST)
+    elif tf[0] == -1:  # cancel
+        return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+    else:
+        return REQUEST.RESPONSE.redirect(
+            "formsemestre_delete2?formsemestre_id=" + formsemestre_id
+        )
+
+
+def formsemestre_delete2(
+    context, formsemestre_id, dialog_confirmed=False, REQUEST=None
+):
+    """Delete a formsemestre (confirmation)"""
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    H = [context.html_sem_header(REQUEST, "Suppression du semestre", sem)]
+    # Confirmation dialog
+    if not dialog_confirmed:
+        return context.confirmDialog(
+            """<h2>Vous voulez vraiment supprimer ce semestre ???</h2><p>(opération irréversible)</p>""",
+            dest_url="",
+            REQUEST=REQUEST,
+            cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
+            parameters={"formsemestre_id": formsemestre_id},
+        )
+    # Bon, s'il le faut...
+    do_formsemestre_delete(context, formsemestre_id, REQUEST)
+    return REQUEST.RESPONSE.redirect(REQUEST.URL2 + "?head_message=Semestre%20supprimé")
+
+
+def formsemestre_has_decisions_or_compensations(context, formsemestre_id):
+    """True if decision de jury dans ce semestre
+    ou bien compensation de ce semestre par d'autre ssemestres.
+    """
+    r = SimpleDictFetch(
+        context,
+        "SELECT v.* FROM scolar_formsemestre_validation v WHERE v.formsemestre_id = %(formsemestre_id)s OR v.compense_formsemestre_id = %(formsemestre_id)s",
+        {"formsemestre_id": formsemestre_id},
+    )
+    return r
+
+
+def do_formsemestre_delete(context, formsemestre_id, REQUEST):
+    """delete formsemestre, and all its moduleimpls.
+    No checks, no warnings: erase all !
+    """
+    cnx = context.GetDBConnexion()
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+
+    # --- Destruction des modules de ce semestre
+    mods = context.do_moduleimpl_list(formsemestre_id=formsemestre_id)
+    for mod in mods:
+        # evaluations
+        evals = context.do_evaluation_list(args={"moduleimpl_id": mod["moduleimpl_id"]})
+        for e in evals:
+            SimpleQuery(
+                context,
+                "DELETE FROM notes_notes WHERE evaluation_id=%(evaluation_id)s",
+                e,
+            )
+            SimpleQuery(
+                context,
+                "DELETE FROM notes_notes_log WHERE evaluation_id=%(evaluation_id)s",
+                e,
+            )
+            SimpleQuery(
+                context,
+                "DELETE FROM notes_evaluation WHERE evaluation_id=%(evaluation_id)s",
+                e,
+            )
+            context.get_evaluations_cache().inval_cache(key=e["evaluation_id"])
+
+        context.do_moduleimpl_delete(
+            mod["moduleimpl_id"], formsemestre_id=formsemestre_id
+        )
+    # --- Desinscription des etudiants
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    req = "DELETE FROM notes_formsemestre_inscription WHERE formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Suppression des evenements
+    req = "DELETE FROM scolar_events WHERE formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Suppression des appreciations
+    req = "DELETE FROM notes_appreciations WHERE formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Supression des validations (!!!)
+    req = "DELETE FROM scolar_formsemestre_validation WHERE formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Supression des references a ce semestre dans les compensations:
+    req = "UPDATE  scolar_formsemestre_validation SET compense_formsemestre_id=NULL WHERE compense_formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Suppression des autorisations
+    req = "DELETE FROM scolar_autorisation_inscription WHERE origin_formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Suppression des coefs d'UE capitalisées
+    req = "DELETE FROM notes_formsemestre_uecoef WHERE formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Suppression des item du menu custom
+    req = "DELETE FROM notes_formsemestre_custommenu WHERE formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Suppression des formules
+    req = "DELETE FROM notes_formsemestre_ue_computation_expr WHERE formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Suppression des preferences
+    req = "DELETE FROM sco_prefs WHERE formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Suppression des groupes et partitions
+    req = "DELETE FROM group_membership  WHERE group_id IN (SELECT gm.group_id FROM group_membership gm, partition p, group_descr gd WHERE gm.group_id = gd.group_id AND gd.partition_id = p.partition_id AND p.formsemestre_id=%(formsemestre_id)s)"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    req = "DELETE FROM group_descr WHERE group_id IN (SELECT gd.group_id FROM group_descr gd, partition p WHERE gd.partition_id = p.partition_id AND p.formsemestre_id=%(formsemestre_id)s)"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    req = "DELETE FROM partition WHERE formsemestre_id=%(formsemestre_id)s"
+    cursor.execute(req, {"formsemestre_id": formsemestre_id})
+    # --- Destruction du semestre
+    sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id)
+
+    # news
+    import sco_news
+
+    sco_news.add(
+        context,
+        REQUEST,
+        typ=sco_news.NEWS_SEM,
+        object=formsemestre_id,
+        text="Suppression du semestre %(titre)s" % sem,
+    )
+
+
+# ---------------------------------------------------------------------------------------
+def formsemestre_edit_options(context, formsemestre_id, target_url=None, REQUEST=None):
+    """dialog to change formsemestre options
+    (accessible par ScoImplement ou dir. etudes)
+    """
+    log("formsemestre_edit_options")
+    ok, err = context._check_access_diretud(formsemestre_id, REQUEST)
+    if not ok:
+        return err
+    return context.get_preferences(formsemestre_id).edit(
+        REQUEST=REQUEST, categories=["bul"]
+    )
+
+
+def formsemestre_change_lock(
+    context, formsemestre_id, REQUEST=None, dialog_confirmed=False
+):
+    """Change etat (verrouille si ouvert, déverrouille si fermé)
+    nota: etat (1 ouvert, 0 fermé)
+    """
+    ok, err = context._check_access_diretud(formsemestre_id, REQUEST)
+    if not ok:
+        return err
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    etat = 1 - int(sem["etat"])
+
+    if REQUEST and not dialog_confirmed:
+        if etat:
+            msg = "déverrouillage"
+        else:
+            msg = "verrouillage"
+        return context.confirmDialog(
+            "<h2>Confirmer le %s du semestre ?</h2>" % msg,
+            helpmsg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
+            Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
+            (par son responsable ou un administrateur).
+            <br/>
+            Le programme d'une formation qui a un semestre verrouillé ne peut plus être modifié.
+            """,
+            dest_url="",
+            REQUEST=REQUEST,
+            cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
+            parameters={"etat": etat, "formsemestre_id": formsemestre_id},
+        )
+
+    if etat not in (0, 1):
+        raise ScoValueError("formsemestre_lock: invalid value for etat (%s)" % etat)
+    args = {"formsemestre_id": formsemestre_id, "etat": etat}
+    context.do_formsemestre_edit(args)
+    if REQUEST:
+        REQUEST.RESPONSE.redirect(
+            "formsemestre_status?formsemestre_id=%s" % formsemestre_id
+        )
+
+
+def formsemestre_change_publication_bul(
+    context, formsemestre_id, REQUEST=None, dialog_confirmed=False
+):
+    """Change etat publication bulletins sur portail
+    """
+    ok, err = context._check_access_diretud(formsemestre_id, REQUEST)
+    if not ok:
+        return err
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    etat = 1 - int(sem["bul_hide_xml"])
+
+    if REQUEST and not dialog_confirmed:
+        if etat:
+            msg = "non"
+        else:
+            msg = ""
+        return context.confirmDialog(
+            "<h2>Confirmer la %s publication des bulletins ?</h2>" % msg,
+            helpmsg="""Il est parfois utile de désactiver la diffusion des bulletins,
+            par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
+            <br/>
+            Ce réglage n'a d'effet que si votre établissement a interfacé ScoDoc et un portail étudiant.
+            """,
+            dest_url="",
+            REQUEST=REQUEST,
+            cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
+            parameters={"bul_hide_xml": etat, "formsemestre_id": formsemestre_id},
+        )
+
+    if etat not in (0, 1):
+        raise ScoValueError(
+            "formsemestre_change_publication_bul: invalid value for etat (%s)" % etat
+        )
+    args = {"formsemestre_id": formsemestre_id, "bul_hide_xml": etat}
+    context.do_formsemestre_edit(args)
+    if REQUEST:
+        REQUEST.RESPONSE.redirect(
+            "formsemestre_status?formsemestre_id=%s" % formsemestre_id
+        )
+
+
+# ----------------------  Coefs des UE
+
+_formsemestre_uecoef_editor = EditableTable(
+    "notes_formsemestre_uecoef",
+    "formsemestre_uecoef_id",
+    ("formsemestre_uecoef_id", "formsemestre_id", "ue_id", "coefficient"),
+)
+
+formsemestre_uecoef_create = _formsemestre_uecoef_editor.create
+formsemestre_uecoef_edit = _formsemestre_uecoef_editor.edit
+formsemestre_uecoef_list = _formsemestre_uecoef_editor.list
+formsemestre_uecoef_delete = _formsemestre_uecoef_editor.delete
+
+
+def formsemestre_edit_uecoefs(context, formsemestre_id, err_ue_id=None, REQUEST=None):
+    """Changement manuel des coefficients des UE capitalisées.
+    """
+    context = context.Notes  # si appele d'en haut, eg par exception ScoValueError
+    ok, err = context._check_access_diretud(formsemestre_id, REQUEST)
+    if not ok:
+        return err
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+
+    footer = context.sco_footer(REQUEST)
+    help = """<p class="help">
+    Seuls les modules ont un coefficient. Cependant, il est nécessaire d'affecter un coefficient aux UE capitalisée pour pouvoir les prendre en compte dans la moyenne générale.
+    </p>
+    <p class="help">ScoDoc calcule normalement le coefficient d'une UE comme la somme des
+    coefficients des modules qui la composent.
+    </p>
+    <p class="help">Dans certains cas, on n'a pas les mêmes modules dans le semestre antérieur
+    (capitalisé) et dans le semestre courant, et le coefficient d'UE est alors variable.
+    Il est alors possible de forcer la valeur du coefficient d'UE.
+    </p>
+    <p class="help">
+    Indiquez "auto" (ou laisser vide) pour que ScoDoc calcule automatiquement le coefficient,
+    ou bien entrez une valeur (nombre réel).
+    </p>
+    <p class="help">Dans le doute, si le mode auto n'est pas applicable et que tous les étudiants sont inscrits aux mêmes modules de ce semestre, prenez comme coefficient la somme indiquée. 
+    Sinon, référez vous au programme pédagogique. Les lignes en <font color="red">rouge</font>
+    sont à changer.
+    </p>
+    <p class="warning">Les coefficients indiqués ici ne s'appliquent que pour le traitement des UE capitalisées.
+    </p>
+    """
+    H = [context.html_sem_header(REQUEST, "Coefficients des UE du semestre", sem), help]
+    #
+    ues, modimpls = notes_table.get_sem_ues_modimpls(context, formsemestre_id)
+    for ue in ues:
+        ue["sum_coefs"] = sum(
+            [
+                mod["module"]["coefficient"]
+                for mod in modimpls
+                if mod["module"]["ue_id"] == ue["ue_id"]
+            ]
+        )
+
+    cnx = context.GetDBConnexion()
+
+    initvalues = {"formsemestre_id": formsemestre_id}
+    form = [("formsemestre_id", {"input_type": "hidden"})]
+    for ue in ues:
+        coefs = formsemestre_uecoef_list(
+            cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
+        )
+        if coefs:
+            initvalues["ue_" + ue["ue_id"]] = coefs[0]["coefficient"]
+        else:
+            initvalues["ue_" + ue["ue_id"]] = "auto"
+        descr = {
+            "size": 10,
+            "title": ue["acronyme"],
+            "explanation": "somme coefs modules = %s" % ue["sum_coefs"],
+        }
+        if ue["ue_id"] == err_ue_id:
+            descr["dom_id"] = "erroneous_ue"
+        form.append(("ue_" + ue["ue_id"], descr))
+
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        form,
+        submitlabel="Changer les coefficients",
+        cancelbutton="Annuler",
+        initvalues=initvalues,
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + footer
+    elif tf[0] == -1:
+        return "<h4>annulation</h4>"  # XXX
+    else:
+        # change values
+        # 1- supprime les coef qui ne sont plus forcés
+        # 2- modifie ou cree les coefs
+        ue_deleted = []
+        ue_modified = []
+        msg = []
+        for ue in ues:
+            val = tf[2]["ue_" + ue["ue_id"]]
+            coefs = formsemestre_uecoef_list(
+                cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
+            )
+            if val == "" or val == "auto":
+                # supprime ce coef (il sera donc calculé automatiquement)
+                if coefs:
+                    ue_deleted.append(ue)
+            else:
+                try:
+                    val = float(val)
+                    if (not coefs) or (coefs[0]["coefficient"] != val):
+                        ue["coef"] = val
+                        ue_modified.append(ue)
+                except:
+                    ok = False
+                    msg.append(
+                        "valeur invalide (%s) pour le coefficient de l'UE %s"
+                        % (val, ue["acronyme"])
+                    )
+
+        if not ok:
+            return (
+                "\n".join(H)
+                + "<p><ul><li>%s</li></ul></p>" % "</li><li>".join(msg)
+                + tf[1]
+                + footer
+            )
+
+        # apply modifications
+        for ue in ue_modified:
+            do_formsemestre_uecoef_edit_or_create(
+                context, cnx, formsemestre_id, ue["ue_id"], ue["coef"]
+            )
+        for ue in ue_deleted:
+            do_formsemestre_uecoef_delete(context, cnx, formsemestre_id, ue["ue_id"])
+
+        if ue_modified or ue_deleted:
+            z = ["""<h3>Modification effectuées</h3>"""]
+            if ue_modified:
+                z.append("""<h4>Coefs modifiés dans les UE:<h4><ul>""")
+                for ue in ue_modified:
+                    z.append("<li>%(acronyme)s : %(coef)s</li>" % ue)
+                z.append("</ul>")
+            if ue_deleted:
+                z.append("""<h4>Coefs supprimés dans les UE:<h4><ul>""")
+                for ue in ue_deleted:
+                    z.append("<li>%(acronyme)s</li>" % ue)
+                z.append("</ul>")
+        else:
+            z = ["""<h3>Aucune modification</h3>"""]
+        context._inval_cache(
+            formsemestre_id=formsemestre_id
+        )  # > modif coef UE cap (modifs notes de _certains_ etudiants)
+
+        header = context.html_sem_header(
+            REQUEST, "Coefficients des UE du semestre", sem
+        )
+        return (
+            header
+            + "\n".join(z)
+            + """<p><a href="formsemestre_status?formsemestre_id=%s">Revenir au tableau de bord</a></p>"""
+            % formsemestre_id
+            + footer
+        )
+
+
+def do_formsemestre_uecoef_edit_or_create(context, cnx, formsemestre_id, ue_id, coef):
+    "modify or create the coef"
+    coefs = formsemestre_uecoef_list(
+        cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue_id}
+    )
+    if coefs:
+        formsemestre_uecoef_edit(
+            cnx,
+            args={
+                "formsemestre_uecoef_id": coefs[0]["formsemestre_uecoef_id"],
+                "coefficient": coef,
+            },
+        )
+    else:
+        formsemestre_uecoef_create(
+            cnx,
+            args={
+                "formsemestre_id": formsemestre_id,
+                "ue_id": ue_id,
+                "coefficient": coef,
+            },
+        )
+
+
+def do_formsemestre_uecoef_delete(context, cnx, formsemestre_id, ue_id):
+    "delete coef for this (ue,sem)"
+    coefs = formsemestre_uecoef_list(
+        cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue_id}
+    )
+    if coefs:
+        formsemestre_uecoef_delete(cnx, coefs[0]["formsemestre_uecoef_id"])
+
+
+# ----- identification externe des sessions (pour SOJA et autres logiciels)
+def get_formsemestre_session_id(context, sem, F, parcours):
+    """Identifiant de session pour ce semestre
+    Exemple:  RT-DUT-FI-S1-ANNEE
+
+    DEPT-TYPE-MODALITE+-S?|SPECIALITE
+    
+    TYPE=DUT|LP*|M*
+    MODALITE=FC|FI|FA (si plusieurs, en inverse alpha)
+    
+    SPECIALITE=[A-Z]+   EON,ASSUR, ... (si pas Sn ou SnD)
+
+    ANNEE=annee universitaire de debut (exemple: un S2 de 2013-2014 sera S2-2013)
+    
+    """
+    # sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    # F = context.formation_list( args={ 'formation_id' : sem['formation_id'] } )[0]
+    # parcours = sco_codes_parcours.get_parcours_from_code(F['type_parcours'])
+
+    ImputationDept = context.get_preference("ImputationDept", sem["formsemestre_id"])
+    if not ImputationDept:
+        ImputationDept = context.get_preference("DeptName")
+    ImputationDept = ImputationDept.upper()
+    parcours_type = parcours.NAME
+    modalite = sem["modalite"]
+    modalite = (
+        (modalite or "").replace("FAP", "FA").replace("APP", "FA")
+    )  # exception pour code Apprentissage
+    if sem["semestre_id"] > 0:
+        decale = sem_decale_str(sem)
+        semestre_id = "S%d" % sem["semestre_id"] + decale
+    else:
+        semestre_id = F["code_specialite"]
+    annee_sco = str(annee_scolaire_debut(sem["annee_debut"], sem["mois_debut_ord"]))
+
+    return sanitize_string(
+        "-".join((ImputationDept, parcours_type, modalite, semestre_id, annee_sco))
+    )
diff --git a/sco_formsemestre_exterieurs.py b/sco_formsemestre_exterieurs.py
new file mode 100644
index 0000000000000000000000000000000000000000..0775ca928768040e6dd07aaf8c4bcd59b499a2be
--- /dev/null
+++ b/sco_formsemestre_exterieurs.py
@@ -0,0 +1,491 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Saisie et gestion des semestres extérieurs à ScoDoc dans un parcours.
+
+On va créer/gérer des semestres de la même formation que le semestre ScoDoc 
+où est inscrit l'étudiant, leur attribuer la modalité 'EXT'.
+Ces semestres n'auront qu'un seul inscrit !
+"""
+import TrivialFormulator
+import sco_formsemestre
+import sco_formsemestre_inscriptions
+import sco_formsemestre_edit
+import sco_formsemestre_validation
+import sco_parcours_dut
+import notesdb
+
+from sco_utils import log
+import pprint
+import time
+
+
+def formsemestre_ext_create(context, etudid, sem_params, REQUEST=None):
+    """Crée un formsemestre exterieur et y inscrit l'étudiant.
+    sem_params: dict nécessaire à la création du formsemestre
+    """
+    # Check args
+    _formation = context.formation_list(
+        args={"formation_id": sem_params["formation_id"]}
+    )[0]
+    if etudid:
+        _etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+
+    # Create formsemestre
+    sem_params["modalite"] = "EXT"
+    sem_params["etapes"] = None
+    sem_params["responsables"] = [str(REQUEST.AUTHENTICATED_USER)]
+    formsemestre_id = context.do_formsemestre_create(sem_params, REQUEST, silent=True)
+    # nota: le semestre est créé vide: pas de modules
+
+    # Inscription au semestre
+    sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
+        context,
+        formsemestre_id,
+        etudid,
+        REQUEST=REQUEST,
+        method="formsemestre_ext_create",
+    )
+    return formsemestre_id
+
+
+def formsemestre_ext_create_form(context, etudid, formsemestre_id, REQUEST=None):
+    """Formulaire creation/inscription à un semestre extérieur"""
+    etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+    H = [
+        context.sco_header(REQUEST),
+        """<h2>Enregistrement d'une inscription antérieure dans un autre établissement</h2>
+        <p class="help">
+        Cette opération créé un semestre extérieur ("ancien") et y inscrit juste cet étudiant. 
+        La décision de jury peut ensuite y être saisie. 
+        </p>
+        <p class="help">
+        Notez que si un semestre extérieur similaire a déjà été créé pour un autre étudiant,
+        il est préférable d'utiliser la fonction 
+        "<a href="formsemestre_inscription_with_modules_form?etudid=%s&amp;only_ext=1">
+        inscrire à un autre semestre</a>"
+        </p>
+        """
+        % (etudid,),
+        """<h3><a href="ficheEtud?etudid=%s" class="stdlink">Etudiant %s</a></h3>"""
+        % (etudid, etud["nomprenom"]),
+    ]
+    F = context.sco_footer(REQUEST)
+    orig_sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    # Ne propose que des semestres de semestre_id strictement inférieur au semestre courant
+    # et seulement si pas inscrit au même semestre_id d'un semestre ordinaire ScoDoc.
+    # Les autres situations (eg redoublements en changeant d'établissement)
+    # doivent être gérées par les validations de semestres "antérieurs"
+    insem = context.do_formsemestre_inscription_list(
+        args={"etudid": etudid, "etat": "I"}
+    )
+    semlist = [
+        sco_formsemestre.get_formsemestre(context, i["formsemestre_id"]) for i in insem
+    ]
+    existing_semestre_ids = set([s["semestre_id"] for s in semlist])
+    min_semestre_id = 1
+    max_semestre_id = orig_sem["semestre_id"]
+    semestre_ids = set(range(min_semestre_id, max_semestre_id)) - existing_semestre_ids
+    H.append(
+        """<p>L'étudiant est déjà inscrit dans des semestres ScoDoc de rangs:
+            %s
+            </p>"""
+        % sorted(list(existing_semestre_ids))
+    )
+    if not semestre_ids:
+        H.append("""<p class="warning">pas de semestres extérieurs possibles</p>""")
+        return "\n".join(H) + F
+    # Formulaire
+    semestre_ids_str = [str(x) for x in sorted(semestre_ids)]
+    descr = [
+        ("formsemestre_id", {"input_type": "hidden"}),
+        ("etudid", {"input_type": "hidden"}),
+        (
+            "semestre_id",
+            {
+                "input_type": "menu",
+                "title": "Indice du semestre dans le cursus",
+                "allowed_values": semestre_ids_str,
+                "labels": semestre_ids_str,
+            },
+        ),
+        (
+            "titre",
+            {
+                "size": 40,
+                "title": "Nom de ce semestre extérieur",
+                "explanation": """par exemple: établissement. N'indiquez pas les dates, ni le semestre, ni la modalité dans
+                 le titre: ils seront automatiquement ajoutés""",
+            },
+        ),
+        (
+            "date_debut",
+            {
+                "title": "Date de début",  # j/m/a
+                "input_type": "date",
+                "explanation": "j/m/a (peut être approximatif)",
+                "size": 9,
+                "allow_null": False,
+            },
+        ),
+        (
+            "date_fin",
+            {
+                "title": "Date de fin",  # j/m/a
+                "input_type": "date",
+                "explanation": "j/m/a (peut être approximatif)",
+                "size": 9,
+                "allow_null": False,
+            },
+        ),
+        (
+            "elt_help_ue",
+            {
+                "title": """Les notes et coefficients des UE 
+                capitalisées seront saisis ensuite""",
+                "input_type": "separator",
+            },
+        ),
+    ]
+
+    tf = TrivialFormulator.TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        cancelbutton="Annuler",
+        method="post",
+        submitlabel="Créer semestre extérieur et y inscrire l'étudiant",
+        cssclass="inscription",
+        name="tf",
+    )
+    if tf[0] == 0:
+        H.append(
+            """<p>Ce formulaire sert à enregistrer un semestre antérieur dans la formation 
+            effectué dans un autre établissement.
+            </p>"""
+        )
+        return "\n".join(H) + "\n" + tf[1] + F
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(
+            "%s/formsemestre_bulletinetud?formsemestre_id==%s&amp;etudid=%s"
+            % (context.ScoURL(), formsemestre_id, etudid)
+        )
+    else:
+        tf[2]["formation_id"] = orig_sem["formation_id"]
+        formsemestre_ext_create(context, etudid, tf[2], REQUEST=REQUEST)
+        return REQUEST.RESPONSE.redirect(
+            "%s/ficheEtud?etudid=%s" % (context.ScoURL(), etudid)
+        )
+
+
+def formsemestre_ext_edit_ue_validations(
+    context, formsemestre_id, etudid, REQUEST=None
+):
+    """Edition des validations d'UE et de semestre (jury)
+    pour un semestre extérieur.
+    On peut saisir pour chaque UE du programme de formation
+    sa validation, son code jury, sa note, son coefficient.
+
+    La moyenne générale du semestre est calculée et affichée,
+    mais pas enregistrée.
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+    ue_list = _list_ue_with_coef_and_validations(context, sem, etudid)
+    descr = _ue_form_description(context, ue_list, REQUEST.form)
+    if REQUEST and REQUEST.method == "GET":
+        initvalues = {
+            "note_" + ue["ue_id"]: ue["validation"].get("moy_ue", "") for ue in ue_list
+        }
+    else:
+        initvalues = {}
+    tf = TrivialFormulator.TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        cssclass="tf_ext_edit_ue_validations",
+        submitlabel="Enregistrer ces validations",
+        cancelbutton="Annuler",
+        initvalues=initvalues,
+    )
+    if tf[0] == -1:
+        return "<h4>annulation</h4>"
+    else:
+        H = _make_page(context, etud, sem, tf, REQUEST=REQUEST)
+        if tf[0] == 0:  # premier affichage
+            return "\n".join(H)
+        else:  # soumission
+            # simule erreur
+            ok, message = _check_values(context, ue_list, tf[2])
+            if not ok:
+                H = _make_page(context, etud, sem, tf, message=message, REQUEST=REQUEST)
+                return "\n".join(H)
+            else:
+                # Submit
+                _record_ue_validations_and_coefs(
+                    context, formsemestre_id, etudid, ue_list, tf[2], REQUEST=REQUEST
+                )
+                return REQUEST.RESPONSE.redirect(
+                    "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s"
+                    % (formsemestre_id, etudid)
+                )
+
+
+def _make_page(context, etud, sem, tf, message="", REQUEST=None):
+    nt = context._getNotesCache().get_NotesTable(context, sem["formsemestre_id"])
+    moy_gen = nt.get_etud_moy_gen(etud["etudid"])
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Validation des UE d'un semestre extérieur",
+            javascripts=["js/formsemestre_ext_edit_ue_validations.js"],
+        ),
+        TrivialFormulator.tf_error_message(message),
+        """<p><b>%(nomprenom)s</b> est inscrit%(ne)s à ce semestre extérieur.</p>
+        <p>Voici les UE entregistrées avec leur notes et coefficients.
+        </p>
+        """
+        % etud,
+        """<p>La moyenne de ce semestre serait: 
+        <span class="ext_sem_moy"><span class="ext_sem_moy_val">%s</span> / 20</span>
+        </p>
+        """
+        % moy_gen,
+        '<div id="formsemestre_ext_edit_ue_validations">',
+        tf[1],
+        "</div>",
+        """<div>
+        <a class="stdlink" 
+        href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s">
+        retour au bulletin de notes
+        </a></div>
+        """
+        % (sem["formsemestre_id"], etud["etudid"]),
+        context.sco_footer(REQUEST),
+    ]
+    return H
+
+
+_UE_VALID_CODES = {
+    None: "Non inscrit",
+    "ADM": "Capitalisée (ADM)",
+    # "CMP": "Acquise (car semestre validé)",
+}
+
+
+def _ue_form_description(context, ue_list, values):
+    """Description du formulaire de saisie des UE / validations
+    Pour chaque UE, on peut saisir: son code jury, sa note, son coefficient.
+    """
+    descr = [
+        (
+            "head_sep",
+            {
+                "input_type": "separator",
+                "template": """<tr %(item_dom_attr)s><th>UE</th>
+            <th>Code jury</th><th>Note/20</th><th>Coefficient UE</th></tr>
+            """,
+            },
+        ),
+        ("formsemestre_id", {"input_type": "hidden"}),
+        ("etudid", {"input_type": "hidden"}),
+    ]
+    for ue in ue_list:
+        # Menu pour code validation UE:
+        # Ne propose que ADM, CMP et "Non inscrit"
+        select_name = "valid_" + ue["ue_id"]
+        menu_code_UE = """<select class="ueext_valid_select" name="%s">""" % (
+            select_name,
+        )
+        cur_value = values.get("valid_" + ue["ue_id"], False)
+        for code in _UE_VALID_CODES:
+            if cur_value is False:  # pas dans le form, cherche en base
+                cur_value = ue["validation"].get("code", None)
+            if str(cur_value) == str(code):
+                selected = "selected"
+            else:
+                selected = ""
+            menu_code_UE += '<option value="%s" %s>%s</option>' % (
+                code,
+                selected,
+                _UE_VALID_CODES[code],
+            )
+            if cur_value is None:
+                disabled = 'disabled="1"'
+            else:
+                disabled = ""
+        menu_code_UE += "</select>"
+        cur_value = values.get("coef_" + ue["ue_id"], False)
+        if cur_value is False:  # pas dans le form, cherche en base
+            cur_value = ue["uecoef"].get("coefficient", "")
+        itemtemplate = (
+            """<tr><td class="tf-fieldlabel">%(label)s</td>"""
+            + "<td>"
+            + menu_code_UE
+            + "</td>"  # code jury
+            + '<td class="tf-field tf_field_note">%(elem)s</td>'  # note
+            + """<td class="tf-field tf_field_coef">
+            <input type="text" size="4" name="coef_%s" value="%s" %s></input></td>
+            """
+            % (ue["ue_id"], cur_value, disabled)
+            + "</td></tr>"
+        )
+        descr.append(
+            (
+                "note_" + str(ue["ue_id"]),
+                {
+                    "input_type": "text",
+                    "size": 4,
+                    "template": itemtemplate,
+                    "title": "<tt><b>%(acronyme)s</b></tt> %(titre)s" % ue,
+                    "attributes": [disabled],
+                },
+            )
+        )
+    return descr
+
+
+def _check_values(context, ue_list, values):
+    """Check that form values are ok
+    for each UE:
+        code != None => note and coef
+        note or coef => code != None
+        note float in [0, 20]
+        note => coef
+        coef float >= 0
+    """
+    for ue in ue_list:
+        pu = " pour UE %s" % ue["acronyme"]
+        code = values.get("valid_" + ue["ue_id"], False)
+        if code == "None":
+            code = None
+        note = values.get("note_" + ue["ue_id"], False)
+        try:
+            note = _convert_field_to_float(note)
+        except ValueError:
+            return False, "note invalide" + pu
+        coef = values.get("coef_" + ue["ue_id"], False)
+        try:
+            coef = _convert_field_to_float(coef)
+        except ValueError:
+            return False, "coefficient invalide" + pu
+        if code != False:
+            if code not in _UE_VALID_CODES:
+                return False, "code invalide" + pu
+            if code != None:
+                if note is False or note is "":
+                    return False, "note manquante" + pu
+        if note != False and note != "":
+            if code == None:
+                return (
+                    False,
+                    "code jury incohérent (code %s, note %s)" % (code, note)
+                    + pu
+                    + " (supprimer note et coef)",
+                )
+            if note < 0 or note > 20:
+                return False, "valeur note invalide" + pu
+            if not isinstance(coef, float):
+                return False, "coefficient manquant pour note %s" % note + pu
+        if coef != False and coef != "":
+            if coef < 0:
+                return False, "valeur coefficient invalide" + pu
+    return True, "ok"
+
+
+def _convert_field_to_float(val):
+    """value may be empty, False, or a float. Raise exception"""
+    if val != False:
+        val = val.strip()
+    if val:
+        val = float(val)
+    return val
+
+
+def _list_ue_with_coef_and_validations(context, sem, etudid):
+    """Liste des UE de la même formation que sem,
+    avec leurs coefs d'UE capitalisée (si déjà saisi)
+    et leur validation pour cet étudiant.
+    """
+    cnx = context.GetDBConnexion()
+    formsemestre_id = sem["formsemestre_id"]
+    ue_list = context.do_ue_list({"formation_id": sem["formation_id"]})
+    for ue in ue_list:
+        # add coefficient
+        uecoef = sco_formsemestre_edit.formsemestre_uecoef_list(
+            cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
+        )
+        if uecoef:
+            ue["uecoef"] = uecoef[0]
+        else:
+            ue["uecoef"] = {}
+        # add validation
+        validation = sco_parcours_dut.scolar_formsemestre_validation_list(
+            cnx,
+            args={
+                "formsemestre_id": formsemestre_id,
+                "etudid": etudid,
+                "ue_id": ue["ue_id"],
+            },
+        )
+        if validation:
+            ue["validation"] = validation[0]
+        else:
+            ue["validation"] = {}
+    return ue_list
+
+
+def _record_ue_validations_and_coefs(
+    context, formsemestre_id, etudid, ue_list, values, REQUEST=None
+):
+    for ue in ue_list:
+        code = values.get("valid_" + ue["ue_id"], False)
+        if code == "None":
+            code = None
+        note = values.get("note_" + ue["ue_id"], False)
+        note = _convert_field_to_float(note)
+        coef = values.get("coef_" + ue["ue_id"], False)
+        coef = _convert_field_to_float(coef)
+        if coef == "" or coef == False:
+            coef = None
+        now_dmy = time.strftime("%d/%m/%Y")
+        log(
+            "_record_ue_validations_and_coefs: %s etudid=%s ue_id=%s moy_ue=%s ue_coef=%s"
+            % (formsemestre_id, etudid, ue["ue_id"], note, repr(coef))
+        )
+        assert code == None or (note)  # si code validant, il faut une note
+        sco_formsemestre_validation.do_formsemestre_validate_previous_ue(
+            context,
+            formsemestre_id,
+            etudid,
+            ue["ue_id"],
+            note,
+            now_dmy,
+            code=code,
+            ue_coefficient=coef,
+            REQUEST=REQUEST,
+        )
diff --git a/sco_formsemestre_inscriptions.py b/sco_formsemestre_inscriptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..a32ff254d877294e81c9546d2920f775d95df5f4
--- /dev/null
+++ b/sco_formsemestre_inscriptions.py
@@ -0,0 +1,632 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Opérations d'inscriptions aux semestres et modules
+"""
+
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+from notes_table import *
+import sco_find_etud
+import sco_formsemestre
+import sco_groups
+
+
+def do_formsemestre_inscription_with_modules(
+    context,
+    formsemestre_id,
+    etudid,
+    group_ids=[],
+    etat="I",
+    REQUEST=None,
+    method="inscription_with_modules",
+):
+    """Inscrit cet etudiant a ce semestre et TOUS ses modules STANDARDS
+    (donc sauf le sport)
+    """
+    # inscription au semestre
+    args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
+    if etat is not None:
+        args["etat"] = etat
+    context.do_formsemestre_inscription_create(args, REQUEST, method=method)
+    log(
+        "do_formsemestre_inscription_with_modules: etudid=%s formsemestre_id=%s"
+        % (etudid, formsemestre_id)
+    )
+    # inscriptions aux groupes
+    # 1- inscrit au groupe 'tous'
+    group_id = sco_groups.get_default_group(context, formsemestre_id)
+    sco_groups.set_group(context, etudid, group_id)
+    gdone = {group_id: 1}  # empeche doublons
+
+    # 2- inscrit aux groupes
+    for group_id in group_ids:
+        if group_id and not group_id in gdone:
+            sco_groups.set_group(context, etudid, group_id)
+            gdone[group_id] = 1
+
+    # inscription a tous les modules de ce semestre
+    modimpls = context.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+    for mod in modimpls:
+        if mod["ue"]["type"] != UE_SPORT:
+            context.do_moduleimpl_inscription_create(
+                {"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid},
+                REQUEST=REQUEST,
+                formsemestre_id=formsemestre_id,
+            )
+
+
+def formsemestre_inscription_with_modules_etud(
+    context, formsemestre_id, etudid=None, group_ids=None, REQUEST=None
+):
+    """Form. inscription d'un étudiant au semestre.
+    Si etudid n'est pas specifié, form. choix etudiant.
+    """
+    if not etudid:
+        return sco_find_etud.form_search_etud(
+            context,
+            title="Choix de l'étudiant à inscrire dans ce semestre",
+            add_headers=True,
+            dest_url="formsemestre_inscription_with_modules_etud",
+            parameters={"formsemestre_id": formsemestre_id},
+            REQUEST=REQUEST,
+        )
+
+    return formsemestre_inscription_with_modules(
+        context, etudid, formsemestre_id, REQUEST=REQUEST, group_ids=group_ids
+    )
+
+
+def formsemestre_inscription_with_modules_form(
+    context, etudid, REQUEST, only_ext=False
+):
+    """Formulaire inscription de l'etud dans l'un des semestres existants.
+    Si only_ext, ne montre que les semestre extérieurs.
+    """
+    etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+    H = [context.sco_header(REQUEST), "<h2>Inscription de %s" % etud["nomprenom"]]
+    if only_ext:
+        H.append(" dans un semestre extérieur")
+    H.append(
+        """</h2>
+    <p class="help">L'étudiant sera inscrit à <em>tous</em> les modules du semestre 
+    choisi (sauf Sport &amp; Culture).
+    </p>
+    <h3>Choisir un semestre:</h3>"""
+    )
+    F = context.sco_footer(REQUEST)
+    sems = sco_formsemestre.do_formsemestre_list(context, args={"etat": "1"})
+    insem = context.do_formsemestre_inscription_list(
+        args={"etudid": etudid, "etat": "I"}
+    )
+    if sems:
+        H.append("<ul>")
+        for sem in sems:
+            # Ne propose que les semestres ou etudid n'est pas déjà inscrit
+            inscrit = False
+            for i in insem:
+                if i["formsemestre_id"] == sem["formsemestre_id"]:
+                    inscrit = True
+            if not inscrit:
+                if (not only_ext) or (sem["modalite"] == "EXT"):
+                    H.append(
+                        """
+                    <li><a class="stdlink" href="formsemestre_inscription_with_modules?etudid=%s&amp;formsemestre_id=%s">%s</a>
+                    """
+                        % (etudid, sem["formsemestre_id"], sem["titremois"])
+                    )
+        H.append("</ul>")
+    else:
+        H.append("<p>aucune session de formation !</p>")
+    H.append(
+        '<h3>ou</h3> <a class="stdlink" href="%s/ficheEtud?etudid=%s">retour à la fiche de %s</a>'
+        % (context.ScoURL(), etudid, etud["nomprenom"])
+    )
+    return "\n".join(H) + F
+
+
+def formsemestre_inscription_with_modules(
+    context, etudid, formsemestre_id, group_ids=None, multiple_ok=False, REQUEST=None
+):
+    """
+    Inscription de l'etud dans ce semestre.
+    Formulaire avec choix groupe.
+    """
+    log(
+        "formsemestre_inscription_with_modules: etudid=%s formsemestre_id=%s group_ids=%s"
+        % (etudid, formsemestre_id, group_ids)
+    )
+    if multiple_ok:
+        multiple_ok = int(multiple_ok)
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+    H = [
+        context.html_sem_header(
+            REQUEST, "Inscription de %s dans ce semestre" % etud["nomprenom"], sem
+        )
+    ]
+    F = context.sco_footer(REQUEST)
+    # Check 1: déjà inscrit ici ?
+    ins = context.Notes.do_formsemestre_inscription_list({"etudid": etudid})
+    already = False
+    for i in ins:
+        if i["formsemestre_id"] == formsemestre_id:
+            already = True
+    if already:
+        H.append(
+            '<p class="warning">%s est déjà inscrit dans le semestre %s</p>'
+            % (etud["nomprenom"], sem["titremois"])
+        )
+        H.append(
+            """<ul><li><a href="ficheEtud?etudid=%s">retour à la fiche de %s</a></li>
+        <li><a href="formsemestre_status?formsemestre_id=%s">retour au tableau de bord de %s</a></li></ul>"""
+            % (etudid, etud["nomprenom"], formsemestre_id, sem["titremois"])
+        )
+        return "\n".join(H) + F
+    # Check 2: déjà inscrit dans un semestre recouvrant les même dates ?
+    # Informe et propose dé-inscriptions
+    others = est_inscrit_ailleurs(context, etudid, formsemestre_id)
+    if others and not multiple_ok:
+        l = []
+        for s in others:
+            l.append(
+                '<a class="discretelink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a>'
+                % s
+            )
+
+        H.append(
+            '<p class="warning">Attention: %s est déjà inscrit sur la même période dans: %s.</p>'
+            % (etud["nomprenom"], ", ".join(l))
+        )
+        H.append("<ul>")
+        for s in others:
+            H.append(
+                '<li><a href="formsemestre_desinscription?formsemestre_id=%s&amp;etudid=%s">déinscrire de %s</li>'
+                % (s["formsemestre_id"], etudid, s["titreannee"])
+            )
+        H.append("</ul>")
+        H.append(
+            """<p><a href="formsemestre_inscription_with_modules?etudid=%s&amp;formsemestre_id=%s&amp;multiple_ok=1&amp;%s">Continuer quand même l'inscription</a></p>"""
+            % (etudid, formsemestre_id, sco_groups.make_query_groups(group_ids))
+        )
+        return "\n".join(H) + F
+    #
+    if group_ids is not None:
+        # OK, inscription
+        do_formsemestre_inscription_with_modules(
+            context,
+            formsemestre_id,
+            etudid,
+            group_ids=group_ids,
+            etat="I",
+            REQUEST=REQUEST,
+            method="formsemestre_inscription_with_modules",
+        )
+        return REQUEST.RESPONSE.redirect(
+            context.ScoURL() + "/ficheEtud?etudid=" + etudid
+        )
+    else:
+        # formulaire choix groupe
+        H.append(
+            """<form method="GET" name="groupesel" action="%s">
+        <input type="hidden" name="etudid" value="%s">
+        <input type="hidden" name="formsemestre_id" value="%s">
+        """
+            % (REQUEST.URL0, etudid, formsemestre_id)
+        )
+
+        H.append(
+            sco_groups.form_group_choice(context, formsemestre_id, allow_none=True)
+        )
+
+        #
+        H.append(
+            """
+        <input type="submit" value="Inscrire"/>
+        <p>Note: l'étudiant sera inscrit dans les groupes sélectionnés</p>
+        </form>
+        """
+        )
+        return "\n".join(H) + F
+
+
+def formsemestre_inscription_option(context, etudid, formsemestre_id, REQUEST=None):
+    """Dialogue pour (des)inscription a des modules optionnels
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    if sem["etat"] != "1":
+        raise ScoValueError("Modification impossible: semestre verrouille")
+
+    etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etud_ue_status
+
+    F = context.sco_footer(REQUEST)
+    H = [
+        context.sco_header(REQUEST)
+        + "<h2>Inscription de %s aux modules de %s (%s - %s)</h2>"
+        % (etud["nomprenom"], sem["titre_num"], sem["date_debut"], sem["date_fin"])
+    ]
+
+    # Cherche les moduleimpls et les inscriptions
+    mods = context.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+    inscr = context.do_moduleimpl_inscription_list(etudid=etudid)
+    # Formulaire
+    modimpls_by_ue_ids = DictDefault(defaultvalue=[])  # ue_id : [ moduleimpl_id ]
+    modimpls_by_ue_names = DictDefault(defaultvalue=[])  # ue_id : [ moduleimpl_name ]
+    ues = []
+    ue_ids = Set()
+    initvalues = {}
+    for mod in mods:
+        ue_id = mod["ue"]["ue_id"]
+        if not ue_id in ue_ids:
+            ues.append(mod["ue"])
+            ue_ids.add(ue_id)
+        modimpls_by_ue_ids[ue_id].append(mod["moduleimpl_id"])
+
+        modimpls_by_ue_names[ue_id].append(
+            "%s %s" % (mod["module"]["code"], mod["module"]["titre"])
+        )
+        if not REQUEST.form.get("tf-submitted", False):
+            # inscrit ?
+            for ins in inscr:
+                if ins["moduleimpl_id"] == mod["moduleimpl_id"]:
+                    key = "moduleimpls_%s" % ue_id
+                    if key in initvalues:
+                        initvalues[key].append(mod["moduleimpl_id"])
+                    else:
+                        initvalues[key] = [mod["moduleimpl_id"]]
+                    break
+
+    descr = [
+        ("formsemestre_id", {"input_type": "hidden"}),
+        ("etudid", {"input_type": "hidden"}),
+    ]
+    for ue in ues:
+        ue_id = ue["ue_id"]
+        ue_descr = ue["acronyme"]
+        if ue["type"] != UE_STANDARD:
+            ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue["type"]]
+        ue_status = nt.get_etud_ue_status(etudid, ue_id)
+        if ue_status["is_capitalized"]:
+            sem_origin = sco_formsemestre.get_formsemestre(
+                context, ue_status["formsemestre_id"]
+            )
+            ue_descr += (
+                ' <a class="discretelink" href="formsemestre_bulletinetud?formsemestre_id=%s&amp;etudid=%s" title="%s">(capitalisée le %s)'
+                % (
+                    sem_origin["formsemestre_id"],
+                    etudid,
+                    sem_origin["titreannee"],
+                    DateISOtoDMY(ue_status["event_date"]),
+                )
+            )
+        descr.append(
+            (
+                "sec_%s" % ue_id,
+                {
+                    "input_type": "separator",
+                    "title": """<b>%s :</b>  <a href="#" onclick="chkbx_select('%s', true);">inscrire</a>|<a href="#" onclick="chkbx_select('%s', false);">désinscrire</a> à tous les modules"""
+                    % (ue_descr, ue_id, ue_id),
+                },
+            )
+        )
+        descr.append(
+            (
+                "moduleimpls_%s" % ue_id,
+                {
+                    "input_type": "checkbox",
+                    "title": "",
+                    "dom_id": ue_id,
+                    "allowed_values": modimpls_by_ue_ids[ue_id],
+                    "labels": modimpls_by_ue_names[ue_id],
+                    "vertical": True,
+                },
+            )
+        )
+
+    H.append(
+        """<script type="text/javascript">
+function chkbx_select(field_id, state) {
+   var elems = document.getElementById(field_id).getElementsByTagName("input");
+   for (var i=0; i < elems.length; i++) {
+      elems[i].checked=state;
+   }
+}
+    </script>
+    """
+    )
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        initvalues,
+        cancelbutton="Annuler",
+        method="post",
+        submitlabel="Modifier les inscriptions",
+        cssclass="inscription",
+        name="tf",
+    )
+    if tf[0] == 0:
+        H.append(
+            """<p>Voici la liste des modules du semestre choisi.</p><p>
+    Les modules cochés sont ceux dans lesquels l'étudiant est inscrit. Vous pouvez l'inscrire ou le désincrire d'un ou plusieurs modules.</p>
+    <p>Attention: cette méthode ne devrait être utilisée que pour les modules <b>optionnels</b> (ou les activités culturelles et sportives) et pour désinscrire les étudiants dispensés (UE validées).</p>
+    """
+        )
+        return "\n".join(H) + "\n" + tf[1] + F
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(
+            "%s/ficheEtud?etudid=%s" % (context.ScoURL(), etudid)
+        )
+    else:
+        # Inscriptions aux modules choisis
+        # il faut desinscrire des modules qui ne figurent pas
+        # et inscrire aux autres, sauf si deja inscrit
+        a_desinscrire = {}.fromkeys([x["moduleimpl_id"] for x in mods])
+        insdict = {}
+        for ins in inscr:
+            insdict[ins["moduleimpl_id"]] = ins
+        for ue in ues:
+            ue_id = ue["ue_id"]
+            for moduleimpl_id in tf[2]["moduleimpls_%s" % ue_id]:
+                if a_desinscrire.has_key(moduleimpl_id):
+                    del a_desinscrire[moduleimpl_id]
+        # supprime ceux auxquel pas inscrit
+        for moduleimpl_id in a_desinscrire.keys():
+            if not insdict.has_key(moduleimpl_id):
+                del a_desinscrire[moduleimpl_id]
+
+        a_inscrire = Set()
+        for ue in ues:
+            ue_id = ue["ue_id"]
+            a_inscrire.update(tf[2]["moduleimpls_%s" % ue_id])
+        # supprime ceux auquel deja inscrit:
+        for ins in inscr:
+            if ins["moduleimpl_id"] in a_inscrire:
+                a_inscrire.remove(ins["moduleimpl_id"])
+        # dict des modules:
+        modsdict = {}
+        for mod in mods:
+            modsdict[mod["moduleimpl_id"]] = mod
+        #
+        if (not a_inscrire) and (not a_desinscrire):
+            H.append(
+                """<h3>Aucune modification à effectuer</h3>
+            <p><a class="stdlink" href="%s/ficheEtud?etudid=%s">retour à la fiche étudiant</a></p>"""
+                % (context.ScoURL(), etudid)
+            )
+            return "\n".join(H) + F
+
+        H.append("<h3>Confirmer les modifications:</h3>")
+        if a_desinscrire:
+            H.append(
+                "<p>%s va être <b>désinscrit%s</b> des modules:<ul><li>"
+                % (etud["nomprenom"], etud["ne"])
+            )
+            H.append(
+                "</li><li>".join(
+                    [
+                        "%s (%s)"
+                        % (
+                            modsdict[x]["module"]["titre"],
+                            modsdict[x]["module"]["code"],
+                        )
+                        for x in a_desinscrire
+                    ]
+                )
+                + "</p>"
+            )
+            H.append("</li></ul>")
+        if a_inscrire:
+            H.append(
+                "<p>%s va être <b>inscrit%s</b> aux modules:<ul><li>"
+                % (etud["nomprenom"], etud["ne"])
+            )
+            H.append(
+                "</li><li>".join(
+                    [
+                        "%s (%s)"
+                        % (
+                            modsdict[x]["module"]["titre"],
+                            modsdict[x]["module"]["code"],
+                        )
+                        for x in a_inscrire
+                    ]
+                )
+                + "</p>"
+            )
+            H.append("</li></ul>")
+        modulesimpls_ainscrire = ",".join(a_inscrire)
+        modulesimpls_adesinscrire = ",".join(a_desinscrire)
+        H.append(
+            """<form action="do_moduleimpl_incription_options">
+        <input type="hidden" name="etudid" value="%s"/>
+        <input type="hidden" name="modulesimpls_ainscrire" value="%s"/>
+        <input type="hidden" name="modulesimpls_adesinscrire" value="%s"/>
+        <input type ="submit" value="Confirmer"/>
+        <input type ="button" value="Annuler" onclick="document.location='%s/ficheEtud?etudid=%s';"/>
+        </form>
+        """
+            % (
+                etudid,
+                modulesimpls_ainscrire,
+                modulesimpls_adesinscrire,
+                context.ScoURL(),
+                etudid,
+            )
+        )
+        return "\n".join(H) + F
+
+
+def do_moduleimpl_incription_options(
+    context, etudid, modulesimpls_ainscrire, modulesimpls_adesinscrire, REQUEST=None
+):
+    """
+    Effectue l'inscription et la description aux modules optionnels
+    """
+    if modulesimpls_ainscrire:
+        a_inscrire = modulesimpls_ainscrire.split(",")
+    else:
+        a_inscrire = []
+    if modulesimpls_adesinscrire:
+        a_desinscrire = modulesimpls_adesinscrire.split(",")
+    else:
+        a_desinscrire = []
+    # inscriptions
+    for moduleimpl_id in a_inscrire:
+        # verifie que ce module existe bien
+        mods = context.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)
+        if len(mods) != 1:
+            raise ScoValueError(
+                "inscription: invalid moduleimpl_id: %s" % moduleimpl_id
+            )
+        mod = mods[0]
+        context.do_moduleimpl_inscription_create(
+            {"moduleimpl_id": moduleimpl_id, "etudid": etudid},
+            REQUEST=REQUEST,
+            formsemestre_id=mod["formsemestre_id"],
+        )
+    # desinscriptions
+    for moduleimpl_id in a_desinscrire:
+        # verifie que ce module existe bien
+        mods = context.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)
+        if len(mods) != 1:
+            raise ScoValueError(
+                "desinscription: invalid moduleimpl_id: %s" % moduleimpl_id
+            )
+        mod = mods[0]
+        inscr = context.do_moduleimpl_inscription_list(
+            moduleimpl_id=moduleimpl_id, etudid=etudid
+        )
+        if not inscr:
+            raise ScoValueError(
+                "pas inscrit a ce module ! (etudid=%s, moduleimpl_id=%)"
+                % (etudid, moduleimpl_id)
+            )
+        oid = inscr[0]["moduleimpl_inscription_id"]
+        context.do_moduleimpl_inscription_delete(
+            oid, formsemestre_id=mod["formsemestre_id"]
+        )
+
+    if REQUEST:
+        H = [
+            context.sco_header(REQUEST),
+            """<h3>Modifications effectuées</h3>
+              <p><a class="stdlink" href="%s/ficheEtud?etudid=%s">
+              Retour à la fiche étudiant</a></p>
+              """
+            % (context.ScoURL(), etudid),
+            context.sco_footer(REQUEST),
+        ]
+        return "\n".join(H)
+
+
+def est_inscrit_ailleurs(context, etudid, formsemestre_id):
+    """Vrai si l'étudiant est inscrit dans un semestre en même
+    temps que celui indiqué (par formsemestre_id).
+    Retourne la liste des semestres concernés (ou liste vide).
+    """
+    etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    debut_s = sem["dateord"]
+    fin_s = DateDMYtoISO(sem["date_fin"])
+    r = []
+    for s in etud["sems"]:
+        if s["formsemestre_id"] != formsemestre_id:
+            debut = s["dateord"]
+            fin = DateDMYtoISO(s["date_fin"])
+            if debut < fin_s and fin > debut_s:
+                r.append(s)  # intersection
+    return r
+
+
+def list_inscrits_ailleurs(context, formsemestre_id):
+    """Liste des etudiants inscrits ailleurs en même temps que formsemestre_id.
+    Pour chacun, donne la liste des semestres.
+    { etudid : [ liste de sems ] }
+    """
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etudids
+    etudids = nt.get_etudids()
+    d = {}
+    for etudid in etudids:
+        d[etudid] = est_inscrit_ailleurs(context, etudid, formsemestre_id)
+    return d
+
+
+def formsemestre_inscrits_ailleurs(context, formsemestre_id, REQUEST=None):
+    """Page listant les étudiants inscrits dans un autre semestre
+    dont les dates recouvrent le semestre indiqué.
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    H = [
+        context.html_sem_header(
+            REQUEST, "Inscriptions multiples parmi les étudiants du semestre ", sem
+        )
+    ]
+    insd = list_inscrits_ailleurs(context, formsemestre_id)
+    # liste ordonnée par nom
+    etudlist = [
+        context.getEtudInfo(etudid=etudid, filled=1)[0]
+        for etudid in insd.keys()
+        if insd[etudid]
+    ]
+    etudlist.sort(key=lambda x: x["nom"])
+    if etudlist:
+        H.append("<ul>")
+        for etud in etudlist:
+            H.append(
+                '<li><a href="ficheEtud?etudid=%(etudid)s" class="discretelink">%(nomprenom)s</a> : '
+                % etud
+            )
+            l = []
+            for s in insd[etud["etudid"]]:
+                l.append(
+                    '<a class="discretelink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a>'
+                    % s
+                )
+            H.append(", ".join(l))
+            H.append("</li>")
+        H.append("</ul>")
+        H.append("<p>Total: %d étudiants concernés.</p>" % len(etudlist))
+        H.append(
+            """<p class="help">Ces étudiants sont inscrits dans le semestre sélectionné et aussi dans d'autres semestres qui se déroulent en même temps ! <br/>Sauf exception, cette situation est anormale:</p>
+        <ul>
+        <li>vérifier que les dates des semestres se suivent sans se chevaucher</li>
+        <li>ou si besoin désinscrire le(s) étudiant(s) de l'un des semestres (via leurs fiches individuelles).</li>
+        </ul>
+        """
+        )
+    else:
+        H.append("""<p>Aucun étudiant en inscription multiple (c'est normal) !</p>""")
+    return "\n".join(H) + context.sco_footer(REQUEST)
diff --git a/sco_formsemestre_status.py b/sco_formsemestre_status.py
new file mode 100644
index 0000000000000000000000000000000000000000..28184576e585663767dc1c8adc142d942cc31ed6
--- /dev/null
+++ b/sco_formsemestre_status.py
@@ -0,0 +1,1041 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Tableau de bord semestre
+"""
+
+# Rewritten from ancient DTML code
+from mx.DateTime import DateTime as mxDateTime
+
+from notesdb import *
+from notes_log import log
+from sco_utils import *
+from sco_formsemestre_custommenu import formsemestre_custommenu_html
+from gen_tables import GenTable
+import sco_archives
+import sco_groups
+import sco_evaluations
+import sco_formsemestre
+import sco_formsemestre_edit
+import sco_compute_moy
+import sco_codes_parcours
+import sco_bulletins
+
+
+def makeMenu(title, items, css_class="", base_url="", alone=False):
+    """HTML snippet to render a simple drop down menu.
+    items is a list of dicts:
+    { 'title' :
+      'url' :
+      'id'  :
+      'attr' : "" # optionnal html <a> attributes
+      'enabled' : # True by default
+      'helpmsg' :
+      'submenu' : [ list of sub-items ]
+    }
+    """
+
+    def gen_menu_items(items):
+        H.append("<ul>")
+        for item in items:
+            if not item.get("enabled", True):
+                cls = ' class="ui-state-disabled"'
+            else:
+                cls = ""
+            the_id = item.get("id", "")
+            if the_id:
+                li_id = 'id="%s" ' % the_id
+            else:
+                li_id = ""
+            if base_url and "url" in item:
+                item["urlq"] = base_url + item["url"]
+            else:
+                item["urlq"] = item.get("url", "#")
+            item["attr"] = item.get("attr", "")
+            submenu = item.get("submenu", None)
+            H.append("<li " + li_id + cls + '><a href="%(urlq)s" %(attr)s>%(title)s</a>' % item)
+            if submenu:
+                gen_menu_items(submenu)
+            H.append("</li>")
+        H.append("</ul>")
+
+    H = []
+    if alone:
+        H.append('<ul class="sco_dropdown_menu %s">' % css_class)
+    H.append("""<li><a href="#">%s</a>""" % title)
+    gen_menu_items(items)
+    H.append("</li>")
+    if alone:
+        H.append("</ul>")
+    return "".join(H)
+
+
+# H = [ """<span class="barrenav"><ul class="nav">
+# <li onmouseover="MenuDisplay(this)" onmouseout="MenuHide(this)"><a href="#" class="menu %s">%s</a><ul>""" % (cssclass, title)
+#       ]
+# for item in items:
+#     if item.get('enabled', True):
+#         if base_url:
+#             item['urlq'] = urllib.quote(item['url'])
+#         else:
+#             item['urlq'] = item['url']
+#         H.append('<li><a href="' + base_url + '%(urlq)s">%(title)s</a></li>' % item)
+#     else:
+#         H.append('<li><span class="disabled_menu_item">%(title)s</span></li>' % item)
+# H.append('</ul></li></ul></%s>' % elem)
+# return ''.join(H)
+
+
+def defMenuStats(context, formsemestre_id):
+    "Définition du menu 'Statistiques' "
+    return [
+        {
+            "title": "Statistiques...",
+            "url": "formsemestre_report_counts?formsemestre_id=" + formsemestre_id,
+        },
+        {
+            "title": "Suivi de cohortes",
+            "url": "formsemestre_suivi_cohorte?formsemestre_id=" + formsemestre_id,
+            "enabled": True,
+        },
+        {
+            "title": "Graphe des parcours",
+            "url": "formsemestre_graph_parcours?formsemestre_id=" + formsemestre_id,
+            "enabled": WITH_PYDOT,
+        },
+        {
+            "title": "Codes des parcours",
+            "url": "formsemestre_suivi_parcours?formsemestre_id=" + formsemestre_id,
+            "enabled": True,
+        },
+        {
+            "title": "Lycées d'origine",
+            "url": "formsemestre_etuds_lycees?formsemestre_id=" + formsemestre_id,
+            "enabled": True,
+        },
+        {
+            "title": 'Table "poursuite"',
+            "url": "formsemestre_poursuite_report?formsemestre_id=" + formsemestre_id,
+            "enabled": True,
+        },
+        {
+            "title": "Documents Avis Poursuite Etudes",
+            "url": "pe_view_sem_recap?formsemestre_id=" + formsemestre_id,
+            "enabled": True,
+        },
+        {"title": 'Table "débouchés"', "url": "report_debouche_date", "enabled": True},
+        {
+            "title": "Estimation du coût de la formation",
+            "url": "formsemestre_estim_cost?formsemestre_id=" + formsemestre_id,
+            "enabled": True,
+        },
+        # { 'title' : 'experimental sub',
+        #   'submenu' : [
+        #         { 'title' : 'sous 1',
+        #           'url' : '#' },
+        #         { 'title' : 'sous 2',
+        #           'url' : '#' },
+        #         { 'title' : 'sous 3',
+        #           'url' : '#' },
+        #  ]
+        # },
+    ]
+
+
+def formsemestre_status_menubar(context, sem, REQUEST):
+    """HTML to render menubar"""
+    authuser = REQUEST.AUTHENTICATED_USER
+    uid = str(authuser)
+    formsemestre_id = sem["formsemestre_id"]
+    if int(sem["etat"]):
+        change_lock_msg = "Verrouiller"
+    else:
+        change_lock_msg = "Déverrouiller"
+
+    F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+
+    menuSemestre = [
+        {
+            "title": "Tableau de bord",
+            "url": "formsemestre_status?formsemestre_id=%(formsemestre_id)s" % sem,
+            "enabled": True,
+            "helpmsg": "Tableau de bord du semestre",
+        },
+        {
+            "title": "Voir la formation %(acronyme)s (v%(version)s)" % F,
+            "url": "ue_list?formation_id=%(formation_id)s" % sem,
+            "enabled": True,
+            "helpmsg": "Tableau de bord du semestre",
+        },
+        {
+            "title": "Modifier le semestre",
+            "url": "formsemestre_editwithmodules?formation_id=%(formation_id)s&amp;formsemestre_id=%(formsemestre_id)s"
+            % sem,
+            "enabled": (
+                authuser.has_permission(ScoImplement, context)
+                or (
+                    str(REQUEST.AUTHENTICATED_USER) in sem["responsables"]
+                    and sem["resp_can_edit"]
+                )
+            )
+            and (sem["etat"] == "1"),
+            "helpmsg": "Modifie le contenu du semestre (modules)",
+        },
+        {
+            "title": "Préférences du semestre",
+            "url": "formsemestre_edit_preferences?formsemestre_id=%(formsemestre_id)s"
+            % sem,
+            "enabled": (
+                authuser.has_permission(ScoImplement, context)
+                or (
+                    str(REQUEST.AUTHENTICATED_USER) in sem["responsables"]
+                    and sem["resp_can_edit"]
+                )
+            )
+            and (sem["etat"] == "1"),
+            "helpmsg": "Préférences du semestre",
+        },
+        {
+            "title": "Réglages bulletins",
+            "url": "formsemestre_edit_options?formsemestre_id=" + formsemestre_id,
+            "enabled": (uid in sem["responsables"])
+            or authuser.has_permission(ScoImplement, context),
+            "helpmsg": "Change les options",
+        },
+        {
+            "title": change_lock_msg,
+            "url": "formsemestre_change_lock?formsemestre_id=" + formsemestre_id,
+            "enabled": (uid in sem["responsables"])
+            or authuser.has_permission(ScoImplement, context),
+            "helpmsg": "",
+        },
+        {
+            "title": "Description du semestre",
+            "url": "formsemestre_description?formsemestre_id=" + formsemestre_id,
+            "enabled": True,
+            "helpmsg": "",
+        },
+        {
+            "title": "Vérifier absences aux évaluations",
+            "url": "formsemestre_check_absences_html?formsemestre_id="
+            + formsemestre_id,
+            "enabled": True,
+            "helpmsg": "",
+        },
+        {
+            "title": "Lister tous les enseignants",
+            "url": "formsemestre_enseignants_list?formsemestre_id=" + formsemestre_id,
+            "enabled": True,
+            "helpmsg": "",
+        },
+        {
+            "title": "Cloner ce semestre",
+            "url": "formsemestre_clone?formsemestre_id=" + formsemestre_id,
+            "enabled": authuser.has_permission(ScoImplement, context),
+            "helpmsg": "",
+        },
+        {
+            "title": "Associer à une nouvelle version du programme",
+            "url": "formsemestre_associate_new_version?formsemestre_id="
+            + formsemestre_id,
+            "enabled": authuser.has_permission(ScoChangeFormation, context)
+            and (sem["etat"] == "1"),
+            "helpmsg": "",
+        },
+        {
+            "title": "Supprimer ce semestre",
+            "url": "formsemestre_delete?formsemestre_id=" + formsemestre_id,
+            "enabled": authuser.has_permission(ScoImplement, context),
+            "helpmsg": "",
+        },
+    ]
+    # debug :
+    if uid == "root" or uid[:7] == "viennet":
+        menuSemestre.append(
+            {
+                "title": "Check integrity",
+                "url": "check_sem_integrity?formsemestre_id=" + formsemestre_id,
+                "enabled": True,
+            }
+        )
+
+    menuInscriptions = [
+        {
+            "title": "Voir les inscriptions aux modules",
+            "url": "moduleimpl_inscriptions_stats?formsemestre_id=" + formsemestre_id,
+        }
+    ]
+    menuInscriptions += [
+        {
+            "title": "Passage des étudiants depuis d'autres semestres",
+            "url": "formsemestre_inscr_passage?formsemestre_id=" + formsemestre_id,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context)
+            and (sem["etat"] == "1"),
+        },
+        {
+            "title": "Synchroniser avec étape Apogée",
+            "url": "formsemestre_synchro_etuds?formsemestre_id=" + formsemestre_id,
+            "enabled": authuser.has_permission(ScoView, context)
+            and context.get_preference("portal_url")
+            and (sem["etat"] == "1"),
+        },
+        {
+            "title": "Inscrire un étudiant",
+            "url": "formsemestre_inscription_with_modules_etud?formsemestre_id="
+            + formsemestre_id,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context)
+            and (sem["etat"] == "1"),
+        },
+        {
+            "title": "Importer des étudiants dans ce semestre (table Excel)",
+            "url": "form_students_import_excel?formsemestre_id=" + formsemestre_id,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context)
+            and (sem["etat"] == "1"),
+        },
+        {
+            "title": "Import/export des données admission",
+            "url": "form_students_import_infos_admissions?formsemestre_id="
+            + formsemestre_id,
+            "enabled": authuser.has_permission(ScoView, context),
+        },
+        {
+            "title": "Resynchroniser données identité",
+            "url": "formsemestre_import_etud_admission?formsemestre_id="
+            + formsemestre_id,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context)
+            and context.get_preference("portal_url"),
+        },
+        {
+            "title": "Exporter table des étudiants",
+            "url": "groups_view?format=allxls&amp;group_ids="
+            + sco_groups.get_default_group(
+                context, formsemestre_id, fix_if_missing=True, REQUEST=REQUEST
+            ),
+        },
+        {
+            "title": "Vérifier inscriptions multiples",
+            "url": "formsemestre_inscrits_ailleurs?formsemestre_id=" + formsemestre_id,
+        },
+    ]
+
+    menuGroupes = [
+        {
+            "title": "Listes, photos, feuilles...",
+            "url": "groups_view?formsemestre_id=" + formsemestre_id,
+            "enabled": True,
+            "helpmsg": "Accès aux listes des groupes d'étudiants",
+        },
+        # On laisse l'accès à l'ancienne page, le temps de tester
+        {
+            "title": "Listes (ancienne page)",
+            "url": "formsemestre_lists?formsemestre_id=" + formsemestre_id,
+            "enabled": True,
+            "helpmsg": "Accès aux listes des groupes d'étudiants",
+        },
+        {
+            "title": "Créer/modifier les partitions...",
+            "url": "editPartitionForm?formsemestre_id=" + formsemestre_id,
+            "enabled": context.can_change_groups(REQUEST, formsemestre_id),
+        },
+    ]
+    # 1 item / partition:
+    partitions = sco_groups.get_partitions_list(
+        context, formsemestre_id, with_default=False
+    )
+    submenu = []
+    enabled = context.can_change_groups(REQUEST, formsemestre_id) and partitions
+    for partition in partitions:
+        submenu.append(
+            {
+                "title": "%s" % partition["partition_name"],
+                "url": "affectGroups?partition_id=%s" % partition["partition_id"],
+                "enabled": enabled,
+            }
+        )
+    menuGroupes.append(
+        {"title": "Modifier les groupes", "submenu": submenu, "enabled": enabled}
+    )
+
+    menuNotes = [
+        {
+            "title": "Tableau des moyennes (et liens bulletins)",
+            "url": "formsemestre_recapcomplet?formsemestre_id=" + formsemestre_id,
+        },
+        {
+            "title": "Saisie des notes",
+            "url": "formsemestre_status?formsemestre_id=%(formsemestre_id)s" % sem,
+            "enabled": True,
+            "helpmsg": "Tableau de bord du semestre",
+        },
+        {
+            "title": "Classeur PDF des bulletins",
+            "url": "formsemestre_bulletins_pdf_choice?formsemestre_id="
+            + formsemestre_id,
+            "helpmsg": "PDF regroupant tous les bulletins",
+        },
+        {
+            "title": "Envoyer à chaque étudiant son bulletin par e-mail",
+            "url": "formsemestre_bulletins_mailetuds_choice?formsemestre_id="
+            + formsemestre_id,
+            "enabled": sco_bulletins.can_send_bulletin_by_mail(
+                context, formsemestre_id, REQUEST
+            ),
+        },
+        {
+            "title": "Calendrier des évaluations",
+            "url": "formsemestre_evaluations_cal?formsemestre_id=" + formsemestre_id,
+        },
+        {
+            "title": "Lister toutes les saisies de notes",
+            "url": "formsemestre_list_saisies_notes?formsemestre_id=" + formsemestre_id,
+        },
+    ]
+    menuJury = [
+        {
+            "title": "Voir les décisions du jury",
+            "url": "formsemestre_pvjury?formsemestre_id=" + formsemestre_id,
+        },
+        {
+            "title": "Générer feuille préparation Jury",
+            "url": "feuille_preparation_jury?formsemestre_id=" + formsemestre_id,
+        },
+        {
+            "title": "Saisie des décisions du jury",
+            "url": "formsemestre_recapcomplet?modejury=1&amp;hidemodules=1&amp;hidebac=1&amp;pref_override=0&amp;formsemestre_id="
+            + formsemestre_id,
+            "enabled": context._can_validate_sem(REQUEST, formsemestre_id),
+        },
+        {
+            "title": "Editer les PV et archiver les résultats",
+            "url": "formsemestre_archive?formsemestre_id=" + formsemestre_id,
+            "enabled": context._can_edit_pv(REQUEST, formsemestre_id),
+        },
+        {
+            "title": "Documents archivés",
+            "url": "formsemestre_list_archives?formsemestre_id=" + formsemestre_id,
+            "enabled": sco_archives.PVArchive.list_obj_archives(
+                context, formsemestre_id
+            ),
+        },
+    ]
+
+    menuStats = defMenuStats(context, formsemestre_id)
+    base_url = context.absolute_url() + "/"  # context must be Notes
+    H = [
+        # <table><tr><td>',
+        '<ul id="sco_menu">',
+        makeMenu("Semestre", menuSemestre, base_url=base_url),
+        makeMenu("Inscriptions", menuInscriptions, base_url=base_url),
+        makeMenu("Groupes", menuGroupes, base_url=base_url),
+        makeMenu("Notes", menuNotes, base_url=base_url),
+        makeMenu("Jury", menuJury, base_url=base_url),
+        makeMenu("Statistiques", menuStats, base_url=base_url),
+        formsemestre_custommenu_html(context, formsemestre_id, base_url=base_url),
+        "</ul>",
+        #'</td></tr></table>'
+    ]
+    return "\n".join(H)
+
+
+def retreive_formsemestre_from_request(context, REQUEST):
+    """Cherche si on a de quoi déduire le semestre affiché à partir des
+    arguments de la requête: 
+    formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
+    """
+    # Search formsemestre
+    group_ids = REQUEST.form.get("group_ids", [])
+    if REQUEST.form.has_key("formsemestre_id"):
+        formsemestre_id = REQUEST.form["formsemestre_id"]
+    elif REQUEST.form.has_key("moduleimpl_id"):
+        modimpl = context.do_moduleimpl_list(
+            moduleimpl_id=REQUEST.form["moduleimpl_id"]
+        )
+        if not modimpl:
+            return None  # suppressed ?
+        modimpl = modimpl[0]
+        formsemestre_id = modimpl["formsemestre_id"]
+    elif REQUEST.form.has_key("evaluation_id"):
+        E = context.do_evaluation_list({"evaluation_id": REQUEST.form["evaluation_id"]})
+        if not E:
+            return None  # evaluation suppressed ?
+        E = E[0]
+        modimpl = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+        formsemestre_id = modimpl["formsemestre_id"]
+    elif REQUEST.form.has_key("group_id"):
+        group = sco_groups.get_group(context, REQUEST.form["group_id"])
+        formsemestre_id = group["formsemestre_id"]
+    elif group_ids:
+        if group_ids:
+            if type(group_ids) == str:
+                group_id = group_ids
+            else:
+                # prend le semestre du 1er groupe de la liste:
+                group_id = group_ids[0]
+            group = sco_groups.get_group(context, group_id)
+        formsemestre_id = group["formsemestre_id"]
+    elif REQUEST.form.has_key("partition_id"):
+        partition = sco_groups.get_partition(context, REQUEST.form["partition_id"])
+        formsemestre_id = partition["formsemestre_id"]
+    else:
+        return None  # no current formsemestre
+
+    return formsemestre_id
+
+
+# Element HTML decrivant un semestre (barre de menu et infos)
+def formsemestre_page_title(context, REQUEST):
+    """Element HTML decrivant un semestre (barre de menu et infos)
+    Cherche dans REQUEST si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group)
+    """
+    try:
+        context = context.Notes
+    except:
+        pass
+    formsemestre_id = retreive_formsemestre_from_request(context, REQUEST)
+    #
+    if not formsemestre_id:
+        return ""
+    try:
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id).copy()
+    except:
+        log("can't find formsemestre_id %s" % formsemestre_id)
+        return ""
+
+    fill_formsemestre(context, sem, REQUEST=REQUEST)
+
+    H = [
+        """<div class="formsemestre_page_title">""",
+        """<div class="infos">
+<span class="semtitle"><a class="stdlink" title="%(session_id)s" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre)s</a><a title="%(etape_apo_str)s">%(num_sem)s</a>%(modalitestr)s</span><span class="dates"><a title="du %(date_debut)s au %(date_fin)s ">%(mois_debut)s - %(mois_fin)s</a></span><span class="resp"><a title="%(nomcomplet)s">%(resp)s</a></span><span class="nbinscrits"><a class="discretelink" href="%(notes_url)s/formsemestre_lists?formsemestre_id=%(formsemestre_id)s">%(nbinscrits)d inscrits</a></span><span class="lock">%(locklink)s</span><span class="eye">%(eyelink)s</span></div>"""
+        % sem,
+        formsemestre_status_menubar(context, sem, REQUEST),
+        """</div>""",
+    ]
+    return "\n".join(H)
+
+
+def fill_formsemestre(context, sem, REQUEST=None):
+    """Add some useful fields to help display formsemestres
+    """
+    # Notes URL
+    notes_url = context.absolute_url()
+    if "/Notes" not in notes_url:
+        notes_url += "/Notes"
+    sem["notes_url"] = notes_url
+    formsemestre_id = sem["formsemestre_id"]
+    if sem["etat"] != "1":
+        sem["locklink"] = (
+            """<a href="%s/formsemestre_change_lock?formsemestre_id=%s">%s</a>"""
+            % (
+                notes_url,
+                sem["formsemestre_id"],
+                icontag("lock_img", border="0", title="Semestre verrouillé"),
+            )
+        )
+    else:
+        sem["locklink"] = ""
+    if context.get_preference("bul_display_publication", formsemestre_id):
+        if sem["bul_hide_xml"] != "0":
+            eyeicon = icontag("hide_img", border="0", title="Bulletins NON publiés")
+        else:
+            eyeicon = icontag("eye_img", border="0", title="Bulletins publiés")
+        sem["eyelink"] = (
+            """<a href="%s/formsemestre_change_publication_bul?formsemestre_id=%s">%s</a>"""
+            % (notes_url, sem["formsemestre_id"], eyeicon)
+        )
+    else:
+        sem["eyelink"] = ""
+    F = context.Notes.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    sem["formation"] = F
+    parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
+    if sem["semestre_id"] != -1:
+        sem["num_sem"] = ", %s %s" % (parcours.SESSION_NAME, sem["semestre_id"])
+    else:
+        sem["num_sem"] = ""  # formation sans semestres
+    if sem["modalite"]:
+        sem["modalitestr"] = " en %s" % sem["modalite"]
+    else:
+        sem["modalitestr"] = ""
+
+    sem["etape_apo_str"] = "Code étape Apogée: " + (
+        sco_formsemestre.formsemestre_etape_apo_str(sem) or "Pas de code étape"
+    )
+
+    inscrits = context.Notes.do_formsemestre_inscription_list(
+        args={"formsemestre_id": formsemestre_id}
+    )
+    sem["nbinscrits"] = len(inscrits)
+    uresps = [
+        context.Users.user_info(responsable_id)
+        for responsable_id in sem["responsables"]
+    ]
+    sem["resp"] = ", ".join([u["prenomnom"] for u in uresps])
+    sem["nomcomplet"] = ", ".join([u["nomcomplet"] for u in uresps])
+
+
+# Description du semestre sous forme de table exportable
+def formsemestre_description_table(
+    context, formsemestre_id, REQUEST=None, with_evals=False
+):
+    """Description du semestre sous forme de table exportable
+    Liste des modules et de leurs coefficients
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > liste evaluations
+    use_ue_coefs = context.get_preference("use_ue_coefs", formsemestre_id)
+    F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
+    Mlist = context.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+
+    R = []
+    sum_coef = 0
+    sum_ects = 0
+    last_ue_id = None
+    for M in Mlist:
+        # Ligne UE avec ECTS:
+        ue = M["ue"]
+        if ue["ue_id"] != last_ue_id:
+            last_ue_id = ue["ue_id"]
+            if ue["ects"] is None:
+                ects_str = "-"
+            else:
+                sum_ects += ue["ects"]
+                ects_str = ue["ects"]
+            ue_info = {
+                "UE": ue["acronyme"],
+                "ects": ects_str,
+                "Module": ue["titre"],
+                "_css_row_class": "table_row_ue",
+            }
+            if use_ue_coefs:
+                ue_info["Coef."] = ue["coefficient"]
+                ue_info["Coef._class"] = "ue_coef"
+            R.append(ue_info)
+
+        ModInscrits = context.do_moduleimpl_inscription_list(
+            moduleimpl_id=M["moduleimpl_id"]
+        )
+        l = {
+            "UE": M["ue"]["acronyme"],
+            "Code": M["module"]["code"],
+            "Module": M["module"]["abbrev"] or M["module"]["titre"],
+            "_Module_class": "scotext",
+            "Inscrits": len(ModInscrits),
+            "Responsable": context.Users.user_info(M["responsable_id"])["nomprenom"],
+            "_Responsable_class": "scotext",
+            "Coef.": M["module"]["coefficient"],
+            # 'ECTS' : M['module']['ects'],
+            # Lien sur titre -> module
+            "_Module_target": "moduleimpl_status?moduleimpl_id=" + M["moduleimpl_id"],
+            "_Code_target": "moduleimpl_status?moduleimpl_id=" + M["moduleimpl_id"],
+        }
+        R.append(l)
+        if M["module"]["coefficient"]:
+            sum_coef += M["module"]["coefficient"]
+
+        if with_evals:
+            # Ajoute lignes pour evaluations
+            evals = nt.get_mod_evaluation_etat_list(M["moduleimpl_id"])
+            evals.reverse()  # ordre chronologique
+            # Ajoute etat:
+            for e in evals:
+                # Cosmetic: conversions pour affichage
+                if e["etat"]["evalcomplete"]:
+                    e["evalcomplete_str"] = "Oui"
+                    e["_evalcomplete_str_td_attrs"] = 'style="color: green;"'
+                else:
+                    e["evalcomplete_str"] = "Non"
+                    e["_evalcomplete_str_td_attrs"] = 'style="color: red;"'
+
+                if int(e["publish_incomplete"]):
+                    e["publish_incomplete_str"] = "Oui"
+                    e["_publish_incomplete_str_td_attrs"] = 'style="color: green;"'
+                else:
+                    e["publish_incomplete_str"] = "Non"
+                    e["_publish_incomplete_str_td_attrs"] = 'style="color: red;"'
+            R += evals
+
+    sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef}
+    R.append(sums)
+    columns_ids = ["UE", "Code", "Module", "Coef."]
+    if context.get_preference("bul_show_ects", formsemestre_id):
+        columns_ids += ["ects"]
+    columns_ids += ["Inscrits", "Responsable"]
+    if with_evals:
+        columns_ids += [
+            "jour",
+            "description",
+            "coefficient",
+            "evalcomplete_str",
+            "publish_incomplete_str",
+        ]
+
+    titles = {title: title for title in columns_ids}
+
+    titles["ects"] = "ECTS"
+    titles["jour"] = "Evaluation"
+    titles["description"] = ""
+    titles["coefficient"] = "Coef. éval."
+    titles["evalcomplete_str"] = "Complète"
+    titles["publish_incomplete_str"] = "Toujours Utilisée"
+    title = "%s %s" % (strcapitalize(parcours.SESSION_NAME), sem["titremois"])
+
+    return GenTable(
+        columns_ids=columns_ids,
+        rows=R,
+        titles=titles,
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        caption=title,
+        html_caption=title,
+        html_class="table_leftalign formsemestre_description",
+        base_url="%s?formsemestre_id=%s&amp;with_evals=%s"
+        % (REQUEST.URL0, formsemestre_id, with_evals),
+        page_title=title,
+        html_title=context.html_sem_header(
+            REQUEST, "Description du semestre", sem, with_page_header=False
+        ),
+        pdf_title=title,
+        preferences=context.get_preferences(formsemestre_id),
+    )
+
+
+def formsemestre_description(
+    context, formsemestre_id, format="html", with_evals=False, REQUEST=None
+):
+    """Description du semestre sous forme de table exportable
+    Liste des modules et de leurs coefficients
+    """
+    with_evals = int(with_evals)
+    tab = formsemestre_description_table(
+        context, formsemestre_id, REQUEST, with_evals=with_evals
+    )
+    tab.html_before_table = """<form name="f" method="get" action="%s">
+    <input type="hidden" name="formsemestre_id" value="%s"></input>
+    <input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()" """ % (
+        REQUEST.URL0,
+        formsemestre_id,
+    )
+    if with_evals:
+        tab.html_before_table += "checked"
+    tab.html_before_table += ">indiquer les évaluations</input></form>"
+
+    return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+
+def formsemestre_lists(context, formsemestre_id, REQUEST=None):
+    """Listes des étudiants
+    XXX (ancienne page, remplacée par groups_view, va être supprimée)
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    H = [
+        context.html_sem_header(REQUEST, "", sem),
+        context.make_listes_sem(sem, REQUEST),
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def html_expr_diagnostic(context, diagnostics):
+    """Affiche messages d'erreur des formules utilisateurs"""
+    H = []
+    H.append('<div class="ue_warning">Erreur dans des formules utilisateurs:<ul>')
+    last_id, last_msg = None, None
+    for diag in diagnostics:
+        if "moduleimpl_id" in diag:
+            mod = context.do_moduleimpl_withmodule_list(
+                moduleimpl_id=diag["moduleimpl_id"]
+            )[0]
+            H.append(
+                '<li>module <a href="moduleimpl_status?moduleimpl_id=%s">%s</a>: %s</li>'
+                % (
+                    diag["moduleimpl_id"],
+                    mod["module"]["abbrev"] or mod["module"]["code"] or "?",
+                    diag["msg"],
+                )
+            )
+        else:
+            if diag["ue_id"] != last_id or diag["msg"] != last_msg:
+                ue = context.do_ue_list({"ue_id": diag["ue_id"]})[0]
+                H.append(
+                    '<li>UE "%s": %s</li>'
+                    % (ue["acronyme"] or ue["titre"] or "?", diag["msg"])
+                )
+                last_id, last_msg = diag["ue_id"], diag["msg"]
+
+    H.append("</ul></div>")
+    return "".join(H)
+
+
+def formsemestre_status_head(
+    context, formsemestre_id=None, REQUEST=None, page_title=None
+):
+    """En-tête HTML des pages "semestre"
+    """
+    semlist = sco_formsemestre.do_formsemestre_list(
+        context, args={"formsemestre_id": formsemestre_id}
+    )
+    if not semlist:
+        raise ScoValueError("Session inexistante (elle a peut être été supprimée ?)")
+    sem = semlist[0]
+    F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
+
+    page_title = page_title or "Modules de "
+
+    H = [
+        context.html_sem_header(
+            REQUEST, page_title, sem, with_page_header=False, with_h2=False
+        ),
+        """<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,
+    ]
+    if sem["semestre_id"] >= 0:
+        H.append(", %s %s" % (parcours.SESSION_NAME, sem["semestre_id"]))
+    if sem["modalite"]:
+        H.append("&nbsp;en %(modalite)s" % sem)
+    if sem["etapes"]:
+        H.append(
+            "&nbsp;&nbsp;&nbsp;(étape <b><tt>%s</tt></b>)"
+            % (sem["etapes_apo_str"] or "-")
+        )
+    H.append("</td></tr>")
+
+    evals = sco_evaluations.do_evaluation_etat_in_sem(context, formsemestre_id)
+    H.append(
+        '<tr><td class="fichetitre2">Evaluations: </td><td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides'
+        % evals
+    )
+    if evals["last_modif"]:
+        H.append(
+            " <em>(dernière note saisie le %s)</em>"
+            % evals["last_modif"].strftime("%d/%m/%Y à %Hh%M")
+        )
+    H.append("</td></tr>")
+    if evals["attente"]:
+        H.append(
+            """<tr><td class="fichetitre2"></td><td class="redboldtext">
+Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur indicative. 
+</td></tr>"""
+        )
+    H.append("</table>")
+    if sem["bul_hide_xml"] != "0":
+        H.append(
+            '<p class="fontorange"><em>Bulletins non publiés sur le portail</em></p>'
+        )
+    if sem["semestre_id"] >= 0 and not sco_formsemestre.sem_une_annee(context, sem):
+        H.append(
+            '<p class="fontorange"><em>Attention: ce semestre couvre plusieurs années scolaires !</em></p>'
+        )
+    # elif context.get_preference('bul_display_publication', formsemestre_id):
+    #    H.append('<p><em>Bulletins publiés sur le portail</em></p>')
+
+    return "".join(H)
+
+
+def formsemestre_status(context, formsemestre_id=None, REQUEST=None):
+    """Tableau de bord semestre HTML"""
+    # porté du DTML
+    cnx = context.GetDBConnexion()
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    Mlist = context.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+    inscrits = context.do_formsemestre_inscription_list(
+        args={"formsemestre_id": formsemestre_id}
+    )
+    prev_ue_id = None
+
+    can_edit = sco_formsemestre_edit.can_edit_sem(
+        context, REQUEST, formsemestre_id, sem=sem
+    )
+
+    H = [
+        context.sco_header(REQUEST, page_title="Semestre %s" % sem["titreannee"]),
+        '<div class="formsemestre_status">',
+        formsemestre_status_head(
+            context, formsemestre_id=formsemestre_id, page_title="Tableau de bord"
+        ),
+        """<p><b style="font-size: 130%">Tableau de bord: </b><span class="help">cliquez sur un module pour saisir des notes</span></p>""",
+    ]
+    nt = context._getNotesCache().get_NotesTable(context, formsemestre_id)
+    if nt.expr_diagnostics:
+        H.append(html_expr_diagnostic(context, nt.expr_diagnostics))
+    H.append(
+        """
+<p>
+<table class="formsemestre_status">
+<tr>
+<th class="formsemestre_status">Code</th>
+<th class="formsemestre_status">Module</th>
+<th class="formsemestre_status">Inscrits</th>
+<th class="resp">Responsable</th>
+<th class="evals">Evaluations</th></tr>"""
+    )
+    for M in Mlist:
+        Mod = M["module"]
+        ModDescr = (
+            "Module "
+            + M["module"]["titre"]
+            + ", coef. "
+            + str(M["module"]["coefficient"])
+        )
+        ModEns = context.Users.user_info(M["responsable_id"])["nomcomplet"]
+        if M["ens"]:
+            ModEns += " (resp.), " + ", ".join(
+                [context.Users.user_info(e["ens_id"])["nomcomplet"] for e in M["ens"]]
+            )
+        ModInscrits = context.do_moduleimpl_inscription_list(
+            moduleimpl_id=M["moduleimpl_id"]
+        )
+        ue = M["ue"]
+        if prev_ue_id != ue["ue_id"]:
+            prev_ue_id = ue["ue_id"]
+            acronyme = ue["acronyme"]
+            titre = ue["titre"]
+            if context.get_preference("use_ue_coefs", formsemestre_id):
+                titre += " <b>(coef. %s)</b>" % (ue["coefficient"] or 0.0)
+            H.append(
+                """<tr class="formsemestre_status_ue"><td colspan="4">
+<span class="status_ue_acro">%s</span>
+<span class="status_ue_title">%s</span>
+</td><td>"""
+                % (acronyme, titre)
+            )
+
+            expr = sco_compute_moy.get_ue_expression(
+                formsemestre_id, ue["ue_id"], cnx, html_quote=True
+            )
+
+            if can_edit:
+                H.append(
+                    ' <a href="edit_ue_expr?formsemestre_id=%s&amp;ue_id=%s">'
+                    % (formsemestre_id, ue["ue_id"])
+                )
+            H.append(
+                icontag(
+                    "formula",
+                    title="Mode calcul moyenne d'UE",
+                    style="vertical-align:middle",
+                )
+            )
+            if can_edit:
+                H.append("</a>")
+            if expr:
+                H.append(
+                    """ <span class="formula" title="mode de calcul de la moyenne d'UE">%s</span>"""
+                    % expr
+                )
+
+            H.append("</td></tr>")
+
+        if M["ue"]["type"] != UE_STANDARD:
+            fontorange = " fontorange"  # style css additionnel
+        else:
+            fontorange = ""
+
+        etat = sco_evaluations.do_evaluation_etat_in_mod(
+            context, nt, M["moduleimpl_id"]
+        )
+        if (
+            etat["nb_evals_completes"] > 0
+            and etat["nb_evals_en_cours"] == 0
+            and etat["nb_evals_vides"] == 0
+        ):
+            H.append('<tr class="formsemestre_status_green%s">' % fontorange)
+        else:
+            H.append('<tr class="formsemestre_status%s">' % fontorange)
+
+        H.append(
+            '<td class="formsemestre_status_code"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="stdlink">%s</a></td>'
+            % (M["moduleimpl_id"], ModDescr, Mod["code"])
+        )
+        H.append(
+            '<td class="scotext"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="formsemestre_status_link">%s</a></td>'
+            % (M["moduleimpl_id"], ModDescr, Mod["abbrev"] or Mod["titre"])
+        )
+        H.append('<td class="formsemestre_status_inscrits">%s</td>' % len(ModInscrits))
+        H.append(
+            '<td class="resp scotext"><a class="discretelink" href="moduleimpl_status?moduleimpl_id=%s" title="%s">%s</a></td>'
+            % (
+                M["moduleimpl_id"],
+                ModEns,
+                context.Users.user_info(M["responsable_id"])["prenomnom"],
+            )
+        )
+
+        if Mod["module_type"] == MODULE_STANDARD:
+            H.append('<td class="evals">')
+            nb_evals = (
+                etat["nb_evals_completes"]
+                + etat["nb_evals_en_cours"]
+                + etat["nb_evals_vides"]
+            )
+            if nb_evals != 0:
+                H.append(
+                    '<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">%s prévues, %s ok</a>'
+                    % (M["moduleimpl_id"], nb_evals, etat["nb_evals_completes"])
+                )
+                if etat["nb_evals_en_cours"] > 0:
+                    H.append(
+                        ', <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il manque des notes">%s en cours</a></span>'
+                        % (M["moduleimpl_id"], etat["nb_evals_en_cours"])
+                    )
+                    if etat["attente"]:
+                        H.append(
+                            ' <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il y a des notes en attente">[en attente]</a></span>'
+                            % M["moduleimpl_id"]
+                        )
+        elif Mod["module_type"] == MODULE_MALUS:
+            nb_malus_notes = sum(
+                [
+                    e["etat"]["nb_notes"]
+                    for e in nt.get_mod_evaluation_etat_list(M["moduleimpl_id"])
+                ]
+            )
+            H.append(
+                """<td class="malus">
+            <a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">malus (%d notes)</a>
+            """
+                % (M["moduleimpl_id"], nb_malus_notes)
+            )
+        else:
+            raise ValueError("Invalid module_type")  # a bug
+
+        H.append("</td></tr>")
+    H.append("</table></p>")
+    if context.get_preference("use_ue_coefs", formsemestre_id):
+        H.append(
+            """
+        <p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
+        """
+        )
+    # --- LISTE DES ETUDIANTS
+    H += ['<div id="groupes">', context.make_listes_sem(sem, REQUEST), "</div>"]
+
+    return "".join(H) + context.sco_footer(REQUEST)
diff --git a/sco_formsemestre_validation.py b/sco_formsemestre_validation.py
new file mode 100644
index 0000000000000000000000000000000000000000..c8cba955e624392fd4543d0d7e8601a6dc2d36b1
--- /dev/null
+++ b/sco_formsemestre_validation.py
@@ -0,0 +1,1375 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Semestres: validation semestre et UE dans parcours
+"""
+import urllib, time, datetime
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from scolog import logdb
+from notes_table import *
+import notes_table
+from ZAbsences import getAbsSemEtud
+
+import sco_formsemestre
+import sco_formsemestre_edit
+import sco_formsemestre_status
+import sco_parcours_dut
+import sco_codes_parcours
+from sco_codes_parcours import *
+import sco_pvjury
+import sco_photos
+
+# ------------------------------------------------------------------------------------
+def formsemestre_validation_etud_form(
+    context,  # ZNotes instance
+    formsemestre_id=None,  # required
+    etudid=None,  # one of etudid or etud_index is required
+    etud_index=None,
+    check=0,  # opt: si true, propose juste une relecture du parcours
+    desturl=None,
+    sortcol=None,
+    readonly=True,
+    REQUEST=None,
+):
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_table_moyennes_triees, get_etud_decision_sem
+    T = nt.get_table_moyennes_triees()
+    if not etudid and not etud_index:
+        raise ValueError("formsemestre_validation_etud_form: missing argument etudid")
+    if etud_index:
+        etud_index = int(etud_index)
+        # cherche l'etudid correspondant
+        if etud_index < 0 or etud_index >= len(T):
+            raise ValueError(
+                "formsemestre_validation_etud_form: invalid etud_index value"
+            )
+        etudid = T[etud_index][-1]
+    else:
+        # cherche index pour liens navigation
+        etud_index = len(T) - 1
+        while etud_index >= 0 and T[etud_index][-1] != etudid:
+            etud_index -= 1
+        if etud_index < 0:
+            raise ValueError(
+                "formsemestre_validation_etud_form: can't retreive etud_index !"
+            )
+    # prev, next pour liens navigation
+    etud_index_next = etud_index + 1
+    if etud_index_next >= len(T):
+        etud_index_next = None
+    etud_index_prev = etud_index - 1
+    if etud_index_prev < 0:
+        etud_index_prev = None
+    if readonly:
+        check = True
+
+    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+    Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id)
+    if Se.sem["etat"] != "1":
+        raise ScoValueError("validation: semestre verrouille")
+
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Parcours %(nomprenom)s" % etud,
+            javascripts=["js/recap_parcours.js"],
+        )
+    ]
+
+    Footer = ["<p>"]
+    # Navigation suivant/precedent
+    if etud_index_prev != None:
+        etud_p = context.getEtudInfo(etudid=T[etud_index_prev][-1], filled=True)[0]
+        Footer.append(
+            '<span><a href="formsemestre_validation_etud_form?formsemestre_id=%s&amp;etud_index=%s">Etud. précédent (%s)</a></span>'
+            % (formsemestre_id, etud_index_prev, etud_p["nomprenom"])
+        )
+    if etud_index_next != None:
+        etud_n = context.getEtudInfo(etudid=T[etud_index_next][-1], filled=True)[0]
+        Footer.append(
+            '<span style="padding-left: 50px;"><a href="formsemestre_validation_etud_form?formsemestre_id=%s&amp;etud_index=%s">Etud. suivant (%s)</a></span>'
+            % (formsemestre_id, etud_index_next, etud_n["nomprenom"])
+        )
+    Footer.append("</p>")
+    Footer.append(context.sco_footer(REQUEST))
+
+    H.append('<table style="width: 100%"><tr><td>')
+    if not check:
+        H.append(
+            '<h2 class="formsemestre">%s: validation %s%s</h2>Parcours: %s'
+            % (
+                etud["nomprenom"],
+                Se.parcours.SESSION_NAME_A,
+                Se.parcours.SESSION_NAME,
+                Se.get_parcours_descr(),
+            )
+        )
+    else:
+        H.append(
+            '<h2 class="formsemestre">Parcours de %s</h2>%s'
+            % (etud["nomprenom"], Se.get_parcours_descr())
+        )
+
+    H.append(
+        '</td><td style="text-align: right;"><a href="%s/ficheEtud?etudid=%s">%s</a></td></tr></table>'
+        % (
+            context.ScoURL(),
+            etudid,
+            sco_photos.etud_photo_html(
+                context, etud, title="fiche de %s" % etud["nom"], REQUEST=REQUEST
+            ),
+        )
+    )
+
+    etud_etat = nt.get_etud_etat(etudid)
+    if etud_etat == "D":
+        H.append('<div class="ue_warning"><span>Etudiant démissionnaire</span></div>')
+    if etud_etat == DEF:
+        H.append('<div class="ue_warning"><span>Etudiant défaillant</span></div>')
+    if etud_etat != "I":
+        H.append(
+            tf_error_message(
+                """Impossible de statuer sur cet étudiant: il est démissionnaire ou défaillant (voir <a href="%s/ficheEtud?etudid=%s">sa fiche</a>)"""
+                % (context.ScoURL(), etudid)
+            )
+        )
+        return "\n".join(H + Footer)
+
+    H.append(
+        formsemestre_recap_parcours_table(
+            context, Se, etudid, with_links=(check and not readonly)
+        )
+    )
+    if check:
+        if not desturl:
+            desturl = (
+                "formsemestre_recapcomplet?modejury=1&amp;hidemodules=1&amp;hidebac=1&amp;pref_override=0&amp;formsemestre_id="
+                + formsemestre_id
+            )
+            if sortcol:
+                desturl += (
+                    "&amp;sortcol=" + sortcol
+                )  # pour refaire tri sorttable du tableau de notes
+            desturl += "#etudid%s" % etudid  # va a la bonne ligne
+        H.append('<ul><li><a href="%s">Continuer</a></li></ul>' % desturl)
+
+        return "\n".join(H + Footer)
+
+    decision_jury = Se.nt.get_etud_decision_sem(etudid)
+
+    # Bloque si note en attente
+    if nt.etud_has_notes_attente(etudid):
+        H.append(
+            tf_error_message(
+                """Impossible de statuer sur cet étudiant: il a des notes en attente dans des évaluations de ce semestre (voir <a href="formsemestre_status?formsemestre_id=%s">tableau de bord</a>)"""
+                % formsemestre_id
+            )
+        )
+        return "\n".join(H + Footer)
+
+    # Infos si pas de semestre précédent
+    if not Se.prev:
+        if Se.sem["semestre_id"] == 1:
+            H.append("<p>Premier semestre (pas de précédent)</p>")
+        else:
+            H.append("<p>Pas de semestre précédent !</p>")
+    else:
+        if not Se.prev_decision:
+            H.append(
+                tf_error_message(
+                    """Le jury n\'a pas statué sur le semestre précédent ! (<a href="formsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s">le faire maintenant</a>)"""
+                    % (Se.prev["formsemestre_id"], etudid)
+                )
+            )
+            if decision_jury:
+                H.append(
+                    '<a href="formsemestre_validation_suppress_etud?etudid=%s&amp;formsemestre_id=%s" class="stdlink">Supprimer décision existante</a>'
+                    % (etudid, formsemestre_id)
+                )
+            H.append(context.sco_footer(REQUEST))
+            return "\n".join(H)
+
+    # Infos sur decisions déjà saisies
+    if decision_jury:
+        if decision_jury["assidu"]:
+            ass = "assidu"
+        else:
+            ass = "non assidu"
+        H.append("<p>Décision existante du %(event_date)s: %(code)s" % decision_jury)
+        H.append(" (%s)" % ass)
+        auts = sco_parcours_dut.formsemestre_get_autorisation_inscription(
+            context, etudid, formsemestre_id
+        )
+        if auts:
+            H.append(". Autorisé%s à s'inscrire en " % etud["ne"])
+            alist = []
+            for aut in auts:
+                alist.append(str(aut["semestre_id"]))
+            H.append(", ".join(["S%s" % x for x in alist]) + ".")
+        H.append("</p>")
+
+    # Cas particulier pour ATJ: corriger precedent avant de continuer
+    if Se.prev_decision and Se.prev_decision["code"] == ATJ:
+        H.append(
+            """<div class="sfv_warning"><p>La décision du semestre précédent est en
+        <b>attente</b> à cause d\'un <b>problème d\'assiduité<b>.</p>
+        <p>Vous devez la corriger avant de continuer ce jury. Soit vous considérez que le
+        problème d'assiduité n'est pas réglé et choisissez de ne pas valider le semestre
+        précédent (échec), soit vous entrez une décision sans prendre en compte
+        l'assiduité.</p>
+        <form method="get" action="formsemestre_validation_etud_form">
+        <input type="submit" value="Statuer sur le semestre précédent"/>
+        <input type="hidden" name="formsemestre_id" value="%s"/>
+        <input type="hidden" name="etudid" value="%s"/>
+        <input type="hidden" name="desturl" value="formsemestre_validation_etud_form?etudid=%s&amp;formsemestre_id=%s"/>
+        """
+            % (Se.prev["formsemestre_id"], etudid, etudid, formsemestre_id)
+        )
+        if sortcol:
+            H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)
+        H.append("</form></div>")
+
+        H.append(context.sco_footer(REQUEST))
+        return "\n".join(H)
+
+    # Explication sur barres actuelles
+    H.append('<p class="sfv_explication">L\'étudiant ')
+    if Se.barre_moy_ok:
+        H.append("a la moyenne générale, ")
+    else:
+        H.append("<b>n'a pas</b> la moyenne générale, ")
+
+    H.append(Se.barres_ue_diag)  # eg 'les UEs sont au dessus des barres'
+
+    if (not Se.barre_moy_ok) and Se.can_compensate_with_prev:
+        H.append(", et ce semestre peut se <b>compenser</b> avec le précédent")
+    H.append(".</p>")
+
+    # Décisions possibles
+    rows_assidu = decisions_possible_rows(
+        Se, True, subtitle="Etudiant assidu:", trclass="sfv_ass"
+    )
+    rows_non_assidu = decisions_possible_rows(
+        Se, False, subtitle="Si problème d'assiduité:", trclass="sfv_pbass"
+    )
+    # s'il y a des decisions recommandees issues des regles:
+    if rows_assidu or rows_non_assidu:
+        H.append(
+            """<form method="get" action="formsemestre_validation_etud" id="formvalid" class="sfv_decisions">
+        <input type="hidden" name="etudid" value="%s"/>
+        <input type="hidden" name="formsemestre_id" value="%s"/>"""
+            % (etudid, formsemestre_id)
+        )
+        if desturl:
+            H.append('<input type="hidden" name="desturl" value="%s"/>' % desturl)
+        if sortcol:
+            H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)
+
+        H.append('<h3 class="sfv">Décisions <em>recommandées</em> :</h3>')
+        H.append("<table>")
+        H.append(rows_assidu)
+        if rows_non_assidu:
+            H.append("<tr><td>&nbsp;</td></tr>")  # spacer
+            H.append(rows_non_assidu)
+
+        H.append("</table>")
+        H.append(
+            '<p><br/></p><input type="submit" value="Valider ce choix" disabled="1" id="subut"/>'
+        )
+        H.append("</form>")
+
+    H.append(form_decision_manuelle(context, Se, formsemestre_id, etudid))
+
+    H.append(
+        """<div class="link_defaillance">Ou <a class="stdlink" href="formDef?etudid=%s&amp;formsemestre_id=%s">déclarer l'étudiant comme défaillant dans ce semestre</a></div>"""
+        % (etudid, formsemestre_id)
+    )
+
+    H.append('<p style="font-size: 50%;">Formation ')
+    if Se.sem["gestion_semestrielle"] == "1":
+        H.append("avec semestres décalés</p>")
+    else:
+        H.append("sans semestres décalés</p>")
+
+    return "".join(H + Footer)
+
+
+def formsemestre_validation_etud(
+    context,  # ZNotes instance
+    formsemestre_id=None,  # required
+    etudid=None,  # required
+    codechoice=None,  # required
+    desturl="",
+    sortcol=None,
+    REQUEST=None,
+):
+    """Enregistre validation"""
+    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+    Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id)
+    # retrouve la decision correspondant au code:
+    choices = Se.get_possible_choices(assiduite=True)
+    choices += Se.get_possible_choices(assiduite=False)
+    found = False
+    for choice in choices:
+        if choice.codechoice == codechoice:
+            found = True
+            break
+    if not found:
+        raise ValueError("code choix invalide ! (%s)" % codechoice)
+    #
+    Se.valide_decision(choice, REQUEST)  # enregistre
+    _redirect_valid_choice(
+        formsemestre_id, etudid, Se, choice, desturl, sortcol, REQUEST
+    )
+
+
+def formsemestre_validation_etud_manu(
+    context,  # ZNotes instance
+    formsemestre_id=None,  # required
+    etudid=None,  # required
+    code_etat="",
+    new_code_prev="",
+    devenir="",  # required (la decision manuelle)
+    assidu=False,
+    desturl="",
+    sortcol=None,
+    REQUEST=None,
+    redirect=True,
+):
+    """Enregistre validation"""
+    if assidu:
+        assidu = 1
+    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+    Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id)
+    if code_etat in Se.parcours.UNUSED_CODES:
+        raise ScoValueError("code decision invalide dans ce parcours")
+    # Si code ADC, extrait le semestre utilisé:
+    if code_etat[:3] == ADC:
+        formsemestre_id_utilise_pour_compenser = code_etat.split("_")[1]
+        if not formsemestre_id_utilise_pour_compenser:
+            formsemestre_id_utilise_pour_compenser = (
+                None  # compense avec semestre hors ScoDoc
+            )
+        code_etat = ADC
+    else:
+        formsemestre_id_utilise_pour_compenser = None
+
+    # Construit le choix correspondant:
+    choice = sco_parcours_dut.DecisionSem(
+        code_etat=code_etat,
+        new_code_prev=new_code_prev,
+        devenir=devenir,
+        assiduite=assidu,
+        formsemestre_id_utilise_pour_compenser=formsemestre_id_utilise_pour_compenser,
+    )
+    #
+    Se.valide_decision(choice, REQUEST)  # enregistre
+    if redirect:
+        _redirect_valid_choice(
+            formsemestre_id, etudid, Se, choice, desturl, sortcol, REQUEST
+        )
+
+
+def _redirect_valid_choice(
+    formsemestre_id, etudid, Se, choice, desturl, sortcol, REQUEST
+):
+    adr = (
+        "formsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s&amp;check=1"
+        % (formsemestre_id, etudid)
+    )
+    if sortcol:
+        adr += "&amp;sortcol=" + sortcol
+    if desturl:
+        desturl += "&amp;desturl=" + desturl
+    REQUEST.RESPONSE.redirect(adr)
+    # Si le precedent a été modifié, demande relecture du parcours.
+    # sinon  renvoie au listing general,
+
+
+#     if choice.new_code_prev:
+#         REQUEST.RESPONSE.redirect( 'formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&amp;check=1&amp;desturl=%s' % (formsemestre_id, etudid, desturl) )
+#     else:
+#         if not desturl:
+#             desturl = 'formsemestre_recapcomplet?modejury=1&amp;hidemodules=1&amp;formsemestre_id=' + formsemestre_id
+#         REQUEST.RESPONSE.redirect(desturl)
+
+
+def _dispcode(c):
+    if not c:
+        return ""
+    return c
+
+
+def decisions_possible_rows(Se, assiduite, subtitle="", trclass=""):
+    "Liste HTML des decisions possibles"
+    choices = Se.get_possible_choices(assiduite=assiduite)
+    if not choices:
+        return ""
+    TitlePrev = ""
+    if Se.prev:
+        if Se.prev["semestre_id"] >= 0:
+            TitlePrev = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.prev["semestre_id"])
+        else:
+            TitlePrev = "Prec."
+
+    if Se.sem["semestre_id"] >= 0:
+        TitleCur = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.sem["semestre_id"])
+    else:
+        TitleCur = Se.parcours.SESSION_NAME
+
+    H = [
+        '<tr class="%s titles"><th class="sfv_subtitle">%s</em></th>'
+        % (trclass, subtitle)
+    ]
+    if Se.prev:
+        H.append("<th>Code %s</th>" % TitlePrev)
+    H.append("<th>Code %s</th><th>Devenir</th></tr>" % TitleCur)
+    for ch in choices:
+        H.append(
+            """<tr class="%s"><td title="règle %s"><input type="radio" name="codechoice" value="%s" onClick="document.getElementById('subut').disabled=false;">"""
+            % (trclass, ch.rule_id, ch.codechoice)
+        )
+        H.append("%s </input></td>" % ch.explication)
+        if Se.prev:
+            H.append('<td class="centercell">%s</td>' % _dispcode(ch.new_code_prev))
+        H.append(
+            '<td class="centercell">%s</td><td>%s</td>'
+            % (_dispcode(ch.code_etat), Se.explique_devenir(ch.devenir))
+        )
+        H.append("</tr>")
+
+    return "\n".join(H)
+
+
+def formsemestre_recap_parcours_table(
+    context,
+    Se,
+    etudid,
+    with_links=False,
+    with_all_columns=True,
+    a_url="",
+    sem_info={},
+    show_details=False,
+):
+    """Tableau HTML recap parcours
+    Si with_links, ajoute liens pour modifier decisions (colonne de droite)   
+    sem_info = { formsemestre_id : txt } permet d'ajouter des informations associées à chaque semestre
+    with_all_columns: si faux, pas de colonne "assiduité".
+    """
+    H = []
+    linktmpl = '<span onclick="toggle_vis(this);" class="toggle_sem sem_%%s">%s</span>'
+    minuslink = linktmpl % icontag("minus_img", border="0", alt="-")
+    pluslink = linktmpl % icontag("plus_img", border="0", alt="+")
+    if show_details:
+        sd = " recap_show_details"
+        plusminus = minuslink
+    else:
+        sd = " recap_hide_details"
+        plusminus = pluslink
+    H.append('<table class="recap_parcours%s"><tr>' % sd)
+    H.append(
+        '<th><span onclick="toggle_all_sems(this);" title="Ouvrir/fermer tous les semestres">%s</span></th><th></th><th>Semestre</th>'
+        % icontag("plus18_img", width=18, height=18, border=0, title="", alt="+")
+    )
+    H.append("<th>Etat</th><th>Abs</th>")
+    # titres des UE
+    H.append("<th></th>" * Se.nb_max_ue)
+    #
+    if with_links:
+        H.append("<th></th>")
+    H.append("<th></th></tr>")
+    num_sem = 0
+
+    for sem in Se.get_semestres():
+        is_prev = Se.prev and (Se.prev["formsemestre_id"] == sem["formsemestre_id"])
+        is_cur = Se.formsemestre_id == sem["formsemestre_id"]
+        num_sem += 1
+
+        dpv = sco_pvjury.dict_pvjury(context, sem["formsemestre_id"], etudids=[etudid])
+        pv = dpv["decisions"][0]
+        decision_sem = pv["decision_sem"]
+        decisions_ue = pv["decisions_ue"]
+        if with_all_columns and decision_sem and decision_sem["assidu"] == 0:
+            ass = " (non ass.)"
+        else:
+            ass = ""
+
+        nt = context._getNotesCache().get_NotesTable(
+            context, sem["formsemestre_id"]
+        )  # > get_ues, get_etud_moy_gen, get_etud_ue_status
+        if is_cur:
+            type_sem = "*"  # now unused
+            class_sem = "sem_courant"
+        elif is_prev:
+            type_sem = "p"
+            class_sem = "sem_precedent"
+        else:
+            type_sem = ""
+            class_sem = "sem_autre"
+        if sem["formation_code"] != Se.formation["formation_code"]:
+            class_sem += " sem_autre_formation"
+        if sem["bul_bgcolor"]:
+            bgcolor = sem["bul_bgcolor"]
+        else:
+            bgcolor = "background-color: rgb(255,255,240)"
+        # 1ere ligne: titre sem, decision, acronymes UE
+        H.append('<tr class="%s rcp_l1 sem_%s">' % (class_sem, sem["formsemestre_id"]))
+        if is_cur:
+            pm = ""
+        elif is_prev:
+            pm = minuslink % sem["formsemestre_id"]
+        else:
+            pm = plusminus % sem["formsemestre_id"]
+
+        H.append(
+            '<td class="rcp_type_sem" style="background-color:%s;">%s%s</td>'
+            % (bgcolor, num_sem, pm)
+        )
+        H.append('<td class="datedebut">%(mois_debut)s</td>' % sem)
+        H.append(
+            '<td class="rcp_titre_sem"><a class="formsemestre_status_link" href="%sformsemestre_bulletinetud?formsemestre_id=%s&amp;etudid=%s" title="Bulletin de notes">%s</a></td>'
+            % (a_url, sem["formsemestre_id"], etudid, sem["titreannee"])
+        )
+        if decision_sem:
+            H.append('<td class="rcp_dec">%s</td>' % decision_sem["code"])
+        else:
+            H.append('<td colspan="%d"><em>en cours</em></td>')
+        H.append('<td class="rcp_nonass">%s</td>' % ass)  # abs
+        # acronymes UEs
+        ues = nt.get_ues(filter_sport=True, filter_non_inscrit=True, etudid=etudid)
+        for ue in ues:
+            H.append('<td class="ue_acro"><span>%s</span></td>' % ue["acronyme"])
+        if len(ues) < Se.nb_max_ue:
+            H.append('<td colspan="%d"></td>' % (Se.nb_max_ue - len(ues)))
+        # indique le semestre compensé par celui ci:
+        if decision_sem and decision_sem["compense_formsemestre_id"]:
+            csem = sco_formsemestre.get_formsemestre(
+                context, decision_sem["compense_formsemestre_id"]
+            )
+            H.append("<td><em>compense S%s</em></td>" % csem["semestre_id"])
+        else:
+            H.append("<td></td>")
+        if with_links:
+            H.append("<td></td>")
+        H.append("</tr>")
+        # 2eme ligne: notes
+        H.append('<tr class="%s rcp_l2 sem_%s">' % (class_sem, sem["formsemestre_id"]))
+        H.append(
+            '<td class="rcp_type_sem" style="background-color:%s;">&nbsp;</td>'
+            % (bgcolor)
+        )
+        if is_prev:
+            default_sem_info = '<span class="fontred">[sem. précédent]</span>'
+        else:
+            default_sem_info = ""
+        if sem["etat"] != "1":  # locked
+            lockicon = icontag("lock32_img", title="verrouillé", border="0")
+            default_sem_info += lockicon
+        if sem["formation_code"] != Se.formation["formation_code"]:
+            default_sem_info += "Autre formation: %s" % sem["formation_code"]
+        H.append(
+            '<td class="datefin">%s</td><td class="sem_info">%s</td>'
+            % (sem["mois_fin"], sem_info.get(sem["formsemestre_id"], default_sem_info))
+        )
+        # Moy Gen (sous le code decision)
+        H.append(
+            '<td class="rcp_moy">%s</td>'
+            % notes_table.fmt_note(nt.get_etud_moy_gen(etudid))
+        )
+        # Absences (nb d'abs non just. dans ce semestre)
+        AbsEtudSem = getAbsSemEtud(context, sem, etudid)
+        nbabs = AbsEtudSem.CountAbs()
+        nbabsjust = AbsEtudSem.CountAbsJust()
+        H.append('<td class="rcp_abs">%d</td>' % (nbabs - nbabsjust))
+
+        # UEs
+        for ue in ues:
+            if decisions_ue and decisions_ue.has_key(ue["ue_id"]):
+                code = decisions_ue[ue["ue_id"]]["code"]
+            else:
+                code = ""
+            ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
+            moy_ue = ue_status["moy"]
+            explanation_ue = []  # list of strings
+            if code == ADM:
+                class_ue = "ue_adm"
+            elif code == CMP:
+                class_ue = "ue_cmp"
+            else:
+                class_ue = "ue"
+            if ue_status["is_external"]:  # validation externe
+                explanation_ue.append("UE externe.")
+                # log('x'*12+' EXTERNAL %s' % notes_table.fmt_note(moy_ue)) XXXXXXX
+                # log('UE=%s' % pprint.pformat(ue))
+                # log('explanation_ue=%s\n'%explanation_ue)
+            if ue_status["is_capitalized"]:
+                class_ue += " ue_capitalized"
+                explanation_ue.append(
+                    "Capitalisée le %s." % (ue_status["event_date"] or "?")
+                )
+                # log('x'*12+' CAPITALIZED %s' % notes_table.fmt_note(moy_ue))
+                # log('UE=%s' % pprint.pformat(ue))
+                # log('UE_STATUS=%s'  % pprint.pformat(ue_status)) XXXXXX
+                # log('')
+
+            H.append(
+                '<td class="%s" title="%s">%s</td>'
+                % (class_ue, " ".join(explanation_ue), notes_table.fmt_note(moy_ue))
+            )
+        if len(ues) < Se.nb_max_ue:
+            H.append('<td colspan="%d"></td>' % (Se.nb_max_ue - len(ues)))
+
+        H.append("<td></td>")
+        if with_links:
+            H.append(
+                '<td><a href="%sformsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s">modifier</a></td>'
+                % (a_url, sem["formsemestre_id"], etudid)
+            )
+
+        H.append("</tr>")
+        # 3eme ligne: ECTS
+        if (
+            context.get_preference("bul_show_ects", sem["formsemestre_id"])
+            or nt.parcours.ECTS_ONLY
+        ):
+            etud_moy_infos = nt.get_etud_moy_infos(etudid)
+            H.append(
+                '<tr class="%s rcp_l2 sem_%s">' % (class_sem, sem["formsemestre_id"])
+            )
+            H.append(
+                '<td class="rcp_type_sem" style="background-color:%s;">&nbsp;</td><td></td>'
+                % (bgcolor)
+            )
+            # total ECTS (affiché sous la moyenne générale)
+            H.append(
+                '<td class="sem_ects_tit"><a title="crédit potentiels (dont nb de fondamentaux)">ECTS:</a></td><td class="sem_ects">%g <span class="ects_fond">%g</span></td>'
+                % (etud_moy_infos["ects_pot"], etud_moy_infos["ects_pot_fond"])
+            )
+            H.append('<td class="rcp_abs"></td>')
+            # ECTS validables dans chaque UE
+            for ue in ues:
+                ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
+                H.append(
+                    '<td class="ue">%g <span class="ects_fond">%g</span></td>'
+                    % (ue_status["ects_pot"], ue_status["ects_pot_fond"])
+                )
+            H.append("<td></td></tr>")
+
+    H.append("</table>")
+    return "\n".join(H)
+
+
+def form_decision_manuelle(
+    context, Se, formsemestre_id, etudid, desturl="", sortcol=None
+):
+    """Formulaire pour saisie décision manuelle
+    """
+    H = [
+        """
+    <script type="text/javascript">
+    function IsEmpty(aTextField) {
+    if ((aTextField.value.length==0) || (aTextField.value==null)) {
+        return true;
+     } else { return false; }
+    }
+    function check_sfv_form() {
+    if (IsEmpty(document.forms.formvalidmanu.code_etat)) {
+       alert('Choisir un code semestre !');
+       return false;
+    }
+    return true;
+    }
+    </script>
+    
+    <form method="get" action="formsemestre_validation_etud_manu" name="formvalidmanu" id="formvalidmanu" class="sfv_decisions sfv_decisions_manuelles" onsubmit="return check_sfv_form()">
+    <input type="hidden" name="etudid" value="%s"/>
+    <input type="hidden" name="formsemestre_id" value="%s"/>
+    """
+        % (etudid, formsemestre_id)
+    ]
+    if desturl:
+        H.append('<input type="hidden" name="desturl" value="%s"/>' % desturl)
+    if sortcol:
+        H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)
+
+    H.append(
+        '<h3 class="sfv">Décisions manuelles : <em>(vérifiez bien votre choix !)</em></h3><table>'
+    )
+
+    # Choix code semestre:
+    codes = sco_codes_parcours.CODES_EXPL.keys()
+    codes.sort()  # fortuitement, cet ordre convient bien !
+
+    H.append(
+        '<tr><td>Code semestre: </td><td><select name="code_etat"><option value="" selected>Choisir...</option>'
+    )
+    for cod in codes:
+        if cod in Se.parcours.UNUSED_CODES:
+            continue
+        if cod != ADC:
+            H.append(
+                '<option value="%s">%s (code %s)</option>'
+                % (cod, sco_codes_parcours.CODES_EXPL[cod], cod)
+            )
+        elif Se.sem["gestion_compensation"] == "1":
+            # traitement spécial pour ADC (compensation)
+            # ne propose que les semestres avec lesquels on peut compenser
+            # le code transmis est ADC_formsemestre_id
+            # on propose aussi une compensation sans utiliser de semestre, pour les cas ou le semestre
+            # précédent n'est pas géré dans ScoDoc (code ADC_)
+            # log(str(Se.sems))
+            for sem in Se.sems:
+                if sem["can_compensate"]:
+                    H.append(
+                        '<option value="%s_%s">Admis par compensation avec S%s (%s)</option>'
+                        % (
+                            cod,
+                            sem["formsemestre_id"],
+                            sem["semestre_id"],
+                            sem["date_debut"],
+                        )
+                    )
+            if Se.could_be_compensated():
+                H.append(
+                    '<option value="ADC_">Admis par compensation (avec un semestre hors ScoDoc)</option>'
+                )
+    H.append("</select></td></tr>")
+
+    # Choix code semestre precedent:
+    if Se.prev:
+        H.append(
+            '<tr><td>Code semestre précédent: </td><td><select name="new_code_prev"><option value="">Choisir une décision...</option>'
+        )
+        for cod in codes:
+            if cod == ADC:  # ne propose pas ce choix
+                continue
+            if Se.prev_decision and cod == Se.prev_decision["code"]:
+                sel = "selected"
+            else:
+                sel = ""
+            H.append(
+                '<option value="%s" %s>%s (code %s)</option>'
+                % (cod, sel, sco_codes_parcours.CODES_EXPL[cod], cod)
+            )
+        H.append("</select></td></tr>")
+
+    # Choix code devenir
+    codes = sco_codes_parcours.DEVENIR_EXPL.keys()
+    codes.sort()  # fortuitement, cet ordre convient aussi bien !
+
+    if Se.sem["semestre_id"] == -1:
+        allowed_codes = sco_codes_parcours.DEVENIRS_MONO
+    else:
+        allowed_codes = set(sco_codes_parcours.DEVENIRS_STD)
+        # semestres decales ?
+        if Se.sem["gestion_semestrielle"] == "1":
+            allowed_codes = allowed_codes.union(sco_codes_parcours.DEVENIRS_DEC)
+        # n'autorise les codes NEXT2 que si semestres décalés et s'il ne manque qu'un semestre avant le n+2
+        if Se.can_jump_to_next2():
+            allowed_codes = allowed_codes.union(sco_codes_parcours.DEVENIRS_NEXT2)
+
+    H.append(
+        '<tr><td>Devenir: </td><td><select name="devenir"><option value="" selected>Choisir...</option>'
+    )
+    for cod in codes:
+        if cod in allowed_codes:  # or Se.sem['gestion_semestrielle'] == '1'
+            H.append('<option value="%s">%s</option>' % (cod, Se.explique_devenir(cod)))
+    H.append("</select></td></tr>")
+
+    H.append(
+        '<tr><td><input type="checkbox" name="assidu" checked="checked">assidu</input></td></tr>'
+    )
+
+    H.append(
+        """</table>
+    <input type="submit" name="formvalidmanu_submit" value="Valider décision manuelle"/>
+    <span style="padding-left: 5em;"><a href="formsemestre_validation_suppress_etud?etudid=%s&amp;formsemestre_id=%s" class="stdlink">Supprimer décision existante</a></span>
+    </form>
+    """
+        % (etudid, formsemestre_id)
+    )
+    return "\n".join(H)
+
+
+# -----------
+def formsemestre_validation_auto(context, formsemestre_id, REQUEST):
+    "Formulaire saisie automatisee des decisions d'un semestre"
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    H = [
+        context.html_sem_header(
+            REQUEST, "Saisie automatique des décisions du semestre", sem
+        ),
+        """
+    <ul>
+    <li>Seuls les étudiants qui obtiennent le semestre seront affectés (code ADM, moyenne générale et
+    toutes les barres, semestre précédent validé);</li>
+    <li>le semestre précédent, s'il y en a un, doit avoir été validé;</li>
+    <li>les décisions du semestre précédent ne seront pas modifiées;</li>
+    <li>l'assiduité n'est <b>pas</b> prise en compte;</li>
+    <li>les étudiants avec des notes en attente sont ignorés.</li>
+    </ul>
+    <p>Il est donc vivement conseillé de relire soigneusement les décisions à l'issue
+    de cette procédure !</p>
+    <form action="do_formsemestre_validation_auto">
+    <input type="hidden" name="formsemestre_id" value="%s"/>
+    <input type="submit" value="Calculer automatiquement ces décisions"/>
+    <p><em>Le calcul prend quelques minutes, soyez patients !</em></p>
+    </form>
+    """
+        % formsemestre_id,
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def do_formsemestre_validation_auto(context, formsemestre_id, REQUEST):
+    "Saisie automatisee des decisions d'un semestre"
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    next_semestre_id = sem["semestre_id"] + 1
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etudids, get_etud_decision_sem,
+    etudids = nt.get_etudids()
+    nb_valid = 0
+    conflicts = []  # liste des etudiants avec decision differente déjà saisie
+    for etudid in etudids:
+        etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+        Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id)
+        ins = context.do_formsemestre_inscription_list(
+            {"etudid": etudid, "formsemestre_id": formsemestre_id}
+        )[0]
+
+        # Conditions pour validation automatique:
+        if ins["etat"] == "I" and (
+            (
+                (not Se.prev)
+                or (Se.prev_decision and Se.prev_decision["code"] in (ADM, ADC, ADJ))
+            )
+            and Se.barre_moy_ok
+            and Se.barres_ue_ok
+            and not nt.etud_has_notes_attente(etudid)
+        ):
+            # check: s'il existe une decision ou autorisation et qu'elles sont differentes,
+            # warning (et ne fait rien)
+            decision_sem = nt.get_etud_decision_sem(etudid)
+            ok = True
+            if decision_sem and decision_sem["code"] != ADM:
+                ok = False
+                conflicts.append(etud)
+            autorisations = sco_parcours_dut.formsemestre_get_autorisation_inscription(
+                context, etudid, formsemestre_id
+            )
+            if (
+                len(autorisations) != 0
+            ):  # accepte le cas ou il n'y a pas d'autorisation : BUG 23/6/7, A RETIRER ENSUITE
+                if (
+                    len(autorisations) != 1
+                    or autorisations[0]["semestre_id"] != next_semestre_id
+                ):
+                    if ok:
+                        conflicts.append(etud)
+                        ok = False
+
+            # ok, valide !
+            if ok:
+                formsemestre_validation_etud_manu(
+                    context,
+                    formsemestre_id,
+                    etudid,
+                    code_etat=ADM,
+                    devenir="NEXT",
+                    assidu=True,
+                    REQUEST=REQUEST,
+                    redirect=False,
+                )
+                nb_valid += 1
+    log(
+        "do_formsemestre_validation_auto: %d validations, %d conflicts"
+        % (nb_valid, len(conflicts))
+    )
+    H = [context.sco_header(REQUEST, page_title="Saisie automatique")]
+    H.append(
+        """<h2>Saisie automatique des décisions du semestre %s</h2>
+    <p>Opération effectuée.</p>
+    <p>%d étudiants validés (sur %s)</p>"""
+        % (sem["titreannee"], nb_valid, len(etudids))
+    )
+    if conflicts:
+        H.append(
+            """<p><b>Attention:</b> %d étudiants non modifiés car décisions différentes
+        déja saisies :<ul>"""
+            % len(conflicts)
+        )
+        for etud in conflicts:
+            H.append(
+                '<li><a href="formsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s&amp;check=1">%s</li>'
+                % (formsemestre_id, etud["etudid"], etud["nomprenom"])
+            )
+        H.append("</ul>")
+    H.append(
+        '<a href="formsemestre_recapcomplet?formsemestre_id=%s&amp;modejury=1&amp;hidemodules=1&amp;hidebac=1&amp;pref_override=0">continuer</a>'
+        % formsemestre_id
+    )
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def formsemestre_fix_validation_ues(context, formsemestre_id, REQUEST=None):
+    """
+    Suite à un bug (fix svn 502, 26 juin 2008), les UE sans notes ont parfois été validées
+    avec un code ADM (au lieu de AJ ou ADC, suivant le code semestre).
+
+    Cette fonction vérifie les codes d'UE et les modifie si nécessaire.
+
+    Si semestre validé: decision UE == CMP ou ADM
+    Sinon: si moy UE > barre et assiduité au semestre: ADM, sinon AJ
+
+
+    N'affecte que le semestre indiqué, pas les précédents.
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etudids, get_etud_decision_sem, get_ues, get_etud_decision_ues, get_etud_ue_status
+    etudids = nt.get_etudids()
+    modifs = []  # liste d'étudiants modifiés
+    cnx = context.GetDBConnexion(autocommit=False)
+    for etudid in etudids:
+        etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+        Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id)
+        ins = context.do_formsemestre_inscription_list(
+            {"etudid": etudid, "formsemestre_id": formsemestre_id}
+        )[0]
+        decision_sem = nt.get_etud_decision_sem(etudid)
+        if not decision_sem:
+            continue  # pas encore de decision semestre
+        valid_semestre = CODES_SEM_VALIDES.get(decision_sem["code"], False)
+        ue_ids = [x["ue_id"] for x in nt.get_ues(etudid=etudid, filter_sport=True)]
+        for ue_id in ue_ids:
+            existing_code = nt.get_etud_decision_ues(etudid)[ue_id]["code"]
+            if existing_code == None:
+                continue  # pas encore de decision UE
+            ue_status = nt.get_etud_ue_status(etudid, ue_id)
+            moy_ue = ue_status["moy"]
+            if valid_semestre:
+                if (
+                    type(moy_ue) == FloatType
+                    and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE
+                ):
+                    code_ue = ADM
+                else:
+                    code_ue = CMP
+            else:
+                if not decision_sem["assidu"]:
+                    code_ue = AJ
+                elif (
+                    type(moy_ue) == FloatType
+                    and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE
+                ):
+                    code_ue = ADM
+                else:
+                    code_ue = AJ
+
+            if code_ue != existing_code:
+                msg = "%s: %s: code %s changé en %s" % (
+                    etud["nomprenom"],
+                    ue_id,
+                    existing_code,
+                    code_ue,
+                )
+                modifs.append(msg)
+                log(msg)
+                sco_parcours_dut.do_formsemestre_validate_ue(
+                    cnx, nt, formsemestre_id, etudid, ue_id, code_ue
+                )
+    cnx.commit()
+    #
+    H = [
+        context.sco_header(REQUEST, page_title="Réparation des codes UE"),
+        sco_formsemestre_status.formsemestre_status_head(
+            context, REQUEST=REQUEST, formsemestre_id=formsemestre_id
+        ),
+    ]
+    if modifs:
+        H = H + [
+            "<h2>Modifications des codes UE</h2>",
+            "<ul><li>",
+            "</li><li>".join(modifs),
+            "</li></ul>",
+        ]
+        context._inval_cache(formsemestre_id=formsemestre_id)  # > modif decision UE
+    else:
+        H.append("<h2>Aucune modification: codes UE corrects ou inexistants</h2>")
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def formsemestre_validation_suppress_etud(context, formsemestre_id, etudid):
+    """Suppression des decisions de jury pour un etudiant.
+    """
+    log("formsemestre_validation_suppress_etud( %s, %s)" % (formsemestre_id, etudid))
+    cnx = context.GetDBConnexion(autocommit=False)
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
+    try:
+        # -- Validation du semestre et des UEs
+        cursor.execute(
+            """delete from scolar_formsemestre_validation
+        where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s""",
+            args,
+        )
+        # -- Autorisations d'inscription
+        cursor.execute(
+            """delete from scolar_autorisation_inscription
+        where etudid = %(etudid)s and origin_formsemestre_id=%(formsemestre_id)s""",
+            args,
+        )
+        cnx.commit()
+    except:
+        cnx.rollback()
+        raise
+
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    _invalidate_etud_formation_caches(
+        context, etudid, sem["formation_id"]
+    )  # > suppr. decision jury (peut affecter de plusieurs semestres utilisant UE capitalisée)
+
+
+def formsemestre_validate_previous_ue(context, formsemestre_id, etudid, REQUEST=None):
+    """Form. saisie UE validée hors ScoDoc 
+    (pour étudiants arrivant avec un UE antérieurement validée).
+    """
+    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    Fo = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Validation UE",
+            javascripts=["js/validate_previous_ue.js"],
+        ),
+        '<table style="width: 100%"><tr><td>',
+        """<h2 class="formsemestre">%s: validation d'une UE antérieure</h2>"""
+        % etud["nomprenom"],
+        (
+            '</td><td style="text-align: right;"><a href="%s/ficheEtud?etudid=%s">%s</a></td></tr></table>'
+            % (
+                context.ScoURL(),
+                etudid,
+                sco_photos.etud_photo_html(
+                    context, etud, title="fiche de %s" % etud["nom"], REQUEST=REQUEST
+                ),
+            )
+        ),
+        """<p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement, 
+    <em>dans un semestre hors ScoDoc</em>.</p>
+    <p><b>Les UE validées dans ScoDoc sont déjà
+    automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant 
+    suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré <b>sans 
+    ScoDoc</b> et qui <b>redouble</b> ce semestre (<em>ne pas utiliser pour les semestres précédents !</em>). 
+    </p>
+    <p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et 
+    l'attribution des ECTS.</p>""",
+        "<p>On ne peut prendre en compte ici que les UE du cursus <b>%(titre)s</b></p>"
+        % Fo,
+    ]
+
+    # Toutes les UE de cette formation sont présentées (même celles des autres semestres)
+    ues = context.do_ue_list({"formation_id": Fo["formation_id"]})
+    ue_names = ["Choisir..."] + ["%(acronyme)s %(titre)s" % ue for ue in ues]
+    ue_ids = [""] + [ue["ue_id"] for ue in ues]
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("etudid", {"input_type": "hidden"}),
+            ("formsemestre_id", {"input_type": "hidden"}),
+            (
+                "ue_id",
+                {
+                    "input_type": "menu",
+                    "title": "Unité d'Enseignement (UE)",
+                    "allow_null": False,
+                    "allowed_values": ue_ids,
+                    "labels": ue_names,
+                },
+            ),
+            (
+                "semestre_id",
+                {
+                    "input_type": "menu",
+                    "title": "Indice du semestre",
+                    "explanation": "Facultatif: indice du semestre dans la formation",
+                    "allow_null": True,
+                    "allowed_values": [""] + [str(x) for x in range(11)],
+                    "labels": ["-"] + range(11),
+                },
+            ),
+            (
+                "date",
+                {
+                    "input_type": "date",
+                    "size": 9,
+                    "explanation": "j/m/a",
+                    "default": time.strftime("%d/%m/%Y"),
+                },
+            ),
+            (
+                "moy_ue",
+                {
+                    "type": "float",
+                    "allow_null": False,
+                    "min_value": 0,
+                    "max_value": 20,
+                    "title": "Moyenne (/20) obtenue dans cette UE:",
+                },
+            ),
+        ),
+        cancelbutton="Annuler",
+        submitlabel="Enregistrer validation d'UE",
+    )
+    if tf[0] == 0:
+        X = """
+           <div id="ue_list_etud_validations"><!-- filled by get_etud_ue_cap_html --></div>
+           <div id="ue_list_code"><!-- filled by ue_sharing_code --></div>
+        """
+        warn, ue_multiples = check_formation_ues(context, Fo["formation_id"])
+        return "\n".join(H) + tf[1] + X + warn + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(
+            context.ScoURL()
+            + "/Notes/formsemestre_status?formsemestre_id="
+            + formsemestre_id
+        )
+    else:
+        if tf[2]["semestre_id"]:
+            semestre_id = int(tf[2]["semestre_id"])
+        else:
+            semestre_id = None
+        do_formsemestre_validate_previous_ue(
+            context,
+            formsemestre_id,
+            etudid,
+            tf[2]["ue_id"],
+            tf[2]["moy_ue"],
+            tf[2]["date"],
+            semestre_id=semestre_id,
+            REQUEST=REQUEST,
+        )
+        return REQUEST.RESPONSE.redirect(
+            context.ScoURL()
+            + "/Notes/formsemestre_bulletinetud?formsemestre_id=%s&amp;etudid=%s&amp;head_message=Validation%%20d'UE%%20enregistree"
+            % (formsemestre_id, etudid)
+        )
+
+
+def do_formsemestre_validate_previous_ue(
+    context,
+    formsemestre_id,
+    etudid,
+    ue_id,
+    moy_ue,
+    date,
+    code=ADM,
+    semestre_id=None,
+    ue_coefficient=None,
+    REQUEST=None,
+):
+    """Enregistre (ou modifie) validation d'UE (obtenue hors ScoDoc).
+    Si le coefficient est spécifié, modifie le coefficient de 
+    cette UE (utile seulement pour les semestres extérieurs).
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    cnx = context.GetDBConnexion(autocommit=False)
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etud_ue_status
+    if ue_coefficient != None:
+        sco_formsemestre_edit.do_formsemestre_uecoef_edit_or_create(
+            context, cnx, formsemestre_id, ue_id, ue_coefficient
+        )
+    else:
+        sco_formsemestre_edit.do_formsemestre_uecoef_delete(
+            context, cnx, formsemestre_id, ue_id
+        )
+    sco_parcours_dut.do_formsemestre_validate_ue(
+        cnx,
+        nt,
+        formsemestre_id,  # "importe" cette UE dans le semestre (new 3/2015)
+        etudid,
+        ue_id,
+        code,
+        moy_ue=moy_ue,
+        date=date,
+        semestre_id=semestre_id,
+        is_external=1,
+    )
+
+    logdb(
+        REQUEST,
+        cnx,
+        method="formsemestre_validate_previous_ue",
+        etudid=etudid,
+        msg="Validation UE %s" % ue_id,
+        commit=False,
+    )
+    _invalidate_etud_formation_caches(context, etudid, sem["formation_id"])
+    cnx.commit()
+
+
+def _invalidate_etud_formation_caches(context, etudid, formation_id):
+    "Invalide tous les semestres de cette formation où l'etudiant est inscrit..."
+    r = SimpleDictFetch(
+        context,
+        """SELECT sem.* 
+        FROM notes_formsemestre sem, notes_formsemestre_inscription i
+        WHERE sem.formation_id = %(formation_id)s
+        AND i.formsemestre_id = sem.formsemestre_id 
+        AND i.etudid = %(etudid)s
+        """,
+        {"etudid": etudid, "formation_id": formation_id},
+    )
+    for fsid in [s["formsemestre_id"] for s in r]:
+        context._inval_cache(
+            formsemestre_id=fsid
+        )  # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif)
+
+
+def get_etud_ue_cap_html(context, etudid, formsemestre_id, ue_id, REQUEST=None):
+    """Ramene bout de HTML pour pouvoir supprimer une validation de cette UE
+    """
+    valids = SimpleDictFetch(
+        context,
+        """SELECT SFV.* FROM scolar_formsemestre_validation SFV
+        WHERE ue_id=%(ue_id)s AND etudid=%(etudid)s""",
+        {"etudid": etudid, "ue_id": ue_id},
+    )
+    if not valids:
+        return ""
+    H = [
+        '<div class="existing_valids"><span>Validations existantes pour cette UE:</span><ul>'
+    ]
+    for valid in valids:
+        valid["event_date"] = DateISOtoDMY(valid["event_date"])
+        if valid["moy_ue"] != None:
+            valid["m"] = ", moyenne %(moy_ue)g/20" % valid
+        else:
+            valid["m"] = ""
+        if valid["formsemestre_id"]:
+            sem = sco_formsemestre.get_formsemestre(context, valid["formsemestre_id"])
+            valid["s"] = ", du semestre %s" % sem["titreannee"]
+        else:
+            valid["s"] = " enregistrée d'un parcours antérieur (hors ScoDoc)"
+        if valid["semestre_id"]:
+            valid["s"] += " (<b>S%d</b>)" % valid["semestre_id"]
+        valid["ds"] = formsemestre_id
+        H.append(
+            '<li>%(code)s%(m)s%(s)s, le %(event_date)s  <a class="stdlink" href="etud_ue_suppress_validation?etudid=%(etudid)s&amp;ue_id=%(ue_id)s&amp;formsemestre_id=%(ds)s" title="supprime cette validation">effacer</a></li>'
+            % valid
+        )
+    H.append("</ul></div>")
+    return "\n".join(H)
+
+
+def etud_ue_suppress_validation(context, etudid, formsemestre_id, ue_id, REQUEST=None):
+    """Suppress a validation (ue_id, etudid) and redirect to formsemestre"""
+    log("etud_ue_suppress_validation( %s, %s, %s)" % (etudid, formsemestre_id, ue_id))
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        "DELETE FROM scolar_formsemestre_validation WHERE etudid=%(etudid)s and ue_id=%(ue_id)s",
+        {"etudid": etudid, "ue_id": ue_id},
+    )
+
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    _invalidate_etud_formation_caches(context, etudid, sem["formation_id"])
+
+    return REQUEST.RESPONSE.redirect(
+        context.ScoURL()
+        + "/Notes/formsemestre_validate_previous_ue?etudid=%s&amp;formsemestre_id=%s"
+        % (etudid, formsemestre_id)
+    )
+
+
+def check_formation_ues(context, formation_id):
+    """Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id
+    Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de
+    définition du programme: cette fonction retourne un bout de HTML
+    à afficher pour prévenir l'utilisateur, ou '' si tout est ok.
+    """
+    ues = context.do_ue_list({"formation_id": formation_id})
+    ue_multiples = {}  # { ue_id : [ liste des formsemestre ] }
+    for ue in ues:
+        # formsemestres utilisant cette ue ?
+        sems = SimpleDictFetch(
+            context,
+            """SELECT DISTINCT sem.* 
+             FROM notes_formsemestre sem, notes_modules mod, notes_moduleimpl mi
+             WHERE sem.formation_id = %(formation_id)s
+             AND mod.module_id = mi.module_id
+             AND mi.formsemestre_id = sem.formsemestre_id
+             AND mod.ue_id = %(ue_id)s""",
+            {"ue_id": ue["ue_id"], "formation_id": formation_id},
+        )
+        semestre_ids = set([x["semestre_id"] for x in sems])
+        if (
+            len(semestre_ids) > 1
+        ):  # plusieurs semestres d'indices differents dans le cursus
+            ue_multiples[ue["ue_id"]] = sems
+
+    if not ue_multiples:
+        return "", {}
+    # Genere message HTML:
+    H = [
+        """<div class="ue_warning"><span>Attention:</span> les UE suivantes de cette formation 
+        sont utilisées dans des
+        semestres de rangs différents (eg S1 et S3). <br/>Cela peut engendrer des problèmes pour 
+        la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation: 
+        soit modifier le programme de la formation (définir des UE dans chaque semestre), 
+        soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une
+        UE extérieure.
+        <ul>
+        """
+    ]
+    for ue in ues:
+        if ue["ue_id"] in ue_multiples:
+            sems = [
+                sco_formsemestre.get_formsemestre(context, x["formsemestre_id"])
+                for x in ue_multiples[ue["ue_id"]]
+            ]
+            slist = ", ".join(
+                ["%(titreannee)s (<em>semestre %(semestre_id)s</em>)" % s for s in sems]
+            )
+            H.append("<li><b>%s</b> : %s</li>" % (ue["acronyme"], slist))
+    H.append("</ul></div>")
+
+    return "\n".join(H), ue_multiples
diff --git a/sco_formulas.py b/sco_formulas.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae0a7b1f6567388ec60aa696c9f2c7b956e3b8c2
--- /dev/null
+++ b/sco_formulas.py
@@ -0,0 +1,182 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Une classe "vecteur" pour les formules utilisateurs de calcul des moyennes
+"""
+
+from sets import Set
+import operator
+import traceback
+from types import FloatType, IntType, LongType, StringType
+
+from sco_utils import *
+from notes_log import log
+
+
+class NoteVector:
+    """Vecteur de notes (ou coefficients) utilisé pour les formules définies par l'utilisateur.
+    Les éléments sont accessibles soit par index v[i], soit par leur nom v['nom'] s'il en ont un.
+    Les éléments sont toujours numériques (float). Les valeurs non numériques ('NI', ...) sont
+    considérées comme nulles (0.0).
+    """
+
+    def __init__(self, *args, **kwargs):
+        if args:
+            self.v = map(float, args)  # cast to list of float
+        elif "v" in kwargs:
+            v = kwargs["v"]
+            if not isinstance(v, NoteVector):
+                # replace all non-numeric values by zeroes: (formulas should check cmask !)
+                for i in range(len(v)):
+                    try:
+                        v[i] = float(v[i])
+                    except:
+                        v[i] = 0.0
+            self.v = v
+        else:
+            self.v = []
+        self.name_idx = {}  # { name : index in vector }
+
+    def __len__(self):
+        return len(self.v)
+
+    def __getitem__(self, i):
+        try:
+            return self.v[i]
+        except:
+            if type(i) == StringType:
+                return self.v[self.name_idx[i]]
+            else:
+                raise IndexError("index %s out of range" % i)
+
+    def append(self, value, name=None):
+        """Append a value to the vector."""
+        try:
+            v = float(value)
+        except:
+            v = 0.0
+        self.v.append(v)
+        if name:
+            self.name_idx[name] = len(self.v) - 1
+
+    def __repr__(self):
+        return "NVector(%s, name_idx=%s)" % (str(self.v), self.name_idx)
+
+    def __add__(self, x):
+        return binary_op(self.v, x, operator.add)
+
+    __radd__ = __add__
+
+    def __sub__(self, x):
+        return binary_op(self.v, x, operator.sub)
+
+    def __rsub__(self, x):
+        return binary_op(x, self.v, operator.sub)
+
+    def __mul__(self, x):
+        return binary_op(self.v, x, operator.mul)
+
+    __rmul__ = __mul__
+
+    def __div__(self, x):
+        return binary_op(self.v, x, operator.div)
+
+    def __rdiv__(self, x):
+        return binary_op(x, self.v, operator.div)
+
+
+def isScalar(x):
+    return isinstance(x, FloatType) or isinstance(x, IntType) or isinstance(x, LongType)
+
+
+def binary_op(x, y, op):
+    if isScalar(x):
+        if isScalar(y):
+            x, y = [x], [y]
+        else:
+            x = [x] * len(y)
+    if isScalar(y):
+        y = [y] * len(x)
+
+    if len(x) != len(y):
+        raise ValueError("vectors sizes don't match")
+
+    return NoteVector(v=[op(a, b) for (a, b) in zip(x, y)])
+
+
+def dot(u, v):
+    """Dot product between 2 lists or vectors"""
+    return sum([x * y for (x, y) in zip(u, v)])
+
+
+def ternary_op(cond, a, b):
+    if cond:
+        return a
+    else:
+        return b
+
+
+def geometrical_mean(v, w=None):
+    """Geometrical mean of v, with optional weights w"""
+    if w is None:
+        return pow(reduce(operator.mul, v), 1.0 / len(v))
+    else:
+        if len(w) != len(v):
+            raise ValueError("vectors sizes don't match")
+        vw = [pow(x, y) for (x, y) in zip(v, w)]
+        return pow(reduce(operator.mul, vw), 1.0 / sum(w))
+
+
+# Les builtins autorisées dans les formules utilisateur:
+formula_builtins = {
+    "V": NoteVector,
+    "dot": dot,
+    "max": max,
+    "min": min,
+    "abs": abs,
+    "cmp": cmp,
+    "len": len,
+    "map": map,
+    "pow": pow,
+    "reduce": reduce,
+    "round": round,
+    "sum": sum,
+    "ifelse": ternary_op,
+    "geomean": geometrical_mean,
+}
+
+# v = NoteVector(1,2)
+# eval("max(4,5)", {'__builtins__': formula_builtins, {'x' : 1, 'v' : NoteVector(1,2) }, {})
+
+
+def eval_user_expression(context, expression, variables):
+    """Evalue l'expression (formule utilisateur) avec les variables (dict) données.
+    """
+    variables["__builtins__"] = formula_builtins
+    # log('Evaluating %s with %s' % (expression, variables))
+    # may raise exception if user expression is invalid
+    return eval(expression, variables, {})  # this should be safe
diff --git a/sco_groups.py b/sco_groups.py
new file mode 100644
index 0000000000000000000000000000000000000000..73228867bc5bfbc5682987fd1755a29cc088df79
--- /dev/null
+++ b/sco_groups.py
@@ -0,0 +1,1489 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""Gestion des groupes, nouvelle mouture (juin/nov 2009)
+
+TODO:
+
+* Groupes:
+
+ - changement de groupe d'un seul etudiant:
+     formChangeGroupe: pour l'instant supprimé, pas vraiment utile ?
+     doChangeGroupe
+
+Optimisation possible:
+ revoir do_evaluation_listeetuds_groups() pour extraire aussi les groupes (de chaque etudiant)
+ et eviter ainsi l'appel ulterieur à get_etud_groups() dans _make_table_notes
+
+"""
+
+import re, sets
+import operator
+
+# XML generation package (apt-get install jaxml)
+import jaxml
+import xml.dom.minidom
+
+from sco_utils import *
+from notesdb import *
+from notes_log import log
+from scolog import logdb
+from TrivialFormulator import TrivialFormulator, TF
+import sco_formsemestre
+import scolars
+import sco_parcours_dut
+
+
+def checkGroupName(
+    groupName,
+):  # XXX unused: now allow any string as a group or partition name
+    "Raises exception if not a valid group name"
+    if groupName and (
+        not re.match("^\w+$", groupName) or (simplesqlquote(groupName) != groupName)
+    ):
+        log("!!! invalid group name: " + groupName)
+        raise ValueError("invalid group name: " + groupName)
+
+
+partitionEditor = EditableTable(
+    "partition",
+    "partition_id",
+    (
+        "partition_id",
+        "formsemestre_id",
+        "partition_name",
+        "compute_ranks",
+        "numero",
+        "bul_show_rank",
+        "show_in_lists",
+    ),
+)
+
+groupEditor = EditableTable(
+    "group_descr", "group_id", ("group_id", "partition_id", "group_name")
+)
+
+group_list = groupEditor.list
+
+
+def get_group(context, group_id):
+    """Returns group object, with partition"""
+    r = SimpleDictFetch(
+        context,
+        "SELECT gd.*, p.* FROM group_descr gd, partition p WHERE gd.group_id=%(group_id)s AND p.partition_id = gd.partition_id",
+        {"group_id": group_id},
+    )
+    if not r:
+        raise ValueError("invalid group_id (%s)" % group_id)
+    return r[0]
+
+
+def group_delete(context, group, force=False):
+    """Delete a group.    
+    """
+    # if not group['group_name'] and not force:
+    #    raise ValueError('cannot suppress this group')
+    # remove memberships:
+    SimpleQuery(
+        context, "DELETE FROM group_membership WHERE group_id=%(group_id)s", group
+    )
+    # delete group:
+    SimpleQuery(context, "DELETE FROM group_descr WHERE group_id=%(group_id)s", group)
+
+
+def get_partition(context, partition_id):
+    r = SimpleDictFetch(
+        context,
+        "SELECT p.* FROM partition p WHERE p.partition_id = %(partition_id)s",
+        {"partition_id": partition_id},
+    )
+    if not r:
+        raise ValueError("invalid partition_id (%s)" % partition_id)
+    return r[0]
+
+
+def get_partitions_list(context, formsemestre_id, with_default=True):
+    """Liste des partitions pour ce semestre (list of dicts)"""
+    partitions = SimpleDictFetch(
+        context,
+        "SELECT * FROM partition WHERE formsemestre_id=%(formsemestre_id)s order by numero",
+        {"formsemestre_id": formsemestre_id},
+    )
+    # Move 'all' at end of list (for menus)
+    R = [p for p in partitions if p["partition_name"] != None]
+    if with_default:
+        R += [p for p in partitions if p["partition_name"] == None]
+    return R
+
+
+def get_default_partition(context, formsemestre_id):
+    """Get partition for 'all' students (this one always exists, with NULL name)"""
+    r = SimpleDictFetch(
+        context,
+        "SELECT * FROM partition WHERE formsemestre_id=%(formsemestre_id)s AND partition_name is NULL",
+        {"formsemestre_id": formsemestre_id},
+    )
+    if len(r) != 1:
+        raise ScoException(
+            "inconsistent partition: %d with NULL name for formsemestre_id=%s"
+            % (len(r), formsemestre_id)
+        )
+    return r[0]
+
+
+def get_formsemestre_groups(context, formsemestre_id, with_default=False):
+    """Returns  ( partitions, { partition_id : { etudid : group } } )
+    """
+    partitions = get_partitions_list(
+        context, formsemestre_id, with_default=with_default
+    )
+    partitions_etud_groups = {}  # { partition_id : { etudid : group } }
+    for partition in partitions:
+        pid = partition["partition_id"]
+        partitions_etud_groups[pid] = get_etud_groups_in_partition(context, pid)
+    return partitions, partitions_etud_groups
+
+
+def get_partition_groups(context, partition):
+    """List of groups in this partition (list of dicts).
+    Some groups may be empty."""
+    return SimpleDictFetch(
+        context,
+        "SELECT gd.*, p.* FROM group_descr gd, partition p WHERE gd.partition_id=%(partition_id)s AND gd.partition_id=p.partition_id ORDER BY group_name",
+        partition,
+    )
+
+
+def get_default_group(context, formsemestre_id, fix_if_missing=False, REQUEST=None):
+    """Returns group_id for default ('tous') group
+    """
+    r = SimpleDictFetch(
+        context,
+        "SELECT gd.group_id FROM group_descr gd, partition p WHERE p.formsemestre_id=%(formsemestre_id)s AND p.partition_name is NULL AND p.partition_id = gd.partition_id",
+        {"formsemestre_id": formsemestre_id},
+    )
+    if len(r) == 0 and fix_if_missing:
+        # No default group (problem during sem creation)
+        # Try to create it
+        log(
+            "*** Warning: get_default_group(formsemestre_id=%s): default group missing, recreating it"
+            % formsemestre_id
+        )
+        try:
+            partition_id = get_default_partition(context, formsemestre_id)[
+                "partition_id"
+            ]
+        except ScoException:
+            log("creating default partition for %s" % formsemestre_id)
+            partition_id = partition_create(
+                context, formsemestre_id, default=True, REQUEST=REQUEST
+            )
+        group_id = createGroup(context, partition_id, default=True, REQUEST=REQUEST)
+        return group_id
+    # debug check
+    if len(r) != 1:
+        raise ScoException("invalid group structure for %s" % formsemestre_id)
+    group_id = r[0]["group_id"]
+    return group_id
+
+
+def get_sem_groups(context, formsemestre_id):
+    """Returns groups for this sem (in all partitions)."""
+    return SimpleDictFetch(
+        context,
+        "SELECT gd.*, p.* FROM group_descr gd, partition p WHERE p.formsemestre_id=%(formsemestre_id)s AND p.partition_id = gd.partition_id",
+        {"formsemestre_id": formsemestre_id},
+    )
+
+
+def get_group_members(context, group_id, etat=None):
+    """Liste des etudiants d'un groupe.
+    Si etat, filtre selon l'état de l'inscription
+    Trié par nom_usuel (ou nom) puis prénom
+    """
+    req = "SELECT i.*, a.*, gm.*, ins.etat FROM identite i, adresse a, group_membership gm, group_descr gd, partition p, notes_formsemestre_inscription ins WHERE i.etudid = gm.etudid and a.etudid = i.etudid and ins.etudid = i.etudid and ins.formsemestre_id = p.formsemestre_id and p.partition_id = gd.partition_id and gd.group_id = gm.group_id and gm.group_id=%(group_id)s"
+    if etat is not None:
+        req += " and ins.etat = %(etat)s"
+
+    r = SimpleDictFetch(context, req, {"group_id": group_id, "etat": etat})
+
+    for etud in r:
+        scolars.format_etud_ident(etud)
+
+    r.sort(key=operator.itemgetter("nom_disp", "prenom"))  # tri selon nom_usuel ou nom
+
+    if CONFIG.ALLOW_NULL_PRENOM:
+        for x in r:
+            x["prenom"] = x["prenom"] or ""
+
+    return r
+
+
+# obsolete:  sco_groups_view.DisplayedGroupsInfos
+# def get_groups_members(context, group_ids, etat=None):
+#     """Liste les étudiants d'une liste de groupes
+#     chaque étudiant n'apparait qu'une seule fois dans le résultat.
+#     La liste est triée par nom / prenom
+#     """
+#     D = {} # { etudid : etud  }
+#     for group_id in group_ids:
+#         members = get_group_members(context, group_id, etat=etat)
+#         for m in members:
+#             D[m['etudid']] = m
+#     r = D.values()
+#     r.sort(key=operator.itemgetter('nom_disp', 'prenom')) # tri selon nom_usuel ou nom
+
+#     return r
+
+
+def get_group_infos(context, group_id, etat=None):  # was _getlisteetud
+    """legacy code: used by group_list and trombino
+    """
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    group = get_group(context, group_id)
+    sem = sco_formsemestre.get_formsemestre(context, group["formsemestre_id"])
+
+    members = get_group_members(context, group_id, etat=etat)
+    # add human readable description of state:
+    nbdem = 0
+    for t in members:
+        if t["etat"] == "I":
+            t["etath"] = ""  # etudiant inscrit, ne l'indique pas dans la liste HTML
+        elif t["etat"] == "D":
+            events = scolars.scolar_events_list(
+                cnx,
+                args={
+                    "etudid": t["etudid"],
+                    "formsemestre_id": group["formsemestre_id"],
+                },
+            )
+            for event in events:
+                event_type = event["event_type"]
+                if event_type == "DEMISSION":
+                    t["date_dem"] = event["event_date"]
+                    break
+            if "date_dem" in t:
+                t["etath"] = "démission le %s" % t["date_dem"]
+            else:
+                t["etath"] = "(dem.)"
+            nbdem += 1
+        elif t["etat"] == DEF:
+            t["etath"] = "Défaillant"
+        else:
+            t["etath"] = t["etat"]
+    # Add membership for all partitions, 'partition_id' : group
+    for etud in members:  # long: comment eviter ces boucles ?
+        etud_add_group_infos(context, etud, sem)
+
+    if group["group_name"] != None:
+        group_tit = "%s %s" % (group["partition_name"], group["group_name"])
+    else:
+        group_tit = "tous"
+
+    return members, group, group_tit, sem, nbdem
+
+
+def get_group_other_partitions(context, group):
+    """Liste des partitions du même semestre que ce groupe,
+    sans celle qui contient ce groupe.
+    """
+    other_partitions = [
+        p
+        for p in get_partitions_list(context, group["formsemestre_id"])
+        if p["partition_id"] != group["partition_id"] and p["partition_name"]
+    ]
+    return other_partitions
+
+
+def get_etud_groups(context, etudid, sem, exclude_default=False):
+    """Infos sur groupes de l'etudiant dans ce semestre
+    [ group + partition_name ]
+    """
+    req = "SELECT p.*, g.* from group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s and gm.group_id = g.group_id and g.partition_id = p.partition_id and p.formsemestre_id = %(formsemestre_id)s"
+    if exclude_default:
+        req += " and p.partition_name is not NULL"
+    groups = SimpleDictFetch(
+        context,
+        req + " ORDER BY p.numero",
+        {"etudid": etudid, "formsemestre_id": sem["formsemestre_id"]},
+    )
+    return _sortgroups(groups)
+
+
+def get_etud_main_group(context, etudid, sem):
+    """Return main group (the first one) for etud, or default one if no groups"""
+    groups = get_etud_groups(context, etudid, sem, exclude_default=True)
+    if groups:
+        return groups[0]
+    else:
+        return get_group(context, get_default_group(context, sem["formsemestre_id"]))
+
+
+def formsemestre_get_main_partition(context, formsemestre_id):
+    """Return main partition (the first one) for sem, or default one if no groups
+    (rappel: default == tous, main == principale (groupes TD habituellement)
+    """
+    return get_partitions_list(context, formsemestre_id, with_default=True)[0]
+
+
+def formsemestre_get_etud_groupnames(context, formsemestre_id, attr="group_name"):
+    """Recupere les groupes de tous les etudiants d'un semestre
+    { etudid : { partition_id : group_name  }}  (attr=group_name or group_id)
+    """
+    infos = SimpleDictFetch(
+        context,
+        "select i.etudid, p.partition_id, gd.group_name, gd.group_id from notes_formsemestre_inscription i, partition p, group_descr gd, group_membership gm where i.formsemestre_id=%(formsemestre_id)s and i.formsemestre_id=p.formsemestre_id and p.partition_id=gd.partition_id and gm.etudid=i.etudid and gm.group_id = gd.group_id and p.partition_name is not NULL",
+        {"formsemestre_id": formsemestre_id},
+    )
+    R = {}
+    for info in infos:
+        if info["etudid"] in R:
+            R[info["etudid"]][info["partition_id"]] = info[attr]
+        else:
+            R[info["etudid"]] = {info["partition_id"]: info[attr]}
+    return R
+
+
+def etud_add_group_infos(context, etud, sem, sep=" "):
+    """Add informations on partitions and group memberships to etud (a dict with an etudid)
+    """
+    etud[
+        "partitions"
+    ] = collections.OrderedDict()  # partition_id : group + partition_name
+    if not sem:
+        etud["groupes"] = ""
+        return etud
+
+    infos = SimpleDictFetch(
+        context,
+        "SELECT p.partition_name, g.* from group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s and gm.group_id = g.group_id and g.partition_id = p.partition_id and p.formsemestre_id = %(formsemestre_id)s ORDER BY p.numero",
+        {"etudid": etud["etudid"], "formsemestre_id": sem["formsemestre_id"]},
+    )
+
+    for info in infos:
+        if info["partition_name"]:
+            etud["partitions"][info["partition_id"]] = info
+
+    # resume textuel des groupes:
+    etud["groupes"] = sep.join(
+        [g["group_name"] for g in infos if g["group_name"] != None]
+    )
+    etud["partitionsgroupes"] = sep.join(
+        [
+            g["partition_name"] + ":" + g["group_name"]
+            for g in infos
+            if g["group_name"] != None
+        ]
+    )
+
+    return etud
+
+
+def get_etud_groups_in_partition(context, partition_id):
+    """Returns { etudid : group }, with all students in this partition"""
+    infos = SimpleDictFetch(
+        context,
+        "SELECT gd.*, etudid from group_descr gd, group_membership gm where gd.partition_id = %(partition_id)s and gm.group_id = gd.group_id",
+        {"partition_id": partition_id},
+    )
+    R = {}
+    for i in infos:
+        R[i["etudid"]] = i
+    return R
+
+
+def formsemestre_partition_list(context, formsemestre_id, format="xml", REQUEST=None):
+    """Get partitions and groups in this semestre
+    Supported formats: xml, json
+    """
+    partitions = get_partitions_list(context, formsemestre_id, with_default=True)
+    # Ajoute les groupes
+    for p in partitions:
+        p["group"] = get_partition_groups(context, p)
+    return sendResult(REQUEST, partitions, name="partition", format=format)
+
+
+def XMLgetGroupsInPartition(context, partition_id, REQUEST=None):  # was XMLgetGroupesTD
+    """
+    Deprecated: use group_list
+    Liste des étudiants dans chaque groupe de cette partition.
+    <group partition_id="" partition_name="" group_id="" group_name="">
+    <etud etuid="" sexe="" nom="" prenom="" origin=""/>
+    </groupe>
+    """
+    t0 = time.time()
+    partition = get_partition(context, partition_id)
+    formsemestre_id = partition["formsemestre_id"]
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    groups = get_partition_groups(context, partition)
+    nt = context.Notes._getNotesCache().get_NotesTable(
+        context.Notes, formsemestre_id
+    )  # > inscrdict
+    etuds_set = set(nt.inscrdict)
+    # XML response:
+    REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+    doc = jaxml.XML_document(encoding=SCO_ENCODING)
+    doc._text('<ajax-response><response type="object" id="MyUpdater">')
+    doc._push()
+
+    for group in groups:
+        doc._push()
+        doc.group(
+            partition_id=partition_id,
+            partition_name=partition["partition_name"],
+            group_id=group["group_id"],
+            group_name=group["group_name"],
+        )
+        for e in get_group_members(context, group["group_id"]):
+            etud = context.getEtudInfo(etudid=e["etudid"], filled=1)[0]
+            doc._push()
+            doc.etud(
+                etudid=e["etudid"],
+                sexe=scolars.format_sexe(etud["sexe"]),
+                nom=scolars.format_nom(etud["nom"]),
+                prenom=scolars.format_prenom(etud["prenom"]),
+                origin=comp_origin(etud, sem),
+            )
+            if e["etudid"] in etuds_set:
+                etuds_set.remove(e["etudid"])  # etudiant vu dans un groupe
+            doc._pop()
+        doc._pop()
+
+    # Ajoute les etudiants inscrits au semestre mais dans aucun groupe de cette partition:
+    if etuds_set:
+        doc._push()
+        doc.group(
+            partition_id=partition_id,
+            partition_name=partition["partition_name"],
+            group_id="_none_",
+            group_name="",
+        )
+        for etudid in etuds_set:
+            etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+            doc._push()
+            doc.etud(
+                etudid=etud["etudid"],
+                sexe=scolars.format_sexe(etud["sexe"]),
+                nom=scolars.format_nom(etud["nom"]),
+                prenom=scolars.format_prenom(etud["prenom"]),
+                origin=comp_origin(etud, sem),
+            )
+            doc._pop()
+        doc._pop()
+    doc._pop()
+
+    doc._text("</response></ajax-response>")
+    log("XMLgetGroupsInPartition: %s seconds" % (time.time() - t0))
+    return repr(doc)
+
+
+def comp_origin(etud, cur_sem):
+    """breve description de l'origine de l'étudiant (sem. precedent)
+    (n'indique l'origine que si ce n'est pas le semestre precedent normal)
+    """
+    # cherche le semestre suivant le sem. courant dans la liste
+    cur_sem_idx = None
+    for i in range(len(etud["sems"])):
+        if etud["sems"][i]["formsemestre_id"] == cur_sem["formsemestre_id"]:
+            cur_sem_idx = i
+            break
+
+    if cur_sem_idx is None or (cur_sem_idx + 1) >= (len(etud["sems"]) - 1):
+        return ""  # on pourrait indiquer le bac mais en general on ne l'a pas en debut d'annee
+
+    prev_sem = etud["sems"][cur_sem_idx + 1]
+    if prev_sem["semestre_id"] != (cur_sem["semestre_id"] - 1):
+        return " (S%s)" % prev_sem["semestre_id"]
+    else:
+        return ""  # parcours normal, ne le signale pas
+
+
+def set_group(context, etudid, group_id):
+    """Inscrit l'étudiant au groupe.
+    Return True if ok, False si deja inscrit.
+    Warning: don't check if group_id exists (the caller should check).
+    """
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    args = {"etudid": etudid, "group_id": group_id}
+    # déjà inscrit ?
+    r = SimpleDictFetch(
+        context,
+        "SELECT * FROM group_membership gm WHERE etudid=%(etudid)s and group_id=%(group_id)s",
+        args,
+        cursor=cursor,
+    )
+    if len(r):
+        return False
+    # inscrit
+    SimpleQuery(
+        context,
+        "INSERT INTO group_membership (etudid, group_id) VALUES (%(etudid)s, %(group_id)s)",
+        args,
+        cursor=cursor,
+    )
+    return True
+
+
+def change_etud_group_in_partition(
+    context, etudid, group_id, partition=None, REQUEST=None
+):
+    """Inscrit etud au groupe de cette partition, et le desinscrit d'autres groupes de cette partition.
+    """
+    log("change_etud_group_in_partition: etudid=%s group_id=%s" % (etudid, group_id))
+
+    # 0- La partition
+    group = get_group(context, group_id)
+    if partition:
+        # verifie que le groupe est bien dans cette partition:
+        if group["partition_id"] != partition["partition_id"]:
+            raise ValueError(
+                "inconsistent group/partition (group_id=%s, partition_id=%s)"
+                % (group_id, partition["partition_id"])
+            )
+    else:
+        partition = get_partition(context, group["partition_id"])
+    # 1- Supprime membership dans cette partition
+    SimpleQuery(
+        context,
+        """DELETE FROM group_membership WHERE group_membership_id IN 
+        (SELECT gm.group_membership_id 
+         FROM group_membership gm, group_descr gd 
+         WHERE gm.etudid=%(etudid)s AND gm.group_id=gd.group_id AND gd.partition_id=%(partition_id)s)""",
+        {"etudid": etudid, "partition_id": partition["partition_id"]},
+    )
+
+    # 2- associe au nouveau groupe
+    set_group(context, etudid, group_id)
+
+    # 3- log
+    formsemestre_id = partition["formsemestre_id"]
+    if REQUEST:
+        cnx = context.GetDBConnexion()
+        logdb(
+            REQUEST,
+            cnx,
+            method="changeGroup",
+            etudid=etudid,
+            msg="formsemestre_id=%s,partition_name=%s, group_name=%s"
+            % (formsemestre_id, partition["partition_name"], group["group_name"]),
+        )
+        cnx.commit()
+    # 4- invalidate cache
+    context.Notes._inval_cache(formsemestre_id=formsemestre_id)  # > change etud group
+
+
+def setGroups(
+    context,
+    partition_id,
+    groupsLists="",  # members of each existing group
+    groupsToCreate="",  # name and members of new groups
+    groupsToDelete="",  # groups to delete
+    REQUEST=None,
+):
+    """Affect groups (Ajax request)
+    groupsLists: lignes de la forme "group_id;etudid;...\n"
+    groupsToCreate: lignes "group_name;etudid;...\n"
+    groupsToDelete: group_id;group_id;...
+    """
+    partition = get_partition(context, partition_id)
+    formsemestre_id = partition["formsemestre_id"]
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    log("***setGroups: partition_id=%s" % partition_id)
+    log("groupsLists=%s" % groupsLists)
+    log("groupsToCreate=%s" % groupsToCreate)
+    log("groupsToDelete=%s" % groupsToDelete)
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    if sem["etat"] != "1":
+        raise AccessDenied("Modification impossible: semestre verrouillé")
+
+    groupsToDelete = [g for g in groupsToDelete.split(";") if g]
+
+    etud_groups = formsemestre_get_etud_groupnames(
+        context, formsemestre_id, attr="group_id"
+    )
+    for line in groupsLists.split("\n"):  # for each group_id (one per line)
+        fs = line.split(";")
+        group_id = fs[0].strip()
+        if not group_id:
+            continue
+        group = get_group(context, group_id)
+        # Anciens membres du groupe:
+        old_members = get_group_members(context, group_id)
+        old_members_set = set([x["etudid"] for x in old_members])
+        # Place dans ce groupe les etudiants indiqués:
+        for etudid in fs[1:-1]:
+            if etudid in old_members_set:
+                old_members_set.remove(
+                    etudid
+                )  # a nouveau dans ce groupe, pas besoin de l'enlever
+            if (etudid not in etud_groups) or (
+                group_id != etud_groups[etudid].get(partition_id, "")
+            ):  # pas le meme groupe qu'actuel
+                change_etud_group_in_partition(
+                    context, etudid, group_id, partition, REQUEST=REQUEST
+                )
+        # Retire les anciens membres:
+        cnx = context.GetDBConnexion()
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        for etudid in old_members_set:
+            log("removing %s from group %s" % (etudid, group_id))
+            SimpleQuery(
+                context,
+                "DELETE FROM group_membership WHERE etudid=%(etudid)s and group_id=%(group_id)s",
+                {"etudid": etudid, "group_id": group_id},
+                cursor=cursor,
+            )
+            logdb(
+                REQUEST,
+                cnx,
+                method="removeFromGroup",
+                etudid=etudid,
+                msg="formsemestre_id=%s,partition_name=%s, group_name=%s"
+                % (formsemestre_id, partition["partition_name"], group["group_name"]),
+            )
+
+    # Supprime les groupes indiqués comme supprimés:
+    for group_id in groupsToDelete:
+        suppressGroup(context, group_id, partition_id=partition_id, REQUEST=REQUEST)
+
+    # Crée les nouveaux groupes
+    for line in groupsToCreate.split("\n"):  # for each group_name (one per line)
+        fs = line.split(";")
+        group_name = fs[0].strip()
+        if not group_name:
+            continue
+        # ajax arguments are encoded in utf-8:
+        group_name = unicode(group_name, "utf-8").encode(SCO_ENCODING)
+        group_id = createGroup(context, partition_id, group_name, REQUEST=REQUEST)
+        # Place dans ce groupe les etudiants indiqués:
+        for etudid in fs[1:-1]:
+            change_etud_group_in_partition(
+                context, etudid, group_id, partition, REQUEST=REQUEST
+            )
+
+    REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
+    return (
+        '<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
+    )
+
+
+def createGroup(context, partition_id, group_name="", default=False, REQUEST=None):
+    """Create a new group in this partition
+    (called from JS)
+    """
+    partition = get_partition(context, partition_id)
+    formsemestre_id = partition["formsemestre_id"]
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    #
+    if group_name:
+        group_name = group_name.strip()
+    if not group_name and not default:
+        raise ValueError("invalid group name: ()")
+    # checkGroupName(group_name)
+    if group_name in [
+        g["group_name"] for g in get_partition_groups(context, partition)
+    ]:
+        raise ValueError(
+            "group_name %s already exists in partition" % group_name
+        )  # XXX FIX: incorrect error handling (in AJAX)
+    cnx = context.GetDBConnexion()
+    group_id = groupEditor.create(
+        cnx, {"partition_id": partition_id, "group_name": group_name}
+    )
+    log("createGroup: created group_id=%s" % group_id)
+    #
+    return group_id
+
+
+def suppressGroup(context, group_id, partition_id=None, REQUEST=None):
+    """form suppression d'un groupe.
+    (ne desinscrit pas les etudiants, change juste leur
+    affectation aux groupes)
+    partition_id est optionnel et ne sert que pour verifier que le groupe
+    est bien dans cette partition.
+    """
+    group = get_group(context, group_id)
+    if partition_id:
+        if partition_id != group["partition_id"]:
+            raise ValueError("inconsistent partition/group")
+    else:
+        partition_id = group["partition_id"]
+    partition = get_partition(context, partition_id)
+    if not context.Notes.can_change_groups(REQUEST, partition["formsemestre_id"]):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    log(
+        "suppressGroup: group_id=%s group_name=%s partition_name=%s"
+        % (group_id, group["group_name"], partition["partition_name"])
+    )
+    group_delete(context, group)
+
+
+def partition_create(
+    context,
+    formsemestre_id,
+    partition_name="",
+    default=False,
+    numero=None,
+    REQUEST=None,
+    redirect=1,
+):
+    """Create a new partition"""
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    if partition_name:
+        partition_name = partition_name.strip()
+    if default:
+        partition_name = None
+    if not partition_name and not default:
+        raise ScoValueError("Nom de partition invalide (vide)")
+    redirect = int(redirect)
+    # checkGroupName(partition_name)
+    if partition_name in [
+        p["partition_name"] for p in get_partitions_list(context, formsemestre_id)
+    ]:
+        raise ScoValueError(
+            "Il existe déjà une partition %s dans ce semestre" % partition_name
+        )
+
+    cnx = context.GetDBConnexion()
+    partition_id = partitionEditor.create(
+        cnx, {"formsemestre_id": formsemestre_id, "partition_name": partition_name}
+    )
+    log("createPartition: created partition_id=%s" % partition_id)
+    #
+    if redirect:
+        return REQUEST.RESPONSE.redirect(
+            "editPartitionForm?formsemestre_id=" + formsemestre_id
+        )
+    else:
+        return partition_id
+
+
+def getArrowIconsTags(context, REQUEST):
+    """returns html tags for arrows"""
+    #
+    arrow_up = icontag("arrow_up", title="remonter")
+    arrow_down = icontag("arrow_down", title="descendre")
+    arrow_none = icontag("arrow_none", title="")
+
+    return arrow_up, arrow_down, arrow_none
+
+
+def editPartitionForm(context, formsemestre_id=None, REQUEST=None):
+    """Form to create/suppress partitions"""
+    # ad-hoc form
+    canedit = context.Notes.can_change_groups(REQUEST, formsemestre_id)
+    partitions = get_partitions_list(context, formsemestre_id)
+    arrow_up, arrow_down, arrow_none = getArrowIconsTags(context, REQUEST)
+    suppricon = icontag(
+        "delete_small_img", border="0", alt="supprimer", title="Supprimer"
+    )
+    #
+    H = [
+        context.sco_header(
+            REQUEST, page_title="Partitions...", javascripts=["js/editPartitionForm.js"]
+        ),
+        """<script type="text/javascript">
+          function checkname() {
+ var val = document.editpart.partition_name.value.replace(/^\s+/, "").replace(/\s+$/, "");
+ if (val.length > 0) {
+   document.editpart.ok.disabled = false;
+ } else {
+   document.editpart.ok.disabled = true;
+ }
+}
+          </script>
+          """,
+        """<h2>Partitions du semestre</h2>
+          <form name="editpart" id="editpart" method="POST" action="partition_create">
+          <div id="epmsg"></div>
+          <table><tr class="eptit"><th></th><th></th><th></th><th>Partition</th><th>Groupes</th><th></th><th></th><th></th></tr>
+    """,
+    ]
+    i = 0
+    for p in partitions:
+        if p["partition_name"] is not None:
+            H.append(
+                '<tr><td class="epnav"><a class="stdlink" href="partition_delete?partition_id=%s">%s</a>&nbsp;</td><td class="epnav">'
+                % (p["partition_id"], suppricon)
+            )
+            if i != 0:
+                H.append(
+                    '<a href="partition_move?partition_id=%s&amp;after=0">%s</a>'
+                    % (p["partition_id"], arrow_up)
+                )
+            H.append('</td><td class="epnav">')
+            if i < len(partitions) - 2:
+                H.append(
+                    '<a href="partition_move?partition_id=%s&amp;after=1">%s</a>'
+                    % (p["partition_id"], arrow_down)
+                )
+            i += 1
+            H.append("</td>")
+            pname = p["partition_name"] or ""
+            H.append("<td>%s</td>" % pname)
+            H.append("<td>")
+            lg = [
+                "%s (%d)"
+                % (
+                    group["group_name"],
+                    len(get_group_members(context, group["group_id"])),
+                )
+                for group in get_partition_groups(context, p)
+            ]
+            H.append(", ".join(lg))
+            H.append(
+                '</td><td><a class="stdlink" href="affectGroups?partition_id=%s">répartir</a></td>'
+                % p["partition_id"]
+            )
+            H.append(
+                '<td><a class="stdlink" href="partition_rename?partition_id=%s">renommer</a></td>'
+                % p["partition_id"]
+            )
+            # classement:
+            H.append('<td width="250px">')
+            if p["bul_show_rank"]:
+                checked = 'checked="1"'
+            else:
+                checked = ""
+            H.append(
+                '<div><input type="checkbox" class="rkbox" data-partition_id="%s" %s onchange="update_rk(this);"/>afficher rang sur bulletins</div>'
+                % (p["partition_id"], checked)
+            )
+            if p["show_in_lists"]:
+                checked = 'checked="1"'
+            else:
+                checked = ""
+            H.append(
+                '<div><input type="checkbox" class="rkbox" data-partition_id="%s" %s onchange="update_show_in_list(this);"/>afficher sur noms groupes</div>'
+                % (p["partition_id"], checked)
+            )
+            H.append("</td>")
+            #
+            H.append("</tr>")
+    H.append("</table>")
+    H.append('<div class="form_rename_partition">')
+    H.append(
+        '<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id
+    )
+    H.append('<input type="hidden" name="redirect" value="1"/>')
+    H.append(
+        '<input type="text" name="partition_name" size="12" onkeyup="checkname();"/>'
+    )
+    H.append('<input type="submit" name="ok" disabled="1" value="Nouvelle partition"/>')
+    H.append("</div></form>")
+    H.append(
+        """<div class="help">
+    <p>Les partitions sont des découpages de l'ensemble des étudiants. 
+    Par exemple, les "groupes de TD" sont une partition. 
+    On peut créer autant de partitions que nécessaire. 
+    </p>
+    <ul>
+    <li>Dans chaque partition, un nombre de groupes quelconque peuvent être créés (suivre le lien "répartir").
+    <li>On peut faire afficher le classement de l'étudiant dans son groupe d'une partition en cochant "afficher rang sur bulletins" (ainsi, on peut afficher le classement en groupes de TD mais pas en groupe de TP, si ce sont deux partitions).
+    </li>
+    <li>Décocher "afficher sur noms groupes" pour ne pas que cette partition apparaisse dans les noms de groupes
+    </li>    
+    </ul>
+    </div>
+    """
+    )
+    return "\n".join(H) + context.sco_footer(REQUEST)
+
+
+def partition_set_attr(context, partition_id, attr, value, REQUEST=None):
+    """Set partition attribute: bul_show_rank or show_in_lists"""
+    if attr not in {"bul_show_rank", "show_in_lists"}:
+        raise ValueError("invalid partition attribute: %s" % attr)
+
+    partition = get_partition(context, partition_id)
+    formsemestre_id = partition["formsemestre_id"]
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+
+    log("partition_set_attr(%s, %s, %s)" % (partition_id, attr, value))
+    value = int(value)
+
+    cnx = context.GetDBConnexion()
+    partition[attr] = value
+    partitionEditor.edit(cnx, partition)
+    # invalid bulletin cache
+    context.Notes._inval_cache(
+        pdfonly=True, formsemestre_id=partition["formsemestre_id"]
+    )
+    return "enregistré"
+
+
+def partition_delete(
+    context, partition_id, REQUEST=None, force=False, redirect=1, dialog_confirmed=False
+):
+    """Suppress a partition (and all groups within).
+    default partition cannot be suppressed (unless force)"""
+    partition = get_partition(context, partition_id)
+    formsemestre_id = partition["formsemestre_id"]
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+
+    if not partition["partition_name"] and not force:
+        raise ValueError("cannot suppress this partition")
+    redirect = int(redirect)
+    cnx = context.GetDBConnexion()
+    groups = get_partition_groups(context, partition)
+
+    if not dialog_confirmed:
+        if groups:
+            grnames = "(" + ", ".join([g["group_name"] or "" for g in groups]) + ")"
+        else:
+            grnames = ""
+        return context.confirmDialog(
+            """<h2>Supprimer la partition "%s" ?</h2>
+                <p>Les groupes %s de cette partition seront supprimés</p>
+                """
+            % (partition["partition_name"], grnames),
+            dest_url="",
+            REQUEST=REQUEST,
+            cancel_url="editPartitionForm?formsemestre_id=%s" % formsemestre_id,
+            parameters={"redirect": redirect, "partition_id": partition_id},
+        )
+
+    log("partition_delete: partition_id=%s" % partition_id)
+    # 1- groups
+    for group in groups:
+        group_delete(context, group, force=force)
+    # 2- partition
+    partitionEditor.delete(cnx, partition_id)
+
+    # redirect to partition edit page:
+    if redirect:
+        return REQUEST.RESPONSE.redirect(
+            "editPartitionForm?formsemestre_id=" + formsemestre_id
+        )
+
+
+def partition_move(context, partition_id, after=0, REQUEST=None, redirect=1):
+    """Move before/after previous one (decrement/increment numero)"""
+    partition = get_partition(context, partition_id)
+    formsemestre_id = partition["formsemestre_id"]
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    #
+    redirect = int(redirect)
+    after = int(after)  # 0: deplace avant, 1 deplace apres
+    if after not in (0, 1):
+        raise ValueError('invalid value for "after"')
+    others = get_partitions_list(context, formsemestre_id)
+    if len(others) > 1:
+        pidx = [p["partition_id"] for p in others].index(partition_id)
+        log("partition_move: after=%s pidx=%s" % (after, pidx))
+        neigh = None  # partition to swap with
+        if after == 0 and pidx > 0:
+            neigh = others[pidx - 1]
+        elif after == 1 and pidx < len(others) - 1:
+            neigh = others[pidx + 1]
+        if neigh:  #
+            # swap numero between partition and its neighbor
+            log("moving partition %s" % partition_id)
+            cnx = context.GetDBConnexion()
+            partition["numero"], neigh["numero"] = neigh["numero"], partition["numero"]
+            partitionEditor.edit(cnx, partition)
+            partitionEditor.edit(cnx, neigh)
+
+    # redirect to partition edit page:
+    if redirect:
+        return REQUEST.RESPONSE.redirect(
+            "editPartitionForm?formsemestre_id=" + formsemestre_id
+        )
+
+
+def partition_rename(context, partition_id, REQUEST=None):
+    """Form to rename a partition"""
+    partition = get_partition(context, partition_id)
+    formsemestre_id = partition["formsemestre_id"]
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    H = ["<h2>Renommer une partition</h2>"]
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("partition_id", {"default": partition_id, "input_type": "hidden"}),
+            (
+                "partition_name",
+                {
+                    "title": "Nouveau nom",
+                    "default": partition["partition_name"],
+                    "allow_null": False,
+                    "size": 12,
+                },
+            ),
+        ),
+        submitlabel="Renommer",
+        cancelbutton="Annuler",
+    )
+    if tf[0] == 0:
+        return (
+            context.sco_header(REQUEST)
+            + "\n".join(H)
+            + "\n"
+            + tf[1]
+            + context.sco_footer(REQUEST)
+        )
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(
+            "editPartitionForm?formsemestre_id=" + formsemestre_id
+        )
+    else:
+        # form submission
+        return partition_set_name(
+            context, partition_id, tf[2]["partition_name"], REQUEST=REQUEST, redirect=1
+        )
+
+
+def partition_set_name(context, partition_id, partition_name, REQUEST=None, redirect=1):
+    """Set partition name"""
+    partition_name = partition_name.strip()
+    if not partition_name:
+        raise ValueError("partition name must be non empty")
+    partition = get_partition(context, partition_id)
+    if partition["partition_name"] is None:
+        raise ValueError("can't set a name to default partition")
+    formsemestre_id = partition["formsemestre_id"]
+
+    # check unicity
+    r = SimpleDictFetch(
+        context,
+        "SELECT p.* FROM partition p WHERE p.partition_name = %(partition_name)s AND formsemestre_id = %(formsemestre_id)s",
+        {"partition_name": partition_name, "formsemestre_id": formsemestre_id},
+    )
+    if len(r) > 1 or (len(r) == 1 and r[0]["partition_id"] != partition_id):
+        raise ScoValueError(
+            "Partition %s déjà existante dans ce semestre !" % partition_name
+        )
+
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    redirect = int(redirect)
+    cnx = context.GetDBConnexion()
+    partitionEditor.edit(
+        cnx, {"partition_id": partition_id, "partition_name": partition_name}
+    )
+
+    # redirect to partition edit page:
+    if redirect:
+        return REQUEST.RESPONSE.redirect(
+            "editPartitionForm?formsemestre_id=" + formsemestre_id
+        )
+
+
+def group_set_name(context, group_id, group_name, REQUEST=None, redirect=1):
+    """Set group name"""
+    if group_name:
+        group_name = group_name.strip()
+    if not group_name:
+        raise ScoValueError("nom de groupe vide !")
+    group = get_group(context, group_id)
+    if group["group_name"] is None:
+        raise ValueError("can't set a name to default group")
+    formsemestre_id = group["formsemestre_id"]
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    redirect = int(redirect)
+    cnx = context.GetDBConnexion()
+    groupEditor.edit(cnx, {"group_id": group_id, "group_name": group_name})
+
+    # redirect to partition edit page:
+    if redirect:
+        return REQUEST.RESPONSE.redirect(
+            "affectGroups?partition_id=" + group["partition_id"]
+        )
+
+
+def group_rename(context, group_id, REQUEST=None):
+    """Form to rename a group"""
+    group = get_group(context, group_id)
+    formsemestre_id = group["formsemestre_id"]
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    H = ["<h2>Renommer un groupe de %s</h2>" % group["partition_name"]]
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("group_id", {"default": group_id, "input_type": "hidden"}),
+            (
+                "group_name",
+                {
+                    "title": "Nouveau nom",
+                    "default": group["group_name"],
+                    "size": 12,
+                    "allow_null": False,
+                },
+            ),
+        ),
+        submitlabel="Renommer",
+        cancelbutton="Annuler",
+    )
+    if tf[0] == 0:
+        return (
+            context.sco_header(REQUEST)
+            + "\n".join(H)
+            + "\n"
+            + tf[1]
+            + context.sco_footer(REQUEST)
+        )
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(
+            "affectGroups?partition_id=" + group["partition_id"]
+        )
+    else:
+        # form submission
+        return group_set_name(
+            context, group_id, tf[2]["group_name"], REQUEST=REQUEST, redirect=1
+        )
+
+
+def groups_auto_repartition(context, partition_id=None, REQUEST=None):
+    """Reparti les etudiants dans des groupes dans une partition, en respectant le niveau
+    et la mixité.
+    """
+    partition = get_partition(context, partition_id)
+    formsemestre_id = partition["formsemestre_id"]
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+
+    descr = [
+        ("partition_id", {"input_type": "hidden"}),
+        (
+            "groupNames",
+            {
+                "size": 40,
+                "title": "Groupes à créer",
+                "allow_null": False,
+                "explanation": "noms des groupes à former, séparés par des virgules (les groupes existants seront effacés)",
+            },
+        ),
+    ]
+
+    H = [
+        context.sco_header(REQUEST, page_title="Répartition des groupes"),
+        "<h2>Répartition des groupes de %s</h2>" % partition["partition_name"],
+        "<p>Semestre %s</p>" % sem["titreannee"],
+        """<p class="help">Les groupes existants seront <b>effacés</b> et remplacés par
+          ceux créés ici. La répartition aléatoire tente d'uniformiser le niveau
+          des groupes (en utilisant la dernière moyenne générale disponible pour
+          chaque étudiant) et de maximiser la mixité de chaque groupe.</p>""",
+    ]
+
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        {},
+        cancelbutton="Annuler",
+        method="GET",
+        submitlabel="Créer et peupler les groupes",
+        name="tf",
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + "\n" + tf[1] + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(REQUEST.URL1)
+    else:
+        # form submission
+        log(
+            "groups_auto_repartition( partition_id=%s partition_name=%s"
+            % (partition_id, partition["partition_name"])
+        )
+        groupNames = tf[2]["groupNames"]
+        group_names = sorted(set([x.strip() for x in groupNames.split(",")]))
+        # Détruit les groupes existant de cette partition
+        for old_group in get_partition_groups(context, partition):
+            group_delete(context, old_group)
+        # Crée les nouveaux groupes
+        group_ids = []
+        for group_name in group_names:
+            # try:
+            #     checkGroupName(group_name)
+            # except:
+            #     H.append('<p class="warning">Nom de groupe invalide: %s</p>'%group_name)
+            #     return '\n'.join(H) + tf[1] + context.sco_footer(REQUEST)
+            group_ids.append(
+                createGroup(context, partition_id, group_name, REQUEST=REQUEST)
+            )
+        #
+        nt = context.Notes._getNotesCache().get_NotesTable(
+            context.Notes, formsemestre_id
+        )  # > identdict
+        identdict = nt.identdict
+        # build:  { sexe : liste etudids trie par niveau croissant }
+        sexes = sets.Set([x["sexe"] for x in identdict.values()])
+        listes = {}
+        for sexe in sexes:
+            listes[sexe] = [
+                (get_prev_moy(context.Notes, x["etudid"], formsemestre_id), x["etudid"])
+                for x in identdict.values()
+                if x["sexe"] == sexe
+            ]
+            listes[sexe].sort()
+            log("listes[%s] = %s" % (sexe, listes[sexe]))
+        # affect aux groupes:
+        n = len(identdict)
+        igroup = 0
+        nbgroups = len(group_ids)
+        while n > 0:
+            for sexe in sexes:
+                if len(listes[sexe]):
+                    n -= 1
+                    etudid = listes[sexe].pop()[1]
+                    group_id = group_ids[igroup]
+                    igroup = (igroup + 1) % nbgroups
+                    change_etud_group_in_partition(
+                        context, etudid, group_id, partition, REQUEST=REQUEST
+                    )
+                    log("%s in group %s" % (etudid, group_id))
+        # envoie sur page edition groupes
+        return REQUEST.RESPONSE.redirect("affectGroups?partition_id=%s" % partition_id)
+
+
+def get_prev_moy(context, etudid, formsemestre_id):
+    """Donne la derniere moyenne generale calculee pour cette étudiant,
+    ou 0 si on n'en trouve pas (nouvel inscrit,...).
+    """
+    import sco_parcours_dut
+
+    info = context.getEtudInfo(etudid=etudid, filled=True)
+    if not info:
+        raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
+    etud = info[0]
+    Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id)
+    if Se.prev:
+        nt = context._getNotesCache().get_NotesTable(
+            context, Se.prev["formsemestre_id"]
+        )  # > get_etud_moy_gen
+        return nt.get_etud_moy_gen(etudid)
+    else:
+        return 0.0
+
+
+def do_evaluation_listeetuds_groups(
+    context, evaluation_id, groups=None, getallstudents=False, include_dems=False
+):
+    """Donne la liste des etudids inscrits a cette evaluation dans les
+    groupes indiqués.
+    Si getallstudents==True, donne tous les etudiants inscrits a cette
+    evaluation.
+    Si include_dems, compte aussi les etudiants démissionnaires
+    (sinon, par défaut, seulement les 'I')
+    """
+    fromtables = [
+        "notes_moduleimpl_inscription Im",
+        "notes_formsemestre_inscription Isem",
+        "notes_moduleimpl M",
+        "notes_evaluation E",
+    ]
+    # construit condition sur les groupes
+    if not getallstudents:
+        if not groups:
+            return []  # no groups, so no students
+        rg = ["gm.group_id = '%(group_id)s'" % g for g in groups]
+        rq = "and Isem.etudid = gm.etudid and gd.partition_id = p.partition_id and p.formsemestre_id = Isem.formsemestre_id"
+        r = rq + " AND (" + " or ".join(rg) + " )"
+        fromtables += ["group_membership gm", "group_descr gd", "partition p"]
+    else:
+        r = ""
+
+    # requete complete
+    req = (
+        "SELECT distinct Im.etudid FROM "
+        + ", ".join(fromtables)
+        + " WHERE Isem.etudid=Im.etudid and Im.moduleimpl_id=M.moduleimpl_id and Isem.formsemestre_id=M.formsemestre_id and E.moduleimpl_id=M.moduleimpl_id and E.evaluation_id = %(evaluation_id)s"
+    )
+    if not include_dems:
+        req += " and Isem.etat='I'"
+    req += r
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(req, {"evaluation_id": evaluation_id})
+    # log('listeetuds_groups: getallstudents=%s  groups=%s' % (getallstudents,groups))
+    # log('req=%s' % (req % { 'evaluation_id' : "'"+evaluation_id+"'" }))
+    res = cursor.fetchall()
+    return [x[0] for x in res]
+
+
+def do_evaluation_listegroupes(context, evaluation_id, include_default=False):
+    """Donne la liste des groupes dans lesquels figurent des etudiants inscrits 
+    au module/semestre auquel appartient cette evaluation.
+    Si include_default, inclue aussi le groupe par defaut ('tous')
+    [ group ]
+    """
+    if include_default:
+        c = ""
+    else:
+        c = " AND p.partition_name is not NULL"
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        "SELECT DISTINCT gd.group_id FROM group_descr gd, group_membership gm, partition p, notes_moduleimpl m, notes_evaluation e WHERE gm.group_id = gd.group_id and gd.partition_id = p.partition_id and p.formsemestre_id = m.formsemestre_id and m.moduleimpl_id = e.moduleimpl_id and e.evaluation_id = %(evaluation_id)s"
+        + c,
+        {"evaluation_id": evaluation_id},
+    )
+    res = cursor.fetchall()
+    group_ids = [x[0] for x in res]
+    return listgroups(context, group_ids)
+
+
+def listgroups(context, group_ids):
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    groups = []
+    for group_id in group_ids:
+        cursor.execute(
+            "SELECT gd.*, p.* FROM group_descr gd, partition p WHERE p.partition_id = gd.partition_id AND gd.group_id = %(group_id)s",
+            {"group_id": group_id},
+        )
+        r = cursor.dictfetchall()
+        if r:
+            groups.append(r[0])
+    return _sortgroups(groups)
+
+
+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"]))
+
+    return R + o
+
+
+def listgroups_filename(groups):
+    """Build a filename representing groups"""
+    return "gr" + "+".join([g["group_name"] or "tous" for g in groups])
+
+
+def listgroups_abbrev(groups):
+    """Human readable abbreviation descring groups (eg "A / AB / B3")
+    Ne retient que les partitions avec show_in_lists
+    """
+    return " / ".join(
+        [g["group_name"] for g in groups if g["group_name"] and g["show_in_lists"]]
+    )
+
+
+# form_group_choice replaces formChoixGroupe
+def form_group_choice(
+    context,
+    formsemestre_id,
+    allow_none=True,  #  offre un choix vide dans chaque partition
+    select_default=True,  # Le groupe par defaut est mentionné (hidden).
+    display_sem_title=False,
+):
+    """Partie de formulaire pour le choix d'un ou plusieurs groupes.
+    Variable : group_ids
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    if display_sem_title:
+        sem_title = "%s: " % sem["titremois"]
+    else:
+        sem_title = ""
+    #
+    H = ["""<table>"""]
+    for p in get_partitions_list(context, formsemestre_id):
+        if p["partition_name"] is None:
+            if select_default:
+                H.append(
+                    '<input type="hidden" name="group_ids:list" value="%s"/>'
+                    % get_partition_groups(context, p)[0]["group_id"]
+                )
+        else:
+            H.append("<tr><td>Groupe de %(partition_name)s</td><td>" % p)
+            H.append('<select name="group_ids:list">')
+            if allow_none:
+                H.append('<option value="">aucun</option>')
+            for group in get_partition_groups(context, p):
+                H.append(
+                    '<option value="%s">%s %s</option>'
+                    % (group["group_id"], sem_title, group["group_name"])
+                )
+            H.append("</select></td></tr>")
+    H.append("""</table>""")
+    return "\n".join(H)
+
+
+def make_query_groups(group_ids):
+    if group_ids:
+        return "&amp;".join(["group_ids%3Alist=" + group_id for group_id in group_ids])
+    else:
+        return ""
+
+
+class GroupIdInferer:
+    """Sert à retrouver l'id d'un groupe dans un semestre donné
+    à partir de son nom.
+    Attention: il peut y avoir plusieurs groupes de même nom
+    dans des partitions différentes. Dans ce cas, prend le dernier listé.
+    On peut indiquer la partition en écrivant
+    partition_name:group_name
+    """
+
+    def __init__(self, context, formsemestre_id):
+        groups = get_sem_groups(context, formsemestre_id)
+        self.name2group_id = {}
+        self.partitionname2group_id = {}
+        for group in groups:
+            self.name2group_id[group["group_name"]] = group["group_id"]
+            self.partitionname2group_id[
+                (group["partition_name"], group["group_name"])
+            ] = group["group_id"]
+
+    def __getitem__(self, name):
+        """Get group_id from group_name, or None is nonexistent.
+        The group name can be prefixed by the partition's name, using
+        syntax partition_name:group_name
+        """
+        l = name.split(":", 1)
+        if len(l) > 1:
+            partition_name, group_name = l
+        else:
+            partition_name = None
+            group_name = name
+        if partition_name is None:
+            group_id = self.name2group_id.get(group_name, None)
+            if group_id is None and name[-2:] == ".0":
+                # si nom groupe numerique, excel ajoute parfois ".0" !
+                group_name = group_name[:-2]
+                group_id = self.name2group_id.get(group_name, None)
+        else:
+            group_id = self.partitionname2group_id.get(
+                (partition_name, group_name), None
+            )
+        return group_id
diff --git a/sco_groups_edit.py b/sco_groups_edit.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b866c87be4d9379a6fcd01ae9a8518347276500
--- /dev/null
+++ b/sco_groups_edit.py
@@ -0,0 +1,105 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Formulaires gestion des groupes
+"""
+
+import re
+
+from sco_utils import *
+from notesdb import *
+from notes_log import log
+import sco_formsemestre
+import sco_groups
+
+
+def affectGroups(context, partition_id, REQUEST=None):
+    """Formulaire affectation des etudiants aux groupes de la partition.
+    Permet aussi la creation et la suppression de groupes.
+    """
+    # Ported from DTML and adapted to new group management (nov 2009)
+    partition = sco_groups.get_partition(context, partition_id)
+    formsemestre_id = partition["formsemestre_id"]
+    if not context.Notes.can_change_groups(REQUEST, formsemestre_id):
+        raise AccessDenied("vous n'avez pas la permission d'effectuer cette opération")
+
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title="Affectation aux groupes",
+            javascripts=["js/groupmgr.js"],
+            cssstyles=["css/groups.css"],
+        ),
+        """<h2 class="formsemestre">Affectation aux groupes de %s</h2><form id="sp">"""
+        % partition["partition_name"],
+    ]
+
+    H += [
+        """</select></form>""",
+        """<p>Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne sont enregistrées que lorsque vous cliquez sur le bouton "<em>Enregistrer ces groupes</em>". Vous pouvez créer de nouveaux groupes. Pour <em>supprimer</em> un groupe, utiliser le lien "suppr." en haut à droite de sa boite. Vous pouvez aussi <a class="stdlink" href="groups_auto_repartition?partition_id=%(partition_id)s">répartir automatiquement les groupes</a>.
+</p>"""
+        % partition,
+        """<div id="gmsg" class="head_message"></div>""",
+        """<div id="ginfo"></div>""",
+        """<div id="savedinfo"></div>""",
+        """<form name="formGroup" id="formGroup" onSubmit="return false;">""",
+        """<input type="hidden" name="partition_id" value="%s"/>""" % partition_id,
+        """<input name="groupName" size="6"/>
+<input type="button" onClick="createGroup();" value="Créer groupe"/>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+<input type="button" onClick="submitGroups( target='gmsg' );" value="Enregistrer ces groupes" />
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+<input type="button" onClick="document.location = 'formsemestre_status?formsemestre_id=%s'" value="Annuler" />&nbsp;&nbsp;&nbsp;&nbsp;
+Editer groupes de
+<select name="other_partition_id" onchange="GotoAnother();">"""
+        % formsemestre_id,
+    ]
+    for p in sco_groups.get_partitions_list(
+        context, formsemestre_id, with_default=False
+    ):
+        H.append('<option value="%s"' % p["partition_id"])
+        if p["partition_id"] == partition_id:
+            H.append(" selected")
+        H.append(">%s</option>" % p["partition_name"])
+    H += [
+        """</select>
+</form>
+
+<div id="groups">
+</div>
+
+<div style="clear: left; margin-top: 15px;">
+<p class="help"></p>
+</div>
+
+</div>
+""",
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
diff --git a/sco_groups_view.py b/sco_groups_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..2b0d9c3f28338c0e294b1ef5ce58641a4061d616
--- /dev/null
+++ b/sco_groups_view.py
@@ -0,0 +1,928 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Affichage étudiants d'un ou plusieurs groupes
+   sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf)
+"""
+
+# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code)
+
+from sco_utils import *
+import html_sco_header
+from gen_tables import GenTable
+import scolars
+import ZAbsences
+import sco_excel
+import sco_formsemestre
+import sco_groups
+import sco_trombino
+import sco_portal_apogee
+import sco_parcours_dut
+import sco_report
+
+JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [
+    "js/etud_info.js",
+    "js/groups_view.js",
+]
+
+CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
+
+
+def groups_view(
+    context,
+    group_ids=[],
+    format="html",
+    REQUEST=None,
+    # Options pour listes:
+    with_codes=0,
+    etat=None,
+    with_paiement=0,  # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
+    with_archives=0,  # ajoute colonne avec noms fichiers archivés
+    with_annotations=0,
+    formsemestre_id=None,  # utilise si aucun groupe selectionné
+):
+    """Affichage des étudiants des groupes indiqués
+    group_ids: liste de group_id
+    format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf
+    """
+    # Informations sur les groupes à afficher:
+    groups_infos = DisplayedGroupsInfos(
+        context,
+        group_ids,
+        formsemestre_id=formsemestre_id,
+        etat=etat,
+        REQUEST=REQUEST,
+        select_all_when_unspecified=True,
+    )
+    # Formats spéciaux: download direct
+    if format != "html":
+        return groups_table(
+            context=context,
+            groups_infos=groups_infos,
+            format=format,
+            REQUEST=REQUEST,
+            with_codes=with_codes,
+            etat=etat,
+            with_paiement=with_paiement,
+            with_archives=with_archives,
+            with_annotations=with_annotations,
+        )
+
+    H = [
+        context.sco_header(
+            REQUEST, javascripts=JAVASCRIPTS, cssstyles=CSSSTYLES, init_qtip=True
+        )
+    ]
+    # Menu choix groupe
+    H.append("""<div id="group-tabs">""")
+    H.append(form_groups_choice(context, groups_infos, submit_on_change=True))
+    # Note: le formulaire est soumis a chaque modif des groupes
+    # on pourrait faire comme pour le form de saisie des notes. Il faudrait pour cela:
+    #  - charger tous les etudiants au debut, quels que soient les groupes selectionnés
+    #  - ajouter du JS pour modifier les liens (arguments group_ids) quand le menu change
+
+    # Tabs
+    # H.extend( ("""<span>toto</span><ul id="toto"><li>item 1</li><li>item 2</li></ul>""",) )
+    H.extend(
+        (
+            """<ul class="nav nav-tabs">
+    <li class="active"><a href="#tab-listes" data-toggle="tab">Listes</a></li>
+    <li><a href="#tab-photos" data-toggle="tab">Photos</a></li>
+    <li><a href="#tab-abs" data-toggle="tab">Absences et feuilles...</a></li>
+    </ul>
+    </div>
+    <!-- Tab panes -->
+    <div class="tab-content">
+    <div class="tab-pane active" id="tab-listes">
+    """,
+            groups_table(
+                context=context,
+                groups_infos=groups_infos,
+                format=format,
+                REQUEST=REQUEST,
+                with_codes=with_codes,
+                etat=etat,
+                with_paiement=with_paiement,
+                with_archives=with_archives,
+                with_annotations=with_annotations,
+            ),
+            "</div>",
+            """<div class="tab-pane" id="tab-photos">""",
+            tab_photos_html(context, groups_infos, etat=etat, REQUEST=REQUEST),
+            #'<p>hello</p>',
+            "</div>",
+            '<div class="tab-pane" id="tab-abs">',
+            tab_absences_html(context, groups_infos, etat=etat, REQUEST=REQUEST),
+            "</div>",
+        )
+    )
+
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def form_groups_choice(
+    context, groups_infos, with_selectall_butt=False, submit_on_change=False
+):
+    """form pour selection groupes
+    group_ids est la liste des groupes actuellement sélectionnés
+    et doit comporter au moins un élément, sauf si formsemestre_id est spécifié.
+    (utilisé pour retrouver le semestre et proposer la liste des autres groupes)
+
+    Si submit_on_change, ajoute une classe "submit_on_change" qui est utilisee en JS
+    """
+    default_group_id = sco_groups.get_default_group(
+        context, groups_infos.formsemestre_id
+    )
+
+    H = [
+        """<form id="group_selector" method="get">
+    <input type="hidden" name="formsemestre_id" id="formsemestre_id" value="%s"/>
+    <input type="hidden" name="default_group_id" id="default_group_id" value="%s"/>
+    Groupes: 
+    """
+        % (groups_infos.formsemestre_id, default_group_id)
+    ]
+
+    H.append(
+        menu_groups_choice(context, groups_infos, submit_on_change=submit_on_change)
+    )
+
+    if with_selectall_butt:
+        H.append(
+            """<input type="button" value="sélectionner tous" onmousedown="select_tous();"/>"""
+        )
+    H.append("</form>")
+
+    return "\n".join(H)
+
+
+def menu_groups_choice(context, groups_infos, submit_on_change=False):
+    """menu pour selection groupes
+    group_ids est la liste des groupes actuellement sélectionnés
+    et doit comporter au moins un élément, sauf si formsemestre_id est spécifié.
+    (utilisé pour retrouver le semestre et proposer la liste des autres groupes)
+    """
+    default_group_id = sco_groups.get_default_group(
+        context, groups_infos.formsemestre_id
+    )
+
+    if submit_on_change:
+        klass = "submit_on_change"
+    else:
+        klass = ""
+    H = [
+        """<select name="group_ids" id="group_ids_sel" class="multiselect %s" multiple="multiple">
+    """
+        % (klass,)
+    ]
+
+    n_members = len(sco_groups.get_group_members(context, default_group_id))
+    if default_group_id in groups_infos.group_ids:
+        selected = "selected"
+    else:
+        selected = ""
+    H.append(
+        '<option class="default_group" value="%s" %s>%s (%s)</option>'
+        % (default_group_id, selected, "Tous", n_members)
+    )
+
+    for partition in groups_infos.partitions:
+        H.append('<optgroup label="%s">' % partition["partition_name"])
+        # Les groupes dans cette partition:
+        for g in sco_groups.get_partition_groups(context, partition):
+            if g["group_id"] in groups_infos.group_ids:
+                selected = "selected"
+            else:
+                selected = ""
+            if g["group_name"]:
+                n_members = len(sco_groups.get_group_members(context, g["group_id"]))
+                H.append(
+                    '<option value="%s" %s>%s (%s)</option>'
+                    % (g["group_id"], selected, g["group_name"], n_members)
+                )
+        H.append("</optgroup>")
+    H.append("</select> ")
+    return "\n".join(H)
+
+
+def menu_group_choice(context, group_id=None, formsemestre_id=None):
+    """Un bête menu pour choisir un seul groupe
+    group_id est le groupe actuellement sélectionné.
+    Si aucun groupe selectionné, utilise formsemestre_id pour lister les groupes.
+    """
+    if group_id:
+        group = sco_groups.get_group(context, group_id)
+        formsemestre_id = group["formsemestre_id"]
+    elif not formsemestre_id:
+        raise ValueError("missing formsemestre_id")
+    H = [
+        """
+    <select id="group_selector_u" name="group_id" onchange="reload_selected_group();">
+    """
+    ]
+    if not group_id:
+        H.append('<option value="">choisir...</option>')
+    for partition in sco_groups.get_partitions_list(context, formsemestre_id):
+        if partition["partition_name"]:
+            H.append('<optgroup label="%s">' % partition["partition_name"])
+        groups = sco_groups.get_partition_groups(context, partition)
+        for group in groups:
+            if group["group_id"] == group_id:
+                selected = "selected"
+            else:
+                selected = ""
+            name = group["group_name"] or "Tous"
+            n_members = len(sco_groups.get_group_members(context, group["group_id"]))
+            H.append(
+                '<option value="%s" %s>%s (%s)</option>'
+                % (group["group_id"], selected, name, n_members)
+            )
+        if partition["partition_name"]:
+            H.append("</optgroup>")
+    H.append(
+        """</select>
+    <script>
+function reload_selected_group() {
+var url = $.url();
+var group_id = $("#group_selector_u").val();
+if (group_id) {
+  url.param()['group_id'] = group_id;
+  var query_string = $.param(url.param(), traditional=true );
+  window.location = url.attr('base') + url.attr('path') + '?' + query_string;
+}
+}
+    </script>
+    """
+    )
+    return "\n".join(H)
+
+
+class DisplayedGroupsInfos:
+    """Container with attributes describing groups to display in the page
+    .groups_query_args : 'group_ids=xxx&group_ids=yyy'
+    .base_url : url de la requete, avec les groupes, sans les autres paramètres
+    .formsemestre_id : semestre "principal" (en fait celui du 1er groupe de la liste)
+    .members
+    .groups_titles
+    """
+
+    def __init__(
+        self,
+        context,
+        group_ids=[],  # groupes specifies dans l'URL
+        formsemestre_id=None,
+        etat=None,
+        select_all_when_unspecified=False,
+        REQUEST=None,
+    ):
+        # log('DisplayedGroupsInfos %s' % group_ids)
+        if type(group_ids) == str:
+            if group_ids:
+                group_ids = [group_ids]  # cas ou un seul parametre, pas de liste
+            else:
+                group_ids = []
+
+        if not group_ids:  # appel sans groupe (eg page accueil)
+            if not formsemestre_id:
+                raise Exception("missing parameter")  # formsemestre_id or group_ids
+            if select_all_when_unspecified:
+                group_ids = [sco_groups.get_default_group(context, formsemestre_id)]
+            else:
+                # selectionne le premier groupe trouvé, s'il y en a un
+                partition = sco_groups.get_partitions_list(
+                    context, formsemestre_id, with_default=True
+                )[0]
+                groups = sco_groups.get_partition_groups(context, partition)
+                if groups:
+                    group_ids = [groups[0]["group_id"]]
+                else:
+                    group_ids = [sco_groups.get_default_group(context, formsemestre_id)]
+
+        gq = []
+        for group_id in group_ids:
+            gq.append("group_ids=" + group_id)
+        self.groups_query_args = "&amp;".join(gq)
+        self.base_url = REQUEST.URL0 + "?" + self.groups_query_args
+        self.group_ids = group_ids
+        self.groups = []
+        groups_titles = []
+        self.members = []
+        self.tous_les_etuds_du_sem = (
+            False  # affiche tous les etuds du semestre ? (si un seul semestre)
+        )
+        self.sems = collections.OrderedDict()  # formsemestre_id : sem
+        self.formsemestre = None
+        self.formsemestre_id = formsemestre_id
+        self.nbdem = 0  # nombre d'étudiants démissionnaires en tout
+        sem = None
+        selected_partitions = set()
+        for group_id in group_ids:
+            group_members, group, group_tit, sem, nbdem = sco_groups.get_group_infos(
+                context, group_id, etat=etat
+            )
+            self.groups.append(group)
+            self.nbdem += nbdem
+            self.sems[sem["formsemestre_id"]] = sem
+            if not self.formsemestre_id:
+                self.formsemestre_id = sem["formsemestre_id"]
+                self.formsemestre = sem
+            self.members.extend(group_members)
+            groups_titles.append(group_tit)
+            if group["group_name"] == None:
+                self.tous_les_etuds_du_sem = True
+            else:
+                # liste les partitions explicitement sélectionnés (= des groupes de group_ids)
+                selected_partitions.add((group["numero"], group["partition_id"]))
+
+        self.selected_partitions = [
+            x[1] for x in sorted(list(selected_partitions))
+        ]  # -> [ partition_id ]
+
+        if not self.formsemestre:  # aucun groupe selectionne
+            self.formsemestre = sco_formsemestre.get_formsemestre(
+                context, formsemestre_id
+            )
+
+        self.sortuniq()
+
+        if len(self.sems) > 1:
+            self.tous_les_etuds_du_sem = False  # plusieurs semestres
+        if self.tous_les_etuds_du_sem:
+            if sem and sem["semestre_id"] >= 0:
+                self.groups_titles = "S%d" % sem["semestre_id"]
+            else:
+                self.groups_titles = "tous"
+            self.groups_filename = self.groups_titles
+        else:
+            self.groups_titles = ", ".join(groups_titles)
+            self.groups_filename = "_".join(groups_titles).replace(" ", "_")
+            # Sanitize filename:
+            self.groups_filename = self.groups_filename.translate(None, ":/\\")
+
+        # colonnes pour affichages nom des groupes:
+        # gère le cas où les étudiants appartiennent à des semestres différents
+        self.partitions = []  # les partitions, sans celle par defaut
+        for formsemestre_id in self.sems:
+            for partition in sco_groups.get_partitions_list(context, formsemestre_id):
+                if partition["partition_name"]:
+                    self.partitions.append(partition)
+
+    def sortuniq(self):
+        "Trie les étudiants (de plusieurs groupes) et elimine les doublons"
+        if (len(self.group_ids) <= 1) or len(self.members) <= 1:
+            return  # on suppose que les etudiants d'un groupe sont deja triés
+        self.members.sort(
+            key=operator.itemgetter("nom_disp", "prenom")
+        )  # tri selon nom_usuel ou nom
+        to_remove = []
+        T = self.members
+        for i in range(len(T) - 1, 0, -1):
+            if T[i - 1]["etudid"] == T[i]["etudid"]:
+                to_remove.append(i)
+        for i in to_remove:
+            del T[i]
+
+    def get_form_elem(self):
+        """html hidden input with groups"""
+        H = []
+        for group_id in self.group_ids:
+            H.append('<input type="hidden" name="group_ids" value="%s"/>' % group_id)
+        return "\n".join(H)
+
+
+# Ancien ZScolar.group_list renommé ici en group_table
+def groups_table(
+    context=None,
+    REQUEST=None,
+    groups_infos=None,  # instance of DisplayedGroupsInfos
+    with_codes=0,
+    etat=None,
+    format="html",
+    with_paiement=0,  # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
+    with_archives=0,  # ajoute colonne avec noms fichiers archivés
+    with_annotations=0,
+):
+    """liste etudiants inscrits dans ce semestre
+    format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf
+    Si with_codes, ajoute 3 colonnes avec les codes etudid, NIP, INE
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+
+    with_codes = int(with_codes)
+    with_paiement = int(with_paiement)
+    with_archives = int(with_archives)
+    with_annotations = int(with_annotations)
+
+    base_url_np = groups_infos.base_url + "&amp;with_codes=%s" % with_codes
+    base_url = (
+        base_url_np
+        + "&amp;with_paiement=%s&amp;with_archives=%s&amp;with_annotations=%s"
+        % (with_paiement, with_archives, with_annotations)
+    )
+    #
+    columns_ids = ["nom_disp", "prenom"]  # colonnes a inclure
+    titles = {
+        "nom_disp": "Nom",
+        "prenom": "Prénom",
+        "email": "Mail",
+        "emailperso": "Personnel",
+        "etat": "Etat",
+        "etudid": "etudid",
+        "code_nip": "code_nip",
+        "code_ine": "code_ine",
+        "datefinalisationinscription_str": "Finalisation inscr.",
+        "paiementinscription_str": "Paiement",
+        "etudarchive": "Fichiers",
+        "annotations_str": "Annotations",
+        "etape": "Etape",
+        "semestre_groupe": "Semestre-Groupe",  # pour Moodle
+    }
+
+    # ajoute colonnes pour groupes
+    columns_ids.extend([p["partition_id"] for p in groups_infos.partitions])
+    titles.update(
+        dict(
+            [(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")
+    columns_ids.append("email")
+    columns_ids.append("emailperso")
+
+    if format == "moodlecsv":
+        columns_ids = ["email", "semestre_groupe"]
+
+    if with_codes:
+        columns_ids += ["etape", "etudid", "code_nip", "code_ine"]
+    if with_paiement:
+        columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"]
+    if with_paiement or with_codes:
+        sco_portal_apogee.check_paiement_etuds(context, groups_infos.members)
+    if with_archives:
+        import sco_archives_etud
+
+        sco_archives_etud.add_archives_info_to_etud_list(context, groups_infos.members)
+        columns_ids += ["etudarchive"]
+    if with_annotations:
+        scolars.add_annotations_to_etud_list(context, 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_groupenames = set()
+    # ajoute liens
+    for etud in groups_infos.members:
+        if etud["email"]:
+            etud["_email_target"] = "mailto:" + etud["email"]
+        else:
+            etud["_email_target"] = ""
+        if etud["emailperso"]:
+            etud["_emailperso_target"] = "mailto:" + etud["emailperso"]
+        else:
+            etud["_emailperso_target"] = ""
+        etud["_nom_disp_target"] = "ficheEtud?etudid=" + etud["etudid"]
+        etud["_prenom_target"] = "ficheEtud?etudid=" + etud["etudid"]
+
+        etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"])
+
+        if etud["etat"] == "D":
+            etud["_css_row_class"] = "etuddem"
+        # 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
+        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"]
+                    )
+        else:
+            # pas de groupes sélectionnés: prend le premier s'il y en a un
+            if etud["partitions"]:
+                for p in etud["partitions"].items():  # partitions is an OrderedDict
+                    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)
+
+    if groups_infos.nbdem > 1:
+        s = "s"
+    else:
+        s = ""
+
+    if format == "moodlecsv":
+        # de la forme S1-[FI][FA]-groupe.csv
+        if not moodle_groupenames:
+            moodle_groupenames = {"tous"}
+        filename = (
+            moodle_sem_name
+            + "-"
+            + groups_infos.formsemestre["modalite"]
+            + "-"
+            + "+".join(sorted(moodle_groupenames))
+        )
+    else:
+        filename = "etudiants_%s" % groups_infos.groups_filename
+
+    tab = GenTable(
+        rows=groups_infos.members,
+        columns_ids=columns_ids,
+        titles=titles,
+        caption="soit %d étudiants inscrits et %d démissionaire%s."
+        % (len(groups_infos.members) - groups_infos.nbdem, groups_infos.nbdem, s),
+        base_url=base_url,
+        filename=filename,
+        pdf_link=False,  # pas d'export pdf
+        html_sortable=True,
+        html_class="table_leftalign table_listegroupe",
+        xml_outer_tag="group_list",
+        xml_row_tag="etud",
+        text_fields_separator=",",  # pour csvmoodle
+        preferences=context.get_preferences(groups_infos.formsemestre_id),
+    )
+    #
+    if format == "html":
+        amail_inst = [
+            x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D"
+        ]
+        amail_perso = [
+            x["emailperso"]
+            for x in groups_infos.members
+            if x["emailperso"] and x["etat"] != "D"
+        ]
+
+        if len(groups_infos.members):
+            if groups_infos.tous_les_etuds_du_sem:
+                htitle = "Les %d étudiants inscrits" % len(groups_infos.members)
+            else:
+                htitle = "Groupe %s (%d étudiants)" % (
+                    groups_infos.groups_titles,
+                    len(groups_infos.members),
+                )
+        else:
+            htitle = "Aucun étudiant !"
+        H = [
+            '<div class="tab-content"><form>' '<h3 class="formsemestre"><span>',
+            htitle,
+            "</span>",
+        ]
+        if groups_infos.members:
+            Of = []
+            options = {
+                "with_paiement": "Paiement inscription",
+                "with_archives": "Fichiers archivés",
+                "with_annotations": "Annotations",
+                "with_codes": "Codes",
+            }
+            for option in options:
+                if locals().get(option, False):
+                    selected = "selected"
+                else:
+                    selected = ""
+                Of.append(
+                    """<option value="%s" %s>%s</option>"""
+                    % (option, selected, options[option])
+                )
+
+            H.extend(
+                [
+                    """<span style="margin-left: 2em;"><select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
+                    "\n".join(Of),
+                    """
+            </select></span>
+            <script type="text/javascript">
+  $(document).ready(function() {
+  $('#group_list_options').multiselect(
+  {
+    includeSelectAllOption: false,
+    nonSelectedText:'Options...',
+    onChange: function(element, checked){
+        change_list_options();
+    }
+    }
+  );
+  });
+  </script>
+                """,
+                ]
+            )
+        H.append("</h3></form>")
+        if groups_infos.members:
+            H.extend(
+                [
+                    tab.html(),
+                    "<ul>",
+                    '<li><a class="stdlink" href="%s&amp;format=xlsappel">Feuille d\'appel Excel</a></li>'
+                    % (tab.base_url,),
+                    '<li><a class="stdlink" href="%s&amp;format=xls">Table Excel</a></li>'
+                    % (tab.base_url,),
+                    '<li><a class="stdlink" href="%s&amp;format=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a></li>'
+                    % (tab.base_url,),
+                    '<li><a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id=%s">Fichier CSV pour Moodle (tous les groupes)</a></li>'
+                    % groups_infos.formsemestre_id,
+                ]
+            )
+            if amail_inst:
+                H.append(
+                    '<li><a class="stdlink" href="mailto:?bcc=%s">Envoyer un mail collectif au groupe de %s (via %d adresses institutionnelles)</a></li>'
+                    % (
+                        ",".join(amail_inst),
+                        groups_infos.groups_titles,
+                        len(amail_inst),
+                    )
+                )
+
+            if amail_perso:
+                H.append(
+                    '<li><a class="stdlink" href="mailto:?bcc=%s">Envoyer un mail collectif au groupe de %s (via %d adresses personnelles)</a></li>'
+                    % (
+                        ",".join(amail_perso),
+                        groups_infos.groups_titles,
+                        len(amail_perso),
+                    )
+                )
+            else:
+                H.append("<li><em>Adresses personnelles non renseignées</em></li>")
+
+            H.append("</ul>")
+
+        return "".join(H) + "</div>"
+
+    elif (
+        format == "pdf"
+        or format == "xml"
+        or format == "json"
+        or format == "xls"
+        or format == "moodlecsv"
+    ):
+        if format == "moodlecsv":
+            format = "csv"
+        return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+    elif format == "xlsappel":
+        xls = sco_excel.Excel_feuille_listeappel(
+            context,
+            groups_infos.formsemestre,  # le 1er semestre, serait à modifier si plusieurs
+            groups_infos.groups_titles,
+            groups_infos.members,
+            partitions=groups_infos.partitions,
+            with_codes=with_codes,
+            with_paiement=with_paiement,
+            server_name=REQUEST.BASE0,
+        )
+        filename = "liste_%s" % groups_infos.groups_filename + ".xls"
+        return sco_excel.sendExcelFile(REQUEST, xls, filename)
+    elif format == "allxls":
+        # feuille Excel avec toutes les infos etudiants
+        if not groups_infos.members:
+            return ""
+        keys = [
+            "etudid",
+            "code_nip",
+            "etat",
+            "sexe",
+            "nom",
+            "nom_usuel",
+            "prenom",
+            "inscriptionstr",
+        ]
+        if with_paiement:
+            keys.append("paiementinscription")
+        keys += [
+            "email",
+            "emailperso",
+            "domicile",
+            "villedomicile",
+            "codepostaldomicile",
+            "paysdomicile",
+            "telephone",
+            "telephonemobile",
+            "fax",
+            "date_naissance",
+            "lieu_naissance",
+            "bac",
+            "specialite",
+            "annee_bac",
+            "nomlycee",
+            "villelycee",
+            "codepostallycee",
+            "codelycee",
+            "type_admission",
+            "boursier_prec",
+            "debouche",
+            "parcours",
+            "codeparcours",
+        ]
+        titles = keys[:]
+        other_partitions = sco_groups.get_group_other_partitions(
+            context, groups_infos.groups[0]
+        )
+        keys += [p["partition_id"] for p in other_partitions]
+        titles += [p["partition_name"] for p in other_partitions]
+        # remplis infos lycee si on a que le code lycée
+        # et ajoute infos inscription
+        for m in groups_infos.members:
+            etud = context.getEtudInfo(m["etudid"], filled=True)[0]
+            m.update(etud)
+            scolars.etud_add_lycee_infos(etud)
+            # et ajoute le parcours
+            Se = sco_parcours_dut.SituationEtudParcours(
+                context.Notes, etud, groups_infos.formsemestre_id
+            )
+            m["parcours"] = Se.get_parcours_descr()
+            m["codeparcours"], decisions_jury = sco_report.get_codeparcoursetud(
+                context.Notes, etud
+            )
+
+        def dicttakestr(d, keys):
+            r = []
+            for k in keys:
+                r.append(str(d.get(k, "")))
+            return r
+
+        L = [dicttakestr(m, keys) for m in groups_infos.members]
+        title = "etudiants_%s" % groups_infos.groups_filename
+        xls = sco_excel.Excel_SimpleTable(titles=titles, lines=L, SheetName=title)
+        filename = title + ".xls"
+        return sco_excel.sendExcelFile(REQUEST, xls, filename)
+    else:
+        raise ValueError("unsupported format")
+
+
+def tab_absences_html(context, groups_infos, etat=None, REQUEST=None):
+    """contenu du tab "absences et feuilles diverses"
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    H = ['<div class="tab-content">']
+    if not groups_infos.members:
+        return "".join(H) + "<h3>Aucun étudiant !</h3></div>"
+    H.extend(
+        [
+            "<h3>Absences</h3>",
+            '<ul class="ul_abs">',
+            "<li>",
+            form_choix_jour_saisie_hebdo(context, groups_infos, REQUEST=REQUEST),
+            "</li>",
+            """<li><a class="stdlink" href="Absences/EtatAbsencesGr?%s&amp;debut=%s&amp;fin=%s">Etat des absences du groupe</a></li>"""
+            % (
+                groups_infos.groups_query_args,
+                groups_infos.formsemestre["date_debut"],
+                groups_infos.formsemestre["date_fin"],
+            ),
+            "</ul>",
+            "<h3>Feuilles</h3>",
+            '<ul class="ul_feuilles">',
+            """<li><a class="stdlink" href="%s&amp;format=xlsappel">Feuille d'émargement %s (Excel)</a></li>"""
+            % (groups_infos.base_url, groups_infos.groups_titles),
+            """<li><a class="stdlink" href="trombino?%s&amp;format=pdf">Trombinoscope en PDF</a></li>"""
+            % groups_infos.groups_query_args,
+            """<li><a class="stdlink" href="pdf_trombino_tours?%s&amp;format=pdf">Trombinoscope en PDF (format "IUT de Tours", beta)</a></li>"""
+            % groups_infos.groups_query_args,
+            """<li><a class="stdlink" href="pdf_feuille_releve_absences?%s&amp;format=pdf">Feuille relevé absences hebdomadaire (beta)</a></li>"""
+            % groups_infos.groups_query_args,
+            """<li><a class="stdlink" href="trombino?%s&amp;format=pdflist">Liste d'appel avec photos</a></li>"""
+            % groups_infos.groups_query_args,
+            "</ul>",
+        ]
+    )
+
+    H.append('<h3>Opérations diverses</h3><ul class="ul_misc">')
+    # Lien pour verif codes INE/NIP
+    # (pour tous les etudiants du semestre)
+    group_id = sco_groups.get_default_group(context, groups_infos.formsemestre_id)
+    if authuser.has_permission(ScoEtudInscrit, context):
+        H.append(
+            '<li><a class="stdlink" href="check_group_apogee?group_id=%s&amp;etat=%s">Vérifier codes Apogée</a> (de tous les groupes)</li>'
+            % (group_id, etat or "")
+        )
+    # Lien pour ajout fichiers étudiants
+    if authuser.has_permission(ScoEtudAddAnnotations, context):
+        H.append(
+            """<li><a class="stdlink" href="etudarchive_import_files_form?group_id=%s">Télécharger des fichiers associés aux étudiants (e.g. dossiers d'admission)</a></li>"""
+            % (group_id)
+        )
+
+    H.append("</ul></div>")
+    return "".join(H)
+
+
+def tab_photos_html(context, groups_infos, etat=None, REQUEST=None):
+    """contenu du tab "photos"
+    """
+    if not groups_infos.members:
+        return '<div class="tab-content"><h3>Aucun étudiant !</h3></div>'
+
+    return sco_trombino.trombino_html(context, groups_infos, REQUEST=REQUEST)
+
+
+def form_choix_jour_saisie_hebdo(context, groups_infos, REQUEST=None):
+    """Formulaire choix jour semaine pour saisie.
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    if not authuser.has_permission(ScoAbsChange, context):
+        return ""
+    sem = groups_infos.formsemestre
+    first_monday = ZAbsences.ddmmyyyy(sem["date_debut"]).prev_monday()
+    today_idx = datetime.date.today().weekday()
+
+    FA = []  # formulaire avec menu saisi absences
+    FA.append(
+        '<form id="form_choix_jour_saisie_hebdo" action="Absences/SignaleAbsenceGrSemestre" method="get">'
+    )
+    FA.append('<input type="hidden" name="datefin" value="%(date_fin)s"/>' % sem)
+    FA.append(groups_infos.get_form_elem())
+
+    FA.append('<input type="hidden" name="destination" value=""/>')
+
+    FA.append(
+        """<input type="button" onclick="$('#form_choix_jour_saisie_hebdo')[0].destination.value=get_current_url(); $('#form_choix_jour_saisie_hebdo').submit();" value="Saisir absences du "/>"""
+    )
+    FA.append("""<select name="datedebut">""")
+    date = first_monday
+    i = 0
+    for jour in context.Absences.day_names():
+        if i == today_idx:
+            sel = "selected"
+        else:
+            sel = ""
+        i += 1
+        FA.append('<option value="%s" %s>%s</option>' % (date, sel, jour))
+        date = date.next()
+    FA.append("</select>")
+    FA.append("</form>")
+    return "\n".join(FA)
+
+
+def export_groups_as_moodle_csv(context, formsemestre_id=None, REQUEST=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
+    jo@univ.fr,S3-B1
+    if jo belongs to group A in a partition, and B1 in another one.
+    Caution: if groups in different partitions share the same name, there will be 
+    duplicates... should we prefix the group names with the partition's name ?
+    """
+    if not formsemestre_id:
+        raise ScoValueError("missing parameter: formsemestre_id")
+    partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
+        context, formsemestre_id, with_default=True
+    )
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    moodle_sem_name = sem["session_id"]
+
+    T = []
+    for partition_id in partitions_etud_groups:
+        partition = sco_groups.get_partition(context, partition_id)
+        members = partitions_etud_groups[partition_id]
+        for etudid in members:
+            etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+            group_name = members[etudid]["group_name"]
+            elts = [moodle_sem_name]
+            if partition["partition_name"]:
+                elts.append(partition["partition_name"])
+            if group_name:
+                elts.append(group_name)
+            T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
+    # Make table
+    tab = GenTable(
+        rows=T,
+        columns_ids=("email", "semestre_groupe"),
+        filename=moodle_sem_name + "-moodle",
+        text_fields_separator=",",
+        preferences=context.get_preferences(formsemestre_id),
+    )
+    return tab.make_page(context, format="csv", REQUEST=REQUEST)
diff --git a/sco_import_users.py b/sco_import_users.py
new file mode 100644
index 0000000000000000000000000000000000000000..e614a0fa09dd94f461d3451d18cdb7edd7541b96
--- /dev/null
+++ b/sco_import_users.py
@@ -0,0 +1,236 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Import d'utilisateurs via fichier Excel
+"""
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+import sco_news
+import sco_excel
+
+TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept")
+
+
+def generate_excel_sample():
+    """generates an excel document suitable to import users
+    """
+    style = sco_excel.Excel_MakeStyle(bold=True)
+    titles = TITLES
+    titlesStyles = [style] * len(titles)
+    return sco_excel.Excel_SimpleTable(
+        titles=titles, titlesStyles=titlesStyles, SheetName="Utilisateurs ScoDoc"
+    )
+
+
+def import_excel_file(datafile, REQUEST=None, context=None):
+    "Create users from Excel file"
+    authuser = REQUEST.AUTHENTICATED_USER
+    auth_name = str(authuser)
+    authuser_info = context._user_list(args={"user_name": auth_name})
+    zope_roles = authuser.getRolesInContext(context)
+    if not authuser_info and not ("Manager" in zope_roles):
+        # not admin, and not in database
+        raise AccessDenied("invalid user (%s)" % auth_name)
+    if authuser_info:
+        auth_dept = authuser_info[0]["dept"]
+    else:
+        auth_dept = ""
+    log("sco_import_users.import_excel_file by %s" % auth_name)
+
+    exceldata = datafile.read()
+    if not exceldata:
+        raise ScoValueError("Ficher excel vide ou invalide")
+    diag, data = sco_excel.Excel_to_list(exceldata)
+    if not data:  # probably a bug
+        raise ScoException("import_excel_file: empty file !")
+    # 1-  --- check title line
+    fs = [strlower(stripquotes(s)) for s in data[0]]
+    log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
+    # check cols
+    cols = {}.fromkeys(TITLES)
+    unknown = []
+    for tit in fs:
+        if not cols.has_key(tit):
+            unknown.append(tit)
+        else:
+            del cols[tit]
+    if cols or unknown:
+        raise ScoValueError(
+            "colonnes incorrectes (on attend %d, et non %d) <br/> (colonnes manquantes: %s, colonnes invalides: %s)"
+            % (len(TITLES), len(fs), cols.keys(), unknown)
+        )
+    # ok, same titles...
+    U = []
+    for line in data[1:]:
+        d = {}
+        for i in range(len(fs)):
+            d[fs[i]] = line[i]
+        U.append(d)
+
+    return import_users(U, auth_dept=auth_dept, context=context)
+
+
+def import_users(U, auth_dept="", context=None):
+    """Import des utilisateurs:
+    Pour chaque utilisateur à créer:
+    - vérifier données
+    - générer mot de passe aléatoire
+    - créer utilisateur et mettre le mot de passe
+    - envoyer mot de passe par mail
+
+    En cas d'erreur: supprimer tous les utilisateurs que l'on vient de créer.    
+    """
+    created = []  # liste de uid créés
+    try:
+        for u in U:
+            ok, msg = context._check_modif_user(
+                0,
+                user_name=u["user_name"],
+                nom=u["nom"],
+                prenom=u["prenom"],
+                email=u["email"],
+                roles=u["roles"],
+            )
+            if not ok:
+                raise ScoValueError(
+                    "données invalides pour %s: %s" % (u["user_name"], msg)
+                )
+            u["passwd"] = generate_password()
+            # si auth_dept, crée tous les utilisateurs dans ce departement
+            if auth_dept:
+                u["dept"] = auth_dept
+            #
+            context.create_user(u.copy())
+            created.append(u["user_name"])
+    except:
+        log("import_users: exception: deleting %s" % str(created))
+        # delete created users
+        for user_name in created:
+            context._user_delete(user_name)
+        raise  # re-raise exception
+
+    for u in U:
+        mail_password(u, context=context)
+
+    return "ok"
+
+
+#  --------- Génération du mot de passe initial -----------
+# Adapté de http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440564
+# Alphabet tres simple pour des mots de passe simples...
+
+import getpass, random, sha, string, md5, time, base64
+
+ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU"""
+PASSLEN = 6
+RNG = random.Random(time.time())
+
+
+def generate_password():
+    """This function creates a pseudo random number generator object, seeded with
+    the cryptographic hash of the passString. The contents of the character set
+    is then shuffled and a selection of passLength words is made from this list.
+    This selection is returned as the generated password."""
+    l = list(ALPHABET)  # make this mutable so that we can shuffle the characters
+    RNG.shuffle(l)  # shuffle the character set
+    # pick up only a subset from the available characters:
+    return "".join(RNG.sample(l, PASSLEN))
+
+
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email.MIMEBase import MIMEBase
+from email.Header import Header
+from email import Encoders
+
+
+def mail_password(u, context=None, reset=False):
+    "Send password by email"
+    if not u["email"]:
+        return
+
+    u["url"] = context.ScoURL()
+
+    txt = (
+        """
+Bonjour %(prenom)s %(nom)s,
+
+"""
+        % u
+    )
+    if reset:
+        txt += (
+            """
+votre mot de passe ScoDoc a été ré-initialisé.
+
+Le nouveau mot de passe est:  %(passwd)s
+Votre nom d'utilisateur est %(user_name)s
+
+Vous devrez changer ce mot de passe lors de votre première connexion 
+sur %(url)s
+"""
+            % u
+        )
+    else:
+        txt += (
+            """
+vous avez été déclaré comme utilisateur du logiciel de gestion de scolarité ScoDoc.
+
+Votre nom d'utilisateur est %(user_name)s
+Votre mot de passe est: %(passwd)s
+
+Le logiciel est accessible sur: %(url)s
+
+Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur
+votre nom en haut à gauche de la page d'accueil).
+"""
+            % u
+        )
+
+    txt += (
+        """
+        
+ScoDoc est un logiciel libre développé à l'Université Paris 13 par Emmanuel Viennet.
+Pour plus d'informations sur ce logiciel, voir %s
+
+"""
+        % SCO_WEBSITE
+    )
+    msg = MIMEMultipart()
+    if reset:
+        msg["Subject"] = Header("Mot de passe ScoDoc", SCO_ENCODING)
+    else:
+        msg["Subject"] = Header("Votre accès ScoDoc", SCO_ENCODING)
+    msg["From"] = context.get_preference("email_from_addr")
+    msg["To"] = u["email"]
+    msg.epilogue = ""
+    txt = MIMEText(txt, "plain", SCO_ENCODING)
+    msg.attach(txt)
+    context.sendEmail(msg)
diff --git a/sco_inscr_passage.py b/sco_inscr_passage.py
new file mode 100644
index 0000000000000000000000000000000000000000..aee4b4ff0e0e31a7886de80a21e5e5d8c376524b
--- /dev/null
+++ b/sco_inscr_passage.py
@@ -0,0 +1,642 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Form. pour inscription rapide des etudiants d'un semestre dans un autre
+   Utilise les autorisations d'inscription délivrées en jury.
+"""
+from sets import Set
+
+from gen_tables import GenTable
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+import sco_codes_parcours
+import sco_pvjury
+import sco_formsemestre
+import sco_formsemestre_inscriptions
+import sco_formsemestre
+import sco_groups
+import scolars
+
+
+def list_authorized_etuds_by_sem(context, sem, delai=274):
+    """Liste des etudiants autorisés à s'inscrire dans sem.
+    delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible.
+    """
+    src_sems = list_source_sems(context, sem, delai=delai)
+    inscrits = list_inscrits(context, sem["formsemestre_id"])
+    r = {}
+    candidats = {}  # etudid : etud (tous les etudiants candidats)
+    nb = 0  # debug
+    for src in src_sems:
+        liste = list_etuds_from_sem(context, src, sem)
+        liste_filtree = []
+        for e in liste:
+            # Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src
+            auth_used = False  # autorisation deja utilisée ?
+            etud = context.getEtudInfo(etudid=e["etudid"], filled=True)[0]
+            for isem in etud["sems"]:
+                if DateDMYtoISO(isem["date_debut"]) >= DateDMYtoISO(src["date_fin"]):
+                    auth_used = True
+            if not auth_used:
+                candidats[e["etudid"]] = etud
+                liste_filtree.append(e)
+                nb += 1
+        r[src["formsemestre_id"]] = {
+            "etuds": liste_filtree,
+            "infos": {
+                "id": src["formsemestre_id"],
+                "title": src["titreannee"],
+                "title_target": "formsemestre_status?formsemestre_id=%s"
+                % src["formsemestre_id"],
+            },
+        }
+        # ajoute attribut inscrit qui indique si l'étudiant est déjà inscrit dans le semestre dest.
+        for e in r[src["formsemestre_id"]]["etuds"]:
+            e["inscrit"] = inscrits.has_key(e["etudid"])
+
+    # Ajoute liste des etudiants actuellement inscrits
+    for e in inscrits.values():
+        e["inscrit"] = True
+    r[sem["formsemestre_id"]] = {
+        "etuds": inscrits.values(),
+        "infos": {
+            "id": sem["formsemestre_id"],
+            "title": "Semestre cible: " + sem["titreannee"],
+            "title_target": "formsemestre_status?formsemestre_id=%s"
+            % sem["formsemestre_id"],
+            "comment": " actuellement inscrits dans ce semestre",
+            "help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.",
+        },
+    }
+
+    return r, inscrits, candidats
+
+
+def list_inscrits(context, formsemestre_id, with_dems=False):
+    """Etudiants déjà inscrits à ce semestre
+    { etudid : etud }
+    """
+    if not with_dems:
+        ins = context.Notes.do_formsemestre_inscription_listinscrits(
+            formsemestre_id
+        )  # optimized
+    else:
+        args = {"formsemestre_id": formsemestre_id}
+        ins = context.Notes.do_formsemestre_inscription_list(args=args)
+    inscr = {}
+    for i in ins:
+        etudid = i["etudid"]
+        inscr[etudid] = context.getEtudInfo(etudid=etudid, filled=True)[0]
+    return inscr
+
+
+def list_etuds_from_sem(context, src, dst):
+    """Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst.
+    """
+    target = dst["semestre_id"]
+    dpv = sco_pvjury.dict_pvjury(context, src["formsemestre_id"])
+    if not dpv:
+        return []
+    etuds = [
+        x["identite"]
+        for x in dpv["decisions"]
+        if target in [a["semestre_id"] for a in x["autorisations"]]
+    ]
+    return etuds
+
+
+def list_inscrits_date(context, sem):
+    """Liste les etudiants inscrits dans n'importe quel semestre
+    SAUF sem à la date de début de sem.
+    """
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    sem["date_debut_iso"] = DateDMYtoISO(sem["date_debut"])
+    cursor.execute(
+        """select I.etudid
+                      from notes_formsemestre_inscription I, notes_formsemestre S
+                      where I.formsemestre_id = S.formsemestre_id
+                      and I.formsemestre_id != %(formsemestre_id)s
+                      and S.date_debut <= %(date_debut_iso)s
+                      and S.date_fin >= %(date_debut_iso)s""",
+        sem,
+    )
+    return [x[0] for x in cursor.fetchall()]
+
+
+def do_inscrit(context, sem, etudids, REQUEST=None, inscrit_groupes=False):
+    """Inscrit ces etudiants dans ce semestre
+    (la liste doit avoir été vérifiée au préalable)
+    En option: inscrit aux mêmes groupes que dans le semestre origine
+    """
+    log("do_inscrit (inscrit_groupes=%s): %s" % (inscrit_groupes, etudids))
+    for etudid in etudids:
+        sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
+            context,
+            sem["formsemestre_id"],
+            etudid,
+            etat="I",
+            REQUEST=REQUEST,
+            method="formsemestre_inscr_passage",
+        )
+        if inscrit_groupes:
+            # Inscription dans les mêmes groupes que ceux du semestre  d'origine,
+            # s'ils existent.
+            # (mise en correspondance à partir du nom du groupe, sans tenir compte
+            #  du nom de la partition: évidemment, cela ne marche pas si on a les
+            #   même noms de groupes dans des partitions différentes)
+            etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+            log("cherche groupes de %(nom)s" % etud)
+
+            # recherche le semestre origine (il serait plus propre de l'avoir conservé!)
+            if len(etud["sems"]) < 2:
+                continue
+            prev_formsemestre = etud["sems"][1]
+            sco_groups.etud_add_group_infos(context, etud, prev_formsemestre)
+
+            cursem_groups_by_name = dict(
+                [
+                    (g["group_name"], g)
+                    for g in sco_groups.get_sem_groups(context, sem["formsemestre_id"])
+                    if g["group_name"]
+                ]
+            )
+
+            # forme la liste des groupes présents dans les deux semestres:
+            partition_groups = []  # [ partition+group ] (ds nouveau sem.)
+            for partition_id in etud["partitions"]:
+                prev_group_name = etud["partitions"][partition_id]["group_name"]
+                if prev_group_name in cursem_groups_by_name:
+                    new_group = cursem_groups_by_name[prev_group_name]
+                    partition_groups.append(new_group)
+
+            # inscrit aux groupes
+            for partition_group in partition_groups:
+                sco_groups.change_etud_group_in_partition(
+                    context,
+                    etudid,
+                    partition_group["group_id"],
+                    partition_group,
+                    REQUEST=REQUEST,
+                )
+
+
+def do_desinscrit(context, sem, etudids, REQUEST):
+    log("do_desinscrit: %s" % etudids)
+    for etudid in etudids:
+        context.do_formsemestre_desinscription(
+            etudid, sem["formsemestre_id"], REQUEST=REQUEST
+        )
+
+
+def list_source_sems(context, sem, delai=None):
+    """Liste des semestres sources
+    sem est le semestre destination
+    """
+    # liste des semestres débutant a moins
+    # de delai (en jours) de la date de fin du semestre d'origine.
+    sems = sco_formsemestre.do_formsemestre_list(context)
+    othersems = []
+    d, m, y = [int(x) for x in sem["date_debut"].split("/")]
+    date_debut_dst = datetime.date(y, m, d)
+    d, m, y = [int(x) for x in sem["date_fin"].split("/")]
+    date_fin_dst = datetime.date(y, m, d)
+
+    delais = datetime.timedelta(delai)
+    for s in sems:
+        # pdb.set_trace()
+        # if s['etat'] != '1':
+        #    continue # saute semestres pas ouverts
+        if s["formsemestre_id"] == sem["formsemestre_id"]:
+            continue  # saute le semestre destination
+        if s["date_fin"]:
+            d, m, y = [int(x) for x in s["date_fin"].split("/")]
+            date_fin = datetime.date(y, m, d)
+            if date_debut_dst - date_fin > delais:
+                continue  # semestre trop ancien
+            if date_fin > date_debut_dst:
+                continue  # semestre trop récent
+        # Elimine les semestres de formations speciales (sans parcours)
+        if s["semestre_id"] == sco_codes_parcours.NO_SEMESTRE_ID:
+            continue
+        #
+        F = context.formation_list(args={"formation_id": s["formation_id"]})[0]
+        parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
+        if not parcours.ALLOW_SEM_SKIP:
+            if s["semestre_id"] < (sem["semestre_id"] - 1):
+                continue
+        othersems.append(s)
+    return othersems
+
+
+def formsemestre_inscr_passage(
+    context,
+    formsemestre_id,
+    etuds=[],
+    inscrit_groupes=False,
+    submitted=False,
+    dialog_confirmed=False,
+    REQUEST=None,
+):
+    """Form. pour inscription des etudiants d'un semestre dans un autre
+    (donné par formsemestre_id).
+    Permet de selectionner parmi les etudiants autorisés à s'inscrire.
+    Principe:
+    - trouver liste d'etud, par semestre
+    - afficher chaque semestre "boites" avec cases à cocher
+    - si l'étudiant est déjà inscrit, le signaler (gras, nom de groupes): il peut être désinscrit
+    - on peut choisir les groupes TD, TP, TA
+    - seuls les etudiants non inscrits changent (de groupe)
+    - les etudiants inscrit qui se trouvent décochés sont désinscrits
+    - Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant.    
+
+    """
+    inscrit_groupes = int(inscrit_groupes)
+    # log('formsemestre_inscr_passage: formsemestre_id=%s submitted=%s, dialog_confirmed=%s len(etuds)=%d'
+    #    % (formsemestre_id, submitted, dialog_confirmed, len(etuds)) )
+    cnx = context.GetDBConnexion()
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    # -- check lock
+    if sem["etat"] != "1":
+        raise ScoValueError("opération impossible: semestre verrouille")
+    header = context.sco_header(REQUEST, page_title="Passage des étudiants")
+    footer = context.sco_footer(REQUEST)
+    H = [header]
+    if type(etuds) == type(""):
+        etuds = etuds.split(",")  # vient du form de confirmation
+
+    auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(context, sem)
+    etuds_set = Set(etuds)
+    candidats_set = Set(candidats)
+    inscrits_set = Set(inscrits)
+    candidats_non_inscrits = candidats_set - inscrits_set
+    inscrits_ailleurs = Set(list_inscrits_date(context, sem))
+
+    def set_to_sorted_etud_list(etudset):
+        etuds = [candidats[etudid] for etudid in etudset]
+        etuds.sort(lambda x, y: cmp(x["nom"], y["nom"]))
+        return etuds
+
+    if submitted:
+        a_inscrire = etuds_set.intersection(candidats_set) - inscrits_set
+        a_desinscrire = inscrits_set - etuds_set
+    else:
+        a_inscrire = a_desinscrire = []
+    # log('formsemestre_inscr_passage: a_inscrire=%s' % str(a_inscrire) )
+    # log('formsemestre_inscr_passage: a_desinscrire=%s' % str(a_desinscrire) )
+
+    if not submitted:
+        H += build_page(
+            context,
+            REQUEST,
+            sem,
+            auth_etuds_by_sem,
+            inscrits,
+            candidats_non_inscrits,
+            inscrits_ailleurs,
+            inscrit_groupes=inscrit_groupes,
+        )
+    else:
+        if not dialog_confirmed:
+            # Confirmation
+            if a_inscrire:
+                H.append("<h3>Etudiants à inscrire</h3><ol>")
+                for etud in set_to_sorted_etud_list(a_inscrire):
+                    H.append("<li>%(nomprenom)s</li>" % etud)
+                H.append("</ol>")
+            a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire)
+            if a_inscrire_en_double:
+                H.append("<h3>dont étudiants déjà inscrits:</h3><ul>")
+                for etud in set_to_sorted_etud_list(a_inscrire_en_double):
+                    H.append('<li class="inscrailleurs">%(nomprenom)s</li>' % etud)
+                H.append("</ul>")
+            if a_desinscrire:
+                H.append("<h3>Etudiants à désinscrire</h3><ol>")
+                for etudid in a_desinscrire:
+                    H.append(
+                        '<li class="desinscription">%(nomprenom)s</li>'
+                        % inscrits[etudid]
+                    )
+                H.append("</ol>")
+            if not a_inscrire and not a_desinscrire:
+                H.append("""<h3>Il n'y a rien à modifier !</h3>""")
+            H.append(
+                context.confirmDialog(
+                    dest_url="formsemestre_inscr_passage",
+                    add_headers=False,
+                    cancel_url="formsemestre_inscr_passage?formsemestre_id="
+                    + formsemestre_id,
+                    OK="Effectuer l'opération",
+                    parameters={
+                        "formsemestre_id": formsemestre_id,
+                        "etuds": ",".join(etuds),
+                        "inscrit_groupes": inscrit_groupes,
+                        "submitted": 1,
+                    },
+                    REQUEST=REQUEST,
+                )
+            )
+        else:
+            # Inscription des étudiants au nouveau semestre:
+            do_inscrit(
+                context,
+                sem,
+                a_inscrire,
+                REQUEST=REQUEST,
+                inscrit_groupes=inscrit_groupes,
+            )
+
+            # Desincriptions:
+            do_desinscrit(context, sem, a_desinscrire, REQUEST)
+
+            H.append(
+                """<h3>Opération effectuée</h3>
+            <ul><li><a class="stdlink" href="formsemestre_inscr_passage?formsemestre_id=%s">Continuer les inscriptions</a></li>
+                <li><a class="stdlink" href="formsemestre_status?formsemestre_id=%s">Tableau de bord du semestre</a></li>"""
+                % (formsemestre_id, formsemestre_id)
+            )
+            partition = sco_groups.formsemestre_get_main_partition(
+                context, formsemestre_id
+            )
+            if (
+                partition["partition_id"]
+                != sco_groups.formsemestre_get_main_partition(context, formsemestre_id)[
+                    "partition_id"
+                ]
+            ):  # il y a au moins une vraie partition
+                H.append(
+                    """<li><a class="stdlink" href="affectGroups?partition_id=%s">Répartir les groupes de %s</a></li>
+                """
+                    % (partition["partition_id"], partition["partition_name"])
+                )
+
+    #
+    H.append(footer)
+    return "\n".join(H)
+
+
+def build_page(
+    context,
+    REQUEST,
+    sem,
+    auth_etuds_by_sem,
+    inscrits,
+    candidats_non_inscrits,
+    inscrits_ailleurs,
+    inscrit_groupes=False,
+):
+    inscrit_groupes = int(inscrit_groupes)
+    if inscrit_groupes:
+        inscrit_groupes_checked = " checked"
+    else:
+        inscrit_groupes_checked = ""
+
+    H = [
+        context.html_sem_header(
+            REQUEST, "Passages dans le semestre", sem, with_page_header=False
+        ),
+        """<form method="post" action="%s">""" % REQUEST.URL0,
+        """<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>
+    <input type="submit" name="submitted" value="Appliquer les modifications"/>
+    &nbsp;<a href="#help">aide</a>
+    """
+        % sem,  # "
+        """<input name="inscrit_groupes" type="checkbox" value="1" %s>inscrire aux mêmes groupes</input>"""
+        % inscrit_groupes_checked,
+        """<div class="pas_recap">Actuellement <span id="nbinscrits">%s</span> inscrits
+        et %d candidats supplémentaires
+        </div>"""
+        % (len(inscrits), len(candidats_non_inscrits)),
+        etuds_select_boxes(context, auth_etuds_by_sem, inscrits_ailleurs),
+        """<p/><input type="submit" name="submitted" value="Appliquer les modifications"/>""",
+        formsemestre_inscr_passage_help(sem),
+        """</form>""",
+    ]
+
+    # Semestres sans etudiants autorisés
+    empty_sems = []
+    for formsemestre_id in auth_etuds_by_sem.keys():
+        if not auth_etuds_by_sem[formsemestre_id]["etuds"]:
+            empty_sems.append(auth_etuds_by_sem[formsemestre_id]["infos"])
+    if empty_sems:
+        H.append(
+            """<div class="pas_empty_sems"><h3>Autres semestres sans candidats :</h3><ul>"""
+        )
+        for infos in empty_sems:
+            H.append("""<li><a href="%(title_target)s">%(title)s</a></li>""" % infos)
+        H.append("""</ul></div>""")
+
+    return H
+
+
+def formsemestre_inscr_passage_help(sem):
+    return (
+        """<div class="pas_help pas_help_left"><h3><a name="help">Explications</a></h3>
+    <p>Cette page permet d'inscrire des étudiants dans le semestre destination
+    <a class="stdlink"
+    href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a>, 
+    et d'en désincrire si besoin.
+    </p>
+    <p>Les étudiants sont groupés par semestres d'origines. Ceux qui sont en caractères
+    <span class="inscrit">gras</span> sont déjà inscrits dans le semestre destination.
+    Ceux qui sont en <span class"inscrailleurs">gras et en rouge</span> sont inscrits
+    dans un <em>autre</em> semestre.</p>
+    <p>Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter d'autres
+    étudiants à inscrire dans le semestre destination.</p>
+    <p>Si vous dé-selectionnez un étudiant déjà inscrit (en gras), il sera désinscrit.</p>
+    <p class="help">Aucune action ne sera effectuée si vous n'appuyez pas sur le bouton "Appliquer les modifications" !</p>
+    </div>"""
+        % sem
+    )
+
+
+def etuds_select_boxes(
+    context,
+    auth_etuds_by_cat,
+    inscrits_ailleurs={},
+    sel_inscrits=True,
+    show_empty_boxes=False,
+    export_cat_xls=None,
+    base_url="",
+    read_only=False,
+):
+    """Boites pour selection étudiants par catégorie
+    auth_etuds_by_cat = { category : { 'info' : {}, 'etuds' : ... }
+    inscrits_ailleurs =
+    sel_inscrits= 
+    export_cat_xls =
+    """
+    if export_cat_xls:
+        return etuds_select_box_xls(context, auth_etuds_by_cat[export_cat_xls])
+
+    H = [
+        """<script type="text/javascript">
+    function sem_select(formsemestre_id, state) {
+    var elems = document.getElementById(formsemestre_id).getElementsByTagName("input");
+    for (var i =0; i < elems.length; i++) { elems[i].checked=state; }
+    }
+    function sem_select_inscrits(formsemestre_id) {
+    var elems = document.getElementById(formsemestre_id).getElementsByTagName("input");
+    for (var i =0; i < elems.length; i++) {
+      if (elems[i].parentNode.className.indexOf('inscrit') >= 0) {
+         elems[i].checked=true;
+      } else {
+         elems[i].checked=false;
+      }      
+    }
+    }
+    </script>
+    <div class="etuds_select_boxes">"""
+    ]  # "
+
+    for src_cat in auth_etuds_by_cat.keys():
+        infos = auth_etuds_by_cat[src_cat]["infos"]
+        infos["comment"] = infos.get("comment", "")  # commentaire dans sous-titre boite
+        help = infos.get("help", "")
+        etuds = auth_etuds_by_cat[src_cat]["etuds"]
+        etuds.sort(lambda x, y: cmp(x["nom"], y["nom"]))
+
+        with_checkbox = (not read_only) and auth_etuds_by_cat[src_cat]["infos"].get(
+            "with_checkbox", True
+        )
+        checkbox_name = auth_etuds_by_cat[src_cat]["infos"].get(
+            "checkbox_name", "etuds"
+        )
+        etud_key = auth_etuds_by_cat[src_cat]["infos"].get("etud_key", "etudid")
+        if etuds or show_empty_boxes:
+            infos["nbetuds"] = len(etuds)
+            H.append(
+                """<div class="pas_sembox" id="%(id)s">
+                <div class="pas_sembox_title"><a href="%(title_target)s" """
+                % infos
+            )
+            if help:  # bubble
+                H.append('title="%s"' % help)
+            H.append(
+                """>%(title)s</a></div>
+                <div class="pas_sembox_subtitle">(%(nbetuds)d étudiants%(comment)s)"""
+                % infos
+            )
+            if with_checkbox:
+                H.append(
+                    """ (Select.
+                <a href="#" onclick="sem_select('%(id)s', true);">tous</a>
+                <a href="#" onclick="sem_select('%(id)s', false );">aucun</a>"""  # "
+                    % infos
+                )
+            if sel_inscrits:
+                H.append(
+                    """<a href="#" onclick="sem_select_inscrits('%(id)s');">inscrits</a>"""
+                    % infos
+                )
+            if with_checkbox or sel_inscrits:
+                H.append(")")
+            if base_url and etuds:
+                H.append(
+                    '<a href="%s&amp;export_cat_xls=%s">%s</a>&nbsp;'
+                    % (base_url, src_cat, ICON_XLS)
+                )
+            H.append("</div>")
+            for etud in etuds:
+                if etud.get("inscrit", False):
+                    c = " inscrit"
+                    checked = 'checked="checked"'
+                else:
+                    checked = ""
+                    if etud["etudid"] in inscrits_ailleurs:
+                        c = " inscrailleurs"
+                    else:
+                        c = ""
+                scolars.format_etud_ident(etud)
+                if etud["etudid"]:
+                    elink = (
+                        """<a class="discretelink %s" href="ficheEtud?etudid=%s">%s</a>"""
+                        % (c, etud["etudid"], etud["nomprenom"])
+                    )
+                else:
+                    # ce n'est pas un etudiant ScoDoc
+                    elink = etud["nomprenom"]
+
+                if etud.get("datefinalisationinscription", None):
+                    elink += (
+                        '<span class="finalisationinscription">'
+                        + " : inscription finalisée le "
+                        + etud["datefinalisationinscription"].strftime("%d/%m/%Y")
+                        + "</span>"
+                    )
+
+                if not etud.get("paiementinscription", True):
+                    elink += '<span class="paspaye"> (non paiement)</span>'
+
+                H.append("""<div class="pas_etud%s">""" % c)
+                if "etape" in etud:
+                    etape_str = etud["etape"] or ""
+                else:
+                    etape_str = ""
+                H.append("""<span class="sp_etape">%s</span>""" % etape_str)
+                if with_checkbox:
+                    H.append(
+                        """<input type="checkbox" name="%s:list" value="%s" %s>"""
+                        % (checkbox_name, etud[etud_key], checked)
+                    )
+                H.append(elink)
+                if with_checkbox:
+                    H.append("""</input>""")
+                H.append("</div>")
+            H.append("</div>")
+
+    H.append("</div>")
+    return "\n".join(H)
+
+
+def etuds_select_box_xls(context, src_cat):
+    "export a box to excel"
+    etuds = src_cat["etuds"]
+    columns_ids = ["etudid", "sexe", "nom", "prenom", "etape"]
+    titles = {}
+    map(
+        lambda x, titles=titles: titles.__setitem__(x[0], x[1]),
+        zip(columns_ids, columns_ids),
+    )
+    # Ajoute colonne paiement inscription
+    columns_ids.append("paiementinscription_str")
+    titles["paiementinscription_str"] = "paiement inscription"
+    for e in etuds:
+        if not e.get("paiementinscription", True):
+            e["paiementinscription_str"] = "NON"
+        else:
+            e["paiementinscription_str"] = "-"
+    tab = GenTable(
+        titles=titles,
+        columns_ids=columns_ids,
+        rows=etuds,
+        caption="%(title)s. %(help)s" % src_cat["infos"],
+        preferences=context.get_preferences(),
+    )
+    return tab.excel()
diff --git a/sco_liste_notes.py b/sco_liste_notes.py
new file mode 100644
index 0000000000000000000000000000000000000000..aeac6acc439b101cdb05a9353c180f0782b0bde1
--- /dev/null
+++ b/sco_liste_notes.py
@@ -0,0 +1,874 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Liste des notes d'une évaluation
+"""
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+from notes_table import *
+import sco_formsemestre
+import sco_groups
+import sco_evaluations
+import htmlutils
+import sco_excel
+from gen_tables import GenTable
+from htmlutils import histogram_notes
+
+from sets import Set
+
+
+def do_evaluation_listenotes(context, REQUEST):
+    """
+    Affichage des notes d'une évaluation
+
+    args: evaluation_id 
+    """
+    cnx = context.GetDBConnexion()
+    mode = None
+    if REQUEST.form.has_key("evaluation_id"):
+        evaluation_id = REQUEST.form["evaluation_id"]
+        mode = "eval"
+        evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
+    if REQUEST.form.has_key("moduleimpl_id"):
+        moduleimpl_id = REQUEST.form["moduleimpl_id"]
+        mode = "module"
+        evals = context.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
+    if not mode:
+        raise ValueError("missing argument: evaluation or module")
+    if not evals:
+        return "<p>Aucune évaluation !</p>"
+
+    format = REQUEST.form.get("format", "html")
+    E = evals[0]  # il y a au moins une evaluation
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    formsemestre_id = M["formsemestre_id"]
+
+    # description de l'evaluation
+    if mode == "eval":
+        H = [
+            sco_evaluations.evaluation_describe(
+                context, evaluation_id=evaluation_id, REQUEST=REQUEST
+            )
+        ]
+    else:
+        H = []
+    # groupes
+    groups = sco_groups.do_evaluation_listegroupes(
+        context, E["evaluation_id"], include_default=True
+    )
+    grlabs = [g["group_name"] or "tous" for g in groups]  # legendes des boutons
+    grnams = [g["group_id"] for g in groups]  # noms des checkbox
+
+    if len(evals) > 1:
+        descr = [
+            ("moduleimpl_id", {"default": E["moduleimpl_id"], "input_type": "hidden"})
+        ]
+    else:
+        descr = [
+            ("evaluation_id", {"default": E["evaluation_id"], "input_type": "hidden"})
+        ]
+    if len(grnams) > 1:
+        descr += [
+            (
+                "s",
+                {
+                    "input_type": "separator",
+                    "title": "<b>Choix du ou des groupes d'étudiants:</b>",
+                },
+            ),
+            (
+                "group_ids",
+                {
+                    "input_type": "checkbox",
+                    "title": "",
+                    "allowed_values": grnams,
+                    "labels": grlabs,
+                    "attributes": ('onclick="document.tf.submit();"',),
+                },
+            ),
+        ]
+    else:
+        if grnams:
+            def_nam = grnams[0]
+        else:
+            def_nam = ""
+        descr += [
+            (
+                "group_ids",
+                {"input_type": "hidden", "type": "list", "default": [def_nam]},
+            )
+        ]
+    descr += [
+        (
+            "anonymous_listing",
+            {
+                "input_type": "checkbox",
+                "title": "",
+                "allowed_values": ("yes",),
+                "labels": ('listing "anonyme"',),
+                "attributes": ('onclick="document.tf.submit();"',),
+                "template": '<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s &nbsp;&nbsp;',
+            },
+        ),
+        (
+            "hide_groups",
+            {
+                "input_type": "checkbox",
+                "title": "",
+                "allowed_values": ("yes",),
+                "labels": ("masquer les groupes",),
+                "attributes": ('onclick="document.tf.submit();"',),
+                "template": "%(elem)s &nbsp;&nbsp;",
+            },
+        ),
+        (
+            "note_sur_20",
+            {
+                "input_type": "checkbox",
+                "title": "",
+                "allowed_values": ("yes",),
+                "labels": ("notes sur 20",),
+                "attributes": ('onclick="document.tf.submit();"',),
+                "template": "%(elem)s</td></tr>",
+            },
+        ),
+    ]
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        cancelbutton=None,
+        submitbutton=None,
+        bottom_buttons=False,
+        method="GET",
+        cssclass="noprint",
+        name="tf",
+        is_submitted=True,  # toujours "soumis" (démarre avec liste complète)
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + "\n" + tf[1]
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(
+            "%s/Notes/moduleimpl_status?moduleimpl_id=%s"
+            % (context.ScoURL(), E["moduleimpl_id"])
+        )
+    else:
+        anonymous_listing = tf[2]["anonymous_listing"]
+        note_sur_20 = tf[2]["note_sur_20"]
+        hide_groups = tf[2]["hide_groups"]
+        return _make_table_notes(
+            context,
+            REQUEST,
+            tf[1],
+            evals,
+            format=format,
+            note_sur_20=note_sur_20,
+            anonymous_listing=anonymous_listing,
+            group_ids=tf[2]["group_ids"],
+            hide_groups=hide_groups,
+        )
+
+
+def _make_table_notes(
+    context,
+    REQUEST,
+    html_form,
+    evals,
+    format="",
+    note_sur_20=False,
+    anonymous_listing=False,
+    hide_groups=False,
+    group_ids=[],
+):
+    """Generate table for evaluations marks"""
+    if not evals:
+        return "<p>Aucune évaluation !</p>"
+    E = evals[0]
+    moduleimpl_id = E["moduleimpl_id"]
+    M = context.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"])
+    # (debug) check that all evals are in same module:
+    for e in evals:
+        if e["moduleimpl_id"] != moduleimpl_id:
+            raise ValueError("invalid evaluations list")
+
+    if format == "xls":
+        keep_numeric = True  # pas de conversion des notes en strings
+    else:
+        keep_numeric = False
+    # Si pas de groupe, affiche tout
+    if not group_ids:
+        group_ids = [sco_groups.get_default_group(context, M["formsemestre_id"])]
+    groups = sco_groups.listgroups(context, group_ids)
+
+    gr_title = sco_groups.listgroups_abbrev(groups)
+    gr_title_filename = sco_groups.listgroups_filename(groups)
+
+    etudids = sco_groups.do_evaluation_listeetuds_groups(
+        context, E["evaluation_id"], groups, include_dems=True
+    )
+
+    if anonymous_listing:
+        columns_ids = ["code"]  # cols in table
+    else:
+        if format == "xls" or format == "xml":
+            columns_ids = ["nom", "prenom"]
+        else:
+            columns_ids = ["nomprenom"]
+    if not hide_groups:
+        columns_ids.append("group")
+
+    titles = {
+        "code": "Code",
+        "group": "Groupe",
+        "nom": "Nom",
+        "prenom": "Prénom",
+        "nomprenom": "Nom",
+        "expl_key": "Rem.",
+    }
+
+    rows = []
+
+    class keymgr(dict):  # comment : key (pour regrouper les comments a la fin)
+        def __init__(self):
+            self.lastkey = 1
+
+        def nextkey(self):
+            r = self.lastkey
+            self.lastkey += 1
+            # self.lastkey = chr(ord(self.lastkey)+1)
+            return str(r)
+
+    K = keymgr()
+    for etudid in etudids:
+        css_row_class = None
+        # infos identite etudiant
+        etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+        # infos inscription
+        inscr = context.do_formsemestre_inscription_list(
+            {"etudid": etudid, "formsemestre_id": M["formsemestre_id"]}
+        )[0]
+
+        if inscr["etat"] == "I":  # si inscrit, indique groupe
+            groups = sco_groups.get_etud_groups(context, etudid, sem)
+            grc = sco_groups.listgroups_abbrev(groups)
+        else:
+            if inscr["etat"] == "D":
+                grc = "DEM"  # attention: ce code est re-ecrit plus bas, ne pas le changer (?)
+                css_row_class = "etuddem"
+            else:
+                grc = inscr["etat"]
+
+        code = ""  # code pour listings anonyme, à la place du nom
+        if context.get_preference("anonymous_lst_code") == "INE":
+            code = etud["code_ine"]
+        elif context.get_preference("anonymous_lst_code") == "NIP":
+            code = etud["code_nip"]
+        if not code:  # laisser le code vide n'aurait aucun sens, prenons l'etudid
+            code = etudid
+
+        rows.append(
+            {
+                "code": code,
+                "_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"',
+                "etudid": etudid,
+                "nom": strupper(etud["nom"]),
+                "_nomprenom_target": "formsemestre_bulletinetud?formsemestre_id=%s&amp;etudid=%s"
+                % (M["formsemestre_id"], etudid),
+                "_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]),
+                "prenom": strcapitalize(strlower(etud["prenom"])),
+                "nomprenom": etud["nomprenom"],
+                "group": grc,
+                "_css_row_class": css_row_class or "",
+            }
+        )
+
+    # Lignes en tête:
+    coefs = {
+        "nom": "",
+        "prenom": "",
+        "nomprenom": "",
+        "group": "",
+        "code": "",
+        "_css_row_class": "sorttop fontitalic",
+        "_table_part": "head",
+    }
+    note_max = {
+        "nom": "",
+        "prenom": "",
+        "nomprenom": "",
+        "group": "",
+        "code": "",
+        "_css_row_class": "sorttop fontitalic",
+        "_table_part": "head",
+    }
+    moys = {
+        "_css_row_class": "moyenne sortbottom",
+        "_table_part": "foot",
+        #'_nomprenom_td_attrs' : 'colspan="2" ',
+        "nomprenom": "Moyenne (sans les absents) :",
+        "comment": "",
+    }
+    # Ajoute les notes de chaque évaluation:
+    for e in evals:
+        e["eval_state"] = sco_evaluations.do_evaluation_etat(
+            context, e["evaluation_id"]
+        )
+        notes, nb_abs, nb_att = _add_eval_columns(
+            context,
+            e,
+            rows,
+            titles,
+            coefs,
+            note_max,
+            moys,
+            K,
+            note_sur_20,
+            keep_numeric,
+        )
+        columns_ids.append(e["evaluation_id"])
+    #
+    if anonymous_listing:
+        rows.sort(key=lambda x: x["code"])
+    else:
+        rows.sort(key=lambda x: (x["nom"], x["prenom"]))  # sort by nom, prenom
+
+    # Si module, ajoute moyenne du module:
+    if len(evals) > 1:
+        notes = _add_moymod_column(
+            context,
+            sem["formsemestre_id"],
+            e,
+            rows,
+            titles,
+            coefs,
+            note_max,
+            moys,
+            note_sur_20,
+            keep_numeric,
+        )
+        columns_ids.append("moymod")
+
+    # ajoute lignes en tête et moyennes
+    if len(evals) > 0:
+        rows = [coefs, note_max] + rows
+    rows.append(moys)
+    # ajout liens HTMl vers affichage une evaluation:
+    if format == "html" and len(evals) > 1:
+        rlinks = {"_table_part": "head"}
+        for e in evals:
+            rlinks[e["evaluation_id"]] = "afficher"
+            rlinks[
+                "_" + e["evaluation_id"] + "_help"
+            ] = "afficher seulement les notes de cette évaluation"
+            rlinks["_" + e["evaluation_id"] + "_target"] = (
+                "evaluation_listenotes?evaluation_id=" + e["evaluation_id"]
+            )
+            rlinks["_" + e["evaluation_id"] + "_td_attrs"] = ' class="tdlink" '
+        rows.append(rlinks)
+
+    if len(evals) == 1:  # colonne "Rem." seulement si une eval
+        if format == "html":  # pas d'indication d'origine en pdf (pour affichage)
+            columns_ids.append("expl_key")
+        elif format == "xls" or format == "xml":
+            columns_ids.append("comment")
+
+    # titres divers:
+    gl = "".join(["&amp;group_ids%3Alist=" + g for g in group_ids])
+    if note_sur_20:
+        gl = "&amp;note_sur_20%3Alist=yes" + gl
+    if anonymous_listing:
+        gl = "&amp;anonymous_listing%3Alist=yes" + gl
+    if hide_groups:
+        gl = "&amp;hide_groups%3Alist=yes" + gl
+    if len(evals) == 1:
+        evalname = "%s-%s" % (Mod["code"], DateDMYtoISO(E["jour"]))
+        hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudids))
+        filename = make_filename("notes_%s_%s" % (evalname, gr_title_filename))
+        caption = hh
+        pdf_title = "%(description)s (%(jour)s)" % e
+        html_title = ""
+        base_url = "evaluation_listenotes?evaluation_id=%s" % E["evaluation_id"] + gl
+        html_next_section = (
+            '<div class="notes_evaluation_stats">%d absents, %d en attente.</div>'
+            % (nb_abs, nb_att)
+        )
+    else:
+        filename = make_filename("notes_%s_%s" % (Mod["code"], gr_title_filename))
+        title = "Notes du module %(code)s %(titre)s" % Mod
+        title += " semestre %(titremois)s" % sem
+        if gr_title and gr_title != "tous":
+            title += " %s" % gr_title
+        caption = title
+        html_next_section = ""
+        if format == "pdf":
+            caption = ""  # same as pdf_title
+        pdf_title = title
+        html_title = (
+            """<h2 class="formsemestre">Notes du module <a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a></h2>"""
+            % (moduleimpl_id, Mod["code"], Mod["titre"])
+        )
+        base_url = "evaluation_listenotes?moduleimpl_id=%s" % moduleimpl_id + gl
+    # display
+    tab = GenTable(
+        titles=titles,
+        columns_ids=columns_ids,
+        rows=rows,
+        html_sortable=True,
+        base_url=base_url,
+        filename=filename,
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        caption=caption,
+        html_next_section=html_next_section,
+        page_title="Notes de " + sem["titremois"],
+        html_title=html_title,
+        pdf_title=pdf_title,
+        html_class="table_leftalign notes_evaluation",
+        preferences=context.get_preferences(M["formsemestre_id"]),
+        # html_generate_cells=False # la derniere ligne (moyennes) est incomplete
+    )
+
+    t = tab.make_page(context, format=format, with_html_headers=False, REQUEST=REQUEST)
+    if format != "html":
+        return t
+
+    if len(evals) > 1:
+        all_complete = True
+        for e in evals:
+            if not e["eval_state"]["evalcomplete"]:
+                all_complete = False
+        if all_complete:
+            eval_info = '<span class="eval_info eval_complete">Evaluations prises en compte dans les moyennes</span>'
+        else:
+            eval_info = '<span class="eval_info help">Les évaluations en vert et orange sont prises en compte dans les moyennes. Celles en rouge n\'ont pas toutes leurs notes.</span>'
+        return html_form + eval_info + t + "<p></p>"
+    else:
+        # Une seule evaluation: ajoute histogramme
+        histo = histogram_notes(notes)
+        # 2 colonnes: histo, comments
+        C = [
+            "<table><tr><td><div><h4>Répartition des notes:</h4>"
+            + histo
+            + "</div></td>\n",
+            '<td style="padding-left: 50px; vertical-align: top;"><p>',
+        ]
+        commentkeys = K.items()  # [ (comment, key), ... ]
+        commentkeys.sort(lambda x, y: cmp(int(x[1]), int(y[1])))
+        for (comment, key) in commentkeys:
+            C.append(
+                '<span class="colcomment">(%s)</span> <em>%s</em><br/>' % (key, comment)
+            )
+        if commentkeys:
+            C.append(
+                '<span><a class=stdlink" href="evaluation_list_operations?evaluation_id=%s">Gérer les opérations</a></span><br/>'
+                % E["evaluation_id"]
+            )
+        eval_info = "xxx"
+        if E["eval_state"]["evalcomplete"]:
+            eval_info = '<span class="eval_info eval_complete">Evaluation prise en compte dans les moyennes</span>'
+        elif E["eval_state"]["evalattente"]:
+            eval_info = '<span class="eval_info eval_attente">Il y a des notes en attente (les autres sont prises en compte)</span>'
+        else:
+            eval_info = '<span class="eval_info eval_incomplete">Notes incomplètes, évaluation non prise en compte dans les moyennes</span>'
+
+        return (
+            sco_evaluations.evaluation_describe(
+                context, evaluation_id=E["evaluation_id"], REQUEST=REQUEST
+            )
+            + eval_info
+            + html_form
+            + t
+            + "\n".join(C)
+        )
+
+
+def _add_eval_columns(
+    context, e, rows, titles, coefs, note_max, moys, K, note_sur_20, keep_numeric
+):
+    """Add eval e"""
+    nb_notes = 0
+    nb_abs = 0
+    nb_att = 0
+    sum_notes = 0
+    notes = []  # liste des notes numeriques, pour calcul histogramme uniquement
+    evaluation_id = e["evaluation_id"]
+    NotesDB = context._notes_getall(evaluation_id)
+    for row in rows:
+        etudid = row["etudid"]
+        if NotesDB.has_key(etudid):
+            val = NotesDB[etudid]["value"]
+            if val is None:
+                nb_abs += 1
+            if val == NOTES_ATTENTE:
+                nb_att += 1
+            # calcul moyenne SANS LES ABSENTS
+            if val != None and val != NOTES_NEUTRALISE and val != NOTES_ATTENTE:
+                if e["note_max"] > 0:
+                    valsur20 = val * 20.0 / e["note_max"]  # remet sur 20
+                else:
+                    valsur20 = 0
+                notes.append(valsur20)  # toujours sur 20 pour l'histogramme
+                if note_sur_20:
+                    val = valsur20  # affichage notes / 20 demandé
+                nb_notes = nb_notes + 1
+                sum_notes += val
+            val_fmt = fmt_note(val, keep_numeric=keep_numeric)
+            comment = NotesDB[etudid]["comment"]
+            if comment is None:
+                comment = ""
+            explanation = "%s (%s) %s" % (
+                NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
+                NotesDB[etudid]["uid"],
+                comment,
+            )
+        else:
+            explanation = ""
+            val_fmt = ""
+            val = None
+
+        if val is None:
+            row["_" + evaluation_id + "_td_attrs"] = 'class="etudabs" '
+            if not row.get("_css_row_class", ""):
+                row["_css_row_class"] = "etudabs"
+        # regroupe les commentaires
+        if explanation:
+            if K.has_key(explanation):
+                expl_key = "(%s)" % K[explanation]
+            else:
+                K[explanation] = K.nextkey()
+                expl_key = "(%s)" % K[explanation]
+        else:
+            expl_key = ""
+
+        row.update(
+            {
+                evaluation_id: val_fmt,
+                "_" + evaluation_id + "_help": explanation,
+                # si plusieurs evals seront ecrasés et non affichés:
+                "comment": explanation,
+                "expl_key": expl_key,
+                "_expl_key_help": explanation,
+            }
+        )
+
+        coefs[evaluation_id] = "coef. %s" % e["coefficient"]
+        if note_sur_20:
+            nmx = 20.0
+        else:
+            nmx = e["note_max"]
+        if keep_numeric:
+            note_max[evaluation_id] = nmx
+        else:
+            note_max[evaluation_id] = "/ %s" % nmx
+
+        if nb_notes > 0:
+            moys[evaluation_id] = "%.3g" % (sum_notes / nb_notes)
+            moys["_" + evaluation_id + "_help"] = "moyenne sur %d notes (%s le %s)" % (
+                nb_notes,
+                e["description"],
+                e["jour"],
+            )
+        else:
+            moys[evaluation_id] = ""
+
+        titles[evaluation_id] = "%(description)s (%(jour)s)" % e
+
+        if e["eval_state"]["evalcomplete"]:
+            titles["_" + evaluation_id + "_td_attrs"] = 'class="eval_complete"'
+        elif e["eval_state"]["evalattente"]:
+            titles["_" + evaluation_id + "_td_attrs"] = 'class="eval_attente"'
+        else:
+            titles["_" + evaluation_id + "_td_attrs"] = 'class="eval_incomplete"'
+
+    return notes, nb_abs, nb_att  # pour histogramme
+
+
+def _add_moymod_column(
+    context,
+    formsemestre_id,
+    e,
+    rows,
+    titles,
+    coefs,
+    note_max,
+    moys,
+    note_sur_20,
+    keep_numeric,
+):
+    col_id = "moymod"
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etud_mod_moy
+    nb_notes = 0
+    sum_notes = 0
+    notes = []  # liste des notes numeriques, pour calcul histogramme uniquement
+    for row in rows:
+        etudid = row["etudid"]
+        val = nt.get_etud_mod_moy(
+            e["moduleimpl_id"], etudid
+        )  # note sur 20, ou 'NA','NI'
+        row[col_id] = fmt_note(val, keep_numeric=keep_numeric)
+        row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
+        if type(val) != StringType:
+            notes.append(val)
+            nb_notes = nb_notes + 1
+            sum_notes += val
+    coefs[col_id] = ""
+    if keep_numeric:
+        note_max[col_id] = 20.0
+    else:
+        note_max[col_id] = "/ 20"
+    titles[col_id] = "Moyenne module"
+    if nb_notes > 0:
+        moys[col_id] = "%.3g" % (sum_notes / nb_notes)
+        moys["_" + col_id + "_help"] = "moyenne des moyennes"
+    else:
+        moys[col_id] = ""
+
+
+# ---------------------------------------------------------------------------------
+
+
+# matin et/ou après-midi ?
+def _eval_demijournee(E):
+    "1 si matin, 0 si apres midi, 2 si toute la journee"
+    am, pm = False, False
+    if E["heure_debut"] < "13:00":
+        am = True
+    if E["heure_fin"] > "13:00":
+        pm = True
+    if am and pm:
+        demijournee = 2
+    elif am:
+        demijournee = 1
+    else:
+        demijournee = 0
+        pm = True
+    return am, pm, demijournee
+
+
+def evaluation_check_absences(context, evaluation_id):
+    """Vérifie les absences au moment de cette évaluation.
+    Cas incohérents que l'on peut rencontrer pour chaque étudiant:
+      note et absent  
+      ABS et pas noté absent
+      ABS et absent justifié
+      EXC et pas noté absent
+      EXC et pas justifie
+    Ramene 3 listes d'etudid
+    """
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+    if not E["jour"]:
+        return [], [], [], [], []  # evaluation sans date
+
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    formsemestre_id = M["formsemestre_id"]
+    etudids = sco_groups.do_evaluation_listeetuds_groups(
+        context, evaluation_id, getallstudents=True
+    )
+
+    am, pm, demijournee = _eval_demijournee(E)
+
+    # Liste les absences à ce moment:
+    A = context.Absences.ListeAbsJour(DateDMYtoISO(E["jour"]), am=am, pm=pm)
+    As = Set([x["etudid"] for x in A])  # ensemble des etudiants absents
+    NJ = context.Absences.ListeAbsNonJustJour(DateDMYtoISO(E["jour"]), am=am, pm=pm)
+    NJs = Set([x["etudid"] for x in NJ])  # ensemble des etudiants absents non justifies
+    Just = context.Absences.ListeAbsJour(
+        DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
+    )
+    Justs = Set([x["etudid"] for x in Just])  # ensemble des etudiants avec justif
+
+    # Les notes:
+    NotesDB = context._notes_getall(evaluation_id)
+    ValButAbs = []  # une note mais noté absent
+    AbsNonSignalee = []  # note ABS mais pas noté absent
+    ExcNonSignalee = []  # note EXC mais pas noté absent
+    ExcNonJust = []  #  note EXC mais absent non justifie
+    AbsButExc = []  # note ABS mais justifié
+    for etudid in etudids:
+        if NotesDB.has_key(etudid):
+            val = NotesDB[etudid]["value"]
+            if (
+                val != None and val != NOTES_NEUTRALISE and val != NOTES_ATTENTE
+            ) and etudid in As:
+                # note valide et absent
+                ValButAbs.append(etudid)
+            if val is None and not etudid in As:
+                # absent mais pas signale comme tel
+                AbsNonSignalee.append(etudid)
+            if val == NOTES_NEUTRALISE and not etudid in As:
+                # Neutralisé mais pas signale absent
+                ExcNonSignalee.append(etudid)
+            if val == NOTES_NEUTRALISE and etudid in NJs:
+                # EXC mais pas justifié
+                ExcNonJust.append(etudid)
+            if val is None and etudid in Justs:
+                # ABS mais justificatif
+                AbsButExc.append(etudid)
+
+    return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
+
+
+def evaluation_check_absences_html(
+    context, evaluation_id, with_header=True, show_ok=True, REQUEST=None
+):
+    """Affiche etat verification absences d'une evaluation"""
+
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+    am, pm, demijournee = _eval_demijournee(E)
+
+    (
+        ValButAbs,
+        AbsNonSignalee,
+        ExcNonSignalee,
+        ExcNonJust,
+        AbsButExc,
+    ) = evaluation_check_absences(context, evaluation_id)
+
+    if with_header:
+        H = [
+            context.html_sem_header(REQUEST, "Vérification absences à l'évaluation"),
+            sco_evaluations.evaluation_describe(
+                context, evaluation_id=evaluation_id, REQUEST=REQUEST
+            ),
+            """<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
+        ]
+    else:
+        # pas de header, mais un titre
+        H = [
+            """<h2 class="eval_check_absences">%s du %s """
+            % (E["description"], E["jour"])
+        ]
+        if (
+            not ValButAbs
+            and not AbsNonSignalee
+            and not ExcNonSignalee
+            and not ExcNonJust
+        ):
+            H.append(': <span class="eval_check_absences_ok">ok</span>')
+        H.append("</h2>")
+
+    def etudlist(etudids, linkabs=False):
+        H.append("<ul>")
+        if not etudids and show_ok:
+            H.append("<li>aucun</li>")
+        for etudid in etudids:
+            etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+            H.append(
+                '<li><a class="discretelink" href="ficheEtud?etudid=%(etudid)s">%(nomprenom)s</a>'
+                % etud
+            )
+            if linkabs:
+                H.append(
+                    '<a class="stdlink" href="Absences/doSignaleAbsence?etudid=%s&amp;datedebut=%s&amp;datefin=%s&amp;demijournee=%s&amp;moduleimpl_id=%s">signaler cette absence</a>'
+                    % (
+                        etud["etudid"],
+                        urllib.quote(E["jour"]),
+                        urllib.quote(E["jour"]),
+                        demijournee,
+                        E["moduleimpl_id"],
+                    )
+                )
+            H.append("</li>")
+        H.append("</ul>")
+
+    if ValButAbs or show_ok:
+        H.append(
+            "<h3>Etudiants ayant une note alors qu'ils sont signalés absents:</h3>"
+        )
+        etudlist(ValButAbs)
+
+    if AbsNonSignalee or show_ok:
+        H.append(
+            """<h3>Etudiants avec note "ABS" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
+        )
+        etudlist(AbsNonSignalee, linkabs=True)
+
+    if ExcNonSignalee or show_ok:
+        H.append(
+            """<h3>Etudiants avec note "EXC" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
+        )
+        etudlist(ExcNonSignalee)
+
+    if ExcNonJust or show_ok:
+        H.append(
+            """<h3>Etudiants avec note "EXC" alors qu'ils sont absents <em>non justifiés</em>:</h3>"""
+        )
+        etudlist(ExcNonJust)
+
+    if AbsButExc or show_ok:
+        H.append(
+            """<h3>Etudiants avec note "ABS" alors qu'ils ont une <em>justification</em>:</h3>"""
+        )
+        etudlist(AbsButExc)
+
+    if with_header:
+        H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def formsemestre_check_absences_html(context, formsemestre_id, REQUEST=None):
+    """Affiche etat verification absences pour toutes les evaluations du semestre !
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    H = [
+        context.html_sem_header(
+            REQUEST, "Vérification absences aux évaluations de ce semestre", sem
+        ),
+        """<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.
+          Sont listés tous les modules avec des évaluations.<br/>Aucune action n'est effectuée:
+          il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
+          </p>""",
+    ]
+    # Modules, dans l'ordre
+    Mlist = context.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+    for M in Mlist:
+        evals = context.do_evaluation_list({"moduleimpl_id": M["moduleimpl_id"]})
+        if evals:
+            H.append(
+                '<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
+                % (M["moduleimpl_id"], M["module"]["code"], M["module"]["abbrev"])
+            )
+        for E in evals:
+            H.append(
+                evaluation_check_absences_html(
+                    context,
+                    E["evaluation_id"],
+                    with_header=False,
+                    show_ok=False,
+                    REQUEST=REQUEST,
+                )
+            )
+        if evals:
+            H.append("</div>")
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
diff --git a/sco_lycee.py b/sco_lycee.py
new file mode 100644
index 0000000000000000000000000000000000000000..059416cb9c39279dc4cb1f1c77907548685aef99
--- /dev/null
+++ b/sco_lycee.py
@@ -0,0 +1,250 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Rapports sur lycées d'origine des étudiants d'un  semestre.
+  - statistiques decisions
+  - suivi cohortes
+"""
+
+import tempfile, urllib, re
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+import scolars
+import sco_groups
+import sco_report
+from gen_tables import GenTable
+import sco_formsemestre
+
+
+def formsemestre_table_etuds_lycees(
+    context, formsemestre_id, group_lycees=True, only_primo=False
+):
+    """Récupère liste d'etudiants avec etat et decision.
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    etuds = sco_report.tsp_etud_list(context, formsemestre_id, only_primo=only_primo)[0]
+    if only_primo:
+        primostr = "primo-entrants du "
+    else:
+        primostr = "du "
+    title = "Lycées des étudiants %ssemestre " % primostr + sem["titreannee"]
+    return _table_etuds_lycees(
+        context, etuds, group_lycees, title, context.get_preferences(formsemestre_id)
+    )
+
+
+def scodoc_table_etuds_lycees(context, format="html", REQUEST=None):
+    """Table avec _tous_ les étudiants des semestres non verrouillés de _tous_ les départements.
+    """
+    semdepts = sco_formsemestre.scodoc_get_all_unlocked_sems(context)
+    etuds = []
+    for (sem, deptcontext) in semdepts:
+        etuds += sco_report.tsp_etud_list(deptcontext, sem["formsemestre_id"])[0]
+
+    tab, etuds_by_lycee = _table_etuds_lycees(
+        context,
+        etuds,
+        False,
+        "Lycées de TOUS les étudiants",
+        context.get_preferences(),
+        no_links=True,
+    )
+    tab.base_url = REQUEST.URL0
+    t = tab.make_page(context, format=format, with_html_headers=False, REQUEST=REQUEST)
+    if format != "html":
+        return t
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title=tab.page_title,
+            init_google_maps=True,
+            init_qtip=True,
+            javascripts=["js/etud_info.js", "js/map_lycees.js"],
+        ),
+        """<h2 class="formsemestre">Lycées d'origine des %d étudiants (%d semestres)</h2>"""
+        % (len(etuds), len(semdepts)),
+        t,
+        """<div id="lyc_map_canvas"></div>          
+          """,
+        js_coords_lycees(etuds_by_lycee),
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def _table_etuds_lycees(
+    context, etuds, group_lycees, title, preferences, no_links=False
+):
+    etuds = [scolars.etud_add_lycee_infos(e) for e in etuds]
+    etuds_by_lycee = group_by_key(etuds, "codelycee")
+    #
+    if group_lycees:
+        L = [etuds_by_lycee[codelycee][0] for codelycee in etuds_by_lycee]
+        for l in L:
+            l["nbetuds"] = len(etuds_by_lycee[l["codelycee"]])
+        # L.sort( key=operator.itemgetter('codepostallycee', 'nomlycee') ) argh, only python 2.5+ !!!
+        L.sort(
+            cmp=lambda x, y: cmp(
+                (x["codepostallycee"], x["nomlycee"]),
+                (y["codepostallycee"], y["nomlycee"]),
+            )
+        )
+        columns_ids = (
+            "nbetuds",
+            "codelycee",
+            "codepostallycee",
+            "villelycee",
+            "nomlycee",
+        )
+        bottom_titles = {
+            "nbetuds": len(etuds),
+            "nomlycee": "%d lycées"
+            % len([x for x in etuds_by_lycee if etuds_by_lycee[x][0]["codelycee"]]),
+        }
+    else:
+        L = etuds
+        columns_ids = (
+            "sexe",
+            "nom",
+            "prenom",
+            "codelycee",
+            "codepostallycee",
+            "villelycee",
+            "nomlycee",
+        )
+        bottom_titles = None
+        if not no_links:
+            for etud in etuds:
+                etud["_nom_target"] = "ficheEtud?etudid=" + etud["etudid"]
+                etud["_prenom_target"] = "ficheEtud?etudid=" + etud["etudid"]
+                etud["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"])
+
+    tab = GenTable(
+        columns_ids=columns_ids,
+        rows=L,
+        titles={
+            "nbetuds": "Nb d'étudiants",
+            "sexe": "",
+            "nom": "Nom",
+            "prenom": "Prénom",
+            "etudid": "etudid",
+            "codelycee": "Code Lycée",
+            "codepostallycee": "Code postal",
+            "nomlycee": "Lycée",
+            "villelycee": "Commune",
+        },
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        caption=title,
+        page_title="Carte lycées d'origine",
+        html_sortable=True,
+        html_class="table_leftalign table_listegroupe",
+        bottom_titles=bottom_titles,
+        preferences=preferences,
+    )
+    return tab, etuds_by_lycee
+
+
+def formsemestre_etuds_lycees(
+    context,
+    formsemestre_id,
+    format="html",
+    only_primo=False,
+    no_grouping=False,
+    REQUEST=None,
+):
+    """Table des lycées d'origine"""
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    tab, etuds_by_lycee = formsemestre_table_etuds_lycees(
+        context, formsemestre_id, only_primo=only_primo, group_lycees=not no_grouping
+    )
+    tab.base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id)
+    if only_primo:
+        tab.base_url += "&amp;only_primo=1"
+    if no_grouping:
+        tab.base_url += "&amp;no_grouping=1"
+    t = tab.make_page(context, format=format, with_html_headers=False, REQUEST=REQUEST)
+    if format != "html":
+        return t
+    F = [
+        sco_report.tsp_form_primo_group(
+            REQUEST, only_primo, no_grouping, formsemestre_id, format
+        )
+    ]
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title=tab.page_title,
+            init_google_maps=True,
+            init_qtip=True,
+            javascripts=["js/etud_info.js", "js/map_lycees.js"],
+        ),
+        """<h2 class="formsemestre">Lycées d'origine des étudiants</h2>""",
+        "\n".join(F),
+        t,
+        """<div id="lyc_map_canvas"></div>          
+          """,
+        js_coords_lycees(etuds_by_lycee),
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def qjs(txt):  # quote for JS
+    return txt.replace("'", r"\'").replace('"', r"\"")
+
+
+def js_coords_lycees(etuds_by_lycee):
+    """Formatte liste des lycees en JSON pour Google Map"""
+    L = []
+    for codelycee in etuds_by_lycee:
+        if codelycee:
+            lyc = etuds_by_lycee[codelycee][0]
+            if not lyc.get("positionlycee", False):
+                continue
+            listeetuds = "<br/>%d étudiants: " % len(
+                etuds_by_lycee[codelycee]
+            ) + ", ".join(
+                [
+                    '<a class="discretelink" href="ficheEtud?etudid=%s" title="">%s</a>'
+                    % (e["etudid"], qjs(e["nomprenom"]))
+                    for e in etuds_by_lycee[codelycee]
+                ]
+            )
+            pos = qjs(lyc["positionlycee"])
+            legend = "%s %s" % (qjs("%(nomlycee)s (%(villelycee)s)" % lyc), listeetuds)
+            L.append(
+                "{'position' : '%s', 'name' : '%s', 'number' : %d }"
+                % (pos, legend, len(etuds_by_lycee[codelycee]))
+            )
+
+    return """<script type="text/javascript">
+          var lycees_coords = [%s];
+          </script>""" % ",".join(
+        L
+    )
diff --git a/sco_modalites.py b/sco_modalites.py
new file mode 100644
index 0000000000000000000000000000000000000000..0ce8cfe2903f2335f2e99bd2dc47f173cac176e9
--- /dev/null
+++ b/sco_modalites.py
@@ -0,0 +1,114 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Modalites des semestres
+
+La "modalite" est utilisee pour organiser les listes de semestres sur la page d'accueil.
+
+Elle n'est pas utilisée pour les parcours, ni pour rien d'autre 
+(c'est donc un attribut "cosmétique").
+
+"""
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+import sco_codes_parcours
+
+
+def list_formsemestres_modalites(context, sems):
+    """Liste ordonnée des modalités présentes dans ces formsemestres
+    """
+    modalites = {}
+    for sem in sems:
+        if sem["modalite"] not in modalites:
+            m = do_modalite_list(context, args={"modalite": sem["modalite"]})[0]
+            modalites[m["modalite"]] = m
+    modalites = modalites.values()
+    modalites.sort(key=lambda x: x["numero"])
+    return modalites
+
+
+def group_sems_by_modalite(context, sems):
+    """Given the list of fromsemestre, group them by modalite,
+    sorted in each one by semestre id and date
+    """
+    sems_by_mod = DictDefault(defaultvalue=[])
+    modalites = list_formsemestres_modalites(context, sems)
+    for modalite in modalites:
+        for sem in sems:
+            if sem["semestre_id"] < 0:  # formations en un semestre
+                sem["sortkey"] = (-100 * sem["semestre_id"], sem["dateord"])
+            else:
+                sem["sortkey"] = (sem["semestre_id"], sem["dateord"])
+            if sem["modalite"] == modalite["modalite"]:
+                sems_by_mod[modalite["modalite"]].append(sem)
+    # tri dans chaque modalité par indice de semestre et date debut
+    for modalite in modalites:
+        sems_by_mod[modalite["modalite"]].sort(key=lambda x: x["sortkey"])
+
+    return sems_by_mod, modalites
+
+
+# ------ Low level interface (database) ------
+
+_modaliteEditor = EditableTable(
+    "notes_form_modalites",
+    "form_modalite_id",
+    ("form_modalite_id", "modalite", "titre", "numero"),
+    sortkey="numero",
+    output_formators={"numero": int_null_is_zero},
+)
+
+
+def do_modalite_list(context, *args, **kw):
+    """Liste des modalites
+    """
+    cnx = context.GetDBConnexion()
+    return _modaliteEditor.list(cnx, *args, **kw)
+
+
+def do_modalite_create(context, args, REQUEST):
+    "create a modalite"
+    cnx = self.GetDBConnexion()
+    r = _modaliteEditor.create(cnx, args)
+    return r
+
+
+def do_modalite_delete(context, oid, REQUEST=None):
+    "delete a modalite"
+    cnx = self.GetDBConnexion()
+    log("do_modalite_delete: form_modalite_id=%s" % oid)
+    _modaliteEditor.delete(cnx, oid)
+
+
+def do_modalite_edit(context, *args, **kw):
+    "edit a modalite"
+    cnx = self.GetDBConnexion()
+    # check
+    m = do_modalite_list(context, {"form_modalite_id": args[0]["form_modalite_id"]})[0]
+    _modaliteEditor.edit(cnx, *args, **kw)
diff --git a/sco_moduleimpl_inscriptions.py b/sco_moduleimpl_inscriptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd30a7ef32ef2dbd6bbc0e9342bee16dc098bd99
--- /dev/null
+++ b/sco_moduleimpl_inscriptions.py
@@ -0,0 +1,588 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
+"""
+
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from scolog import logdb
+from notes_table import *
+import sco_formsemestre
+from sco_formsemestre_status import makeMenu
+import sco_groups
+
+
+def moduleimpl_inscriptions_edit(
+    context, moduleimpl_id, etuds=[], submitted=False, REQUEST=None
+):
+    """Formulaire inscription des etudiants a ce module
+    * Gestion des inscriptions
+         Nom          TD     TA    TP  (triable)
+     [x] M. XXX YYY   -      -     -
+     
+     
+     ajouter TD A, TD B, TP 1, TP 2 ...
+     supprimer TD A, TD B, TP 1, TP 2 ...
+     
+     * Si pas les droits: idem en readonly
+    """
+    M = context.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+    formsemestre_id = M["formsemestre_id"]
+    mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    # -- check lock
+    if sem["etat"] != "1":
+        raise ScoValueError("opération impossible: semestre verrouille")
+    header = context.sco_header(
+        REQUEST,
+        page_title="Inscription au module",
+        init_qtip=True,
+        javascripts=["js/etud_info.js"],
+    )
+    footer = context.sco_footer(REQUEST)
+    H = [
+        header,
+        """<h2>Inscriptions au module <a href="moduleimpl_status?moduleimpl_id=%s">%s</a> (%s)</a></h2>
+    <p class="help">Cette page permet d'éditer les étudiants inscrits à ce module
+    (ils doivent évidemment être inscrits au semestre).
+    Les étudiants cochés sont (ou seront) inscrits. Vous pouvez facilement inscrire ou
+    désinscrire tous les étudiants d'un groupe à l'aide des menus "Ajouter" et "Enlever".
+    </p>
+    <p class="help">Aucune modification n'est prise en compte tant que l'on n'appuie pas sur le bouton
+    "Appliquer les modifications".
+    </p>
+    """
+        % (moduleimpl_id, mod["titre"], mod["code"]),
+    ]
+    # Liste des inscrits à ce semestre
+    inscrits = context.Notes.do_formsemestre_inscription_listinscrits(formsemestre_id)
+    for ins in inscrits:
+        etuds_info = context.getEtudInfo(etudid=ins["etudid"], filled=1)
+        if not etuds_info:
+            log(
+                "moduleimpl_inscriptions_edit: incoherency for etudid=%s !"
+                % ins["etudid"]
+            )
+            raise ScoValueError(
+                "Etudiant %s inscrit mais inconnu dans la base !!!!!" % ins["etudid"]
+            )
+        ins["etud"] = etuds_info[0]
+    inscrits.sort(lambda x, y: cmp(x["etud"]["nom"], y["etud"]["nom"]))
+    in_m = context.do_moduleimpl_inscription_list(moduleimpl_id=M["moduleimpl_id"])
+    in_module = set([x["etudid"] for x in in_m])
+    #
+    partitions = sco_groups.get_partitions_list(context, formsemestre_id)
+    #
+    if not submitted:
+        H.append(
+            """<script type="text/javascript">
+    function group_select(groupName, partitionIdx, check) {
+    var nb_inputs_to_skip = 2; // nb d'input avant les checkbox !!!
+    var elems = document.getElementById("mi_form").getElementsByTagName("input");
+
+    if (partitionIdx==-1) {
+      for (var i =nb_inputs_to_skip; i < elems.length; i++) {
+         elems[i].checked=check;
+      }
+    } else {
+     for (var i =nb_inputs_to_skip; i < elems.length; i++) {
+       var cells = elems[i].parentNode.parentNode.getElementsByTagName("td")[partitionIdx].childNodes;
+       if (cells.length && cells[0].nodeValue == groupName) {
+          elems[i].checked=check;
+       }      
+     }
+    }
+    }
+
+    </script>"""
+        )
+        H.append("""<form method="post" id="mi_form" action="%s">""" % REQUEST.URL0)
+        H.append(
+            """        
+        <input type="hidden" name="moduleimpl_id" value="%(moduleimpl_id)s"/>
+        <input type="submit" name="submitted" value="Appliquer les modifications"/><p></p>
+        """
+            % M
+        )
+        H.append('<table><tr>')
+        H.append(_make_menu(context, partitions, "Ajouter", "true"))
+        H.append(_make_menu(context, partitions, "Enlever", "false"))
+        H.append('</tr></table>')
+        H.append(
+            """
+        <p><br/></p>
+        <table class="sortable" id="mi_table"><tr>
+        <th>Nom</th>"""
+            % sem
+        )
+        for partition in partitions:
+            if partition["partition_name"]:
+                H.append("<th>%s</th>" % partition["partition_name"])
+        H.append("</tr>")
+
+        for ins in inscrits:
+            etud = ins["etud"]
+            if etud["etudid"] in in_module:
+                checked = 'checked="checked"'
+            else:
+                checked = ""
+            H.append(
+                """<tr><td><input type="checkbox" name="etuds:list" value="%s" %s>"""
+                % (etud["etudid"], checked)
+            )
+            H.append(
+                """<a class="discretelink etudinfo" href="ficheEtud?etudid=%s" id="%s">%s</a>"""
+                % (etud["etudid"], etud["etudid"], etud["nomprenom"])
+            )
+            H.append("""</input></td>""")
+
+            groups = sco_groups.get_etud_groups(context, etud["etudid"], sem)
+            for partition in partitions:
+                if partition["partition_name"]:
+                    gr_name = ""
+                    for group in groups:
+                        if group["partition_id"] == partition["partition_id"]:
+                            gr_name = group["group_name"]
+                            break
+                    # gr_name == '' si etud non inscrit dans un groupe de cette partition
+                    H.append("<td>%s</td>" % gr_name)
+        H.append("""</table></form>""")
+    else:  # SUBMISSION
+        # inscrit a ce module tous les etuds selectionnes
+        context.do_moduleimpl_inscrit_etuds(
+            moduleimpl_id, formsemestre_id, etuds, reset=True, REQUEST=REQUEST
+        )
+        REQUEST.RESPONSE.redirect(
+            "moduleimpl_status?moduleimpl_id=%s" % (moduleimpl_id)
+        )
+    #
+    H.append(footer)
+    return "\n".join(H)
+
+
+def _make_menu_old_xxx(context, partitions, title="", check="true"):
+    H = [
+        """<div class="barrenav"><ul class="nav">
+    <li onmouseover="MenuDisplay(this)" onmouseout="MenuHide(this)"><a href="#" class="menu custommenu">%s</a><ul>"""
+        % title
+    ]
+    # 'tous'
+    H.append(
+        """<li><a href="#" onclick="group_select('', -1, %s)">Tous</a></li>""" % (check)
+    )
+    p_idx = 0
+    for partition in partitions:
+        if partition["partition_name"] != None:
+            p_idx += 1
+            for group in sco_groups.get_partition_groups(context, partition):
+                H.append(
+                    """<li><a href="#" onclick="group_select('%s', %s, %s)">%s %s</a></li>"""
+                    % (
+                        group["group_name"],
+                        p_idx,
+                        check,
+                        partition["partition_name"],
+                        group["group_name"],
+                    )
+                )
+
+    H.append("</ul></ul></div>")
+
+    return "".join(H)
+
+def _make_menu(context, partitions, title="", check="true"):
+    """Menu with list of all groups"""
+    items = [
+        {
+            "title" : "Tous",
+            "attr" : "onclick=\"group_select('', -1, %s)\"" % check
+        }
+    ]
+    p_idx = 0
+    for partition in partitions:
+        if partition["partition_name"] != None:
+            p_idx += 1
+            for group in sco_groups.get_partition_groups(context, partition):
+                items.append(
+                    {
+                        "title" : "%s %s" % (partition["partition_name"], group["group_name"]),
+                        "attr" : "onclick=\"group_select('%s', %s, %s)\"" % (
+                            group["group_name"], p_idx, check)
+                    }
+                )
+    return '<td class="inscr_addremove_menu">' + makeMenu( title, items, base_url=context.absolute_url(), alone=True ) + "</td>"
+
+def moduleimpl_inscriptions_stats(context, formsemestre_id, REQUEST=None):
+    """Affiche quelques informations sur les inscriptions
+    aux modules de ce semestre.
+
+    Inscrits au semestre: <nb>
+
+    Modules communs (tous inscrits): <liste des modules (codes)
+
+    Autres modules: (regroupés par UE)
+    UE 1
+    <code du module>: <nb inscrits> (<description en termes de groupes>)
+    ...
+
+
+    descriptions:
+      groupes de TD A, B et C
+      tous sauf groupe de TP Z (?)
+      tous sauf <liste d'au plus 7 noms>
+      
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    inscrits = context.do_formsemestre_inscription_list(
+        args={"formsemestre_id": formsemestre_id}
+    )
+    set_all = set([x["etudid"] for x in inscrits])
+    partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
+        context, formsemestre_id
+    )
+
+    can_change = authuser.has_permission(ScoEtudInscrit, context) and sem["etat"] == "1"
+
+    # Liste des modules
+    Mlist = context.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
+    # Decrit les inscriptions aux modules:
+    commons = []  # modules communs a tous les etuds du semestre
+    options = []  # modules ou seuls quelques etudiants sont inscrits
+    for mod in Mlist:
+        all, nb_inscrits, descr = descr_inscrs_module(
+            context,
+            sem,
+            mod["moduleimpl_id"],
+            set_all,
+            partitions,
+            partitions_etud_groups,
+        )
+        if all:
+            commons.append(mod)
+        else:
+            mod["descri"] = descr
+            mod["nb_inscrits"] = nb_inscrits
+            options.append(mod)
+    # Page HTML:
+    H = [context.html_sem_header(REQUEST, "Inscriptions aux modules du semestre")]
+
+    H.append("<h3>Inscrits au semestre: %d étudiants</h3>" % len(inscrits))
+
+    if options:
+        H.append("<h3>Modules auxquels tous les étudiants ne sont pas inscrits:</h3>")
+        H.append(
+            '<table class="formsemestre_status formsemestre_inscr"><tr><th>UE</th><th>Code</th><th>Inscrits</th><th></th></tr>'
+        )
+        for mod in options:
+            if can_change:
+                c_link = (
+                    '<a class="discretelink" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">%s</a>'
+                    % (mod["moduleimpl_id"], mod["descri"])
+                )
+            else:
+                c_link = mod["descri"]
+            H.append(
+                '<tr class="formsemestre_status"><td>%s</td><td class="formsemestre_status_code">%s</td><td class="formsemestre_status_inscrits">%s</td><td>%s</td></tr>'
+                % (
+                    mod["ue"]["acronyme"],
+                    mod["module"]["code"],
+                    mod["nb_inscrits"],
+                    c_link,
+                )
+            )
+        H.append("</table>")
+    else:
+        H.append(
+            '<span style="font-size:110%; font-style:italic; color: red;"">Tous les étudiants sont inscrits à tous les modules.</span>'
+        )
+
+    if commons:
+        H.append(
+            "<h3>Modules communs (auxquels tous les étudiants sont inscrits):</h3>"
+        )
+        H.append(
+            '<table class="formsemestre_status formsemestre_inscr"><tr><th>UE</th><th>Code</th><th>Module</th></tr>'
+        )
+        for mod in commons:
+            if can_change:
+                c_link = (
+                    '<a class="discretelink" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">%s</a>'
+                    % (mod["moduleimpl_id"], mod["module"]["titre"])
+                )
+            else:
+                c_link = mod["module"]["titre"]
+            H.append(
+                '<tr class="formsemestre_status_green"><td>%s</td><td class="formsemestre_status_code">%s</td><td>%s</td></tr>'
+                % (mod["ue"]["acronyme"], mod["module"]["code"], c_link)
+            )
+        H.append("</table>")
+
+    # Etudiants "dispensés" d'une UE (capitalisée)
+    UECaps = get_etuds_with_capitalized_ue(context, formsemestre_id)
+    if UECaps:
+        H.append('<h3>Etudiants avec UEs capitalisées:</h3><ul class="ue_inscr_list">')
+        ues = [context.do_ue_list({"ue_id": ue_id})[0] for ue_id in UECaps.keys()]
+        ues.sort(key=lambda u: u["numero"])
+        for ue in ues:
+            H.append(
+                '<li class="tit"><span class="tit">%(acronyme)s: %(titre)s</span>' % ue
+            )
+            H.append("<ul>")
+            for info in UECaps[ue["ue_id"]]:
+                etud = context.getEtudInfo(etudid=info["etudid"], filled=True)[0]
+                H.append(
+                    '<li class="etud"><a class="discretelink" href="ficheEtud?etudid=%(etudid)s">%(nomprenom)s</a>'
+                    % etud
+                )
+                if info["ue_status"]["event_date"]:
+                    H.append(
+                        "(cap. le %s)"
+                        % (info["ue_status"]["event_date"]).strftime("%d/%m/%Y")
+                    )
+
+                if info["is_ins"]:
+                    dm = ", ".join(
+                        [
+                            m["code"] or m["abbrev"] or "pas_de_code"
+                            for m in info["is_ins"]
+                        ]
+                    )
+                    H.append(
+                        'actuellement inscrit dans <a title="%s" class="discretelink">%d modules</a>'
+                        % (dm, len(info["is_ins"]))
+                    )
+                    if info["ue_status"]["is_capitalized"]:
+                        H.append(
+                            """<div><em style="font-size: 70%">UE actuelle moins bonne que l'UE capitalisée</em></div>"""
+                        )
+                    else:
+                        H.append(
+                            """<div><em style="font-size: 70%">UE actuelle meilleure que l'UE capitalisée</em></div>"""
+                        )
+                    if can_change:
+                        H.append(
+                            '<div><a class="stdlink" href="etud_desinscrit_ue?etudid=%s&amp;formsemestre_id=%s&amp;ue_id=%s">désinscrire des modules de cette UE</a></div>'
+                            % (etud["etudid"], formsemestre_id, ue["ue_id"])
+                        )
+                else:
+                    H.append("(non réinscrit dans cette UE)")
+                    if can_change:
+                        H.append(
+                            '<div><a class="stdlink" href="etud_inscrit_ue?etudid=%s&amp;formsemestre_id=%s&amp;ue_id=%s">inscrire à tous les modules de cette UE</a></div>'
+                            % (etud["etudid"], formsemestre_id, ue["ue_id"])
+                        )
+                H.append("</li>")
+            H.append("</ul></li>")
+        H.append("</ul>")
+
+        H.append(
+            """<hr/><p class="help">Cette page décrit les inscriptions actuelles. 
+        Vous pouvez changer (si vous en avez le droit) les inscrits dans chaque module en 
+        cliquant sur la ligne du module.</p>
+        <p  class="help">Note: la déinscription d'un module ne perd pas les notes. Ainsi, si 
+        l'étudiant est ensuite réinscrit au même module, il retrouvera ses notes.</p>
+        """
+        )
+
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def descr_inscrs_module(
+    context, sem, moduleimpl_id, set_all, partitions, partitions_etud_groups
+):
+    """returns All, nb_inscrits, descr      All true si tous inscrits
+    """
+    ins = context.do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
+    set_m = set([x["etudid"] for x in ins])  # ens. des inscrits au module
+    non_inscrits = set_all - set_m
+    if len(non_inscrits) == 0:
+        return True, len(ins), ""  # tous inscrits
+    if len(non_inscrits) <= 7:  # seuil arbitraire
+        return False, len(ins), "tous sauf " + _fmt_etud_set(context, non_inscrits)
+    # Cherche les groupes:
+    gr = []  #  [ ( partition_name , [ group_names ] ) ]
+    for partition in partitions:
+        grp = []  # groupe de cette partition
+        for group in sco_groups.get_partition_groups(context, partition):
+            members = sco_groups.get_group_members(context, group["group_id"])
+            set_g = set([m["etudid"] for m in members])
+            if set_g.issubset(set_m):
+                grp.append(group["group_name"])
+                set_m = set_m - set_g
+        gr.append((partition["partition_name"], grp))
+    #
+    d = []
+    for (partition_name, grp) in gr:
+        if grp:
+            d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
+    r = []
+    if d:
+        r.append(", ".join(d))
+    if set_m:
+        r.append(_fmt_etud_set(context, set_m))
+    #
+    return False, len(ins), " et ".join(r)
+
+
+def _fmt_etud_set(context, ins, max_list_size=7):
+    # max_list_size est le nombre max de noms d'etudiants listés
+    # au delà, on indique juste le nombre, sans les noms.
+    if len(ins) > max_list_size:
+        return "%d étudiants" % len(ins)
+    etuds = []
+    for etudid in ins:
+        etuds.append(context.getEtudInfo(etudid=etudid, filled=True)[0])
+    etuds.sort(lambda x, y: cmp(x["nom"], y["nom"]))
+    return ", ".join(
+        [
+            '<a class="discretelink" href="ficheEtud?etudid=%(etudid)s">%(nomprenom)s</a>'
+            % etud
+            for etud in etuds
+        ]
+    )
+
+
+def get_etuds_with_capitalized_ue(context, formsemestre_id):
+    """For each UE, computes list of students capitalizing the UE.
+    returns { ue_id : [ { infos } ] }
+    """
+    UECaps = DictDefault(defaultvalue=[])
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_ues, get_etud_ue_status
+    inscrits = context.do_formsemestre_inscription_list(
+        args={"formsemestre_id": formsemestre_id}
+    )
+    ues = nt.get_ues()
+    for ue in ues:
+        for etud in inscrits:
+            status = nt.get_etud_ue_status(etud["etudid"], ue["ue_id"])
+            if status["was_capitalized"]:
+                UECaps[ue["ue_id"]].append(
+                    {
+                        "etudid": etud["etudid"],
+                        "ue_status": status,
+                        "is_ins": is_inscrit_ue(
+                            context, etud["etudid"], formsemestre_id, ue["ue_id"]
+                        ),
+                    }
+                )
+    return UECaps
+
+
+def is_inscrit_ue(context, etudid, formsemestre_id, ue_id):
+    """Modules de cette UE dans ce semestre
+    auxquels l'étudiant est inscrit.
+    """
+    r = SimpleDictFetch(
+        context,
+        """SELECT mod.*
+    FROM 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
+    AND mod.module_id = mi.module_id
+    AND mod.ue_id = %(ue_id)s
+    AND i.moduleimpl_id = mi.moduleimpl_id
+    AND i.etudid = %(etudid)s
+    ORDER BY mod.numero
+    """,
+        {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
+    )
+    return r
+
+
+def do_etud_desinscrit_ue(context, etudid, formsemestre_id, ue_id, REQUEST=None):
+    """Desincrit l'etudiant de tous les modules de cette UE dans ce semestre.
+    """
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        """DELETE FROM notes_moduleimpl_inscription 
+    WHERE moduleimpl_inscription_id IN (
+      SELECT i.moduleimpl_inscription_id FROM
+        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
+      AND mod.module_id = mi.module_id
+      AND mod.ue_id = %(ue_id)s
+      AND i.moduleimpl_id = mi.moduleimpl_id
+      AND i.etudid = %(etudid)s
+    )
+    """,
+        {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
+    )
+    if REQUEST:
+        logdb(
+            REQUEST,
+            cnx,
+            method="etud_desinscrit_ue",
+            etudid=etudid,
+            msg="desinscription UE %s" % ue_id,
+            commit=False,
+        )
+    context._inval_cache(
+        formsemestre_id=formsemestre_id
+    )  # > desinscription etudiant des modules
+
+
+def do_etud_inscrit_ue(context, etudid, formsemestre_id, ue_id, REQUEST=None):
+    """Incrit l'etudiant de tous les modules de cette UE dans ce semestre.
+    """
+    # Verifie qu'il est bien inscrit au semestre
+    insem = context.do_formsemestre_inscription_list(
+        args={"formsemestre_id": formsemestre_id, "etudid": etudid}
+    )
+    if not insem:
+        raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)
+
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        """SELECT mi.moduleimpl_id 
+      FROM notes_moduleimpl mi, notes_modules mod, notes_formsemestre sem
+      WHERE sem.formsemestre_id = %(formsemestre_id)s
+      AND mi.formsemestre_id = sem.formsemestre_id
+      AND mod.module_id = mi.module_id
+      AND mod.ue_id = %(ue_id)s
+     """,
+        {"formsemestre_id": formsemestre_id, "ue_id": ue_id},
+    )
+    res = cursor.dictfetchall()
+    for moduleimpl_id in [x["moduleimpl_id"] for x in res]:
+        context.do_moduleimpl_inscription_create(
+            {"moduleimpl_id": moduleimpl_id, "etudid": etudid},
+            REQUEST=REQUEST,
+            formsemestre_id=formsemestre_id,
+        )
diff --git a/sco_moduleimpl_status.py b/sco_moduleimpl_status.py
new file mode 100644
index 0000000000000000000000000000000000000000..e9d969f5141762f9e501039eb91a46fe6dc6912e
--- /dev/null
+++ b/sco_moduleimpl_status.py
@@ -0,0 +1,580 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Tableau de bord module
+"""
+from sets import Set
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+from notes_table import *
+import sco_groups
+import sco_evaluations
+import htmlutils
+import sco_excel
+from gen_tables import GenTable
+from htmlutils import histogram_notes
+import sco_formsemestre
+import sco_formsemestre_status
+from sco_formsemestre_status import makeMenu
+import sco_compute_moy
+
+
+# ported from old DTML code in oct 2009
+
+# menu evaluation dans moduleimpl
+def moduleimpl_evaluation_menu(context, evaluation_id, nbnotes=0, REQUEST=None):
+    "Menu avec actions sur une evaluation"
+    authuser = REQUEST.AUTHENTICATED_USER
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+    modimpl = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+
+    group_id = sco_groups.get_default_group(context, modimpl["formsemestre_id"])
+
+    if (
+        context.can_edit_notes(
+            REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False
+        )
+        and nbnotes != 0
+    ):
+        sup_label = "Supprimer évaluation impossible (il y a des notes)"
+    else:
+        sup_label = "Supprimer évaluation"
+
+    menuEval = [
+        {
+            "title": "Saisir notes",
+            "url": "saisie_notes?evaluation_id=" + evaluation_id,
+            "enabled": context.can_edit_notes(
+                REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"]
+            ),
+        },
+        {
+            "title": "Modifier évaluation",
+            "url": "evaluation_edit?evaluation_id=" + evaluation_id,
+            "enabled": context.can_edit_notes(
+                REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False
+            ),
+        },
+        {
+            "title": sup_label,
+            "url": "evaluation_delete?evaluation_id=" + evaluation_id,
+            "enabled": nbnotes == 0
+            and context.can_edit_notes(
+                REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False
+            ),
+        },
+        {
+            "title": "Supprimer toutes les notes",
+            "url": "evaluation_suppress_alln?evaluation_id=" + evaluation_id,
+            "enabled": context.can_edit_notes(
+                REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False
+            ),
+        },
+        {
+            "title": "Afficher les notes",
+            "url": "evaluation_listenotes?evaluation_id=" + evaluation_id,
+            "enabled": nbnotes > 0,
+        },
+        {
+            "title": "Placement étudiants",
+            "url": "placement_eval_selectetuds?evaluation_id=" + evaluation_id,
+            "enabled": nbnotes == 0
+            and context.can_edit_notes(REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"]),
+        },
+        {
+            "title": "Absences ce jour",
+            "url": "Absences/EtatAbsencesDate?date=%s&amp;group_ids=%s"
+            % (urllib.quote(E["jour"], safe=""), group_id),
+            "enabled": E["jour"],
+        },
+        {
+            "title": "Vérifier notes vs absents",
+            "url": "evaluation_check_absences_html?evaluation_id=%s" % (evaluation_id),
+            "enabled": nbnotes > 0 and E["jour"],
+        },
+    ]
+
+    return makeMenu("actions", menuEval, alone=True)
+
+
+def moduleimpl_status(context, moduleimpl_id=None, partition_id=None, REQUEST=None):
+    """Tableau de bord module (liste des evaluations etc)"""
+    authuser = REQUEST.AUTHENTICATED_USER
+    M = context.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
+    formsemestre_id = M["formsemestre_id"]
+    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    ModInscrits = context.do_moduleimpl_inscription_list(
+        moduleimpl_id=M["moduleimpl_id"]
+    )
+
+    nt = context._getNotesCache().get_NotesTable(context, formsemestre_id)
+    ModEvals = context.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
+    ModEvals.sort(
+        key=lambda x: (x["numero"], x["jour"], x["heure_debut"]), reverse=True
+    )  # la plus RECENTE en tête
+
+    #
+    caneditevals = context.can_edit_notes(
+        authuser, moduleimpl_id, allow_ens=sem["ens_can_edit_eval"]
+    )
+    caneditnotes = context.can_edit_notes(authuser, moduleimpl_id)
+    arrow_up, arrow_down, arrow_none = sco_groups.getArrowIconsTags(context, REQUEST)
+    #
+    H = [
+        context.sco_header(REQUEST, page_title="Module %(titre)s" % Mod),
+        """<h2 class="formsemestre">Module <tt>%(code)s</tt> %(titre)s</h2>""" % Mod,
+        # XXX """caneditevals=%s caneditnotes=%s""" % (caneditevals,caneditnotes),
+        """<div class="moduleimpl_tableaubord">
+
+<table>
+<tr>
+<td class="fichetitre2">Responsable: </td><td class="redboldtext">""",
+        context.Users.user_info(M["responsable_id"])["nomprenom"],
+        """<span class="blacktt">(%(responsable_id)s)</span>""" % M,
+    ]
+    try:
+        context.can_change_module_resp(REQUEST, moduleimpl_id)
+        H.append(
+            """<a class="stdlink" href="edit_moduleimpl_resp?moduleimpl_id=%s">modifier</a>"""
+            % moduleimpl_id
+        )
+    except:
+        pass
+    H.append("""</td><td>""")
+    H.append(
+        ", ".join([context.Users.user_info(m["ens_id"])["nomprenom"] for m in M["ens"]])
+    )
+    H.append("""</td><td>""")
+    try:
+        context.can_change_ens(REQUEST, moduleimpl_id)
+        H.append(
+            """<a class="stdlink" href="edit_enseignants_form?moduleimpl_id=%s">modifier les enseignants</a>"""
+            % moduleimpl_id
+        )
+    except:
+        pass
+    H.append("""</td></tr>""")
+
+    # 2ieme ligne: Semestre, Coef
+    H.append("""<tr><td class="fichetitre2">""")
+    if sem["semestre_id"] >= 0:
+        H.append("""Semestre: </td><td>%s""" % sem["semestre_id"])
+    else:
+        H.append("""</td><td>""")
+    if sem["etat"] != "1":
+        H.append(icontag("lock32_img", title="verrouillé"))
+    H.append(
+        """</td><td class="fichetitre2">Coef dans le semestre: %(coefficient)s</td><td></td></tr>"""
+        % Mod
+    )
+    # 3ieme ligne: Formation
+    H.append(
+        """<tr><td class="fichetitre2">Formation: </td><td>%(titre)s</td></tr>""" % F
+    )
+    # Ligne: Inscrits
+    H.append(
+        """<tr><td class="fichetitre2">Inscrits: </td><td> %d étudiants"""
+        % len(ModInscrits)
+    )
+    if authuser.has_permission(ScoEtudInscrit, context):
+        H.append(
+            """<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">modifier</a>"""
+            % M["moduleimpl_id"]
+        )
+    H.append("</td></tr>")
+    # Ligne: règle de calcul
+    has_expression = sco_compute_moy.moduleimpl_has_expression(context, M)
+    if has_expression:
+        H.append(
+            '<tr><td class="fichetitre2" colspan="4">Règle de calcul: <span class="formula" title="mode de calcul de la moyenne du module">moyenne=<tt>%s</tt></span>'
+            % M["computation_expr"]
+        )
+        if context.can_change_ens(REQUEST, moduleimpl_id, raise_exc=False):
+            H.append(
+                '<span class="fl"><a class="stdlink"  href="edit_moduleimpl_expr?moduleimpl_id=%s">modifier</a></span>'
+                % moduleimpl_id
+            )
+        H.append("</td></tr>")
+    else:
+        t0, t1 = "<em>règle de calcul standard</em>", ""
+        H.append(
+            '<tr><td colspan="4"><em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
+        )
+        if context.can_change_ens(REQUEST, moduleimpl_id, raise_exc=False):
+            H.append(
+                ' (<a class="stdlink" href="edit_moduleimpl_expr?moduleimpl_id=%s">changer</a>)'
+                % moduleimpl_id
+            )
+        H.append("</td></tr>")
+    H.append(
+        '<tr><td colspan="2"><a class="stdlink" href="view_module_abs?moduleimpl_id=%s">Absences</a>&nbsp;'
+        % moduleimpl_id
+    )
+    H.append("</table>")
+    #
+    if has_expression and nt.expr_diagnostics:
+        H.append(
+            sco_formsemestre_status.html_expr_diagnostic(context, nt.expr_diagnostics)
+        )
+    #
+    if nt.sem_has_decisions():
+        H.append(
+            """<ul class="tf-msg"><li class="tf-msg warning">Décisions de jury saisies: seul le responsable du semestre peut saisir des notes (il devra modifier les décisions de jury).</li></ul>"""
+        )
+    #
+    H.append(
+        """<p><form name="f"><span style="font-size:120%%; font-weight: bold;">%d évaluations :</span>
+<span style="padding-left: 30px;">
+<input type="hidden" name="moduleimpl_id" value="%s"/>"""
+        % (len(ModEvals), moduleimpl_id)
+    )
+    #
+    # Liste les noms de partitions
+    partitions = sco_groups.get_partitions_list(context, sem["formsemestre_id"])
+    H.append(
+        """Afficher les groupes de&nbsp;<select name="partition_id" onchange="document.f.submit();">"""
+    )
+    for partition in partitions:
+        if partition["partition_id"] == partition_id:
+            selected = "selected"
+        else:
+            selected = ""
+        name = partition["partition_name"]
+        if name is None:
+            name = "Tous"
+        H.append(
+            """<option value="%s" %s>%s</option>"""
+            % (partition["partition_id"], selected, name)
+        )
+    H.append(
+        """</select>
+&nbsp;&nbsp;&nbsp;&nbsp;
+<a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%(moduleimpl_id)s">Voir toutes les notes</a>
+</span>
+</form>
+</p>
+"""
+        % M
+    )
+
+    # -------- Tableau des evaluations
+    top_table_links = ""
+    if sem["etat"] == "1":  # non verrouillé
+        top_table_links = (
+            """<a class="stdlink" href="evaluation_create?moduleimpl_id=%(moduleimpl_id)s">Créer nouvelle évaluation</a>
+        <a class="stdlink" style="margin-left:2em;" href="module_evaluation_renumber?moduleimpl_id=%(moduleimpl_id)s&amp;redirect=1">Trier par date</a>
+        """
+            % M
+        )
+    if ModEvals:
+        H.append(
+            '<div class="moduleimpl_evaluations_top_links">'
+            + top_table_links
+            + "</div>"
+        )
+    H.append("""<table class="moduleimpl_evaluations">""")
+    eval_index = len(ModEvals) - 1
+    first = True
+    for eval in ModEvals:
+        etat = sco_evaluations.do_evaluation_etat(
+            context,
+            eval["evaluation_id"],
+            partition_id=partition_id,
+            select_first_partition=True,
+        )
+        if eval["evaluation_type"] == EVALUATION_RATTRAPAGE:
+            tr_class = "mievr_rattr"
+        else:
+            tr_class = "mievr"
+        tr_class_1 = "mievr"
+        if first:
+            first = False
+        else:
+            H.append("""<tr><td colspan="8">&nbsp;</td></tr>""")
+            tr_class_1 += " mievr_spaced"
+        H.append("""<tr class="%s"><td class="mievr_tit" colspan="8">""" % tr_class_1)
+        if eval["jour"]:
+            H.append("""Le %(jour)s%(descrheure)s""" % eval)
+        else:
+            H.append(
+                """<a href="evaluation_edit?evaluation_id=%(evaluation_id)s" class="mievr_evalnodate">Evaluation sans date</a>"""
+                % eval
+            )
+        H.append("&nbsp;&nbsp;&nbsp; <em>%(description)s</em>" % eval)
+        if eval["evaluation_type"] == EVALUATION_RATTRAPAGE:
+            H.append("""<span class="mievr_rattr">rattrapage</span>""")
+        if etat["last_modif"]:
+            H.append(
+                """<span class="mievr_lastmodif">(dernière modif le %s)</span>"""
+                % etat["last_modif"].strftime("%d/%m/%Y à %Hh%M")
+            )
+        H.append('<span class="evalindex_cont">')
+        if has_expression:
+            H.append(
+                """<span class="evalindex" title="Indice dans les vecteurs (formules)">%02d</span>"""
+                % eval_index
+            )
+
+        # Fleches:
+        H.append('<span class="eval_arrows_chld">')
+        if eval_index != (len(ModEvals) - 1) and caneditevals:
+            H.append(
+                '<a href="module_evaluation_move?evaluation_id=%s&amp;after=0" class="aud">%s</a>'
+                % (eval["evaluation_id"], arrow_up)
+            )
+        else:
+            H.append(arrow_none)
+        if (eval_index > 0) and caneditevals:
+            H.append(
+                '<a href="module_evaluation_move?evaluation_id=%s&amp;after=1" class="aud">%s</a>'
+                % (eval["evaluation_id"], arrow_down)
+            )
+        else:
+            H.append(arrow_none)
+        H.append("</span></span>")
+
+        eval_index -= 1
+        H.append("""</td></tr>""")
+        H.append(
+            """<tr class="%s"><th class="moduleimpl_evaluations" colspan="2">&nbsp;</th><th class="moduleimpl_evaluations">Durée</th><th class="moduleimpl_evaluations">Coef.</th><th class="moduleimpl_evaluations">Notes</th><th class="moduleimpl_evaluations">Abs</th><th class="moduleimpl_evaluations">N</th><th class="moduleimpl_evaluations">Moyenne """
+            % tr_class
+        )
+
+        if etat["evalcomplete"]:
+            etat_txt = """(prise en compte)"""
+            etat_descr = "notes utilisées dans les moyennes"
+        elif eval["publish_incomplete"] != "0":
+            etat_txt = """(prise en compte <b>immédiate</b>)"""
+            etat_descr = (
+                "il manque des notes, mais la prise en compte immédiate a été demandée"
+            )
+        elif etat["nb_notes"] != 0:
+            etat_txt = "(<b>non</b> prise en compte)"
+            etat_descr = "il manque des notes"
+        else:
+            etat_txt = ""
+        if caneditevals and etat_txt:
+            etat_txt = (
+                '<a href="evaluation_edit?evaluation_id=%s" title="%s">%s</a>'
+                % (eval["evaluation_id"], etat_descr, etat_txt)
+            )
+        H.append(etat_txt)
+        H.append("""</th></tr>""")
+
+        H.append("""<tr class="%s"><td class="mievr">""" % tr_class)
+        if caneditevals:
+            H.append(
+                """<a class="smallbutton" href="evaluation_edit?evaluation_id=%s">%s</a>"""
+                % (
+                    eval["evaluation_id"],
+                    icontag("edit_img", alt="modifier", title="Modifier informations"),
+                )
+            )
+        if caneditnotes:
+            H.append(
+                """<a class="smallbutton" href="saisie_notes?evaluation_id=%s">%s</a>"""
+                % (
+                    eval["evaluation_id"],
+                    icontag("notes_img", alt="saisie notes", title="Saisie des notes"),
+                )
+            )
+        if etat["nb_notes"] == 0:
+            if caneditevals:
+                H.append(
+                    """<a class="smallbutton" href="evaluation_delete?evaluation_id=%(evaluation_id)s">"""
+                    % eval
+                )
+            H.append(icontag("delete_img", alt="supprimer", title="Supprimer"))
+            if caneditevals:
+                H.append("""</a>""")
+        elif etat["evalcomplete"]:
+            H.append(
+                """<a class="smallbutton" href="evaluation_listenotes?evaluation_id=%s">%s</a>"""
+                % (eval["evaluation_id"], icontag("status_green_img", title="ok"))
+            )
+        else:
+            if etat["evalattente"]:
+                H.append(
+                    """<a class="smallbutton" href="evaluation_listenotes?evaluation_id=%s">%s</a>"""
+                    % (
+                        eval["evaluation_id"],
+                        icontag(
+                            "status_greenorange_img",
+                            file_format="gif",
+                            title="notes en attente",
+                        ),
+                    )
+                )
+            else:
+                H.append(
+                    """<a class="smallbutton" href="evaluation_listenotes?evaluation_id=%s">%s</a>"""
+                    % (
+                        eval["evaluation_id"],
+                        icontag("status_orange_img", title="il manque des notes"),
+                    )
+                )
+        #
+        if eval["visibulletin"] == "1":
+            H.append(
+                icontag(
+                    "status_visible_img", title="visible dans bulletins intermédiaires"
+                )
+            )
+        else:
+            H.append("&nbsp;")
+        H.append('</td><td class="mievr_menu">')
+        if caneditnotes:
+            H.append(
+                moduleimpl_evaluation_menu(
+                    context,
+                    eval["evaluation_id"],
+                    nbnotes=etat["nb_notes"],
+                    REQUEST=REQUEST,
+                )
+            )
+        H.append("</td>")
+        #
+        H.append(
+            """
+<td class="mievr_dur">%s</td><td class="rightcell mievr_coef">%s</td>"""
+            % (eval["duree"], "%g" % eval["coefficient"])
+        )
+        H.append(
+            """<td class="rightcell mievr_nbnotes">%(nb_notes)s / %(nb_inscrits)s</td>
+<td class="rightcell mievr_coef">%(nb_abs)s</td>
+<td class="rightcell mievr_coef">%(nb_neutre)s</td>
+<td class="rightcell">"""
+            % etat
+        )
+        if etat["moy"]:
+            H.append("%s / %g" % (etat["moy"], eval["note_max"]))
+        else:
+            H.append(
+                """<a class="redlink" href="saisie_notes?evaluation_id=%s">saisir notes</a>"""
+                % (eval["evaluation_id"])
+            )
+        H.append("""</td></tr>""")
+        #
+        if etat["nb_notes"] == 0:
+            H.append("""<tr class="%s"><td colspan="8">&nbsp;""" % tr_class)
+            # XXX
+            H.append("""</td></tr>""")
+        else:  # il y a deja des notes saisies
+            gr_moyennes = etat["gr_moyennes"]
+            for gr_moyenne in gr_moyennes:
+                H.append("""<tr class="%s">""" % tr_class)
+                H.append("""<td colspan="2">&nbsp;</td>""")
+                if gr_moyenne["group_name"] is None:
+                    name = "Tous"  # tous
+                else:
+                    name = "Groupe %s" % gr_moyenne["group_name"]
+                H.append(
+                    """<td colspan="5" class="mievr_grtit">%s &nbsp;</td><td>""" % name
+                )
+                if gr_moyenne["gr_nb_notes"] > 0:
+                    H.append("%(gr_moy)s" % gr_moyenne)
+                    H.append(
+                        """&nbsp; (<a href="evaluation_listenotes?tf-submitted=1&amp;evaluation_id=%s&amp;group_ids%%3Alist=%s">%s</a> notes"""
+                        % (
+                            eval["evaluation_id"],
+                            gr_moyenne["group_id"],
+                            gr_moyenne["gr_nb_notes"],
+                        )
+                    )
+                    if gr_moyenne["gr_nb_att"] > 0:
+                        H.append(
+                            """, <span class="redboldtext">%s en attente</span>"""
+                            % gr_moyenne["gr_nb_att"]
+                        )
+                    H.append(""")""")
+                    if gr_moyenne["group_id"] in etat["gr_incomplets"]:
+                        H.append("""[<font color="red">""")
+                        if caneditnotes:
+                            H.append(
+                                """<a class="redlink" href="saisie_notes?evaluation_id=%s&amp;group_ids:list=%s">incomplet</a></font>]"""
+                                % (eval["evaluation_id"], gr_moyenne["group_id"])
+                            )
+                        else:
+                            H.append("""incomplet</font>]""")
+                else:
+                    H.append("""<span class="redboldtext">&nbsp; """)
+                    if caneditnotes:
+                        H.append(
+                            """<a class="redlink" href="saisie_notes?evaluation_id=%s&amp;group_ids:list=%s">"""
+                            % (eval["evaluation_id"], gr_moyenne["group_id"])
+                        )
+                    H.append("pas de notes")
+                    if caneditnotes:
+                        H.append("""</a>""")
+                    H.append("</span>")
+                H.append("""</td></tr>""")
+
+    #
+    if caneditevals or sem["etat"] != "1":
+        H.append("""<tr><td colspan="8">""")
+        if sem["etat"] != "1":
+            H.append("""%s semestre verrouillé""" % icontag("lock32_img"))
+        else:
+            H.append(top_table_links)
+
+    H.append(
+        """</td></tr>
+</table>
+
+</div>
+
+<!-- LEGENDE -->
+<hr>
+<h4>Légende</h4>
+<ul>
+<li>%s : modifie description de l'évaluation (date, heure, coefficient, ...)</li>
+<li>%s : saisie des notes</li>
+<li>%s : indique qu'il n'y a aucune note entrée (cliquer pour supprimer cette évaluation)</li>
+<li>%s : indique qu'il manque quelques notes dans cette évaluation</li>
+<li>%s : toutes les notes sont entrées (cliquer pour les afficher)</li>
+<li>%s : indique que cette évaluation sera mentionnée dans les bulletins au format "intermédiaire"
+</ul>
+
+<p>Rappel : seules les notes des évaluations complètement saisies (affichées en vert) apparaissent dans les bulletins.
+</p>
+    """
+        % (
+            icontag("edit_img"),
+            icontag("notes_img"),
+            icontag("delete_img"),
+            icontag("status_orange_img"),
+            icontag("status_green_img"),
+            icontag("status_visible_img"),
+        )
+    )
+    H.append(context.sco_footer(REQUEST))
+    return "".join(H)
diff --git a/sco_news.py b/sco_news.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b5d2368b23cfa9d1fe7eec232805a148961b7e6
--- /dev/null
+++ b/sco_news.py
@@ -0,0 +1,302 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Gestions des "nouvelles"
+"""
+
+import PyRSS2Gen
+from cStringIO import StringIO
+import datetime, re
+import time
+from stripogram import html2text, html2safehtml
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email.Header import Header
+from email import Encoders
+
+
+from notesdb import *
+from notes_log import log
+import scolars
+from sco_utils import SCO_ENCODING, SCO_ANNONCES_WEBSITE
+import sco_formsemestre
+
+_scolar_news_editor = EditableTable(
+    "scolar_news",
+    "news_id",
+    ("date", "authenticated_user", "type", "object", "text", "url"),
+    sortkey="date desc",
+    output_formators={"date": DateISOtoDMY},
+    input_formators={"date": DateDMYtoISO},
+    html_quote=False,  # no user supplied data, needed to store html links
+)
+
+NEWS_INSCR = "INSCR"  # inscription d'étudiants (object=None ou formsemestre_id)
+NEWS_NOTE = "NOTES"  # saisie note (object=moduleimpl_id)
+NEWS_FORM = "FORM"  # modification formation (object=formation_id)
+NEWS_SEM = "SEM"  # creation semestre (object=None)
+NEWS_MISC = "MISC"  # unused
+NEWS_MAP = {
+    NEWS_INSCR: "inscription d'étudiants",
+    NEWS_NOTE: "saisie note",
+    NEWS_FORM: "modification formation",
+    NEWS_SEM: "création semestre",
+    NEWS_MISC: "opération",  # unused
+}
+NEWS_TYPES = NEWS_MAP.keys()
+
+scolar_news_create = _scolar_news_editor.create
+scolar_news_list = _scolar_news_editor.list
+
+_LAST_NEWS = {}  # { (authuser_name, type, object) : time }
+
+
+def add(context, REQUEST, typ, object=None, text="", url=None, max_frequency=False):
+    """Ajoute une nouvelle.
+    Si max_frequency, ne genere pas 2 nouvelles identiques à moins de max_frequency
+    secondes d'intervalle.
+    """
+    authuser_name = str(REQUEST.AUTHENTICATED_USER)
+    cnx = context.GetDBConnexion()
+    args = {
+        "authenticated_user": authuser_name,
+        "user_info": context.Users.user_info(user_name=authuser_name),
+        "type": typ,
+        "object": object,
+        "text": text,
+        "url": url,
+    }
+
+    log("news: %s" % args)
+    t = time.time()
+    if max_frequency:
+        last_news_time = _LAST_NEWS.get((authuser_name, typ, object), False)
+        if last_news_time and (t - last_news_time < max_frequency):
+            log("not recording")
+            return
+
+    _LAST_NEWS[(authuser_name, typ, object)] = t
+
+    _send_news_by_mail(context, args)
+    return scolar_news_create(cnx, args, has_uniq_values=False)
+
+
+def scolar_news_summary(context, n=5):
+    """Return last n news.
+    News are "compressed", ie redondant events are joined.
+    """
+
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute("select * from scolar_news order by date desc limit 100")
+    selected_news = {}  # (type,object) : news dict
+    news = cursor.dictfetchall()  # la plus récente d'abord
+
+    for r in reversed(news):  # la plus ancienne d'abord
+        # si on a deja une news avec meme (type,object)
+        # et du meme jour, on la remplace
+        dmy = DateISOtoDMY(r["date"])  # round
+        key = (r["type"], r["object"], dmy)
+        selected_news[key] = r
+
+    news = selected_news.values()
+    # sort by date, descending
+    news.sort(lambda x, y: cmp(y["date"], x["date"]))
+    news = news[:n]
+    # mimic EditableTable.list output formatting:
+    for n in news:
+        d = n["date"]
+        n["date822"] = n["date"].strftime("%a, %d %b %Y %H:%M:%S %z")
+        # heure
+        n["hm"] = n["date"].strftime("%Hh%M")
+        n["rssdate"] = n["date"].strftime("%d/%m %Hh%M")  # pour affichage
+        for k in n.keys():
+            if n[k] is None:
+                n[k] = ""
+            if _scolar_news_editor.output_formators.has_key(k):
+                n[k] = _scolar_news_editor.output_formators[k](n[k])
+        # date resumee
+        j, m = n["date"].split("/")[:2]
+        mois = scolars.abbrvmonthsnames[int(m) - 1]
+        n["formatted_date"] = "%s %s %s" % (j, mois, n["hm"])
+        # indication semestre si ajout notes:
+        infos = _get_formsemestre_infos_from_news(context, n)
+        if infos:
+            n["text"] += (
+                ' (<a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(descr_sem)s</a>)'
+                % infos
+            )
+        n["text"] += (
+            " par "
+            + context.Users.user_info(user_name=n["authenticated_user"])["nomcomplet"]
+        )
+    return news
+
+
+def _get_formsemestre_infos_from_news(context, n):
+    """Informations sur le semestre concerné par la nouvelle n
+    {} si inexistant
+    """
+    formsemestre_id = None
+    if n["type"] == NEWS_INSCR:
+        formsemestre_id = n["object"]
+    elif n["type"] == NEWS_NOTE:
+        moduleimpl_id = n["object"]
+        mods = context.Notes.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)
+        if not mods:
+            return {}  # module does not exists anymore
+        mod = mods[0]
+        formsemestre_id = mod["formsemestre_id"]
+
+    if not formsemestre_id:
+        return {}
+
+    try:
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    except:
+        # semestre n'existe plus
+        return {}
+
+    if sem["semestre_id"] > 0:
+        descr_sem = "S%d" % sem["semestre_id"]
+    else:
+        descr_sem = ""
+    if sem["modalite"]:
+        descr_sem += " " + sem["modalite"]
+    return {"formsemestre_id": formsemestre_id, "sem": sem, "descr_sem": descr_sem}
+
+
+def scolar_news_summary_html(context, n=5, rssicon=None):
+    """News summary, formated in HTML"""
+    news = scolar_news_summary(context, n=n)
+    if not news:
+        return ""
+    H = ['<div class="news"><span class="newstitle">Dernières opérations']
+    if rssicon:
+        H.append('<a href="rssnews">' + rssicon + "</a>")
+    H.append('</span><ul class="newslist">')
+
+    for n in news:
+        H.append(
+            '<li class="newslist"><span class="newsdate">%(formatted_date)s</span><span class="newstext">%(text)s</span></li>'
+            % n
+        )
+    H.append("</ul>")
+
+    # Informations générales
+    H.append(
+        """<div>
+    Pour être informé des évolutions de ScoDoc,
+    vous pouvez vous
+    <a class="stdlink" href="%s">
+    abonner à la liste de diffusion</a>.
+    </div>
+    """
+        % SCO_ANNONCES_WEBSITE
+    )
+
+    H.append("</div>")
+    return "\n".join(H)
+
+
+def scolar_news_summary_rss(context, title, sco_url, n=5):
+    """rss feed for scolar news"""
+    news = scolar_news_summary(context, n=n)
+    items = []
+    for n in news:
+        text = html2text(n["text"])
+        items.append(
+            PyRSS2Gen.RSSItem(
+                title=unicode("%s %s" % (n["rssdate"], text), SCO_ENCODING),
+                link=sco_url + "/" + n["url"],
+                pubDate=n["date822"],
+            )
+        )
+    rss = PyRSS2Gen.RSS2(
+        title=unicode(title, SCO_ENCODING),
+        link=sco_url,
+        description=unicode(title, SCO_ENCODING),
+        lastBuildDate=datetime.datetime.now(),
+        items=items,
+    )
+    f = StringIO()
+    rss.write_xml(f, encoding=SCO_ENCODING)
+    f.seek(0)
+    data = f.read()
+    f.close()
+    return data
+
+
+def _send_news_by_mail(context, n):
+    """Notify by email
+    """
+    infos = _get_formsemestre_infos_from_news(context, n)
+    formsemestre_id = infos.get("formsemestre_id", None)
+    prefs = context.get_preferences(formsemestre_id=formsemestre_id)
+    destinations = prefs["emails_notifications"] or ""
+    destinations = [x.strip() for x in destinations.split(",")]
+    destinations = [x for x in destinations if x]
+    if not destinations:
+        return
+    #
+    txt = n["text"]
+    if infos:
+        txt += (
+            '\n\nSemestre <a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(descr_sem)s</a>)'
+            % infos
+        )
+        txt += "\n\nEffectué par: %(nomcomplet)s\n" % n["user_info"]
+
+    txt = (
+        "\n"
+        + txt
+        + """\n
+--- Ceci est un message de notification automatique issu de ScoDoc
+--- vous recevez ce message car votre adresse est indiquée dans les paramètres de ScoDoc.
+"""
+    )
+
+    # Transforme les URL en URL absolue
+    base = context.ScoURL()
+    txt = re.sub('href=.*?"', 'href="' + base + "/", txt)
+
+    # Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
+    # (si on veut des messages non html)
+    txt = re.sub(r'<a.*?href\s*=\s*"(.*?)".*?>(.*?)</a>', r"\2: \1", txt)
+
+    msg = MIMEMultipart()
+    msg["Subject"] = Header("[ScoDoc] " + NEWS_MAP.get(n["type"], "?"), SCO_ENCODING)
+    msg["From"] = prefs["email_from_addr"]
+    txt = MIMEText(txt, "plain", SCO_ENCODING)
+    msg.attach(txt)
+
+    for email_addr in destinations:
+        if email_addr:
+            del msg["To"]
+            msg["To"] = email_addr
+            # log('xxx mail: %s' % msg)
+            context.sendEmail(msg)
diff --git a/sco_page_etud.py b/sco_page_etud.py
new file mode 100644
index 0000000000000000000000000000000000000000..67add92165302caf07d6147a595850b29b1c962b
--- /dev/null
+++ b/sco_page_etud.py
@@ -0,0 +1,589 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""ScoDoc ficheEtud
+
+   Fiche description d'un étudiant et de son parcours
+
+"""
+
+from sco_utils import *
+from notesdb import *
+import scolars
+import sco_bac
+import sco_photos
+import sco_groups
+import sco_formsemestre
+from scolars import format_telephone, format_pays, make_etud_args
+import sco_formsemestre_status
+from sco_formsemestre_status import makeMenu
+from sco_bulletins import etud_descr_situation_semestre
+import sco_parcours_dut
+from sco_formsemestre_validation import formsemestre_recap_parcours_table
+import sco_archives_etud
+import sco_report
+
+
+def _menuScolarite(context, authuser, sem, etudid):
+    """HTML pour menu "scolarite" pour un etudiant dans un semestre.
+    Le contenu du menu depend des droits de l'utilisateur et de l'état de l'étudiant.
+    """
+    locked = sem["etat"] != "1"
+    if locked:
+        lockicon = icontag("lock32_img", title="verrouillé", border="0")
+        return lockicon  # no menu
+    if not authuser.has_permission(
+        ScoEtudInscrit, context
+    ) and not authuser.has_permission(ScoEtudChangeGroups, context):
+        return ""  # no menu
+    ins = sem["ins"]
+    args = {"etudid": etudid, "formsemestre_id": ins["formsemestre_id"]}
+
+    if ins["etat"] != "D":
+        dem_title = "Démission"
+        dem_url = (
+            "formDem?etudid=%(etudid)s&amp;formsemestre_id=%(formsemestre_id)s" % args
+        )
+    else:
+        dem_title = "Annuler la démission"
+        dem_url = (
+            "doCancelDem?etudid=%(etudid)s&amp;formsemestre_id=%(formsemestre_id)s"
+            % args
+        )
+
+    # Note: seul un etudiant inscrit (I) peut devenir défaillant.
+    if ins["etat"] != DEF:
+        def_title = "Déclarer défaillance"
+        def_url = (
+            "formDef?etudid=%(etudid)s&amp;formsemestre_id=%(formsemestre_id)s" % args
+        )
+    elif ins["etat"] == DEF:
+        def_title = "Annuler la défaillance"
+        def_url = (
+            "doCancelDef?etudid=%(etudid)s&amp;formsemestre_id=%(formsemestre_id)s"
+            % args
+        )
+    def_enabled = (
+        (ins["etat"] != "D")
+        and authuser.has_permission(ScoEtudInscrit, context)
+        and not locked
+    )
+    items = [
+        #        { 'title' : 'Changer de groupe',
+        #          'url' : 'formChangeGroup?etudid=%s&amp;formsemestre_id=%s' % (etudid,ins['formsemestre_id']),
+        #          'enabled' : authuser.has_permission(ScoEtudChangeGroups,context) and not locked,
+        #        },
+        {
+            "title": dem_title,
+            "url": dem_url,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context) and not locked,
+        },
+        {
+            "title": "Validation du semestre (jury)",
+            "url": "Notes/formsemestre_validation_etud_form?etudid=%(etudid)s&amp;formsemestre_id=%(formsemestre_id)s"
+            % args,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context) and not locked,
+        },
+        {"title": def_title, "url": def_url, "enabled": def_enabled},
+        {
+            "title": "Inscrire à un module optionnel (ou au sport)",
+            "url": "Notes/formsemestre_inscription_option?formsemestre_id=%(formsemestre_id)s&amp;etudid=%(etudid)s"
+            % args,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context) and not locked,
+        },
+        {
+            "title": "Désinscrire (en cas d'erreur)",
+            "url": "Notes/formsemestre_desinscription?formsemestre_id=%(formsemestre_id)s&amp;etudid=%(etudid)s"
+            % args,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context) and not locked,
+        },
+        {
+            "title": "Inscrire à un autre semestre",
+            "url": "Notes/formsemestre_inscription_with_modules_form?etudid=%(etudid)s"
+            % args,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context),
+        },
+        {
+            "title": "Enregistrer un semestre effectué ailleurs",
+            "url": "Notes/formsemestre_ext_create_form?formsemestre_id=%(formsemestre_id)s&amp;etudid=%(etudid)s"
+            % args,
+            "enabled": authuser.has_permission(ScoImplement, context),
+        },
+    ]
+
+    return makeMenu("Scolarité", items, css_class="direction_etud", alone=True)
+
+
+def ficheEtud(context, etudid=None, REQUEST=None):
+    "fiche d'informations sur un etudiant"
+    authuser = REQUEST.AUTHENTICATED_USER
+    cnx = context.GetDBConnexion()
+    args = make_etud_args(etudid=etudid, REQUEST=REQUEST)
+    etuds = scolars.etudident_list(cnx, args)
+    if not etuds:
+        log("ficheEtud: etudid=%s REQUEST.form=%s" % (etudid, REQUEST.form))
+        raise ScoValueError("Etudiant inexistant !")
+    etud = etuds[0]
+    etudid = etud["etudid"]
+    context.fillEtudsInfo([etud])
+    #
+    info = etud
+    info["ScoURL"] = context.ScoURL()
+    info["authuser"] = authuser
+    info["info_naissance"] = info["date_naissance"]
+    if info["lieu_naissance"]:
+        info["info_naissance"] += " à " + info["lieu_naissance"]
+    if info["dept_naissance"]:
+        info["info_naissance"] += " (%s)" % info["dept_naissance"]
+    info["etudfoto"] = sco_photos.etud_photo_html(context, etud, REQUEST=REQUEST)
+    if (
+        (not info["domicile"])
+        and (not info["codepostaldomicile"])
+        and (not info["villedomicile"])
+    ):
+        info["domicile"] = "<em>inconnue</em>"
+    if info["paysdomicile"]:
+        pays = format_pays(info["paysdomicile"])
+        if pays:
+            info["paysdomicile"] = "(%s)" % pays
+        else:
+            info["paysdomicile"] = ""
+    if info["telephone"] or info["telephonemobile"]:
+        info["telephones"] = "<br/>%s &nbsp;&nbsp; %s" % (
+            info["telephonestr"],
+            info["telephonemobilestr"],
+        )
+    else:
+        info["telephones"] = ""
+    # e-mail:
+    if info["email_default"]:
+        info["emaillink"] = ", ".join(
+            [
+                '<a class="stdlink" href="mailto:%s">%s</a>' % (m, m)
+                for m in [etud["email"], etud["emailperso"]]
+                if m
+            ]
+        )
+    else:
+        info["emaillink"] = "<em>(pas d'adresse e-mail)</em>"
+    # champs dependant des permissions
+    if authuser.has_permission(ScoEtudChangeAdr, context):
+        info["modifadresse"] = (
+            '<a class="stdlink" href="formChangeCoordonnees?etudid=%s">modifier adresse</a>'
+            % etudid
+        )
+    else:
+        info["modifadresse"] = ""
+
+    # Groupes:
+    sco_groups.etud_add_group_infos(context, info, info["cursem"])
+
+    # Parcours de l'étudiant
+    if info["sems"]:
+        info["last_formsemestre_id"] = info["sems"][0]["formsemestre_id"]
+    else:
+        info["last_formsemestre_id"] = ""
+    sem_info = {}
+    for sem in info["sems"]:
+        if sem["ins"]["etat"] != "I":
+            descr, junk = etud_descr_situation_semestre(
+                context.Notes,
+                etudid,
+                sem["formsemestre_id"],
+                info["ne"],
+                show_date_inscr=False,
+            )
+            grlink = '<span class="fontred">%s</span>' % descr["situation"]
+        else:
+            group = sco_groups.get_etud_main_group(context, etudid, sem)
+            if group["partition_name"]:
+                gr_name = group["group_name"]
+            else:
+                gr_name = "tous"
+            grlink = (
+                '<a class="discretelink" href="groups_view?group_ids=%s" title="Liste du groupe">groupe %s</a>'
+                % (group["group_id"], gr_name)
+            )
+        # infos ajoutées au semestre dans le parcours (groupe, menu)
+        menu = _menuScolarite(context, authuser, sem, etudid)
+        if menu:
+            sem_info[sem["formsemestre_id"]] = (
+                "<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"
+            )
+        else:
+            sem_info[sem["formsemestre_id"]] = grlink
+
+    if info["sems"]:
+        Se = sco_parcours_dut.SituationEtudParcours(
+            context.Notes, etud, info["last_formsemestre_id"]
+        )
+        info["liste_inscriptions"] = formsemestre_recap_parcours_table(
+            context.Notes,
+            Se,
+            etudid,
+            with_links=False,
+            sem_info=sem_info,
+            with_all_columns=False,
+            a_url="Notes/",
+        )
+        info["link_bul_pdf"] = (
+            '<span class="link_bul_pdf"><a class="stdlink" href="Notes/etud_bulletins_pdf?etudid=%(etudid)s">tous les bulletins</a></span>'
+            % etud
+        )
+    else:
+        # non inscrit
+        l = ["<p><b>Etudiant%s non inscrit%s" % (info["ne"], info["ne"])]
+        if authuser.has_permission(ScoEtudInscrit, context):
+            l.append(
+                '<a href="%s/Notes/formsemestre_inscription_with_modules_form?etudid=%s">inscrire</a></li>'
+                % (context.ScoURL(), etudid)
+            )
+        l.append("</b></b>")
+        info["liste_inscriptions"] = "\n".join(l)
+        info["link_bul_pdf"] = ""
+
+    # Liste des annotations
+    alist = []
+    annos = scolars.etud_annotations_list(cnx, args={"etudid": etudid})
+    for a in annos:
+        if not context.canSuppressAnnotation(a["id"], REQUEST):
+            a["dellink"] = ""
+        else:
+            a["dellink"] = (
+                '<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&amp;annotation_id=%s">%s</a></td>'
+                % (
+                    etudid,
+                    a["id"],
+                    icontag(
+                        "delete_img",
+                        border="0",
+                        alt="suppress",
+                        title="Supprimer cette annotation",
+                    ),
+                )
+            )
+        alist.append(
+            '<tr><td><span class="annodate">Le %(date)s par %(zope_authenticated_user)s : </span><span class="annoc">%(comment)s</span></td>%(dellink)s</tr>'
+            % a
+        )
+    info["liste_annotations"] = "\n".join(alist)
+    # fiche admission
+    has_adm_notes = (
+        info["math"] or info["physique"] or info["anglais"] or info["francais"]
+    )
+    has_bac_info = (
+        info["bac"]
+        or info["specialite"]
+        or info["annee_bac"]
+        or info["rapporteur"]
+        or info["commentaire"]
+        or info["classement"]
+        or info["type_admission"]
+    )
+    if has_bac_info or has_adm_notes:
+        adm_tmpl = """<!-- Donnees admission -->
+<div class="fichetitre">Informations admission</div>
+"""
+        if has_adm_notes:
+            adm_tmpl += """
+<table>
+<tr><th>Bac</th><th>Année</th><th>Rg</th>
+<th>Math</th><th>Physique</th><th>Anglais</th><th>Français</th></tr>
+<tr>
+<td>%(bac)s (%(specialite)s)</td>
+<td>%(annee_bac)s </td>
+<td>%(classement)s</td>
+<td>%(math)s</td><td>%(physique)s</td><td>%(anglais)s</td><td>%(francais)s</td>
+</tr>
+</table>
+"""
+        adm_tmpl += """
+<div>Bac %(bac)s (%(specialite)s) obtenu en %(annee_bac)s </div>
+<div class="ilycee">%(ilycee)s</div>"""
+        if info["type_admission"] or info["classement"]:
+            adm_tmpl += """<div class="vadmission">"""
+        if info["type_admission"]:
+            adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
+        if info["classement"]:
+            adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
+        if info["type_admission"] or info["classement"]:
+            adm_tmpl += "</div>"
+        if info["rap"]:
+            adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
+        adm_tmpl += """</div>"""
+    else:
+        adm_tmpl = ""  # pas de boite "info admission"
+    info["adm_data"] = adm_tmpl % info
+
+    # Fichiers archivés:
+    info["fichiers_archive_htm"] = (
+        '<div class="fichetitre">Fichiers associés</div>'
+        + sco_archives_etud.etud_list_archives_html(context, REQUEST, etudid)
+    )
+
+    # Devenir de l'étudiant:
+    has_debouche = True  # info['debouche']
+    if context.can_edit_suivi(REQUEST):
+        suivi_readonly = "0"
+        link_add_suivi = """<li class="adddebouche">
+            <a id="adddebouchelink" class="stdlink" href="#">ajouter une ligne</a>
+            </li>"""
+    else:
+        suivi_readonly = "1"
+        link_add_suivi = ""
+    if has_debouche:
+        info[
+            "debouche_html"
+        ] = """<div id="fichedebouche" data-readonly="%s" data-etudid="%s">
+        <span class="debouche_tit">Devenir:</span>
+        <div><form>
+        <ul class="listdebouches">
+        %s
+        </ul>
+        </form></div>
+        </div>""" % (
+            suivi_readonly,
+            info["etudid"],
+            link_add_suivi,
+        )
+    else:
+        info["debouche_html"] = ""  # pas de boite "devenir"
+    #
+    if info["liste_annotations"]:
+        info["tit_anno"] = '<div class="fichetitre">Annotations</div>'
+    else:
+        info["tit_anno"] = ""
+    # Inscriptions
+    if info["sems"]:
+        rcl = (
+            """(<a href="%(ScoURL)s/Notes/formsemestre_validation_etud_form?check=1&amp;etudid=%(etudid)s&amp;formsemestre_id=%(last_formsemestre_id)s&amp;desturl=ficheEtud?etudid=%(etudid)s">récapitulatif parcours</a>)"""
+            % info
+        )
+    else:
+        rcl = ""
+    info[
+        "inscriptions_mkup"
+    ] = """<div class="ficheinscriptions" id="ficheinscriptions">
+<div class="fichetitre">Parcours</div>%s
+%s
+</div>""" % (
+        info["liste_inscriptions"],
+        info["link_bul_pdf"],
+    )
+
+    #
+    if info["groupes"].strip():
+        info["groupes_row"] = (
+            '<tr><td class="fichetitre2">Groupe :</td><td>%(groupes)s</td></tr>' % info
+        )
+    else:
+        info["groupes_row"] = ""
+    info["menus_etud"] = menus_etud(context, REQUEST)
+    tmpl = """<div class="menus_etud">%(menus_etud)s</div>
+<div class="ficheEtud" id="ficheEtud"><table>
+<tr><td>
+<h2>%(nomprenom)s (%(inscription)s)</h2>
+
+<span>%(emaillink)s</span> 
+</td><td class="photocell">
+<a href="etud_photo_orig_page?etudid=%(etudid)s">%(etudfoto)s</a>
+</td></tr></table>
+
+<div class="fichesituation">
+<div class="fichetablesitu">
+<table>
+<tr><td class="fichetitre2">Situation :</td><td>%(situation)s</td></tr>
+%(groupes_row)s
+<tr><td class="fichetitre2">Né%(ne)s le :</td><td>%(info_naissance)s</td></tr>
+</table>
+
+
+<!-- Adresse -->
+<div class="ficheadresse" id="ficheadresse">
+<table><tr>
+<td class="fichetitre2">Adresse :</td><td> %(domicile)s %(codepostaldomicile)s %(villedomicile)s %(paysdomicile)s
+%(modifadresse)s
+%(telephones)s
+</td></tr></table>
+</div>
+</div>
+</div>
+
+%(inscriptions_mkup)s
+
+<div class="ficheadmission">
+%(adm_data)s
+
+%(fichiers_archive_htm)s
+</div>
+
+%(debouche_html)s
+
+<div class="ficheannotations">
+%(tit_anno)s
+<table id="etudannotations">%(liste_annotations)s</table>
+
+<form action="doAddAnnotation" method="GET" class="noprint">
+<input type="hidden" name="etudid" value="%(etudid)s">
+<b>Ajouter une annotation sur %(nomprenom)s: </b>
+<table><tr>
+<tr><td><textarea name="comment" rows="4" cols="50" value=""></textarea>
+<br/><font size=-1>
+<i>Ces annotations sont lisibles par tous les enseignants et le secrétariat.</i>
+<br/>
+<i>L'annotation commençant par "PE:" est un avis de poursuite d'études.</i>
+</font>
+</td></tr>
+<tr><td>
+ <input type="hidden" name="author" width=12 value="%(authuser)s">
+<input type="submit" value="Ajouter annotation"></td></tr>
+</table>
+</form>
+</div>
+
+<div class="code_nip">code NIP: %(code_nip)s</div>
+
+</div>
+        """
+    header = context.sco_header(
+        REQUEST,
+        page_title="Fiche étudiant %(prenom)s %(nom)s" % info,
+        cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
+        javascripts=[
+            "libjs/jinplace-1.2.1.min.js",
+            "js/ue_list.js",
+            "libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
+            "libjs/jQuery-tagEditor/jquery.caret.min.js",
+            "js/recap_parcours.js",
+            "js/etud_debouche.js",
+        ],
+    )
+    return header + tmpl % info + context.sco_footer(REQUEST)
+
+
+def menus_etud(context, REQUEST=None):
+    """Menu etudiant (operations sur l'etudiant)
+    """
+    if not REQUEST.form.has_key("etudid"):
+        return ""
+    authuser = REQUEST.AUTHENTICATED_USER
+
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+
+    menuEtud = [
+        {
+            "title": etud["nomprenom"],
+            "url": "ficheEtud?etudid=%(etudid)s" % etud,
+            "enabled": True,
+            "helpmsg": "Fiche étudiant",
+        },
+        {
+            "title": "Changer la photo",
+            "url": "formChangePhoto?etudid=%(etudid)s" % etud,
+            "enabled": authuser.has_permission(ScoEtudChangeAdr, context),
+        },
+        {
+            "title": "Changer les données identité/admission",
+            "url": "etudident_edit_form?etudid=%(etudid)s" % etud,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context),
+        },
+        {
+            "title": "Supprimer cet étudiant...",
+            "url": "etudident_delete?etudid=%(etudid)s" % etud,
+            "enabled": authuser.has_permission(ScoEtudInscrit, context),
+        },
+        {
+            "title": "Voir le journal...",
+            "url": "showEtudLog?etudid=%(etudid)s" % etud,
+            "enabled": True,
+        },
+    ]
+
+    return makeMenu(
+        "Etudiant", menuEtud, base_url=context.absolute_url() + "/", alone=True
+    )
+
+
+def etud_info_html(context, etudid, with_photo="1", REQUEST=None, debug=False):
+    """An HTML div with basic information and links about this etud.
+    Used for popups information windows.
+    """
+    try:
+        context = context.Notes
+    except:
+        pass
+    # log('etud_info_html: %s' % REQUEST.QUERY_STRING)
+    formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request(
+        context, REQUEST
+    )
+    # log('etud_info_html: formsemestre_id=%s' % formsemestre_id)
+
+    with_photo = int(with_photo)
+    etud = context.getEtudInfo(filled=1, REQUEST=REQUEST)[0]
+    photo_html = sco_photos.etud_photo_html(
+        context, etud, title="fiche de " + etud["nom"], REQUEST=REQUEST
+    )
+    # experimental: may be too slow to be here
+    etud["codeparcours"], etud["decisions_jury"] = sco_report.get_codeparcoursetud(
+        context.Notes, etud, prefix="S", separator=", "
+    )
+
+    bac = sco_bac.Baccalaureat(etud["bac"], etud["specialite"])
+    etud["bac_abbrev"] = bac.abbrev()
+    H = (
+        """<div class="etud_info_div">
+    <div class="eid_left">
+     <div class="eid_nom"><div>%(nomprenom)s</div></div>
+     <div class="eid_info eid_bac">Bac: <span class="eid_bac">%(bac_abbrev)s</span></div>
+     <div class="eid_info eid_parcours">%(codeparcours)s</div>
+    """
+        % etud
+    )
+
+    # Informations sur l'etudiant dans le semestre courant:
+    sem = None
+    if formsemestre_id:  # un semestre est spécifié par la page
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    elif etud["cursem"]:  # le semestre "en cours" pour l'étudiant
+        sem = etud["cursem"]
+    if sem:
+        groups = sco_groups.get_etud_groups(context, etudid, sem)
+        grc = sco_groups.listgroups_abbrev(groups)
+        H += '<div class="eid_info">En <b>S%d</b>: %s</div>' % (sem["semestre_id"], grc)
+    H += "</div>"  # fin partie gauche (eid_left)
+    if with_photo:
+        H += '<span class="eid_right">' + photo_html + "</span>"
+
+    H += "</div>"
+    if debug:
+        return (
+            context.standard_html_header(context)
+            + H
+            + context.standard_html_footer(context)
+        )
+    else:
+        return H
diff --git a/sco_parcours_dut.py b/sco_parcours_dut.py
new file mode 100644
index 0000000000000000000000000000000000000000..2da3d0342af72e7c0cb5b89e081ee2f1fd0d75ec
--- /dev/null
+++ b/sco_parcours_dut.py
@@ -0,0 +1,1099 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Semestres: gestion parcours DUT (Arreté du 13 août 2005)
+"""
+import urllib, time, datetime
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from scolog import logdb
+from notes_table import *
+import sco_formsemestre
+from sco_codes_parcours import *
+from dutrules import DUTRules  # regles generees a partir du CSV
+
+
+class DecisionSem:
+    "Decision prenable pour un semestre"
+
+    def __init__(
+        self,
+        code_etat=None,
+        code_etat_ues={},  # { ue_id : code }
+        new_code_prev="",
+        explication="",  # aide pour le jury
+        formsemestre_id_utilise_pour_compenser=None,  # None si code != ADC
+        devenir=None,  # code devenir
+        assiduite=True,
+        rule_id=None,  # id regle correspondante
+    ):
+        self.code_etat = code_etat
+        self.code_etat_ues = code_etat_ues
+        self.new_code_prev = new_code_prev
+        self.explication = explication
+        self.formsemestre_id_utilise_pour_compenser = (
+            formsemestre_id_utilise_pour_compenser
+        )
+        self.devenir = devenir
+        self.assiduite = assiduite
+        self.rule_id = rule_id
+        # code unique utilise pour la gestion du formulaire
+        self.codechoice = str(
+            hash(
+                (
+                    code_etat,
+                    new_code_prev,
+                    formsemestre_id_utilise_pour_compenser,
+                    devenir,
+                    assiduite,
+                )
+            )
+        )
+        # xxx debug
+        # log('%s: %s %s %s %s %s' % (self.codechoice,code_etat,new_code_prev,formsemestre_id_utilise_pour_compenser,devenir,assiduite) )
+
+
+def SituationEtudParcours(context, etud, formsemestre_id):
+    """renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)
+    """
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etud_decision_sem, get_etud_moy_gen, get_ues, get_etud_ue_status, etud_check_conditions_ues
+    parcours = nt.parcours
+    #
+    if parcours.ECTS_ONLY:
+        return SituationEtudParcoursECTS(context, etud, formsemestre_id, nt)
+    else:
+        return SituationEtudParcoursGeneric(context, etud, formsemestre_id, nt)
+
+
+class SituationEtudParcoursGeneric:
+    "Semestre dans un parcours"
+
+    def __init__(self, context, etud, formsemestre_id, nt):
+        """
+        etud: dict filled by fillEtudsInfo()
+        """
+        self.context = context
+        self.etud = etud
+        self.etudid = etud["etudid"]
+        self.formsemestre_id = formsemestre_id
+        self.sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        self.nt = nt
+        self.formation = self.nt.formation
+        self.parcours = self.nt.parcours
+        # Ce semestre est-il le dernier de la formation ? (e.g. semestre 4 du DUT)
+        # pour le DUT, le dernier est toujours S4.
+        # Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1
+        #        (licences et autres formations en 1 seule session))
+        self.semestre_non_terminal = (
+            self.sem["semestre_id"] != self.parcours.NB_SEM
+        )  # True | False
+        if self.sem["semestre_id"] == NO_SEMESTRE_ID:
+            self.semestre_non_terminal = False
+        # Liste des semestres du parcours de cet étudiant:
+        self._comp_semestres()
+        # Determine le semestre "precedent"
+        self.prev_formsemestre_id = self._search_prev()
+        # Verifie barres
+        self._comp_barres()
+        # Verifie compensation
+        if self.prev and self.sem["gestion_compensation"] == "1":
+            self.can_compensate_with_prev = self.prev["can_compensate"]
+        else:
+            self.can_compensate_with_prev = False
+
+    def get_possible_choices(self, assiduite=True):
+        """Donne la liste des décisions possibles en jury (hors décisions manuelles)
+        (liste d'instances de DecisionSem)
+        assiduite = True si pas de probleme d'assiduité
+        """
+        choices = []
+        if self.prev_decision:
+            prev_code_etat = self.prev_decision["code"]
+        else:
+            prev_code_etat = None
+
+        state = (
+            prev_code_etat,
+            assiduite,
+            self.barre_moy_ok,
+            self.barres_ue_ok,
+            self.can_compensate_with_prev,
+            self.semestre_non_terminal,
+        )
+        # log('get_possible_choices: state=%s' % str(state) )
+        for rule in DUTRules:
+            # Saute codes non autorisés dans ce parcours (eg ATT en LP)
+            if rule.conclusion[0] in self.parcours.UNUSED_CODES:
+                continue
+            # Saute regles REDOSEM si pas de semestres decales:
+            if (
+                self.sem["gestion_semestrielle"] != "1"
+                and rule.conclusion[3] == "REDOSEM"
+            ):
+                continue
+            if rule.match(state):
+                if rule.conclusion[0] == ADC:
+                    # dans les regles on ne peut compenser qu'avec le PRECEDENT:
+                    fiduc = self.prev_formsemestre_id
+                    assert fiduc
+                else:
+                    fiduc = None
+                # Detection d'incoherences (regles BUG)
+                if rule.conclusion[5] == BUG:
+                    log("get_possible_choices: inconsistency: state=%s" % str(state))
+                #
+                valid_semestre = code_semestre_validant(rule.conclusion[0])
+                choices.append(
+                    DecisionSem(
+                        code_etat=rule.conclusion[0],
+                        new_code_prev=rule.conclusion[2],
+                        devenir=rule.conclusion[3],
+                        formsemestre_id_utilise_pour_compenser=fiduc,
+                        explication=rule.conclusion[5],
+                        assiduite=assiduite,
+                        rule_id=rule.rule_id,
+                    )
+                )
+        return choices
+
+    def explique_devenir(self, devenir):
+        "Phrase d'explication pour le code devenir"
+        if not devenir:
+            return ""
+        s = self.sem["semestre_id"]  # numero semestre courant
+        if s < 0:  # formation sans semestres (eg licence)
+            next_s = 1
+        else:
+            next_s = self._get_next_semestre_id()
+        # log('s=%s  next=%s' % (s, next_s))
+        SA = self.parcours.SESSION_ABBRV  # 'S' ou 'A'
+        if self.semestre_non_terminal and not self.all_other_validated():
+            passage = "Passe en %s%s" % (SA, next_s)
+        else:
+            passage = "Formation terminée"
+        if devenir == NEXT:
+            return passage
+        elif devenir == REO:
+            return "Réorienté"
+        elif devenir == REDOANNEE:
+            return "Redouble année (recommence %s%s)" % (SA, (s - 1))
+        elif devenir == REDOSEM:
+            return "Redouble semestre (recommence en %s%s)" % (SA, s)
+        elif devenir == RA_OR_NEXT:
+            return passage + ", ou redouble année (en %s%s)" % (SA, (s - 1))
+        elif devenir == RA_OR_RS:
+            return "Redouble semestre %s%s, ou redouble année (en %s%s)" % (
+                SA,
+                s,
+                SA,
+                s - 1,
+            )
+        elif devenir == RS_OR_NEXT:
+            return passage + ", ou semestre %s%s" % (SA, s)
+        elif devenir == NEXT_OR_NEXT2:
+            return passage + ", ou en semestre %s%s" % (
+                SA,
+                s + 2,
+            )  # coherent avec  get_next_semestre_ids
+        elif devenir == NEXT2:
+            return "Passe en %s%s" % (SA, s + 2)
+        else:
+            log("explique_devenir: code devenir inconnu: %s" % devenir)
+            return "Code devenir inconnu !"
+
+    def all_other_validated(self):
+        "True si tous les autres semestres de cette formation sont validés"
+        return self._sems_validated(exclude_current=True)
+
+    def sem_idx_is_validated(self, semestre_id):
+        "True si le semestre d'indice indiqué est validé dans ce parcours"
+        return self._sem_list_validated(set([semestre_id]))
+
+    def parcours_validated(self):
+        "True si parcours validé (diplôme obtenu, donc)."
+        return self._sems_validated()
+
+    def _sems_validated(self, exclude_current=False):
+        "True si semestres du parcours validés"
+        if self.sem["semestre_id"] == NO_SEMESTRE_ID:
+            # mono-semestre: juste celui ci
+            decision = self.nt.get_etud_decision_sem(self.etudid)
+            return decision and code_semestre_validant(decision["code"])
+        else:
+            to_validate = Set(
+                range(1, self.parcours.NB_SEM + 1)
+            )  # ensemble des indices à valider
+            if exclude_current and self.sem["semestre_id"] in to_validate:
+                to_validate.remove(self.sem["semestre_id"])
+            return self._sem_list_validated(to_validate)
+
+    def can_jump_to_next2(self):
+        """True si l'étudiant peut passer directement en Sn+2 (eg de S2 en S4).
+        Il faut donc que tous les semestres 1...n-1 soient validés et que n+1 soit en attente.
+        (et que le sem courant n soit validé, ce qui n'est pas testé ici)
+        """
+        n = self.sem["semestre_id"]
+        if self.sem["gestion_semestrielle"] != "1":
+            return False  # pas de semestre décalés
+        if n == NO_SEMESTRE_ID or n > self.parcours.NB_SEM - 2:
+            return False  # n+2 en dehors du parcours
+        if self._sem_list_validated(Set(range(1, n))):
+            # antérieurs validé, teste suivant
+            n1 = n + 1
+            for sem in self.get_semestres():
+                if (
+                    sem["semestre_id"] == n1
+                    and sem["formation_code"] == self.formation["formation_code"]
+                ):
+                    nt = self.context._getNotesCache().get_NotesTable(
+                        self.context, sem["formsemestre_id"]
+                    )  # > get_etud_decision_sem
+                    decision = nt.get_etud_decision_sem(self.etudid)
+                    if decision and (
+                        code_semestre_validant(decision["code"])
+                        or code_semestre_attente(decision["code"])
+                    ):
+                        return True
+        return False
+
+    def _sem_list_validated(self, sem_idx_set):
+        """True si les semestres dont les indices sont donnés en argument (modifié)
+        sont validés. En sortie, sem_idx_set contient ceux qui n'ont pas été validés."""
+        for sem in self.get_semestres():
+            if sem["formation_code"] == self.formation["formation_code"]:
+                nt = self.context._getNotesCache().get_NotesTable(
+                    self.context, sem["formsemestre_id"]
+                )  # > get_etud_decision_sem
+                decision = nt.get_etud_decision_sem(self.etudid)
+                if decision and code_semestre_validant(decision["code"]):
+                    # validé
+                    sem_idx_set.discard(sem["semestre_id"])
+
+        return not sem_idx_set
+
+    def _comp_semestres(self):
+        # etud['sems'] est trie par date decroissante (voir fillEtudsInfo)
+        sems = self.etud["sems"][:]  # copy
+        sems.reverse()
+        # Nb max d'UE et acronymes
+        ue_acros = {}  # acronyme ue : 1
+        nb_max_ue = 0
+        for sem in sems:
+            nt = self.context._getNotesCache().get_NotesTable(
+                self.context, sem["formsemestre_id"]
+            )  # > get_ues
+            ues = nt.get_ues(filter_sport=True)
+            for ue in ues:
+                ue_acros[ue["acronyme"]] = 1
+            nb_ue = len(ues)
+            if nb_ue > nb_max_ue:
+                nb_max_ue = nb_ue
+            # add formation_code to each sem:
+            sem["formation_code"] = self.context.formation_list(
+                args={"formation_id": sem["formation_id"]}
+            )[0]["formation_code"]
+            # si sem peut servir à compenser le semestre courant, positionne
+            #  can_compensate
+            sem["can_compensate"] = check_compensation(
+                self.etudid, self.sem, self.nt, sem, nt
+            )
+
+        self.ue_acros = ue_acros.keys()
+        self.ue_acros.sort()
+        self.nb_max_ue = nb_max_ue
+        self.sems = sems
+
+    def get_semestres(self):
+        """Liste des semestres dans lesquels a été inscrit
+        l'étudiant (quelle que soit la formation), le plus ancien en tête"""
+        return self.sems
+
+    def get_parcours_descr(self, filter_futur=False):
+        """Description brève du parcours: "S1, S2, ..."
+        Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant.
+        """
+        cur_begin_date = self.sem["dateord"]
+        p = []
+        for s in self.sems:
+            if s["ins"]["etat"] == "D":
+                dem = " (dem.)"
+            else:
+                dem = ""
+            if filter_futur and s["dateord"] > cur_begin_date:
+                continue  # skip semestres demarrant apres le courant
+            SA = self.parcours.SESSION_ABBRV  # 'S' ou 'A'
+            if s["semestre_id"] < 0:
+                SA = "A"  # force, cas des DUT annuels par exemple
+                p.append("%s%d%s" % (SA, -s["semestre_id"], dem))
+            else:
+                p.append("%s%d%s" % (SA, s["semestre_id"], dem))
+        return ", ".join(p)
+
+    def get_parcours_decisions(self):
+        """Decisions de jury de chacun des semestres du parcours,
+        du S1 au NB_SEM+1, ou mono-semestre.
+        Returns: { semestre_id : code }
+        """
+        r = {}
+        if self.sem["semestre_id"] == NO_SEMESTRE_ID:
+            indices = [NO_SEMESTRE_ID]
+        else:
+            indices = range(1, self.parcours.NB_SEM + 1)
+        for i in indices:
+            # cherche dans les semestres de l'étudiant, en partant du plus récent
+            sem = None
+            for asem in reversed(self.get_semestres()):
+                if asem["semestre_id"] == i:
+                    sem = asem
+                    break
+            if not sem:
+                code = ""  # non inscrit à ce semestre
+            else:
+                nt = self.context._getNotesCache().get_NotesTable(
+                    self.context, sem["formsemestre_id"]
+                )  # > get_etud_decision_sem
+                decision = nt.get_etud_decision_sem(self.etudid)
+                if decision:
+                    code = decision["code"]
+                else:
+                    code = "-"
+            r[i] = code
+        return r
+
+    def _comp_barres(self):
+        "calcule barres_ue_ok et barre_moy_ok:  barre moy. gen. et barres UE"
+        self.barres_ue_ok, self.barres_ue_diag = self.nt.etud_check_conditions_ues(
+            self.etudid
+        )
+        self.moy_gen = self.nt.get_etud_moy_gen(self.etudid)
+        self.barre_moy_ok = (type(self.moy_gen) == FloatType) and (
+            self.moy_gen >= (self.parcours.BARRE_MOY - NOTES_TOLERANCE)
+        )
+        # conserve etat UEs
+        ue_ids = [
+            x["ue_id"] for x in self.nt.get_ues(etudid=self.etudid, filter_sport=True)
+        ]
+        self.ues_status = {}  # ue_id : status
+        for ue_id in ue_ids:
+            self.ues_status[ue_id] = self.nt.get_etud_ue_status(self.etudid, ue_id)
+
+    def could_be_compensated(self):
+        "true si ce semestre pourrait etre compensé par un autre (e.g. barres UE > 8)"
+        return self.barres_ue_ok
+
+    def _search_prev(self):
+        """Recherche semestre 'precedent'.
+        return prev_formsemestre_id
+        """
+        self.prev = None
+        self.prev_decision = None
+        if len(self.sems) < 2:
+            return None
+        # Cherche sem courant dans la liste triee par date_debut
+        cur = None
+        icur = -1
+        for cur in self.sems:
+            icur += 1
+            if cur["formsemestre_id"] == self.formsemestre_id:
+                break
+        if not cur or cur["formsemestre_id"] != self.formsemestre_id:
+            log(
+                "*** SituationEtudParcours: search_prev: cur not found (formsemestre_id=%s, etudid=%s)"
+                % (formsemestre_id, etudid)
+            )
+            return None  # pas de semestre courant !!!
+        # Cherche semestre antérieur de même formation (code) et semestre_id precedent
+        #
+        # i = icur - 1 # part du courant, remonte vers le passé
+        i = len(self.sems) - 1  # par du dernier, remonte vers le passé
+        prev = None
+        while i >= 0:
+            if (
+                self.sems[i]["formation_code"] == self.formation["formation_code"]
+                and self.sems[i]["semestre_id"] == cur["semestre_id"] - 1
+            ):
+                prev = self.sems[i]
+                break
+            i -= 1
+        if not prev:
+            return None  # pas de precedent trouvé
+        self.prev = prev
+        # Verifications basiques:
+        # ?
+        # Code etat du semestre precedent:
+        nt = self.context._getNotesCache().get_NotesTable(
+            self.context, prev["formsemestre_id"]
+        )  # > get_etud_decision_sem, get_etud_moy_gen, etud_check_conditions_ues
+        self.prev_decision = nt.get_etud_decision_sem(self.etudid)
+        self.prev_moy_gen = nt.get_etud_moy_gen(self.etudid)
+        self.prev_barres_ue_ok = nt.etud_check_conditions_ues(self.etudid)[0]
+        return self.prev["formsemestre_id"]
+
+    def get_next_semestre_ids(self, devenir):
+        """Liste des numeros de semestres autorises avec ce devenir
+        Ne vérifie pas que le devenir est possible (doit être fait avant),
+        juste que le rang du semestre est dans le parcours [1..NB_SEM]
+        """
+        s = self.sem["semestre_id"]
+        if devenir == NEXT:
+            ids = [self._get_next_semestre_id()]
+        elif devenir == REDOANNEE:
+            ids = [s - 1]
+        elif devenir == REDOSEM:
+            ids = [s]
+        elif devenir == RA_OR_NEXT:
+            ids = [s - 1, self._get_next_semestre_id()]
+        elif devenir == RA_OR_RS:
+            ids = [s - 1, s]
+        elif devenir == RS_OR_NEXT:
+            ids = [s, self._get_next_semestre_id()]
+        elif devenir == NEXT_OR_NEXT2:
+            ids = [
+                self._get_next_semestre_id(),
+                s + 2,
+            ]  # cohérent avec explique_devenir()
+        elif devenir == NEXT2:
+            ids = [s + 2]
+        else:
+            ids = []  # reoriente ou autre: pas de next !
+        # clip [1..NB_SEM]
+        r = []
+        for idx in ids:
+            if idx > 0 and idx <= self.parcours.NB_SEM:
+                r.append(idx)
+        return r
+
+    def _get_next_semestre_id(self):
+        """Indice du semestre suivant non validé.
+        S'il n'y en a pas, ramène NB_SEM+1
+        """
+        s = self.sem["semestre_id"]
+        if s >= self.parcours.NB_SEM:
+            return self.parcours.NB_SEM + 1
+        validated = True
+        while validated and (s < self.parcours.NB_SEM):
+            s = s + 1
+            # semestre s validé ?
+            validated = False
+            for sem in self.sems:
+                if (
+                    sem["formation_code"] == self.formation["formation_code"]
+                    and sem["semestre_id"] == s
+                ):
+                    nt = self.context._getNotesCache().get_NotesTable(
+                        self.context, sem["formsemestre_id"]
+                    )  # > get_etud_decision_sem
+                    decision = nt.get_etud_decision_sem(self.etudid)
+                    if decision and code_semestre_validant(decision["code"]):
+                        validated = True
+        return s
+
+    def valide_decision(self, decision, REQUEST):
+        """Enregistre la decision (instance de DecisionSem)
+        Enregistre codes semestre et UE, et autorisations inscription.
+        """
+        cnx = self.context.GetDBConnexion(autocommit=False)
+        # -- check
+        if decision.code_etat in self.parcours.UNUSED_CODES:
+            raise ScoValueError("code decision invalide dans ce parcours")
+        #
+        if decision.code_etat == ADC:
+            fsid = decision.formsemestre_id_utilise_pour_compenser
+            if fsid:
+                ok = False
+                for sem in self.sems:
+                    if sem["formsemestre_id"] == fsid and sem["can_compensate"]:
+                        ok = True
+                        break
+                if not ok:
+                    raise ScoValueError("valide_decision: compensation impossible")
+        # -- supprime decision precedente et enregistre decision
+        to_invalidate = []
+        if self.nt.get_etud_decision_sem(self.etudid):
+            to_invalidate = formsemestre_update_validation_sem(
+                cnx,
+                self.formsemestre_id,
+                self.etudid,
+                decision.code_etat,
+                decision.assiduite,
+                decision.formsemestre_id_utilise_pour_compenser,
+            )
+        else:
+            formsemestre_validate_sem(
+                cnx,
+                self.formsemestre_id,
+                self.etudid,
+                decision.code_etat,
+                decision.assiduite,
+                decision.formsemestre_id_utilise_pour_compenser,
+            )
+        logdb(
+            REQUEST,
+            cnx,
+            method="validate_sem",
+            etudid=self.etudid,
+            commit=False,
+            msg="formsemestre_id=%s code=%s"
+            % (self.formsemestre_id, decision.code_etat),
+        )
+        # -- decisions UEs
+        formsemestre_validate_ues(
+            self.context,
+            self.formsemestre_id,
+            self.etudid,
+            decision.code_etat,
+            decision.assiduite,
+            REQUEST=REQUEST,
+        )
+        # -- modification du code du semestre precedent
+        if self.prev and decision.new_code_prev:
+            if decision.new_code_prev == ADC:
+                # ne compense le prec. qu'avec le sem. courant
+                fsid = self.formsemestre_id
+            else:
+                fsid = None
+            to_invalidate += formsemestre_update_validation_sem(
+                cnx,
+                self.prev["formsemestre_id"],
+                self.etudid,
+                decision.new_code_prev,
+                assidu=1,
+                formsemestre_id_utilise_pour_compenser=fsid,
+            )
+            logdb(
+                REQUEST,
+                cnx,
+                method="validate_sem",
+                etudid=self.etudid,
+                commit=False,
+                msg="formsemestre_id=%s code=%s"
+                % (self.prev["formsemestre_id"], decision.new_code_prev),
+            )
+            # modifs des codes d'UE (pourraient passer de ADM a CMP, meme sans modif des notes)
+            formsemestre_validate_ues(
+                self.context,
+                self.prev["formsemestre_id"],
+                self.etudid,
+                decision.new_code_prev,
+                decision.assiduite,  # XXX attention: en toute rigueur il faudrait utiliser une indication de l'assiduite au sem. precedent, que nous n'avons pas...
+                REQUEST=REQUEST,
+            )
+
+            self.context._inval_cache(
+                formsemestre_id=self.prev["formsemestre_id"]
+            )  # > modif decisions jury (sem, UE)
+
+        # -- supprime autorisations venant de ce formsemestre
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        try:
+            cursor.execute(
+                """delete from scolar_autorisation_inscription
+            where etudid = %(etudid)s and origin_formsemestre_id=%(origin_formsemestre_id)s
+            """,
+                {"etudid": self.etudid, "origin_formsemestre_id": self.formsemestre_id},
+            )
+
+            # -- enregistre autorisations inscription
+            next_semestre_ids = self.get_next_semestre_ids(decision.devenir)
+            for next_semestre_id in next_semestre_ids:
+                _scolar_autorisation_inscription_editor.create(
+                    cnx,
+                    {
+                        "etudid": self.etudid,
+                        "formation_code": self.formation["formation_code"],
+                        "semestre_id": next_semestre_id,
+                        "origin_formsemestre_id": self.formsemestre_id,
+                    },
+                )
+            cnx.commit()
+        except:
+            cnx.rollback()
+            raise
+        self.context._inval_cache(
+            formsemestre_id=self.formsemestre_id
+        )  # > modif decisions jury et autorisations inscription
+        if decision.formsemestre_id_utilise_pour_compenser:
+            # inval aussi le semestre utilisé pour compenser:
+            self.context._inval_cache(
+                formsemestre_id=decision.formsemestre_id_utilise_pour_compenser
+            )  # > modif decision jury
+        for formsemestre_id in to_invalidate:
+            self.context._inval_cache(
+                formsemestre_id=formsemestre_id
+            )  # > modif decision jury
+
+
+class SituationEtudParcoursECTS(SituationEtudParcoursGeneric):
+    """Gestion parcours basés sur ECTS
+    """
+
+    def __init__(self, context, etud, formsemestre_id, nt):
+        SituationEtudParcoursGeneric.__init__(self, context, etud, formsemestre_id, nt)
+
+    def could_be_compensated(self):
+        return False  # jamais de compensations dans ce parcours
+
+    def get_possible_choices(self, assiduite=True):
+        """Listes de décisions "recommandées" (hors décisions manuelles)
+        
+        Dans ce type de parcours, on n'utilise que ADM, AJ, et ADJ (?).
+        """
+        etud_moy_infos = self.nt.get_etud_moy_infos(self.etudid)
+        if (
+            etud_moy_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR
+            and etud_moy_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR
+        ):
+            choices = [
+                DecisionSem(
+                    code_etat=ADM,
+                    new_code_prev=None,
+                    devenir=NEXT,
+                    formsemestre_id_utilise_pour_compenser=None,
+                    explication="Semestre validé",
+                    assiduite=assiduite,
+                    rule_id="1000",
+                )
+            ]
+        else:
+            choices = [
+                DecisionSem(
+                    code_etat=AJ,
+                    new_code_prev=None,
+                    devenir=NEXT,
+                    formsemestre_id_utilise_pour_compenser=None,
+                    explication="Semestre non validé",
+                    assiduite=assiduite,
+                    rule_id="1001",
+                )
+            ]
+        return choices
+
+
+#
+def check_compensation(etudid, sem, nt, semc, ntc):
+    """Verifie si le semestre sem peut se compenser en utilisant semc
+    - semc non utilisé par un autre semestre
+    - decision du jury prise  ADM ou ADJ ou ATT ou ADC
+    - barres UE (moy ue > 8) dans sem et semc
+    - moyenne des moy_gen > 10
+    Return boolean
+    """
+    # -- deja utilise ?
+    decc = ntc.get_etud_decision_sem(etudid)
+    if (
+        decc
+        and decc["compense_formsemestre_id"]
+        and decc["compense_formsemestre_id"] != sem["formsemestre_id"]
+    ):
+        return False
+    # -- semestres consecutifs ?
+    if abs(sem["semestre_id"] - semc["semestre_id"]) != 1:
+        return False
+    # -- decision jury:
+    if decc and not decc["code"] in (ADM, ADJ, ATT, ADC):
+        return False
+    # -- barres UE et moyenne des moyennes:
+    moy_gen = nt.get_etud_moy_gen(etudid)
+    moy_genc = ntc.get_etud_moy_gen(etudid)
+    try:
+        moy_moy = (moy_gen + moy_genc) / 2
+    except:  # un des semestres sans aucune note !
+        return False
+
+    if (
+        nt.etud_check_conditions_ues(etudid)[0]
+        and ntc.etud_check_conditions_ues(etudid)[0]
+        and moy_moy >= NOTES_BARRE_GEN_COMPENSATION
+    ):
+        return True
+    else:
+        return False
+
+
+# -------------------------------------------------------------------------------------------
+
+
+def int_or_null(s):
+    if s == "":
+        return None
+    else:
+        return int(s)
+
+
+_scolar_formsemestre_validation_editor = EditableTable(
+    "scolar_formsemestre_validation",
+    "formsemestre_validation_id",
+    (
+        "formsemestre_validation_id",
+        "etudid",
+        "formsemestre_id",
+        "ue_id",
+        "code",
+        "assidu",
+        "event_date",
+        "compense_formsemestre_id",
+        "moy_ue",
+        "semestre_id",
+        "is_external",
+    ),
+    output_formators={"event_date": DateISOtoDMY, "assidu": str},
+    input_formators={"event_date": DateDMYtoISO, "assidu": int_or_null},
+)
+
+scolar_formsemestre_validation_create = _scolar_formsemestre_validation_editor.create
+scolar_formsemestre_validation_list = _scolar_formsemestre_validation_editor.list
+scolar_formsemestre_validation_delete = _scolar_formsemestre_validation_editor.delete
+scolar_formsemestre_validation_edit = _scolar_formsemestre_validation_editor.edit
+
+
+def formsemestre_validate_sem(
+    cnx,
+    formsemestre_id,
+    etudid,
+    code,
+    assidu=True,
+    formsemestre_id_utilise_pour_compenser=None,
+):
+    "Ajoute ou change validation semestre"
+    args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
+    # delete existing
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    try:
+        cursor.execute(
+            """delete from scolar_formsemestre_validation
+        where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s and ue_id is null""",
+            args,
+        )
+        # insert
+        args["code"] = code
+        args["assidu"] = assidu
+        log("formsemestre_validate_sem: %s" % args)
+        scolar_formsemestre_validation_create(cnx, args)
+        # marque sem. utilise pour compenser:
+        if formsemestre_id_utilise_pour_compenser:
+            assert code == ADC
+            args2 = {
+                "formsemestre_id": formsemestre_id_utilise_pour_compenser,
+                "compense_formsemestre_id": formsemestre_id,
+                "etudid": etudid,
+            }
+            cursor.execute(
+                """update scolar_formsemestre_validation
+            set compense_formsemestre_id=%(compense_formsemestre_id)s
+            where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s
+            and ue_id is null""",
+                args2,
+            )
+    except:
+        cnx.rollback()
+        raise
+
+
+def formsemestre_update_validation_sem(
+    cnx,
+    formsemestre_id,
+    etudid,
+    code,
+    assidu=1,
+    formsemestre_id_utilise_pour_compenser=None,
+):
+    "Update validation semestre"
+    args = {
+        "formsemestre_id": formsemestre_id,
+        "etudid": etudid,
+        "code": code,
+        "assidu": int(assidu),
+    }
+    log("formsemestre_update_validation_sem: %s" % args)
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    to_invalidate = []
+
+    # enleve compensations si necessaire
+    # recupere les semestres auparavant utilisés pour invalider les caches
+    # correspondants:
+    cursor.execute(
+        """select formsemestre_id from scolar_formsemestre_validation
+    where compense_formsemestre_id=%(formsemestre_id)s and etudid = %(etudid)s""",
+        args,
+    )
+    to_invalidate = [x[0] for x in cursor.fetchall()]
+    # suppress:
+    cursor.execute(
+        """update scolar_formsemestre_validation set compense_formsemestre_id=NULL
+    where compense_formsemestre_id=%(formsemestre_id)s and etudid = %(etudid)s""",
+        args,
+    )
+    if formsemestre_id_utilise_pour_compenser:
+        assert code == ADC
+        # marque sem. utilise pour compenser:
+        args2 = {
+            "formsemestre_id": formsemestre_id_utilise_pour_compenser,
+            "compense_formsemestre_id": formsemestre_id,
+            "etudid": etudid,
+        }
+        cursor.execute(
+            """update scolar_formsemestre_validation
+        set compense_formsemestre_id=%(compense_formsemestre_id)s
+        where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s
+        and ue_id is null""",
+            args2,
+        )
+
+    cursor.execute(
+        """update scolar_formsemestre_validation
+    set code = %(code)s, event_date=DEFAULT, assidu=%(assidu)s
+    where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s
+    and ue_id is null""",
+        args,
+    )
+    return to_invalidate
+
+
+def formsemestre_validate_ues(
+    context, formsemestre_id, etudid, code_etat_sem, assiduite, REQUEST=None
+):
+    """Enregistre codes UE, selon état semestre.
+    Les codes UE sont toujours calculés ici, et non passés en paramètres
+    car ils ne dépendent que de la note d'UE et de la validation ou non du semestre.
+    Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ).
+    """
+    valid_semestre = CODES_SEM_VALIDES.get(code_etat_sem, False)
+    cnx = context.GetDBConnexion(autocommit=False)
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_ues, get_etud_ue_status
+    ue_ids = [x["ue_id"] for x in nt.get_ues(etudid=etudid, filter_sport=True)]
+    for ue_id in ue_ids:
+        ue_status = nt.get_etud_ue_status(etudid, ue_id)
+        if not assiduite:
+            code_ue = AJ
+        else:
+            # log('%s: %s: ue_status=%s' % (formsemestre_id,ue_id,ue_status))
+            if (
+                type(ue_status["moy"]) == FloatType
+                and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE
+            ):
+                code_ue = ADM
+            elif type(ue_status["moy"]) != FloatType:
+                # aucune note (pas de moyenne) dans l'UE: ne la valide pas
+                code_ue = None
+            elif valid_semestre:
+                code_ue = CMP
+            else:
+                code_ue = AJ
+        # log('code_ue=%s' % code_ue)
+        if etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id) and code_ue:
+            do_formsemestre_validate_ue(
+                cnx, nt, formsemestre_id, etudid, ue_id, code_ue
+            )
+
+        if REQUEST:
+            logdb(
+                REQUEST,
+                cnx,
+                method="validate_ue",
+                etudid=etudid,
+                msg="ue_id=%s code=%s" % (ue_id, code_ue),
+                commit=False,
+            )
+    cnx.commit()
+
+
+def do_formsemestre_validate_ue(
+    cnx,
+    nt,
+    formsemestre_id,
+    etudid,
+    ue_id,
+    code,
+    moy_ue=None,
+    date=None,
+    semestre_id=None,
+    is_external=0,
+):
+    """Ajoute ou change validation UE
+    """
+    args = {
+        "formsemestre_id": formsemestre_id,
+        "etudid": etudid,
+        "ue_id": ue_id,
+        "semestre_id": semestre_id,
+        "is_external": is_external,
+    }
+    if date:
+        args["event_date"] = date
+
+    # delete existing
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    try:
+        cond = "etudid = %(etudid)s and ue_id=%(ue_id)s"
+        if formsemestre_id:
+            cond += " and formsemestre_id=%(formsemestre_id)s"
+        if semestre_id:
+            cond += " and semestre_id=%(semestre_id)s"
+        cursor.execute("delete from scolar_formsemestre_validation where " + cond, args)
+        # insert
+        args["code"] = code
+        if code == ADM:
+            if moy_ue is None:
+                # stocke la moyenne d'UE capitalisée:
+                moy_ue = nt.get_etud_ue_status(etudid, ue_id)["moy"]
+            args["moy_ue"] = moy_ue
+        log("formsemestre_validate_ue: %s" % args)
+        if code != None:
+            scolar_formsemestre_validation_create(cnx, args)
+        else:
+            log("formsemestre_validate_ue: code is None, not recording validation")
+    except:
+        cnx.rollback()
+        raise
+
+
+def formsemestre_has_decisions(context, formsemestre_id):
+    """True s'il y a au moins une validation (decision de jury) dans ce semestre
+    equivalent to notes_table.sem_has_decisions() but much faster when nt not cached
+    """
+    cnx = context.GetDBConnexion()
+    validations = scolar_formsemestre_validation_list(
+        cnx, args={"formsemestre_id": formsemestre_id}
+    )
+    return len(validations) > 0
+
+
+def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id):
+    """Vrai si l'étudiant est inscrit a au moins un module de cette UE dans ce semestre"""
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        """select mi.* from notes_moduleimpl mi, notes_modules mo, notes_ue ue, notes_moduleimpl_inscription i
+    where i.etudid = %(etudid)s and i.moduleimpl_id=mi.moduleimpl_id
+    and mi.formsemestre_id = %(formsemestre_id)s
+    and mi.module_id = mo.module_id
+    and mo.ue_id = %(ue_id)s
+    """,
+        {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
+    )
+
+    return len(cursor.fetchall())
+
+
+_scolar_autorisation_inscription_editor = EditableTable(
+    "scolar_autorisation_inscription",
+    "autorisation_inscription_id",
+    ("etudid", "formation_code", "semestre_id", "date", "origin_formsemestre_id"),
+    output_formators={"date": DateISOtoDMY},
+    input_formators={"date": DateDMYtoISO},
+)
+scolar_autorisation_inscription_list = _scolar_autorisation_inscription_editor.list
+
+
+def formsemestre_get_autorisation_inscription(context, etudid, origin_formsemestre_id):
+    """Liste des autorisations d'inscription pour cet étudiant
+    émanant du semestre indiqué.
+    """
+    cnx = context.GetDBConnexion()
+    return scolar_autorisation_inscription_list(
+        cnx, {"origin_formsemestre_id": origin_formsemestre_id, "etudid": etudid}
+    )
+
+
+def formsemestre_get_etud_capitalisation(context, sem, etudid):
+    """Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant.
+    
+    Recherche dans les semestres de la même formation (code) avec le même
+    semestre_id et une date de début antérieure à celle du semestre mentionné.
+    Et aussi les UE externes validées.
+    
+    Resultat: [ { 'formsemestre_id' :
+                  'ue_id' : ue_id dans le semestre origine
+                  'ue_code' : 
+                  'moy_ue' :
+                  'event_date' : 
+                  'is_external'                 
+                  } ]
+    """
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        """select distinct SFV.*, ue.ue_code from notes_ue ue, notes_formations nf, notes_formations nf2,
+    scolar_formsemestre_validation SFV, notes_formsemestre sem
+
+    where ue.formation_id = nf.formation_id    
+    and nf.formation_code = nf2.formation_code 
+    and nf2.formation_id=%(formation_id)s
+
+    and SFV.ue_id = ue.ue_id
+    and SFV.code = 'ADM'
+    and SFV.etudid = %(etudid)s
+    
+    and (  (sem.formsemestre_id = SFV.formsemestre_id
+           and sem.date_debut < %(date_debut)s
+           and sem.semestre_id = %(semestre_id)s )
+         or (
+             ((SFV.formsemestre_id is NULL) OR (SFV.is_external = 1)) -- les UE externes ou "anterieures"
+             AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
+           ) )
+    """,
+        {
+            "etudid": etudid,
+            "formation_id": sem["formation_id"],
+            "semestre_id": sem["semestre_id"],
+            "date_debut": DateDMYtoISO(sem["date_debut"]),
+        },
+    )
+
+    return cursor.dictfetchall()
+
+
+def list_formsemestre_utilisateurs_uecap(context, formsemestre_id):
+    """Liste des formsemestres pouvant utiliser une UE capitalisee de ce semestre
+    (et qui doivent donc etre sortis du cache si l'on modifie ce
+    semestre): meme code formation, meme semestre_id, date posterieure"""
+    cnx = context.GetDBConnexion()
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        """select sem.formsemestre_id
+    from notes_formsemestre sem, notes_formations F
+    where sem.formation_id = F.formation_id
+    and F.formation_code = %(formation_code)s
+    and sem.semestre_id = %(semestre_id)s
+    and sem.date_debut >= %(date_debut)s
+    and sem.formsemestre_id != %(formsemestre_id)s;
+    """,
+        {
+            "formation_code": F["formation_code"],
+            "semestre_id": sem["semestre_id"],
+            "formsemestre_id": formsemestre_id,
+            "date_debut": DateDMYtoISO(sem["date_debut"]),
+        },
+    )
+    return [x[0] for x in cursor.fetchall()]
diff --git a/sco_pdf.py b/sco_pdf.py
new file mode 100644
index 0000000000000000000000000000000000000000..18d8af292a65c96e85661538f7ff8395a6975d3e
--- /dev/null
+++ b/sco_pdf.py
@@ -0,0 +1,330 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Generation de PDF: définitions diverses et gestion du verrou
+
+    reportlab n'est pas réentrante: il ne faut qu'une seule opération PDF au même moment.
+    Tout accès à ReportLab doit donc être précédé d'un PDFLOCK.acquire()
+    et terminé par un PDFLOCK.release()
+"""
+import time, cStringIO
+from types import StringType
+import unicodedata
+import traceback
+import reportlab
+from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
+from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
+from reportlab.platypus.flowables import Flowable
+from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.rl_config import defaultPageSize
+from reportlab.lib.units import inch, cm, mm
+from reportlab.lib.colors import pink, black, red, blue, green, magenta, red
+from reportlab.lib.colors import Color
+from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
+from reportlab.lib import styles
+from reportlab.lib.pagesizes import letter, A4, landscape
+
+from sco_utils import *
+from notes_log import log
+from SuppressAccents import suppression_diacritics
+from VERSION import SCOVERSION, SCONAME
+
+PAGE_HEIGHT = defaultPageSize[1]
+PAGE_WIDTH = defaultPageSize[0]
+
+
+DEFAULT_PDF_FOOTER_TEMPLATE = CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE
+
+
+def SU(s):
+    "convert s from SCO default encoding to UTF8"
+    # Mis en service le 4/11/06, passage à ReportLab 2.0
+    if not s:
+        s = ""
+    # Remplace caractères composés
+    #  eg 'e\xcc\x81' COMBINING ACUTE ACCENT  par '\xc3\xa9' LATIN SMALL LETTER E WITH ACUTE
+    # car les "combining accents" ne sont pas traités par ReportLab mais peuvent
+    # nous être envoyés par certains navigaters ou imports
+    u = unicodedata.normalize("NFC", unicode(s, SCO_ENCODING, "replace"))
+    return u.encode("utf8")
+
+
+def _splitPara(txt):
+    "split a string, returns a list of <para > ... </para>"
+    L = []
+    closetag = "</para>"
+    l = len(closetag)
+    start = 0
+    e = -1
+    while 1:
+        b = txt.find("<para", start)
+        if b < 0:
+            if e < 0:
+                L.append(txt)  # no para, just return text
+            break
+        e = txt.find(closetag, b)
+        if e < 0:
+            raise ValueError("unbalanced para tags")
+        L.append(txt[b : e + l])
+        start = e
+    # fix para: must be followed by a newline (? Reportlab bug turnaround ?)
+    L = [re.sub("<para(.*?)>([^\n\r])", "<para\\1>\n\\2", p) for p in L]
+
+    return L
+
+
+def makeParas(txt, style, suppress_empty=False):
+    """Returns a list of Paragraph instances from a text
+    with one or more <para> ... </para>
+    """
+    try:
+        paras = _splitPara(txt)
+        if suppress_empty:
+            r = []
+            for para in paras:
+                m = re.match("\s*<\s*para.*>\s*(.*)\s*<\s*/\s*para\s*>\s*", para)
+                if not m:
+                    r.append(para)  # not a paragraph, keep it
+                else:
+                    if m.group(1):  # non empty paragraph
+                        r.append(para)
+            paras = r
+        return [Paragraph(SU(s), style) for s in paras]
+    except Exception as e:
+        if type(e) is IOError:
+            detail = " " + e.message
+        else:
+            detail = ""
+        log(traceback.format_exc())
+        log("Invalid pdf para format: %s" % txt)
+        return [
+            Paragraph(
+                SU(
+                    '<font color="red"><i>Erreur: format invalide{}</i></font>'.format(
+                        detail
+                    )
+                ),
+                style,
+            )
+        ]
+
+
+def bold_paras(L, tag="b", close=None):
+    """Put each (string) element of L between  <b>
+    L is a dict or sequence. (if dict, elements with key begining by _ are unaffected)
+    """
+    b = "<" + tag + ">"
+    if not close:
+        close = "</" + tag + ">"
+    if hasattr(L, "keys"):
+        # L is a dict
+        for k in L:
+            x = L[k]
+            if k[0] != "_":
+                L[k] = b + L[k] or "" + close
+        return L
+    else:
+        # L is a sequence
+        return [b + (x or "") + close for x in L]
+
+
+class ScolarsPageTemplate(PageTemplate):
+    """Our own page template."""
+
+    def __init__(
+        self,
+        document,
+        pagesbookmarks={},
+        author=None,
+        title=None,
+        subject=None,
+        margins=(0, 0, 0, 0),  # additional margins in mm (left,top,right, bottom)
+        server_name="",
+        footer_template=DEFAULT_PDF_FOOTER_TEMPLATE,
+        filigranne=None,
+        preferences=None,  # dictionnary with preferences, required
+    ):
+        """Initialise our page template."""
+        self.preferences = preferences
+        self.pagesbookmarks = pagesbookmarks
+        self.pdfmeta_author = author
+        self.pdfmeta_title = title
+        self.pdfmeta_subject = subject
+        self.server_name = server_name
+        self.filigranne = filigranne
+        self.footer_template = footer_template
+        # Our doc is made of a single frame
+        left, top, right, bottom = [float(x) for x in margins]
+        content = Frame(
+            10.0 * mm + left * mm,
+            13.0 * mm + bottom * mm,
+            document.pagesize[0] - 20.0 * mm - left * mm - right * mm,
+            document.pagesize[1] - 18.0 * mm - top * mm - bottom * mm,
+        )
+        PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
+        self.logo = None
+
+    def beforeDrawPage(self, canvas, doc):
+        """Draws a logo and an contribution message on each page.
+
+        day   : Day of the month as a decimal number [01,31]
+        month : Month as a decimal number [01,12].
+        year  : Year without century as a decimal number [00,99].
+        Year  : Year with century as a decimal number.
+        hour  : Hour (24-hour clock) as a decimal number [00,23].
+        minute: Minute as a decimal number [00,59].
+
+        server_url: URL du serveur ScoDoc
+        
+        """
+        canvas.saveState()
+        if self.logo is not None:
+            # draws the logo if it exists
+            ((width, height), image) = self.logo
+            canvas.drawImage(image, inch, doc.pagesize[1] - inch, width, height)
+
+        # ---- Filigranne (texte en diagonal en haut a gauche de chaque page)
+        if self.filigranne:
+            if type(self.filigranne) == StringType:
+                filigranne = self.filigranne  # same for all pages
+            else:
+                filigranne = self.filigranne.get(doc.page, None)
+            if filigranne:
+                canvas.saveState()
+                canvas.translate(9 * cm, 27.6 * cm)
+                canvas.rotate(30)
+                canvas.scale(4.5, 4.5)
+                canvas.setFillColorRGB(1.0, 0.65, 0.65)
+                canvas.drawRightString(0, 0, SU(filigranne))
+                canvas.restoreState()
+
+        # ---- Add some meta data and bookmarks
+        if self.pdfmeta_author:
+            canvas.setAuthor(SU(self.pdfmeta_author))
+        if self.pdfmeta_title:
+            canvas.setTitle(SU(self.pdfmeta_title))
+        if self.pdfmeta_subject:
+            canvas.setSubject(SU(self.pdfmeta_subject))
+        bm = self.pagesbookmarks.get(doc.page, None)
+        if bm != None:
+            key = bm
+            txt = SU(bm)
+            canvas.bookmarkPage(key)
+            canvas.addOutlineEntry(txt, bm)
+        # ---- Footer
+        canvas.setFont(
+            self.preferences["SCOLAR_FONT"], self.preferences["SCOLAR_FONT_SIZE_FOOT"]
+        )
+        d = _makeTimeDict()
+        d["scodoc_name"] = VERSION.SCONAME
+        d["server_url"] = self.server_name
+        footer_str = SU(self.footer_template % d)
+        canvas.drawString(
+            self.preferences["pdf_footer_x"] * mm,
+            self.preferences["pdf_footer_y"] * mm,
+            footer_str,
+        )
+        canvas.restoreState()
+
+
+def _makeTimeDict():
+    # ... suboptimal but we don't care
+    return {
+        "day": time.strftime("%d"),
+        "month": time.strftime("%m"),
+        "year": time.strftime("%y"),
+        "Year": time.strftime("%Y"),
+        "hour": time.strftime("%H"),
+        "minute": time.strftime("%M"),
+    }
+
+
+def pdf_basic_page(
+    objects, title="", preferences=None
+):  # used by gen_table.make_page()
+    """Simple convenience fonction: build a page from a list of platypus objects,
+    adding a title if specified.
+    """
+    StyleSheet = styles.getSampleStyleSheet()
+    report = cStringIO.StringIO()  # in-memory document, no disk file
+    document = BaseDocTemplate(report)
+    document.addPageTemplates(
+        ScolarsPageTemplate(
+            document,
+            title=title,
+            author="%s %s (E. Viennet)" % (SCONAME, SCOVERSION),
+            footer_template="Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s",
+            preferences=preferences,
+        )
+    )
+    if title:
+        head = Paragraph(SU(title), StyleSheet["Heading3"])
+        objects = [head] + objects
+    document.build(objects)
+    data = report.getvalue()
+    return data
+
+
+# Gestion du lock pdf
+import threading, time, Queue, thread
+
+
+class PDFLock:
+    def __init__(self, timeout=15):
+        self.Q = Queue.Queue(1)
+        self.timeout = timeout
+        self.current_thread = None
+        self.nref = 0
+
+    def release(self):
+        "Release lock. Raise Empty if not acquired first"
+        if self.current_thread == thread.get_ident():
+            self.nref -= 1
+            if self.nref == 0:
+                log("PDFLock: release from %s" % self.current_thread)
+                self.current_thread = None
+                self.Q.get(False)
+            return
+        else:
+            self.Q.get(False)
+
+    def acquire(self):
+        "Acquire lock. Raise ScoGenError if can't lock after timeout."
+        if self.current_thread == thread.get_ident():
+            self.nref += 1
+            return  # deja lock pour ce thread
+        try:
+            self.Q.put(1, True, self.timeout)
+        except Queue.Full:
+            raise ScoGenError(msg="Traitement PDF occupé: ré-essayez")
+        self.current_thread = thread.get_ident()
+        self.nref = 1
+        log("PDFLock: granted to %s" % self.current_thread)
+
+
+PDFLOCK = PDFLock()
diff --git a/sco_permissions.py b/sco_permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..b656db15dc89344c9fc1c11c94ee26b271fe3cee
--- /dev/null
+++ b/sco_permissions.py
@@ -0,0 +1,74 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Definitions of Zope permissions used by ScoDoc"""
+
+# prefix all permissions by "Sco" to group them in Zope management tab
+
+# Attention: si on change ces valeurs, il faut verifier les codes
+# DTML qui utilisent directement les chaines de caractères...
+
+ScoChangeFormation = "Sco Change Formation"
+ScoEditAllNotes = "Sco Modifier toutes notes"
+ScoEditAllEvals = "Sco Modifier toutes les evaluations"
+
+ScoImplement = "Sco Implement Formation"
+
+ScoAbsChange = "Sco Change Absences"
+ScoAbsAddBillet = (
+    "Sco Add Abs Billet"  # ajouter un billet d'absence via AddBilletAbsence
+)
+ScoEtudChangeAdr = "Sco Change Etud Address"  # changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche
+ScoEtudChangeGroups = "Sco Change Etud Groups"
+ScoEtudInscrit = "Sco Inscrire Etud"  # aussi pour demissions, diplomes
+ScoEtudAddAnnotations = "Sco Etud Add Annotations"  # aussi pour archives
+ScoEtudSupprAnnotations = "Sco Etud Suppr Annotations"  # XXX inutile: utiliser Add !
+ScoEntrepriseView = "Sco View Entreprises"
+ScoEntrepriseChange = "Sco Change Entreprises"
+ScoEditPVJury = "Sco Edit PV Jury"
+
+ScoEditApo = ScoEtudChangeAdr  # ajouter maquettes Apogee (=> chef dept et secr)
+
+ScoEditFormationTags = (
+    "Sco Tagguer les formations"  # mettre/modifier des tags sur les modules
+)
+
+ScoView = "Sco View"
+ScoEnsView = "Sco View Ens"  # parties visibles par enseignants slt
+
+ScoUsersAdmin = "Sco Users Manage"
+ScoUsersView = "Sco Users View"
+
+ScoChangePreferences = "Sco Change Preferences"
+
+ScoSuperAdmin = "Sco Super Admin"
+# ScoSuperAdmin est utilisé pour:
+#   - ZScoDoc: add/delete departments
+#   - tous rôles lors creation utilisateurs
+#
+
+
+# Default permissions for default roles
+# (set once on instance creation):
+Sco_Default_Permissions = {
+    ScoView: ("Ens", "Secr", "Admin", "RespPe"),
+    ScoEnsView: ("Ens", "Admin", "RespPe"),
+    ScoUsersView: ("Ens", "Secr", "Admin", "RespPe"),
+    ScoEtudAddAnnotations: ("Ens", "Secr", "Admin", "RespPe"),
+    ScoEtudSupprAnnotations: ("Admin",),
+    ScoAbsChange: ("Ens", "Secr", "Admin", "RespPe"),
+    ScoAbsAddBillet: ("Ens", "Secr", "Admin", "RespPe"),
+    ScoEntrepriseView: ("Ens", "Secr", "Admin", "RespPe"),
+    ScoEntrepriseChange: ("Secr", "Admin"),
+    ScoEtudChangeAdr: ("Secr", "Admin"),  # utilisé aussi pour pv jury secretariats
+    ScoChangeFormation: ("Admin",),
+    ScoEditFormationTags: ("Admin", "RespPe"),
+    ScoEditAllNotes: ("Admin",),
+    ScoEditAllEvals: ("Admin",),
+    ScoImplement: ("Admin",),
+    ScoEtudChangeGroups: ("Admin",),
+    ScoEtudInscrit: ("Admin",),
+    ScoUsersAdmin: ("Admin",),
+    ScoChangePreferences: ("Admin",),
+    ScoSuperAdmin: (),  # lister tt les permissions
+}
diff --git a/sco_photos.py b/sco_photos.py
new file mode 100644
index 0000000000000000000000000000000000000000..575c8c4ea6b108e310617c8b3d17517a9ed7cc65
--- /dev/null
+++ b/sco_photos.py
@@ -0,0 +1,360 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""(Nouvelle) (Nouvelle) gestion des photos d'etudiants
+
+Les images sont stockées dans .../var/scodoc/photos
+L'attribut "photo_filename" de la table identite donne le nom du fichier image, 
+sans extension (e.g. "F44/RT_EID31545").
+Toutes les images sont converties en jpg, et stockées dans photo_filename.jpg en taille originale.
+Elles sont aussi réduites en 90 pixels de hauteur, et stockées dans photo_filename.h90.jpg
+
+Les images sont servies par ScoDoc, via la méthode getphotofile?etudid=xxx
+
+
+## Historique:
+ - jusqu'à novembre 2009, les images étaient stockées dans Zope (ZODB). 
+ - jusqu'à v1908, stockées dans .../static/photos (et donc accessibles sans authentification).
+ - support for legacy ZODB removed in v1909.
+
+"""
+
+import os
+import time
+import datetime
+import random
+import urllib2
+import traceback
+from PIL import Image as PILImage
+from cStringIO import StringIO
+import glob
+
+from sco_utils import *
+from notes_log import log
+from notesdb import *
+import scolars
+import sco_portal_apogee
+from scolog import logdb
+
+# Full paths on server's filesystem. Something like "/opt/scodoc/var/scodoc/photos"
+PHOTO_DIR = os.path.join(os.environ["INSTANCE_HOME"], "var", "scodoc", "photos")
+ICONS_DIR = os.path.join(SCO_SRCDIR, "static", "icons")
+UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg")
+UNKNOWN_IMAGE_URL = "get_photo_image?etudid=" # with empty etudid => unknown face image
+IMAGE_EXT = ".jpg"
+JPG_QUALITY = 0.92
+REDUCED_HEIGHT = 90  # pixels
+MAX_FILE_SIZE = 1024 * 1024  # max allowed size for uploaded image, in bytes
+H90 = ".h90"  # suffix for reduced size images
+
+
+def photo_portal_url(context, etud):
+    """Returns external URL to retreive photo on portal,
+    or None if no portal configured"""
+    photo_url = sco_portal_apogee.get_photo_url(context)
+    if photo_url and etud["code_nip"]:
+        return photo_url + "?nip=" + etud["code_nip"]
+    else:
+        return None
+
+def etud_photo_url(context, etud, size="small", REQUEST=None):
+    """url to the image of the student, in "small" size or "orig" size.
+    If ScoDoc doesn't have an image and a portal is configured, link to it.
+    """
+    photo_url = "get_photo_image?etudid=%s&size=%s" % (etud["etudid"], size)
+    path = photo_pathname(context, etud, size=size)
+    if not path:
+        # Portail ?
+        ext_url = photo_portal_url(context, etud)
+        if not ext_url:
+            # fallback: Photo "unknown"
+            photo_url = UNKNOWN_IMAGE_URL
+        else:
+            # essaie de copier la photo du portail
+            new_path, diag = copy_portal_photo_to_fs(context, etud, REQUEST=REQUEST)
+            if not new_path:
+                # copy failed, can we use external url ?
+                # nb: rarement utile, car le portail est rarement accessible sans authentification
+                if CONFIG.PUBLISH_PORTAL_PHOTO_URL:
+                    photo_url = ext_url
+                else:
+                    photo_url = UNKNOWN_IMAGE_URL
+    return photo_url
+
+
+def get_photo_image(context, etudid=None, size="small", REQUEST=None):
+    """Returns photo image (HTTP response)
+    If not etudid, use "unknown" image
+    """
+    if not etudid:
+        filename = UNKNOWN_IMAGE_PATH
+    else:
+        etud = context.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+        filename = photo_pathname(context, etud, size=size) # os.path.join( PHOTO_DIR, etud["photo_filename"] )
+        if not filename:
+            filename = UNKNOWN_IMAGE_PATH
+    return _http_jpeg_file(context, filename, REQUEST=REQUEST)
+
+
+def _http_jpeg_file(context, filename, REQUEST=None):
+    """returns an image.
+    This function will be modified when we kill #zope
+    """
+    st = os.stat(filename)
+    last_modified = st.st_mtime  # float timestamp
+    last_modified_str = time.strftime(
+        "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified)
+    )
+    file_size = st.st_size
+    RESPONSE = REQUEST.RESPONSE
+    RESPONSE.setHeader("Content-Type", "image/jpeg")
+    RESPONSE.setHeader("Last-Modified", last_modified_str)
+    RESPONSE.setHeader("Cache-Control", "max-age=3600")
+    RESPONSE.setHeader("Content-Length", str(file_size))
+    header = REQUEST.get_header("If-Modified-Since", None)
+    if header is not None:
+        header = header.split(";")[0]
+        # Some proxies seem to send invalid date strings for this
+        # header. If the date string is not valid, we ignore it
+        # rather than raise an error to be generally consistent
+        # with common servers such as Apache (which can usually
+        # understand the screwy date string as a lucky side effect
+        # of the way they parse it).
+        try:
+            dt = datetime.datetime.strptime(header, "%a, %d %b %Y %H:%M:%S GMT")
+            mod_since = dt.timestamp()
+        except:
+            mod_since = None
+        if (mod_since is not None) and last_modified <= mod_since:
+            RESPONSE.setStatus(304)  # not modified
+            return ""
+
+    return open(filename, mode="rb").read()
+
+
+def etud_photo_is_local(context, etud, size="small"):
+    return photo_pathname(context, etud, size=size)
+
+
+def etud_photo_html(context, etud=None, etudid=None, title=None, size="small", REQUEST=None):
+    """HTML img tag for the photo, either in small size (h90) 
+    or original size (size=="orig")
+    """
+    if not etud:
+        if etudid:
+            etud = context.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+        else:
+            raise ValueError('etud_photo_html: either etud or etudid must be specified')
+    photo_url = etud_photo_url(context, etud, size=size, REQUEST=REQUEST)
+    nom = etud.get("nomprenom", etud["nom_disp"])
+    if title is None:
+        title = nom
+    if not etud_photo_is_local(context, etud):
+        fallback = (
+            """onerror='this.onerror = null; this.src="%s"'"""
+            % UNKNOWN_IMAGE_URL
+        )
+    else:
+        fallback = ""
+    if size == "small":
+        height_attr = 'height="%s"' % REDUCED_HEIGHT
+    else:
+        height_attr = ""
+    return '<img src="%s" alt="photo %s" title="%s" border="0" %s %s />' % (
+        photo_url,
+        nom,
+        title,
+        height_attr,
+        fallback,
+    )
+
+def etud_photo_orig_html(context, etud=None, etudid=None, title=None, REQUEST=None):
+    """HTML img tag for the photo, in full size.
+    Full-size images are always stored locally in the filesystem.
+    They are the original uploaded images, converted in jpeg.
+    """
+    return etud_photo_html(context, etud=etud, etudid=etudid, title=title, size="orig", REQUEST=REQUEST)
+
+def photo_pathname(context, etud, size="orig"):
+    """Returns full path of image file if etud has a photo (in the filesystem), or False.
+    Do not distinguish the cases: no photo, or file missing.
+    """
+    if size == "small":
+        version = H90
+    elif size == "orig":
+        version = ""
+    else:
+        raise ValueError("invalid size parameter for photo")
+    if not etud["photo_filename"]:
+        return False
+    path = os.path.join(PHOTO_DIR, etud["photo_filename"]) + version + IMAGE_EXT
+    if os.path.exists(path):
+        return path
+    else:
+        return False
+
+
+def store_photo(context, etud, data, REQUEST=None):
+    """Store image for this etud.
+    If there is an existing photo, it is erased and replaced.
+    data is a string with image raw data.
+
+    Update database to store filename.
+
+    Returns (status, msg)
+    """
+    # basic checks
+    filesize = len(data)
+    if filesize < 10 or filesize > MAX_FILE_SIZE:
+        return 0, "Fichier image de taille invalide ! (%d)" % filesize
+    filename = save_image(context, etud["etudid"], data)
+    # update database:
+    etud["photo_filename"] = filename
+    etud["foto"] = None
+
+    cnx = context.GetDBConnexion()
+    scolars.identite_edit_nocheck(cnx, etud)
+    cnx.commit()
+    #
+    if REQUEST:
+        logdb(REQUEST, cnx, method="changePhoto", msg=filename, etudid=etud["etudid"])
+    #
+    return 1, "ok"
+
+
+def suppress_photo(context, etud, REQUEST=None):
+    """Suppress a photo"""
+    log("suppress_photo etudid=%s" % etud["etudid"])
+    rel_path = photo_pathname(context, etud)
+    # 1- remove ref. from database
+    etud["photo_filename"] = None
+    cnx = context.GetDBConnexion()
+    scolars.identite_edit_nocheck(cnx, etud)
+    cnx.commit()
+    # 2- erase images files
+    #log("rel_path=%s" % rel_path)
+    if rel_path:
+        # remove extension and glob
+        rel_path = rel_path[: -len(IMAGE_EXT)]
+        filenames = glob.glob(rel_path + "*" + IMAGE_EXT)
+        for filename in filenames:
+            log("removing file %s" % filename)
+            os.remove(filename)
+    # 3- log
+    if REQUEST:
+        logdb(
+            REQUEST, cnx, method="changePhoto", msg="suppression", etudid=etud["etudid"]
+        )
+
+
+# ---------------------------------------------------------------------------
+# Internal functions
+
+def save_image(context, etudid, data):
+    """img_file is a file-like object.
+    Save image in JPEG in 2 sizes (original and h90).
+    Returns filename (relative to PHOTO_DIR), without extension
+    """
+    data_file = StringIO()
+    data_file.write(data)
+    data_file.seek(0)
+    img = PILImage.open(data_file)
+    filename = get_new_filename(context, etudid)
+    path = os.path.join( PHOTO_DIR, filename )
+    log("saving %dx%d jpeg to %s" % (img.size[0], img.size[1], path))
+    img.save(path + IMAGE_EXT, format="JPEG", quality=92)
+    # resize:
+    img = scale_height(img)
+    log("saving %dx%d jpeg to %s.h90" % (img.size[0], img.size[1], filename))
+    img.save(path + H90 + IMAGE_EXT, format="JPEG", quality=92)
+    return filename
+
+
+def scale_height(img, W=None, H=REDUCED_HEIGHT):
+    if W is None:
+        # keep aspect
+        W = (img.size[0] * H) / img.size[1]
+    img.thumbnail((W, H), PILImage.ANTIALIAS)
+    return img
+
+
+def get_new_filename(context, etudid):
+    """Constructs a random filename to store a new image.
+    The path is constructed as: Fxx/etudid
+    """
+    dept = context.DeptId()
+    return find_new_dir() + dept + "_" + etudid
+
+
+def find_new_dir():
+    """select randomly a new subdirectory to store a new file.
+    We define 100 subdirectories named from F00 to F99.
+    Returns a path relative to the PHOTO_DIR.
+    """
+    d = "F" + "%02d" % random.randint(0, 99)
+    path = os.path.join(PHOTO_DIR, d)
+    if not os.path.exists(path):
+        # ensure photos directory exists
+        if not os.path.exists(PHOTO_DIR):
+            os.mkdir(PHOTO_DIR)
+        # create subdirectory
+        log("creating directory %s" % path)
+        os.mkdir(path)
+    return d + "/"
+
+
+def copy_portal_photo_to_fs(context, etud, REQUEST=None):
+    """Copy the photo from portal (distant website) to local fs.
+    Returns rel. path or None if copy failed, with a diagnotic message
+    """
+    scolars.format_etud_ident(etud)
+    url = photo_portal_url(context, etud)
+    if not url:
+        return None, "%(nomprenom)s: pas de code NIP" % etud
+    portal_timeout = context.get_preference("portal_timeout")
+    f = None
+    try:
+        log("copy_portal_photo_to_fs: getting %s" % url)
+        f = urllib2.urlopen(url, timeout=portal_timeout)  # python >= 2.7
+    except:
+        log("download failed: exception:\n%s" % traceback.format_exc())
+        return None, "%s: erreur chargement de %s" % (etud["nomprenom"], url)
+    if not f:
+        log("download failed")
+        return None, "%s: erreur chargement de %s" % (etud["nomprenom"], url)
+    data = f.read()
+    try:
+        status, diag = store_photo(context, etud, data, REQUEST=REQUEST)
+    except:
+        status = 0
+        diag = "Erreur chargement photo du portail"
+        log("copy_portal_photo_to_fs: failure (exception in store_photo)!")
+    if status == 1:
+        log("copy_portal_photo_to_fs: copied %s" % url)
+        return photo_pathname(context, etud), "%s: photo chargée" % etud["nomprenom"]
+    else:
+        return None, "%s: <b>%s</b>" % (etud["nomprenom"], diag)
diff --git a/sco_placement.py b/sco_placement.py
new file mode 100644
index 0000000000000000000000000000000000000000..4dd8fd4224e18a674d433c04a1fc406581c3438a
--- /dev/null
+++ b/sco_placement.py
@@ -0,0 +1,795 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""ScoDoc: génération feuille émargement et placement
+
+Contribution M. Salomon, UFC / IUT DE BELFORT-MONTBÉLIARD, 2016
+
+"""
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+import scolars
+import sco_formsemestre
+import sco_groups
+import sco_evaluations
+import sco_excel
+from sco_excel import *
+from gen_tables import GenTable
+import random
+
+
+def do_placement_selectetuds(context, REQUEST):
+    """
+    Choisi les etudiants et les infos sur la salle pour le placement des etudiants
+    """
+    evaluation_id = REQUEST.form["evaluation_id"]
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})
+    if not E:
+        raise ScoValueError("invalid evaluation_id")
+    E = E[0]
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    formsemestre_id = M["formsemestre_id"]
+    # groupes
+    groups = sco_groups.do_evaluation_listegroupes(
+        context, evaluation_id, include_default=True
+    )
+    grlabs = [g["group_name"] or "tous" for g in groups]  # legendes des boutons
+    grnams = [g["group_id"] for g in groups]  # noms des checkbox
+    no_groups = (len(groups) == 1) and groups[0]["group_name"] is None
+
+    # description de l'evaluation
+    H = [
+        sco_evaluations.evaluation_describe(
+            context, evaluation_id=evaluation_id, REQUEST=REQUEST
+        ),
+        "<h3>Placement et émargement des étudiants</h3>",
+    ]
+    #
+    descr = [
+        ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
+        (
+            "placement_method",
+            {
+                "input_type": "radio",
+                "default": "xls",
+                "allow_null": False,
+                "allowed_values": ["pdf", "xls"],
+                "labels": ["fichier pdf", "fichier xls"],
+                "title": "Format de fichier :",
+            },
+        ),
+        ("teachers", {"size": 25, "title": "Surveillants :"}),
+        ("building", {"size": 25, "title": "Batiment :"}),
+        ("room", {"size": 10, "title": "Salle :"}),
+        (
+            "columns",
+            {
+                "input_type": "radio",
+                "default": "5",
+                "allow_null": False,
+                "allowed_values": ["3", "4", "5", "6", "7", "8"],
+                "labels": [
+                    "3 colonnes",
+                    "4 colonnes",
+                    "5 colonnes",
+                    "6 colonnes",
+                    "7 colonnes",
+                    "8 colonnes",
+                ],
+                "title": "Nombre de colonnes :",
+            },
+        ),
+        (
+            "numbering",
+            {
+                "input_type": "radio",
+                "default": "coordinate",
+                "allow_null": False,
+                "allowed_values": ["continuous", "coordinate"],
+                "labels": ["continue", "coordonnées"],
+                "title": "Numérotation :",
+            },
+        ),
+    ]
+    if no_groups:
+        submitbuttonattributes = []
+        descr += [
+            (
+                "group_ids",
+                {
+                    "default": [g["group_id"] for g in groups],
+                    "input_type": "hidden",
+                    "type": "list",
+                },
+            )
+        ]
+    else:
+        descr += [
+            (
+                "group_ids",
+                {
+                    "input_type": "checkbox",
+                    "title": "Choix groupe(s) d'étudiants :",
+                    "allowed_values": grnams,
+                    "labels": grlabs,
+                    "attributes": ['onchange="gr_change(this);"'],
+                },
+            )
+        ]
+
+        if not (REQUEST.form.has_key("group_ids") and REQUEST.form["group_ids"]):
+            submitbuttonattributes = ['disabled="1"']
+        else:
+            submitbuttonattributes = []  # groupe(s) preselectionnés
+        H.append(
+            # JS pour desactiver le bouton OK si aucun groupe selectionné
+            """<script type="text/javascript">
+          function gr_change(e) {
+          var boxes = document.getElementsByName("group_ids:list");
+          var nbchecked = 0;
+          for (var i=0; i < boxes.length; i++) {
+              if (boxes[i].checked)
+                 nbchecked++;
+          }
+          if (nbchecked > 0) {
+              document.getElementsByName('gr_submit')[0].disabled=false;
+          } else {
+              document.getElementsByName('gr_submit')[0].disabled=true;
+          }
+          }
+          </script>
+          """
+        )
+
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        cancelbutton="Annuler",
+        submitbuttonattributes=submitbuttonattributes,
+        submitlabel="OK",
+        formid="gr",
+    )
+    if tf[0] == 0:
+        # H.append( """<div class="saisienote_etape1">
+        # <span class="titredivplacementetudiants">Choix du groupe et de la localisation</span>
+        # """)
+        H.append("""<div class="saisienote_etape1">""")
+        return "\n".join(H) + "\n" + tf[1] + "\n</div>"
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(
+            "%s/Notes/moduleimpl_status?moduleimpl_id=%s"
+            % (context.ScoURL(), E["moduleimpl_id"])
+        )
+    else:
+        placement_method = tf[2]["placement_method"]
+        teachers = tf[2]["teachers"]
+        building = tf[2]["building"]
+        room = tf[2]["room"]
+        group_ids = tf[2]["group_ids"]
+        columns = tf[2]["columns"]
+        numbering = tf[2]["numbering"]
+        if columns in ("3", "4", "5", "6", "7", "8"):
+            gs = [("group_ids%3Alist=" + urllib.quote_plus(x)) for x in group_ids]
+            query = "evaluation_id=%s&amp;placement_method=%s&amp;teachers=%s&amp;building=%s&amp;room=%s&amp;columns=%s&amp;numbering=%s&amp;" % (
+                evaluation_id,
+                placement_method,
+                teachers,
+                building,
+                room,
+                columns,
+                numbering,
+            ) + "&amp;".join(
+                gs
+            )
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1 + "/do_placement?" + query)
+        else:
+            raise ValueError(
+                "invalid placement_method (%s)" % tf[2]["placement_method"]
+            )
+
+
+def do_placement(context, REQUEST):
+    """
+    Choisi le placement
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    authusername = str(authuser)
+    try:
+        evaluation_id = REQUEST.form["evaluation_id"]
+    except:
+        raise ScoValueError(
+            "Formulaire incomplet ! Vous avez sans doute attendu trop longtemps, veuillez vous reconnecter. Si le problème persiste, contacter l'administrateur. Merci."
+        )
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+    jour_iso = DateDMYtoISO(E["jour"])
+
+    # Check access
+    # (admin, respformation, and responsable_id)
+    if not context.can_edit_notes(authuser, E["moduleimpl_id"]):
+        return (
+            "<h2>Génération du placement impossible pour %s</h2>" % authusername
+            + """<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>
+               """
+            % E["moduleimpl_id"]
+        )
+    cnx = context.GetDBConnexion()
+    # Infos transmises
+    placement_method = REQUEST.form["placement_method"]
+    teachers = REQUEST.form["teachers"]
+    building = REQUEST.form["building"]
+    room = REQUEST.form["room"]
+    columns = REQUEST.form["columns"]
+    numbering = REQUEST.form["numbering"]
+
+    # Construit liste des etudiants
+    group_ids = REQUEST.form.get("group_ids", [])
+    groups = sco_groups.listgroups(context, group_ids)
+    gr_title_filename = sco_groups.listgroups_filename(groups)
+    gr_title = sco_groups.listgroups_abbrev(groups)
+
+    if None in [g["group_name"] for g in groups]:  # tous les etudiants
+        getallstudents = True
+        gr_title = "tous"
+        gr_title_filename = "tous"
+    else:
+        getallstudents = False
+    etudids = sco_groups.do_evaluation_listeetuds_groups(
+        context, evaluation_id, groups, getallstudents=getallstudents, include_dems=True
+    )
+    if not etudids:
+        return "<p>Aucun groupe sélectionné !</p>"
+
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"])
+    evalname = "%s-%s" % (Mod["code"], DateDMYtoISO(E["jour"]))
+    if E["description"]:
+        evaltitre = E["description"]
+    else:
+        evaltitre = "évaluation du %s" % E["jour"]
+
+    desceval = []  # une liste de liste de chaines: description de l'evaluation
+    desceval.append(["%s" % sem["titreannee"]])
+    desceval.append(["Module : %s - %s" % (Mod["code"], Mod["abbrev"])])
+    desceval.append(["Surveillants : %s" % teachers])
+    desceval.append(["Batiment : %s - Salle : %s" % (building, room)])
+    desceval.append(["Controle : %s (coef. %g)" % (evaltitre, E["coefficient"])])
+
+    listetud = []  # liste de couples (nom,prenom)
+    for etudid in etudids:
+        # infos identite etudiant (xxx sous-optimal: 1/select par etudiant)
+        ident = scolars.etudident_list(cnx, {"etudid": etudid})[
+            0
+        ]  # XXX utiliser ZScolar (parent)
+        # infos inscription
+        inscr = context.do_formsemestre_inscription_list(
+            {"etudid": etudid, "formsemestre_id": M["formsemestre_id"]}
+        )[0]
+        if inscr["etat"] != "D":
+            nom = strupper(ident["nom"])
+            prenom = strcapitalize(strlower(ident["prenom"]))
+            listetud.append((nom, prenom))
+    random.shuffle(listetud)
+
+    sem_preferences = context.get_preferences()
+    space = sem_preferences.get("feuille_placement_emargement")
+    maxlines = sem_preferences.get("feuille_placement_positions")
+
+    if placement_method == "xls":
+        filename = "placement_%s_%s.xls" % (evalname, gr_title_filename)
+        xls = Excel_feuille_placement(
+            E, desceval, listetud, columns, space, maxlines, building, room, numbering
+        )
+        return sco_excel.sendExcelFile(REQUEST, xls, filename)
+    else:
+        nbcolumns = int(columns)
+
+        pdf_title = "%s<br/>" % sem["titreannee"]
+        pdf_title += "Module : %s - %s<br/>" % (Mod["code"], Mod["abbrev"])
+        pdf_title += "Surveillants : %s<br/>" % teachers
+        pdf_title += "Batiment : %s - Salle : %s<br/>" % (building, room)
+        pdf_title += "Controle : %s (coef. %g)<br/>" % (evaltitre, E["coefficient"])
+        pdf_title += "Date : %s - Horaire : %s à %s" % (
+            E["jour"],
+            E["heure_debut"],
+            E["heure_fin"],
+        )
+
+        filename = "placement_%s_%s.pdf" % (evalname, gr_title_filename)
+        titles = {
+            "nom": "Nom",
+            "prenom": "Prenom",
+            "colonne": "Colonne",
+            "ligne": "Ligne",
+            "place": "Place",
+        }
+        if numbering == "coordinate":
+            columns_ids = ["nom", "prenom", "colonne", "ligne"]
+        else:
+            columns_ids = ["nom", "prenom", "place"]
+
+        # etudiants
+        line = 1
+        col = 1
+        orderetud = []
+        for etudid in listetud:
+            if numbering == "coordinate":
+                orderetud.append((etudid[0], etudid[1], col, line))
+            else:
+                orderetud.append((etudid[0], etudid[1], col + (line - 1) * nbcolumns))
+
+            if col == nbcolumns:
+                col = 0
+                line += 1
+            col += 1
+
+        rows = []
+        orderetud.sort()
+        for etudid in orderetud:
+            if numbering == "coordinate":
+                rows.append(
+                    {
+                        "nom": etudid[0],
+                        "prenom": etudid[1],
+                        "colonne": etudid[2],
+                        "ligne": etudid[3],
+                    }
+                )
+            else:
+                rows.append({"nom": etudid[0], "prenom": etudid[1], "place": etudid[2]})
+
+        tab = GenTable(
+            titles=titles,
+            columns_ids=columns_ids,
+            rows=rows,
+            filename=filename,
+            origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+            pdf_title=pdf_title,
+            # pdf_shorttitle = '',
+            preferences=context.get_preferences(M["formsemestre_id"]),
+            # html_generate_cells=False # la derniere ligne (moyennes) est incomplete
+        )
+        t = tab.make_page(
+            context, format="pdf", with_html_headers=False, REQUEST=REQUEST
+        )
+        return t
+
+
+def placement_eval_selectetuds(context, evaluation_id, REQUEST=None):
+    """Dialogue placement etudiants: choix methode et localisation
+    """
+    evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
+    if not evals:
+        raise ScoValueError("invalid evaluation_id")
+    theeval = evals[0]
+
+    if theeval["description"]:
+        page_title = 'Placement "%s"' % theeval["description"]
+    else:
+        page_title = "Placement des étudiants"
+    H = [context.sco_header(REQUEST, page_title=page_title)]
+
+    formid = "placementfile"
+    if not REQUEST.form.get("%s-submitted" % formid, False):
+        # not submitted, choix groupe
+        r = do_placement_selectetuds(context, REQUEST)
+        if r:
+            H.append(r)
+
+    H.append(
+        """<h3>Explications</h3>
+<ul>
+<li>Choisir le format du fichier résultat :</li>
+<ul>
+<li>le format pdf consiste en un tableau précisant pour chaque étudiant la localisation de sa table;</li>
+<li>le format xls produit un classeur avec deux onglets</li>
+<ul>
+<li>le premier onglet donne une vue de la salle avec la localisation des étudiants et peut servir de feuille d'émargement;</li>
+<li>le second onglet est un tableau similaire à celui du fichier pdf;</li>
+</ul>
+</ul> 
+<li>préciser les surveillants et la localisation (bâtiment et salle) et indiquer le nombre de colonnes;</li> 
+<li>deux types de placements sont possibles :</li>
+<ul>
+<li>continue suppose que les tables ont toutes un numéro unique;</li>
+<li>coordonnées localise chaque table via un numéro de colonne et un numéro de ligne (ou rangée).</li>
+</ul>
+</ul>
+"""
+    )
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def Excel_feuille_placement(
+    E, description, listetud, columns, space, maxlines, building, room, numbering
+):
+    """Genere feuille excel pour placement des etudiants.
+    E: evaluation (dict)
+    lines: liste de tuples
+               (etudid, nom, prenom, etat, groupe, val, explanation)
+    """
+    nbcolumns = int(columns)
+
+    wb = Workbook()
+
+    SheetName0 = "Emargement"
+    ws0 = wb.add_sheet(SheetName0.decode(SCO_ENCODING))
+    # ajuste largeurs colonnes (unite inconnue, empirique)
+    width = 4500
+    if nbcolumns > 5:
+        width = 22500 / nbcolumns
+
+    for col in range(nbcolumns):
+        ws0.col(col + 1).width = width
+        ws0.col(0).width = 750
+
+    SheetName1 = "Positions"
+    ws1 = wb.add_sheet(SheetName1.decode(SCO_ENCODING))
+    if numbering == "coordinate":
+        ws1.col(0).width = 4000
+        ws1.col(1).width = 4500
+        ws1.col(2).width = 1500
+        ws1.col(3).width = 1500
+
+        ws1.col(4).width = 500
+
+        ws1.col(5).width = 4000
+        ws1.col(6).width = 4500
+        ws1.col(7).width = 1500
+        ws1.col(8).width = 1500
+    else:
+        ws1.col(0).width = 4000
+        ws1.col(1).width = 4500
+        ws1.col(2).width = 3000
+
+        ws1.col(3).width = 500
+
+        ws1.col(4).width = 4000
+        ws1.col(5).width = 4500
+        ws1.col(6).width = 3000
+
+    # styles
+    font0 = Font()
+    font0.name = "Arial"
+    font0.bold = True
+    font0.height = 12 * 0x14
+
+    font1b = Font()
+    font1b.name = "Arial"
+    font1b.bold = True
+    font1b.height = 9 * 0x14
+
+    font1i = Font()
+    font1i.name = "Arial"
+    font1i.height = 10 * 0x14
+    font1i.italic = True
+
+    font1o = Font()
+    font1o.name = "Arial"
+    font1o.height = 10 * 0x14
+    font1o.outline = True
+
+    font2bi = Font()
+    font2bi.name = "Arial"
+    font2bi.height = 8 * 0x14
+    font2bi.bold = True
+    font2bi.italic = True
+
+    font2 = Font()
+    font2.name = "Arial"
+    font2.height = 10 * 0x14
+
+    style_titres = XFStyle()
+    style_titres.font = font0
+
+    style1t = XFStyle()
+    style1t.font = font1b
+    alignment = Alignment()
+    alignment.horz = Alignment.HORZ_CENTER
+    alignment.vert = Alignment.VERT_CENTER
+    style1t.alignment = alignment
+    borders = Borders()
+    borders.left = Borders.DOUBLE
+    borders.top = Borders.DOUBLE
+    borders.bottom = Borders.NO_LINE
+    borders.right = Borders.DOUBLE
+    style1t.borders = borders
+
+    style1m = XFStyle()
+    style1m.font = font1b
+    alignment = Alignment()
+    alignment.horz = Alignment.HORZ_CENTER
+    alignment.vert = Alignment.VERT_CENTER
+    style1m.alignment = alignment
+    borders = Borders()
+    borders.left = Borders.DOUBLE
+    borders.top = Borders.NO_LINE
+    borders.bottom = Borders.THIN
+    borders.right = Borders.DOUBLE
+    style1m.borders = borders
+
+    style1bm = XFStyle()
+    borders = Borders()
+    borders.left = Borders.DOUBLE
+    borders.top = Borders.NO_LINE
+    borders.bottom = Borders.NO_LINE
+    borders.right = Borders.DOUBLE
+    style1bm.borders = borders
+
+    style1bb = XFStyle()
+    style1bb.font = font1o
+    alignment = Alignment()
+    alignment.horz = Alignment.HORZ_RIGHT
+    alignment.vert = Alignment.VERT_BOTTOM
+    style1bb.alignment = alignment
+    borders = Borders()
+    borders.left = Borders.DOUBLE
+    borders.top = Borders.NO_LINE
+    borders.bottom = Borders.DOUBLE
+    borders.right = Borders.DOUBLE
+    style1bb.borders = borders
+
+    style2b = XFStyle()
+    style2b.font = font1i
+    alignment = Alignment()
+    alignment.horz = Alignment.HORZ_CENTER
+    alignment.vert = Alignment.VERT_CENTER
+    style2b.alignment = alignment
+    borders = Borders()
+    borders.left = Borders.THIN
+    borders.top = Borders.THIN
+    borders.bottom = Borders.THIN
+    borders.right = Borders.THIN
+    style2b.borders = borders
+
+    style2bi = XFStyle()
+    style2bi.font = font2bi
+    alignment = Alignment()
+    alignment.horz = Alignment.HORZ_CENTER
+    alignment.vert = Alignment.VERT_CENTER
+    style2bi.alignment = alignment
+    borders = Borders()
+    borders.left = Borders.THIN
+    borders.top = Borders.THIN
+    borders.bottom = Borders.THIN
+    borders.right = Borders.THIN
+    style2bi.borders = borders
+    pattern = Pattern()
+    pattern.pattern = Pattern.SOLID_PATTERN
+    pattern._pattern_back_colour = "gray"
+    style2bi.pattern = pattern
+
+    style2l = XFStyle()
+    style2l.font = font2
+    alignment = Alignment()
+    alignment.horz = Alignment.HORZ_LEFT
+    alignment.vert = Alignment.VERT_CENTER
+    style2l.alignment = alignment
+    borders = Borders()
+    borders.left = Borders.THIN
+    borders.top = Borders.THIN
+    borders.bottom = Borders.THIN
+    borders.right = Borders.NO_LINE
+    style2l.borders = borders
+
+    style2m1 = XFStyle()
+    style2m1.font = font2
+    alignment = Alignment()
+    alignment.horz = Alignment.HORZ_LEFT
+    alignment.vert = Alignment.VERT_CENTER
+    style2m1.alignment = alignment
+    borders = Borders()
+    borders.left = Borders.NO_LINE
+    borders.top = Borders.THIN
+    borders.bottom = Borders.THIN
+    borders.right = Borders.NO_LINE
+    style2m1.borders = borders
+
+    style2m2 = XFStyle()
+    style2l.font = font2
+    alignment = Alignment()
+    alignment.horz = Alignment.HORZ_RIGHT
+    alignment.vert = Alignment.VERT_CENTER
+    style2m2.alignment = alignment
+    borders = Borders()
+    borders.left = Borders.NO_LINE
+    borders.top = Borders.THIN
+    borders.bottom = Borders.THIN
+    borders.right = Borders.NO_LINE
+    style2m2.borders = borders
+
+    style2r = XFStyle()
+    style2l.font = font2
+    alignment = Alignment()
+    alignment.horz = Alignment.HORZ_RIGHT
+    alignment.vert = Alignment.VERT_CENTER
+    style2r.alignment = alignment
+    borders = Borders()
+    borders.left = Borders.NO_LINE
+    borders.top = Borders.THIN
+    borders.bottom = Borders.THIN
+    borders.right = Borders.THIN
+    style2r.borders = borders
+
+    # ligne de titres
+    li = 0
+    line = 0
+    dt = time.strftime("%d/%m/%Y a %Hh%M")
+    ws0.write(li, 0, u"Feuille placement etudiants éditée le %s" % dt, style_titres)
+    ws1.write(li, 0, u"Feuille placement etudiants éditée le %s" % dt, style_titres)
+    for desceval in description:
+        if line % 2 == 0:
+            li += 2
+        else:
+            li += 1
+        line += 1
+        ws0.write(li, 0, desceval[0].decode(SCO_ENCODING), style_titres)
+        ws1.write(li, 0, desceval[0].decode(SCO_ENCODING), style_titres)
+    li += 1
+    ws0.write(
+        li,
+        0,
+        u"Date : %s - Horaire : %s à %s"
+        % (E["jour"], E["heure_debut"], E["heure_fin"]),
+        style_titres,
+    )
+    ws1.write(
+        li,
+        0,
+        u"Date : %s - Horaire : %s à %s"
+        % (E["jour"], E["heure_debut"], E["heure_fin"]),
+        style_titres,
+    )
+    li += 1
+
+    # entetes colonnes - feuille0
+    for col in range(nbcolumns):
+        ws0.write(li, col + 1, u"colonne %s" % (col + 1), style2b)
+    # entetes colonnes - feuille1
+    if numbering == "coordinate":
+        ws1.write(li, 0, u"Nom", style2bi)
+        ws1.write(li, 1, u"Prénom", style2bi)
+        ws1.write(li, 2, u"Colonne", style2bi)
+        ws1.write(li, 3, u"Ligne", style2bi)
+
+        ws1.write(li, 5, u"Nom", style2bi)
+        ws1.write(li, 6, u"Prénom", style2bi)
+        ws1.write(li, 7, u"Colonne", style2bi)
+        ws1.write(li, 8, u"Ligne", style2bi)
+    else:
+        ws1.write(li, 0, u"Nom", style2bi)
+        ws1.write(li, 1, u"Prénom", style2bi)
+        ws1.write(li, 2, u"Place", style2bi)
+
+        ws1.write(li, 4, u"Nom", style2bi)
+        ws1.write(li, 5, u"Prénom", style2bi)
+        ws1.write(li, 6, u"Place", style2bi)
+
+    # etudiants
+    line = 1
+    col = 1
+    linetud = []
+    orderetud = []
+    placementetud = []
+    for etudid in listetud:
+        linetud.append(etudid)
+        if numbering == "coordinate":
+            orderetud.append((etudid[0], etudid[1], col, line))
+        else:
+            orderetud.append((etudid[0], etudid[1], col + (line - 1) * nbcolumns))
+
+        if col == nbcolumns:
+            placementetud.append(linetud)
+            linetud = []
+            col = 0
+            line += 1
+        col += 1
+    if len(linetud) > 0:
+        placementetud.append(linetud)
+
+    # etudiants - feuille0
+    line = 0
+    li0 = li
+    for linetud in placementetud:
+        li0 += 1
+        line += 1
+        ws0.write(li0, 0, line, style2b)
+        col = 1
+        for etudid in linetud:
+            ws0.write(li0, col, (etudid[0]).decode(SCO_ENCODING), style1t)
+            ws0.write(li0 + 1, col, (etudid[1]).decode(SCO_ENCODING), style1m)
+            ws0.row(li0 + 2).height = space
+            if numbering == "coordinate":
+                ws0.write(li0 + 2, col, " ", style1bb)
+            else:
+                ws0.write(
+                    li0 + 2, col, u"place %s" % (col + (line - 1) * nbcolumns), style1bb
+                )
+            # ws0.write(li+3,col, ' ', style1bm )
+            # ws0.write(li+4,col, ' ', style1bb )
+
+            if col == nbcolumns:
+                col = 0
+                li0 += 2
+            col += 1
+
+    # etudiants - feuille1
+    if numbering == "coordinate":
+        coloffset = 5
+    else:
+        coloffset = 4
+    line = 0
+    li1 = li
+    nbcol = 0
+    col = 0
+    orderetud.sort()
+    for etudid in orderetud:
+        li1 += 1
+        line += 1
+        ws1.write(li1, col, (etudid[0]).decode(SCO_ENCODING), style2l)
+        ws1.write(li1, col + 1, (etudid[1]).decode(SCO_ENCODING), style2m1)
+        if numbering == "coordinate":
+            ws1.write(li1, col + 2, etudid[2], style2m2)
+            ws1.write(li1, col + 3, etudid[3], style2r)
+        else:
+            ws1.write(li1, col + 2, etudid[2], style2r)
+
+        if line == maxlines:
+            line = 0
+            li1 = li
+            nbcol = nbcol + 1
+            col = col + coloffset
+            if nbcol == 2:
+                li = li + maxlines + 2
+                li1 = li
+                nbcol = 0
+                col = 0
+                if numbering == "coordinate":
+                    ws1.write(li, 0, u"Nom", style2bi)
+                    ws1.write(li, 1, u"Prénom", style2bi)
+                    ws1.write(li, 2, u"Colonne", style2bi)
+                    ws1.write(li, 3, u"Ligne", style2bi)
+
+                    ws1.write(li, 5, u"Nom", style2bi)
+                    ws1.write(li, 6, u"Prénom", style2bi)
+                    ws1.write(li, 7, u"Colonne", style2bi)
+                    ws1.write(li, 8, u"Ligne", style2bi)
+                else:
+                    ws1.write(li, 0, u"Nom", style2bi)
+                    ws1.write(li, 1, u"Prénom", style2bi)
+                    ws1.write(li, 2, u"Place", style2bi)
+
+                    ws1.write(li, 4, u"Nom", style2bi)
+                    ws1.write(li, 5, u"Prénom", style2bi)
+                    ws1.write(li, 6, u"Place", style2bi)
+    return wb.savetostr()
diff --git a/sco_portal_apogee.py b/sco_portal_apogee.py
new file mode 100644
index 0000000000000000000000000000000000000000..2847b0f5d678a4cd576b6c301b12abf40882a90a
--- /dev/null
+++ b/sco_portal_apogee.py
@@ -0,0 +1,552 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Liaison avec le portail ENT (qui donne accès aux infos Apogée)
+"""
+
+from sco_utils import *
+
+SCO_CACHE_ETAPE_FILENAME = os.path.join(SCO_TMPDIR, "last_etapes.xml")
+
+
+def has_portal(context):
+    "True if we are connected to a portal"
+    return get_portal_url(context)
+
+
+class PortalInterface:
+    def __init__(self):
+        self.warning = False
+
+    def get_portal_url(self, context):
+        "URL of portal"
+        portal_url = context.get_preference("portal_url")
+        if not self.warning:
+            if portal_url:
+                log("Portal URL=%s" % portal_url)
+            else:
+                log("Portal not configured")
+            self.warning = True
+        return portal_url
+
+    def get_etapes_url(self, context):
+        "Full URL of service giving list of etapes (in XML)"
+        etapes_url = context.get_preference("etapes_url")
+        if not etapes_url:
+            # Default:
+            portal_url = self.get_portal_url(context)
+            if not portal_url:
+                return None
+            api_ver = self.get_portal_api_version(context)
+            if api_ver > 1:
+                etapes_url = portal_url + "scodocEtapes.php"
+            else:
+                etapes_url = portal_url + "getEtapes.php"
+        return etapes_url
+
+    def get_etud_url(self, context):
+        "Full URL of service giving list of students (in XML)"
+        etud_url = context.get_preference("etud_url")
+        if not etud_url:
+            # Default:
+            portal_url = self.get_portal_url(context)
+            if not portal_url:
+                return None
+            api_ver = self.get_portal_api_version(context)
+            if api_ver > 1:
+                etud_url = portal_url + "scodocEtudiant.php"
+            else:
+                etud_url = portal_url + "getEtud.php"
+        return etud_url
+
+    def get_photo_url(self, context):
+        "Full URL of service giving photo of student"
+        photo_url = context.get_preference("photo_url")
+        if not photo_url:
+            # Default:
+            portal_url = self.get_portal_url(context)
+            if not portal_url:
+                return None
+            api_ver = self.get_portal_api_version(context)
+            if api_ver > 1:
+                photo_url = portal_url + "scodocPhoto.php"
+            else:
+                photo_url = portal_url + "getPhoto.php"
+        return photo_url
+
+    def get_maquette_url(self, context):
+        """Full URL of service giving Apogee maquette pour une étape (fichier "CSV")
+        """
+        maquette_url = context.get_preference("maquette_url")
+        if not maquette_url:
+            # Default:
+            portal_url = self.get_portal_url(context)
+            if not portal_url:
+                return None
+            maquette_url = portal_url + "scodocMaquette.php"
+        return maquette_url
+
+    def get_portal_api_version(self, context):
+        "API version of the portal software"
+        api_ver = context.get_preference("portal_api")
+        if not api_ver:
+            # Default:
+            api_ver = 1
+        return api_ver
+
+
+_PI = PortalInterface()
+get_portal_url = _PI.get_portal_url
+get_etapes_url = _PI.get_etapes_url
+get_etud_url = _PI.get_etud_url
+get_photo_url = _PI.get_photo_url
+get_maquette_url = _PI.get_maquette_url
+get_portal_api_version = _PI.get_portal_api_version
+
+
+def get_inscrits_etape(context, code_etape, anneeapogee=None):
+    """Liste des inscrits à une étape Apogée
+    Result = list of dicts
+    """
+    log("get_inscrits_etape: code=%s anneeapogee=%s" % (code_etape, anneeapogee))
+    if anneeapogee is None:
+        anneeapogee = str(time.localtime()[0])
+
+    etud_url = get_etud_url(context)
+    api_ver = get_portal_api_version(context)
+    if not etud_url:
+        return []
+    portal_timeout = context.get_preference("portal_timeout")
+    if api_ver > 1:
+        req = (
+            etud_url
+            + "?"
+            + urllib.urlencode((("etape", code_etape), ("annee", anneeapogee)))
+        )
+    else:
+        req = etud_url + "?" + urllib.urlencode((("etape", code_etape),))
+    doc = query_portal(req, timeout=portal_timeout)
+    if not doc:
+        raise ScoValueError("pas de réponse du portail ! (timeout=%s)" % portal_timeout)
+    etuds = _normalize_apo_fields(xml_to_list_of_dicts(doc, req=req))
+
+    # Filtre sur annee inscription Apogee:
+    def check_inscription(e):
+        if e.has_key("inscription"):
+            if e["inscription"] == anneeapogee:
+                return True
+            else:
+                return False
+        else:
+            log(
+                "get_inscrits_etape: pas inscription dans code_etape=%s e=%s"
+                % (code_etape, e)
+            )
+            return False  # ??? pas d'annee d'inscription dans la réponse
+
+    # Non disponible avec l'API v2. Apparemment non utilisée de toute
+    # façon, rien dans le code ne mettait anneeapogee à *, mais
+    # seulement à l'année par de début de semestre. Donc on laisse le test.
+    if anneeapogee != "*":
+        etuds = [e for e in etuds if check_inscription(e)]
+    return etuds
+
+
+def query_apogee_portal(context, **args):
+    """Recupere les infos sur les etudiants nommés
+    args: nom, prenom, code_nip
+    (nom et prenom matchent des parties de noms)
+    """
+    etud_url = get_etud_url(context)
+    api_ver = get_portal_api_version(context)
+    if not etud_url:
+        return []
+    if api_ver > 1:
+        if args["nom"] or args["prenom"]:
+            # Ne fonctionne pas avec l'API 2 sur nom et prenom
+            # XXX TODO : va poser problème pour la page modif données étudiants : A VOIR
+            return []
+    portal_timeout = context.get_preference("portal_timeout")
+    req = etud_url + "?" + urllib.urlencode(args.items())
+    doc = query_portal(req, timeout=portal_timeout)  # sco_utils
+    return xml_to_list_of_dicts(doc, req=req)
+
+
+def xml_to_list_of_dicts(doc, req=None):
+    """Convert an XML 1.0 str to a list of dicts.
+    """
+    if not doc:
+        return []
+    # Fix for buggy XML returned by some APIs (eg USPN)
+    invalid_entities = {
+        '&CCEDIL;' : 'Ç',
+        '& ' : '&amp; ', # only when followed by a space (avoid affecting entities)
+        # to be completed...
+    }
+    for k in invalid_entities:
+        doc = doc.replace(k,invalid_entities[k])
+    #
+    try:
+        dom = xml.dom.minidom.parseString(doc)
+    except xml.parsers.expat.ExpatError as e:
+        # Find faulty part
+        err_zone = doc.splitlines()[e.lineno-1][e.offset:e.offset+20]
+        # catch bug: log and re-raise exception
+        log(
+            "xml_to_list_of_dicts: exception in XML parseString\ndoc:\n%s\n(end xml doc)\n"
+            % doc
+        )
+        raise ScoValueError("erreur dans la réponse reçue du portail ! (peut être : \"%s\")" % err_zone)
+    infos = []
+    try:
+        if dom.childNodes[0].nodeName != u"etudiants":
+            raise ValueError
+        etudiants = dom.getElementsByTagName("etudiant")
+        for etudiant in etudiants:
+            d = {}
+            # recupere toutes les valeurs <valeur>XXX</valeur>
+            for e in etudiant.childNodes:
+                if e.nodeType == e.ELEMENT_NODE:
+                    childs = e.childNodes
+                    if len(childs):
+                        d[str(e.nodeName)] = childs[0].nodeValue.encode(SCO_ENCODING)
+            infos.append(d)
+    except:
+        log("*** invalid XML response from Etudiant Web Service")
+        log("req=%s" % req)
+        log("doc=%s" % doc)
+        raise ValueError("invalid XML response from Etudiant Web Service\n%s" % doc)
+    return infos
+
+
+def get_infos_apogee_allaccents(context, nom, prenom):
+    "essai recup infos avec differents codages des accents"
+    if nom:
+        unom = unicode(nom, SCO_ENCODING)
+        nom_noaccents = str(suppression_diacritics(unom))
+        nom_utf8 = unom.encode("utf-8")
+    else:
+        nom_noaccents = nom
+        nom_utf8 = nom
+
+    if prenom:
+        uprenom = unicode(prenom, SCO_ENCODING)
+        prenom_noaccents = str(suppression_diacritics(uprenom))
+        prenom_utf8 = uprenom.encode("utf-8")
+    else:
+        prenom_noaccents = prenom
+        prenom_utf8 = prenom
+
+    # avec accents
+    infos = query_apogee_portal(context, nom=nom, prenom=prenom)
+    # sans accents
+    if nom != nom_noaccents or prenom != prenom_noaccents:
+        infos += query_apogee_portal(
+            context, nom=nom_noaccents, prenom=prenom_noaccents
+        )
+    # avec accents en UTF-8
+    if nom_utf8 != nom_noaccents or prenom_utf8 != prenom_noaccents:
+        infos += query_apogee_portal(context, nom=nom_utf8, prenom=prenom_utf8)
+    return infos
+
+
+def get_infos_apogee(context, nom, prenom):
+    """recupere les codes Apogee en utilisant le web service CRIT
+    """
+    if (not nom) and (not prenom):
+        return []
+    # essaie plusieurs codages: tirets, accents
+    infos = get_infos_apogee_allaccents(context, nom, prenom)
+    nom_st = nom.replace("-", " ")
+    prenom_st = prenom.replace("-", " ")
+    if nom_st != nom or prenom_st != prenom:
+        infos += get_infos_apogee_allaccents(context, nom_st, prenom_st)
+    # si pas de match et nom ou prenom composé, essaie en coupant
+    if not infos:
+        if nom:
+            nom1 = nom.split()[0]
+        else:
+            nom1 = nom
+        if prenom:
+            prenom1 = prenom.split()[0]
+        else:
+            prenom1 = prenom
+        if nom != nom1 or prenom != prenom1:
+            infos += get_infos_apogee_allaccents(context, nom1, prenom1)
+    return infos
+
+
+def get_etud_apogee(context, code_nip):
+    """Informations à partir du code NIP.
+    None si pas d'infos sur cet etudiant.
+    Exception si reponse invalide.
+    """
+    if not code_nip:
+        return {}
+    etud_url = get_etud_url(context)
+    if not etud_url:
+        return {}
+    portal_timeout = context.get_preference("portal_timeout")
+    req = etud_url + "?" + urllib.urlencode((("nip", code_nip),))
+    doc = query_portal(req, timeout=portal_timeout)
+    d = _normalize_apo_fields(xml_to_list_of_dicts(doc, req=req))
+    if not d:
+        return None
+    if len(d) > 1:
+        raise ValueError("invalid XML response from Etudiant Web Service\n%s" % doc)
+    return d[0]
+
+
+def get_default_etapes(context):
+    """Liste par défaut: devrait etre lue d'un fichier de config
+    """
+    filename = context.file_path + "/config/default-etapes.txt"
+    log("get_default_etapes: reading %s" % filename)
+    f = open(filename)
+    etapes = {}
+    for line in f.readlines():
+        line = line.strip()
+        if line and line[0] != "#":
+            dept, code, intitule = [x.strip() for x in line.split(":")]
+            if dept and code:
+                if etapes.has_key(dept):
+                    etapes[dept][code] = intitule
+                else:
+                    etapes[dept] = {code: intitule}
+    return etapes
+
+
+def _parse_etapes_from_xml(context, doc):
+    """
+    may raise exception if invalid xml doc
+    """
+    xml_etapes_by_dept = context.get_preference("xml_etapes_by_dept")
+    # parser XML
+    dom = xml.dom.minidom.parseString(doc)
+    infos = {}
+    if dom.childNodes[0].nodeName != u"etapes":
+        raise ValueError
+    if xml_etapes_by_dept:
+        # Ancien format XML avec des sections par departement:
+        for d in dom.childNodes[0].childNodes:
+            if d.nodeType == d.ELEMENT_NODE:
+                dept = d.nodeName.encode(SCO_ENCODING)
+                _xml_list_codes(infos, dept, d.childNodes)
+    else:
+        # Toutes les étapes:
+        dept = ""
+        _xml_list_codes(infos, "", dom.childNodes[0].childNodes)
+    return infos
+
+
+def get_etapes_apogee(context):
+    """Liste des etapes apogee
+    { departement : { code_etape : intitule } }
+    Demande la liste au portail, ou si échec utilise liste
+    par défaut
+    """
+    etapes_url = get_etapes_url(context)
+    if not etapes_url:
+        log("get_etapes_apogee: no configured URL")
+        return {}
+    portal_timeout = context.get_preference("portal_timeout")
+    log(
+        "get_etapes_apogee: requesting '%s' with timeout=%s"
+        % (etapes_url, portal_timeout)
+    )
+    doc = query_portal(etapes_url, timeout=portal_timeout)
+    try:
+        infos = _parse_etapes_from_xml(context, doc)
+        # cache le resultat (utile si le portail repond de façon intermitente)
+        if infos:
+            log("get_etapes_apogee: caching result")
+            open(SCO_CACHE_ETAPE_FILENAME, "w").write(doc)
+    except:
+        log("invalid XML response from getEtapes Web Service\n%s" % etapes_url)
+        # Avons nous la copie d'une réponse récente ?
+        try:
+            doc = open(SCO_CACHE_ETAPE_FILENAME).read()
+            infos = _parse_etapes_from_xml(context, doc)
+            log("using last saved version from " + SCO_CACHE_ETAPE_FILENAME)
+        except:
+            # derniere chance: utilise étapes par défaut livrées avec ScoDoc
+            return get_default_etapes(context)
+    return infos
+
+
+def _xml_list_codes(target_dict, dept, nodes):
+    for e in nodes:
+        if e.nodeType == e.ELEMENT_NODE:
+            intitule = e.childNodes[0].nodeValue.encode(SCO_ENCODING)
+            code = e.attributes["code"].value.encode(SCO_ENCODING)
+            if target_dict.has_key(dept):
+                target_dict[dept][code] = intitule
+            else:
+                target_dict[dept] = {code: intitule}
+
+
+def get_etapes_apogee_dept(context):
+    """Liste des etapes apogee pour ce departement.
+    Utilise la propriete 'portal_dept_name' pour identifier le departement.
+
+    Si xml_etapes_by_dept est faux (nouveau format XML depuis sept 2014),
+    le departement n'est pas utilisé: toutes les étapes sont présentées.
+    
+    Returns [ ( code, intitule) ], ordonnée
+    """
+    xml_etapes_by_dept = context.get_preference("xml_etapes_by_dept")
+    if xml_etapes_by_dept:
+        portal_dept_name = context.get_preference("portal_dept_name")
+        log('get_etapes_apogee_dept: portal_dept_name="%s"' % portal_dept_name)
+    else:
+        portal_dept_name = ""
+        log("get_etapes_apogee_dept: pas de sections par departement")
+
+    infos = get_etapes_apogee(context)
+    if portal_dept_name and not infos.has_key(portal_dept_name):
+        log(
+            "get_etapes_apogee_dept: pas de section '%s' dans la reponse portail"
+            % portal_dept_name
+        )
+        return []
+    if portal_dept_name:
+        etapes = infos[portal_dept_name].items()
+    else:
+        # prend toutes les etapes
+        etapes = []
+        for k in infos.keys():
+            etapes += infos[k].items()
+
+    etapes.sort()  # tri sur le code etape
+    return etapes
+
+
+def _portal_date_dmy2date(s):
+    """date inscription renvoyée sous la forme dd/mm/yy
+    renvoie un objet date, ou None
+    """
+    s = s.strip()
+    if not s:
+        return None
+    else:
+        d, m, y = [int(x) for x in s.split("/")]  # raises ValueError if bad format
+        if y < 100:
+            y += 2000  # 21ème siècle
+        return datetime.date(y, m, d)
+
+
+def _normalize_apo_fields(infolist):
+    """
+    infolist: liste de dict renvoyés par le portail Apogee
+
+    recode les champs: paiementinscription (-> booleen), datefinalisationinscription (date)
+    ajoute le champs 'paiementinscription_str' : 'ok', 'Non' ou '?' 
+    ajuoute le champs 'etape' (= None) s'il n'est pas présent
+    """
+    for infos in infolist:
+        if infos.has_key("paiementinscription"):
+            infos["paiementinscription"] = (
+                strlower(infos["paiementinscription"]) == "true"
+            )
+            if infos["paiementinscription"]:
+                infos["paiementinscription_str"] = "ok"
+            else:
+                infos["paiementinscription_str"] = "Non"
+        else:
+            infos["paiementinscription"] = None
+            infos["paiementinscription_str"] = "?"
+
+        if infos.has_key("datefinalisationinscription"):
+            infos["datefinalisationinscription"] = _portal_date_dmy2date(
+                infos["datefinalisationinscription"]
+            )
+            infos["datefinalisationinscription_str"] = infos[
+                "datefinalisationinscription"
+            ].strftime("%d/%m/%Y")
+        else:
+            infos["datefinalisationinscription"] = None
+            infos["datefinalisationinscription_str"] = ""
+
+        if not infos.has_key("etape"):
+            infos["etape"] = None
+
+    return infolist
+
+
+def check_paiement_etuds(context, etuds):
+    """Interroge le portail pour vérifier l'état de "paiement" et l'étape d'inscription.
+
+    Seuls les etudiants avec code NIP sont renseignés.
+    
+    Renseigne l'attribut booleen 'paiementinscription' dans chaque etud.
+
+    En sortie: modif les champs de chaque etud
+    'paiementinscription' : True, False ou None
+    'paiementinscription_str' : 'ok', 'Non' ou '?' ou '(pas de code)'
+    'etape' : etape Apogee ou None
+    """
+    # interrogation séquentielle longue...
+    for etud in etuds:
+        if not etud.has_key("code_nip"):
+            etud["paiementinscription"] = None
+            etud["paiementinscription_str"] = "(pas de code)"
+            etud["datefinalisationinscription"] = None
+            etud["datefinalisationinscription_str"] = "NA"
+            etud["etape"] = None
+        else:
+            # Modifie certains champs de l'étudiant:
+            infos = get_etud_apogee(context, etud["code_nip"])
+            if infos:
+                for k in (
+                    "paiementinscription",
+                    "paiementinscription_str",
+                    "datefinalisationinscription",
+                    "datefinalisationinscription_str",
+                    "etape",
+                ):
+                    etud[k] = infos[k]
+            else:
+                etud["datefinalisationinscription"] = None
+                etud["datefinalisationinscription_str"] = "Erreur"
+                etud["datefinalisationinscription"] = None
+                etud["paiementinscription_str"] = "(pb cnx Apogée)"
+
+
+def get_maquette_apogee(context, etape="", annee_scolaire=""):
+    """Maquette CSV Apogee pour une étape et une annee scolaire
+    """
+    maquette_url = get_maquette_url(context)
+    if not maquette_url:
+        return None
+    portal_timeout = context.get_preference("portal_timeout")
+    req = (
+        maquette_url
+        + "?"
+        + urllib.urlencode((("etape", etape), ("annee", annee_scolaire)))
+    )
+    doc = query_portal(req, timeout=portal_timeout)
+    return doc
diff --git a/sco_poursuite_dut.py b/sco_poursuite_dut.py
new file mode 100644
index 0000000000000000000000000000000000000000..edbec9490a9db3d68f14da5a3e15c3a06c5dff13
--- /dev/null
+++ b/sco_poursuite_dut.py
@@ -0,0 +1,210 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""Extraction de données pour poursuites d'études
+
+Recapitule tous les semestres validés dans une feuille excel.
+"""
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from gen_tables import GenTable
+import sco_formsemestre
+import sco_groups
+import ZAbsences
+from sco_codes_parcours import code_semestre_validant, code_semestre_attente
+
+
+def etud_get_poursuite_info(context, sem, etud):
+    """{ 'nom' : ..., 'semlist' : [ { 'semestre_id': , 'moy' : ... }, {}, ...] }
+    """
+    I = {}
+    I.update(etud)  # copie nom, prenom, sexe, ...
+
+    # Now add each semester, starting from the first one
+    semlist = []
+    current_id = sem["semestre_id"]
+    for sem_id in range(1, current_id + 1):
+        sem_descr = None
+        for s in etud["sems"]:
+            if s["semestre_id"] == sem_id:
+                etudid = etud["etudid"]
+                nt = context._getNotesCache().get_NotesTable(
+                    context, s["formsemestre_id"]
+                )
+                dec = nt.get_etud_decision_sem(etudid)
+                # Moyennes et rangs des UE
+                ues = nt.get_ues(filter_sport=True)
+                moy_ues = [
+                    (
+                        ue["acronyme"],
+                        fmt_note(nt.get_etud_ue_status(etudid, ue["ue_id"])["moy"]),
+                    )
+                    for ue in ues
+                ]
+                rg_ues = [
+                    ("rang_" + ue["acronyme"], nt.ue_rangs[ue["ue_id"]][0][etudid])
+                    for ue in ues
+                ]
+
+                # Moyennes et rang des modules
+                modimpls = nt.get_modimpls()  # recupération des modules
+                modules = []
+                rangs = []
+                for ue in ues:  # on parcourt chaque UE
+                    for modimpl in modimpls:  # dans chaque UE les modules
+                        if modimpl["module"]["ue_id"] == ue["ue_id"]:
+                            codeModule = modimpl["module"]["code"]
+                            noteModule = fmt_note(
+                                nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
+                            )
+                            if noteModule != "NI":  # si étudiant inscrit au module
+                                rangModule = nt.mod_rangs[modimpl["moduleimpl_id"]][0][
+                                    etudid
+                                ]
+                                modules.append([codeModule, noteModule])
+                                rangs.append(["rang_" + codeModule, rangModule])
+
+                # Absences
+                AbsSemEtud = ZAbsences.getAbsSemEtud(context, nt.sem, etudid)
+                NbAbs = AbsSemEtud.CountAbs()
+                NbAbsJust = AbsSemEtud.CountAbsJust()
+                if (
+                    dec
+                    and not sem_descr  # not sem_descr pour ne prendre que le semestre validé le plus récent
+                    and (
+                        code_semestre_validant(dec["code"])
+                        or code_semestre_attente(dec["code"])
+                    )
+                    and nt.get_etud_etat(etudid) == "I"
+                ):
+                    d = [
+                        ("moy", fmt_note(nt.get_etud_moy_gen(etudid))),
+                        ("moy_promo", fmt_note(nt.moy_moy)),
+                        ("rang", nt.get_etud_rang(etudid)),
+                        ("effectif", len(nt.T)),
+                        ("date_debut", s["date_debut"]),
+                        ("date_fin", s["date_fin"]),
+                        ("periode", "%s - %s" % (s["mois_debut"], s["mois_fin"])),
+                        ("AbsNonJust", NbAbs - NbAbsJust),
+                        ("AbsJust", NbAbsJust),
+                    ]
+                    d += (
+                        moy_ues + rg_ues + modules + rangs
+                    )  # ajout des 2 champs notes des modules et classement dans chaque module
+                    sem_descr = collections.OrderedDict(d)
+        if not sem_descr:
+            sem_descr = collections.OrderedDict(
+                [
+                    ("moy", ""),
+                    ("moy_promo", ""),
+                    ("rang", ""),
+                    ("effectif", ""),
+                    ("date_debut", ""),
+                    ("date_fin", ""),
+                    ("periode", ""),
+                ]
+            )
+        sem_descr["semestre_id"] = sem_id
+        semlist.append(sem_descr)
+
+    I["semlist"] = semlist
+    return I
+
+
+def _flatten_info(info):
+    # met la liste des infos semestres "a plat"
+    # S1_moy, S1_rang, ..., S2_moy, ...
+    ids = []
+    for s in info["semlist"]:
+        for k, v in s.items():
+            if k != "semestre_id":
+                label = "S%s_%s" % (s["semestre_id"], k)
+                info[label] = v
+                ids.append(label)
+    return ids
+
+
+def formsemestre_poursuite_report(
+    context, formsemestre_id, format="html", REQUEST=None
+):
+    """Table avec informations "poursuite"
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    nt = context._getNotesCache().get_NotesTable(context, formsemestre_id)
+    etuds = context.getEtudInfoGroupes(
+        [sco_groups.get_default_group(context, formsemestre_id)]
+    )
+
+    infos = []
+    ids = []
+    for etud in etuds:
+        etud["_nom_target"] = "ficheEtud?etudid=" + etud["etudid"]
+        etud["_prenom_target"] = "ficheEtud?etudid=" + etud["etudid"]
+        etud["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"])
+        info = etud_get_poursuite_info(context, sem, etud)
+        idd = _flatten_info(info)
+        # On recupere la totalite des UEs dans ids
+        for id in idd:
+            if id not in ids:
+                ids += [id]
+        infos.append(info)
+    #
+    column_ids = (
+        ("sexe", "nom", "prenom", "annee", "date_naissance")
+        + tuple(ids)
+        + ("debouche",)
+    )
+    titles = {}
+    for c in column_ids:
+        titles[c] = c
+    tab = GenTable(
+        titles=titles,
+        columns_ids=column_ids,
+        rows=infos,
+        # html_col_width='4em',
+        html_sortable=True,
+        html_class="table_leftalign table_listegroupe",
+        pdf_link=False,  # pas d'export pdf
+        preferences=context.get_preferences(formsemestre_id),
+    )
+    tab.filename = make_filename("poursuite " + sem["titreannee"])
+
+    tab.origin = "Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + ""
+    tab.caption = "Récapitulatif %s." % sem["titreannee"]
+    tab.html_caption = "Récapitulatif %s." % sem["titreannee"]
+    tab.base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id)
+    return tab.make_page(
+        context,
+        title="""<h2 class="formsemestre">Poursuite d'études</h2>""",
+        init_qtip=True,
+        javascripts=["js/etud_info.js"],
+        format=format,
+        REQUEST=REQUEST,
+        with_html_headers=True,
+    )
diff --git a/sco_preferences.py b/sco_preferences.py
new file mode 100644
index 0000000000000000000000000000000000000000..cff9a667d7ef7c1b0519508f9241bf9a24e15c8e
--- /dev/null
+++ b/sco_preferences.py
@@ -0,0 +1,2074 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""ScoDoc preferences (replaces old Zope properties)
+"""
+
+from sco_utils import *
+from notesdb import *
+from TrivialFormulator import TrivialFormulator, TF
+import sco_formsemestre
+import sco_bulletins_generator
+
+"""Global/Semestre Preferences for ScoDoc (version dec 2008)
+
+Preferences (paramètres) communs à tous les utilisateurs.
+Peuvent être définis globalement (pour tous les semestres)
+ou bien seulement pour un semestre précis.
+
+Chaque parametre est défini dans la base de données SQL par:
+ - name : nom du parametre
+ - value: valeur du parametre, ou NULL si on doit utiliser une valeur par défaut
+ - formsemestre_id: semestre associé, ou NULL si applicable à tous les semestres
+                    pour lesquels une valeur spécifique n'est pas définie.
+
+Au niveau du code interface, on défini pour chaque préférence:
+ - name (clé)
+ - title : titre en français
+ - initvalue : valeur initiale, chargée depuis config/scodoc_config.py
+ - explanation: explication en français
+ - size: longueur du chap texte
+ - input_type: textarea,separator,... type de widget TrivialFormulator a utiliser
+ - rows, rols: geometrie des textareas
+ - category: misc ou bul ou page_bulletins ou abs ou general ou portal 
+             ou pdf ou pvpdf ou ...
+ - only_global (default False): si vraie, ne peut pas etre associée a un seul semestre.
+
+Les titres et sous-titres de chaque catégorie sont définis dans PREFS_CATEGORIES
+
+On peut éditer les préférences d'une ou plusieurs catégories au niveau d'un 
+semestre ou au niveau global. 
+* niveau global: changer les valeurs, liste de catégories.
+   
+* niveau d'un semestre:
+   présenter valeur courante: valeur ou "definie globalement" ou par defaut
+    lien "changer valeur globale"
+   
+------------------------------------------------------------------------------
+Doc technique:
+
+* Base de données:
+Toutes les préférences sont stockées dans la table sco_prefs, qui contient
+des tuples (name, value, formsemestre_id).
+Si formsemestre_id est NULL, la valeur concerne tous les semestres,
+sinon, elle ne concerne que le semestre indiqué. 
+
+* Utilisation dans ScoDoc
+  - lire une valeur: 
+      context.get_preference(name, formsemestre_id)
+      nb: les valeurs sont des chaines, sauf:
+         . si le type est spécfié (float ou int)
+         . les boolcheckbox qui sont des entiers 0 ou 1
+  - avoir un mapping (read only) de toutes les valeurs:
+      context.get_preferences(formsemestre_id)
+  - editer les preferences globales:
+      sco_preferences.get_base_preferences(self).edit(REQUEST=REQUEST)
+  - editer les preferences d'un semestre:
+      sem_preferences(context,formsemestre_id).edit()
+
+* Valeurs par défaut:
+On a deux valeurs par défaut possibles: 
+ - via le fichier scodoc_config.py, qui peut être modifié localement.
+ - si rien dans scodoc_config.py, la valeur définie par
+   sco_preferences.py est utilisée (ne pas modifier ce fichier).
+
+
+* Implémentation: sco_preferences.py
+
+PREF_CATEGORIES : définition des catégories de préférences (pour
+dialogues édition)
+PREFS : pour chaque pref, donne infos pour édition (titre, type...) et
+valeur par défaut.
+
+class sco_base_preferences
+Une instance unique par site (département, repéré par URL).
+- charge les preferences pour tous le semestres depuis la BD.
+ .get(formsemestre_id, name)
+ .is_global(formsemestre_id, name)
+ .save(formsemestre_id=None, name=None)
+ .set(formsemestre_id, name, value)
+ .deleteformsemestre_id, name)
+ .edit() (HTML dialog)
+
+class sem_preferences(context,formsemestre_id)
+Une instance par semestre, et une instance pour prefs globales.
+L'attribut .base_prefs point sur sco_base_preferences.
+ .__getitem__   [name]
+ .is_global(name)
+ .edit(categories=[])
+
+
+get_base_preferences(context, formsemestre_id)
+ Return base preferences for this context (instance sco_base_preferences)
+
+"""
+
+PREF_CATEGORIES = (
+    # sur page "Paramètres"
+    ("general", {}),
+    ("misc", {"title": "Divers"}),
+    ("abs", {"title": "Suivi des absences", "related": ("bul",)}),
+    ("portal", {"title": "Liaison avec portail (Apogée, etc)"}),
+    (
+        "pdf",
+        {
+            "title": "Mise en forme des documents PDF",
+            "related": ("pvpdf", "bul_margins"),
+        },
+    ),
+    (
+        "pvpdf",
+        {
+            "title": "Procès verbaux de jury (documents PDF)",
+            "related": ("pdf", "bul_margins"),
+        },
+    ),
+    # sur page "Réglages des bulletins de notes"
+    (
+        "bul",
+        {
+            "title": "Réglages des bulletins de notes",
+            "related": ("abs", "bul_margins", "bul_mail"),
+        },
+    ),
+    # sur page "Mise en page des bulletins"
+    (
+        "bul_margins",
+        {
+            "title": "Marges additionnelles des bulletins, en millimètres",
+            "subtitle": "Le bulletin de notes notes est toujours redimensionné pour occuper l'espace disponible entre les marges.",
+            "related": ("bul", "bul_mail", "pdf"),
+        },
+    ),
+    (
+        "bul_mail",
+        {
+            "title": "Envoi des bulletins par e-mail",
+            "related": ("bul", "bul_margins", "pdf"),
+        },
+    ),
+    (
+        "feuilles",
+        {"title": "Mise en forme des feuilles (Absences, Trombinoscopes, ...)"},
+    ),
+    ("pe", {"title": "Avis de poursuites d'études"}),
+    ("edt", {"title": "Connexion avec le logiciel d'emplois du temps"}),
+)
+
+
+PREFS = (
+    (
+        "DeptName",
+        {
+            "initvalue": "Dept",
+            "title": "Nom abrégé du département",
+            "size": 12,
+            "category": "general",
+            "only_global": True,
+        },
+    ),
+    (
+        "DeptFullName",
+        {
+            "initvalue": "nom du département",
+            "title": "Nom complet du département",
+            "explanation": "inutilisé par défaut",
+            "size": 40,
+            "category": "general",
+            "only_global": True,
+        },
+    ),
+    (
+        "UnivName",
+        {
+            "initvalue": "",
+            "title": "Nom de l'Université",
+            "explanation": "apparait sur les bulletins et PV de jury",
+            "size": 40,
+            "category": "general",
+            "only_global": True,
+        },
+    ),
+    (
+        "InstituteName",
+        {
+            "initvalue": "",
+            "title": "Nom de l'Institut",
+            "explanation": 'exemple "IUT de Villetaneuse". Peut être utilisé sur les bulletins.',
+            "size": 40,
+            "category": "general",
+            "only_global": True,
+        },
+    ),
+    (
+        "DeptIntranetTitle",
+        {
+            "initvalue": "Intranet",
+            "title": "Nom lien intranet",
+            "size": 40,
+            "explanation": 'titre du lien "Intranet" en haut à gauche',
+            "category": "general",
+            "only_global": True,
+        },
+    ),
+    (
+        "DeptIntranetURL",
+        {
+            "initvalue": "",
+            "title": """URL de l'"intranet" du département""",
+            "size": 40,
+            "explanation": 'lien "Intranet" en haut à gauche',
+            "category": "general",
+            "only_global": True,
+        },
+    ),
+    (
+        "emails_notifications",
+        {
+            "initvalue": "",
+            "title": "e-mails à qui notifier les opérations",
+            "size": 70,
+            "explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc). (vous pouvez préférer utiliser le flux rss)",
+            "category": "general",
+            "only_global": False,  # peut être spécifique à un semestre
+        },
+    ),
+    # ------------------ MISC
+    (
+        "use_ue_coefs",
+        {
+            "initvalue": 0,
+            "title": "Utiliser les coefficients d'UE pour calculer la moyenne générale",
+            "explanation": """Calcule les moyennes dans chaque UE, puis pondère ces résultats pour obtenir la moyenne générale. Par défaut, le coefficient d'une UE est simplement la somme des coefficients des modules dans lesquels l'étudiant a des notes. <b>Attention: changer ce réglage va modifier toutes les moyennes du semestre !</b>""",
+            "input_type": "boolcheckbox",
+            "category": "misc",
+            "labels": ["non", "oui"],
+            "only_global": False,
+        },
+    ),
+    (
+        "recap_hidebac",
+        {
+            "initvalue": 0,
+            "title": "Cacher la colonne Bac",
+            "explanation": "sur la table récapitulative",
+            "input_type": "boolcheckbox",
+            "category": "misc",
+            "labels": ["non", "oui"],
+            "only_global": False,
+        },
+    ),
+    # ------------------ Absences
+    (
+        "email_chefdpt",
+        {
+            "initvalue": "",
+            "title": "e-mail chef du département",
+            "size": 40,
+            "explanation": "utilisé pour envoi mail notification absences",
+            "category": "abs",
+            "only_global": True,
+        },
+    ),
+    (
+        "work_saturday",
+        {
+            "initvalue": 0,
+            "title": "Considérer le samedi comme travaillé",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "abs",
+            "only_global": True,  # devrait etre par semestre, mais demanderait modif gestion absences
+        },
+    ),
+    (
+        "handle_billets_abs",
+        {
+            "initvalue": 0,
+            "title": 'Gestion de "billets" d\'absence',
+            "explanation": 'fonctions pour traiter les "billets" déclarés par les étudiants sur un portail externe',
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "abs",
+            "only_global": True,
+        },
+    ),
+    (
+        "abs_notify_chief",  # renamed from "send_mail_absence_to_chef"
+        {
+            "initvalue": 0,
+            "title": "Notifier les absences au chef",
+            "explanation": "Envoyer un mail au chef si un étudiant a beaucoup d'absences",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "abs",
+            "only_global": True,
+        },
+    ),
+    (
+        "abs_notify_respsem",
+        {
+            "initvalue": 0,
+            "title": "Notifier les absences au dir. des études",
+            "explanation": "Envoyer un mail au responsable du semestre si un étudiant a beaucoup d'absences",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "abs",
+        },
+    ),
+    (
+        "abs_notify_respeval",
+        {
+            "initvalue": 0,
+            "title": "Notifier les absences aux resp. de modules",
+            "explanation": "Envoyer un mail à chaque absence aux responsable des modules avec évaluation à cette date",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "abs",
+        },
+    ),
+    (
+        "abs_notify_etud",
+        {
+            "initvalue": 0,
+            "title": "Notifier les absences aux étudiants concernés",
+            "explanation": "Envoyer un mail à l'étudiant s'il a \"beaucoup\" d'absences",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "abs",
+        },
+    ),
+    (
+        "abs_notify_email",
+        {
+            "initvalue": "",
+            "title": "Notifier à:",
+            "explanation": "e-mail à qui envoyer des notification d'absences (en sus des autres destinataires éventuels, comme le chef etc.)",
+            "size": 40,
+            "explanation": "utilisé pour envoi mail absences",
+            "category": "abs",
+        },
+    ),
+    (
+        "abs_notify_max_freq",
+        {
+            "initvalue": 7,
+            "title": "Fréquence maximale de notification",
+            "explanation": "en jours (pas plus de X envois de mail pour chaque étudiant/destinataire)",
+            "size": 4,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "abs",
+        },
+    ),
+    (
+        "abs_notify_abs_threshold",
+        {
+            "initvalue": 10,
+            "title": "Seuil de première notification",
+            "explanation": "nb minimum d'absences (en 1/2 journées) avant notification",
+            "size": 4,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "abs",
+        },
+    ),
+    (
+        "abs_notify_abs_increment",
+        {
+            "initvalue": 20,  # les notification suivantes seront donc rares
+            "title": "Seuil notifications suivantes",
+            "explanation": "nb minimum d'absences (en 1/2 journées supplémentaires)",
+            "size": 4,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "abs",
+        },
+    ),
+    (
+        "abs_notification_mail_tmpl",
+        {
+            "initvalue": """
+--- Ceci est un message de notification automatique issu de ScoDoc ---
+
+L'étudiant %(nomprenom)s  
+inscrit en %(inscription)s) 
+
+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.
+
+
+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.
+""",
+            "title": """Message notification e-mail""",
+            "explanation": """Balises remplacées, voir la documentation""",
+            "input_type": "textarea",
+            "rows": 15,
+            "cols": 64,
+            "category": "abs",
+        },
+    ),
+    # portal
+    (
+        "portal_url",
+        {
+            "initvalue": "",
+            "title": "URL du portail",
+            "size": 40,
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "portal_timeout",
+        {
+            "initvalue": 3,
+            "title": "timeout",
+            "explanation": "secondes",
+            "size": 3,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "portal_dept_name",
+        {
+            "initvalue": "Dept",
+            "title": "Code du département sur le portail",
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "etapes_url",
+        {
+            "initvalue": "",
+            "title": "URL listant les étapes Apogée",
+            "size": 40,
+            "category": "portal",
+            "only_global": True,
+            "explanation": "par defaut, selon l'api, getEtapes ou scodocEtapes sur l'URL du portail",
+        },
+    ),
+    (
+        "maquette_url",
+        {
+            "initvalue": "",
+            "title": "URL maquettes Apogee",
+            "size": 40,
+            "category": "portal",
+            "only_global": True,
+            "explanation": "par defaut, scodocMaquette sur l'URL du portail",
+        },
+    ),
+    (
+        "portal_api",
+        {
+            "initvalue": 1,
+            "title": "Version de l'API",
+            "explanation": "1 ou 2",
+            "size": 3,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "etud_url",
+        {
+            "initvalue": "",
+            "title": "URL listant les étudiants Apogée",
+            "size": 40,
+            "category": "portal",
+            "only_global": True,
+            "explanation": "par defaut, selon l'api, getEtud ou scodocEtudiant sur l'URL du portail",
+        },
+    ),
+    (
+        "photo_url",
+        {
+            "initvalue": "",
+            "title": "URL donnant la photo d'un étudiant avec argument nip=",
+            "size": 40,
+            "category": "portal",
+            "only_global": True,
+            "explanation": "par defaut, selon l'api, getPhoto ou scodocPhoto sur l'URL du portail",
+        },
+    ),
+    (
+        "xml_etapes_by_dept",
+        {
+            "initvalue": 1,
+            "title": "Etapes séparées par département",
+            "explanation": "XML getEtapes structuré en départements ?",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "notify_etud_changes_to",
+        {
+            "initvalue": "",
+            "title": "e-mail à qui notifier les changements d'identité des étudiants",
+            "explanation": "utile pour mettre à jour manuellement d'autres bases de données",
+            "size": 40,
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "always_require_ine",
+        {
+            "initvalue": 0,
+            "title": "Impose la présence du code INE",
+            "explanation": "lors de toute création d'étudiant (manuelle ou non)",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "always_require_apo_sem_codes",
+        {
+            "initvalue": 0,
+            "title": "Impose la présence des codes Apogée",
+            "explanation": "lors des créations de semestres",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    # exports Apogée
+    (
+        "export_res_etape",
+        {
+            "initvalue": 1,
+            "title": "Exporter résultat de l'étape",
+            "explanation": "remplissage maquettes export Apogée",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "export_res_sem",
+        {
+            "initvalue": 1,
+            "title": "Exporter résultat du semestre",
+            "explanation": "remplissage maquettes export Apogée",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "export_res_ues",
+        {
+            "initvalue": 1,
+            "title": "Exporter les résultats d'UE",
+            "explanation": "remplissage maquettes export Apogée",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "export_res_modules",
+        {
+            "initvalue": 1,
+            "title": "Exporter les résultats de modules",
+            "explanation": "remplissage maquettes export Apogée",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "export_res_sdj",
+        {
+            "initvalue": 0,
+            "title": "Exporter les résultats même sans décision de jury",
+            "explanation": "si coché, exporte exporte étudiants même si pas décision de jury saisie (sinon laisse vide)",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    (
+        "export_res_rat",
+        {
+            "initvalue": 1,
+            "title": "Exporter les RAT comme ATT",
+            "explanation": "si coché, exporte exporte étudiants en attente de ratrapage comme ATT (sinon laisse vide)",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "portal",
+            "only_global": True,
+        },
+    ),
+    # pdf
+    (
+        "SCOLAR_FONT",
+        {
+            "initvalue": "Helvetica",
+            "title": "Police de caractère principale",
+            "explanation": "pour les pdf",
+            "size": 25,
+            "category": "pdf",
+        },
+    ),
+    (
+        "SCOLAR_FONT_SIZE",
+        {
+            "initvalue": 10,
+            "title": "Taille des caractères",
+            "explanation": "pour les pdf",
+            "size": 4,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "pdf",
+        },
+    ),
+    (
+        "SCOLAR_FONT_SIZE_FOOT",
+        {
+            "initvalue": 6,
+            "title": "Taille des caractères pied de page",
+            "explanation": "pour les pdf",
+            "size": 4,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "pdf",
+        },
+    ),
+    (
+        "pdf_footer_x",
+        {
+            "initvalue": 20,
+            "title": "Position horizontale du pied de page pdf (en mm)",
+            "size": 8,
+            "type": "float",
+            "category": "pdf",
+        },
+    ),
+    (
+        "pdf_footer_y",
+        {
+            "initvalue": 6.35,
+            "title": "Position verticale du pied de page pdf (en mm)",
+            "size": 8,
+            "type": "float",
+            "category": "pdf",
+        },
+    ),
+    # pvpdf
+    (
+        "DirectorName",
+        {
+            "initvalue": "",
+            "title": "Nom du directeur de l'établissement",
+            "size": 32,
+            "explanation": "pour les PV de jury",
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "DirectorTitle",
+        {
+            "initvalue": """directeur de l'IUT""",
+            "title": 'Titre du "directeur"',
+            "explanation": "titre apparaissant à côté de la signature sur les PV de jury",
+            "size": 64,
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "ChiefDeptName",
+        {
+            "initvalue": "",
+            "title": "Nom du chef de département",
+            "size": 32,
+            "explanation": "pour les bulletins pdf",
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "INSTITUTION_NAME",
+        {
+            "initvalue": "<b>Institut Universitaire de Technologie - Université Paris 13</b>",
+            "title": "Nom institution sur pied de pages PV",
+            "explanation": "(pdf, balises &lt;b&gt; interprétées)",
+            "input_type": "textarea",
+            "rows": 4,
+            "cols": 64,
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "INSTITUTION_ADDRESS",
+        {
+            "initvalue": "Web <b>www.iutv.univ-paris13.fr</b> - 99 avenue Jean-Baptiste Clément - F 93430 Villetaneuse",
+            "title": "Adresse institution sur pied de pages PV",
+            "explanation": "(pdf, balises &lt;b&gt; interprétées)",
+            "input_type": "textarea",
+            "rows": 4,
+            "cols": 64,
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "INSTITUTION_CITY",
+        {
+            "initvalue": "Villetaneuse",
+            "title": "Ville de l'institution",
+            "explanation": "pour les lettres individuelles",
+            "size": 64,
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "PV_INTRO",
+        {
+            "initvalue": """<bullet>-</bullet>  
+Vu l'arrêté du 3 août 2005 relatif au diplôme universitaire de technologie et notamment son article 4 et 6;
+</para>
+<para><bullet>-</bullet>  
+vu l'arrêté n° %(Decnum)s du Président de l'%(UnivName)s;
+</para>
+<para><bullet>-</bullet> 
+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) """,
+            "input_type": "textarea",
+            "cols": 80,
+            "rows": 10,
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "PV_LETTER_DIPLOMA_SIGNATURE",
+        {
+            "initvalue": """Le %(DirectorTitle)s, <br/>%(DirectorName)s""",
+            "title": """Signature des lettres individuelles de diplôme""",
+            "explanation": """%(DirectorName)s et %(DirectorTitle)s remplacés""",
+            "input_type": "textarea",
+            "rows": 4,
+            "cols": 64,
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "PV_LETTER_PASSAGE_SIGNATURE",
+        {
+            "initvalue": """Pour le Directeur de l'IUT<br/>
+et par délégation<br/>
+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",
+            "rows": 4,
+            "cols": 64,
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "pv_sig_image_height",
+        {
+            "initvalue": 11,
+            "size": 10,
+            "title": "Hauteur de l'image de la signature",
+            "type": "float",
+            "explanation": "Lorsqu'on donne une image de signature, elle est redimensionnée à cette taille (en millimètres)",
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "PV_LETTER_TEMPLATE",
+        {
+            "initvalue": """<para spaceBefore="1mm"> </para>
+<para spaceBefore="20mm" leftindent="%(pv_htab1)s">%(INSTITUTION_CITY)s, le %(date_jury)s
+</para>
+
+<para leftindent="%(pv_htab1)s" spaceBefore="10mm">
+à <b>%(nomprenom)s</b>
+</para>
+<para leftindent="%(pv_htab1)s">%(domicile)s</para>
+<para leftindent="%(pv_htab1)s">%(codepostaldomicile)s %(villedomicile)s</para>
+
+<para spaceBefore="25mm" fontSize="14" alignment="center">
+<b>Jury de %(type_jury)s  <br/> %(titre_formation)s</b>
+</para>
+
+<para spaceBefore="10mm" fontSize="14" leftindent="0">
+Le jury de %(type_jury_abbrv)s du département %(DeptName)s
+s'est réuni le %(date_jury)s. 
+</para>
+<para fontSize="14" leftindent="0">Les décisions vous concernant sont :
+</para>
+
+<para leftindent="%(pv_htab2)s" spaceBefore="5mm" fontSize="14">%(prev_decision_sem_txt)s</para>
+<para leftindent="%(pv_htab2)s" spaceBefore="5mm" fontSize="14">
+    <b>Décision %(decision_orig)s :</b> %(decision_sem_descr)s
+</para>
+
+<para leftindent="%(pv_htab2)s" spaceBefore="0mm" fontSize="14">
+%(decision_ue_txt)s
+</para>
+
+<para leftindent="%(pv_htab2)s" spaceBefore="0mm" fontSize="14">
+%(observation_txt)s
+</para>
+
+<para spaceBefore="10mm" fontSize="14">%(autorisations_txt)s</para>
+
+<para spaceBefore="10mm" fontSize="14">%(diplome_txt)s</para>
+""",
+            "title": """Lettre individuelle""",
+            "explanation": """Balises remplacées et balisage XML, voir la documentation""",
+            "input_type": "textarea",
+            "rows": 15,
+            "cols": 64,
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "PV_LETTER_WITH_HEADER",
+        {
+            "initvalue": 0,
+            "title": "Imprimer le logo en tête des lettres individuelles de décision",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "PV_LETTER_WITH_FOOTER",
+        {
+            "initvalue": 0,
+            "title": "Imprimer le pied de page sur les lettres individuelles de décision",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "pv_htab1",
+        {
+            "initvalue": "8cm",
+            "title": "marge colonne droite lettre",
+            "explanation": "pour les courriers pdf",
+            "size": 10,
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "pv_htab2",
+        {
+            "initvalue": "5mm",
+            "title": "marge colonne gauche lettre",
+            "explanation": "pour les courriers pdf",
+            "size": 10,
+            "category": "pvpdf",
+        },
+    ),
+    (
+        "PV_FONTNAME",
+        {
+            "initvalue": "Times-Roman",
+            "title": "Police de caractère pour les PV",
+            "explanation": "pour les pdf",
+            "size": 25,
+            "category": "pvpdf",
+        },
+    ),
+    # bul
+    (
+        "bul_title",
+        {
+            "initvalue": "Université Paris 13 - IUT de Villetaneuse - Département %(DeptName)s",
+            "size": 70,
+            "title": "Titre des bulletins",
+            "explanation": "<tt>%(DeptName)s</tt> est remplacé par le nom du département",
+            "category": "bul",
+        },
+    ),
+    (
+        "bul_class_name",
+        {
+            "initvalue": sco_bulletins_generator.bulletin_default_class_name(),
+            "input_type": "menu",
+            "labels": sco_bulletins_generator.bulletin_class_descriptions(),
+            "allowed_values": sco_bulletins_generator.bulletin_class_names(),
+            "title": "Format des bulletins",
+            "explanation": "format de présentation des bulletins de note (web et pdf)",
+            "category": "bul",
+        },
+    ),
+    (
+        "bul_show_abs",  # ex "gestion_absence"
+        {
+            "initvalue": 1,
+            "title": "Indiquer les absences sous les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_abs_modules",
+        {
+            "initvalue": 0,
+            "title": "Indiquer les absences dans chaque module",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_decision",
+        {
+            "initvalue": 0,
+            "title": "Faire figurer les décisions sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_ects",
+        {
+            "initvalue": 1,
+            "title": "Faire figurer les ECTS sur les bulletins",
+            "explanation": "crédits associés aux UE ou aux modules, selon réglage",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_codemodules",
+        {
+            "initvalue": 0,
+            "title": "Afficher codes des modules sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_matieres",
+        {
+            "initvalue": 0,
+            "title": "Afficher les matières sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_all_evals",
+        {
+            "initvalue": 0,
+            "title": "Afficher toutes les évaluations sur les bulletins",
+            "explanation": "y compris incomplètes ou futures",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_rangs",
+        {
+            "initvalue": 1,
+            "title": "Afficher le classement sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_ue_rangs",
+        {
+            "initvalue": 1,
+            "title": "Afficher le classement dans chaque UE sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_mod_rangs",
+        {
+            "initvalue": 1,
+            "title": "Afficher le classement dans chaque module sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_moypromo",
+        {
+            "initvalue": 0,
+            "title": "Afficher moyennes de la promotion sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_minmax",
+        {
+            "initvalue": 0,
+            "title": "Afficher min/max moyennes sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_minmax_mod",
+        {
+            "initvalue": 0,
+            "title": "Afficher min/max moyennes des modules sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_minmax_eval",
+        {
+            "initvalue": 0,
+            "title": "Afficher min/max moyennes des évaluations sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_coef",
+        {
+            "initvalue": 1,
+            "title": "Afficher coefficient des ue/modules sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_ue_cap_details",
+        {
+            "initvalue": 0,
+            "title": "Afficher détail des notes des UE capitalisées sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_ue_cap_current",
+        {
+            "initvalue": 1,
+            "title": "Afficher les UE en cours mais capitalisées sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_temporary_forced",
+        {
+            "initvalue": 0,
+            "title": 'Bannière "provisoire" sur les bulletins',
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_temporary",
+        {
+            "initvalue": 1,
+            "title": 'Bannière "provisoire" si pas de décision de jury',
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_temporary_txt",
+        {
+            "initvalue": "Provisoire",
+            "title": 'Texte de la bannière "provisoire',
+            "explanation": "",
+            "size": 40,
+            "category": "bul",
+        },
+    ),
+    (
+        "bul_show_uevalid",
+        {
+            "initvalue": 1,
+            "title": "Faire figurer les UE validées sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_mention",
+        {
+            "initvalue": 0,
+            "title": "Faire figurer les mentions sur les bulletins et les PV",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_date_inscr",
+        {
+            "initvalue": 1,
+            "title": "Faire figurer la date d'inscription sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_sig_left",
+        {
+            "initvalue": 0,
+            "title": "Faire figurer le pied de page de gauche (ex.: nom du directeur) sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_show_sig_right",
+        {
+            "initvalue": 0,
+            "title": "Faire figurer le pied de page de droite (ex.: nom du chef de département) sur les bulletins",
+            "input_type": "boolcheckbox",
+            "category": "bul",
+            "labels": ["non", "oui"],
+        },
+    ),
+    (
+        "bul_display_publication",
+        {
+            "initvalue": 1,
+            "title": "Indique si les bulletins sont publiés",
+            "explanation": "décocher si vous n'avez pas de portal étudiant publiant les bulletins",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "bul",
+            "only_global": False,
+        },
+    ),
+    # champs des bulletins PDF:
+    (
+        "bul_pdf_title",
+        {
+            "initvalue": """<para fontSize="14" align="center">
+<b>%(UnivName)s</b>
+</para>
+<para fontSize="16" align="center" spaceBefore="2mm">
+<b>%(InstituteName)s</b>
+</para>
+<para fontSize="16" align="center" spaceBefore="4mm">
+<b>RELEVÉ DE NOTES</b>
+</para>
+
+<para fontSize="15" spaceBefore="3mm">
+%(nomprenom)s <b>%(demission)s</b>
+</para>
+
+<para fontSize="14" spaceBefore="3mm">
+Formation: %(titre_num)s</para>
+<para fontSize="14" spaceBefore="2mm">
+Année scolaire: %(anneescolaire)s
+</para>""",
+            "title": "Bulletins PDF: paragraphe de titre",
+            "explanation": "(balises interprétées, voir documentation)",
+            "input_type": "textarea",
+            "rows": 10,
+            "cols": 64,
+            "category": "bul",
+        },
+    ),
+    (
+        "bul_pdf_caption",
+        {
+            "initvalue": """<para spaceBefore="5mm" fontSize="14"><i>%(situation)s</i></para>""",
+            "title": "Bulletins PDF: paragraphe sous table note",
+            "explanation": '(visible seulement si "Faire figurer les décision" est coché)',
+            "input_type": "textarea",
+            "rows": 4,
+            "cols": 64,
+            "category": "bul",
+        },
+    ),
+    (
+        "bul_pdf_sig_left",
+        {
+            "initvalue": """<para>La direction des études
+<br/>
+%(responsable)s
+</para>
+""",
+            "title": "Bulletins PDF: signature gauche",
+            "explanation": "(balises interprétées, voir documentation)",
+            "input_type": "textarea",
+            "rows": 4,
+            "cols": 64,
+            "category": "bul",
+        },
+    ),
+    (
+        "bul_pdf_sig_right",
+        {
+            "initvalue": """<para>Le chef de département
+<br/>
+%(ChiefDeptName)s
+</para>
+""",
+            "title": "Bulletins PDF: signature droite",
+            "explanation": "(balises interprétées, voir documentation)",
+            "input_type": "textarea",
+            "rows": 4,
+            "cols": 64,
+            "category": "bul",
+        },
+    ),
+    (
+        "bul_pdf_mod_colwidth",
+        {
+            "initvalue": None,
+            "title": "Bulletins PDF: largeur col. modules",
+            "explanation": "en cm (vide si auto)",
+            "type": "float",
+            "category": "bul",
+        },
+    ),
+    (
+        "SCOLAR_FONT_BUL_FIELDS",
+        {
+            "initvalue": "Times-Roman",
+            "title": "Police titres bulletins",
+            "explanation": "pour les pdf",
+            "size": 25,
+            "category": "bul",
+        },
+    ),
+    # XXX A COMPLETER, voir sco_formsemestre_edit.py XXX
+    # bul_mail
+    (
+        "email_copy_bulletins",
+        {
+            "initvalue": "",
+            "title": "e-mail copie bulletins",
+            "size": 40,
+            "explanation": "adresse recevant une copie des bulletins envoyés aux étudiants",
+            "category": "bul_mail",
+        },
+    ),
+    (
+        "email_from_addr",
+        {
+            "initvalue": "noreply@scodoc.example.com",
+            "title": "adresse mail origine",
+            "size": 40,
+            "explanation": "adresse expéditeur pour les envois par mails (bulletins)",
+            "category": "bul_mail",
+            "only_global": True,
+        },
+    ),
+    (
+        "bul_intro_mail",
+        {
+            "initvalue": """%(nomprenom)s,\n\nvous trouverez ci-joint votre relevé de notes au format PDF.\nIl s\'agit d\'un relevé indicatif. Seule la version papier signée par le responsable pédagogique de l\'établissement prend valeur officielle.\n\nPour toute question sur ce document, contactez votre enseignant ou le directeur des études (ne pas répondre à ce message).\n\nCordialement,\nla scolarité du département %(dept)s.\n\nPS: si vous recevez ce message par erreur, merci de contacter %(webmaster)s""",
+            "input_type": "textarea",
+            "title": "Message d'accompagnement",
+            "explanation": "<tt>%(DeptName)s</tt> est remplacé par le nom du département, <tt>%(nomprenom)s</tt> par les noms et prénoms de l'étudiant, <tt>%(dept)s</tt> par le nom du département, et <tt>%(webmaster)s</tt> par l'adresse mail du Webmaster.",
+            "rows": 18,
+            "cols": 85,
+            "category": "bul_mail",
+        },
+    ),
+    (
+        "bul_mail_list_abs",
+        {
+            "initvalue": 0,
+            "title": "Indiquer la liste des dates d'absences par mail",
+            "explanation": "dans le mail envoyant le bulletin de notes",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "bul_mail",
+        },
+    ),
+    (
+        "bul_mail_contact_addr",
+        {
+            "initvalue": "l'administrateur",
+            "title": 'Adresse mail contact "webmaster"',
+            "explanation": 'apparait dans le mail accompagnant le bulletin, voir balise "webmaster" ci-dessus.',
+            "category": "bul_mail",
+            "size": 32,
+        },
+    ),
+    (
+        "bul_mail_allowed_for_all",
+        {
+            "initvalue": 1,
+            "title": "Autoriser tous les utilisateurs à expédier des bulletins par mail",
+            "input_type": "boolcheckbox",
+            "category": "bul_mail",
+            "labels": ["non", "oui"],
+        },
+    ),
+    # bul_margins
+    (
+        "left_margin",
+        {
+            "initvalue": 0,
+            "size": 10,
+            "title": "Marge gauche",
+            "type": "float",
+            "category": "bul_margins",
+        },
+    ),
+    (
+        "top_margin",
+        {
+            "initvalue": 0,
+            "size": 10,
+            "title": "Marge haute",
+            "type": "float",
+            "category": "bul_margins",
+        },
+    ),
+    (
+        "right_margin",
+        {
+            "initvalue": 0,
+            "size": 10,
+            "title": "Marge droite",
+            "type": "float",
+            "category": "bul_margins",
+        },
+    ),
+    (
+        "bottom_margin",
+        {
+            "initvalue": 0,
+            "size": 10,
+            "title": "Marge basse",
+            "type": "float",
+            "category": "bul_margins",
+        },
+    ),
+    # Mise en page feuilles absences/trombinoscopes
+    (
+        "feuille_releve_abs_taille",
+        {
+            "initvalue": "A3",
+            "input_type": "menu",
+            "labels": ["A3", "A4"],
+            "allowed_values": ["A3", "A4"],
+            "title": "Taille feuille relevé absences",
+            "explanation": "Dimensions du papier pour les feuilles de relevés d'absences hebdomadaire",
+            "category": "feuilles",
+        },
+    ),
+    (
+        "feuille_releve_abs_format",
+        {
+            "initvalue": "Paysage",
+            "input_type": "menu",
+            "labels": ["Paysage", "Portrait"],
+            "allowed_values": ["Paysage", "Portrait"],
+            "title": "Format feuille relevé absences",
+            "explanation": "Format du papier pour les feuilles de relevés d'absences hebdomadaire",
+            "category": "feuilles",
+        },
+    ),
+    (
+        "feuille_releve_abs_samedi",
+        {
+            "initvalue": 1,
+            "title": "Samedi travaillé",
+            "input_type": "boolcheckbox",
+            "labels": ["non", "oui"],
+            "category": "feuilles",
+        },
+    ),
+    (
+        "feuille_releve_abs_AM",
+        {
+            "initvalue": "2",
+            "title": "Créneaux cours matin",
+            "explanation": "Nombre de créneaux de cours le matin",
+            "size": 4,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "feuilles",
+        },
+    ),
+    (
+        "feuille_releve_abs_PM",
+        {
+            "initvalue": "3",
+            "title": "Créneaux cours après-midi",
+            "explanation": "Nombre de créneaux de cours l'après-midi",
+            "size": 4,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "feuilles",
+        },
+    ),
+    (
+        "feuille_placement_emargement",
+        {
+            "initvalue": "625",
+            "title": "Feuille d'émargement des contrôles - Signature étudiant",
+            "explanation": "Hauteur de l'espace pour signer",
+            "size": 4,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "feuilles",
+        },
+    ),
+    (
+        "feuille_placement_positions",
+        {
+            "initvalue": "45",
+            "title": "Feuille des places lors des contrôles",
+            "explanation": "Nombre maximum de lignes par colonne",
+            "size": 4,
+            "type": "int",
+            "convert_numbers": True,
+            "category": "feuilles",
+        },
+    ),
+    # Feuille prepa jury
+    (
+        "prepa_jury_nip",
+        {
+            "initvalue": 0,
+            "title": "Code NIP sur la feuille préparation jury",
+            "input_type": "boolcheckbox",
+            "category": "feuilles",
+            "labels": ["non", "oui"],
+            "only_global": True,
+        },
+    ),
+    (
+        "prepa_jury_ine",
+        {
+            "initvalue": 0,
+            "title": "Code INE sur la feuille préparation jury",
+            "input_type": "boolcheckbox",
+            "category": "feuilles",
+            "labels": ["non", "oui"],
+            "only_global": True,
+        },
+    ),
+    (
+        "anonymous_lst_code",
+        {
+            "initvalue": "NIP",
+            "input_type": "menu",
+            "labels": ["NIP", "INE"],
+            "allowed_values": ["NIP", "INE"],
+            "title": "Code pour listes anonymes",
+            "explanation": "à défaut, un code interne sera utilisé",
+            "category": "feuilles",
+            "only_global": True,
+        },
+    ),
+    # Experimental: avis poursuite d'études
+    (
+        "NomResponsablePE",
+        {
+            "initvalue": "",
+            "title": "Nom du responsable des poursuites d'études",
+            "size": 32,
+            "explanation": "pour les avis pdf de poursuite",
+            "category": "pe",
+        },
+    ),
+    (
+        "pe_avis_latex_tmpl",
+        {
+            "title": "Template LaTeX des avis",
+            "initvalue": "",
+            "explanation": "préparez-le dans un éditeur de texte puis copier le contenu ici (en utf8). Sinon, le fichier un_avis.tex du serveur sera utilisé.",
+            "input_type": "textarea",
+            "rows": 4,
+            "cols": 80,
+            "category": "pe",
+        },
+    ),
+    (
+        "pe_avis_latex_footer",
+        {
+            "title": "Code LaTeX en fin d'avis",
+            "initvalue": "",
+            "explanation": "",
+            "input_type": "textarea",
+            "rows": 5,
+            "cols": 80,
+            "category": "pe",
+        },
+    ),
+    (
+        "pe_tag_annotation_avis_latex",
+        {
+            "title": "Tag désignant l'avis PE",
+            "initvalue": "PE&gt;",
+            "explanation": """ajoutez une annotation aux étudiants précédée du tag désigné ici pour qu'elle soit interprétée comme un avis de poursuites d'études et ajoutée aux avis LaTeX.""",
+            "size": 25,
+            "category": "pe",
+        },
+    ),
+    # Lien avec logiciel emplois du temps
+    (
+        "edt_sem_ics_url",
+        {
+            "title": "Lien EDT",
+            "initvalue": "",
+            "explanation": "URL du calendrier ics emploi du temps du semestre (template)",
+            "size": 80,
+            "category": "edt",
+        },
+    ),
+    (
+        "edt_groups2scodoc",
+        {
+            "input_type": "textarea",
+            "initvalue": "",
+            "title": "Noms Groupes",
+            "explanation": "Transcodage: nom de groupe EDT ; non de groupe ScoDoc (sur plusieurs lignes)",
+            "rows": 8,
+            "cols": 16,
+            "category": "edt",
+        },
+    ),
+    (
+        "ImputationDept",
+        {
+            "title": "Département d'imputation",
+            "initvalue": "",
+            "explanation": "préfixe id de session (optionnel, remplace nom département)",
+            "size": 10,
+            "category": "edt",
+        },
+    ),
+)
+
+PREFS_NAMES = set([x[0] for x in PREFS])
+PREFS_ONLY_GLOBAL = set([x[0] for x in PREFS if x[1].get("only_global", False)])
+
+PREFS_DICT = dict(PREFS)
+
+
+class sco_base_preferences:
+    _editor = EditableTable(
+        "sco_prefs",
+        "pref_id",
+        ("pref_id", "name", "value", "formsemestre_id"),
+        sortkey="name",
+        convert_null_outputs_to_empty=False,
+        allow_set_id=True,
+        html_quote=False,  # car markup pdf reportlab  (<b> etc)
+        filter_nulls=False,
+    )
+
+    def __init__(self, context):
+        self.context = context
+        self.load()
+
+    def load(self):
+        """Load all preferences from db
+        """
+        log("loading preferences")
+        try:
+            GSL.acquire()
+            cnx = self.context.GetDBConnexion()
+            preflist = self._editor.list(cnx)
+            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"]] = {}
+
+                # Convert types:
+                if p["name"] in PREFS_DICT and PREFS_DICT[p["name"]].has_key("type"):
+                    typ = 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 PREFS_DICT
+                    and PREFS_DICT[p["name"]].get("input_type", None) == "boolcheckbox"
+                ):
+                    if p["value"]:
+                        p["value"] = int(p["value"])  # boolcheckboxes are always 0/1
+                    else:
+                        p["value"] = 0  # NULL (backward compat)
+                self.prefs[p["formsemestre_id"]][p["name"]] = p["value"]
+
+            # log('prefs=%s' % self.prefs)
+
+            # add defaults for missing prefs
+            for pref in PREFS:
+                name = pref[0]
+
+                # Migration from previous ScoDoc installations (before june 2008)
+                # search preferences in Zope properties and in configuration file
+                if name and name[0] != "_" and not name in self.prefs[None]:
+                    try:
+                        value = getattr(self.context, name)
+                        log(
+                            "sco_preferences: found default value in Zope for %s=%s"
+                            % (name, value)
+                        )
+                    except:
+                        # search in CONFIG
+                        if hasattr(CONFIG, name):
+                            value = getattr(CONFIG, name)
+                            log(
+                                "sco_preferences: found default value in config for %s=%s"
+                                % (name, value)
+                            )
+                        else:
+                            # uses hardcoded default
+                            value = pref[1]["initvalue"]
+
+                    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, {"name": name, "value": value})
+        finally:
+            GSL.release()
+
+    def get(self, formsemestre_id, name):
+        """Returns preference value.
+        If 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]
+
+    def __contains__(self, item):
+        return item in self.prefs[None]
+
+    def __len__(self):
+        return len(self.prefs[None])
+
+    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
+
+    def save(self, formsemestre_id=None, name=None):
+        """Write one or all (if name is None) values to db"""
+        try:
+            GSL.acquire()
+            modif = False
+            cnx = self.context.GetDBConnexion()
+            if name is None:
+                names = self.prefs[formsemestre_id].keys()
+            else:
+                names = [name]
+            for name in names:
+                value = self.get(formsemestre_id, name)
+                # existe deja ?
+                pdb = self._editor.list(
+                    cnx, args={"formsemestre_id": formsemestre_id, "name": name}
+                )
+                if not pdb:
+                    # cree preference
+                    log("create pref sem=%s %s=%s" % (formsemestre_id, name, value))
+                    self._editor.create(
+                        cnx,
+                        {
+                            "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))
+
+            # les preferences peuvent affecter les PDF cachés et les notes calculées:
+            if modif:
+                self.context.Notes._inval_cache(pdfonly=False)  # > modif preferences
+        finally:
+            GSL.release()
+
+    def set(self, formsemestre_id, name, value):
+        if not name or name[0] == "_" or name not in PREFS_NAMES:
+            raise ValueError("invalid preference name: %s" % name)
+        if formsemestre_id and name in PREFS_ONLY_GLOBAL:
+            raise ValueError("pref %s is always defined globaly")
+        if not formsemestre_id in self.prefs:
+            self.prefs[formsemestre_id] = {}
+        self.prefs[formsemestre_id][name] = value
+        self.save(formsemestre_id, name)  # immediately write back to db
+
+    def delete(self, formsemestre_id, name):
+        if not formsemestre_id:
+            raise ScoException()
+        try:
+            GSL.acquire()
+            if formsemestre_id in self.prefs and name in self.prefs[formsemestre_id]:
+                del self.prefs[formsemestre_id][name]
+            cnx = self.context.GetDBConnexion()
+            pdb = self._editor.list(
+                cnx, args={"formsemestre_id": formsemestre_id, "name": name}
+            )
+            if pdb:
+                log("deleting pref sem=%s %s" % (formsemestre_id, name))
+                self._editor.delete(cnx, pdb[0]["pref_id"])
+                self.context.Notes._inval_cache(pdfonly=False)  # > modif preferences
+        finally:
+            GSL.release()
+
+    def edit(self, REQUEST):
+        """HTML dialog: edit global preferences"""
+        H = [
+            self.context.sco_header(REQUEST, page_title="Préférences"),
+            "<h2>Préférences globales pour %s</h2>" % self.context.ScoURL(),
+            """<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>
+              """,
+        ]
+        form = _build_form(self, global_edit=True)
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            form,
+            initvalues=self.prefs[None],
+            submitlabel="Enregistrer les modifications",
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + self.context.sco_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(REQUEST.URL1)  # cancel
+        else:
+            for pref in PREFS:
+                self.prefs[None][pref[0]] = tf[2][pref[0]]
+            self.save()
+            return REQUEST.RESPONSE.redirect(
+                REQUEST.URL1 + "?head_message=Préférences modifiées"
+            )
+
+
+_SCO_BASE_PREFERENCES = {}  # { URL: sco_base_preferences instance }
+
+
+def get_base_preferences(context):
+    """Return global preferences for this context"""
+    u = context.ScoURL()
+    if not u in _SCO_BASE_PREFERENCES:
+        _SCO_BASE_PREFERENCES[u] = sco_base_preferences(context)
+    return _SCO_BASE_PREFERENCES[u]
+
+
+class sem_preferences:
+    def __init__(self, context, formsemestre_id=None):
+        self.context = context
+        self.formsemestre_id = formsemestre_id
+        self.base_prefs = get_base_preferences(self.context)
+
+    def __getitem__(self, name):
+        return self.base_prefs.get(self.formsemestre_id, name)
+
+    def __contains__(self, item):
+        "check if item is in (global) preferences"
+        return item in self.base_prefs
+
+    def get(self, name, defaultvalue=None):
+        # utilisé seulement par TF
+        try:
+            return self[name]  # ignore supplied default value
+        except:
+            return defaultvalue
+
+    def is_global(self, name):
+        "True if preference defined for all semestres"
+        return self.base_prefs.is_global(self.formsemestre_id, name)
+
+    # The dialog
+    def edit(self, categories=[], REQUEST=None):
+        """Dialog to edit semestre preferences in given categories"""
+        # log('XXX edit')
+        if not self.formsemestre_id:
+            raise ScoValueError(
+                "sem_preferences.edit doit etre appele sur un semestre !"
+            )  # a bug !
+        sem = sco_formsemestre.get_formsemestre(self.context, self.formsemestre_id)
+        H = [
+            self.context.Notes.html_sem_header(REQUEST, "Préférences du semestre", sem),
+            """
+<p class="help">Les paramètres définis ici ne s'appliqueront qu'à ce semestre.</p>
+<p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
+<script type="text/javascript">
+function sel_global(el, pref_name) {
+     var tf = document.getElementById("tf");
+     if (el.value == 'create') {
+        tf.create_local.value = pref_name;
+        tf.destination.value = 'again';
+        tf.submit();
+     } else if (el.value == 'changeglobal') {
+        tf.destination.value = 'global';
+        tf.submit();
+     }
+}
+function set_global_pref(el, pref_name) {
+     var tf = document.getElementById("tf");
+     tf.suppress.value = pref_name;
+     tf.destination.value = 'again';
+     var f = tf[pref_name];
+     if (f) {
+       f.disabled = true;
+     } else {
+       f =tf[pref_name+':list'];
+       if (f) {
+         f.disabled = true;
+       }
+     }
+    tf.submit();
+}
+</script>
+""",
+        ]
+        # build the form:
+        form = _build_form(self, categories=categories)
+        form.append(("suppress", {"input_type": "hidden"}))
+        form.append(("create_local", {"input_type": "hidden"}))
+        form.append(("destination", {"input_type": "hidden"}))
+        form.append(("formsemestre_id", {"input_type": "hidden"}))
+        # log('REQUEST form=%s'%REQUEST.form)
+        tf = TrivialFormulator(
+            REQUEST.URL0,
+            REQUEST.form,
+            form,
+            initvalues=self,
+            cssclass="sco_pref",
+            submitlabel="Enregistrer les modifications",
+        )
+        if tf[0] == 0:
+            return "\n".join(H) + tf[1] + self.context.sco_footer(REQUEST)
+        elif tf[0] == -1:
+            return REQUEST.RESPONSE.redirect(
+                REQUEST.URL1 + "?head_message=Annulé"
+            )  # cancel
+        else:
+            # log('tf[2]=%s' % tf[2]) # XXX
+            # Supprime pref locale du semestre (retour à la valeur globale)
+            if tf[2]["suppress"]:
+                # log('suppress %s' % tf[2]['suppress']) # XXX
+                self.base_prefs.delete(self.formsemestre_id, tf[2]["suppress"])
+            # Cree pref local (copie valeur globale)
+            if tf[2]["create_local"]:
+                cur_value = self[tf[2]["create_local"]]
+                self.base_prefs.set(
+                    self.formsemestre_id, tf[2]["create_local"], cur_value
+                )
+            # Modifie valeurs:
+            for (pref_name, descr) in PREFS:
+                if (
+                    pref_name in tf[2]
+                    and not descr.get("only_global", False)
+                    and pref_name != tf[2]["suppress"]
+                ):
+                    form_value = tf[2][pref_name]
+                    cur_value = self[pref_name]
+                    if cur_value is None:
+                        cur_value = ""
+                    else:
+                        cur_value = str(cur_value)
+                    if cur_value != str(form_value):
+                        # log('cur_value=%s (type %s), form_value=%s (type %s)' % (cur_value,type(cur_value),form_value, type(form_value)))
+                        self.base_prefs.set(self.formsemestre_id, pref_name, form_value)
+
+            # destination:
+            # global: change pref and redirect to global params
+            # again: change prefs and redisplay this dialog
+            # done: change prefs and redirect to semestre status
+            destination = tf[2]["destination"]
+            if destination == "done" or destination == "":
+                return REQUEST.RESPONSE.redirect(
+                    REQUEST.URL1
+                    + "/formsemestre_status?head_message=Préférences modifiées&amp;formsemestre_id="
+                    + self.formsemestre_id
+                )
+            elif destination == "again":
+                return REQUEST.RESPONSE.redirect(
+                    REQUEST.URL0 + "?formsemestre_id=" + self.formsemestre_id
+                )
+            elif destination == "global":
+                return REQUEST.RESPONSE.redirect(
+                    self.context.ScoURL() + "/edit_preferences"
+                )
+
+
+# Build list of elements for TrivialFormulator...
+def _build_form(self, categories=[], global_edit=False):
+    form = []
+    for cat, cat_descr in PREF_CATEGORIES:
+        if categories and cat not in categories:
+            continue  # skip this category
+        #
+        cat_elems = []
+        for pref_name, pref in PREFS:
+            if pref["category"] == cat:
+                if pref.get("only_global", False) and not global_edit:
+                    continue  # saute les prefs seulement globales
+                descr = pref.copy()
+                descr["comment"] = descr.get("explanation", None)
+                if "explanation" in descr:
+                    del descr["explanation"]
+                if not global_edit:
+                    descr["explanation"] = (
+                        """ou <span class="spanlink" onclick="set_global_pref(this, '%s');">utiliser paramètre global</span>"""
+                        % pref_name
+                    )
+                # if descr.get('only_global',False):
+                #    # pas modifiable, donne juste la valeur courante
+                #    descr['readonly'] = True
+                #    descr['explanation'] = '(valeur globale, non modifiable)'
+                # elif
+                if not global_edit and self.is_global(pref_name):
+                    # valeur actuelle globale (ou vient d'etre supprimee localement):
+                    # montre la valeur et menus pour la rendre locale
+                    descr["readonly"] = True
+                    menu_global = (
+                        """<select class="tf-selglobal" onchange="sel_global(this, '%s');">
+                        <option value="">Valeur définie globalement</option>
+                        <option value="create">Spécifier valeur pour ce semestre seulement</option>
+                    </select>
+                    """
+                        % pref_name
+                    )
+                    #                         <option value="changeglobal">Changer paramètres globaux</option>
+                    descr["explanation"] = menu_global
+
+                cat_elems.append((pref_name, descr))
+        if cat_elems:
+            # category titles:
+            title = cat_descr.get("title", None)
+            if title:
+                form.append(
+                    (
+                        "sep_%s" % cat,
+                        {"input_type": "separator", "title": "<h3>%s</h3>" % title},
+                    )
+                )
+            subtitle = cat_descr.get("subtitle", None)
+            if subtitle:
+                form.append(
+                    (
+                        "sepsub_%s" % cat,
+                        {
+                            "input_type": "separator",
+                            "title": '<p class="help">%s</p>' % subtitle,
+                        },
+                    )
+                )
+            form.extend(cat_elems)
+    return form
+
+
+#
+def doc_preferences(context):
+    """ Liste les preferences en MarkDown, pour la documentation"""
+    L = []
+    for cat, cat_descr in PREF_CATEGORIES:
+        L.append([""])
+        L.append(["## " + cat_descr.get("title", "") ])
+        L.append([""])
+        L.append( ["Nom", "&nbsp;", "&nbsp;" ] )
+        L.append( ["----", "----", "----"] )
+        for pref_name, pref in PREFS:
+            if pref["category"] == cat:
+                L.append(['`'+pref_name+'`', pref["title"], pref.get("explanation", "")])
+    
+    return "\n".join([" | ".join(x) for x in L])
diff --git a/sco_prepajury.py b/sco_prepajury.py
new file mode 100644
index 0000000000000000000000000000000000000000..eed81aae4061b02b903e7518b62df9f5512b0340
--- /dev/null
+++ b/sco_prepajury.py
@@ -0,0 +1,314 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Feuille excel pour preparation des jurys
+"""
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+import notes_table
+import sco_groups
+import sco_excel
+import sco_formsemestre
+import sco_parcours_dut
+import sco_codes_parcours
+from scolars import format_nom, format_prenom, format_sexe, format_lycee
+from ZAbsences import getAbsSemEtud
+
+
+def feuille_preparation_jury(context, formsemestre_id, REQUEST):
+    "Feuille excel pour preparation des jurys"
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etudids, get_etud_moy_gen, get_ues, get_etud_ue_status, get_etud_decision_sem, identdict,
+    etudids = nt.get_etudids(sorted=True)  # tri par moy gen
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+
+    etud_groups = sco_groups.formsemestre_get_etud_groupnames(context, formsemestre_id)
+    main_partition_id = sco_groups.formsemestre_get_main_partition(
+        context, formsemestre_id
+    )["partition_id"]
+
+    prev_moy_ue = DictDefault(defaultvalue={})  # ue_code_s : { etudid : moy ue }
+    prev_ue_acro = {}  # ue_code_s : acronyme (à afficher)
+    prev_moy = {}  # moyennes gen sem prec
+    moy_ue = DictDefault(defaultvalue={})  # ue_acro : moyennes { etudid : moy ue }
+    ue_acro = {}  #  ue_code_s : acronyme (à afficher)
+    moy = {}  # moyennes gen
+    moy_inter = {}  # moyenne gen. sur les 2 derniers semestres
+    code = {}  # decision existantes s'il y en a
+    autorisations = {}
+    prev_code = {}  # decisions sem prec
+    assidu = {}
+    parcours = {}  # etudid : parcours, sous la forme S1, S2, S2, S3
+    groupestd = {}  # etudid : nom groupe principal
+    nbabs = {}
+    nbabsjust = {}
+    for etudid in etudids:
+        info = context.getEtudInfo(etudid=etudid, filled=True)
+        if not info:
+            continue  # should not occur...
+        etud = info[0]
+        Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id)
+        if Se.prev:
+            ntp = context._getNotesCache().get_NotesTable(
+                context, Se.prev["formsemestre_id"]
+            )  # > get_ues, get_etud_ue_status, get_etud_moy_gen, get_etud_decision_sem
+            for ue in ntp.get_ues(filter_sport=True):
+                ue_status = ntp.get_etud_ue_status(etudid, ue["ue_id"])
+                ue_code_s = (
+                    ue["ue_code"] + "_%s" % ntp.sem["semestre_id"]
+                )  # code indentifiant l'UE
+                prev_moy_ue[ue_code_s][etudid] = ue_status["moy"]
+                #                prev_ue_acro[ue_code_s] = (ue['numero'], ue['acronyme'])
+                prev_ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"])
+            prev_moy[etudid] = ntp.get_etud_moy_gen(etudid)
+            prev_decision = ntp.get_etud_decision_sem(etudid)
+            if prev_decision:
+                prev_code[etudid] = prev_decision["code"]
+                if prev_decision["compense_formsemestre_id"]:
+                    prev_code[etudid] += "+"  # indique qu'il a servi a compenser
+
+        moy[etudid] = nt.get_etud_moy_gen(etudid)
+        for ue in nt.get_ues(filter_sport=True):
+            ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
+            ue_code_s = ue["ue_code"] + "_%s" % nt.sem["semestre_id"]
+            moy_ue[ue_code_s][etudid] = ue_status["moy"]
+            #            ue_acro[ue_code_s] = (ue['numero'], ue['acronyme'])
+            ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"])
+
+        if Se.prev:
+            try:
+                moy_inter[etudid] = (moy[etudid] + prev_moy[etudid]) / 2.0
+            except:
+                pass
+
+        decision = nt.get_etud_decision_sem(etudid)
+        if decision:
+            code[etudid] = decision["code"]
+            if decision["compense_formsemestre_id"]:
+                code[etudid] += "+"  # indique qu'il a servi a compenser
+            assidu[etudid] = {0: "Non", 1: "Oui"}.get(decision["assidu"], "")
+        aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription(
+            context, etudid, formsemestre_id
+        )
+        autorisations[etudid] = ", ".join(["S%s" % x["semestre_id"] for x in aut_list])
+        # parcours:
+        parcours[etudid] = Se.get_parcours_descr()
+        # groupe principal (td)
+        groupestd[etudid] = ""
+        for s in etud["sems"]:
+            if s["formsemestre_id"] == formsemestre_id:
+                groupestd[etudid] = etud_groups.get(etudid, {}).get(
+                    main_partition_id, ""
+                )
+        # absences:
+        AbsEtudSem = getAbsSemEtud(context, sem, etudid)
+        nbabs[etudid] = AbsEtudSem.CountAbs()
+        # nbabsjust[etudid] = AbsEtudSem.CountAbsJust()
+        nbabsjust[etudid] = AbsEtudSem.CountAbs() - AbsEtudSem.CountAbsJust()
+
+    # Codes des UE "semestre précédent":
+    ue_prev_codes = prev_moy_ue.keys()
+    ue_prev_codes.sort(
+        lambda x, y, prev_ue_acro=prev_ue_acro: cmp(prev_ue_acro[x], prev_ue_acro[y])
+    )
+    # Codes des UE "semestre courant":
+    ue_codes = moy_ue.keys()
+    ue_codes.sort(lambda x, y, ue_acro=ue_acro: cmp(ue_acro[x], ue_acro[y]))
+
+    sid = sem["semestre_id"]
+    sn = sp = ""
+    if sid >= 0:
+        sn = "S%s" % sid
+        if prev_moy:  # si qq chose dans precedent
+            sp = "S%s" % (sid - 1)
+
+    L = sco_excel.ScoExcelSheet(sheet_name="Prepa Jury %s" % sn)
+    L.append(["Feuille préparation Jury %s" % unescape_html(sem["titreannee"])])
+    L.append([])  # empty line
+
+    titles = ["Rang"]
+    if context.get_preference("prepa_jury_nip"):
+        titles.append("NIP")
+    if context.get_preference("prepa_jury_ine"):
+        titles.append("INE")
+    titles += [
+        "etudid",
+        "Civ.",
+        "Nom",
+        "Prénom",
+        "Naissance",
+        "Bac",
+        "Spe",
+        "Rg Adm",
+        "Parcours",
+        "Groupe",
+    ]
+
+    if prev_moy:  # si qq chose dans precedent
+        titles += [prev_ue_acro[x][1] for x in ue_prev_codes] + [
+            "Moy %s" % sp,
+            "Décision %s" % sp,
+        ]
+    titles += [ue_acro[x][1] for x in ue_codes] + ["Moy %s" % sn]
+    if moy_inter:
+        titles += ["Moy %s-%s" % (sp, sn)]
+    titles += ["Abs", "Abs Injust."]
+    if code:
+        titles.append("Proposit. %s" % sn)
+    if autorisations:
+        titles.append("Autorisations")
+    #    titles.append('Assidu')
+    L.append(titles)
+    style_bold = sco_excel.Excel_MakeStyle(bold=True)
+    style_center = sco_excel.Excel_MakeStyle(halign="center")
+    style_boldcenter = sco_excel.Excel_MakeStyle(bold=True, halign="center")
+    style_moy = sco_excel.Excel_MakeStyle(
+        bold=True, halign="center", bgcolor="lightyellow"
+    )
+    style_note = sco_excel.Excel_MakeStyle(halign="right")
+    style_note_bold = sco_excel.Excel_MakeStyle(halign="right", bold=True)
+    if prev_moy:
+        tit_prev_moy = "Moy " + sp
+        col_prev_moy = titles.index(tit_prev_moy)
+    tit_moy = "Moy " + sn
+    col_moy = titles.index(tit_moy)
+    col_abs = titles.index("Abs")
+
+    L.set_style(style_bold, li=0)
+    L.set_style(style_boldcenter, li=2)
+
+    def fmt(x):
+        "reduit les notes a deux chiffres"
+        x = notes_table.fmt_note(x, keep_numeric=False)
+        try:
+            return float(x)
+        except:
+            return x
+
+    i = 1  # numero etudiant
+    for etudid in etudids:
+        etud = nt.identdict[etudid]
+        l = [str(i)]
+        if context.get_preference("prepa_jury_nip"):
+            l.append(etud["code_nip"])
+        if context.get_preference("prepa_jury_ine"):
+            l.append(etud["code_ine"])
+        l += [
+            etudid,
+            format_sexe(etud["sexe"]),
+            format_nom(etud["nom"]),
+            format_prenom(etud["prenom"]),
+            etud["date_naissance"],
+            etud["bac"],
+            etud["specialite"],
+            etud["classement"],
+            parcours[etudid],
+            groupestd[etudid],
+        ]
+        co = len(l)
+        if prev_moy:
+            for ue_acro in ue_prev_codes:
+                l.append(fmt(prev_moy_ue.get(ue_acro, {}).get(etudid, "")))
+                L.set_style(style_note, li=i + 2, co=co)
+                co += 1
+            l.append(fmt(prev_moy.get(etudid, "")))
+            l.append(prev_code.get(etudid, ""))
+            #            L.set_style(style_bold, li=i+2, co=col_prev_moy+1) # moy gen prev
+            #            L.set_style(style_moy,  li=i+2, co=col_prev_moy+2) # decision prev
+            L.set_style(style_bold, li=i + 2, co=col_prev_moy)  # moy gen prev
+            L.set_style(style_moy, li=i + 2, co=col_prev_moy + 1)  # decision prev
+            co += 2
+
+        for ue_acro in ue_codes:
+            l.append(fmt(moy_ue.get(ue_acro, {}).get(etudid, "")))
+            L.set_style(style_note, li=i + 2, co=co)
+            co += 1
+        l.append(fmt(moy.get(etudid, "")))
+        #        L.set_style(style_note_bold, li=i+2, co=col_moy+1) # moy gen
+        L.set_style(style_note_bold, li=i + 2, co=col_moy)  # moy gen
+        co += 1
+        if moy_inter:
+            l.append(fmt(moy_inter.get(etudid, "")))
+            L.set_style(style_note, li=i + 2, co=co)
+        l.append(fmt(str(nbabs.get(etudid, ""))))
+        l.append(fmt(str(nbabsjust.get(etudid, ""))))
+        if code:
+            l.append(code.get(etudid, ""))
+        if autorisations:
+            l.append(autorisations.get(etudid, ""))
+        #        l.append(assidu.get(etudid, ''))
+        L.append(l)
+        i += 1
+        L.set_style(style_center, li=i + 1, co=col_abs)  # absences
+        L.set_style(style_center, li=i + 1, co=col_abs + 1)  # absences injustifiées
+        L.set_style(style_moy, li=i + 1, co=col_abs + 2)  # décision semestre
+        L.set_style(style_center, li=i + 1, co=col_abs + 3)  # Autorisations
+    #
+    L.append([""])
+    # Explications des codes
+    codes = sco_codes_parcours.CODES_EXPL.keys()
+    codes.sort()
+    L.append(["Explication des codes"])
+    for code in codes:
+        L.append(["", "", "", code, sco_codes_parcours.CODES_EXPL[code]])
+    L.append(
+        [
+            "",
+            "",
+            "",
+            "ADM+",
+            "indique que le semestre a déjà servi à en compenser un autre",
+        ]
+    )
+    # UE : Correspondances acronyme et titre complet
+    L.append([""])
+    L.append(["Titre des UE"])
+    if prev_moy:
+        for ue in ntp.get_ues(filter_sport=True):
+            L.append(["", "", "", ue["acronyme"], ue["titre"]])
+    for ue in nt.get_ues(filter_sport=True):
+        L.append(["", "", "", ue["acronyme"], ue["titre"]])
+    #
+    L.append([""])
+    L.append(
+        [
+            "Préparé par %s le %s sur %s pour %s"
+            % (
+                VERSION.SCONAME,
+                time.strftime("%d/%m/%Y"),
+                REQUEST.BASE0,
+                REQUEST.AUTHENTICATED_USER,
+            )
+        ]
+    )
+
+    xls = L.gen_workbook()
+
+    return sco_excel.sendExcelFile(REQUEST, xls, "PrepaJury%s.xls" % sn)
diff --git a/sco_pvjury.py b/sco_pvjury.py
new file mode 100644
index 0000000000000000000000000000000000000000..19955cb11246c37e423e9e0e09a6d33950adff5f
--- /dev/null
+++ b/sco_pvjury.py
@@ -0,0 +1,893 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Edition des PV de jury
+"""
+
+import scolars
+import sco_formsemestre
+import sco_groups
+import sco_groups_view
+import sco_parcours_dut
+import sco_codes_parcours
+from sco_codes_parcours import NO_SEMESTRE_ID
+import sco_excel
+from notesdb import *
+from sco_utils import *
+from gen_tables import GenTable
+import sco_pvpdf
+from sco_pdf import *
+
+"""PV Jury IUTV 2006: on détaillait 8 cas:
+Jury de semestre n
+    On a 8 types de décisions:
+    Passages:
+    1. passage de ceux qui ont validés Sn-1
+    2. passage avec compensation Sn-1, Sn
+    3. passage sans validation de Sn avec validation d'UE
+    4. passage sans validation de Sn sans validation d'UE
+
+    Redoublements:
+    5. redoublement de Sn-1 et Sn sans validation d'UE pour Sn
+    6. redoublement de Sn-1 et Sn avec validation d'UE pour Sn
+
+    Reports
+    7. report sans validation d'UE
+
+    8. non validation de Sn-1 et Sn et non redoublement
+"""
+
+
+def _descr_decisions_ues(context, nt, etudid, decisions_ue, decision_sem):
+    """Liste des UE validées dans ce semestre
+    """
+    if not decisions_ue:
+        return []
+    uelist = []
+    # Les UE validées dans ce semestre:
+    for ue_id in decisions_ue.keys():
+        try:
+            if decisions_ue[ue_id] and (
+                decisions_ue[ue_id]["code"] == ADM
+                or (
+                    CONFIG.CAPITALIZE_ALL_UES
+                    and sco_codes_parcours.code_semestre_validant(decision_sem["code"])
+                )
+            ):
+                ue = context.do_ue_list(args={"ue_id": ue_id})[0]
+                uelist.append(ue)
+        except:
+            log("descr_decisions_ues: ue_id=%s decisions_ue=%s" % (ue_id, decisions_ue))
+            pass
+    # Les UE capitalisées dans d'autres semestres:
+    for ue in nt.ue_capitalisees[etudid]:
+        try:
+            uelist.append(nt.get_etud_ue_status(etudid, ue["ue_id"])["ue"])
+        except KeyError:
+            pass
+    uelist.sort(lambda x, y: cmp(x["numero"], y["numero"]))
+
+    return uelist
+
+
+def _descr_decision_sem(context, etat, decision_sem):
+    "résumé textuel de la décision de semestre"
+    if etat == "D":
+        decision = "Démission"
+    else:
+        if decision_sem:
+            cod = decision_sem["code"]
+            decision = sco_codes_parcours.CODES_EXPL.get(cod, "")  # + ' (%s)' % cod
+        else:
+            decision = ""
+    return decision
+
+
+def _descr_decision_sem_abbrev(context, etat, decision_sem):
+    "résumé textuel tres court (code) de la décision de semestre"
+    if etat == "D":
+        decision = "Démission"
+    else:
+        if decision_sem:
+            decision = decision_sem["code"]
+        else:
+            decision = ""
+    return decision
+
+
+def descr_autorisations(context, autorisations):
+    "résumé textuel des autorisations d'inscription (-> 'S1, S3' )"
+    alist = []
+    for aut in autorisations:
+        alist.append("S" + str(aut["semestre_id"]))
+    return ", ".join(alist)
+
+
+def _comp_ects_by_ue_code_and_type(nt, decision_ues):
+    """Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées)
+    decision_ues est le resultat de nt.get_etud_decision_ues
+    Chaque resultat est un dict: { ue_code : ects }
+    """
+    if not decision_ues:
+        return {}, {}
+
+    ects_by_ue_code = {}
+    ects_by_ue_type = DictDefault(defaultvalue=0)  # { ue_type : ects validés }
+    for ue_id in decision_ues:
+        d = decision_ues[ue_id]
+        ue = nt.uedict[ue_id]
+        ects_by_ue_code[ue["ue_code"]] = d["ects"]
+        ects_by_ue_type[ue["type"]] += d["ects"]
+
+    return ects_by_ue_code, ects_by_ue_type
+
+
+def _comp_ects_capitalises_by_ue_code(nt, etudid):
+    """Calcul somme des ECTS des UE capitalisees
+    """
+    ues = nt.get_ues()
+    ects_by_ue_code = {}
+    for ue in ues:
+        ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
+        if ue_status["is_capitalized"]:
+            try:
+                ects_val = float(ue_status["ue"]["ects"])
+            except (ValueError, TypeError):
+                ects_val = 0.0
+            ects_by_ue_code[ue["ue_code"]] = ects_val
+
+    return ects_by_ue_code
+
+
+def _sum_ects_dicts(s, t):
+    """Somme deux dictionnaires { ue_code : ects },
+    quand une UE de même code apparait deux fois, prend celle avec le plus d'ECTS.
+    """
+    sum_ects = sum(s.values()) + sum(t.values())
+    for ue_code in set(s).intersection(set(t)):
+        sum_ects -= min(s[ue_code], t[ue_code])
+    return sum_ects
+
+
+def dict_pvjury(
+    context,
+    formsemestre_id,
+    etudids=None,
+    with_prev=False,
+    with_parcours_decisions=False,
+):
+    """Données pour édition jury
+    etudids == None => tous les inscrits, sinon donne la liste des ids
+    Si with_prev: ajoute infos sur code jury semestre precedent
+    Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours
+    Résultat:
+    {
+    'date' : date de la decision la plus recente,
+    'formsemestre' : sem,
+    'formation' : { 'acronyme' :, 'titre': ... }
+    'decisions' : { [ { 'identite' : {'nom' :, 'prenom':,  ...,},
+                        'etat' : I ou D ou DEF
+                        'decision_sem' : {'code':, 'code_prev': },
+                        'decisions_ue' : {  ue_id : { 'code' : ADM|CMP|AJ, 'event_date' :,
+                                             'acronyme', 'numero': } },
+                        'autorisations' : [ { 'semestre_id' : { ... } } ],
+                        'validation_parcours' : True si parcours validé (diplome obtenu)
+                        'prev_code' : code (calculé slt si with_prev),
+                        'mention' : mention (en fct moy gen),
+                        'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
+                        'sum_ects_capitalises' : somme des ECTS des UE capitalisees
+                    }
+                    ]
+                  },
+     'decisions_dict' : { etudid : decision (comme ci-dessous) },
+    }    
+    """
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etudids, get_etud_etat, get_etud_decision_sem, get_etud_decision_ues
+    if etudids is None:
+        etudids = nt.get_etudids()
+    if not etudids:
+        return {}
+    cnx = context.GetDBConnexion()
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    max_date = "0000-01-01"
+    has_prev = False  # vrai si au moins un etudiant a un code prev
+    semestre_non_terminal = False  # True si au moins un etudiant a un devenir
+
+    L = []
+    D = {}  # même chose que L, mais { etudid : dec }
+    for etudid in etudids:
+        etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+        Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id)
+        semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal
+        d = {}
+        d["identite"] = nt.identdict[etudid]
+        d["etat"] = nt.get_etud_etat(
+            etudid
+        )  # I|D|DEF  (inscription ou démission ou défaillant)
+        d["decision_sem"] = nt.get_etud_decision_sem(etudid)
+        d["decisions_ue"] = nt.get_etud_decision_ues(etudid)
+        d["last_formsemestre_id"] = Se.get_semestres()[
+            -1
+        ]  # id du dernier semestre (chronologiquement) dans lequel il a été inscrit
+
+        ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid)
+        d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values())
+        ects_by_ue_code, ects_by_ue_type = _comp_ects_by_ue_code_and_type(
+            nt, d["decisions_ue"]
+        )
+        d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code)
+        d["sum_ects_by_type"] = ects_by_ue_type
+
+        if d["decision_sem"] and sco_codes_parcours.code_semestre_validant(
+            d["decision_sem"]["code"]
+        ):
+            d["mention"] = get_mention(nt.get_etud_moy_gen(etudid))
+        else:
+            d["mention"] = ""
+        # Versions "en français": (avec les UE capitalisées d'ailleurs)
+        dec_ue_list = _descr_decisions_ues(
+            context, nt, etudid, d["decisions_ue"], d["decision_sem"]
+        )
+        d["decisions_ue_nb"] = len(
+            dec_ue_list
+        )  # avec les UE capitalisées, donc des éventuels doublons
+        # Mais sur la description (eg sur les bulletins), on ne veut pas
+        # afficher ces doublons: on uniquifie sur ue_code
+        _codes = set()
+        ue_uniq = []
+        for ue in dec_ue_list:
+            if ue["ue_code"] not in _codes:
+                ue_uniq.append(ue)
+                _codes.add(ue["ue_code"])
+
+        d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq])
+        d["decision_sem_descr"] = _descr_decision_sem(
+            context, d["etat"], d["decision_sem"]
+        )
+
+        d["autorisations"] = sco_parcours_dut.formsemestre_get_autorisation_inscription(
+            context, etudid, formsemestre_id
+        )
+        d["autorisations_descr"] = descr_autorisations(context, d["autorisations"])
+
+        d["validation_parcours"] = Se.parcours_validated()
+        d["parcours"] = Se.get_parcours_descr(filter_futur=True)
+        if with_parcours_decisions:
+            d["parcours_decisions"] = Se.get_parcours_decisions()
+        # Observations sur les compensations:
+        compensators = sco_parcours_dut.scolar_formsemestre_validation_list(
+            cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid}
+        )
+        obs = []
+        for compensator in compensators:
+            # nb: il ne devrait y en avoir qu'un !
+            csem = sco_formsemestre.get_formsemestre(
+                context, compensator["formsemestre_id"]
+            )
+            obs.append(
+                "%s compensé par %s (%s)"
+                % (sem["sem_id_txt"], csem["sem_id_txt"], csem["anneescolaire"])
+            )
+
+        if d["decision_sem"] and d["decision_sem"]["compense_formsemestre_id"]:
+            compensed = sco_formsemestre.get_formsemestre(
+                context, d["decision_sem"]["compense_formsemestre_id"]
+            )
+            obs.append(
+                "%s compense %s (%s)"
+                % (
+                    sem["sem_id_txt"],
+                    compensed["sem_id_txt"],
+                    compensed["anneescolaire"],
+                )
+            )
+
+        d["observation"] = ", ".join(obs)
+
+        # Cherche la date de decision (sem ou UE) la plus récente:
+        if d["decision_sem"]:
+            date = DateDMYtoISO(d["decision_sem"]["event_date"])
+            if date > max_date:  # decision plus recente
+                max_date = date
+        if d["decisions_ue"]:
+            for dec_ue in d["decisions_ue"].values():
+                if dec_ue:
+                    date = DateDMYtoISO(dec_ue["event_date"])
+                    if date > max_date:  # decision plus recente
+                        max_date = date
+        # Code semestre precedent
+        if with_prev:  # optionnel car un peu long...
+            info = context.getEtudInfo(etudid=etudid, filled=True)
+            if not info:
+                continue  # should not occur
+            etud = info[0]
+            if Se.prev and Se.prev_decision:
+                d["prev_decision_sem"] = Se.prev_decision
+                d["prev_code"] = Se.prev_decision["code"]
+                d["prev_code_descr"] = _descr_decision_sem(
+                    context, "I", Se.prev_decision
+                )
+                d["prev"] = Se.prev
+                has_prev = True
+            else:
+                d["prev_decision_sem"] = None
+                d["prev_code"] = ""
+                d["prev_code_descr"] = ""
+            d["Se"] = Se
+
+        L.append(d)
+        D[etudid] = d
+
+    return {
+        "date": DateISOtoDMY(max_date),
+        "formsemestre": sem,
+        "has_prev": has_prev,
+        "semestre_non_terminal": semestre_non_terminal,
+        "formation": context.formation_list(args={"formation_id": sem["formation_id"]})[
+            0
+        ],
+        "decisions": L,
+        "decisions_dict": D,
+    }
+
+
+def pvjury_table(
+    context,
+    dpv,
+    only_diplome=False,
+    anonymous=False,
+    with_parcours_decisions=False,
+    with_paragraph_nom=False,  # cellule paragraphe avec nom, date, code NIP
+):
+    """idem mais rend list de dicts
+    Si only_diplome, n'extrait que les etudiants qui valident leur diplome.
+    """
+    sem = dpv["formsemestre"]
+    formsemestre_id = sem["formsemestre_id"]
+    sem_id_txt_sp = sem["sem_id_txt"]
+    if sem_id_txt_sp:
+        sem_id_txt_sp = " " + sem_id_txt_sp
+    titles = {
+        "etudid": "etudid",
+        "code_nip": "NIP",
+        "nomprenom": "Nom",  # si with_paragraph_nom, sera un Paragraph
+        "parcours": "Parcours",
+        "decision": "Décision" + sem_id_txt_sp,
+        "mention": "Mention",
+        "ue_cap": "UE" + sem_id_txt_sp + " capitalisées",
+        "ects": "ECTS",
+        "devenir": "Devenir",
+        "observations": "Observations",
+    }
+    if anonymous:
+        titles["nomprenom"] = "Code"
+    columns_ids = ["nomprenom", "parcours"]
+
+    if with_parcours_decisions:
+        all_idx = set()
+        for e in dpv["decisions"]:
+            all_idx |= set(e["parcours_decisions"].keys())
+        sem_ids = sorted(all_idx)
+        for i in sem_ids:
+            if i != NO_SEMESTRE_ID:
+                titles[i] = "S%d" % i
+            else:
+                titles[i] = "S"  # pas très parlant ?
+            columns_ids += [i]
+
+    if dpv["has_prev"]:
+        id_prev = sem["semestre_id"] - 1  # numero du semestre precedent
+        titles["prev_decision"] = "Décision S%s" % id_prev
+        columns_ids += ["prev_decision"]
+
+    columns_ids += ["decision"]
+    if context.get_preference("bul_show_mention", formsemestre_id):
+        columns_ids += ["mention"]
+    columns_ids += ["ue_cap"]
+    if context.get_preference("bul_show_ects", formsemestre_id):
+        columns_ids += ["ects"]
+    # if dpv['semestre_non_terminal']:
+    # dec 2017: indique toujours le devenir ("diplôme obtenu" ou semestre suivant)
+    columns_ids += ["devenir"]
+    columns_ids += ["observations"]
+
+    lines = []
+    for e in dpv["decisions"]:
+        scolars.format_etud_ident(e["identite"])
+        l = {
+            "etudid": e["identite"]["etudid"],
+            "code_nip": e["identite"]["code_nip"],
+            "nomprenom": e["identite"]["nomprenom"],
+            "_nomprenom_target": "%s/ficheEtud?etudid=%s"
+            % (context.ScoURL(), e["identite"]["etudid"]),
+            "_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % e["identite"]["etudid"],
+            "parcours": e["parcours"],
+            "decision": _descr_decision_sem_abbrev(
+                context, e["etat"], e["decision_sem"]
+            ),
+            "ue_cap": e["decisions_ue_descr"],
+            "devenir": e["autorisations_descr"],
+            "observations": unquote(e["observation"]),
+            "mention": e["mention"],
+            "ects": str(e["sum_ects"]),
+        }
+        if with_paragraph_nom:
+            cell_style = styles.ParagraphStyle({})
+            cell_style.fontSize = context.get_preference(
+                "SCOLAR_FONT_SIZE", formsemestre_id
+            )
+            cell_style.fontName = context.get_preference("PV_FONTNAME", formsemestre_id)
+            cell_style.leading = 1.0 * context.get_preference(
+                "SCOLAR_FONT_SIZE", formsemestre_id
+            )  # vertical space
+            i = e["identite"]
+            l["nomprenom"] = [
+                Paragraph(SU(i["nomprenom"]), cell_style),
+                Paragraph(SU(i["code_nip"]), cell_style),
+                Paragraph(
+                    SU(
+                        "Né le %s" % i["date_naissance"]
+                        + (" à %s" % i["lieu_naissance"] if i["lieu_naissance"] else "")
+                        + (" (%s)" % i["dept_naissance"] if i["dept_naissance"] else "")
+                    ),
+                    cell_style,
+                ),
+            ]
+        if anonymous:
+            # Mode anonyme: affiche INE ou sinon NIP, ou id
+            l["nomprenom"] = (
+                e["identite"]["code_ine"]
+                or e["identite"]["code_nip"]
+                or e["identite"]["etudid"]
+            )
+        if with_parcours_decisions:
+            for i in e[
+                "parcours_decisions"
+            ]:  # or equivalently: l.update(e['parcours_decisions'])
+                l[i] = e["parcours_decisions"][i]
+
+        if e["validation_parcours"]:
+            l["devenir"] = "Diplôme obtenu"
+        if dpv["has_prev"]:
+            l["prev_decision"] = _descr_decision_sem_abbrev(
+                context, None, e["prev_decision_sem"]
+            )
+        if e["validation_parcours"] or not only_diplome:
+            lines.append(l)
+    return lines, titles, columns_ids
+
+
+def formsemestre_pvjury(
+    context, formsemestre_id, format="html", publish=True, REQUEST=None
+):
+    """Page récapitulant les décisions de jury
+    dpv: result of dict_pvjury
+    """
+    footer = context.sco_footer(REQUEST)
+
+    dpv = dict_pvjury(context, formsemestre_id, with_prev=True)
+    if not dpv:
+        if format == "html":
+            return (
+                context.sco_header(REQUEST)
+                + "<h2>Aucune information disponible !</h2>"
+                + footer
+            )
+        else:
+            return None
+    sem = dpv["formsemestre"]
+    formsemestre_id = sem["formsemestre_id"]
+
+    rows, titles, columns_ids = pvjury_table(context, dpv)
+    if format != "html" and format != "pdf":
+        columns_ids = ["etudid", "code_nip"] + columns_ids
+
+    tab = GenTable(
+        rows=rows,
+        titles=titles,
+        columns_ids=columns_ids,
+        filename=make_filename("decisions " + sem["titreannee"]),
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        caption="Décisions jury pour " + sem["titreannee"],
+        html_class="table_leftalign",
+        html_sortable=True,
+        preferences=context.get_preferences(formsemestre_id),
+    )
+    if format != "html":
+        return tab.make_page(
+            context,
+            format=format,
+            with_html_headers=False,
+            REQUEST=REQUEST,
+            publish=publish,
+        )
+    tab.base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id)
+    H = [
+        context.html_sem_header(
+            REQUEST,
+            "Décisions du jury pour le semestre",
+            sem,
+            init_qtip=True,
+            javascripts=["js/etud_info.js"],
+        ),
+        """<p>(dernière modif le %s)</p>""" % dpv["date"],
+    ]
+
+    H.append(
+        '<ul><li><a class="stdlink" href="formsemestre_lettres_individuelles?formsemestre_id=%s">Courriers individuels (classeur pdf)</a></li>'
+        % formsemestre_id
+    )
+    H.append(
+        '<li><a class="stdlink" href="formsemestre_pvjury_pdf?formsemestre_id=%s">PV officiel (pdf)</a></li></ul>'
+        % formsemestre_id
+    )
+
+    H.append(tab.html())
+
+    # Count number of cases for each decision
+    counts = DictDefault()
+    for row in rows:
+        counts[row["decision"]] += 1
+        # add codes for previous (for explanation, without count)
+        if row.has_key("prev_decision") and row["prev_decision"]:
+            counts[row["prev_decision"]] += 0
+    # Légende des codes
+    codes = counts.keys()  # sco_codes_parcours.CODES_EXPL.keys()
+    codes.sort()
+    H.append("<h3>Explication des codes</h3>")
+    lines = []
+    for code in codes:
+        lines.append(
+            {
+                "code": code,
+                "count": counts[code],
+                "expl": sco_codes_parcours.CODES_EXPL.get(code, ""),
+            }
+        )
+
+    H.append(
+        GenTable(
+            rows=lines,
+            titles={"code": "Code", "count": "Nombre", "expl": ""},
+            columns_ids=("code", "count", "expl"),
+            html_class="table_leftalign",
+            html_sortable=True,
+            preferences=context.get_preferences(formsemestre_id),
+        ).html()
+    )
+    H.append("<p></p>")  # force space at bottom
+    return "\n".join(H) + footer
+
+
+# ---------------------------------------------------------------------------
+
+
+def formsemestre_pvjury_pdf(
+    context, formsemestre_id, group_ids=[], etudid=None, REQUEST=None
+):
+    """Generation PV jury en PDF: saisie des paramètres
+    Si etudid, PV pour un seul etudiant. Sinon, tout les inscrits au groupe indiqué.
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    groups_infos = None
+    if etudid:
+        # PV pour ce seul étudiant:
+        etud = context.getEtudInfo(etudid=etudid, filled=1)[0]
+        etuddescr = '<a class="discretelink" href="ficheEtud?etudid=%s">%s</a>' % (
+            etudid,
+            etud["nomprenom"],
+        )
+        etudids = [etudid]
+    else:
+        etuddescr = ""
+        if not group_ids:
+            # tous les inscrits du semestre
+            group_ids = [sco_groups.get_default_group(context, formsemestre_id)]
+
+        groups_infos = sco_groups_view.DisplayedGroupsInfos(
+            context, group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST
+        )
+        etudids = [m["etudid"] for m in groups_infos.members]
+
+    H = [
+        context.html_sem_header(
+            REQUEST,
+            "Edition du PV de jury %s" % etuddescr,
+            sem=sem,
+            javascripts=sco_groups_view.JAVASCRIPTS,
+            cssstyles=sco_groups_view.CSSSTYLES,
+            init_qtip=True,
+        ),
+        """<p class="help">Utiliser cette page pour éditer des versions provisoires des PV.
+          <span class="fontred">Il est recommandé d'archiver les versions définitives: <a href="formsemestre_archive?formsemestre_id=%s">voir cette page</a></span>
+          </p>"""
+        % formsemestre_id,
+    ]
+    F = [
+        """<p><em>Voir aussi si besoin les réglages sur la page "Paramétrage" (accessible à l'administrateur du département).</em>
+        </p>""",
+        context.sco_footer(REQUEST),
+    ]
+    descr = descrform_pvjury(sem)
+    if etudid:
+        descr.append(("etudid", {"input_type": "hidden"}))
+
+    if groups_infos:
+        menu_choix_groupe = (
+            """<div class="group_ids_sel_menu">Groupes d'étudiants à lister sur le PV: """
+            + sco_groups_view.menu_groups_choice(context, groups_infos)
+            + """</div>"""
+        )
+    else:
+        menu_choix_groupe = ""  # un seul etudiant à editer
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        cancelbutton="Annuler",
+        method="get",
+        submitlabel="Générer document",
+        name="tf",
+        formid="group_selector",
+        html_foot_markup=menu_choix_groupe,
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + "\n" + tf[1] + "\n".join(F)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(
+            "formsemestre_pvjury?formsemestre_id=%s" % (formsemestre_id)
+        )
+    else:
+        # submit
+        dpv = dict_pvjury(context, formsemestre_id, etudids=etudids, with_prev=True)
+        if tf[2]["showTitle"]:
+            tf[2]["showTitle"] = True
+        else:
+            tf[2]["showTitle"] = False
+        if tf[2]["with_paragraph_nom"]:
+            tf[2]["with_paragraph_nom"] = True
+        else:
+            tf[2]["with_paragraph_nom"] = False
+        if tf[2]["anonymous"]:
+            tf[2]["anonymous"] = True
+        else:
+            tf[2]["anonymous"] = False
+        try:
+            PDFLOCK.acquire()
+            pdfdoc = sco_pvpdf.pvjury_pdf(
+                context,
+                dpv,
+                REQUEST,
+                numeroArrete=tf[2]["numeroArrete"],
+                VDICode=tf[2]["VDICode"],
+                date_commission=tf[2]["date_commission"],
+                date_jury=tf[2]["date_jury"],
+                showTitle=tf[2]["showTitle"],
+                with_paragraph_nom=tf[2]["with_paragraph_nom"],
+                anonymous=tf[2]["anonymous"],
+            )
+        finally:
+            PDFLOCK.release()
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        dt = time.strftime("%Y-%m-%d")
+        if groups_infos:
+            groups_filename = "-" + groups_infos.groups_filename
+        else:
+            groups_filename = ""
+        filename = "PV-%s%s-%s.pdf" % (sem["titre_num"], groups_filename, dt)
+        return sendPDFFile(REQUEST, pdfdoc, filename)
+
+
+def descrform_pvjury(sem):
+    """Définition de formulaire pour PV jury PDF
+    """
+    return [
+        (
+            "date_commission",
+            {
+                "input_type": "text",
+                "size": 50,
+                "title": "Date de la commission",
+                "explanation": "(format libre)",
+            },
+        ),
+        (
+            "date_jury",
+            {
+                "input_type": "text",
+                "size": 50,
+                "title": "Date du Jury",
+                "explanation": "(si le jury a eu lieu)",
+            },
+        ),
+        (
+            "numeroArrete",
+            {
+                "input_type": "text",
+                "size": 50,
+                "title": "Numéro de l'arrêté du président",
+                "explanation": "le président de l'Université prend chaque année un arrêté formant les jurys",
+            },
+        ),
+        (
+            "VDICode",
+            {
+                "input_type": "text",
+                "size": 15,
+                "title": "VDI et Code",
+                "explanation": "VDI et code du diplôme Apogée (format libre, n'est pas vérifié par ScoDoc)",
+            },
+        ),
+        (
+            "showTitle",
+            {
+                "input_type": "checkbox",
+                "title": "Indiquer le titre du semestre sur le PV",
+                "explanation": '(le titre est "%s")' % sem["titre"],
+                "labels": [""],
+                "allowed_values": ("1",),
+            },
+        ),
+        (
+            "with_paragraph_nom",
+            {
+                "input_type": "checkbox",
+                "title": "Avec date naissance et code",
+                "explanation": "ajoute informations sous le nom",
+                "default": "1",
+                "labels": [""],
+                "allowed_values": ("1",),
+            },
+        ),
+        (
+            "anonymous",
+            {
+                "input_type": "checkbox",
+                "title": "PV anonyme",
+                "explanation": "remplace nom par code étudiant (INE ou NIP)",
+                "labels": [""],
+                "allowed_values": ("1",),
+            },
+        ),
+        ("formsemestre_id", {"input_type": "hidden"}),
+    ]
+
+
+def formsemestre_lettres_individuelles(
+    context, formsemestre_id, group_ids=[], REQUEST=None
+):
+    "Lettres avis jury en PDF"
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    if not group_ids:
+        # tous les inscrits du semestre
+        group_ids = [sco_groups.get_default_group(context, formsemestre_id)]
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST
+    )
+    etudids = [m["etudid"] for m in groups_infos.members]
+
+    H = [
+        context.html_sem_header(
+            REQUEST,
+            "Edition des lettres individuelles",
+            sem=sem,
+            javascripts=sco_groups_view.JAVASCRIPTS,
+            cssstyles=sco_groups_view.CSSSTYLES,
+            init_qtip=True,
+        ),
+        """<p class="help">Utiliser cette page pour éditer des versions provisoires des PV.
+          <span class="fontred">Il est recommandé d'archiver les versions définitives: <a href="formsemestre_archive?formsemestre_id=%s">voir cette page</a></span></p>
+         """
+        % formsemestre_id,
+    ]
+    F = context.sco_footer(REQUEST)
+    descr = descrform_lettres_individuelles()
+    menu_choix_groupe = (
+        """<div class="group_ids_sel_menu">Groupes d'étudiants à lister: """
+        + sco_groups_view.menu_groups_choice(context, groups_infos)
+        + """</div>"""
+    )
+
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        descr,
+        cancelbutton="Annuler",
+        method="POST",
+        submitlabel="Générer document",
+        name="tf",
+        formid="group_selector",
+        html_foot_markup=menu_choix_groupe,
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + "\n" + tf[1] + F
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(
+            "formsemestre_pvjury?formsemestre_id=%s" % (formsemestre_id)
+        )
+    else:
+        # submit
+        sf = tf[2]["signature"]
+        # pdb.set_trace()
+        signature = sf.read()  # image of signature
+        try:
+            PDFLOCK.acquire()
+            pdfdoc = sco_pvpdf.pdf_lettres_individuelles(
+                context,
+                formsemestre_id,
+                etudids=etudids,
+                date_jury=tf[2]["date_jury"],
+                date_commission=tf[2]["date_commission"],
+                signature=signature,
+            )
+        finally:
+            PDFLOCK.release()
+        if not pdfdoc:
+            return REQUEST.RESPONSE.redirect(
+                "formsemestre_status?formsemestre_id={}&amp;head_message=Aucun%20%C3%A9tudiant%20n%27a%20de%20d%C3%A9cision%20de%20jury".format(
+                    formsemestre_id
+                )
+            )
+        sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+        dt = time.strftime("%Y-%m-%d")
+        groups_filename = "-" + groups_infos.groups_filename
+        filename = "lettres-%s%s-%s.pdf" % (sem["titre_num"], groups_filename, dt)
+        return sendPDFFile(REQUEST, pdfdoc, filename)
+
+
+def descrform_lettres_individuelles():
+    return [
+        (
+            "date_commission",
+            {
+                "input_type": "text",
+                "size": 50,
+                "title": "Date de la commission",
+                "explanation": "(format libre)",
+            },
+        ),
+        (
+            "date_jury",
+            {
+                "input_type": "text",
+                "size": 50,
+                "title": "Date du Jury",
+                "explanation": "(si le jury a eu lieu)",
+            },
+        ),
+        (
+            "signature",
+            {
+                "input_type": "file",
+                "size": 30,
+                "explanation": "optionnel: image scannée de la signature",
+            },
+        ),
+        ("formsemestre_id", {"input_type": "hidden"}),
+    ]
diff --git a/sco_pvpdf.py b/sco_pvpdf.py
new file mode 100644
index 0000000000000000000000000000000000000000..92531c4c652be1184ffee6a914ff90e0fc611b12
--- /dev/null
+++ b/sco_pvpdf.py
@@ -0,0 +1,806 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Edition des PV de jury
+"""
+
+import sco_formsemestre
+from sco_pdf import *
+import sco_pvjury
+import sco_codes_parcours
+from sco_utils import *
+from sco_pdf import PDFLOCK
+import sco_preferences
+import sco_bulletins_pdf
+
+import os
+import types
+
+LOGO_FOOTER_ASPECT = CONFIG.LOGO_FOOTER_ASPECT  # XXX A AUTOMATISER
+LOGO_FOOTER_HEIGHT = CONFIG.LOGO_FOOTER_HEIGHT * mm
+LOGO_FOOTER_WIDTH = LOGO_FOOTER_HEIGHT * CONFIG.LOGO_FOOTER_ASPECT
+
+LOGO_HEADER_ASPECT = CONFIG.LOGO_HEADER_ASPECT  # XXX logo IUTV (A AUTOMATISER)
+LOGO_HEADER_HEIGHT = CONFIG.LOGO_HEADER_HEIGHT * mm
+LOGO_HEADER_WIDTH = LOGO_HEADER_HEIGHT * CONFIG.LOGO_HEADER_ASPECT
+
+
+def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True):
+    "Add footer on page"
+    width = doc.pagesize[0]  # - doc.pageTemplate.left_p - doc.pageTemplate.right_p
+    foot = Frame(
+        0.1 * mm,
+        0.2 * cm,
+        width - 1 * mm,
+        2 * cm,
+        leftPadding=0,
+        rightPadding=0,
+        topPadding=0,
+        bottomPadding=0,
+        id="monfooter",
+        showBoundary=0,
+    )
+
+    LeftFootStyle = reportlab.lib.styles.ParagraphStyle({})
+    LeftFootStyle.fontName = preferences["SCOLAR_FONT"]
+    LeftFootStyle.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
+    LeftFootStyle.leftIndent = 0
+    LeftFootStyle.firstLineIndent = 0
+    LeftFootStyle.alignment = TA_RIGHT
+    RightFootStyle = reportlab.lib.styles.ParagraphStyle({})
+    RightFootStyle.fontName = preferences["SCOLAR_FONT"]
+    RightFootStyle.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
+    RightFootStyle.alignment = TA_RIGHT
+
+    p = makeParas(
+        """<para>%s</para><para>%s</para>"""
+        % (preferences["INSTITUTION_NAME"], preferences["INSTITUTION_ADDRESS"]),
+        LeftFootStyle,
+    )
+
+    np = Paragraph('<para fontSize="14">%d</para>' % doc.page, RightFootStyle)
+    tabstyle = TableStyle(
+        [
+            ("LEFTPADDING", (0, 0), (-1, -1), 0),
+            ("RIGHTPADDING", (0, 0), (-1, -1), 0),
+            ("ALIGN", (0, 0), (-1, -1), "RIGHT"),
+            # ('INNERGRID', (0,0), (-1,-1), 0.25, black),#debug
+            # ('LINEABOVE', (0,0), (-1,0), 0.5, black),
+            ("VALIGN", (1, 0), (1, 0), "MIDDLE"),
+            ("RIGHTPADDING", (-1, 0), (-1, 0), 1 * cm),
+        ]
+    )
+    elems = [p, logo]
+    colWidths = [None, LOGO_FOOTER_WIDTH + 2 * mm]
+    if with_page_numbers:
+        elems.append(np)
+        colWidths.append(2 * cm)
+    else:
+        elems.append("")
+        colWidths.append(8 * mm)  # force marge droite
+    tab = Table([elems], style=tabstyle, colWidths=colWidths)
+    canvas.saveState()  # is it necessary ?
+    foot.addFromList([tab], canvas)
+    canvas.restoreState()
+
+
+def pageHeader(canvas, doc, logo, preferences, only_on_first_page=False):
+    if only_on_first_page and int(doc.page) > 1:
+        return
+    height = doc.pagesize[1]
+    head = Frame(
+        -22 * mm,
+        height - 13 * mm - LOGO_HEADER_HEIGHT,
+        10 * cm,
+        LOGO_HEADER_HEIGHT + 2 * mm,
+        leftPadding=0,
+        rightPadding=0,
+        topPadding=0,
+        bottomPadding=0,
+        id="monheader",
+        showBoundary=0,
+    )
+    canvas.saveState()  # is it necessary ?
+    head.addFromList([logo], canvas)
+    canvas.restoreState()
+
+
+class CourrierIndividuelTemplate(PageTemplate):
+    """Template pour courrier avisant des decisions de jury (1 page /etudiant)
+    """
+
+    def __init__(
+        self,
+        document,
+        pagesbookmarks={},
+        author=None,
+        title=None,
+        subject=None,
+        margins=(0, 0, 0, 0),  # additional margins in mm (left,top,right, bottom)
+        image_dir="",
+        preferences=None,  # dictionnary with preferences, required
+        force_header=False,
+        force_footer=False,  # always add a footer (whatever the preferences, use for PV)
+        template_name="CourrierJuryTemplate",
+    ):
+        """Initialise our page template."""
+        self.pagesbookmarks = pagesbookmarks
+        self.pdfmeta_author = author
+        self.pdfmeta_title = title
+        self.pdfmeta_subject = subject
+        self.image_dir = image_dir
+        self.preferences = preferences
+        self.force_header = force_header
+        self.force_footer = force_footer
+        self.with_page_numbers = False
+        self.header_only_on_first_page = False
+        # Our doc is made of a single frame
+        left, top, right, bottom = margins  # marge additionnelle en mm
+        # marges du Frame principal
+        self.bot_p = 2 * cm
+        self.left_p = 2.5 * cm
+        self.right_p = 2.5 * cm
+        self.top_p = 0 * cm
+        # log("margins=%s" % str(margins))
+        content = Frame(
+            self.left_p + left * mm,
+            self.bot_p + bottom * mm,
+            document.pagesize[0] - self.right_p - self.left_p - left * mm - right * mm,
+            document.pagesize[1] - self.top_p - self.bot_p - top * mm - bottom * mm,
+        )
+
+        PageTemplate.__init__(self, template_name, [content])
+
+        self.logo_footer = Image(
+            image_dir + "/logo_footer.jpg",
+            height=LOGO_FOOTER_HEIGHT,
+            width=LOGO_FOOTER_WIDTH,
+        )
+        self.logo_header = Image(
+            image_dir + "/logo_header.jpg",
+            height=LOGO_HEADER_HEIGHT,
+            width=LOGO_HEADER_WIDTH,
+        )
+
+    def beforeDrawPage(self, canvas, doc):
+        """Draws a logo and an contribution message on each page."""
+        # ---- Add some meta data and bookmarks
+        if self.pdfmeta_author:
+            canvas.setAuthor(SU(self.pdfmeta_author))
+        if self.pdfmeta_title:
+            canvas.setTitle(SU(self.pdfmeta_title))
+        if self.pdfmeta_subject:
+            canvas.setSubject(SU(self.pdfmeta_subject))
+        bm = self.pagesbookmarks.get(doc.page, None)
+        if bm != None:
+            key = bm
+            txt = SU(bm)
+            canvas.bookmarkPage(key)
+            canvas.addOutlineEntry(txt, bm)
+        if self.force_footer or self.preferences["PV_LETTER_WITH_HEADER"]:
+            # --- Add header
+            pageHeader(
+                canvas,
+                doc,
+                self.logo_header,
+                self.preferences,
+                self.header_only_on_first_page,
+            )
+        if self.force_footer or self.preferences["PV_LETTER_WITH_FOOTER"]:
+            # --- Add footer
+            pageFooter(
+                canvas,
+                doc,
+                self.logo_footer,
+                self.preferences,
+                with_page_numbers=self.with_page_numbers,
+            )
+
+
+class PVTemplate(CourrierIndividuelTemplate):
+    """Template pour les pages des PV de jury
+    """
+
+    def __init__(
+        self,
+        document,
+        author=None,
+        title=None,
+        subject=None,
+        margins=(0, 23, 0, 5),  # additional margins in mm (left,top,right, bottom)
+        image_dir="",
+        preferences=None,  # dictionnary with preferences, required
+    ):
+        CourrierIndividuelTemplate.__init__(
+            self,
+            document,
+            author=author,
+            title=title,
+            subject=subject,
+            margins=margins,
+            image_dir=image_dir,
+            preferences=preferences,
+            force_header=True,
+            force_footer=True,
+            template_name="PVJuryTemplate",
+        )
+        self.with_page_numbers = True
+        self.header_only_on_first_page = True
+
+    def afterDrawPage(self, canvas, doc):
+        """Called after all flowables have been drawn on a page"""
+        pass
+
+    def beforeDrawPage(self, canvas, doc):
+        """Called before any flowables are drawn on a page"""
+        # If the page number is even, force a page break
+        CourrierIndividuelTemplate.beforeDrawPage(self, canvas, doc)
+        # Note: on cherche un moyen de generer un saut de page double
+        #  (redémarrer sur page impaire, nouvelle feuille en recto/verso). Pas trouvé en Platypus.
+        #
+        # if self.__pageNum % 2 == 0:
+        #    canvas.showPage()
+        #    # Increment pageNum again since we've added a blank page
+        #    self.__pageNum += 1
+
+
+def pdf_lettres_individuelles(
+    context,
+    formsemestre_id,
+    etudids=None,
+    date_jury="",
+    date_commission="",
+    signature=None,
+):
+    """Document PDF avec les lettres d'avis pour les etudiants mentionnés
+    (tous ceux du semestre, ou la liste indiquée par etudids)
+    Renvoie pdf data ou chaine vide si aucun etudiant avec décision de jury.
+    """
+
+    dpv = sco_pvjury.dict_pvjury(
+        context, formsemestre_id, etudids=etudids, with_prev=True
+    )
+    if not dpv:
+        return ""
+    # Ajoute infos sur etudiants
+    etuds = [x["identite"] for x in dpv["decisions"]]
+    context.fillEtudsInfo(etuds)
+    #
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    prefs = context.get_preferences(formsemestre_id)
+    params = {
+        "date_jury": date_jury,
+        "date_commission": date_commission,
+        "titre_formation": dpv["formation"]["titre_officiel"],
+        "htab1": "8cm",  # lignes à droite (entete, signature)
+        "htab2": "1cm",
+    }
+    # copie preferences
+    for name in sco_preferences.PREFS_NAMES:
+        params[name] = context.get_preference(name, sem["formsemestre_id"])
+
+    bookmarks = {}
+    objects = []  # list of PLATYPUS objects
+    npages = 0
+    for e in dpv["decisions"]:
+        if e["decision_sem"]:  # decision prise
+            etud = context.getEtudInfo(e["identite"]["etudid"], filled=True)[0]
+            params["nomEtud"] = etud["nomprenom"]
+            bookmarks[npages + 1] = suppress_accents(etud["nomprenom"])
+            objects += pdf_lettre_individuelle(
+                dpv["formsemestre"], e, etud, params, signature, context=context
+            )
+            objects.append(PageBreak())
+            npages += 1
+    if npages == 0:
+        return ""
+    # Paramètres de mise en page
+    margins = (
+        prefs["left_margin"],
+        prefs["top_margin"],
+        prefs["right_margin"],
+        prefs["bottom_margin"],
+    )
+
+    # ----- Build PDF
+    report = cStringIO.StringIO()  # in-memory document, no disk file
+    document = BaseDocTemplate(report)
+    image_dir = SCODOC_LOGOS_DIR + "/logos_" + context.DeptId() + "/"
+    if not os.path.exists(image_dir):
+        image_dir = SCODOC_LOGOS_DIR + "/" # use global logos
+    document.addPageTemplates(
+        CourrierIndividuelTemplate(
+            document,
+            author="%s %s (E. Viennet)" % (SCONAME, SCOVERSION),
+            title="Lettres décision %s" % sem["titreannee"],
+            subject="Décision jury",
+            margins=margins,
+            pagesbookmarks=bookmarks,
+            image_dir=image_dir,
+            preferences=prefs,
+        )
+    )
+
+    document.build(objects)
+    data = report.getvalue()
+    return data
+
+
+def _descr_jury(sem, diplome):
+    if not diplome:
+        t = "passage de Semestre %d en Semestre %d" % (
+            sem["semestre_id"],
+            sem["semestre_id"] + 1,
+        )
+        s = "passage de semestre"
+    else:
+        t = "délivrance du diplôme"
+        s = t
+    return t, s  # titre long, titre court
+
+
+def pdf_lettre_individuelle(sem, decision, etud, params, signature=None, context=None):
+    """
+    Renvoie une liste d'objets PLATYPUS pour intégration
+    dans un autre document.
+    """
+    #
+    formsemestre_id = sem["formsemestre_id"]
+    Se = decision["Se"]
+    t, s = _descr_jury(sem, Se.parcours_validated() or not Se.semestre_non_terminal)
+    objects = []
+    style = reportlab.lib.styles.ParagraphStyle({})
+    style.fontSize = 14
+    style.fontName = context.get_preference("PV_FONTNAME", formsemestre_id)
+    style.leading = 18
+    style.alignment = TA_JUSTIFY
+
+    params["semestre_id"] = sem["semestre_id"]
+    params["decision_sem_descr"] = decision["decision_sem_descr"]
+    params["type_jury"] = t  # type de jury (passage ou delivrance)
+    params["type_jury_abbrv"] = s  # idem, abbrégé
+    params["decisions_ue_descr"] = decision["decisions_ue_descr"]
+    if decision["decisions_ue_nb"] > 1:
+        params["decisions_ue_descr_plural"] = "s"
+    else:
+        params["decisions_ue_descr_plural"] = ""
+
+    params["INSTITUTION_CITY"] = context.get_preference(
+        "INSTITUTION_CITY", formsemestre_id
+    )
+    if decision["prev_decision_sem"]:
+        params["prev_semestre_id"] = decision["prev"]["semestre_id"]
+        params["prev_code_descr"] = decision["prev_code_descr"]
+
+    params.update(decision["identite"])
+    # fix domicile
+    if params["domicile"]:
+        params["domicile"] = params["domicile"].replace("\\n", "<br/>")
+
+    # Décision semestre courant:
+    if sem["semestre_id"] >= 0:
+        params["decision_orig"] = "du semestre S%s" % sem["semestre_id"]
+    else:
+        params["decision_orig"] = ""
+
+    if decision["prev_decision_sem"]:
+        params["prev_decision_sem_txt"] = (
+            """<b>Décision du semestre antérieur S%(prev_semestre_id)s :</b> %(prev_code_descr)s"""
+            % params
+        )
+    else:
+        params["prev_decision_sem_txt"] = ""
+    # UE capitalisées:
+    if decision["decisions_ue"] and decision["decisions_ue_descr"]:
+        params["decision_ue_txt"] = (
+            """<b>Unité%(decisions_ue_descr_plural)s d'Enseignement %(decision_orig)s capitalisée%(decisions_ue_descr_plural)s : %(decisions_ue_descr)s</b>"""
+            % params
+        )
+    else:
+        params["decision_ue_txt"] = ""
+    # Mention
+    params["mention"] = decision["mention"]
+    # Informations sur compensations
+    if decision["observation"]:
+        params["observation_txt"] = (
+            """<b>Observation :</b> %(observation)s.""" % decision
+        )
+    else:
+        params["observation_txt"] = ""
+    # Autorisations de passage
+    if decision["autorisations"] and not Se.parcours_validated():
+        if len(decision["autorisations"]) > 1:
+            s = "s"
+        else:
+            s = ""
+        params["autorisations_txt"] = (
+            """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>"""
+            % (etud["ne"], s, s, decision["autorisations_descr"])
+        )
+    else:
+        params["autorisations_txt"] = ""
+
+    if decision["decision_sem"] and Se.parcours_validated():
+        params["diplome_txt"] = (
+            """Vous avez donc obtenu le diplôme : <b>%(titre_formation)s</b>""" % params
+        )
+    else:
+        params["diplome_txt"] = ""
+
+    # Corps de la lettre:
+    objects += sco_bulletins_pdf.process_field(
+        context,
+        context.get_preference("PV_LETTER_TEMPLATE", sem["formsemestre_id"]),
+        params,
+        style,
+        suppress_empty_pars=True,
+    )
+
+    # Signature:
+    # nota: si semestre terminal, signature par directeur IUT, sinon, signature par
+    # chef de département.
+    if Se.semestre_non_terminal:
+        sig = (
+            context.get_preference("PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id)
+            % params
+        )
+        sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
+        objects += makeParas(
+            (
+                """<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+                + sig
+                + """</para>"""
+            )
+            % params,
+            style,
+        )
+    else:
+        sig = (
+            context.get_preference("PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id)
+            % params
+        )
+        sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
+        objects += makeParas(
+            (
+                """<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+                + sig
+                + """</para>"""
+            )
+            % params,
+            style,
+        )
+
+    if signature:
+        objects.append(
+            _make_signature_image(
+                signature, params["htab1"], formsemestre_id, context=context
+            )
+        )
+
+    return objects
+
+
+def _simulate_br(p, para="<para>"):
+    """Reportlab bug turnaround (could be removed in a future version).
+    p is a string with Reportlab intra-paragraph XML tags.
+    Replaces <br/> (currently ignored by Reportlab) by </para><para>
+    """
+    l = re.split(r"<.*?br.*?/>", p)
+    return ("</para>" + para).join(l)
+
+
+def _make_signature_image(signature, leftindent, formsemestre_id, context=None):
+    "cree un paragraphe avec l'image signature"
+    # cree une image PIL pour avoir la taille (W,H)
+    from PIL import Image as PILImage
+
+    f = cStringIO.StringIO(signature)
+    im = PILImage.open(f)
+    width, height = im.size
+    pdfheight = (
+        1.0 * context.get_preference("pv_sig_image_height", formsemestre_id) * mm
+    )
+    f.seek(0, 0)
+
+    style = styles.ParagraphStyle({})
+    style.leading = 1.0 * context.get_preference(
+        "SCOLAR_FONT_SIZE", formsemestre_id
+    )  # vertical space
+    style.leftIndent = leftindent
+    return Table(
+        [("", Image(f, width=width * pdfheight / float(height), height=pdfheight))],
+        colWidths=(9 * cm, 7 * cm),
+    )
+
+
+# ----------------------------------------------
+# PV complet, tableau en format paysage
+
+
+def pvjury_pdf(
+    context,
+    dpv,
+    REQUEST,
+    date_commission=None,
+    date_jury=None,
+    numeroArrete=None,
+    VDICode=None,
+    showTitle=False,
+    with_paragraph_nom=False,
+    anonymous=False,
+):
+    """Doc PDF récapitulant les décisions de jury
+    dpv: result of dict_pvjury
+    """
+    if not dpv:
+        return {}
+    sem = dpv["formsemestre"]
+    formsemestre_id = sem["formsemestre_id"]
+
+    objects = _pvjury_pdf_type(
+        context,
+        dpv,
+        only_diplome=False,
+        date_commission=date_commission,
+        numeroArrete=numeroArrete,
+        VDICode=VDICode,
+        date_jury=date_jury,
+        showTitle=showTitle,
+        with_paragraph_nom=with_paragraph_nom,
+        anonymous=anonymous,
+    )
+
+    jury_de_diplome = not dpv["semestre_non_terminal"]
+
+    # Si Jury de passage et qu'un étudiant valide le parcours (car il a validé antérieurement le dernier semestre)
+    # alors on génère aussi un PV de diplome (à la suite dans le même doc PDF)
+    if not jury_de_diplome:
+        validations_parcours = [x["validation_parcours"] for x in dpv["decisions"]]
+        if True in validations_parcours:
+            # au moins un etudiant a validé son diplome:
+            objects.append(PageBreak())
+            objects += _pvjury_pdf_type(
+                context,
+                dpv,
+                only_diplome=True,
+                date_commission=date_commission,
+                date_jury=date_jury,
+                numeroArrete=numeroArrete,
+                VDICode=VDICode,
+                showTitle=showTitle,
+                with_paragraph_nom=with_paragraph_nom,
+                anonymous=anonymous,
+            )
+
+    # ----- Build PDF
+    report = cStringIO.StringIO()  # in-memory document, no disk file
+    document = BaseDocTemplate(report)
+    document.pagesize = landscape(A4)
+    image_dir = SCODOC_LOGOS_DIR + "/logos_" + context.DeptId() + "/"
+    if not os.path.exists(image_dir):
+        image_dir = SCODOC_LOGOS_DIR + "/" # use global logos
+    document.addPageTemplates(
+        PVTemplate(
+            document,
+            author="%s %s (E. Viennet)" % (SCONAME, SCOVERSION),
+            title=SU("PV du jury de %s" % sem["titre_num"]),
+            subject="PV jury",
+            image_dir=image_dir,
+            preferences=context.get_preferences(formsemestre_id),
+        )
+    )
+
+    document.build(objects)
+    data = report.getvalue()
+    return data
+
+
+def _pvjury_pdf_type(
+    context,
+    dpv,
+    only_diplome=False,
+    date_commission=None,
+    date_jury=None,
+    numeroArrete=None,
+    VDICode=None,
+    showTitle=False,
+    anonymous=False,
+    with_paragraph_nom=False,
+):
+    """Doc PDF récapitulant les décisions de jury pour un type de jury (passage ou delivrance)
+    dpv: result of dict_pvjury
+    """
+    # Jury de diplome si sem. terminal OU que l'on demande les diplomés d'un semestre antérieur
+    diplome = (not dpv["semestre_non_terminal"]) or only_diplome
+
+    sem = dpv["formsemestre"]
+    formsemestre_id = sem["formsemestre_id"]
+    titre_jury, titre_court_jury = _descr_jury(sem, diplome)
+    titre_diplome = dpv["formation"]["titre_officiel"]
+    objects = []
+
+    style = reportlab.lib.styles.ParagraphStyle({})
+    style.fontSize = 12
+    style.fontName = context.get_preference("PV_FONTNAME", formsemestre_id)
+    style.leading = 18
+    style.alignment = TA_JUSTIFY
+
+    indent = 1 * cm
+    bulletStyle = reportlab.lib.styles.ParagraphStyle({})
+    bulletStyle.fontSize = 12
+    bulletStyle.fontName = context.get_preference("PV_FONTNAME", formsemestre_id)
+    bulletStyle.leading = 12
+    bulletStyle.alignment = TA_JUSTIFY
+    bulletStyle.firstLineIndent = 0
+    bulletStyle.leftIndent = indent
+    bulletStyle.bulletIndent = indent
+    bulletStyle.bulletFontName = "Times-Roman"
+    bulletStyle.bulletFontSize = 11
+    bulletStyle.spaceBefore = 5 * mm
+    bulletStyle.spaceAfter = 5 * mm
+
+    objects += [Spacer(0, 5 * mm)]
+    objects += makeParas(
+        """
+    <para align="center"><b>Procès-verbal de %s du département %s - Session %s</b></para>    
+    """
+        % (
+            titre_jury,
+            context.get_preference("DeptName", formsemestre_id),
+            sem["annee"],
+        ),
+        style,
+    )
+
+    objects += makeParas(
+        """
+    <para align="center"><b><i>%s</i></b></para>
+    """
+        % titre_diplome,
+        style,
+    )
+
+    if showTitle:
+        objects += makeParas(
+            """<para align="center"><b>Semestre: %s</b></para>""" % sem["titre"], style
+        )
+    if VDICode:
+        objects += makeParas("""<para align="center">%s</para>""" % VDICode, style)
+    if date_jury:
+        objects += makeParas(
+            """<para align="center">Jury tenu le %s</para>""" % date_jury, style
+        )
+
+    objects += makeParas(
+        "<para>"
+        + context.get_preference("PV_INTRO", formsemestre_id)
+        % {
+            "Decnum": numeroArrete,
+            "VDICode": VDICode,
+            "UnivName": context.get_preference("UnivName", formsemestre_id),
+            "Type": titre_jury,
+            "Date": date_commission,  # deprecated
+            "date_commission": date_commission,
+        }
+        + "</para>",
+        bulletStyle,
+    )
+
+    objects += makeParas(
+        """<para>Le jury propose les décisions suivantes :</para>""", style
+    )
+    objects += [Spacer(0, 4 * mm)]
+    lines, titles, columns_ids = sco_pvjury.pvjury_table(
+        context,
+        dpv,
+        only_diplome=only_diplome,
+        anonymous=anonymous,
+        with_paragraph_nom=with_paragraph_nom,
+    )
+    # convert to lists of tuples:
+    columns_ids = ["etudid"] + columns_ids
+    lines = [[line.get(x, "") for x in columns_ids] for line in lines]
+    titles = [titles.get(x, "") for x in columns_ids]
+    # Make a new cell style and put all cells in paragraphs
+    cell_style = styles.ParagraphStyle({})
+    cell_style.fontSize = context.get_preference("SCOLAR_FONT_SIZE", formsemestre_id)
+    cell_style.fontName = context.get_preference("PV_FONTNAME", formsemestre_id)
+    cell_style.leading = 1.0 * context.get_preference(
+        "SCOLAR_FONT_SIZE", formsemestre_id
+    )  # vertical space
+    LINEWIDTH = 0.5
+    table_style = [
+        (
+            "FONTNAME",
+            (0, 0),
+            (-1, 0),
+            context.get_preference("PV_FONTNAME", formsemestre_id),
+        ),
+        ("LINEBELOW", (0, 0), (-1, 0), LINEWIDTH, Color(0, 0, 0)),
+        ("GRID", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
+        ("VALIGN", (0, 0), (-1, -1), "TOP"),
+    ]
+    titles = ["<para><b>%s</b></para>" % x for x in titles]
+
+    def _format_pv_cell(x):
+        """convert string to paragraph"""
+        if type(x) == types.StringType:
+            return Paragraph(SU(x), cell_style)
+        else:
+            return x
+
+    Pt = [[_format_pv_cell(x) for x in line[1:]] for line in ([titles] + lines)]
+    widths = [6 * cm, 2.8 * cm, 2.8 * cm, None, None, None, None]
+    if dpv["has_prev"]:
+        widths[2:2] = [2.8 * cm]
+    if context.get_preference("bul_show_mention", formsemestre_id):
+        widths += [None]
+    objects.append(Table(Pt, repeatRows=1, colWidths=widths, style=table_style))
+
+    # Signature du directeur
+    objects += makeParas(
+        """<para spaceBefore="10mm" align="right">
+        Le %s, %s</para>"""
+        % (
+            context.get_preference("DirectorTitle", formsemestre_id),
+            context.get_preference("DirectorName", formsemestre_id),
+        ),
+        style,
+    )
+
+    # Légende des codes
+    codes = sco_codes_parcours.CODES_EXPL.keys()
+    codes.sort()
+    objects += makeParas(
+        """<para spaceBefore="15mm" fontSize="14">
+    <b>Codes utilisés :</b></para>""",
+        style,
+    )
+    L = []
+    for code in codes:
+        L.append((code, sco_codes_parcours.CODES_EXPL[code]))
+    TableStyle2 = [
+        (
+            "FONTNAME",
+            (0, 0),
+            (-1, 0),
+            context.get_preference("PV_FONTNAME", formsemestre_id),
+        ),
+        ("LINEBELOW", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
+        ("LINEABOVE", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
+        ("LINEBEFORE", (0, 0), (0, -1), LINEWIDTH, Color(0, 0, 0)),
+        ("LINEAFTER", (-1, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
+    ]
+    objects.append(
+        Table(
+            [[Paragraph(SU(x), cell_style) for x in line] for line in L],
+            colWidths=(2 * cm, None),
+            style=TableStyle2,
+        )
+    )
+
+    return objects
diff --git a/sco_recapcomplet.py b/sco_recapcomplet.py
new file mode 100644
index 0000000000000000000000000000000000000000..54f94974f410b921f835cdd44bf2a0a464f8c3db
--- /dev/null
+++ b/sco_recapcomplet.py
@@ -0,0 +1,826 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Tableau recapitulatif des notes d'un semestre
+"""
+
+from notes_table import *
+import sco_bulletins, sco_excel
+import sco_groups
+import sco_evaluations
+import sco_formsemestre
+import sco_formsemestre_status
+import sco_bulletins_xml
+import sco_codes_parcours
+import sco_bac
+
+
+def formsemestre_recapcomplet(
+    context,
+    formsemestre_id=None,
+    modejury=False,  # affiche lien saisie decision jury
+    hidemodules=False,  # cache colonnes notes modules
+    hidebac=False,  # cache colonne Bac
+    tabformat="html",
+    sortcol=None,
+    xml_with_decisions=False,  # XML avec decisions
+    rank_partition_id=None,  # si None, calcul rang global
+    pref_override=True,  # si vrai, les prefs ont la priorite sur le param hidebac
+    REQUEST=None,
+):
+    """Page récapitulant les notes d'un semestre.
+    Grand tableau récapitulatif avec toutes les notes de modules
+    pour tous les étudiants, les moyennes par UE et générale,
+    trié par moyenne générale décroissante.
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    F = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
+    # traduit du DTML
+    modejury = int(modejury)
+    hidemodules = (
+        int(hidemodules) or parcours.UE_IS_MODULE
+    )  # cache les colonnes des modules
+    pref_override = int(pref_override)
+    if pref_override:
+        hidebac = int(context.get_preference("recap_hidebac", formsemestre_id))
+    else:
+        hidebac = int(hidebac)
+    xml_with_decisions = int(xml_with_decisions)
+    isFile = tabformat in ("csv", "xls", "xml", "xlsall")
+    H = []
+    if not isFile:
+        H += [
+            context.sco_header(
+                REQUEST,
+                page_title="Récapitulatif",
+                no_side_bar=True,
+                init_qtip=True,
+                javascripts=["libjs/sorttable.js", "js/etud_info.js"],
+            ),
+            sco_formsemestre_status.formsemestre_status_head(
+                context, formsemestre_id=formsemestre_id, REQUEST=REQUEST
+            ),
+            '<form name="f" method="get" action="%s">' % REQUEST.URL0,
+            '<input type="hidden" name="formsemestre_id" value="%s"></input>'
+            % formsemestre_id,
+            '<input type="hidden" name="pref_override" value="0"></input>',
+        ]
+        if modejury:
+            H.append(
+                '<input type="hidden" name="modejury" value="%s"></input>' % modejury
+            )
+        H.append(
+            '<select name="tabformat" onchange="document.f.submit()" class="noprint">'
+        )
+        for (format, label) in (
+            ("html", "HTML"),
+            ("xls", "Fichier tableur (Excel)"),
+            ("xlsall", "Fichier tableur avec toutes les évals"),
+            ("csv", "Fichier tableur (CSV)"),
+            ("xml", "Fichier XML"),
+        ):
+            if format == tabformat:
+                selected = " selected"
+            else:
+                selected = ""
+            H.append('<option value="%s"%s>%s</option>' % (format, selected, label))
+        H.append("</select>")
+
+        H.append(
+            """(cliquer sur un nom pour afficher son bulletin ou <a class="stdlink" href="%s/Notes/formsemestre_bulletins_pdf?formsemestre_id=%s">ici avoir le classeur papier</a>)"""
+            % (context.ScoURL(), formsemestre_id)
+        )
+        if not parcours.UE_IS_MODULE:
+            H.append(
+                """<input type="checkbox" name="hidemodules" value="1" onchange="document.f.submit()" """
+            )
+            if hidemodules:
+                H.append("checked")
+            H.append(""" >cacher les modules</input>""")
+        H.append(
+            """<input type="checkbox" name="hidebac" value="1" onchange="document.f.submit()" """
+        )
+        if hidebac:
+            H.append("checked")
+        H.append(""" >cacher bac</input>""")
+    if tabformat == "xml":
+        REQUEST.RESPONSE.setHeader("content-type", "text/xml")
+
+    H.append(
+        do_formsemestre_recapcomplet(
+            context,
+            REQUEST,
+            formsemestre_id,
+            format=tabformat,
+            hidemodules=hidemodules,
+            hidebac=hidebac,
+            modejury=modejury,
+            sortcol=sortcol,
+            xml_with_decisions=xml_with_decisions,
+            rank_partition_id=rank_partition_id,
+        )
+    )
+
+    if not isFile:
+        H.append("</form>")
+        H.append(
+            """<p><a class="stdlink" href="formsemestre_pvjury?formsemestre_id=%s">Voir les décisions du jury</a></p>"""
+            % formsemestre_id
+        )
+        if context._can_validate_sem(REQUEST, formsemestre_id):
+            H.append("<p>")
+            if modejury:
+                H.append(
+                    """<a class="stdlink" href="formsemestre_validation_auto?formsemestre_id=%s">Calcul automatique des décisions du jury</a></p><p><a class="stdlink" href="formsemestre_fix_validation_ues?formsemestre_id=%s">Vérification décisions UE</a> 
+<span style="font-size: 75%%;">(corrige incohérences éventuelles introduites avant juin 2008)<span>
+</p>"""
+                    % (formsemestre_id, formsemestre_id)
+                )
+            else:
+                H.append(
+                    """<a class="stdlink" href="formsemestre_recapcomplet?formsemestre_id=%s&amp;modejury=1&amp;hidemodules=1">Saisie des décisions du jury</a>"""
+                    % formsemestre_id
+                )
+            H.append("</p>")
+        if context.get_preference("use_ue_coefs", formsemestre_id):
+            H.append(
+                """
+            <p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
+            """
+            )
+        H.append(context.sco_footer(REQUEST))
+    return "".join(H)  # HTML or binary data...
+
+
+def do_formsemestre_recapcomplet(
+    context=None,
+    REQUEST=None,
+    formsemestre_id=None,
+    format="html",  # html, xml, xls, xlsall
+    hidemodules=False,  # ne pas montrer les modules (ignoré en XML)
+    hidebac=False,  # pas de colonne Bac (ignoré en XML)
+    xml_nodate=False,  # format XML sans dates (sert pour debug cache: comparaison de XML)
+    modejury=False,  # saisie décisions jury
+    sortcol=None,  # indice colonne a trier dans table T
+    xml_with_decisions=False,
+    disable_etudlink=False,
+    rank_partition_id=None,  # si None, calcul rang global
+):
+    """Calcule et renvoie le tableau récapitulatif.
+    """
+    data, filename, format = make_formsemestre_recapcomplet(**vars())
+    if format == "xml" or format == "html":
+        return data
+    elif format == "csv":
+        return sendCSVFile(REQUEST, data, filename)
+    elif format[:3] == "xls":
+        return sco_excel.sendExcelFile(REQUEST, data, filename)
+    else:
+        raise ValueError("unknown format %s" % format)
+
+
+def make_formsemestre_recapcomplet(
+    context=None,
+    REQUEST=None,
+    formsemestre_id=None,
+    format="html",  # html, xml, xls, xlsall
+    hidemodules=False,  # ne pas montrer les modules (ignoré en XML)
+    hidebac=False,  # pas de colonne Bac (ignoré en XML)
+    xml_nodate=False,  # format XML sans dates (sert pour debug cache: comparaison de XML)
+    modejury=False,  # saisie décisions jury
+    sortcol=None,  # indice colonne a trier dans table T
+    xml_with_decisions=False,
+    disable_etudlink=False,
+    rank_partition_id=None,  # si None, calcul rang global
+):
+    """Grand tableau récapitulatif avec toutes les notes de modules
+    pour tous les étudiants, les moyennes par UE et générale,
+    trié par moyenne générale décroissante.
+    """
+    if format == "xml":
+        return _formsemestre_recapcomplet_xml(
+            context, formsemestre_id, xml_nodate, xml_with_decisions=xml_with_decisions
+        )
+    if format[:3] == "xls":
+        keep_numeric = True  # pas de conversion des notes en strings
+    else:
+        keep_numeric = False
+
+    if hidebac:
+        admission_extra_cols = []
+    else:
+        admission_extra_cols = [
+            "type_admission",
+            "classement",
+            "apb_groupe",
+            "apb_classement_gr",
+        ]
+
+    sem = sco_formsemestre.do_formsemestre_list(
+        context, args={"formsemestre_id": formsemestre_id}
+    )[0]
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # >  get_modimpls, get_ues, get_table_moyennes_triees, get_etud_decision_sem, get_etud_etat, get_etud_rang, get_nom_short, get_mod_stats, nt.moy_moy, get_nom_long, get_etud_decision_sem,
+    modimpls = nt.get_modimpls()
+    ues = nt.get_ues()  # incluant le(s) UE de sport
+    #
+    partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
+        context, formsemestre_id
+    )
+    if rank_partition_id and format == "html":
+        # Calcul rang sur une partition et non sur l'ensemble
+        # seulement en format HTML (car colonnes rangs toujours presentes en xls)
+        rank_partition = sco_groups.get_partition(context, rank_partition_id)
+        rank_label = "Rg (%s)" % rank_partition["partition_name"]
+    else:
+        rank_partition = sco_groups.get_default_partition(context, formsemestre_id)
+        rank_label = "Rg"
+    # pdb.set_trace()
+    T = nt.get_table_moyennes_triees()
+    if not T:
+        return "", "", format
+
+    # Construit une liste de listes de chaines: le champs du tableau resultat (HTML ou CSV)
+    F = []
+    h = [rank_label, "Nom"]
+    if not hidebac:
+        h.append("Bac")
+
+    # Si CSV ou XLS, indique tous les groupes
+    if format[:3] == "xls" or format == "csv":
+        for partition in partitions:
+            h.append("%s" % partition["partition_name"])
+    else:
+        h.append("Gr")
+
+    h.append("Moy")
+    # Ajoute rangs dans groupe seulement si CSV ou XLS
+    if format[:3] == "xls" or format == "csv":
+        for partition in partitions:
+            h.append("rang_%s" % partition["partition_name"])
+
+    cod2mod = {}  # code : moduleimpl
+    mod_evals = {}  # moduleimpl_id : liste de toutes les evals de ce module
+    for ue in ues:
+        if ue["type"] != UE_SPORT:
+            h.append(ue["acronyme"])
+        else:  # UE_SPORT:
+            # n'affiche pas la moyenne d'UE dans ce cas
+            # mais laisse col. vide si modules affichés (pour séparer les UE)
+            if not hidemodules:
+                h.append("")
+            pass
+
+        if not hidemodules and not ue["is_external"]:
+            for modimpl in modimpls:
+                if modimpl["module"]["ue_id"] == ue["ue_id"]:
+                    code = modimpl["module"]["code"]
+                    h.append(code)
+                    cod2mod[code] = modimpl  # pour fabriquer le lien
+                    if format == "xlsall":
+                        evals = nt.get_mod_evaluation_etat_list(
+                            modimpl["moduleimpl_id"]
+                        )
+                        mod_evals[modimpl["moduleimpl_id"]] = evals
+                        h += _list_notes_evals_titles(context, code, evals)
+
+    h += admission_extra_cols
+    h += ["code_nip", "etudid"]
+    F.append(h)
+
+    ue_index = []  # indices des moy UE dans l (pour appliquer style css)
+
+    def fmtnum(val):  # conversion en nombre pour cellules excel
+        if keep_numeric:
+            try:
+                return float(val)
+            except:
+                return val
+        else:
+            return val
+
+    # Compte les decisions de jury
+    codes_nb = DictDefault(defaultvalue=0)
+    #
+    is_dem = {}  # etudid : bool
+    for t in T:
+        etudid = t[-1]
+        dec = nt.get_etud_decision_sem(etudid)
+        if dec:
+            codes_nb[dec["code"]] += 1
+        etud_etat = nt.get_etud_etat(etudid)
+        if etud_etat == "D":
+            gr_name = "Dém."
+            is_dem[etudid] = True
+        elif etud_etat == DEF:
+            gr_name = "Déf."
+            is_dem[etudid] = False
+        else:
+            group = sco_groups.get_etud_main_group(context, etudid, sem)
+            gr_name = group["group_name"] or ""
+            is_dem[etudid] = False
+        if rank_partition_id:
+            rang_gr, ninscrits_gr, rank_gr_name = sco_bulletins.get_etud_rangs_groups(
+                context, etudid, formsemestre_id, partitions, partitions_etud_groups, nt
+            )
+            if rank_gr_name[rank_partition_id]:
+                rank = "%s %s" % (
+                    rank_gr_name[rank_partition_id],
+                    rang_gr[rank_partition_id],
+                )
+            else:
+                rank = ""
+        else:
+            rank = nt.get_etud_rang(etudid)
+
+        l = [rank, nt.get_nom_short(etudid)]  # rang, nom,
+        e = nt.identdict[etudid]
+        if not hidebac:
+            bac = sco_bac.Baccalaureat(e["bac"], e["specialite"])
+            l.append(bac.abbrev())
+
+        if format[:3] == "xls" or format == "csv":  # tous les groupes
+            for partition in partitions:
+                group = partitions_etud_groups[partition["partition_id"]].get(
+                    etudid, None
+                )
+                if group:
+                    l.append(group["group_name"])
+                else:
+                    l.append("")
+        else:
+            l.append(gr_name)  # groupe
+
+        l.append(fmtnum(fmt_note(t[0], keep_numeric=keep_numeric)))  # moy_gen
+        # Ajoute rangs dans groupes seulement si CSV ou XLS
+        if format[:3] == "xls" or format == "csv":
+            rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups(
+                context, etudid, formsemestre_id, partitions, partitions_etud_groups, nt
+            )
+
+            for partition in partitions:
+                l.append(rang_gr[partition["partition_id"]])
+        i = 0
+        for ue in ues:
+            i += 1
+            if ue["type"] != UE_SPORT:
+                l.append(
+                    fmtnum(fmt_note(t[i], keep_numeric=keep_numeric))
+                )  # moyenne etud dans ue
+            else:  # UE_SPORT:
+                # n'affiche pas la moyenne d'UE dans ce cas
+                if not hidemodules:
+                    l.append("")
+            ue_index.append(len(l) - 1)
+            if not hidemodules and not ue["is_external"]:
+                j = 0
+                for modimpl in modimpls:
+                    if modimpl["module"]["ue_id"] == ue["ue_id"]:
+                        l.append(
+                            fmtnum(
+                                fmt_note(t[j + len(ues) + 1], keep_numeric=keep_numeric)
+                            )
+                        )  # moyenne etud dans module
+                        if format == "xlsall":
+                            l += _list_notes_evals(
+                                context, mod_evals[modimpl["moduleimpl_id"]], etudid
+                            )
+                    j += 1
+        if not hidebac:
+            for k in admission_extra_cols:
+                l.append(e[k])
+        l.append(
+            nt.identdict[etudid]["code_nip"] or ""
+        )  # avant-derniere colonne = code_nip
+        l.append(etudid)  # derniere colonne = etudid
+        F.append(l)
+
+    # Dernière ligne: moyennes, min et max des UEs et modules
+    if not hidemodules:  # moy/min/max dans chaque module
+        mods_stats = {}  # moduleimpl_id : stats
+        for modimpl in modimpls:
+            mods_stats[modimpl["moduleimpl_id"]] = nt.get_mod_stats(
+                modimpl["moduleimpl_id"]
+            )
+
+    def add_bottom_stat(key, title, corner_value=""):
+        l = ["", title]
+        if not hidebac:
+            l.append("")
+        if format[:3] == "xls" or format == "csv":
+            l += [""] * len(partitions)
+        else:
+            l += [""]
+        l.append(corner_value)
+        if format[:3] == "xls" or format == "csv":
+            for partition in partitions:
+                l += [""]  # rangs dans les groupes
+        for ue in ues:
+            if ue["type"] != UE_SPORT:
+                if key == "nb_valid_evals":
+                    l.append("")
+                elif key == "coef":
+                    if context.get_preference("use_ue_coefs", formsemestre_id):
+                        l.append("%2.3f" % ue["coefficient"])
+                    else:
+                        l.append("")
+                else:
+                    if key == "ects":
+                        l.append(ue[key])
+                    else:
+                        l.append(fmt_note(ue[key], keep_numeric=keep_numeric))
+            else:  # UE_SPORT:
+                # n'affiche pas la moyenne d'UE dans ce cas
+                if not hidemodules:
+                    l.append("")
+            ue_index.append(len(l) - 1)
+            if not hidemodules and not ue["is_external"]:
+                for modimpl in modimpls:
+                    if modimpl["module"]["ue_id"] == ue["ue_id"]:
+                        if key == "coef":
+                            coef = modimpl["module"]["coefficient"]
+                            if format[:3] != "xls":
+                                coef = str(coef)
+                            l.append(coef)
+                        elif key == "ects":
+                            l.append("")  # ECTS module ?
+                        else:
+                            val = mods_stats[modimpl["moduleimpl_id"]][key]
+                            if key == "nb_valid_evals":
+                                if (
+                                    format[:3] != "xls"
+                                ):  # garde val numerique pour excel
+                                    val = str(val)
+                            else:  # moyenne du module
+                                val = fmt_note(val, keep_numeric=keep_numeric)
+                            l.append(val)
+
+                        if format == "xlsall":
+                            l += _list_notes_evals_stats(
+                                context, mod_evals[modimpl["moduleimpl_id"]], key
+                            )
+        if modejury:
+            l.append("")  # case vide sur ligne "Moyennes"
+
+        l += [""] * len(admission_extra_cols)  # infos admission vides ici
+        F.append(l + ["", ""])  # ajoute cellules code_nip et etudid inutilisees ici
+
+    add_bottom_stat(
+        "min", "Min", corner_value=fmt_note(nt.moy_min, keep_numeric=keep_numeric)
+    )
+    add_bottom_stat(
+        "max", "Max", corner_value=fmt_note(nt.moy_max, keep_numeric=keep_numeric)
+    )
+    add_bottom_stat(
+        "moy", "Moyennes", corner_value=fmt_note(nt.moy_moy, keep_numeric=keep_numeric)
+    )
+    add_bottom_stat("coef", "Coef")
+    add_bottom_stat("nb_valid_evals", "Nb évals")
+    add_bottom_stat("ects", "ECTS")
+
+    # Generation table au format demandé
+    if format == "html":
+        # Table format HTML
+        H = [
+            """
+        <script type="text/javascript">
+        function va_saisir(formsemestre_id, etudid) {
+        loc = 'formsemestre_validation_etud_form?formsemestre_id='+formsemestre_id+'&amp;etudid='+etudid;
+        if (SORT_COLUMN_INDEX) {
+           loc += '&amp;sortcol=' + SORT_COLUMN_INDEX;
+        }
+        loc += '#etudid' + etudid;   
+        document.location=loc;
+        }
+        </script>        
+        <table class="notes_recapcomplet sortable" id="recapcomplet">
+        """
+        ]
+        if sortcol:  # sort table using JS sorttable
+            H.append(
+                """<script type="text/javascript">
+            function resort_recap() {
+            var clid = %d;
+            // element <a place par sorttable (ligne de titre)
+            lnk = document.getElementById("recap_trtit").childNodes[clid].childNodes[0];
+            ts_resortTable(lnk,clid);
+            // Scroll window:
+            eid = document.location.hash;
+            if (eid) {
+              var eid = eid.substring(1); // remove #
+              var e = document.getElementById(eid);
+              if (e) {
+                var y = e.offsetTop + e.offsetParent.offsetTop;            
+                window.scrollTo(0,y);                
+                } 
+              
+            }
+            }
+            addEvent(window, "load", resort_recap);
+            </script>
+            """
+                % (int(sortcol))
+            )
+        cells = '<tr class="recap_row_tit sortbottom" id="recap_trtit">'
+        for i in range(len(F[0]) - 2):
+            if i in ue_index:
+                cls = "recap_tit_ue"
+            else:
+                cls = "recap_tit"
+            if i == 0:  # Rang: force tri numerique pour sortable
+                cls = cls + " sortnumeric"
+            if cod2mod.has_key(F[0][i]):  # lien vers etat module
+                mod = cod2mod[F[0][i]]
+                cells += (
+                    '<td class="%s"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s (%s)">%s</a></td>'
+                    % (
+                        cls,
+                        mod["moduleimpl_id"],
+                        mod["module"]["titre"],
+                        context.Users.user_info(mod["responsable_id"])["nomcomplet"],
+                        F[0][i],
+                    )
+                )
+            else:
+                cells += '<td class="%s">%s</td>' % (cls, F[0][i])
+        if modejury:
+            cells += '<td class="recap_tit">Décision</td>'
+        ligne_titres = cells + "</tr>"
+        H.append(ligne_titres)  # titres
+        if disable_etudlink:
+            etudlink = "%(name)s"
+        else:
+            etudlink = '<a href="formsemestre_bulletinetud?formsemestre_id=%(formsemestre_id)s&amp;etudid=%(etudid)s&amp;version=selectedevals" id="%(etudid)s" class="etudinfo">%(name)s</a>'
+        ir = 0
+        nblines = len(F) - 1
+        for l in F[1:]:
+            etudid = l[-1]
+            if ir >= nblines - 6:
+                # dernieres lignes:
+                el = l[1]
+                styl = (
+                    "recap_row_min",
+                    "recap_row_max",
+                    "recap_row_moy",
+                    "recap_row_coef",
+                    "recap_row_nbeval",
+                    "recap_row_ects",
+                )[ir - nblines + 6]
+                cells = '<tr class="%s sortbottom">' % styl
+            else:
+                el = etudlink % {
+                    "formsemestre_id": formsemestre_id,
+                    "etudid": etudid,
+                    "name": l[1],
+                    "nomprenom": nt.get_nom_long(etudid),
+                }
+                if ir % 2 == 0:
+                    cells = '<tr class="recap_row_even" id="etudid%s">' % etudid
+                else:
+                    cells = '<tr class="recap_row_odd" id="etudid%s">' % etudid
+            ir += 1
+            # XXX nsn = [ x.replace('NA0', '-') for x in l[:-2] ]
+            # notes sans le NA0:
+            nsn = l[:-2]  # copy
+            for i in range(len(nsn)):
+                if nsn[i] == "NA0":
+                    nsn[i] = "-"
+            cells += '<td class="recap_col">%s</td>' % nsn[0]  # rang
+            cells += '<td class="recap_col">%s</td>' % el  # nom etud (lien)
+            if not hidebac:
+                cells += '<td class="recap_col_bac">%s</td>' % nsn[2]  # bac
+                idx_col_gr = 3
+            else:
+                idx_col_gr = 2
+            cells += '<td class="recap_col">%s</td>' % nsn[idx_col_gr]  # group name
+
+            # Style si moyenne generale < barre
+            idx_col_moy = idx_col_gr + 1
+            cssclass = "recap_col_moy"
+            try:
+                if float(nsn[idx_col_moy]) < (nt.parcours.BARRE_MOY - NOTES_TOLERANCE):
+                    cssclass = "recap_col_moy_inf"
+            except:
+                pass
+            cells += '<td class="%s">%s</td>' % (cssclass, nsn[idx_col_moy])
+            ue_number = 0
+            for i in range(idx_col_moy + 1, len(nsn)):
+                if i in ue_index:
+                    cssclass = "recap_col_ue"
+                    # grise si moy UE < barre
+                    ue = ues[ue_number]
+                    ue_number += 1
+
+                    if (ir < (nblines - 4)) or (ir == nblines - 3):
+                        try:
+                            if float(nsn[i]) < nt.parcours.get_barre_ue(
+                                ue["type"]
+                            ):  # NOTES_BARRE_UE
+                                cssclass = "recap_col_ue_inf"
+                            elif float(nsn[i]) >= nt.parcours.NOTES_BARRE_VALID_UE:
+                                cssclass = "recap_col_ue_val"
+                        except:
+                            pass
+                else:
+                    cssclass = "recap_col"
+                    if (
+                        ir == nblines - 3
+                    ):  # si moyenne generale module < barre ue, surligne:
+                        try:
+                            if float(nsn[i]) < nt.parcours.get_barre_ue(ue["type"]):
+                                cssclass = "recap_col_moy_inf"
+                        except:
+                            pass
+                cells += '<td class="%s">%s</td>' % (cssclass, nsn[i])
+            if modejury and etudid:
+                decision_sem = nt.get_etud_decision_sem(etudid)
+                if is_dem[etudid]:
+                    code = "DEM"
+                    act = ""
+                elif decision_sem:
+                    code = decision_sem["code"]
+                    act = "(modifier)"
+                else:
+                    code = ""
+                    act = "saisir"
+                cells += '<td class="decision">%s' % code
+                if act:
+                    # cells += ' <a href="formsemestre_validation_etud_form?formsemestre_id=%s&amp;etudid=%s">%s</a>' % (formsemestre_id, etudid, act)
+                    cells += (
+                        """ <a href="#" onclick="va_saisir('%s', '%s')">%s</a>"""
+                        % (formsemestre_id, etudid, act)
+                    )
+                cells += "</td>"
+            H.append(cells + "</tr>")
+
+        H.append(ligne_titres)
+        H.append("</table>")
+
+        # Form pour choisir partition de classement:
+        if not modejury and partitions:
+            H.append("Afficher le rang des groupes de: ")
+            if not rank_partition_id:
+                checked = "checked"
+            else:
+                checked = ""
+            H.append(
+                '<input type="radio" name="rank_partition_id" value="" onchange="document.f.submit()" %s/>tous '
+                % (checked)
+            )
+            for p in partitions:
+                if p["partition_id"] == rank_partition_id:
+                    checked = "checked"
+                else:
+                    checked = ""
+                H.append(
+                    '<input type="radio" name="rank_partition_id" value="%s" onchange="document.f.submit()" %s/>%s '
+                    % (p["partition_id"], checked, p["partition_name"])
+                )
+
+        # recap des decisions jury (nombre dans chaque code):
+        if codes_nb:
+            H.append("<h4>Décisions du jury</h4><table>")
+            cods = codes_nb.keys()
+            cods.sort()
+            for cod in cods:
+                H.append("<tr><td>%s</td><td>%d</td></tr>" % (cod, codes_nb[cod]))
+            H.append("</table>")
+        return "\n".join(H), "", "html"
+    elif format == "csv":
+        CSV = CSV_LINESEP.join([CSV_FIELDSEP.join(x) for x in F])
+        semname = sem["titre_num"].replace(" ", "_")
+        date = time.strftime("%d-%m-%Y")
+        filename = "notes_modules-%s-%s.csv" % (semname, date)
+        return CSV, filename, "csv"
+    elif format[:3] == "xls":
+        semname = sem["titre_num"].replace(" ", "_")
+        date = time.strftime("%d-%m-%Y")
+        if format == "xls":
+            filename = "notes_modules-%s-%s.xls" % (semname, date)
+        else:
+            filename = "notes_modules_evals-%s-%s.xls" % (semname, date)
+        xls = sco_excel.Excel_SimpleTable(
+            titles=["etudid", "code_nip"] + F[0][:-2],
+            lines=[
+                [x[-1], x[-2]] + x[:-2] for x in F[1:]
+            ],  # reordonne cols (etudid et nip en 1er)
+            SheetName="notes %s %s" % (semname, date),
+        )
+        return xls, filename, "xls"
+    else:
+        raise ValueError("unknown format %s" % format)
+
+
+def _list_notes_evals(context, evals, etudid):
+    """Liste des notes des evaluations completes de ce module
+    (pour table xls avec evals)
+    """
+    L = []
+    for e in evals:
+        if e["etat"]["evalcomplete"]:
+            NotesDB = context._notes_getall(e["evaluation_id"])
+            if NotesDB.has_key(etudid):
+                val = NotesDB[etudid]["value"]
+            else:
+                val = None
+            val_fmt = fmt_note(val, keep_numeric=True)
+            L.append(val_fmt)
+    return L
+
+
+def _list_notes_evals_titles(context, codemodule, evals):
+    """Liste des titres des evals completes"""
+    L = []
+    for e in evals:
+        if e["etat"]["evalcomplete"]:
+            L.append(codemodule + "-" + DateISOtoDMY(e["jour"]))
+    return L
+
+
+def _list_notes_evals_stats(context, evals, key):
+    """Liste des stats (moy, ou rien!) des evals completes"""
+    L = []
+    for e in evals:
+        if e["etat"]["evalcomplete"]:
+            if key == "moy":
+                val = e["etat"]["moy_num"]
+                L.append(fmt_note(val, keep_numeric=True))
+            elif key == "max":
+                L.append(e["note_max"])
+            elif key == "min":
+                L.append(0.0)
+            elif key == "coef":
+                L.append(e["coefficient"])
+            else:
+                L.append("")  # on n'a pas sous la main min/max
+    return L
+
+
+def _formsemestre_recapcomplet_xml(
+    context, formsemestre_id, xml_nodate, xml_with_decisions=False
+):
+    "XML export: liste tous les bulletins XML"
+    # REQUEST.RESPONSE.setHeader('content-type', XML_MIMETYPE)
+
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_table_moyennes_triees
+    T = nt.get_table_moyennes_triees()
+    if not T:
+        return "", "", "xml"
+
+    doc = jaxml.XML_document(encoding=SCO_ENCODING)
+    if xml_nodate:
+        docdate = ""
+    else:
+        docdate = datetime.datetime.now().isoformat()
+    doc.recapsemestre(formsemestre_id=formsemestre_id, date=docdate)
+    evals = sco_evaluations.do_evaluation_etat_in_sem(context, formsemestre_id)
+    doc._push()
+    doc.evals_info(
+        nb_evals_completes=evals["nb_evals_completes"],
+        nb_evals_en_cours=evals["nb_evals_en_cours"],
+        nb_evals_vides=evals["nb_evals_vides"],
+        date_derniere_note=evals["last_modif"],
+    )
+    doc._pop()
+    for t in T:
+        etudid = t[-1]
+        doc._push()
+        sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
+            context,
+            formsemestre_id,
+            etudid,
+            doc=doc,
+            force_publishing=True,
+            xml_nodate=xml_nodate,
+            xml_with_decisions=xml_with_decisions,
+        )
+        doc._pop()
+    return repr(doc), "", "xml"
diff --git a/sco_report.py b/sco_report.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f21f10987ec81b11fac3980435a1612a1ab370d
--- /dev/null
+++ b/sco_report.py
@@ -0,0 +1,1494 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Rapports suivi:
+  - statistiques decisions
+  - suivi cohortes
+"""
+from mx.DateTime import DateTime as mxDateTime
+import mx.DateTime
+import tempfile, urllib, re
+
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from gen_tables import GenTable
+import sco_excel, sco_pdf
+import sco_codes_parcours
+from sco_codes_parcours import code_semestre_validant
+import sco_parcours_dut
+import sco_formsemestre
+import sco_formsemestre_status
+from sco_pdf import SU
+
+MAX_ETUD_IN_DESCR = 20
+
+
+def formsemestre_etuds_stats(context, sem, only_primo=False):
+    """Récupère liste d'etudiants avec etat et decision.
+    """
+    nt = context._getNotesCache().get_NotesTable(
+        context, sem["formsemestre_id"]
+    )  # > get_table_moyennes_triees, identdict, get_etud_decision_sem, get_etud_etat,
+    T = nt.get_table_moyennes_triees()
+    # Construit liste d'étudiants du semestre avec leur decision
+    etuds = []
+    for t in T:
+        etudid = t[-1]
+        etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+        decision = nt.get_etud_decision_sem(etudid)
+        if decision:
+            etud["codedecision"] = decision["code"]
+        etud["etat"] = nt.get_etud_etat(etudid)
+        if etud["etat"] == "D":
+            etud["codedecision"] = "DEM"
+        if not etud.has_key("codedecision"):
+            etud["codedecision"] = "(nd)"  # pas de decision jury
+        # Ajout devenir (autorisations inscriptions), utile pour stats passage
+        aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription(
+            context, etudid, sem["formsemestre_id"]
+        )
+        autorisations = ["S%s" % x["semestre_id"] for x in aut_list]
+        autorisations.sort()
+        autorisations_str = ", ".join(autorisations)
+        etud["devenir"] = autorisations_str
+        # Ajout clé 'bac-specialite'
+        bs = []
+        if etud["bac"]:
+            bs.append(etud["bac"])
+        if etud["specialite"]:
+            bs.append(etud["specialite"])
+        etud["bac-specialite"] = " ".join(bs)
+        #
+        if (not only_primo) or context.isPrimoEtud(etud, sem):
+            etuds.append(etud)
+    return etuds
+
+
+def _categories_and_results(etuds, category, result):
+    categories = {}
+    results = {}
+    for etud in etuds:
+        categories[etud[category]] = True
+        results[etud[result]] = True
+    categories = categories.keys()
+    categories.sort()
+    results = results.keys()
+    results.sort()
+    return categories, results
+
+
+def _results_by_category(
+    etuds,
+    category="",
+    result="",
+    category_name=None,
+    context=None,
+    formsemestre_id=None,
+):
+    """Construit table: categories (eg types de bacs) en ligne, décisions jury en colonnes
+
+    etuds est une liste d'etuds (dicts).
+    category et result sont des clés de etud (category définie les lignes, result les colonnes).
+
+    Retourne une table.
+    """
+    if category_name is None:
+        category_name = category
+    # types de bacs differents:
+    categories, results = _categories_and_results(etuds, category, result)
+    #
+    Count = {}  # { bac : { decision : nb_avec_ce_bac_et_ce_code } }
+    results = {}  # { result_value : True }
+    for etud in etuds:
+        results[etud[result]] = True
+        if Count.has_key(etud[category]):
+            Count[etud[category]][etud[result]] += 1
+        else:
+            Count[etud[category]] = DictDefault(kv_dict={etud[result]: 1})
+    # conversion en liste de dict
+    C = [Count[cat] for cat in categories]
+    # Totaux par lignes et colonnes
+    tot = 0
+    for l in [Count[cat] for cat in categories]:
+        l["sum"] = sum(l.values())
+        tot += l["sum"]
+    # pourcentages sur chaque total de ligne
+    for l in C:
+        l["sumpercent"] = "%2.1f%%" % ((100.0 * l["sum"]) / tot)
+    #
+    codes = results.keys()
+    codes.sort()
+
+    bottom_titles = []
+    if C:  # ligne du bas avec totaux:
+        bottom_titles = {}
+        for code in codes:
+            bottom_titles[code] = sum([l[code] for l in C])
+        bottom_titles["sum"] = tot
+        bottom_titles["sumpercent"] = "100%"
+        bottom_titles["row_title"] = "Total"
+
+    # ajout titre ligne:
+    for (cat, l) in zip(categories, C):
+        l["row_title"] = cat or "?"
+
+    #
+    codes.append("sum")
+    codes.append("sumpercent")
+
+    # on veut { ADM : ADM, ... }, était peu elegant en python 2.3:
+    # titles = {}
+    # map( lambda x,titles=titles: titles.__setitem__(x[0],x[1]), zip(codes,codes) )
+    # Version moderne:
+    titles = {x: x for x in codes}
+    titles["sum"] = "Total"
+    titles["sumpercent"] = "%"
+    titles["DEM"] = "Dém."  # démissions
+    titles["row_title"] = category_name
+    return GenTable(
+        titles=titles,
+        columns_ids=codes,
+        rows=C,
+        bottom_titles=bottom_titles,
+        html_col_width="4em",
+        html_sortable=True,
+        preferences=context.get_preferences(formsemestre_id),
+    )
+
+
+# pages
+def formsemestre_report(
+    context,
+    formsemestre_id,
+    etuds,
+    REQUEST=None,
+    category="bac",
+    result="codedecision",
+    category_name="",
+    result_name="",
+    title="Statistiques",
+    only_primo=None,
+):
+    """
+    Tableau sur résultats (result) par type de category bac
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    if not category_name:
+        category_name = category
+    if not result_name:
+        result_name = result
+    if result_name == "codedecision":
+        result_name = "résultats"
+    #
+    tab = _results_by_category(
+        etuds,
+        category=category,
+        category_name=category_name,
+        result=result,
+        context=context,
+        formsemestre_id=formsemestre_id,
+    )
+    #
+    tab.filename = make_filename("stats " + sem["titreannee"])
+
+    tab.origin = "Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + ""
+    tab.caption = "Répartition des résultats par %s, semestre %s" % (
+        category_name,
+        sem["titreannee"],
+    )
+    tab.html_caption = "Répartition des résultats par %s." % category_name
+    tab.base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id)
+    if only_primo:
+        tab.base_url += "&amp;only_primo=on"
+    return tab
+
+
+# def formsemestre_report_bacs(context, formsemestre_id, format='html', REQUEST=None):
+#     """
+#     Tableau sur résultats par type de bac
+#     """
+#     sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+#     title = 'Statistiques bacs ' + sem['titreannee']
+#     etuds = formsemestre_etuds_stats(context, sem)
+#     tab = formsemestre_report(context, formsemestre_id, etuds, REQUEST=REQUEST,
+#                               category='bac', result='codedecision',
+#                               category_name='Bac',
+#                               title=title)
+#     return tab.make_page(
+#         context,
+#         title =  """<h2>Résultats de <a href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a></h2>""" % sem,
+#         format=format, page_title = title, REQUEST=REQUEST )
+
+
+def formsemestre_report_counts(
+    context,
+    formsemestre_id,
+    format="html",
+    REQUEST=None,
+    category="bac",
+    result="codedecision",
+    allkeys=False,
+    only_primo=False,
+):
+    """
+    Tableau comptage avec choix des categories
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    category_name = strcapitalize(category)
+    title = "Comptages " + category_name
+    etuds = formsemestre_etuds_stats(context, sem, only_primo=only_primo)
+    tab = formsemestre_report(
+        context,
+        formsemestre_id,
+        etuds,
+        REQUEST=REQUEST,
+        category=category,
+        result=result,
+        category_name=category_name,
+        title=title,
+        only_primo=only_primo,
+    )
+    if not etuds:
+        F = ["""<p><em>Aucun étudiant</em></p>"""]
+    else:
+        if allkeys:
+            keys = etuds[0].keys()
+        else:
+            # clés présentées à l'utilisateur:
+            keys = [
+                "annee_bac",
+                "annee_naissance",
+                "bac",
+                "specialite",
+                "bac-specialite",
+                "codedecision",
+                "devenir",
+                "etat",
+                "sexe",
+                "qualite",
+                "villelycee",
+                "statut",
+                "type_admission",
+                "boursier_prec",
+            ]
+        keys.sort()
+        F = [
+            """<form name="f" method="get" action="%s"><p>
+              Colonnes: <select name="result" onchange="document.f.submit()">"""
+            % REQUEST.URL0
+        ]
+        for k in keys:
+            if k == result:
+                selected = "selected"
+            else:
+                selected = ""
+            F.append('<option value="%s" %s>%s</option>' % (k, selected, k))
+        F.append("</select>")
+        F.append(' Lignes: <select name="category" onchange="document.f.submit()">')
+        for k in keys:
+            if k == category:
+                selected = "selected"
+            else:
+                selected = ""
+            F.append('<option value="%s" %s>%s</option>' % (k, selected, k))
+        F.append("</select>")
+        if only_primo:
+            checked = 'checked="1"'
+        else:
+            checked = ""
+        F.append(
+            '<br/><input type="checkbox" name="only_primo" onchange="document.f.submit()" %s>Restreindre aux primo-entrants</input>'
+            % checked
+        )
+        F.append(
+            '<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id
+        )
+        F.append("</p></form>")
+
+    t = tab.make_page(
+        context,
+        title="""<h2 class="formsemestre">Comptes croisés</h2>""",
+        format=format,
+        REQUEST=REQUEST,
+        with_html_headers=False,
+    )
+    if format != "html":
+        return t
+    H = [
+        context.sco_header(REQUEST, page_title=title),
+        t,
+        "\n".join(F),
+        """<p class="help">Le tableau affiche le nombre d'étudiants de ce semestre dans chacun
+          des cas choisis: à l'aide des deux menus, vous pouvez choisir les catégories utilisées
+          pour les lignes et les colonnes. Le <tt>codedecision</tt> est le code de la décision 
+          du jury.
+          </p>""",
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+# --------------------------------------------------------------------------
+def table_suivi_cohorte(
+    context,
+    formsemestre_id,
+    percent=False,
+    bac="",  # selection sur type de bac
+    bacspecialite="",
+    sexe="",
+    statut="",
+    only_primo=False,
+):
+    """
+    Tableau indicant le nombre d'etudiants de la cohorte dans chaque état:
+    Etat     date_debut_Sn   date1  date2 ...
+    S_n       #inscrits en Sn
+    S_n+1
+    ...
+    S_last
+    Diplome
+    Sorties
+
+    Determination des dates: on regroupe les semestres commençant à des dates proches
+
+    """
+    sem = sco_formsemestre.get_formsemestre(
+        context, formsemestre_id
+    )  # sem est le semestre origine
+    t0 = time.time()
+
+    def logt(op):
+        if 0:  # debug, set to 0 in production
+            log("%s: %s" % (op, time.time() - t0))
+
+    logt("table_suivi_cohorte: start")
+    # 1-- Liste des semestres posterieurs dans lesquels ont été les etudiants de sem
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etudids, get_etud_decision_sem
+    etudids = nt.get_etudids()
+
+    logt("A: orig etuds set")
+    S = {formsemestre_id: sem}  # ensemble de formsemestre_id
+    orig_set = Set()  # ensemble d'etudid du semestre d'origine
+    bacs = Set()
+    bacspecialites = Set()
+    sexes = Set()
+    statuts = Set()
+    for etudid in etudids:
+        etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+        bacspe = etud["bac"] + " / " + etud["specialite"]
+        # sélection sur bac:
+        if (
+            (not bac or (bac == etud["bac"]))
+            and (not bacspecialite or (bacspecialite == bacspe))
+            and (not sexe or (sexe == etud["sexe"]))
+            and (not statut or (statut == etud["statut"]))
+            and (not only_primo or context.isPrimoEtud(etud, sem))
+        ):
+            orig_set.add(etudid)
+            # semestres suivants:
+            for s in etud["sems"]:
+                if DateDMYtoISO(s["date_debut"]) > DateDMYtoISO(sem["date_debut"]):
+                    S[s["formsemestre_id"]] = s
+        bacs.add(etud["bac"])
+        bacspecialites.add(bacspe)
+        sexes.add(etud["sexe"])
+        if etud["statut"]:  # ne montre pas les statuts non renseignés
+            statuts.add(etud["statut"])
+    sems = S.values()
+    # tri les semestres par date de debut
+    for s in sems:
+        d, m, y = [int(x) for x in s["date_debut"].split("/")]
+        s["date_debut_mx"] = mxDateTime(y, m, d)
+    sems.sort(lambda x, y: cmp(x["date_debut_mx"], y["date_debut_mx"]))
+
+    # 2-- Pour chaque semestre, trouve l'ensemble des etudiants venant de sem
+    logt("B: etuds sets")
+    sem["members"] = orig_set
+    for s in sems:
+        ins = context.do_formsemestre_inscription_list(
+            args={"formsemestre_id": s["formsemestre_id"]}
+        )  # avec dems
+        inset = Set([i["etudid"] for i in ins])
+        s["members"] = orig_set.intersection(inset)
+        nb_dipl = 0  # combien de diplomes dans ce semestre ?
+        if s["semestre_id"] == nt.parcours.NB_SEM:
+            nt = context._getNotesCache().get_NotesTable(
+                context, s["formsemestre_id"]
+            )  # > get_etud_decision_sem
+            for etudid in s["members"]:
+                dec = nt.get_etud_decision_sem(etudid)
+                if dec and code_semestre_validant(dec["code"]):
+                    nb_dipl += 1
+        s["nb_dipl"] = nb_dipl
+
+    # 3-- Regroupe les semestres par date de debut
+    P = []  #  liste de periodsem
+
+    class periodsem:
+        pass
+
+    # semestre de depart:
+    porigin = periodsem()
+    d, m, y = [int(x) for x in sem["date_debut"].split("/")]
+    porigin.datedebut = mxDateTime(y, m, d)
+    porigin.sems = [sem]
+
+    #
+    tolerance = mx.DateTime.DateTimeDelta(45)  # 45 days
+    for s in sems:
+        merged = False
+        for p in P:
+            if abs(s["date_debut_mx"] - p.datedebut) < tolerance:
+                p.sems.append(s)
+                merged = True
+                break
+        if not merged:
+            p = periodsem()
+            p.datedebut = s["date_debut_mx"]
+            p.sems = [s]
+            P.append(p)
+
+    # 4-- regroupe par indice de semestre S_i
+    indices_sems = list(Set([s["semestre_id"] for s in sems]))
+    indices_sems.sort()
+    for p in P:
+        p.nb_etuds = 0  # nombre total d'etudiants dans la periode
+        p.sems_by_id = DictDefault(defaultvalue=[])
+        for s in p.sems:
+            p.sems_by_id[s["semestre_id"]].append(s)
+            p.nb_etuds += len(s["members"])
+
+    # 5-- Contruit table
+    logt("C: build table")
+    nb_initial = len(sem["members"])
+
+    def fmtval(x):
+        if not x:
+            return ""  # ne montre pas les 0
+        if percent:
+            return "%2.1f%%" % (100.0 * x / nb_initial)
+        else:
+            return x
+
+    L = [
+        {
+            "row_title": "Origine: S%s" % sem["semestre_id"],
+            porigin.datedebut: nb_initial,
+            "_css_row_class": "sorttop",
+        }
+    ]
+    if nb_initial <= MAX_ETUD_IN_DESCR:
+        etud_descr = _descr_etud_set(context, sem["members"])
+        L[0]["_%s_help" % porigin.datedebut] = etud_descr
+    for idx_sem in indices_sems:
+        if idx_sem >= 0:
+            d = {"row_title": "S%s" % idx_sem}
+        else:
+            d = {"row_title": "Autre semestre"}
+
+        for p in P:
+            etuds_period = Set()
+            for s in p.sems:
+                if s["semestre_id"] == idx_sem:
+                    etuds_period = etuds_period.union(s["members"])
+            nbetuds = len(etuds_period)
+            if nbetuds:
+                d[p.datedebut] = fmtval(nbetuds)
+                if nbetuds <= MAX_ETUD_IN_DESCR:  # si peu d'etudiants, indique la liste
+                    etud_descr = _descr_etud_set(context, etuds_period)
+                    d["_%s_help" % p.datedebut] = etud_descr
+        L.append(d)
+    # Compte nb de démissions et de ré-orientation par période
+    logt("D: cout dems reos")
+    sem["dems"], sem["reos"] = _count_dem_reo(context, formsemestre_id, sem["members"])
+    for p in P:
+        p.dems = Set()
+        p.reos = Set()
+        for s in p.sems:
+            d, r = _count_dem_reo(context, s["formsemestre_id"], s["members"])
+            p.dems.update(d)
+            p.reos.update(r)
+    # Nombre total d'etudiants par periode
+    l = {
+        "row_title": "Inscrits",
+        "row_title_help": "Nombre d'étudiants inscrits",
+        "_table_part": "foot",
+        porigin.datedebut: fmtval(nb_initial),
+    }
+    for p in P:
+        l[p.datedebut] = fmtval(p.nb_etuds)
+    L.append(l)
+    # Nombre de démissions par période
+    l = {
+        "row_title": "Démissions",
+        "row_title_help": "Nombre de démissions pendant la période",
+        "_table_part": "foot",
+        porigin.datedebut: fmtval(len(sem["dems"])),
+    }
+    if len(sem["dems"]) <= MAX_ETUD_IN_DESCR:
+        etud_descr = _descr_etud_set(context, sem["dems"])
+        l["_%s_help" % porigin.datedebut] = etud_descr
+    for p in P:
+        l[p.datedebut] = fmtval(len(p.dems))
+        if len(p.dems) <= MAX_ETUD_IN_DESCR:
+            etud_descr = _descr_etud_set(context, p.dems)
+            l["_%s_help" % p.datedebut] = etud_descr
+    L.append(l)
+    # Nombre de réorientations par période
+    l = {
+        "row_title": "Echecs",
+        "row_title_help": "Ré-orientations (décisions NAR)",
+        "_table_part": "foot",
+        porigin.datedebut: fmtval(len(sem["reos"])),
+    }
+    if len(sem["reos"]) < 10:
+        etud_descr = _descr_etud_set(context, sem["reos"])
+        l["_%s_help" % porigin.datedebut] = etud_descr
+    for p in P:
+        l[p.datedebut] = fmtval(len(p.reos))
+        if len(p.reos) <= MAX_ETUD_IN_DESCR:
+            etud_descr = _descr_etud_set(context, p.reos)
+            l["_%s_help" % p.datedebut] = etud_descr
+    L.append(l)
+    # derniere ligne: nombre et pourcentage de diplomes
+    l = {
+        "row_title": "Diplômes",
+        "row_title_help": "Nombre de diplômés à la fin de la période",
+        "_table_part": "foot",
+    }
+    for p in P:
+        nb_dipl = 0
+        for s in p.sems:
+            nb_dipl += s["nb_dipl"]
+        l[p.datedebut] = fmtval(nb_dipl)
+    L.append(l)
+
+    columns_ids = [p.datedebut for p in P]
+    titles = dict([(p.datedebut, p.datedebut.strftime("%d/%m/%y")) for p in P])
+    titles[porigin.datedebut] = porigin.datedebut.strftime("%d/%m/%y")
+    if percent:
+        pp = "(en % de la population initiale) "
+        titles["row_title"] = "%"
+    else:
+        pp = ""
+        titles["row_title"] = ""
+    if only_primo:
+        pp += "(restreint aux primo-entrants) "
+    if bac:
+        dbac = " (bacs %s)" % bac
+    else:
+        dbac = ""
+    if bacspecialite:
+        dbac += " (spécialité %s)" % bacspecialite
+    if sexe:
+        dbac += " genre: %s" % sexe
+    if statut:
+        dbac += " statut: %s" % statut
+    tab = GenTable(
+        titles=titles,
+        columns_ids=columns_ids,
+        rows=L,
+        html_col_width="4em",
+        html_sortable=True,
+        filename=make_filename("cohorte " + sem["titreannee"]),
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
+        page_title="Suivi cohorte " + sem["titreannee"],
+        html_class="table_cohorte",
+        preferences=context.get_preferences(formsemestre_id),
+    )
+    # Explication: liste des semestres associés à chaque date
+    if not P:
+        expl = [
+            '<p class="help">(aucun étudiant trouvé dans un semestre ultérieur)</p>'
+        ]
+    else:
+        expl = ["<h3>Semestres associés à chaque date:</h3><ul>"]
+        for p in P:
+            expl.append("<li><b>%s</b>:" % p.datedebut.strftime("%d/%m/%y"))
+            ls = []
+            for s in p.sems:
+                ls.append(
+                    '<a href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a>'
+                    % s
+                )
+            expl.append(", ".join(ls) + "</li>")
+        expl.append("</ul>")
+    logt("Z: table_suivi_cohorte done")
+    return tab, "\n".join(expl), bacs, bacspecialites, sexes, statuts
+
+
+def formsemestre_suivi_cohorte(
+    context,
+    formsemestre_id,
+    format="html",
+    percent=1,
+    bac="",
+    bacspecialite="",
+    sexe="",
+    statut="",
+    only_primo=False,
+    REQUEST=None,
+):
+    """Affiche suivi cohortes par numero de semestre
+    """
+    percent = int(percent)
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    tab, expl, bacs, bacspecialites, sexes, statuts = table_suivi_cohorte(
+        context,
+        formsemestre_id,
+        percent=percent,
+        bac=bac,
+        bacspecialite=bacspecialite,
+        sexe=sexe,
+        statut=statut,
+        only_primo=only_primo,
+    )
+    tab.base_url = (
+        "%s?formsemestre_id=%s&amp;percent=%s&amp;bac=%s&amp;bacspecialite=%s&amp;sexe=%s"
+        % (REQUEST.URL0, formsemestre_id, percent, bac, bacspecialite, sexe)
+    )
+    if only_primo:
+        tab.base_url += "&amp;only_primo=on"
+    t = tab.make_page(context, format=format, with_html_headers=False, REQUEST=REQUEST)
+    if format != "html":
+        return t
+
+    base_url = REQUEST.URL0
+    burl = (
+        "%s?formsemestre_id=%s&amp;bac=%s&amp;bacspecialite=%s&amp;sexe=%s&amp;statut=%s"
+        % (base_url, formsemestre_id, bac, bacspecialite, sexe, statut)
+    )
+    if percent:
+        pplink = (
+            '<p><a href="%s&amp;percent=0">Afficher les résultats bruts</a></p>' % burl
+        )
+    else:
+        pplink = (
+            '<p><a href="%s&amp;percent=1">Afficher les résultats en pourcentages</a></p>'
+            % burl
+        )
+    help = (
+        pplink
+        + """    
+    <p class="help">Nombre d'étudiants dans chaque semestre. Les dates indiquées sont les dates approximatives de <b>début</b> des semestres (les semestres commençant à des dates proches sont groupés). Le nombre de diplômés est celui à la <b>fin</b> du semestre correspondant. Lorsqu'il y a moins de %s étudiants dans une case, vous pouvez afficher leurs noms en passant le curseur sur le chiffre.</p>
+<p class="help">Les menus permettent de n'étudier que certaines catégories d'étudiants (titulaires d'un type de bac, garçons ou filles). La case "restreindre aux primo-entrants" permet de ne considérer que les étudiants qui n'ont jamais été inscrits dans ScoDoc avant le semestre considéré.</p>
+    """
+        % (MAX_ETUD_IN_DESCR,)
+    )
+
+    H = [
+        context.sco_header(REQUEST, page_title=tab.page_title),
+        """<h2 class="formsemestre">Suivi cohorte: devenir des étudiants de ce semestre</h2>""",
+        _gen_form_selectetuds(
+            formsemestre_id,
+            REQUEST=REQUEST,
+            only_primo=only_primo,
+            bac=bac,
+            bacspecialite=bacspecialite,
+            sexe=sexe,
+            statut=statut,
+            bacs=bacs,
+            bacspecialites=bacspecialites,
+            sexes=sexes,
+            statuts=statuts,
+            percent=percent,
+        ),
+        t,
+        help,
+        expl,
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+def _gen_form_selectetuds(
+    formsemestre_id,
+    REQUEST=None,
+    percent=None,
+    only_primo=None,
+    bac=None,
+    bacspecialite=None,
+    sexe=None,
+    statut=None,
+    bacs=None,
+    bacspecialites=None,
+    sexes=None,
+    statuts=None,
+):
+    """HTML form pour choix criteres selection etudiants"""
+    bacs = list(bacs)
+    bacs.sort()
+    bacspecialites = list(bacspecialites)
+    bacspecialites.sort()
+    sexes = list(sexes)
+    sexes.sort()
+    statuts = list(statuts)
+    statuts.sort()
+    #
+    if bac:
+        selected = ""
+    else:
+        selected = 'selected="selected"'
+    F = [
+        """<form id="f" method="get" action="%s">
+    <p>Bac: <select name="bac" onchange="javascript: submit(this);">
+    <option value="" %s>tous</option>
+    """
+        % (REQUEST.URL0, selected)
+    ]
+    for b in bacs:
+        if bac == b:
+            selected = 'selected="selected"'
+        else:
+            selected = ""
+        F.append('<option value="%s" %s>%s</option>' % (b, selected, b))
+    F.append("</select>")
+    if bacspecialite:
+        selected = ""
+    else:
+        selected = 'selected="selected"'
+    F.append(
+        """&nbsp; Bac/Specialité: <select name="bacspecialite" onchange="javascript: submit(this);">
+    <option value="" %s>tous</option>
+    """
+        % selected
+    )
+    for b in bacspecialites:
+        if bacspecialite == b:
+            selected = 'selected="selected"'
+        else:
+            selected = ""
+        F.append('<option value="%s" %s>%s</option>' % (b, selected, b))
+    F.append("</select>")
+    F.append(
+        """&nbsp; Genre: <select name="sexe" onchange="javascript: submit(this);">
+    <option value="" %s>tous</option>
+    """
+        % selected
+    )
+    for b in sexes:
+        if sexe == b:
+            selected = 'selected="selected"'
+        else:
+            selected = ""
+        F.append('<option value="%s" %s>%s</option>' % (b, selected, b))
+    F.append("</select>")
+
+    F.append(
+        """&nbsp; Statut: <select name="statut" onchange="javascript: submit(this);">
+    <option value="" %s>tous</option>
+    """
+        % selected
+    )
+    for b in statuts:
+        if statut == b:
+            selected = 'selected="selected"'
+        else:
+            selected = ""
+        F.append('<option value="%s" %s>%s</option>' % (b, selected, b))
+    F.append("</select>")
+
+    if only_primo:
+        checked = 'checked="1"'
+    else:
+        checked = ""
+    F.append(
+        '<br/><input type="checkbox" name="only_primo" onchange="javascript: submit(this);" %s/>Restreindre aux primo-entrants'
+        % checked
+    )
+    F.append(
+        '<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id
+    )
+    F.append('<input type="hidden" name="percent" value="%s"/>' % percent)
+    F.append("</p></form>")
+    return "\n".join(F)
+
+
+def _descr_etud_set(context, etudids):
+    "textual html description of a set of etudids"
+    etuds = []
+    for etudid in etudids:
+        etuds.append(context.getEtudInfo(etudid=etudid, filled=True)[0])
+    # sort by name
+    etuds.sort(lambda x, y: cmp(x["nom"], y["nom"]))
+    return ", ".join([e["nomprenom"] for e in etuds])
+
+
+def _count_dem_reo(context, formsemestre_id, etudids):
+    "count nb of demissions and reorientation in this etud set"
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etud_etat, get_etud_decision_sem
+    dems = Set()
+    reos = Set()
+    for etudid in etudids:
+        if nt.get_etud_etat(etudid) == "D":
+            dems.add(etudid)
+        dec = nt.get_etud_decision_sem(etudid)
+        if dec and dec["code"] in sco_codes_parcours.CODES_SEM_REO:
+            reos.add(etudid)
+    return dems, reos
+
+
+"""OLDGEA:
+27s pour  S1 F.I. classique Semestre 1 2006-2007
+B 2.3s
+C 5.6s
+D 5.9s
+Z 27s  => cache des semestres pour nt
+
+à chaud: 3s
+B: etuds sets: 2.4s => lent: N x getEtudInfo (non caché)
+"""
+
+EXP_LIC = re.compile(r"licence", re.I)
+EXP_LPRO = re.compile(r"professionnelle", re.I)
+
+
+def _codesem(sem, short=True, prefix=""):
+    "code semestre: S1 ou S1d"
+    idx = sem["semestre_id"]
+    # semestre décalé ?
+    # les semestres pairs normaux commencent entre janvier et mars
+    # les impairs normaux entre aout et decembre
+    d = ""
+    if idx and idx > 0 and sem["date_debut"]:
+        mois_debut = int(sem["date_debut"].split("/")[1])
+        if (idx % 2 and mois_debut < 3) or (idx % 2 == 0 and mois_debut >= 8):
+            d = "d"
+    if idx == -1:
+        if short:
+            idx = "Autre "
+        else:
+            idx = sem["titre"] + " "
+            idx = EXP_LPRO.sub("pro.", idx)
+            idx = EXP_LIC.sub("Lic.", idx)
+            prefix = ""  # indique titre au lieu de Sn
+    return "%s%s%s" % (prefix, idx, d)
+
+
+def get_codeparcoursetud(context, etud, prefix="", separator=""):
+    """calcule un code de parcours pour un etudiant
+    exemples:
+       1234A pour un etudiant ayant effectué S1, S2, S3, S4 puis diplome
+       12D   pour un étudiant en S1, S2 puis démission en S2
+       12R   pour un etudiant en S1, S2 réorienté en fin de S2
+    Construit aussi un dict: { semestre_id : decision_jury | None }    
+    """
+    p = []
+    decisions_jury = {}
+    # élimine les semestres spéciaux sans parcours (LP...)
+    sems = [s for s in etud["sems"] if s["semestre_id"] >= 0]
+    i = len(sems) - 1
+    while i >= 0:
+        s = sems[i]  # 'sems' est a l'envers, du plus recent au plus ancien
+        nt = context._getNotesCache().get_NotesTable(
+            context, s["formsemestre_id"]
+        )  # > get_etud_etat, get_etud_decision_sem
+        p.append(_codesem(s, prefix=prefix))
+        # code decisions jury de chaque semestre:
+        if nt.get_etud_etat(etud["etudid"]) == "D":
+            decisions_jury[s["semestre_id"]] = "DEM"
+        else:
+            dec = nt.get_etud_decision_sem(etud["etudid"])
+            if not dec:
+                decisions_jury[s["semestre_id"]] = ""
+            else:
+                decisions_jury[s["semestre_id"]] = dec["code"]
+        # code etat dans le codeparcours sur dernier semestre seulement
+        if i == 0:
+            # Démission
+            if nt.get_etud_etat(etud["etudid"]) == "D":
+                p.append(":D")
+            else:
+                dec = nt.get_etud_decision_sem(etud["etudid"])
+                if dec and dec["code"] in sco_codes_parcours.CODES_SEM_REO:
+                    p.append(":R")
+                if (
+                    dec
+                    and s["semestre_id"] == nt.parcours.NB_SEM
+                    and code_semestre_validant(dec["code"])
+                ):
+                    p.append(":A")
+        i -= 1
+    return separator.join(p), decisions_jury
+
+
+def tsp_etud_list(
+    context,
+    formsemestre_id,
+    only_primo=False,
+    bac="",  # selection sur type de bac
+    bacspecialite="",
+    sexe="",
+    statut="",
+):
+    """Liste des etuds a considerer dans table suivi parcours 
+    ramene aussi ensembles des bacs, genres, statuts de (tous) les etudiants   
+    """
+    # log('tsp_etud_list(%s, bac="%s")' % (formsemestre_id,bac))
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etudids,
+    etudids = nt.get_etudids()
+    etuds = []
+    bacs = Set()
+    bacspecialites = Set()
+    sexes = Set()
+    statuts = Set()
+    for etudid in etudids:
+        etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+        bacspe = etud["bac"] + " / " + etud["specialite"]
+        # sélection sur bac, primo, ...:
+        if (
+            (not bac or (bac == etud["bac"]))
+            and (not bacspecialite or (bacspecialite == bacspe))
+            and (not sexe or (sexe == etud["sexe"]))
+            and (not statut or (statut == etud["statut"]))
+            and (not only_primo or context.isPrimoEtud(etud, sem))
+        ):
+            etuds.append(etud)
+
+        bacs.add(etud["bac"])
+        bacspecialites.add(bacspe)
+        sexes.add(etud["sexe"])
+        if etud["statut"]:  # ne montre pas les statuts non renseignés
+            statuts.add(etud["statut"])
+    # log('tsp_etud_list: %s etuds' % len(etuds))
+    return etuds, bacs, bacspecialites, sexes, statuts
+
+
+def tsp_grouped_list(context, codes_etuds):
+    """Liste pour table regroupant le nombre d'étudiants (+ bulle avec les noms) de chaque parcours
+    """
+    L = []
+    parcours = codes_etuds.keys()
+    parcours.sort()
+    for p in parcours:
+        nb = len(codes_etuds[p])
+        l = {"parcours": p, "nb": nb}
+        if nb <= MAX_ETUD_IN_DESCR:
+            l["_nb_help"] = _descr_etud_set(
+                context, [e["etudid"] for e in codes_etuds[p]]
+            )
+        L.append(l)
+    # tri par effectifs décroissants
+    L.sort(lambda x, y: cmp(y["nb"], x["nb"]))
+    return L
+
+
+def table_suivi_parcours(
+    context, formsemestre_id, only_primo=False, grouped_parcours=True
+):
+    """Tableau recapitulant tous les parcours
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    etuds, bacs, bacspecialites, sexes, statuts = tsp_etud_list(
+        context, formsemestre_id, only_primo=only_primo
+    )
+    codes_etuds = DictDefault(defaultvalue=[])
+    for etud in etuds:
+        etud["codeparcours"], etud["decisions_jury"] = get_codeparcoursetud(
+            context, etud
+        )
+        codes_etuds[etud["codeparcours"]].append(etud)
+        etud["_nom_target"] = "ficheEtud?etudid=" + etud["etudid"]
+        etud["_prenom_target"] = "ficheEtud?etudid=" + etud["etudid"]
+        etud["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"])
+
+    titles = {
+        "parcours": "Code parcours",
+        "nb": "Nombre d'étudiants",
+        "sexe": "",
+        "nom": "Nom",
+        "prenom": "Prénom",
+        "etudid": "etudid",
+        "codeparcours": "Code parcours",
+        "bac": "Bac",
+        "specialite": "Spe.",
+    }
+
+    if grouped_parcours:
+        L = tsp_grouped_list(context, codes_etuds)
+        columns_ids = ("parcours", "nb")
+    else:
+        # Table avec le parcours de chaque étudiant:
+        L = etuds
+        columns_ids = (
+            "etudid",
+            "sexe",
+            "nom",
+            "prenom",
+            "bac",
+            "specialite",
+            "codeparcours",
+        )
+        # Calcule intitulés de colonnes
+        S = set()
+        sems_ids = list(apply(S.union, [e["decisions_jury"].keys() for e in etuds]))
+        sems_ids.sort()
+        sem_tits = ["S%s" % s for s in sems_ids]
+        titles.update([(s, s) for s in sem_tits])
+        columns_ids += tuple(sem_tits)
+        for etud in etuds:
+            for s in etud["decisions_jury"]:
+                etud["S%s" % s] = etud["decisions_jury"][s]
+
+    if only_primo:
+        primostr = "primo-entrants du"
+    else:
+        primostr = "passés dans le"
+    tab = GenTable(
+        columns_ids=columns_ids,
+        rows=L,
+        titles=titles,
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+        caption="Parcours suivis, étudiants %s semestre " % primostr
+        + sem["titreannee"],
+        page_title="Parcours " + sem["titreannee"],
+        html_sortable=True,
+        html_class="table_leftalign table_listegroupe",
+        html_next_section="""<table class="help">
+                    <tr><td><tt>1, 2, ...</tt></td><td> numéros de semestres</td></tr>
+                    <tr><td><tt>1d, 2d, ...</tt></td><td>semestres "décalés"</td></tr>
+                    <tr><td><tt>:A</tt></td><td> étudiants diplômés</td></tr>
+                    <tr><td><tt>:R</tt></td><td> étudiants réorientés</td></tr>
+                    <tr><td><tt>:D</tt></td><td> étudiants démissionnaires</td></tr>
+                    </table>""",
+        bottom_titles={
+            "parcours": "Total",
+            "nb": len(etuds),
+            "codeparcours": len(etuds),
+        },
+        preferences=context.get_preferences(formsemestre_id),
+    )
+    return tab
+
+
+def tsp_form_primo_group(REQUEST, only_primo, no_grouping, formsemestre_id, format):
+    """Element de formulaire pour choisir si restriction aux primos entrants et groupement par lycees
+    """
+    F = ["""<form name="f" method="get" action="%s">""" % REQUEST.URL0]
+    if only_primo:
+        checked = 'checked="1"'
+    else:
+        checked = ""
+    F.append(
+        '<input type="checkbox" name="only_primo" onchange="document.f.submit()" %s>Restreindre aux primo-entrants</input>'
+        % checked
+    )
+    if no_grouping:
+        checked = 'checked="1"'
+    else:
+        checked = ""
+    F.append(
+        '<input type="checkbox" name="no_grouping" onchange="document.f.submit()" %s>Lister chaque étudiant</input>'
+        % checked
+    )
+    F.append(
+        '<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id
+    )
+    F.append('<input type="hidden" name="format" value="%s"/>' % format)
+    F.append("""</form>""")
+    return "\n".join(F)
+
+
+def formsemestre_suivi_parcours(
+    context,
+    formsemestre_id,
+    format="html",
+    only_primo=False,
+    no_grouping=False,
+    REQUEST=None,
+):
+    """Effectifs dans les differents parcours possibles.
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    tab = table_suivi_parcours(
+        context,
+        formsemestre_id,
+        only_primo=only_primo,
+        grouped_parcours=not no_grouping,
+    )
+    tab.base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id)
+    if only_primo:
+        tab.base_url += "&amp;only_primo=1"
+    if no_grouping:
+        tab.base_url += "&amp;no_grouping=1"
+    t = tab.make_page(context, format=format, with_html_headers=False, REQUEST=REQUEST)
+    if format != "html":
+        return t
+    F = [
+        tsp_form_primo_group(REQUEST, only_primo, no_grouping, formsemestre_id, format)
+    ]
+
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title=tab.page_title,
+            init_qtip=True,
+            javascripts=["js/etud_info.js"],
+        ),
+        """<h2 class="formsemestre">Parcours suivis par les étudiants de ce semestre</h2>""",
+        "\n".join(F),
+        t,
+        context.sco_footer(REQUEST),
+    ]
+    return "\n".join(H)
+
+
+# -------------
+def graph_parcours(
+    context,
+    formsemestre_id,
+    format="svg",
+    only_primo=False,
+    bac="",  # selection sur type de bac
+    bacspecialite="",
+    sexe="",
+    statut="",
+):
+    """
+    """
+    if not WITH_PYDOT:
+        raise ScoValueError("pydot module is not installed")
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    etuds, bacs, bacspecialites, sexes, statuts = tsp_etud_list(
+        context,
+        formsemestre_id,
+        only_primo=only_primo,
+        bac=bac,
+        bacspecialite=bacspecialite,
+        sexe=sexe,
+        statut=statut,
+    )
+    # log('graph_parcours: %s etuds (only_primo=%s)' % (len(etuds), only_primo))
+    if not etuds:
+        return "", bacs, bacspecialites, sexes, statuts
+    edges = DictDefault(
+        defaultvalue=Set()
+    )  # {(formsemestre_id_origin, formsemestre_id_dest) : etud_set}
+    sems = {}
+    effectifs = DictDefault(defaultvalue=Set())  # formsemestre_id : etud_set
+    decisions = DictDefault(defaultvalue={})  # formsemestre_id : { code : nb_etud }
+    isolated_nodes = []
+    connected_nodes = Set()
+    diploma_nodes = []
+    dem_nodes = {}  # formsemestre_id : noeud pour demissionnaires
+    nar_nodes = {}  # formsemestre_id : noeud pour NAR
+    for etud in etuds:
+        next = None
+        etudid = etud["etudid"]
+        for s in etud["sems"]:  # du plus recent au plus ancien
+            nt = context._getNotesCache().get_NotesTable(
+                context, s["formsemestre_id"]
+            )  # > get_etud_decision_sem, get_etud_etat
+            dec = nt.get_etud_decision_sem(etudid)
+            if next:
+                if (
+                    s["semestre_id"] == nt.parcours.NB_SEM
+                    and dec
+                    and code_semestre_validant(dec["code"])
+                    and nt.get_etud_etat(etudid) == "I"
+                ):
+                    # cas particulier du diplome puis poursuite etude
+                    edges[
+                        ("_dipl_" + s["formsemestre_id"], next["formsemestre_id"])
+                    ].add(etudid)
+                else:
+                    edges[(s["formsemestre_id"], next["formsemestre_id"])].add(etudid)
+                connected_nodes.add(s["formsemestre_id"])
+                connected_nodes.add(next["formsemestre_id"])
+            else:
+                isolated_nodes.append(s["formsemestre_id"])
+            sems[s["formsemestre_id"]] = s
+            effectifs[s["formsemestre_id"]].add(etudid)
+            next = s
+            # Compte decisions jury de chaque semestres:
+            dc = decisions[s["formsemestre_id"]]
+            if dec:
+                if dec["code"] in dc:
+                    dc[dec["code"]] += 1
+                else:
+                    dc[dec["code"]] = 1
+            # ajout noeud pour demissionnaires
+            if nt.get_etud_etat(etudid) == "D":
+                nid = "_dem_" + s["formsemestre_id"]
+                dem_nodes[s["formsemestre_id"]] = nid
+                edges[(s["formsemestre_id"], nid)].add(etudid)
+            # ajout noeud pour NAR (seulement pour noeud de depart)
+            if s["formsemestre_id"] == formsemestre_id and dec and dec["code"] == NAR:
+                nid = "_nar_" + s["formsemestre_id"]
+                nar_nodes[s["formsemestre_id"]] = nid
+                edges[(s["formsemestre_id"], nid)].add(etudid)
+
+            # si "terminal", ajoute noeud pour diplomes
+            if s["semestre_id"] == nt.parcours.NB_SEM:
+                if (
+                    dec
+                    and code_semestre_validant(dec["code"])
+                    and nt.get_etud_etat(etudid) == "I"
+                ):
+                    nid = "_dipl_" + s["formsemestre_id"]
+                    edges[(s["formsemestre_id"], nid)].add(etudid)
+                    diploma_nodes.append(nid)
+    #
+    g = pydot.graph_from_edges(edges.keys())
+    for fid in isolated_nodes:
+        if not fid in connected_nodes:
+            n = pydot.Node(name=fid)
+            g.add_node(n)
+    g.set("rankdir", "LR")  # left to right
+    g.set_fontname("Helvetica")
+    if format == "svg":
+        g.set_bgcolor("#fffff0")  # ou 'transparent'
+    # titres des semestres:
+    for s in sems.values():
+        n = pydot_get_node(g, s["formsemestre_id"])
+        log("s['formsemestre_id'] = %s" % s["formsemestre_id"])
+        log("n=%s" % n)
+        log("get=%s" % g.get_node(s["formsemestre_id"]))
+        log("nodes names = %s" % [x.get_name() for x in g.get_node_list()])
+        if s["modalite"] and s["modalite"] != "FI":
+            modalite = " " + s["modalite"]
+        else:
+            modalite = ""
+        label = "%s%s\\n%d/%s - %d/%s\\n%d" % (
+            _codesem(s, short=False, prefix="S"),
+            modalite,
+            s["mois_debut_ord"],
+            s["annee_debut"][2:],
+            s["mois_fin_ord"],
+            s["annee_fin"][2:],
+            len(effectifs[s["formsemestre_id"]]),
+        )
+        n.set("label", suppress_accents(label))
+        n.set_fontname("Helvetica")
+        n.set_fontsize(8.0)
+        n.set_width(1.2)
+        n.set_shape("box")
+        n.set_URL("formsemestre_status?formsemestre_id=" + s["formsemestre_id"])
+    # semestre de depart en vert
+    n = pydot_get_node(g, formsemestre_id)
+    n.set_color("green")
+    # demissions en rouge, octagonal
+    for nid in dem_nodes.values():
+        n = pydot_get_node(g, nid)
+        n.set_color("red")
+        n.set_shape("octagon")
+        n.set("label", "Dem.")
+
+    # NAR en rouge, Mcircle
+    for nid in nar_nodes.values():
+        n = pydot_get_node(g, nid)
+        n.set_color("red")
+        n.set_shape("Mcircle")
+        n.set("label", NAR)
+    # diplomes:
+    for nid in diploma_nodes:
+        n = pydot_get_node(g, nid)
+        n.set_color("red")
+        n.set_shape("ellipse")
+        n.set("label", "Diplome")  # bug si accent (pas compris pourquoi)
+    # Arètes:
+    bubbles = {}  # substitue titres pour bulle aides: src_id:dst_id : etud_descr
+    for (src_id, dst_id) in edges.keys():
+        e = g.get_edge(src_id, dst_id)
+        e.set("arrowhead", "normal")
+        e.set("arrowsize", 1)
+        e.set_label(len(edges[(src_id, dst_id)]))
+        e.set_fontname("Helvetica")
+        e.set_fontsize(8.0)
+        # bulle avec liste etudiants
+        if len(edges[(src_id, dst_id)]) <= MAX_ETUD_IN_DESCR:
+            etud_descr = _descr_etud_set(context, edges[(src_id, dst_id)])
+            bubbles[src_id + ":" + dst_id] = etud_descr
+            e.set_URL("__xxxetudlist__?" + src_id + ":" + dst_id)
+    # Genere graphe
+    f, path = tempfile.mkstemp(".gr")
+    g.write(path=path, format=format)
+    data = open(path, "r").read()
+    log("dot generated %d bytes in %s format" % (len(data), format))
+    if not data:
+        log("graph.to_string=%s" % g.to_string())
+        raise ValueError(
+            "Erreur lors de la génération du document au format %s" % format
+        )
+    os.unlink(path)
+    if format == "svg":
+        # dot génère un document XML complet, il faut enlever l'en-tête
+        data = "<svg" + "<svg".join(data.split("<svg")[1:])
+        # Substitution des titres des URL des aretes pour bulles aide
+        def repl(m):
+            return '<a title="%s"' % bubbles[m.group("sd")]
+
+        exp = re.compile(
+            r'<a.*?href="__xxxetudlist__\?(?P<sd>\w*:\w*).*?".*?xlink:title=".*?"', re.M
+        )
+        data = exp.sub(repl, data)
+        # Substitution des titres des boites (semestres)
+        exp1 = re.compile(
+            r'<a xlink:href="formsemestre_status\?formsemestre_id=(?P<fid>\w*).*?".*?xlink:title="(?P<title>.*?)"',
+            re.M | re.DOTALL,
+        )
+
+        def repl_title(m):
+            fid = m.group("fid")
+            title = sems[fid]["titreannee"]
+            if decisions[fid]:
+                title += (
+                    (" (" + str(decisions[fid]) + ")").replace("{", "").replace("'", "")
+                )
+            return (
+                '<a xlink:href="formsemestre_status?formsemestre_id=%s" xlink:title="%s"'
+                % (fid, suppress_accents(title))
+            )  # evite accents car svg utf-8 vs page en latin1...
+
+        data = exp1.sub(repl_title, data)
+        # Substitution de Arial par Helvetica (new prblem in Debian 5) ???
+        # bug turnaround: il doit bien y avoir un endroit ou regler cela ?
+        # cf http://groups.google.com/group/pydot/browse_thread/thread/b3704c53e331e2ec
+        data = data.replace("font-family:Arial", "font-family:Helvetica")
+
+    return data, bacs, bacspecialites, sexes, statuts
+
+
+def formsemestre_graph_parcours(
+    context,
+    formsemestre_id,
+    format="html",
+    only_primo=False,
+    bac="",  # selection sur type de bac
+    bacspecialite="",
+    sexe="",
+    statut="",
+    allkeys=False,
+    REQUEST=None,
+):
+    """Graphe suivi cohortes
+    """
+    # log("formsemestre_graph_parcours")
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    if format == "pdf":
+        doc, bacs, bacspecialites, sexes, statuts = graph_parcours(
+            context,
+            formsemestre_id,
+            format="pdf",
+            only_primo=only_primo,
+            bac=bac,
+            bacspecialite=bacspecialite,
+            sexe=sexe,
+            statut=statut,
+        )
+        filename = make_filename("flux " + sem["titreannee"])
+        return sco_pdf.sendPDFFile(REQUEST, doc, filename + ".pdf")
+    elif format == "png":
+        #
+        doc, bacs, bacspecialites, sexes, statuts = graph_parcours(
+            context,
+            formsemestre_id,
+            format="png",
+            only_primo=only_primo,
+            bac=bac,
+            bacspecialite=bacspecialite,
+            sexe=sexe,
+            statut=statut,
+        )
+        filename = make_filename("flux " + sem["titreannee"])
+        REQUEST.RESPONSE.setHeader(
+            "content-disposition", 'attachment; filename="%s"' % filename
+        )
+        REQUEST.RESPONSE.setHeader("content-type", "image/png")
+        return doc
+    elif format == "html":
+        if only_primo:
+            op = "only_primo=on&amp;"
+        else:
+            op = ""
+        url = urllib.quote(
+            "formsemestre_graph_parcours?formsemestre_id=%s&amp;%sbac=%s&amp;bacspecialite=%s&amp;sexe=%s&amp;statut=%s&amp;format="
+            % (formsemestre_id, op, bac, bacspecialite, sexe, statut)
+        )
+        doc, bacs, bacspecialites, sexes, statuts = graph_parcours(
+            context,
+            formsemestre_id,
+            only_primo=only_primo,
+            bac=bac,
+            bacspecialite=bacspecialite,
+            sexe=sexe,
+            statut=statut,
+        )
+
+        H = [
+            context.sco_header(
+                REQUEST,
+                page_title="Parcours étudiants de %(titreannee)s" % sem,
+                no_side_bar=True,
+            ),
+            """<h2 class="formsemestre">Parcours des étudiants de ce semestre</h2>""",
+            doc,
+            _gen_form_selectetuds(
+                formsemestre_id,
+                REQUEST=REQUEST,
+                only_primo=only_primo,
+                bac=bac,
+                bacspecialite=bacspecialite,
+                sexe=sexe,
+                statut=statut,
+                bacs=bacs,
+                bacspecialites=bacspecialites,
+                sexes=sexes,
+                statuts=statuts,
+                percent=0,
+            ),
+            """<p>Origine et devenir des étudiants inscrits dans %(titreannee)s"""
+            % sem,
+            # En Debian 4, dot ne genere pas du pdf, et epstopdf ne marche pas sur le .ps ou ps2 générés par dot
+            # mais c'est OK en Debian 5
+            """(<a href="%spdf">version pdf</a>""" % url,
+            """, <a href="%spng">image PNG</a>)""" % url,
+            """</p>""",
+            """<p class="help">Cette page ne s'affiche correctement que sur les navigateurs récents.</p>""",
+            """<p class="help">Le graphe permet de suivre les étudiants inscrits dans le semestre
+              sélectionné (dessiné en vert). Chaque rectangle représente un semestre (cliquez dedans
+              pour afficher son tableau de bord). Les flèches indiquent le nombre d'étudiants passant
+              d'un semestre à l'autre (s'il y en a moins de %s, vous pouvez visualiser leurs noms en
+              passant la souris sur le chiffre).
+              </p>"""
+            % MAX_ETUD_IN_DESCR,
+            context.sco_footer(REQUEST),
+        ]
+        REQUEST.RESPONSE.setHeader("content-type", "application/xhtml+xml")
+        return "\n".join(H)
+    else:
+        raise ValueError("invalid format: %s" % format)
diff --git a/sco_saisie_notes.py b/sco_saisie_notes.py
new file mode 100644
index 0000000000000000000000000000000000000000..883021959afedf71f0c4439bf01978b9e29b9de7
--- /dev/null
+++ b/sco_saisie_notes.py
@@ -0,0 +1,1270 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Saisie des notes
+
+   Formulaire revu en juillet 2016
+"""
+import datetime
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+from TrivialFormulator import TrivialFormulator, TF
+from notes_table import *
+import sco_formsemestre
+import sco_groups
+import sco_groups_view
+from sco_formsemestre_status import makeMenu
+import sco_evaluations
+import sco_undo_notes
+import htmlutils
+import sco_excel
+import scolars
+import sco_news
+from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC
+
+
+def convert_note_from_string(
+    note,
+    note_max,
+    note_min=NOTES_MIN,
+    etudid=None,
+    absents=[],
+    tosuppress=[],
+    invalids=[],
+):
+    """converti une valeur (chaine saisie) vers une note numérique (float)
+    Les listes absents, tosuppress et invalids sont modifiées
+    """
+    invalid = False
+    note_value = None
+    note = note.replace(",", ".")
+    if note[:3] == "ABS":
+        note_value = None
+        absents.append(etudid)
+    elif note[:3] == "NEU" or note[:3] == "EXC":
+        note_value = NOTES_NEUTRALISE
+    elif note[:3] == "ATT":
+        note_value = NOTES_ATTENTE
+    elif note[:3] == "SUP":
+        note_value = NOTES_SUPPRESS
+        tosuppress.append(etudid)
+    else:
+        try:
+            note_value = float(note)
+            if (note_value < note_min) or (note_value > note_max):
+                raise ValueError
+        except:
+            invalids.append(etudid)
+            invalid = True
+
+    return note_value, invalid
+
+
+def _displayNote(val):
+    """Convert note from DB to viewable string.
+    Utilisé seulement pour I/O vers formulaires (sans perte de precision)
+    (Utiliser fmt_note pour les affichages)
+    """
+    if val is None:
+        val = "ABS"
+    elif val == NOTES_NEUTRALISE:
+        val = "EXC"  # excuse, note neutralise
+    elif val == NOTES_ATTENTE:
+        val = "ATT"  # attente, note neutralise
+    elif val == NOTES_SUPPRESS:
+        val = "SUPR"
+    else:
+        val = "%g" % val
+    return val
+
+
+def _check_notes(notes, evaluation, mod):
+    """notes is a list of tuples (etudid, value)
+    mod is the module (used to ckeck type, for malus)
+    returns list of valid notes (etudid, float value)
+    and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
+    """
+    note_max = evaluation["note_max"]
+    if mod["module_type"] == MODULE_STANDARD:
+        note_min = NOTES_MIN
+    elif mod["module_type"] == MODULE_MALUS:
+        note_min = -20.0
+    else:
+        raise ValueError("Invalid module type")  # bug
+    L = []  # liste (etudid, note) des notes ok (ou absent)
+    invalids = []  # etudid avec notes invalides
+    withoutnotes = []  # etudid sans notes (champs vides)
+    absents = []  # etudid absents
+    tosuppress = []  # etudids avec ancienne note à supprimer
+
+    for (etudid, note) in notes:
+        note = str(note).strip().upper()
+        if note[:3] == "DEM":
+            continue  # skip !
+        if note:
+            value, invalid = convert_note_from_string(
+                note,
+                note_max,
+                note_min=note_min,
+                etudid=etudid,
+                absents=absents,
+                tosuppress=tosuppress,
+                invalids=invalids,
+            )
+            if not invalid:
+                L.append((etudid, value))
+        else:
+            withoutnotes.append(etudid)
+
+    return L, invalids, withoutnotes, absents, tosuppress
+
+
+def do_evaluation_upload_xls(context, REQUEST):
+    """
+    Soumission d'un fichier XLS (evaluation_id, notefile)
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    evaluation_id = REQUEST.form["evaluation_id"]
+    comment = REQUEST.form["comment"]
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+    M = context.do_moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    # Check access
+    # (admin, respformation, and responsable_id)
+    if not context.can_edit_notes(authuser, E["moduleimpl_id"]):
+        # XXX imaginer un redirect + msg erreur
+        raise AccessDenied("Modification des notes impossible pour %s" % authuser)
+    #
+    data = REQUEST.form["notefile"].read()
+    diag, lines = sco_excel.Excel_to_list(data)
+    try:
+        if not lines:
+            raise InvalidNoteValue()
+        # -- search eval code
+        n = len(lines)
+        i = 0
+        ok = True
+        while i < n:
+            if not lines[i]:
+                diag.append("Erreur: format invalide (ligne vide ?)")
+                raise InvalidNoteValue()
+            f0 = lines[i][0].strip()
+            if f0 and f0[0] == "!":
+                break
+            i = i + 1
+        if i == n:
+            diag.append("Erreur: format invalide ! (pas de ligne evaluation_id)")
+            raise InvalidNoteValue()
+
+        eval_id = lines[i][0].strip()[1:]
+        if eval_id != evaluation_id:
+            diag.append(
+                "Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('%s' != '%s')"
+                % (eval_id, evaluation_id)
+            )
+            raise InvalidNoteValue()
+        # --- get notes -> list (etudid, value)
+        # ignore toutes les lignes ne commençant pas par !
+        notes = []
+        ni = i + 1
+        try:
+            for line in lines[i + 1 :]:
+                if line:
+                    cell0 = line[0].strip()
+                    if cell0 and cell0[0] == "!":
+                        etudid = cell0[1:]
+                        if len(line) > 4:
+                            val = line[4].strip()
+                        else:
+                            val = ""  # ligne courte: cellule vide
+                        if etudid:
+                            notes.append((etudid, val))
+                ni += 1
+        except:
+            diag.append(
+                'Erreur: feuille invalide ! (erreur ligne %d)<br/>"%s"'
+                % (ni, str(lines[ni]))
+            )
+            raise InvalidNoteValue()
+        # -- check values
+        L, invalids, withoutnotes, absents, tosuppress = _check_notes(
+            notes, E, M["module"]
+        )
+        if len(invalids):
+            diag.append(
+                "Erreur: la feuille contient %d notes invalides</p>" % len(invalids)
+            )
+            if len(invalids) < 25:
+                etudsnames = [
+                    context.getEtudInfo(etudid=etudid, filled=True)[0]["nomprenom"]
+                    for etudid in invalids
+                ]
+                diag.append("Notes invalides pour: " + ", ".join(etudsnames))
+            raise InvalidNoteValue()
+        else:
+            nb_changed, nb_suppress, existing_decisions = _notes_add(
+                context, authuser, evaluation_id, L, comment
+            )
+            # news
+            cnx = context.GetDBConnexion()
+            E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+            M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+            mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+            mod["moduleimpl_id"] = M["moduleimpl_id"]
+            mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
+            sco_news.add(
+                context,
+                REQUEST,
+                typ=NEWS_NOTE,
+                object=M["moduleimpl_id"],
+                text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
+                url=mod["url"],
+            )
+
+            msg = (
+                "<p>%d notes changées (%d sans notes, %d absents, %d note supprimées)</p>"
+                % (nb_changed, len(withoutnotes), len(absents), nb_suppress)
+            )
+            if existing_decisions:
+                msg += """<p class="warning">Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !</p>"""
+            # msg += '<p>' + str(notes) # debug
+            return 1, msg
+
+    except InvalidNoteValue:
+        if diag:
+            msg = (
+                '<ul class="tf-msg"><li class="tf_msg">'
+                + '</li><li class="tf_msg">'.join(diag)
+                + "</li></ul>"
+            )
+        else:
+            msg = '<ul class="tf-msg"><li class="tf_msg">Une erreur est survenue</li></ul>'
+        return 0, msg + "<p>(pas de notes modifiées)</p>"
+
+
+def do_evaluation_set_missing(
+    context, evaluation_id, value, REQUEST=None, dialog_confirmed=False
+):
+    """Initialisation des notes manquantes
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    evaluation_id = REQUEST.form["evaluation_id"]
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+    M = context.do_moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    # Check access
+    # (admin, respformation, and responsable_id)
+    if not context.can_edit_notes(authuser, E["moduleimpl_id"]):
+        # XXX imaginer un redirect + msg erreur
+        raise AccessDenied("Modification des notes impossible pour %s" % authuser)
+    #
+    NotesDB = context._notes_getall(evaluation_id)
+    etudids = sco_groups.do_evaluation_listeetuds_groups(
+        context, evaluation_id, getallstudents=True, include_dems=False
+    )
+    notes = []
+    for etudid in etudids:  # pour tous les inscrits
+        if not NotesDB.has_key(etudid):  # pas de note
+            notes.append((etudid, value))
+    # Check value
+    L, invalids, withoutnotes, absents, tosuppress = _check_notes(notes, E, M["module"])
+    diag = ""
+    if len(invalids):
+        diag = "Valeur %s invalide" % value
+    if diag:
+        return (
+            context.sco_header(REQUEST)
+            + '<h2>%s</h2><p><a href="saisie_notes?evaluation_id=%s">Recommencer</a>'
+            % (diag, evaluation_id)
+            + context.sco_footer(REQUEST)
+        )
+    # Confirm action
+    if not dialog_confirmed:
+        return context.confirmDialog(
+            """<h2>Mettre toutes les notes manquantes de l'évaluation
+            à la valeur %s ?</h2>
+            <p>Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC)
+            n'a été rentrée seront affectés.</p>
+            <p><b>%d étudiants concernés par ce changement de note.</b></p>
+            <p class="warning">Attention, les étudiants sans notes de tous les groupes de ce semestre seront affectés.</p>
+            """
+            % (value, len(L)),
+            dest_url="",
+            REQUEST=REQUEST,
+            cancel_url="saisie_notes?evaluation_id=%s" % evaluation_id,
+            parameters={"evaluation_id": evaluation_id, "value": value},
+        )
+    # ok
+    comment = "Initialisation notes manquantes"
+    nb_changed, nb_suppress, existing_decisions = _notes_add(
+        context, authuser, evaluation_id, L, comment
+    )
+    # news
+    cnx = context.GetDBConnexion()
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    mod["moduleimpl_id"] = M["moduleimpl_id"]
+    mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
+    sco_news.add(
+        context,
+        REQUEST,
+        typ=NEWS_NOTE,
+        object=M["moduleimpl_id"],
+        text='Initialisation notes dans <a href="%(url)s">%(titre)s</a>' % mod,
+        url=mod["url"],
+    )
+    return (
+        context.sco_header(REQUEST)
+        + """<h2>%d notes changées</h2>
+               <ul>
+               <li><a class="stdlink" href="saisie_notes?evaluation_id=%s">
+               Revenir au formulaire de saisie des notes</a></li>
+               <li><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%s">
+               Tableau de bord du module</a></li>
+               </ul>
+               """
+        % (nb_changed, evaluation_id, M["moduleimpl_id"])
+        + context.sco_footer(REQUEST)
+    )
+
+
+def evaluation_suppress_alln(context, evaluation_id, REQUEST, dialog_confirmed=False):
+    "suppress all notes in this eval"
+    authuser = REQUEST.AUTHENTICATED_USER
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+
+    if context.can_edit_notes(authuser, E["moduleimpl_id"], allow_ens=False):
+        # On a le droit de modifier toutes les notes
+        # recupere les etuds ayant une note
+        NotesDB = context._notes_getall(evaluation_id)
+    elif context.can_edit_notes(authuser, E["moduleimpl_id"], allow_ens=True):
+        # Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
+        NotesDB = context._notes_getall(evaluation_id, by_uid=str(authuser))
+    else:
+        raise AccessDenied("Modification des notes impossible pour %s" % authuser)
+
+    notes = [(etudid, NOTES_SUPPRESS) for etudid in NotesDB.keys()]
+
+    if not dialog_confirmed:
+        nb_changed, nb_suppress, existing_decisions = _notes_add(
+            context, authuser, evaluation_id, notes, do_it=False
+        )
+        msg = "<p>Confirmer la suppression des %d notes ?</p>" % nb_suppress
+        if existing_decisions:
+            msg += """<p class="warning">Important: il y a déjà des décisions de jury enregistrées, qui seront potentiellement à revoir suite à cette modification !</p>"""
+        return context.confirmDialog(
+            msg,
+            dest_url="",
+            REQUEST=REQUEST,
+            OK="Supprimer les notes",
+            cancel_url="moduleimpl_status?moduleimpl_id=%s" % E["moduleimpl_id"],
+            parameters={"evaluation_id": evaluation_id},
+        )
+
+    # modif
+    nb_changed, nb_suppress, existing_decisions = _notes_add(
+        context, authuser, evaluation_id, notes, comment="effacer tout"
+    )
+    assert nb_changed == nb_suppress
+    H = ["<p>%s notes supprimées</p>" % nb_suppress]
+    if existing_decisions:
+        H.append(
+            """<p class="warning">Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !</p>"""
+        )
+    H += [
+        '<p><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%s">continuer</a>'
+        % E["moduleimpl_id"]
+    ]
+    # news
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    mod["moduleimpl_id"] = M["moduleimpl_id"]
+    cnx = context.GetDBConnexion()
+    mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
+    sco_news.add(
+        context,
+        REQUEST,
+        typ=NEWS_NOTE,
+        object=M["moduleimpl_id"],
+        text='Suppression des notes d\'une évaluation dans <a href="%(url)s">%(titre)s</a>'
+        % mod,
+        url=mod["url"],
+    )
+
+    return context.sco_header(REQUEST) + "\n".join(H) + context.sco_footer(REQUEST)
+
+
+def _notes_add(context, uid, evaluation_id, notes, comment=None, do_it=True):
+    """
+    Insert or update notes
+    notes is a list of tuples (etudid,value)
+    If do_it is False, simulate the process and returns the number of values that
+    WOULD be changed or suppressed.
+    Nota:
+    - si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
+    Return number of changed notes
+    """
+    uid = str(uid)
+    now = apply(
+        psycopg2.Timestamp, time.localtime()[:6]
+    )  # datetime.datetime.now().isoformat()
+    # Verifie inscription et valeur note
+    inscrits = {}.fromkeys(
+        sco_groups.do_evaluation_listeetuds_groups(
+            context, evaluation_id, getallstudents=True, include_dems=True
+        )
+    )
+    for (etudid, value) in notes:
+        if not ((value is None) or (type(value) == type(1.0))):
+            raise NoteProcessError(
+                "etudiant %s: valeur de note invalide (%s)" % (etudid, value)
+            )
+    # Recherche notes existantes
+    NotesDB = context._notes_getall(evaluation_id)
+    # Met a jour la base
+    cnx = context.GetDBConnexion(autocommit=False)
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    nb_changed = 0
+    nb_suppress = 0
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    existing_decisions = (
+        []
+    )  # etudids pour lesquels il y a une decision de jury et que la note change
+    try:
+        for (etudid, value) in notes:
+            changed = False
+            if not NotesDB.has_key(etudid):
+                # nouvelle note
+                if value != NOTES_SUPPRESS:
+                    if do_it:
+                        aa = {
+                            "etudid": etudid,
+                            "evaluation_id": evaluation_id,
+                            "value": value,
+                            "comment": comment,
+                            "uid": uid,
+                            "date": now,
+                        }
+                        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)",
+                            aa,
+                        )
+                    changed = True
+            else:
+                # il y a deja une note
+                oldval = NotesDB[etudid]["value"]
+                if type(value) != type(oldval):
+                    changed = True
+                elif type(value) == type(1.0) and (
+                    abs(value - oldval) > NOTES_PRECISION
+                ):
+                    changed = True
+                elif value != oldval:
+                    changed = True
+                if changed:
+                    # recopie l'ancienne note dans notes_notes_log, puis update
+                    if do_it:
+                        cursor.execute(
+                            "insert into notes_notes_log (etudid,evaluation_id,value,comment,date,uid) select etudid,evaluation_id,value,comment,date,uid from notes_notes where etudid=%(etudid)s and evaluation_id=%(evaluation_id)s",
+                            {"etudid": etudid, "evaluation_id": evaluation_id},
+                        )
+                        aa = {
+                            "etudid": etudid,
+                            "evaluation_id": evaluation_id,
+                            "value": value,
+                            "date": now,
+                            "comment": comment,
+                            "uid": uid,
+                        }
+                        quote_dict(aa)
+                    if value != NOTES_SUPPRESS:
+                        if do_it:
+                            cursor.execute(
+                                "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",
+                                aa,
+                            )
+                    else:  # suppression ancienne note
+                        if do_it:
+                            log(
+                                "_notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s"
+                                % (evaluation_id, etudid, oldval)
+                            )
+                            cursor.execute(
+                                "delete from notes_notes where etudid=%(etudid)s and evaluation_id=%(evaluation_id)s",
+                                aa,
+                            )
+                            # garde trace de la suppression dans l'historique:
+                            aa["value"] = NOTES_SUPPRESS
+                            cursor.execute(
+                                "insert into notes_notes_log (etudid,evaluation_id,value,comment,date,uid) values (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)",
+                                aa,
+                            )
+                        nb_suppress += 1
+            if changed:
+                nb_changed += 1
+                if has_existing_decision(context, M, E, etudid):
+                    existing_decisions.append(etudid)
+    except:
+        log("*** exception in _notes_add")
+        if do_it:
+            # inval cache
+            context._inval_cache(
+                formsemestre_id=M["formsemestre_id"]
+            )  # > modif notes (exception)
+            cnx.rollback()  # abort
+        raise  # re-raise exception
+    if do_it:
+        cnx.commit()
+        context._inval_cache(formsemestre_id=M["formsemestre_id"])  # > modif notes
+        context.get_evaluations_cache().inval_cache(key=evaluation_id)
+    return nb_changed, nb_suppress, existing_decisions
+
+
+def saisie_notes_tableur(context, evaluation_id, group_ids=[], REQUEST=None):
+    """Saisie des notes via un fichier Excel
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
+    if not evals:
+        raise ScoValueError("invalid evaluation_id")
+    E = evals[0]
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    formsemestre_id = M["formsemestre_id"]
+    if not context.can_edit_notes(authuser, E["moduleimpl_id"]):
+        return (
+            context.sco_header(REQUEST)
+            + "<h2>Modification des notes impossible pour %s</h2>" % authusername
+            + """<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>
+               """
+            % E["moduleimpl_id"]
+            + context.sco_footer(REQUEST)
+        )
+
+    if E["description"]:
+        page_title = 'Saisie des notes de "%s"' % E["description"]
+    else:
+        page_title = "Saisie des notes"
+
+    # Informations sur les groupes à afficher:
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context,
+        group_ids=group_ids,
+        formsemestre_id=formsemestre_id,
+        select_all_when_unspecified=True,
+        etat=None,
+        REQUEST=REQUEST,
+    )
+
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title=page_title,
+            javascripts=sco_groups_view.JAVASCRIPTS,
+            cssstyles=sco_groups_view.CSSSTYLES,
+            init_qtip=True,
+        ),
+        sco_evaluations.evaluation_describe(
+            context, evaluation_id=evaluation_id, REQUEST=REQUEST
+        ),
+        """<span class="eval_title">Saisie des notes par fichier</span>""",
+    ]
+
+    # Menu choix groupe:
+    H.append("""<div id="group-tabs"><table><tr><td>""")
+    H.append(sco_groups_view.form_groups_choice(context, groups_infos))
+    H.append("</td></tr></table></div>")
+
+    H.append(
+        """<div class="saisienote_etape1">
+        <span class="titredivsaisienote">Etape 1 : </span>
+        <ul>
+        <li><a href="feuille_saisie_notes?evaluation_id=%s&%s" class="stdlink" id="lnk_feuille_saisie">obtenir le fichier tableur à remplir</a></li>
+        <li>ou <a class="stdlink" href="saisie_notes?evaluation_id=%s">aller au formulaire de saisie</a></li>
+        </ul>
+        </div>
+        <form><input type="hidden" name="evaluation_id" id="formnotes_evaluation_id" value="%s"/></form>
+        """
+        % (evaluation_id, groups_infos.groups_query_args, evaluation_id, evaluation_id)
+    )
+
+    H.append(
+        """<div class="saisienote_etape2">
+    <span class="titredivsaisienote">Etape 2 : chargement d'un fichier de notes</span>"""  #'
+    )
+
+    nf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
+            (
+                "notefile",
+                {"input_type": "file", "title": "Fichier de note (.xls)", "size": 44},
+            ),
+            (
+                "comment",
+                {
+                    "size": 44,
+                    "title": "Commentaire",
+                    "explanation": "(la colonne remarque du fichier excel est ignorée)",
+                },
+            ),
+        ),
+        formid="notesfile",
+        submitlabel="Télécharger",
+    )
+    if nf[0] == 0:
+        H.append(
+            """<p>Le fichier doit être un fichier tableur obtenu via
+        l'étape 1 ci-dessus, puis complété et enregistré au format Excel.
+        </p>"""
+        )
+        H.append(nf[1])
+    elif nf[0] == -1:
+        H.append("<p>Annulation</p>")
+    elif nf[0] == 1:
+        updiag = do_evaluation_upload_xls(context, REQUEST)
+        if updiag[0]:
+            H.append(updiag[1])
+            H.append(
+                """<p>Notes chargées.&nbsp;&nbsp;&nbsp;
+            <a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">
+            Revenir au tableau de bord du module</a>
+            &nbsp;&nbsp;&nbsp;
+            <a class="stdlink" href="saisie_notes?evaluation_id=%(evaluation_id)s">Charger d'autres notes dans cette évaluation</a>
+            </p>"""
+                % E
+            )
+        else:
+            H.append("""<p class="redboldtext">Notes non chargées !</p>""" + updiag[1])
+            H.append(
+                """
+            <p><a class="stdlink" href="saisie_notes_tableur?evaluation_id=%(evaluation_id)s">
+            Reprendre</a>
+            </p>"""
+                % E
+            )
+    #
+    H.append("""</div><h3>Autres opérations</h3><ul>""")
+    if context.can_edit_notes(
+        REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False
+    ):
+        H.append(
+            """
+        <li>
+        <form action="do_evaluation_set_missing" method="get">
+        Mettre toutes les notes manquantes à <input type="text" size="5" name="value"/>
+        <input type="submit" value="OK"/> 
+        <input type="hidden" name="evaluation_id" value="%s"/> 
+        <em>ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
+        </form>
+        </li>        
+        <li><a class="stdlink" href="evaluation_suppress_alln?evaluation_id=%s">Effacer toutes les notes de cette évaluation</a> (ceci permet ensuite de supprimer l'évaluation si besoin)
+        </li>"""
+            % (evaluation_id, evaluation_id)
+        )  #'
+    H.append(
+        """<li><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">Revenir au module</a></li>
+    <li><a class="stdlink" href="saisie_notes?evaluation_id=%(evaluation_id)s">Revenir au formulaire de saisie</a></li>
+    </ul>"""
+        % E
+    )
+
+    H.append(
+        """<h3>Explications</h3>
+<ol>
+<li>Etape 1: 
+<ol><li>choisir le ou les groupes d'étudiants;</li>
+    <li>télécharger le fichier Excel à remplir.</li>
+</ol>
+</li>
+<li>Etape 2 (cadre vert): Indiquer le fichier Excel <em>téléchargé à l'étape 1</em> et dans lequel on a saisi des notes. Remarques:
+<ul>
+<li>le fichier Excel peut être incomplet: on peut ne saisir que quelques notes et répéter l'opération (en téléchargeant un nouveau fichier) plus tard;</li>
+<li>seules les valeurs des notes modifiées sont prises en compte;</li>
+<li>seules les notes sont extraites du fichier Excel;</li>
+<li>on peut optionnellement ajouter un commentaire (type "copies corrigées par Dupont", ou "Modif. suite à contestation") dans la case "Commentaire".
+</li>
+<li>le fichier Excel <em>doit impérativement être celui chargé à l'étape 1 pour cette évaluation</em>. Il n'est pas possible d'utiliser une liste d'appel ou autre document Excel téléchargé d'une autre page.</li>
+</ul>
+</li>
+</ol>
+"""
+    )
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def feuille_saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None):
+    """Document Excel pour saisie notes dans l'évaluation et les groupes indiqués
+    """
+    evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
+    if not evals:
+        raise ScoValueError("invalid evaluation_id")
+    E = evals[0]
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    formsemestre_id = M["formsemestre_id"]
+    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"])
+    if E["jour"]:
+        indication_date = DateDMYtoISO(E["jour"])
+    else:
+        indication_date = sanitize_filename(E["description"])[:12]
+    evalname = "%s-%s" % (Mod["code"], indication_date)
+
+    if E["description"]:
+        evaltitre = "%s du %s" % (E["description"], E["jour"])
+    else:
+        evaltitre = "évaluation du %s" % E["jour"]
+    description = "%s en %s (%s) resp. %s" % (
+        evaltitre,
+        Mod["abbrev"],
+        Mod["code"],
+        strcapitalize(M["responsable_id"]),
+    )
+
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context,
+        group_ids=group_ids,
+        formsemestre_id=formsemestre_id,
+        select_all_when_unspecified=True,
+        etat=None,
+        REQUEST=REQUEST,
+    )
+    groups = sco_groups.listgroups(context, groups_infos.group_ids)
+    gr_title_filename = sco_groups.listgroups_filename(groups)
+    gr_title = sco_groups.listgroups_abbrev(groups)
+    if None in [g["group_name"] for g in groups]:  # tous les etudiants
+        getallstudents = True
+        gr_title = "tous"
+        gr_title_filename = "tous"
+    else:
+        getallstudents = False
+    etudids = sco_groups.do_evaluation_listeetuds_groups(
+        context, evaluation_id, groups, getallstudents=getallstudents, include_dems=True
+    )
+    # Notes existantes
+    NotesDB = context._notes_getall(evaluation_id)
+
+    # une liste de liste de chaines: lignes de la feuille de calcul
+    L = []
+
+    etuds = _get_sorted_etuds(context, E, etudids, formsemestre_id)
+    for e in etuds:
+        etudid = e["etudid"]
+        groups = sco_groups.get_etud_groups(context, etudid, sem)
+        grc = sco_groups.listgroups_abbrev(groups)
+
+        L.append(
+            [
+                "%s" % etudid,
+                strupper(e["nom"]),
+                strcapitalize(strlower(e["prenom"])),
+                e["inscr"]["etat"],
+                grc,
+                e["val"],
+                e["explanation"],
+            ]
+        )
+
+    filename = "notes_%s_%s.xls" % (evalname, gr_title_filename)
+    xls = sco_excel.Excel_feuille_saisie(E, sem["titreannee"], description, lines=L)
+    return sco_excel.sendExcelFile(REQUEST, xls, filename)
+
+
+def has_existing_decision(context, M, E, etudid):
+    """Verifie s'il y a une validation pour cet etudiant dans ce semestre ou UE
+    Si oui, return True
+    """
+    formsemestre_id = M["formsemestre_id"]
+    nt = context._getNotesCache().get_NotesTable(
+        context, formsemestre_id
+    )  # > get_etud_decision_sem, get_etud_decision_ues
+    if nt.get_etud_decision_sem(etudid):
+        return True
+    dec_ues = nt.get_etud_decision_ues(etudid)
+    if dec_ues:
+        mod = context.do_module_list({"module_id": M["module_id"]})[0]
+        ue_id = mod["ue_id"]
+        if ue_id in dec_ues:
+            return True  # decision pour l'UE a laquelle appartient cette evaluation
+
+    return False  # pas de decision de jury affectee par cette note
+
+
+# -----------------------------
+# Nouveau formulaire saisie notes (2016)
+
+
+def saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None):
+    """Formulaire saisie notes d'une évaluation pour un groupe
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    authusername = str(authuser)
+
+    evals = context.do_evaluation_list({"evaluation_id": evaluation_id})
+    if not evals:
+        raise ScoValueError("invalid evaluation_id")
+    E = evals[0]
+    M = context.do_moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    formsemestre_id = M["formsemestre_id"]
+    # Check access
+    # (admin, respformation, and responsable_id)
+    if not context.can_edit_notes(authuser, E["moduleimpl_id"]):
+        return (
+            context.sco_header(REQUEST)
+            + "<h2>Modification des notes impossible pour %s</h2>" % authusername
+            + """<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>
+               """
+            % E["moduleimpl_id"]
+            + context.sco_footer(REQUEST)
+        )
+
+    # Informations sur les groupes à afficher:
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context,
+        group_ids=group_ids,
+        formsemestre_id=formsemestre_id,
+        select_all_when_unspecified=True,
+        etat=None,
+        REQUEST=REQUEST,
+    )
+
+    if E["description"]:
+        page_title = 'Saisie "%s"' % E["description"]
+    else:
+        page_title = "Saisie des notes"
+
+    # HTML page:
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title=page_title,
+            javascripts=sco_groups_view.JAVASCRIPTS + ["js/saisie_notes.js"],
+            cssstyles=sco_groups_view.CSSSTYLES,
+            init_qtip=True,
+        ),
+        sco_evaluations.evaluation_describe(
+            context, evaluation_id=evaluation_id, REQUEST=REQUEST
+        ),
+        '<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>',
+    ]
+    H.append("""<div id="group-tabs"><table><tr><td>""")
+    H.append(sco_groups_view.form_groups_choice(context, groups_infos))
+    H.append('</td><td style="padding-left: 35px;">')
+    H.append(
+        makeMenu(
+            "Autres opérations",
+            [
+                {
+                    "title": "Saisie par fichier tableur",
+                    "id": "menu_saisie_tableur",
+                    "url": "/saisie_notes_tableur?evaluation_id=%s&%s"
+                    % (E["evaluation_id"], groups_infos.groups_query_args),
+                },
+                {
+                    "title": "Voir toutes les notes du module",
+                    "url": "/evaluation_listenotes?moduleimpl_id=%s"
+                    % E["moduleimpl_id"],
+                },
+                {
+                    "title": "Effacer toutes les notes de cette évaluation",
+                    "url": "/evaluation_suppress_alln?evaluation_id=%s"
+                    % (E["evaluation_id"],),
+                },
+            ],
+            base_url=context.absolute_url(),
+            alone=True,
+        )
+    )
+    H.append("""</td></tr></table></div>""")
+
+    # Le formulaire de saisie des notes:
+    form = _form_saisie_notes(context, E, M, groups_infos.group_ids, REQUEST=REQUEST)
+    if form is None:
+        return ""  # redirect
+    H.append(form)
+    #
+    H.append("</div>")  # /saisie_notes
+
+    H.append(
+        """<div class="sco_help">
+    <p>Les modifications sont enregistrées au fur et à mesure.</p>
+    <h4>Codes spéciaux:</h4>
+    <ul>
+    <li>ABS: absent (compte comme un zéro)</li>
+    <li>EXC: excusé (note neutralisée)</li>
+    <li>SUPR: pour supprimer une note existante</li>
+    <li>ATT: note en attente (permet de publier une évaluation avec des notes manquantes)</li>
+    </ul>
+    </div>"""
+    )
+
+    H.append(context.sco_footer(REQUEST))
+    return "\n".join(H)
+
+
+def _get_sorted_etuds(context, E, etudids, formsemestre_id):
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    NotesDB = context._notes_getall(E["evaluation_id"])  # Notes existantes
+    cnx = context.GetDBConnexion()
+    etuds = []
+    for etudid in etudids:
+        # infos identite etudiant
+        e = scolars.etudident_list(cnx, {"etudid": etudid})[0]
+        scolars.format_etud_ident(e)
+        etuds.append(e)
+        # infos inscription dans ce semestre
+        e["inscr"] = context.do_formsemestre_inscription_list(
+            {"etudid": etudid, "formsemestre_id": formsemestre_id}
+        )[0]
+        # Groupes auxquels appartient cet étudiant:
+        e["groups"] = sco_groups.get_etud_groups(context, etudid, sem)
+
+        # Information sur absence (tenant compte de la demi-journée)
+        jour_iso = DateDMYtoISO(E["jour"])
+        warn_abs_lst = []
+        if E["matin"]:
+            nbabs = context.Absences.CountAbs(etudid, jour_iso, jour_iso, matin=1)
+            nbabsjust = context.Absences.CountAbsJust(
+                etudid, jour_iso, jour_iso, matin=1
+            )
+            if nbabs:
+                if nbabsjust:
+                    warn_abs_lst.append("absent justifié le matin !")
+                else:
+                    warn_abs_lst.append("absent le matin !")
+        if E["apresmidi"]:
+            nbabs = context.Absences.CountAbs(etudid, jour_iso, jour_iso, matin=0)
+            nbabsjust = context.Absences.CountAbsJust(
+                etudid, jour_iso, jour_iso, matin=0
+            )
+            if nbabs:
+                if nbabsjust:
+                    warn_abs_lst.append("absent justifié l'après-midi !")
+                else:
+                    warn_abs_lst.append("absent l'après-midi !")
+
+        e["absinfo"] = '<span class="sn_abs">' + " ".join(warn_abs_lst) + "</span>  "
+
+        # Note actuelle de l'étudiant:
+        if NotesDB.has_key(etudid):
+            e["val"] = _displayNote(NotesDB[etudid]["value"])
+            comment = NotesDB[etudid]["comment"]
+            if comment is None:
+                comment = ""
+            e["explanation"] = "%s (%s) %s" % (
+                NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
+                NotesDB[etudid]["uid"],
+                comment,
+            )
+        else:
+            e["val"] = ""
+            e["explanation"] = ""
+        # Démission ?
+        if e["inscr"]["etat"] == "D":
+            # if not e['val']:
+            e["val"] = "DEM"
+            e["explanation"] = "Démission"
+
+    etuds.sort(key=lambda x: (x["nom"], x["prenom"]))
+
+    return etuds
+
+
+def _form_saisie_notes(context, E, M, group_ids, REQUEST=None):
+    """Formulaire HTML saisie des notes  dans l'évaluation E du moduleimpl M
+    pour les groupes indiqués.
+
+    On charge tous les étudiants, ne seront montrés que ceux
+    des groupes sélectionnés grace a un filtre en javascript.
+    """
+    evaluation_id = E["evaluation_id"]
+    formsemestre_id = M["formsemestre_id"]
+    groups = sco_groups.listgroups(context, group_ids)
+
+    etudids = sco_groups.do_evaluation_listeetuds_groups(
+        context, evaluation_id, getallstudents=True, include_dems=True
+    )
+    if not etudids:
+        return '<div class="ue_warning"><span>Aucun étudiant sélectionné !</span></div>'
+
+    # Decisions de jury existantes ?
+    decisions_jury = {
+        etudid: has_existing_decision(context, M, E, etudid) for etudid in etudids
+    }
+    nb_decisions = sum(
+        decisions_jury.values()
+    )  # Nb de decisions de jury (pour les inscrits à l'évaluation)
+
+    etuds = _get_sorted_etuds(context, E, etudids, formsemestre_id)
+
+    # Build form:
+    descr = [
+        ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
+        ("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
+        ("group_ids", {"default": group_ids, "input_type": "hidden", "type": "list"}),
+        # ('note_method', { 'default' : note_method, 'input_type' : 'hidden'}),
+        ("comment", {"size": 44, "title": "Commentaire", "return_focus_next": True}),
+        ("changed", {"default": "0", "input_type": "hidden"}),  # changed in JS
+    ]
+    if M["module"]["module_type"] == MODULE_STANDARD:
+        descr.append(
+            (
+                "s3",
+                {
+                    "input_type": "text",  # affiche le barème
+                    "title": "Notes ",
+                    "cssclass": "formnote_bareme",
+                    "readonly": True,
+                    "default": "&nbsp;/ %g" % E["note_max"],
+                },
+            )
+        )
+    elif M["module"]["module_type"] == MODULE_MALUS:
+        descr.append(
+            (
+                "s3",
+                {
+                    "input_type": "text",  # affiche le barème
+                    "title": "",
+                    "cssclass": "formnote_bareme",
+                    "readonly": True,
+                    "default": "Points de malus (soustraits à la moyenne de l'UE, entre -20 et 20)",
+                },
+            )
+        )
+    else:
+        raise ValueError("invalid module type (%s)" % M["module"]["module_type"])  # bug
+
+    initvalues = {}
+    for e in etuds:
+        etudid = e["etudid"]
+        disabled = e["val"] == "DEM"
+        etud_classes = []
+        if disabled:
+            classdem = " etud_dem"
+            etud_classes.append("etud_dem")
+            disabled_attr = 'disabled="%d"' % disabled
+        else:
+            classdem = ""
+            disabled_attr = ""
+        # attribue a chaque element une classe css par groupe:
+        for group_info in e["groups"]:
+            etud_classes.append(group_info["group_id"])
+
+        label = '<span class="%s">' % classdem + e["nomprenom"] + "</span>"
+
+        # Historique des saisies de notes:
+        if not disabled:
+            explanation = (
+                '<span id="hist_%s">' % etudid
+                + get_note_history_menu(context, evaluation_id, etudid)
+                + "</span>"
+            )
+        else:
+            explanation = ""
+        explanation = e["absinfo"] + explanation
+
+        # Lien modif decision de jury:
+        explanation += '<span id="jurylink_%s" class="jurylink"></span>' % etudid
+
+        # Valeur actuelle du champ:
+        initvalues["note_" + etudid] = e["val"]
+        label_link = '<a class="etudinfo" id="%s">%s</a>' % (etudid, label)
+
+        # Element de formulaire:
+        descr.append(
+            (
+                "note_" + etudid,
+                {
+                    "size": 5,
+                    "title": label_link,
+                    "explanation": explanation,
+                    "return_focus_next": True,
+                    "attributes": [
+                        'class="note%s"' % classdem,
+                        disabled_attr,
+                        "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)
+                    + """"><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>""",
+                },
+            )
+        )
+    #
+    H = []
+    if nb_decisions > 0:
+        H.append(
+            """<div class="saisie_warn">
+        <ul class="tf-msg">
+        <li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour %d étudiants. Après changement des notes, vérifiez la situation !</li>
+        </ul>
+        </div>"""
+            % nb_decisions
+        )
+    # H.append('''<div id="sco_msg" class="head_message"></div>''')
+
+    destination = "%s/Notes/moduleimpl_status?moduleimpl_id=%s" % (
+        context.ScoURL(),
+        M["moduleimpl_id"],
+    )
+
+    tf = TF(
+        destination,
+        REQUEST.form,
+        descr,
+        initvalues=initvalues,
+        submitlabel="Terminer",
+        formid="formnotes",
+    )
+    H.append(tf.getform())  # check and init
+    if tf.canceled():
+        REQUEST.RESPONSE.redirect(destination)
+        return None
+    elif (not tf.submitted()) or not tf.result:
+        # ajout formularie saisie notes manquantes
+        H.append(
+            """
+        <div>
+        <form action="do_evaluation_set_missing" method="get">
+        Mettre <em>toutes</em> les notes manquantes à <input type="text" size="5" name="value"/>
+        <input type="submit" value="OK"/> 
+        <input type="hidden" name="evaluation_id" value="%s"/> 
+        <em>affecte tous les groupes. ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
+        </form>
+        </div>
+        """
+            % evaluation_id
+        )
+        # affiche formulaire
+        return "\n".join(H)
+    else:
+        # form submission
+        # rien à faire
+        REQUEST.RESPONSE.redirect(destination)
+        return None
+
+
+def save_note(
+    context, etudid=None, evaluation_id=None, value=None, comment="", REQUEST=None
+):
+    """Enregistre une note (ajax)    
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    log(
+        "save_note: evaluation_id=%s etudid=%s uid=%s value=%s"
+        % (evaluation_id, etudid, authuser, value)
+    )
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+    Mod = context.do_module_list(args={"module_id": M["module_id"]})[0]
+    Mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % M
+    result = {"nbchanged": 0}  # JSON
+    # Check access: admin, respformation, or responsable_id
+    if not context.can_edit_notes(authuser, E["moduleimpl_id"]):
+        result["status"] = "unauthorized"
+    else:
+        L, invalids, withoutnotes, absents, tosuppress = _check_notes(
+            [(etudid, value)], E, Mod
+        )
+        if L:
+            nbchanged, nbsuppress, existing_decisions = _notes_add(
+                context, authuser, evaluation_id, L, comment=comment, do_it=True
+            )
+            sco_news.add(
+                context,
+                REQUEST,
+                typ=NEWS_NOTE,
+                object=M["moduleimpl_id"],
+                text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
+                url=Mod["url"],
+                max_frequency=30 * 60,  # 30 minutes
+            )
+            result["nbchanged"] = nbchanged
+            result["existing_decisions"] = existing_decisions
+            if nbchanged > 0:
+                result["history_menu"] = get_note_history_menu(
+                    context, evaluation_id, etudid
+                )
+            else:
+                result["history_menu"] = ""  # no update needed
+        result["status"] = "ok"
+
+    # time.sleep(5)
+    return sendJSON(REQUEST, result)
+
+
+def get_note_history_menu(context, evaluation_id, etudid):
+    """Menu HTML historique de la note"""
+    history = sco_undo_notes.get_note_history(context, evaluation_id, etudid)
+    if not history:
+        return ""
+
+    H = []
+    if len(history) > 1:
+        H.append(
+            '<select data-etudid="%s" class="note_history" onchange="change_history(this);">'
+            % etudid
+        )
+        envir = "select"
+        item = "option"
+    else:
+        # pas de menu
+        H.append('<span class="history">')
+        envir = "span"
+        item = "span"
+
+    first = True
+    for i in history:
+        jt = i["date"].strftime("le %d/%m/%Y à %H:%M") + " (%s)" % i["user_name"]
+        dispnote = _displayNote(i["value"])
+        if first:
+            nv = ""  # ne repete pas la valeur de la note courante
+        else:
+            # ancienne valeur
+            nv = '<span class="histvalue">: %s</span>' % dispnote
+        first = False
+        if i["comment"]:
+            comment = ' <span class="histcomment">%s</span>' % i["comment"]
+        else:
+            comment = ""
+        H.append(
+            '<%s data-note="%s">%s %s%s</%s>' % (item, dispnote, jt, nv, comment, item)
+        )
+
+    H.append("</%s>" % envir)
+    return "\n".join(H)
diff --git a/sco_semset.py b/sco_semset.py
new file mode 100644
index 0000000000000000000000000000000000000000..65e3219f085e363575ca11abe7533f2b860176d6
--- /dev/null
+++ b/sco_semset.py
@@ -0,0 +1,530 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Gestion des ensembles de semestres:
+
+class SemSet:  un ensemble de semestres d'un département, à exporter ves Apogée. En principe de la meme annee scolaire.
+ 
+ SemSet.annees_scolaires() : les annees scolaires. e.g. [ 2015, 2016 ], ou le plus souvent, une seule: [2016]
+ SemSet.list_etapes(): listes des étapes apogee et vdi des semestres (instances de ApoEtapeVDI)
+
+ SemSet.add(sem): ajoute un semestre à l'ensemble
+
+
+sem_set_list(context)
+
+"""
+
+from sco_utils import *
+from notesdb import *
+from notes_log import log
+import sco_formsemestre
+from sco_formsemestre import ApoEtapeVDI
+import sco_formsemestre_status
+import sco_etape_apogee
+from sco_etape_bilan import EtapeBilan
+import sco_portal_apogee
+from gen_tables import GenTable
+
+
+_semset_editor = EditableTable(
+    "notes_semset", "semset_id", ("semset_id", "title", "annee_scolaire", "sem_id")
+)
+
+semset_create = _semset_editor.create
+semset_edit = _semset_editor.edit
+semset_list = _semset_editor.list
+semset_delete = _semset_editor.delete
+
+
+class SemSet(dict):
+    def __init__(self, context, semset_id=None, title="", annee_scolaire="", sem_id=""):
+        """Load and init, or, if semset_id is not specified, create
+        """
+        if not annee_scolaire and not semset_id:
+            # on autorise annee_scolaire null si sem_id pour pouvoir lire les anciens semsets
+            # mal construits...
+            raise ScoValueError("Année scolaire invalide !")
+        self.semset_id = semset_id
+        self["semset_id"] = semset_id
+        self.context = context
+        self.sems = []
+        self.formsemestre_ids = []
+        cnx = context.GetDBConnexion()
+        if semset_id:  # read existing set
+            L = semset_list(cnx, args={"semset_id": semset_id})
+            if not L:
+                raise ValueError("invalid semset_id %s" % semset_id)
+            self["title"] = L[0]["title"]
+            self["annee_scolaire"] = L[0]["annee_scolaire"]
+            self["sem_id"] = L[0]["sem_id"]
+            r = SimpleDictFetch(
+                context,
+                "SELECT formsemestre_id FROM notes_semset_formsemestre WHERE semset_id = %(semset_id)s",
+                {"semset_id": semset_id},
+            )
+            if r:
+                self.formsemestre_ids = {x["formsemestre_id"] for x in r}  # a set
+        else:  # create a new empty set
+            self.semset_id = semset_create(
+                cnx,
+                {"title": title, "annee_scolaire": annee_scolaire, "sem_id": sem_id},
+            )
+            log("created new semset_id=%s" % self.semset_id)
+        self.load_sems()
+        # analyse des semestres pour construire le bilan par semestre et par étape
+        self.bilan = EtapeBilan(context)
+        for sem in self.sems:
+            self.bilan.add_sem(sem)
+
+    def delete(self):
+        """delete"""
+        cnx = self.context.GetDBConnexion()
+        semset_delete(cnx, self.semset_id)
+
+    def edit(self, args):
+        cnx = self.context.GetDBConnexion()
+        semset_edit(cnx, args)
+
+    def load_sems(self):
+        """Load formsemestres"""
+        self.sems = []
+        for formsemestre_id in self.formsemestre_ids:
+            self.sems.append(
+                sco_formsemestre.get_formsemestre(self.context, formsemestre_id)
+            )
+
+        if self.sems:
+            self["date_debut"] = min([sem["date_debut_iso"] for sem in self.sems])
+            self["date_fin"] = max([sem["date_fin_iso"] for sem in self.sems])
+        else:
+            self["date_debut"] = ""
+            self["date_fin"] = ""
+
+        self["etapes"] = self.list_etapes()
+        self["semtitles"] = [sem["titre_num"] for sem in self.sems]
+
+        # Construction du ou des lien(s) vers le semestre
+        pattern = '<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a>'
+        self["semlinks"] = [(pattern % sem) for sem in self.sems]
+        self["semtitles_str"] = "<br/>".join(self["semlinks"])
+
+    def fill_formsemestres(self, REQUEST):
+        for sem in self.sems:
+            sco_formsemestre_status.fill_formsemestre(self.context, sem, REQUEST)
+            ets = sco_etape_apogee.apo_get_sem_etapes(self.context, sem)
+            sem["etapes_apo_str"] = sco_formsemestre.etapes_apo_str(sorted(list(ets)))
+
+    def add(self, formsemestre_id):
+        # check
+        if formsemestre_id in self.formsemestre_ids:
+            return  # already there
+        if formsemestre_id not in [
+            sem["formsemestre_id"] for sem in self.list_possible_sems()
+        ]:
+            raise ValueError(
+                "can't add %s to set %s: incompatible sem_id"
+                % (formsemestre_id, self.semset_id)
+            )
+
+        SimpleQuery(
+            self.context,
+            "INSERT INTO notes_semset_formsemestre (formsemestre_id, semset_id) VALUES (%(formsemestre_id)s, %(semset_id)s)",
+            {"formsemestre_id": formsemestre_id, "semset_id": self.semset_id},
+        )
+        self.load_sems()  # update our list
+
+    def remove(self, formsemestre_id):
+        SimpleQuery(
+            self.context,
+            "DELETE FROM notes_semset_formsemestre WHERE semset_id=%(semset_id)s AND formsemestre_id=%(formsemestre_id)s",
+            {"formsemestre_id": formsemestre_id, "semset_id": self.semset_id},
+        )
+        self.load_sems()  # update our list
+
+    def annees_scolaires(self):
+        """Les annees scolaires. e.g. [ 2015, 2016 ], ou le plus souvent, une seule: [2016]
+        L'année scolaire est l'année de début du semestre (2015 pour 2015-2016)
+        """
+        annees = list(set([int(s["annee_debut"]) for s in self.sems]))
+        annees.sort()
+        return annees
+
+    def list_etapes(self):
+        """Listes triée des étapes Apogée des semestres (instances de ApoEtapeVDI).
+        Chaque étape apparait une seule fois, dans sa forme la plus générale.
+        Si on a [ 'V1RT', 'V1RT!111' ], le résultat sera [ 'V1RT' ]
+        Si on a [ 'V1RT!111', 'V1RT!112' ], le résultat sera [ 'V1RT!111', 'V1RT!112' ]
+        """
+        D = {}  # { etape : { versions vdi } }
+        for s in self.sems:
+            for et in s["etapes"]:
+                if et:
+                    if et.etape in D:
+                        D[et.etape].add(et.vdi)
+                    else:
+                        D[et.etape] = {et.vdi}
+        # enlève les versions excédentaires:
+        for etape in D:
+            if "" in D[etape]:
+                D[etape] = [""]
+        # forme liste triée d'instances:
+        etapes = []
+        for etape in D:
+            for vdi in D[etape]:
+                etapes.append(ApoEtapeVDI(etape=etape, vdi=vdi))
+        etapes.sort()
+        return etapes
+
+    def list_possible_sems(self):
+        """List sems that can be added to this set
+        """
+        sems = sco_formsemestre.do_formsemestre_list(self.context)
+        # remove sems already here:
+        sems = [
+            sem for sem in sems if sem["formsemestre_id"] not in self.formsemestre_ids
+        ]
+        # filter annee, sem_id:
+        # Remplacement du filtre de proposition des semestres potentiels
+        # au lieu de la parité (sem 1 et 3 / sem 2 et 4) on filtre sur la date de
+        # debut du semestre: ceci permet d'ajouter les semestres décalés
+        if self["annee_scolaire"]:
+            sems = [
+                sem
+                for sem in sems
+                if sco_formsemestre.sem_in_semestre_scolaire(
+                    self.context,
+                    sem,
+                    year=self["annee_scolaire"],
+                    saison=self["sem_id"],
+                )
+            ]
+        return sems
+
+    def load_etuds(self):
+        context = self.context
+        self["etuds_without_nip"] = set()  # etudids
+        self["jury_ok"] = True
+        for sem in self.sems:
+            nt = context._getNotesCache().get_NotesTable(
+                context, sem["formsemestre_id"]
+            )
+            sem["etuds"] = nt.identdict.values()
+            sem["nips"] = {e["code_nip"] for e in sem["etuds"] if e["code_nip"]}
+            sem["etuds_without_nip"] = {
+                e["etudid"] for e in sem["etuds"] if not e["code_nip"]
+            }
+            self["etuds_without_nip"] |= sem["etuds_without_nip"]
+            sem["jury_ok"] = nt.all_etuds_have_sem_decisions()
+            self["jury_ok"] &= sem["jury_ok"]
+
+    def html_descr(self):
+        """Short HTML description
+        """
+        H = [
+            """<span class="box_title">Ensemble de semestres %(title)s</span>""" % self
+        ]
+        if self["annee_scolaire"]:
+            H.append("<p>Année scolaire: %(annee_scolaire)s</p>" % self)
+        else:
+            H.append(
+                "<p>Année(s) scolaire(s) présentes: %s"
+                % ", ".join([str(x) for x in self.annees_scolaires()])
+            )
+            if len(self.annees_scolaires()) > 1:
+                H.append(
+                    ' <span class="redboldtext">(attention, plusieurs années !)</span>'
+                )
+            H.append("</p>")
+        if self["sem_id"]:
+            H.append(
+                "<p>Période: %(sem_id)s (<em>1: septembre, 2: janvier</em>)</p>" % self
+            )
+        H.append(
+            "<p>Etapes: <tt>%s</tt></p>"
+            % sco_formsemestre.etapes_apo_str(self.list_etapes())
+        )
+        H.append("""<h4>Semestres de l'ensemble:</h4><ul class="semset_listsems">""")
+
+        for sem in self.sems:
+            H.append(
+                '<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a> %(mois_debut)s - %(mois_fin)s'
+                % sem
+            )
+            H.append(
+                ' <a class="stdlink" href="do_semset_remove_sem?semset_id=%s&formsemestre_id=%s">(retirer)</a>'
+                % (self["semset_id"], sem["formsemestre_id"])
+            )
+            H.append(
+                "<br/>Etapes: <tt>%(etapes_apo_str)s</tt>, %(nbinscrits)s inscrits"
+                % sem
+            )
+            H.append("<br/>Elément Apogée année: ")
+            if sem["elt_annee_apo"]:
+                H.append("<tt>%(elt_annee_apo)s</tt>" % sem)
+            else:
+                H.append('<span style="color: red;">manquant</span>')
+
+            H.append("<br/>Elément Apogée semestre: ")
+            if sem["elt_sem_apo"]:
+                H.append("<tt>%(elt_sem_apo)s</tt>" % sem)
+            else:
+                H.append('<span style="color: red;">manquant</span>')
+
+            H.append("</br><em>vérifier les semestres antécédents !</em>")
+            H.append("</li>")
+
+        return "\n".join(H)
+
+    def html_form_sems(self):
+        """HTML form to manage sems"""
+        H = []
+        possible_sems = self.list_possible_sems()
+        if possible_sems:
+            menu_sem = """<select name="formsemestre_id">
+            <option value="" selected>(semestre)</option>"""
+            for sem in possible_sems:
+                menu_sem += (
+                    """<option value="%(formsemestre_id)s">%(titreannee)s</option>\n"""
+                    % sem
+                )
+            menu_sem += """</select>"""
+            H.append(
+                '<form action="do_semset_add_sem" method="post">Ajouter un semestre:'
+            )
+            H.append(menu_sem)
+            H.append(
+                '<input type="hidden" name="semset_id" value="%s"/>' % self.semset_id
+            )
+            H.append('<input type="submit" value="Ajouter"/>')
+            H.append("</form>")
+        else:
+            H.append("<em>pas de semestres à ajouter</em>")
+        return "\n".join(H)
+
+    def html_diagnostic(self):
+        """Affichage de la partie Effectifs et Liste des étudiants
+        (actif seulement si un portail est configuré)
+        """
+        if sco_portal_apogee.has_portal(self.context):
+            return self.bilan.html_diagnostic()
+        else:
+            return ""
+
+
+def get_semsets_list(context):
+    """Liste de tous les semsets
+    Trié par date_debut, le plus récent d'abord
+    """
+    cnx = context.GetDBConnexion()
+    L = []
+    for s in semset_list(cnx):
+        L.append(SemSet(context, semset_id=s["semset_id"]))
+    L.sort(key=lambda s: s["date_debut"], reverse=True)
+    return L
+
+
+def do_semset_create(context, title="", annee_scolaire=None, sem_id=None, REQUEST=None):
+    """Create new setset"""
+    log(
+        "do_semset_create(title=%s, annee_scolaire=%s, sem_id=%s)"
+        % (title, annee_scolaire, sem_id)
+    )
+    s = SemSet(context, title=title, annee_scolaire=annee_scolaire, sem_id=sem_id)
+    return REQUEST.RESPONSE.redirect("semset_page")
+
+
+def do_semset_delete(context, semset_id, dialog_confirmed=False, REQUEST=None):
+    """Delete a semset"""
+    if not semset_id:
+        raise ScoValueError("empty semset_id")
+    s = SemSet(context, semset_id=semset_id)
+    if not dialog_confirmed:
+        return context.confirmDialog(
+            "<h2>Suppression de l'ensemble %(title)s ?</h2>" % s,
+            dest_url="",
+            REQUEST=REQUEST,
+            parameters={"semset_id": semset_id},
+            cancel_url="semset_page",
+        )
+    s.delete()
+    return REQUEST.RESPONSE.redirect("semset_page")
+
+
+def edit_semset_set_title(context, id=None, value=None, REQUEST=None):
+    """Change title of semset
+    """
+    title = value.strip()
+    if not id:
+        raise ScoValueError("empty semset_id")
+    s = SemSet(context, semset_id=id)
+    cnx = context.GetDBConnexion()
+    semset_edit(cnx, {"semset_id": id, "title": title})
+    return title
+
+
+def do_semset_add_sem(context, semset_id, formsemestre_id, REQUEST=None):
+    """Add a sem to a semset"""
+    if not semset_id:
+        raise ScoValueError("empty semset_id")
+    s = SemSet(context, semset_id=semset_id)
+    # check for valid formsemestre_id
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)  # raise exc
+
+    s.add(formsemestre_id)
+
+    return REQUEST.RESPONSE.redirect("apo_semset_maq_status?semset_id=%s" % semset_id)
+
+
+def do_semset_remove_sem(context, semset_id, formsemestre_id, REQUEST=None):
+    """Add a sem to a semset"""
+    if not semset_id:
+        raise ScoValueError("empty semset_id")
+    s = SemSet(context, semset_id)
+
+    s.remove(formsemestre_id)
+
+    return REQUEST.RESPONSE.redirect("apo_semset_maq_status?semset_id=%s" % semset_id)
+
+
+# ----------------------------------------
+
+
+def semset_page(context, format="html", REQUEST=None):
+    """Page avec liste semsets:
+    Table avec : date_debut date_fin titre liste des semestres
+    """
+    semsets = get_semsets_list(context)
+    for s in semsets:
+        s["suppress"] = icontag(
+            "delete_small_img", border="0", alt="supprimer", title="Supprimer"
+        )
+        s["_suppress_target"] = "do_semset_delete?semset_id=%s" % (s["semset_id"])
+        s["export_link"] = "Export Apogée"
+        s["_export_link_target"] = "apo_semset_maq_status?semset_id=%s" % s.semset_id
+        s["_export_link_link_class"] = "stdlink"
+        # Le lien associé au nom de semestre redirigeait vers le semset
+        # (remplacé par n liens vers chacun des semestres)
+        # s['_semtitles_str_target'] = s['_export_link_target']
+        # Experimental:
+        s["_title_td_attrs"] = (
+            'class="inplace_edit" data-url="edit_semset_set_title" id="%s"'
+            % (s["semset_id"])
+        )
+
+    tab = GenTable(
+        rows=semsets,
+        titles={
+            "annee_scolaire": "Année scolaire",
+            "sem_id": "P",
+            "date_debut": "Début",
+            "date_fin": "Fin",
+            "title": "Titre",
+            "export_link": "",
+            "semtitles_str": "semestres",
+        },
+        columns_ids=[
+            "suppress",
+            "annee_scolaire",
+            "sem_id",
+            "date_debut",
+            "date_fin",
+            "title",
+            "export_link",
+            "semtitles_str",
+        ],
+        html_sortable=True,
+        html_class="table_leftalign",
+        filename="semsets",
+        preferences=context.get_preferences(),
+    )
+    if format != "html":
+        return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+    page_title = "Ensembles de semestres"
+    H = [
+        context.sco_header(
+            REQUEST,
+            page_title=page_title,
+            init_qtip=True,
+            javascripts=["libjs/jinplace-1.2.1.min.js"],
+        ),
+        """<script>$(function() {
+           $('.inplace_edit').jinplace();
+           });
+           </script>""",
+        "<h2>%s</h2>" % page_title,
+    ]
+    H.append(tab.html())
+
+    annee_courante = int(AnneeScolaire(REQUEST))
+    menu_annee = "\n".join(
+        [
+            '<option value="%s">%s</option>' % (i, i)
+            for i in range(2014, annee_courante + 1)
+        ]
+    )
+
+    H.append(
+        """
+    <div style="margin-top:20px;">
+    <h4>Création nouvel ensemble</h4>
+    <form method="POST" action="do_semset_create">
+    <select name="annee_scolaire">
+    <option value="" selected>(année scolaire)</option>"""
+    )
+    H.append(menu_annee)
+    H.append(
+        """</select>
+    <select name="sem_id">
+    <option value="1">1re période (S1, S3)</option>
+    <option value="2">2de période (S2, S4)</option>
+    <option value="0">non semestrialisée (LP, ...)</option>
+    </select>
+    <input type="text" name="title" size="32"/>
+    <input type="submit" value="Créer"/>
+    </form></div>
+    """
+    )
+
+    H.append(
+        """
+    <div>
+    <h4>Autres opérations:</h4>
+    <ul>
+    <li><a class="stdlink" href="scodoc_table_results">
+    Table des résultats de tous les semestres
+    </a></li>
+    <li><a class="stdlink" href="apo_compare_csv_form">
+    Comparaison de deux maquettes Apogée
+    </a></li>
+    </ul>
+    </div>
+    """
+    )
+
+    return "\n".join(H) + context.sco_footer(REQUEST)
diff --git a/sco_synchro_etuds.py b/sco_synchro_etuds.py
new file mode 100644
index 0000000000000000000000000000000000000000..6deec8bf72b412403c80d07655047d3758b701d0
--- /dev/null
+++ b/sco_synchro_etuds.py
@@ -0,0 +1,831 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Synchronisation des listes d'étudiants avec liste portail (Apogée)
+"""
+
+
+from sco_utils import *
+from notesdb import *
+
+import sco_portal_apogee
+import sco_inscr_passage
+import scolars
+import sco_groups
+import sco_news, sco_excel
+import sco_formsemestre
+import sco_formsemestre_inscriptions
+import sco_formsemestre_status
+from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC
+
+import time
+
+# Clés utilisées pour la synchro
+EKEY_APO = "nip"
+EKEY_SCO = "code_nip"
+EKEY_NAME = "code NIP"
+
+
+def formsemestre_synchro_etuds(
+    context,
+    formsemestre_id,
+    etuds=[],  # liste des codes NIP des etudiants a inscrire (ou deja inscrits)
+    inscrits_without_key=[],  # codes etudid des etudiants sans code NIP a laisser inscrits
+    anneeapogee=None,
+    submitted=False,
+    dialog_confirmed=False,
+    export_cat_xls=None,
+    read_only=False,  # Affiche sans permettre modifications
+    REQUEST=None,
+):
+    """Synchronise les étudiants de ce semestre avec ceux d'Apogée.
+    On a plusieurs cas de figure: L'étudiant peut être
+    1- présent dans Apogée et inscrit dans le semestre ScoDoc (etuds_ok)
+    2- dans Apogée, dans ScoDoc, mais pas inscrit dans le semestre (etuds_noninscrits)
+    3- dans Apogée et pas dans ScoDoc (a_importer)
+    4- inscrit dans le semestre ScoDoc, mais pas trouvé dans Apogée (sur la base du code NIP)
+    
+    Que faire ?
+    Cas 1: rien à faire
+    Cas 2: inscrire dans le semestre
+    Cas 3: importer l'étudiant (le créer)
+            puis l'inscrire à ce semestre.
+    Cas 4: lister les etudiants absents d'Apogée (indiquer leur code NIP...)
+
+    - présenter les différents cas
+    - l'utilisateur valide (cocher les étudiants à importer/inscrire)
+    - go
+
+    etuds: apres selection par utilisateur, la liste des etudiants selectionnes
+    que l'on va importer/inscrire
+    """
+    log("formsemestre_synchro_etuds: formsemestre_id=%s" % formsemestre_id)
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    sem["etape_apo_str"] = sco_formsemestre.formsemestre_etape_apo_str(sem)
+    # Write access ?
+    authuser = REQUEST.AUTHENTICATED_USER
+    if not authuser.has_permission(ScoEtudInscrit, context):
+        read_only = True
+    if read_only:
+        submitted = False
+        dialog_confirmed = False
+    # -- check lock
+    if sem["etat"] != "1":
+        raise ScoValueError("opération impossible: semestre verrouille")
+    if not sem["etapes"]:
+        raise ScoValueError(
+            """opération impossible: ce semestre n'a pas de code étape
+        (voir "<a href="formsemestre_editwithmodules?formation_id=%(formation_id)s&amp;formsemestre_id=%(formsemestre_id)s">Modifier ce semestre</a>")
+        """
+            % sem
+        )
+    header = context.sco_header(REQUEST, page_title="Synchronisation étudiants")
+    footer = context.sco_footer(REQUEST)
+    base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id)
+    if anneeapogee:
+        base_url += "&amp;anneeapogee=%s" % anneeapogee
+
+    if anneeapogee == None:  # année d'inscription par défaut
+        anneeapogee = str(
+            annee_scolaire_debut(sem["annee_debut"], sem["mois_debut_ord"])
+        )
+
+    if type(etuds) == type(""):
+        etuds = etuds.split(",")  # vient du form de confirmation
+    if type(inscrits_without_key) == type(""):
+        inscrits_without_key = inscrits_without_key.split(",")
+
+    (
+        etuds_by_cat,
+        a_importer,
+        a_inscrire,
+        inscrits_set,
+        inscrits_without_key_all,
+        etudsapo_ident,
+    ) = list_synch(context, sem, anneeapogee=anneeapogee)
+
+    if export_cat_xls:
+        filename = export_cat_xls
+        xls = build_page(
+            context,
+            sem,
+            etuds_by_cat,
+            anneeapogee,
+            export_cat_xls=export_cat_xls,
+            base_url=base_url,
+            read_only=read_only,
+        )
+        return sco_excel.sendExcelFile(REQUEST, xls, filename + ".xls")
+
+    H = [header]
+    if not submitted:
+        H += build_page(
+            context,
+            sem,
+            etuds_by_cat,
+            anneeapogee,
+            base_url=base_url,
+            read_only=read_only,
+        )
+    else:
+        etuds_set = set(etuds)
+        a_importer = a_importer.intersection(etuds_set)
+        a_desinscrire = inscrits_set - etuds_set
+        log("inscrits_without_key_all=%s" % set(inscrits_without_key_all))
+        log("inscrits_without_key=%s" % inscrits_without_key)
+        a_desinscrire_without_key = set(inscrits_without_key_all) - set(
+            inscrits_without_key
+        )
+        log("a_desinscrire_without_key=%s" % a_desinscrire_without_key)
+        inscrits_ailleurs = set(sco_inscr_passage.list_inscrits_date(context, sem))
+        a_inscrire = a_inscrire.intersection(etuds_set)
+
+        if not dialog_confirmed:
+            # Confirmation
+            if a_importer:
+                H.append("<h3>Etudiants à importer et inscrire :</h3><ol>")
+                for key in a_importer:
+                    H.append("<li>%(fullname)s</li>" % etudsapo_ident[key])
+                H.append("</ol>")
+
+            if a_inscrire:
+                H.append("<h3>Etudiants à inscrire :</h3><ol>")
+                for key in a_inscrire:
+                    H.append("<li>%(fullname)s</li>" % etudsapo_ident[key])
+                H.append("</ol>")
+
+            a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire)
+            if a_inscrire_en_double:
+                H.append("<h3>dont étudiants déjà inscrits:</h3><ol>")
+                for key in a_inscrire_en_double:
+                    H.append(
+                        '<li class="inscrailleurs">%(fullname)s</li>'
+                        % etudsapo_ident[key]
+                    )
+                H.append("</ol>")
+
+            if a_desinscrire or a_desinscrire_without_key:
+                H.append("<h3>Etudiants à désinscrire :</h3><ol>")
+                for key in a_desinscrire:
+                    etud = context.getEtudInfo(filled=1, code_nip=key)[0]
+                    H.append('<li class="desinscription">%(nomprenom)s</li>' % etud)
+                for etudid in a_desinscrire_without_key:
+                    etud = inscrits_without_key_all[etudid]
+                    scolars.format_etud_ident(etud)
+                    H.append('<li class="desinscription">%(nomprenom)s</li>' % etud)
+                H.append("</ol>")
+
+            if not a_importer and not a_inscrire and not a_desinscrire:
+                H.append("""<h3>Il n'y a rien à modifier !</h3>""")
+
+            H.append(
+                context.confirmDialog(
+                    dest_url="formsemestre_synchro_etuds",
+                    add_headers=False,
+                    cancel_url="formsemestre_synchro_etuds?formsemestre_id="
+                    + formsemestre_id,
+                    OK="Effectuer l'opération",
+                    REQUEST=REQUEST,
+                    parameters={
+                        "formsemestre_id": formsemestre_id,
+                        "etuds": ",".join(etuds),
+                        "inscrits_without_key": ",".join(inscrits_without_key),
+                        "submitted": 1,
+                        "anneeapogee": anneeapogee,
+                    },
+                )
+            )
+        else:
+            # OK, do it
+
+            # Conversions des listes de codes NIP en listes de codes etudid
+            def nip2etudid(code_nip):
+                etud = context.getEtudInfo(code_nip=code_nip)[0]
+                return etud["etudid"]
+
+            etudids_a_inscrire = [nip2etudid(x) for x in a_inscrire]
+            etudids_a_desinscrire = [nip2etudid(x) for x in a_desinscrire]
+            etudids_a_desinscrire += a_desinscrire_without_key
+            #
+            do_import_etuds_from_portal(
+                context, sem, a_importer, etudsapo_ident, REQUEST
+            )
+            sco_inscr_passage.do_inscrit(context, sem, etudids_a_inscrire, REQUEST)
+            sco_inscr_passage.do_desinscrit(
+                context, sem, etudids_a_desinscrire, REQUEST
+            )
+
+            H.append(
+                """<h3>Opération effectuée</h3>
+            <ul>
+                <li><a class="stdlink" href="formsemestre_synchro_etuds?formsemestre_id=%s">Continuer la synchronisation</a></li>"""
+                % formsemestre_id
+            )
+            #
+            partitions = sco_groups.get_partitions_list(
+                context, formsemestre_id, with_default=False
+            )
+            if partitions:  # il y a au moins une vraie partition
+                H.append(
+                    """<li><a class="stdlink" href="affectGroups?partition_id=%s">Répartir les groupes de %s</a></li>
+                """
+                    % (partitions[0]["partition_id"], partitions[0]["partition_name"])
+                )
+
+    H.append(footer)
+    return "\n".join(H)
+
+
+def build_page(
+    context,
+    sem,
+    etuds_by_cat,
+    anneeapogee,
+    export_cat_xls=None,
+    base_url="",
+    read_only=False,
+):
+    if export_cat_xls:
+        return sco_inscr_passage.etuds_select_boxes(
+            context, etuds_by_cat, export_cat_xls=export_cat_xls, base_url=base_url
+        )
+    year = time.localtime()[0]
+    if anneeapogee and abs(year - int(anneeapogee)) < 50:
+        years = range(
+            min(year - 1, int(anneeapogee) - 1), max(year, int(anneeapogee)) + 1
+        )
+    else:
+        years = range(year - 1, year + 1)
+        anneeapogee = ""
+    options = []
+    for y in years:
+        if str(y) == anneeapogee:
+            sel = "selected"
+        else:
+            sel = ""
+        options.append('<option value="%s" %s>%s</option>' % (str(y), sel, str(y)))
+    if anneeapogee:
+        sel = ""
+    else:
+        sel = "selected"
+    options.append('<option value="" %s>toutes</option>' % sel)
+    # sem['etape_apo_str'] = sem['etape_apo'] or '-'
+
+    H = [
+        """<h2 class="formsemestre">Synchronisation des étudiants du semestre avec Apogée</h2>""",
+        """<p>Actuellement <b>%d</b> inscrits dans ce semestre.</p>"""
+        % (
+            len(etuds_by_cat["etuds_ok"]["etuds"])
+            + len(etuds_by_cat["etuds_nonapogee"]["etuds"])
+            + len(etuds_by_cat["inscrits_without_key"]["etuds"])
+        ),
+        """<p>Code étape Apogée: %(etape_apo_str)s</p>
+        <form method="post" action="formsemestre_synchro_etuds">
+        """
+        % sem,
+        """
+        Année Apogée: <select id="anneeapogee" name="anneeapogee" onchange="document.location='formsemestre_synchro_etuds?formsemestre_id=%s&amp;anneeapogee='+document.getElementById('anneeapogee').value">"""
+        % (sem["formsemestre_id"]),
+        "\n".join(options),
+        """
+        </select>
+        """,
+        ""
+        if read_only
+        else """
+        <input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>
+        <input type="submit" name="submitted" value="Appliquer les modifications"/>
+        &nbsp;<a href="#help">aide</a>
+        """
+        % sem,  # "
+        sco_inscr_passage.etuds_select_boxes(
+            context,
+            etuds_by_cat,
+            sel_inscrits=False,
+            show_empty_boxes=True,
+            base_url=base_url,
+            read_only=read_only,
+        ),
+        ""
+        if read_only
+        else """<p/><input type="submit" name="submitted" value="Appliquer les modifications"/>""",
+        formsemestre_synchro_etuds_help(context, sem),
+        """</form>""",
+    ]
+    return H
+
+
+def list_synch(context, sem, anneeapogee=None):
+    """
+    """
+    inscrits = sco_inscr_passage.list_inscrits(
+        context, sem["formsemestre_id"], with_dems=True
+    )
+    # Tous les ensembles d'etudiants sont ici des ensembles de codes NIP (voir EKEY_SCO)
+    # (sauf inscrits_without_key)
+    inscrits_set = set()
+    inscrits_without_key = {}  # etudid : etud sans code NIP
+    for e in inscrits.values():
+        if not e[EKEY_SCO]:
+            inscrits_without_key[e["etudid"]] = e
+            e["inscrit"] = True  # checkbox state
+        else:
+            inscrits_set.add(e[EKEY_SCO])
+    #     allinscrits_set = set() # tous les inscrits scodoc avec code_nip, y compris les demissionnaires
+    #     for e in inscrits.values():
+    #         if e[EKEY_SCO]:
+    #             allinscrits_set.add(e[EKEY_SCO])
+
+    datefinalisationinscription_by_NIP = {}  # nip : datefinalisationinscription_str
+
+    etapes = sem["etapes"]
+    etudsapo_set = set()
+    etudsapo_ident = {}
+    for etape in etapes:
+        if etape:
+            etudsapo = sco_portal_apogee.get_inscrits_etape(
+                context, etape, anneeapogee=anneeapogee
+            )
+            etudsapo_set = etudsapo_set.union(set([x[EKEY_APO] for x in etudsapo]))
+            for e in etudsapo:
+                if e[EKEY_APO] not in etudsapo_ident:
+                    etudsapo_ident[e[EKEY_APO]] = e
+                datefinalisationinscription_by_NIP[e[EKEY_APO]] = e[
+                    "datefinalisationinscription"
+                ]
+
+    # categories:
+    etuds_ok = etudsapo_set.intersection(inscrits_set)
+    etuds_aposco, a_importer, key2etudid = list_all(context, etudsapo_set)
+    etuds_noninscrits = etuds_aposco - inscrits_set
+    etuds_nonapogee = inscrits_set - etudsapo_set
+    # Etudiants ayant payé (avec balise <paiementinscription> true)
+    # note: si le portail ne renseigne pas cette balise, suppose que paiement ok
+    etuds_payes = set(
+        [x[EKEY_APO] for x in etudsapo if x.get("paiementinscription", True)]
+    )
+    #
+    cnx = context.GetDBConnexion()
+    # Tri listes
+    def set_to_sorted_list(etudset, etud_apo=False, is_inscrit=False):
+        def key2etud(key, etud_apo=False):
+            if not etud_apo:
+                etudid = key2etudid[key]
+                etud = scolars.identite_list(cnx, {"etudid": etudid})[0]
+                etud["inscrit"] = is_inscrit  # checkbox state
+                etud[
+                    "datefinalisationinscription"
+                ] = datefinalisationinscription_by_NIP.get(key, None)
+                if key in etudsapo_ident:
+                    etud["etape"] = etudsapo_ident[key].get("etape", "")
+            else:
+                # etudiant Apogee
+                etud = etudsapo_ident[key]
+
+                etud["etudid"] = ""
+                etud["sexe"] = etud.get(
+                    "sexe", etud.get("gender", "")
+                )  # la cle 'sexe' est prioritaire sur 'gender'
+                etud["inscrit"] = is_inscrit  # checkbox state
+            if key in etuds_payes:
+                etud["paiementinscription"] = True
+            else:
+                etud["paiementinscription"] = False
+            return etud
+
+        etuds = [key2etud(x, etud_apo) for x in etudset]
+        etuds.sort(lambda x, y: cmp(x["nom"], y["nom"]))
+        return etuds
+
+    #
+    r = {
+        "etuds_ok": {
+            "etuds": set_to_sorted_list(etuds_ok, is_inscrit=True),
+            "infos": {
+                "id": "etuds_ok",
+                "title": "Etudiants dans Apogée et déjà inscrits",
+                "help": "Ces etudiants sont inscrits dans le semestre ScoDoc et sont présents dans Apogée: tout est donc correct. Décocher les étudiants que vous souhaitez désinscrire.",
+                "title_target": "",
+                "with_checkbox": True,
+                "etud_key": EKEY_SCO,
+            },
+        },
+        "etuds_noninscrits": {
+            "etuds": set_to_sorted_list(etuds_noninscrits, is_inscrit=True),
+            "infos": {
+                "id": "etuds_noninscrits",
+                "title": "Etudiants non inscrits dans ce semestre",
+                "help": """Ces étudiants sont déjà connus par ScoDoc, sont inscrits dans cette étape Apogée mais ne sont pas inscrits à ce semestre ScoDoc. Cochez les étudiants à inscrire.""",
+                "comment": """ dans ScoDoc et Apogée, <br/>mais pas inscrits
+                      dans ce semestre""",
+                "title_target": "",
+                "with_checkbox": True,
+                "etud_key": EKEY_SCO,
+            },
+        },
+        "etuds_a_importer": {
+            "etuds": set_to_sorted_list(a_importer, is_inscrit=True, etud_apo=True),
+            "infos": {
+                "id": "etuds_a_importer",
+                "title": "Etudiants dans Apogée à importer",
+                "help": """Ces étudiants sont inscrits dans cette étape Apogée mais ne sont pas connus par ScoDoc: cocher les noms à importer et inscrire puis appuyer sur le bouton "Appliquer".""",
+                "title_target": "",
+                "with_checkbox": True,
+                "etud_key": EKEY_APO,  # clé à stocker dans le formulaire html
+            },
+            "nomprenoms": etudsapo_ident,
+        },
+        "etuds_nonapogee": {
+            "etuds": set_to_sorted_list(etuds_nonapogee, is_inscrit=True),
+            "infos": {
+                "id": "etuds_nonapogee",
+                "title": "Etudiants ScoDoc inconnus dans cette étape Apogée",
+                "help": """Ces étudiants sont inscrits dans ce semestre ScoDoc, ont un code NIP, mais ne sont pas inscrits dans cette étape Apogée. Soit ils sont en retard pour leur inscription, soit il s'agit d'une erreur: vérifiez avec le service Scolarité de votre établissement. Autre possibilité: votre code étape semestre (%s) est incorrect ou vous n'avez pas choisi la bonne année d'inscription."""
+                % sem["etape_apo_str"],
+                "comment": " à vérifier avec la Scolarité",
+                "title_target": "",
+                "with_checkbox": True,
+                "etud_key": EKEY_SCO,
+            },
+        },
+        "inscrits_without_key": {
+            "etuds": inscrits_without_key.values(),
+            "infos": {
+                "id": "inscrits_without_key",
+                "title": "Etudiants ScoDoc sans clé Apogée (NIP)",
+                "help": """Ces étudiants sont inscrits dans ce semestre ScoDoc, mais n'ont pas de code NIP: on ne peut pas les mettre en correspondance avec Apogée. Utiliser le lien 'Changer les données identité' dans le menu 'Etudiant' sur leur fiche pour ajouter cette information.""",
+                "title_target": "",
+                "with_checkbox": True,
+                "checkbox_name": "inscrits_without_key",
+            },
+        },
+    }
+    return (
+        r,
+        a_importer,
+        etuds_noninscrits,
+        inscrits_set,
+        inscrits_without_key,
+        etudsapo_ident,
+    )
+
+
+def list_all(context, etudsapo_set):
+    """Cherche le sous-ensemble des etudiants Apogee de ce semestre
+    qui existent dans ScoDoc.
+    """
+    # on charge TOUS les etudiants (au pire qq 100000 ?)
+    # si tres grosse base, il serait mieux de faire une requete
+    # d'interrogation par etudiant.
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute("select " + EKEY_SCO + ", etudid from identite")
+    key2etudid = dict([(x[0], x[1]) for x in cursor.fetchall()])
+    all_set = set(key2etudid.keys())
+
+    # ne retient que ceux dans Apo
+    etuds_aposco = etudsapo_set.intersection(
+        all_set
+    )  # a la fois dans Apogee et dans ScoDoc
+    a_importer = etudsapo_set - all_set  # dans Apogee, mais inconnus dans ScoDoc
+    return etuds_aposco, a_importer, key2etudid
+
+
+def formsemestre_synchro_etuds_help(context, sem):
+    sem["default_group_id"] = sco_groups.get_default_group(
+        context, sem["formsemestre_id"]
+    )
+    return (
+        """<div class="pas_help pas_help_left"><h3><a name="help">Explications</a></h3>
+    <p>Cette page permet d'importer dans le semestre destination
+    <a class="stdlink"
+    href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a>
+    les étudiants inscrits dans l'étape Apogée correspondante (<b><tt>%(etape_apo_str)s</tt></b>) 
+    </p>
+    <p>Au départ, tous les étudiants d'Apogée sont sélectionnés; vous pouvez 
+    en déselectionner certains. Tous les étudiants cochés seront inscrits au semestre ScoDoc, 
+    les autres seront si besoin désinscrits. Aucune modification n'est effectuée avant 
+    d'appuyer sur le bouton "Appliquer les modifications".</p>
+
+    <h4>Autres fonctions utiles</h4>
+    <ul>
+    <li><a href="check_group_apogee?group_id=%(default_group_id)s">vérification
+    des codes Apogée</a> (des étudiants déjà inscrits)</li>
+    <li>le <a href="formsemestre_inscr_passage?formsemestre_id=%(formsemestre_id)s">
+    formulaire de passage</a> qui permet aussi de désinscrire des étudiants
+    en cas d'erreur, etc.</li>
+    </ul>
+    </div>"""
+        % sem
+    )
+
+
+def gender2sex(gender):
+    """Le portail code en 'M', 'F', et ScoDoc en 'MR', 'MME'
+    Les F sont ici codées en MME
+    """
+    if gender == "M":
+        return "MR"
+    elif gender == "F":
+        return "MME"
+    log('gender2sex: invalid value "%s", defaulting to "M"' % gender)
+    return "MR"
+
+
+def get_opt_str(etud, k):
+    v = etud.get(k, None)
+    if not v:
+        return v
+    return v.strip()
+
+
+def get_annee_naissance(ddmmyyyyy):  # stokee en dd/mm/yyyy dans le XML portail
+    if not ddmmyyyyy:
+        return None
+    try:
+        return int(ddmmyyyyy.split("/")[2])
+    except:
+        return None
+
+
+def do_import_etuds_from_portal(context, sem, a_importer, etudsapo_ident, REQUEST):
+    """Inscrit les etudiants Apogee dans ce semestre.
+    """
+    log("do_import_etuds_from_portal: a_importer=%s" % a_importer)
+    if not a_importer:
+        return
+    cnx = context.GetDBConnexion()
+    created_etudids = []
+
+    try:  # --- begin DB transaction
+        for key in a_importer:
+            etud = etudsapo_ident[
+                key
+            ]  # on a ici toutes les infos renvoyées par le portail
+
+            # Traduit les infos portail en infos pour ScoDoc:
+            address = etud["address"].strip()
+            if address[-2:] == "\\n":  # certains champs se terminent par \n
+                address = address[:-2]
+            # Les mails et le code INE sont facultatifs (pas toujours renvoyés par le portail)
+            etud["mail"] = etud.get("mail", "")
+            etud["mailperso"] = etud.get("mailperso", "")
+            etud["ine"] = etud.get("ine", "")
+            #
+            args = {
+                "nom": etud["nom"].strip(),
+                "prenom": etud["prenom"].strip(),
+                "sexe": gender2sex(etud["gender"].strip()),
+                "date_naissance": etud["naissance"].strip(),
+                "code_nip": etud["nip"],
+                "code_ine": etud["ine"],
+                "email": etud["mail"].strip(),
+                "emailperso": etud[
+                    "mailperso"
+                ].strip(),  # pas toujours fourni par le portail
+                "domicile": address,
+                "codepostaldomicile": etud.get("postalcode", "").strip(),
+                "villedomicile": etud["city"].strip(),
+                "paysdomicile": etud["country"].strip(),
+                "telephone": etud.get("phone", "").strip(),
+                "typeadresse": "domicile",
+                "boursier": etud.get("bourse", None),
+                "description": "infos portail",
+            }
+            # Identite
+            args["etudid"] = scolars.identite_create(cnx, args)
+            created_etudids.append(args["etudid"])
+            # Admissions
+            do_import_etud_admission(context, cnx, args["etudid"], etud)
+
+            # Adresse
+            adresse_id = scolars.adresse_create(cnx, args)
+
+            # Inscription au semestre
+            sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
+                context,
+                sem["formsemestre_id"],
+                args["etudid"],
+                etat="I",
+                REQUEST=REQUEST,
+                method="synchro_apogee",
+            )
+    except:
+        cnx.rollback()
+        log("do_import_etuds_from_portal: aborting transaction !")
+        # Nota: db transaction is sometimes partly commited...
+        # here we try to remove all created students
+        cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+        for etudid in created_etudids:
+            log("do_import_etuds_from_portal: deleting etudid=%s" % etudid)
+            cursor.execute(
+                "delete from notes_moduleimpl_inscription where etudid=%(etudid)s",
+                {"etudid": etudid},
+            )
+            cursor.execute(
+                "delete from notes_formsemestre_inscription where etudid=%(etudid)s",
+                {"etudid": etudid},
+            )
+            cursor.execute(
+                "delete from scolar_events where etudid=%(etudid)s", {"etudid": etudid}
+            )
+            cursor.execute(
+                "delete from adresse where etudid=%(etudid)s", {"etudid": etudid}
+            )
+            cursor.execute(
+                "delete from admissions where etudid=%(etudid)s", {"etudid": etudid}
+            )
+            cursor.execute(
+                "delete from group_membership where etudid=%(etudid)s",
+                {"etudid": etudid},
+            )
+            cursor.execute(
+                "delete from identite where etudid=%(etudid)s", {"etudid": etudid}
+            )
+        cnx.commit()
+        log("do_import_etuds_from_portal: re-raising exception")
+        context._inval_cache()  # > import: modif identite, adresses, inscriptions
+        raise
+
+    sco_news.add(
+        context,
+        REQUEST,
+        typ=NEWS_INSCR,
+        text="Import Apogée de %d étudiants en " % len(created_etudids),
+        object=sem["formsemestre_id"],
+    )
+
+
+def do_import_etud_admission(
+    context, cnx, etudid, etud, import_naissance=False, import_identite=False
+):
+    """Importe les donnees admission pour cet etud.
+    etud est un dictionnaire traduit du XML portail
+    """
+    annee_courante = time.localtime()[0]
+    serie_bac, spe_bac = get_bac(etud)
+    args = {
+        "etudid": etudid,
+        "annee": get_opt_str(etud, "inscription") or annee_courante,
+        "bac": serie_bac,
+        "specialite": spe_bac,
+        "annee_bac": get_opt_str(etud, "anneebac"),
+        "codelycee": get_opt_str(etud, "lycee"),
+        "boursier": get_opt_str(etud, "bourse"),
+    }
+    log("do_import_etud_admission: etud=%s" % etud)
+    al = scolars.admission_list(cnx, args={"etudid": etudid})
+    if not al:
+        adm_id = scolars.admission_create(cnx, args)
+    else:
+        # existing data: merge
+        e = al[0]
+        if get_opt_str(etud, "inscription"):
+            e["annee"] = args["annee"]
+        keys = args.keys()
+        for k in keys:
+            if not args[k]:
+                del args[k]
+        e.update(args)
+        scolars.admission_edit(cnx, e)
+    # Traite cas particulier de la date de naissance pour anciens
+    # etudiants IUTV
+    if import_naissance:
+        date_naissance = etud["naissance"].strip()
+        if date_naissance:
+            scolars.identite_edit_nocheck(
+                cnx, {"etudid": etudid, "date_naissance": date_naissance}
+            )
+    # Reimport des identités
+    if import_identite:
+        args = {"etudid": etudid}
+        # Les champs n'ont pas les mêmes noms dans Apogee et dans ScoDoc:
+        fields_apo_sco = [
+            ("naissance", "date_naissance"),
+            ("ville_naissance", "lieu_naissance"),
+            ("code_dep_naissance", "dept_naissance"),
+            ("nom", "nom"),
+            ("prenom", "prenom"),
+            ("ine", "code_ine"),
+            ("bourse", "boursier"),
+        ]
+        for apo_field, sco_field in fields_apo_sco:
+            x = etud.get(apo_field, "").strip()
+            if x:
+                args[sco_field] = x.strip()
+        # Champs spécifiques:
+        sexe = gender2sex(etud["gender"].strip())
+        if sexe:
+            args["sexe"] = sexe
+
+        scolars.identite_edit_nocheck(cnx, args)
+
+
+def get_bac(etud):
+    bac = get_opt_str(etud, "bac")
+    if not bac:
+        return None, None
+    serie_bac = bac.split("-")[0]
+    if len(serie_bac) < 8:
+        spe_bac = bac[len(serie_bac) + 1 :]
+    else:
+        serie_bac = bac
+        spe_bac = None
+    return serie_bac, spe_bac
+
+
+def formsemestre_import_etud_admission(
+    context, formsemestre_id, import_identite=True, import_email=False
+):
+    """Tente d'importer les données admission depuis le portail 
+    pour tous les étudiants du semestre.
+    Si  import_identite==True, recopie l'identité (nom/prenom/sexe/date_naissance)
+    de chaque étudiant depuis le portail.
+    N'affecte pas les etudiants inconnus sur le portail. 
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    ins = context.do_formsemestre_inscription_list({"formsemestre_id": formsemestre_id})
+    log(
+        "formsemestre_import_etud_admission: %s (%d etuds)"
+        % (formsemestre_id, len(ins))
+    )
+    no_nip = []  # liste d'etudids sans code NIP
+    unknowns = []  # etudiants avec NIP mais inconnus du portail
+    changed_mails = []  # modification d'adresse mails
+    cnx = context.GetDBConnexion()
+    for i in ins:
+        etudid = i["etudid"]
+        info = context.getEtudInfo(etudid=etudid, filled=1)[0]
+        code_nip = info["code_nip"]
+        if not code_nip:
+            no_nip.append(etudid)
+        else:
+            etud = sco_portal_apogee.get_etud_apogee(context, code_nip)
+            if etud:
+                do_import_etud_admission(
+                    context,
+                    cnx,
+                    etudid,
+                    etud,
+                    import_naissance=True,
+                    import_identite=import_identite,
+                )
+                if (
+                    import_email
+                    and info["email"] != etud["mail"]
+                    or info["emailperso"] != etud.get("mailperso", "")
+                ):
+                    scolars.adresse_edit(
+                        cnx,
+                        args={
+                            "etudid": etudid,
+                            "adresse_id": info["adresse_id"],
+                            "email": etud["mail"],
+                            "emailperso": etud.get("mailperso", ""),
+                        },
+                        context=context,
+                    )
+                    # notifie seulement les changements d'adresse mail institutionnelle
+                    if info["email"] != etud["mail"]:
+                        changed_mails.append((info, etud["mail"]))
+            else:
+                unknowns.append(code_nip)
+    return no_nip, unknowns, changed_mails
+
+
+def do_synch_inscrits_etuds(context, sem, etuds, REQUEST=None):
+    """inscrits ces etudiants (déja dans ScoDoc) au semestre"""
+    log("do_synch_inscrits_etuds: inscription de %d etudiants" % len(etuds))
+    for etud in etuds:
+        sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
+            context,
+            sem["formsemestre_id"],
+            etud["etudid"],
+            etat="I",
+            REQUEST=REQUEST,
+            method="synchro_apogee",
+        )
diff --git a/sco_tag_module.py b/sco_tag_module.py
new file mode 100644
index 0000000000000000000000000000000000000000..b4798c9b17c2465f54536ddaf3906a5cf9b8d3a1
--- /dev/null
+++ b/sco_tag_module.py
@@ -0,0 +1,344 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Gestion des tags sur les modules
+
+   Implementation expérimentale (Jul. 2016) pour grouper les modules sur
+   les avis de poursuites d'études.
+
+
+   Pour l'UI, voir https://goodies.pixabay.com/jquery/tag-editor/demo.html
+"""
+
+from sco_utils import *
+from notesdb import *
+from notes_log import log
+
+# Opérations à implementer:
+#  + liste des modules des formations de code donné (formation_code) avec ce tag
+#  + liste de tous les noms de tag
+#  + tag pour un nom
+#  + creer un tag (nom)
+#  + lier un tag à un module
+#  + enlever un tag d'un module (si c'est le dernier, supprimer le tag lui même)
+#
+# API publiée:
+#   module_tag_list_all  -> tous les noms d etags (pour l'autocomplete)
+#   module_tag_list( module_id ) -> les noms de tags associés à ce module
+#   module_tag_set( module_id, taglist ) -> modifie les tags
+
+
+class ScoTag:
+    """Generic tags for ScoDoc
+    """
+
+    # must be overloaded:
+    tag_table = None  # table (tag_id, title)
+    assoc_table = None  # table (tag_id, object_id)
+    obj_colname = None  # column name for object_id in assoc_table
+
+    def __init__(self, context, title, object_id=""):
+        """Load tag, or create if does not exist
+        """
+        self.context = context
+        self.title = title.strip()
+        if not self.title:
+            raise ScoValueError("invalid empty tag")
+        r = SimpleDictFetch(
+            context,
+            "SELECT * FROM " + self.tag_table + " WHERE title = %(title)s",
+            {"title": self.title},
+        )
+        if r:
+            self.tag_id = r[0]["tag_id"]
+        else:
+            # Create new tag:
+            log("creating new tag: %s" % self.title)
+            cnx = context.GetDBConnexion()
+            oid = DBInsertDict(cnx, self.tag_table, {"title": self.title}, commit=True)
+            self.tag_id = SimpleDictFetch(
+                context,
+                "SELECT tag_id FROM " + self.tag_table + " WHERE oid=%(oid)s",
+                {"oid": oid},
+            )[0]["tag_id"]
+        if object_id:
+            self.tag_object(object_id)
+
+    def __repr__(self):  # debug
+        return '<tag "%s">' % self.title
+
+    def delete(self):
+        """Delete this tag.
+        This object should not be used after this call !
+        """
+        args = {"tag_id": self.tag_id}
+        SimpleQuery(
+            self.context,
+            "DELETE FROM " + self.tag_table + " t WHERE t.tag_id = %(tag_id)s",
+            args,
+        )
+
+    def tag_object(self, object_id):
+        """Associate tag to given object"""
+        args = {self.obj_colname: object_id, "tag_id": self.tag_id}
+        r = SimpleDictFetch(
+            self.context,
+            "SELECT * FROM "
+            + self.assoc_table
+            + " a WHERE a."
+            + self.obj_colname
+            + " = %("
+            + self.obj_colname
+            + ")s AND a.tag_id = %(tag_id)s",
+            args,
+        )
+        if not r:
+            log("tag %s with %s" % (object_id, self.title))
+            cnx = self.context.GetDBConnexion()
+            DBInsertDict(cnx, self.assoc_table, args, commit=True)
+
+    def remove_tag_from_object(self, object_id):
+        """Remove tag from module.
+        If no more modules tagged with this tag, delete it.
+        Return True if Tag still exists.
+        """
+        log("removing tag %s from %s" % (self.title, object_id))
+        args = {"object_id": object_id, "tag_id": self.tag_id}
+        SimpleQuery(
+            self.context,
+            "DELETE FROM  "
+            + self.assoc_table
+            + " a WHERE a."
+            + self.obj_colname
+            + " = %(object_id)s AND a.tag_id = %(tag_id)s",
+            args,
+        )
+        r = SimpleDictFetch(
+            self.context,
+            """SELECT * FROM notes_modules_tags mt WHERE tag_id = %(tag_id)s
+            """,
+            args,
+        )
+        if not r:
+            # tag no more used, delete
+            SimpleQuery(
+                self.context,
+                """DELETE FROM notes_tags t WHERE t.tag_id = %(tag_id)s""",
+                args,
+            )
+
+
+class ModuleTag(ScoTag):
+    """Tags sur les modules dans les programmes pédagogiques
+    """
+
+    tag_table = "notes_tags"  # table (tag_id, title)
+    assoc_table = "notes_modules_tags"  # table (tag_id, object_id)
+    obj_colname = "module_id"  # column name for object_id in assoc_table
+
+    def list_modules(self, formation_code=""):
+        """Liste des modules des formations de code donné (formation_code) avec ce tag
+        """
+        args = {"tag_id": self.tag_id}
+        if not formation_code:
+            # tous les modules de toutes les formations !
+            r = SimpleDictFetch(
+                self.context,
+                "SELECT "
+                + self.obj_colname
+                + " FROM "
+                + self.assoc_table
+                + " WHERE tag_id = %(tag_id)s",
+                args,
+            )
+        else:
+            args["formation_code"] = formation_code
+
+            r = SimpleDictFetch(
+                self.context,
+                """SELECT mt.module_id 
+                FROM notes_modules_tags mt, notes_modules m, notes_formations f
+                WHERE mt.tag_id = %(tag_id)s 
+                AND m.module_id = mt.module_id 
+                AND m.formation_id = f.formation_id
+                AND f.formation_code = %(formation_code)s
+                """,
+                args,
+            )
+        return [x["module_id"] for x in r]
+
+
+# API
+
+
+def module_tag_search(context, term, REQUEST=None):
+    """List all used tag names (for auto-completion)"""
+    # restrict charset to avoid injections
+    if not ALPHANUM_EXP.match(term.decode(SCO_ENCODING)):
+        data = []
+    else:
+        r = SimpleDictFetch(
+            context,
+            "SELECT title FROM notes_tags WHERE title LIKE %(term)s",
+            {"term": term + "%"},
+        )
+        data = [x["title"] for x in r]
+
+    return sendJSON(REQUEST, data)
+
+
+def module_tag_list(context, module_id=""):
+    """les noms de tags associés à ce module
+    """
+    r = SimpleDictFetch(
+        context,
+        """SELECT t.title
+          FROM notes_modules_tags mt, notes_tags t
+          WHERE mt.tag_id = t.tag_id
+          AND mt.module_id = %(module_id)s
+          """,
+        {"module_id": module_id},
+    )
+    return [x["title"] for x in r]
+
+
+def module_tag_set(context, module_id="", taglist=[], REQUEST=None):
+    """taglist may either be:
+    a string with tag names separated by commas ("un;deux")
+    or a list of strings (["un", "deux"])
+    """
+    # We check permission here to allow old Admins (withn only ScoChangeFormation perm)
+    if REQUEST:  # called from Web
+        authuser = REQUEST.AUTHENTICATED_USER
+        tag_editable = authuser.has_permission(
+            ScoEditFormationTags, context
+        ) or authuser.has_permission(ScoChangeFormation, context)
+        if not tag_editable:
+            raise AccessDenied("Modification des tags impossible pour %s" % authuser)
+    #
+    if not taglist:
+        taglist = []
+    elif type(taglist) == StringType:
+        taglist = taglist.split(",")
+    taglist = [t.strip() for t in taglist]
+    log("module_tag_set: module_id=%s taglist=%s" % (module_id, taglist))
+    # Sanity check:
+    Mod = context.do_module_list(args={"module_id": module_id})
+    if not Mod:
+        raise ScoValueError("invalid module !")
+
+    newtags = set(taglist)
+    oldtags = set(module_tag_list(context, module_id))
+    to_del = oldtags - newtags
+    to_add = newtags - oldtags
+
+    # should be atomic, but it's not.
+    for tagname in to_add:
+        t = ModuleTag(context, tagname, object_id=module_id)
+    for tagname in to_del:
+        t = ModuleTag(context, tagname)
+        t.remove_tag_from_object(module_id)
+
+
+def get_etud_tagged_modules(context, etudid, tagname):
+    """Liste d'infos sur les modules de ce semestre avec ce tag.
+    Cherche dans tous les semestres dans lesquel l'étudiant est ou a été inscrit.
+    Construit la liste des modules avec le tag donné par tagname    
+    """
+    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+    R = []
+    for sem in etud["sems"]:
+        nt = context._getNotesCache().get_NotesTable(context, sem["formsemestre_id"])
+        modimpls = nt.get_modimpls()
+        for modimpl in modimpls:
+            tags = module_tag_list(context, module_id=modimpl["module_id"])
+            if tagname in tags:
+                moy = nt.get_etud_mod_moy(
+                    modimpl["moduleimpl_id"], etudid
+                )  # ou NI si non inscrit
+                R.append(
+                    {
+                        "sem": sem,
+                        "moy": moy,  # valeur réelle, ou NI (non inscrit au module ou NA0 (pas de note)
+                        "moduleimpl": modimpl,
+                        "tags": tags,
+                    }
+                )
+    return R
+
+
+def split_tagname_coeff(tag, separateur=":"):
+    """Découpe un tag saisi par un utilisateur pour en extraire un tagname 
+    (chaine de caractère correspondant au tag)
+    et un éventuel coefficient de pondération, avec le séparateur fourni (par défaut ":").
+    Renvoie le résultat sous la forme d'une liste [tagname, pond] où pond est un float
+
+    Auteur: CB
+    """
+    if separateur in tag:
+        temp = tag.split(":")
+        try:
+            pond = float(temp[1])
+            return [temp[0], pond]
+        except:
+            return [tag, 1.0]  # renvoie tout le tag si le découpage à échouer
+    else:
+        # initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération n'est indiqué dans le tag
+        return [tag, 1.0]
+
+
+"""Tests:
+from debug import *
+from sco_tag_module import *
+context = go_dept(app, 'RT').Notes
+
+t = ModuleTag(context, 'essai')
+t.tag_module('totoro') # error (module invalide)
+t.tag_module('MOD21460')
+t.delete() # detruit tag et assoc
+t = ModuleTag(context, 'essai2')
+t.tag_module('MOD21460')
+t.tag_module('MOD21464')
+t.list_modules()
+t.list_modules(formation_code='ccc') # empty list
+t.list_modules(formation_code='FCOD2')
+
+
+Un essai de get_etud_tagged_modules:
+from debug import *
+from sco_tag_module import *
+context = go_dept(app, 'GEA').Notes
+
+etudid='GEAEID80687'
+etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+sem = etud['sems'][0]
+
+[ tm['moy'] for tm in get_etud_tagged_modules(context, etudid, 'allo') ]
+
+# si besoin après modif par le Web:
+# context._inval_cache()
+"""
diff --git a/sco_trombino.py b/sco_trombino.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3f8eb452105267df3130fcb3186962146aa14dc
--- /dev/null
+++ b/sco_trombino.py
@@ -0,0 +1,732 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Photos: trombinoscopes
+"""
+
+try:
+    from cStringIO import StringIO
+except:
+    from StringIO import StringIO
+from zipfile import ZipFile, BadZipfile
+import xml
+import tempfile
+
+from notes_log import log
+from sco_utils import *
+import scolars
+import sco_photos
+import sco_formsemestre
+import sco_groups
+import sco_groups_view
+import sco_portal_apogee
+from sco_formsemestre_status import makeMenu
+from sco_pdf import *
+import ImportScolars
+import sco_excel
+from reportlab.lib import colors
+
+
+def trombino(
+    context,
+    REQUEST=None,
+    group_ids=[],  # liste des groupes à afficher
+    formsemestre_id=None,  # utilisé si pas de groupes selectionné
+    etat=None,
+    format="html",
+    dialog_confirmed=False,
+):
+    """Trombinoscope"""
+    if not etat:
+        etat = None  # may be passed as ''
+    # Informations sur les groupes à afficher:
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids, formsemestre_id=formsemestre_id, etat=etat, REQUEST=REQUEST
+    )
+
+    #
+    if format != "html" and not dialog_confirmed:
+        ok, dialog = check_local_photos_availability(
+            context, groups_infos, REQUEST, format=format
+        )
+        if not ok:
+            return dialog
+
+    if format == "zip":
+        return _trombino_zip(context, groups_infos, REQUEST)
+    elif format == "pdf":
+        return _trombino_pdf(context, groups_infos, REQUEST)
+    elif format == "pdflist":
+        return _listeappel_photos_pdf(context, groups_infos, REQUEST)
+    else:
+        raise Exception("invalid format")
+        # return _trombino_html_header(context, REQUEST) + trombino_html(context, group, members, REQUEST=REQUEST) + context.sco_footer(REQUEST)
+
+
+def _trombino_html_header(context, REQUEST):
+    return context.sco_header(REQUEST, javascripts=["js/trombino.js"])
+
+
+def trombino_html(context, groups_infos, REQUEST=None):
+    "HTML snippet for trombino (with title and menu)"
+    args = groups_infos.groups_query_args
+    menuTrombi = [
+        {"title": "Charger des photos...", "url": "photos_import_files_form?%s" % args},
+        {
+            "title": "Obtenir archive Zip des photos",
+            "url": "trombino?%s&amp;format=zip" % args,
+        },
+        {
+            "title": "Recopier les photos depuis le portail",
+            "url": "trombino_copy_photos?%s" % args,
+        },
+    ]
+
+    if groups_infos.members:
+        if groups_infos.tous_les_etuds_du_sem:
+            ng = "Tous les étudiants"
+        else:
+            ng = "Groupe %s" % groups_infos.groups_titles
+    else:
+        ng = "Aucun étudiant inscrit dans ce groupe !"
+    H = [
+        '<table style="padding-top: 10px; padding-bottom: 10px;"><tr><td><span style="font-style: bold; font-size: 150%%; padding-right: 20px;">%s</span></td>'
+        % (ng)
+    ]
+    if groups_infos.members:
+        H.append(
+            "<td>" + makeMenu("Gérer les photos", menuTrombi, alone=True) + "</td>"
+        )
+    H.append("</tr></table>")
+    H.append("<div>")
+    i = 0
+    for t in groups_infos.members:
+        H.append(
+            '<span class="trombi_box"><span class="trombi-photo" id="trombi-%s">'
+            % t["etudid"]
+        )
+        if sco_photos.etud_photo_is_local(context, t, size="small"):
+            foto = sco_photos.etud_photo_html(context, t, title="", REQUEST=REQUEST)
+        else:  # la photo n'est pas immédiatement dispo
+            foto = (
+                '<span class="unloaded_img" id="%s"><img border="0" height="90" alt="en cours" src="/ScoDoc/static/icons/loading.jpg"/></span>'
+                % t["etudid"]
+            )
+        H.append('<a href="ficheEtud?etudid=' + t["etudid"] + '">' + foto + "</a>")
+        H.append("</span>")
+        H.append(
+            '<span class="trombi_legend"><span class="trombi_prenom">'
+            + scolars.format_prenom(t["prenom"])
+            + '</span><span class="trombi_nom">'
+            + scolars.format_nom(t["nom"])
+        )
+        H.append("</span></span></span>")
+        i += 1
+
+    H.append("</div>")
+    H.append(
+        '<div style="margin-bottom:15px;"><a class="stdlink" href="trombino?format=pdf&amp;%s">Version PDF</a></div>'
+        % args
+    )
+    return "\n".join(H)
+
+
+def check_local_photos_availability(context, groups_infos, REQUEST, format=""):
+    """Verifie que toutes les photos (des gropupes indiqués) sont copiées localement
+    dans ScoDoc (seules les photos dont nous disposons localement peuvent être exportées 
+    en pdf ou en zip).
+    Si toutes ne sont pas dispo, retourne un dialogue d'avertissement pour l'utilisateur.
+    """
+    nb_missing = 0
+    for t in groups_infos.members:
+        etudid = t["etudid"]
+        url = sco_photos.etud_photo_url(context, t, REQUEST=REQUEST)  # -> copy distant files if needed
+        if not sco_photos.etud_photo_is_local(context, t):
+            nb_missing += 1
+    if nb_missing > 0:
+        parameters = {"group_ids": groups_infos.group_ids, "format": format}
+        return (
+            False,
+            context.confirmDialog(
+                """<p>Attention: %d photos ne sont pas disponibles et ne peuvent pas être exportées.</p><p>Vous pouvez <a class="stdlink" href="%s">exporter seulement les photos existantes</a>"""
+                % (
+                    nb_missing,
+                    groups_infos.base_url
+                    + "&amp;dialog_confirmed=1&amp;format=%s" % format,
+                ),
+                dest_url="trombino",
+                OK="Exporter seulement les photos existantes",
+                cancel_url="groups_view?curtab=tab-photos&amp;"
+                + groups_infos.groups_query_args,
+                REQUEST=REQUEST,
+                parameters=parameters,
+            ),
+        )
+    else:
+        return True, ""
+
+
+def _trombino_zip(context, groups_infos, REQUEST):
+    "Send photos as zip archive"
+    data = StringIO()
+    Z = ZipFile(data, "w")
+    # assume we have the photos (or the user acknowledged the fact)
+    # Archive originals (not reduced) images, in JPEG
+    for t in groups_infos.members:
+        im_path = sco_photos.photo_pathname(context, t, size="orig")
+        if not im_path:
+            continue
+        img = open(im_path).read()
+        code_nip = t["code_nip"]
+        if code_nip:
+            filename = code_nip + ".jpg"
+        else:
+            filename = t["nom"] + "_" + t["prenom"] + "_" + t["etudid"] + ".jpg"
+        Z.writestr(filename, img)
+    Z.close()
+    size = data.tell()
+    log("trombino_zip: %d bytes" % size)
+    content_type = "application/zip"
+    REQUEST.RESPONSE.setHeader(
+        "content-disposition", 'attachement; filename="trombi.zip"'
+    )
+    REQUEST.RESPONSE.setHeader("content-type", content_type)
+    REQUEST.RESPONSE.setHeader("content-length", size)
+    return data.getvalue()
+
+
+# Copy photos from portal to ScoDoc
+def trombino_copy_photos(context, group_ids=[], REQUEST=None, dialog_confirmed=False):
+    "Copy photos from portal to ScoDoc (overwriting local copy)"
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids, REQUEST=REQUEST
+    )
+    back_url = "groups_view?%s&amp;curtab=tab-photos" % groups_infos.groups_query_args
+
+    portal_url = sco_portal_apogee.get_portal_url(context)
+    header = context.sco_header(REQUEST, page_title="Chargement des photos")
+    footer = context.sco_footer(REQUEST)
+    if not portal_url:
+        return (
+            header
+            + '<p>portail non configuré</p><p><a href="%s">Retour au trombinoscope</a></p>'
+            % back_url
+            + footer
+        )
+    if not dialog_confirmed:
+        return context.confirmDialog(
+            """<h2>Copier les photos du portail vers ScoDoc ?</h2>
+                <p>Les photos du groupe %s présentes dans ScoDoc seront remplacées par celles du portail (si elles existent).</p>
+                <p>(les photos sont normalement automatiquement copiées lors de leur première utilisation, l'usage de cette fonction n'est nécessaire que si les photos du portail ont été modifiées)</p>
+                """
+            % (groups_infos.groups_titles),
+            dest_url="",
+            REQUEST=REQUEST,
+            cancel_url=back_url,
+            parameters={"group_ids": group_ids},
+        )
+
+    msg = []
+    nok = 0
+    for etud in groups_infos.members:
+        path, diag = sco_photos.copy_portal_photo_to_fs(context, etud, REQUEST=REQUEST)
+        msg.append(diag)
+        if path:
+            nok += 1
+
+    msg.append("<b>%d photos correctement chargées</b>" % nok)
+
+    return (
+        header
+        + "<h2>Chargement des photos depuis le portail</h2><ul><li>"
+        + "</li><li>".join(msg)
+        + "</li></ul>"
+        + '<p><a href="%s">retour au trombinoscope</a>' % back_url
+        + footer
+    )
+
+
+def _get_etud_platypus_image(context, t, image_width=2 * cm):
+    """Returns aplatypus object for the photo of student t
+    """
+    try:
+        path = sco_photos.photo_pathname(context, t, size="small")
+        if not path:
+            # log('> unknown')
+            path = sco_photos.UNKNOWN_IMAGE_PATH
+        im = PILImage.open(path)
+        w0, h0 = im.size[0], im.size[1]
+        if w0 > h0:
+            W = image_width
+            H = h0 * W / w0
+        else:
+            H = image_width
+            W = w0 * H / h0
+        return reportlab.platypus.Image(path, width=W, height=H)
+    except:
+        log(
+            "*** exception while processing photo of %s (%s) (path=%s)"
+            % (t["nom"], t["etudid"], path)
+        )
+        raise
+
+
+def _trombino_pdf(context, groups_infos, REQUEST):
+    "Send photos as pdf page"
+    # Generate PDF page
+    filename = "trombino_%s" % groups_infos.groups_filename + ".pdf"
+    sem = groups_infos.formsemestre  # suppose 1 seul semestre
+
+    PHOTOWIDTH = 3 * cm
+    COLWIDTH = 3.6 * cm
+    N_PER_ROW = 5  # XXX should be in ScoDoc preferences
+
+    StyleSheet = styles.getSampleStyleSheet()
+    report = StringIO()  # in-memory document, no disk file
+    objects = [
+        Paragraph(
+            SU("Trombinoscope " + sem["titreannee"] + " " + groups_infos.groups_titles),
+            StyleSheet["Heading3"],
+        )
+    ]
+    L = []
+    n = 0
+    currow = []
+    log("_trombino_pdf %d elements" % len(groups_infos.members))
+    for t in groups_infos.members:
+        img = _get_etud_platypus_image(context, t, image_width=PHOTOWIDTH)
+        elem = Table(
+            [
+                [img],
+                [
+                    Paragraph(
+                        SU(
+                            scolars.format_sexe(t["sexe"])
+                            + " "
+                            + scolars.format_prenom(t["prenom"])
+                            + " "
+                            + scolars.format_nom(t["nom"])
+                        ),
+                        StyleSheet["Normal"],
+                    )
+                ],
+            ],
+            colWidths=[PHOTOWIDTH],
+        )
+        currow.append(elem)
+        if n == (N_PER_ROW - 1):
+            L.append(currow)
+            currow = []
+        n = (n + 1) % N_PER_ROW
+    if currow:
+        currow += [" "] * (N_PER_ROW - len(currow))
+        L.append(currow)
+    if not L:
+        table = Paragraph(SU("Aucune photo à exporter !"), StyleSheet["Normal"])
+    else:
+        table = Table(
+            L,
+            colWidths=[COLWIDTH] * N_PER_ROW,
+            style=TableStyle(
+                [
+                    # ('RIGHTPADDING', (0,0), (-1,-1), -5*mm),
+                    ("VALIGN", (0, 0), (-1, -1), "TOP"),
+                    ("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
+                ]
+            ),
+        )
+    objects.append(table)
+    # Build document
+    document = BaseDocTemplate(report)
+    document.addPageTemplates(
+        ScolarsPageTemplate(
+            document, preferences=context.get_preferences(sem["formsemestre_id"])
+        )
+    )
+    document.build(objects)
+    data = report.getvalue()
+
+    return sendPDFFile(REQUEST, data, filename)
+
+
+# --------------------- Sur une idée de l'IUT d'Orléans:
+def _listeappel_photos_pdf(context, groups_infos, REQUEST):
+    "Doc pdf pour liste d'appel avec photos"
+    filename = "trombino_%s" % groups_infos.groups_filename + ".pdf"
+    sem = groups_infos.formsemestre  # suppose 1 seul semestre
+
+    PHOTOWIDTH = 2 * cm
+    COLWIDTH = 3.6 * cm
+    ROWS_PER_PAGE = 26  # XXX should be in ScoDoc preferences
+
+    StyleSheet = styles.getSampleStyleSheet()
+    report = StringIO()  # in-memory document, no disk file
+    objects = [
+        Paragraph(
+            SU(
+                sem["titreannee"]
+                + " "
+                + groups_infos.groups_titles
+                + " (%d)" % len(groups_infos.members)
+            ),
+            StyleSheet["Heading3"],
+        )
+    ]
+    L = []
+    n = 0
+    currow = []
+    log("_listeappel_photos_pdf %d elements" % len(groups_infos.members))
+    n = len(groups_infos.members)
+    # npages = n / 2*ROWS_PER_PAGE + 1 # nb de pages papier
+    # for page in range(npages):
+    for i in range(n):  # page*2*ROWS_PER_PAGE, (page+1)*2*ROWS_PER_PAGE):
+        t = groups_infos.members[i]
+        img = _get_etud_platypus_image(context, t, image_width=PHOTOWIDTH)
+        txt = Paragraph(
+            SU(
+                scolars.format_sexe(t["sexe"])
+                + " "
+                + scolars.format_prenom(t["prenom"])
+                + " "
+                + scolars.format_nom(t["nom"])
+            ),
+            StyleSheet["Normal"],
+        )
+        if currow:
+            currow += [""]
+        currow += [img, txt, ""]
+        if i % 2:
+            L.append(currow)
+            currow = []
+    if currow:
+        currow += [" "] * 3
+        L.append(currow)
+    if not L:
+        table = Paragraph(SU("Aucune photo à exporter !"), StyleSheet["Normal"])
+    else:
+        table = Table(
+            L,
+            colWidths=[2 * cm, 4 * cm, 27 * mm, 5 * mm, 2 * cm, 4 * cm, 27 * mm],
+            style=TableStyle(
+                [
+                    # ('RIGHTPADDING', (0,0), (-1,-1), -5*mm),
+                    ("VALIGN", (0, 0), (-1, -1), "TOP"),
+                    ("GRID", (0, 0), (2, -1), 0.25, colors.grey),
+                    ("GRID", (4, 0), (-1, -1), 0.25, colors.grey),
+                ]
+            ),
+        )
+    objects.append(table)
+    # Build document
+    document = BaseDocTemplate(report)
+    document.addPageTemplates(
+        ScolarsPageTemplate(
+            document, preferences=context.get_preferences(sem["formsemestre_id"])
+        )
+    )
+    document.build(objects)
+    data = report.getvalue()
+
+    return sendPDFFile(REQUEST, data, filename)
+
+    objects = []
+    StyleSheet = styles.getSampleStyleSheet()
+    report = StringIO()  # in-memory document, no disk file
+    filename = ("trombino-%s.pdf" % ng).replace(
+        " ", "_"
+    )  # XXX should sanitize this filename
+    objects.append(
+        Paragraph(SU("Liste " + sem["titreannee"] + " " + ng), StyleSheet["Heading3"])
+    )
+    PHOTOWIDTH = 3 * cm
+    COLWIDTH = 3.6 * cm
+
+    L = []  # cells
+    n = 0
+    currow = []
+    for t in T:
+        n = n + 1
+        img = _get_etud_platypus_image(context, t, image_width=2 * cm)
+        currow += [
+            Paragraph(
+                SU(
+                    scolars.format_sexe(t["sexe"])
+                    + " "
+                    + scolars.format_prenom(t["prenom"])
+                    + " "
+                    + scolars.format_nom(t["nom"])
+                ),
+                StyleSheet["Normal"],
+            ),
+            "",  # empty cell (signature ou autre info a remplir sur papier)
+            img,
+        ]
+
+    if not L:
+        table = Paragraph(SU("Aucune photo à exporter !"), StyleSheet["Normal"])
+    else:
+        table = Table(
+            L,
+            colWidths=[COLWIDTH] * 7,
+            style=TableStyle(
+                [
+                    ("VALIGN", (0, 0), (-1, -1), "TOP"),
+                    ("GRID", (0, 0), (2, -1), 0.25, colors.grey),
+                    ("GRID", (2, 0), (-1, -1), 0.25, colors.red),  # <<<
+                ]
+            ),
+        )
+    objects.append(table)
+
+    # Réduit sur une page
+    objects = [KeepInFrame(0, 0, objects, mode="shrink")]
+
+    # --- Build document
+    document = BaseDocTemplate(report)
+    document.addPageTemplates(
+        ScolarsPageTemplate(
+            document, preferences=context.get_preferences(sem["formsemestre_id"])
+        )
+    )
+    document.build(objects)
+    data = report.getvalue()
+    return sendPDFFile(REQUEST, data, filename)
+
+
+# ---------------------    Upload des photos de tout un groupe
+def photos_generate_excel_sample(context, group_ids=[], REQUEST=None):
+    """Feuille excel pour import fichiers photos
+    """
+    fmt = ImportScolars.sco_import_format()
+    data = ImportScolars.sco_import_generate_excel_sample(
+        fmt,
+        context=context,
+        group_ids=group_ids,
+        only_tables=["identite"],
+        exclude_cols=[
+            "date_naissance",
+            "lieu_naissance",
+            "nationalite",
+            "statut",
+            "photo_filename",
+        ],
+        extra_cols=["fichier_photo"],
+        REQUEST=REQUEST,
+    )
+    return sco_excel.sendExcelFile(REQUEST, data, "ImportPhotos.xls")
+
+
+def photos_import_files_form(context, group_ids=[], REQUEST=None):
+    """Formulaire pour importation photos
+    """
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids, REQUEST=REQUEST
+    )
+    back_url = "groups_view?%s&amp;curtab=tab-photos" % groups_infos.groups_query_args
+
+    H = [
+        context.sco_header(REQUEST, page_title="Import des photos des étudiants"),
+        """<h2 class="formsemestre">Téléchargement des photos des étudiants</h2>
+         <p><b>Vous pouvez aussi charger les photos individuellement via la fiche de chaque étudiant (menu "Etudiant" / "Changer la photo").</b></p>
+         <p class="help">Cette page permet de charger en une seule fois les photos de plusieurs étudiants.<br/>
+          Il faut d'abord remplir une feuille excel donnant les noms 
+          des fichiers images (une image par étudiant).
+         </p>
+         <p class="help">Ensuite, réunir vos images dans un fichier zip, puis télécharger 
+         simultanément le fichier excel et le fichier zip.
+         </p>
+        <ol>
+        <li><a class="stdlink" href="photos_generate_excel_sample?%s">
+        Obtenir la feuille excel à remplir</a>
+        </li>
+        <li style="padding-top: 2em;">
+         """
+        % groups_infos.groups_query_args,
+    ]
+    F = context.sco_footer(REQUEST)
+    REQUEST.form["group_ids"] = groups_infos.group_ids
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("xlsfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}),
+            ("zipfile", {"title": "Fichier zip:", "input_type": "file", "size": 40}),
+            ("group_ids", {"input_type": "hidden", "type": "list"}),
+        ),
+    )
+
+    if tf[0] == 0:
+        return "\n".join(H) + tf[1] + "</li></ol>" + F
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(back_url)
+    else:
+        return photos_import_files(
+            context,
+            group_ids=tf[2]["group_ids"],
+            xlsfile=tf[2]["xlsfile"],
+            zipfile=tf[2]["zipfile"],
+            REQUEST=REQUEST,
+        )
+
+
+def photos_import_files(
+    context, group_ids=[], xlsfile=None, zipfile=None, REQUEST=None
+):
+    """Importation des photos
+    """
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids, REQUEST=REQUEST
+    )
+    back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args
+    filename_title = "fichier_photo"
+    page_title = "Téléchargement des photos des étudiants"
+
+    def callback(context, etud, data, filename, REQUEST):
+        sco_photos.store_photo(context, etud, data, REQUEST)
+
+    r = zip_excel_import_files(
+        context, xlsfile, zipfile, REQUEST, callback, filename_title, page_title
+    )
+    return REQUEST.RESPONSE.redirect(back_url + "&amp;head_message=photos%20 importees")
+
+
+def zip_excel_import_files(
+    context,
+    xlsfile=None,
+    zipfile=None,
+    REQUEST=None,
+    callback=None,
+    filename_title="",  # doit obligatoirement etre specifié
+    page_title="",
+):
+    """Importation de fichiers à partir d'un excel et d'un zip
+    La fonction
+       callback()
+    est appelé pour chaque fichier trouvé.
+    """
+    # 1- build mapping etudid -> filename
+    exceldata = xlsfile.read()
+    if not exceldata:
+        raise ScoValueError("Fichier excel vide ou invalide")
+    diag, data = sco_excel.Excel_to_list(exceldata)
+    if not data:  # probably a bug
+        raise ScoValueError("Fichier excel vide !")
+    # on doit avoir une colonne etudid et une colonne filename_title ('fichier_photo')
+    titles = data[0]
+    try:
+        etudid_idx = titles.index("etudid")
+        filename_idx = titles.index(filename_title)
+    except:
+        raise ScoValueError(
+            "Fichier excel incorrect (il faut une colonne etudid et une colonne %s) !"
+            % filename_title
+        )
+
+    def normfilename(fn, lowercase=True):
+        "normalisation used to match filenames"
+        fn = fn.replace("\\", "/")  # not sure if this is necessary ?
+        fn = fn.strip()
+        if lowercase:
+            fn = strlower(fn)
+        fn = fn.split("/")[-1]  # use only last component, not directories
+        return fn
+
+    Filename2Etud = {}  # filename : etudid
+    for l in data[1:]:
+        filename = l[filename_idx].strip()
+        if filename:
+            Filename2Etud[normfilename(filename)] = l[etudid_idx]
+
+    # 2- Ouvre le zip et
+    try:
+        z = ZipFile(zipfile)
+    except BadZipfile:
+        raise ScoValueError("Fichier ZIP incorrect !")
+    ignored_zipfiles = []
+    stored = []  # [ (etud, filename) ]
+    for name in z.namelist():
+        if len(name) > 4 and name[-1] != "/" and "." in name:
+            data = z.read(name)
+            # match zip filename with name given in excel
+            normname = normfilename(name)
+            if normname in Filename2Etud:
+                etudid = Filename2Etud[normname]
+                # ok, store photo
+                try:
+                    etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+                    del Filename2Etud[normname]
+                except:
+                    raise ScoValueError("ID étudiant invalide: %s" % etudid)
+
+                callback(
+                    context,
+                    etud,
+                    data,
+                    normfilename(name, lowercase=False),
+                    REQUEST=REQUEST,
+                )
+
+                stored.append((etud, name))
+            else:
+                log("zip: zip name %s not in excel !" % name)
+                ignored_zipfiles.append(name)
+        else:
+            if name[-1] != "/":
+                ignored_zipfiles.append(name)
+            log("zip: ignoring %s" % name)
+    if Filename2Etud:
+        # lignes excel non traitées
+        unmatched_files = Filename2Etud.keys()
+    else:
+        unmatched_files = []
+    # 3- Result page
+    H = [
+        _trombino_html_header(context, REQUEST),
+        """<h2 class="formsemestre">%s</h2>
+         <h3>Opération effectuée</h3>
+         """
+        % page_title,
+    ]
+    if ignored_zipfiles:
+        H.append("<h4>Fichiers ignorés dans le zip:</h4><ul>")
+        for name in ignored_zipfiles:
+            H.append("<li>%s</li>" % name)
+        H.append("</ul>")
+    if unmatched_files:
+        H.append(
+            "<h4>Fichiers indiqués dans feuille mais non trouvés dans le zip:</h4><ul>"
+        )
+        for name in unmatched_files:
+            H.append("<li>%s</li>" % name)
+        H.append("</ul>")
+    if stored:
+        H.append("<h4>Fichiers chargés:</h4><ul>")
+        for (etud, name) in stored:
+            H.append("<li>%s: <tt>%s</tt></li>" % (etud["nomprenom"], name))
+        H.append("</ul>")
+
+    return "\n".join(H)
diff --git a/sco_trombino_tours.py b/sco_trombino_tours.py
new file mode 100644
index 0000000000000000000000000000000000000000..58155fd9c42747ccb1ce7d17bf63c994a11cf739
--- /dev/null
+++ b/sco_trombino_tours.py
@@ -0,0 +1,478 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Photos: trombinoscopes - Version IUT Tours
+   Code contribué par Jérôme Billoue, IUT de Tours, 2014
+   Modification Jérome Billoue,Vincent Grimaud, IUT de Tours, 2017
+"""
+
+try:
+    from cStringIO import StringIO
+except:
+    from StringIO import StringIO
+from zipfile import ZipFile, BadZipfile
+import xml
+import tempfile
+
+from notes_log import log
+from sco_utils import *
+import ZAbsences
+import scolars
+import sco_photos
+import sco_formsemestre
+import sco_groups
+import sco_groups_view
+
+import sco_trombino
+from sco_pdf import *
+from reportlab.lib import colors
+from reportlab.lib import pagesizes
+from reportlab.lib.pagesizes import A4, A3
+
+
+# Paramétrage de l'aspect graphique:
+PHOTOWIDTH = 2.8 * cm
+COLWIDTH = 3.4 * cm
+N_PER_ROW = 5
+
+
+def pdf_trombino_tours(
+    context,
+    group_ids=[],  # liste des groupes à afficher
+    formsemestre_id=None,  # utilisé si pas de groupes selectionné
+    REQUEST=None,
+):
+    """Generation du trombinoscope en fichier PDF
+    """
+    # Informations sur les groupes à afficher:
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST
+    )
+
+    DeptName = context.get_preference("DeptName")
+    DeptFullName = context.get_preference("DeptFullName")
+    UnivName = context.get_preference("UnivName")
+    InstituteName = context.get_preference("InstituteName")
+    # Generate PDF page
+    StyleSheet = styles.getSampleStyleSheet()
+    objects = []
+    T = Table(
+        [
+            [Paragraph(SU(InstituteName), StyleSheet["Heading3"])],
+            [Paragraph(SU("Département " + DeptFullName), StyleSheet["Heading3"])],
+            [
+                Paragraph(
+                    SU("Date ............ / ............ / ......................"),
+                    StyleSheet["Normal"],
+                ),
+                Paragraph(
+                    SU(
+                        "Module ......................................................."
+                    ),
+                    StyleSheet["Normal"],
+                ),
+            ],
+            [
+                Paragraph(
+                    SU("de ............h............ à ............h............ "),
+                    StyleSheet["Normal"],
+                ),
+                Paragraph(
+                    SU("Enseignant ................................................."),
+                    StyleSheet["Normal"],
+                ),
+            ],
+            [
+                Table(
+                    [
+                        [
+                            "Séance notée :",
+                            " ",
+                            "DS   ",
+                            " ",
+                            "TP Contrôle   ",
+                            " ",
+                            "Autre cas (TD ou TP noté, QCM, etc...)",
+                        ]
+                    ],
+                    style=TableStyle(
+                        [
+                            ("ALIGN", (0, 0), (-1, -1), "LEFT"),
+                            ("BOX", (1, 0), (1, 0), 0.75, black),
+                            ("BOX", (3, 0), (3, 0), 0.75, black),
+                            ("BOX", (5, 0), (5, 0), 0.75, black),
+                        ]
+                    ),
+                )
+            ],
+        ],
+        colWidths=(COLWIDTH * N_PER_ROW) / 2,
+        style=TableStyle(
+            [
+                ("ALIGN", (0, 0), (-1, -1), "LEFT"),
+                ("SPAN", (0, 1), (1, 1)),
+                ("SPAN", (0, 4), (1, 4)),
+                ("BOTTOMPADDING", (0, -1), (-1, -1), 10),
+                ("BOX", (0, 0), (-1, -1), 0.75, black),
+            ]
+        ),
+    )
+
+    objects.append(T)
+
+    groups = ""
+
+    for group_id in groups_infos.group_ids:
+        if group_id != "None":
+            members, group, group_tit, sem, nbdem = sco_groups.get_group_infos(
+                context, group_id, "I"
+            )
+            groups += " %s" % group_tit
+            L = []
+            currow = []
+
+            if sem["semestre_id"] != -1:
+                currow = [
+                    Paragraph(
+                        SU(
+                            "<para align=center>Semestre %s</para>" % sem["semestre_id"]
+                        ),
+                        StyleSheet["Normal"],
+                    )
+                ]
+            currow += [" "] * (N_PER_ROW - len(currow) - 1)
+            currow += [
+                Paragraph(
+                    SU("<para align=center>%s</para>" % sem["anneescolaire"]),
+                    StyleSheet["Normal"],
+                )
+            ]
+            L.append(currow)
+            currow = [" "] * N_PER_ROW
+            L.append(currow)
+
+            currow = []
+            currow.append(
+                Paragraph(
+                    SU("<para align=center><b>" + group_tit + "</b></para>"),
+                    StyleSheet["Heading3"],
+                )
+            )
+            n = 1
+            for m in members:
+                img = sco_trombino._get_etud_platypus_image(
+                    context, m, image_width=PHOTOWIDTH
+                )
+                etud_main_group = sco_groups.get_etud_main_group(
+                    context, m["etudid"], sem
+                )
+                if group_id != etud_main_group["group_id"]:
+                    text_group = " (" + etud_main_group["group_name"] + ")"
+                else:
+                    text_group = ""
+                elem = Table(
+                    [
+                        [img],
+                        [
+                            Paragraph(
+                                SU(
+                                    "<para align=center><font size=8>"
+                                    + scolars.format_prenom(m["prenom"])
+                                    + " "
+                                    + scolars.format_nom(m["nom"])
+                                    + text_group
+                                    + "</font></para>"
+                                ),
+                                StyleSheet["Normal"],
+                            )
+                        ],
+                    ],
+                    colWidths=[COLWIDTH],
+                    style=TableStyle([("ALIGN", (0, 0), (-1, -1), "CENTER")]),
+                )
+                currow.append(elem)
+                if n == (N_PER_ROW - 1):
+                    L.append(currow)
+                    currow = []
+                n = (n + 1) % N_PER_ROW
+            if currow:
+                currow += [" "] * (N_PER_ROW - len(currow))
+                L.append(currow)
+            if not L:
+                T = Paragraph(SU("Aucune photo à exporter !"), StyleSheet["Normal"])
+            else:
+                T = Table(
+                    L,
+                    colWidths=[COLWIDTH] * N_PER_ROW,
+                    style=TableStyle(
+                        [
+                            ("LEFTPADDING", (0, 0), (-1, -1), 0),
+                            ("RIGHTPADDING", (0, 0), (-1, -1), 0),
+                            ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
+                            ("TOPPADDING", (0, 1), (-1, -1), 0),
+                            ("TOPPADDING", (0, 0), (-1, 0), 10),
+                            ("LINEBELOW", (1, 0), (-2, 0), 0.75, black),
+                            ("VALIGN", (0, 0), (-1, 1), "MIDDLE"),
+                            ("VALIGN", (0, 2), (-1, -1), "TOP"),
+                            ("VALIGN", (0, 2), (0, 2), "MIDDLE"),
+                            ("SPAN", (0, 0), (0, 1)),
+                            ("SPAN", (-1, 0), (-1, 1)),
+                        ]
+                    ),
+                )
+
+            objects.append(T)
+
+    T = Table(
+        [
+            [
+                Paragraph(
+                    SU(
+                        "Nombre d'absents : ................. (Merci d'entourer les absents SVP)"
+                    ),
+                    StyleSheet["Normal"],
+                )
+            ]
+        ],
+        colWidths=(COLWIDTH * N_PER_ROW),
+        style=TableStyle(
+            [
+                ("ALIGN", (0, 0), (-1, -1), "CENTER"),
+                ("BOTTOMPADDING", (0, -1), (-1, -1), 10),
+                ("BOX", (0, 0), (-1, -1), 0.75, black),
+            ]
+        ),
+    )
+
+    objects.append(T)
+
+    # Réduit sur une page
+    objects = [KeepInFrame(0, 0, objects, mode="shrink")]
+    # Build document
+    report = StringIO()  # in-memory document, no disk file
+    filename = "trombino-%s-%s.pdf" % (DeptName, groups_infos.groups_filename)
+    document = BaseDocTemplate(report)
+    document.addPageTemplates(
+        ScolarsPageTemplate(document, preferences=context.get_preferences())
+    )
+    document.build(objects)
+    data = report.getvalue()
+
+    return sendPDFFile(REQUEST, data, filename)
+
+
+# Feuille d'absences en pdf avec photos:
+
+
+def pdf_feuille_releve_absences(
+    context,
+    group_ids=[],  # liste des groupes à afficher
+    formsemestre_id=None,  # utilisé si pas de groupes selectionné
+    REQUEST=None,
+):
+    """Generation de la feuille d'absence en fichier PDF, avec photos
+    """
+
+    NB_CELL_AM = context.get_preference("feuille_releve_abs_AM")
+    NB_CELL_PM = context.get_preference("feuille_releve_abs_PM")
+    COLWIDTH = 0.85 * cm
+    if context.get_preference("feuille_releve_abs_samedi"):
+        days = ZAbsences.DAYNAMES[:6]  # Lundi, ..., Samedi
+    else:
+        days = ZAbsences.DAYNAMES[:5]  # Lundi, ..., Vendredi
+    nb_days = len(days)
+
+    # Informations sur les groupes à afficher:
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids, formsemestre_id=formsemestre_id, REQUEST=REQUEST
+    )
+
+    DeptName = context.get_preference("DeptName")
+    DeptFullName = context.get_preference("DeptFullName")
+    UnivName = context.get_preference("UnivName")
+    InstituteName = context.get_preference("InstituteName")
+    # Generate PDF page
+    StyleSheet = styles.getSampleStyleSheet()
+    objects = [
+        Table(
+            [
+                [
+                    Paragraph(SU(InstituteName), StyleSheet["Heading3"]),
+                    Paragraph(
+                        SU(
+                            "<para align=right>Semaine ..................................................................</para>"
+                        ),
+                        StyleSheet["Normal"],
+                    ),
+                ],
+                [
+                    Paragraph(
+                        SU("Département " + DeptFullName), StyleSheet["Heading3"]
+                    ),
+                    "",
+                ],
+            ],
+            style=TableStyle(
+                [("SPAN", (0, 1), (1, 1)), ("BOTTOMPADDING", (0, -1), (-1, -1), 10)]
+            ),
+        )
+    ]
+
+    currow = [""] * (NB_CELL_AM + 1 + NB_CELL_PM + 1)
+    elem_day = Table(
+        [currow],
+        colWidths=([COLWIDTH] * (NB_CELL_AM + 1 + NB_CELL_PM + 1)),
+        style=TableStyle(
+            [
+                ("GRID", (0, 0), (NB_CELL_AM - 1, 0), 0.25, black),
+                (
+                    "GRID",
+                    (NB_CELL_AM + 1, 0),
+                    (NB_CELL_AM + NB_CELL_PM, 0),
+                    0.25,
+                    black,
+                ),
+            ]
+        ),
+    )
+    W = []
+    currow = []
+    for n in range(nb_days):
+        currow.append(elem_day)
+    W.append(currow)
+
+    elem_week = Table(
+        W,
+        colWidths=([COLWIDTH * (NB_CELL_AM + 1 + NB_CELL_PM + 1)] * nb_days),
+        style=TableStyle(
+            [
+                ("LEFTPADDING", (0, 0), (-1, -1), 0),
+                ("RIGHTPADDING", (0, 0), (-1, -1), 0),
+                ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
+                ("TOPPADDING", (0, 0), (-1, -1), 0),
+            ]
+        ),
+    )
+    currow = []
+    for n in range(nb_days):
+        currow += [Paragraph(SU("<b>" + days[n] + "</b>"), StyleSheet["Normal"])]
+
+    elem_day_name = Table(
+        [currow],
+        colWidths=([COLWIDTH * (NB_CELL_AM + 1 + NB_CELL_PM + 1)] * nb_days),
+        style=TableStyle(
+            [
+                ("LEFTPADDING", (0, 0), (-1, -1), 0),
+                ("RIGHTPADDING", (0, 0), (-1, -1), 0),
+                ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
+                ("TOPPADDING", (0, 0), (-1, -1), 0),
+            ]
+        ),
+    )
+
+    for group_id in groups_infos.group_ids:
+        members, group, group_tit, sem, nbdem = sco_groups.get_group_infos(
+            context, group_id, "I"
+        )
+        L = []
+
+        currow = [
+            Paragraph(SU("<b>Groupe " + group_tit + "</b>"), StyleSheet["Normal"])
+        ]
+        currow.append(elem_day_name)
+        L.append(currow)
+
+        currow = [Paragraph(SU("Initiales enseignant :"), StyleSheet["Normal"])]
+        currow.append(elem_week)
+        L.append(currow)
+
+        currow = [Paragraph(SU("Initiales module :"), StyleSheet["Normal"])]
+        currow.append(elem_week)
+        L.append(currow)
+
+        for m in members:
+            currow = [
+                Paragraph(
+                    SU(
+                        scolars.format_nom(m["nom"])
+                        + " "
+                        + scolars.format_prenom(m["prenom"])
+                    ),
+                    StyleSheet["Normal"],
+                )
+            ]
+            currow.append(elem_week)
+            L.append(currow)
+
+        if not L:
+            T = Paragraph(SU("Aucun étudiant !"), StyleSheet["Normal"])
+        else:
+            T = Table(
+                L,
+                colWidths=(
+                    [5.0 * cm, (COLWIDTH * (NB_CELL_AM + 1 + NB_CELL_PM + 1) * nb_days)]
+                ),
+                style=TableStyle(
+                    [
+                        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
+                        ("LEFTPADDING", (0, 0), (-1, -1), 0),
+                        ("RIGHTPADDING", (0, 0), (-1, -1), 0),
+                        ("BOTTOMPADDING", (0, 0), (-1, -1), 3),
+                        ("TOPPADDING", (0, 0), (-1, -1), 3),
+                        ("BOTTOMPADDING", (0, -1), (-1, -1), 10),
+                        (
+                            "ROWBACKGROUNDS",
+                            (0, 2),
+                            (-1, -1),
+                            (colors.white, colors.lightgrey),
+                        ),
+                    ]
+                ),
+            )
+
+        objects.append(T)
+
+    # Réduit sur une page
+    objects = [KeepInFrame(0, 0, objects, mode="shrink")]
+    # Build document
+    report = StringIO()  # in-memory document, no disk file
+    filename = "absences-%s-%s.pdf" % (DeptName, groups_infos.groups_filename)
+    if context.get_preference("feuille_releve_abs_taille") == "A3":
+        taille = A3
+    elif context.get_preference("feuille_releve_abs_taille") == "A4":
+        taille = A4
+    if context.get_preference("feuille_releve_abs_format") == "Paysage":
+        document = BaseDocTemplate(report, pagesize=landscape(taille))
+    else:
+        document = BaseDocTemplate(report, pagesize=taille)
+    document.addPageTemplates(
+        ScolarsPageTemplate(document, preferences=context.get_preferences())
+    )
+    document.build(objects)
+    data = report.getvalue()
+
+    return sendPDFFile(REQUEST, data, filename)
diff --git a/sco_ue_external.py b/sco_ue_external.py
new file mode 100644
index 0000000000000000000000000000000000000000..c50ce5fddbfed9f1a6ad7996912fea9f96b4eeb9
--- /dev/null
+++ b/sco_ue_external.py
@@ -0,0 +1,369 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""Fonction de gestion des UE "externes" (effectuees dans un cursus exterieur)
+
+On rapatrie (saisit) les notes (et crédits ECTS).
+
+Contexte: les étudiants d'une formation gérée par ScoDoc peuvent
+suivre un certain nombre d'UE à l'extérieur. L'établissement a reconnu
+au préalable une forme d'équivalence entre ces UE et celles du
+programme. Les UE effectuées à l'extérieur sont par nature variable
+d'un étudiant à l'autre et d'une année à l'autre, et ne peuvent pas
+être introduites dans le programme pédagogique ScoDoc sans alourdir
+considérablement les opérations (saisie, affichage du programme,
+gestion des inscriptions).
+En outre, un  suivi détaillé de ces UE n'est pas nécessaire: il suffit
+de pouvoir y associer une note et une quantité de crédits ECTS.
+
+Solution proposée (nov 2014):
+ - un nouveau type d'UE qui
+
+    -  s'affichera à part dans le programme pédagogique
+    et les bulletins
+    - pas présentées lors de la mise en place de semestres
+    - affichage sur bulletin des étudiants qui y sont inscrit
+    - création en même temps que la saisie de la note
+       (chaine creation: UE/matière/module, inscription étudiant, entrée valeur note)
+       avec auto-suggestion du nom pour limiter la création de doublons
+    - seront aussi présentées (à part) sur la page "Voir les inscriptions aux modules"
+
+"""
+
+from notesdb import *
+from sco_utils import *
+from notes_log import log
+import sco_formsemestre
+import sco_edit_ue
+import sco_saisie_notes
+import sco_codes_parcours
+
+
+def external_ue_create(
+    context,
+    formsemestre_id,
+    titre="",
+    acronyme="",
+    ue_type=UE_STANDARD,
+    ects=0.0,
+    REQUEST=None,
+):
+    """Crée UE/matiere/module/evaluation puis saisie les notes
+    """
+    log("external_ue_create( formsemestre_id=%s, titre=%s )" % (formsemestre_id, titre))
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    # Contrôle d'accès:
+    authuser = REQUEST.AUTHENTICATED_USER
+    if not authuser.has_permission(ScoImplement, context):
+        if not sem["resp_can_edit"] or str(authuser) not in sem["responsables"]:
+            raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
+    #
+    formation_id = sem["formation_id"]
+    log("creating external UE in %s: %s" % (formsemestre_id, acronyme))
+
+    numero = sco_edit_ue.next_ue_numero(
+        context, formation_id, semestre_id=sem["semestre_id"]
+    )
+    ue_id = context.do_ue_create(
+        {
+            "formation_id": formation_id,
+            "titre": titre,
+            "acronyme": acronyme,
+            "numero": numero,
+            "type": ue_type,
+            "ects": ects,
+            "is_external": 1,
+        },
+        REQUEST,
+    )
+
+    matiere_id = context.do_matiere_create(
+        {"ue_id": ue_id, "titre": titre or acronyme, "numero": 1}, REQUEST
+    )
+
+    module_id = context.do_module_create(
+        {
+            "titre": "UE extérieure",
+            "code": acronyme,
+            "coefficient": ects,  # tous le coef. module est egal à la quantite d'ECTS
+            "ue_id": ue_id,
+            "matiere_id": matiere_id,
+            "formation_id": formation_id,
+            "semestre_id": sem["semestre_id"],
+        },
+        REQUEST,
+    )
+
+    moduleimpl_id = context.do_moduleimpl_create(
+        {
+            "module_id": module_id,
+            "formsemestre_id": formsemestre_id,
+            "responsable_id": sem["responsables"][
+                0
+            ],  # affecte le 1er responsable du semestre comme resp. du module
+        }
+    )
+
+    return moduleimpl_id
+
+
+def external_ue_inscrit_et_note(
+    context, moduleimpl_id, formsemestre_id, notes_etuds, REQUEST=None
+):
+    log(
+        "external_ue_inscrit_et_note(moduleimpl_id=%s, notes_etuds=%s)"
+        % (moduleimpl_id, notes_etuds)
+    )
+    # Inscription des étudiants
+    context.do_moduleimpl_inscrit_etuds(
+        moduleimpl_id, formsemestre_id, notes_etuds.keys(), REQUEST=REQUEST
+    )
+
+    # Création d'une évaluation si il n'y en a pas déjà:
+    ModEvals = context.do_evaluation_list(args={"moduleimpl_id": moduleimpl_id})
+    if len(ModEvals):
+        # met la note dans le première évaluation existante:
+        evaluation_id = ModEvals[0]["evaluation_id"]
+    else:
+        # crée une évaluation:
+        evaluation_id = context.do_evaluation_create(
+            REQUEST=REQUEST,
+            moduleimpl_id=moduleimpl_id,
+            note_max=20.0,
+            coefficient=1.0,
+            publish_incomplete=1,
+            evaluation_type=0,
+            visibulletin=0,
+            description="note externe",
+        )
+    # Saisie des notes
+    nbchanged, nbsuppress, existing_decisions = sco_saisie_notes._notes_add(
+        context,
+        REQUEST.AUTHENTICATED_USER,
+        evaluation_id,
+        notes_etuds.items(),
+        do_it=True,
+    )
+
+
+def get_existing_external_ue(context, formation_id):
+    "la liste de toutes les UE externes définies dans cette formation"
+    return context.do_ue_list(args={"formation_id": formation_id, "is_external": 1})
+
+
+def get_external_moduleimpl_id(context, formsemestre_id, ue_id):
+    "moduleimpl correspondant à l'UE externe indiquée de ce formsemestre"
+    r = SimpleDictFetch(
+        context,
+        """
+    SELECT moduleimpl_id FROM notes_moduleimpl mi, notes_modules mo
+    WHERE mi.formsemestre_id = %(formsemestre_id)s
+    AND mi.module_id = mo.module_id
+    AND mo.ue_id = %(ue_id)s
+    """,
+        {"ue_id": ue_id, "formsemestre_id": formsemestre_id},
+    )
+    if r:
+        return r[0]["moduleimpl_id"]
+    else:
+        raise ScoValueError("aucun module externe ne correspond")
+
+
+# Web function
+def external_ue_create_form(context, formsemestre_id, etudid, REQUEST=None):
+    """Formulaire création UE externe + inscription étudiant et saisie note
+    - Demande UE: peut-être existante (liste les UE externes de cette formation), 
+       ou sinon spécifier titre, acronyme, type, ECTS
+    - Demande note à enregistrer.
+
+    Note: pour l'édition éventuelle de ces informations, on utilisera les 
+    fonctions standards sur les UE/modules/notes
+    """
+    # Contrôle d'accès:
+    authuser = REQUEST.AUTHENTICATED_USER
+    if not authuser.has_permission(ScoImplement, context):
+        if not sem["resp_can_edit"] or str(authuser) not in sem["responsables"]:
+            raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
+
+    etud = context.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    formation_id = sem["formation_id"]
+    F = context.formation_list(args={"formation_id": formation_id})[0]
+    existing_external_ue = get_existing_external_ue(context, formation_id)
+
+    H = [
+        context.html_sem_header(
+            REQUEST,
+            "Ajout d'une UE externe pour %(nomprenom)s" % etud,
+            sem,
+            javascripts=["js/sco_ue_external.js"],
+        ),
+        """<p class="help">Cette page permet d'indiquer que l'étudiant a suivi une UE 
+    dans un autre établissement et qu'elle doit être intégrée dans le semestre courant.<br/>
+    La note (/20) obtenue par l'étudiant doit toujours être spécifiée.</br>
+    On peut choisir une UE externe existante (dans le menu), ou bien en créer une, qui sera 
+    alors ajoutée à la formation.
+    </p>
+    """,
+    ]
+    html_footer = context.sco_footer(REQUEST)
+    Fo = context.formation_list(args={"formation_id": sem["formation_id"]})[0]
+    parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
+    ue_types = parcours.ALLOWED_UE_TYPES
+    ue_types.sort()
+    ue_types_names = [UE_TYPE_NAME[k] for k in ue_types]
+    ue_types = [str(x) for x in ue_types]
+
+    if existing_external_ue:
+        default_label = "Nouvelle UE"
+    else:
+        default_label = "Aucune UE externe existante"
+
+    tf = TrivialFormulator(
+        REQUEST.URL0,
+        REQUEST.form,
+        (
+            ("formsemestre_id", {"input_type": "hidden"}),
+            ("etudid", {"input_type": "hidden"}),
+            (
+                "existing_ue",
+                {
+                    "input_type": "menu",
+                    "title": "UE externe existante:",
+                    "allowed_values": [""]
+                    + [ue["ue_id"] for ue in existing_external_ue],
+                    "labels": [default_label]
+                    + [
+                        "%s (%s)" % (ue["titre"], ue["acronyme"])
+                        for ue in existing_external_ue
+                    ],
+                    "attributes": ['onchange="update_external_ue_form();"'],
+                    "explanation": "inscrire cet étudiant dans cette UE",
+                },
+            ),
+            (
+                "sep",
+                {
+                    "input_type": "separator",
+                    "title": "Ou bien déclarer une nouvelle UE externe:",
+                    "dom_id": "tf_extue_decl",
+                },
+            ),
+            # champs a desactiver si une UE existante est choisie
+            (
+                "titre",
+                {"size": 30, "explanation": "nom de l'UE", "dom_id": "tf_extue_titre"},
+            ),
+            (
+                "acronyme",
+                {
+                    "size": 8,
+                    "explanation": "abbréviation",
+                    "allow_null": True,  # attention: verifier
+                    "dom_id": "tf_extue_acronyme",
+                },
+            ),
+            (
+                "type",
+                {
+                    "explanation": "type d'UE",
+                    "input_type": "menu",
+                    "allowed_values": ue_types,
+                    "labels": ue_types_names,
+                    "dom_id": "tf_extue_type",
+                },
+            ),
+            (
+                "ects",
+                {
+                    "size": 4,
+                    "type": "float",
+                    "title": "ECTS",
+                    "explanation": "nombre de crédits ECTS",
+                    "dom_id": "tf_extue_ects",
+                },
+            ),
+            #
+            (
+                "note",
+                {"size": 4, "explanation": "note sur 20", "dom_id": "tf_extue_note"},
+            ),
+        ),
+        submitlabel="Enregistrer",
+        cancelbutton="Annuler",
+    )
+
+    bull_url = "formsemestre_bulletinetud?formsemestre_id=%s&amp;etudid=%s" % (
+        formsemestre_id,
+        etudid,
+    )
+    if tf[0] == 0:
+        return "\n".join(H) + "\n" + tf[1] + html_footer
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect(bull_url)
+    else:
+        note = tf[2]["note"].strip().upper()
+        note_value, invalid = sco_saisie_notes.convert_note_from_string(note, 20.0)
+        if invalid:
+            return (
+                "\n".join(H)
+                + "\n"
+                + tf_error_message("valeur note invalide")
+                + tf[1]
+                + html_footer
+            )
+        if tf[2]["existing_ue"]:
+            ue_id = tf[2]["existing_ue"]
+            moduleimpl_id = get_external_moduleimpl_id(context, formsemestre_id, ue_id)
+        else:
+            acronyme = tf[2]["acronyme"].strip()
+            if not acronyme:
+                return (
+                    "\n".join(H)
+                    + "\n"
+                    + tf_error_message("spécifier acronyme d'UE")
+                    + tf[1]
+                    + html_footer
+                )
+            moduleimpl_id = external_ue_create(
+                context,
+                formsemestre_id,
+                REQUEST=REQUEST,
+                titre=tf[2]["titre"],
+                acronyme=acronyme,
+                ue_type=tf[2]["type"],  # type de l'UE
+                ects=tf[2]["ects"],
+            )
+
+        external_ue_inscrit_et_note(
+            context,
+            moduleimpl_id,
+            formsemestre_id,
+            {etudid: note_value},
+            REQUEST=REQUEST,
+        )
+        return REQUEST.RESPONSE.redirect(bull_url + "&head_message=Ajout%20effectué")
diff --git a/sco_undo_notes.py b/sco_undo_notes.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a94132e27984cb8473cdf9d9d71f8b37428f1f1
--- /dev/null
+++ b/sco_undo_notes.py
@@ -0,0 +1,255 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+"""ScoDoc : annulation des saisies de notes
+
+
+note = {evaluation_id, etudid, value, date, uid, comment}
+
+Pour une évaluation:
+ - notes actuelles: table notes_notes
+ - historique: table notes_notes_log 
+
+saisie de notes == saisir ou supprimer une ou plusieurs notes (mêmes date et uid)
+/!\ tolérance sur les dates (200ms ?)
+Chaque saisie affecte ou remplace une ou plusieurs notes.
+
+Opérations:
+ - lister les saisies de notes
+ - annuler une saisie complète
+ - lister les modifs d'une seule note
+ - annuler une modif d'une note
+"""
+
+import datetime
+from intervals import intervalmap
+
+from sco_utils import *
+from notesdb import *
+from notes_log import log
+from gen_tables import GenTable
+import sco_formsemestre
+
+
+# deux notes (de même uid) sont considérées comme de la même opération si
+# elles sont séparées de moins de 2*tolerance:
+OPERATION_DATE_TOLERANCE = datetime.timedelta(seconds=0.1)
+
+
+class NotesOperation(dict):
+    """Represents an operation on an evaluation
+    Keys: evaluation_id, date, uid, notes
+    """
+
+    def get_comment(self):
+        if self["notes"]:
+            return self["notes"][0]["comment"]
+        else:
+            return ""
+
+    def comp_values(self):
+        "compute keys: comment, nb_notes"
+        self["comment"] = self.get_comment()
+        self["nb_notes"] = len(self["notes"])
+        self["datestr"] = self["date"].strftime("%a %d/%m/%y %Hh%M")
+
+    def undo(self, context):
+        "undo operation"
+        pass
+        # replace notes by last found in notes_log
+        # and suppress log entry
+        # select * from notes_notes_log where evaluation_id= and etudid= and date <
+        #
+        # verrouille tables notes, notes_log
+        # pour chaque note qui n'est pas plus recente que l'operation:
+        #   recupere valeurs precedentes dans log
+        #   affecte valeurs notes
+        #   suppr log
+        # deverrouille tablesj
+        # for note in self['notes']:
+        #    # il y a-t-il une modif plus recente ?
+        #    if self['current_notes_by_etud']['date'] <= self['date'] + OPERATION_DATE_TOLERANCE:
+        #
+        # + invalider cache   context.get_evaluations_cache().inval_cache(key=evaluation_id)
+
+
+def list_operations(context, evaluation_id):
+    """returns list of NotesOperation for this evaluation"""
+    notes = context._notes_getall(evaluation_id, filter_suppressed=False).values()
+    notes_log = context._notes_getall(
+        evaluation_id, filter_suppressed=False, table="notes_notes_log"
+    ).values()
+    dt = OPERATION_DATE_TOLERANCE
+    NotesDates = {}  # { uid : intervalmap }
+
+    for note in notes + notes_log:
+        if not NotesDates.has_key(note["uid"]):
+            NotesDates[note["uid"]] = intervalmap()
+        nd = NotesDates[note["uid"]]
+        if nd[note["date"]] is None:
+            nd[note["date"] - dt : note["date"] + dt] = [note]
+        else:
+            nd[note["date"]].append(note)
+
+    current_notes_by_etud = {}  # { etudid : note }
+    for note in notes:
+        current_notes_by_etud[note["etudid"]] = note
+
+    Ops = []
+    for uid in NotesDates.keys():
+        for (t0, t1), notes in NotesDates[uid].items():
+            Op = NotesOperation(
+                evaluation_id=evaluation_id,
+                date=t0,
+                uid=uid,
+                notes=NotesDates[uid][t0],
+                current_notes_by_etud=current_notes_by_etud,
+            )
+            Op.comp_values()
+            Ops.append(Op)
+
+    return Ops
+
+
+def evaluation_list_operations(context, REQUEST, evaluation_id):
+    """Page listing operations on evaluation"""
+    E = context.do_evaluation_list({"evaluation_id": evaluation_id})[0]
+    M = context.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
+
+    Ops = list_operations(context, evaluation_id)
+
+    columns_ids = ("datestr", "uid", "nb_notes", "comment")
+    titles = {
+        "datestr": "Date",
+        "uid": "Enseignant",
+        "nb_notes": "Nb de notes",
+        "comment": "Commentaire",
+    }
+    tab = GenTable(
+        titles=titles,
+        columns_ids=columns_ids,
+        rows=Ops,
+        html_sortable=False,
+        html_title="<h2>Opérations sur l'évaluation %s du %s</h2>"
+        % (E["description"], E["jour"]),
+        preferences=context.get_preferences(M["formsemestre_id"]),
+    )
+    return tab.make_page(context, REQUEST=REQUEST)
+
+
+def formsemestre_list_saisies_notes(
+    context, formsemestre_id, format="html", REQUEST=None
+):
+    """Table listant toutes les operations de saisies de notes, dans toutes les evaluations du semestre."""
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    r = SimpleDictFetch(
+        context,
+        """select i.nom, n.*, mod.titre, e.description, e.jour from notes_notes n, notes_evaluation e, notes_moduleimpl m, notes_modules mod, identite i where m.moduleimpl_id = e.moduleimpl_id and m.module_id = mod.module_id and e.evaluation_id=n.evaluation_id and i.etudid=n.etudid and m.formsemestre_id=%(formsemestre_id)s order by date desc""",
+        {"formsemestre_id": formsemestre_id},
+    )
+    columns_ids = (
+        "date",
+        "nom",
+        "value",
+        "uid",
+        "titre",
+        "description",
+        "jour",
+        "comment",
+    )
+    titles = {
+        "nom": "Etudiant",
+        "date": "Date",
+        "value": "Note",
+        "comment": "Remarque",
+        "uid": "Enseignant",
+        "titre": "Module",
+        "description": "Evaluation",
+        "jour": "Date éval.",
+    }
+    tab = GenTable(
+        titles=titles,
+        columns_ids=columns_ids,
+        rows=r,
+        html_title="<h2>Saisies de notes dans %s</h2>" % sem["titreannee"],
+        html_class="table_leftalign table_coldate",
+        html_sortable=True,
+        caption="Saisies de notes dans %s" % sem["titreannee"],
+        preferences=context.get_preferences(formsemestre_id),
+        base_url="%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id),
+        origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "",
+    )
+    return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+
+def get_note_history(context, evaluation_id, etudid, REQUEST=None, fmt=""):
+    """Historique d'une note
+    = liste chronologique d'opérations, la plus récente d'abord
+    [ { 'value', 'date', 'comment', 'uid' } ]
+    """
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+
+    # Valeur courante
+    cursor.execute(
+        """
+    SELECT * FROM notes_notes
+    WHERE evaluation_id=%(evaluation_id)s AND etudid=%(etudid)s 
+    """,
+        {"evaluation_id": evaluation_id, "etudid": etudid},
+    )
+    history = cursor.dictfetchall()
+
+    # Historique
+    cursor.execute(
+        """
+    SELECT * FROM notes_notes_log
+    WHERE evaluation_id=%(evaluation_id)s AND etudid=%(etudid)s 
+    ORDER BY date DESC""",
+        {"evaluation_id": evaluation_id, "etudid": etudid},
+    )
+
+    history += cursor.dictfetchall()
+
+    # Replace None comments by ''
+    # et cherche nom complet de l'enseignant:
+    for x in history:
+        x["comment"] = x["comment"] or ""
+        x["user_name"] = context.Users.user_info(x["uid"])["nomcomplet"]
+
+    if fmt == "json":
+        return sendJSON(REQUEST, history)
+    else:
+        return history
+
+
+"""
+from debug import *
+from sco_undo_notes import *
+context = go_dept(app, 'RT').Notes
+get_note_history(context, 'EVAL29740', 'EID28403')
+"""
diff --git a/sco_up_to_date.py b/sco_up_to_date.py
new file mode 100644
index 0000000000000000000000000000000000000000..29a7b2808b7e07b17829b30008b6a41e6393ad01
--- /dev/null
+++ b/sco_up_to_date.py
@@ -0,0 +1,137 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+
+""" Verification version logiciel vs version "stable" sur serveur
+    N'effectue pas la mise à jour automatiquement, mais permet un affichage d'avertissement.
+"""
+
+from sco_utils import *
+
+# Appel renvoyant la subversion "stable"
+# La notion de "stable" est juste là pour éviter d'afficher trop frequemment
+# des avertissements de mise à jour: on veut pouvoir inciter à mettre à jour lors de
+# correctifs majeurs.
+
+GET_VER_URL = "http://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/last_stable_version"
+
+
+def get_last_stable_version():
+    """request last stable version number from server
+    (returns string as given by server, empty if failure)
+    (do not wait server answer more than 3 seconds)
+    """
+    global _LAST_UP_TO_DATE_REQUEST
+    ans = query_portal(GET_VER_URL, msg="ScoDoc version server", timeout=3)  # sco_utils
+    if ans:
+        ans = ans.strip()
+    _LAST_UP_TO_DATE_REQUEST = datetime.datetime.now()
+    log(
+        'get_last_stable_version: updated at %s, answer="%s"'
+        % (_LAST_UP_TO_DATE_REQUEST, ans)
+    )
+    return ans
+
+
+_LAST_UP_TO_DATE_REQUEST = None  # datetime of last request to server
+_UP_TO_DATE = True  # cached result (limit requests to 1 per day)
+_UP_TO_DATE_MSG = ""
+
+
+def is_up_to_date(context):
+    """True if up_to_date
+    Returns status, message
+    """
+    global _LAST_UP_TO_DATE_REQUEST, _UP_TO_DATE, _UP_TO_DATE_MSG
+    if _LAST_UP_TO_DATE_REQUEST and (
+        datetime.datetime.now() - _LAST_UP_TO_DATE_REQUEST
+    ) < datetime.timedelta(1):
+        # requete deja effectuee aujourd'hui:
+        return _UP_TO_DATE, _UP_TO_DATE_MSG
+
+    last_stable_ver = get_last_stable_version()
+    cur_ver = get_svn_version(context.file_path)  # in sco_utils
+    cur_ver2 = cur_ver
+    cur_ver_num = -1
+    # Convert versions to integers:
+    try:
+        # cur_ver can be "1234" or "1234M' or '1234:1245M'...
+        fs = cur_ver.split(":", 1)
+        if len(fs) > 1:
+            cur_ver2 = fs[-1]
+        m = re.match(r"([0-9]*)", cur_ver2)
+        if not m:
+            raise ValueError(
+                "invalid svn version"
+            )  # should never occur, regexp always (maybe empty) match
+        cur_ver_num = int(m.group(1))
+    except:
+        log('Warning: no numeric subversion ! (cur_ver="%s")' % cur_ver)
+        return _UP_TO_DATE, _UP_TO_DATE_MSG  # silently ignore misconfiguration ?
+    try:
+        last_stable_ver_num = int(last_stable_ver)
+    except:
+        log("Warning: last_stable_version returned by server is invalid !")
+        return (
+            _UP_TO_DATE,
+            _UP_TO_DATE_MSG,
+        )  # should ignore this error (maybe server is unreachable)
+    #
+    if cur_ver_num < last_stable_ver_num:
+        _UP_TO_DATE = False
+        _UP_TO_DATE_MSG = "Version %s disponible (version %s installée)" % (
+            last_stable_ver,
+            cur_ver_num,
+        )
+        log(
+            "Warning: ScoDoc installation is not up-to-date, should upgrade\n%s"
+            % _UP_TO_DATE_MSG
+        )
+    else:
+        _UP_TO_DATE = True
+        _UP_TO_DATE_MSG = ""
+        log(
+            "ScoDoc is up-to-date (cur_ver: %s, using %s=%s)"
+            % (cur_ver, cur_ver2, cur_ver_num)
+        )
+
+    return _UP_TO_DATE, _UP_TO_DATE_MSG
+
+
+def html_up_to_date_box(context):
+    """
+    """
+    status, msg = is_up_to_date(context)
+    if status:
+        return ""
+    return (
+        """<div class="update_warning">
+    <span>Attention: cette installation de ScoDoc n'est pas à jour.</span>
+    <div class="update_warning_sub">Contactez votre administrateur. %s</div>
+    </div>"""
+        % msg
+    )
diff --git a/sco_utils.py b/sco_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..732ed896fe0f49029ec8213fc76cd0e19526d167
--- /dev/null
+++ b/sco_utils.py
@@ -0,0 +1,911 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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 pdb
+import os, sys, copy, re
+import pprint
+import traceback
+from types import (
+    StringType,
+    IntType,
+    FloatType,
+    UnicodeType,
+    ListType,
+    TupleType,
+    InstanceType,
+)
+import operator, bisect
+import collections
+import numbers
+import thread
+import urllib
+import urllib2
+import socket
+import xml.sax.saxutils
+import xml, xml.dom.minidom
+import time, datetime, cgi
+import mx
+
+try:
+    import six
+
+    STRING_TYPES = six.string_types
+except ImportError:
+    # fallback for very old ScoDoc instances
+    STRING_TYPES = StringType
+
+from sets import Set
+
+from PIL import Image as PILImage
+
+# XML generation package (apt-get install jaxml)
+import jaxml
+
+import json
+
+from VERSION import SCOVERSION
+import VERSION
+
+from SuppressAccents import suppression_diacritics
+from sco_exceptions import *
+from sco_permissions import *
+from TrivialFormulator import TrivialFormulator, TF, tf_error_message
+from notes_log import log, logCallStack
+
+from sco_codes_parcours import *
+
+# ----- 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) == FloatType or type(val) == 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
+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
+
+CODES_EXPL.update(CONFIG.CODES_EXPL)  # peremt 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"
+
+
+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) == 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 = [StringType, UnicodeType, IntType, FloatType]
+    for d in dictlist:
+        doc._push()
+        if (
+            type(d) == 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) == 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
+
+
+#    head = """Content-type: %s; name="%s"
+# Content-disposition: filename="%s"
+# Title: %s
+#
+# """ % (CSV_MIMETYPE,filename,filename,title)
+#    return head + str(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):
+        import sco_formsemestre
+
+        # horrible hack pour encoder les dates
+        if str(type(o)) == "<type 'mx.DateTime.DateTime'>":
+            return o.strftime("%Y-%m-%dT%H:%M:%S")
+        elif isinstance(o, sco_formsemestre.ApoEtapeVDI):
+            return str(o)
+        else:
+            log("not mx: %s" % type(o))
+            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) != 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([]):
+
+        def pydot_get_node(g, name):
+            r = g.get_node(name)
+            if not r:
+                return r
+            else:
+                return r[0]
+
+    else:
+
+        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("^.+@.+\..{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):
+    """retreive external data using http request
+    (used to connect to Apogee portal, or ScoDoc server)
+    """
+    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
diff --git a/sco_zope.py b/sco_zope.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c8ccbfa03c07bcb45bca6c9943fec670974a261
--- /dev/null
+++ b/sco_zope.py
@@ -0,0 +1,47 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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@gmail.com
+#
+##############################################################################
+
+"""Imports et configuration des composants Zope
+"""
+
+from OFS.SimpleItem import Item  # Basic zope object
+from OFS.PropertyManager import PropertyManager  # provide the 'Properties' tab with the
+
+# 'manage_propertiesForm' method
+from OFS.ObjectManager import ObjectManager
+from AccessControl.Role import RoleManager  # provide the 'Ownership' tab with
+
+# the 'manage_owner' method
+from AccessControl import ClassSecurityInfo
+import Globals
+from Globals import DTMLFile  # can use DTML files
+from Globals import Persistent
+from Globals import INSTANCE_HOME
+from Acquisition import Implicit
+
+# where we exist on the file system
+file_path = Globals.package_home(globals())
diff --git a/scolars.py b/scolars.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2217e2b8fde9c8fe18e1e3e62d07ca3655d747a
--- /dev/null
+++ b/scolars.py
@@ -0,0 +1,785 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+""" Acces donnees etudiants
+"""
+
+from sco_utils import *
+
+from notesdb import *
+from TrivialFormulator import TrivialFormulator
+import safehtml
+from scolog import logdb
+from notes_table import *
+
+
+# XXXXXXXXX HACK: zope 2.7.7 bug turaround ?
+import locale
+
+locale.setlocale(locale.LC_ALL, ("en_US", SCO_ENCODING))
+
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email.MIMEBase import MIMEBase
+from email.Header import Header
+from email import Encoders
+
+abbrvmonthsnames = [
+    "Jan ",
+    "Fev ",
+    "Mars",
+    "Avr ",
+    "Mai ",
+    "Juin",
+    "Jul ",
+    "Aout",
+    "Sept",
+    "Oct ",
+    "Nov ",
+    "Dec ",
+]
+
+monthsnames = [
+    "janvier",
+    "février",
+    "mars",
+    "avril",
+    "mai",
+    "juin",
+    "juillet",
+    "aout",
+    "septembre",
+    "octobre",
+    "novembre",
+    "décembre",
+]
+
+
+def format_etud_ident(etud):
+    """Format identite de l'étudiant (modifié en place)
+    nom, prénom et formes associees
+    """
+    etud["nom"] = format_nom(etud["nom"])
+    if "nom_usuel" in etud:
+        etud["nom_usuel"] = format_nom(etud["nom_usuel"])
+    else:
+        etud["nom_usuel"] = ""
+    etud["prenom"] = format_prenom(etud["prenom"])
+    etud["sexe"] = format_sexe(etud["sexe"])
+    # Nom à afficher:
+    if etud["nom_usuel"]:
+        etud["nom_disp"] = etud["nom_usuel"]
+        if etud["nom"]:
+            etud["nom_disp"] += " (" + etud["nom"] + ")"
+    else:
+        etud["nom_disp"] = etud["nom"]
+
+    etud["nomprenom"] = format_nomprenom(etud)  # M. Pierre DUPONT
+    if etud["sexe"] == "M.":
+        etud["ne"] = ""
+    else:
+        etud["ne"] = "e"
+    # Mail à utiliser pour les envois vers l'étudiant:
+    # choix qui pourrait être controé par une preference
+    # ici priorité au mail institutionnel:
+    etud["email_default"] = etud.get("email", "") or etud.get("emailperso", "")
+
+
+def force_uppercase(s):
+    if s:
+        s = strupper(s)
+    return s
+
+
+def format_nomprenom(etud):
+    "formatte sexe/nom/prenom pour affichages"
+    return " ".join(
+        [format_sexe(etud["sexe"]), format_prenom(etud["prenom"]), etud["nom_disp"]]
+    )
+
+
+def format_prenom(s):
+    "formatte prenom etudiant pour affichage"
+    if not s:
+        return ""
+    frags = s.split()
+    r = []
+    for frag in frags:
+        fs = frag.split("-")
+        r.append(
+            "-".join(
+                [
+                    x.decode(SCO_ENCODING).lower().capitalize().encode(SCO_ENCODING)
+                    for x in fs
+                ]
+            )
+        )
+    return " ".join(r)
+
+
+def format_nom(s, uppercase=True):
+    if not s:
+        return ""
+    if uppercase:
+        return strupper(s)
+    else:
+        return format_prenom(s)
+
+
+def format_sexe(sexe):
+    sexe = strlower(sexe)
+    if sexe == "mr" or sexe == "m." or sexe == "m":
+        return "M."
+    else:
+        return "Mme"
+
+
+def normalize_sexe(sexe):
+    "returns 'MR' ou 'MME'"
+    sexe = strupper(sexe).strip()
+    if sexe in ("M.", "M", "MR", "H"):
+        return "MR"
+    elif sexe in ("MLLE", "MLLE.", "MELLE", "MME", "F"):
+        return "MME"
+    raise ValueError("valeur invalide pour le sexe: %s" % sexe)
+
+
+def format_lycee(nomlycee):
+    nomlycee = nomlycee.strip()
+    s = strlower(nomlycee)
+    if s[:5] == "lycee" or s[:5] == "lycée":
+        return nomlycee[5:]
+    else:
+        return nomlycee
+
+
+def format_telephone(n):
+    if n is None:
+        return ""
+    if len(n) < 7:
+        return n
+    else:
+        n = n.replace(" ", "").replace(".", "")
+        i = 0
+        r = ""
+        j = len(n) - 1
+        while j >= 0:
+            r = n[j] + r
+            if i % 2 == 1 and j != 0:
+                r = " " + r
+            i += 1
+            j -= 1
+        if len(r) == 13 and r[0] != "0":
+            r = "0" + r
+        return r
+
+
+def format_pays(s):
+    "laisse le pays seulement si != FRANCE"
+    if strupper(s) != "FRANCE":
+        return s
+    else:
+        return ""
+
+
+PIVOT_YEAR = 70
+
+
+def pivot_year(y):
+    if y == "" or y is None:
+        return None
+    y = int(round(float(y)))
+    if y >= 0 and y < 100:
+        if y < PIVOT_YEAR:
+            y = y + 2000
+        else:
+            y = y + 1900
+    return y
+
+
+_identiteEditor = EditableTable(
+    "identite",
+    "etudid",
+    (
+        "etudid",
+        "nom",
+        "nom_usuel",
+        "prenom",
+        "sexe",
+        "date_naissance",
+        "lieu_naissance",
+        "dept_naissance",
+        "nationalite",
+        "statut",
+        "boursier",
+        "foto",
+        "photo_filename",
+        "code_ine",
+        "code_nip",
+    ),
+    sortkey="nom",
+    input_formators={
+        "nom": force_uppercase,
+        "prenom": force_uppercase,
+        "sexe": force_uppercase,
+        "date_naissance": DateDMYtoISO,
+    },
+    output_formators={"date_naissance": DateISOtoDMY, "sexe": normalize_sexe},
+    convert_null_outputs_to_empty=True,
+    allow_set_id=True,  # car on specifie le code Apogee a la creation
+)
+
+identite_delete = _identiteEditor.delete
+
+
+def identite_list(cnx, *a, **kw):
+    "list, add 'annee_naissance'"
+    objs = _identiteEditor.list(cnx, *a, **kw)
+    for o in objs:
+        if o["date_naissance"]:
+            o["annee_naissance"] = int(o["date_naissance"].split("/")[2])
+        else:
+            o["annee_naissance"] = o["date_naissance"]
+    return objs
+
+
+def identite_edit_nocheck(cnx, args):
+    """Modifie les champs mentionnes dans args, sans verification ni notification.
+    """
+    _identiteEditor.edit(cnx, args)
+
+
+def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
+    """Check if nom and prenom are valid.
+    Also check for duplicates (homonyms), excluding etudid : 
+    in general, homonyms are allowed, but it may be useful to generate a warning.
+    Returns:
+    True | False, NbHomonyms
+    """
+    if not nom or (not prenom and not CONFIG.ALLOW_NULL_PRENOM):
+        return False, 0
+    nom = nom.decode(SCO_ENCODING).lower().strip().encode(SCO_ENCODING)
+    if prenom:
+        prenom = prenom.decode(SCO_ENCODING).lower().strip().encode(SCO_ENCODING)
+    # Don't allow some special cars (eg used in sql regexps)
+    if FORBIDDEN_CHARS_EXP.search(nom) or FORBIDDEN_CHARS_EXP.search(prenom):
+        return False, 0
+    # Now count homonyms:
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    req = "select etudid from identite where lower(nom) ~ %(nom)s and lower(prenom) ~ %(prenom)s"
+    if etudid:
+        req += "  and etudid <> %(etudid)s"
+    cursor.execute(req, {"nom": nom, "prenom": prenom, "etudid": etudid})
+    res = cursor.dictfetchall()
+    return True, len(res)
+
+
+def _check_duplicate_code(cnx, args, code_name, context, edit=True, REQUEST=None):
+    etudid = args.get("etudid", None)
+    if args.get(code_name, None):
+        etuds = identite_list(cnx, {code_name: str(args[code_name])})
+        # log('etuds=%s'%etuds)
+        nb_max = 0
+        if edit:
+            nb_max = 1
+        if len(etuds) > nb_max:
+            listh = []  # liste des doubles
+            for e in etuds:
+                listh.append(
+                    """Autre étudiant: <a href="ficheEtud?etudid=%(etudid)s">%(nom)s %(prenom)s</a>"""
+                    % e
+                )
+            if etudid:
+                OK = "retour à la fiche étudiant"
+                dest_url = "ficheEtud"
+                parameters = {"etudid": etudid}
+            else:
+                if args.has_key("tf-submitted"):
+                    del args["tf-submitted"]
+                    OK = "Continuer"
+                    dest_url = "etudident_create_form"
+                    parameters = args
+                else:
+                    OK = "Annuler"
+                    dest_url = ""
+                    parameters = {}
+            if context:
+                err_page = context.confirmDialog(
+                    message="""<h3>Code étudiant (%s) dupliqué !</h3>""" % code_name,
+                    helpmsg="""Le %s %s est déjà utilisé: un seul étudiant peut avoir ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.<p><ul><li>"""
+                    % (code_name, args[code_name])
+                    + "</li><li>".join(listh)
+                    + "</li></ul><p>",
+                    OK=OK,
+                    dest_url=dest_url,
+                    parameters=parameters,
+                    REQUEST=REQUEST,
+                )
+            else:
+                err_page = """<h3>Code étudiant (%s) dupliqué !</h3>""" % code_name
+            log("*** error: code %s duplique: %s" % (code_name, args[code_name]))
+            raise ScoGenError(err_page)
+
+
+def identite_edit(cnx, args, context=None, REQUEST=None):
+    """Modifie l'identite d'un étudiant.
+    Si context et notification et difference, envoie message notification.
+    """
+    _check_duplicate_code(cnx, args, "code_nip", context, edit=True, REQUEST=REQUEST)
+    _check_duplicate_code(cnx, args, "code_ine", context, edit=True, REQUEST=REQUEST)
+    notify_to = None
+    if context:
+        try:
+            notify_to = context.get_preference("notify_etud_changes_to")
+        except:
+            pass
+
+    if notify_to:
+        # etat AVANT edition pour envoyer diffs
+        before = identite_list(cnx, {"etudid": args["etudid"]})[0]
+        before["sexe"] = format_sexe(before["sexe"])
+
+    identite_edit_nocheck(cnx, args)
+
+    # Notification du changement par e-mail:
+    if notify_to:
+        etud = context.getEtudInfo(etudid=args["etudid"], filled=True)[0]
+        after = identite_list(cnx, {"etudid": args["etudid"]})[0]
+        after["sexe"] = format_sexe(after["sexe"])
+        notify_etud_change(
+            context,
+            notify_to,
+            etud,
+            before,
+            after,
+            "Modification identite %(nomprenom)s" % etud,
+        )
+
+
+def identite_create(cnx, args, context=None, REQUEST=None):
+    "check unique etudid, then create"
+    _check_duplicate_code(cnx, args, "code_nip", context, edit=False, REQUEST=REQUEST)
+    _check_duplicate_code(cnx, args, "code_ine", context, edit=False, REQUEST=REQUEST)
+
+    if args.has_key("etudid"):
+        etudid = args["etudid"]
+        r = identite_list(cnx, {"etudid": etudid})
+        if r:
+            raise ScoValueError(
+                "Code identifiant (etudid) déjà utilisé ! (%s)" % etudid
+            )
+    return _identiteEditor.create(cnx, args)
+
+
+def notify_etud_change(context, email_addr, etud, before, after, subject):
+    """Send email notifying changes to etud
+    before and after are two dicts, with values before and after the change.
+    """
+    txt = [
+        "Code NIP:" + etud["code_nip"],
+        "Genre: " + etud["sexe"],
+        "Nom: " + etud["nom"],
+        "Prénom: " + etud["prenom"],
+        "Etudid: " + etud["etudid"],
+        "\n",
+        "Changements effectués:",
+    ]
+    n = 0
+    for key in after.keys():
+        if before[key] != after[key]:
+            txt.append('%s: %s (auparavant: "%s")' % (key, after[key], before[key]))
+            n += 1
+    if not n:
+        return  # pas de changements
+    txt = "\n".join(txt)
+    # build mail
+    log("notify_etud_change: sending notification to %s" % email_addr)
+    msg = MIMEMultipart()
+    subj = Header("[ScoDoc] " + subject, SCO_ENCODING)
+    msg["Subject"] = subj
+    msg["From"] = context.get_preference("email_from_addr")
+    msg["To"] = email_addr
+    txt = MIMEText(txt, "plain", SCO_ENCODING)
+    msg.attach(txt)
+    context.sendEmail(msg)
+
+
+# --------
+# Note: la table adresse n'est pas dans dans la table "identite"
+#       car on prevoit plusieurs adresses par etudiant (ie domicile, entreprise)
+
+_adresseEditor = EditableTable(
+    "adresse",
+    "adresse_id",
+    (
+        "adresse_id",
+        "etudid",
+        "email",
+        "emailperso",
+        "domicile",
+        "codepostaldomicile",
+        "villedomicile",
+        "paysdomicile",
+        "telephone",
+        "telephonemobile",
+        "fax",
+        "typeadresse",
+        "entreprise_id",
+        "description",
+    ),
+    convert_null_outputs_to_empty=True,
+)
+
+adresse_create = _adresseEditor.create
+adresse_delete = _adresseEditor.delete
+adresse_list = _adresseEditor.list
+
+
+def adresse_edit(cnx, args, context=None):
+    """Modifie l'adresse d'un étudiant.
+    Si context et notification et difference, envoie message notification.
+    """
+    notify_to = None
+    if context:
+        try:
+            notify_to = context.get_preference("notify_etud_changes_to")
+        except:
+            pass
+    if notify_to:
+        # etat AVANT edition pour envoyer diffs
+        before = adresse_list(cnx, {"etudid": args["etudid"]})[0]
+
+    _adresseEditor.edit(cnx, args)
+
+    # Notification du changement par e-mail:
+    if notify_to:
+        etud = context.getEtudInfo(etudid=args["etudid"], filled=True)[0]
+        after = adresse_list(cnx, {"etudid": args["etudid"]})[0]
+        notify_etud_change(
+            context,
+            notify_to,
+            etud,
+            before,
+            after,
+            "Modification adresse %(nomprenom)s" % etud,
+        )
+
+
+def getEmail(cnx, etudid):
+    "get email institutionnel etudiant (si plusieurs adresses, prend le premier non null"
+    adrs = adresse_list(cnx, {"etudid": etudid})
+    for adr in adrs:
+        if adr["email"]:
+            return adr["email"]
+    return ""
+
+
+# ---------
+_admissionEditor = EditableTable(
+    "admissions",
+    "adm_id",
+    (
+        "adm_id",
+        "etudid",
+        "annee",
+        "bac",
+        "specialite",
+        "annee_bac",
+        "math",
+        "physique",
+        "anglais",
+        "francais",
+        "rang",
+        "qualite",
+        "rapporteur",
+        "decision",
+        "score",
+        "classement",
+        "apb_groupe",
+        "apb_classement_gr",
+        "commentaire",
+        "nomlycee",
+        "villelycee",
+        "codepostallycee",
+        "codelycee",
+        "debouche",
+        "type_admission",
+        "boursier_prec",
+    ),
+    input_formators={
+        "annee": pivot_year,
+        "bac": force_uppercase,
+        "specialite": force_uppercase,
+        "annee_bac": pivot_year,
+        "classement": int_null_is_null,
+        "apb_classsment_gr": int_null_is_null,
+    },
+    output_formators={"type_admission": lambda x: x or TYPE_ADMISSION_DEFAULT},
+    convert_null_outputs_to_empty=True,
+)
+
+admission_create = _admissionEditor.create
+admission_delete = _admissionEditor.delete
+admission_list = _admissionEditor.list
+admission_edit = _admissionEditor.edit
+
+# Edition simultanee de identite et admission
+class EtudIdentEditor:
+    def create(self, cnx, args, context=None, REQUEST=None):
+        etudid = identite_create(cnx, args, context, REQUEST)
+        args["etudid"] = etudid
+        admission_create(cnx, args)
+        return etudid
+
+    def list(self, *args, **kw):
+        R = identite_list(*args, **kw)
+        Ra = admission_list(*args, **kw)
+        # print len(R), len(Ra)
+        # merge: add admission fields to identite
+        A = {}
+        for r in Ra:
+            A[r["etudid"]] = r
+        res = []
+        for i in R:
+            res.append(i)
+            if A.has_key(i["etudid"]):
+                # merge
+                res[-1].update(A[i["etudid"]])
+            else:  # pas d'etudiant trouve
+                # print "*** pas d'info admission pour %s" % str(i)
+                void_adm = {
+                    k: None
+                    for k in scolars._admissionEditor.dbfields
+                    if k != "etudid" and k != "adm_id"
+                }
+                res[-1].update(void_adm)
+        # tri par nom
+        res.sort(lambda x, y: cmp(x["nom"] + x["prenom"], y["nom"] + y["prenom"]))
+        return res
+
+    def edit(self, cnx, args, context=None, REQUEST=None):
+        identite_edit(cnx, args, context, REQUEST)
+        if "adm_id" in args:  # safety net
+            admission_edit(cnx, args)
+
+
+_etudidentEditor = EtudIdentEditor()
+etudident_list = _etudidentEditor.list
+etudident_edit = _etudidentEditor.edit
+etudident_create = _etudidentEditor.create
+
+
+def make_etud_args(etudid=None, code_nip=None, REQUEST=None, raise_exc=True):
+    """forme args dict pour requete recherche etudiant
+    On peut specifier etudid
+    ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine
+    (dans cet ordre).
+    """
+    args = None
+    if etudid:
+        args = {"etudid": etudid}
+    elif code_nip:
+        args = {"code_nip": code_nip}
+    elif REQUEST:
+        if REQUEST.form.has_key("etudid"):
+            args = {"etudid": REQUEST.form["etudid"]}
+        elif REQUEST.form.has_key("code_nip"):
+            args = {"code_nip": REQUEST.form["code_nip"]}
+        elif REQUEST.form.has_key("code_ine"):
+            args = {"code_ine": REQUEST.form["code_ine"]}
+    if not args and raise_exc:
+        raise ValueError("getEtudInfo: no parameter !")
+    return args
+
+
+# ---------- "EVENTS"
+_scolar_eventsEditor = EditableTable(
+    "scolar_events",
+    "event_id",
+    (
+        "event_id",
+        "etudid",
+        "event_date",
+        "formsemestre_id",
+        "ue_id",
+        "event_type",
+        "comp_formsemestre_id",
+    ),
+    sortkey="event_date",
+    convert_null_outputs_to_empty=True,
+    output_formators={"event_date": DateISOtoDMY},
+    input_formators={"event_date": DateDMYtoISO},
+)
+
+# scolar_events_create = _scolar_eventsEditor.create
+scolar_events_delete = _scolar_eventsEditor.delete
+scolar_events_list = _scolar_eventsEditor.list
+scolar_events_edit = _scolar_eventsEditor.edit
+
+
+def scolar_events_create(cnx, args):
+    # several "events" may share the same values
+    _scolar_eventsEditor.create(cnx, args, has_uniq_values=False)
+
+
+# --------
+_etud_annotationsEditor = EditableTable(
+    "etud_annotations",
+    "id",
+    (
+        "id",
+        "date",
+        "etudid",
+        "author",
+        "comment",
+        "zope_authenticated_user",
+        "zope_remote_addr",
+    ),
+    sortkey="date desc",
+    convert_null_outputs_to_empty=True,
+    output_formators={"comment": safehtml.HTML2SafeHTML, "date": DateISOtoDMY},
+)
+
+
+etud_annotations_create = _etud_annotationsEditor.create
+etud_annotations_delete = _etud_annotationsEditor.delete
+etud_annotations_list = _etud_annotationsEditor.list
+etud_annotations_edit = _etud_annotationsEditor.edit
+
+
+def add_annotations_to_etud_list(context, etuds):
+    """Add key 'annotations' describing annotations of etuds
+    (used to list all annotations of a group)
+    """
+    cnx = context.GetDBConnexion()
+    for etud in etuds:
+        l = []
+        for a in etud_annotations_list(cnx, args={"etudid": etud["etudid"]}):
+            l.append("%(comment)s (%(date)s)" % a)
+        etud["annotations_str"] = ", ".join(l)
+
+
+# -------- APPRECIATIONS (sur bulletins) -------------------
+# Les appreciations sont dans la table postgres notes_appreciations
+_appreciationsEditor = EditableTable(
+    "notes_appreciations",
+    "id",
+    (
+        "id",
+        "date",
+        "etudid",
+        "formsemestre_id",
+        "author",
+        "comment",
+        "zope_authenticated_user",
+        "zope_remote_addr",
+    ),
+    sortkey="date desc",
+    convert_null_outputs_to_empty=True,
+    output_formators={"comment": safehtml.HTML2SafeHTML, "date": DateISOtoDMY},
+)
+
+appreciations_create = _appreciationsEditor.create
+appreciations_delete = _appreciationsEditor.delete
+appreciations_list = _appreciationsEditor.list
+appreciations_edit = _appreciationsEditor.edit
+
+
+# -------- Noms des Lycées à partir du code
+def read_etablissements():
+    filename = SCO_SRCDIR + "/" + CONFIG.ETABL_FILENAME
+    log("reading %s" % filename)
+    f = open(filename)
+    L = [x[:-1].split(";") for x in f]
+    E = {}
+    for l in L[1:]:
+        E[l[0]] = {
+            "name": l[1],
+            "address": l[2],
+            "codepostal": l[3],
+            "commune": l[4],
+            "position": l[5] + "," + l[6],
+        }
+    return E
+
+
+ETABLISSEMENTS = None
+
+
+def get_etablissements():
+    global ETABLISSEMENTS
+    if ETABLISSEMENTS is None:
+        ETABLISSEMENTS = read_etablissements()
+    return ETABLISSEMENTS
+
+
+def get_lycee_infos(codelycee):
+    E = get_etablissements()
+    return E.get(codelycee, None)
+
+
+def format_lycee_from_code(codelycee):
+    "Description lycee à partir du code"
+    E = get_etablissements()
+    if codelycee in E:
+        e = E[codelycee]
+        nomlycee = e["name"]
+        return "%s (%s)" % (nomlycee, e["commune"])
+    else:
+        return "%s (établissement inconnu)" % codelycee
+
+
+def etud_add_lycee_infos(etud):
+    """Si codelycee est renseigné, ajout les champs au dict"""
+    if etud["codelycee"]:
+        il = get_lycee_infos(etud["codelycee"])
+        if il:
+            if not etud["codepostallycee"]:
+                etud["codepostallycee"] = il["codepostal"]
+            if not etud["nomlycee"]:
+                etud["nomlycee"] = il["name"]
+            if not etud["villelycee"]:
+                etud["villelycee"] = il["commune"]
+            if not etud.get("positionlycee", None):
+                if il["position"] != "0.0,0.0":
+                    etud["positionlycee"] = il["position"]
+    return etud
+
+
+""" Conversion fichier original:
+f = open('etablissements.csv')
+o = open('etablissements2.csv', 'w')
+o.write( f.readline() )
+for l in f:
+    fs = l.split(';')
+    nom = ' '.join( [ strcapitalize(x) for x in fs[1].split() ] )
+    adr = ' '.join( [ strcapitalize(x) for x in fs[2].split() ] )
+    ville=' '.join( [ strcapitalize(x) for x in fs[4].split() ] )
+    o.write( '%s;%s;%s;%s;%s\n' % (fs[0], nom, adr, fs[3], ville))
+
+o.close()
+"""
diff --git a/scolog.py b/scolog.py
new file mode 100644
index 0000000000000000000000000000000000000000..f7f1c061e84b769e2a6a6c081e6f64cdd1d1452e
--- /dev/null
+++ b/scolog.py
@@ -0,0 +1,67 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2020 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
+#
+##############################################################################
+
+
+import pdb, os, sys
+from sco_exceptions import *
+from notesdb import *
+from notes_log import retreive_request
+
+
+def logdb(REQUEST=None, cnx=None, method=None, etudid=None, msg=None, commit=True):
+    if not cnx:
+        raise ValueError("logdb: cnx is None")
+    if not REQUEST:
+        REQUEST = retreive_request(skip=1)
+    if REQUEST:
+        args = {
+            "authenticated_user": str(REQUEST.AUTHENTICATED_USER),
+            "remote_addr": REQUEST.REMOTE_ADDR,
+            "remote_host": REQUEST.REMOTE_HOST,
+        }
+    else:
+        args = {"authenticated_user": None, "remote_addr": None, "remote_host": None}
+    args.update({"method": method, "etudid": etudid, "msg": msg})
+    quote_dict(args)
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        "insert into scolog (authenticated_user,remote_addr,remote_host,method,etudid,msg) values (%(authenticated_user)s,%(remote_addr)s,%(remote_host)s,%(method)s,%(etudid)s,%(msg)s)",
+        args,
+    )
+    if commit:
+        cnx.commit()
+
+
+def loglist(cnx, method=None, authenticated_user=None):
+    """List of events logged for these method and user
+    """
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+    cursor.execute(
+        "select * from scolog where method=%(method)s and authenticated_user=%(authenticated_user)s",
+        {"method": method, "authenticated_user": authenticated_user},
+    )
+    return cursor.dictfetchall()
diff --git a/static/DataTables/ColReorder-1.3.3/css/colReorder.bootstrap.css b/static/DataTables/ColReorder-1.3.3/css/colReorder.bootstrap.css
new file mode 100644
index 0000000000000000000000000000000000000000..4d399a55e5f8510efec4b93cbccaad84b3109031
--- /dev/null
+++ b/static/DataTables/ColReorder-1.3.3/css/colReorder.bootstrap.css
@@ -0,0 +1,11 @@
+table.DTCR_clonedTable.dataTable {
+  position: absolute !important;
+  background-color: rgba(255, 255, 255, 0.7);
+  z-index: 202;
+}
+
+div.DTCR_pointer {
+  width: 1px;
+  background-color: #337ab7;
+  z-index: 201;
+}
diff --git a/static/DataTables/ColReorder-1.3.3/css/colReorder.bootstrap.min.css b/static/DataTables/ColReorder-1.3.3/css/colReorder.bootstrap.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..5a84d69b874c185e9dd005d99a7e72e34d16b15d
--- /dev/null
+++ b/static/DataTables/ColReorder-1.3.3/css/colReorder.bootstrap.min.css
@@ -0,0 +1 @@
+table.DTCR_clonedTable.dataTable{position:absolute !important;background-color:rgba(255,255,255,0.7);z-index:202}div.DTCR_pointer{width:1px;background-color:#337ab7;z-index:201}
diff --git a/static/DataTables/ColReorder-1.3.3/css/colReorder.dataTables.css b/static/DataTables/ColReorder-1.3.3/css/colReorder.dataTables.css
new file mode 100644
index 0000000000000000000000000000000000000000..a2854c0aae345e19ead6931016a5435c79be3884
--- /dev/null
+++ b/static/DataTables/ColReorder-1.3.3/css/colReorder.dataTables.css
@@ -0,0 +1,11 @@
+table.DTCR_clonedTable.dataTable {
+  position: absolute !important;
+  background-color: rgba(255, 255, 255, 0.7);
+  z-index: 202;
+}
+
+div.DTCR_pointer {
+  width: 1px;
+  background-color: #0259C4;
+  z-index: 201;
+}
diff --git a/static/DataTables/ColReorder-1.3.3/css/colReorder.dataTables.min.css b/static/DataTables/ColReorder-1.3.3/css/colReorder.dataTables.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..4f83a4085daa375c5f985ed2643d240af4550b0e
--- /dev/null
+++ b/static/DataTables/ColReorder-1.3.3/css/colReorder.dataTables.min.css
@@ -0,0 +1 @@
+table.DTCR_clonedTable.dataTable{position:absolute !important;background-color:rgba(255,255,255,0.7);z-index:202}div.DTCR_pointer{width:1px;background-color:#0259C4;z-index:201}
diff --git a/static/DataTables/ColReorder-1.3.3/css/colReorder.foundation.css b/static/DataTables/ColReorder-1.3.3/css/colReorder.foundation.css
new file mode 100644
index 0000000000000000000000000000000000000000..2b66af0cdd2f1e55f7b9550c2e2103b0868168ab
--- /dev/null
+++ b/static/DataTables/ColReorder-1.3.3/css/colReorder.foundation.css
@@ -0,0 +1,11 @@
+table.DTCR_clonedTable.dataTable {
+  position: absolute !important;
+  background-color: rgba(255, 255, 255, 0.7);
+  z-index: 202;
+}
+
+div.DTCR_pointer {
+  width: 1px;
+  background-color: #008CBA;
+  z-index: 201;
+}
diff --git a/static/DataTables/ColReorder-1.3.3/css/colReorder.foundation.min.css b/static/DataTables/ColReorder-1.3.3/css/colReorder.foundation.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..36f780ed5c555ef164cd03f200a2228c09f7a4d5
--- /dev/null
+++ b/static/DataTables/ColReorder-1.3.3/css/colReorder.foundation.min.css
@@ -0,0 +1 @@
+table.DTCR_clonedTable.dataTable{position:absolute !important;background-color:rgba(255,255,255,0.7);z-index:202}div.DTCR_pointer{width:1px;background-color:#008CBA;z-index:201}
diff --git a/static/DataTables/ColReorder-1.3.3/css/colReorder.jqueryui.css b/static/DataTables/ColReorder-1.3.3/css/colReorder.jqueryui.css
new file mode 100644
index 0000000000000000000000000000000000000000..a2854c0aae345e19ead6931016a5435c79be3884
--- /dev/null
+++ b/static/DataTables/ColReorder-1.3.3/css/colReorder.jqueryui.css
@@ -0,0 +1,11 @@
+table.DTCR_clonedTable.dataTable {
+  position: absolute !important;
+  background-color: rgba(255, 255, 255, 0.7);
+  z-index: 202;
+}
+
+div.DTCR_pointer {
+  width: 1px;
+  background-color: #0259C4;
+  z-index: 201;
+}
diff --git a/static/DataTables/ColReorder-1.3.3/css/colReorder.jqueryui.min.css b/static/DataTables/ColReorder-1.3.3/css/colReorder.jqueryui.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..4f83a4085daa375c5f985ed2643d240af4550b0e
--- /dev/null
+++ b/static/DataTables/ColReorder-1.3.3/css/colReorder.jqueryui.min.css
@@ -0,0 +1 @@
+table.DTCR_clonedTable.dataTable{position:absolute !important;background-color:rgba(255,255,255,0.7);z-index:202}div.DTCR_pointer{width:1px;background-color:#0259C4;z-index:201}
diff --git a/static/DataTables/ColReorder-1.3.3/js/dataTables.colReorder.js b/static/DataTables/ColReorder-1.3.3/js/dataTables.colReorder.js
new file mode 100644
index 0000000000000000000000000000000000000000..44cc582134186e439a7f75e3756abac00bf56ce8
--- /dev/null
+++ b/static/DataTables/ColReorder-1.3.3/js/dataTables.colReorder.js
@@ -0,0 +1,1353 @@
+/*! ColReorder 1.3.3
+ * ©2010-2015 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary     ColReorder
+ * @description Provide the ability to reorder columns in a DataTable
+ * @version     1.3.3
+ * @file        dataTables.colReorder.js
+ * @author      SpryMedia Ltd (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ * @copyright   Copyright 2010-2014 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ *   MIT license - http://datatables.net/license/mit
+ *
+ * This source file 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 license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+/**
+ * Switch the key value pairing of an index array to be value key (i.e. the old value is now the
+ * key). For example consider [ 2, 0, 1 ] this would be returned as [ 1, 2, 0 ].
+ *  @method  fnInvertKeyValues
+ *  @param   array aIn Array to switch around
+ *  @returns array
+ */
+function fnInvertKeyValues( aIn )
+{
+	var aRet=[];
+	for ( var i=0, iLen=aIn.length ; i<iLen ; i++ )
+	{
+		aRet[ aIn[i] ] = i;
+	}
+	return aRet;
+}
+
+
+/**
+ * Modify an array by switching the position of two elements
+ *  @method  fnArraySwitch
+ *  @param   array aArray Array to consider, will be modified by reference (i.e. no return)
+ *  @param   int iFrom From point
+ *  @param   int iTo Insert point
+ *  @returns void
+ */
+function fnArraySwitch( aArray, iFrom, iTo )
+{
+	var mStore = aArray.splice( iFrom, 1 )[0];
+	aArray.splice( iTo, 0, mStore );
+}
+
+
+/**
+ * Switch the positions of nodes in a parent node (note this is specifically designed for
+ * table rows). Note this function considers all element nodes under the parent!
+ *  @method  fnDomSwitch
+ *  @param   string sTag Tag to consider
+ *  @param   int iFrom Element to move
+ *  @param   int Point to element the element to (before this point), can be null for append
+ *  @returns void
+ */
+function fnDomSwitch( nParent, iFrom, iTo )
+{
+	var anTags = [];
+	for ( var i=0, iLen=nParent.childNodes.length ; i<iLen ; i++ )
+	{
+		if ( nParent.childNodes[i].nodeType == 1 )
+		{
+			anTags.push( nParent.childNodes[i] );
+		}
+	}
+	var nStore = anTags[ iFrom ];
+
+	if ( iTo !== null )
+	{
+		nParent.insertBefore( nStore, anTags[iTo] );
+	}
+	else
+	{
+		nParent.appendChild( nStore );
+	}
+}
+
+
+/**
+ * Plug-in for DataTables which will reorder the internal column structure by taking the column
+ * from one position (iFrom) and insert it into a given point (iTo).
+ *  @method  $.fn.dataTableExt.oApi.fnColReorder
+ *  @param   object oSettings DataTables settings object - automatically added by DataTables!
+ *  @param   int iFrom Take the column to be repositioned from this point
+ *  @param   int iTo and insert it into this point
+ *  @param   bool drop Indicate if the reorder is the final one (i.e. a drop)
+ *    not a live reorder
+ *  @param   bool invalidateRows speeds up processing if false passed
+ *  @returns void
+ */
+$.fn.dataTableExt.oApi.fnColReorder = function ( oSettings, iFrom, iTo, drop, invalidateRows )
+{
+	var i, iLen, j, jLen, jen, iCols=oSettings.aoColumns.length, nTrs, oCol;
+	var attrMap = function ( obj, prop, mapping ) {
+		if ( ! obj[ prop ] || typeof obj[ prop ] === 'function' ) {
+			return;
+		}
+
+		var a = obj[ prop ].split('.');
+		var num = a.shift();
+
+		if ( isNaN( num*1 ) ) {
+			return;
+		}
+
+		obj[ prop ] = mapping[ num*1 ]+'.'+a.join('.');
+	};
+
+	/* Sanity check in the input */
+	if ( iFrom == iTo )
+	{
+		/* Pointless reorder */
+		return;
+	}
+
+	if ( iFrom < 0 || iFrom >= iCols )
+	{
+		this.oApi._fnLog( oSettings, 1, "ColReorder 'from' index is out of bounds: "+iFrom );
+		return;
+	}
+
+	if ( iTo < 0 || iTo >= iCols )
+	{
+		this.oApi._fnLog( oSettings, 1, "ColReorder 'to' index is out of bounds: "+iTo );
+		return;
+	}
+
+	/*
+	 * Calculate the new column array index, so we have a mapping between the old and new
+	 */
+	var aiMapping = [];
+	for ( i=0, iLen=iCols ; i<iLen ; i++ )
+	{
+		aiMapping[i] = i;
+	}
+	fnArraySwitch( aiMapping, iFrom, iTo );
+	var aiInvertMapping = fnInvertKeyValues( aiMapping );
+
+
+	/*
+	 * Convert all internal indexing to the new column order indexes
+	 */
+	/* Sorting */
+	for ( i=0, iLen=oSettings.aaSorting.length ; i<iLen ; i++ )
+	{
+		oSettings.aaSorting[i][0] = aiInvertMapping[ oSettings.aaSorting[i][0] ];
+	}
+
+	/* Fixed sorting */
+	if ( oSettings.aaSortingFixed !== null )
+	{
+		for ( i=0, iLen=oSettings.aaSortingFixed.length ; i<iLen ; i++ )
+		{
+			oSettings.aaSortingFixed[i][0] = aiInvertMapping[ oSettings.aaSortingFixed[i][0] ];
+		}
+	}
+
+	/* Data column sorting (the column which the sort for a given column should take place on) */
+	for ( i=0, iLen=iCols ; i<iLen ; i++ )
+	{
+		oCol = oSettings.aoColumns[i];
+		for ( j=0, jLen=oCol.aDataSort.length ; j<jLen ; j++ )
+		{
+			oCol.aDataSort[j] = aiInvertMapping[ oCol.aDataSort[j] ];
+		}
+
+		// Update the column indexes
+		oCol.idx = aiInvertMapping[ oCol.idx ];
+	}
+
+	// Update 1.10 optimised sort class removal variable
+	$.each( oSettings.aLastSort, function (i, val) {
+		oSettings.aLastSort[i].src = aiInvertMapping[ val.src ];
+	} );
+
+	/* Update the Get and Set functions for each column */
+	for ( i=0, iLen=iCols ; i<iLen ; i++ )
+	{
+		oCol = oSettings.aoColumns[i];
+
+		if ( typeof oCol.mData == 'number' ) {
+			oCol.mData = aiInvertMapping[ oCol.mData ];
+		}
+		else if ( $.isPlainObject( oCol.mData ) ) {
+			// HTML5 data sourced
+			attrMap( oCol.mData, '_',      aiInvertMapping );
+			attrMap( oCol.mData, 'filter', aiInvertMapping );
+			attrMap( oCol.mData, 'sort',   aiInvertMapping );
+			attrMap( oCol.mData, 'type',   aiInvertMapping );
+		}
+	}
+
+	/*
+	 * Move the DOM elements
+	 */
+	if ( oSettings.aoColumns[iFrom].bVisible )
+	{
+		/* Calculate the current visible index and the point to insert the node before. The insert
+		 * before needs to take into account that there might not be an element to insert before,
+		 * in which case it will be null, and an appendChild should be used
+		 */
+		var iVisibleIndex = this.oApi._fnColumnIndexToVisible( oSettings, iFrom );
+		var iInsertBeforeIndex = null;
+
+		i = iTo < iFrom ? iTo : iTo + 1;
+		while ( iInsertBeforeIndex === null && i < iCols )
+		{
+			iInsertBeforeIndex = this.oApi._fnColumnIndexToVisible( oSettings, i );
+			i++;
+		}
+
+		/* Header */
+		nTrs = oSettings.nTHead.getElementsByTagName('tr');
+		for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+		{
+			fnDomSwitch( nTrs[i], iVisibleIndex, iInsertBeforeIndex );
+		}
+
+		/* Footer */
+		if ( oSettings.nTFoot !== null )
+		{
+			nTrs = oSettings.nTFoot.getElementsByTagName('tr');
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				fnDomSwitch( nTrs[i], iVisibleIndex, iInsertBeforeIndex );
+			}
+		}
+
+		/* Body */
+		for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+		{
+			if ( oSettings.aoData[i].nTr !== null )
+			{
+				fnDomSwitch( oSettings.aoData[i].nTr, iVisibleIndex, iInsertBeforeIndex );
+			}
+		}
+	}
+
+	/*
+	 * Move the internal array elements
+	 */
+	/* Columns */
+	fnArraySwitch( oSettings.aoColumns, iFrom, iTo );
+
+	// regenerate the get / set functions
+	for ( i=0, iLen=iCols ; i<iLen ; i++ ) {
+		oSettings.oApi._fnColumnOptions( oSettings, i, {} );
+	}
+
+	/* Search columns */
+	fnArraySwitch( oSettings.aoPreSearchCols, iFrom, iTo );
+
+	/* Array array - internal data anodes cache */
+	for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+	{
+		var data = oSettings.aoData[i];
+		var cells = data.anCells;
+
+		if ( cells ) {
+			fnArraySwitch( cells, iFrom, iTo );
+
+			// Longer term, should this be moved into the DataTables' invalidate
+			// methods?
+			for ( j=0, jen=cells.length ; j<jen ; j++ ) {
+				if ( cells[j] && cells[j]._DT_CellIndex ) {
+					cells[j]._DT_CellIndex.column = j;
+				}
+			}
+		}
+
+		// For DOM sourced data, the invalidate will reread the cell into
+		// the data array, but for data sources as an array, they need to
+		// be flipped
+		if ( data.src !== 'dom' && $.isArray( data._aData ) ) {
+			fnArraySwitch( data._aData, iFrom, iTo );
+		}
+	}
+
+	/* Reposition the header elements in the header layout array */
+	for ( i=0, iLen=oSettings.aoHeader.length ; i<iLen ; i++ )
+	{
+		fnArraySwitch( oSettings.aoHeader[i], iFrom, iTo );
+	}
+
+	if ( oSettings.aoFooter !== null )
+	{
+		for ( i=0, iLen=oSettings.aoFooter.length ; i<iLen ; i++ )
+		{
+			fnArraySwitch( oSettings.aoFooter[i], iFrom, iTo );
+		}
+	}
+
+	if ( invalidateRows || invalidateRows === undefined )
+	{
+		$.fn.dataTable.Api( oSettings ).rows().invalidate();
+	}
+
+	/*
+	 * Update DataTables' event handlers
+	 */
+
+	/* Sort listener */
+	for ( i=0, iLen=iCols ; i<iLen ; i++ )
+	{
+		$(oSettings.aoColumns[i].nTh).off('click.DT');
+		this.oApi._fnSortAttachListener( oSettings, oSettings.aoColumns[i].nTh, i );
+	}
+
+
+	/* Fire an event so other plug-ins can update */
+	$(oSettings.oInstance).trigger( 'column-reorder.dt', [ oSettings, {
+		from: iFrom,
+		to: iTo,
+		mapping: aiInvertMapping,
+		drop: drop,
+
+		// Old style parameters for compatibility
+		iFrom: iFrom,
+		iTo: iTo,
+		aiInvertMapping: aiInvertMapping
+	} ] );
+};
+
+/**
+ * ColReorder provides column visibility control for DataTables
+ * @class ColReorder
+ * @constructor
+ * @param {object} dt DataTables settings object
+ * @param {object} opts ColReorder options
+ */
+var ColReorder = function( dt, opts )
+{
+	var settings = new $.fn.dataTable.Api( dt ).settings()[0];
+
+	// Ensure that we can't initialise on the same table twice
+	if ( settings._colReorder ) {
+		return settings._colReorder;
+	}
+
+	// Allow the options to be a boolean for defaults
+	if ( opts === true ) {
+		opts = {};
+	}
+
+	// Convert from camelCase to Hungarian, just as DataTables does
+	var camelToHungarian = $.fn.dataTable.camelToHungarian;
+	if ( camelToHungarian ) {
+		camelToHungarian( ColReorder.defaults, ColReorder.defaults, true );
+		camelToHungarian( ColReorder.defaults, opts || {} );
+	}
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public class variables
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+	/**
+	 * @namespace Settings object which contains customisable information for ColReorder instance
+	 */
+	this.s = {
+		/**
+		 * DataTables settings object
+		 *  @property dt
+		 *  @type     Object
+		 *  @default  null
+		 */
+		"dt": null,
+
+		/**
+		 * Initialisation object used for this instance
+		 *  @property init
+		 *  @type     object
+		 *  @default  {}
+		 */
+		"init": $.extend( true, {}, ColReorder.defaults, opts ),
+
+		/**
+		 * Number of columns to fix (not allow to be reordered)
+		 *  @property fixed
+		 *  @type     int
+		 *  @default  0
+		 */
+		"fixed": 0,
+
+		/**
+		 * Number of columns to fix counting from right (not allow to be reordered)
+		 *  @property fixedRight
+		 *  @type     int
+		 *  @default  0
+		 */
+		"fixedRight": 0,
+
+		/**
+		 * Callback function for once the reorder has been done
+		 *  @property reorderCallback
+		 *  @type     function
+		 *  @default  null
+		 */
+		"reorderCallback": null,
+
+		/**
+		 * @namespace Information used for the mouse drag
+		 */
+		"mouse": {
+			"startX": -1,
+			"startY": -1,
+			"offsetX": -1,
+			"offsetY": -1,
+			"target": -1,
+			"targetIndex": -1,
+			"fromIndex": -1
+		},
+
+		/**
+		 * Information which is used for positioning the insert cusor and knowing where to do the
+		 * insert. Array of objects with the properties:
+		 *   x: x-axis position
+		 *   to: insert point
+		 *  @property aoTargets
+		 *  @type     array
+		 *  @default  []
+		 */
+		"aoTargets": []
+	};
+
+
+	/**
+	 * @namespace Common and useful DOM elements for the class instance
+	 */
+	this.dom = {
+		/**
+		 * Dragging element (the one the mouse is moving)
+		 *  @property drag
+		 *  @type     element
+		 *  @default  null
+		 */
+		"drag": null,
+
+		/**
+		 * The insert cursor
+		 *  @property pointer
+		 *  @type     element
+		 *  @default  null
+		 */
+		"pointer": null
+	};
+
+
+	/* Constructor logic */
+	this.s.dt = settings;
+	this.s.dt._colReorder = this;
+	this._fnConstruct();
+
+	return this;
+};
+
+
+
+$.extend( ColReorder.prototype, {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public methods
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+	/**
+	 * Reset the column ordering to the original ordering that was detected on
+	 * start up.
+	 *  @return {this} Returns `this` for chaining.
+	 *
+	 *  @example
+	 *    // DataTables initialisation with ColReorder
+	 *    var table = $('#example').dataTable( {
+	 *        "sDom": 'Rlfrtip'
+	 *    } );
+	 *
+	 *    // Add click event to a button to reset the ordering
+	 *    $('#resetOrdering').click( function (e) {
+	 *        e.preventDefault();
+	 *        $.fn.dataTable.ColReorder( table ).fnReset();
+	 *    } );
+	 */
+	"fnReset": function ()
+	{
+		this._fnOrderColumns( this.fnOrder() );
+
+		return this;
+	},
+
+	/**
+	 * `Deprecated` - Get the current order of the columns, as an array.
+	 *  @return {array} Array of column identifiers
+	 *  @deprecated `fnOrder` should be used in preference to this method.
+	 *      `fnOrder` acts as a getter/setter.
+	 */
+	"fnGetCurrentOrder": function ()
+	{
+		return this.fnOrder();
+	},
+
+	/**
+	 * Get the current order of the columns, as an array. Note that the values
+	 * given in the array are unique identifiers for each column. Currently
+	 * these are the original ordering of the columns that was detected on
+	 * start up, but this could potentially change in future.
+	 *  @return {array} Array of column identifiers
+	 *
+	 *  @example
+	 *    // Get column ordering for the table
+	 *    var order = $.fn.dataTable.ColReorder( dataTable ).fnOrder();
+	 *//**
+	 * Set the order of the columns, from the positions identified in the
+	 * ordering array given. Note that ColReorder takes a brute force approach
+	 * to reordering, so it is possible multiple reordering events will occur
+	 * before the final order is settled upon.
+	 *  @param {array} [set] Array of column identifiers in the new order. Note
+	 *    that every column must be included, uniquely, in this array.
+	 *  @return {this} Returns `this` for chaining.
+	 *
+	 *  @example
+	 *    // Swap the first and second columns
+	 *    $.fn.dataTable.ColReorder( dataTable ).fnOrder( [1, 0, 2, 3, 4] );
+	 *
+	 *  @example
+	 *    // Move the first column to the end for the table `#example`
+	 *    var curr = $.fn.dataTable.ColReorder( '#example' ).fnOrder();
+	 *    var first = curr.shift();
+	 *    curr.push( first );
+	 *    $.fn.dataTable.ColReorder( '#example' ).fnOrder( curr );
+	 *
+	 *  @example
+	 *    // Reverse the table's order
+	 *    $.fn.dataTable.ColReorder( '#example' ).fnOrder(
+	 *      $.fn.dataTable.ColReorder( '#example' ).fnOrder().reverse()
+	 *    );
+	 */
+	"fnOrder": function ( set, original )
+	{
+		var a = [], i, ien, j, jen;
+		var columns = this.s.dt.aoColumns;
+
+		if ( set === undefined ){
+			for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+				a.push( columns[i]._ColReorder_iOrigCol );
+			}
+
+			return a;
+		}
+
+		// The order given is based on the original indexes, rather than the
+		// existing ones, so we need to translate from the original to current
+		// before then doing the order
+		if ( original ) {
+			var order = this.fnOrder();
+
+			for ( i=0, ien=set.length ; i<ien ; i++ ) {
+				a.push( $.inArray( set[i], order ) );
+			}
+
+			set = a;
+		}
+
+		this._fnOrderColumns( fnInvertKeyValues( set ) );
+
+		return this;
+	},
+
+
+	/**
+	 * Convert from the original column index, to the original
+	 *
+	 * @param  {int|array} idx Index(es) to convert
+	 * @param  {string} dir Transpose direction - `fromOriginal` / `toCurrent`
+	 *   or `'toOriginal` / `fromCurrent`
+	 * @return {int|array}     Converted values
+	 */
+	fnTranspose: function ( idx, dir )
+	{
+		if ( ! dir ) {
+			dir = 'toCurrent';
+		}
+
+		var order = this.fnOrder();
+		var columns = this.s.dt.aoColumns;
+
+		if ( dir === 'toCurrent' ) {
+			// Given an original index, want the current
+			return ! $.isArray( idx ) ?
+				$.inArray( idx, order ) :
+				$.map( idx, function ( index ) {
+					return $.inArray( index, order );
+				} );
+		}
+		else {
+			// Given a current index, want the original
+			return ! $.isArray( idx ) ?
+				columns[idx]._ColReorder_iOrigCol :
+				$.map( idx, function ( index ) {
+					return columns[index]._ColReorder_iOrigCol;
+				} );
+		}
+	},
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods (they are of course public in JS, but recommended as private)
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+	/**
+	 * Constructor logic
+	 *  @method  _fnConstruct
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnConstruct": function ()
+	{
+		var that = this;
+		var iLen = this.s.dt.aoColumns.length;
+		var table = this.s.dt.nTable;
+		var i;
+
+		/* Columns discounted from reordering - counting left to right */
+		if ( this.s.init.iFixedColumns )
+		{
+			this.s.fixed = this.s.init.iFixedColumns;
+		}
+
+		if ( this.s.init.iFixedColumnsLeft )
+		{
+			this.s.fixed = this.s.init.iFixedColumnsLeft;
+		}
+
+		/* Columns discounted from reordering - counting right to left */
+		this.s.fixedRight = this.s.init.iFixedColumnsRight ?
+			this.s.init.iFixedColumnsRight :
+			0;
+
+		/* Drop callback initialisation option */
+		if ( this.s.init.fnReorderCallback )
+		{
+			this.s.reorderCallback = this.s.init.fnReorderCallback;
+		}
+
+		/* Add event handlers for the drag and drop, and also mark the original column order */
+		for ( i = 0; i < iLen; i++ )
+		{
+			if ( i > this.s.fixed-1 && i < iLen - this.s.fixedRight )
+			{
+				this._fnMouseListener( i, this.s.dt.aoColumns[i].nTh );
+			}
+
+			/* Mark the original column order for later reference */
+			this.s.dt.aoColumns[i]._ColReorder_iOrigCol = i;
+		}
+
+		/* State saving */
+		this.s.dt.oApi._fnCallbackReg( this.s.dt, 'aoStateSaveParams', function (oS, oData) {
+			that._fnStateSave.call( that, oData );
+		}, "ColReorder_State" );
+
+		/* An initial column order has been specified */
+		var aiOrder = null;
+		if ( this.s.init.aiOrder )
+		{
+			aiOrder = this.s.init.aiOrder.slice();
+		}
+
+		/* State loading, overrides the column order given */
+		if ( this.s.dt.oLoadedState && typeof this.s.dt.oLoadedState.ColReorder != 'undefined' &&
+		  this.s.dt.oLoadedState.ColReorder.length == this.s.dt.aoColumns.length )
+		{
+			aiOrder = this.s.dt.oLoadedState.ColReorder;
+		}
+
+		/* If we have an order to apply - do so */
+		if ( aiOrder )
+		{
+			/* We might be called during or after the DataTables initialisation. If before, then we need
+			 * to wait until the draw is done, if after, then do what we need to do right away
+			 */
+			if ( !that.s.dt._bInitComplete )
+			{
+				var bDone = false;
+				$(table).on( 'draw.dt.colReorder', function () {
+					if ( !that.s.dt._bInitComplete && !bDone )
+					{
+						bDone = true;
+						var resort = fnInvertKeyValues( aiOrder );
+						that._fnOrderColumns.call( that, resort );
+					}
+				} );
+			}
+			else
+			{
+				var resort = fnInvertKeyValues( aiOrder );
+				that._fnOrderColumns.call( that, resort );
+			}
+		}
+		else {
+			this._fnSetColumnIndexes();
+		}
+
+		// Destroy clean up
+		$(table).on( 'destroy.dt.colReorder', function () {
+			$(table).off( 'destroy.dt.colReorder draw.dt.colReorder' );
+			$(that.s.dt.nTHead).find( '*' ).off( '.ColReorder' );
+
+			$.each( that.s.dt.aoColumns, function (i, column) {
+				$(column.nTh).removeAttr('data-column-index');
+			} );
+
+			that.s.dt._colReorder = null;
+			that.s = null;
+		} );
+	},
+
+
+	/**
+	 * Set the column order from an array
+	 *  @method  _fnOrderColumns
+	 *  @param   array a An array of integers which dictate the column order that should be applied
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnOrderColumns": function ( a )
+	{
+		var changed = false;
+
+		if ( a.length != this.s.dt.aoColumns.length )
+		{
+			this.s.dt.oInstance.oApi._fnLog( this.s.dt, 1, "ColReorder - array reorder does not "+
+				"match known number of columns. Skipping." );
+			return;
+		}
+
+		for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+		{
+			var currIndex = $.inArray( i, a );
+			if ( i != currIndex )
+			{
+				/* Reorder our switching array */
+				fnArraySwitch( a, currIndex, i );
+
+				/* Do the column reorder in the table */
+				this.s.dt.oInstance.fnColReorder( currIndex, i, true, false );
+
+				changed = true;
+			}
+		}
+
+		$.fn.dataTable.Api( this.s.dt ).rows().invalidate();
+
+		this._fnSetColumnIndexes();
+
+		// Has anything actually changed? If not, then nothing else to do
+		if ( ! changed ) {
+			return;
+		}
+
+		/* When scrolling we need to recalculate the column sizes to allow for the shift */
+		if ( this.s.dt.oScroll.sX !== "" || this.s.dt.oScroll.sY !== "" )
+		{
+			this.s.dt.oInstance.fnAdjustColumnSizing( false );
+		}
+
+		/* Save the state */
+		this.s.dt.oInstance.oApi._fnSaveState( this.s.dt );
+
+		if ( this.s.reorderCallback !== null )
+		{
+			this.s.reorderCallback.call( this );
+		}
+	},
+
+
+	/**
+	 * Because we change the indexes of columns in the table, relative to their starting point
+	 * we need to reorder the state columns to what they are at the starting point so we can
+	 * then rearrange them again on state load!
+	 *  @method  _fnStateSave
+	 *  @param   object oState DataTables state
+	 *  @returns string JSON encoded cookie string for DataTables
+	 *  @private
+	 */
+	"_fnStateSave": function ( oState )
+	{
+		var i, iLen, aCopy, iOrigColumn;
+		var oSettings = this.s.dt;
+		var columns = oSettings.aoColumns;
+
+		oState.ColReorder = [];
+
+		/* Sorting */
+		if ( oState.aaSorting ) {
+			// 1.10.0-
+			for ( i=0 ; i<oState.aaSorting.length ; i++ ) {
+				oState.aaSorting[i][0] = columns[ oState.aaSorting[i][0] ]._ColReorder_iOrigCol;
+			}
+
+			var aSearchCopy = $.extend( true, [], oState.aoSearchCols );
+
+			for ( i=0, iLen=columns.length ; i<iLen ; i++ )
+			{
+				iOrigColumn = columns[i]._ColReorder_iOrigCol;
+
+				/* Column filter */
+				oState.aoSearchCols[ iOrigColumn ] = aSearchCopy[i];
+
+				/* Visibility */
+				oState.abVisCols[ iOrigColumn ] = columns[i].bVisible;
+
+				/* Column reordering */
+				oState.ColReorder.push( iOrigColumn );
+			}
+		}
+		else if ( oState.order ) {
+			// 1.10.1+
+			for ( i=0 ; i<oState.order.length ; i++ ) {
+				oState.order[i][0] = columns[ oState.order[i][0] ]._ColReorder_iOrigCol;
+			}
+
+			var stateColumnsCopy = $.extend( true, [], oState.columns );
+
+			for ( i=0, iLen=columns.length ; i<iLen ; i++ )
+			{
+				iOrigColumn = columns[i]._ColReorder_iOrigCol;
+
+				/* Columns */
+				oState.columns[ iOrigColumn ] = stateColumnsCopy[i];
+
+				/* Column reordering */
+				oState.ColReorder.push( iOrigColumn );
+			}
+		}
+	},
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Mouse drop and drag
+	 */
+
+	/**
+	 * Add a mouse down listener to a particluar TH element
+	 *  @method  _fnMouseListener
+	 *  @param   int i Column index
+	 *  @param   element nTh TH element clicked on
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnMouseListener": function ( i, nTh )
+	{
+		var that = this;
+		$(nTh)
+			.on( 'mousedown.ColReorder', function (e) {
+				e.preventDefault();
+				that._fnMouseDown.call( that, e, nTh );
+			} )
+			.on( 'touchstart.ColReorder', function (e) {
+				that._fnMouseDown.call( that, e, nTh );
+			} );
+	},
+
+
+	/**
+	 * Mouse down on a TH element in the table header
+	 *  @method  _fnMouseDown
+	 *  @param   event e Mouse event
+	 *  @param   element nTh TH element to be dragged
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnMouseDown": function ( e, nTh )
+	{
+		var that = this;
+
+		/* Store information about the mouse position */
+		var target = $(e.target).closest('th, td');
+		var offset = target.offset();
+		var idx = parseInt( $(nTh).attr('data-column-index'), 10 );
+
+		if ( idx === undefined ) {
+			return;
+		}
+
+		this.s.mouse.startX = this._fnCursorPosition( e, 'pageX' );
+		this.s.mouse.startY = this._fnCursorPosition( e, 'pageY' );
+		this.s.mouse.offsetX = this._fnCursorPosition( e, 'pageX' ) - offset.left;
+		this.s.mouse.offsetY = this._fnCursorPosition( e, 'pageY' ) - offset.top;
+		this.s.mouse.target = this.s.dt.aoColumns[ idx ].nTh;//target[0];
+		this.s.mouse.targetIndex = idx;
+		this.s.mouse.fromIndex = idx;
+
+		this._fnRegions();
+
+		/* Add event handlers to the document */
+		$(document)
+			.on( 'mousemove.ColReorder touchmove.ColReorder', function (e) {
+				that._fnMouseMove.call( that, e );
+			} )
+			.on( 'mouseup.ColReorder touchend.ColReorder', function (e) {
+				that._fnMouseUp.call( that, e );
+			} );
+	},
+
+
+	/**
+	 * Deal with a mouse move event while dragging a node
+	 *  @method  _fnMouseMove
+	 *  @param   event e Mouse event
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnMouseMove": function ( e )
+	{
+		var that = this;
+
+		if ( this.dom.drag === null )
+		{
+			/* Only create the drag element if the mouse has moved a specific distance from the start
+			 * point - this allows the user to make small mouse movements when sorting and not have a
+			 * possibly confusing drag element showing up
+			 */
+			if ( Math.pow(
+				Math.pow(this._fnCursorPosition( e, 'pageX') - this.s.mouse.startX, 2) +
+				Math.pow(this._fnCursorPosition( e, 'pageY') - this.s.mouse.startY, 2), 0.5 ) < 5 )
+			{
+				return;
+			}
+			this._fnCreateDragNode();
+		}
+
+		/* Position the element - we respect where in the element the click occured */
+		this.dom.drag.css( {
+			left: this._fnCursorPosition( e, 'pageX' ) - this.s.mouse.offsetX,
+			top: this._fnCursorPosition( e, 'pageY' ) - this.s.mouse.offsetY
+		} );
+
+		/* Based on the current mouse position, calculate where the insert should go */
+		var bSet = false;
+		var lastToIndex = this.s.mouse.toIndex;
+
+		for ( var i=1, iLen=this.s.aoTargets.length ; i<iLen ; i++ )
+		{
+			if ( this._fnCursorPosition(e, 'pageX') < this.s.aoTargets[i-1].x + ((this.s.aoTargets[i].x-this.s.aoTargets[i-1].x)/2) )
+			{
+				this.dom.pointer.css( 'left', this.s.aoTargets[i-1].x );
+				this.s.mouse.toIndex = this.s.aoTargets[i-1].to;
+				bSet = true;
+				break;
+			}
+		}
+
+		// The insert element wasn't positioned in the array (less than
+		// operator), so we put it at the end
+		if ( !bSet )
+		{
+			this.dom.pointer.css( 'left', this.s.aoTargets[this.s.aoTargets.length-1].x );
+			this.s.mouse.toIndex = this.s.aoTargets[this.s.aoTargets.length-1].to;
+		}
+
+		// Perform reordering if realtime updating is on and the column has moved
+		if ( this.s.init.bRealtime && lastToIndex !== this.s.mouse.toIndex ) {
+			this.s.dt.oInstance.fnColReorder( this.s.mouse.fromIndex, this.s.mouse.toIndex, false );
+			this.s.mouse.fromIndex = this.s.mouse.toIndex;
+			this._fnRegions();
+		}
+	},
+
+
+	/**
+	 * Finish off the mouse drag and insert the column where needed
+	 *  @method  _fnMouseUp
+	 *  @param   event e Mouse event
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnMouseUp": function ( e )
+	{
+		var that = this;
+
+		$(document).off( '.ColReorder' );
+
+		if ( this.dom.drag !== null )
+		{
+			/* Remove the guide elements */
+			this.dom.drag.remove();
+			this.dom.pointer.remove();
+			this.dom.drag = null;
+			this.dom.pointer = null;
+
+			/* Actually do the reorder */
+			this.s.dt.oInstance.fnColReorder( this.s.mouse.fromIndex, this.s.mouse.toIndex, true );
+			this._fnSetColumnIndexes();
+
+			/* When scrolling we need to recalculate the column sizes to allow for the shift */
+			if ( this.s.dt.oScroll.sX !== "" || this.s.dt.oScroll.sY !== "" )
+			{
+				this.s.dt.oInstance.fnAdjustColumnSizing( false );
+			}
+
+			/* Save the state */
+			this.s.dt.oInstance.oApi._fnSaveState( this.s.dt );
+
+			if ( this.s.reorderCallback !== null )
+			{
+				this.s.reorderCallback.call( this );
+			}
+		}
+	},
+
+
+	/**
+	 * Calculate a cached array with the points of the column inserts, and the
+	 * 'to' points
+	 *  @method  _fnRegions
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnRegions": function ()
+	{
+		var aoColumns = this.s.dt.aoColumns;
+
+		this.s.aoTargets.splice( 0, this.s.aoTargets.length );
+
+		this.s.aoTargets.push( {
+			"x":  $(this.s.dt.nTable).offset().left,
+			"to": 0
+		} );
+
+		var iToPoint = 0;
+		var total = this.s.aoTargets[0].x;
+
+		for ( var i=0, iLen=aoColumns.length ; i<iLen ; i++ )
+		{
+			/* For the column / header in question, we want it's position to remain the same if the
+			 * position is just to it's immediate left or right, so we only increment the counter for
+			 * other columns
+			 */
+			if ( i != this.s.mouse.fromIndex )
+			{
+				iToPoint++;
+			}
+
+			if ( aoColumns[i].bVisible && aoColumns[i].nTh.style.display !=='none' )
+			{
+				total += $(aoColumns[i].nTh).outerWidth();
+
+				this.s.aoTargets.push( {
+					"x":  total,
+					"to": iToPoint
+				} );
+			}
+		}
+
+		/* Disallow columns for being reordered by drag and drop, counting right to left */
+		if ( this.s.fixedRight !== 0 )
+		{
+			this.s.aoTargets.splice( this.s.aoTargets.length - this.s.fixedRight );
+		}
+
+		/* Disallow columns for being reordered by drag and drop, counting left to right */
+		if ( this.s.fixed !== 0 )
+		{
+			this.s.aoTargets.splice( 0, this.s.fixed );
+		}
+	},
+
+
+	/**
+	 * Copy the TH element that is being drags so the user has the idea that they are actually
+	 * moving it around the page.
+	 *  @method  _fnCreateDragNode
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnCreateDragNode": function ()
+	{
+		var scrolling = this.s.dt.oScroll.sX !== "" || this.s.dt.oScroll.sY !== "";
+
+		var origCell = this.s.dt.aoColumns[ this.s.mouse.targetIndex ].nTh;
+		var origTr = origCell.parentNode;
+		var origThead = origTr.parentNode;
+		var origTable = origThead.parentNode;
+		var cloneCell = $(origCell).clone();
+
+		// This is a slightly odd combination of jQuery and DOM, but it is the
+		// fastest and least resource intensive way I could think of cloning
+		// the table with just a single header cell in it.
+		this.dom.drag = $(origTable.cloneNode(false))
+			.addClass( 'DTCR_clonedTable' )
+			.append(
+				$(origThead.cloneNode(false)).append(
+					$(origTr.cloneNode(false)).append(
+						cloneCell[0]
+					)
+				)
+			)
+			.css( {
+				position: 'absolute',
+				top: 0,
+				left: 0,
+				width: $(origCell).outerWidth(),
+				height: $(origCell).outerHeight()
+			} )
+			.appendTo( 'body' );
+
+		this.dom.pointer = $('<div></div>')
+			.addClass( 'DTCR_pointer' )
+			.css( {
+				position: 'absolute',
+				top: scrolling ?
+					$('div.dataTables_scroll', this.s.dt.nTableWrapper).offset().top :
+					$(this.s.dt.nTable).offset().top,
+				height : scrolling ?
+					$('div.dataTables_scroll', this.s.dt.nTableWrapper).height() :
+					$(this.s.dt.nTable).height()
+			} )
+			.appendTo( 'body' );
+	},
+
+
+	/**
+	 * Add a data attribute to the column headers, so we know the index of
+	 * the row to be reordered. This allows fast detection of the index, and
+	 * for this plug-in to work with FixedHeader which clones the nodes.
+	 *  @private
+	 */
+	"_fnSetColumnIndexes": function ()
+	{
+		$.each( this.s.dt.aoColumns, function (i, column) {
+			$(column.nTh).attr('data-column-index', i);
+		} );
+	},
+
+
+	/**
+	 * Get cursor position regardless of mouse or touch input
+	 * @param  {Event}  e    jQuery Event
+	 * @param  {string} prop Property to get
+	 * @return {number}      Value
+	 */
+	_fnCursorPosition: function ( e, prop ) {
+		if ( e.type.indexOf('touch') !== -1 ) {
+			return e.originalEvent.touches[0][ prop ];
+		}
+		return e[ prop ];
+	}
+} );
+
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Static parameters
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+/**
+ * ColReorder default settings for initialisation
+ *  @namespace
+ *  @static
+ */
+ColReorder.defaults = {
+	/**
+	 * Predefined ordering for the columns that will be applied automatically
+	 * on initialisation. If not specified then the order that the columns are
+	 * found to be in the HTML is the order used.
+	 *  @type array
+	 *  @default null
+	 *  @static
+	 */
+	aiOrder: null,
+
+	/**
+	 * Redraw the table's column ordering as the end user draws the column
+	 * (`true`) or wait until the mouse is released (`false` - default). Note
+	 * that this will perform a redraw on each reordering, which involves an
+	 * Ajax request each time if you are using server-side processing in
+	 * DataTables.
+	 *  @type boolean
+	 *  @default false
+	 *  @static
+	 */
+	bRealtime: true,
+
+	/**
+	 * Indicate how many columns should be fixed in position (counting from the
+	 * left). This will typically be 1 if used, but can be as high as you like.
+	 *  @type int
+	 *  @default 0
+	 *  @static
+	 */
+	iFixedColumnsLeft: 0,
+
+	/**
+	 * As `iFixedColumnsRight` but counting from the right.
+	 *  @type int
+	 *  @default 0
+	 *  @static
+	 */
+	iFixedColumnsRight: 0,
+
+	/**
+	 * Callback function that is fired when columns are reordered. The `column-
+	 * reorder` event is preferred over this callback
+	 *  @type function():void
+	 *  @default null
+	 *  @static
+	 */
+	fnReorderCallback: null
+};
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Constants
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * ColReorder version
+ *  @constant  version
+ *  @type      String
+ *  @default   As code
+ */
+ColReorder.version = "1.3.3";
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables interfaces
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+// Expose
+$.fn.dataTable.ColReorder = ColReorder;
+$.fn.DataTable.ColReorder = ColReorder;
+
+
+// Register a new feature with DataTables
+if ( typeof $.fn.dataTable == "function" &&
+     typeof $.fn.dataTableExt.fnVersionCheck == "function" &&
+     $.fn.dataTableExt.fnVersionCheck('1.10.8') )
+{
+	$.fn.dataTableExt.aoFeatures.push( {
+		"fnInit": function( settings ) {
+			var table = settings.oInstance;
+
+			if ( ! settings._colReorder ) {
+				var dtInit = settings.oInit;
+				var opts = dtInit.colReorder || dtInit.oColReorder || {};
+
+				new ColReorder( settings, opts );
+			}
+			else {
+				table.oApi._fnLog( settings, 1, "ColReorder attempted to initialise twice. Ignoring second" );
+			}
+
+			return null; /* No node for DataTables to insert */
+		},
+		"cFeature": "R",
+		"sFeature": "ColReorder"
+	} );
+}
+else {
+	alert( "Warning: ColReorder requires DataTables 1.10.8 or greater - www.datatables.net/download");
+}
+
+
+// Attach a listener to the document which listens for DataTables initialisation
+// events so we can automatically initialise
+$(document).on( 'preInit.dt.colReorder', function (e, settings) {
+	if ( e.namespace !== 'dt' ) {
+		return;
+	}
+
+	var init = settings.oInit.colReorder;
+	var defaults = DataTable.defaults.colReorder;
+
+	if ( init || defaults ) {
+		var opts = $.extend( {}, init, defaults );
+
+		if ( init !== false ) {
+			new ColReorder( settings, opts  );
+		}
+	}
+} );
+
+
+// API augmentation
+$.fn.dataTable.Api.register( 'colReorder.reset()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		ctx._colReorder.fnReset();
+	} );
+} );
+
+$.fn.dataTable.Api.register( 'colReorder.order()', function ( set, original ) {
+	if ( set ) {
+		return this.iterator( 'table', function ( ctx ) {
+			ctx._colReorder.fnOrder( set, original );
+		} );
+	}
+
+	return this.context.length ?
+		this.context[0]._colReorder.fnOrder() :
+		null;
+} );
+
+$.fn.dataTable.Api.register( 'colReorder.transpose()', function ( idx, dir ) {
+	return this.context.length && this.context[0]._colReorder ?
+		this.context[0]._colReorder.fnTranspose( idx, dir ) :
+		idx;
+} );
+
+
+return ColReorder;
+}));
diff --git a/static/DataTables/ColReorder-1.3.3/js/dataTables.colReorder.min.js b/static/DataTables/ColReorder-1.3.3/js/dataTables.colReorder.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..24d2c4938105b8d21b194c67b03931a50b33e43c
--- /dev/null
+++ b/static/DataTables/ColReorder-1.3.3/js/dataTables.colReorder.min.js
@@ -0,0 +1,27 @@
+/*!
+ ColReorder 1.3.3
+ ©2010-2015 SpryMedia Ltd - datatables.net/license
+*/
+(function(f){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(o){return f(o,window,document)}):"object"===typeof exports?module.exports=function(o,l){o||(o=window);if(!l||!l.fn.dataTable)l=require("datatables.net")(o,l).$;return f(l,o,o.document)}:f(jQuery,window,document)})(function(f,o,l,r){function q(a){for(var b=[],d=0,e=a.length;d<e;d++)b[a[d]]=d;return b}function p(a,b,d){b=a.splice(b,1)[0];a.splice(d,0,b)}function s(a,b,d){for(var e=[],f=0,c=a.childNodes.length;f<
+c;f++)1==a.childNodes[f].nodeType&&e.push(a.childNodes[f]);b=e[b];null!==d?a.insertBefore(b,e[d]):a.appendChild(b)}var t=f.fn.dataTable;f.fn.dataTableExt.oApi.fnColReorder=function(a,b,d,e,g){var c,h,j,m,i,l=a.aoColumns.length,k;i=function(a,b,c){if(a[b]&&"function"!==typeof a[b]){var d=a[b].split("."),e=d.shift();isNaN(1*e)||(a[b]=c[1*e]+"."+d.join("."))}};if(b!=d)if(0>b||b>=l)this.oApi._fnLog(a,1,"ColReorder 'from' index is out of bounds: "+b);else if(0>d||d>=l)this.oApi._fnLog(a,1,"ColReorder 'to' index is out of bounds: "+
+d);else{j=[];c=0;for(h=l;c<h;c++)j[c]=c;p(j,b,d);var n=q(j);c=0;for(h=a.aaSorting.length;c<h;c++)a.aaSorting[c][0]=n[a.aaSorting[c][0]];if(null!==a.aaSortingFixed){c=0;for(h=a.aaSortingFixed.length;c<h;c++)a.aaSortingFixed[c][0]=n[a.aaSortingFixed[c][0]]}c=0;for(h=l;c<h;c++){k=a.aoColumns[c];j=0;for(m=k.aDataSort.length;j<m;j++)k.aDataSort[j]=n[k.aDataSort[j]];k.idx=n[k.idx]}f.each(a.aLastSort,function(b,c){a.aLastSort[b].src=n[c.src]});c=0;for(h=l;c<h;c++)k=a.aoColumns[c],"number"==typeof k.mData?
+k.mData=n[k.mData]:f.isPlainObject(k.mData)&&(i(k.mData,"_",n),i(k.mData,"filter",n),i(k.mData,"sort",n),i(k.mData,"type",n));if(a.aoColumns[b].bVisible){i=this.oApi._fnColumnIndexToVisible(a,b);m=null;for(c=d<b?d:d+1;null===m&&c<l;)m=this.oApi._fnColumnIndexToVisible(a,c),c++;j=a.nTHead.getElementsByTagName("tr");c=0;for(h=j.length;c<h;c++)s(j[c],i,m);if(null!==a.nTFoot){j=a.nTFoot.getElementsByTagName("tr");c=0;for(h=j.length;c<h;c++)s(j[c],i,m)}c=0;for(h=a.aoData.length;c<h;c++)null!==a.aoData[c].nTr&&
+s(a.aoData[c].nTr,i,m)}p(a.aoColumns,b,d);c=0;for(h=l;c<h;c++)a.oApi._fnColumnOptions(a,c,{});p(a.aoPreSearchCols,b,d);c=0;for(h=a.aoData.length;c<h;c++){m=a.aoData[c];if(k=m.anCells){p(k,b,d);j=0;for(i=k.length;j<i;j++)k[j]&&k[j]._DT_CellIndex&&(k[j]._DT_CellIndex.column=j)}"dom"!==m.src&&f.isArray(m._aData)&&p(m._aData,b,d)}c=0;for(h=a.aoHeader.length;c<h;c++)p(a.aoHeader[c],b,d);if(null!==a.aoFooter){c=0;for(h=a.aoFooter.length;c<h;c++)p(a.aoFooter[c],b,d)}(g||g===r)&&f.fn.dataTable.Api(a).rows().invalidate();
+c=0;for(h=l;c<h;c++)f(a.aoColumns[c].nTh).off("click.DT"),this.oApi._fnSortAttachListener(a,a.aoColumns[c].nTh,c);f(a.oInstance).trigger("column-reorder.dt",[a,{from:b,to:d,mapping:n,drop:e,iFrom:b,iTo:d,aiInvertMapping:n}])}};var i=function(a,b){var d=(new f.fn.dataTable.Api(a)).settings()[0];if(d._colReorder)return d._colReorder;!0===b&&(b={});var e=f.fn.dataTable.camelToHungarian;e&&(e(i.defaults,i.defaults,!0),e(i.defaults,b||{}));this.s={dt:null,init:f.extend(!0,{},i.defaults,b),fixed:0,fixedRight:0,
+reorderCallback:null,mouse:{startX:-1,startY:-1,offsetX:-1,offsetY:-1,target:-1,targetIndex:-1,fromIndex:-1},aoTargets:[]};this.dom={drag:null,pointer:null};this.s.dt=d;this.s.dt._colReorder=this;this._fnConstruct();return this};f.extend(i.prototype,{fnReset:function(){this._fnOrderColumns(this.fnOrder());return this},fnGetCurrentOrder:function(){return this.fnOrder()},fnOrder:function(a,b){var d=[],e,g,c=this.s.dt.aoColumns;if(a===r){e=0;for(g=c.length;e<g;e++)d.push(c[e]._ColReorder_iOrigCol);return d}if(b){c=
+this.fnOrder();e=0;for(g=a.length;e<g;e++)d.push(f.inArray(a[e],c));a=d}this._fnOrderColumns(q(a));return this},fnTranspose:function(a,b){b||(b="toCurrent");var d=this.fnOrder(),e=this.s.dt.aoColumns;return"toCurrent"===b?!f.isArray(a)?f.inArray(a,d):f.map(a,function(a){return f.inArray(a,d)}):!f.isArray(a)?e[a]._ColReorder_iOrigCol:f.map(a,function(a){return e[a]._ColReorder_iOrigCol})},_fnConstruct:function(){var a=this,b=this.s.dt.aoColumns.length,d=this.s.dt.nTable,e;this.s.init.iFixedColumns&&
+(this.s.fixed=this.s.init.iFixedColumns);this.s.init.iFixedColumnsLeft&&(this.s.fixed=this.s.init.iFixedColumnsLeft);this.s.fixedRight=this.s.init.iFixedColumnsRight?this.s.init.iFixedColumnsRight:0;this.s.init.fnReorderCallback&&(this.s.reorderCallback=this.s.init.fnReorderCallback);for(e=0;e<b;e++)e>this.s.fixed-1&&e<b-this.s.fixedRight&&this._fnMouseListener(e,this.s.dt.aoColumns[e].nTh),this.s.dt.aoColumns[e]._ColReorder_iOrigCol=e;this.s.dt.oApi._fnCallbackReg(this.s.dt,"aoStateSaveParams",function(b,
+c){a._fnStateSave.call(a,c)},"ColReorder_State");var g=null;this.s.init.aiOrder&&(g=this.s.init.aiOrder.slice());this.s.dt.oLoadedState&&("undefined"!=typeof this.s.dt.oLoadedState.ColReorder&&this.s.dt.oLoadedState.ColReorder.length==this.s.dt.aoColumns.length)&&(g=this.s.dt.oLoadedState.ColReorder);if(g)if(a.s.dt._bInitComplete)b=q(g),a._fnOrderColumns.call(a,b);else{var c=!1;f(d).on("draw.dt.colReorder",function(){if(!a.s.dt._bInitComplete&&!c){c=true;var b=q(g);a._fnOrderColumns.call(a,b)}})}else this._fnSetColumnIndexes();
+f(d).on("destroy.dt.colReorder",function(){f(d).off("destroy.dt.colReorder draw.dt.colReorder");f(a.s.dt.nTHead).find("*").off(".ColReorder");f.each(a.s.dt.aoColumns,function(a,b){f(b.nTh).removeAttr("data-column-index")});a.s.dt._colReorder=null;a.s=null})},_fnOrderColumns:function(a){var b=!1;if(a.length!=this.s.dt.aoColumns.length)this.s.dt.oInstance.oApi._fnLog(this.s.dt,1,"ColReorder - array reorder does not match known number of columns. Skipping.");else{for(var d=0,e=a.length;d<e;d++){var g=
+f.inArray(d,a);d!=g&&(p(a,g,d),this.s.dt.oInstance.fnColReorder(g,d,!0,!1),b=!0)}f.fn.dataTable.Api(this.s.dt).rows().invalidate();this._fnSetColumnIndexes();b&&((""!==this.s.dt.oScroll.sX||""!==this.s.dt.oScroll.sY)&&this.s.dt.oInstance.fnAdjustColumnSizing(!1),this.s.dt.oInstance.oApi._fnSaveState(this.s.dt),null!==this.s.reorderCallback&&this.s.reorderCallback.call(this))}},_fnStateSave:function(a){var b,d,e,g=this.s.dt.aoColumns;a.ColReorder=[];if(a.aaSorting){for(b=0;b<a.aaSorting.length;b++)a.aaSorting[b][0]=
+g[a.aaSorting[b][0]]._ColReorder_iOrigCol;var c=f.extend(!0,[],a.aoSearchCols);b=0;for(d=g.length;b<d;b++)e=g[b]._ColReorder_iOrigCol,a.aoSearchCols[e]=c[b],a.abVisCols[e]=g[b].bVisible,a.ColReorder.push(e)}else if(a.order){for(b=0;b<a.order.length;b++)a.order[b][0]=g[a.order[b][0]]._ColReorder_iOrigCol;c=f.extend(!0,[],a.columns);b=0;for(d=g.length;b<d;b++)e=g[b]._ColReorder_iOrigCol,a.columns[e]=c[b],a.ColReorder.push(e)}},_fnMouseListener:function(a,b){var d=this;f(b).on("mousedown.ColReorder",
+function(a){a.preventDefault();d._fnMouseDown.call(d,a,b)}).on("touchstart.ColReorder",function(a){d._fnMouseDown.call(d,a,b)})},_fnMouseDown:function(a,b){var d=this,e=f(a.target).closest("th, td").offset(),g=parseInt(f(b).attr("data-column-index"),10);g!==r&&(this.s.mouse.startX=this._fnCursorPosition(a,"pageX"),this.s.mouse.startY=this._fnCursorPosition(a,"pageY"),this.s.mouse.offsetX=this._fnCursorPosition(a,"pageX")-e.left,this.s.mouse.offsetY=this._fnCursorPosition(a,"pageY")-e.top,this.s.mouse.target=
+this.s.dt.aoColumns[g].nTh,this.s.mouse.targetIndex=g,this.s.mouse.fromIndex=g,this._fnRegions(),f(l).on("mousemove.ColReorder touchmove.ColReorder",function(a){d._fnMouseMove.call(d,a)}).on("mouseup.ColReorder touchend.ColReorder",function(a){d._fnMouseUp.call(d,a)}))},_fnMouseMove:function(a){if(null===this.dom.drag){if(5>Math.pow(Math.pow(this._fnCursorPosition(a,"pageX")-this.s.mouse.startX,2)+Math.pow(this._fnCursorPosition(a,"pageY")-this.s.mouse.startY,2),0.5))return;this._fnCreateDragNode()}this.dom.drag.css({left:this._fnCursorPosition(a,
+"pageX")-this.s.mouse.offsetX,top:this._fnCursorPosition(a,"pageY")-this.s.mouse.offsetY});for(var b=!1,d=this.s.mouse.toIndex,e=1,f=this.s.aoTargets.length;e<f;e++)if(this._fnCursorPosition(a,"pageX")<this.s.aoTargets[e-1].x+(this.s.aoTargets[e].x-this.s.aoTargets[e-1].x)/2){this.dom.pointer.css("left",this.s.aoTargets[e-1].x);this.s.mouse.toIndex=this.s.aoTargets[e-1].to;b=!0;break}b||(this.dom.pointer.css("left",this.s.aoTargets[this.s.aoTargets.length-1].x),this.s.mouse.toIndex=this.s.aoTargets[this.s.aoTargets.length-
+1].to);this.s.init.bRealtime&&d!==this.s.mouse.toIndex&&(this.s.dt.oInstance.fnColReorder(this.s.mouse.fromIndex,this.s.mouse.toIndex,!1),this.s.mouse.fromIndex=this.s.mouse.toIndex,this._fnRegions())},_fnMouseUp:function(){f(l).off(".ColReorder");null!==this.dom.drag&&(this.dom.drag.remove(),this.dom.pointer.remove(),this.dom.drag=null,this.dom.pointer=null,this.s.dt.oInstance.fnColReorder(this.s.mouse.fromIndex,this.s.mouse.toIndex,!0),this._fnSetColumnIndexes(),(""!==this.s.dt.oScroll.sX||""!==
+this.s.dt.oScroll.sY)&&this.s.dt.oInstance.fnAdjustColumnSizing(!1),this.s.dt.oInstance.oApi._fnSaveState(this.s.dt),null!==this.s.reorderCallback&&this.s.reorderCallback.call(this))},_fnRegions:function(){var a=this.s.dt.aoColumns;this.s.aoTargets.splice(0,this.s.aoTargets.length);this.s.aoTargets.push({x:f(this.s.dt.nTable).offset().left,to:0});for(var b=0,d=this.s.aoTargets[0].x,e=0,g=a.length;e<g;e++)e!=this.s.mouse.fromIndex&&b++,a[e].bVisible&&"none"!==a[e].nTh.style.display&&(d+=f(a[e].nTh).outerWidth(),
+this.s.aoTargets.push({x:d,to:b}));0!==this.s.fixedRight&&this.s.aoTargets.splice(this.s.aoTargets.length-this.s.fixedRight);0!==this.s.fixed&&this.s.aoTargets.splice(0,this.s.fixed)},_fnCreateDragNode:function(){var a=""!==this.s.dt.oScroll.sX||""!==this.s.dt.oScroll.sY,b=this.s.dt.aoColumns[this.s.mouse.targetIndex].nTh,d=b.parentNode,e=d.parentNode,g=e.parentNode,c=f(b).clone();this.dom.drag=f(g.cloneNode(!1)).addClass("DTCR_clonedTable").append(f(e.cloneNode(!1)).append(f(d.cloneNode(!1)).append(c[0]))).css({position:"absolute",
+top:0,left:0,width:f(b).outerWidth(),height:f(b).outerHeight()}).appendTo("body");this.dom.pointer=f("<div></div>").addClass("DTCR_pointer").css({position:"absolute",top:a?f("div.dataTables_scroll",this.s.dt.nTableWrapper).offset().top:f(this.s.dt.nTable).offset().top,height:a?f("div.dataTables_scroll",this.s.dt.nTableWrapper).height():f(this.s.dt.nTable).height()}).appendTo("body")},_fnSetColumnIndexes:function(){f.each(this.s.dt.aoColumns,function(a,b){f(b.nTh).attr("data-column-index",a)})},_fnCursorPosition:function(a,
+b){return-1!==a.type.indexOf("touch")?a.originalEvent.touches[0][b]:a[b]}});i.defaults={aiOrder:null,bRealtime:!0,iFixedColumnsLeft:0,iFixedColumnsRight:0,fnReorderCallback:null};i.version="1.3.3";f.fn.dataTable.ColReorder=i;f.fn.DataTable.ColReorder=i;"function"==typeof f.fn.dataTable&&"function"==typeof f.fn.dataTableExt.fnVersionCheck&&f.fn.dataTableExt.fnVersionCheck("1.10.8")?f.fn.dataTableExt.aoFeatures.push({fnInit:function(a){var b=a.oInstance;a._colReorder?b.oApi._fnLog(a,1,"ColReorder attempted to initialise twice. Ignoring second"):
+(b=a.oInit,new i(a,b.colReorder||b.oColReorder||{}));return null},cFeature:"R",sFeature:"ColReorder"}):alert("Warning: ColReorder requires DataTables 1.10.8 or greater - www.datatables.net/download");f(l).on("preInit.dt.colReorder",function(a,b){if("dt"===a.namespace){var d=b.oInit.colReorder,e=t.defaults.colReorder;if(d||e)e=f.extend({},d,e),!1!==d&&new i(b,e)}});f.fn.dataTable.Api.register("colReorder.reset()",function(){return this.iterator("table",function(a){a._colReorder.fnReset()})});f.fn.dataTable.Api.register("colReorder.order()",
+function(a,b){return a?this.iterator("table",function(d){d._colReorder.fnOrder(a,b)}):this.context.length?this.context[0]._colReorder.fnOrder():null});f.fn.dataTable.Api.register("colReorder.transpose()",function(a,b){return this.context.length&&this.context[0]._colReorder?this.context[0]._colReorder.fnTranspose(a,b):a});return i});
diff --git a/static/DataTables/DataTables-1.10.15/css/dataTables.bootstrap.css b/static/DataTables/DataTables-1.10.15/css/dataTables.bootstrap.css
new file mode 100644
index 0000000000000000000000000000000000000000..2bbb3ef290bcd79142a12827cebecc99fa7f2f5e
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/dataTables.bootstrap.css
@@ -0,0 +1,184 @@
+table.dataTable {
+  clear: both;
+  margin-top: 6px !important;
+  margin-bottom: 6px !important;
+  max-width: none !important;
+  border-collapse: separate !important;
+}
+table.dataTable td,
+table.dataTable th {
+  -webkit-box-sizing: content-box;
+  box-sizing: content-box;
+}
+table.dataTable td.dataTables_empty,
+table.dataTable th.dataTables_empty {
+  text-align: center;
+}
+table.dataTable.nowrap th,
+table.dataTable.nowrap td {
+  white-space: nowrap;
+}
+
+div.dataTables_wrapper div.dataTables_length label {
+  font-weight: normal;
+  text-align: left;
+  white-space: nowrap;
+}
+div.dataTables_wrapper div.dataTables_length select {
+  width: 75px;
+  display: inline-block;
+}
+div.dataTables_wrapper div.dataTables_filter {
+  text-align: right;
+}
+div.dataTables_wrapper div.dataTables_filter label {
+  font-weight: normal;
+  white-space: nowrap;
+  text-align: left;
+}
+div.dataTables_wrapper div.dataTables_filter input {
+  margin-left: 0.5em;
+  display: inline-block;
+  width: auto;
+}
+div.dataTables_wrapper div.dataTables_info {
+  padding-top: 8px;
+  white-space: nowrap;
+}
+div.dataTables_wrapper div.dataTables_paginate {
+  margin: 0;
+  white-space: nowrap;
+  text-align: right;
+}
+div.dataTables_wrapper div.dataTables_paginate ul.pagination {
+  margin: 2px 0;
+  white-space: nowrap;
+}
+div.dataTables_wrapper div.dataTables_processing {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 200px;
+  margin-left: -100px;
+  margin-top: -26px;
+  text-align: center;
+  padding: 1em 0;
+}
+
+table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting,
+table.dataTable thead > tr > td.sorting_asc,
+table.dataTable thead > tr > td.sorting_desc,
+table.dataTable thead > tr > td.sorting {
+  padding-right: 30px;
+}
+table.dataTable thead > tr > th:active,
+table.dataTable thead > tr > td:active {
+  outline: none;
+}
+table.dataTable thead .sorting,
+table.dataTable thead .sorting_asc,
+table.dataTable thead .sorting_desc,
+table.dataTable thead .sorting_asc_disabled,
+table.dataTable thead .sorting_desc_disabled {
+  cursor: pointer;
+  position: relative;
+}
+table.dataTable thead .sorting:after,
+table.dataTable thead .sorting_asc:after,
+table.dataTable thead .sorting_desc:after,
+table.dataTable thead .sorting_asc_disabled:after,
+table.dataTable thead .sorting_desc_disabled:after {
+  position: absolute;
+  bottom: 8px;
+  right: 8px;
+  display: block;
+  font-family: 'Glyphicons Halflings';
+  opacity: 0.5;
+}
+table.dataTable thead .sorting:after {
+  opacity: 0.2;
+  content: "\e150";
+  /* sort */
+}
+table.dataTable thead .sorting_asc:after {
+  content: "\e155";
+  /* sort-by-attributes */
+}
+table.dataTable thead .sorting_desc:after {
+  content: "\e156";
+  /* sort-by-attributes-alt */
+}
+table.dataTable thead .sorting_asc_disabled:after,
+table.dataTable thead .sorting_desc_disabled:after {
+  color: #eee;
+}
+
+div.dataTables_scrollHead table.dataTable {
+  margin-bottom: 0 !important;
+}
+
+div.dataTables_scrollBody > table {
+  border-top: none;
+  margin-top: 0 !important;
+  margin-bottom: 0 !important;
+}
+div.dataTables_scrollBody > table > thead .sorting:after,
+div.dataTables_scrollBody > table > thead .sorting_asc:after,
+div.dataTables_scrollBody > table > thead .sorting_desc:after {
+  display: none;
+}
+div.dataTables_scrollBody > table > tbody > tr:first-child > th,
+div.dataTables_scrollBody > table > tbody > tr:first-child > td {
+  border-top: none;
+}
+
+div.dataTables_scrollFoot > table {
+  margin-top: 0 !important;
+  border-top: none;
+}
+
+@media screen and (max-width: 767px) {
+  div.dataTables_wrapper div.dataTables_length,
+  div.dataTables_wrapper div.dataTables_filter,
+  div.dataTables_wrapper div.dataTables_info,
+  div.dataTables_wrapper div.dataTables_paginate {
+    text-align: center;
+  }
+}
+table.dataTable.table-condensed > thead > tr > th {
+  padding-right: 20px;
+}
+table.dataTable.table-condensed .sorting:after,
+table.dataTable.table-condensed .sorting_asc:after,
+table.dataTable.table-condensed .sorting_desc:after {
+  top: 6px;
+  right: 6px;
+}
+
+table.table-bordered.dataTable th,
+table.table-bordered.dataTable td {
+  border-left-width: 0;
+}
+table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child,
+table.table-bordered.dataTable td:last-child,
+table.table-bordered.dataTable td:last-child {
+  border-right-width: 0;
+}
+table.table-bordered.dataTable tbody th,
+table.table-bordered.dataTable tbody td {
+  border-bottom-width: 0;
+}
+
+div.dataTables_scrollHead table.table-bordered {
+  border-bottom-width: 0;
+}
+
+div.table-responsive > div.dataTables_wrapper > div.row {
+  margin: 0;
+}
+div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child {
+  padding-left: 0;
+}
+div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child {
+  padding-right: 0;
+}
diff --git a/static/DataTables/DataTables-1.10.15/css/dataTables.bootstrap.min.css b/static/DataTables/DataTables-1.10.15/css/dataTables.bootstrap.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..66a70ab2f5d61feb67b70c1443f4be89c9985c2f
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/dataTables.bootstrap.min.css
@@ -0,0 +1 @@
+table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody>table>thead .sorting:after,div.dataTables_scrollBody>table>thead .sorting_asc:after,div.dataTables_scrollBody>table>thead .sorting_desc:after{display:none}div.dataTables_scrollBody>table>tbody>tr:first-child>th,div.dataTables_scrollBody>table>tbody>tr:first-child>td{border-top:none}div.dataTables_scrollFoot>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:0}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0}
diff --git a/static/DataTables/DataTables-1.10.15/css/dataTables.foundation.css b/static/DataTables/DataTables-1.10.15/css/dataTables.foundation.css
new file mode 100644
index 0000000000000000000000000000000000000000..79848c9581f516074bad28a5ef0ded05a0a398ba
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/dataTables.foundation.css
@@ -0,0 +1,118 @@
+table.dataTable {
+  clear: both;
+  margin: 0.5em 0 !important;
+  max-width: none !important;
+  width: 100%;
+}
+table.dataTable td,
+table.dataTable th {
+  -webkit-box-sizing: content-box;
+  box-sizing: content-box;
+}
+table.dataTable td.dataTables_empty,
+table.dataTable th.dataTables_empty {
+  text-align: center;
+}
+table.dataTable.nowrap th, table.dataTable.nowrap td {
+  white-space: nowrap;
+}
+
+div.dataTables_wrapper {
+  position: relative;
+}
+div.dataTables_wrapper div.dataTables_length label {
+  float: left;
+  text-align: left;
+  margin-bottom: 0;
+}
+div.dataTables_wrapper div.dataTables_length select {
+  width: 75px;
+  margin-bottom: 0;
+}
+div.dataTables_wrapper div.dataTables_filter label {
+  float: right;
+  margin-bottom: 0;
+}
+div.dataTables_wrapper div.dataTables_filter input {
+  display: inline-block !important;
+  width: auto !important;
+  margin-bottom: 0;
+  margin-left: 0.5em;
+}
+div.dataTables_wrapper div.dataTables_info {
+  padding-top: 2px;
+}
+div.dataTables_wrapper div.dataTables_paginate {
+  float: right;
+  margin: 0;
+}
+div.dataTables_wrapper div.dataTables_processing {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 200px;
+  margin-left: -100px;
+  margin-top: -26px;
+  text-align: center;
+  padding: 1rem 0;
+}
+
+table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting,
+table.dataTable thead > tr > td.sorting_asc,
+table.dataTable thead > tr > td.sorting_desc,
+table.dataTable thead > tr > td.sorting {
+  padding-right: 1.5rem;
+}
+table.dataTable thead > tr > th:active,
+table.dataTable thead > tr > td:active {
+  outline: none;
+}
+table.dataTable thead .sorting,
+table.dataTable thead .sorting_asc,
+table.dataTable thead .sorting_desc,
+table.dataTable thead .sorting_asc_disabled,
+table.dataTable thead .sorting_desc_disabled {
+  cursor: pointer;
+}
+table.dataTable thead .sorting,
+table.dataTable thead .sorting_asc,
+table.dataTable thead .sorting_desc,
+table.dataTable thead .sorting_asc_disabled,
+table.dataTable thead .sorting_desc_disabled {
+  background-repeat: no-repeat;
+  background-position: center right;
+}
+table.dataTable thead .sorting {
+  background-image: url("../images/sort_both.png");
+}
+table.dataTable thead .sorting_asc {
+  background-image: url("../images/sort_asc.png");
+}
+table.dataTable thead .sorting_desc {
+  background-image: url("../images/sort_desc.png");
+}
+table.dataTable thead .sorting_asc_disabled {
+  background-image: url("../images/sort_asc_disabled.png");
+}
+table.dataTable thead .sorting_desc_disabled {
+  background-image: url("../images/sort_desc_disabled.png");
+}
+
+div.dataTables_scrollHead table {
+  margin-bottom: 0 !important;
+}
+
+div.dataTables_scrollBody table {
+  border-top: none;
+  margin-top: 0 !important;
+  margin-bottom: 0 !important;
+}
+div.dataTables_scrollBody table tbody tr:first-child th,
+div.dataTables_scrollBody table tbody tr:first-child td {
+  border-top: none;
+}
+
+div.dataTables_scrollFoot table {
+  margin-top: 0 !important;
+  border-top: none;
+}
diff --git a/static/DataTables/DataTables-1.10.15/css/dataTables.foundation.min.css b/static/DataTables/DataTables-1.10.15/css/dataTables.foundation.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..73af41efc03b88e9247a786fc76e31d9bc9116fe
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/dataTables.foundation.min.css
@@ -0,0 +1 @@
+table.dataTable{clear:both;margin:0.5em 0 !important;max-width:none !important;width:100%}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper{position:relative}div.dataTables_wrapper div.dataTables_length label{float:left;text-align:left;margin-bottom:0}div.dataTables_wrapper div.dataTables_length select{width:75px;margin-bottom:0}div.dataTables_wrapper div.dataTables_filter label{float:right;margin-bottom:0}div.dataTables_wrapper div.dataTables_filter input{display:inline-block !important;width:auto !important;margin-bottom:0;margin-left:0.5em}div.dataTables_wrapper div.dataTables_info{padding-top:2px}div.dataTables_wrapper div.dataTables_paginate{float:right;margin:0}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1rem 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:1.5rem}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("../images/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("../images/sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("../images/sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("../images/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("../images/sort_desc_disabled.png")}div.dataTables_scrollHead table{margin-bottom:0 !important}div.dataTables_scrollBody table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody table tbody tr:first-child th,div.dataTables_scrollBody table tbody tr:first-child td{border-top:none}div.dataTables_scrollFoot table{margin-top:0 !important;border-top:none}
diff --git a/static/DataTables/DataTables-1.10.15/css/dataTables.jqueryui.css b/static/DataTables/DataTables-1.10.15/css/dataTables.jqueryui.css
new file mode 100644
index 0000000000000000000000000000000000000000..dcc8f82f7981a102f6bb033eb074a48bffe37b4f
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/dataTables.jqueryui.css
@@ -0,0 +1,482 @@
+/*
+ * Table styles
+ */
+table.dataTable {
+  width: 100%;
+  margin: 0 auto;
+  clear: both;
+  border-collapse: separate;
+  border-spacing: 0;
+  /*
+   * Header and footer styles
+   */
+  /*
+   * Body styles
+   */
+}
+table.dataTable thead th,
+table.dataTable tfoot th {
+  font-weight: bold;
+}
+table.dataTable thead th,
+table.dataTable thead td {
+  padding: 10px 18px;
+}
+table.dataTable thead th:active,
+table.dataTable thead td:active {
+  outline: none;
+}
+table.dataTable tfoot th,
+table.dataTable tfoot td {
+  padding: 10px 18px 6px 18px;
+}
+table.dataTable tbody tr {
+  background-color: #ffffff;
+}
+table.dataTable tbody tr.selected {
+  background-color: #B0BED9;
+}
+table.dataTable tbody th,
+table.dataTable tbody td {
+  padding: 8px 10px;
+}
+table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td {
+  border-top: 1px solid #ddd;
+}
+table.dataTable.row-border tbody tr:first-child th,
+table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th,
+table.dataTable.display tbody tr:first-child td {
+  border-top: none;
+}
+table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td {
+  border-top: 1px solid #ddd;
+  border-right: 1px solid #ddd;
+}
+table.dataTable.cell-border tbody tr th:first-child,
+table.dataTable.cell-border tbody tr td:first-child {
+  border-left: 1px solid #ddd;
+}
+table.dataTable.cell-border tbody tr:first-child th,
+table.dataTable.cell-border tbody tr:first-child td {
+  border-top: none;
+}
+table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd {
+  background-color: #f9f9f9;
+}
+table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected {
+  background-color: #acbad4;
+}
+table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover {
+  background-color: #f6f6f6;
+}
+table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected {
+  background-color: #aab7d1;
+}
+table.dataTable.order-column tbody tr > .sorting_1,
+table.dataTable.order-column tbody tr > .sorting_2,
+table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1,
+table.dataTable.display tbody tr > .sorting_2,
+table.dataTable.display tbody tr > .sorting_3 {
+  background-color: #fafafa;
+}
+table.dataTable.order-column tbody tr.selected > .sorting_1,
+table.dataTable.order-column tbody tr.selected > .sorting_2,
+table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1,
+table.dataTable.display tbody tr.selected > .sorting_2,
+table.dataTable.display tbody tr.selected > .sorting_3 {
+  background-color: #acbad5;
+}
+table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
+  background-color: #f1f1f1;
+}
+table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
+  background-color: #f3f3f3;
+}
+table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
+  background-color: whitesmoke;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
+  background-color: #a6b4cd;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
+  background-color: #a8b5cf;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
+  background-color: #a9b7d1;
+}
+table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
+  background-color: #fafafa;
+}
+table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
+  background-color: #fcfcfc;
+}
+table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
+  background-color: #fefefe;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
+  background-color: #acbad5;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
+  background-color: #aebcd6;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
+  background-color: #afbdd8;
+}
+table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
+  background-color: #eaeaea;
+}
+table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
+  background-color: #ececec;
+}
+table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
+  background-color: #efefef;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
+  background-color: #a2aec7;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
+  background-color: #a3b0c9;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
+  background-color: #a5b2cb;
+}
+table.dataTable.no-footer {
+  border-bottom: 1px solid #111;
+}
+table.dataTable.nowrap th, table.dataTable.nowrap td {
+  white-space: nowrap;
+}
+table.dataTable.compact thead th,
+table.dataTable.compact thead td {
+  padding: 4px 17px 4px 4px;
+}
+table.dataTable.compact tfoot th,
+table.dataTable.compact tfoot td {
+  padding: 4px;
+}
+table.dataTable.compact tbody th,
+table.dataTable.compact tbody td {
+  padding: 4px;
+}
+table.dataTable th.dt-left,
+table.dataTable td.dt-left {
+  text-align: left;
+}
+table.dataTable th.dt-center,
+table.dataTable td.dt-center,
+table.dataTable td.dataTables_empty {
+  text-align: center;
+}
+table.dataTable th.dt-right,
+table.dataTable td.dt-right {
+  text-align: right;
+}
+table.dataTable th.dt-justify,
+table.dataTable td.dt-justify {
+  text-align: justify;
+}
+table.dataTable th.dt-nowrap,
+table.dataTable td.dt-nowrap {
+  white-space: nowrap;
+}
+table.dataTable thead th.dt-head-left,
+table.dataTable thead td.dt-head-left,
+table.dataTable tfoot th.dt-head-left,
+table.dataTable tfoot td.dt-head-left {
+  text-align: left;
+}
+table.dataTable thead th.dt-head-center,
+table.dataTable thead td.dt-head-center,
+table.dataTable tfoot th.dt-head-center,
+table.dataTable tfoot td.dt-head-center {
+  text-align: center;
+}
+table.dataTable thead th.dt-head-right,
+table.dataTable thead td.dt-head-right,
+table.dataTable tfoot th.dt-head-right,
+table.dataTable tfoot td.dt-head-right {
+  text-align: right;
+}
+table.dataTable thead th.dt-head-justify,
+table.dataTable thead td.dt-head-justify,
+table.dataTable tfoot th.dt-head-justify,
+table.dataTable tfoot td.dt-head-justify {
+  text-align: justify;
+}
+table.dataTable thead th.dt-head-nowrap,
+table.dataTable thead td.dt-head-nowrap,
+table.dataTable tfoot th.dt-head-nowrap,
+table.dataTable tfoot td.dt-head-nowrap {
+  white-space: nowrap;
+}
+table.dataTable tbody th.dt-body-left,
+table.dataTable tbody td.dt-body-left {
+  text-align: left;
+}
+table.dataTable tbody th.dt-body-center,
+table.dataTable tbody td.dt-body-center {
+  text-align: center;
+}
+table.dataTable tbody th.dt-body-right,
+table.dataTable tbody td.dt-body-right {
+  text-align: right;
+}
+table.dataTable tbody th.dt-body-justify,
+table.dataTable tbody td.dt-body-justify {
+  text-align: justify;
+}
+table.dataTable tbody th.dt-body-nowrap,
+table.dataTable tbody td.dt-body-nowrap {
+  white-space: nowrap;
+}
+
+table.dataTable,
+table.dataTable th,
+table.dataTable td {
+  -webkit-box-sizing: content-box;
+  box-sizing: content-box;
+}
+
+/*
+ * Control feature layout
+ */
+.dataTables_wrapper {
+  position: relative;
+  clear: both;
+  *zoom: 1;
+  zoom: 1;
+}
+.dataTables_wrapper .dataTables_length {
+  float: left;
+}
+.dataTables_wrapper .dataTables_filter {
+  float: right;
+  text-align: right;
+}
+.dataTables_wrapper .dataTables_filter input {
+  margin-left: 0.5em;
+}
+.dataTables_wrapper .dataTables_info {
+  clear: both;
+  float: left;
+  padding-top: 0.755em;
+}
+.dataTables_wrapper .dataTables_paginate {
+  float: right;
+  text-align: right;
+  padding-top: 0.25em;
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button {
+  box-sizing: border-box;
+  display: inline-block;
+  min-width: 1.5em;
+  padding: 0.5em 1em;
+  margin-left: 2px;
+  text-align: center;
+  text-decoration: none !important;
+  cursor: pointer;
+  *cursor: hand;
+  color: #333 !important;
+  border: 1px solid transparent;
+  border-radius: 2px;
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
+  color: #333 !important;
+  border: 1px solid #979797;
+  background-color: white;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc));
+  /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* Chrome10+,Safari5.1+ */
+  background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* FF3.6+ */
+  background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* IE10+ */
+  background: -o-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* Opera 11.10+ */
+  background: linear-gradient(to bottom, white 0%, #dcdcdc 100%);
+  /* W3C */
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active {
+  cursor: default;
+  color: #666 !important;
+  border: 1px solid transparent;
+  background: transparent;
+  box-shadow: none;
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
+  color: white !important;
+  border: 1px solid #111;
+  background-color: #585858;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));
+  /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(top, #585858 0%, #111 100%);
+  /* Chrome10+,Safari5.1+ */
+  background: -moz-linear-gradient(top, #585858 0%, #111 100%);
+  /* FF3.6+ */
+  background: -ms-linear-gradient(top, #585858 0%, #111 100%);
+  /* IE10+ */
+  background: -o-linear-gradient(top, #585858 0%, #111 100%);
+  /* Opera 11.10+ */
+  background: linear-gradient(to bottom, #585858 0%, #111 100%);
+  /* W3C */
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button:active {
+  outline: none;
+  background-color: #2b2b2b;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));
+  /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* Chrome10+,Safari5.1+ */
+  background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* FF3.6+ */
+  background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* IE10+ */
+  background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* Opera 11.10+ */
+  background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);
+  /* W3C */
+  box-shadow: inset 0 0 3px #111;
+}
+.dataTables_wrapper .dataTables_paginate .ellipsis {
+  padding: 0 1em;
+}
+.dataTables_wrapper .dataTables_processing {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 100%;
+  height: 40px;
+  margin-left: -50%;
+  margin-top: -25px;
+  padding-top: 20px;
+  text-align: center;
+  font-size: 1.2em;
+  background-color: white;
+  background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
+  background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+}
+.dataTables_wrapper .dataTables_length,
+.dataTables_wrapper .dataTables_filter,
+.dataTables_wrapper .dataTables_info,
+.dataTables_wrapper .dataTables_processing,
+.dataTables_wrapper .dataTables_paginate {
+  color: #333;
+}
+.dataTables_wrapper .dataTables_scroll {
+  clear: both;
+}
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody {
+  *margin-top: -1px;
+  -webkit-overflow-scrolling: touch;
+}
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td {
+  vertical-align: middle;
+}
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing,
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing,
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing {
+  height: 0;
+  overflow: hidden;
+  margin: 0 !important;
+  padding: 0 !important;
+}
+.dataTables_wrapper.no-footer .dataTables_scrollBody {
+  border-bottom: 1px solid #111;
+}
+.dataTables_wrapper.no-footer div.dataTables_scrollHead > table,
+.dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
+  border-bottom: none;
+}
+.dataTables_wrapper:after {
+  visibility: hidden;
+  display: block;
+  content: "";
+  clear: both;
+  height: 0;
+}
+
+@media screen and (max-width: 767px) {
+  .dataTables_wrapper .dataTables_info,
+  .dataTables_wrapper .dataTables_paginate {
+    float: none;
+    text-align: center;
+  }
+  .dataTables_wrapper .dataTables_paginate {
+    margin-top: 0.5em;
+  }
+}
+@media screen and (max-width: 640px) {
+  .dataTables_wrapper .dataTables_length,
+  .dataTables_wrapper .dataTables_filter {
+    float: none;
+    text-align: center;
+  }
+  .dataTables_wrapper .dataTables_filter {
+    margin-top: 0.5em;
+  }
+}
+table.dataTable thead th div.DataTables_sort_wrapper {
+  position: relative;
+}
+table.dataTable thead th div.DataTables_sort_wrapper span {
+  position: absolute;
+  top: 50%;
+  margin-top: -8px;
+  right: -18px;
+}
+table.dataTable thead th.ui-state-default,
+table.dataTable tfoot th.ui-state-default {
+  border-left-width: 0;
+}
+table.dataTable thead th.ui-state-default:first-child,
+table.dataTable tfoot th.ui-state-default:first-child {
+  border-left-width: 1px;
+}
+
+/*
+ * Control feature layout
+ */
+.dataTables_wrapper .dataTables_paginate .fg-button {
+  box-sizing: border-box;
+  display: inline-block;
+  min-width: 1.5em;
+  padding: 0.5em;
+  margin-left: 2px;
+  text-align: center;
+  text-decoration: none !important;
+  cursor: pointer;
+  *cursor: hand;
+  border: 1px solid transparent;
+}
+.dataTables_wrapper .dataTables_paginate .fg-button:active {
+  outline: none;
+}
+.dataTables_wrapper .dataTables_paginate .fg-button:first-child {
+  border-top-left-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+.dataTables_wrapper .dataTables_paginate .fg-button:last-child {
+  border-top-right-radius: 3px;
+  border-bottom-right-radius: 3px;
+}
+.dataTables_wrapper .ui-widget-header {
+  font-weight: normal;
+}
+.dataTables_wrapper .ui-toolbar {
+  padding: 8px;
+}
+.dataTables_wrapper.no-footer .dataTables_scrollBody {
+  border-bottom: none;
+}
+.dataTables_wrapper .dataTables_length,
+.dataTables_wrapper .dataTables_filter,
+.dataTables_wrapper .dataTables_info,
+.dataTables_wrapper .dataTables_processing,
+.dataTables_wrapper .dataTables_paginate {
+  color: inherit;
+}
diff --git a/static/DataTables/DataTables-1.10.15/css/dataTables.jqueryui.min.css b/static/DataTables/DataTables-1.10.15/css/dataTables.jqueryui.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..fe64abca985b94c33573327415e513ae91c91e8c
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/dataTables.jqueryui.min.css
@@ -0,0 +1 @@
+table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px}table.dataTable tbody tr{background-color:#ffffff}table.dataTable tbody tr.selected{background-color:#B0BED9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{-webkit-box-sizing:content-box;box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead>table,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}}table.dataTable thead th div.DataTables_sort_wrapper{position:relative}table.dataTable thead th div.DataTables_sort_wrapper span{position:absolute;top:50%;margin-top:-8px;right:-18px}table.dataTable thead th.ui-state-default,table.dataTable tfoot th.ui-state-default{border-left-width:0}table.dataTable thead th.ui-state-default:first-child,table.dataTable tfoot th.ui-state-default:first-child{border-left-width:1px}.dataTables_wrapper .dataTables_paginate .fg-button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;border:1px solid transparent}.dataTables_wrapper .dataTables_paginate .fg-button:active{outline:none}.dataTables_wrapper .dataTables_paginate .fg-button:first-child{border-top-left-radius:3px;border-bottom-left-radius:3px}.dataTables_wrapper .dataTables_paginate .fg-button:last-child{border-top-right-radius:3px;border-bottom-right-radius:3px}.dataTables_wrapper .ui-widget-header{font-weight:normal}.dataTables_wrapper .ui-toolbar{padding:8px}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:none}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:inherit}
diff --git a/static/DataTables/DataTables-1.10.15/css/dataTables.semanticui.css b/static/DataTables/DataTables-1.10.15/css/dataTables.semanticui.css
new file mode 100644
index 0000000000000000000000000000000000000000..2583842659a3816c2ae5c9e1d5e6ed41e7dda4c9
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/dataTables.semanticui.css
@@ -0,0 +1,102 @@
+/*
+ * Styling for DataTables with Semantic UI
+ */
+table.dataTable.table {
+  margin: 0;
+}
+table.dataTable.table thead th,
+table.dataTable.table thead td {
+  position: relative;
+}
+table.dataTable.table thead th.sorting, table.dataTable.table thead th.sorting_asc, table.dataTable.table thead th.sorting_desc,
+table.dataTable.table thead td.sorting,
+table.dataTable.table thead td.sorting_asc,
+table.dataTable.table thead td.sorting_desc {
+  padding-right: 20px;
+}
+table.dataTable.table thead th.sorting:after, table.dataTable.table thead th.sorting_asc:after, table.dataTable.table thead th.sorting_desc:after,
+table.dataTable.table thead td.sorting:after,
+table.dataTable.table thead td.sorting_asc:after,
+table.dataTable.table thead td.sorting_desc:after {
+  position: absolute;
+  top: 12px;
+  right: 8px;
+  display: block;
+  font-family: Icons;
+}
+table.dataTable.table thead th.sorting:after,
+table.dataTable.table thead td.sorting:after {
+  content: "\f0dc";
+  color: #ddd;
+  font-size: 0.8em;
+}
+table.dataTable.table thead th.sorting_asc:after,
+table.dataTable.table thead td.sorting_asc:after {
+  content: "\f0de";
+}
+table.dataTable.table thead th.sorting_desc:after,
+table.dataTable.table thead td.sorting_desc:after {
+  content: "\f0dd";
+}
+table.dataTable.table td,
+table.dataTable.table th {
+  -webkit-box-sizing: content-box;
+  box-sizing: content-box;
+}
+table.dataTable.table td.dataTables_empty,
+table.dataTable.table th.dataTables_empty {
+  text-align: center;
+}
+table.dataTable.table.nowrap th,
+table.dataTable.table.nowrap td {
+  white-space: nowrap;
+}
+
+div.dataTables_wrapper div.dataTables_length select {
+  vertical-align: middle;
+  min-height: 2.7142em;
+}
+div.dataTables_wrapper div.dataTables_length .ui.selection.dropdown {
+  min-width: 0;
+}
+div.dataTables_wrapper div.dataTables_filter input {
+  margin-left: 0.5em;
+}
+div.dataTables_wrapper div.dataTables_info {
+  padding-top: 13px;
+  white-space: nowrap;
+}
+div.dataTables_wrapper div.dataTables_processing {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 200px;
+  margin-left: -100px;
+  text-align: center;
+}
+div.dataTables_wrapper div.row.dt-table {
+  padding: 0;
+}
+div.dataTables_wrapper div.dataTables_scrollHead table.dataTable {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+  border-bottom: none;
+}
+div.dataTables_wrapper div.dataTables_scrollBody thead .sorting:after,
+div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_asc:after,
+div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_desc:after {
+  display: none;
+}
+div.dataTables_wrapper div.dataTables_scrollBody table.dataTable {
+  border-radius: 0;
+  border-top: none;
+  border-bottom-width: 0;
+}
+div.dataTables_wrapper div.dataTables_scrollBody table.dataTable.no-footer {
+  border-bottom-width: 1px;
+}
+div.dataTables_wrapper div.dataTables_scrollFoot table.dataTable {
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+  border-top: none;
+}
diff --git a/static/DataTables/DataTables-1.10.15/css/dataTables.semanticui.min.css b/static/DataTables/DataTables-1.10.15/css/dataTables.semanticui.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..c192a34209b42542285e80f0f4d92ef2d490c2a3
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/dataTables.semanticui.min.css
@@ -0,0 +1 @@
+table.dataTable.table{margin:0}table.dataTable.table thead th,table.dataTable.table thead td{position:relative}table.dataTable.table thead th.sorting,table.dataTable.table thead th.sorting_asc,table.dataTable.table thead th.sorting_desc,table.dataTable.table thead td.sorting,table.dataTable.table thead td.sorting_asc,table.dataTable.table thead td.sorting_desc{padding-right:20px}table.dataTable.table thead th.sorting:after,table.dataTable.table thead th.sorting_asc:after,table.dataTable.table thead th.sorting_desc:after,table.dataTable.table thead td.sorting:after,table.dataTable.table thead td.sorting_asc:after,table.dataTable.table thead td.sorting_desc:after{position:absolute;top:12px;right:8px;display:block;font-family:Icons}table.dataTable.table thead th.sorting:after,table.dataTable.table thead td.sorting:after{content:"\f0dc";color:#ddd;font-size:0.8em}table.dataTable.table thead th.sorting_asc:after,table.dataTable.table thead td.sorting_asc:after{content:"\f0de"}table.dataTable.table thead th.sorting_desc:after,table.dataTable.table thead td.sorting_desc:after{content:"\f0dd"}table.dataTable.table td,table.dataTable.table th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable.table td.dataTables_empty,table.dataTable.table th.dataTables_empty{text-align:center}table.dataTable.table.nowrap th,table.dataTable.table.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{vertical-align:middle;min-height:2.7142em}div.dataTables_wrapper div.dataTables_length .ui.selection.dropdown{min-width:0}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em}div.dataTables_wrapper div.dataTables_info{padding-top:13px;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;text-align:center}div.dataTables_wrapper div.row.dt-table{padding:0}div.dataTables_wrapper div.dataTables_scrollHead table.dataTable{border-bottom-right-radius:0;border-bottom-left-radius:0;border-bottom:none}div.dataTables_wrapper div.dataTables_scrollBody thead .sorting:after,div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_asc:after,div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_desc:after{display:none}div.dataTables_wrapper div.dataTables_scrollBody table.dataTable{border-radius:0;border-top:none;border-bottom-width:0}div.dataTables_wrapper div.dataTables_scrollBody table.dataTable.no-footer{border-bottom-width:1px}div.dataTables_wrapper div.dataTables_scrollFoot table.dataTable{border-top-right-radius:0;border-top-left-radius:0;border-top:none}
diff --git a/static/DataTables/DataTables-1.10.15/css/jquery.dataTables.css b/static/DataTables/DataTables-1.10.15/css/jquery.dataTables.css
new file mode 100644
index 0000000000000000000000000000000000000000..6c55f8b6d52287cba6bf238d9b2662f1cf7aa7f5
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/jquery.dataTables.css
@@ -0,0 +1,455 @@
+/*
+ * Table styles
+ */
+table.dataTable {
+  width: 100%;
+  margin: 0 auto;
+  clear: both;
+  border-collapse: separate;
+  border-spacing: 0;
+  /*
+   * Header and footer styles
+   */
+  /*
+   * Body styles
+   */
+}
+table.dataTable thead th,
+table.dataTable tfoot th {
+  font-weight: bold;
+}
+table.dataTable thead th,
+table.dataTable thead td {
+  padding: 10px 18px;
+  border-bottom: 1px solid #111;
+}
+table.dataTable thead th:active,
+table.dataTable thead td:active {
+  outline: none;
+}
+table.dataTable tfoot th,
+table.dataTable tfoot td {
+  padding: 10px 18px 6px 18px;
+  border-top: 1px solid #111;
+}
+table.dataTable thead .sorting,
+table.dataTable thead .sorting_asc,
+table.dataTable thead .sorting_desc,
+table.dataTable thead .sorting_asc_disabled,
+table.dataTable thead .sorting_desc_disabled {
+  cursor: pointer;
+  *cursor: hand;
+}
+table.dataTable thead .sorting,
+table.dataTable thead .sorting_asc,
+table.dataTable thead .sorting_desc,
+table.dataTable thead .sorting_asc_disabled,
+table.dataTable thead .sorting_desc_disabled {
+  background-repeat: no-repeat;
+  background-position: center right;
+}
+table.dataTable thead .sorting {
+  background-image: url("../images/sort_both.png");
+}
+table.dataTable thead .sorting_asc {
+  background-image: url("../images/sort_asc.png");
+}
+table.dataTable thead .sorting_desc {
+  background-image: url("../images/sort_desc.png");
+}
+table.dataTable thead .sorting_asc_disabled {
+  background-image: url("../images/sort_asc_disabled.png");
+}
+table.dataTable thead .sorting_desc_disabled {
+  background-image: url("../images/sort_desc_disabled.png");
+}
+table.dataTable tbody tr {
+  background-color: #ffffff;
+}
+table.dataTable tbody tr.selected {
+  background-color: #B0BED9;
+}
+table.dataTable tbody th,
+table.dataTable tbody td {
+  padding: 8px 10px;
+}
+table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td {
+  border-top: 1px solid #ddd;
+}
+table.dataTable.row-border tbody tr:first-child th,
+table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th,
+table.dataTable.display tbody tr:first-child td {
+  border-top: none;
+}
+table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td {
+  border-top: 1px solid #ddd;
+  border-right: 1px solid #ddd;
+}
+table.dataTable.cell-border tbody tr th:first-child,
+table.dataTable.cell-border tbody tr td:first-child {
+  border-left: 1px solid #ddd;
+}
+table.dataTable.cell-border tbody tr:first-child th,
+table.dataTable.cell-border tbody tr:first-child td {
+  border-top: none;
+}
+table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd {
+  background-color: #f9f9f9;
+}
+table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected {
+  background-color: #acbad4;
+}
+table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover {
+  background-color: #f6f6f6;
+}
+table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected {
+  background-color: #aab7d1;
+}
+table.dataTable.order-column tbody tr > .sorting_1,
+table.dataTable.order-column tbody tr > .sorting_2,
+table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1,
+table.dataTable.display tbody tr > .sorting_2,
+table.dataTable.display tbody tr > .sorting_3 {
+  background-color: #fafafa;
+}
+table.dataTable.order-column tbody tr.selected > .sorting_1,
+table.dataTable.order-column tbody tr.selected > .sorting_2,
+table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1,
+table.dataTable.display tbody tr.selected > .sorting_2,
+table.dataTable.display tbody tr.selected > .sorting_3 {
+  background-color: #acbad5;
+}
+table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
+  background-color: #f1f1f1;
+}
+table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
+  background-color: #f3f3f3;
+}
+table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
+  background-color: whitesmoke;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
+  background-color: #a6b4cd;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
+  background-color: #a8b5cf;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
+  background-color: #a9b7d1;
+}
+table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
+  background-color: #fafafa;
+}
+table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
+  background-color: #fcfcfc;
+}
+table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
+  background-color: #fefefe;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
+  background-color: #acbad5;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
+  background-color: #aebcd6;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
+  background-color: #afbdd8;
+}
+table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
+  background-color: #eaeaea;
+}
+table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
+  background-color: #ececec;
+}
+table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
+  background-color: #efefef;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
+  background-color: #a2aec7;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
+  background-color: #a3b0c9;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
+  background-color: #a5b2cb;
+}
+table.dataTable.no-footer {
+  border-bottom: 1px solid #111;
+}
+table.dataTable.nowrap th, table.dataTable.nowrap td {
+  white-space: nowrap;
+}
+table.dataTable.compact thead th,
+table.dataTable.compact thead td {
+  padding: 4px 17px 4px 4px;
+}
+table.dataTable.compact tfoot th,
+table.dataTable.compact tfoot td {
+  padding: 4px;
+}
+table.dataTable.compact tbody th,
+table.dataTable.compact tbody td {
+  padding: 4px;
+}
+table.dataTable th.dt-left,
+table.dataTable td.dt-left {
+  text-align: left;
+}
+table.dataTable th.dt-center,
+table.dataTable td.dt-center,
+table.dataTable td.dataTables_empty {
+  text-align: center;
+}
+table.dataTable th.dt-right,
+table.dataTable td.dt-right {
+  text-align: right;
+}
+table.dataTable th.dt-justify,
+table.dataTable td.dt-justify {
+  text-align: justify;
+}
+table.dataTable th.dt-nowrap,
+table.dataTable td.dt-nowrap {
+  white-space: nowrap;
+}
+table.dataTable thead th.dt-head-left,
+table.dataTable thead td.dt-head-left,
+table.dataTable tfoot th.dt-head-left,
+table.dataTable tfoot td.dt-head-left {
+  text-align: left;
+}
+table.dataTable thead th.dt-head-center,
+table.dataTable thead td.dt-head-center,
+table.dataTable tfoot th.dt-head-center,
+table.dataTable tfoot td.dt-head-center {
+  text-align: center;
+}
+table.dataTable thead th.dt-head-right,
+table.dataTable thead td.dt-head-right,
+table.dataTable tfoot th.dt-head-right,
+table.dataTable tfoot td.dt-head-right {
+  text-align: right;
+}
+table.dataTable thead th.dt-head-justify,
+table.dataTable thead td.dt-head-justify,
+table.dataTable tfoot th.dt-head-justify,
+table.dataTable tfoot td.dt-head-justify {
+  text-align: justify;
+}
+table.dataTable thead th.dt-head-nowrap,
+table.dataTable thead td.dt-head-nowrap,
+table.dataTable tfoot th.dt-head-nowrap,
+table.dataTable tfoot td.dt-head-nowrap {
+  white-space: nowrap;
+}
+table.dataTable tbody th.dt-body-left,
+table.dataTable tbody td.dt-body-left {
+  text-align: left;
+}
+table.dataTable tbody th.dt-body-center,
+table.dataTable tbody td.dt-body-center {
+  text-align: center;
+}
+table.dataTable tbody th.dt-body-right,
+table.dataTable tbody td.dt-body-right {
+  text-align: right;
+}
+table.dataTable tbody th.dt-body-justify,
+table.dataTable tbody td.dt-body-justify {
+  text-align: justify;
+}
+table.dataTable tbody th.dt-body-nowrap,
+table.dataTable tbody td.dt-body-nowrap {
+  white-space: nowrap;
+}
+
+table.dataTable,
+table.dataTable th,
+table.dataTable td {
+  -webkit-box-sizing: content-box;
+  box-sizing: content-box;
+}
+
+/*
+ * Control feature layout
+ */
+.dataTables_wrapper {
+  position: relative;
+  clear: both;
+  *zoom: 1;
+  zoom: 1;
+}
+.dataTables_wrapper .dataTables_length {
+  float: left;
+}
+.dataTables_wrapper .dataTables_filter {
+  float: right;
+  text-align: right;
+}
+.dataTables_wrapper .dataTables_filter input {
+  margin-left: 0.5em;
+}
+.dataTables_wrapper .dataTables_info {
+  clear: both;
+  float: left;
+  padding-top: 0.755em;
+}
+.dataTables_wrapper .dataTables_paginate {
+  float: right;
+  text-align: right;
+  padding-top: 0.25em;
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button {
+  box-sizing: border-box;
+  display: inline-block;
+  min-width: 1.5em;
+  padding: 0.5em 1em;
+  margin-left: 2px;
+  text-align: center;
+  text-decoration: none !important;
+  cursor: pointer;
+  *cursor: hand;
+  color: #333 !important;
+  border: 1px solid transparent;
+  border-radius: 2px;
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
+  color: #333 !important;
+  border: 1px solid #979797;
+  background-color: white;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc));
+  /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* Chrome10+,Safari5.1+ */
+  background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* FF3.6+ */
+  background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* IE10+ */
+  background: -o-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* Opera 11.10+ */
+  background: linear-gradient(to bottom, white 0%, #dcdcdc 100%);
+  /* W3C */
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active {
+  cursor: default;
+  color: #666 !important;
+  border: 1px solid transparent;
+  background: transparent;
+  box-shadow: none;
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
+  color: white !important;
+  border: 1px solid #111;
+  background-color: #585858;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));
+  /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(top, #585858 0%, #111 100%);
+  /* Chrome10+,Safari5.1+ */
+  background: -moz-linear-gradient(top, #585858 0%, #111 100%);
+  /* FF3.6+ */
+  background: -ms-linear-gradient(top, #585858 0%, #111 100%);
+  /* IE10+ */
+  background: -o-linear-gradient(top, #585858 0%, #111 100%);
+  /* Opera 11.10+ */
+  background: linear-gradient(to bottom, #585858 0%, #111 100%);
+  /* W3C */
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button:active {
+  outline: none;
+  background-color: #2b2b2b;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));
+  /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* Chrome10+,Safari5.1+ */
+  background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* FF3.6+ */
+  background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* IE10+ */
+  background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* Opera 11.10+ */
+  background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);
+  /* W3C */
+  box-shadow: inset 0 0 3px #111;
+}
+.dataTables_wrapper .dataTables_paginate .ellipsis {
+  padding: 0 1em;
+}
+.dataTables_wrapper .dataTables_processing {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 100%;
+  height: 40px;
+  margin-left: -50%;
+  margin-top: -25px;
+  padding-top: 20px;
+  text-align: center;
+  font-size: 1.2em;
+  background-color: white;
+  background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
+  background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+}
+.dataTables_wrapper .dataTables_length,
+.dataTables_wrapper .dataTables_filter,
+.dataTables_wrapper .dataTables_info,
+.dataTables_wrapper .dataTables_processing,
+.dataTables_wrapper .dataTables_paginate {
+  color: #333;
+}
+.dataTables_wrapper .dataTables_scroll {
+  clear: both;
+}
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody {
+  *margin-top: -1px;
+  -webkit-overflow-scrolling: touch;
+}
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td {
+  vertical-align: middle;
+}
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing,
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing,
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing {
+  height: 0;
+  overflow: hidden;
+  margin: 0 !important;
+  padding: 0 !important;
+}
+.dataTables_wrapper.no-footer .dataTables_scrollBody {
+  border-bottom: 1px solid #111;
+}
+.dataTables_wrapper.no-footer div.dataTables_scrollHead > table,
+.dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
+  border-bottom: none;
+}
+.dataTables_wrapper:after {
+  visibility: hidden;
+  display: block;
+  content: "";
+  clear: both;
+  height: 0;
+}
+
+@media screen and (max-width: 767px) {
+  .dataTables_wrapper .dataTables_info,
+  .dataTables_wrapper .dataTables_paginate {
+    float: none;
+    text-align: center;
+  }
+  .dataTables_wrapper .dataTables_paginate {
+    margin-top: 0.5em;
+  }
+}
+@media screen and (max-width: 640px) {
+  .dataTables_wrapper .dataTables_length,
+  .dataTables_wrapper .dataTables_filter {
+    float: none;
+    text-align: center;
+  }
+  .dataTables_wrapper .dataTables_filter {
+    margin-top: 0.5em;
+  }
+}
diff --git a/static/DataTables/DataTables-1.10.15/css/jquery.dataTables.min.css b/static/DataTables/DataTables-1.10.15/css/jquery.dataTables.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..471c29329357ead7691773842f239dbabf4d31ad
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/jquery.dataTables.min.css
@@ -0,0 +1 @@
+table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;*cursor:hand}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("../images/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("../images/sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("../images/sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("../images/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("../images/sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#ffffff}table.dataTable tbody tr.selected{background-color:#B0BED9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{-webkit-box-sizing:content-box;box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead>table,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}}
diff --git a/static/DataTables/DataTables-1.10.15/css/jquery.dataTables_themeroller.css b/static/DataTables/DataTables-1.10.15/css/jquery.dataTables_themeroller.css
new file mode 100644
index 0000000000000000000000000000000000000000..1426a44a18bf40baaf52c108b041b31bc44a1811
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/css/jquery.dataTables_themeroller.css
@@ -0,0 +1,416 @@
+/*
+ * Table styles
+ */
+table.dataTable {
+  width: 100%;
+  margin: 0 auto;
+  clear: both;
+  border-collapse: separate;
+  border-spacing: 0;
+  /*
+   * Header and footer styles
+   */
+  /*
+   * Body styles
+   */
+}
+table.dataTable thead th,
+table.dataTable thead td,
+table.dataTable tfoot th,
+table.dataTable tfoot td {
+  padding: 4px 10px;
+}
+table.dataTable thead th,
+table.dataTable tfoot th {
+  font-weight: bold;
+}
+table.dataTable thead th:active,
+table.dataTable thead td:active {
+  outline: none;
+}
+table.dataTable thead .sorting_asc,
+table.dataTable thead .sorting_desc,
+table.dataTable thead .sorting {
+  cursor: pointer;
+  *cursor: hand;
+}
+table.dataTable thead th div.DataTables_sort_wrapper {
+  position: relative;
+  padding-right: 10px;
+}
+table.dataTable thead th div.DataTables_sort_wrapper span {
+  position: absolute;
+  top: 50%;
+  margin-top: -8px;
+  right: -5px;
+}
+table.dataTable thead th.ui-state-default {
+  border-right-width: 0;
+}
+table.dataTable thead th.ui-state-default:last-child {
+  border-right-width: 1px;
+}
+table.dataTable tbody tr {
+  background-color: #ffffff;
+}
+table.dataTable tbody tr.selected {
+  background-color: #B0BED9;
+}
+table.dataTable tbody th,
+table.dataTable tbody td {
+  padding: 8px 10px;
+}
+table.dataTable th.center,
+table.dataTable td.center,
+table.dataTable td.dataTables_empty {
+  text-align: center;
+}
+table.dataTable th.right,
+table.dataTable td.right {
+  text-align: right;
+}
+table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td {
+  border-top: 1px solid #ddd;
+}
+table.dataTable.row-border tbody tr:first-child th,
+table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th,
+table.dataTable.display tbody tr:first-child td {
+  border-top: none;
+}
+table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td {
+  border-top: 1px solid #ddd;
+  border-right: 1px solid #ddd;
+}
+table.dataTable.cell-border tbody tr th:first-child,
+table.dataTable.cell-border tbody tr td:first-child {
+  border-left: 1px solid #ddd;
+}
+table.dataTable.cell-border tbody tr:first-child th,
+table.dataTable.cell-border tbody tr:first-child td {
+  border-top: none;
+}
+table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd {
+  background-color: #f9f9f9;
+}
+table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected {
+  background-color: #abb9d3;
+}
+table.dataTable.hover tbody tr:hover,
+table.dataTable.hover tbody tr.odd:hover,
+table.dataTable.hover tbody tr.even:hover, table.dataTable.display tbody tr:hover,
+table.dataTable.display tbody tr.odd:hover,
+table.dataTable.display tbody tr.even:hover {
+  background-color: whitesmoke;
+}
+table.dataTable.hover tbody tr:hover.selected,
+table.dataTable.hover tbody tr.odd:hover.selected,
+table.dataTable.hover tbody tr.even:hover.selected, table.dataTable.display tbody tr:hover.selected,
+table.dataTable.display tbody tr.odd:hover.selected,
+table.dataTable.display tbody tr.even:hover.selected {
+  background-color: #a9b7d1;
+}
+table.dataTable.order-column tbody tr > .sorting_1,
+table.dataTable.order-column tbody tr > .sorting_2,
+table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1,
+table.dataTable.display tbody tr > .sorting_2,
+table.dataTable.display tbody tr > .sorting_3 {
+  background-color: #f9f9f9;
+}
+table.dataTable.order-column tbody tr.selected > .sorting_1,
+table.dataTable.order-column tbody tr.selected > .sorting_2,
+table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1,
+table.dataTable.display tbody tr.selected > .sorting_2,
+table.dataTable.display tbody tr.selected > .sorting_3 {
+  background-color: #acbad4;
+}
+table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
+  background-color: #f1f1f1;
+}
+table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
+  background-color: #f3f3f3;
+}
+table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
+  background-color: whitesmoke;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
+  background-color: #a6b3cd;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
+  background-color: #a7b5ce;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
+  background-color: #a9b6d0;
+}
+table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
+  background-color: #f9f9f9;
+}
+table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
+  background-color: #fbfbfb;
+}
+table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
+  background-color: #fdfdfd;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
+  background-color: #acbad4;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
+  background-color: #adbbd6;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
+  background-color: #afbdd8;
+}
+table.dataTable.display tbody tr:hover > .sorting_1,
+table.dataTable.display tbody tr.odd:hover > .sorting_1,
+table.dataTable.display tbody tr.even:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1,
+table.dataTable.order-column.hover tbody tr.odd:hover > .sorting_1,
+table.dataTable.order-column.hover tbody tr.even:hover > .sorting_1 {
+  background-color: #eaeaea;
+}
+table.dataTable.display tbody tr:hover > .sorting_2,
+table.dataTable.display tbody tr.odd:hover > .sorting_2,
+table.dataTable.display tbody tr.even:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2,
+table.dataTable.order-column.hover tbody tr.odd:hover > .sorting_2,
+table.dataTable.order-column.hover tbody tr.even:hover > .sorting_2 {
+  background-color: #ebebeb;
+}
+table.dataTable.display tbody tr:hover > .sorting_3,
+table.dataTable.display tbody tr.odd:hover > .sorting_3,
+table.dataTable.display tbody tr.even:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3,
+table.dataTable.order-column.hover tbody tr.odd:hover > .sorting_3,
+table.dataTable.order-column.hover tbody tr.even:hover > .sorting_3 {
+  background-color: #eeeeee;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_1,
+table.dataTable.display tbody tr.odd:hover.selected > .sorting_1,
+table.dataTable.display tbody tr.even:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1,
+table.dataTable.order-column.hover tbody tr.odd:hover.selected > .sorting_1,
+table.dataTable.order-column.hover tbody tr.even:hover.selected > .sorting_1 {
+  background-color: #a1aec7;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_2,
+table.dataTable.display tbody tr.odd:hover.selected > .sorting_2,
+table.dataTable.display tbody tr.even:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2,
+table.dataTable.order-column.hover tbody tr.odd:hover.selected > .sorting_2,
+table.dataTable.order-column.hover tbody tr.even:hover.selected > .sorting_2 {
+  background-color: #a2afc8;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_3,
+table.dataTable.display tbody tr.odd:hover.selected > .sorting_3,
+table.dataTable.display tbody tr.even:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3,
+table.dataTable.order-column.hover tbody tr.odd:hover.selected > .sorting_3,
+table.dataTable.order-column.hover tbody tr.even:hover.selected > .sorting_3 {
+  background-color: #a4b2cb;
+}
+table.dataTable.nowrap th, table.dataTable.nowrap td {
+  white-space: nowrap;
+}
+table.dataTable.compact thead th,
+table.dataTable.compact thead td {
+  padding: 5px 9px;
+}
+table.dataTable.compact tfoot th,
+table.dataTable.compact tfoot td {
+  padding: 5px 9px 3px 9px;
+}
+table.dataTable.compact tbody th,
+table.dataTable.compact tbody td {
+  padding: 4px 5px;
+}
+table.dataTable th.dt-left,
+table.dataTable td.dt-left {
+  text-align: left;
+}
+table.dataTable th.dt-center,
+table.dataTable td.dt-center,
+table.dataTable td.dataTables_empty {
+  text-align: center;
+}
+table.dataTable th.dt-right,
+table.dataTable td.dt-right {
+  text-align: right;
+}
+table.dataTable th.dt-justify,
+table.dataTable td.dt-justify {
+  text-align: justify;
+}
+table.dataTable th.dt-nowrap,
+table.dataTable td.dt-nowrap {
+  white-space: nowrap;
+}
+table.dataTable thead th.dt-head-left,
+table.dataTable thead td.dt-head-left,
+table.dataTable tfoot th.dt-head-left,
+table.dataTable tfoot td.dt-head-left {
+  text-align: left;
+}
+table.dataTable thead th.dt-head-center,
+table.dataTable thead td.dt-head-center,
+table.dataTable tfoot th.dt-head-center,
+table.dataTable tfoot td.dt-head-center {
+  text-align: center;
+}
+table.dataTable thead th.dt-head-right,
+table.dataTable thead td.dt-head-right,
+table.dataTable tfoot th.dt-head-right,
+table.dataTable tfoot td.dt-head-right {
+  text-align: right;
+}
+table.dataTable thead th.dt-head-justify,
+table.dataTable thead td.dt-head-justify,
+table.dataTable tfoot th.dt-head-justify,
+table.dataTable tfoot td.dt-head-justify {
+  text-align: justify;
+}
+table.dataTable thead th.dt-head-nowrap,
+table.dataTable thead td.dt-head-nowrap,
+table.dataTable tfoot th.dt-head-nowrap,
+table.dataTable tfoot td.dt-head-nowrap {
+  white-space: nowrap;
+}
+table.dataTable tbody th.dt-body-left,
+table.dataTable tbody td.dt-body-left {
+  text-align: left;
+}
+table.dataTable tbody th.dt-body-center,
+table.dataTable tbody td.dt-body-center {
+  text-align: center;
+}
+table.dataTable tbody th.dt-body-right,
+table.dataTable tbody td.dt-body-right {
+  text-align: right;
+}
+table.dataTable tbody th.dt-body-justify,
+table.dataTable tbody td.dt-body-justify {
+  text-align: justify;
+}
+table.dataTable tbody th.dt-body-nowrap,
+table.dataTable tbody td.dt-body-nowrap {
+  white-space: nowrap;
+}
+
+table.dataTable,
+table.dataTable th,
+table.dataTable td {
+  -webkit-box-sizing: content-box;
+  -moz-box-sizing: content-box;
+  box-sizing: content-box;
+}
+
+/*
+ * Control feature layout
+ */
+.dataTables_wrapper {
+  position: relative;
+  clear: both;
+  *zoom: 1;
+  zoom: 1;
+}
+.dataTables_wrapper .dataTables_length {
+  float: left;
+}
+.dataTables_wrapper .dataTables_filter {
+  float: right;
+  text-align: right;
+}
+.dataTables_wrapper .dataTables_filter input {
+  margin-left: 0.5em;
+}
+.dataTables_wrapper .dataTables_info {
+  clear: both;
+  float: left;
+  padding-top: 0.55em;
+}
+.dataTables_wrapper .dataTables_paginate {
+  float: right;
+  text-align: right;
+}
+.dataTables_wrapper .dataTables_paginate .fg-button {
+  box-sizing: border-box;
+  display: inline-block;
+  min-width: 1.5em;
+  padding: 0.5em;
+  margin-left: 2px;
+  text-align: center;
+  text-decoration: none !important;
+  cursor: pointer;
+  *cursor: hand;
+  color: #333 !important;
+  border: 1px solid transparent;
+}
+.dataTables_wrapper .dataTables_paginate .fg-button:active {
+  outline: none;
+}
+.dataTables_wrapper .dataTables_paginate .fg-button:first-child {
+  border-top-left-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+.dataTables_wrapper .dataTables_paginate .fg-button:last-child {
+  border-top-right-radius: 3px;
+  border-bottom-right-radius: 3px;
+}
+.dataTables_wrapper .dataTables_processing {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 100%;
+  height: 40px;
+  margin-left: -50%;
+  margin-top: -25px;
+  padding-top: 20px;
+  text-align: center;
+  font-size: 1.2em;
+  background-color: white;
+  background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
+  /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  /* Chrome10+,Safari5.1+ */
+  background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  /* FF3.6+ */
+  background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  /* IE10+ */
+  background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  /* Opera 11.10+ */
+  background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  /* W3C */
+}
+.dataTables_wrapper .dataTables_length,
+.dataTables_wrapper .dataTables_filter,
+.dataTables_wrapper .dataTables_info,
+.dataTables_wrapper .dataTables_processing,
+.dataTables_wrapper .dataTables_paginate {
+  color: #333;
+}
+.dataTables_wrapper .dataTables_scroll {
+  clear: both;
+}
+.dataTables_wrapper .dataTables_scrollBody {
+  *margin-top: -1px;
+  -webkit-overflow-scrolling: touch;
+}
+.dataTables_wrapper .ui-widget-header {
+  font-weight: normal;
+}
+.dataTables_wrapper .ui-toolbar {
+  padding: 8px;
+}
+.dataTables_wrapper:after {
+  visibility: hidden;
+  display: block;
+  content: "";
+  clear: both;
+  height: 0;
+}
+
+@media screen and (max-width: 767px) {
+  .dataTables_wrapper .dataTables_length,
+  .dataTables_wrapper .dataTables_filter,
+  .dataTables_wrapper .dataTables_info,
+  .dataTables_wrapper .dataTables_paginate {
+    float: none;
+    text-align: center;
+  }
+  .dataTables_wrapper .dataTables_filter,
+  .dataTables_wrapper .dataTables_paginate {
+    margin-top: 0.5em;
+  }
+}
diff --git a/static/DataTables/DataTables-1.10.15/images/sort_asc.png b/static/DataTables/DataTables-1.10.15/images/sort_asc.png
new file mode 100644
index 0000000000000000000000000000000000000000..e1ba61a8055fcb18273f2468d335572204667b1f
Binary files /dev/null and b/static/DataTables/DataTables-1.10.15/images/sort_asc.png differ
diff --git a/static/DataTables/DataTables-1.10.15/images/sort_asc_disabled.png b/static/DataTables/DataTables-1.10.15/images/sort_asc_disabled.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb11dfe24a6c564cb7ddf8bc96703ebb121df1e7
Binary files /dev/null and b/static/DataTables/DataTables-1.10.15/images/sort_asc_disabled.png differ
diff --git a/static/DataTables/DataTables-1.10.15/images/sort_both.png b/static/DataTables/DataTables-1.10.15/images/sort_both.png
new file mode 100644
index 0000000000000000000000000000000000000000..af5bc7c5a10b9d6d57cb641aeec752428a07f0ca
Binary files /dev/null and b/static/DataTables/DataTables-1.10.15/images/sort_both.png differ
diff --git a/static/DataTables/DataTables-1.10.15/images/sort_desc.png b/static/DataTables/DataTables-1.10.15/images/sort_desc.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e156deb5f61d18f9e2ec5da4f6a8c94a5b4fb41
Binary files /dev/null and b/static/DataTables/DataTables-1.10.15/images/sort_desc.png differ
diff --git a/static/DataTables/DataTables-1.10.15/images/sort_desc_disabled.png b/static/DataTables/DataTables-1.10.15/images/sort_desc_disabled.png
new file mode 100644
index 0000000000000000000000000000000000000000..c9fdd8a1502fda301682e907afde86bc450da10f
Binary files /dev/null and b/static/DataTables/DataTables-1.10.15/images/sort_desc_disabled.png differ
diff --git a/static/DataTables/DataTables-1.10.15/js/dataTables.bootstrap.js b/static/DataTables/DataTables-1.10.15/js/dataTables.bootstrap.js
new file mode 100644
index 0000000000000000000000000000000000000000..f69acdc6fb4cf25f1654b4d40a019709806797fa
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/js/dataTables.bootstrap.js
@@ -0,0 +1,182 @@
+/*! DataTables Bootstrap 3 integration
+ * ©2011-2015 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * DataTables integration for Bootstrap 3. This requires Bootstrap 3 and
+ * DataTables 1.10 or newer.
+ *
+ * This file sets the defaults and adds options to DataTables to style its
+ * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap
+ * for further information.
+ */
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				// Require DataTables, which attaches to jQuery, including
+				// jQuery if needed and have a $ property so we can access the
+				// jQuery object that is used
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+/* Set the defaults for DataTables initialisation */
+$.extend( true, DataTable.defaults, {
+	dom:
+		"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
+		"<'row'<'col-sm-12'tr>>" +
+		"<'row'<'col-sm-5'i><'col-sm-7'p>>",
+	renderer: 'bootstrap'
+} );
+
+
+/* Default class modification */
+$.extend( DataTable.ext.classes, {
+	sWrapper:      "dataTables_wrapper form-inline dt-bootstrap",
+	sFilterInput:  "form-control input-sm",
+	sLengthSelect: "form-control input-sm",
+	sProcessing:   "dataTables_processing panel panel-default"
+} );
+
+
+/* Bootstrap paging button renderer */
+DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) {
+	var api     = new DataTable.Api( settings );
+	var classes = settings.oClasses;
+	var lang    = settings.oLanguage.oPaginate;
+	var aria = settings.oLanguage.oAria.paginate || {};
+	var btnDisplay, btnClass, counter=0;
+
+	var attach = function( container, buttons ) {
+		var i, ien, node, button;
+		var clickHandler = function ( e ) {
+			e.preventDefault();
+			if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) {
+				api.page( e.data.action ).draw( 'page' );
+			}
+		};
+
+		for ( i=0, ien=buttons.length ; i<ien ; i++ ) {
+			button = buttons[i];
+
+			if ( $.isArray( button ) ) {
+				attach( container, button );
+			}
+			else {
+				btnDisplay = '';
+				btnClass = '';
+
+				switch ( button ) {
+					case 'ellipsis':
+						btnDisplay = '&#x2026;';
+						btnClass = 'disabled';
+						break;
+
+					case 'first':
+						btnDisplay = lang.sFirst;
+						btnClass = button + (page > 0 ?
+							'' : ' disabled');
+						break;
+
+					case 'previous':
+						btnDisplay = lang.sPrevious;
+						btnClass = button + (page > 0 ?
+							'' : ' disabled');
+						break;
+
+					case 'next':
+						btnDisplay = lang.sNext;
+						btnClass = button + (page < pages-1 ?
+							'' : ' disabled');
+						break;
+
+					case 'last':
+						btnDisplay = lang.sLast;
+						btnClass = button + (page < pages-1 ?
+							'' : ' disabled');
+						break;
+
+					default:
+						btnDisplay = button + 1;
+						btnClass = page === button ?
+							'active' : '';
+						break;
+				}
+
+				if ( btnDisplay ) {
+					node = $('<li>', {
+							'class': classes.sPageButton+' '+btnClass,
+							'id': idx === 0 && typeof button === 'string' ?
+								settings.sTableId +'_'+ button :
+								null
+						} )
+						.append( $('<a>', {
+								'href': '#',
+								'aria-controls': settings.sTableId,
+								'aria-label': aria[ button ],
+								'data-dt-idx': counter,
+								'tabindex': settings.iTabIndex
+							} )
+							.html( btnDisplay )
+						)
+						.appendTo( container );
+
+					settings.oApi._fnBindAction(
+						node, {action: button}, clickHandler
+					);
+
+					counter++;
+				}
+			}
+		}
+	};
+
+	// IE9 throws an 'unknown error' if document.activeElement is used
+	// inside an iframe or frame. 
+	var activeEl;
+
+	try {
+		// Because this approach is destroying and recreating the paging
+		// elements, focus is lost on the select button which is bad for
+		// accessibility. So we want to restore focus once the draw has
+		// completed
+		activeEl = $(host).find(document.activeElement).data('dt-idx');
+	}
+	catch (e) {}
+
+	attach(
+		$(host).empty().html('<ul class="pagination"/>').children('ul'),
+		buttons
+	);
+
+	if ( activeEl !== undefined ) {
+		$(host).find( '[data-dt-idx='+activeEl+']' ).focus();
+	}
+};
+
+
+return DataTable;
+}));
diff --git a/static/DataTables/DataTables-1.10.15/js/dataTables.bootstrap.min.js b/static/DataTables/DataTables-1.10.15/js/dataTables.bootstrap.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..98661c6c73792a5e0613bfcd4566ecbd48ac89d8
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/js/dataTables.bootstrap.min.js
@@ -0,0 +1,8 @@
+/*!
+ DataTables Bootstrap 3 integration
+ ©2011-2015 SpryMedia Ltd - datatables.net/license
+*/
+(function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d,m){var f=b.fn.dataTable;b.extend(!0,f.defaults,{dom:"<'row'<'col-sm-6'l><'col-sm-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-5'i><'col-sm-7'p>>",renderer:"bootstrap"});b.extend(f.ext.classes,
+{sWrapper:"dataTables_wrapper form-inline dt-bootstrap",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm",sProcessing:"dataTables_processing panel panel-default"});f.ext.renderer.pageButton.bootstrap=function(a,h,r,s,j,n){var o=new f.Api(a),t=a.oClasses,k=a.oLanguage.oPaginate,u=a.oLanguage.oAria.paginate||{},e,g,p=0,q=function(d,f){var l,h,i,c,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")};
+l=0;for(h=f.length;l<h;l++)if(c=f[l],b.isArray(c))q(d,c);else{g=e="";switch(c){case "ellipsis":e="&#x2026;";g="disabled";break;case "first":e=k.sFirst;g=c+(0<j?"":" disabled");break;case "previous":e=k.sPrevious;g=c+(0<j?"":" disabled");break;case "next":e=k.sNext;g=c+(j<n-1?"":" disabled");break;case "last":e=k.sLast;g=c+(j<n-1?"":" disabled");break;default:e=c+1,g=j===c?"active":""}e&&(i=b("<li>",{"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("<a>",{href:"#",
+"aria-controls":a.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:a.iTabIndex}).html(e)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(d.activeElement).data("dt-idx")}catch(v){}q(b(h).empty().html('<ul class="pagination"/>').children("ul"),s);i!==m&&b(h).find("[data-dt-idx="+i+"]").focus()};return f});
diff --git a/static/DataTables/DataTables-1.10.15/js/dataTables.foundation.js b/static/DataTables/DataTables-1.10.15/js/dataTables.foundation.js
new file mode 100644
index 0000000000000000000000000000000000000000..4d3bb45eb7cc02ce4785c5b699f51b86e7a626c7
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/js/dataTables.foundation.js
@@ -0,0 +1,174 @@
+/*! DataTables Foundation integration
+ * ©2011-2015 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * DataTables integration for Foundation. This requires Foundation 5 and
+ * DataTables 1.10 or newer.
+ *
+ * This file sets the defaults and adds options to DataTables to style its
+ * controls using Foundation. See http://datatables.net/manual/styling/foundation
+ * for further information.
+ */
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+// Detect Foundation 5 / 6 as they have different element and class requirements
+var meta = $('<meta class="foundation-mq"/>').appendTo('head');
+DataTable.ext.foundationVersion = meta.css('font-family').match(/small|medium|large/) ? 6 : 5;
+meta.remove();
+
+
+$.extend( DataTable.ext.classes, {
+	sWrapper:    "dataTables_wrapper dt-foundation",
+	sProcessing: "dataTables_processing panel callout"
+} );
+
+
+/* Set the defaults for DataTables initialisation */
+$.extend( true, DataTable.defaults, {
+	dom:
+		"<'row'<'small-6 columns'l><'small-6 columns'f>r>"+
+		"t"+
+		"<'row'<'small-6 columns'i><'small-6 columns'p>>",
+	renderer: 'foundation'
+} );
+
+
+/* Page button renderer */
+DataTable.ext.renderer.pageButton.foundation = function ( settings, host, idx, buttons, page, pages ) {
+	var api = new DataTable.Api( settings );
+	var classes = settings.oClasses;
+	var lang = settings.oLanguage.oPaginate;
+	var aria = settings.oLanguage.oAria.paginate || {};
+	var btnDisplay, btnClass;
+	var tag;
+	var v5 = DataTable.ext.foundationVersion === 5;
+
+	var attach = function( container, buttons ) {
+		var i, ien, node, button;
+		var clickHandler = function ( e ) {
+			e.preventDefault();
+			if ( !$(e.currentTarget).hasClass('unavailable') && api.page() != e.data.action ) {
+				api.page( e.data.action ).draw( 'page' );
+			}
+		};
+
+		for ( i=0, ien=buttons.length ; i<ien ; i++ ) {
+			button = buttons[i];
+
+			if ( $.isArray( button ) ) {
+				attach( container, button );
+			}
+			else {
+				btnDisplay = '';
+				btnClass = '';
+				tag = null;
+
+				switch ( button ) {
+					case 'ellipsis':
+						btnDisplay = '&#x2026;';
+						btnClass = 'unavailable disabled';
+						tag = null;
+						break;
+
+					case 'first':
+						btnDisplay = lang.sFirst;
+						btnClass = button + (page > 0 ?
+							'' : ' unavailable disabled');
+						tag = page > 0 ? 'a' : null;
+						break;
+
+					case 'previous':
+						btnDisplay = lang.sPrevious;
+						btnClass = button + (page > 0 ?
+							'' : ' unavailable disabled');
+						tag = page > 0 ? 'a' : null;
+						break;
+
+					case 'next':
+						btnDisplay = lang.sNext;
+						btnClass = button + (page < pages-1 ?
+							'' : ' unavailable disabled');
+						tag = page < pages-1 ? 'a' : null;
+						break;
+
+					case 'last':
+						btnDisplay = lang.sLast;
+						btnClass = button + (page < pages-1 ?
+							'' : ' unavailable disabled');
+						tag = page < pages-1 ? 'a' : null;
+						break;
+
+					default:
+						btnDisplay = button + 1;
+						btnClass = page === button ?
+							'current' : '';
+						tag = page === button ?
+							null : 'a';
+						break;
+				}
+
+				if ( v5 ) {
+					tag = 'a';
+				}
+
+				if ( btnDisplay ) {
+					node = $('<li>', {
+							'class': classes.sPageButton+' '+btnClass,
+							'aria-controls': settings.sTableId,
+							'aria-label': aria[ button ],
+							'tabindex': settings.iTabIndex,
+							'id': idx === 0 && typeof button === 'string' ?
+								settings.sTableId +'_'+ button :
+								null
+						} )
+						.append( tag ?
+							$('<'+tag+'/>', {'href': '#'} ).html( btnDisplay ) :
+							btnDisplay
+						)
+						.appendTo( container );
+
+					settings.oApi._fnBindAction(
+						node, {action: button}, clickHandler
+					);
+				}
+			}
+		}
+	};
+
+	attach(
+		$(host).empty().html('<ul class="pagination"/>').children('ul'),
+		buttons
+	);
+};
+
+
+return DataTable;
+}));
diff --git a/static/DataTables/DataTables-1.10.15/js/dataTables.foundation.min.js b/static/DataTables/DataTables-1.10.15/js/dataTables.foundation.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..d47166b4e2b881eb1583fef66768844233594e27
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/js/dataTables.foundation.min.js
@@ -0,0 +1,8 @@
+/*!
+ DataTables Foundation integration
+ ©2011-2015 SpryMedia Ltd - datatables.net/license
+*/
+(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return d(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);if(!b||!b.fn.dataTable)b=require("datatables.net")(a,b).$;return d(b,a,a.document)}:d(jQuery,window,document)})(function(d){var a=d.fn.dataTable,b=d('<meta class="foundation-mq"/>').appendTo("head");a.ext.foundationVersion=b.css("font-family").match(/small|medium|large/)?6:5;b.remove();d.extend(a.ext.classes,
+{sWrapper:"dataTables_wrapper dt-foundation",sProcessing:"dataTables_processing panel callout"});d.extend(!0,a.defaults,{dom:"<'row'<'small-6 columns'l><'small-6 columns'f>r>t<'row'<'small-6 columns'i><'small-6 columns'p>>",renderer:"foundation"});a.ext.renderer.pageButton.foundation=function(b,l,r,s,e,i){var m=new a.Api(b),t=b.oClasses,j=b.oLanguage.oPaginate,u=b.oLanguage.oAria.paginate||{},f,h,g,v=5===a.ext.foundationVersion,q=function(a,n){var k,o,p,c,l=function(a){a.preventDefault();!d(a.currentTarget).hasClass("unavailable")&&
+m.page()!=a.data.action&&m.page(a.data.action).draw("page")};k=0;for(o=n.length;k<o;k++)if(c=n[k],d.isArray(c))q(a,c);else{h=f="";g=null;switch(c){case "ellipsis":f="&#x2026;";h="unavailable disabled";g=null;break;case "first":f=j.sFirst;h=c+(0<e?"":" unavailable disabled");g=0<e?"a":null;break;case "previous":f=j.sPrevious;h=c+(0<e?"":" unavailable disabled");g=0<e?"a":null;break;case "next":f=j.sNext;h=c+(e<i-1?"":" unavailable disabled");g=e<i-1?"a":null;break;case "last":f=j.sLast;h=c+(e<i-1?
+"":" unavailable disabled");g=e<i-1?"a":null;break;default:f=c+1,h=e===c?"current":"",g=e===c?null:"a"}v&&(g="a");f&&(p=d("<li>",{"class":t.sPageButton+" "+h,"aria-controls":b.sTableId,"aria-label":u[c],tabindex:b.iTabIndex,id:0===r&&"string"===typeof c?b.sTableId+"_"+c:null}).append(g?d("<"+g+"/>",{href:"#"}).html(f):f).appendTo(a),b.oApi._fnBindAction(p,{action:c},l))}};q(d(l).empty().html('<ul class="pagination"/>').children("ul"),s)};return a});
diff --git a/static/DataTables/DataTables-1.10.15/js/dataTables.jqueryui.js b/static/DataTables/DataTables-1.10.15/js/dataTables.jqueryui.js
new file mode 100644
index 0000000000000000000000000000000000000000..64d4325239292d8db2c3e36f228385a344efc5dd
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/js/dataTables.jqueryui.js
@@ -0,0 +1,164 @@
+/*! DataTables jQuery UI integration
+ * ©2011-2014 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * DataTables integration for jQuery UI. This requires jQuery UI and
+ * DataTables 1.10 or newer.
+ *
+ * This file sets the defaults and adds options to DataTables to style its
+ * controls using jQuery UI. See http://datatables.net/manual/styling/jqueryui
+ * for further information.
+ */
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+var sort_prefix = 'css_right ui-icon ui-icon-';
+var toolbar_prefix = 'fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix ui-corner-';
+
+/* Set the defaults for DataTables initialisation */
+$.extend( true, DataTable.defaults, {
+	dom:
+		'<"'+toolbar_prefix+'tl ui-corner-tr"lfr>'+
+		't'+
+		'<"'+toolbar_prefix+'bl ui-corner-br"ip>',
+	renderer: 'jqueryui'
+} );
+
+
+$.extend( DataTable.ext.classes, {
+	"sWrapper":            "dataTables_wrapper dt-jqueryui",
+
+	/* Full numbers paging buttons */
+	"sPageButton":         "fg-button ui-button ui-state-default",
+	"sPageButtonActive":   "ui-state-disabled",
+	"sPageButtonDisabled": "ui-state-disabled",
+
+	/* Features */
+	"sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+
+		"ui-buttonset-multi paging_", /* Note that the type is postfixed */
+
+	/* Sorting */
+	"sSortAsc":            "ui-state-default sorting_asc",
+	"sSortDesc":           "ui-state-default sorting_desc",
+	"sSortable":           "ui-state-default sorting",
+	"sSortableAsc":        "ui-state-default sorting_asc_disabled",
+	"sSortableDesc":       "ui-state-default sorting_desc_disabled",
+	"sSortableNone":       "ui-state-default sorting_disabled",
+	"sSortIcon":           "DataTables_sort_icon",
+
+	/* Scrolling */
+	"sScrollHead": "dataTables_scrollHead "+"ui-state-default",
+	"sScrollFoot": "dataTables_scrollFoot "+"ui-state-default",
+
+	/* Misc */
+	"sHeaderTH":  "ui-state-default",
+	"sFooterTH":  "ui-state-default"
+} );
+
+
+DataTable.ext.renderer.header.jqueryui = function ( settings, cell, column, classes ) {
+	// Calculate what the unsorted class should be
+	var noSortAppliedClass = sort_prefix+'carat-2-n-s';
+	var asc = $.inArray('asc', column.asSorting) !== -1;
+	var desc = $.inArray('desc', column.asSorting) !== -1;
+
+	if ( !column.bSortable || (!asc && !desc) ) {
+		noSortAppliedClass = '';
+	}
+	else if ( asc && !desc ) {
+		noSortAppliedClass = sort_prefix+'carat-1-n';
+	}
+	else if ( !asc && desc ) {
+		noSortAppliedClass = sort_prefix+'carat-1-s';
+	}
+
+	// Setup the DOM structure
+	$('<div/>')
+		.addClass( 'DataTables_sort_wrapper' )
+		.append( cell.contents() )
+		.append( $('<span/>')
+			.addClass( classes.sSortIcon+' '+noSortAppliedClass )
+		)
+		.appendTo( cell );
+
+	// Attach a sort listener to update on sort
+	$(settings.nTable).on( 'order.dt', function ( e, ctx, sorting, columns ) {
+		if ( settings !== ctx ) {
+			return;
+		}
+
+		var colIdx = column.idx;
+
+		cell
+			.removeClass( classes.sSortAsc +" "+classes.sSortDesc )
+			.addClass( columns[ colIdx ] == 'asc' ?
+				classes.sSortAsc : columns[ colIdx ] == 'desc' ?
+					classes.sSortDesc :
+					column.sSortingClass
+			);
+
+		cell
+			.find( 'span.'+classes.sSortIcon )
+			.removeClass(
+				sort_prefix+'triangle-1-n' +" "+
+				sort_prefix+'triangle-1-s' +" "+
+				sort_prefix+'carat-2-n-s' +" "+
+				sort_prefix+'carat-1-n' +" "+
+				sort_prefix+'carat-1-s'
+			)
+			.addClass( columns[ colIdx ] == 'asc' ?
+				sort_prefix+'triangle-1-n' : columns[ colIdx ] == 'desc' ?
+					sort_prefix+'triangle-1-s' :
+					noSortAppliedClass
+			);
+	} );
+};
+
+
+/*
+ * TableTools jQuery UI compatibility
+ * Required TableTools 2.1+
+ */
+if ( DataTable.TableTools ) {
+	$.extend( true, DataTable.TableTools.classes, {
+		"container": "DTTT_container ui-buttonset ui-buttonset-multi",
+		"buttons": {
+			"normal": "DTTT_button ui-button ui-state-default"
+		},
+		"collection": {
+			"container": "DTTT_collection ui-buttonset ui-buttonset-multi"
+		}
+	} );
+}
+
+
+return DataTable;
+}));
diff --git a/static/DataTables/DataTables-1.10.15/js/dataTables.jqueryui.min.js b/static/DataTables/DataTables-1.10.15/js/dataTables.jqueryui.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..782d10fda665109bf07c75b6d14421a7c1252966
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/js/dataTables.jqueryui.min.js
@@ -0,0 +1,9 @@
+/*!
+ DataTables jQuery UI integration
+ ©2011-2014 SpryMedia Ltd - datatables.net/license
+*/
+(function(a){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(b){return a(b,window,document)}):"object"===typeof exports?module.exports=function(b,d){b||(b=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(b,d).$;return a(d,b,b.document)}:a(jQuery,window,document)})(function(a){var b=a.fn.dataTable;a.extend(!0,b.defaults,{dom:'<"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix ui-corner-tl ui-corner-tr"lfr>t<"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix ui-corner-bl ui-corner-br"ip>',
+renderer:"jqueryui"});a.extend(b.ext.classes,{sWrapper:"dataTables_wrapper dt-jqueryui",sPageButton:"fg-button ui-button ui-state-default",sPageButtonActive:"ui-state-disabled",sPageButtonDisabled:"ui-state-disabled",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",sSortAsc:"ui-state-default sorting_asc",sSortDesc:"ui-state-default sorting_desc",sSortable:"ui-state-default sorting",sSortableAsc:"ui-state-default sorting_asc_disabled",sSortableDesc:"ui-state-default sorting_desc_disabled",
+sSortableNone:"ui-state-default sorting_disabled",sSortIcon:"DataTables_sort_icon",sScrollHead:"dataTables_scrollHead ui-state-default",sScrollFoot:"dataTables_scrollFoot ui-state-default",sHeaderTH:"ui-state-default",sFooterTH:"ui-state-default"});b.ext.renderer.header.jqueryui=function(b,h,e,c){var f="css_right ui-icon ui-icon-carat-2-n-s",g=-1!==a.inArray("asc",e.asSorting),i=-1!==a.inArray("desc",e.asSorting);!e.bSortable||!g&&!i?f="":g&&!i?f="css_right ui-icon ui-icon-carat-1-n":!g&&i&&(f="css_right ui-icon ui-icon-carat-1-s");
+a("<div/>").addClass("DataTables_sort_wrapper").append(h.contents()).append(a("<span/>").addClass(c.sSortIcon+" "+f)).appendTo(h);a(b.nTable).on("order.dt",function(a,g,i,j){b===g&&(a=e.idx,h.removeClass(c.sSortAsc+" "+c.sSortDesc).addClass("asc"==j[a]?c.sSortAsc:"desc"==j[a]?c.sSortDesc:e.sSortingClass),h.find("span."+c.sSortIcon).removeClass("css_right ui-icon ui-icon-triangle-1-n css_right ui-icon ui-icon-triangle-1-s css_right ui-icon ui-icon-carat-2-n-s css_right ui-icon ui-icon-carat-1-n css_right ui-icon ui-icon-carat-1-s").addClass("asc"==
+j[a]?"css_right ui-icon ui-icon-triangle-1-n":"desc"==j[a]?"css_right ui-icon ui-icon-triangle-1-s":f))})};b.TableTools&&a.extend(!0,b.TableTools.classes,{container:"DTTT_container ui-buttonset ui-buttonset-multi",buttons:{normal:"DTTT_button ui-button ui-state-default"},collection:{container:"DTTT_collection ui-buttonset ui-buttonset-multi"}});return b});
diff --git a/static/DataTables/DataTables-1.10.15/js/dataTables.semanticui.js b/static/DataTables/DataTables-1.10.15/js/dataTables.semanticui.js
new file mode 100644
index 0000000000000000000000000000000000000000..c19ffe06cdf029914a5138fffdb6ac32fc4da838
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/js/dataTables.semanticui.js
@@ -0,0 +1,208 @@
+/*! DataTables Bootstrap 3 integration
+ * ©2011-2015 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * DataTables integration for Bootstrap 3. This requires Bootstrap 3 and
+ * DataTables 1.10 or newer.
+ *
+ * This file sets the defaults and adds options to DataTables to style its
+ * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap
+ * for further information.
+ */
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				// Require DataTables, which attaches to jQuery, including
+				// jQuery if needed and have a $ property so we can access the
+				// jQuery object that is used
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+/* Set the defaults for DataTables initialisation */
+$.extend( true, DataTable.defaults, {
+	dom:
+		"<'ui stackable grid'"+
+			"<'row'"+
+				"<'eight wide column'l>"+
+				"<'right aligned eight wide column'f>"+
+			">"+
+			"<'row dt-table'"+
+				"<'sixteen wide column'tr>"+
+			">"+
+			"<'row'"+
+				"<'seven wide column'i>"+
+				"<'right aligned nine wide column'p>"+
+			">"+
+		">",
+	renderer: 'semanticUI'
+} );
+
+
+/* Default class modification */
+$.extend( DataTable.ext.classes, {
+	sWrapper:      "dataTables_wrapper dt-semanticUI",
+	sFilter:       "dataTables_filter ui input",
+	sProcessing:   "dataTables_processing ui segment",
+	sPageButton:   "paginate_button item"
+} );
+
+
+/* Bootstrap paging button renderer */
+DataTable.ext.renderer.pageButton.semanticUI = function ( settings, host, idx, buttons, page, pages ) {
+	var api     = new DataTable.Api( settings );
+	var classes = settings.oClasses;
+	var lang    = settings.oLanguage.oPaginate;
+	var aria = settings.oLanguage.oAria.paginate || {};
+	var btnDisplay, btnClass, counter=0;
+
+	var attach = function( container, buttons ) {
+		var i, ien, node, button;
+		var clickHandler = function ( e ) {
+			e.preventDefault();
+			if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) {
+				api.page( e.data.action ).draw( 'page' );
+			}
+		};
+
+		for ( i=0, ien=buttons.length ; i<ien ; i++ ) {
+			button = buttons[i];
+
+			if ( $.isArray( button ) ) {
+				attach( container, button );
+			}
+			else {
+				btnDisplay = '';
+				btnClass = '';
+
+				switch ( button ) {
+					case 'ellipsis':
+						btnDisplay = '&#x2026;';
+						btnClass = 'disabled';
+						break;
+
+					case 'first':
+						btnDisplay = lang.sFirst;
+						btnClass = button + (page > 0 ?
+							'' : ' disabled');
+						break;
+
+					case 'previous':
+						btnDisplay = lang.sPrevious;
+						btnClass = button + (page > 0 ?
+							'' : ' disabled');
+						break;
+
+					case 'next':
+						btnDisplay = lang.sNext;
+						btnClass = button + (page < pages-1 ?
+							'' : ' disabled');
+						break;
+
+					case 'last':
+						btnDisplay = lang.sLast;
+						btnClass = button + (page < pages-1 ?
+							'' : ' disabled');
+						break;
+
+					default:
+						btnDisplay = button + 1;
+						btnClass = page === button ?
+							'active' : '';
+						break;
+				}
+
+				var tag = btnClass.indexOf( 'disabled' ) === -1 ?
+					'a' :
+					'div';
+
+				if ( btnDisplay ) {
+					node = $('<'+tag+'>', {
+							'class': classes.sPageButton+' '+btnClass,
+							'id': idx === 0 && typeof button === 'string' ?
+								settings.sTableId +'_'+ button :
+								null,
+							'href': '#',
+							'aria-controls': settings.sTableId,
+							'aria-label': aria[ button ],
+							'data-dt-idx': counter,
+							'tabindex': settings.iTabIndex
+						} )
+						.html( btnDisplay )
+						.appendTo( container );
+
+					settings.oApi._fnBindAction(
+						node, {action: button}, clickHandler
+					);
+
+					counter++;
+				}
+			}
+		}
+	};
+
+	// IE9 throws an 'unknown error' if document.activeElement is used
+	// inside an iframe or frame. 
+	var activeEl;
+
+	try {
+		// Because this approach is destroying and recreating the paging
+		// elements, focus is lost on the select button which is bad for
+		// accessibility. So we want to restore focus once the draw has
+		// completed
+		activeEl = $(host).find(document.activeElement).data('dt-idx');
+	}
+	catch (e) {}
+
+	attach(
+		$(host).empty().html('<div class="ui stackable pagination menu"/>').children(),
+		buttons
+	);
+
+	if ( activeEl !== undefined ) {
+		$(host).find( '[data-dt-idx='+activeEl+']' ).focus();
+	}
+};
+
+
+// Javascript enhancements on table initialisation
+$(document).on( 'init.dt', function (e, ctx) {
+	if ( e.namespace !== 'dt' ) {
+		return;
+	}
+
+	// Length menu drop down
+	if ( $.fn.dropdown ) {
+		var api = new $.fn.dataTable.Api( ctx );
+
+		$( 'div.dataTables_length select', api.table().container() ).dropdown();
+	}
+} );
+
+
+return DataTable;
+}));
\ No newline at end of file
diff --git a/static/DataTables/DataTables-1.10.15/js/dataTables.semanticui.min.js b/static/DataTables/DataTables-1.10.15/js/dataTables.semanticui.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..3b2d3d90891b7bb54a6f5ec8010677b00453da46
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/js/dataTables.semanticui.min.js
@@ -0,0 +1,9 @@
+/*!
+ DataTables Bootstrap 3 integration
+ ©2011-2015 SpryMedia Ltd - datatables.net/license
+*/
+(function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d,m){var e=b.fn.dataTable;b.extend(!0,e.defaults,{dom:"<'ui stackable grid'<'row'<'eight wide column'l><'right aligned eight wide column'f>><'row dt-table'<'sixteen wide column'tr>><'row'<'seven wide column'i><'right aligned nine wide column'p>>>",
+renderer:"semanticUI"});b.extend(e.ext.classes,{sWrapper:"dataTables_wrapper dt-semanticUI",sFilter:"dataTables_filter ui input",sProcessing:"dataTables_processing ui segment",sPageButton:"paginate_button item"});e.ext.renderer.pageButton.semanticUI=function(h,a,r,s,j,n){var o=new e.Api(h),t=h.oClasses,k=h.oLanguage.oPaginate,u=h.oLanguage.oAria.paginate||{},f,g,p=0,q=function(a,d){var e,i,l,c,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")};
+e=0;for(i=d.length;e<i;e++)if(c=d[e],b.isArray(c))q(a,c);else{g=f="";switch(c){case "ellipsis":f="&#x2026;";g="disabled";break;case "first":f=k.sFirst;g=c+(0<j?"":" disabled");break;case "previous":f=k.sPrevious;g=c+(0<j?"":" disabled");break;case "next":f=k.sNext;g=c+(j<n-1?"":" disabled");break;case "last":f=k.sLast;g=c+(j<n-1?"":" disabled");break;default:f=c+1,g=j===c?"active":""}l=-1===g.indexOf("disabled")?"a":"div";f&&(l=b("<"+l+">",{"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c?
+h.sTableId+"_"+c:null,href:"#","aria-controls":h.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:h.iTabIndex}).html(f).appendTo(a),h.oApi._fnBindAction(l,{action:c},m),p++)}},i;try{i=b(a).find(d.activeElement).data("dt-idx")}catch(v){}q(b(a).empty().html('<div class="ui stackable pagination menu"/>').children(),s);i!==m&&b(a).find("[data-dt-idx="+i+"]").focus()};b(d).on("init.dt",function(a,d){if("dt"===a.namespace&&b.fn.dropdown){var e=new b.fn.dataTable.Api(d);b("div.dataTables_length select",
+e.table().container()).dropdown()}});return e});
diff --git a/static/DataTables/DataTables-1.10.15/js/jquery.dataTables.js b/static/DataTables/DataTables-1.10.15/js/jquery.dataTables.js
new file mode 100644
index 0000000000000000000000000000000000000000..e749018a657c64e4e28592c11a5331390218c911
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/js/jquery.dataTables.js
@@ -0,0 +1,15345 @@
+/*! DataTables 1.10.15
+ * ©2008-2017 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary     DataTables
+ * @description Paginate, search and order HTML tables
+ * @version     1.10.15
+ * @file        jquery.dataTables.js
+ * @author      SpryMedia Ltd
+ * @contact     www.datatables.net
+ * @copyright   Copyright 2008-2017 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ *   MIT license - http://datatables.net/license
+ *
+ * This source file 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 license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+
+/*jslint evil: true, undef: true, browser: true */
+/*globals $,require,jQuery,define,_selector_run,_selector_opts,_selector_first,_selector_row_indexes,_ext,_Api,_api_register,_api_registerPlural,_re_new_lines,_re_html,_re_formatted_numeric,_re_escape_regex,_empty,_intVal,_numToDecimal,_isNumber,_isHtml,_htmlNumeric,_pluck,_pluck_order,_range,_stripHtml,_unique,_fnBuildAjax,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnAjaxDataSrc,_fnAddColumn,_fnColumnOptions,_fnAdjustColumnSizing,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnVisbleColumns,_fnGetColumns,_fnColumnTypes,_fnApplyColumnDefs,_fnHungarianMap,_fnCamelToHungarian,_fnLanguageCompat,_fnBrowserDetect,_fnAddData,_fnAddTr,_fnNodeToDataIndex,_fnNodeToColumnIndex,_fnGetCellData,_fnSetCellData,_fnSplitObjNotation,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnGetDataMaster,_fnClearTable,_fnDeleteIndex,_fnInvalidate,_fnGetRowElements,_fnCreateTr,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAddOptionsHtml,_fnDetectHeader,_fnGetUniqueThs,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnFilterCreateSearch,_fnEscapeRegex,_fnFilterData,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnInfoMacros,_fnInitialise,_fnInitComplete,_fnLengthChange,_fnFeatureHtmlLength,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnFeatureHtmlTable,_fnScrollDraw,_fnApplyToChildren,_fnCalculateColumnWidths,_fnThrottle,_fnConvertToWidth,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnSortFlatten,_fnSort,_fnSortAria,_fnSortListener,_fnSortAttachListener,_fnSortingClasses,_fnSortData,_fnSaveState,_fnLoadState,_fnSettingsFromNode,_fnLog,_fnMap,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnLengthOverflow,_fnRenderer,_fnDataSource,_fnRowAttributes*/
+
+(function( factory ) {
+	"use strict";
+
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				// CommonJS environments without a window global must pass a
+				// root. This will give an error otherwise
+				root = window;
+			}
+
+			if ( ! $ ) {
+				$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
+					require('jquery') :
+					require('jquery')( root );
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}
+(function( $, window, document, undefined ) {
+	"use strict";
+
+	/**
+	 * DataTables is a plug-in for the jQuery Javascript library. It is a highly
+	 * flexible tool, based upon the foundations of progressive enhancement,
+	 * which will add advanced interaction controls to any HTML table. For a
+	 * full list of features please refer to
+	 * [DataTables.net](href="http://datatables.net).
+	 *
+	 * Note that the `DataTable` object is not a global variable but is aliased
+	 * to `jQuery.fn.DataTable` and `jQuery.fn.dataTable` through which it may
+	 * be  accessed.
+	 *
+	 *  @class
+	 *  @param {object} [init={}] Configuration object for DataTables. Options
+	 *    are defined by {@link DataTable.defaults}
+	 *  @requires jQuery 1.7+
+	 *
+	 *  @example
+	 *    // Basic initialisation
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable();
+	 *    } );
+	 *
+	 *  @example
+	 *    // Initialisation with configuration options - in this case, disable
+	 *    // pagination and sorting.
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable( {
+	 *        "paginate": false,
+	 *        "sort": false
+	 *      } );
+	 *    } );
+	 */
+	var DataTable = function ( options )
+	{
+		/**
+		 * Perform a jQuery selector action on the table's TR elements (from the tbody) and
+		 * return the resulting jQuery object.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select TR elements that meet the current filter
+		 *    criterion ("applied") or all TR elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the TR elements in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {object} jQuery object, filtered by the given selector.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Highlight every second row
+		 *      oTable.$('tr:odd').css('backgroundColor', 'blue');
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to rows with 'Webkit' in them, add a background colour and then
+		 *      // remove the filter, thus highlighting the 'Webkit' rows only.
+		 *      oTable.fnFilter('Webkit');
+		 *      oTable.$('tr', {"search": "applied"}).css('backgroundColor', 'blue');
+		 *      oTable.fnFilter('');
+		 *    } );
+		 */
+		this.$ = function ( sSelector, oOpts )
+		{
+			return this.api(true).$( sSelector, oOpts );
+		};
+		
+		
+		/**
+		 * Almost identical to $ in operation, but in this case returns the data for the matched
+		 * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes
+		 * rather than any descendants, so the data can be obtained for the row/cell. If matching
+		 * rows are found, the data returned is the original data array/object that was used to
+		 * create the row (or a generated array if from a DOM source).
+		 *
+		 * This method is often useful in-combination with $ where both functions are given the
+		 * same parameters and the array indexes will match identically.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select elements that meet the current filter
+		 *    criterion ("applied") or all elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the data in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {array} Data for the matched elements. If any elements, as a result of the
+		 *    selector, were not TR, TD or TH elements in the DataTable, they will have a null
+		 *    entry in the array.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Get the data from the first row in the table
+		 *      var data = oTable._('tr:first');
+		 *
+		 *      // Do something useful with the data
+		 *      alert( "First cell is: "+data[0] );
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to 'Webkit' and get all data for
+		 *      oTable.fnFilter('Webkit');
+		 *      var data = oTable._('tr', {"search": "applied"});
+		 *
+		 *      // Do something with the data
+		 *      alert( data.length+" rows matched the search" );
+		 *    } );
+		 */
+		this._ = function ( sSelector, oOpts )
+		{
+			return this.api(true).rows( sSelector, oOpts ).data();
+		};
+		
+		
+		/**
+		 * Create a DataTables Api instance, with the currently selected tables for
+		 * the Api's context.
+		 * @param {boolean} [traditional=false] Set the API instance's context to be
+		 *   only the table referred to by the `DataTable.ext.iApiIndex` option, as was
+		 *   used in the API presented by DataTables 1.9- (i.e. the traditional mode),
+		 *   or if all tables captured in the jQuery object should be used.
+		 * @return {DataTables.Api}
+		 */
+		this.api = function ( traditional )
+		{
+			return traditional ?
+				new _Api(
+					_fnSettingsFromNode( this[ _ext.iApiIndex ] )
+				) :
+				new _Api( this );
+		};
+		
+		
+		/**
+		 * Add a single new row or multiple rows of data to the table. Please note
+		 * that this is suitable for client-side processing only - if you are using
+		 * server-side processing (i.e. "bServerSide": true), then to add data, you
+		 * must add it to the data source, i.e. the server-side, through an Ajax call.
+		 *  @param {array|object} data The data to be added to the table. This can be:
+		 *    <ul>
+		 *      <li>1D array of data - add a single row with the data provided</li>
+		 *      <li>2D array of arrays - add multiple rows in a single call</li>
+		 *      <li>object - data object when using <i>mData</i></li>
+		 *      <li>array of objects - multiple data objects when using <i>mData</i></li>
+		 *    </ul>
+		 *  @param {bool} [redraw=true] redraw the table or not
+		 *  @returns {array} An array of integers, representing the list of indexes in
+		 *    <i>aoData</i> ({@link DataTable.models.oSettings}) that have been added to
+		 *    the table.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    // Global var for counter
+		 *    var giCount = 2;
+		 *
+		 *    $(document).ready(function() {
+		 *      $('#example').dataTable();
+		 *    } );
+		 *
+		 *    function fnClickAddRow() {
+		 *      $('#example').dataTable().fnAddData( [
+		 *        giCount+".1",
+		 *        giCount+".2",
+		 *        giCount+".3",
+		 *        giCount+".4" ]
+		 *      );
+		 *
+		 *      giCount++;
+		 *    }
+		 */
+		this.fnAddData = function( data, redraw )
+		{
+			var api = this.api( true );
+		
+			/* Check if we want to add multiple rows or not */
+			var rows = $.isArray(data) && ( $.isArray(data[0]) || $.isPlainObject(data[0]) ) ?
+				api.rows.add( data ) :
+				api.row.add( data );
+		
+			if ( redraw === undefined || redraw ) {
+				api.draw();
+			}
+		
+			return rows.flatten().toArray();
+		};
+		
+		
+		/**
+		 * This function will make DataTables recalculate the column sizes, based on the data
+		 * contained in the table and the sizes applied to the columns (in the DOM, CSS or
+		 * through the sWidth parameter). This can be useful when the width of the table's
+		 * parent element changes (for example a window resize).
+		 *  @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *
+		 *      $(window).on('resize', function () {
+		 *        oTable.fnAdjustColumnSizing();
+		 *      } );
+		 *    } );
+		 */
+		this.fnAdjustColumnSizing = function ( bRedraw )
+		{
+			var api = this.api( true ).columns.adjust();
+			var settings = api.settings()[0];
+			var scroll = settings.oScroll;
+		
+			if ( bRedraw === undefined || bRedraw ) {
+				api.draw( false );
+			}
+			else if ( scroll.sX !== "" || scroll.sY !== "" ) {
+				/* If not redrawing, but scrolling, we want to apply the new column sizes anyway */
+				_fnScrollDraw( settings );
+			}
+		};
+		
+		
+		/**
+		 * Quickly and simply clear a table
+		 *  @param {bool} [bRedraw=true] redraw the table or not
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...)
+		 *      oTable.fnClearTable();
+		 *    } );
+		 */
+		this.fnClearTable = function( bRedraw )
+		{
+			var api = this.api( true ).clear();
+		
+			if ( bRedraw === undefined || bRedraw ) {
+				api.draw();
+			}
+		};
+		
+		
+		/**
+		 * The exact opposite of 'opening' a row, this function will close any rows which
+		 * are currently 'open'.
+		 *  @param {node} nTr the table row to 'close'
+		 *  @returns {int} 0 on success, or 1 if failed (can't find the row)
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnClose = function( nTr )
+		{
+			this.api( true ).row( nTr ).child.hide();
+		};
+		
+		
+		/**
+		 * Remove a row for the table
+		 *  @param {mixed} target The index of the row from aoData to be deleted, or
+		 *    the TR element you want to delete
+		 *  @param {function|null} [callBack] Callback function
+		 *  @param {bool} [redraw=true] Redraw the table or not
+		 *  @returns {array} The row that was deleted
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Immediately remove the first row
+		 *      oTable.fnDeleteRow( 0 );
+		 *    } );
+		 */
+		this.fnDeleteRow = function( target, callback, redraw )
+		{
+			var api = this.api( true );
+			var rows = api.rows( target );
+			var settings = rows.settings()[0];
+			var data = settings.aoData[ rows[0][0] ];
+		
+			rows.remove();
+		
+			if ( callback ) {
+				callback.call( this, settings, data );
+			}
+		
+			if ( redraw === undefined || redraw ) {
+				api.draw();
+			}
+		
+			return data;
+		};
+		
+		
+		/**
+		 * Restore the table to it's original state in the DOM by removing all of DataTables
+		 * enhancements, alterations to the DOM structure of the table and event listeners.
+		 *  @param {boolean} [remove=false] Completely remove the table from the DOM
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      // This example is fairly pointless in reality, but shows how fnDestroy can be used
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnDestroy();
+		 *    } );
+		 */
+		this.fnDestroy = function ( remove )
+		{
+			this.api( true ).destroy( remove );
+		};
+		
+		
+		/**
+		 * Redraw the table
+		 *  @param {bool} [complete=true] Re-filter and resort (if enabled) the table before the draw.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Re-draw the table - you wouldn't want to do it here, but it's an example :-)
+		 *      oTable.fnDraw();
+		 *    } );
+		 */
+		this.fnDraw = function( complete )
+		{
+			// Note that this isn't an exact match to the old call to _fnDraw - it takes
+			// into account the new data, but can hold position.
+			this.api( true ).draw( complete );
+		};
+		
+		
+		/**
+		 * Filter the input based on data
+		 *  @param {string} sInput String to filter the table on
+		 *  @param {int|null} [iColumn] Column to limit filtering to
+		 *  @param {bool} [bRegex=false] Treat as regular expression or not
+		 *  @param {bool} [bSmart=true] Perform smart filtering or not
+		 *  @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es)
+		 *  @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false)
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Sometime later - filter...
+		 *      oTable.fnFilter( 'test string' );
+		 *    } );
+		 */
+		this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive )
+		{
+			var api = this.api( true );
+		
+			if ( iColumn === null || iColumn === undefined ) {
+				api.search( sInput, bRegex, bSmart, bCaseInsensitive );
+			}
+			else {
+				api.column( iColumn ).search( sInput, bRegex, bSmart, bCaseInsensitive );
+			}
+		
+			api.draw();
+		};
+		
+		
+		/**
+		 * Get the data for the whole table, an individual row or an individual cell based on the
+		 * provided parameters.
+		 *  @param {int|node} [src] A TR row node, TD/TH cell node or an integer. If given as
+		 *    a TR node then the data source for the whole row will be returned. If given as a
+		 *    TD/TH cell node then iCol will be automatically calculated and the data for the
+		 *    cell returned. If given as an integer, then this is treated as the aoData internal
+		 *    data index for the row (see fnGetPosition) and the data for that row used.
+		 *  @param {int} [col] Optional column index that you want the data of.
+		 *  @returns {array|object|string} If mRow is undefined, then the data for all rows is
+		 *    returned. If mRow is defined, just data for that row, and is iCol is
+		 *    defined, only data for the designated cell is returned.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    // Row data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('tr').click( function () {
+		 *        var data = oTable.fnGetData( this );
+		 *        // ... do something with the array / object of data for the row
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Individual cell data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('td').click( function () {
+		 *        var sData = oTable.fnGetData( this );
+		 *        alert( 'The cell clicked on had the value of '+sData );
+		 *      } );
+		 *    } );
+		 */
+		this.fnGetData = function( src, col )
+		{
+			var api = this.api( true );
+		
+			if ( src !== undefined ) {
+				var type = src.nodeName ? src.nodeName.toLowerCase() : '';
+		
+				return col !== undefined || type == 'td' || type == 'th' ?
+					api.cell( src, col ).data() :
+					api.row( src ).data() || null;
+			}
+		
+			return api.data().toArray();
+		};
+		
+		
+		/**
+		 * Get an array of the TR nodes that are used in the table's body. Note that you will
+		 * typically want to use the '$' API method in preference to this as it is more
+		 * flexible.
+		 *  @param {int} [iRow] Optional row index for the TR element you want
+		 *  @returns {array|node} If iRow is undefined, returns an array of all TR elements
+		 *    in the table's body, or iRow is defined, just the TR element requested.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Get the nodes from the table
+		 *      var nNodes = oTable.fnGetNodes( );
+		 *    } );
+		 */
+		this.fnGetNodes = function( iRow )
+		{
+			var api = this.api( true );
+		
+			return iRow !== undefined ?
+				api.row( iRow ).node() :
+				api.rows().nodes().flatten().toArray();
+		};
+		
+		
+		/**
+		 * Get the array indexes of a particular cell from it's DOM element
+		 * and column index including hidden columns
+		 *  @param {node} node this can either be a TR, TD or TH in the table's body
+		 *  @returns {int} If nNode is given as a TR, then a single index is returned, or
+		 *    if given as a cell, an array of [row index, column index (visible),
+		 *    column index (all)] is given.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      $('#example tbody td').click( function () {
+		 *        // Get the position of the current data from the node
+		 *        var aPos = oTable.fnGetPosition( this );
+		 *
+		 *        // Get the data array for this row
+		 *        var aData = oTable.fnGetData( aPos[0] );
+		 *
+		 *        // Update the data array and return the value
+		 *        aData[ aPos[1] ] = 'clicked';
+		 *        this.innerHTML = 'clicked';
+		 *      } );
+		 *
+		 *      // Init DataTables
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnGetPosition = function( node )
+		{
+			var api = this.api( true );
+			var nodeName = node.nodeName.toUpperCase();
+		
+			if ( nodeName == 'TR' ) {
+				return api.row( node ).index();
+			}
+			else if ( nodeName == 'TD' || nodeName == 'TH' ) {
+				var cell = api.cell( node ).index();
+		
+				return [
+					cell.row,
+					cell.columnVisible,
+					cell.column
+				];
+			}
+			return null;
+		};
+		
+		
+		/**
+		 * Check to see if a row is 'open' or not.
+		 *  @param {node} nTr the table row to check
+		 *  @returns {boolean} true if the row is currently open, false otherwise
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnIsOpen = function( nTr )
+		{
+			return this.api( true ).row( nTr ).child.isShown();
+		};
+		
+		
+		/**
+		 * This function will place a new row directly after a row which is currently
+		 * on display on the page, with the HTML contents that is passed into the
+		 * function. This can be used, for example, to ask for confirmation that a
+		 * particular record should be deleted.
+		 *  @param {node} nTr The table row to 'open'
+		 *  @param {string|node|jQuery} mHtml The HTML to put into the row
+		 *  @param {string} sClass Class to give the new TD cell
+		 *  @returns {node} The row opened. Note that if the table row passed in as the
+		 *    first parameter, is not found in the table, this method will silently
+		 *    return.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnOpen = function( nTr, mHtml, sClass )
+		{
+			return this.api( true )
+				.row( nTr )
+				.child( mHtml, sClass )
+				.show()
+				.child()[0];
+		};
+		
+		
+		/**
+		 * Change the pagination - provides the internal logic for pagination in a simple API
+		 * function. With this function you can have a DataTables table go to the next,
+		 * previous, first or last pages.
+		 *  @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+		 *    or page number to jump to (integer), note that page 0 is the first page.
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnPageChange( 'next' );
+		 *    } );
+		 */
+		this.fnPageChange = function ( mAction, bRedraw )
+		{
+			var api = this.api( true ).page( mAction );
+		
+			if ( bRedraw === undefined || bRedraw ) {
+				api.draw(false);
+			}
+		};
+		
+		
+		/**
+		 * Show a particular column
+		 *  @param {int} iCol The column whose display should be changed
+		 *  @param {bool} bShow Show (true) or hide (false) the column
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Hide the second column after initialisation
+		 *      oTable.fnSetColumnVis( 1, false );
+		 *    } );
+		 */
+		this.fnSetColumnVis = function ( iCol, bShow, bRedraw )
+		{
+			var api = this.api( true ).column( iCol ).visible( bShow );
+		
+			if ( bRedraw === undefined || bRedraw ) {
+				api.columns.adjust().draw();
+			}
+		};
+		
+		
+		/**
+		 * Get the settings for a particular table for external manipulation
+		 *  @returns {object} DataTables settings object. See
+		 *    {@link DataTable.models.oSettings}
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      var oSettings = oTable.fnSettings();
+		 *
+		 *      // Show an example parameter from the settings
+		 *      alert( oSettings._iDisplayStart );
+		 *    } );
+		 */
+		this.fnSettings = function()
+		{
+			return _fnSettingsFromNode( this[_ext.iApiIndex] );
+		};
+		
+		
+		/**
+		 * Sort the table by a particular column
+		 *  @param {int} iCol the data index to sort on. Note that this will not match the
+		 *    'display index' if you have hidden data entries
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Sort immediately with columns 0 and 1
+		 *      oTable.fnSort( [ [0,'asc'], [1,'asc'] ] );
+		 *    } );
+		 */
+		this.fnSort = function( aaSort )
+		{
+			this.api( true ).order( aaSort ).draw();
+		};
+		
+		
+		/**
+		 * Attach a sort listener to an element for a given column
+		 *  @param {node} nNode the element to attach the sort listener to
+		 *  @param {int} iColumn the column that a click on this node will sort on
+		 *  @param {function} [fnCallback] callback function when sort is run
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Sort on column 1, when 'sorter' is clicked on
+		 *      oTable.fnSortListener( document.getElementById('sorter'), 1 );
+		 *    } );
+		 */
+		this.fnSortListener = function( nNode, iColumn, fnCallback )
+		{
+			this.api( true ).order.listener( nNode, iColumn, fnCallback );
+		};
+		
+		
+		/**
+		 * Update a table cell or row - this method will accept either a single value to
+		 * update the cell with, an array of values with one element for each column or
+		 * an object in the same format as the original data source. The function is
+		 * self-referencing in order to make the multi column updates easier.
+		 *  @param {object|array|string} mData Data to update the cell/row with
+		 *  @param {node|int} mRow TR element you want to update or the aoData index
+		 *  @param {int} [iColumn] The column to update, give as null or undefined to
+		 *    update a whole row.
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @param {bool} [bAction=true] Perform pre-draw actions or not
+		 *  @returns {int} 0 on success, 1 on error
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell
+		 *      oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], $('tbody tr')[0] ); // Row
+		 *    } );
+		 */
+		this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )
+		{
+			var api = this.api( true );
+		
+			if ( iColumn === undefined || iColumn === null ) {
+				api.row( mRow ).data( mData );
+			}
+			else {
+				api.cell( mRow, iColumn ).data( mData );
+			}
+		
+			if ( bAction === undefined || bAction ) {
+				api.columns.adjust();
+			}
+		
+			if ( bRedraw === undefined || bRedraw ) {
+				api.draw();
+			}
+			return 0;
+		};
+		
+		
+		/**
+		 * Provide a common method for plug-ins to check the version of DataTables being used, in order
+		 * to ensure compatibility.
+		 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+		 *    formats "X" and "X.Y" are also acceptable.
+		 *  @returns {boolean} true if this version of DataTables is greater or equal to the required
+		 *    version, or false if this version of DataTales is not suitable
+		 *  @method
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      alert( oTable.fnVersionCheck( '1.9.0' ) );
+		 *    } );
+		 */
+		this.fnVersionCheck = _ext.fnVersionCheck;
+		
+
+		var _that = this;
+		var emptyInit = options === undefined;
+		var len = this.length;
+
+		if ( emptyInit ) {
+			options = {};
+		}
+
+		this.oApi = this.internal = _ext.internal;
+
+		// Extend with old style plug-in API methods
+		for ( var fn in DataTable.ext.internal ) {
+			if ( fn ) {
+				this[fn] = _fnExternApiFunc(fn);
+			}
+		}
+
+		this.each(function() {
+			// For each initialisation we want to give it a clean initialisation
+			// object that can be bashed around
+			var o = {};
+			var oInit = len > 1 ? // optimisation for single table case
+				_fnExtend( o, options, true ) :
+				options;
+
+			/*global oInit,_that,emptyInit*/
+			var i=0, iLen, j, jLen, k, kLen;
+			var sId = this.getAttribute( 'id' );
+			var bInitHandedOff = false;
+			var defaults = DataTable.defaults;
+			var $this = $(this);
+			
+			
+			/* Sanity check */
+			if ( this.nodeName.toLowerCase() != 'table' )
+			{
+				_fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 );
+				return;
+			}
+			
+			/* Backwards compatibility for the defaults */
+			_fnCompatOpts( defaults );
+			_fnCompatCols( defaults.column );
+			
+			/* Convert the camel-case defaults to Hungarian */
+			_fnCamelToHungarian( defaults, defaults, true );
+			_fnCamelToHungarian( defaults.column, defaults.column, true );
+			
+			/* Setting up the initialisation object */
+			_fnCamelToHungarian( defaults, $.extend( oInit, $this.data() ) );
+			
+			
+			
+			/* Check to see if we are re-initialising a table */
+			var allSettings = DataTable.settings;
+			for ( i=0, iLen=allSettings.length ; i<iLen ; i++ )
+			{
+				var s = allSettings[i];
+			
+				/* Base check on table node */
+				if ( s.nTable == this || s.nTHead.parentNode == this || (s.nTFoot && s.nTFoot.parentNode == this) )
+				{
+					var bRetrieve = oInit.bRetrieve !== undefined ? oInit.bRetrieve : defaults.bRetrieve;
+					var bDestroy = oInit.bDestroy !== undefined ? oInit.bDestroy : defaults.bDestroy;
+			
+					if ( emptyInit || bRetrieve )
+					{
+						return s.oInstance;
+					}
+					else if ( bDestroy )
+					{
+						s.oInstance.fnDestroy();
+						break;
+					}
+					else
+					{
+						_fnLog( s, 0, 'Cannot reinitialise DataTable', 3 );
+						return;
+					}
+				}
+			
+				/* If the element we are initialising has the same ID as a table which was previously
+				 * initialised, but the table nodes don't match (from before) then we destroy the old
+				 * instance by simply deleting it. This is under the assumption that the table has been
+				 * destroyed by other methods. Anyone using non-id selectors will need to do this manually
+				 */
+				if ( s.sTableId == this.id )
+				{
+					allSettings.splice( i, 1 );
+					break;
+				}
+			}
+			
+			/* Ensure the table has an ID - required for accessibility */
+			if ( sId === null || sId === "" )
+			{
+				sId = "DataTables_Table_"+(DataTable.ext._unique++);
+				this.id = sId;
+			}
+			
+			/* Create the settings object for this table and set some of the default parameters */
+			var oSettings = $.extend( true, {}, DataTable.models.oSettings, {
+				"sDestroyWidth": $this[0].style.width,
+				"sInstance":     sId,
+				"sTableId":      sId
+			} );
+			oSettings.nTable = this;
+			oSettings.oApi   = _that.internal;
+			oSettings.oInit  = oInit;
+			
+			allSettings.push( oSettings );
+			
+			// Need to add the instance after the instance after the settings object has been added
+			// to the settings array, so we can self reference the table instance if more than one
+			oSettings.oInstance = (_that.length===1) ? _that : $this.dataTable();
+			
+			// Backwards compatibility, before we apply all the defaults
+			_fnCompatOpts( oInit );
+			
+			if ( oInit.oLanguage )
+			{
+				_fnLanguageCompat( oInit.oLanguage );
+			}
+			
+			// If the length menu is given, but the init display length is not, use the length menu
+			if ( oInit.aLengthMenu && ! oInit.iDisplayLength )
+			{
+				oInit.iDisplayLength = $.isArray( oInit.aLengthMenu[0] ) ?
+					oInit.aLengthMenu[0][0] : oInit.aLengthMenu[0];
+			}
+			
+			// Apply the defaults and init options to make a single init object will all
+			// options defined from defaults and instance options.
+			oInit = _fnExtend( $.extend( true, {}, defaults ), oInit );
+			
+			
+			// Map the initialisation options onto the settings object
+			_fnMap( oSettings.oFeatures, oInit, [
+				"bPaginate",
+				"bLengthChange",
+				"bFilter",
+				"bSort",
+				"bSortMulti",
+				"bInfo",
+				"bProcessing",
+				"bAutoWidth",
+				"bSortClasses",
+				"bServerSide",
+				"bDeferRender"
+			] );
+			_fnMap( oSettings, oInit, [
+				"asStripeClasses",
+				"ajax",
+				"fnServerData",
+				"fnFormatNumber",
+				"sServerMethod",
+				"aaSorting",
+				"aaSortingFixed",
+				"aLengthMenu",
+				"sPaginationType",
+				"sAjaxSource",
+				"sAjaxDataProp",
+				"iStateDuration",
+				"sDom",
+				"bSortCellsTop",
+				"iTabIndex",
+				"fnStateLoadCallback",
+				"fnStateSaveCallback",
+				"renderer",
+				"searchDelay",
+				"rowId",
+				[ "iCookieDuration", "iStateDuration" ], // backwards compat
+				[ "oSearch", "oPreviousSearch" ],
+				[ "aoSearchCols", "aoPreSearchCols" ],
+				[ "iDisplayLength", "_iDisplayLength" ],
+				[ "bJQueryUI", "bJUI" ]
+			] );
+			_fnMap( oSettings.oScroll, oInit, [
+				[ "sScrollX", "sX" ],
+				[ "sScrollXInner", "sXInner" ],
+				[ "sScrollY", "sY" ],
+				[ "bScrollCollapse", "bCollapse" ]
+			] );
+			_fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" );
+			
+			/* Callback functions which are array driven */
+			_fnCallbackReg( oSettings, 'aoDrawCallback',       oInit.fnDrawCallback,      'user' );
+			_fnCallbackReg( oSettings, 'aoServerParams',       oInit.fnServerParams,      'user' );
+			_fnCallbackReg( oSettings, 'aoStateSaveParams',    oInit.fnStateSaveParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoadParams',    oInit.fnStateLoadParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoaded',        oInit.fnStateLoaded,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCallback',        oInit.fnRowCallback,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow,        'user' );
+			_fnCallbackReg( oSettings, 'aoHeaderCallback',     oInit.fnHeaderCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoFooterCallback',     oInit.fnFooterCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoInitComplete',       oInit.fnInitComplete,      'user' );
+			_fnCallbackReg( oSettings, 'aoPreDrawCallback',    oInit.fnPreDrawCallback,   'user' );
+			
+			oSettings.rowIdFn = _fnGetObjectDataFn( oInit.rowId );
+			
+			/* Browser support detection */
+			_fnBrowserDetect( oSettings );
+			
+			var oClasses = oSettings.oClasses;
+			
+			// @todo Remove in 1.11
+			if ( oInit.bJQueryUI )
+			{
+				/* Use the JUI classes object for display. You could clone the oStdClasses object if
+				 * you want to have multiple tables with multiple independent classes
+				 */
+				$.extend( oClasses, DataTable.ext.oJUIClasses, oInit.oClasses );
+			
+				if ( oInit.sDom === defaults.sDom && defaults.sDom === "lfrtip" )
+				{
+					/* Set the DOM to use a layout suitable for jQuery UI's theming */
+					oSettings.sDom = '<"H"lfr>t<"F"ip>';
+				}
+			
+				if ( ! oSettings.renderer ) {
+					oSettings.renderer = 'jqueryui';
+				}
+				else if ( $.isPlainObject( oSettings.renderer ) && ! oSettings.renderer.header ) {
+					oSettings.renderer.header = 'jqueryui';
+				}
+			}
+			else
+			{
+				$.extend( oClasses, DataTable.ext.classes, oInit.oClasses );
+			}
+			$this.addClass( oClasses.sTable );
+			
+			
+			if ( oSettings.iInitDisplayStart === undefined )
+			{
+				/* Display start point, taking into account the save saving */
+				oSettings.iInitDisplayStart = oInit.iDisplayStart;
+				oSettings._iDisplayStart = oInit.iDisplayStart;
+			}
+			
+			if ( oInit.iDeferLoading !== null )
+			{
+				oSettings.bDeferLoading = true;
+				var tmp = $.isArray( oInit.iDeferLoading );
+				oSettings._iRecordsDisplay = tmp ? oInit.iDeferLoading[0] : oInit.iDeferLoading;
+				oSettings._iRecordsTotal = tmp ? oInit.iDeferLoading[1] : oInit.iDeferLoading;
+			}
+			
+			/* Language definitions */
+			var oLanguage = oSettings.oLanguage;
+			$.extend( true, oLanguage, oInit.oLanguage );
+			
+			if ( oLanguage.sUrl )
+			{
+				/* Get the language definitions from a file - because this Ajax call makes the language
+				 * get async to the remainder of this function we use bInitHandedOff to indicate that
+				 * _fnInitialise will be fired by the returned Ajax handler, rather than the constructor
+				 */
+				$.ajax( {
+					dataType: 'json',
+					url: oLanguage.sUrl,
+					success: function ( json ) {
+						_fnLanguageCompat( json );
+						_fnCamelToHungarian( defaults.oLanguage, json );
+						$.extend( true, oLanguage, json );
+						_fnInitialise( oSettings );
+					},
+					error: function () {
+						// Error occurred loading language file, continue on as best we can
+						_fnInitialise( oSettings );
+					}
+				} );
+				bInitHandedOff = true;
+			}
+			
+			/*
+			 * Stripes
+			 */
+			if ( oInit.asStripeClasses === null )
+			{
+				oSettings.asStripeClasses =[
+					oClasses.sStripeOdd,
+					oClasses.sStripeEven
+				];
+			}
+			
+			/* Remove row stripe classes if they are already on the table row */
+			var stripeClasses = oSettings.asStripeClasses;
+			var rowOne = $this.children('tbody').find('tr').eq(0);
+			if ( $.inArray( true, $.map( stripeClasses, function(el, i) {
+				return rowOne.hasClass(el);
+			} ) ) !== -1 ) {
+				$('tbody tr', this).removeClass( stripeClasses.join(' ') );
+				oSettings.asDestroyStripes = stripeClasses.slice();
+			}
+			
+			/*
+			 * Columns
+			 * See if we should load columns automatically or use defined ones
+			 */
+			var anThs = [];
+			var aoColumnsInit;
+			var nThead = this.getElementsByTagName('thead');
+			if ( nThead.length !== 0 )
+			{
+				_fnDetectHeader( oSettings.aoHeader, nThead[0] );
+				anThs = _fnGetUniqueThs( oSettings );
+			}
+			
+			/* If not given a column array, generate one with nulls */
+			if ( oInit.aoColumns === null )
+			{
+				aoColumnsInit = [];
+				for ( i=0, iLen=anThs.length ; i<iLen ; i++ )
+				{
+					aoColumnsInit.push( null );
+				}
+			}
+			else
+			{
+				aoColumnsInit = oInit.aoColumns;
+			}
+			
+			/* Add the columns */
+			for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ )
+			{
+				_fnAddColumn( oSettings, anThs ? anThs[i] : null );
+			}
+			
+			/* Apply the column definitions */
+			_fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, aoColumnsInit, function (iCol, oDef) {
+				_fnColumnOptions( oSettings, iCol, oDef );
+			} );
+			
+			/* HTML5 attribute detection - build an mData object automatically if the
+			 * attributes are found
+			 */
+			if ( rowOne.length ) {
+				var a = function ( cell, name ) {
+					return cell.getAttribute( 'data-'+name ) !== null ? name : null;
+				};
+			
+				$( rowOne[0] ).children('th, td').each( function (i, cell) {
+					var col = oSettings.aoColumns[i];
+			
+					if ( col.mData === i ) {
+						var sort = a( cell, 'sort' ) || a( cell, 'order' );
+						var filter = a( cell, 'filter' ) || a( cell, 'search' );
+			
+						if ( sort !== null || filter !== null ) {
+							col.mData = {
+								_:      i+'.display',
+								sort:   sort !== null   ? i+'.@data-'+sort   : undefined,
+								type:   sort !== null   ? i+'.@data-'+sort   : undefined,
+								filter: filter !== null ? i+'.@data-'+filter : undefined
+							};
+			
+							_fnColumnOptions( oSettings, i );
+						}
+					}
+				} );
+			}
+			
+			var features = oSettings.oFeatures;
+			var loadedInit = function () {
+				/*
+				 * Sorting
+				 * @todo For modularisation (1.11) this needs to do into a sort start up handler
+				 */
+			
+				// If aaSorting is not defined, then we use the first indicator in asSorting
+				// in case that has been altered, so the default sort reflects that option
+				if ( oInit.aaSorting === undefined ) {
+					var sorting = oSettings.aaSorting;
+					for ( i=0, iLen=sorting.length ; i<iLen ; i++ ) {
+						sorting[i][1] = oSettings.aoColumns[ i ].asSorting[0];
+					}
+				}
+			
+				/* Do a first pass on the sorting classes (allows any size changes to be taken into
+				 * account, and also will apply sorting disabled classes if disabled
+				 */
+				_fnSortingClasses( oSettings );
+			
+				if ( features.bSort ) {
+					_fnCallbackReg( oSettings, 'aoDrawCallback', function () {
+						if ( oSettings.bSorted ) {
+							var aSort = _fnSortFlatten( oSettings );
+							var sortedColumns = {};
+			
+							$.each( aSort, function (i, val) {
+								sortedColumns[ val.src ] = val.dir;
+							} );
+			
+							_fnCallbackFire( oSettings, null, 'order', [oSettings, aSort, sortedColumns] );
+							_fnSortAria( oSettings );
+						}
+					} );
+				}
+			
+				_fnCallbackReg( oSettings, 'aoDrawCallback', function () {
+					if ( oSettings.bSorted || _fnDataSource( oSettings ) === 'ssp' || features.bDeferRender ) {
+						_fnSortingClasses( oSettings );
+					}
+				}, 'sc' );
+			
+			
+				/*
+				 * Final init
+				 * Cache the header, body and footer as required, creating them if needed
+				 */
+			
+				// Work around for Webkit bug 83867 - store the caption-side before removing from doc
+				var captions = $this.children('caption').each( function () {
+					this._captionSide = $(this).css('caption-side');
+				} );
+			
+				var thead = $this.children('thead');
+				if ( thead.length === 0 ) {
+					thead = $('<thead/>').appendTo($this);
+				}
+				oSettings.nTHead = thead[0];
+			
+				var tbody = $this.children('tbody');
+				if ( tbody.length === 0 ) {
+					tbody = $('<tbody/>').appendTo($this);
+				}
+				oSettings.nTBody = tbody[0];
+			
+				var tfoot = $this.children('tfoot');
+				if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") ) {
+					// If we are a scrolling table, and no footer has been given, then we need to create
+					// a tfoot element for the caption element to be appended to
+					tfoot = $('<tfoot/>').appendTo($this);
+				}
+			
+				if ( tfoot.length === 0 || tfoot.children().length === 0 ) {
+					$this.addClass( oClasses.sNoFooter );
+				}
+				else if ( tfoot.length > 0 ) {
+					oSettings.nTFoot = tfoot[0];
+					_fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot );
+				}
+			
+				/* Check if there is data passing into the constructor */
+				if ( oInit.aaData ) {
+					for ( i=0 ; i<oInit.aaData.length ; i++ ) {
+						_fnAddData( oSettings, oInit.aaData[ i ] );
+					}
+				}
+				else if ( oSettings.bDeferLoading || _fnDataSource( oSettings ) == 'dom' ) {
+					/* Grab the data from the page - only do this when deferred loading or no Ajax
+					 * source since there is no point in reading the DOM data if we are then going
+					 * to replace it with Ajax data
+					 */
+					_fnAddTr( oSettings, $(oSettings.nTBody).children('tr') );
+				}
+			
+				/* Copy the data index array */
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+				/* Initialisation complete - table can be drawn */
+				oSettings.bInitialised = true;
+			
+				/* Check if we need to initialise the table (it might not have been handed off to the
+				 * language processor)
+				 */
+				if ( bInitHandedOff === false ) {
+					_fnInitialise( oSettings );
+				}
+			};
+			
+			/* Must be done after everything which can be overridden by the state saving! */
+			if ( oInit.bStateSave )
+			{
+				features.bStateSave = true;
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' );
+				_fnLoadState( oSettings, oInit, loadedInit );
+			}
+			else {
+				loadedInit();
+			}
+			
+		} );
+		_that = null;
+		return this;
+	};
+
+	
+	/*
+	 * It is useful to have variables which are scoped locally so only the
+	 * DataTables functions can access them and they don't leak into global space.
+	 * At the same time these functions are often useful over multiple files in the
+	 * core and API, so we list, or at least document, all variables which are used
+	 * by DataTables as private variables here. This also ensures that there is no
+	 * clashing of variable names and that they can easily referenced for reuse.
+	 */
+	
+	
+	// Defined else where
+	//  _selector_run
+	//  _selector_opts
+	//  _selector_first
+	//  _selector_row_indexes
+	
+	var _ext; // DataTable.ext
+	var _Api; // DataTable.Api
+	var _api_register; // DataTable.Api.register
+	var _api_registerPlural; // DataTable.Api.registerPlural
+	
+	var _re_dic = {};
+	var _re_new_lines = /[\r\n]/g;
+	var _re_html = /<.*?>/g;
+	
+	// This is not strict ISO8601 - Date.parse() is quite lax, although
+	// implementations differ between browsers.
+	var _re_date = /^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/;
+	
+	// Escape regular expression special characters
+	var _re_escape_regex = new RegExp( '(\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ].join('|\\') + ')', 'g' );
+	
+	// http://en.wikipedia.org/wiki/Foreign_exchange_market
+	// - \u20BD - Russian ruble.
+	// - \u20a9 - South Korean Won
+	// - \u20BA - Turkish Lira
+	// - \u20B9 - Indian Rupee
+	// - R - Brazil (R$) and South Africa
+	// - fr - Swiss Franc
+	// - kr - Swedish krona, Norwegian krone and Danish krone
+	// - \u2009 is thin space and \u202F is narrow no-break space, both used in many
+	//   standards as thousands separators.
+	var _re_formatted_numeric = /[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi;
+	
+	
+	var _empty = function ( d ) {
+		return !d || d === true || d === '-' ? true : false;
+	};
+	
+	
+	var _intVal = function ( s ) {
+		var integer = parseInt( s, 10 );
+		return !isNaN(integer) && isFinite(s) ? integer : null;
+	};
+	
+	// Convert from a formatted number with characters other than `.` as the
+	// decimal place, to a Javascript number
+	var _numToDecimal = function ( num, decimalPoint ) {
+		// Cache created regular expressions for speed as this function is called often
+		if ( ! _re_dic[ decimalPoint ] ) {
+			_re_dic[ decimalPoint ] = new RegExp( _fnEscapeRegex( decimalPoint ), 'g' );
+		}
+		return typeof num === 'string' && decimalPoint !== '.' ?
+			num.replace( /\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) :
+			num;
+	};
+	
+	
+	var _isNumber = function ( d, decimalPoint, formatted ) {
+		var strType = typeof d === 'string';
+	
+		// If empty return immediately so there must be a number if it is a
+		// formatted string (this stops the string "k", or "kr", etc being detected
+		// as a formatted number for currency
+		if ( _empty( d ) ) {
+			return true;
+		}
+	
+		if ( decimalPoint && strType ) {
+			d = _numToDecimal( d, decimalPoint );
+		}
+	
+		if ( formatted && strType ) {
+			d = d.replace( _re_formatted_numeric, '' );
+		}
+	
+		return !isNaN( parseFloat(d) ) && isFinite( d );
+	};
+	
+	
+	// A string without HTML in it can be considered to be HTML still
+	var _isHtml = function ( d ) {
+		return _empty( d ) || typeof d === 'string';
+	};
+	
+	
+	var _htmlNumeric = function ( d, decimalPoint, formatted ) {
+		if ( _empty( d ) ) {
+			return true;
+		}
+	
+		var html = _isHtml( d );
+		return ! html ?
+			null :
+			_isNumber( _stripHtml( d ), decimalPoint, formatted ) ?
+				true :
+				null;
+	};
+	
+	
+	var _pluck = function ( a, prop, prop2 ) {
+		var out = [];
+		var i=0, ien=a.length;
+	
+		// Could have the test in the loop for slightly smaller code, but speed
+		// is essential here
+		if ( prop2 !== undefined ) {
+			for ( ; i<ien ; i++ ) {
+				if ( a[i] && a[i][ prop ] ) {
+					out.push( a[i][ prop ][ prop2 ] );
+				}
+			}
+		}
+		else {
+			for ( ; i<ien ; i++ ) {
+				if ( a[i] ) {
+					out.push( a[i][ prop ] );
+				}
+			}
+		}
+	
+		return out;
+	};
+	
+	
+	// Basically the same as _pluck, but rather than looping over `a` we use `order`
+	// as the indexes to pick from `a`
+	var _pluck_order = function ( a, order, prop, prop2 )
+	{
+		var out = [];
+		var i=0, ien=order.length;
+	
+		// Could have the test in the loop for slightly smaller code, but speed
+		// is essential here
+		if ( prop2 !== undefined ) {
+			for ( ; i<ien ; i++ ) {
+				if ( a[ order[i] ][ prop ] ) {
+					out.push( a[ order[i] ][ prop ][ prop2 ] );
+				}
+			}
+		}
+		else {
+			for ( ; i<ien ; i++ ) {
+				out.push( a[ order[i] ][ prop ] );
+			}
+		}
+	
+		return out;
+	};
+	
+	
+	var _range = function ( len, start )
+	{
+		var out = [];
+		var end;
+	
+		if ( start === undefined ) {
+			start = 0;
+			end = len;
+		}
+		else {
+			end = start;
+			start = len;
+		}
+	
+		for ( var i=start ; i<end ; i++ ) {
+			out.push( i );
+		}
+	
+		return out;
+	};
+	
+	
+	var _removeEmpty = function ( a )
+	{
+		var out = [];
+	
+		for ( var i=0, ien=a.length ; i<ien ; i++ ) {
+			if ( a[i] ) { // careful - will remove all falsy values!
+				out.push( a[i] );
+			}
+		}
+	
+		return out;
+	};
+	
+	
+	var _stripHtml = function ( d ) {
+		return d.replace( _re_html, '' );
+	};
+	
+	
+	/**
+	 * Determine if all values in the array are unique. This means we can short
+	 * cut the _unique method at the cost of a single loop. A sorted array is used
+	 * to easily check the values.
+	 *
+	 * @param  {array} src Source array
+	 * @return {boolean} true if all unique, false otherwise
+	 * @ignore
+	 */
+	var _areAllUnique = function ( src ) {
+		if ( src.length < 2 ) {
+			return true;
+		}
+	
+		var sorted = src.slice().sort();
+		var last = sorted[0];
+	
+		for ( var i=1, ien=sorted.length ; i<ien ; i++ ) {
+			if ( sorted[i] === last ) {
+				return false;
+			}
+	
+			last = sorted[i];
+		}
+	
+		return true;
+	};
+	
+	
+	/**
+	 * Find the unique elements in a source array.
+	 *
+	 * @param  {array} src Source array
+	 * @return {array} Array of unique items
+	 * @ignore
+	 */
+	var _unique = function ( src )
+	{
+		if ( _areAllUnique( src ) ) {
+			return src.slice();
+		}
+	
+		// A faster unique method is to use object keys to identify used values,
+		// but this doesn't work with arrays or objects, which we must also
+		// consider. See jsperf.com/compare-array-unique-versions/4 for more
+		// information.
+		var
+			out = [],
+			val,
+			i, ien=src.length,
+			j, k=0;
+	
+		again: for ( i=0 ; i<ien ; i++ ) {
+			val = src[i];
+	
+			for ( j=0 ; j<k ; j++ ) {
+				if ( out[j] === val ) {
+					continue again;
+				}
+			}
+	
+			out.push( val );
+			k++;
+		}
+	
+		return out;
+	};
+	
+	
+	/**
+	 * DataTables utility methods
+	 * 
+	 * This namespace provides helper methods that DataTables uses internally to
+	 * create a DataTable, but which are not exclusively used only for DataTables.
+	 * These methods can be used by extension authors to save the duplication of
+	 * code.
+	 *
+	 *  @namespace
+	 */
+	DataTable.util = {
+		/**
+		 * Throttle the calls to a function. Arguments and context are maintained
+		 * for the throttled function.
+		 *
+		 * @param {function} fn Function to be called
+		 * @param {integer} freq Call frequency in mS
+		 * @return {function} Wrapped function
+		 */
+		throttle: function ( fn, freq ) {
+			var
+				frequency = freq !== undefined ? freq : 200,
+				last,
+				timer;
+	
+			return function () {
+				var
+					that = this,
+					now  = +new Date(),
+					args = arguments;
+	
+				if ( last && now < last + frequency ) {
+					clearTimeout( timer );
+	
+					timer = setTimeout( function () {
+						last = undefined;
+						fn.apply( that, args );
+					}, frequency );
+				}
+				else {
+					last = now;
+					fn.apply( that, args );
+				}
+			};
+		},
+	
+	
+		/**
+		 * Escape a string such that it can be used in a regular expression
+		 *
+		 *  @param {string} val string to escape
+		 *  @returns {string} escaped string
+		 */
+		escapeRegex: function ( val ) {
+			return val.replace( _re_escape_regex, '\\$1' );
+		}
+	};
+	
+	
+	
+	/**
+	 * Create a mapping object that allows camel case parameters to be looked up
+	 * for their Hungarian counterparts. The mapping is stored in a private
+	 * parameter called `_hungarianMap` which can be accessed on the source object.
+	 *  @param {object} o
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnHungarianMap ( o )
+	{
+		var
+			hungarian = 'a aa ai ao as b fn i m o s ',
+			match,
+			newKey,
+			map = {};
+	
+		$.each( o, function (key, val) {
+			match = key.match(/^([^A-Z]+?)([A-Z])/);
+	
+			if ( match && hungarian.indexOf(match[1]+' ') !== -1 )
+			{
+				newKey = key.replace( match[0], match[2].toLowerCase() );
+				map[ newKey ] = key;
+	
+				if ( match[1] === 'o' )
+				{
+					_fnHungarianMap( o[key] );
+				}
+			}
+		} );
+	
+		o._hungarianMap = map;
+	}
+	
+	
+	/**
+	 * Convert from camel case parameters to Hungarian, based on a Hungarian map
+	 * created by _fnHungarianMap.
+	 *  @param {object} src The model object which holds all parameters that can be
+	 *    mapped.
+	 *  @param {object} user The object to convert from camel case to Hungarian.
+	 *  @param {boolean} force When set to `true`, properties which already have a
+	 *    Hungarian value in the `user` object will be overwritten. Otherwise they
+	 *    won't be.
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnCamelToHungarian ( src, user, force )
+	{
+		if ( ! src._hungarianMap ) {
+			_fnHungarianMap( src );
+		}
+	
+		var hungarianKey;
+	
+		$.each( user, function (key, val) {
+			hungarianKey = src._hungarianMap[ key ];
+	
+			if ( hungarianKey !== undefined && (force || user[hungarianKey] === undefined) )
+			{
+				// For objects, we need to buzz down into the object to copy parameters
+				if ( hungarianKey.charAt(0) === 'o' )
+				{
+					// Copy the camelCase options over to the hungarian
+					if ( ! user[ hungarianKey ] ) {
+						user[ hungarianKey ] = {};
+					}
+					$.extend( true, user[hungarianKey], user[key] );
+	
+					_fnCamelToHungarian( src[hungarianKey], user[hungarianKey], force );
+				}
+				else {
+					user[hungarianKey] = user[ key ];
+				}
+			}
+		} );
+	}
+	
+	
+	/**
+	 * Language compatibility - when certain options are given, and others aren't, we
+	 * need to duplicate the values over, in order to provide backwards compatibility
+	 * with older language files.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnLanguageCompat( lang )
+	{
+		var defaults = DataTable.defaults.oLanguage;
+		var zeroRecords = lang.sZeroRecords;
+	
+		/* Backwards compatibility - if there is no sEmptyTable given, then use the same as
+		 * sZeroRecords - assuming that is given.
+		 */
+		if ( ! lang.sEmptyTable && zeroRecords &&
+			defaults.sEmptyTable === "No data available in table" )
+		{
+			_fnMap( lang, lang, 'sZeroRecords', 'sEmptyTable' );
+		}
+	
+		/* Likewise with loading records */
+		if ( ! lang.sLoadingRecords && zeroRecords &&
+			defaults.sLoadingRecords === "Loading..." )
+		{
+			_fnMap( lang, lang, 'sZeroRecords', 'sLoadingRecords' );
+		}
+	
+		// Old parameter name of the thousands separator mapped onto the new
+		if ( lang.sInfoThousands ) {
+			lang.sThousands = lang.sInfoThousands;
+		}
+	
+		var decimal = lang.sDecimal;
+		if ( decimal ) {
+			_addNumericSort( decimal );
+		}
+	}
+	
+	
+	/**
+	 * Map one parameter onto another
+	 *  @param {object} o Object to map
+	 *  @param {*} knew The new parameter name
+	 *  @param {*} old The old parameter name
+	 */
+	var _fnCompatMap = function ( o, knew, old ) {
+		if ( o[ knew ] !== undefined ) {
+			o[ old ] = o[ knew ];
+		}
+	};
+	
+	
+	/**
+	 * Provide backwards compatibility for the main DT options. Note that the new
+	 * options are mapped onto the old parameters, so this is an external interface
+	 * change only.
+	 *  @param {object} init Object to map
+	 */
+	function _fnCompatOpts ( init )
+	{
+		_fnCompatMap( init, 'ordering',      'bSort' );
+		_fnCompatMap( init, 'orderMulti',    'bSortMulti' );
+		_fnCompatMap( init, 'orderClasses',  'bSortClasses' );
+		_fnCompatMap( init, 'orderCellsTop', 'bSortCellsTop' );
+		_fnCompatMap( init, 'order',         'aaSorting' );
+		_fnCompatMap( init, 'orderFixed',    'aaSortingFixed' );
+		_fnCompatMap( init, 'paging',        'bPaginate' );
+		_fnCompatMap( init, 'pagingType',    'sPaginationType' );
+		_fnCompatMap( init, 'pageLength',    'iDisplayLength' );
+		_fnCompatMap( init, 'searching',     'bFilter' );
+	
+		// Boolean initialisation of x-scrolling
+		if ( typeof init.sScrollX === 'boolean' ) {
+			init.sScrollX = init.sScrollX ? '100%' : '';
+		}
+		if ( typeof init.scrollX === 'boolean' ) {
+			init.scrollX = init.scrollX ? '100%' : '';
+		}
+	
+		// Column search objects are in an array, so it needs to be converted
+		// element by element
+		var searchCols = init.aoSearchCols;
+	
+		if ( searchCols ) {
+			for ( var i=0, ien=searchCols.length ; i<ien ; i++ ) {
+				if ( searchCols[i] ) {
+					_fnCamelToHungarian( DataTable.models.oSearch, searchCols[i] );
+				}
+			}
+		}
+	}
+	
+	
+	/**
+	 * Provide backwards compatibility for column options. Note that the new options
+	 * are mapped onto the old parameters, so this is an external interface change
+	 * only.
+	 *  @param {object} init Object to map
+	 */
+	function _fnCompatCols ( init )
+	{
+		_fnCompatMap( init, 'orderable',     'bSortable' );
+		_fnCompatMap( init, 'orderData',     'aDataSort' );
+		_fnCompatMap( init, 'orderSequence', 'asSorting' );
+		_fnCompatMap( init, 'orderDataType', 'sortDataType' );
+	
+		// orderData can be given as an integer
+		var dataSort = init.aDataSort;
+		if ( typeof dataSort === 'number' && ! $.isArray( dataSort ) ) {
+			init.aDataSort = [ dataSort ];
+		}
+	}
+	
+	
+	/**
+	 * Browser feature detection for capabilities, quirks
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnBrowserDetect( settings )
+	{
+		// We don't need to do this every time DataTables is constructed, the values
+		// calculated are specific to the browser and OS configuration which we
+		// don't expect to change between initialisations
+		if ( ! DataTable.__browser ) {
+			var browser = {};
+			DataTable.__browser = browser;
+	
+			// Scrolling feature / quirks detection
+			var n = $('<div/>')
+				.css( {
+					position: 'fixed',
+					top: 0,
+					left: $(window).scrollLeft()*-1, // allow for scrolling
+					height: 1,
+					width: 1,
+					overflow: 'hidden'
+				} )
+				.append(
+					$('<div/>')
+						.css( {
+							position: 'absolute',
+							top: 1,
+							left: 1,
+							width: 100,
+							overflow: 'scroll'
+						} )
+						.append(
+							$('<div/>')
+								.css( {
+									width: '100%',
+									height: 10
+								} )
+						)
+				)
+				.appendTo( 'body' );
+	
+			var outer = n.children();
+			var inner = outer.children();
+	
+			// Numbers below, in order, are:
+			// inner.offsetWidth, inner.clientWidth, outer.offsetWidth, outer.clientWidth
+			//
+			// IE6 XP:                           100 100 100  83
+			// IE7 Vista:                        100 100 100  83
+			// IE 8+ Windows:                     83  83 100  83
+			// Evergreen Windows:                 83  83 100  83
+			// Evergreen Mac with scrollbars:     85  85 100  85
+			// Evergreen Mac without scrollbars: 100 100 100 100
+	
+			// Get scrollbar width
+			browser.barWidth = outer[0].offsetWidth - outer[0].clientWidth;
+	
+			// IE6/7 will oversize a width 100% element inside a scrolling element, to
+			// include the width of the scrollbar, while other browsers ensure the inner
+			// element is contained without forcing scrolling
+			browser.bScrollOversize = inner[0].offsetWidth === 100 && outer[0].clientWidth !== 100;
+	
+			// In rtl text layout, some browsers (most, but not all) will place the
+			// scrollbar on the left, rather than the right.
+			browser.bScrollbarLeft = Math.round( inner.offset().left ) !== 1;
+	
+			// IE8- don't provide height and width for getBoundingClientRect
+			browser.bBounding = n[0].getBoundingClientRect().width ? true : false;
+	
+			n.remove();
+		}
+	
+		$.extend( settings.oBrowser, DataTable.__browser );
+		settings.oScroll.iBarWidth = DataTable.__browser.barWidth;
+	}
+	
+	
+	/**
+	 * Array.prototype reduce[Right] method, used for browsers which don't support
+	 * JS 1.6. Done this way to reduce code size, since we iterate either way
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnReduce ( that, fn, init, start, end, inc )
+	{
+		var
+			i = start,
+			value,
+			isSet = false;
+	
+		if ( init !== undefined ) {
+			value = init;
+			isSet = true;
+		}
+	
+		while ( i !== end ) {
+			if ( ! that.hasOwnProperty(i) ) {
+				continue;
+			}
+	
+			value = isSet ?
+				fn( value, that[i], i, that ) :
+				that[i];
+	
+			isSet = true;
+			i += inc;
+		}
+	
+		return value;
+	}
+	
+	/**
+	 * Add a column to the list used for the table with default values
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {node} nTh The th element for this column
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAddColumn( oSettings, nTh )
+	{
+		// Add column to aoColumns array
+		var oDefaults = DataTable.defaults.column;
+		var iCol = oSettings.aoColumns.length;
+		var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, {
+			"nTh": nTh ? nTh : document.createElement('th'),
+			"sTitle":    oDefaults.sTitle    ? oDefaults.sTitle    : nTh ? nTh.innerHTML : '',
+			"aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol],
+			"mData": oDefaults.mData ? oDefaults.mData : iCol,
+			idx: iCol
+		} );
+		oSettings.aoColumns.push( oCol );
+	
+		// Add search object for column specific search. Note that the `searchCols[ iCol ]`
+		// passed into extend can be undefined. This allows the user to give a default
+		// with only some of the parameters defined, and also not give a default
+		var searchCols = oSettings.aoPreSearchCols;
+		searchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] );
+	
+		// Use the default column options function to initialise classes etc
+		_fnColumnOptions( oSettings, iCol, $(nTh).data() );
+	}
+	
+	
+	/**
+	 * Apply options for a column
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {int} iCol column index to consider
+	 *  @param {object} oOptions object with sType, bVisible and bSearchable etc
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnColumnOptions( oSettings, iCol, oOptions )
+	{
+		var oCol = oSettings.aoColumns[ iCol ];
+		var oClasses = oSettings.oClasses;
+		var th = $(oCol.nTh);
+	
+		// Try to get width information from the DOM. We can't get it from CSS
+		// as we'd need to parse the CSS stylesheet. `width` option can override
+		if ( ! oCol.sWidthOrig ) {
+			// Width attribute
+			oCol.sWidthOrig = th.attr('width') || null;
+	
+			// Style attribute
+			var t = (th.attr('style') || '').match(/width:\s*(\d+[pxem%]+)/);
+			if ( t ) {
+				oCol.sWidthOrig = t[1];
+			}
+		}
+	
+		/* User specified column options */
+		if ( oOptions !== undefined && oOptions !== null )
+		{
+			// Backwards compatibility
+			_fnCompatCols( oOptions );
+	
+			// Map camel case parameters to their Hungarian counterparts
+			_fnCamelToHungarian( DataTable.defaults.column, oOptions );
+	
+			/* Backwards compatibility for mDataProp */
+			if ( oOptions.mDataProp !== undefined && !oOptions.mData )
+			{
+				oOptions.mData = oOptions.mDataProp;
+			}
+	
+			if ( oOptions.sType )
+			{
+				oCol._sManualType = oOptions.sType;
+			}
+	
+			// `class` is a reserved word in Javascript, so we need to provide
+			// the ability to use a valid name for the camel case input
+			if ( oOptions.className && ! oOptions.sClass )
+			{
+				oOptions.sClass = oOptions.className;
+			}
+	
+			$.extend( oCol, oOptions );
+			_fnMap( oCol, oOptions, "sWidth", "sWidthOrig" );
+	
+			/* iDataSort to be applied (backwards compatibility), but aDataSort will take
+			 * priority if defined
+			 */
+			if ( oOptions.iDataSort !== undefined )
+			{
+				oCol.aDataSort = [ oOptions.iDataSort ];
+			}
+			_fnMap( oCol, oOptions, "aDataSort" );
+		}
+	
+		/* Cache the data get and set functions for speed */
+		var mDataSrc = oCol.mData;
+		var mData = _fnGetObjectDataFn( mDataSrc );
+		var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null;
+	
+		var attrTest = function( src ) {
+			return typeof src === 'string' && src.indexOf('@') !== -1;
+		};
+		oCol._bAttrSrc = $.isPlainObject( mDataSrc ) && (
+			attrTest(mDataSrc.sort) || attrTest(mDataSrc.type) || attrTest(mDataSrc.filter)
+		);
+		oCol._setter = null;
+	
+		oCol.fnGetData = function (rowData, type, meta) {
+			var innerData = mData( rowData, type, undefined, meta );
+	
+			return mRender && type ?
+				mRender( innerData, type, rowData, meta ) :
+				innerData;
+		};
+		oCol.fnSetData = function ( rowData, val, meta ) {
+			return _fnSetObjectDataFn( mDataSrc )( rowData, val, meta );
+		};
+	
+		// Indicate if DataTables should read DOM data as an object or array
+		// Used in _fnGetRowElements
+		if ( typeof mDataSrc !== 'number' ) {
+			oSettings._rowReadObject = true;
+		}
+	
+		/* Feature sorting overrides column specific when off */
+		if ( !oSettings.oFeatures.bSort )
+		{
+			oCol.bSortable = false;
+			th.addClass( oClasses.sSortableNone ); // Have to add class here as order event isn't called
+		}
+	
+		/* Check that the class assignment is correct for sorting */
+		var bAsc = $.inArray('asc', oCol.asSorting) !== -1;
+		var bDesc = $.inArray('desc', oCol.asSorting) !== -1;
+		if ( !oCol.bSortable || (!bAsc && !bDesc) )
+		{
+			oCol.sSortingClass = oClasses.sSortableNone;
+			oCol.sSortingClassJUI = "";
+		}
+		else if ( bAsc && !bDesc )
+		{
+			oCol.sSortingClass = oClasses.sSortableAsc;
+			oCol.sSortingClassJUI = oClasses.sSortJUIAscAllowed;
+		}
+		else if ( !bAsc && bDesc )
+		{
+			oCol.sSortingClass = oClasses.sSortableDesc;
+			oCol.sSortingClassJUI = oClasses.sSortJUIDescAllowed;
+		}
+		else
+		{
+			oCol.sSortingClass = oClasses.sSortable;
+			oCol.sSortingClassJUI = oClasses.sSortJUI;
+		}
+	}
+	
+	
+	/**
+	 * Adjust the table column widths for new data. Note: you would probably want to
+	 * do a redraw after calling this function!
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAdjustColumnSizing ( settings )
+	{
+		/* Not interested in doing column width calculation if auto-width is disabled */
+		if ( settings.oFeatures.bAutoWidth !== false )
+		{
+			var columns = settings.aoColumns;
+	
+			_fnCalculateColumnWidths( settings );
+			for ( var i=0 , iLen=columns.length ; i<iLen ; i++ )
+			{
+				columns[i].nTh.style.width = columns[i].sWidth;
+			}
+		}
+	
+		var scroll = settings.oScroll;
+		if ( scroll.sY !== '' || scroll.sX !== '')
+		{
+			_fnScrollDraw( settings );
+		}
+	
+		_fnCallbackFire( settings, null, 'column-sizing', [settings] );
+	}
+	
+	
+	/**
+	 * Covert the index of a visible column to the index in the data array (take account
+	 * of hidden columns)
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {int} iMatch Visible column index to lookup
+	 *  @returns {int} i the data index
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnVisibleToColumnIndex( oSettings, iMatch )
+	{
+		var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+	
+		return typeof aiVis[iMatch] === 'number' ?
+			aiVis[iMatch] :
+			null;
+	}
+	
+	
+	/**
+	 * Covert the index of an index in the data array and convert it to the visible
+	 *   column index (take account of hidden columns)
+	 *  @param {int} iMatch Column index to lookup
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns {int} i the data index
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnColumnIndexToVisible( oSettings, iMatch )
+	{
+		var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+		var iPos = $.inArray( iMatch, aiVis );
+	
+		return iPos !== -1 ? iPos : null;
+	}
+	
+	
+	/**
+	 * Get the number of visible columns
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns {int} i the number of visible columns
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnVisbleColumns( oSettings )
+	{
+		var vis = 0;
+	
+		// No reduce in IE8, use a loop for now
+		$.each( oSettings.aoColumns, function ( i, col ) {
+			if ( col.bVisible && $(col.nTh).css('display') !== 'none' ) {
+				vis++;
+			}
+		} );
+	
+		return vis;
+	}
+	
+	
+	/**
+	 * Get an array of column indexes that match a given property
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {string} sParam Parameter in aoColumns to look for - typically
+	 *    bVisible or bSearchable
+	 *  @returns {array} Array of indexes with matched properties
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetColumns( oSettings, sParam )
+	{
+		var a = [];
+	
+		$.map( oSettings.aoColumns, function(val, i) {
+			if ( val[sParam] ) {
+				a.push( i );
+			}
+		} );
+	
+		return a;
+	}
+	
+	
+	/**
+	 * Calculate the 'type' of a column
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnColumnTypes ( settings )
+	{
+		var columns = settings.aoColumns;
+		var data = settings.aoData;
+		var types = DataTable.ext.type.detect;
+		var i, ien, j, jen, k, ken;
+		var col, cell, detectedType, cache;
+	
+		// For each column, spin over the 
+		for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+			col = columns[i];
+			cache = [];
+	
+			if ( ! col.sType && col._sManualType ) {
+				col.sType = col._sManualType;
+			}
+			else if ( ! col.sType ) {
+				for ( j=0, jen=types.length ; j<jen ; j++ ) {
+					for ( k=0, ken=data.length ; k<ken ; k++ ) {
+						// Use a cache array so we only need to get the type data
+						// from the formatter once (when using multiple detectors)
+						if ( cache[k] === undefined ) {
+							cache[k] = _fnGetCellData( settings, k, i, 'type' );
+						}
+	
+						detectedType = types[j]( cache[k], settings );
+	
+						// If null, then this type can't apply to this column, so
+						// rather than testing all cells, break out. There is an
+						// exception for the last type which is `html`. We need to
+						// scan all rows since it is possible to mix string and HTML
+						// types
+						if ( ! detectedType && j !== types.length-1 ) {
+							break;
+						}
+	
+						// Only a single match is needed for html type since it is
+						// bottom of the pile and very similar to string
+						if ( detectedType === 'html' ) {
+							break;
+						}
+					}
+	
+					// Type is valid for all data points in the column - use this
+					// type
+					if ( detectedType ) {
+						col.sType = detectedType;
+						break;
+					}
+				}
+	
+				// Fall back - if no type was detected, always use string
+				if ( ! col.sType ) {
+					col.sType = 'string';
+				}
+			}
+		}
+	}
+	
+	
+	/**
+	 * Take the column definitions and static columns arrays and calculate how
+	 * they relate to column indexes. The callback function will then apply the
+	 * definition found for a column to a suitable configuration object.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {array} aoColDefs The aoColumnDefs array that is to be applied
+	 *  @param {array} aoCols The aoColumns array that defines columns individually
+	 *  @param {function} fn Callback function - takes two parameters, the calculated
+	 *    column index and the definition for that column.
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, fn )
+	{
+		var i, iLen, j, jLen, k, kLen, def;
+		var columns = oSettings.aoColumns;
+	
+		// Column definitions with aTargets
+		if ( aoColDefs )
+		{
+			/* Loop over the definitions array - loop in reverse so first instance has priority */
+			for ( i=aoColDefs.length-1 ; i>=0 ; i-- )
+			{
+				def = aoColDefs[i];
+	
+				/* Each definition can target multiple columns, as it is an array */
+				var aTargets = def.targets !== undefined ?
+					def.targets :
+					def.aTargets;
+	
+				if ( ! $.isArray( aTargets ) )
+				{
+					aTargets = [ aTargets ];
+				}
+	
+				for ( j=0, jLen=aTargets.length ; j<jLen ; j++ )
+				{
+					if ( typeof aTargets[j] === 'number' && aTargets[j] >= 0 )
+					{
+						/* Add columns that we don't yet know about */
+						while( columns.length <= aTargets[j] )
+						{
+							_fnAddColumn( oSettings );
+						}
+	
+						/* Integer, basic index */
+						fn( aTargets[j], def );
+					}
+					else if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 )
+					{
+						/* Negative integer, right to left column counting */
+						fn( columns.length+aTargets[j], def );
+					}
+					else if ( typeof aTargets[j] === 'string' )
+					{
+						/* Class name matching on TH element */
+						for ( k=0, kLen=columns.length ; k<kLen ; k++ )
+						{
+							if ( aTargets[j] == "_all" ||
+							     $(columns[k].nTh).hasClass( aTargets[j] ) )
+							{
+								fn( k, def );
+							}
+						}
+					}
+				}
+			}
+		}
+	
+		// Statically defined columns array
+		if ( aoCols )
+		{
+			for ( i=0, iLen=aoCols.length ; i<iLen ; i++ )
+			{
+				fn( i, aoCols[i] );
+			}
+		}
+	}
+	
+	/**
+	 * Add a data array to the table, creating DOM node etc. This is the parallel to
+	 * _fnGatherData, but for adding rows from a Javascript source, rather than a
+	 * DOM source.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {array} aData data array to be added
+	 *  @param {node} [nTr] TR element to add to the table - optional. If not given,
+	 *    DataTables will create a row automatically
+	 *  @param {array} [anTds] Array of TD|TH elements for the row - must be given
+	 *    if nTr is.
+	 *  @returns {int} >=0 if successful (index of new aoData entry), -1 if failed
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAddData ( oSettings, aDataIn, nTr, anTds )
+	{
+		/* Create the object for storing information about this new row */
+		var iRow = oSettings.aoData.length;
+		var oData = $.extend( true, {}, DataTable.models.oRow, {
+			src: nTr ? 'dom' : 'data',
+			idx: iRow
+		} );
+	
+		oData._aData = aDataIn;
+		oSettings.aoData.push( oData );
+	
+		/* Create the cells */
+		var nTd, sThisType;
+		var columns = oSettings.aoColumns;
+	
+		// Invalidate the column types as the new data needs to be revalidated
+		for ( var i=0, iLen=columns.length ; i<iLen ; i++ )
+		{
+			columns[i].sType = null;
+		}
+	
+		/* Add to the display array */
+		oSettings.aiDisplayMaster.push( iRow );
+	
+		var id = oSettings.rowIdFn( aDataIn );
+		if ( id !== undefined ) {
+			oSettings.aIds[ id ] = oData;
+		}
+	
+		/* Create the DOM information, or register it if already present */
+		if ( nTr || ! oSettings.oFeatures.bDeferRender )
+		{
+			_fnCreateTr( oSettings, iRow, nTr, anTds );
+		}
+	
+		return iRow;
+	}
+	
+	
+	/**
+	 * Add one or more TR elements to the table. Generally we'd expect to
+	 * use this for reading data from a DOM sourced table, but it could be
+	 * used for an TR element. Note that if a TR is given, it is used (i.e.
+	 * it is not cloned).
+	 *  @param {object} settings dataTables settings object
+	 *  @param {array|node|jQuery} trs The TR element(s) to add to the table
+	 *  @returns {array} Array of indexes for the added rows
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAddTr( settings, trs )
+	{
+		var row;
+	
+		// Allow an individual node to be passed in
+		if ( ! (trs instanceof $) ) {
+			trs = $(trs);
+		}
+	
+		return trs.map( function (i, el) {
+			row = _fnGetRowElements( settings, el );
+			return _fnAddData( settings, row.data, el, row.cells );
+		} );
+	}
+	
+	
+	/**
+	 * Take a TR element and convert it to an index in aoData
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {node} n the TR element to find
+	 *  @returns {int} index if the node is found, null if not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnNodeToDataIndex( oSettings, n )
+	{
+		return (n._DT_RowIndex!==undefined) ? n._DT_RowIndex : null;
+	}
+	
+	
+	/**
+	 * Take a TD element and convert it into a column data index (not the visible index)
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {int} iRow The row number the TD/TH can be found in
+	 *  @param {node} n The TD/TH element to find
+	 *  @returns {int} index if the node is found, -1 if not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnNodeToColumnIndex( oSettings, iRow, n )
+	{
+		return $.inArray( n, oSettings.aoData[ iRow ].anCells );
+	}
+	
+	
+	/**
+	 * Get the data for a given cell from the internal cache, taking into account data mapping
+	 *  @param {object} settings dataTables settings object
+	 *  @param {int} rowIdx aoData row id
+	 *  @param {int} colIdx Column index
+	 *  @param {string} type data get type ('display', 'type' 'filter' 'sort')
+	 *  @returns {*} Cell data
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetCellData( settings, rowIdx, colIdx, type )
+	{
+		var draw           = settings.iDraw;
+		var col            = settings.aoColumns[colIdx];
+		var rowData        = settings.aoData[rowIdx]._aData;
+		var defaultContent = col.sDefaultContent;
+		var cellData       = col.fnGetData( rowData, type, {
+			settings: settings,
+			row:      rowIdx,
+			col:      colIdx
+		} );
+	
+		if ( cellData === undefined ) {
+			if ( settings.iDrawError != draw && defaultContent === null ) {
+				_fnLog( settings, 0, "Requested unknown parameter "+
+					(typeof col.mData=='function' ? '{function}' : "'"+col.mData+"'")+
+					" for row "+rowIdx+", column "+colIdx, 4 );
+				settings.iDrawError = draw;
+			}
+			return defaultContent;
+		}
+	
+		// When the data source is null and a specific data type is requested (i.e.
+		// not the original data), we can use default column data
+		if ( (cellData === rowData || cellData === null) && defaultContent !== null && type !== undefined ) {
+			cellData = defaultContent;
+		}
+		else if ( typeof cellData === 'function' ) {
+			// If the data source is a function, then we run it and use the return,
+			// executing in the scope of the data object (for instances)
+			return cellData.call( rowData );
+		}
+	
+		if ( cellData === null && type == 'display' ) {
+			return '';
+		}
+		return cellData;
+	}
+	
+	
+	/**
+	 * Set the value for a specific cell, into the internal data cache
+	 *  @param {object} settings dataTables settings object
+	 *  @param {int} rowIdx aoData row id
+	 *  @param {int} colIdx Column index
+	 *  @param {*} val Value to set
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSetCellData( settings, rowIdx, colIdx, val )
+	{
+		var col     = settings.aoColumns[colIdx];
+		var rowData = settings.aoData[rowIdx]._aData;
+	
+		col.fnSetData( rowData, val, {
+			settings: settings,
+			row:      rowIdx,
+			col:      colIdx
+		}  );
+	}
+	
+	
+	// Private variable that is used to match action syntax in the data property object
+	var __reArray = /\[.*?\]$/;
+	var __reFn = /\(\)$/;
+	
+	/**
+	 * Split string on periods, taking into account escaped periods
+	 * @param  {string} str String to split
+	 * @return {array} Split string
+	 */
+	function _fnSplitObjNotation( str )
+	{
+		return $.map( str.match(/(\\.|[^\.])+/g) || [''], function ( s ) {
+			return s.replace(/\\\./g, '.');
+		} );
+	}
+	
+	
+	/**
+	 * Return a function that can be used to get data from a source object, taking
+	 * into account the ability to use nested objects as a source
+	 *  @param {string|int|function} mSource The data source for the object
+	 *  @returns {function} Data get function
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetObjectDataFn( mSource )
+	{
+		if ( $.isPlainObject( mSource ) )
+		{
+			/* Build an object of get functions, and wrap them in a single call */
+			var o = {};
+			$.each( mSource, function (key, val) {
+				if ( val ) {
+					o[key] = _fnGetObjectDataFn( val );
+				}
+			} );
+	
+			return function (data, type, row, meta) {
+				var t = o[type] || o._;
+				return t !== undefined ?
+					t(data, type, row, meta) :
+					data;
+			};
+		}
+		else if ( mSource === null )
+		{
+			/* Give an empty string for rendering / sorting etc */
+			return function (data) { // type, row and meta also passed, but not used
+				return data;
+			};
+		}
+		else if ( typeof mSource === 'function' )
+		{
+			return function (data, type, row, meta) {
+				return mSource( data, type, row, meta );
+			};
+		}
+		else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 ||
+			      mSource.indexOf('[') !== -1 || mSource.indexOf('(') !== -1) )
+		{
+			/* If there is a . in the source string then the data source is in a
+			 * nested object so we loop over the data for each level to get the next
+			 * level down. On each loop we test for undefined, and if found immediately
+			 * return. This allows entire objects to be missing and sDefaultContent to
+			 * be used if defined, rather than throwing an error
+			 */
+			var fetchData = function (data, type, src) {
+				var arrayNotation, funcNotation, out, innerSrc;
+	
+				if ( src !== "" )
+				{
+					var a = _fnSplitObjNotation( src );
+	
+					for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+					{
+						// Check if we are dealing with special notation
+						arrayNotation = a[i].match(__reArray);
+						funcNotation = a[i].match(__reFn);
+	
+						if ( arrayNotation )
+						{
+							// Array notation
+							a[i] = a[i].replace(__reArray, '');
+	
+							// Condition allows simply [] to be passed in
+							if ( a[i] !== "" ) {
+								data = data[ a[i] ];
+							}
+							out = [];
+	
+							// Get the remainder of the nested object to get
+							a.splice( 0, i+1 );
+							innerSrc = a.join('.');
+	
+							// Traverse each entry in the array getting the properties requested
+							if ( $.isArray( data ) ) {
+								for ( var j=0, jLen=data.length ; j<jLen ; j++ ) {
+									out.push( fetchData( data[j], type, innerSrc ) );
+								}
+							}
+	
+							// If a string is given in between the array notation indicators, that
+							// is used to join the strings together, otherwise an array is returned
+							var join = arrayNotation[0].substring(1, arrayNotation[0].length-1);
+							data = (join==="") ? out : out.join(join);
+	
+							// The inner call to fetchData has already traversed through the remainder
+							// of the source requested, so we exit from the loop
+							break;
+						}
+						else if ( funcNotation )
+						{
+							// Function call
+							a[i] = a[i].replace(__reFn, '');
+							data = data[ a[i] ]();
+							continue;
+						}
+	
+						if ( data === null || data[ a[i] ] === undefined )
+						{
+							return undefined;
+						}
+						data = data[ a[i] ];
+					}
+				}
+	
+				return data;
+			};
+	
+			return function (data, type) { // row and meta also passed, but not used
+				return fetchData( data, type, mSource );
+			};
+		}
+		else
+		{
+			/* Array or flat object mapping */
+			return function (data, type) { // row and meta also passed, but not used
+				return data[mSource];
+			};
+		}
+	}
+	
+	
+	/**
+	 * Return a function that can be used to set data from a source object, taking
+	 * into account the ability to use nested objects as a source
+	 *  @param {string|int|function} mSource The data source for the object
+	 *  @returns {function} Data set function
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSetObjectDataFn( mSource )
+	{
+		if ( $.isPlainObject( mSource ) )
+		{
+			/* Unlike get, only the underscore (global) option is used for for
+			 * setting data since we don't know the type here. This is why an object
+			 * option is not documented for `mData` (which is read/write), but it is
+			 * for `mRender` which is read only.
+			 */
+			return _fnSetObjectDataFn( mSource._ );
+		}
+		else if ( mSource === null )
+		{
+			/* Nothing to do when the data source is null */
+			return function () {};
+		}
+		else if ( typeof mSource === 'function' )
+		{
+			return function (data, val, meta) {
+				mSource( data, 'set', val, meta );
+			};
+		}
+		else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 ||
+			      mSource.indexOf('[') !== -1 || mSource.indexOf('(') !== -1) )
+		{
+			/* Like the get, we need to get data from a nested object */
+			var setData = function (data, val, src) {
+				var a = _fnSplitObjNotation( src ), b;
+				var aLast = a[a.length-1];
+				var arrayNotation, funcNotation, o, innerSrc;
+	
+				for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ )
+				{
+					// Check if we are dealing with an array notation request
+					arrayNotation = a[i].match(__reArray);
+					funcNotation = a[i].match(__reFn);
+	
+					if ( arrayNotation )
+					{
+						a[i] = a[i].replace(__reArray, '');
+						data[ a[i] ] = [];
+	
+						// Get the remainder of the nested object to set so we can recurse
+						b = a.slice();
+						b.splice( 0, i+1 );
+						innerSrc = b.join('.');
+	
+						// Traverse each entry in the array setting the properties requested
+						if ( $.isArray( val ) )
+						{
+							for ( var j=0, jLen=val.length ; j<jLen ; j++ )
+							{
+								o = {};
+								setData( o, val[j], innerSrc );
+								data[ a[i] ].push( o );
+							}
+						}
+						else
+						{
+							// We've been asked to save data to an array, but it
+							// isn't array data to be saved. Best that can be done
+							// is to just save the value.
+							data[ a[i] ] = val;
+						}
+	
+						// The inner call to setData has already traversed through the remainder
+						// of the source and has set the data, thus we can exit here
+						return;
+					}
+					else if ( funcNotation )
+					{
+						// Function call
+						a[i] = a[i].replace(__reFn, '');
+						data = data[ a[i] ]( val );
+					}
+	
+					// If the nested object doesn't currently exist - since we are
+					// trying to set the value - create it
+					if ( data[ a[i] ] === null || data[ a[i] ] === undefined )
+					{
+						data[ a[i] ] = {};
+					}
+					data = data[ a[i] ];
+				}
+	
+				// Last item in the input - i.e, the actual set
+				if ( aLast.match(__reFn ) )
+				{
+					// Function call
+					data = data[ aLast.replace(__reFn, '') ]( val );
+				}
+				else
+				{
+					// If array notation is used, we just want to strip it and use the property name
+					// and assign the value. If it isn't used, then we get the result we want anyway
+					data[ aLast.replace(__reArray, '') ] = val;
+				}
+			};
+	
+			return function (data, val) { // meta is also passed in, but not used
+				return setData( data, val, mSource );
+			};
+		}
+		else
+		{
+			/* Array or flat object mapping */
+			return function (data, val) { // meta is also passed in, but not used
+				data[mSource] = val;
+			};
+		}
+	}
+	
+	
+	/**
+	 * Return an array with the full table data
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns array {array} aData Master data array
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetDataMaster ( settings )
+	{
+		return _pluck( settings.aoData, '_aData' );
+	}
+	
+	
+	/**
+	 * Nuke the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnClearTable( settings )
+	{
+		settings.aoData.length = 0;
+		settings.aiDisplayMaster.length = 0;
+		settings.aiDisplay.length = 0;
+		settings.aIds = {};
+	}
+	
+	
+	 /**
+	 * Take an array of integers (index array) and remove a target integer (value - not
+	 * the key!)
+	 *  @param {array} a Index array to target
+	 *  @param {int} iTarget value to find
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnDeleteIndex( a, iTarget, splice )
+	{
+		var iTargetIndex = -1;
+	
+		for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+		{
+			if ( a[i] == iTarget )
+			{
+				iTargetIndex = i;
+			}
+			else if ( a[i] > iTarget )
+			{
+				a[i]--;
+			}
+		}
+	
+		if ( iTargetIndex != -1 && splice === undefined )
+		{
+			a.splice( iTargetIndex, 1 );
+		}
+	}
+	
+	
+	/**
+	 * Mark cached data as invalid such that a re-read of the data will occur when
+	 * the cached data is next requested. Also update from the data source object.
+	 *
+	 * @param {object} settings DataTables settings object
+	 * @param {int}    rowIdx   Row index to invalidate
+	 * @param {string} [src]    Source to invalidate from: undefined, 'auto', 'dom'
+	 *     or 'data'
+	 * @param {int}    [colIdx] Column index to invalidate. If undefined the whole
+	 *     row will be invalidated
+	 * @memberof DataTable#oApi
+	 *
+	 * @todo For the modularisation of v1.11 this will need to become a callback, so
+	 *   the sort and filter methods can subscribe to it. That will required
+	 *   initialisation options for sorting, which is why it is not already baked in
+	 */
+	function _fnInvalidate( settings, rowIdx, src, colIdx )
+	{
+		var row = settings.aoData[ rowIdx ];
+		var i, ien;
+		var cellWrite = function ( cell, col ) {
+			// This is very frustrating, but in IE if you just write directly
+			// to innerHTML, and elements that are overwritten are GC'ed,
+			// even if there is a reference to them elsewhere
+			while ( cell.childNodes.length ) {
+				cell.removeChild( cell.firstChild );
+			}
+	
+			cell.innerHTML = _fnGetCellData( settings, rowIdx, col, 'display' );
+		};
+	
+		// Are we reading last data from DOM or the data object?
+		if ( src === 'dom' || ((! src || src === 'auto') && row.src === 'dom') ) {
+			// Read the data from the DOM
+			row._aData = _fnGetRowElements(
+					settings, row, colIdx, colIdx === undefined ? undefined : row._aData
+				)
+				.data;
+		}
+		else {
+			// Reading from data object, update the DOM
+			var cells = row.anCells;
+	
+			if ( cells ) {
+				if ( colIdx !== undefined ) {
+					cellWrite( cells[colIdx], colIdx );
+				}
+				else {
+					for ( i=0, ien=cells.length ; i<ien ; i++ ) {
+						cellWrite( cells[i], i );
+					}
+				}
+			}
+		}
+	
+		// For both row and cell invalidation, the cached data for sorting and
+		// filtering is nulled out
+		row._aSortData = null;
+		row._aFilterData = null;
+	
+		// Invalidate the type for a specific column (if given) or all columns since
+		// the data might have changed
+		var cols = settings.aoColumns;
+		if ( colIdx !== undefined ) {
+			cols[ colIdx ].sType = null;
+		}
+		else {
+			for ( i=0, ien=cols.length ; i<ien ; i++ ) {
+				cols[i].sType = null;
+			}
+	
+			// Update DataTables special `DT_*` attributes for the row
+			_fnRowAttributes( settings, row );
+		}
+	}
+	
+	
+	/**
+	 * Build a data source object from an HTML row, reading the contents of the
+	 * cells that are in the row.
+	 *
+	 * @param {object} settings DataTables settings object
+	 * @param {node|object} TR element from which to read data or existing row
+	 *   object from which to re-read the data from the cells
+	 * @param {int} [colIdx] Optional column index
+	 * @param {array|object} [d] Data source object. If `colIdx` is given then this
+	 *   parameter should also be given and will be used to write the data into.
+	 *   Only the column in question will be written
+	 * @returns {object} Object with two parameters: `data` the data read, in
+	 *   document order, and `cells` and array of nodes (they can be useful to the
+	 *   caller, so rather than needing a second traversal to get them, just return
+	 *   them from here).
+	 * @memberof DataTable#oApi
+	 */
+	function _fnGetRowElements( settings, row, colIdx, d )
+	{
+		var
+			tds = [],
+			td = row.firstChild,
+			name, col, o, i=0, contents,
+			columns = settings.aoColumns,
+			objectRead = settings._rowReadObject;
+	
+		// Allow the data object to be passed in, or construct
+		d = d !== undefined ?
+			d :
+			objectRead ?
+				{} :
+				[];
+	
+		var attr = function ( str, td  ) {
+			if ( typeof str === 'string' ) {
+				var idx = str.indexOf('@');
+	
+				if ( idx !== -1 ) {
+					var attr = str.substring( idx+1 );
+					var setter = _fnSetObjectDataFn( str );
+					setter( d, td.getAttribute( attr ) );
+				}
+			}
+		};
+	
+		// Read data from a cell and store into the data object
+		var cellProcess = function ( cell ) {
+			if ( colIdx === undefined || colIdx === i ) {
+				col = columns[i];
+				contents = $.trim(cell.innerHTML);
+	
+				if ( col && col._bAttrSrc ) {
+					var setter = _fnSetObjectDataFn( col.mData._ );
+					setter( d, contents );
+	
+					attr( col.mData.sort, cell );
+					attr( col.mData.type, cell );
+					attr( col.mData.filter, cell );
+				}
+				else {
+					// Depending on the `data` option for the columns the data can
+					// be read to either an object or an array.
+					if ( objectRead ) {
+						if ( ! col._setter ) {
+							// Cache the setter function
+							col._setter = _fnSetObjectDataFn( col.mData );
+						}
+						col._setter( d, contents );
+					}
+					else {
+						d[i] = contents;
+					}
+				}
+			}
+	
+			i++;
+		};
+	
+		if ( td ) {
+			// `tr` element was passed in
+			while ( td ) {
+				name = td.nodeName.toUpperCase();
+	
+				if ( name == "TD" || name == "TH" ) {
+					cellProcess( td );
+					tds.push( td );
+				}
+	
+				td = td.nextSibling;
+			}
+		}
+		else {
+			// Existing row object passed in
+			tds = row.anCells;
+	
+			for ( var j=0, jen=tds.length ; j<jen ; j++ ) {
+				cellProcess( tds[j] );
+			}
+		}
+	
+		// Read the ID from the DOM if present
+		var rowNode = row.firstChild ? row : row.nTr;
+	
+		if ( rowNode ) {
+			var id = rowNode.getAttribute( 'id' );
+	
+			if ( id ) {
+				_fnSetObjectDataFn( settings.rowId )( d, id );
+			}
+		}
+	
+		return {
+			data: d,
+			cells: tds
+		};
+	}
+	/**
+	 * Create a new TR element (and it's TD children) for a row
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {int} iRow Row to consider
+	 *  @param {node} [nTrIn] TR element to add to the table - optional. If not given,
+	 *    DataTables will create a row automatically
+	 *  @param {array} [anTds] Array of TD|TH elements for the row - must be given
+	 *    if nTr is.
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnCreateTr ( oSettings, iRow, nTrIn, anTds )
+	{
+		var
+			row = oSettings.aoData[iRow],
+			rowData = row._aData,
+			cells = [],
+			nTr, nTd, oCol,
+			i, iLen;
+	
+		if ( row.nTr === null )
+		{
+			nTr = nTrIn || document.createElement('tr');
+	
+			row.nTr = nTr;
+			row.anCells = cells;
+	
+			/* Use a private property on the node to allow reserve mapping from the node
+			 * to the aoData array for fast look up
+			 */
+			nTr._DT_RowIndex = iRow;
+	
+			/* Special parameters can be given by the data source to be used on the row */
+			_fnRowAttributes( oSettings, row );
+	
+			/* Process each column */
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oCol = oSettings.aoColumns[i];
+	
+				nTd = nTrIn ? anTds[i] : document.createElement( oCol.sCellType );
+				nTd._DT_CellIndex = {
+					row: iRow,
+					column: i
+				};
+				
+				cells.push( nTd );
+	
+				// Need to create the HTML if new, or if a rendering function is defined
+				if ( (!nTrIn || oCol.mRender || oCol.mData !== i) &&
+					 (!$.isPlainObject(oCol.mData) || oCol.mData._ !== i+'.display')
+				) {
+					nTd.innerHTML = _fnGetCellData( oSettings, iRow, i, 'display' );
+				}
+	
+				/* Add user defined class */
+				if ( oCol.sClass )
+				{
+					nTd.className += ' '+oCol.sClass;
+				}
+	
+				// Visibility - add or remove as required
+				if ( oCol.bVisible && ! nTrIn )
+				{
+					nTr.appendChild( nTd );
+				}
+				else if ( ! oCol.bVisible && nTrIn )
+				{
+					nTd.parentNode.removeChild( nTd );
+				}
+	
+				if ( oCol.fnCreatedCell )
+				{
+					oCol.fnCreatedCell.call( oSettings.oInstance,
+						nTd, _fnGetCellData( oSettings, iRow, i ), rowData, iRow, i
+					);
+				}
+			}
+	
+			_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [nTr, rowData, iRow] );
+		}
+	
+		// Remove once webkit bug 131819 and Chromium bug 365619 have been resolved
+		// and deployed
+		row.nTr.setAttribute( 'role', 'row' );
+	}
+	
+	
+	/**
+	 * Add attributes to a row based on the special `DT_*` parameters in a data
+	 * source object.
+	 *  @param {object} settings DataTables settings object
+	 *  @param {object} DataTables row object for the row to be modified
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnRowAttributes( settings, row )
+	{
+		var tr = row.nTr;
+		var data = row._aData;
+	
+		if ( tr ) {
+			var id = settings.rowIdFn( data );
+	
+			if ( id ) {
+				tr.id = id;
+			}
+	
+			if ( data.DT_RowClass ) {
+				// Remove any classes added by DT_RowClass before
+				var a = data.DT_RowClass.split(' ');
+				row.__rowc = row.__rowc ?
+					_unique( row.__rowc.concat( a ) ) :
+					a;
+	
+				$(tr)
+					.removeClass( row.__rowc.join(' ') )
+					.addClass( data.DT_RowClass );
+			}
+	
+			if ( data.DT_RowAttr ) {
+				$(tr).attr( data.DT_RowAttr );
+			}
+	
+			if ( data.DT_RowData ) {
+				$(tr).data( data.DT_RowData );
+			}
+		}
+	}
+	
+	
+	/**
+	 * Create the HTML header for the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnBuildHead( oSettings )
+	{
+		var i, ien, cell, row, column;
+		var thead = oSettings.nTHead;
+		var tfoot = oSettings.nTFoot;
+		var createHeader = $('th, td', thead).length === 0;
+		var classes = oSettings.oClasses;
+		var columns = oSettings.aoColumns;
+	
+		if ( createHeader ) {
+			row = $('<tr/>').appendTo( thead );
+		}
+	
+		for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+			column = columns[i];
+			cell = $( column.nTh ).addClass( column.sClass );
+	
+			if ( createHeader ) {
+				cell.appendTo( row );
+			}
+	
+			// 1.11 move into sorting
+			if ( oSettings.oFeatures.bSort ) {
+				cell.addClass( column.sSortingClass );
+	
+				if ( column.bSortable !== false ) {
+					cell
+						.attr( 'tabindex', oSettings.iTabIndex )
+						.attr( 'aria-controls', oSettings.sTableId );
+	
+					_fnSortAttachListener( oSettings, column.nTh, i );
+				}
+			}
+	
+			if ( column.sTitle != cell[0].innerHTML ) {
+				cell.html( column.sTitle );
+			}
+	
+			_fnRenderer( oSettings, 'header' )(
+				oSettings, cell, column, classes
+			);
+		}
+	
+		if ( createHeader ) {
+			_fnDetectHeader( oSettings.aoHeader, thead );
+		}
+		
+		/* ARIA role for the rows */
+	 	$(thead).find('>tr').attr('role', 'row');
+	
+		/* Deal with the footer - add classes if required */
+		$(thead).find('>tr>th, >tr>td').addClass( classes.sHeaderTH );
+		$(tfoot).find('>tr>th, >tr>td').addClass( classes.sFooterTH );
+	
+		// Cache the footer cells. Note that we only take the cells from the first
+		// row in the footer. If there is more than one row the user wants to
+		// interact with, they need to use the table().foot() method. Note also this
+		// allows cells to be used for multiple columns using colspan
+		if ( tfoot !== null ) {
+			var cells = oSettings.aoFooter[0];
+	
+			for ( i=0, ien=cells.length ; i<ien ; i++ ) {
+				column = columns[i];
+				column.nTf = cells[i].cell;
+	
+				if ( column.sClass ) {
+					$(column.nTf).addClass( column.sClass );
+				}
+			}
+		}
+	}
+	
+	
+	/**
+	 * Draw the header (or footer) element based on the column visibility states. The
+	 * methodology here is to use the layout array from _fnDetectHeader, modified for
+	 * the instantaneous column visibility, to construct the new layout. The grid is
+	 * traversed over cell at a time in a rows x columns grid fashion, although each
+	 * cell insert can cover multiple elements in the grid - which is tracks using the
+	 * aApplied array. Cell inserts in the grid will only occur where there isn't
+	 * already a cell in that position.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param array {objects} aoSource Layout array from _fnDetectHeader
+	 *  @param {boolean} [bIncludeHidden=false] If true then include the hidden columns in the calc,
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnDrawHead( oSettings, aoSource, bIncludeHidden )
+	{
+		var i, iLen, j, jLen, k, kLen, n, nLocalTr;
+		var aoLocal = [];
+		var aApplied = [];
+		var iColumns = oSettings.aoColumns.length;
+		var iRowspan, iColspan;
+	
+		if ( ! aoSource )
+		{
+			return;
+		}
+	
+		if (  bIncludeHidden === undefined )
+		{
+			bIncludeHidden = false;
+		}
+	
+		/* Make a copy of the master layout array, but without the visible columns in it */
+		for ( i=0, iLen=aoSource.length ; i<iLen ; i++ )
+		{
+			aoLocal[i] = aoSource[i].slice();
+			aoLocal[i].nTr = aoSource[i].nTr;
+	
+			/* Remove any columns which are currently hidden */
+			for ( j=iColumns-1 ; j>=0 ; j-- )
+			{
+				if ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden )
+				{
+					aoLocal[i].splice( j, 1 );
+				}
+			}
+	
+			/* Prep the applied array - it needs an element for each row */
+			aApplied.push( [] );
+		}
+	
+		for ( i=0, iLen=aoLocal.length ; i<iLen ; i++ )
+		{
+			nLocalTr = aoLocal[i].nTr;
+	
+			/* All cells are going to be replaced, so empty out the row */
+			if ( nLocalTr )
+			{
+				while( (n = nLocalTr.firstChild) )
+				{
+					nLocalTr.removeChild( n );
+				}
+			}
+	
+			for ( j=0, jLen=aoLocal[i].length ; j<jLen ; j++ )
+			{
+				iRowspan = 1;
+				iColspan = 1;
+	
+				/* Check to see if there is already a cell (row/colspan) covering our target
+				 * insert point. If there is, then there is nothing to do.
+				 */
+				if ( aApplied[i][j] === undefined )
+				{
+					nLocalTr.appendChild( aoLocal[i][j].cell );
+					aApplied[i][j] = 1;
+	
+					/* Expand the cell to cover as many rows as needed */
+					while ( aoLocal[i+iRowspan] !== undefined &&
+					        aoLocal[i][j].cell == aoLocal[i+iRowspan][j].cell )
+					{
+						aApplied[i+iRowspan][j] = 1;
+						iRowspan++;
+					}
+	
+					/* Expand the cell to cover as many columns as needed */
+					while ( aoLocal[i][j+iColspan] !== undefined &&
+					        aoLocal[i][j].cell == aoLocal[i][j+iColspan].cell )
+					{
+						/* Must update the applied array over the rows for the columns */
+						for ( k=0 ; k<iRowspan ; k++ )
+						{
+							aApplied[i+k][j+iColspan] = 1;
+						}
+						iColspan++;
+					}
+	
+					/* Do the actual expansion in the DOM */
+					$(aoLocal[i][j].cell)
+						.attr('rowspan', iRowspan)
+						.attr('colspan', iColspan);
+				}
+			}
+		}
+	}
+	
+	
+	/**
+	 * Insert the required TR nodes into the table for display
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnDraw( oSettings )
+	{
+		/* Provide a pre-callback function which can be used to cancel the draw is false is returned */
+		var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] );
+		if ( $.inArray( false, aPreDraw ) !== -1 )
+		{
+			_fnProcessingDisplay( oSettings, false );
+			return;
+		}
+	
+		var i, iLen, n;
+		var anRows = [];
+		var iRowCount = 0;
+		var asStripeClasses = oSettings.asStripeClasses;
+		var iStripes = asStripeClasses.length;
+		var iOpenRows = oSettings.aoOpenRows.length;
+		var oLang = oSettings.oLanguage;
+		var iInitDisplayStart = oSettings.iInitDisplayStart;
+		var bServerSide = _fnDataSource( oSettings ) == 'ssp';
+		var aiDisplay = oSettings.aiDisplay;
+	
+		oSettings.bDrawing = true;
+	
+		/* Check and see if we have an initial draw position from state saving */
+		if ( iInitDisplayStart !== undefined && iInitDisplayStart !== -1 )
+		{
+			oSettings._iDisplayStart = bServerSide ?
+				iInitDisplayStart :
+				iInitDisplayStart >= oSettings.fnRecordsDisplay() ?
+					0 :
+					iInitDisplayStart;
+	
+			oSettings.iInitDisplayStart = -1;
+		}
+	
+		var iDisplayStart = oSettings._iDisplayStart;
+		var iDisplayEnd = oSettings.fnDisplayEnd();
+	
+		/* Server-side processing draw intercept */
+		if ( oSettings.bDeferLoading )
+		{
+			oSettings.bDeferLoading = false;
+			oSettings.iDraw++;
+			_fnProcessingDisplay( oSettings, false );
+		}
+		else if ( !bServerSide )
+		{
+			oSettings.iDraw++;
+		}
+		else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) )
+		{
+			return;
+		}
+	
+		if ( aiDisplay.length !== 0 )
+		{
+			var iStart = bServerSide ? 0 : iDisplayStart;
+			var iEnd = bServerSide ? oSettings.aoData.length : iDisplayEnd;
+	
+			for ( var j=iStart ; j<iEnd ; j++ )
+			{
+				var iDataIndex = aiDisplay[j];
+				var aoData = oSettings.aoData[ iDataIndex ];
+				if ( aoData.nTr === null )
+				{
+					_fnCreateTr( oSettings, iDataIndex );
+				}
+	
+				var nRow = aoData.nTr;
+	
+				/* Remove the old striping classes and then add the new one */
+				if ( iStripes !== 0 )
+				{
+					var sStripe = asStripeClasses[ iRowCount % iStripes ];
+					if ( aoData._sRowStripe != sStripe )
+					{
+						$(nRow).removeClass( aoData._sRowStripe ).addClass( sStripe );
+						aoData._sRowStripe = sStripe;
+					}
+				}
+	
+				// Row callback functions - might want to manipulate the row
+				// iRowCount and j are not currently documented. Are they at all
+				// useful?
+				_fnCallbackFire( oSettings, 'aoRowCallback', null,
+					[nRow, aoData._aData, iRowCount, j] );
+	
+				anRows.push( nRow );
+				iRowCount++;
+			}
+		}
+		else
+		{
+			/* Table is empty - create a row with an empty message in it */
+			var sZero = oLang.sZeroRecords;
+			if ( oSettings.iDraw == 1 &&  _fnDataSource( oSettings ) == 'ajax' )
+			{
+				sZero = oLang.sLoadingRecords;
+			}
+			else if ( oLang.sEmptyTable && oSettings.fnRecordsTotal() === 0 )
+			{
+				sZero = oLang.sEmptyTable;
+			}
+	
+			anRows[ 0 ] = $( '<tr/>', { 'class': iStripes ? asStripeClasses[0] : '' } )
+				.append( $('<td />', {
+					'valign':  'top',
+					'colSpan': _fnVisbleColumns( oSettings ),
+					'class':   oSettings.oClasses.sRowEmpty
+				} ).html( sZero ) )[0];
+		}
+	
+		/* Header and footer callbacks */
+		_fnCallbackFire( oSettings, 'aoHeaderCallback', 'header', [ $(oSettings.nTHead).children('tr')[0],
+			_fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] );
+	
+		_fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0],
+			_fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] );
+	
+		var body = $(oSettings.nTBody);
+	
+		body.children().detach();
+		body.append( $(anRows) );
+	
+		/* Call all required callback functions for the end of a draw */
+		_fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] );
+	
+		/* Draw is complete, sorting and filtering must be as well */
+		oSettings.bSorted = false;
+		oSettings.bFiltered = false;
+		oSettings.bDrawing = false;
+	}
+	
+	
+	/**
+	 * Redraw the table - taking account of the various features which are enabled
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {boolean} [holdPosition] Keep the current paging position. By default
+	 *    the paging is reset to the first page
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnReDraw( settings, holdPosition )
+	{
+		var
+			features = settings.oFeatures,
+			sort     = features.bSort,
+			filter   = features.bFilter;
+	
+		if ( sort ) {
+			_fnSort( settings );
+		}
+	
+		if ( filter ) {
+			_fnFilterComplete( settings, settings.oPreviousSearch );
+		}
+		else {
+			// No filtering, so we want to just use the display master
+			settings.aiDisplay = settings.aiDisplayMaster.slice();
+		}
+	
+		if ( holdPosition !== true ) {
+			settings._iDisplayStart = 0;
+		}
+	
+		// Let any modules know about the draw hold position state (used by
+		// scrolling internally)
+		settings._drawHold = holdPosition;
+	
+		_fnDraw( settings );
+	
+		settings._drawHold = false;
+	}
+	
+	
+	/**
+	 * Add the options to the page HTML for the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAddOptionsHtml ( oSettings )
+	{
+		var classes = oSettings.oClasses;
+		var table = $(oSettings.nTable);
+		var holding = $('<div/>').insertBefore( table ); // Holding element for speed
+		var features = oSettings.oFeatures;
+	
+		// All DataTables are wrapped in a div
+		var insert = $('<div/>', {
+			id:      oSettings.sTableId+'_wrapper',
+			'class': classes.sWrapper + (oSettings.nTFoot ? '' : ' '+classes.sNoFooter)
+		} );
+	
+		oSettings.nHolding = holding[0];
+		oSettings.nTableWrapper = insert[0];
+		oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling;
+	
+		/* Loop over the user set positioning and place the elements as needed */
+		var aDom = oSettings.sDom.split('');
+		var featureNode, cOption, nNewNode, cNext, sAttr, j;
+		for ( var i=0 ; i<aDom.length ; i++ )
+		{
+			featureNode = null;
+			cOption = aDom[i];
+	
+			if ( cOption == '<' )
+			{
+				/* New container div */
+				nNewNode = $('<div/>')[0];
+	
+				/* Check to see if we should append an id and/or a class name to the container */
+				cNext = aDom[i+1];
+				if ( cNext == "'" || cNext == '"' )
+				{
+					sAttr = "";
+					j = 2;
+					while ( aDom[i+j] != cNext )
+					{
+						sAttr += aDom[i+j];
+						j++;
+					}
+	
+					/* Replace jQuery UI constants @todo depreciated */
+					if ( sAttr == "H" )
+					{
+						sAttr = classes.sJUIHeader;
+					}
+					else if ( sAttr == "F" )
+					{
+						sAttr = classes.sJUIFooter;
+					}
+	
+					/* The attribute can be in the format of "#id.class", "#id" or "class" This logic
+					 * breaks the string into parts and applies them as needed
+					 */
+					if ( sAttr.indexOf('.') != -1 )
+					{
+						var aSplit = sAttr.split('.');
+						nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1);
+						nNewNode.className = aSplit[1];
+					}
+					else if ( sAttr.charAt(0) == "#" )
+					{
+						nNewNode.id = sAttr.substr(1, sAttr.length-1);
+					}
+					else
+					{
+						nNewNode.className = sAttr;
+					}
+	
+					i += j; /* Move along the position array */
+				}
+	
+				insert.append( nNewNode );
+				insert = $(nNewNode);
+			}
+			else if ( cOption == '>' )
+			{
+				/* End container div */
+				insert = insert.parent();
+			}
+			// @todo Move options into their own plugins?
+			else if ( cOption == 'l' && features.bPaginate && features.bLengthChange )
+			{
+				/* Length */
+				featureNode = _fnFeatureHtmlLength( oSettings );
+			}
+			else if ( cOption == 'f' && features.bFilter )
+			{
+				/* Filter */
+				featureNode = _fnFeatureHtmlFilter( oSettings );
+			}
+			else if ( cOption == 'r' && features.bProcessing )
+			{
+				/* pRocessing */
+				featureNode = _fnFeatureHtmlProcessing( oSettings );
+			}
+			else if ( cOption == 't' )
+			{
+				/* Table */
+				featureNode = _fnFeatureHtmlTable( oSettings );
+			}
+			else if ( cOption ==  'i' && features.bInfo )
+			{
+				/* Info */
+				featureNode = _fnFeatureHtmlInfo( oSettings );
+			}
+			else if ( cOption == 'p' && features.bPaginate )
+			{
+				/* Pagination */
+				featureNode = _fnFeatureHtmlPaginate( oSettings );
+			}
+			else if ( DataTable.ext.feature.length !== 0 )
+			{
+				/* Plug-in features */
+				var aoFeatures = DataTable.ext.feature;
+				for ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ )
+				{
+					if ( cOption == aoFeatures[k].cFeature )
+					{
+						featureNode = aoFeatures[k].fnInit( oSettings );
+						break;
+					}
+				}
+			}
+	
+			/* Add to the 2D features array */
+			if ( featureNode )
+			{
+				var aanFeatures = oSettings.aanFeatures;
+	
+				if ( ! aanFeatures[cOption] )
+				{
+					aanFeatures[cOption] = [];
+				}
+	
+				aanFeatures[cOption].push( featureNode );
+				insert.append( featureNode );
+			}
+		}
+	
+		/* Built our DOM structure - replace the holding div with what we want */
+		holding.replaceWith( insert );
+		oSettings.nHolding = null;
+	}
+	
+	
+	/**
+	 * Use the DOM source to create up an array of header cells. The idea here is to
+	 * create a layout grid (array) of rows x columns, which contains a reference
+	 * to the cell that that point in the grid (regardless of col/rowspan), such that
+	 * any column / row could be removed and the new grid constructed
+	 *  @param array {object} aLayout Array to store the calculated layout in
+	 *  @param {node} nThead The header/footer element for the table
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnDetectHeader ( aLayout, nThead )
+	{
+		var nTrs = $(nThead).children('tr');
+		var nTr, nCell;
+		var i, k, l, iLen, jLen, iColShifted, iColumn, iColspan, iRowspan;
+		var bUnique;
+		var fnShiftCol = function ( a, i, j ) {
+			var k = a[i];
+	                while ( k[j] ) {
+				j++;
+			}
+			return j;
+		};
+	
+		aLayout.splice( 0, aLayout.length );
+	
+		/* We know how many rows there are in the layout - so prep it */
+		for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+		{
+			aLayout.push( [] );
+		}
+	
+		/* Calculate a layout array */
+		for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+		{
+			nTr = nTrs[i];
+			iColumn = 0;
+	
+			/* For every cell in the row... */
+			nCell = nTr.firstChild;
+			while ( nCell ) {
+				if ( nCell.nodeName.toUpperCase() == "TD" ||
+				     nCell.nodeName.toUpperCase() == "TH" )
+				{
+					/* Get the col and rowspan attributes from the DOM and sanitise them */
+					iColspan = nCell.getAttribute('colspan') * 1;
+					iRowspan = nCell.getAttribute('rowspan') * 1;
+					iColspan = (!iColspan || iColspan===0 || iColspan===1) ? 1 : iColspan;
+					iRowspan = (!iRowspan || iRowspan===0 || iRowspan===1) ? 1 : iRowspan;
+	
+					/* There might be colspan cells already in this row, so shift our target
+					 * accordingly
+					 */
+					iColShifted = fnShiftCol( aLayout, i, iColumn );
+	
+					/* Cache calculation for unique columns */
+					bUnique = iColspan === 1 ? true : false;
+	
+					/* If there is col / rowspan, copy the information into the layout grid */
+					for ( l=0 ; l<iColspan ; l++ )
+					{
+						for ( k=0 ; k<iRowspan ; k++ )
+						{
+							aLayout[i+k][iColShifted+l] = {
+								"cell": nCell,
+								"unique": bUnique
+							};
+							aLayout[i+k].nTr = nTr;
+						}
+					}
+				}
+				nCell = nCell.nextSibling;
+			}
+		}
+	}
+	
+	
+	/**
+	 * Get an array of unique th elements, one for each column
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {node} nHeader automatically detect the layout from this node - optional
+	 *  @param {array} aLayout thead/tfoot layout from _fnDetectHeader - optional
+	 *  @returns array {node} aReturn list of unique th's
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetUniqueThs ( oSettings, nHeader, aLayout )
+	{
+		var aReturn = [];
+		if ( !aLayout )
+		{
+			aLayout = oSettings.aoHeader;
+			if ( nHeader )
+			{
+				aLayout = [];
+				_fnDetectHeader( aLayout, nHeader );
+			}
+		}
+	
+		for ( var i=0, iLen=aLayout.length ; i<iLen ; i++ )
+		{
+			for ( var j=0, jLen=aLayout[i].length ; j<jLen ; j++ )
+			{
+				if ( aLayout[i][j].unique &&
+					 (!aReturn[j] || !oSettings.bSortCellsTop) )
+				{
+					aReturn[j] = aLayout[i][j].cell;
+				}
+			}
+		}
+	
+		return aReturn;
+	}
+	
+	/**
+	 * Create an Ajax call based on the table's settings, taking into account that
+	 * parameters can have multiple forms, and backwards compatibility.
+	 *
+	 * @param {object} oSettings dataTables settings object
+	 * @param {array} data Data to send to the server, required by
+	 *     DataTables - may be augmented by developer callbacks
+	 * @param {function} fn Callback function to run when data is obtained
+	 */
+	function _fnBuildAjax( oSettings, data, fn )
+	{
+		// Compatibility with 1.9-, allow fnServerData and event to manipulate
+		_fnCallbackFire( oSettings, 'aoServerParams', 'serverParams', [data] );
+	
+		// Convert to object based for 1.10+ if using the old array scheme which can
+		// come from server-side processing or serverParams
+		if ( data && $.isArray(data) ) {
+			var tmp = {};
+			var rbracket = /(.*?)\[\]$/;
+	
+			$.each( data, function (key, val) {
+				var match = val.name.match(rbracket);
+	
+				if ( match ) {
+					// Support for arrays
+					var name = match[0];
+	
+					if ( ! tmp[ name ] ) {
+						tmp[ name ] = [];
+					}
+					tmp[ name ].push( val.value );
+				}
+				else {
+					tmp[val.name] = val.value;
+				}
+			} );
+			data = tmp;
+		}
+	
+		var ajaxData;
+		var ajax = oSettings.ajax;
+		var instance = oSettings.oInstance;
+		var callback = function ( json ) {
+			_fnCallbackFire( oSettings, null, 'xhr', [oSettings, json, oSettings.jqXHR] );
+			fn( json );
+		};
+	
+		if ( $.isPlainObject( ajax ) && ajax.data )
+		{
+			ajaxData = ajax.data;
+	
+			var newData = $.isFunction( ajaxData ) ?
+				ajaxData( data, oSettings ) :  // fn can manipulate data or return
+				ajaxData;                      // an object object or array to merge
+	
+			// If the function returned something, use that alone
+			data = $.isFunction( ajaxData ) && newData ?
+				newData :
+				$.extend( true, data, newData );
+	
+			// Remove the data property as we've resolved it already and don't want
+			// jQuery to do it again (it is restored at the end of the function)
+			delete ajax.data;
+		}
+	
+		var baseAjax = {
+			"data": data,
+			"success": function (json) {
+				var error = json.error || json.sError;
+				if ( error ) {
+					_fnLog( oSettings, 0, error );
+				}
+	
+				oSettings.json = json;
+				callback( json );
+			},
+			"dataType": "json",
+			"cache": false,
+			"type": oSettings.sServerMethod,
+			"error": function (xhr, error, thrown) {
+				var ret = _fnCallbackFire( oSettings, null, 'xhr', [oSettings, null, oSettings.jqXHR] );
+	
+				if ( $.inArray( true, ret ) === -1 ) {
+					if ( error == "parsererror" ) {
+						_fnLog( oSettings, 0, 'Invalid JSON response', 1 );
+					}
+					else if ( xhr.readyState === 4 ) {
+						_fnLog( oSettings, 0, 'Ajax error', 7 );
+					}
+				}
+	
+				_fnProcessingDisplay( oSettings, false );
+			}
+		};
+	
+		// Store the data submitted for the API
+		oSettings.oAjaxData = data;
+	
+		// Allow plug-ins and external processes to modify the data
+		_fnCallbackFire( oSettings, null, 'preXhr', [oSettings, data] );
+	
+		if ( oSettings.fnServerData )
+		{
+			// DataTables 1.9- compatibility
+			oSettings.fnServerData.call( instance,
+				oSettings.sAjaxSource,
+				$.map( data, function (val, key) { // Need to convert back to 1.9 trad format
+					return { name: key, value: val };
+				} ),
+				callback,
+				oSettings
+			);
+		}
+		else if ( oSettings.sAjaxSource || typeof ajax === 'string' )
+		{
+			// DataTables 1.9- compatibility
+			oSettings.jqXHR = $.ajax( $.extend( baseAjax, {
+				url: ajax || oSettings.sAjaxSource
+			} ) );
+		}
+		else if ( $.isFunction( ajax ) )
+		{
+			// Is a function - let the caller define what needs to be done
+			oSettings.jqXHR = ajax.call( instance, data, callback, oSettings );
+		}
+		else
+		{
+			// Object to extend the base settings
+			oSettings.jqXHR = $.ajax( $.extend( baseAjax, ajax ) );
+	
+			// Restore for next time around
+			ajax.data = ajaxData;
+		}
+	}
+	
+	
+	/**
+	 * Update the table using an Ajax call
+	 *  @param {object} settings dataTables settings object
+	 *  @returns {boolean} Block the table drawing or not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAjaxUpdate( settings )
+	{
+		if ( settings.bAjaxDataGet ) {
+			settings.iDraw++;
+			_fnProcessingDisplay( settings, true );
+	
+			_fnBuildAjax(
+				settings,
+				_fnAjaxParameters( settings ),
+				function(json) {
+					_fnAjaxUpdateDraw( settings, json );
+				}
+			);
+	
+			return false;
+		}
+		return true;
+	}
+	
+	
+	/**
+	 * Build up the parameters in an object needed for a server-side processing
+	 * request. Note that this is basically done twice, is different ways - a modern
+	 * method which is used by default in DataTables 1.10 which uses objects and
+	 * arrays, or the 1.9- method with is name / value pairs. 1.9 method is used if
+	 * the sAjaxSource option is used in the initialisation, or the legacyAjax
+	 * option is set.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns {bool} block the table drawing or not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAjaxParameters( settings )
+	{
+		var
+			columns = settings.aoColumns,
+			columnCount = columns.length,
+			features = settings.oFeatures,
+			preSearch = settings.oPreviousSearch,
+			preColSearch = settings.aoPreSearchCols,
+			i, data = [], dataProp, column, columnSearch,
+			sort = _fnSortFlatten( settings ),
+			displayStart = settings._iDisplayStart,
+			displayLength = features.bPaginate !== false ?
+				settings._iDisplayLength :
+				-1;
+	
+		var param = function ( name, value ) {
+			data.push( { 'name': name, 'value': value } );
+		};
+	
+		// DataTables 1.9- compatible method
+		param( 'sEcho',          settings.iDraw );
+		param( 'iColumns',       columnCount );
+		param( 'sColumns',       _pluck( columns, 'sName' ).join(',') );
+		param( 'iDisplayStart',  displayStart );
+		param( 'iDisplayLength', displayLength );
+	
+		// DataTables 1.10+ method
+		var d = {
+			draw:    settings.iDraw,
+			columns: [],
+			order:   [],
+			start:   displayStart,
+			length:  displayLength,
+			search:  {
+				value: preSearch.sSearch,
+				regex: preSearch.bRegex
+			}
+		};
+	
+		for ( i=0 ; i<columnCount ; i++ ) {
+			column = columns[i];
+			columnSearch = preColSearch[i];
+			dataProp = typeof column.mData=="function" ? 'function' : column.mData ;
+	
+			d.columns.push( {
+				data:       dataProp,
+				name:       column.sName,
+				searchable: column.bSearchable,
+				orderable:  column.bSortable,
+				search:     {
+					value: columnSearch.sSearch,
+					regex: columnSearch.bRegex
+				}
+			} );
+	
+			param( "mDataProp_"+i, dataProp );
+	
+			if ( features.bFilter ) {
+				param( 'sSearch_'+i,     columnSearch.sSearch );
+				param( 'bRegex_'+i,      columnSearch.bRegex );
+				param( 'bSearchable_'+i, column.bSearchable );
+			}
+	
+			if ( features.bSort ) {
+				param( 'bSortable_'+i, column.bSortable );
+			}
+		}
+	
+		if ( features.bFilter ) {
+			param( 'sSearch', preSearch.sSearch );
+			param( 'bRegex', preSearch.bRegex );
+		}
+	
+		if ( features.bSort ) {
+			$.each( sort, function ( i, val ) {
+				d.order.push( { column: val.col, dir: val.dir } );
+	
+				param( 'iSortCol_'+i, val.col );
+				param( 'sSortDir_'+i, val.dir );
+			} );
+	
+			param( 'iSortingCols', sort.length );
+		}
+	
+		// If the legacy.ajax parameter is null, then we automatically decide which
+		// form to use, based on sAjaxSource
+		var legacy = DataTable.ext.legacy.ajax;
+		if ( legacy === null ) {
+			return settings.sAjaxSource ? data : d;
+		}
+	
+		// Otherwise, if legacy has been specified then we use that to decide on the
+		// form
+		return legacy ? data : d;
+	}
+	
+	
+	/**
+	 * Data the data from the server (nuking the old) and redraw the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {object} json json data return from the server.
+	 *  @param {string} json.sEcho Tracking flag for DataTables to match requests
+	 *  @param {int} json.iTotalRecords Number of records in the data set, not accounting for filtering
+	 *  @param {int} json.iTotalDisplayRecords Number of records in the data set, accounting for filtering
+	 *  @param {array} json.aaData The data to display on this page
+	 *  @param {string} [json.sColumns] Column ordering (sName, comma separated)
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAjaxUpdateDraw ( settings, json )
+	{
+		// v1.10 uses camelCase variables, while 1.9 uses Hungarian notation.
+		// Support both
+		var compat = function ( old, modern ) {
+			return json[old] !== undefined ? json[old] : json[modern];
+		};
+	
+		var data = _fnAjaxDataSrc( settings, json );
+		var draw            = compat( 'sEcho',                'draw' );
+		var recordsTotal    = compat( 'iTotalRecords',        'recordsTotal' );
+		var recordsFiltered = compat( 'iTotalDisplayRecords', 'recordsFiltered' );
+	
+		if ( draw ) {
+			// Protect against out of sequence returns
+			if ( draw*1 < settings.iDraw ) {
+				return;
+			}
+			settings.iDraw = draw * 1;
+		}
+	
+		_fnClearTable( settings );
+		settings._iRecordsTotal   = parseInt(recordsTotal, 10);
+		settings._iRecordsDisplay = parseInt(recordsFiltered, 10);
+	
+		for ( var i=0, ien=data.length ; i<ien ; i++ ) {
+			_fnAddData( settings, data[i] );
+		}
+		settings.aiDisplay = settings.aiDisplayMaster.slice();
+	
+		settings.bAjaxDataGet = false;
+		_fnDraw( settings );
+	
+		if ( ! settings._bInitComplete ) {
+			_fnInitComplete( settings, json );
+		}
+	
+		settings.bAjaxDataGet = true;
+		_fnProcessingDisplay( settings, false );
+	}
+	
+	
+	/**
+	 * Get the data from the JSON data source to use for drawing a table. Using
+	 * `_fnGetObjectDataFn` allows the data to be sourced from a property of the
+	 * source object, or from a processing function.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param  {object} json Data source object / array from the server
+	 *  @return {array} Array of data to use
+	 */
+	function _fnAjaxDataSrc ( oSettings, json )
+	{
+		var dataSrc = $.isPlainObject( oSettings.ajax ) && oSettings.ajax.dataSrc !== undefined ?
+			oSettings.ajax.dataSrc :
+			oSettings.sAjaxDataProp; // Compatibility with 1.9-.
+	
+		// Compatibility with 1.9-. In order to read from aaData, check if the
+		// default has been changed, if not, check for aaData
+		if ( dataSrc === 'data' ) {
+			return json.aaData || json[dataSrc];
+		}
+	
+		return dataSrc !== "" ?
+			_fnGetObjectDataFn( dataSrc )( json ) :
+			json;
+	}
+	
+	/**
+	 * Generate the node required for filtering text
+	 *  @returns {node} Filter control element
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlFilter ( settings )
+	{
+		var classes = settings.oClasses;
+		var tableId = settings.sTableId;
+		var language = settings.oLanguage;
+		var previousSearch = settings.oPreviousSearch;
+		var features = settings.aanFeatures;
+		var input = '<input type="search" class="'+classes.sFilterInput+'"/>';
+	
+		var str = language.sSearch;
+		str = str.match(/_INPUT_/) ?
+			str.replace('_INPUT_', input) :
+			str+input;
+	
+		var filter = $('<div/>', {
+				'id': ! features.f ? tableId+'_filter' : null,
+				'class': classes.sFilter
+			} )
+			.append( $('<label/>' ).append( str ) );
+	
+		var searchFn = function() {
+			/* Update all other filter input elements for the new display */
+			var n = features.f;
+			var val = !this.value ? "" : this.value; // mental IE8 fix :-(
+	
+			/* Now do the filter */
+			if ( val != previousSearch.sSearch ) {
+				_fnFilterComplete( settings, {
+					"sSearch": val,
+					"bRegex": previousSearch.bRegex,
+					"bSmart": previousSearch.bSmart ,
+					"bCaseInsensitive": previousSearch.bCaseInsensitive
+				} );
+	
+				// Need to redraw, without resorting
+				settings._iDisplayStart = 0;
+				_fnDraw( settings );
+			}
+		};
+	
+		var searchDelay = settings.searchDelay !== null ?
+			settings.searchDelay :
+			_fnDataSource( settings ) === 'ssp' ?
+				400 :
+				0;
+	
+		var jqFilter = $('input', filter)
+			.val( previousSearch.sSearch )
+			.attr( 'placeholder', language.sSearchPlaceholder )
+			.on(
+				'keyup.DT search.DT input.DT paste.DT cut.DT',
+				searchDelay ?
+					_fnThrottle( searchFn, searchDelay ) :
+					searchFn
+			)
+			.on( 'keypress.DT', function(e) {
+				/* Prevent form submission */
+				if ( e.keyCode == 13 ) {
+					return false;
+				}
+			} )
+			.attr('aria-controls', tableId);
+	
+		// Update the input elements whenever the table is filtered
+		$(settings.nTable).on( 'search.dt.DT', function ( ev, s ) {
+			if ( settings === s ) {
+				// IE9 throws an 'unknown error' if document.activeElement is used
+				// inside an iframe or frame...
+				try {
+					if ( jqFilter[0] !== document.activeElement ) {
+						jqFilter.val( previousSearch.sSearch );
+					}
+				}
+				catch ( e ) {}
+			}
+		} );
+	
+		return filter[0];
+	}
+	
+	
+	/**
+	 * Filter the table using both the global filter and column based filtering
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {object} oSearch search information
+	 *  @param {int} [iForce] force a research of the master array (1) or not (undefined or 0)
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFilterComplete ( oSettings, oInput, iForce )
+	{
+		var oPrevSearch = oSettings.oPreviousSearch;
+		var aoPrevSearch = oSettings.aoPreSearchCols;
+		var fnSaveFilter = function ( oFilter ) {
+			/* Save the filtering values */
+			oPrevSearch.sSearch = oFilter.sSearch;
+			oPrevSearch.bRegex = oFilter.bRegex;
+			oPrevSearch.bSmart = oFilter.bSmart;
+			oPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive;
+		};
+		var fnRegex = function ( o ) {
+			// Backwards compatibility with the bEscapeRegex option
+			return o.bEscapeRegex !== undefined ? !o.bEscapeRegex : o.bRegex;
+		};
+	
+		// Resolve any column types that are unknown due to addition or invalidation
+		// @todo As per sort - can this be moved into an event handler?
+		_fnColumnTypes( oSettings );
+	
+		/* In server-side processing all filtering is done by the server, so no point hanging around here */
+		if ( _fnDataSource( oSettings ) != 'ssp' )
+		{
+			/* Global filter */
+			_fnFilter( oSettings, oInput.sSearch, iForce, fnRegex(oInput), oInput.bSmart, oInput.bCaseInsensitive );
+			fnSaveFilter( oInput );
+	
+			/* Now do the individual column filter */
+			for ( var i=0 ; i<aoPrevSearch.length ; i++ )
+			{
+				_fnFilterColumn( oSettings, aoPrevSearch[i].sSearch, i, fnRegex(aoPrevSearch[i]),
+					aoPrevSearch[i].bSmart, aoPrevSearch[i].bCaseInsensitive );
+			}
+	
+			/* Custom filtering */
+			_fnFilterCustom( oSettings );
+		}
+		else
+		{
+			fnSaveFilter( oInput );
+		}
+	
+		/* Tell the draw function we have been filtering */
+		oSettings.bFiltered = true;
+		_fnCallbackFire( oSettings, null, 'search', [oSettings] );
+	}
+	
+	
+	/**
+	 * Apply custom filtering functions
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFilterCustom( settings )
+	{
+		var filters = DataTable.ext.search;
+		var displayRows = settings.aiDisplay;
+		var row, rowIdx;
+	
+		for ( var i=0, ien=filters.length ; i<ien ; i++ ) {
+			var rows = [];
+	
+			// Loop over each row and see if it should be included
+			for ( var j=0, jen=displayRows.length ; j<jen ; j++ ) {
+				rowIdx = displayRows[ j ];
+				row = settings.aoData[ rowIdx ];
+	
+				if ( filters[i]( settings, row._aFilterData, rowIdx, row._aData, j ) ) {
+					rows.push( rowIdx );
+				}
+			}
+	
+			// So the array reference doesn't break set the results into the
+			// existing array
+			displayRows.length = 0;
+			$.merge( displayRows, rows );
+		}
+	}
+	
+	
+	/**
+	 * Filter the table on a per-column basis
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {string} sInput string to filter on
+	 *  @param {int} iColumn column to filter
+	 *  @param {bool} bRegex treat search string as a regular expression or not
+	 *  @param {bool} bSmart use smart filtering or not
+	 *  @param {bool} bCaseInsensitive Do case insenstive matching or not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFilterColumn ( settings, searchStr, colIdx, regex, smart, caseInsensitive )
+	{
+		if ( searchStr === '' ) {
+			return;
+		}
+	
+		var data;
+		var out = [];
+		var display = settings.aiDisplay;
+		var rpSearch = _fnFilterCreateSearch( searchStr, regex, smart, caseInsensitive );
+	
+		for ( var i=0 ; i<display.length ; i++ ) {
+			data = settings.aoData[ display[i] ]._aFilterData[ colIdx ];
+	
+			if ( rpSearch.test( data ) ) {
+				out.push( display[i] );
+			}
+		}
+	
+		settings.aiDisplay = out;
+	}
+	
+	
+	/**
+	 * Filter the data table based on user input and draw the table
+	 *  @param {object} settings dataTables settings object
+	 *  @param {string} input string to filter on
+	 *  @param {int} force optional - force a research of the master array (1) or not (undefined or 0)
+	 *  @param {bool} regex treat as a regular expression or not
+	 *  @param {bool} smart perform smart filtering or not
+	 *  @param {bool} caseInsensitive Do case insenstive matching or not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFilter( settings, input, force, regex, smart, caseInsensitive )
+	{
+		var rpSearch = _fnFilterCreateSearch( input, regex, smart, caseInsensitive );
+		var prevSearch = settings.oPreviousSearch.sSearch;
+		var displayMaster = settings.aiDisplayMaster;
+		var display, invalidated, i;
+		var filtered = [];
+	
+		// Need to take account of custom filtering functions - always filter
+		if ( DataTable.ext.search.length !== 0 ) {
+			force = true;
+		}
+	
+		// Check if any of the rows were invalidated
+		invalidated = _fnFilterData( settings );
+	
+		// If the input is blank - we just want the full data set
+		if ( input.length <= 0 ) {
+			settings.aiDisplay = displayMaster.slice();
+		}
+		else {
+			// New search - start from the master array
+			if ( invalidated ||
+				 force ||
+				 prevSearch.length > input.length ||
+				 input.indexOf(prevSearch) !== 0 ||
+				 settings.bSorted // On resort, the display master needs to be
+				                  // re-filtered since indexes will have changed
+			) {
+				settings.aiDisplay = displayMaster.slice();
+			}
+	
+			// Search the display array
+			display = settings.aiDisplay;
+	
+			for ( i=0 ; i<display.length ; i++ ) {
+				if ( rpSearch.test( settings.aoData[ display[i] ]._sFilterRow ) ) {
+					filtered.push( display[i] );
+				}
+			}
+	
+			settings.aiDisplay = filtered;
+		}
+	}
+	
+	
+	/**
+	 * Build a regular expression object suitable for searching a table
+	 *  @param {string} sSearch string to search for
+	 *  @param {bool} bRegex treat as a regular expression or not
+	 *  @param {bool} bSmart perform smart filtering or not
+	 *  @param {bool} bCaseInsensitive Do case insensitive matching or not
+	 *  @returns {RegExp} constructed object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFilterCreateSearch( search, regex, smart, caseInsensitive )
+	{
+		search = regex ?
+			search :
+			_fnEscapeRegex( search );
+		
+		if ( smart ) {
+			/* For smart filtering we want to allow the search to work regardless of
+			 * word order. We also want double quoted text to be preserved, so word
+			 * order is important - a la google. So this is what we want to
+			 * generate:
+			 * 
+			 * ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$
+			 */
+			var a = $.map( search.match( /"[^"]+"|[^ ]+/g ) || [''], function ( word ) {
+				if ( word.charAt(0) === '"' ) {
+					var m = word.match( /^"(.*)"$/ );
+					word = m ? m[1] : word;
+				}
+	
+				return word.replace('"', '');
+			} );
+	
+			search = '^(?=.*?'+a.join( ')(?=.*?' )+').*$';
+		}
+	
+		return new RegExp( search, caseInsensitive ? 'i' : '' );
+	}
+	
+	
+	/**
+	 * Escape a string such that it can be used in a regular expression
+	 *  @param {string} sVal string to escape
+	 *  @returns {string} escaped string
+	 *  @memberof DataTable#oApi
+	 */
+	var _fnEscapeRegex = DataTable.util.escapeRegex;
+	
+	var __filter_div = $('<div>')[0];
+	var __filter_div_textContent = __filter_div.textContent !== undefined;
+	
+	// Update the filtering data for each row if needed (by invalidation or first run)
+	function _fnFilterData ( settings )
+	{
+		var columns = settings.aoColumns;
+		var column;
+		var i, j, ien, jen, filterData, cellData, row;
+		var fomatters = DataTable.ext.type.search;
+		var wasInvalidated = false;
+	
+		for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
+			row = settings.aoData[i];
+	
+			if ( ! row._aFilterData ) {
+				filterData = [];
+	
+				for ( j=0, jen=columns.length ; j<jen ; j++ ) {
+					column = columns[j];
+	
+					if ( column.bSearchable ) {
+						cellData = _fnGetCellData( settings, i, j, 'filter' );
+	
+						if ( fomatters[ column.sType ] ) {
+							cellData = fomatters[ column.sType ]( cellData );
+						}
+	
+						// Search in DataTables 1.10 is string based. In 1.11 this
+						// should be altered to also allow strict type checking.
+						if ( cellData === null ) {
+							cellData = '';
+						}
+	
+						if ( typeof cellData !== 'string' && cellData.toString ) {
+							cellData = cellData.toString();
+						}
+					}
+					else {
+						cellData = '';
+					}
+	
+					// If it looks like there is an HTML entity in the string,
+					// attempt to decode it so sorting works as expected. Note that
+					// we could use a single line of jQuery to do this, but the DOM
+					// method used here is much faster http://jsperf.com/html-decode
+					if ( cellData.indexOf && cellData.indexOf('&') !== -1 ) {
+						__filter_div.innerHTML = cellData;
+						cellData = __filter_div_textContent ?
+							__filter_div.textContent :
+							__filter_div.innerText;
+					}
+	
+					if ( cellData.replace ) {
+						cellData = cellData.replace(/[\r\n]/g, '');
+					}
+	
+					filterData.push( cellData );
+				}
+	
+				row._aFilterData = filterData;
+				row._sFilterRow = filterData.join('  ');
+				wasInvalidated = true;
+			}
+		}
+	
+		return wasInvalidated;
+	}
+	
+	
+	/**
+	 * Convert from the internal Hungarian notation to camelCase for external
+	 * interaction
+	 *  @param {object} obj Object to convert
+	 *  @returns {object} Inverted object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSearchToCamel ( obj )
+	{
+		return {
+			search:          obj.sSearch,
+			smart:           obj.bSmart,
+			regex:           obj.bRegex,
+			caseInsensitive: obj.bCaseInsensitive
+		};
+	}
+	
+	
+	
+	/**
+	 * Convert from camelCase notation to the internal Hungarian. We could use the
+	 * Hungarian convert function here, but this is cleaner
+	 *  @param {object} obj Object to convert
+	 *  @returns {object} Inverted object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSearchToHung ( obj )
+	{
+		return {
+			sSearch:          obj.search,
+			bSmart:           obj.smart,
+			bRegex:           obj.regex,
+			bCaseInsensitive: obj.caseInsensitive
+		};
+	}
+	
+	/**
+	 * Generate the node required for the info display
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns {node} Information element
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlInfo ( settings )
+	{
+		var
+			tid = settings.sTableId,
+			nodes = settings.aanFeatures.i,
+			n = $('<div/>', {
+				'class': settings.oClasses.sInfo,
+				'id': ! nodes ? tid+'_info' : null
+			} );
+	
+		if ( ! nodes ) {
+			// Update display on each draw
+			settings.aoDrawCallback.push( {
+				"fn": _fnUpdateInfo,
+				"sName": "information"
+			} );
+	
+			n
+				.attr( 'role', 'status' )
+				.attr( 'aria-live', 'polite' );
+	
+			// Table is described by our info div
+			$(settings.nTable).attr( 'aria-describedby', tid+'_info' );
+		}
+	
+		return n[0];
+	}
+	
+	
+	/**
+	 * Update the information elements in the display
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnUpdateInfo ( settings )
+	{
+		/* Show information about the table */
+		var nodes = settings.aanFeatures.i;
+		if ( nodes.length === 0 ) {
+			return;
+		}
+	
+		var
+			lang  = settings.oLanguage,
+			start = settings._iDisplayStart+1,
+			end   = settings.fnDisplayEnd(),
+			max   = settings.fnRecordsTotal(),
+			total = settings.fnRecordsDisplay(),
+			out   = total ?
+				lang.sInfo :
+				lang.sInfoEmpty;
+	
+		if ( total !== max ) {
+			/* Record set after filtering */
+			out += ' ' + lang.sInfoFiltered;
+		}
+	
+		// Convert the macros
+		out += lang.sInfoPostFix;
+		out = _fnInfoMacros( settings, out );
+	
+		var callback = lang.fnInfoCallback;
+		if ( callback !== null ) {
+			out = callback.call( settings.oInstance,
+				settings, start, end, max, total, out
+			);
+		}
+	
+		$(nodes).html( out );
+	}
+	
+	
+	function _fnInfoMacros ( settings, str )
+	{
+		// When infinite scrolling, we are always starting at 1. _iDisplayStart is used only
+		// internally
+		var
+			formatter  = settings.fnFormatNumber,
+			start      = settings._iDisplayStart+1,
+			len        = settings._iDisplayLength,
+			vis        = settings.fnRecordsDisplay(),
+			all        = len === -1;
+	
+		return str.
+			replace(/_START_/g, formatter.call( settings, start ) ).
+			replace(/_END_/g,   formatter.call( settings, settings.fnDisplayEnd() ) ).
+			replace(/_MAX_/g,   formatter.call( settings, settings.fnRecordsTotal() ) ).
+			replace(/_TOTAL_/g, formatter.call( settings, vis ) ).
+			replace(/_PAGE_/g,  formatter.call( settings, all ? 1 : Math.ceil( start / len ) ) ).
+			replace(/_PAGES_/g, formatter.call( settings, all ? 1 : Math.ceil( vis / len ) ) );
+	}
+	
+	
+	
+	/**
+	 * Draw the table for the first time, adding all required features
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnInitialise ( settings )
+	{
+		var i, iLen, iAjaxStart=settings.iInitDisplayStart;
+		var columns = settings.aoColumns, column;
+		var features = settings.oFeatures;
+		var deferLoading = settings.bDeferLoading; // value modified by the draw
+	
+		/* Ensure that the table data is fully initialised */
+		if ( ! settings.bInitialised ) {
+			setTimeout( function(){ _fnInitialise( settings ); }, 200 );
+			return;
+		}
+	
+		/* Show the display HTML options */
+		_fnAddOptionsHtml( settings );
+	
+		/* Build and draw the header / footer for the table */
+		_fnBuildHead( settings );
+		_fnDrawHead( settings, settings.aoHeader );
+		_fnDrawHead( settings, settings.aoFooter );
+	
+		/* Okay to show that something is going on now */
+		_fnProcessingDisplay( settings, true );
+	
+		/* Calculate sizes for columns */
+		if ( features.bAutoWidth ) {
+			_fnCalculateColumnWidths( settings );
+		}
+	
+		for ( i=0, iLen=columns.length ; i<iLen ; i++ ) {
+			column = columns[i];
+	
+			if ( column.sWidth ) {
+				column.nTh.style.width = _fnStringToCss( column.sWidth );
+			}
+		}
+	
+		_fnCallbackFire( settings, null, 'preInit', [settings] );
+	
+		// If there is default sorting required - let's do it. The sort function
+		// will do the drawing for us. Otherwise we draw the table regardless of the
+		// Ajax source - this allows the table to look initialised for Ajax sourcing
+		// data (show 'loading' message possibly)
+		_fnReDraw( settings );
+	
+		// Server-side processing init complete is done by _fnAjaxUpdateDraw
+		var dataSrc = _fnDataSource( settings );
+		if ( dataSrc != 'ssp' || deferLoading ) {
+			// if there is an ajax source load the data
+			if ( dataSrc == 'ajax' ) {
+				_fnBuildAjax( settings, [], function(json) {
+					var aData = _fnAjaxDataSrc( settings, json );
+	
+					// Got the data - add it to the table
+					for ( i=0 ; i<aData.length ; i++ ) {
+						_fnAddData( settings, aData[i] );
+					}
+	
+					// Reset the init display for cookie saving. We've already done
+					// a filter, and therefore cleared it before. So we need to make
+					// it appear 'fresh'
+					settings.iInitDisplayStart = iAjaxStart;
+	
+					_fnReDraw( settings );
+	
+					_fnProcessingDisplay( settings, false );
+					_fnInitComplete( settings, json );
+				}, settings );
+			}
+			else {
+				_fnProcessingDisplay( settings, false );
+				_fnInitComplete( settings );
+			}
+		}
+	}
+	
+	
+	/**
+	 * Draw the table for the first time, adding all required features
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {object} [json] JSON from the server that completed the table, if using Ajax source
+	 *    with client-side processing (optional)
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnInitComplete ( settings, json )
+	{
+		settings._bInitComplete = true;
+	
+		// When data was added after the initialisation (data or Ajax) we need to
+		// calculate the column sizing
+		if ( json || settings.oInit.aaData ) {
+			_fnAdjustColumnSizing( settings );
+		}
+	
+		_fnCallbackFire( settings, null, 'plugin-init', [settings, json] );
+		_fnCallbackFire( settings, 'aoInitComplete', 'init', [settings, json] );
+	}
+	
+	
+	function _fnLengthChange ( settings, val )
+	{
+		var len = parseInt( val, 10 );
+		settings._iDisplayLength = len;
+	
+		_fnLengthOverflow( settings );
+	
+		// Fire length change event
+		_fnCallbackFire( settings, null, 'length', [settings, len] );
+	}
+	
+	
+	/**
+	 * Generate the node required for user display length changing
+	 *  @param {object} settings dataTables settings object
+	 *  @returns {node} Display length feature node
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlLength ( settings )
+	{
+		var
+			classes  = settings.oClasses,
+			tableId  = settings.sTableId,
+			menu     = settings.aLengthMenu,
+			d2       = $.isArray( menu[0] ),
+			lengths  = d2 ? menu[0] : menu,
+			language = d2 ? menu[1] : menu;
+	
+		var select = $('<select/>', {
+			'name':          tableId+'_length',
+			'aria-controls': tableId,
+			'class':         classes.sLengthSelect
+		} );
+	
+		for ( var i=0, ien=lengths.length ; i<ien ; i++ ) {
+			select[0][ i ] = new Option( language[i], lengths[i] );
+		}
+	
+		var div = $('<div><label/></div>').addClass( classes.sLength );
+		if ( ! settings.aanFeatures.l ) {
+			div[0].id = tableId+'_length';
+		}
+	
+		div.children().append(
+			settings.oLanguage.sLengthMenu.replace( '_MENU_', select[0].outerHTML )
+		);
+	
+		// Can't use `select` variable as user might provide their own and the
+		// reference is broken by the use of outerHTML
+		$('select', div)
+			.val( settings._iDisplayLength )
+			.on( 'change.DT', function(e) {
+				_fnLengthChange( settings, $(this).val() );
+				_fnDraw( settings );
+			} );
+	
+		// Update node value whenever anything changes the table's length
+		$(settings.nTable).on( 'length.dt.DT', function (e, s, len) {
+			if ( settings === s ) {
+				$('select', div).val( len );
+			}
+		} );
+	
+		return div[0];
+	}
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Note that most of the paging logic is done in
+	 * DataTable.ext.pager
+	 */
+	
+	/**
+	 * Generate the node required for default pagination
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns {node} Pagination feature node
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlPaginate ( settings )
+	{
+		var
+			type   = settings.sPaginationType,
+			plugin = DataTable.ext.pager[ type ],
+			modern = typeof plugin === 'function',
+			redraw = function( settings ) {
+				_fnDraw( settings );
+			},
+			node = $('<div/>').addClass( settings.oClasses.sPaging + type )[0],
+			features = settings.aanFeatures;
+	
+		if ( ! modern ) {
+			plugin.fnInit( settings, node, redraw );
+		}
+	
+		/* Add a draw callback for the pagination on first instance, to update the paging display */
+		if ( ! features.p )
+		{
+			node.id = settings.sTableId+'_paginate';
+	
+			settings.aoDrawCallback.push( {
+				"fn": function( settings ) {
+					if ( modern ) {
+						var
+							start      = settings._iDisplayStart,
+							len        = settings._iDisplayLength,
+							visRecords = settings.fnRecordsDisplay(),
+							all        = len === -1,
+							page = all ? 0 : Math.ceil( start / len ),
+							pages = all ? 1 : Math.ceil( visRecords / len ),
+							buttons = plugin(page, pages),
+							i, ien;
+	
+						for ( i=0, ien=features.p.length ; i<ien ; i++ ) {
+							_fnRenderer( settings, 'pageButton' )(
+								settings, features.p[i], i, buttons, page, pages
+							);
+						}
+					}
+					else {
+						plugin.fnUpdate( settings, redraw );
+					}
+				},
+				"sName": "pagination"
+			} );
+		}
+	
+		return node;
+	}
+	
+	
+	/**
+	 * Alter the display settings to change the page
+	 *  @param {object} settings DataTables settings object
+	 *  @param {string|int} action Paging action to take: "first", "previous",
+	 *    "next" or "last" or page number to jump to (integer)
+	 *  @param [bool] redraw Automatically draw the update or not
+	 *  @returns {bool} true page has changed, false - no change
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnPageChange ( settings, action, redraw )
+	{
+		var
+			start     = settings._iDisplayStart,
+			len       = settings._iDisplayLength,
+			records   = settings.fnRecordsDisplay();
+	
+		if ( records === 0 || len === -1 )
+		{
+			start = 0;
+		}
+		else if ( typeof action === "number" )
+		{
+			start = action * len;
+	
+			if ( start > records )
+			{
+				start = 0;
+			}
+		}
+		else if ( action == "first" )
+		{
+			start = 0;
+		}
+		else if ( action == "previous" )
+		{
+			start = len >= 0 ?
+				start - len :
+				0;
+	
+			if ( start < 0 )
+			{
+			  start = 0;
+			}
+		}
+		else if ( action == "next" )
+		{
+			if ( start + len < records )
+			{
+				start += len;
+			}
+		}
+		else if ( action == "last" )
+		{
+			start = Math.floor( (records-1) / len) * len;
+		}
+		else
+		{
+			_fnLog( settings, 0, "Unknown paging action: "+action, 5 );
+		}
+	
+		var changed = settings._iDisplayStart !== start;
+		settings._iDisplayStart = start;
+	
+		if ( changed ) {
+			_fnCallbackFire( settings, null, 'page', [settings] );
+	
+			if ( redraw ) {
+				_fnDraw( settings );
+			}
+		}
+	
+		return changed;
+	}
+	
+	
+	
+	/**
+	 * Generate the node required for the processing node
+	 *  @param {object} settings dataTables settings object
+	 *  @returns {node} Processing element
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlProcessing ( settings )
+	{
+		return $('<div/>', {
+				'id': ! settings.aanFeatures.r ? settings.sTableId+'_processing' : null,
+				'class': settings.oClasses.sProcessing
+			} )
+			.html( settings.oLanguage.sProcessing )
+			.insertBefore( settings.nTable )[0];
+	}
+	
+	
+	/**
+	 * Display or hide the processing indicator
+	 *  @param {object} settings dataTables settings object
+	 *  @param {bool} show Show the processing indicator (true) or not (false)
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnProcessingDisplay ( settings, show )
+	{
+		if ( settings.oFeatures.bProcessing ) {
+			$(settings.aanFeatures.r).css( 'display', show ? 'block' : 'none' );
+		}
+	
+		_fnCallbackFire( settings, null, 'processing', [settings, show] );
+	}
+	
+	/**
+	 * Add any control elements for the table - specifically scrolling
+	 *  @param {object} settings dataTables settings object
+	 *  @returns {node} Node to add to the DOM
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlTable ( settings )
+	{
+		var table = $(settings.nTable);
+	
+		// Add the ARIA grid role to the table
+		table.attr( 'role', 'grid' );
+	
+		// Scrolling from here on in
+		var scroll = settings.oScroll;
+	
+		if ( scroll.sX === '' && scroll.sY === '' ) {
+			return settings.nTable;
+		}
+	
+		var scrollX = scroll.sX;
+		var scrollY = scroll.sY;
+		var classes = settings.oClasses;
+		var caption = table.children('caption');
+		var captionSide = caption.length ? caption[0]._captionSide : null;
+		var headerClone = $( table[0].cloneNode(false) );
+		var footerClone = $( table[0].cloneNode(false) );
+		var footer = table.children('tfoot');
+		var _div = '<div/>';
+		var size = function ( s ) {
+			return !s ? null : _fnStringToCss( s );
+		};
+	
+		if ( ! footer.length ) {
+			footer = null;
+		}
+	
+		/*
+		 * The HTML structure that we want to generate in this function is:
+		 *  div - scroller
+		 *    div - scroll head
+		 *      div - scroll head inner
+		 *        table - scroll head table
+		 *          thead - thead
+		 *    div - scroll body
+		 *      table - table (master table)
+		 *        thead - thead clone for sizing
+		 *        tbody - tbody
+		 *    div - scroll foot
+		 *      div - scroll foot inner
+		 *        table - scroll foot table
+		 *          tfoot - tfoot
+		 */
+		var scroller = $( _div, { 'class': classes.sScrollWrapper } )
+			.append(
+				$(_div, { 'class': classes.sScrollHead } )
+					.css( {
+						overflow: 'hidden',
+						position: 'relative',
+						border: 0,
+						width: scrollX ? size(scrollX) : '100%'
+					} )
+					.append(
+						$(_div, { 'class': classes.sScrollHeadInner } )
+							.css( {
+								'box-sizing': 'content-box',
+								width: scroll.sXInner || '100%'
+							} )
+							.append(
+								headerClone
+									.removeAttr('id')
+									.css( 'margin-left', 0 )
+									.append( captionSide === 'top' ? caption : null )
+									.append(
+										table.children('thead')
+									)
+							)
+					)
+			)
+			.append(
+				$(_div, { 'class': classes.sScrollBody } )
+					.css( {
+						position: 'relative',
+						overflow: 'auto',
+						width: size( scrollX )
+					} )
+					.append( table )
+			);
+	
+		if ( footer ) {
+			scroller.append(
+				$(_div, { 'class': classes.sScrollFoot } )
+					.css( {
+						overflow: 'hidden',
+						border: 0,
+						width: scrollX ? size(scrollX) : '100%'
+					} )
+					.append(
+						$(_div, { 'class': classes.sScrollFootInner } )
+							.append(
+								footerClone
+									.removeAttr('id')
+									.css( 'margin-left', 0 )
+									.append( captionSide === 'bottom' ? caption : null )
+									.append(
+										table.children('tfoot')
+									)
+							)
+					)
+			);
+		}
+	
+		var children = scroller.children();
+		var scrollHead = children[0];
+		var scrollBody = children[1];
+		var scrollFoot = footer ? children[2] : null;
+	
+		// When the body is scrolled, then we also want to scroll the headers
+		if ( scrollX ) {
+			$(scrollBody).on( 'scroll.DT', function (e) {
+				var scrollLeft = this.scrollLeft;
+	
+				scrollHead.scrollLeft = scrollLeft;
+	
+				if ( footer ) {
+					scrollFoot.scrollLeft = scrollLeft;
+				}
+			} );
+		}
+	
+		$(scrollBody).css(
+			scrollY && scroll.bCollapse ? 'max-height' : 'height', 
+			scrollY
+		);
+	
+		settings.nScrollHead = scrollHead;
+		settings.nScrollBody = scrollBody;
+		settings.nScrollFoot = scrollFoot;
+	
+		// On redraw - align columns
+		settings.aoDrawCallback.push( {
+			"fn": _fnScrollDraw,
+			"sName": "scrolling"
+		} );
+	
+		return scroller[0];
+	}
+	
+	
+	
+	/**
+	 * Update the header, footer and body tables for resizing - i.e. column
+	 * alignment.
+	 *
+	 * Welcome to the most horrible function DataTables. The process that this
+	 * function follows is basically:
+	 *   1. Re-create the table inside the scrolling div
+	 *   2. Take live measurements from the DOM
+	 *   3. Apply the measurements to align the columns
+	 *   4. Clean up
+	 *
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnScrollDraw ( settings )
+	{
+		// Given that this is such a monster function, a lot of variables are use
+		// to try and keep the minimised size as small as possible
+		var
+			scroll         = settings.oScroll,
+			scrollX        = scroll.sX,
+			scrollXInner   = scroll.sXInner,
+			scrollY        = scroll.sY,
+			barWidth       = scroll.iBarWidth,
+			divHeader      = $(settings.nScrollHead),
+			divHeaderStyle = divHeader[0].style,
+			divHeaderInner = divHeader.children('div'),
+			divHeaderInnerStyle = divHeaderInner[0].style,
+			divHeaderTable = divHeaderInner.children('table'),
+			divBodyEl      = settings.nScrollBody,
+			divBody        = $(divBodyEl),
+			divBodyStyle   = divBodyEl.style,
+			divFooter      = $(settings.nScrollFoot),
+			divFooterInner = divFooter.children('div'),
+			divFooterTable = divFooterInner.children('table'),
+			header         = $(settings.nTHead),
+			table          = $(settings.nTable),
+			tableEl        = table[0],
+			tableStyle     = tableEl.style,
+			footer         = settings.nTFoot ? $(settings.nTFoot) : null,
+			browser        = settings.oBrowser,
+			ie67           = browser.bScrollOversize,
+			dtHeaderCells  = _pluck( settings.aoColumns, 'nTh' ),
+			headerTrgEls, footerTrgEls,
+			headerSrcEls, footerSrcEls,
+			headerCopy, footerCopy,
+			headerWidths=[], footerWidths=[],
+			headerContent=[], footerContent=[],
+			idx, correction, sanityWidth,
+			zeroOut = function(nSizer) {
+				var style = nSizer.style;
+				style.paddingTop = "0";
+				style.paddingBottom = "0";
+				style.borderTopWidth = "0";
+				style.borderBottomWidth = "0";
+				style.height = 0;
+			};
+	
+		// If the scrollbar visibility has changed from the last draw, we need to
+		// adjust the column sizes as the table width will have changed to account
+		// for the scrollbar
+		var scrollBarVis = divBodyEl.scrollHeight > divBodyEl.clientHeight;
+		
+		if ( settings.scrollBarVis !== scrollBarVis && settings.scrollBarVis !== undefined ) {
+			settings.scrollBarVis = scrollBarVis;
+			_fnAdjustColumnSizing( settings );
+			return; // adjust column sizing will call this function again
+		}
+		else {
+			settings.scrollBarVis = scrollBarVis;
+		}
+	
+		/*
+		 * 1. Re-create the table inside the scrolling div
+		 */
+	
+		// Remove the old minimised thead and tfoot elements in the inner table
+		table.children('thead, tfoot').remove();
+	
+		if ( footer ) {
+			footerCopy = footer.clone().prependTo( table );
+			footerTrgEls = footer.find('tr'); // the original tfoot is in its own table and must be sized
+			footerSrcEls = footerCopy.find('tr');
+		}
+	
+		// Clone the current header and footer elements and then place it into the inner table
+		headerCopy = header.clone().prependTo( table );
+		headerTrgEls = header.find('tr'); // original header is in its own table
+		headerSrcEls = headerCopy.find('tr');
+		headerCopy.find('th, td').removeAttr('tabindex');
+	
+	
+		/*
+		 * 2. Take live measurements from the DOM - do not alter the DOM itself!
+		 */
+	
+		// Remove old sizing and apply the calculated column widths
+		// Get the unique column headers in the newly created (cloned) header. We want to apply the
+		// calculated sizes to this header
+		if ( ! scrollX )
+		{
+			divBodyStyle.width = '100%';
+			divHeader[0].style.width = '100%';
+		}
+	
+		$.each( _fnGetUniqueThs( settings, headerCopy ), function ( i, el ) {
+			idx = _fnVisibleToColumnIndex( settings, i );
+			el.style.width = settings.aoColumns[idx].sWidth;
+		} );
+	
+		if ( footer ) {
+			_fnApplyToChildren( function(n) {
+				n.style.width = "";
+			}, footerSrcEls );
+		}
+	
+		// Size the table as a whole
+		sanityWidth = table.outerWidth();
+		if ( scrollX === "" ) {
+			// No x scrolling
+			tableStyle.width = "100%";
+	
+			// IE7 will make the width of the table when 100% include the scrollbar
+			// - which is shouldn't. When there is a scrollbar we need to take this
+			// into account.
+			if ( ie67 && (table.find('tbody').height() > divBodyEl.offsetHeight ||
+				divBody.css('overflow-y') == "scroll")
+			) {
+				tableStyle.width = _fnStringToCss( table.outerWidth() - barWidth);
+			}
+	
+			// Recalculate the sanity width
+			sanityWidth = table.outerWidth();
+		}
+		else if ( scrollXInner !== "" ) {
+			// legacy x scroll inner has been given - use it
+			tableStyle.width = _fnStringToCss(scrollXInner);
+	
+			// Recalculate the sanity width
+			sanityWidth = table.outerWidth();
+		}
+	
+		// Hidden header should have zero height, so remove padding and borders. Then
+		// set the width based on the real headers
+	
+		// Apply all styles in one pass
+		_fnApplyToChildren( zeroOut, headerSrcEls );
+	
+		// Read all widths in next pass
+		_fnApplyToChildren( function(nSizer) {
+			headerContent.push( nSizer.innerHTML );
+			headerWidths.push( _fnStringToCss( $(nSizer).css('width') ) );
+		}, headerSrcEls );
+	
+		// Apply all widths in final pass
+		_fnApplyToChildren( function(nToSize, i) {
+			// Only apply widths to the DataTables detected header cells - this
+			// prevents complex headers from having contradictory sizes applied
+			if ( $.inArray( nToSize, dtHeaderCells ) !== -1 ) {
+				nToSize.style.width = headerWidths[i];
+			}
+		}, headerTrgEls );
+	
+		$(headerSrcEls).height(0);
+	
+		/* Same again with the footer if we have one */
+		if ( footer )
+		{
+			_fnApplyToChildren( zeroOut, footerSrcEls );
+	
+			_fnApplyToChildren( function(nSizer) {
+				footerContent.push( nSizer.innerHTML );
+				footerWidths.push( _fnStringToCss( $(nSizer).css('width') ) );
+			}, footerSrcEls );
+	
+			_fnApplyToChildren( function(nToSize, i) {
+				nToSize.style.width = footerWidths[i];
+			}, footerTrgEls );
+	
+			$(footerSrcEls).height(0);
+		}
+	
+	
+		/*
+		 * 3. Apply the measurements
+		 */
+	
+		// "Hide" the header and footer that we used for the sizing. We need to keep
+		// the content of the cell so that the width applied to the header and body
+		// both match, but we want to hide it completely. We want to also fix their
+		// width to what they currently are
+		_fnApplyToChildren( function(nSizer, i) {
+			nSizer.innerHTML = '<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+headerContent[i]+'</div>';
+			nSizer.style.width = headerWidths[i];
+		}, headerSrcEls );
+	
+		if ( footer )
+		{
+			_fnApplyToChildren( function(nSizer, i) {
+				nSizer.innerHTML = '<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+footerContent[i]+'</div>';
+				nSizer.style.width = footerWidths[i];
+			}, footerSrcEls );
+		}
+	
+		// Sanity check that the table is of a sensible width. If not then we are going to get
+		// misalignment - try to prevent this by not allowing the table to shrink below its min width
+		if ( table.outerWidth() < sanityWidth )
+		{
+			// The min width depends upon if we have a vertical scrollbar visible or not */
+			correction = ((divBodyEl.scrollHeight > divBodyEl.offsetHeight ||
+				divBody.css('overflow-y') == "scroll")) ?
+					sanityWidth+barWidth :
+					sanityWidth;
+	
+			// IE6/7 are a law unto themselves...
+			if ( ie67 && (divBodyEl.scrollHeight >
+				divBodyEl.offsetHeight || divBody.css('overflow-y') == "scroll")
+			) {
+				tableStyle.width = _fnStringToCss( correction-barWidth );
+			}
+	
+			// And give the user a warning that we've stopped the table getting too small
+			if ( scrollX === "" || scrollXInner !== "" ) {
+				_fnLog( settings, 1, 'Possible column misalignment', 6 );
+			}
+		}
+		else
+		{
+			correction = '100%';
+		}
+	
+		// Apply to the container elements
+		divBodyStyle.width = _fnStringToCss( correction );
+		divHeaderStyle.width = _fnStringToCss( correction );
+	
+		if ( footer ) {
+			settings.nScrollFoot.style.width = _fnStringToCss( correction );
+		}
+	
+	
+		/*
+		 * 4. Clean up
+		 */
+		if ( ! scrollY ) {
+			/* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting
+			 * the scrollbar height from the visible display, rather than adding it on. We need to
+			 * set the height in order to sort this. Don't want to do it in any other browsers.
+			 */
+			if ( ie67 ) {
+				divBodyStyle.height = _fnStringToCss( tableEl.offsetHeight+barWidth );
+			}
+		}
+	
+		/* Finally set the width's of the header and footer tables */
+		var iOuterWidth = table.outerWidth();
+		divHeaderTable[0].style.width = _fnStringToCss( iOuterWidth );
+		divHeaderInnerStyle.width = _fnStringToCss( iOuterWidth );
+	
+		// Figure out if there are scrollbar present - if so then we need a the header and footer to
+		// provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar)
+		var bScrolling = table.height() > divBodyEl.clientHeight || divBody.css('overflow-y') == "scroll";
+		var padding = 'padding' + (browser.bScrollbarLeft ? 'Left' : 'Right' );
+		divHeaderInnerStyle[ padding ] = bScrolling ? barWidth+"px" : "0px";
+	
+		if ( footer ) {
+			divFooterTable[0].style.width = _fnStringToCss( iOuterWidth );
+			divFooterInner[0].style.width = _fnStringToCss( iOuterWidth );
+			divFooterInner[0].style[padding] = bScrolling ? barWidth+"px" : "0px";
+		}
+	
+		// Correct DOM ordering for colgroup - comes before the thead
+		table.children('colgroup').insertBefore( table.children('thead') );
+	
+		/* Adjust the position of the header in case we loose the y-scrollbar */
+		divBody.scroll();
+	
+		// If sorting or filtering has occurred, jump the scrolling back to the top
+		// only if we aren't holding the position
+		if ( (settings.bSorted || settings.bFiltered) && ! settings._drawHold ) {
+			divBodyEl.scrollTop = 0;
+		}
+	}
+	
+	
+	
+	/**
+	 * Apply a given function to the display child nodes of an element array (typically
+	 * TD children of TR rows
+	 *  @param {function} fn Method to apply to the objects
+	 *  @param array {nodes} an1 List of elements to look through for display children
+	 *  @param array {nodes} an2 Another list (identical structure to the first) - optional
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnApplyToChildren( fn, an1, an2 )
+	{
+		var index=0, i=0, iLen=an1.length;
+		var nNode1, nNode2;
+	
+		while ( i < iLen ) {
+			nNode1 = an1[i].firstChild;
+			nNode2 = an2 ? an2[i].firstChild : null;
+	
+			while ( nNode1 ) {
+				if ( nNode1.nodeType === 1 ) {
+					if ( an2 ) {
+						fn( nNode1, nNode2, index );
+					}
+					else {
+						fn( nNode1, index );
+					}
+	
+					index++;
+				}
+	
+				nNode1 = nNode1.nextSibling;
+				nNode2 = an2 ? nNode2.nextSibling : null;
+			}
+	
+			i++;
+		}
+	}
+	
+	
+	
+	var __re_html_remove = /<.*?>/g;
+	
+	
+	/**
+	 * Calculate the width of columns for the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnCalculateColumnWidths ( oSettings )
+	{
+		var
+			table = oSettings.nTable,
+			columns = oSettings.aoColumns,
+			scroll = oSettings.oScroll,
+			scrollY = scroll.sY,
+			scrollX = scroll.sX,
+			scrollXInner = scroll.sXInner,
+			columnCount = columns.length,
+			visibleColumns = _fnGetColumns( oSettings, 'bVisible' ),
+			headerCells = $('th', oSettings.nTHead),
+			tableWidthAttr = table.getAttribute('width'), // from DOM element
+			tableContainer = table.parentNode,
+			userInputs = false,
+			i, column, columnIdx, width, outerWidth,
+			browser = oSettings.oBrowser,
+			ie67 = browser.bScrollOversize;
+	
+		var styleWidth = table.style.width;
+		if ( styleWidth && styleWidth.indexOf('%') !== -1 ) {
+			tableWidthAttr = styleWidth;
+		}
+	
+		/* Convert any user input sizes into pixel sizes */
+		for ( i=0 ; i<visibleColumns.length ; i++ ) {
+			column = columns[ visibleColumns[i] ];
+	
+			if ( column.sWidth !== null ) {
+				column.sWidth = _fnConvertToWidth( column.sWidthOrig, tableContainer );
+	
+				userInputs = true;
+			}
+		}
+	
+		/* If the number of columns in the DOM equals the number that we have to
+		 * process in DataTables, then we can use the offsets that are created by
+		 * the web- browser. No custom sizes can be set in order for this to happen,
+		 * nor scrolling used
+		 */
+		if ( ie67 || ! userInputs && ! scrollX && ! scrollY &&
+		     columnCount == _fnVisbleColumns( oSettings ) &&
+		     columnCount == headerCells.length
+		) {
+			for ( i=0 ; i<columnCount ; i++ ) {
+				var colIdx = _fnVisibleToColumnIndex( oSettings, i );
+	
+				if ( colIdx !== null ) {
+					columns[ colIdx ].sWidth = _fnStringToCss( headerCells.eq(i).width() );
+				}
+			}
+		}
+		else
+		{
+			// Otherwise construct a single row, worst case, table with the widest
+			// node in the data, assign any user defined widths, then insert it into
+			// the DOM and allow the browser to do all the hard work of calculating
+			// table widths
+			var tmpTable = $(table).clone() // don't use cloneNode - IE8 will remove events on the main table
+				.css( 'visibility', 'hidden' )
+				.removeAttr( 'id' );
+	
+			// Clean up the table body
+			tmpTable.find('tbody tr').remove();
+			var tr = $('<tr/>').appendTo( tmpTable.find('tbody') );
+	
+			// Clone the table header and footer - we can't use the header / footer
+			// from the cloned table, since if scrolling is active, the table's
+			// real header and footer are contained in different table tags
+			tmpTable.find('thead, tfoot').remove();
+			tmpTable
+				.append( $(oSettings.nTHead).clone() )
+				.append( $(oSettings.nTFoot).clone() );
+	
+			// Remove any assigned widths from the footer (from scrolling)
+			tmpTable.find('tfoot th, tfoot td').css('width', '');
+	
+			// Apply custom sizing to the cloned header
+			headerCells = _fnGetUniqueThs( oSettings, tmpTable.find('thead')[0] );
+	
+			for ( i=0 ; i<visibleColumns.length ; i++ ) {
+				column = columns[ visibleColumns[i] ];
+	
+				headerCells[i].style.width = column.sWidthOrig !== null && column.sWidthOrig !== '' ?
+					_fnStringToCss( column.sWidthOrig ) :
+					'';
+	
+				// For scrollX we need to force the column width otherwise the
+				// browser will collapse it. If this width is smaller than the
+				// width the column requires, then it will have no effect
+				if ( column.sWidthOrig && scrollX ) {
+					$( headerCells[i] ).append( $('<div/>').css( {
+						width: column.sWidthOrig,
+						margin: 0,
+						padding: 0,
+						border: 0,
+						height: 1
+					} ) );
+				}
+			}
+	
+			// Find the widest cell for each column and put it into the table
+			if ( oSettings.aoData.length ) {
+				for ( i=0 ; i<visibleColumns.length ; i++ ) {
+					columnIdx = visibleColumns[i];
+					column = columns[ columnIdx ];
+	
+					$( _fnGetWidestNode( oSettings, columnIdx ) )
+						.clone( false )
+						.append( column.sContentPadding )
+						.appendTo( tr );
+				}
+			}
+	
+			// Tidy the temporary table - remove name attributes so there aren't
+			// duplicated in the dom (radio elements for example)
+			$('[name]', tmpTable).removeAttr('name');
+	
+			// Table has been built, attach to the document so we can work with it.
+			// A holding element is used, positioned at the top of the container
+			// with minimal height, so it has no effect on if the container scrolls
+			// or not. Otherwise it might trigger scrolling when it actually isn't
+			// needed
+			var holder = $('<div/>').css( scrollX || scrollY ?
+					{
+						position: 'absolute',
+						top: 0,
+						left: 0,
+						height: 1,
+						right: 0,
+						overflow: 'hidden'
+					} :
+					{}
+				)
+				.append( tmpTable )
+				.appendTo( tableContainer );
+	
+			// When scrolling (X or Y) we want to set the width of the table as 
+			// appropriate. However, when not scrolling leave the table width as it
+			// is. This results in slightly different, but I think correct behaviour
+			if ( scrollX && scrollXInner ) {
+				tmpTable.width( scrollXInner );
+			}
+			else if ( scrollX ) {
+				tmpTable.css( 'width', 'auto' );
+				tmpTable.removeAttr('width');
+	
+				// If there is no width attribute or style, then allow the table to
+				// collapse
+				if ( tmpTable.width() < tableContainer.clientWidth && tableWidthAttr ) {
+					tmpTable.width( tableContainer.clientWidth );
+				}
+			}
+			else if ( scrollY ) {
+				tmpTable.width( tableContainer.clientWidth );
+			}
+			else if ( tableWidthAttr ) {
+				tmpTable.width( tableWidthAttr );
+			}
+	
+			// Get the width of each column in the constructed table - we need to
+			// know the inner width (so it can be assigned to the other table's
+			// cells) and the outer width so we can calculate the full width of the
+			// table. This is safe since DataTables requires a unique cell for each
+			// column, but if ever a header can span multiple columns, this will
+			// need to be modified.
+			var total = 0;
+			for ( i=0 ; i<visibleColumns.length ; i++ ) {
+				var cell = $(headerCells[i]);
+				var border = cell.outerWidth() - cell.width();
+	
+				// Use getBounding... where possible (not IE8-) because it can give
+				// sub-pixel accuracy, which we then want to round up!
+				var bounding = browser.bBounding ?
+					Math.ceil( headerCells[i].getBoundingClientRect().width ) :
+					cell.outerWidth();
+	
+				// Total is tracked to remove any sub-pixel errors as the outerWidth
+				// of the table might not equal the total given here (IE!).
+				total += bounding;
+	
+				// Width for each column to use
+				columns[ visibleColumns[i] ].sWidth = _fnStringToCss( bounding - border );
+			}
+	
+			table.style.width = _fnStringToCss( total );
+	
+			// Finished with the table - ditch it
+			holder.remove();
+		}
+	
+		// If there is a width attr, we want to attach an event listener which
+		// allows the table sizing to automatically adjust when the window is
+		// resized. Use the width attr rather than CSS, since we can't know if the
+		// CSS is a relative value or absolute - DOM read is always px.
+		if ( tableWidthAttr ) {
+			table.style.width = _fnStringToCss( tableWidthAttr );
+		}
+	
+		if ( (tableWidthAttr || scrollX) && ! oSettings._reszEvt ) {
+			var bindResize = function () {
+				$(window).on('resize.DT-'+oSettings.sInstance, _fnThrottle( function () {
+					_fnAdjustColumnSizing( oSettings );
+				} ) );
+			};
+	
+			// IE6/7 will crash if we bind a resize event handler on page load.
+			// To be removed in 1.11 which drops IE6/7 support
+			if ( ie67 ) {
+				setTimeout( bindResize, 1000 );
+			}
+			else {
+				bindResize();
+			}
+	
+			oSettings._reszEvt = true;
+		}
+	}
+	
+	
+	/**
+	 * Throttle the calls to a function. Arguments and context are maintained for
+	 * the throttled function
+	 *  @param {function} fn Function to be called
+	 *  @param {int} [freq=200] call frequency in mS
+	 *  @returns {function} wrapped function
+	 *  @memberof DataTable#oApi
+	 */
+	var _fnThrottle = DataTable.util.throttle;
+	
+	
+	/**
+	 * Convert a CSS unit width to pixels (e.g. 2em)
+	 *  @param {string} width width to be converted
+	 *  @param {node} parent parent to get the with for (required for relative widths) - optional
+	 *  @returns {int} width in pixels
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnConvertToWidth ( width, parent )
+	{
+		if ( ! width ) {
+			return 0;
+		}
+	
+		var n = $('<div/>')
+			.css( 'width', _fnStringToCss( width ) )
+			.appendTo( parent || document.body );
+	
+		var val = n[0].offsetWidth;
+		n.remove();
+	
+		return val;
+	}
+	
+	
+	/**
+	 * Get the widest node
+	 *  @param {object} settings dataTables settings object
+	 *  @param {int} colIdx column of interest
+	 *  @returns {node} widest table node
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetWidestNode( settings, colIdx )
+	{
+		var idx = _fnGetMaxLenString( settings, colIdx );
+		if ( idx < 0 ) {
+			return null;
+		}
+	
+		var data = settings.aoData[ idx ];
+		return ! data.nTr ? // Might not have been created when deferred rendering
+			$('<td/>').html( _fnGetCellData( settings, idx, colIdx, 'display' ) )[0] :
+			data.anCells[ colIdx ];
+	}
+	
+	
+	/**
+	 * Get the maximum strlen for each data column
+	 *  @param {object} settings dataTables settings object
+	 *  @param {int} colIdx column of interest
+	 *  @returns {string} max string length for each column
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetMaxLenString( settings, colIdx )
+	{
+		var s, max=-1, maxIdx = -1;
+	
+		for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
+			s = _fnGetCellData( settings, i, colIdx, 'display' )+'';
+			s = s.replace( __re_html_remove, '' );
+			s = s.replace( /&nbsp;/g, ' ' );
+	
+			if ( s.length > max ) {
+				max = s.length;
+				maxIdx = i;
+			}
+		}
+	
+		return maxIdx;
+	}
+	
+	
+	/**
+	 * Append a CSS unit (only if required) to a string
+	 *  @param {string} value to css-ify
+	 *  @returns {string} value with css unit
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnStringToCss( s )
+	{
+		if ( s === null ) {
+			return '0px';
+		}
+	
+		if ( typeof s == 'number' ) {
+			return s < 0 ?
+				'0px' :
+				s+'px';
+		}
+	
+		// Check it has a unit character already
+		return s.match(/\d$/) ?
+			s+'px' :
+			s;
+	}
+	
+	
+	
+	function _fnSortFlatten ( settings )
+	{
+		var
+			i, iLen, k, kLen,
+			aSort = [],
+			aiOrig = [],
+			aoColumns = settings.aoColumns,
+			aDataSort, iCol, sType, srcCol,
+			fixed = settings.aaSortingFixed,
+			fixedObj = $.isPlainObject( fixed ),
+			nestedSort = [],
+			add = function ( a ) {
+				if ( a.length && ! $.isArray( a[0] ) ) {
+					// 1D array
+					nestedSort.push( a );
+				}
+				else {
+					// 2D array
+					$.merge( nestedSort, a );
+				}
+			};
+	
+		// Build the sort array, with pre-fix and post-fix options if they have been
+		// specified
+		if ( $.isArray( fixed ) ) {
+			add( fixed );
+		}
+	
+		if ( fixedObj && fixed.pre ) {
+			add( fixed.pre );
+		}
+	
+		add( settings.aaSorting );
+	
+		if (fixedObj && fixed.post ) {
+			add( fixed.post );
+		}
+	
+		for ( i=0 ; i<nestedSort.length ; i++ )
+		{
+			srcCol = nestedSort[i][0];
+			aDataSort = aoColumns[ srcCol ].aDataSort;
+	
+			for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ )
+			{
+				iCol = aDataSort[k];
+				sType = aoColumns[ iCol ].sType || 'string';
+	
+				if ( nestedSort[i]._idx === undefined ) {
+					nestedSort[i]._idx = $.inArray( nestedSort[i][1], aoColumns[iCol].asSorting );
+				}
+	
+				aSort.push( {
+					src:       srcCol,
+					col:       iCol,
+					dir:       nestedSort[i][1],
+					index:     nestedSort[i]._idx,
+					type:      sType,
+					formatter: DataTable.ext.type.order[ sType+"-pre" ]
+				} );
+			}
+		}
+	
+		return aSort;
+	}
+	
+	/**
+	 * Change the order of the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 *  @todo This really needs split up!
+	 */
+	function _fnSort ( oSettings )
+	{
+		var
+			i, ien, iLen, j, jLen, k, kLen,
+			sDataType, nTh,
+			aiOrig = [],
+			oExtSort = DataTable.ext.type.order,
+			aoData = oSettings.aoData,
+			aoColumns = oSettings.aoColumns,
+			aDataSort, data, iCol, sType, oSort,
+			formatters = 0,
+			sortCol,
+			displayMaster = oSettings.aiDisplayMaster,
+			aSort;
+	
+		// Resolve any column types that are unknown due to addition or invalidation
+		// @todo Can this be moved into a 'data-ready' handler which is called when
+		//   data is going to be used in the table?
+		_fnColumnTypes( oSettings );
+	
+		aSort = _fnSortFlatten( oSettings );
+	
+		for ( i=0, ien=aSort.length ; i<ien ; i++ ) {
+			sortCol = aSort[i];
+	
+			// Track if we can use the fast sort algorithm
+			if ( sortCol.formatter ) {
+				formatters++;
+			}
+	
+			// Load the data needed for the sort, for each cell
+			_fnSortData( oSettings, sortCol.col );
+		}
+	
+		/* No sorting required if server-side or no sorting array */
+		if ( _fnDataSource( oSettings ) != 'ssp' && aSort.length !== 0 )
+		{
+			// Create a value - key array of the current row positions such that we can use their
+			// current position during the sort, if values match, in order to perform stable sorting
+			for ( i=0, iLen=displayMaster.length ; i<iLen ; i++ ) {
+				aiOrig[ displayMaster[i] ] = i;
+			}
+	
+			/* Do the sort - here we want multi-column sorting based on a given data source (column)
+			 * and sorting function (from oSort) in a certain direction. It's reasonably complex to
+			 * follow on it's own, but this is what we want (example two column sorting):
+			 *  fnLocalSorting = function(a,b){
+			 *    var iTest;
+			 *    iTest = oSort['string-asc']('data11', 'data12');
+			 *      if (iTest !== 0)
+			 *        return iTest;
+			 *    iTest = oSort['numeric-desc']('data21', 'data22');
+			 *    if (iTest !== 0)
+			 *      return iTest;
+			 *    return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+			 *  }
+			 * Basically we have a test for each sorting column, if the data in that column is equal,
+			 * test the next column. If all columns match, then we use a numeric sort on the row
+			 * positions in the original data array to provide a stable sort.
+			 *
+			 * Note - I know it seems excessive to have two sorting methods, but the first is around
+			 * 15% faster, so the second is only maintained for backwards compatibility with sorting
+			 * methods which do not have a pre-sort formatting function.
+			 */
+			if ( formatters === aSort.length ) {
+				// All sort types have formatting functions
+				displayMaster.sort( function ( a, b ) {
+					var
+						x, y, k, test, sort,
+						len=aSort.length,
+						dataA = aoData[a]._aSortData,
+						dataB = aoData[b]._aSortData;
+	
+					for ( k=0 ; k<len ; k++ ) {
+						sort = aSort[k];
+	
+						x = dataA[ sort.col ];
+						y = dataB[ sort.col ];
+	
+						test = x<y ? -1 : x>y ? 1 : 0;
+						if ( test !== 0 ) {
+							return sort.dir === 'asc' ? test : -test;
+						}
+					}
+	
+					x = aiOrig[a];
+					y = aiOrig[b];
+					return x<y ? -1 : x>y ? 1 : 0;
+				} );
+			}
+			else {
+				// Depreciated - remove in 1.11 (providing a plug-in option)
+				// Not all sort types have formatting methods, so we have to call their sorting
+				// methods.
+				displayMaster.sort( function ( a, b ) {
+					var
+						x, y, k, l, test, sort, fn,
+						len=aSort.length,
+						dataA = aoData[a]._aSortData,
+						dataB = aoData[b]._aSortData;
+	
+					for ( k=0 ; k<len ; k++ ) {
+						sort = aSort[k];
+	
+						x = dataA[ sort.col ];
+						y = dataB[ sort.col ];
+	
+						fn = oExtSort[ sort.type+"-"+sort.dir ] || oExtSort[ "string-"+sort.dir ];
+						test = fn( x, y );
+						if ( test !== 0 ) {
+							return test;
+						}
+					}
+	
+					x = aiOrig[a];
+					y = aiOrig[b];
+					return x<y ? -1 : x>y ? 1 : 0;
+				} );
+			}
+		}
+	
+		/* Tell the draw function that we have sorted the data */
+		oSettings.bSorted = true;
+	}
+	
+	
+	function _fnSortAria ( settings )
+	{
+		var label;
+		var nextSort;
+		var columns = settings.aoColumns;
+		var aSort = _fnSortFlatten( settings );
+		var oAria = settings.oLanguage.oAria;
+	
+		// ARIA attributes - need to loop all columns, to update all (removing old
+		// attributes as needed)
+		for ( var i=0, iLen=columns.length ; i<iLen ; i++ )
+		{
+			var col = columns[i];
+			var asSorting = col.asSorting;
+			var sTitle = col.sTitle.replace( /<.*?>/g, "" );
+			var th = col.nTh;
+	
+			// IE7 is throwing an error when setting these properties with jQuery's
+			// attr() and removeAttr() methods...
+			th.removeAttribute('aria-sort');
+	
+			/* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */
+			if ( col.bSortable ) {
+				if ( aSort.length > 0 && aSort[0].col == i ) {
+					th.setAttribute('aria-sort', aSort[0].dir=="asc" ? "ascending" : "descending" );
+					nextSort = asSorting[ aSort[0].index+1 ] || asSorting[0];
+				}
+				else {
+					nextSort = asSorting[0];
+				}
+	
+				label = sTitle + ( nextSort === "asc" ?
+					oAria.sSortAscending :
+					oAria.sSortDescending
+				);
+			}
+			else {
+				label = sTitle;
+			}
+	
+			th.setAttribute('aria-label', label);
+		}
+	}
+	
+	
+	/**
+	 * Function to run on user sort request
+	 *  @param {object} settings dataTables settings object
+	 *  @param {node} attachTo node to attach the handler to
+	 *  @param {int} colIdx column sorting index
+	 *  @param {boolean} [append=false] Append the requested sort to the existing
+	 *    sort if true (i.e. multi-column sort)
+	 *  @param {function} [callback] callback function
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSortListener ( settings, colIdx, append, callback )
+	{
+		var col = settings.aoColumns[ colIdx ];
+		var sorting = settings.aaSorting;
+		var asSorting = col.asSorting;
+		var nextSortIdx;
+		var next = function ( a, overflow ) {
+			var idx = a._idx;
+			if ( idx === undefined ) {
+				idx = $.inArray( a[1], asSorting );
+			}
+	
+			return idx+1 < asSorting.length ?
+				idx+1 :
+				overflow ?
+					null :
+					0;
+		};
+	
+		// Convert to 2D array if needed
+		if ( typeof sorting[0] === 'number' ) {
+			sorting = settings.aaSorting = [ sorting ];
+		}
+	
+		// If appending the sort then we are multi-column sorting
+		if ( append && settings.oFeatures.bSortMulti ) {
+			// Are we already doing some kind of sort on this column?
+			var sortIdx = $.inArray( colIdx, _pluck(sorting, '0') );
+	
+			if ( sortIdx !== -1 ) {
+				// Yes, modify the sort
+				nextSortIdx = next( sorting[sortIdx], true );
+	
+				if ( nextSortIdx === null && sorting.length === 1 ) {
+					nextSortIdx = 0; // can't remove sorting completely
+				}
+	
+				if ( nextSortIdx === null ) {
+					sorting.splice( sortIdx, 1 );
+				}
+				else {
+					sorting[sortIdx][1] = asSorting[ nextSortIdx ];
+					sorting[sortIdx]._idx = nextSortIdx;
+				}
+			}
+			else {
+				// No sort on this column yet
+				sorting.push( [ colIdx, asSorting[0], 0 ] );
+				sorting[sorting.length-1]._idx = 0;
+			}
+		}
+		else if ( sorting.length && sorting[0][0] == colIdx ) {
+			// Single column - already sorting on this column, modify the sort
+			nextSortIdx = next( sorting[0] );
+	
+			sorting.length = 1;
+			sorting[0][1] = asSorting[ nextSortIdx ];
+			sorting[0]._idx = nextSortIdx;
+		}
+		else {
+			// Single column - sort only on this column
+			sorting.length = 0;
+			sorting.push( [ colIdx, asSorting[0] ] );
+			sorting[0]._idx = 0;
+		}
+	
+		// Run the sort by calling a full redraw
+		_fnReDraw( settings );
+	
+		// callback used for async user interaction
+		if ( typeof callback == 'function' ) {
+			callback( settings );
+		}
+	}
+	
+	
+	/**
+	 * Attach a sort handler (click) to a node
+	 *  @param {object} settings dataTables settings object
+	 *  @param {node} attachTo node to attach the handler to
+	 *  @param {int} colIdx column sorting index
+	 *  @param {function} [callback] callback function
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSortAttachListener ( settings, attachTo, colIdx, callback )
+	{
+		var col = settings.aoColumns[ colIdx ];
+	
+		_fnBindAction( attachTo, {}, function (e) {
+			/* If the column is not sortable - don't to anything */
+			if ( col.bSortable === false ) {
+				return;
+			}
+	
+			// If processing is enabled use a timeout to allow the processing
+			// display to be shown - otherwise to it synchronously
+			if ( settings.oFeatures.bProcessing ) {
+				_fnProcessingDisplay( settings, true );
+	
+				setTimeout( function() {
+					_fnSortListener( settings, colIdx, e.shiftKey, callback );
+	
+					// In server-side processing, the draw callback will remove the
+					// processing display
+					if ( _fnDataSource( settings ) !== 'ssp' ) {
+						_fnProcessingDisplay( settings, false );
+					}
+				}, 0 );
+			}
+			else {
+				_fnSortListener( settings, colIdx, e.shiftKey, callback );
+			}
+		} );
+	}
+	
+	
+	/**
+	 * Set the sorting classes on table's body, Note: it is safe to call this function
+	 * when bSort and bSortClasses are false
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSortingClasses( settings )
+	{
+		var oldSort = settings.aLastSort;
+		var sortClass = settings.oClasses.sSortColumn;
+		var sort = _fnSortFlatten( settings );
+		var features = settings.oFeatures;
+		var i, ien, colIdx;
+	
+		if ( features.bSort && features.bSortClasses ) {
+			// Remove old sorting classes
+			for ( i=0, ien=oldSort.length ; i<ien ; i++ ) {
+				colIdx = oldSort[i].src;
+	
+				// Remove column sorting
+				$( _pluck( settings.aoData, 'anCells', colIdx ) )
+					.removeClass( sortClass + (i<2 ? i+1 : 3) );
+			}
+	
+			// Add new column sorting
+			for ( i=0, ien=sort.length ; i<ien ; i++ ) {
+				colIdx = sort[i].src;
+	
+				$( _pluck( settings.aoData, 'anCells', colIdx ) )
+					.addClass( sortClass + (i<2 ? i+1 : 3) );
+			}
+		}
+	
+		settings.aLastSort = sort;
+	}
+	
+	
+	// Get the data to sort a column, be it from cache, fresh (populating the
+	// cache), or from a sort formatter
+	function _fnSortData( settings, idx )
+	{
+		// Custom sorting function - provided by the sort data type
+		var column = settings.aoColumns[ idx ];
+		var customSort = DataTable.ext.order[ column.sSortDataType ];
+		var customData;
+	
+		if ( customSort ) {
+			customData = customSort.call( settings.oInstance, settings, idx,
+				_fnColumnIndexToVisible( settings, idx )
+			);
+		}
+	
+		// Use / populate cache
+		var row, cellData;
+		var formatter = DataTable.ext.type.order[ column.sType+"-pre" ];
+	
+		for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
+			row = settings.aoData[i];
+	
+			if ( ! row._aSortData ) {
+				row._aSortData = [];
+			}
+	
+			if ( ! row._aSortData[idx] || customSort ) {
+				cellData = customSort ?
+					customData[i] : // If there was a custom sort function, use data from there
+					_fnGetCellData( settings, i, idx, 'sort' );
+	
+				row._aSortData[ idx ] = formatter ?
+					formatter( cellData ) :
+					cellData;
+			}
+		}
+	}
+	
+	
+	
+	/**
+	 * Save the state of a table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSaveState ( settings )
+	{
+		if ( !settings.oFeatures.bStateSave || settings.bDestroying )
+		{
+			return;
+		}
+	
+		/* Store the interesting variables */
+		var state = {
+			time:    +new Date(),
+			start:   settings._iDisplayStart,
+			length:  settings._iDisplayLength,
+			order:   $.extend( true, [], settings.aaSorting ),
+			search:  _fnSearchToCamel( settings.oPreviousSearch ),
+			columns: $.map( settings.aoColumns, function ( col, i ) {
+				return {
+					visible: col.bVisible,
+					search: _fnSearchToCamel( settings.aoPreSearchCols[i] )
+				};
+			} )
+		};
+	
+		_fnCallbackFire( settings, "aoStateSaveParams", 'stateSaveParams', [settings, state] );
+	
+		settings.oSavedState = state;
+		settings.fnStateSaveCallback.call( settings.oInstance, settings, state );
+	}
+	
+	
+	/**
+	 * Attempt to load a saved table state
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {object} oInit DataTables init object so we can override settings
+	 *  @param {function} callback Callback to execute when the state has been loaded
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnLoadState ( settings, oInit, callback )
+	{
+		var i, ien;
+		var columns = settings.aoColumns;
+		var loaded = function ( s ) {
+			if ( ! s || ! s.time ) {
+				callback();
+				return;
+			}
+	
+			// Allow custom and plug-in manipulation functions to alter the saved data set and
+			// cancelling of loading by returning false
+			var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );
+			if ( $.inArray( false, abStateLoad ) !== -1 ) {
+				callback();
+				return;
+			}
+	
+			// Reject old data
+			var duration = settings.iStateDuration;
+			if ( duration > 0 && s.time < +new Date() - (duration*1000) ) {
+				callback();
+				return;
+			}
+	
+			// Number of columns have changed - all bets are off, no restore of settings
+			if ( s.columns && columns.length !== s.columns.length ) {
+				callback();
+				return;
+			}
+	
+			// Store the saved state so it might be accessed at any time
+			settings.oLoadedState = $.extend( true, {}, s );
+	
+			// Restore key features - todo - for 1.11 this needs to be done by
+			// subscribed events
+			if ( s.start !== undefined ) {
+				settings._iDisplayStart    = s.start;
+				settings.iInitDisplayStart = s.start;
+			}
+			if ( s.length !== undefined ) {
+				settings._iDisplayLength   = s.length;
+			}
+	
+			// Order
+			if ( s.order !== undefined ) {
+				settings.aaSorting = [];
+				$.each( s.order, function ( i, col ) {
+					settings.aaSorting.push( col[0] >= columns.length ?
+						[ 0, col[1] ] :
+						col
+					);
+				} );
+			}
+	
+			// Search
+			if ( s.search !== undefined ) {
+				$.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) );
+			}
+	
+			// Columns
+			//
+			if ( s.columns ) {
+				for ( i=0, ien=s.columns.length ; i<ien ; i++ ) {
+					var col = s.columns[i];
+	
+					// Visibility
+					if ( col.visible !== undefined ) {
+						columns[i].bVisible = col.visible;
+					}
+	
+					// Search
+					if ( col.search !== undefined ) {
+						$.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) );
+					}
+				}
+			}
+	
+			_fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );
+			callback();
+		}
+	
+		if ( ! settings.oFeatures.bStateSave ) {
+			callback();
+			return;
+		}
+	
+		var state = settings.fnStateLoadCallback.call( settings.oInstance, settings, loaded );
+	
+		if ( state !== undefined ) {
+			loaded( state );
+		}
+		// otherwise, wait for the loaded callback to be executed
+	}
+	
+	
+	/**
+	 * Return the settings object for a particular table
+	 *  @param {node} table table we are using as a dataTable
+	 *  @returns {object} Settings object - or null if not found
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSettingsFromNode ( table )
+	{
+		var settings = DataTable.settings;
+		var idx = $.inArray( table, _pluck( settings, 'nTable' ) );
+	
+		return idx !== -1 ?
+			settings[ idx ] :
+			null;
+	}
+	
+	
+	/**
+	 * Log an error message
+	 *  @param {object} settings dataTables settings object
+	 *  @param {int} level log error messages, or display them to the user
+	 *  @param {string} msg error message
+	 *  @param {int} tn Technical note id to get more information about the error.
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnLog( settings, level, msg, tn )
+	{
+		msg = 'DataTables warning: '+
+			(settings ? 'table id='+settings.sTableId+' - ' : '')+msg;
+	
+		if ( tn ) {
+			msg += '. For more information about this error, please see '+
+			'http://datatables.net/tn/'+tn;
+		}
+	
+		if ( ! level  ) {
+			// Backwards compatibility pre 1.10
+			var ext = DataTable.ext;
+			var type = ext.sErrMode || ext.errMode;
+	
+			if ( settings ) {
+				_fnCallbackFire( settings, null, 'error', [ settings, tn, msg ] );
+			}
+	
+			if ( type == 'alert' ) {
+				alert( msg );
+			}
+			else if ( type == 'throw' ) {
+				throw new Error(msg);
+			}
+			else if ( typeof type == 'function' ) {
+				type( settings, tn, msg );
+			}
+		}
+		else if ( window.console && console.log ) {
+			console.log( msg );
+		}
+	}
+	
+	
+	/**
+	 * See if a property is defined on one object, if so assign it to the other object
+	 *  @param {object} ret target object
+	 *  @param {object} src source object
+	 *  @param {string} name property
+	 *  @param {string} [mappedName] name to map too - optional, name used if not given
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnMap( ret, src, name, mappedName )
+	{
+		if ( $.isArray( name ) ) {
+			$.each( name, function (i, val) {
+				if ( $.isArray( val ) ) {
+					_fnMap( ret, src, val[0], val[1] );
+				}
+				else {
+					_fnMap( ret, src, val );
+				}
+			} );
+	
+			return;
+		}
+	
+		if ( mappedName === undefined ) {
+			mappedName = name;
+		}
+	
+		if ( src[name] !== undefined ) {
+			ret[mappedName] = src[name];
+		}
+	}
+	
+	
+	/**
+	 * Extend objects - very similar to jQuery.extend, but deep copy objects, and
+	 * shallow copy arrays. The reason we need to do this, is that we don't want to
+	 * deep copy array init values (such as aaSorting) since the dev wouldn't be
+	 * able to override them, but we do want to deep copy arrays.
+	 *  @param {object} out Object to extend
+	 *  @param {object} extender Object from which the properties will be applied to
+	 *      out
+	 *  @param {boolean} breakRefs If true, then arrays will be sliced to take an
+	 *      independent copy with the exception of the `data` or `aaData` parameters
+	 *      if they are present. This is so you can pass in a collection to
+	 *      DataTables and have that used as your data source without breaking the
+	 *      references
+	 *  @returns {object} out Reference, just for convenience - out === the return.
+	 *  @memberof DataTable#oApi
+	 *  @todo This doesn't take account of arrays inside the deep copied objects.
+	 */
+	function _fnExtend( out, extender, breakRefs )
+	{
+		var val;
+	
+		for ( var prop in extender ) {
+			if ( extender.hasOwnProperty(prop) ) {
+				val = extender[prop];
+	
+				if ( $.isPlainObject( val ) ) {
+					if ( ! $.isPlainObject( out[prop] ) ) {
+						out[prop] = {};
+					}
+					$.extend( true, out[prop], val );
+				}
+				else if ( breakRefs && prop !== 'data' && prop !== 'aaData' && $.isArray(val) ) {
+					out[prop] = val.slice();
+				}
+				else {
+					out[prop] = val;
+				}
+			}
+		}
+	
+		return out;
+	}
+	
+	
+	/**
+	 * Bind an event handers to allow a click or return key to activate the callback.
+	 * This is good for accessibility since a return on the keyboard will have the
+	 * same effect as a click, if the element has focus.
+	 *  @param {element} n Element to bind the action to
+	 *  @param {object} oData Data object to pass to the triggered function
+	 *  @param {function} fn Callback function for when the event is triggered
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnBindAction( n, oData, fn )
+	{
+		$(n)
+			.on( 'click.DT', oData, function (e) {
+					n.blur(); // Remove focus outline for mouse users
+					fn(e);
+				} )
+			.on( 'keypress.DT', oData, function (e){
+					if ( e.which === 13 ) {
+						e.preventDefault();
+						fn(e);
+					}
+				} )
+			.on( 'selectstart.DT', function () {
+					/* Take the brutal approach to cancelling text selection */
+					return false;
+				} );
+	}
+	
+	
+	/**
+	 * Register a callback function. Easily allows a callback function to be added to
+	 * an array store of callback functions that can then all be called together.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {string} sStore Name of the array storage for the callbacks in oSettings
+	 *  @param {function} fn Function to be called back
+	 *  @param {string} sName Identifying name for the callback (i.e. a label)
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnCallbackReg( oSettings, sStore, fn, sName )
+	{
+		if ( fn )
+		{
+			oSettings[sStore].push( {
+				"fn": fn,
+				"sName": sName
+			} );
+		}
+	}
+	
+	
+	/**
+	 * Fire callback functions and trigger events. Note that the loop over the
+	 * callback array store is done backwards! Further note that you do not want to
+	 * fire off triggers in time sensitive applications (for example cell creation)
+	 * as its slow.
+	 *  @param {object} settings dataTables settings object
+	 *  @param {string} callbackArr Name of the array storage for the callbacks in
+	 *      oSettings
+	 *  @param {string} eventName Name of the jQuery custom event to trigger. If
+	 *      null no trigger is fired
+	 *  @param {array} args Array of arguments to pass to the callback function /
+	 *      trigger
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnCallbackFire( settings, callbackArr, eventName, args )
+	{
+		var ret = [];
+	
+		if ( callbackArr ) {
+			ret = $.map( settings[callbackArr].slice().reverse(), function (val, i) {
+				return val.fn.apply( settings.oInstance, args );
+			} );
+		}
+	
+		if ( eventName !== null ) {
+			var e = $.Event( eventName+'.dt' );
+	
+			$(settings.nTable).trigger( e, args );
+	
+			ret.push( e.result );
+		}
+	
+		return ret;
+	}
+	
+	
+	function _fnLengthOverflow ( settings )
+	{
+		var
+			start = settings._iDisplayStart,
+			end = settings.fnDisplayEnd(),
+			len = settings._iDisplayLength;
+	
+		/* If we have space to show extra rows (backing up from the end point - then do so */
+		if ( start >= end )
+		{
+			start = end - len;
+		}
+	
+		// Keep the start record on the current page
+		start -= (start % len);
+	
+		if ( len === -1 || start < 0 )
+		{
+			start = 0;
+		}
+	
+		settings._iDisplayStart = start;
+	}
+	
+	
+	function _fnRenderer( settings, type )
+	{
+		var renderer = settings.renderer;
+		var host = DataTable.ext.renderer[type];
+	
+		if ( $.isPlainObject( renderer ) && renderer[type] ) {
+			// Specific renderer for this type. If available use it, otherwise use
+			// the default.
+			return host[renderer[type]] || host._;
+		}
+		else if ( typeof renderer === 'string' ) {
+			// Common renderer - if there is one available for this type use it,
+			// otherwise use the default
+			return host[renderer] || host._;
+		}
+	
+		// Use the default
+		return host._;
+	}
+	
+	
+	/**
+	 * Detect the data source being used for the table. Used to simplify the code
+	 * a little (ajax) and to make it compress a little smaller.
+	 *
+	 *  @param {object} settings dataTables settings object
+	 *  @returns {string} Data source
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnDataSource ( settings )
+	{
+		if ( settings.oFeatures.bServerSide ) {
+			return 'ssp';
+		}
+		else if ( settings.ajax || settings.sAjaxSource ) {
+			return 'ajax';
+		}
+		return 'dom';
+	}
+	
+
+	
+	
+	/**
+	 * Computed structure of the DataTables API, defined by the options passed to
+	 * `DataTable.Api.register()` when building the API.
+	 *
+	 * The structure is built in order to speed creation and extension of the Api
+	 * objects since the extensions are effectively pre-parsed.
+	 *
+	 * The array is an array of objects with the following structure, where this
+	 * base array represents the Api prototype base:
+	 *
+	 *     [
+	 *       {
+	 *         name:      'data'                -- string   - Property name
+	 *         val:       function () {},       -- function - Api method (or undefined if just an object
+	 *         methodExt: [ ... ],              -- array    - Array of Api object definitions to extend the method result
+	 *         propExt:   [ ... ]               -- array    - Array of Api object definitions to extend the property
+	 *       },
+	 *       {
+	 *         name:     'row'
+	 *         val:       {},
+	 *         methodExt: [ ... ],
+	 *         propExt:   [
+	 *           {
+	 *             name:      'data'
+	 *             val:       function () {},
+	 *             methodExt: [ ... ],
+	 *             propExt:   [ ... ]
+	 *           },
+	 *           ...
+	 *         ]
+	 *       }
+	 *     ]
+	 *
+	 * @type {Array}
+	 * @ignore
+	 */
+	var __apiStruct = [];
+	
+	
+	/**
+	 * `Array.prototype` reference.
+	 *
+	 * @type object
+	 * @ignore
+	 */
+	var __arrayProto = Array.prototype;
+	
+	
+	/**
+	 * Abstraction for `context` parameter of the `Api` constructor to allow it to
+	 * take several different forms for ease of use.
+	 *
+	 * Each of the input parameter types will be converted to a DataTables settings
+	 * object where possible.
+	 *
+	 * @param  {string|node|jQuery|object} mixed DataTable identifier. Can be one
+	 *   of:
+	 *
+	 *   * `string` - jQuery selector. Any DataTables' matching the given selector
+	 *     with be found and used.
+	 *   * `node` - `TABLE` node which has already been formed into a DataTable.
+	 *   * `jQuery` - A jQuery object of `TABLE` nodes.
+	 *   * `object` - DataTables settings object
+	 *   * `DataTables.Api` - API instance
+	 * @return {array|null} Matching DataTables settings objects. `null` or
+	 *   `undefined` is returned if no matching DataTable is found.
+	 * @ignore
+	 */
+	var _toSettings = function ( mixed )
+	{
+		var idx, jq;
+		var settings = DataTable.settings;
+		var tables = $.map( settings, function (el, i) {
+			return el.nTable;
+		} );
+	
+		if ( ! mixed ) {
+			return [];
+		}
+		else if ( mixed.nTable && mixed.oApi ) {
+			// DataTables settings object
+			return [ mixed ];
+		}
+		else if ( mixed.nodeName && mixed.nodeName.toLowerCase() === 'table' ) {
+			// Table node
+			idx = $.inArray( mixed, tables );
+			return idx !== -1 ? [ settings[idx] ] : null;
+		}
+		else if ( mixed && typeof mixed.settings === 'function' ) {
+			return mixed.settings().toArray();
+		}
+		else if ( typeof mixed === 'string' ) {
+			// jQuery selector
+			jq = $(mixed);
+		}
+		else if ( mixed instanceof $ ) {
+			// jQuery object (also DataTables instance)
+			jq = mixed;
+		}
+	
+		if ( jq ) {
+			return jq.map( function(i) {
+				idx = $.inArray( this, tables );
+				return idx !== -1 ? settings[idx] : null;
+			} ).toArray();
+		}
+	};
+	
+	
+	/**
+	 * DataTables API class - used to control and interface with  one or more
+	 * DataTables enhanced tables.
+	 *
+	 * The API class is heavily based on jQuery, presenting a chainable interface
+	 * that you can use to interact with tables. Each instance of the API class has
+	 * a "context" - i.e. the tables that it will operate on. This could be a single
+	 * table, all tables on a page or a sub-set thereof.
+	 *
+	 * Additionally the API is designed to allow you to easily work with the data in
+	 * the tables, retrieving and manipulating it as required. This is done by
+	 * presenting the API class as an array like interface. The contents of the
+	 * array depend upon the actions requested by each method (for example
+	 * `rows().nodes()` will return an array of nodes, while `rows().data()` will
+	 * return an array of objects or arrays depending upon your table's
+	 * configuration). The API object has a number of array like methods (`push`,
+	 * `pop`, `reverse` etc) as well as additional helper methods (`each`, `pluck`,
+	 * `unique` etc) to assist your working with the data held in a table.
+	 *
+	 * Most methods (those which return an Api instance) are chainable, which means
+	 * the return from a method call also has all of the methods available that the
+	 * top level object had. For example, these two calls are equivalent:
+	 *
+	 *     // Not chained
+	 *     api.row.add( {...} );
+	 *     api.draw();
+	 *
+	 *     // Chained
+	 *     api.row.add( {...} ).draw();
+	 *
+	 * @class DataTable.Api
+	 * @param {array|object|string|jQuery} context DataTable identifier. This is
+	 *   used to define which DataTables enhanced tables this API will operate on.
+	 *   Can be one of:
+	 *
+	 *   * `string` - jQuery selector. Any DataTables' matching the given selector
+	 *     with be found and used.
+	 *   * `node` - `TABLE` node which has already been formed into a DataTable.
+	 *   * `jQuery` - A jQuery object of `TABLE` nodes.
+	 *   * `object` - DataTables settings object
+	 * @param {array} [data] Data to initialise the Api instance with.
+	 *
+	 * @example
+	 *   // Direct initialisation during DataTables construction
+	 *   var api = $('#example').DataTable();
+	 *
+	 * @example
+	 *   // Initialisation using a DataTables jQuery object
+	 *   var api = $('#example').dataTable().api();
+	 *
+	 * @example
+	 *   // Initialisation as a constructor
+	 *   var api = new $.fn.DataTable.Api( 'table.dataTable' );
+	 */
+	_Api = function ( context, data )
+	{
+		if ( ! (this instanceof _Api) ) {
+			return new _Api( context, data );
+		}
+	
+		var settings = [];
+		var ctxSettings = function ( o ) {
+			var a = _toSettings( o );
+			if ( a ) {
+				settings = settings.concat( a );
+			}
+		};
+	
+		if ( $.isArray( context ) ) {
+			for ( var i=0, ien=context.length ; i<ien ; i++ ) {
+				ctxSettings( context[i] );
+			}
+		}
+		else {
+			ctxSettings( context );
+		}
+	
+		// Remove duplicates
+		this.context = _unique( settings );
+	
+		// Initial data
+		if ( data ) {
+			$.merge( this, data );
+		}
+	
+		// selector
+		this.selector = {
+			rows: null,
+			cols: null,
+			opts: null
+		};
+	
+		_Api.extend( this, this, __apiStruct );
+	};
+	
+	DataTable.Api = _Api;
+	
+	// Don't destroy the existing prototype, just extend it. Required for jQuery 2's
+	// isPlainObject.
+	$.extend( _Api.prototype, {
+		any: function ()
+		{
+			return this.count() !== 0;
+		},
+	
+	
+		concat:  __arrayProto.concat,
+	
+	
+		context: [], // array of table settings objects
+	
+	
+		count: function ()
+		{
+			return this.flatten().length;
+		},
+	
+	
+		each: function ( fn )
+		{
+			for ( var i=0, ien=this.length ; i<ien; i++ ) {
+				fn.call( this, this[i], i, this );
+			}
+	
+			return this;
+		},
+	
+	
+		eq: function ( idx )
+		{
+			var ctx = this.context;
+	
+			return ctx.length > idx ?
+				new _Api( ctx[idx], this[idx] ) :
+				null;
+		},
+	
+	
+		filter: function ( fn )
+		{
+			var a = [];
+	
+			if ( __arrayProto.filter ) {
+				a = __arrayProto.filter.call( this, fn, this );
+			}
+			else {
+				// Compatibility for browsers without EMCA-252-5 (JS 1.6)
+				for ( var i=0, ien=this.length ; i<ien ; i++ ) {
+					if ( fn.call( this, this[i], i, this ) ) {
+						a.push( this[i] );
+					}
+				}
+			}
+	
+			return new _Api( this.context, a );
+		},
+	
+	
+		flatten: function ()
+		{
+			var a = [];
+			return new _Api( this.context, a.concat.apply( a, this.toArray() ) );
+		},
+	
+	
+		join:    __arrayProto.join,
+	
+	
+		indexOf: __arrayProto.indexOf || function (obj, start)
+		{
+			for ( var i=(start || 0), ien=this.length ; i<ien ; i++ ) {
+				if ( this[i] === obj ) {
+					return i;
+				}
+			}
+			return -1;
+		},
+	
+		iterator: function ( flatten, type, fn, alwaysNew ) {
+			var
+				a = [], ret,
+				i, ien, j, jen,
+				context = this.context,
+				rows, items, item,
+				selector = this.selector;
+	
+			// Argument shifting
+			if ( typeof flatten === 'string' ) {
+				alwaysNew = fn;
+				fn = type;
+				type = flatten;
+				flatten = false;
+			}
+	
+			for ( i=0, ien=context.length ; i<ien ; i++ ) {
+				var apiInst = new _Api( context[i] );
+	
+				if ( type === 'table' ) {
+					ret = fn.call( apiInst, context[i], i );
+	
+					if ( ret !== undefined ) {
+						a.push( ret );
+					}
+				}
+				else if ( type === 'columns' || type === 'rows' ) {
+					// this has same length as context - one entry for each table
+					ret = fn.call( apiInst, context[i], this[i], i );
+	
+					if ( ret !== undefined ) {
+						a.push( ret );
+					}
+				}
+				else if ( type === 'column' || type === 'column-rows' || type === 'row' || type === 'cell' ) {
+					// columns and rows share the same structure.
+					// 'this' is an array of column indexes for each context
+					items = this[i];
+	
+					if ( type === 'column-rows' ) {
+						rows = _selector_row_indexes( context[i], selector.opts );
+					}
+	
+					for ( j=0, jen=items.length ; j<jen ; j++ ) {
+						item = items[j];
+	
+						if ( type === 'cell' ) {
+							ret = fn.call( apiInst, context[i], item.row, item.column, i, j );
+						}
+						else {
+							ret = fn.call( apiInst, context[i], item, i, j, rows );
+						}
+	
+						if ( ret !== undefined ) {
+							a.push( ret );
+						}
+					}
+				}
+			}
+	
+			if ( a.length || alwaysNew ) {
+				var api = new _Api( context, flatten ? a.concat.apply( [], a ) : a );
+				var apiSelector = api.selector;
+				apiSelector.rows = selector.rows;
+				apiSelector.cols = selector.cols;
+				apiSelector.opts = selector.opts;
+				return api;
+			}
+			return this;
+		},
+	
+	
+		lastIndexOf: __arrayProto.lastIndexOf || function (obj, start)
+		{
+			// Bit cheeky...
+			return this.indexOf.apply( this.toArray.reverse(), arguments );
+		},
+	
+	
+		length:  0,
+	
+	
+		map: function ( fn )
+		{
+			var a = [];
+	
+			if ( __arrayProto.map ) {
+				a = __arrayProto.map.call( this, fn, this );
+			}
+			else {
+				// Compatibility for browsers without EMCA-252-5 (JS 1.6)
+				for ( var i=0, ien=this.length ; i<ien ; i++ ) {
+					a.push( fn.call( this, this[i], i ) );
+				}
+			}
+	
+			return new _Api( this.context, a );
+		},
+	
+	
+		pluck: function ( prop )
+		{
+			return this.map( function ( el ) {
+				return el[ prop ];
+			} );
+		},
+	
+		pop:     __arrayProto.pop,
+	
+	
+		push:    __arrayProto.push,
+	
+	
+		// Does not return an API instance
+		reduce: __arrayProto.reduce || function ( fn, init )
+		{
+			return _fnReduce( this, fn, init, 0, this.length, 1 );
+		},
+	
+	
+		reduceRight: __arrayProto.reduceRight || function ( fn, init )
+		{
+			return _fnReduce( this, fn, init, this.length-1, -1, -1 );
+		},
+	
+	
+		reverse: __arrayProto.reverse,
+	
+	
+		// Object with rows, columns and opts
+		selector: null,
+	
+	
+		shift:   __arrayProto.shift,
+	
+	
+		slice: function () {
+			return new _Api( this.context, this );
+		},
+	
+	
+		sort:    __arrayProto.sort, // ? name - order?
+	
+	
+		splice:  __arrayProto.splice,
+	
+	
+		toArray: function ()
+		{
+			return __arrayProto.slice.call( this );
+		},
+	
+	
+		to$: function ()
+		{
+			return $( this );
+		},
+	
+	
+		toJQuery: function ()
+		{
+			return $( this );
+		},
+	
+	
+		unique: function ()
+		{
+			return new _Api( this.context, _unique(this) );
+		},
+	
+	
+		unshift: __arrayProto.unshift
+	} );
+	
+	
+	_Api.extend = function ( scope, obj, ext )
+	{
+		// Only extend API instances and static properties of the API
+		if ( ! ext.length || ! obj || ( ! (obj instanceof _Api) && ! obj.__dt_wrapper ) ) {
+			return;
+		}
+	
+		var
+			i, ien,
+			j, jen,
+			struct, inner,
+			methodScoping = function ( scope, fn, struc ) {
+				return function () {
+					var ret = fn.apply( scope, arguments );
+	
+					// Method extension
+					_Api.extend( ret, ret, struc.methodExt );
+					return ret;
+				};
+			};
+	
+		for ( i=0, ien=ext.length ; i<ien ; i++ ) {
+			struct = ext[i];
+	
+			// Value
+			obj[ struct.name ] = typeof struct.val === 'function' ?
+				methodScoping( scope, struct.val, struct ) :
+				$.isPlainObject( struct.val ) ?
+					{} :
+					struct.val;
+	
+			obj[ struct.name ].__dt_wrapper = true;
+	
+			// Property extension
+			_Api.extend( scope, obj[ struct.name ], struct.propExt );
+		}
+	};
+	
+	
+	// @todo - Is there need for an augment function?
+	// _Api.augment = function ( inst, name )
+	// {
+	// 	// Find src object in the structure from the name
+	// 	var parts = name.split('.');
+	
+	// 	_Api.extend( inst, obj );
+	// };
+	
+	
+	//     [
+	//       {
+	//         name:      'data'                -- string   - Property name
+	//         val:       function () {},       -- function - Api method (or undefined if just an object
+	//         methodExt: [ ... ],              -- array    - Array of Api object definitions to extend the method result
+	//         propExt:   [ ... ]               -- array    - Array of Api object definitions to extend the property
+	//       },
+	//       {
+	//         name:     'row'
+	//         val:       {},
+	//         methodExt: [ ... ],
+	//         propExt:   [
+	//           {
+	//             name:      'data'
+	//             val:       function () {},
+	//             methodExt: [ ... ],
+	//             propExt:   [ ... ]
+	//           },
+	//           ...
+	//         ]
+	//       }
+	//     ]
+	
+	_Api.register = _api_register = function ( name, val )
+	{
+		if ( $.isArray( name ) ) {
+			for ( var j=0, jen=name.length ; j<jen ; j++ ) {
+				_Api.register( name[j], val );
+			}
+			return;
+		}
+	
+		var
+			i, ien,
+			heir = name.split('.'),
+			struct = __apiStruct,
+			key, method;
+	
+		var find = function ( src, name ) {
+			for ( var i=0, ien=src.length ; i<ien ; i++ ) {
+				if ( src[i].name === name ) {
+					return src[i];
+				}
+			}
+			return null;
+		};
+	
+		for ( i=0, ien=heir.length ; i<ien ; i++ ) {
+			method = heir[i].indexOf('()') !== -1;
+			key = method ?
+				heir[i].replace('()', '') :
+				heir[i];
+	
+			var src = find( struct, key );
+			if ( ! src ) {
+				src = {
+					name:      key,
+					val:       {},
+					methodExt: [],
+					propExt:   []
+				};
+				struct.push( src );
+			}
+	
+			if ( i === ien-1 ) {
+				src.val = val;
+			}
+			else {
+				struct = method ?
+					src.methodExt :
+					src.propExt;
+			}
+		}
+	};
+	
+	
+	_Api.registerPlural = _api_registerPlural = function ( pluralName, singularName, val ) {
+		_Api.register( pluralName, val );
+	
+		_Api.register( singularName, function () {
+			var ret = val.apply( this, arguments );
+	
+			if ( ret === this ) {
+				// Returned item is the API instance that was passed in, return it
+				return this;
+			}
+			else if ( ret instanceof _Api ) {
+				// New API instance returned, want the value from the first item
+				// in the returned array for the singular result.
+				return ret.length ?
+					$.isArray( ret[0] ) ?
+						new _Api( ret.context, ret[0] ) : // Array results are 'enhanced'
+						ret[0] :
+					undefined;
+			}
+	
+			// Non-API return - just fire it back
+			return ret;
+		} );
+	};
+	
+	
+	/**
+	 * Selector for HTML tables. Apply the given selector to the give array of
+	 * DataTables settings objects.
+	 *
+	 * @param {string|integer} [selector] jQuery selector string or integer
+	 * @param  {array} Array of DataTables settings objects to be filtered
+	 * @return {array}
+	 * @ignore
+	 */
+	var __table_selector = function ( selector, a )
+	{
+		// Integer is used to pick out a table by index
+		if ( typeof selector === 'number' ) {
+			return [ a[ selector ] ];
+		}
+	
+		// Perform a jQuery selector on the table nodes
+		var nodes = $.map( a, function (el, i) {
+			return el.nTable;
+		} );
+	
+		return $(nodes)
+			.filter( selector )
+			.map( function (i) {
+				// Need to translate back from the table node to the settings
+				var idx = $.inArray( this, nodes );
+				return a[ idx ];
+			} )
+			.toArray();
+	};
+	
+	
+	
+	/**
+	 * Context selector for the API's context (i.e. the tables the API instance
+	 * refers to.
+	 *
+	 * @name    DataTable.Api#tables
+	 * @param {string|integer} [selector] Selector to pick which tables the iterator
+	 *   should operate on. If not given, all tables in the current context are
+	 *   used. This can be given as a jQuery selector (for example `':gt(0)'`) to
+	 *   select multiple tables or as an integer to select a single table.
+	 * @returns {DataTable.Api} Returns a new API instance if a selector is given.
+	 */
+	_api_register( 'tables()', function ( selector ) {
+		// A new instance is created if there was a selector specified
+		return selector ?
+			new _Api( __table_selector( selector, this.context ) ) :
+			this;
+	} );
+	
+	
+	_api_register( 'table()', function ( selector ) {
+		var tables = this.tables( selector );
+		var ctx = tables.context;
+	
+		// Truncate to the first matched table
+		return ctx.length ?
+			new _Api( ctx[0] ) :
+			tables;
+	} );
+	
+	
+	_api_registerPlural( 'tables().nodes()', 'table().node()' , function () {
+		return this.iterator( 'table', function ( ctx ) {
+			return ctx.nTable;
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'tables().body()', 'table().body()' , function () {
+		return this.iterator( 'table', function ( ctx ) {
+			return ctx.nTBody;
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'tables().header()', 'table().header()' , function () {
+		return this.iterator( 'table', function ( ctx ) {
+			return ctx.nTHead;
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'tables().footer()', 'table().footer()' , function () {
+		return this.iterator( 'table', function ( ctx ) {
+			return ctx.nTFoot;
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'tables().containers()', 'table().container()' , function () {
+		return this.iterator( 'table', function ( ctx ) {
+			return ctx.nTableWrapper;
+		}, 1 );
+	} );
+	
+	
+	
+	/**
+	 * Redraw the tables in the current context.
+	 */
+	_api_register( 'draw()', function ( paging ) {
+		return this.iterator( 'table', function ( settings ) {
+			if ( paging === 'page' ) {
+				_fnDraw( settings );
+			}
+			else {
+				if ( typeof paging === 'string' ) {
+					paging = paging === 'full-hold' ?
+						false :
+						true;
+				}
+	
+				_fnReDraw( settings, paging===false );
+			}
+		} );
+	} );
+	
+	
+	
+	/**
+	 * Get the current page index.
+	 *
+	 * @return {integer} Current page index (zero based)
+	 *//**
+	 * Set the current page.
+	 *
+	 * Note that if you attempt to show a page which does not exist, DataTables will
+	 * not throw an error, but rather reset the paging.
+	 *
+	 * @param {integer|string} action The paging action to take. This can be one of:
+	 *  * `integer` - The page index to jump to
+	 *  * `string` - An action to take:
+	 *    * `first` - Jump to first page.
+	 *    * `next` - Jump to the next page
+	 *    * `previous` - Jump to previous page
+	 *    * `last` - Jump to the last page.
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'page()', function ( action ) {
+		if ( action === undefined ) {
+			return this.page.info().page; // not an expensive call
+		}
+	
+		// else, have an action to take on all tables
+		return this.iterator( 'table', function ( settings ) {
+			_fnPageChange( settings, action );
+		} );
+	} );
+	
+	
+	/**
+	 * Paging information for the first table in the current context.
+	 *
+	 * If you require paging information for another table, use the `table()` method
+	 * with a suitable selector.
+	 *
+	 * @return {object} Object with the following properties set:
+	 *  * `page` - Current page index (zero based - i.e. the first page is `0`)
+	 *  * `pages` - Total number of pages
+	 *  * `start` - Display index for the first record shown on the current page
+	 *  * `end` - Display index for the last record shown on the current page
+	 *  * `length` - Display length (number of records). Note that generally `start
+	 *    + length = end`, but this is not always true, for example if there are
+	 *    only 2 records to show on the final page, with a length of 10.
+	 *  * `recordsTotal` - Full data set length
+	 *  * `recordsDisplay` - Data set length once the current filtering criterion
+	 *    are applied.
+	 */
+	_api_register( 'page.info()', function ( action ) {
+		if ( this.context.length === 0 ) {
+			return undefined;
+		}
+	
+		var
+			settings   = this.context[0],
+			start      = settings._iDisplayStart,
+			len        = settings.oFeatures.bPaginate ? settings._iDisplayLength : -1,
+			visRecords = settings.fnRecordsDisplay(),
+			all        = len === -1;
+	
+		return {
+			"page":           all ? 0 : Math.floor( start / len ),
+			"pages":          all ? 1 : Math.ceil( visRecords / len ),
+			"start":          start,
+			"end":            settings.fnDisplayEnd(),
+			"length":         len,
+			"recordsTotal":   settings.fnRecordsTotal(),
+			"recordsDisplay": visRecords,
+			"serverSide":     _fnDataSource( settings ) === 'ssp'
+		};
+	} );
+	
+	
+	/**
+	 * Get the current page length.
+	 *
+	 * @return {integer} Current page length. Note `-1` indicates that all records
+	 *   are to be shown.
+	 *//**
+	 * Set the current page length.
+	 *
+	 * @param {integer} Page length to set. Use `-1` to show all records.
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'page.len()', function ( len ) {
+		// Note that we can't call this function 'length()' because `length`
+		// is a Javascript property of functions which defines how many arguments
+		// the function expects.
+		if ( len === undefined ) {
+			return this.context.length !== 0 ?
+				this.context[0]._iDisplayLength :
+				undefined;
+		}
+	
+		// else, set the page length
+		return this.iterator( 'table', function ( settings ) {
+			_fnLengthChange( settings, len );
+		} );
+	} );
+	
+	
+	
+	var __reload = function ( settings, holdPosition, callback ) {
+		// Use the draw event to trigger a callback
+		if ( callback ) {
+			var api = new _Api( settings );
+	
+			api.one( 'draw', function () {
+				callback( api.ajax.json() );
+			} );
+		}
+	
+		if ( _fnDataSource( settings ) == 'ssp' ) {
+			_fnReDraw( settings, holdPosition );
+		}
+		else {
+			_fnProcessingDisplay( settings, true );
+	
+			// Cancel an existing request
+			var xhr = settings.jqXHR;
+			if ( xhr && xhr.readyState !== 4 ) {
+				xhr.abort();
+			}
+	
+			// Trigger xhr
+			_fnBuildAjax( settings, [], function( json ) {
+				_fnClearTable( settings );
+	
+				var data = _fnAjaxDataSrc( settings, json );
+				for ( var i=0, ien=data.length ; i<ien ; i++ ) {
+					_fnAddData( settings, data[i] );
+				}
+	
+				_fnReDraw( settings, holdPosition );
+				_fnProcessingDisplay( settings, false );
+			} );
+		}
+	};
+	
+	
+	/**
+	 * Get the JSON response from the last Ajax request that DataTables made to the
+	 * server. Note that this returns the JSON from the first table in the current
+	 * context.
+	 *
+	 * @return {object} JSON received from the server.
+	 */
+	_api_register( 'ajax.json()', function () {
+		var ctx = this.context;
+	
+		if ( ctx.length > 0 ) {
+			return ctx[0].json;
+		}
+	
+		// else return undefined;
+	} );
+	
+	
+	/**
+	 * Get the data submitted in the last Ajax request
+	 */
+	_api_register( 'ajax.params()', function () {
+		var ctx = this.context;
+	
+		if ( ctx.length > 0 ) {
+			return ctx[0].oAjaxData;
+		}
+	
+		// else return undefined;
+	} );
+	
+	
+	/**
+	 * Reload tables from the Ajax data source. Note that this function will
+	 * automatically re-draw the table when the remote data has been loaded.
+	 *
+	 * @param {boolean} [reset=true] Reset (default) or hold the current paging
+	 *   position. A full re-sort and re-filter is performed when this method is
+	 *   called, which is why the pagination reset is the default action.
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'ajax.reload()', function ( callback, resetPaging ) {
+		return this.iterator( 'table', function (settings) {
+			__reload( settings, resetPaging===false, callback );
+		} );
+	} );
+	
+	
+	/**
+	 * Get the current Ajax URL. Note that this returns the URL from the first
+	 * table in the current context.
+	 *
+	 * @return {string} Current Ajax source URL
+	 *//**
+	 * Set the Ajax URL. Note that this will set the URL for all tables in the
+	 * current context.
+	 *
+	 * @param {string} url URL to set.
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'ajax.url()', function ( url ) {
+		var ctx = this.context;
+	
+		if ( url === undefined ) {
+			// get
+			if ( ctx.length === 0 ) {
+				return undefined;
+			}
+			ctx = ctx[0];
+	
+			return ctx.ajax ?
+				$.isPlainObject( ctx.ajax ) ?
+					ctx.ajax.url :
+					ctx.ajax :
+				ctx.sAjaxSource;
+		}
+	
+		// set
+		return this.iterator( 'table', function ( settings ) {
+			if ( $.isPlainObject( settings.ajax ) ) {
+				settings.ajax.url = url;
+			}
+			else {
+				settings.ajax = url;
+			}
+			// No need to consider sAjaxSource here since DataTables gives priority
+			// to `ajax` over `sAjaxSource`. So setting `ajax` here, renders any
+			// value of `sAjaxSource` redundant.
+		} );
+	} );
+	
+	
+	/**
+	 * Load data from the newly set Ajax URL. Note that this method is only
+	 * available when `ajax.url()` is used to set a URL. Additionally, this method
+	 * has the same effect as calling `ajax.reload()` but is provided for
+	 * convenience when setting a new URL. Like `ajax.reload()` it will
+	 * automatically redraw the table once the remote data has been loaded.
+	 *
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'ajax.url().load()', function ( callback, resetPaging ) {
+		// Same as a reload, but makes sense to present it for easy access after a
+		// url change
+		return this.iterator( 'table', function ( ctx ) {
+			__reload( ctx, resetPaging===false, callback );
+		} );
+	} );
+	
+	
+	
+	
+	var _selector_run = function ( type, selector, selectFn, settings, opts )
+	{
+		var
+			out = [], res,
+			a, i, ien, j, jen,
+			selectorType = typeof selector;
+	
+		// Can't just check for isArray here, as an API or jQuery instance might be
+		// given with their array like look
+		if ( ! selector || selectorType === 'string' || selectorType === 'function' || selector.length === undefined ) {
+			selector = [ selector ];
+		}
+	
+		for ( i=0, ien=selector.length ; i<ien ; i++ ) {
+			// Only split on simple strings - complex expressions will be jQuery selectors
+			a = selector[i] && selector[i].split && ! selector[i].match(/[\[\(:]/) ?
+				selector[i].split(',') :
+				[ selector[i] ];
+	
+			for ( j=0, jen=a.length ; j<jen ; j++ ) {
+				res = selectFn( typeof a[j] === 'string' ? $.trim(a[j]) : a[j] );
+	
+				if ( res && res.length ) {
+					out = out.concat( res );
+				}
+			}
+		}
+	
+		// selector extensions
+		var ext = _ext.selector[ type ];
+		if ( ext.length ) {
+			for ( i=0, ien=ext.length ; i<ien ; i++ ) {
+				out = ext[i]( settings, opts, out );
+			}
+		}
+	
+		return _unique( out );
+	};
+	
+	
+	var _selector_opts = function ( opts )
+	{
+		if ( ! opts ) {
+			opts = {};
+		}
+	
+		// Backwards compatibility for 1.9- which used the terminology filter rather
+		// than search
+		if ( opts.filter && opts.search === undefined ) {
+			opts.search = opts.filter;
+		}
+	
+		return $.extend( {
+			search: 'none',
+			order: 'current',
+			page: 'all'
+		}, opts );
+	};
+	
+	
+	var _selector_first = function ( inst )
+	{
+		// Reduce the API instance to the first item found
+		for ( var i=0, ien=inst.length ; i<ien ; i++ ) {
+			if ( inst[i].length > 0 ) {
+				// Assign the first element to the first item in the instance
+				// and truncate the instance and context
+				inst[0] = inst[i];
+				inst[0].length = 1;
+				inst.length = 1;
+				inst.context = [ inst.context[i] ];
+	
+				return inst;
+			}
+		}
+	
+		// Not found - return an empty instance
+		inst.length = 0;
+		return inst;
+	};
+	
+	
+	var _selector_row_indexes = function ( settings, opts )
+	{
+		var
+			i, ien, tmp, a=[],
+			displayFiltered = settings.aiDisplay,
+			displayMaster = settings.aiDisplayMaster;
+	
+		var
+			search = opts.search,  // none, applied, removed
+			order  = opts.order,   // applied, current, index (original - compatibility with 1.9)
+			page   = opts.page;    // all, current
+	
+		if ( _fnDataSource( settings ) == 'ssp' ) {
+			// In server-side processing mode, most options are irrelevant since
+			// rows not shown don't exist and the index order is the applied order
+			// Removed is a special case - for consistency just return an empty
+			// array
+			return search === 'removed' ?
+				[] :
+				_range( 0, displayMaster.length );
+		}
+		else if ( page == 'current' ) {
+			// Current page implies that order=current and fitler=applied, since it is
+			// fairly senseless otherwise, regardless of what order and search actually
+			// are
+			for ( i=settings._iDisplayStart, ien=settings.fnDisplayEnd() ; i<ien ; i++ ) {
+				a.push( displayFiltered[i] );
+			}
+		}
+		else if ( order == 'current' || order == 'applied' ) {
+			a = search == 'none' ?
+				displayMaster.slice() :                      // no search
+				search == 'applied' ?
+					displayFiltered.slice() :                // applied search
+					$.map( displayMaster, function (el, i) { // removed search
+						return $.inArray( el, displayFiltered ) === -1 ? el : null;
+					} );
+		}
+		else if ( order == 'index' || order == 'original' ) {
+			for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
+				if ( search == 'none' ) {
+					a.push( i );
+				}
+				else { // applied | removed
+					tmp = $.inArray( i, displayFiltered );
+	
+					if ((tmp === -1 && search == 'removed') ||
+						(tmp >= 0   && search == 'applied') )
+					{
+						a.push( i );
+					}
+				}
+			}
+		}
+	
+		return a;
+	};
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Rows
+	 *
+	 * {}          - no selector - use all available rows
+	 * {integer}   - row aoData index
+	 * {node}      - TR node
+	 * {string}    - jQuery selector to apply to the TR elements
+	 * {array}     - jQuery array of nodes, or simply an array of TR nodes
+	 *
+	 */
+	
+	
+	var __row_selector = function ( settings, selector, opts )
+	{
+		var rows;
+		var run = function ( sel ) {
+			var selInt = _intVal( sel );
+			var i, ien;
+	
+			// Short cut - selector is a number and no options provided (default is
+			// all records, so no need to check if the index is in there, since it
+			// must be - dev error if the index doesn't exist).
+			if ( selInt !== null && ! opts ) {
+				return [ selInt ];
+			}
+	
+			if ( ! rows ) {
+				rows = _selector_row_indexes( settings, opts );
+			}
+	
+			if ( selInt !== null && $.inArray( selInt, rows ) !== -1 ) {
+				// Selector - integer
+				return [ selInt ];
+			}
+			else if ( sel === null || sel === undefined || sel === '' ) {
+				// Selector - none
+				return rows;
+			}
+	
+			// Selector - function
+			if ( typeof sel === 'function' ) {
+				return $.map( rows, function (idx) {
+					var row = settings.aoData[ idx ];
+					return sel( idx, row._aData, row.nTr ) ? idx : null;
+				} );
+			}
+	
+			// Get nodes in the order from the `rows` array with null values removed
+			var nodes = _removeEmpty(
+				_pluck_order( settings.aoData, rows, 'nTr' )
+			);
+	
+			// Selector - node
+			if ( sel.nodeName ) {
+				if ( sel._DT_RowIndex !== undefined ) {
+					return [ sel._DT_RowIndex ]; // Property added by DT for fast lookup
+				}
+				else if ( sel._DT_CellIndex ) {
+					return [ sel._DT_CellIndex.row ];
+				}
+				else {
+					var host = $(sel).closest('*[data-dt-row]');
+					return host.length ?
+						[ host.data('dt-row') ] :
+						[];
+				}
+			}
+	
+			// ID selector. Want to always be able to select rows by id, regardless
+			// of if the tr element has been created or not, so can't rely upon
+			// jQuery here - hence a custom implementation. This does not match
+			// Sizzle's fast selector or HTML4 - in HTML5 the ID can be anything,
+			// but to select it using a CSS selector engine (like Sizzle or
+			// querySelect) it would need to need to be escaped for some characters.
+			// DataTables simplifies this for row selectors since you can select
+			// only a row. A # indicates an id any anything that follows is the id -
+			// unescaped.
+			if ( typeof sel === 'string' && sel.charAt(0) === '#' ) {
+				// get row index from id
+				var rowObj = settings.aIds[ sel.replace( /^#/, '' ) ];
+				if ( rowObj !== undefined ) {
+					return [ rowObj.idx ];
+				}
+	
+				// need to fall through to jQuery in case there is DOM id that
+				// matches
+			}
+	
+			// Selector - jQuery selector string, array of nodes or jQuery object/
+			// As jQuery's .filter() allows jQuery objects to be passed in filter,
+			// it also allows arrays, so this will cope with all three options
+			return $(nodes)
+				.filter( sel )
+				.map( function () {
+					return this._DT_RowIndex;
+				} )
+				.toArray();
+		};
+	
+		return _selector_run( 'row', selector, run, settings, opts );
+	};
+	
+	
+	_api_register( 'rows()', function ( selector, opts ) {
+		// argument shifting
+		if ( selector === undefined ) {
+			selector = '';
+		}
+		else if ( $.isPlainObject( selector ) ) {
+			opts = selector;
+			selector = '';
+		}
+	
+		opts = _selector_opts( opts );
+	
+		var inst = this.iterator( 'table', function ( settings ) {
+			return __row_selector( settings, selector, opts );
+		}, 1 );
+	
+		// Want argument shifting here and in __row_selector?
+		inst.selector.rows = selector;
+		inst.selector.opts = opts;
+	
+		return inst;
+	} );
+	
+	_api_register( 'rows().nodes()', function () {
+		return this.iterator( 'row', function ( settings, row ) {
+			return settings.aoData[ row ].nTr || undefined;
+		}, 1 );
+	} );
+	
+	_api_register( 'rows().data()', function () {
+		return this.iterator( true, 'rows', function ( settings, rows ) {
+			return _pluck_order( settings.aoData, rows, '_aData' );
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'rows().cache()', 'row().cache()', function ( type ) {
+		return this.iterator( 'row', function ( settings, row ) {
+			var r = settings.aoData[ row ];
+			return type === 'search' ? r._aFilterData : r._aSortData;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'rows().invalidate()', 'row().invalidate()', function ( src ) {
+		return this.iterator( 'row', function ( settings, row ) {
+			_fnInvalidate( settings, row, src );
+		} );
+	} );
+	
+	_api_registerPlural( 'rows().indexes()', 'row().index()', function () {
+		return this.iterator( 'row', function ( settings, row ) {
+			return row;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'rows().ids()', 'row().id()', function ( hash ) {
+		var a = [];
+		var context = this.context;
+	
+		// `iterator` will drop undefined values, but in this case we want them
+		for ( var i=0, ien=context.length ; i<ien ; i++ ) {
+			for ( var j=0, jen=this[i].length ; j<jen ; j++ ) {
+				var id = context[i].rowIdFn( context[i].aoData[ this[i][j] ]._aData );
+				a.push( (hash === true ? '#' : '' )+ id );
+			}
+		}
+	
+		return new _Api( context, a );
+	} );
+	
+	_api_registerPlural( 'rows().remove()', 'row().remove()', function () {
+		var that = this;
+	
+		this.iterator( 'row', function ( settings, row, thatIdx ) {
+			var data = settings.aoData;
+			var rowData = data[ row ];
+			var i, ien, j, jen;
+			var loopRow, loopCells;
+	
+			data.splice( row, 1 );
+	
+			// Update the cached indexes
+			for ( i=0, ien=data.length ; i<ien ; i++ ) {
+				loopRow = data[i];
+				loopCells = loopRow.anCells;
+	
+				// Rows
+				if ( loopRow.nTr !== null ) {
+					loopRow.nTr._DT_RowIndex = i;
+				}
+	
+				// Cells
+				if ( loopCells !== null ) {
+					for ( j=0, jen=loopCells.length ; j<jen ; j++ ) {
+						loopCells[j]._DT_CellIndex.row = i;
+					}
+				}
+			}
+	
+			// Delete from the display arrays
+			_fnDeleteIndex( settings.aiDisplayMaster, row );
+			_fnDeleteIndex( settings.aiDisplay, row );
+			_fnDeleteIndex( that[ thatIdx ], row, false ); // maintain local indexes
+	
+			// Check for an 'overflow' they case for displaying the table
+			_fnLengthOverflow( settings );
+	
+			// Remove the row's ID reference if there is one
+			var id = settings.rowIdFn( rowData._aData );
+			if ( id !== undefined ) {
+				delete settings.aIds[ id ];
+			}
+		} );
+	
+		this.iterator( 'table', function ( settings ) {
+			for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
+				settings.aoData[i].idx = i;
+			}
+		} );
+	
+		return this;
+	} );
+	
+	
+	_api_register( 'rows.add()', function ( rows ) {
+		var newRows = this.iterator( 'table', function ( settings ) {
+				var row, i, ien;
+				var out = [];
+	
+				for ( i=0, ien=rows.length ; i<ien ; i++ ) {
+					row = rows[i];
+	
+					if ( row.nodeName && row.nodeName.toUpperCase() === 'TR' ) {
+						out.push( _fnAddTr( settings, row )[0] );
+					}
+					else {
+						out.push( _fnAddData( settings, row ) );
+					}
+				}
+	
+				return out;
+			}, 1 );
+	
+		// Return an Api.rows() extended instance, so rows().nodes() etc can be used
+		var modRows = this.rows( -1 );
+		modRows.pop();
+		$.merge( modRows, newRows );
+	
+		return modRows;
+	} );
+	
+	
+	
+	
+	
+	/**
+	 *
+	 */
+	_api_register( 'row()', function ( selector, opts ) {
+		return _selector_first( this.rows( selector, opts ) );
+	} );
+	
+	
+	_api_register( 'row().data()', function ( data ) {
+		var ctx = this.context;
+	
+		if ( data === undefined ) {
+			// Get
+			return ctx.length && this.length ?
+				ctx[0].aoData[ this[0] ]._aData :
+				undefined;
+		}
+	
+		// Set
+		ctx[0].aoData[ this[0] ]._aData = data;
+	
+		// Automatically invalidate
+		_fnInvalidate( ctx[0], this[0], 'data' );
+	
+		return this;
+	} );
+	
+	
+	_api_register( 'row().node()', function () {
+		var ctx = this.context;
+	
+		return ctx.length && this.length ?
+			ctx[0].aoData[ this[0] ].nTr || null :
+			null;
+	} );
+	
+	
+	_api_register( 'row.add()', function ( row ) {
+		// Allow a jQuery object to be passed in - only a single row is added from
+		// it though - the first element in the set
+		if ( row instanceof $ && row.length ) {
+			row = row[0];
+		}
+	
+		var rows = this.iterator( 'table', function ( settings ) {
+			if ( row.nodeName && row.nodeName.toUpperCase() === 'TR' ) {
+				return _fnAddTr( settings, row )[0];
+			}
+			return _fnAddData( settings, row );
+		} );
+	
+		// Return an Api.rows() extended instance, with the newly added row selected
+		return this.row( rows[0] );
+	} );
+	
+	
+	
+	var __details_add = function ( ctx, row, data, klass )
+	{
+		// Convert to array of TR elements
+		var rows = [];
+		var addRow = function ( r, k ) {
+			// Recursion to allow for arrays of jQuery objects
+			if ( $.isArray( r ) || r instanceof $ ) {
+				for ( var i=0, ien=r.length ; i<ien ; i++ ) {
+					addRow( r[i], k );
+				}
+				return;
+			}
+	
+			// If we get a TR element, then just add it directly - up to the dev
+			// to add the correct number of columns etc
+			if ( r.nodeName && r.nodeName.toLowerCase() === 'tr' ) {
+				rows.push( r );
+			}
+			else {
+				// Otherwise create a row with a wrapper
+				var created = $('<tr><td/></tr>').addClass( k );
+				$('td', created)
+					.addClass( k )
+					.html( r )
+					[0].colSpan = _fnVisbleColumns( ctx );
+	
+				rows.push( created[0] );
+			}
+		};
+	
+		addRow( data, klass );
+	
+		if ( row._details ) {
+			row._details.detach();
+		}
+	
+		row._details = $(rows);
+	
+		// If the children were already shown, that state should be retained
+		if ( row._detailsShow ) {
+			row._details.insertAfter( row.nTr );
+		}
+	};
+	
+	
+	var __details_remove = function ( api, idx )
+	{
+		var ctx = api.context;
+	
+		if ( ctx.length ) {
+			var row = ctx[0].aoData[ idx !== undefined ? idx : api[0] ];
+	
+			if ( row && row._details ) {
+				row._details.remove();
+	
+				row._detailsShow = undefined;
+				row._details = undefined;
+			}
+		}
+	};
+	
+	
+	var __details_display = function ( api, show ) {
+		var ctx = api.context;
+	
+		if ( ctx.length && api.length ) {
+			var row = ctx[0].aoData[ api[0] ];
+	
+			if ( row._details ) {
+				row._detailsShow = show;
+	
+				if ( show ) {
+					row._details.insertAfter( row.nTr );
+				}
+				else {
+					row._details.detach();
+				}
+	
+				__details_events( ctx[0] );
+			}
+		}
+	};
+	
+	
+	var __details_events = function ( settings )
+	{
+		var api = new _Api( settings );
+		var namespace = '.dt.DT_details';
+		var drawEvent = 'draw'+namespace;
+		var colvisEvent = 'column-visibility'+namespace;
+		var destroyEvent = 'destroy'+namespace;
+		var data = settings.aoData;
+	
+		api.off( drawEvent +' '+ colvisEvent +' '+ destroyEvent );
+	
+		if ( _pluck( data, '_details' ).length > 0 ) {
+			// On each draw, insert the required elements into the document
+			api.on( drawEvent, function ( e, ctx ) {
+				if ( settings !== ctx ) {
+					return;
+				}
+	
+				api.rows( {page:'current'} ).eq(0).each( function (idx) {
+					// Internal data grab
+					var row = data[ idx ];
+	
+					if ( row._detailsShow ) {
+						row._details.insertAfter( row.nTr );
+					}
+				} );
+			} );
+	
+			// Column visibility change - update the colspan
+			api.on( colvisEvent, function ( e, ctx, idx, vis ) {
+				if ( settings !== ctx ) {
+					return;
+				}
+	
+				// Update the colspan for the details rows (note, only if it already has
+				// a colspan)
+				var row, visible = _fnVisbleColumns( ctx );
+	
+				for ( var i=0, ien=data.length ; i<ien ; i++ ) {
+					row = data[i];
+	
+					if ( row._details ) {
+						row._details.children('td[colspan]').attr('colspan', visible );
+					}
+				}
+			} );
+	
+			// Table destroyed - nuke any child rows
+			api.on( destroyEvent, function ( e, ctx ) {
+				if ( settings !== ctx ) {
+					return;
+				}
+	
+				for ( var i=0, ien=data.length ; i<ien ; i++ ) {
+					if ( data[i]._details ) {
+						__details_remove( api, i );
+					}
+				}
+			} );
+		}
+	};
+	
+	// Strings for the method names to help minification
+	var _emp = '';
+	var _child_obj = _emp+'row().child';
+	var _child_mth = _child_obj+'()';
+	
+	// data can be:
+	//  tr
+	//  string
+	//  jQuery or array of any of the above
+	_api_register( _child_mth, function ( data, klass ) {
+		var ctx = this.context;
+	
+		if ( data === undefined ) {
+			// get
+			return ctx.length && this.length ?
+				ctx[0].aoData[ this[0] ]._details :
+				undefined;
+		}
+		else if ( data === true ) {
+			// show
+			this.child.show();
+		}
+		else if ( data === false ) {
+			// remove
+			__details_remove( this );
+		}
+		else if ( ctx.length && this.length ) {
+			// set
+			__details_add( ctx[0], ctx[0].aoData[ this[0] ], data, klass );
+		}
+	
+		return this;
+	} );
+	
+	
+	_api_register( [
+		_child_obj+'.show()',
+		_child_mth+'.show()' // only when `child()` was called with parameters (without
+	], function ( show ) {   // it returns an object and this method is not executed)
+		__details_display( this, true );
+		return this;
+	} );
+	
+	
+	_api_register( [
+		_child_obj+'.hide()',
+		_child_mth+'.hide()' // only when `child()` was called with parameters (without
+	], function () {         // it returns an object and this method is not executed)
+		__details_display( this, false );
+		return this;
+	} );
+	
+	
+	_api_register( [
+		_child_obj+'.remove()',
+		_child_mth+'.remove()' // only when `child()` was called with parameters (without
+	], function () {           // it returns an object and this method is not executed)
+		__details_remove( this );
+		return this;
+	} );
+	
+	
+	_api_register( _child_obj+'.isShown()', function () {
+		var ctx = this.context;
+	
+		if ( ctx.length && this.length ) {
+			// _detailsShown as false or undefined will fall through to return false
+			return ctx[0].aoData[ this[0] ]._detailsShow || false;
+		}
+		return false;
+	} );
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Columns
+	 *
+	 * {integer}           - column index (>=0 count from left, <0 count from right)
+	 * "{integer}:visIdx"  - visible column index (i.e. translate to column index)  (>=0 count from left, <0 count from right)
+	 * "{integer}:visible" - alias for {integer}:visIdx  (>=0 count from left, <0 count from right)
+	 * "{string}:name"     - column name
+	 * "{string}"          - jQuery selector on column header nodes
+	 *
+	 */
+	
+	// can be an array of these items, comma separated list, or an array of comma
+	// separated lists
+	
+	var __re_column_selector = /^([^:]+):(name|visIdx|visible)$/;
+	
+	
+	// r1 and r2 are redundant - but it means that the parameters match for the
+	// iterator callback in columns().data()
+	var __columnData = function ( settings, column, r1, r2, rows ) {
+		var a = [];
+		for ( var row=0, ien=rows.length ; row<ien ; row++ ) {
+			a.push( _fnGetCellData( settings, rows[row], column ) );
+		}
+		return a;
+	};
+	
+	
+	var __column_selector = function ( settings, selector, opts )
+	{
+		var
+			columns = settings.aoColumns,
+			names = _pluck( columns, 'sName' ),
+			nodes = _pluck( columns, 'nTh' );
+	
+		var run = function ( s ) {
+			var selInt = _intVal( s );
+	
+			// Selector - all
+			if ( s === '' ) {
+				return _range( columns.length );
+			}
+	
+			// Selector - index
+			if ( selInt !== null ) {
+				return [ selInt >= 0 ?
+					selInt : // Count from left
+					columns.length + selInt // Count from right (+ because its a negative value)
+				];
+			}
+	
+			// Selector = function
+			if ( typeof s === 'function' ) {
+				var rows = _selector_row_indexes( settings, opts );
+	
+				return $.map( columns, function (col, idx) {
+					return s(
+							idx,
+							__columnData( settings, idx, 0, 0, rows ),
+							nodes[ idx ]
+						) ? idx : null;
+				} );
+			}
+	
+			// jQuery or string selector
+			var match = typeof s === 'string' ?
+				s.match( __re_column_selector ) :
+				'';
+	
+			if ( match ) {
+				switch( match[2] ) {
+					case 'visIdx':
+					case 'visible':
+						var idx = parseInt( match[1], 10 );
+						// Visible index given, convert to column index
+						if ( idx < 0 ) {
+							// Counting from the right
+							var visColumns = $.map( columns, function (col,i) {
+								return col.bVisible ? i : null;
+							} );
+							return [ visColumns[ visColumns.length + idx ] ];
+						}
+						// Counting from the left
+						return [ _fnVisibleToColumnIndex( settings, idx ) ];
+	
+					case 'name':
+						// match by name. `names` is column index complete and in order
+						return $.map( names, function (name, i) {
+							return name === match[1] ? i : null;
+						} );
+	
+					default:
+						return [];
+				}
+			}
+	
+			// Cell in the table body
+			if ( s.nodeName && s._DT_CellIndex ) {
+				return [ s._DT_CellIndex.column ];
+			}
+	
+			// jQuery selector on the TH elements for the columns
+			var jqResult = $( nodes )
+				.filter( s )
+				.map( function () {
+					return $.inArray( this, nodes ); // `nodes` is column index complete and in order
+				} )
+				.toArray();
+	
+			if ( jqResult.length || ! s.nodeName ) {
+				return jqResult;
+			}
+	
+			// Otherwise a node which might have a `dt-column` data attribute, or be
+			// a child or such an element
+			var host = $(s).closest('*[data-dt-column]');
+			return host.length ?
+				[ host.data('dt-column') ] :
+				[];
+		};
+	
+		return _selector_run( 'column', selector, run, settings, opts );
+	};
+	
+	
+	var __setColumnVis = function ( settings, column, vis ) {
+		var
+			cols = settings.aoColumns,
+			col  = cols[ column ],
+			data = settings.aoData,
+			row, cells, i, ien, tr;
+	
+		// Get
+		if ( vis === undefined ) {
+			return col.bVisible;
+		}
+	
+		// Set
+		// No change
+		if ( col.bVisible === vis ) {
+			return;
+		}
+	
+		if ( vis ) {
+			// Insert column
+			// Need to decide if we should use appendChild or insertBefore
+			var insertBefore = $.inArray( true, _pluck(cols, 'bVisible'), column+1 );
+	
+			for ( i=0, ien=data.length ; i<ien ; i++ ) {
+				tr = data[i].nTr;
+				cells = data[i].anCells;
+	
+				if ( tr ) {
+					// insertBefore can act like appendChild if 2nd arg is null
+					tr.insertBefore( cells[ column ], cells[ insertBefore ] || null );
+				}
+			}
+		}
+		else {
+			// Remove column
+			$( _pluck( settings.aoData, 'anCells', column ) ).detach();
+		}
+	
+		// Common actions
+		col.bVisible = vis;
+		_fnDrawHead( settings, settings.aoHeader );
+		_fnDrawHead( settings, settings.aoFooter );
+	
+		_fnSaveState( settings );
+	};
+	
+	
+	_api_register( 'columns()', function ( selector, opts ) {
+		// argument shifting
+		if ( selector === undefined ) {
+			selector = '';
+		}
+		else if ( $.isPlainObject( selector ) ) {
+			opts = selector;
+			selector = '';
+		}
+	
+		opts = _selector_opts( opts );
+	
+		var inst = this.iterator( 'table', function ( settings ) {
+			return __column_selector( settings, selector, opts );
+		}, 1 );
+	
+		// Want argument shifting here and in _row_selector?
+		inst.selector.cols = selector;
+		inst.selector.opts = opts;
+	
+		return inst;
+	} );
+	
+	_api_registerPlural( 'columns().header()', 'column().header()', function ( selector, opts ) {
+		return this.iterator( 'column', function ( settings, column ) {
+			return settings.aoColumns[column].nTh;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().footer()', 'column().footer()', function ( selector, opts ) {
+		return this.iterator( 'column', function ( settings, column ) {
+			return settings.aoColumns[column].nTf;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().data()', 'column().data()', function () {
+		return this.iterator( 'column-rows', __columnData, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().dataSrc()', 'column().dataSrc()', function () {
+		return this.iterator( 'column', function ( settings, column ) {
+			return settings.aoColumns[column].mData;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().cache()', 'column().cache()', function ( type ) {
+		return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {
+			return _pluck_order( settings.aoData, rows,
+				type === 'search' ? '_aFilterData' : '_aSortData', column
+			);
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().nodes()', 'column().nodes()', function () {
+		return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {
+			return _pluck_order( settings.aoData, rows, 'anCells', column ) ;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().visible()', 'column().visible()', function ( vis, calc ) {
+		var ret = this.iterator( 'column', function ( settings, column ) {
+			if ( vis === undefined ) {
+				return settings.aoColumns[ column ].bVisible;
+			} // else
+			__setColumnVis( settings, column, vis );
+		} );
+	
+		// Group the column visibility changes
+		if ( vis !== undefined ) {
+			// Second loop once the first is done for events
+			this.iterator( 'column', function ( settings, column ) {
+				_fnCallbackFire( settings, null, 'column-visibility', [settings, column, vis, calc] );
+			} );
+	
+			if ( calc === undefined || calc ) {
+				this.columns.adjust();
+			}
+		}
+	
+		return ret;
+	} );
+	
+	_api_registerPlural( 'columns().indexes()', 'column().index()', function ( type ) {
+		return this.iterator( 'column', function ( settings, column ) {
+			return type === 'visible' ?
+				_fnColumnIndexToVisible( settings, column ) :
+				column;
+		}, 1 );
+	} );
+	
+	_api_register( 'columns.adjust()', function () {
+		return this.iterator( 'table', function ( settings ) {
+			_fnAdjustColumnSizing( settings );
+		}, 1 );
+	} );
+	
+	_api_register( 'column.index()', function ( type, idx ) {
+		if ( this.context.length !== 0 ) {
+			var ctx = this.context[0];
+	
+			if ( type === 'fromVisible' || type === 'toData' ) {
+				return _fnVisibleToColumnIndex( ctx, idx );
+			}
+			else if ( type === 'fromData' || type === 'toVisible' ) {
+				return _fnColumnIndexToVisible( ctx, idx );
+			}
+		}
+	} );
+	
+	_api_register( 'column()', function ( selector, opts ) {
+		return _selector_first( this.columns( selector, opts ) );
+	} );
+	
+	
+	
+	var __cell_selector = function ( settings, selector, opts )
+	{
+		var data = settings.aoData;
+		var rows = _selector_row_indexes( settings, opts );
+		var cells = _removeEmpty( _pluck_order( data, rows, 'anCells' ) );
+		var allCells = $( [].concat.apply([], cells) );
+		var row;
+		var columns = settings.aoColumns.length;
+		var a, i, ien, j, o, host;
+	
+		var run = function ( s ) {
+			var fnSelector = typeof s === 'function';
+	
+			if ( s === null || s === undefined || fnSelector ) {
+				// All cells and function selectors
+				a = [];
+	
+				for ( i=0, ien=rows.length ; i<ien ; i++ ) {
+					row = rows[i];
+	
+					for ( j=0 ; j<columns ; j++ ) {
+						o = {
+							row: row,
+							column: j
+						};
+	
+						if ( fnSelector ) {
+							// Selector - function
+							host = data[ row ];
+	
+							if ( s( o, _fnGetCellData(settings, row, j), host.anCells ? host.anCells[j] : null ) ) {
+								a.push( o );
+							}
+						}
+						else {
+							// Selector - all
+							a.push( o );
+						}
+					}
+				}
+	
+				return a;
+			}
+			
+			// Selector - index
+			if ( $.isPlainObject( s ) ) {
+				return [s];
+			}
+	
+			// Selector - jQuery filtered cells
+			var jqResult = allCells
+				.filter( s )
+				.map( function (i, el) {
+					return { // use a new object, in case someone changes the values
+						row:    el._DT_CellIndex.row,
+						column: el._DT_CellIndex.column
+	 				};
+				} )
+				.toArray();
+	
+			if ( jqResult.length || ! s.nodeName ) {
+				return jqResult;
+			}
+	
+			// Otherwise the selector is a node, and there is one last option - the
+			// element might be a child of an element which has dt-row and dt-column
+			// data attributes
+			host = $(s).closest('*[data-dt-row]');
+			return host.length ?
+				[ {
+					row: host.data('dt-row'),
+					column: host.data('dt-column')
+				} ] :
+				[];
+		};
+	
+		return _selector_run( 'cell', selector, run, settings, opts );
+	};
+	
+	
+	
+	
+	_api_register( 'cells()', function ( rowSelector, columnSelector, opts ) {
+		// Argument shifting
+		if ( $.isPlainObject( rowSelector ) ) {
+			// Indexes
+			if ( rowSelector.row === undefined ) {
+				// Selector options in first parameter
+				opts = rowSelector;
+				rowSelector = null;
+			}
+			else {
+				// Cell index objects in first parameter
+				opts = columnSelector;
+				columnSelector = null;
+			}
+		}
+		if ( $.isPlainObject( columnSelector ) ) {
+			opts = columnSelector;
+			columnSelector = null;
+		}
+	
+		// Cell selector
+		if ( columnSelector === null || columnSelector === undefined ) {
+			return this.iterator( 'table', function ( settings ) {
+				return __cell_selector( settings, rowSelector, _selector_opts( opts ) );
+			} );
+		}
+	
+		// Row + column selector
+		var columns = this.columns( columnSelector, opts );
+		var rows = this.rows( rowSelector, opts );
+		var a, i, ien, j, jen;
+	
+		var cells = this.iterator( 'table', function ( settings, idx ) {
+			a = [];
+	
+			for ( i=0, ien=rows[idx].length ; i<ien ; i++ ) {
+				for ( j=0, jen=columns[idx].length ; j<jen ; j++ ) {
+					a.push( {
+						row:    rows[idx][i],
+						column: columns[idx][j]
+					} );
+				}
+			}
+	
+			return a;
+		}, 1 );
+	
+		$.extend( cells.selector, {
+			cols: columnSelector,
+			rows: rowSelector,
+			opts: opts
+		} );
+	
+		return cells;
+	} );
+	
+	
+	_api_registerPlural( 'cells().nodes()', 'cell().node()', function () {
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			var data = settings.aoData[ row ];
+	
+			return data && data.anCells ?
+				data.anCells[ column ] :
+				undefined;
+		}, 1 );
+	} );
+	
+	
+	_api_register( 'cells().data()', function () {
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			return _fnGetCellData( settings, row, column );
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'cells().cache()', 'cell().cache()', function ( type ) {
+		type = type === 'search' ? '_aFilterData' : '_aSortData';
+	
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			return settings.aoData[ row ][ type ][ column ];
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'cells().render()', 'cell().render()', function ( type ) {
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			return _fnGetCellData( settings, row, column, type );
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'cells().indexes()', 'cell().index()', function () {
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			return {
+				row: row,
+				column: column,
+				columnVisible: _fnColumnIndexToVisible( settings, column )
+			};
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'cells().invalidate()', 'cell().invalidate()', function ( src ) {
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			_fnInvalidate( settings, row, src, column );
+		} );
+	} );
+	
+	
+	
+	_api_register( 'cell()', function ( rowSelector, columnSelector, opts ) {
+		return _selector_first( this.cells( rowSelector, columnSelector, opts ) );
+	} );
+	
+	
+	_api_register( 'cell().data()', function ( data ) {
+		var ctx = this.context;
+		var cell = this[0];
+	
+		if ( data === undefined ) {
+			// Get
+			return ctx.length && cell.length ?
+				_fnGetCellData( ctx[0], cell[0].row, cell[0].column ) :
+				undefined;
+		}
+	
+		// Set
+		_fnSetCellData( ctx[0], cell[0].row, cell[0].column, data );
+		_fnInvalidate( ctx[0], cell[0].row, 'data', cell[0].column );
+	
+		return this;
+	} );
+	
+	
+	
+	/**
+	 * Get current ordering (sorting) that has been applied to the table.
+	 *
+	 * @returns {array} 2D array containing the sorting information for the first
+	 *   table in the current context. Each element in the parent array represents
+	 *   a column being sorted upon (i.e. multi-sorting with two columns would have
+	 *   2 inner arrays). The inner arrays may have 2 or 3 elements. The first is
+	 *   the column index that the sorting condition applies to, the second is the
+	 *   direction of the sort (`desc` or `asc`) and, optionally, the third is the
+	 *   index of the sorting order from the `column.sorting` initialisation array.
+	 *//**
+	 * Set the ordering for the table.
+	 *
+	 * @param {integer} order Column index to sort upon.
+	 * @param {string} direction Direction of the sort to be applied (`asc` or `desc`)
+	 * @returns {DataTables.Api} this
+	 *//**
+	 * Set the ordering for the table.
+	 *
+	 * @param {array} order 1D array of sorting information to be applied.
+	 * @param {array} [...] Optional additional sorting conditions
+	 * @returns {DataTables.Api} this
+	 *//**
+	 * Set the ordering for the table.
+	 *
+	 * @param {array} order 2D array of sorting information to be applied.
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'order()', function ( order, dir ) {
+		var ctx = this.context;
+	
+		if ( order === undefined ) {
+			// get
+			return ctx.length !== 0 ?
+				ctx[0].aaSorting :
+				undefined;
+		}
+	
+		// set
+		if ( typeof order === 'number' ) {
+			// Simple column / direction passed in
+			order = [ [ order, dir ] ];
+		}
+		else if ( order.length && ! $.isArray( order[0] ) ) {
+			// Arguments passed in (list of 1D arrays)
+			order = Array.prototype.slice.call( arguments );
+		}
+		// otherwise a 2D array was passed in
+	
+		return this.iterator( 'table', function ( settings ) {
+			settings.aaSorting = order.slice();
+		} );
+	} );
+	
+	
+	/**
+	 * Attach a sort listener to an element for a given column
+	 *
+	 * @param {node|jQuery|string} node Identifier for the element(s) to attach the
+	 *   listener to. This can take the form of a single DOM node, a jQuery
+	 *   collection of nodes or a jQuery selector which will identify the node(s).
+	 * @param {integer} column the column that a click on this node will sort on
+	 * @param {function} [callback] callback function when sort is run
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'order.listener()', function ( node, column, callback ) {
+		return this.iterator( 'table', function ( settings ) {
+			_fnSortAttachListener( settings, node, column, callback );
+		} );
+	} );
+	
+	
+	_api_register( 'order.fixed()', function ( set ) {
+		if ( ! set ) {
+			var ctx = this.context;
+			var fixed = ctx.length ?
+				ctx[0].aaSortingFixed :
+				undefined;
+	
+			return $.isArray( fixed ) ?
+				{ pre: fixed } :
+				fixed;
+		}
+	
+		return this.iterator( 'table', function ( settings ) {
+			settings.aaSortingFixed = $.extend( true, {}, set );
+		} );
+	} );
+	
+	
+	// Order by the selected column(s)
+	_api_register( [
+		'columns().order()',
+		'column().order()'
+	], function ( dir ) {
+		var that = this;
+	
+		return this.iterator( 'table', function ( settings, i ) {
+			var sort = [];
+	
+			$.each( that[i], function (j, col) {
+				sort.push( [ col, dir ] );
+			} );
+	
+			settings.aaSorting = sort;
+		} );
+	} );
+	
+	
+	
+	_api_register( 'search()', function ( input, regex, smart, caseInsen ) {
+		var ctx = this.context;
+	
+		if ( input === undefined ) {
+			// get
+			return ctx.length !== 0 ?
+				ctx[0].oPreviousSearch.sSearch :
+				undefined;
+		}
+	
+		// set
+		return this.iterator( 'table', function ( settings ) {
+			if ( ! settings.oFeatures.bFilter ) {
+				return;
+			}
+	
+			_fnFilterComplete( settings, $.extend( {}, settings.oPreviousSearch, {
+				"sSearch": input+"",
+				"bRegex":  regex === null ? false : regex,
+				"bSmart":  smart === null ? true  : smart,
+				"bCaseInsensitive": caseInsen === null ? true : caseInsen
+			} ), 1 );
+		} );
+	} );
+	
+	
+	_api_registerPlural(
+		'columns().search()',
+		'column().search()',
+		function ( input, regex, smart, caseInsen ) {
+			return this.iterator( 'column', function ( settings, column ) {
+				var preSearch = settings.aoPreSearchCols;
+	
+				if ( input === undefined ) {
+					// get
+					return preSearch[ column ].sSearch;
+				}
+	
+				// set
+				if ( ! settings.oFeatures.bFilter ) {
+					return;
+				}
+	
+				$.extend( preSearch[ column ], {
+					"sSearch": input+"",
+					"bRegex":  regex === null ? false : regex,
+					"bSmart":  smart === null ? true  : smart,
+					"bCaseInsensitive": caseInsen === null ? true : caseInsen
+				} );
+	
+				_fnFilterComplete( settings, settings.oPreviousSearch, 1 );
+			} );
+		}
+	);
+	
+	/*
+	 * State API methods
+	 */
+	
+	_api_register( 'state()', function () {
+		return this.context.length ?
+			this.context[0].oSavedState :
+			null;
+	} );
+	
+	
+	_api_register( 'state.clear()', function () {
+		return this.iterator( 'table', function ( settings ) {
+			// Save an empty object
+			settings.fnStateSaveCallback.call( settings.oInstance, settings, {} );
+		} );
+	} );
+	
+	
+	_api_register( 'state.loaded()', function () {
+		return this.context.length ?
+			this.context[0].oLoadedState :
+			null;
+	} );
+	
+	
+	_api_register( 'state.save()', function () {
+		return this.iterator( 'table', function ( settings ) {
+			_fnSaveState( settings );
+		} );
+	} );
+	
+	
+	
+	/**
+	 * Provide a common method for plug-ins to check the version of DataTables being
+	 * used, in order to ensure compatibility.
+	 *
+	 *  @param {string} version Version string to check for, in the format "X.Y.Z".
+	 *    Note that the formats "X" and "X.Y" are also acceptable.
+	 *  @returns {boolean} true if this version of DataTables is greater or equal to
+	 *    the required version, or false if this version of DataTales is not
+	 *    suitable
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    alert( $.fn.dataTable.versionCheck( '1.9.0' ) );
+	 */
+	DataTable.versionCheck = DataTable.fnVersionCheck = function( version )
+	{
+		var aThis = DataTable.version.split('.');
+		var aThat = version.split('.');
+		var iThis, iThat;
+	
+		for ( var i=0, iLen=aThat.length ; i<iLen ; i++ ) {
+			iThis = parseInt( aThis[i], 10 ) || 0;
+			iThat = parseInt( aThat[i], 10 ) || 0;
+	
+			// Parts are the same, keep comparing
+			if (iThis === iThat) {
+				continue;
+			}
+	
+			// Parts are different, return immediately
+			return iThis > iThat;
+		}
+	
+		return true;
+	};
+	
+	
+	/**
+	 * Check if a `<table>` node is a DataTable table already or not.
+	 *
+	 *  @param {node|jquery|string} table Table node, jQuery object or jQuery
+	 *      selector for the table to test. Note that if more than more than one
+	 *      table is passed on, only the first will be checked
+	 *  @returns {boolean} true the table given is a DataTable, or false otherwise
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    if ( ! $.fn.DataTable.isDataTable( '#example' ) ) {
+	 *      $('#example').dataTable();
+	 *    }
+	 */
+	DataTable.isDataTable = DataTable.fnIsDataTable = function ( table )
+	{
+		var t = $(table).get(0);
+		var is = false;
+	
+		if ( table instanceof DataTable.Api ) {
+			return true;
+		}
+	
+		$.each( DataTable.settings, function (i, o) {
+			var head = o.nScrollHead ? $('table', o.nScrollHead)[0] : null;
+			var foot = o.nScrollFoot ? $('table', o.nScrollFoot)[0] : null;
+	
+			if ( o.nTable === t || head === t || foot === t ) {
+				is = true;
+			}
+		} );
+	
+		return is;
+	};
+	
+	
+	/**
+	 * Get all DataTable tables that have been initialised - optionally you can
+	 * select to get only currently visible tables.
+	 *
+	 *  @param {boolean} [visible=false] Flag to indicate if you want all (default)
+	 *    or visible tables only.
+	 *  @returns {array} Array of `table` nodes (not DataTable instances) which are
+	 *    DataTables
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    $.each( $.fn.dataTable.tables(true), function () {
+	 *      $(table).DataTable().columns.adjust();
+	 *    } );
+	 */
+	DataTable.tables = DataTable.fnTables = function ( visible )
+	{
+		var api = false;
+	
+		if ( $.isPlainObject( visible ) ) {
+			api = visible.api;
+			visible = visible.visible;
+		}
+	
+		var a = $.map( DataTable.settings, function (o) {
+			if ( !visible || (visible && $(o.nTable).is(':visible')) ) {
+				return o.nTable;
+			}
+		} );
+	
+		return api ?
+			new _Api( a ) :
+			a;
+	};
+	
+	
+	/**
+	 * Convert from camel case parameters to Hungarian notation. This is made public
+	 * for the extensions to provide the same ability as DataTables core to accept
+	 * either the 1.9 style Hungarian notation, or the 1.10+ style camelCase
+	 * parameters.
+	 *
+	 *  @param {object} src The model object which holds all parameters that can be
+	 *    mapped.
+	 *  @param {object} user The object to convert from camel case to Hungarian.
+	 *  @param {boolean} force When set to `true`, properties which already have a
+	 *    Hungarian value in the `user` object will be overwritten. Otherwise they
+	 *    won't be.
+	 */
+	DataTable.camelToHungarian = _fnCamelToHungarian;
+	
+	
+	
+	/**
+	 *
+	 */
+	_api_register( '$()', function ( selector, opts ) {
+		var
+			rows   = this.rows( opts ).nodes(), // Get all rows
+			jqRows = $(rows);
+	
+		return $( [].concat(
+			jqRows.filter( selector ).toArray(),
+			jqRows.find( selector ).toArray()
+		) );
+	} );
+	
+	
+	// jQuery functions to operate on the tables
+	$.each( [ 'on', 'one', 'off' ], function (i, key) {
+		_api_register( key+'()', function ( /* event, handler */ ) {
+			var args = Array.prototype.slice.call(arguments);
+	
+			// Add the `dt` namespace automatically if it isn't already present
+			args[0] = $.map( args[0].split( /\s/ ), function ( e ) {
+				return ! e.match(/\.dt\b/) ?
+					e+'.dt' :
+					e;
+				} ).join( ' ' );
+	
+			var inst = $( this.tables().nodes() );
+			inst[key].apply( inst, args );
+			return this;
+		} );
+	} );
+	
+	
+	_api_register( 'clear()', function () {
+		return this.iterator( 'table', function ( settings ) {
+			_fnClearTable( settings );
+		} );
+	} );
+	
+	
+	_api_register( 'settings()', function () {
+		return new _Api( this.context, this.context );
+	} );
+	
+	
+	_api_register( 'init()', function () {
+		var ctx = this.context;
+		return ctx.length ? ctx[0].oInit : null;
+	} );
+	
+	
+	_api_register( 'data()', function () {
+		return this.iterator( 'table', function ( settings ) {
+			return _pluck( settings.aoData, '_aData' );
+		} ).flatten();
+	} );
+	
+	
+	_api_register( 'destroy()', function ( remove ) {
+		remove = remove || false;
+	
+		return this.iterator( 'table', function ( settings ) {
+			var orig      = settings.nTableWrapper.parentNode;
+			var classes   = settings.oClasses;
+			var table     = settings.nTable;
+			var tbody     = settings.nTBody;
+			var thead     = settings.nTHead;
+			var tfoot     = settings.nTFoot;
+			var jqTable   = $(table);
+			var jqTbody   = $(tbody);
+			var jqWrapper = $(settings.nTableWrapper);
+			var rows      = $.map( settings.aoData, function (r) { return r.nTr; } );
+			var i, ien;
+	
+			// Flag to note that the table is currently being destroyed - no action
+			// should be taken
+			settings.bDestroying = true;
+	
+			// Fire off the destroy callbacks for plug-ins etc
+			_fnCallbackFire( settings, "aoDestroyCallback", "destroy", [settings] );
+	
+			// If not being removed from the document, make all columns visible
+			if ( ! remove ) {
+				new _Api( settings ).columns().visible( true );
+			}
+	
+			// Blitz all `DT` namespaced events (these are internal events, the
+			// lowercase, `dt` events are user subscribed and they are responsible
+			// for removing them
+			jqWrapper.off('.DT').find(':not(tbody *)').off('.DT');
+			$(window).off('.DT-'+settings.sInstance);
+	
+			// When scrolling we had to break the table up - restore it
+			if ( table != thead.parentNode ) {
+				jqTable.children('thead').detach();
+				jqTable.append( thead );
+			}
+	
+			if ( tfoot && table != tfoot.parentNode ) {
+				jqTable.children('tfoot').detach();
+				jqTable.append( tfoot );
+			}
+	
+			settings.aaSorting = [];
+			settings.aaSortingFixed = [];
+			_fnSortingClasses( settings );
+	
+			$( rows ).removeClass( settings.asStripeClasses.join(' ') );
+	
+			$('th, td', thead).removeClass( classes.sSortable+' '+
+				classes.sSortableAsc+' '+classes.sSortableDesc+' '+classes.sSortableNone
+			);
+	
+			if ( settings.bJUI ) {
+				$('th span.'+classes.sSortIcon+ ', td span.'+classes.sSortIcon, thead).detach();
+				$('th, td', thead).each( function () {
+					var wrapper = $('div.'+classes.sSortJUIWrapper, this);
+					$(this).append( wrapper.contents() );
+					wrapper.detach();
+				} );
+			}
+	
+			// Add the TR elements back into the table in their original order
+			jqTbody.children().detach();
+			jqTbody.append( rows );
+	
+			// Remove the DataTables generated nodes, events and classes
+			var removedMethod = remove ? 'remove' : 'detach';
+			jqTable[ removedMethod ]();
+			jqWrapper[ removedMethod ]();
+	
+			// If we need to reattach the table to the document
+			if ( ! remove && orig ) {
+				// insertBefore acts like appendChild if !arg[1]
+				orig.insertBefore( table, settings.nTableReinsertBefore );
+	
+				// Restore the width of the original table - was read from the style property,
+				// so we can restore directly to that
+				jqTable
+					.css( 'width', settings.sDestroyWidth )
+					.removeClass( classes.sTable );
+	
+				// If the were originally stripe classes - then we add them back here.
+				// Note this is not fool proof (for example if not all rows had stripe
+				// classes - but it's a good effort without getting carried away
+				ien = settings.asDestroyStripes.length;
+	
+				if ( ien ) {
+					jqTbody.children().each( function (i) {
+						$(this).addClass( settings.asDestroyStripes[i % ien] );
+					} );
+				}
+			}
+	
+			/* Remove the settings object from the settings array */
+			var idx = $.inArray( settings, DataTable.settings );
+			if ( idx !== -1 ) {
+				DataTable.settings.splice( idx, 1 );
+			}
+		} );
+	} );
+	
+	
+	// Add the `every()` method for rows, columns and cells in a compact form
+	$.each( [ 'column', 'row', 'cell' ], function ( i, type ) {
+		_api_register( type+'s().every()', function ( fn ) {
+			var opts = this.selector.opts;
+			var api = this;
+	
+			return this.iterator( type, function ( settings, arg1, arg2, arg3, arg4 ) {
+				// Rows and columns:
+				//  arg1 - index
+				//  arg2 - table counter
+				//  arg3 - loop counter
+				//  arg4 - undefined
+				// Cells:
+				//  arg1 - row index
+				//  arg2 - column index
+				//  arg3 - table counter
+				//  arg4 - loop counter
+				fn.call(
+					api[ type ](
+						arg1,
+						type==='cell' ? arg2 : opts,
+						type==='cell' ? opts : undefined
+					),
+					arg1, arg2, arg3, arg4
+				);
+			} );
+		} );
+	} );
+	
+	
+	// i18n method for extensions to be able to use the language object from the
+	// DataTable
+	_api_register( 'i18n()', function ( token, def, plural ) {
+		var ctx = this.context[0];
+		var resolved = _fnGetObjectDataFn( token )( ctx.oLanguage );
+	
+		if ( resolved === undefined ) {
+			resolved = def;
+		}
+	
+		if ( plural !== undefined && $.isPlainObject( resolved ) ) {
+			resolved = resolved[ plural ] !== undefined ?
+				resolved[ plural ] :
+				resolved._;
+		}
+	
+		return resolved.replace( '%d', plural ); // nb: plural might be undefined,
+	} );
+
+	/**
+	 * Version string for plug-ins to check compatibility. Allowed format is
+	 * `a.b.c-d` where: a:int, b:int, c:int, d:string(dev|beta|alpha). `d` is used
+	 * only for non-release builds. See http://semver.org/ for more information.
+	 *  @member
+	 *  @type string
+	 *  @default Version number
+	 */
+	DataTable.version = "1.10.15";
+
+	/**
+	 * Private data store, containing all of the settings objects that are
+	 * created for the tables on a given page.
+	 *
+	 * Note that the `DataTable.settings` object is aliased to
+	 * `jQuery.fn.dataTableExt` through which it may be accessed and
+	 * manipulated, or `jQuery.fn.dataTable.settings`.
+	 *  @member
+	 *  @type array
+	 *  @default []
+	 *  @private
+	 */
+	DataTable.settings = [];
+
+	/**
+	 * Object models container, for the various models that DataTables has
+	 * available to it. These models define the objects that are used to hold
+	 * the active state and configuration of the table.
+	 *  @namespace
+	 */
+	DataTable.models = {};
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * search information for the global filter and individual column filters.
+	 *  @namespace
+	 */
+	DataTable.models.oSearch = {
+		/**
+		 * Flag to indicate if the filtering should be case insensitive or not
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bCaseInsensitive": true,
+	
+		/**
+		 * Applied search term
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sSearch": "",
+	
+		/**
+		 * Flag to indicate if the search term should be interpreted as a
+		 * regular expression (true) or not (false) and therefore and special
+		 * regex characters escaped.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bRegex": false,
+	
+		/**
+		 * Flag to indicate if DataTables is to use its smart filtering or not.
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bSmart": true
+	};
+	
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * each individual row. This is the object format used for the settings
+	 * aoData array.
+	 *  @namespace
+	 */
+	DataTable.models.oRow = {
+		/**
+		 * TR element for the row
+		 *  @type node
+		 *  @default null
+		 */
+		"nTr": null,
+	
+		/**
+		 * Array of TD elements for each row. This is null until the row has been
+		 * created.
+		 *  @type array nodes
+		 *  @default []
+		 */
+		"anCells": null,
+	
+		/**
+		 * Data object from the original data source for the row. This is either
+		 * an array if using the traditional form of DataTables, or an object if
+		 * using mData options. The exact type will depend on the passed in
+		 * data from the data source, or will be an array if using DOM a data
+		 * source.
+		 *  @type array|object
+		 *  @default []
+		 */
+		"_aData": [],
+	
+		/**
+		 * Sorting data cache - this array is ostensibly the same length as the
+		 * number of columns (although each index is generated only as it is
+		 * needed), and holds the data that is used for sorting each column in the
+		 * row. We do this cache generation at the start of the sort in order that
+		 * the formatting of the sort data need be done only once for each cell
+		 * per sort. This array should not be read from or written to by anything
+		 * other than the master sorting methods.
+		 *  @type array
+		 *  @default null
+		 *  @private
+		 */
+		"_aSortData": null,
+	
+		/**
+		 * Per cell filtering data cache. As per the sort data cache, used to
+		 * increase the performance of the filtering in DataTables
+		 *  @type array
+		 *  @default null
+		 *  @private
+		 */
+		"_aFilterData": null,
+	
+		/**
+		 * Filtering data cache. This is the same as the cell filtering cache, but
+		 * in this case a string rather than an array. This is easily computed with
+		 * a join on `_aFilterData`, but is provided as a cache so the join isn't
+		 * needed on every search (memory traded for performance)
+		 *  @type array
+		 *  @default null
+		 *  @private
+		 */
+		"_sFilterRow": null,
+	
+		/**
+		 * Cache of the class name that DataTables has applied to the row, so we
+		 * can quickly look at this variable rather than needing to do a DOM check
+		 * on className for the nTr property.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @private
+		 */
+		"_sRowStripe": "",
+	
+		/**
+		 * Denote if the original data source was from the DOM, or the data source
+		 * object. This is used for invalidating data, so DataTables can
+		 * automatically read data from the original source, unless uninstructed
+		 * otherwise.
+		 *  @type string
+		 *  @default null
+		 *  @private
+		 */
+		"src": null,
+	
+		/**
+		 * Index in the aoData array. This saves an indexOf lookup when we have the
+		 * object, but want to know the index
+		 *  @type integer
+		 *  @default -1
+		 *  @private
+		 */
+		"idx": -1
+	};
+	
+	
+	/**
+	 * Template object for the column information object in DataTables. This object
+	 * is held in the settings aoColumns array and contains all the information that
+	 * DataTables needs about each individual column.
+	 *
+	 * Note that this object is related to {@link DataTable.defaults.column}
+	 * but this one is the internal data store for DataTables's cache of columns.
+	 * It should NOT be manipulated outside of DataTables. Any configuration should
+	 * be done through the initialisation options.
+	 *  @namespace
+	 */
+	DataTable.models.oColumn = {
+		/**
+		 * Column index. This could be worked out on-the-fly with $.inArray, but it
+		 * is faster to just hold it as a variable
+		 *  @type integer
+		 *  @default null
+		 */
+		"idx": null,
+	
+		/**
+		 * A list of the columns that sorting should occur on when this column
+		 * is sorted. That this property is an array allows multi-column sorting
+		 * to be defined for a column (for example first name / last name columns
+		 * would benefit from this). The values are integers pointing to the
+		 * columns to be sorted on (typically it will be a single integer pointing
+		 * at itself, but that doesn't need to be the case).
+		 *  @type array
+		 */
+		"aDataSort": null,
+	
+		/**
+		 * Define the sorting directions that are applied to the column, in sequence
+		 * as the column is repeatedly sorted upon - i.e. the first value is used
+		 * as the sorting direction when the column if first sorted (clicked on).
+		 * Sort it again (click again) and it will move on to the next index.
+		 * Repeat until loop.
+		 *  @type array
+		 */
+		"asSorting": null,
+	
+		/**
+		 * Flag to indicate if the column is searchable, and thus should be included
+		 * in the filtering or not.
+		 *  @type boolean
+		 */
+		"bSearchable": null,
+	
+		/**
+		 * Flag to indicate if the column is sortable or not.
+		 *  @type boolean
+		 */
+		"bSortable": null,
+	
+		/**
+		 * Flag to indicate if the column is currently visible in the table or not
+		 *  @type boolean
+		 */
+		"bVisible": null,
+	
+		/**
+		 * Store for manual type assignment using the `column.type` option. This
+		 * is held in store so we can manipulate the column's `sType` property.
+		 *  @type string
+		 *  @default null
+		 *  @private
+		 */
+		"_sManualType": null,
+	
+		/**
+		 * Flag to indicate if HTML5 data attributes should be used as the data
+		 * source for filtering or sorting. True is either are.
+		 *  @type boolean
+		 *  @default false
+		 *  @private
+		 */
+		"_bAttrSrc": false,
+	
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} nTd The TD node that has been created
+		 *  @param {*} sData The Data for the cell
+		 *  @param {array|object} oData The data for the whole row
+		 *  @param {int} iRow The row index for the aoData data store
+		 *  @default null
+		 */
+		"fnCreatedCell": null,
+	
+		/**
+		 * Function to get data from a cell in a column. You should <b>never</b>
+		 * access data directly through _aData internally in DataTables - always use
+		 * the method attached to this property. It allows mData to function as
+		 * required. This function is automatically assigned by the column
+		 * initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array
+		 *    (i.e. aoData[]._aData)
+		 *  @param {string} sSpecific The specific data type you want to get -
+		 *    'display', 'type' 'filter' 'sort'
+		 *  @returns {*} The data for the cell from the given row's data
+		 *  @default null
+		 */
+		"fnGetData": null,
+	
+		/**
+		 * Function to set data for a cell in the column. You should <b>never</b>
+		 * set the data directly to _aData internally in DataTables - always use
+		 * this method. It allows mData to function as required. This function
+		 * is automatically assigned by the column initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array
+		 *    (i.e. aoData[]._aData)
+		 *  @param {*} sValue Value to set
+		 *  @default null
+		 */
+		"fnSetData": null,
+	
+		/**
+		 * Property to read the value for the cells in the column from the data
+		 * source array / object. If null, then the default content is used, if a
+		 * function is given then the return from the function is used.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mData": null,
+	
+		/**
+		 * Partner property to mData which is used (only when defined) to get
+		 * the data - i.e. it is basically the same as mData, but without the
+		 * 'set' option, and also the data fed to it is the result from mData.
+		 * This is the rendering method to match the data method of mData.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mRender": null,
+	
+		/**
+		 * Unique header TH/TD element for this column - this is what the sorting
+		 * listener is attached to (if sorting is enabled.)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTh": null,
+	
+		/**
+		 * Unique footer TH/TD element for this column (if there is one). Not used
+		 * in DataTables as such, but can be used for plug-ins to reference the
+		 * footer for each column.
+		 *  @type node
+		 *  @default null
+		 */
+		"nTf": null,
+	
+		/**
+		 * The class to apply to all TD elements in the table's TBODY for the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sClass": null,
+	
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 *  @type string
+		 */
+		"sContentPadding": null,
+	
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because mData
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 */
+		"sDefaultContent": null,
+	
+		/**
+		 * Name for the column, allowing reference to the column by name as well as
+		 * by index (needs a lookup to work by name).
+		 *  @type string
+		 */
+		"sName": null,
+	
+		/**
+		 * Custom sorting data type - defines which of the available plug-ins in
+		 * afnSortData the custom sorting will use - if any is defined.
+		 *  @type string
+		 *  @default std
+		 */
+		"sSortDataType": 'std',
+	
+		/**
+		 * Class to be applied to the header element when sorting on this column
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClass": null,
+	
+		/**
+		 * Class to be applied to the header element when sorting on this column -
+		 * when jQuery UI theming is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClassJUI": null,
+	
+		/**
+		 * Title of the column - what is seen in the TH element (nTh).
+		 *  @type string
+		 */
+		"sTitle": null,
+	
+		/**
+		 * Column sorting and filtering type
+		 *  @type string
+		 *  @default null
+		 */
+		"sType": null,
+	
+		/**
+		 * Width of the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidth": null,
+	
+		/**
+		 * Width of the column when it was first "encountered"
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidthOrig": null
+	};
+	
+	
+	/*
+	 * Developer note: The properties of the object below are given in Hungarian
+	 * notation, that was used as the interface for DataTables prior to v1.10, however
+	 * from v1.10 onwards the primary interface is camel case. In order to avoid
+	 * breaking backwards compatibility utterly with this change, the Hungarian
+	 * version is still, internally the primary interface, but is is not documented
+	 * - hence the @name tags in each doc comment. This allows a Javascript function
+	 * to create a map from Hungarian notation to camel case (going the other direction
+	 * would require each property to be listed, which would at around 3K to the size
+	 * of DataTables, while this method is about a 0.5K hit.
+	 *
+	 * Ultimately this does pave the way for Hungarian notation to be dropped
+	 * completely, but that is a massive amount of work and will break current
+	 * installs (therefore is on-hold until v2).
+	 */
+	
+	/**
+	 * Initialisation options that can be given to DataTables at initialisation
+	 * time.
+	 *  @namespace
+	 */
+	DataTable.defaults = {
+		/**
+		 * An array of data to use for the table, passed in at initialisation which
+		 * will be used in preference to any data which is already in the DOM. This is
+		 * particularly useful for constructing tables purely in Javascript, for
+		 * example with a custom Ajax call.
+		 *  @type array
+		 *  @default null
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.data
+		 *
+		 *  @example
+		 *    // Using a 2D array data source
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "data": [
+		 *          ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'],
+		 *          ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'],
+		 *        ],
+		 *        "columns": [
+		 *          { "title": "Engine" },
+		 *          { "title": "Browser" },
+		 *          { "title": "Platform" },
+		 *          { "title": "Version" },
+		 *          { "title": "Grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using an array of objects as a data source (`data`)
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "data": [
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 4.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  4,
+		 *            "grade":    "X"
+		 *          },
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 5.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  5,
+		 *            "grade":    "C"
+		 *          }
+		 *        ],
+		 *        "columns": [
+		 *          { "title": "Engine",   "data": "engine" },
+		 *          { "title": "Browser",  "data": "browser" },
+		 *          { "title": "Platform", "data": "platform" },
+		 *          { "title": "Version",  "data": "version" },
+		 *          { "title": "Grade",    "data": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aaData": null,
+	
+	
+		/**
+		 * If ordering is enabled, then DataTables will perform a first pass sort on
+		 * initialisation. You can define which column(s) the sort is performed
+		 * upon, and the sorting direction, with this variable. The `sorting` array
+		 * should contain an array for each column to be sorted initially containing
+		 * the column's index and a direction string ('asc' or 'desc').
+		 *  @type array
+		 *  @default [[0,'asc']]
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.order
+		 *
+		 *  @example
+		 *    // Sort by 3rd column first, and then 4th column
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "order": [[2,'asc'], [3,'desc']]
+		 *      } );
+		 *    } );
+		 *
+		 *    // No initial sorting
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "order": []
+		 *      } );
+		 *    } );
+		 */
+		"aaSorting": [[0,'asc']],
+	
+	
+		/**
+		 * This parameter is basically identical to the `sorting` parameter, but
+		 * cannot be overridden by user interaction with the table. What this means
+		 * is that you could have a column (visible or hidden) which the sorting
+		 * will always be forced on first - any sorting after that (from the user)
+		 * will then be performed as required. This can be useful for grouping rows
+		 * together.
+		 *  @type array
+		 *  @default null
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.orderFixed
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "orderFixed": [[0,'asc']]
+		 *      } );
+		 *    } )
+		 */
+		"aaSortingFixed": [],
+	
+	
+		/**
+		 * DataTables can be instructed to load data to display in the table from a
+		 * Ajax source. This option defines how that Ajax call is made and where to.
+		 *
+		 * The `ajax` property has three different modes of operation, depending on
+		 * how it is defined. These are:
+		 *
+		 * * `string` - Set the URL from where the data should be loaded from.
+		 * * `object` - Define properties for `jQuery.ajax`.
+		 * * `function` - Custom data get function
+		 *
+		 * `string`
+		 * --------
+		 *
+		 * As a string, the `ajax` property simply defines the URL from which
+		 * DataTables will load data.
+		 *
+		 * `object`
+		 * --------
+		 *
+		 * As an object, the parameters in the object are passed to
+		 * [jQuery.ajax](http://api.jquery.com/jQuery.ajax/) allowing fine control
+		 * of the Ajax request. DataTables has a number of default parameters which
+		 * you can override using this option. Please refer to the jQuery
+		 * documentation for a full description of the options available, although
+		 * the following parameters provide additional options in DataTables or
+		 * require special consideration:
+		 *
+		 * * `data` - As with jQuery, `data` can be provided as an object, but it
+		 *   can also be used as a function to manipulate the data DataTables sends
+		 *   to the server. The function takes a single parameter, an object of
+		 *   parameters with the values that DataTables has readied for sending. An
+		 *   object may be returned which will be merged into the DataTables
+		 *   defaults, or you can add the items to the object that was passed in and
+		 *   not return anything from the function. This supersedes `fnServerParams`
+		 *   from DataTables 1.9-.
+		 *
+		 * * `dataSrc` - By default DataTables will look for the property `data` (or
+		 *   `aaData` for compatibility with DataTables 1.9-) when obtaining data
+		 *   from an Ajax source or for server-side processing - this parameter
+		 *   allows that property to be changed. You can use Javascript dotted
+		 *   object notation to get a data source for multiple levels of nesting, or
+		 *   it my be used as a function. As a function it takes a single parameter,
+		 *   the JSON returned from the server, which can be manipulated as
+		 *   required, with the returned value being that used by DataTables as the
+		 *   data source for the table. This supersedes `sAjaxDataProp` from
+		 *   DataTables 1.9-.
+		 *
+		 * * `success` - Should not be overridden it is used internally in
+		 *   DataTables. To manipulate / transform the data returned by the server
+		 *   use `ajax.dataSrc`, or use `ajax` as a function (see below).
+		 *
+		 * `function`
+		 * ----------
+		 *
+		 * As a function, making the Ajax call is left up to yourself allowing
+		 * complete control of the Ajax request. Indeed, if desired, a method other
+		 * than Ajax could be used to obtain the required data, such as Web storage
+		 * or an AIR database.
+		 *
+		 * The function is given four parameters and no return is required. The
+		 * parameters are:
+		 *
+		 * 1. _object_ - Data to send to the server
+		 * 2. _function_ - Callback function that must be executed when the required
+		 *    data has been obtained. That data should be passed into the callback
+		 *    as the only parameter
+		 * 3. _object_ - DataTables settings object for the table
+		 *
+		 * Note that this supersedes `fnServerData` from DataTables 1.9-.
+		 *
+		 *  @type string|object|function
+		 *  @default null
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.ajax
+		 *  @since 1.10.0
+		 *
+		 * @example
+		 *   // Get JSON data from a file via Ajax.
+		 *   // Note DataTables expects data in the form `{ data: [ ...data... ] }` by default).
+		 *   $('#example').dataTable( {
+		 *     "ajax": "data.json"
+		 *   } );
+		 *
+		 * @example
+		 *   // Get JSON data from a file via Ajax, using `dataSrc` to change
+		 *   // `data` to `tableData` (i.e. `{ tableData: [ ...data... ] }`)
+		 *   $('#example').dataTable( {
+		 *     "ajax": {
+		 *       "url": "data.json",
+		 *       "dataSrc": "tableData"
+		 *     }
+		 *   } );
+		 *
+		 * @example
+		 *   // Get JSON data from a file via Ajax, using `dataSrc` to read data
+		 *   // from a plain array rather than an array in an object
+		 *   $('#example').dataTable( {
+		 *     "ajax": {
+		 *       "url": "data.json",
+		 *       "dataSrc": ""
+		 *     }
+		 *   } );
+		 *
+		 * @example
+		 *   // Manipulate the data returned from the server - add a link to data
+		 *   // (note this can, should, be done using `render` for the column - this
+		 *   // is just a simple example of how the data can be manipulated).
+		 *   $('#example').dataTable( {
+		 *     "ajax": {
+		 *       "url": "data.json",
+		 *       "dataSrc": function ( json ) {
+		 *         for ( var i=0, ien=json.length ; i<ien ; i++ ) {
+		 *           json[i][0] = '<a href="/message/'+json[i][0]+'>View message</a>';
+		 *         }
+		 *         return json;
+		 *       }
+		 *     }
+		 *   } );
+		 *
+		 * @example
+		 *   // Add data to the request
+		 *   $('#example').dataTable( {
+		 *     "ajax": {
+		 *       "url": "data.json",
+		 *       "data": function ( d ) {
+		 *         return {
+		 *           "extra_search": $('#extra').val()
+		 *         };
+		 *       }
+		 *     }
+		 *   } );
+		 *
+		 * @example
+		 *   // Send request as POST
+		 *   $('#example').dataTable( {
+		 *     "ajax": {
+		 *       "url": "data.json",
+		 *       "type": "POST"
+		 *     }
+		 *   } );
+		 *
+		 * @example
+		 *   // Get the data from localStorage (could interface with a form for
+		 *   // adding, editing and removing rows).
+		 *   $('#example').dataTable( {
+		 *     "ajax": function (data, callback, settings) {
+		 *       callback(
+		 *         JSON.parse( localStorage.getItem('dataTablesData') )
+		 *       );
+		 *     }
+		 *   } );
+		 */
+		"ajax": null,
+	
+	
+		/**
+		 * This parameter allows you to readily specify the entries in the length drop
+		 * down menu that DataTables shows when pagination is enabled. It can be
+		 * either a 1D array of options which will be used for both the displayed
+		 * option and the value, or a 2D array which will use the array in the first
+		 * position as the value, and the array in the second position as the
+		 * displayed options (useful for language strings such as 'All').
+		 *
+		 * Note that the `pageLength` property will be automatically set to the
+		 * first value given in this array, unless `pageLength` is also provided.
+		 *  @type array
+		 *  @default [ 10, 25, 50, 100 ]
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.lengthMenu
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "lengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]]
+		 *      } );
+		 *    } );
+		 */
+		"aLengthMenu": [ 10, 25, 50, 100 ],
+	
+	
+		/**
+		 * The `columns` option in the initialisation parameter allows you to define
+		 * details about the way individual columns behave. For a full list of
+		 * column options that can be set, please see
+		 * {@link DataTable.defaults.column}. Note that if you use `columns` to
+		 * define your columns, you must have an entry in the array for every single
+		 * column that you have in your table (these can be null if you don't which
+		 * to specify any options).
+		 *  @member
+		 *
+		 *  @name DataTable.defaults.column
+		 */
+		"aoColumns": null,
+	
+		/**
+		 * Very similar to `columns`, `columnDefs` allows you to target a specific
+		 * column, multiple columns, or all columns, using the `targets` property of
+		 * each object in the array. This allows great flexibility when creating
+		 * tables, as the `columnDefs` arrays can be of any length, targeting the
+		 * columns you specifically want. `columnDefs` may use any of the column
+		 * options available: {@link DataTable.defaults.column}, but it _must_
+		 * have `targets` defined in each object in the array. Values in the `targets`
+		 * array may be:
+		 *   <ul>
+		 *     <li>a string - class name will be matched on the TH for the column</li>
+		 *     <li>0 or a positive integer - column index counting from the left</li>
+		 *     <li>a negative integer - column index counting from the right</li>
+		 *     <li>the string "_all" - all columns (i.e. assign a default)</li>
+		 *   </ul>
+		 *  @member
+		 *
+		 *  @name DataTable.defaults.columnDefs
+		 */
+		"aoColumnDefs": null,
+	
+	
+		/**
+		 * Basically the same as `search`, this parameter defines the individual column
+		 * filtering state at initialisation time. The array must be of the same size
+		 * as the number of columns, and each element be an object with the parameters
+		 * `search` and `escapeRegex` (the latter is optional). 'null' is also
+		 * accepted and the default will be used.
+		 *  @type array
+		 *  @default []
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.searchCols
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "searchCols": [
+		 *          null,
+		 *          { "search": "My filter" },
+		 *          null,
+		 *          { "search": "^[0-9]", "escapeRegex": false }
+		 *        ]
+		 *      } );
+		 *    } )
+		 */
+		"aoSearchCols": [],
+	
+	
+		/**
+		 * An array of CSS classes that should be applied to displayed rows. This
+		 * array may be of any length, and DataTables will apply each class
+		 * sequentially, looping when required.
+		 *  @type array
+		 *  @default null <i>Will take the values determined by the `oClasses.stripe*`
+		 *    options</i>
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.stripeClasses
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stripeClasses": [ 'strip1', 'strip2', 'strip3' ]
+		 *      } );
+		 *    } )
+		 */
+		"asStripeClasses": null,
+	
+	
+		/**
+		 * Enable or disable automatic column width calculation. This can be disabled
+		 * as an optimisation (it takes some time to calculate the widths) if the
+		 * tables widths are passed in using `columns`.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.autoWidth
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "autoWidth": false
+		 *      } );
+		 *    } );
+		 */
+		"bAutoWidth": true,
+	
+	
+		/**
+		 * Deferred rendering can provide DataTables with a huge speed boost when you
+		 * are using an Ajax or JS data source for the table. This option, when set to
+		 * true, will cause DataTables to defer the creation of the table elements for
+		 * each row until they are needed for a draw - saving a significant amount of
+		 * time.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.deferRender
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "ajax": "sources/arrays.txt",
+		 *        "deferRender": true
+		 *      } );
+		 *    } );
+		 */
+		"bDeferRender": false,
+	
+	
+		/**
+		 * Replace a DataTable which matches the given selector and replace it with
+		 * one which has the properties of the new initialisation object passed. If no
+		 * table matches the selector, then the new DataTable will be constructed as
+		 * per normal.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.destroy
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "srollY": "200px",
+		 *        "paginate": false
+		 *      } );
+		 *
+		 *      // Some time later....
+		 *      $('#example').dataTable( {
+		 *        "filter": false,
+		 *        "destroy": true
+		 *      } );
+		 *    } );
+		 */
+		"bDestroy": false,
+	
+	
+		/**
+		 * Enable or disable filtering of data. Filtering in DataTables is "smart" in
+		 * that it allows the end user to input multiple words (space separated) and
+		 * will match a row containing those words, even if not in the order that was
+		 * specified (this allow matching across multiple columns). Note that if you
+		 * wish to use filtering in DataTables this must remain 'true' - to remove the
+		 * default filtering input box and retain filtering abilities, please use
+		 * {@link DataTable.defaults.dom}.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.searching
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "searching": false
+		 *      } );
+		 *    } );
+		 */
+		"bFilter": true,
+	
+	
+		/**
+		 * Enable or disable the table information display. This shows information
+		 * about the data that is currently visible on the page, including information
+		 * about filtered data if that action is being performed.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.info
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "info": false
+		 *      } );
+		 *    } );
+		 */
+		"bInfo": true,
+	
+	
+		/**
+		 * Enable jQuery UI ThemeRoller support (required as ThemeRoller requires some
+		 * slightly different and additional mark-up from what DataTables has
+		 * traditionally used).
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.jQueryUI
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "jQueryUI": true
+		 *      } );
+		 *    } );
+		 */
+		"bJQueryUI": false,
+	
+	
+		/**
+		 * Allows the end user to select the size of a formatted page from a select
+		 * menu (sizes are 10, 25, 50 and 100). Requires pagination (`paginate`).
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.lengthChange
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "lengthChange": false
+		 *      } );
+		 *    } );
+		 */
+		"bLengthChange": true,
+	
+	
+		/**
+		 * Enable or disable pagination.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.paging
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "paging": false
+		 *      } );
+		 *    } );
+		 */
+		"bPaginate": true,
+	
+	
+		/**
+		 * Enable or disable the display of a 'processing' indicator when the table is
+		 * being processed (e.g. a sort). This is particularly useful for tables with
+		 * large amounts of data where it can take a noticeable amount of time to sort
+		 * the entries.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.processing
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "processing": true
+		 *      } );
+		 *    } );
+		 */
+		"bProcessing": false,
+	
+	
+		/**
+		 * Retrieve the DataTables object for the given selector. Note that if the
+		 * table has already been initialised, this parameter will cause DataTables
+		 * to simply return the object that has already been set up - it will not take
+		 * account of any changes you might have made to the initialisation object
+		 * passed to DataTables (setting this parameter to true is an acknowledgement
+		 * that you understand this). `destroy` can be used to reinitialise a table if
+		 * you need.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.retrieve
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      initTable();
+		 *      tableActions();
+		 *    } );
+		 *
+		 *    function initTable ()
+		 *    {
+		 *      return $('#example').dataTable( {
+		 *        "scrollY": "200px",
+		 *        "paginate": false,
+		 *        "retrieve": true
+		 *      } );
+		 *    }
+		 *
+		 *    function tableActions ()
+		 *    {
+		 *      var table = initTable();
+		 *      // perform API operations with oTable
+		 *    }
+		 */
+		"bRetrieve": false,
+	
+	
+		/**
+		 * When vertical (y) scrolling is enabled, DataTables will force the height of
+		 * the table's viewport to the given height at all times (useful for layout).
+		 * However, this can look odd when filtering data down to a small data set,
+		 * and the footer is left "floating" further down. This parameter (when
+		 * enabled) will cause DataTables to collapse the table's viewport down when
+		 * the result set will fit within the given Y height.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.scrollCollapse
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "scrollY": "200",
+		 *        "scrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"bScrollCollapse": false,
+	
+	
+		/**
+		 * Configure DataTables to use server-side processing. Note that the
+		 * `ajax` parameter must also be given in order to give DataTables a
+		 * source to obtain the required data for each draw.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Features
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.serverSide
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "serverSide": true,
+		 *        "ajax": "xhr.php"
+		 *      } );
+		 *    } );
+		 */
+		"bServerSide": false,
+	
+	
+		/**
+		 * Enable or disable sorting of columns. Sorting of individual columns can be
+		 * disabled by the `sortable` option for each column.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.ordering
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "ordering": false
+		 *      } );
+		 *    } );
+		 */
+		"bSort": true,
+	
+	
+		/**
+		 * Enable or display DataTables' ability to sort multiple columns at the
+		 * same time (activated by shift-click by the user).
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.orderMulti
+		 *
+		 *  @example
+		 *    // Disable multiple column sorting ability
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "orderMulti": false
+		 *      } );
+		 *    } );
+		 */
+		"bSortMulti": true,
+	
+	
+		/**
+		 * Allows control over whether DataTables should use the top (true) unique
+		 * cell that is found for a single column, or the bottom (false - default).
+		 * This is useful when using complex headers.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.orderCellsTop
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "orderCellsTop": true
+		 *      } );
+		 *    } );
+		 */
+		"bSortCellsTop": false,
+	
+	
+		/**
+		 * Enable or disable the addition of the classes `sorting\_1`, `sorting\_2` and
+		 * `sorting\_3` to the columns which are currently being sorted on. This is
+		 * presented as a feature switch as it can increase processing time (while
+		 * classes are removed and added) so for large data sets you might want to
+		 * turn this off.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.orderClasses
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "orderClasses": false
+		 *      } );
+		 *    } );
+		 */
+		"bSortClasses": true,
+	
+	
+		/**
+		 * Enable or disable state saving. When enabled HTML5 `localStorage` will be
+		 * used to save table display information such as pagination information,
+		 * display length, filtering and sorting. As such when the end user reloads
+		 * the page the display display will match what thy had previously set up.
+		 *
+		 * Due to the use of `localStorage` the default state saving is not supported
+		 * in IE6 or 7. If state saving is required in those browsers, use
+		 * `stateSaveCallback` to provide a storage solution such as cookies.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.stateSave
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true
+		 *      } );
+		 *    } );
+		 */
+		"bStateSave": false,
+	
+	
+		/**
+		 * This function is called when a TR element is created (and all TD child
+		 * elements have been inserted), or registered if using a DOM source, allowing
+		 * manipulation of the TR element (adding classes etc).
+		 *  @type function
+		 *  @param {node} row "TR" element for the current row
+		 *  @param {array} data Raw data array for this row
+		 *  @param {int} dataIndex The index of this row in the internal aoData array
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.createdRow
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "createdRow": function( row, data, dataIndex ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( data[4] == "A" )
+		 *          {
+		 *            $('td:eq(4)', row).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnCreatedRow": null,
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify any aspect you want about the created DOM.
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.drawCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "drawCallback": function( settings ) {
+		 *          alert( 'DataTables has redrawn the table' );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnDrawCallback": null,
+	
+	
+		/**
+		 * Identical to fnHeaderCallback() but for the table footer this function
+		 * allows you to modify the table footer on every 'draw' event.
+		 *  @type function
+		 *  @param {node} foot "TR" element for the footer
+		 *  @param {array} data Full table data (as derived from the original HTML)
+		 *  @param {int} start Index for the current display starting point in the
+		 *    display array
+		 *  @param {int} end Index for the current display ending point in the
+		 *    display array
+		 *  @param {array int} display Index array to translate the visual position
+		 *    to the full data array
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.footerCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "footerCallback": function( tfoot, data, start, end, display ) {
+		 *          tfoot.getElementsByTagName('th')[0].innerHTML = "Starting index is "+start;
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnFooterCallback": null,
+	
+	
+		/**
+		 * When rendering large numbers in the information element for the table
+		 * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers
+		 * to have a comma separator for the 'thousands' units (e.g. 1 million is
+		 * rendered as "1,000,000") to help readability for the end user. This
+		 * function will override the default method DataTables uses.
+		 *  @type function
+		 *  @member
+		 *  @param {int} toFormat number to be formatted
+		 *  @returns {string} formatted string for DataTables to show the number
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.formatNumber
+		 *
+		 *  @example
+		 *    // Format a number using a single quote for the separator (note that
+		 *    // this can also be done with the language.thousands option)
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "formatNumber": function ( toFormat ) {
+		 *          return toFormat.toString().replace(
+		 *            /\B(?=(\d{3})+(?!\d))/g, "'"
+		 *          );
+		 *        };
+		 *      } );
+		 *    } );
+		 */
+		"fnFormatNumber": function ( toFormat ) {
+			return toFormat.toString().replace(
+				/\B(?=(\d{3})+(?!\d))/g,
+				this.oLanguage.sThousands
+			);
+		},
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify the header row. This can be used to calculate and
+		 * display useful information about the table.
+		 *  @type function
+		 *  @param {node} head "TR" element for the header
+		 *  @param {array} data Full table data (as derived from the original HTML)
+		 *  @param {int} start Index for the current display starting point in the
+		 *    display array
+		 *  @param {int} end Index for the current display ending point in the
+		 *    display array
+		 *  @param {array int} display Index array to translate the visual position
+		 *    to the full data array
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.headerCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fheaderCallback": function( head, data, start, end, display ) {
+		 *          head.getElementsByTagName('th')[0].innerHTML = "Displaying "+(end-start)+" records";
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnHeaderCallback": null,
+	
+	
+		/**
+		 * The information element can be used to convey information about the current
+		 * state of the table. Although the internationalisation options presented by
+		 * DataTables are quite capable of dealing with most customisations, there may
+		 * be times where you wish to customise the string further. This callback
+		 * allows you to do exactly that.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {int} start Starting position in data for the draw
+		 *  @param {int} end End position in data for the draw
+		 *  @param {int} max Total number of rows in the table (regardless of
+		 *    filtering)
+		 *  @param {int} total Total number of rows in the data set, after filtering
+		 *  @param {string} pre The string that DataTables has formatted using it's
+		 *    own rules
+		 *  @returns {string} The string to be displayed in the information element.
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.infoCallback
+		 *
+		 *  @example
+		 *    $('#example').dataTable( {
+		 *      "infoCallback": function( settings, start, end, max, total, pre ) {
+		 *        return start +" to "+ end;
+		 *      }
+		 *    } );
+		 */
+		"fnInfoCallback": null,
+	
+	
+		/**
+		 * Called when the table has been initialised. Normally DataTables will
+		 * initialise sequentially and there will be no need for this function,
+		 * however, this does not hold true when using external language information
+		 * since that is obtained using an async XHR call.
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} json The JSON object request from the server - only
+		 *    present if client-side Ajax sourced data is used
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.initComplete
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "initComplete": function(settings, json) {
+		 *          alert( 'DataTables has finished its initialisation.' );
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnInitComplete": null,
+	
+	
+		/**
+		 * Called at the very start of each table draw and can be used to cancel the
+		 * draw by returning false, any other return (including undefined) results in
+		 * the full draw occurring).
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *  @returns {boolean} False will cancel the draw, anything else (including no
+		 *    return) will allow it to complete.
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.preDrawCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "preDrawCallback": function( settings ) {
+		 *          if ( $('#test').val() == 1 ) {
+		 *            return false;
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnPreDrawCallback": null,
+	
+	
+		/**
+		 * This function allows you to 'post process' each row after it have been
+		 * generated for each table draw, but before it is rendered on screen. This
+		 * function might be used for setting the row class name etc.
+		 *  @type function
+		 *  @param {node} row "TR" element for the current row
+		 *  @param {array} data Raw data array for this row
+		 *  @param {int} displayIndex The display index for the current table draw
+		 *  @param {int} displayIndexFull The index of the data in the full list of
+		 *    rows (after filtering)
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.rowCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "rowCallback": function( row, data, displayIndex, displayIndexFull ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( data[4] == "A" ) {
+		 *            $('td:eq(4)', row).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnRowCallback": null,
+	
+	
+		/**
+		 * __Deprecated__ The functionality provided by this parameter has now been
+		 * superseded by that provided through `ajax`, which should be used instead.
+		 *
+		 * This parameter allows you to override the default function which obtains
+		 * the data from the server so something more suitable for your application.
+		 * For example you could use POST data, or pull information from a Gears or
+		 * AIR database.
+		 *  @type function
+		 *  @member
+		 *  @param {string} source HTTP source to obtain the data from (`ajax`)
+		 *  @param {array} data A key/value pair object containing the data to send
+		 *    to the server
+		 *  @param {function} callback to be called on completion of the data get
+		 *    process that will draw the data on the page.
+		 *  @param {object} settings DataTables settings object
+		 *
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.serverData
+		 *
+		 *  @deprecated 1.10. Please use `ajax` for this functionality now.
+		 */
+		"fnServerData": null,
+	
+	
+		/**
+		 * __Deprecated__ The functionality provided by this parameter has now been
+		 * superseded by that provided through `ajax`, which should be used instead.
+		 *
+		 *  It is often useful to send extra data to the server when making an Ajax
+		 * request - for example custom filtering information, and this callback
+		 * function makes it trivial to send extra information to the server. The
+		 * passed in parameter is the data set that has been constructed by
+		 * DataTables, and you can add to this or modify it as you require.
+		 *  @type function
+		 *  @param {array} data Data array (array of objects which are name/value
+		 *    pairs) that has been constructed by DataTables and will be sent to the
+		 *    server. In the case of Ajax sourced data with server-side processing
+		 *    this will be an empty array, for server-side processing there will be a
+		 *    significant number of parameters!
+		 *  @returns {undefined} Ensure that you modify the data array passed in,
+		 *    as this is passed by reference.
+		 *
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.serverParams
+		 *
+		 *  @deprecated 1.10. Please use `ajax` for this functionality now.
+		 */
+		"fnServerParams": null,
+	
+	
+		/**
+		 * Load the table state. With this function you can define from where, and how, the
+		 * state of a table is loaded. By default DataTables will load from `localStorage`
+		 * but you might wish to use a server-side database or cookies.
+		 *  @type function
+		 *  @member
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} callback Callback that can be executed when done. It
+		 *    should be passed the loaded state object.
+		 *  @return {object} The DataTables state object to be loaded
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.stateLoadCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateLoadCallback": function (settings, callback) {
+		 *          $.ajax( {
+		 *            "url": "/state_load",
+		 *            "dataType": "json",
+		 *            "success": function (json) {
+		 *              callback( json );
+		 *            }
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoadCallback": function ( settings ) {
+			try {
+				return JSON.parse(
+					(settings.iStateDuration === -1 ? sessionStorage : localStorage).getItem(
+						'DataTables_'+settings.sInstance+'_'+location.pathname
+					)
+				);
+			} catch (e) {}
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the saved state prior to loading that state.
+		 * This callback is called when the table is loading state from the stored data, but
+		 * prior to the settings object being modified by the saved state. Note that for
+		 * plug-in authors, you should use the `stateLoadParams` event to load parameters for
+		 * a plug-in.
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} data The state object that is to be loaded
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.stateLoadParams
+		 *
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never loaded
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateLoadParams": function (settings, data) {
+		 *          data.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Disallow state loading by returning false
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateLoadParams": function (settings, data) {
+		 *          return false;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoadParams": null,
+	
+	
+		/**
+		 * Callback that is called when the state has been loaded from the state saving method
+		 * and the DataTables settings object has been modified as a result of the loaded state.
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} data The state object that was loaded
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.stateLoaded
+		 *
+		 *  @example
+		 *    // Show an alert with the filtering value that was saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateLoaded": function (settings, data) {
+		 *          alert( 'Saved filter was: '+data.oSearch.sSearch );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoaded": null,
+	
+	
+		/**
+		 * Save the table state. This function allows you to define where and how the state
+		 * information for the table is stored By default DataTables will use `localStorage`
+		 * but you might wish to use a server-side database or cookies.
+		 *  @type function
+		 *  @member
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} data The state object to be saved
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.stateSaveCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateSaveCallback": function (settings, data) {
+		 *          // Send an Ajax request to the server with the state object
+		 *          $.ajax( {
+		 *            "url": "/state_save",
+		 *            "data": data,
+		 *            "dataType": "json",
+		 *            "method": "POST"
+		 *            "success": function () {}
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSaveCallback": function ( settings, data ) {
+			try {
+				(settings.iStateDuration === -1 ? sessionStorage : localStorage).setItem(
+					'DataTables_'+settings.sInstance+'_'+location.pathname,
+					JSON.stringify( data )
+				);
+			} catch (e) {}
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the state to be saved. Called when the table
+		 * has changed state a new state save is required. This method allows modification of
+		 * the state saving object prior to actually doing the save, including addition or
+		 * other state properties or modification. Note that for plug-in authors, you should
+		 * use the `stateSaveParams` event to save parameters for a plug-in.
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} data The state object to be saved
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.stateSaveParams
+		 *
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateSaveParams": function (settings, data) {
+		 *          data.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSaveParams": null,
+	
+	
+		/**
+		 * Duration for which the saved state information is considered valid. After this period
+		 * has elapsed the state will be returned to the default.
+		 * Value is given in seconds.
+		 *  @type int
+		 *  @default 7200 <i>(2 hours)</i>
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.stateDuration
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateDuration": 60*60*24; // 1 day
+		 *      } );
+		 *    } )
+		 */
+		"iStateDuration": 7200,
+	
+	
+		/**
+		 * When enabled DataTables will not make a request to the server for the first
+		 * page draw - rather it will use the data already on the page (no sorting etc
+		 * will be applied to it), thus saving on an XHR at load time. `deferLoading`
+		 * is used to indicate that deferred loading is required, but it is also used
+		 * to tell DataTables how many records there are in the full table (allowing
+		 * the information element and pagination to be displayed correctly). In the case
+		 * where a filtering is applied to the table on initial load, this can be
+		 * indicated by giving the parameter as an array, where the first element is
+		 * the number of records available after filtering and the second element is the
+		 * number of records without filtering (allowing the table information element
+		 * to be shown correctly).
+		 *  @type int | array
+		 *  @default null
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.deferLoading
+		 *
+		 *  @example
+		 *    // 57 records available in the table, no filtering applied
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "serverSide": true,
+		 *        "ajax": "scripts/server_processing.php",
+		 *        "deferLoading": 57
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // 57 records after filtering, 100 without filtering (an initial filter applied)
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "serverSide": true,
+		 *        "ajax": "scripts/server_processing.php",
+		 *        "deferLoading": [ 57, 100 ],
+		 *        "search": {
+		 *          "search": "my_filter"
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"iDeferLoading": null,
+	
+	
+		/**
+		 * Number of rows to display on a single page when using pagination. If
+		 * feature enabled (`lengthChange`) then the end user will be able to override
+		 * this to a custom setting using a pop-up menu.
+		 *  @type int
+		 *  @default 10
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.pageLength
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "pageLength": 50
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayLength": 10,
+	
+	
+		/**
+		 * Define the starting point for data display when using DataTables with
+		 * pagination. Note that this parameter is the number of records, rather than
+		 * the page number, so if you have 10 records per page and want to start on
+		 * the third page, it should be "20".
+		 *  @type int
+		 *  @default 0
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.displayStart
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "displayStart": 20
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayStart": 0,
+	
+	
+		/**
+		 * By default DataTables allows keyboard navigation of the table (sorting, paging,
+		 * and filtering) by adding a `tabindex` attribute to the required elements. This
+		 * allows you to tab through the controls and press the enter key to activate them.
+		 * The tabindex is default 0, meaning that the tab follows the flow of the document.
+		 * You can overrule this using this parameter if you wish. Use a value of -1 to
+		 * disable built-in keyboard navigation.
+		 *  @type int
+		 *  @default 0
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.tabIndex
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "tabIndex": 1
+		 *      } );
+		 *    } );
+		 */
+		"iTabIndex": 0,
+	
+	
+		/**
+		 * Classes that DataTables assigns to the various components and features
+		 * that it adds to the HTML table. This allows classes to be configured
+		 * during initialisation in addition to through the static
+		 * {@link DataTable.ext.oStdClasses} object).
+		 *  @namespace
+		 *  @name DataTable.defaults.classes
+		 */
+		"oClasses": {},
+	
+	
+		/**
+		 * All strings that DataTables uses in the user interface that it creates
+		 * are defined in this object, allowing you to modified them individually or
+		 * completely replace them all as required.
+		 *  @namespace
+		 *  @name DataTable.defaults.language
+		 */
+		"oLanguage": {
+			/**
+			 * Strings that are used for WAI-ARIA labels and controls only (these are not
+			 * actually visible on the page, but will be read by screenreaders, and thus
+			 * must be internationalised as well).
+			 *  @namespace
+			 *  @name DataTable.defaults.language.aria
+			 */
+			"oAria": {
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted ascending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.aria.sortAscending
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "aria": {
+				 *            "sortAscending": " - click/return to sort ascending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortAscending": ": activate to sort column ascending",
+	
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted descending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.aria.sortDescending
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "aria": {
+				 *            "sortDescending": " - click/return to sort descending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortDescending": ": activate to sort column descending"
+			},
+	
+			/**
+			 * Pagination string used by DataTables for the built-in pagination
+			 * control types.
+			 *  @namespace
+			 *  @name DataTable.defaults.language.paginate
+			 */
+			"oPaginate": {
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the first page.
+				 *  @type string
+				 *  @default First
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.paginate.first
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "paginate": {
+				 *            "first": "First page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sFirst": "First",
+	
+	
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the last page.
+				 *  @type string
+				 *  @default Last
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.paginate.last
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "paginate": {
+				 *            "last": "Last page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sLast": "Last",
+	
+	
+				/**
+				 * Text to use for the 'next' pagination button (to take the user to the
+				 * next page).
+				 *  @type string
+				 *  @default Next
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.paginate.next
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "paginate": {
+				 *            "next": "Next page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sNext": "Next",
+	
+	
+				/**
+				 * Text to use for the 'previous' pagination button (to take the user to
+				 * the previous page).
+				 *  @type string
+				 *  @default Previous
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.paginate.previous
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "paginate": {
+				 *            "previous": "Previous page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sPrevious": "Previous"
+			},
+	
+			/**
+			 * This string is shown in preference to `zeroRecords` when the table is
+			 * empty of data (regardless of filtering). Note that this is an optional
+			 * parameter - if it is not given, the value of `zeroRecords` will be used
+			 * instead (either the default or given value).
+			 *  @type string
+			 *  @default No data available in table
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.emptyTable
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "emptyTable": "No data available in table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sEmptyTable": "No data available in table",
+	
+	
+			/**
+			 * This string gives information to the end user about the information
+			 * that is current on display on the page. The following tokens can be
+			 * used in the string and will be dynamically replaced as the table
+			 * display updates. This tokens can be placed anywhere in the string, or
+			 * removed as needed by the language requires:
+			 *
+			 * * `\_START\_` - Display index of the first record on the current page
+			 * * `\_END\_` - Display index of the last record on the current page
+			 * * `\_TOTAL\_` - Number of records in the table after filtering
+			 * * `\_MAX\_` - Number of records in the table without filtering
+			 * * `\_PAGE\_` - Current page number
+			 * * `\_PAGES\_` - Total number of pages of data in the table
+			 *
+			 *  @type string
+			 *  @default Showing _START_ to _END_ of _TOTAL_ entries
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.info
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "info": "Showing page _PAGE_ of _PAGES_"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
+	
+	
+			/**
+			 * Display information string for when the table is empty. Typically the
+			 * format of this string should match `info`.
+			 *  @type string
+			 *  @default Showing 0 to 0 of 0 entries
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.infoEmpty
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "infoEmpty": "No entries to show"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoEmpty": "Showing 0 to 0 of 0 entries",
+	
+	
+			/**
+			 * When a user filters the information in a table, this string is appended
+			 * to the information (`info`) to give an idea of how strong the filtering
+			 * is. The variable _MAX_ is dynamically updated.
+			 *  @type string
+			 *  @default (filtered from _MAX_ total entries)
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.infoFiltered
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "infoFiltered": " - filtering from _MAX_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoFiltered": "(filtered from _MAX_ total entries)",
+	
+	
+			/**
+			 * If can be useful to append extra information to the info string at times,
+			 * and this variable does exactly that. This information will be appended to
+			 * the `info` (`infoEmpty` and `infoFiltered` in whatever combination they are
+			 * being used) at all times.
+			 *  @type string
+			 *  @default <i>Empty string</i>
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.infoPostFix
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "infoPostFix": "All records shown are derived from real information."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoPostFix": "",
+	
+	
+			/**
+			 * This decimal place operator is a little different from the other
+			 * language options since DataTables doesn't output floating point
+			 * numbers, so it won't ever use this for display of a number. Rather,
+			 * what this parameter does is modify the sort methods of the table so
+			 * that numbers which are in a format which has a character other than
+			 * a period (`.`) as a decimal place will be sorted numerically.
+			 *
+			 * Note that numbers with different decimal places cannot be shown in
+			 * the same table and still be sortable, the table must be consistent.
+			 * However, multiple different tables on the page can use different
+			 * decimal place characters.
+			 *  @type string
+			 *  @default 
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.decimal
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "decimal": ","
+			 *          "thousands": "."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sDecimal": "",
+	
+	
+			/**
+			 * DataTables has a build in number formatter (`formatNumber`) which is
+			 * used to format large numbers that are used in the table information.
+			 * By default a comma is used, but this can be trivially changed to any
+			 * character you wish with this parameter.
+			 *  @type string
+			 *  @default ,
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.thousands
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "thousands": "'"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sThousands": ",",
+	
+	
+			/**
+			 * Detail the action that will be taken when the drop down menu for the
+			 * pagination length option is changed. The '_MENU_' variable is replaced
+			 * with a default select list of 10, 25, 50 and 100, and can be replaced
+			 * with a custom select box if required.
+			 *  @type string
+			 *  @default Show _MENU_ entries
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.lengthMenu
+			 *
+			 *  @example
+			 *    // Language change only
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "lengthMenu": "Display _MENU_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 *
+			 *  @example
+			 *    // Language and options change
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "lengthMenu": 'Display <select>'+
+			 *            '<option value="10">10</option>'+
+			 *            '<option value="20">20</option>'+
+			 *            '<option value="30">30</option>'+
+			 *            '<option value="40">40</option>'+
+			 *            '<option value="50">50</option>'+
+			 *            '<option value="-1">All</option>'+
+			 *            '</select> records'
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLengthMenu": "Show _MENU_ entries",
+	
+	
+			/**
+			 * When using Ajax sourced data and during the first draw when DataTables is
+			 * gathering the data, this message is shown in an empty row in the table to
+			 * indicate to the end user the the data is being loaded. Note that this
+			 * parameter is not used when loading data by server-side processing, just
+			 * Ajax sourced data with client-side processing.
+			 *  @type string
+			 *  @default Loading...
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.loadingRecords
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "loadingRecords": "Please wait - loading..."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLoadingRecords": "Loading...",
+	
+	
+			/**
+			 * Text which is displayed when the table is processing a user action
+			 * (usually a sort command or similar).
+			 *  @type string
+			 *  @default Processing...
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.processing
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "processing": "DataTables is currently busy"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sProcessing": "Processing...",
+	
+	
+			/**
+			 * Details the actions that will be taken when the user types into the
+			 * filtering input text box. The variable "_INPUT_", if used in the string,
+			 * is replaced with the HTML text box for the filtering input allowing
+			 * control over where it appears in the string. If "_INPUT_" is not given
+			 * then the input box is appended to the string automatically.
+			 *  @type string
+			 *  @default Search:
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.search
+			 *
+			 *  @example
+			 *    // Input text box will be appended at the end automatically
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "search": "Filter records:"
+			 *        }
+			 *      } );
+			 *    } );
+			 *
+			 *  @example
+			 *    // Specify where the filter should appear
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "search": "Apply filter _INPUT_ to table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sSearch": "Search:",
+	
+	
+			/**
+			 * Assign a `placeholder` attribute to the search `input` element
+			 *  @type string
+			 *  @default 
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.searchPlaceholder
+			 */
+			"sSearchPlaceholder": "",
+	
+	
+			/**
+			 * All of the language information can be stored in a file on the
+			 * server-side, which DataTables will look up if this parameter is passed.
+			 * It must store the URL of the language file, which is in a JSON format,
+			 * and the object has the same properties as the oLanguage object in the
+			 * initialiser object (i.e. the above parameters). Please refer to one of
+			 * the example language files to see how this works in action.
+			 *  @type string
+			 *  @default <i>Empty string - i.e. disabled</i>
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.url
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "url": "http://www.sprymedia.co.uk/dataTables/lang.txt"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sUrl": "",
+	
+	
+			/**
+			 * Text shown inside the table records when the is no information to be
+			 * displayed after filtering. `emptyTable` is shown when there is simply no
+			 * information in the table at all (regardless of filtering).
+			 *  @type string
+			 *  @default No matching records found
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.zeroRecords
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "zeroRecords": "No records to display"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sZeroRecords": "No matching records found"
+		},
+	
+	
+		/**
+		 * This parameter allows you to have define the global filtering state at
+		 * initialisation time. As an object the `search` parameter must be
+		 * defined, but all other parameters are optional. When `regex` is true,
+		 * the search string will be treated as a regular expression, when false
+		 * (default) it will be treated as a straight string. When `smart`
+		 * DataTables will use it's smart filtering methods (to word match at
+		 * any point in the data), when false this will not be done.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.search
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "search": {"search": "Initial search"}
+		 *      } );
+		 *    } )
+		 */
+		"oSearch": $.extend( {}, DataTable.models.oSearch ),
+	
+	
+		/**
+		 * __Deprecated__ The functionality provided by this parameter has now been
+		 * superseded by that provided through `ajax`, which should be used instead.
+		 *
+		 * By default DataTables will look for the property `data` (or `aaData` for
+		 * compatibility with DataTables 1.9-) when obtaining data from an Ajax
+		 * source or for server-side processing - this parameter allows that
+		 * property to be changed. You can use Javascript dotted object notation to
+		 * get a data source for multiple levels of nesting.
+		 *  @type string
+		 *  @default data
+		 *
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.ajaxDataProp
+		 *
+		 *  @deprecated 1.10. Please use `ajax` for this functionality now.
+		 */
+		"sAjaxDataProp": "data",
+	
+	
+		/**
+		 * __Deprecated__ The functionality provided by this parameter has now been
+		 * superseded by that provided through `ajax`, which should be used instead.
+		 *
+		 * You can instruct DataTables to load data from an external
+		 * source using this parameter (use aData if you want to pass data in you
+		 * already have). Simply provide a url a JSON object can be obtained from.
+		 *  @type string
+		 *  @default null
+		 *
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.ajaxSource
+		 *
+		 *  @deprecated 1.10. Please use `ajax` for this functionality now.
+		 */
+		"sAjaxSource": null,
+	
+	
+		/**
+		 * This initialisation variable allows you to specify exactly where in the
+		 * DOM you want DataTables to inject the various controls it adds to the page
+		 * (for example you might want the pagination controls at the top of the
+		 * table). DIV elements (with or without a custom class) can also be added to
+		 * aid styling. The follow syntax is used:
+		 *   <ul>
+		 *     <li>The following options are allowed:
+		 *       <ul>
+		 *         <li>'l' - Length changing</li>
+		 *         <li>'f' - Filtering input</li>
+		 *         <li>'t' - The table!</li>
+		 *         <li>'i' - Information</li>
+		 *         <li>'p' - Pagination</li>
+		 *         <li>'r' - pRocessing</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following constants are allowed:
+		 *       <ul>
+		 *         <li>'H' - jQueryUI theme "header" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')</li>
+		 *         <li>'F' - jQueryUI theme "footer" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following syntax is expected:
+		 *       <ul>
+		 *         <li>'&lt;' and '&gt;' - div elements</li>
+		 *         <li>'&lt;"class" and '&gt;' - div with a class</li>
+		 *         <li>'&lt;"#id" and '&gt;' - div with an ID</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>Examples:
+		 *       <ul>
+		 *         <li>'&lt;"wrapper"flipt&gt;'</li>
+		 *         <li>'&lt;lf&lt;t&gt;ip&gt;'</li>
+		 *       </ul>
+		 *     </li>
+		 *   </ul>
+		 *  @type string
+		 *  @default lfrtip <i>(when `jQueryUI` is false)</i> <b>or</b>
+		 *    <"H"lfr>t<"F"ip> <i>(when `jQueryUI` is true)</i>
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.dom
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "dom": '&lt;"top"i&gt;rt&lt;"bottom"flp&gt;&lt;"clear"&gt;'
+		 *      } );
+		 *    } );
+		 */
+		"sDom": "lfrtip",
+	
+	
+		/**
+		 * Search delay option. This will throttle full table searches that use the
+		 * DataTables provided search input element (it does not effect calls to
+		 * `dt-api search()`, providing a delay before the search is made.
+		 *  @type integer
+		 *  @default 0
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.searchDelay
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "searchDelay": 200
+		 *      } );
+		 *    } )
+		 */
+		"searchDelay": null,
+	
+	
+		/**
+		 * DataTables features six different built-in options for the buttons to
+		 * display for pagination control:
+		 *
+		 * * `numbers` - Page number buttons only
+		 * * `simple` - 'Previous' and 'Next' buttons only
+		 * * 'simple_numbers` - 'Previous' and 'Next' buttons, plus page numbers
+		 * * `full` - 'First', 'Previous', 'Next' and 'Last' buttons
+		 * * `full_numbers` - 'First', 'Previous', 'Next' and 'Last' buttons, plus page numbers
+		 * * `first_last_numbers` - 'First' and 'Last' buttons, plus page numbers
+		 *  
+		 * Further methods can be added using {@link DataTable.ext.oPagination}.
+		 *  @type string
+		 *  @default simple_numbers
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.pagingType
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "pagingType": "full_numbers"
+		 *      } );
+		 *    } )
+		 */
+		"sPaginationType": "simple_numbers",
+	
+	
+		/**
+		 * Enable horizontal scrolling. When a table is too wide to fit into a
+		 * certain layout, or you have a large number of columns in the table, you
+		 * can enable x-scrolling to show the table in a viewport, which can be
+		 * scrolled. This property can be `true` which will allow the table to
+		 * scroll horizontally when needed, or any CSS unit, or a number (in which
+		 * case it will be treated as a pixel measurement). Setting as simply `true`
+		 * is recommended.
+		 *  @type boolean|string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.scrollX
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "scrollX": true,
+		 *        "scrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"sScrollX": "",
+	
+	
+		/**
+		 * This property can be used to force a DataTable to use more width than it
+		 * might otherwise do when x-scrolling is enabled. For example if you have a
+		 * table which requires to be well spaced, this parameter is useful for
+		 * "over-sizing" the table, and thus forcing scrolling. This property can by
+		 * any CSS unit, or a number (in which case it will be treated as a pixel
+		 * measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.scrollXInner
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "scrollX": "100%",
+		 *        "scrollXInner": "110%"
+		 *      } );
+		 *    } );
+		 */
+		"sScrollXInner": "",
+	
+	
+		/**
+		 * Enable vertical scrolling. Vertical scrolling will constrain the DataTable
+		 * to the given height, and enable scrolling for any data which overflows the
+		 * current viewport. This can be used as an alternative to paging to display
+		 * a lot of data in a small area (although paging and scrolling can both be
+		 * enabled at the same time). This property can be any CSS unit, or a number
+		 * (in which case it will be treated as a pixel measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.scrollY
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "scrollY": "200px",
+		 *        "paginate": false
+		 *      } );
+		 *    } );
+		 */
+		"sScrollY": "",
+	
+	
+		/**
+		 * __Deprecated__ The functionality provided by this parameter has now been
+		 * superseded by that provided through `ajax`, which should be used instead.
+		 *
+		 * Set the HTTP method that is used to make the Ajax call for server-side
+		 * processing or Ajax sourced data.
+		 *  @type string
+		 *  @default GET
+		 *
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.serverMethod
+		 *
+		 *  @deprecated 1.10. Please use `ajax` for this functionality now.
+		 */
+		"sServerMethod": "GET",
+	
+	
+		/**
+		 * DataTables makes use of renderers when displaying HTML elements for
+		 * a table. These renderers can be added or modified by plug-ins to
+		 * generate suitable mark-up for a site. For example the Bootstrap
+		 * integration plug-in for DataTables uses a paging button renderer to
+		 * display pagination buttons in the mark-up required by Bootstrap.
+		 *
+		 * For further information about the renderers available see
+		 * DataTable.ext.renderer
+		 *  @type string|object
+		 *  @default null
+		 *
+		 *  @name DataTable.defaults.renderer
+		 *
+		 */
+		"renderer": null,
+	
+	
+		/**
+		 * Set the data property name that DataTables should use to get a row's id
+		 * to set as the `id` property in the node.
+		 *  @type string
+		 *  @default DT_RowId
+		 *
+		 *  @name DataTable.defaults.rowId
+		 */
+		"rowId": "DT_RowId"
+	};
+	
+	_fnHungarianMap( DataTable.defaults );
+	
+	
+	
+	/*
+	 * Developer note - See note in model.defaults.js about the use of Hungarian
+	 * notation and camel case.
+	 */
+	
+	/**
+	 * Column options that can be given to DataTables at initialisation time.
+	 *  @namespace
+	 */
+	DataTable.defaults.column = {
+		/**
+		 * Define which column(s) an order will occur on for this column. This
+		 * allows a column's ordering to take multiple columns into account when
+		 * doing a sort or use the data from a different column. For example first
+		 * name / last name columns make sense to do a multi-column sort over the
+		 * two columns.
+		 *  @type array|int
+		 *  @default null <i>Takes the value of the column index automatically</i>
+		 *
+		 *  @name DataTable.defaults.column.orderData
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "orderData": [ 0, 1 ], "targets": [ 0 ] },
+		 *          { "orderData": [ 1, 0 ], "targets": [ 1 ] },
+		 *          { "orderData": 2, "targets": [ 2 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "orderData": [ 0, 1 ] },
+		 *          { "orderData": [ 1, 0 ] },
+		 *          { "orderData": 2 },
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aDataSort": null,
+		"iDataSort": -1,
+	
+	
+		/**
+		 * You can control the default ordering direction, and even alter the
+		 * behaviour of the sort handler (i.e. only allow ascending ordering etc)
+		 * using this parameter.
+		 *  @type array
+		 *  @default [ 'asc', 'desc' ]
+		 *
+		 *  @name DataTable.defaults.column.orderSequence
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "orderSequence": [ "asc" ], "targets": [ 1 ] },
+		 *          { "orderSequence": [ "desc", "asc", "asc" ], "targets": [ 2 ] },
+		 *          { "orderSequence": [ "desc" ], "targets": [ 3 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          null,
+		 *          { "orderSequence": [ "asc" ] },
+		 *          { "orderSequence": [ "desc", "asc", "asc" ] },
+		 *          { "orderSequence": [ "desc" ] },
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"asSorting": [ 'asc', 'desc' ],
+	
+	
+		/**
+		 * Enable or disable filtering on the data in this column.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @name DataTable.defaults.column.searchable
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "searchable": false, "targets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "searchable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSearchable": true,
+	
+	
+		/**
+		 * Enable or disable ordering on this column.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @name DataTable.defaults.column.orderable
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "orderable": false, "targets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "orderable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSortable": true,
+	
+	
+		/**
+		 * Enable or disable the display of this column.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @name DataTable.defaults.column.visible
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "visible": false, "targets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "visible": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bVisible": true,
+	
+	
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} td The TD node that has been created
+		 *  @param {*} cellData The Data for the cell
+		 *  @param {array|object} rowData The data for the whole row
+		 *  @param {int} row The row index for the aoData data store
+		 *  @param {int} col The column index for aoColumns
+		 *
+		 *  @name DataTable.defaults.column.createdCell
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [3],
+		 *          "createdCell": function (td, cellData, rowData, row, col) {
+		 *            if ( cellData == "1.7" ) {
+		 *              $(td).css('color', 'blue')
+		 *            }
+		 *          }
+		 *        } ]
+		 *      });
+		 *    } );
+		 */
+		"fnCreatedCell": null,
+	
+	
+		/**
+		 * This parameter has been replaced by `data` in DataTables to ensure naming
+		 * consistency. `dataProp` can still be used, as there is backwards
+		 * compatibility in DataTables for this option, but it is strongly
+		 * recommended that you use `data` in preference to `dataProp`.
+		 *  @name DataTable.defaults.column.dataProp
+		 */
+	
+	
+		/**
+		 * This property can be used to read data from any data source property,
+		 * including deeply nested objects / properties. `data` can be given in a
+		 * number of different ways which effect its behaviour:
+		 *
+		 * * `integer` - treated as an array index for the data source. This is the
+		 *   default that DataTables uses (incrementally increased for each column).
+		 * * `string` - read an object property from the data source. There are
+		 *   three 'special' options that can be used in the string to alter how
+		 *   DataTables reads the data from the source object:
+		 *    * `.` - Dotted Javascript notation. Just as you use a `.` in
+		 *      Javascript to read from nested objects, so to can the options
+		 *      specified in `data`. For example: `browser.version` or
+		 *      `browser.name`. If your object parameter name contains a period, use
+		 *      `\\` to escape it - i.e. `first\\.name`.
+		 *    * `[]` - Array notation. DataTables can automatically combine data
+		 *      from and array source, joining the data with the characters provided
+		 *      between the two brackets. For example: `name[, ]` would provide a
+		 *      comma-space separated list from the source array. If no characters
+		 *      are provided between the brackets, the original array source is
+		 *      returned.
+		 *    * `()` - Function notation. Adding `()` to the end of a parameter will
+		 *      execute a function of the name given. For example: `browser()` for a
+		 *      simple function on the data source, `browser.version()` for a
+		 *      function in a nested property or even `browser().version` to get an
+		 *      object property if the function called returns an object. Note that
+		 *      function notation is recommended for use in `render` rather than
+		 *      `data` as it is much simpler to use as a renderer.
+		 * * `null` - use the original data source for the row rather than plucking
+		 *   data directly from it. This action has effects on two other
+		 *   initialisation options:
+		 *    * `defaultContent` - When null is given as the `data` option and
+		 *      `defaultContent` is specified for the column, the value defined by
+		 *      `defaultContent` will be used for the cell.
+		 *    * `render` - When null is used for the `data` option and the `render`
+		 *      option is specified for the column, the whole data source for the
+		 *      row is used for the renderer.
+		 * * `function` - the function given will be executed whenever DataTables
+		 *   needs to set or get the data for a cell in the column. The function
+		 *   takes three parameters:
+		 *    * Parameters:
+		 *      * `{array|object}` The data source for the row
+		 *      * `{string}` The type call data requested - this will be 'set' when
+		 *        setting data or 'filter', 'display', 'type', 'sort' or undefined
+		 *        when gathering data. Note that when `undefined` is given for the
+		 *        type DataTables expects to get the raw data for the object back<
+		 *      * `{*}` Data to set when the second parameter is 'set'.
+		 *    * Return:
+		 *      * The return value from the function is not required when 'set' is
+		 *        the type of call, but otherwise the return is what will be used
+		 *        for the data requested.
+		 *
+		 * Note that `data` is a getter and setter option. If you just require
+		 * formatting of data for output, you will likely want to use `render` which
+		 * is simply a getter and thus simpler to use.
+		 *
+		 * Note that prior to DataTables 1.9.2 `data` was called `mDataProp`. The
+		 * name change reflects the flexibility of this property and is consistent
+		 * with the naming of mRender. If 'mDataProp' is given, then it will still
+		 * be used by DataTables, as it automatically maps the old name to the new
+		 * if required.
+		 *
+		 *  @type string|int|function|null
+		 *  @default null <i>Use automatically calculated column index</i>
+		 *
+		 *  @name DataTable.defaults.column.data
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Read table data from objects
+		 *    // JSON structure for each row:
+		 *    //   {
+		 *    //      "engine": {value},
+		 *    //      "browser": {value},
+		 *    //      "platform": {value},
+		 *    //      "version": {value},
+		 *    //      "grade": {value}
+		 *    //   }
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "ajaxSource": "sources/objects.txt",
+		 *        "columns": [
+		 *          { "data": "engine" },
+		 *          { "data": "browser" },
+		 *          { "data": "platform" },
+		 *          { "data": "version" },
+		 *          { "data": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Read information from deeply nested objects
+		 *    // JSON structure for each row:
+		 *    //   {
+		 *    //      "engine": {value},
+		 *    //      "browser": {value},
+		 *    //      "platform": {
+		 *    //         "inner": {value}
+		 *    //      },
+		 *    //      "details": [
+		 *    //         {value}, {value}
+		 *    //      ]
+		 *    //   }
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "ajaxSource": "sources/deep.txt",
+		 *        "columns": [
+		 *          { "data": "engine" },
+		 *          { "data": "browser" },
+		 *          { "data": "platform.inner" },
+		 *          { "data": "platform.details.0" },
+		 *          { "data": "platform.details.1" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `data` as a function to provide different information for
+		 *    // sorting, filtering and display. In this case, currency (price)
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": function ( source, type, val ) {
+		 *            if (type === 'set') {
+		 *              source.price = val;
+		 *              // Store the computed dislay and filter values for efficiency
+		 *              source.price_display = val=="" ? "" : "$"+numberFormat(val);
+		 *              source.price_filter  = val=="" ? "" : "$"+numberFormat(val)+" "+val;
+		 *              return;
+		 *            }
+		 *            else if (type === 'display') {
+		 *              return source.price_display;
+		 *            }
+		 *            else if (type === 'filter') {
+		 *              return source.price_filter;
+		 *            }
+		 *            // 'sort', 'type' and undefined all just use the integer
+		 *            return source.price;
+		 *          }
+		 *        } ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using default content
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": null,
+		 *          "defaultContent": "Click to edit"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using array notation - outputting a list from an array
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": "name[, ]"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 *
+		 */
+		"mData": null,
+	
+	
+		/**
+		 * This property is the rendering partner to `data` and it is suggested that
+		 * when you want to manipulate data for display (including filtering,
+		 * sorting etc) without altering the underlying data for the table, use this
+		 * property. `render` can be considered to be the the read only companion to
+		 * `data` which is read / write (then as such more complex). Like `data`
+		 * this option can be given in a number of different ways to effect its
+		 * behaviour:
+		 *
+		 * * `integer` - treated as an array index for the data source. This is the
+		 *   default that DataTables uses (incrementally increased for each column).
+		 * * `string` - read an object property from the data source. There are
+		 *   three 'special' options that can be used in the string to alter how
+		 *   DataTables reads the data from the source object:
+		 *    * `.` - Dotted Javascript notation. Just as you use a `.` in
+		 *      Javascript to read from nested objects, so to can the options
+		 *      specified in `data`. For example: `browser.version` or
+		 *      `browser.name`. If your object parameter name contains a period, use
+		 *      `\\` to escape it - i.e. `first\\.name`.
+		 *    * `[]` - Array notation. DataTables can automatically combine data
+		 *      from and array source, joining the data with the characters provided
+		 *      between the two brackets. For example: `name[, ]` would provide a
+		 *      comma-space separated list from the source array. If no characters
+		 *      are provided between the brackets, the original array source is
+		 *      returned.
+		 *    * `()` - Function notation. Adding `()` to the end of a parameter will
+		 *      execute a function of the name given. For example: `browser()` for a
+		 *      simple function on the data source, `browser.version()` for a
+		 *      function in a nested property or even `browser().version` to get an
+		 *      object property if the function called returns an object.
+		 * * `object` - use different data for the different data types requested by
+		 *   DataTables ('filter', 'display', 'type' or 'sort'). The property names
+		 *   of the object is the data type the property refers to and the value can
+		 *   defined using an integer, string or function using the same rules as
+		 *   `render` normally does. Note that an `_` option _must_ be specified.
+		 *   This is the default value to use if you haven't specified a value for
+		 *   the data type requested by DataTables.
+		 * * `function` - the function given will be executed whenever DataTables
+		 *   needs to set or get the data for a cell in the column. The function
+		 *   takes three parameters:
+		 *    * Parameters:
+		 *      * {array|object} The data source for the row (based on `data`)
+		 *      * {string} The type call data requested - this will be 'filter',
+		 *        'display', 'type' or 'sort'.
+		 *      * {array|object} The full data source for the row (not based on
+		 *        `data`)
+		 *    * Return:
+		 *      * The return value from the function is what will be used for the
+		 *        data requested.
+		 *
+		 *  @type string|int|function|object|null
+		 *  @default null Use the data source value.
+		 *
+		 *  @name DataTable.defaults.column.render
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Create a comma separated list from an array of objects
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "ajaxSource": "sources/deep.txt",
+		 *        "columns": [
+		 *          { "data": "engine" },
+		 *          { "data": "browser" },
+		 *          {
+		 *            "data": "platform",
+		 *            "render": "[, ].name"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Execute a function to obtain data
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": null, // Use the full data source object for the renderer's source
+		 *          "render": "browserName()"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // As an object, extracting different data for the different types
+		 *    // This would be used with a data source such as:
+		 *    //   { "phone": 5552368, "phone_filter": "5552368 555-2368", "phone_display": "555-2368" }
+		 *    // Here the `phone` integer is used for sorting and type detection, while `phone_filter`
+		 *    // (which has both forms) is used for filtering for if a user inputs either format, while
+		 *    // the formatted phone number is the one that is shown in the table.
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": null, // Use the full data source object for the renderer's source
+		 *          "render": {
+		 *            "_": "phone",
+		 *            "filter": "phone_filter",
+		 *            "display": "phone_display"
+		 *          }
+		 *        } ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Use as a function to create a link from the data source
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": "download_link",
+		 *          "render": function ( data, type, full ) {
+		 *            return '<a href="'+data+'">Download</a>';
+		 *          }
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"mRender": null,
+	
+	
+		/**
+		 * Change the cell type created for the column - either TD cells or TH cells. This
+		 * can be useful as TH cells have semantic meaning in the table body, allowing them
+		 * to act as a header for a row (you may wish to add scope='row' to the TH elements).
+		 *  @type string
+		 *  @default td
+		 *
+		 *  @name DataTable.defaults.column.cellType
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Make the first column use TH cells
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "cellType": "th"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"sCellType": "td",
+	
+	
+		/**
+		 * Class to give to each cell in this column.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *
+		 *  @name DataTable.defaults.column.class
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "class": "my_class", "targets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "class": "my_class" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sClass": "",
+	
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 * Generally you shouldn't need this!
+		 *  @type string
+		 *  @default <i>Empty string<i>
+		 *
+		 *  @name DataTable.defaults.column.contentPadding
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "contentPadding": "mmm"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sContentPadding": "",
+	
+	
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because `data`
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 *
+		 *  @name DataTable.defaults.column.defaultContent
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          {
+		 *            "data": null,
+		 *            "defaultContent": "Edit",
+		 *            "targets": [ -1 ]
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "data": null,
+		 *            "defaultContent": "Edit"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sDefaultContent": null,
+	
+	
+		/**
+		 * This parameter is only used in DataTables' server-side processing. It can
+		 * be exceptionally useful to know what columns are being displayed on the
+		 * client side, and to map these to database fields. When defined, the names
+		 * also allow DataTables to reorder information from the server if it comes
+		 * back in an unexpected order (i.e. if you switch your columns around on the
+		 * client-side, your server-side code does not also need updating).
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *
+		 *  @name DataTable.defaults.column.name
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "name": "engine", "targets": [ 0 ] },
+		 *          { "name": "browser", "targets": [ 1 ] },
+		 *          { "name": "platform", "targets": [ 2 ] },
+		 *          { "name": "version", "targets": [ 3 ] },
+		 *          { "name": "grade", "targets": [ 4 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "name": "engine" },
+		 *          { "name": "browser" },
+		 *          { "name": "platform" },
+		 *          { "name": "version" },
+		 *          { "name": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sName": "",
+	
+	
+		/**
+		 * Defines a data source type for the ordering which can be used to read
+		 * real-time information from the table (updating the internally cached
+		 * version) prior to ordering. This allows ordering to occur on user
+		 * editable elements such as form inputs.
+		 *  @type string
+		 *  @default std
+		 *
+		 *  @name DataTable.defaults.column.orderDataType
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "orderDataType": "dom-text", "targets": [ 2, 3 ] },
+		 *          { "type": "numeric", "targets": [ 3 ] },
+		 *          { "orderDataType": "dom-select", "targets": [ 4 ] },
+		 *          { "orderDataType": "dom-checkbox", "targets": [ 5 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          null,
+		 *          null,
+		 *          { "orderDataType": "dom-text" },
+		 *          { "orderDataType": "dom-text", "type": "numeric" },
+		 *          { "orderDataType": "dom-select" },
+		 *          { "orderDataType": "dom-checkbox" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sSortDataType": "std",
+	
+	
+		/**
+		 * The title of this column.
+		 *  @type string
+		 *  @default null <i>Derived from the 'TH' value for this column in the
+		 *    original HTML table.</i>
+		 *
+		 *  @name DataTable.defaults.column.title
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "title": "My column title", "targets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "title": "My column title" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sTitle": null,
+	
+	
+		/**
+		 * The type allows you to specify how the data for this column will be
+		 * ordered. Four types (string, numeric, date and html (which will strip
+		 * HTML tags before ordering)) are currently available. Note that only date
+		 * formats understood by Javascript's Date() object will be accepted as type
+		 * date. For example: "Mar 26, 2008 5:03 PM". May take the values: 'string',
+		 * 'numeric', 'date' or 'html' (by default). Further types can be adding
+		 * through plug-ins.
+		 *  @type string
+		 *  @default null <i>Auto-detected from raw data</i>
+		 *
+		 *  @name DataTable.defaults.column.type
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "type": "html", "targets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "type": "html" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sType": null,
+	
+	
+		/**
+		 * Defining the width of the column, this parameter may take any CSS value
+		 * (3em, 20px etc). DataTables applies 'smart' widths to columns which have not
+		 * been given a specific width through this interface ensuring that the table
+		 * remains readable.
+		 *  @type string
+		 *  @default null <i>Automatic</i>
+		 *
+		 *  @name DataTable.defaults.column.width
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "width": "20%", "targets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "width": "20%" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sWidth": null
+	};
+	
+	_fnHungarianMap( DataTable.defaults.column );
+	
+	
+	
+	/**
+	 * DataTables settings object - this holds all the information needed for a
+	 * given table, including configuration, data and current application of the
+	 * table options. DataTables does not have a single instance for each DataTable
+	 * with the settings attached to that instance, but rather instances of the
+	 * DataTable "class" are created on-the-fly as needed (typically by a
+	 * $().dataTable() call) and the settings object is then applied to that
+	 * instance.
+	 *
+	 * Note that this object is related to {@link DataTable.defaults} but this
+	 * one is the internal data store for DataTables's cache of columns. It should
+	 * NOT be manipulated outside of DataTables. Any configuration should be done
+	 * through the initialisation options.
+	 *  @namespace
+	 *  @todo Really should attach the settings object to individual instances so we
+	 *    don't need to create new instances on each $().dataTable() call (if the
+	 *    table already exists). It would also save passing oSettings around and
+	 *    into every single function. However, this is a very significant
+	 *    architecture change for DataTables and will almost certainly break
+	 *    backwards compatibility with older installations. This is something that
+	 *    will be done in 2.0.
+	 */
+	DataTable.models.oSettings = {
+		/**
+		 * Primary features of DataTables and their enablement state.
+		 *  @namespace
+		 */
+		"oFeatures": {
+	
+			/**
+			 * Flag to say if DataTables should automatically try to calculate the
+			 * optimum table and columns widths (true) or not (false).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bAutoWidth": null,
+	
+			/**
+			 * Delay the creation of TR and TD elements until they are actually
+			 * needed by a driven page draw. This can give a significant speed
+			 * increase for Ajax source and Javascript source data, but makes no
+			 * difference at all fro DOM and server-side processing tables.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bDeferRender": null,
+	
+			/**
+			 * Enable filtering on the table or not. Note that if this is disabled
+			 * then there is no filtering at all on the table, including fnFilter.
+			 * To just remove the filtering input use sDom and remove the 'f' option.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bFilter": null,
+	
+			/**
+			 * Table information element (the 'Showing x of y records' div) enable
+			 * flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bInfo": null,
+	
+			/**
+			 * Present a user control allowing the end user to change the page size
+			 * when pagination is enabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bLengthChange": null,
+	
+			/**
+			 * Pagination enabled or not. Note that if this is disabled then length
+			 * changing must also be disabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bPaginate": null,
+	
+			/**
+			 * Processing indicator enable flag whenever DataTables is enacting a
+			 * user request - typically an Ajax request for server-side processing.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bProcessing": null,
+	
+			/**
+			 * Server-side processing enabled flag - when enabled DataTables will
+			 * get all data from the server for every draw - there is no filtering,
+			 * sorting or paging done on the client-side.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bServerSide": null,
+	
+			/**
+			 * Sorting enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSort": null,
+	
+			/**
+			 * Multi-column sorting
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSortMulti": null,
+	
+			/**
+			 * Apply a class to the columns which are being sorted to provide a
+			 * visual highlight or not. This can slow things down when enabled since
+			 * there is a lot of DOM interaction.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSortClasses": null,
+	
+			/**
+			 * State saving enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bStateSave": null
+		},
+	
+	
+		/**
+		 * Scrolling settings for a table.
+		 *  @namespace
+		 */
+		"oScroll": {
+			/**
+			 * When the table is shorter in height than sScrollY, collapse the
+			 * table container down to the height of the table (when true).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bCollapse": null,
+	
+			/**
+			 * Width of the scrollbar for the web-browser's platform. Calculated
+			 * during table initialisation.
+			 *  @type int
+			 *  @default 0
+			 */
+			"iBarWidth": 0,
+	
+			/**
+			 * Viewport width for horizontal scrolling. Horizontal scrolling is
+			 * disabled if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sX": null,
+	
+			/**
+			 * Width to expand the table to when using x-scrolling. Typically you
+			 * should not need to use this.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 *  @deprecated
+			 */
+			"sXInner": null,
+	
+			/**
+			 * Viewport height for vertical scrolling. Vertical scrolling is disabled
+			 * if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sY": null
+		},
+	
+		/**
+		 * Language information for the table.
+		 *  @namespace
+		 *  @extends DataTable.defaults.oLanguage
+		 */
+		"oLanguage": {
+			/**
+			 * Information callback function. See
+			 * {@link DataTable.defaults.fnInfoCallback}
+			 *  @type function
+			 *  @default null
+			 */
+			"fnInfoCallback": null
+		},
+	
+		/**
+		 * Browser support parameters
+		 *  @namespace
+		 */
+		"oBrowser": {
+			/**
+			 * Indicate if the browser incorrectly calculates width:100% inside a
+			 * scrolling element (IE6/7)
+			 *  @type boolean
+			 *  @default false
+			 */
+			"bScrollOversize": false,
+	
+			/**
+			 * Determine if the vertical scrollbar is on the right or left of the
+			 * scrolling container - needed for rtl language layout, although not
+			 * all browsers move the scrollbar (Safari).
+			 *  @type boolean
+			 *  @default false
+			 */
+			"bScrollbarLeft": false,
+	
+			/**
+			 * Flag for if `getBoundingClientRect` is fully supported or not
+			 *  @type boolean
+			 *  @default false
+			 */
+			"bBounding": false,
+	
+			/**
+			 * Browser scrollbar width
+			 *  @type integer
+			 *  @default 0
+			 */
+			"barWidth": 0
+		},
+	
+	
+		"ajax": null,
+	
+	
+		/**
+		 * Array referencing the nodes which are used for the features. The
+		 * parameters of this object match what is allowed by sDom - i.e.
+		 *   <ul>
+		 *     <li>'l' - Length changing</li>
+		 *     <li>'f' - Filtering input</li>
+		 *     <li>'t' - The table!</li>
+		 *     <li>'i' - Information</li>
+		 *     <li>'p' - Pagination</li>
+		 *     <li>'r' - pRocessing</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aanFeatures": [],
+	
+		/**
+		 * Store data information - see {@link DataTable.models.oRow} for detailed
+		 * information.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoData": [],
+	
+		/**
+		 * Array of indexes which are in the current display (after filtering etc)
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplay": [],
+	
+		/**
+		 * Array of indexes for display - no filtering
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplayMaster": [],
+	
+		/**
+		 * Map of row ids to data indexes
+		 *  @type object
+		 *  @default {}
+		 */
+		"aIds": {},
+	
+		/**
+		 * Store information about each column that is in use
+		 *  @type array
+		 *  @default []
+		 */
+		"aoColumns": [],
+	
+		/**
+		 * Store information about the table's header
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeader": [],
+	
+		/**
+		 * Store information about the table's footer
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooter": [],
+	
+		/**
+		 * Store the applied global search information in case we want to force a
+		 * research or compare the old search to a new one.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 */
+		"oPreviousSearch": {},
+	
+		/**
+		 * Store the applied search for each column - see
+		 * {@link DataTable.models.oSearch} for the format that is used for the
+		 * filtering information for each column.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreSearchCols": [],
+	
+		/**
+		 * Sorting that is applied to the table. Note that the inner arrays are
+		 * used in the following manner:
+		 * <ul>
+		 *   <li>Index 0 - column number</li>
+		 *   <li>Index 1 - current sorting direction</li>
+		 * </ul>
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @todo These inner arrays should really be objects
+		 */
+		"aaSorting": null,
+	
+		/**
+		 * Sorting that is always applied to the table (i.e. prefixed in front of
+		 * aaSorting).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"aaSortingFixed": [],
+	
+		/**
+		 * Classes to use for the striping of a table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"asStripeClasses": null,
+	
+		/**
+		 * If restoring a table - we should restore its striping classes as well
+		 *  @type array
+		 *  @default []
+		 */
+		"asDestroyStripes": [],
+	
+		/**
+		 * If restoring a table - we should restore its width
+		 *  @type int
+		 *  @default 0
+		 */
+		"sDestroyWidth": 0,
+	
+		/**
+		 * Callback functions array for every time a row is inserted (i.e. on a draw).
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCallback": [],
+	
+		/**
+		 * Callback functions for the header on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeaderCallback": [],
+	
+		/**
+		 * Callback function for the footer on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooterCallback": [],
+	
+		/**
+		 * Array of callback functions for draw callback functions
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDrawCallback": [],
+	
+		/**
+		 * Array of callback functions for row created function
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCreatedCallback": [],
+	
+		/**
+		 * Callback functions for just before the table is redrawn. A return of
+		 * false will be used to cancel the draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreDrawCallback": [],
+	
+		/**
+		 * Callback functions for when the table has been initialised.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoInitComplete": [],
+	
+	
+		/**
+		 * Callbacks for modifying the settings to be stored for state saving, prior to
+		 * saving state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSaveParams": [],
+	
+		/**
+		 * Callbacks for modifying the settings that have been stored for state saving
+		 * prior to using the stored values to restore the state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoadParams": [],
+	
+		/**
+		 * Callbacks for operating on the settings object once the saved state has been
+		 * loaded
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoaded": [],
+	
+		/**
+		 * Cache the table ID for quick access
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sTableId": "",
+	
+		/**
+		 * The TABLE node for the main table
+		 *  @type node
+		 *  @default null
+		 */
+		"nTable": null,
+	
+		/**
+		 * Permanent ref to the thead element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTHead": null,
+	
+		/**
+		 * Permanent ref to the tfoot element - if it exists
+		 *  @type node
+		 *  @default null
+		 */
+		"nTFoot": null,
+	
+		/**
+		 * Permanent ref to the tbody element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTBody": null,
+	
+		/**
+		 * Cache the wrapper node (contains all DataTables controlled elements)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTableWrapper": null,
+	
+		/**
+		 * Indicate if when using server-side processing the loading of data
+		 * should be deferred until the second draw.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDeferLoading": false,
+	
+		/**
+		 * Indicate if all required information has been read in
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bInitialised": false,
+	
+		/**
+		 * Information about open rows. Each object in the array has the parameters
+		 * 'nTr' and 'nParent'
+		 *  @type array
+		 *  @default []
+		 */
+		"aoOpenRows": [],
+	
+		/**
+		 * Dictate the positioning of DataTables' control elements - see
+		 * {@link DataTable.model.oInit.sDom}.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sDom": null,
+	
+		/**
+		 * Search delay (in mS)
+		 *  @type integer
+		 *  @default null
+		 */
+		"searchDelay": null,
+	
+		/**
+		 * Which type of pagination should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default two_button
+		 */
+		"sPaginationType": "two_button",
+	
+		/**
+		 * The state duration (for `stateSave`) in seconds.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type int
+		 *  @default 0
+		 */
+		"iStateDuration": 0,
+	
+		/**
+		 * Array of callback functions for state saving. Each array element is an
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings
+		 *       and the JSON string to save that has been thus far created. Returns
+		 *       a JSON string to be inserted into a json object
+		 *       (i.e. '"param": [ 0, 1, 2]')</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSave": [],
+	
+		/**
+		 * Array of callback functions for state loading. Each array element is an
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings
+		 *       and the object stored. May return false to cancel state loading</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoad": [],
+	
+		/**
+		 * State that was saved. Useful for back reference
+		 *  @type object
+		 *  @default null
+		 */
+		"oSavedState": null,
+	
+		/**
+		 * State that was loaded. Useful for back reference
+		 *  @type object
+		 *  @default null
+		 */
+		"oLoadedState": null,
+	
+		/**
+		 * Source url for AJAX data for the table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sAjaxSource": null,
+	
+		/**
+		 * Property from a given object from which to read the table data from. This
+		 * can be an empty string (when not server-side processing), in which case
+		 * it is  assumed an an array is given directly.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sAjaxDataProp": null,
+	
+		/**
+		 * Note if draw should be blocked while getting data
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bAjaxDataGet": true,
+	
+		/**
+		 * The last jQuery XHR object that was used for server-side data gathering.
+		 * This can be used for working with the XHR information in one of the
+		 * callbacks
+		 *  @type object
+		 *  @default null
+		 */
+		"jqXHR": null,
+	
+		/**
+		 * JSON returned from the server in the last Ajax request
+		 *  @type object
+		 *  @default undefined
+		 */
+		"json": undefined,
+	
+		/**
+		 * Data submitted as part of the last Ajax request
+		 *  @type object
+		 *  @default undefined
+		 */
+		"oAjaxData": undefined,
+	
+		/**
+		 * Function to get the server-side data.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnServerData": null,
+	
+		/**
+		 * Functions which are called prior to sending an Ajax request so extra
+		 * parameters can easily be sent to the server
+		 *  @type array
+		 *  @default []
+		 */
+		"aoServerParams": [],
+	
+		/**
+		 * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if
+		 * required).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sServerMethod": null,
+	
+		/**
+		 * Format numbers for display.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnFormatNumber": null,
+	
+		/**
+		 * List of options that can be used for the user selectable length menu.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"aLengthMenu": null,
+	
+		/**
+		 * Counter for the draws that the table does. Also used as a tracker for
+		 * server-side processing
+		 *  @type int
+		 *  @default 0
+		 */
+		"iDraw": 0,
+	
+		/**
+		 * Indicate if a redraw is being done - useful for Ajax
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDrawing": false,
+	
+		/**
+		 * Draw index (iDraw) of the last error when parsing the returned data
+		 *  @type int
+		 *  @default -1
+		 */
+		"iDrawError": -1,
+	
+		/**
+		 * Paging display length
+		 *  @type int
+		 *  @default 10
+		 */
+		"_iDisplayLength": 10,
+	
+		/**
+		 * Paging start point - aiDisplay index
+		 *  @type int
+		 *  @default 0
+		 */
+		"_iDisplayStart": 0,
+	
+		/**
+		 * Server-side processing - number of records in the result set
+		 * (i.e. before filtering), Use fnRecordsTotal rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type int
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsTotal": 0,
+	
+		/**
+		 * Server-side processing - number of records in the current display set
+		 * (i.e. after filtering). Use fnRecordsDisplay rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type boolean
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsDisplay": 0,
+	
+		/**
+		 * Flag to indicate if jQuery UI marking and classes should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bJUI": null,
+	
+		/**
+		 * The classes to use for the table
+		 *  @type object
+		 *  @default {}
+		 */
+		"oClasses": {},
+	
+		/**
+		 * Flag attached to the settings object so you can check in the draw
+		 * callback if filtering has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bFiltered": false,
+	
+		/**
+		 * Flag attached to the settings object so you can check in the draw
+		 * callback if sorting has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bSorted": false,
+	
+		/**
+		 * Indicate that if multiple rows are in the header and there is more than
+		 * one unique cell per column, if the top one (true) or bottom one (false)
+		 * should be used for sorting / title by DataTables.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bSortCellsTop": null,
+	
+		/**
+		 * Initialisation object that is used for the table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInit": null,
+	
+		/**
+		 * Destroy callback functions - for plug-ins to attach themselves to the
+		 * destroy so they can clean up markup and events.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDestroyCallback": [],
+	
+	
+		/**
+		 * Get the number of records in the current record set, before filtering
+		 *  @type function
+		 */
+		"fnRecordsTotal": function ()
+		{
+			return _fnDataSource( this ) == 'ssp' ?
+				this._iRecordsTotal * 1 :
+				this.aiDisplayMaster.length;
+		},
+	
+		/**
+		 * Get the number of records in the current record set, after filtering
+		 *  @type function
+		 */
+		"fnRecordsDisplay": function ()
+		{
+			return _fnDataSource( this ) == 'ssp' ?
+				this._iRecordsDisplay * 1 :
+				this.aiDisplay.length;
+		},
+	
+		/**
+		 * Get the display end point - aiDisplay index
+		 *  @type function
+		 */
+		"fnDisplayEnd": function ()
+		{
+			var
+				len      = this._iDisplayLength,
+				start    = this._iDisplayStart,
+				calc     = start + len,
+				records  = this.aiDisplay.length,
+				features = this.oFeatures,
+				paginate = features.bPaginate;
+	
+			if ( features.bServerSide ) {
+				return paginate === false || len === -1 ?
+					start + records :
+					Math.min( start+len, this._iRecordsDisplay );
+			}
+			else {
+				return ! paginate || calc>records || len===-1 ?
+					records :
+					calc;
+			}
+		},
+	
+		/**
+		 * The DataTables object for this table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInstance": null,
+	
+		/**
+		 * Unique identifier for each instance of the DataTables object. If there
+		 * is an ID on the table node, then it takes that value, otherwise an
+		 * incrementing internal counter is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sInstance": null,
+	
+		/**
+		 * tabindex attribute value that is added to DataTables control elements, allowing
+		 * keyboard navigation of the table and its controls.
+		 */
+		"iTabIndex": 0,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollHead": null,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollFoot": null,
+	
+		/**
+		 * Last applied sort
+		 *  @type array
+		 *  @default []
+		 */
+		"aLastSort": [],
+	
+		/**
+		 * Stored plug-in instances
+		 *  @type object
+		 *  @default {}
+		 */
+		"oPlugins": {},
+	
+		/**
+		 * Function used to get a row's id from the row's data
+		 *  @type function
+		 *  @default null
+		 */
+		"rowIdFn": null,
+	
+		/**
+		 * Data location where to store a row's id
+		 *  @type string
+		 *  @default null
+		 */
+		"rowId": null
+	};
+
+	/**
+	 * Extension object for DataTables that is used to provide all extension
+	 * options.
+	 *
+	 * Note that the `DataTable.ext` object is available through
+	 * `jQuery.fn.dataTable.ext` where it may be accessed and manipulated. It is
+	 * also aliased to `jQuery.fn.dataTableExt` for historic reasons.
+	 *  @namespace
+	 *  @extends DataTable.models.ext
+	 */
+	
+	
+	/**
+	 * DataTables extensions
+	 * 
+	 * This namespace acts as a collection area for plug-ins that can be used to
+	 * extend DataTables capabilities. Indeed many of the build in methods
+	 * use this method to provide their own capabilities (sorting methods for
+	 * example).
+	 *
+	 * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy
+	 * reasons
+	 *
+	 *  @namespace
+	 */
+	DataTable.ext = _ext = {
+		/**
+		 * Buttons. For use with the Buttons extension for DataTables. This is
+		 * defined here so other extensions can define buttons regardless of load
+		 * order. It is _not_ used by DataTables core.
+		 *
+		 *  @type object
+		 *  @default {}
+		 */
+		buttons: {},
+	
+	
+		/**
+		 * Element class names
+		 *
+		 *  @type object
+		 *  @default {}
+		 */
+		classes: {},
+	
+	
+		/**
+		 * DataTables build type (expanded by the download builder)
+		 *
+		 *  @type string
+		 */
+		builder: "-source-",
+	
+	
+		/**
+		 * Error reporting.
+		 * 
+		 * How should DataTables report an error. Can take the value 'alert',
+		 * 'throw', 'none' or a function.
+		 *
+		 *  @type string|function
+		 *  @default alert
+		 */
+		errMode: "alert",
+	
+	
+		/**
+		 * Feature plug-ins.
+		 * 
+		 * This is an array of objects which describe the feature plug-ins that are
+		 * available to DataTables. These feature plug-ins are then available for
+		 * use through the `dom` initialisation option.
+		 * 
+		 * Each feature plug-in is described by an object which must have the
+		 * following properties:
+		 * 
+		 * * `fnInit` - function that is used to initialise the plug-in,
+		 * * `cFeature` - a character so the feature can be enabled by the `dom`
+		 *   instillation option. This is case sensitive.
+		 *
+		 * The `fnInit` function has the following input parameters:
+		 *
+		 * 1. `{object}` DataTables settings object: see
+		 *    {@link DataTable.models.oSettings}
+		 *
+		 * And the following return is expected:
+		 * 
+		 * * {node|null} The element which contains your feature. Note that the
+		 *   return may also be void if your plug-in does not require to inject any
+		 *   DOM elements into DataTables control (`dom`) - for example this might
+		 *   be useful when developing a plug-in which allows table control via
+		 *   keyboard entry
+		 *
+		 *  @type array
+		 *
+		 *  @example
+		 *    $.fn.dataTable.ext.features.push( {
+		 *      "fnInit": function( oSettings ) {
+		 *        return new TableTools( { "oDTSettings": oSettings } );
+		 *      },
+		 *      "cFeature": "T"
+		 *    } );
+		 */
+		feature: [],
+	
+	
+		/**
+		 * Row searching.
+		 * 
+		 * This method of searching is complimentary to the default type based
+		 * searching, and a lot more comprehensive as it allows you complete control
+		 * over the searching logic. Each element in this array is a function
+		 * (parameters described below) that is called for every row in the table,
+		 * and your logic decides if it should be included in the searching data set
+		 * or not.
+		 *
+		 * Searching functions have the following input parameters:
+		 *
+		 * 1. `{object}` DataTables settings object: see
+		 *    {@link DataTable.models.oSettings}
+		 * 2. `{array|object}` Data for the row to be processed (same as the
+		 *    original format that was passed in as the data source, or an array
+		 *    from a DOM data source
+		 * 3. `{int}` Row index ({@link DataTable.models.oSettings.aoData}), which
+		 *    can be useful to retrieve the `TR` element if you need DOM interaction.
+		 *
+		 * And the following return is expected:
+		 *
+		 * * {boolean} Include the row in the searched result set (true) or not
+		 *   (false)
+		 *
+		 * Note that as with the main search ability in DataTables, technically this
+		 * is "filtering", since it is subtractive. However, for consistency in
+		 * naming we call it searching here.
+		 *
+		 *  @type array
+		 *  @default []
+		 *
+		 *  @example
+		 *    // The following example shows custom search being applied to the
+		 *    // fourth column (i.e. the data[3] index) based on two input values
+		 *    // from the end-user, matching the data in a certain range.
+		 *    $.fn.dataTable.ext.search.push(
+		 *      function( settings, data, dataIndex ) {
+		 *        var min = document.getElementById('min').value * 1;
+		 *        var max = document.getElementById('max').value * 1;
+		 *        var version = data[3] == "-" ? 0 : data[3]*1;
+		 *
+		 *        if ( min == "" && max == "" ) {
+		 *          return true;
+		 *        }
+		 *        else if ( min == "" && version < max ) {
+		 *          return true;
+		 *        }
+		 *        else if ( min < version && "" == max ) {
+		 *          return true;
+		 *        }
+		 *        else if ( min < version && version < max ) {
+		 *          return true;
+		 *        }
+		 *        return false;
+		 *      }
+		 *    );
+		 */
+		search: [],
+	
+	
+		/**
+		 * Selector extensions
+		 *
+		 * The `selector` option can be used to extend the options available for the
+		 * selector modifier options (`selector-modifier` object data type) that
+		 * each of the three built in selector types offer (row, column and cell +
+		 * their plural counterparts). For example the Select extension uses this
+		 * mechanism to provide an option to select only rows, columns and cells
+		 * that have been marked as selected by the end user (`{selected: true}`),
+		 * which can be used in conjunction with the existing built in selector
+		 * options.
+		 *
+		 * Each property is an array to which functions can be pushed. The functions
+		 * take three attributes:
+		 *
+		 * * Settings object for the host table
+		 * * Options object (`selector-modifier` object type)
+		 * * Array of selected item indexes
+		 *
+		 * The return is an array of the resulting item indexes after the custom
+		 * selector has been applied.
+		 *
+		 *  @type object
+		 */
+		selector: {
+			cell: [],
+			column: [],
+			row: []
+		},
+	
+	
+		/**
+		 * Internal functions, exposed for used in plug-ins.
+		 * 
+		 * Please note that you should not need to use the internal methods for
+		 * anything other than a plug-in (and even then, try to avoid if possible).
+		 * The internal function may change between releases.
+		 *
+		 *  @type object
+		 *  @default {}
+		 */
+		internal: {},
+	
+	
+		/**
+		 * Legacy configuration options. Enable and disable legacy options that
+		 * are available in DataTables.
+		 *
+		 *  @type object
+		 */
+		legacy: {
+			/**
+			 * Enable / disable DataTables 1.9 compatible server-side processing
+			 * requests
+			 *
+			 *  @type boolean
+			 *  @default null
+			 */
+			ajax: null
+		},
+	
+	
+		/**
+		 * Pagination plug-in methods.
+		 * 
+		 * Each entry in this object is a function and defines which buttons should
+		 * be shown by the pagination rendering method that is used for the table:
+		 * {@link DataTable.ext.renderer.pageButton}. The renderer addresses how the
+		 * buttons are displayed in the document, while the functions here tell it
+		 * what buttons to display. This is done by returning an array of button
+		 * descriptions (what each button will do).
+		 *
+		 * Pagination types (the four built in options and any additional plug-in
+		 * options defined here) can be used through the `paginationType`
+		 * initialisation parameter.
+		 *
+		 * The functions defined take two parameters:
+		 *
+		 * 1. `{int} page` The current page index
+		 * 2. `{int} pages` The number of pages in the table
+		 *
+		 * Each function is expected to return an array where each element of the
+		 * array can be one of:
+		 *
+		 * * `first` - Jump to first page when activated
+		 * * `last` - Jump to last page when activated
+		 * * `previous` - Show previous page when activated
+		 * * `next` - Show next page when activated
+		 * * `{int}` - Show page of the index given
+		 * * `{array}` - A nested array containing the above elements to add a
+		 *   containing 'DIV' element (might be useful for styling).
+		 *
+		 * Note that DataTables v1.9- used this object slightly differently whereby
+		 * an object with two functions would be defined for each plug-in. That
+		 * ability is still supported by DataTables 1.10+ to provide backwards
+		 * compatibility, but this option of use is now decremented and no longer
+		 * documented in DataTables 1.10+.
+		 *
+		 *  @type object
+		 *  @default {}
+		 *
+		 *  @example
+		 *    // Show previous, next and current page buttons only
+		 *    $.fn.dataTableExt.oPagination.current = function ( page, pages ) {
+		 *      return [ 'previous', page, 'next' ];
+		 *    };
+		 */
+		pager: {},
+	
+	
+		renderer: {
+			pageButton: {},
+			header: {}
+		},
+	
+	
+		/**
+		 * Ordering plug-ins - custom data source
+		 * 
+		 * The extension options for ordering of data available here is complimentary
+		 * to the default type based ordering that DataTables typically uses. It
+		 * allows much greater control over the the data that is being used to
+		 * order a column, but is necessarily therefore more complex.
+		 * 
+		 * This type of ordering is useful if you want to do ordering based on data
+		 * live from the DOM (for example the contents of an 'input' element) rather
+		 * than just the static string that DataTables knows of.
+		 * 
+		 * The way these plug-ins work is that you create an array of the values you
+		 * wish to be ordering for the column in question and then return that
+		 * array. The data in the array much be in the index order of the rows in
+		 * the table (not the currently ordering order!). Which order data gathering
+		 * function is run here depends on the `dt-init columns.orderDataType`
+		 * parameter that is used for the column (if any).
+		 *
+		 * The functions defined take two parameters:
+		 *
+		 * 1. `{object}` DataTables settings object: see
+		 *    {@link DataTable.models.oSettings}
+		 * 2. `{int}` Target column index
+		 *
+		 * Each function is expected to return an array:
+		 *
+		 * * `{array}` Data for the column to be ordering upon
+		 *
+		 *  @type array
+		 *
+		 *  @example
+		 *    // Ordering using `input` node values
+		 *    $.fn.dataTable.ext.order['dom-text'] = function  ( settings, col )
+		 *    {
+		 *      return this.api().column( col, {order:'index'} ).nodes().map( function ( td, i ) {
+		 *        return $('input', td).val();
+		 *      } );
+		 *    }
+		 */
+		order: {},
+	
+	
+		/**
+		 * Type based plug-ins.
+		 *
+		 * Each column in DataTables has a type assigned to it, either by automatic
+		 * detection or by direct assignment using the `type` option for the column.
+		 * The type of a column will effect how it is ordering and search (plug-ins
+		 * can also make use of the column type if required).
+		 *
+		 * @namespace
+		 */
+		type: {
+			/**
+			 * Type detection functions.
+			 *
+			 * The functions defined in this object are used to automatically detect
+			 * a column's type, making initialisation of DataTables super easy, even
+			 * when complex data is in the table.
+			 *
+			 * The functions defined take two parameters:
+			 *
+		     *  1. `{*}` Data from the column cell to be analysed
+		     *  2. `{settings}` DataTables settings object. This can be used to
+		     *     perform context specific type detection - for example detection
+		     *     based on language settings such as using a comma for a decimal
+		     *     place. Generally speaking the options from the settings will not
+		     *     be required
+			 *
+			 * Each function is expected to return:
+			 *
+			 * * `{string|null}` Data type detected, or null if unknown (and thus
+			 *   pass it on to the other type detection functions.
+			 *
+			 *  @type array
+			 *
+			 *  @example
+			 *    // Currency type detection plug-in:
+			 *    $.fn.dataTable.ext.type.detect.push(
+			 *      function ( data, settings ) {
+			 *        // Check the numeric part
+			 *        if ( ! $.isNumeric( data.substring(1) ) ) {
+			 *          return null;
+			 *        }
+			 *
+			 *        // Check prefixed by currency
+			 *        if ( data.charAt(0) == '$' || data.charAt(0) == '&pound;' ) {
+			 *          return 'currency';
+			 *        }
+			 *        return null;
+			 *      }
+			 *    );
+			 */
+			detect: [],
+	
+	
+			/**
+			 * Type based search formatting.
+			 *
+			 * The type based searching functions can be used to pre-format the
+			 * data to be search on. For example, it can be used to strip HTML
+			 * tags or to de-format telephone numbers for numeric only searching.
+			 *
+			 * Note that is a search is not defined for a column of a given type,
+			 * no search formatting will be performed.
+			 * 
+			 * Pre-processing of searching data plug-ins - When you assign the sType
+			 * for a column (or have it automatically detected for you by DataTables
+			 * or a type detection plug-in), you will typically be using this for
+			 * custom sorting, but it can also be used to provide custom searching
+			 * by allowing you to pre-processing the data and returning the data in
+			 * the format that should be searched upon. This is done by adding
+			 * functions this object with a parameter name which matches the sType
+			 * for that target column. This is the corollary of <i>afnSortData</i>
+			 * for searching data.
+			 *
+			 * The functions defined take a single parameter:
+			 *
+		     *  1. `{*}` Data from the column cell to be prepared for searching
+			 *
+			 * Each function is expected to return:
+			 *
+			 * * `{string|null}` Formatted string that will be used for the searching.
+			 *
+			 *  @type object
+			 *  @default {}
+			 *
+			 *  @example
+			 *    $.fn.dataTable.ext.type.search['title-numeric'] = function ( d ) {
+			 *      return d.replace(/\n/g," ").replace( /<.*?>/g, "" );
+			 *    }
+			 */
+			search: {},
+	
+	
+			/**
+			 * Type based ordering.
+			 *
+			 * The column type tells DataTables what ordering to apply to the table
+			 * when a column is sorted upon. The order for each type that is defined,
+			 * is defined by the functions available in this object.
+			 *
+			 * Each ordering option can be described by three properties added to
+			 * this object:
+			 *
+			 * * `{type}-pre` - Pre-formatting function
+			 * * `{type}-asc` - Ascending order function
+			 * * `{type}-desc` - Descending order function
+			 *
+			 * All three can be used together, only `{type}-pre` or only
+			 * `{type}-asc` and `{type}-desc` together. It is generally recommended
+			 * that only `{type}-pre` is used, as this provides the optimal
+			 * implementation in terms of speed, although the others are provided
+			 * for compatibility with existing Javascript sort functions.
+			 *
+			 * `{type}-pre`: Functions defined take a single parameter:
+			 *
+		     *  1. `{*}` Data from the column cell to be prepared for ordering
+			 *
+			 * And return:
+			 *
+			 * * `{*}` Data to be sorted upon
+			 *
+			 * `{type}-asc` and `{type}-desc`: Functions are typical Javascript sort
+			 * functions, taking two parameters:
+			 *
+		     *  1. `{*}` Data to compare to the second parameter
+		     *  2. `{*}` Data to compare to the first parameter
+			 *
+			 * And returning:
+			 *
+			 * * `{*}` Ordering match: <0 if first parameter should be sorted lower
+			 *   than the second parameter, ===0 if the two parameters are equal and
+			 *   >0 if the first parameter should be sorted height than the second
+			 *   parameter.
+			 * 
+			 *  @type object
+			 *  @default {}
+			 *
+			 *  @example
+			 *    // Numeric ordering of formatted numbers with a pre-formatter
+			 *    $.extend( $.fn.dataTable.ext.type.order, {
+			 *      "string-pre": function(x) {
+			 *        a = (a === "-" || a === "") ? 0 : a.replace( /[^\d\-\.]/g, "" );
+			 *        return parseFloat( a );
+			 *      }
+			 *    } );
+			 *
+			 *  @example
+			 *    // Case-sensitive string ordering, with no pre-formatting method
+			 *    $.extend( $.fn.dataTable.ext.order, {
+			 *      "string-case-asc": function(x,y) {
+			 *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+			 *      },
+			 *      "string-case-desc": function(x,y) {
+			 *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+			 *      }
+			 *    } );
+			 */
+			order: {}
+		},
+	
+		/**
+		 * Unique DataTables instance counter
+		 *
+		 * @type int
+		 * @private
+		 */
+		_unique: 0,
+	
+	
+		//
+		// Depreciated
+		// The following properties are retained for backwards compatiblity only.
+		// The should not be used in new projects and will be removed in a future
+		// version
+		//
+	
+		/**
+		 * Version check function.
+		 *  @type function
+		 *  @depreciated Since 1.10
+		 */
+		fnVersionCheck: DataTable.fnVersionCheck,
+	
+	
+		/**
+		 * Index for what 'this' index API functions should use
+		 *  @type int
+		 *  @deprecated Since v1.10
+		 */
+		iApiIndex: 0,
+	
+	
+		/**
+		 * jQuery UI class container
+		 *  @type object
+		 *  @deprecated Since v1.10
+		 */
+		oJUIClasses: {},
+	
+	
+		/**
+		 * Software version
+		 *  @type string
+		 *  @deprecated Since v1.10
+		 */
+		sVersion: DataTable.version
+	};
+	
+	
+	//
+	// Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts
+	//
+	$.extend( _ext, {
+		afnFiltering: _ext.search,
+		aTypes:       _ext.type.detect,
+		ofnSearch:    _ext.type.search,
+		oSort:        _ext.type.order,
+		afnSortData:  _ext.order,
+		aoFeatures:   _ext.feature,
+		oApi:         _ext.internal,
+		oStdClasses:  _ext.classes,
+		oPagination:  _ext.pager
+	} );
+	
+	
+	$.extend( DataTable.ext.classes, {
+		"sTable": "dataTable",
+		"sNoFooter": "no-footer",
+	
+		/* Paging buttons */
+		"sPageButton": "paginate_button",
+		"sPageButtonActive": "current",
+		"sPageButtonDisabled": "disabled",
+	
+		/* Striping classes */
+		"sStripeOdd": "odd",
+		"sStripeEven": "even",
+	
+		/* Empty row */
+		"sRowEmpty": "dataTables_empty",
+	
+		/* Features */
+		"sWrapper": "dataTables_wrapper",
+		"sFilter": "dataTables_filter",
+		"sInfo": "dataTables_info",
+		"sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */
+		"sLength": "dataTables_length",
+		"sProcessing": "dataTables_processing",
+	
+		/* Sorting */
+		"sSortAsc": "sorting_asc",
+		"sSortDesc": "sorting_desc",
+		"sSortable": "sorting", /* Sortable in both directions */
+		"sSortableAsc": "sorting_asc_disabled",
+		"sSortableDesc": "sorting_desc_disabled",
+		"sSortableNone": "sorting_disabled",
+		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
+	
+		/* Filtering */
+		"sFilterInput": "",
+	
+		/* Page length */
+		"sLengthSelect": "",
+	
+		/* Scrolling */
+		"sScrollWrapper": "dataTables_scroll",
+		"sScrollHead": "dataTables_scrollHead",
+		"sScrollHeadInner": "dataTables_scrollHeadInner",
+		"sScrollBody": "dataTables_scrollBody",
+		"sScrollFoot": "dataTables_scrollFoot",
+		"sScrollFootInner": "dataTables_scrollFootInner",
+	
+		/* Misc */
+		"sHeaderTH": "",
+		"sFooterTH": "",
+	
+		// Deprecated
+		"sSortJUIAsc": "",
+		"sSortJUIDesc": "",
+		"sSortJUI": "",
+		"sSortJUIAscAllowed": "",
+		"sSortJUIDescAllowed": "",
+		"sSortJUIWrapper": "",
+		"sSortIcon": "",
+		"sJUIHeader": "",
+		"sJUIFooter": ""
+	} );
+	
+	
+	(function() {
+	
+	// Reused strings for better compression. Closure compiler appears to have a
+	// weird edge case where it is trying to expand strings rather than use the
+	// variable version. This results in about 200 bytes being added, for very
+	// little preference benefit since it this run on script load only.
+	var _empty = '';
+	_empty = '';
+	
+	var _stateDefault = _empty + 'ui-state-default';
+	var _sortIcon     = _empty + 'css_right ui-icon ui-icon-';
+	var _headerFooter = _empty + 'fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix';
+	
+	$.extend( DataTable.ext.oJUIClasses, DataTable.ext.classes, {
+		/* Full numbers paging buttons */
+		"sPageButton":         "fg-button ui-button "+_stateDefault,
+		"sPageButtonActive":   "ui-state-disabled",
+		"sPageButtonDisabled": "ui-state-disabled",
+	
+		/* Features */
+		"sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+
+			"ui-buttonset-multi paging_", /* Note that the type is postfixed */
+	
+		/* Sorting */
+		"sSortAsc":            _stateDefault+" sorting_asc",
+		"sSortDesc":           _stateDefault+" sorting_desc",
+		"sSortable":           _stateDefault+" sorting",
+		"sSortableAsc":        _stateDefault+" sorting_asc_disabled",
+		"sSortableDesc":       _stateDefault+" sorting_desc_disabled",
+		"sSortableNone":       _stateDefault+" sorting_disabled",
+		"sSortJUIAsc":         _sortIcon+"triangle-1-n",
+		"sSortJUIDesc":        _sortIcon+"triangle-1-s",
+		"sSortJUI":            _sortIcon+"carat-2-n-s",
+		"sSortJUIAscAllowed":  _sortIcon+"carat-1-n",
+		"sSortJUIDescAllowed": _sortIcon+"carat-1-s",
+		"sSortJUIWrapper":     "DataTables_sort_wrapper",
+		"sSortIcon":           "DataTables_sort_icon",
+	
+		/* Scrolling */
+		"sScrollHead": "dataTables_scrollHead "+_stateDefault,
+		"sScrollFoot": "dataTables_scrollFoot "+_stateDefault,
+	
+		/* Misc */
+		"sHeaderTH":  _stateDefault,
+		"sFooterTH":  _stateDefault,
+		"sJUIHeader": _headerFooter+" ui-corner-tl ui-corner-tr",
+		"sJUIFooter": _headerFooter+" ui-corner-bl ui-corner-br"
+	} );
+	
+	}());
+	
+	
+	
+	var extPagination = DataTable.ext.pager;
+	
+	function _numbers ( page, pages ) {
+		var
+			numbers = [],
+			buttons = extPagination.numbers_length,
+			half = Math.floor( buttons / 2 ),
+			i = 1;
+	
+		if ( pages <= buttons ) {
+			numbers = _range( 0, pages );
+		}
+		else if ( page <= half ) {
+			numbers = _range( 0, buttons-2 );
+			numbers.push( 'ellipsis' );
+			numbers.push( pages-1 );
+		}
+		else if ( page >= pages - 1 - half ) {
+			numbers = _range( pages-(buttons-2), pages );
+			numbers.splice( 0, 0, 'ellipsis' ); // no unshift in ie6
+			numbers.splice( 0, 0, 0 );
+		}
+		else {
+			numbers = _range( page-half+2, page+half-1 );
+			numbers.push( 'ellipsis' );
+			numbers.push( pages-1 );
+			numbers.splice( 0, 0, 'ellipsis' );
+			numbers.splice( 0, 0, 0 );
+		}
+	
+		numbers.DT_el = 'span';
+		return numbers;
+	}
+	
+	
+	$.extend( extPagination, {
+		simple: function ( page, pages ) {
+			return [ 'previous', 'next' ];
+		},
+	
+		full: function ( page, pages ) {
+			return [  'first', 'previous', 'next', 'last' ];
+		},
+	
+		numbers: function ( page, pages ) {
+			return [ _numbers(page, pages) ];
+		},
+	
+		simple_numbers: function ( page, pages ) {
+			return [ 'previous', _numbers(page, pages), 'next' ];
+		},
+	
+		full_numbers: function ( page, pages ) {
+			return [ 'first', 'previous', _numbers(page, pages), 'next', 'last' ];
+		},
+		
+		first_last_numbers: function (page, pages) {
+	 		return ['first', _numbers(page, pages), 'last'];
+	 	},
+	
+		// For testing and plug-ins to use
+		_numbers: _numbers,
+	
+		// Number of number buttons (including ellipsis) to show. _Must be odd!_
+		numbers_length: 7
+	} );
+	
+	
+	$.extend( true, DataTable.ext.renderer, {
+		pageButton: {
+			_: function ( settings, host, idx, buttons, page, pages ) {
+				var classes = settings.oClasses;
+				var lang = settings.oLanguage.oPaginate;
+				var aria = settings.oLanguage.oAria.paginate || {};
+				var btnDisplay, btnClass, counter=0;
+	
+				var attach = function( container, buttons ) {
+					var i, ien, node, button;
+					var clickHandler = function ( e ) {
+						_fnPageChange( settings, e.data.action, true );
+					};
+	
+					for ( i=0, ien=buttons.length ; i<ien ; i++ ) {
+						button = buttons[i];
+	
+						if ( $.isArray( button ) ) {
+							var inner = $( '<'+(button.DT_el || 'div')+'/>' )
+								.appendTo( container );
+							attach( inner, button );
+						}
+						else {
+							btnDisplay = null;
+							btnClass = '';
+	
+							switch ( button ) {
+								case 'ellipsis':
+									container.append('<span class="ellipsis">&#x2026;</span>');
+									break;
+	
+								case 'first':
+									btnDisplay = lang.sFirst;
+									btnClass = button + (page > 0 ?
+										'' : ' '+classes.sPageButtonDisabled);
+									break;
+	
+								case 'previous':
+									btnDisplay = lang.sPrevious;
+									btnClass = button + (page > 0 ?
+										'' : ' '+classes.sPageButtonDisabled);
+									break;
+	
+								case 'next':
+									btnDisplay = lang.sNext;
+									btnClass = button + (page < pages-1 ?
+										'' : ' '+classes.sPageButtonDisabled);
+									break;
+	
+								case 'last':
+									btnDisplay = lang.sLast;
+									btnClass = button + (page < pages-1 ?
+										'' : ' '+classes.sPageButtonDisabled);
+									break;
+	
+								default:
+									btnDisplay = button + 1;
+									btnClass = page === button ?
+										classes.sPageButtonActive : '';
+									break;
+							}
+	
+							if ( btnDisplay !== null ) {
+								node = $('<a>', {
+										'class': classes.sPageButton+' '+btnClass,
+										'aria-controls': settings.sTableId,
+										'aria-label': aria[ button ],
+										'data-dt-idx': counter,
+										'tabindex': settings.iTabIndex,
+										'id': idx === 0 && typeof button === 'string' ?
+											settings.sTableId +'_'+ button :
+											null
+									} )
+									.html( btnDisplay )
+									.appendTo( container );
+	
+								_fnBindAction(
+									node, {action: button}, clickHandler
+								);
+	
+								counter++;
+							}
+						}
+					}
+				};
+	
+				// IE9 throws an 'unknown error' if document.activeElement is used
+				// inside an iframe or frame. Try / catch the error. Not good for
+				// accessibility, but neither are frames.
+				var activeEl;
+	
+				try {
+					// Because this approach is destroying and recreating the paging
+					// elements, focus is lost on the select button which is bad for
+					// accessibility. So we want to restore focus once the draw has
+					// completed
+					activeEl = $(host).find(document.activeElement).data('dt-idx');
+				}
+				catch (e) {}
+	
+				attach( $(host).empty(), buttons );
+	
+				if ( activeEl !== undefined ) {
+					$(host).find( '[data-dt-idx='+activeEl+']' ).focus();
+				}
+			}
+		}
+	} );
+	
+	
+	
+	// Built in type detection. See model.ext.aTypes for information about
+	// what is required from this methods.
+	$.extend( DataTable.ext.type.detect, [
+		// Plain numbers - first since V8 detects some plain numbers as dates
+		// e.g. Date.parse('55') (but not all, e.g. Date.parse('22')...).
+		function ( d, settings )
+		{
+			var decimal = settings.oLanguage.sDecimal;
+			return _isNumber( d, decimal ) ? 'num'+decimal : null;
+		},
+	
+		// Dates (only those recognised by the browser's Date.parse)
+		function ( d, settings )
+		{
+			// V8 tries _very_ hard to make a string passed into `Date.parse()`
+			// valid, so we need to use a regex to restrict date formats. Use a
+			// plug-in for anything other than ISO8601 style strings
+			if ( d && !(d instanceof Date) && ! _re_date.test(d) ) {
+				return null;
+			}
+			var parsed = Date.parse(d);
+			return (parsed !== null && !isNaN(parsed)) || _empty(d) ? 'date' : null;
+		},
+	
+		// Formatted numbers
+		function ( d, settings )
+		{
+			var decimal = settings.oLanguage.sDecimal;
+			return _isNumber( d, decimal, true ) ? 'num-fmt'+decimal : null;
+		},
+	
+		// HTML numeric
+		function ( d, settings )
+		{
+			var decimal = settings.oLanguage.sDecimal;
+			return _htmlNumeric( d, decimal ) ? 'html-num'+decimal : null;
+		},
+	
+		// HTML numeric, formatted
+		function ( d, settings )
+		{
+			var decimal = settings.oLanguage.sDecimal;
+			return _htmlNumeric( d, decimal, true ) ? 'html-num-fmt'+decimal : null;
+		},
+	
+		// HTML (this is strict checking - there must be html)
+		function ( d, settings )
+		{
+			return _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1) ?
+				'html' : null;
+		}
+	] );
+	
+	
+	
+	// Filter formatting functions. See model.ext.ofnSearch for information about
+	// what is required from these methods.
+	// 
+	// Note that additional search methods are added for the html numbers and
+	// html formatted numbers by `_addNumericSort()` when we know what the decimal
+	// place is
+	
+	
+	$.extend( DataTable.ext.type.search, {
+		html: function ( data ) {
+			return _empty(data) ?
+				data :
+				typeof data === 'string' ?
+					data
+						.replace( _re_new_lines, " " )
+						.replace( _re_html, "" ) :
+					'';
+		},
+	
+		string: function ( data ) {
+			return _empty(data) ?
+				data :
+				typeof data === 'string' ?
+					data.replace( _re_new_lines, " " ) :
+					data;
+		}
+	} );
+	
+	
+	
+	var __numericReplace = function ( d, decimalPlace, re1, re2 ) {
+		if ( d !== 0 && (!d || d === '-') ) {
+			return -Infinity;
+		}
+	
+		// If a decimal place other than `.` is used, it needs to be given to the
+		// function so we can detect it and replace with a `.` which is the only
+		// decimal place Javascript recognises - it is not locale aware.
+		if ( decimalPlace ) {
+			d = _numToDecimal( d, decimalPlace );
+		}
+	
+		if ( d.replace ) {
+			if ( re1 ) {
+				d = d.replace( re1, '' );
+			}
+	
+			if ( re2 ) {
+				d = d.replace( re2, '' );
+			}
+		}
+	
+		return d * 1;
+	};
+	
+	
+	// Add the numeric 'deformatting' functions for sorting and search. This is done
+	// in a function to provide an easy ability for the language options to add
+	// additional methods if a non-period decimal place is used.
+	function _addNumericSort ( decimalPlace ) {
+		$.each(
+			{
+				// Plain numbers
+				"num": function ( d ) {
+					return __numericReplace( d, decimalPlace );
+				},
+	
+				// Formatted numbers
+				"num-fmt": function ( d ) {
+					return __numericReplace( d, decimalPlace, _re_formatted_numeric );
+				},
+	
+				// HTML numeric
+				"html-num": function ( d ) {
+					return __numericReplace( d, decimalPlace, _re_html );
+				},
+	
+				// HTML numeric, formatted
+				"html-num-fmt": function ( d ) {
+					return __numericReplace( d, decimalPlace, _re_html, _re_formatted_numeric );
+				}
+			},
+			function ( key, fn ) {
+				// Add the ordering method
+				_ext.type.order[ key+decimalPlace+'-pre' ] = fn;
+	
+				// For HTML types add a search formatter that will strip the HTML
+				if ( key.match(/^html\-/) ) {
+					_ext.type.search[ key+decimalPlace ] = _ext.type.search.html;
+				}
+			}
+		);
+	}
+	
+	
+	// Default sort methods
+	$.extend( _ext.type.order, {
+		// Dates
+		"date-pre": function ( d ) {
+			return Date.parse( d ) || -Infinity;
+		},
+	
+		// html
+		"html-pre": function ( a ) {
+			return _empty(a) ?
+				'' :
+				a.replace ?
+					a.replace( /<.*?>/g, "" ).toLowerCase() :
+					a+'';
+		},
+	
+		// string
+		"string-pre": function ( a ) {
+			// This is a little complex, but faster than always calling toString,
+			// http://jsperf.com/tostring-v-check
+			return _empty(a) ?
+				'' :
+				typeof a === 'string' ?
+					a.toLowerCase() :
+					! a.toString ?
+						'' :
+						a.toString();
+		},
+	
+		// string-asc and -desc are retained only for compatibility with the old
+		// sort methods
+		"string-asc": function ( x, y ) {
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+	
+		"string-desc": function ( x, y ) {
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		}
+	} );
+	
+	
+	// Numeric sorting types - order doesn't matter here
+	_addNumericSort( '' );
+	
+	
+	$.extend( true, DataTable.ext.renderer, {
+		header: {
+			_: function ( settings, cell, column, classes ) {
+				// No additional mark-up required
+				// Attach a sort listener to update on sort - note that using the
+				// `DT` namespace will allow the event to be removed automatically
+				// on destroy, while the `dt` namespaced event is the one we are
+				// listening for
+				$(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting, columns ) {
+					if ( settings !== ctx ) { // need to check this this is the host
+						return;               // table, not a nested one
+					}
+	
+					var colIdx = column.idx;
+	
+					cell
+						.removeClass(
+							column.sSortingClass +' '+
+							classes.sSortAsc +' '+
+							classes.sSortDesc
+						)
+						.addClass( columns[ colIdx ] == 'asc' ?
+							classes.sSortAsc : columns[ colIdx ] == 'desc' ?
+								classes.sSortDesc :
+								column.sSortingClass
+						);
+				} );
+			},
+	
+			jqueryui: function ( settings, cell, column, classes ) {
+				$('<div/>')
+					.addClass( classes.sSortJUIWrapper )
+					.append( cell.contents() )
+					.append( $('<span/>')
+						.addClass( classes.sSortIcon+' '+column.sSortingClassJUI )
+					)
+					.appendTo( cell );
+	
+				// Attach a sort listener to update on sort
+				$(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting, columns ) {
+					if ( settings !== ctx ) {
+						return;
+					}
+	
+					var colIdx = column.idx;
+	
+					cell
+						.removeClass( classes.sSortAsc +" "+classes.sSortDesc )
+						.addClass( columns[ colIdx ] == 'asc' ?
+							classes.sSortAsc : columns[ colIdx ] == 'desc' ?
+								classes.sSortDesc :
+								column.sSortingClass
+						);
+	
+					cell
+						.find( 'span.'+classes.sSortIcon )
+						.removeClass(
+							classes.sSortJUIAsc +" "+
+							classes.sSortJUIDesc +" "+
+							classes.sSortJUI +" "+
+							classes.sSortJUIAscAllowed +" "+
+							classes.sSortJUIDescAllowed
+						)
+						.addClass( columns[ colIdx ] == 'asc' ?
+							classes.sSortJUIAsc : columns[ colIdx ] == 'desc' ?
+								classes.sSortJUIDesc :
+								column.sSortingClassJUI
+						);
+				} );
+			}
+		}
+	} );
+	
+	/*
+	 * Public helper functions. These aren't used internally by DataTables, or
+	 * called by any of the options passed into DataTables, but they can be used
+	 * externally by developers working with DataTables. They are helper functions
+	 * to make working with DataTables a little bit easier.
+	 */
+	
+	var __htmlEscapeEntities = function ( d ) {
+		return typeof d === 'string' ?
+			d.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') :
+			d;
+	};
+	
+	/**
+	 * Helpers for `columns.render`.
+	 *
+	 * The options defined here can be used with the `columns.render` initialisation
+	 * option to provide a display renderer. The following functions are defined:
+	 *
+	 * * `number` - Will format numeric data (defined by `columns.data`) for
+	 *   display, retaining the original unformatted data for sorting and filtering.
+	 *   It takes 5 parameters:
+	 *   * `string` - Thousands grouping separator
+	 *   * `string` - Decimal point indicator
+	 *   * `integer` - Number of decimal points to show
+	 *   * `string` (optional) - Prefix.
+	 *   * `string` (optional) - Postfix (/suffix).
+	 * * `text` - Escape HTML to help prevent XSS attacks. It has no optional
+	 *   parameters.
+	 *
+	 * @example
+	 *   // Column definition using the number renderer
+	 *   {
+	 *     data: "salary",
+	 *     render: $.fn.dataTable.render.number( '\'', '.', 0, '$' )
+	 *   }
+	 *
+	 * @namespace
+	 */
+	DataTable.render = {
+		number: function ( thousands, decimal, precision, prefix, postfix ) {
+			return {
+				display: function ( d ) {
+					if ( typeof d !== 'number' && typeof d !== 'string' ) {
+						return d;
+					}
+	
+					var negative = d < 0 ? '-' : '';
+					var flo = parseFloat( d );
+	
+					// If NaN then there isn't much formatting that we can do - just
+					// return immediately, escaping any HTML (this was supposed to
+					// be a number after all)
+					if ( isNaN( flo ) ) {
+						return __htmlEscapeEntities( d );
+					}
+	
+					flo = flo.toFixed( precision );
+					d = Math.abs( flo );
+	
+					var intPart = parseInt( d, 10 );
+					var floatPart = precision ?
+						decimal+(d - intPart).toFixed( precision ).substring( 2 ):
+						'';
+	
+					return negative + (prefix||'') +
+						intPart.toString().replace(
+							/\B(?=(\d{3})+(?!\d))/g, thousands
+						) +
+						floatPart +
+						(postfix||'');
+				}
+			};
+		},
+	
+		text: function () {
+			return {
+				display: __htmlEscapeEntities
+			};
+		}
+	};
+	
+	
+	/*
+	 * This is really a good bit rubbish this method of exposing the internal methods
+	 * publicly... - To be fixed in 2.0 using methods on the prototype
+	 */
+	
+	
+	/**
+	 * Create a wrapper function for exporting an internal functions to an external API.
+	 *  @param {string} fn API function name
+	 *  @returns {function} wrapped function
+	 *  @memberof DataTable#internal
+	 */
+	function _fnExternApiFunc (fn)
+	{
+		return function() {
+			var args = [_fnSettingsFromNode( this[DataTable.ext.iApiIndex] )].concat(
+				Array.prototype.slice.call(arguments)
+			);
+			return DataTable.ext.internal[fn].apply( this, args );
+		};
+	}
+	
+	
+	/**
+	 * Reference to internal functions for use by plug-in developers. Note that
+	 * these methods are references to internal functions and are considered to be
+	 * private. If you use these methods, be aware that they are liable to change
+	 * between versions.
+	 *  @namespace
+	 */
+	$.extend( DataTable.ext.internal, {
+		_fnExternApiFunc: _fnExternApiFunc,
+		_fnBuildAjax: _fnBuildAjax,
+		_fnAjaxUpdate: _fnAjaxUpdate,
+		_fnAjaxParameters: _fnAjaxParameters,
+		_fnAjaxUpdateDraw: _fnAjaxUpdateDraw,
+		_fnAjaxDataSrc: _fnAjaxDataSrc,
+		_fnAddColumn: _fnAddColumn,
+		_fnColumnOptions: _fnColumnOptions,
+		_fnAdjustColumnSizing: _fnAdjustColumnSizing,
+		_fnVisibleToColumnIndex: _fnVisibleToColumnIndex,
+		_fnColumnIndexToVisible: _fnColumnIndexToVisible,
+		_fnVisbleColumns: _fnVisbleColumns,
+		_fnGetColumns: _fnGetColumns,
+		_fnColumnTypes: _fnColumnTypes,
+		_fnApplyColumnDefs: _fnApplyColumnDefs,
+		_fnHungarianMap: _fnHungarianMap,
+		_fnCamelToHungarian: _fnCamelToHungarian,
+		_fnLanguageCompat: _fnLanguageCompat,
+		_fnBrowserDetect: _fnBrowserDetect,
+		_fnAddData: _fnAddData,
+		_fnAddTr: _fnAddTr,
+		_fnNodeToDataIndex: _fnNodeToDataIndex,
+		_fnNodeToColumnIndex: _fnNodeToColumnIndex,
+		_fnGetCellData: _fnGetCellData,
+		_fnSetCellData: _fnSetCellData,
+		_fnSplitObjNotation: _fnSplitObjNotation,
+		_fnGetObjectDataFn: _fnGetObjectDataFn,
+		_fnSetObjectDataFn: _fnSetObjectDataFn,
+		_fnGetDataMaster: _fnGetDataMaster,
+		_fnClearTable: _fnClearTable,
+		_fnDeleteIndex: _fnDeleteIndex,
+		_fnInvalidate: _fnInvalidate,
+		_fnGetRowElements: _fnGetRowElements,
+		_fnCreateTr: _fnCreateTr,
+		_fnBuildHead: _fnBuildHead,
+		_fnDrawHead: _fnDrawHead,
+		_fnDraw: _fnDraw,
+		_fnReDraw: _fnReDraw,
+		_fnAddOptionsHtml: _fnAddOptionsHtml,
+		_fnDetectHeader: _fnDetectHeader,
+		_fnGetUniqueThs: _fnGetUniqueThs,
+		_fnFeatureHtmlFilter: _fnFeatureHtmlFilter,
+		_fnFilterComplete: _fnFilterComplete,
+		_fnFilterCustom: _fnFilterCustom,
+		_fnFilterColumn: _fnFilterColumn,
+		_fnFilter: _fnFilter,
+		_fnFilterCreateSearch: _fnFilterCreateSearch,
+		_fnEscapeRegex: _fnEscapeRegex,
+		_fnFilterData: _fnFilterData,
+		_fnFeatureHtmlInfo: _fnFeatureHtmlInfo,
+		_fnUpdateInfo: _fnUpdateInfo,
+		_fnInfoMacros: _fnInfoMacros,
+		_fnInitialise: _fnInitialise,
+		_fnInitComplete: _fnInitComplete,
+		_fnLengthChange: _fnLengthChange,
+		_fnFeatureHtmlLength: _fnFeatureHtmlLength,
+		_fnFeatureHtmlPaginate: _fnFeatureHtmlPaginate,
+		_fnPageChange: _fnPageChange,
+		_fnFeatureHtmlProcessing: _fnFeatureHtmlProcessing,
+		_fnProcessingDisplay: _fnProcessingDisplay,
+		_fnFeatureHtmlTable: _fnFeatureHtmlTable,
+		_fnScrollDraw: _fnScrollDraw,
+		_fnApplyToChildren: _fnApplyToChildren,
+		_fnCalculateColumnWidths: _fnCalculateColumnWidths,
+		_fnThrottle: _fnThrottle,
+		_fnConvertToWidth: _fnConvertToWidth,
+		_fnGetWidestNode: _fnGetWidestNode,
+		_fnGetMaxLenString: _fnGetMaxLenString,
+		_fnStringToCss: _fnStringToCss,
+		_fnSortFlatten: _fnSortFlatten,
+		_fnSort: _fnSort,
+		_fnSortAria: _fnSortAria,
+		_fnSortListener: _fnSortListener,
+		_fnSortAttachListener: _fnSortAttachListener,
+		_fnSortingClasses: _fnSortingClasses,
+		_fnSortData: _fnSortData,
+		_fnSaveState: _fnSaveState,
+		_fnLoadState: _fnLoadState,
+		_fnSettingsFromNode: _fnSettingsFromNode,
+		_fnLog: _fnLog,
+		_fnMap: _fnMap,
+		_fnBindAction: _fnBindAction,
+		_fnCallbackReg: _fnCallbackReg,
+		_fnCallbackFire: _fnCallbackFire,
+		_fnLengthOverflow: _fnLengthOverflow,
+		_fnRenderer: _fnRenderer,
+		_fnDataSource: _fnDataSource,
+		_fnRowAttributes: _fnRowAttributes,
+		_fnCalculateEnd: function () {} // Used by a lot of plug-ins, but redundant
+		                                // in 1.10, so this dead-end function is
+		                                // added to prevent errors
+	} );
+	
+
+	// jQuery access
+	$.fn.dataTable = DataTable;
+
+	// Provide access to the host jQuery object (circular reference)
+	DataTable.$ = $;
+
+	// Legacy aliases
+	$.fn.dataTableSettings = DataTable.settings;
+	$.fn.dataTableExt = DataTable.ext;
+
+	// With a capital `D` we return a DataTables API instance rather than a
+	// jQuery object
+	$.fn.DataTable = function ( opts ) {
+		return $(this).dataTable( opts ).api();
+	};
+
+	// All properties that are available to $.fn.dataTable should also be
+	// available on $.fn.DataTable
+	$.each( DataTable, function ( prop, val ) {
+		$.fn.DataTable[ prop ] = val;
+	} );
+
+
+	// Information about events fired by DataTables - for documentation.
+	/**
+	 * Draw event, fired whenever the table is redrawn on the page, at the same
+	 * point as fnDrawCallback. This may be useful for binding events or
+	 * performing calculations when the table is altered at all.
+	 *  @name DataTable#draw.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Search event, fired when the searching applied to the table (using the
+	 * built-in global search, or column filters) is altered.
+	 *  @name DataTable#search.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Page change event, fired when the paging of the table is altered.
+	 *  @name DataTable#page.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Order event, fired when the ordering applied to the table is altered.
+	 *  @name DataTable#order.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * DataTables initialisation complete event, fired when the table is fully
+	 * drawn, including Ajax data loaded, if Ajax data is required.
+	 *  @name DataTable#init.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The JSON object request from the server - only
+	 *    present if client-side Ajax sourced data is used</li></ol>
+	 */
+
+	/**
+	 * State save event, fired when the table has changed state a new state save
+	 * is required. This event allows modification of the state saving object
+	 * prior to actually doing the save, including addition or other state
+	 * properties (for plug-ins) or modification of a DataTables core property.
+	 *  @name DataTable#stateSaveParams.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The state information to be saved
+	 */
+
+	/**
+	 * State load event, fired when the table is loading state from the stored
+	 * data, but prior to the settings object being modified by the saved state
+	 * - allowing modification of the saved state is required or loading of
+	 * state for a plug-in.
+	 *  @name DataTable#stateLoadParams.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * State loaded event, fired when state has been loaded from stored data and
+	 * the settings object has been modified by the loaded data.
+	 *  @name DataTable#stateLoaded.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * Processing event, fired when DataTables is doing some kind of processing
+	 * (be it, order, searcg or anything else). It can be used to indicate to
+	 * the end user that there is something happening, or that something has
+	 * finished.
+	 *  @name DataTable#processing.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {boolean} bShow Flag for if DataTables is doing processing or not
+	 */
+
+	/**
+	 * Ajax (XHR) event, fired whenever an Ajax request is completed from a
+	 * request to made to the server for new data. This event is called before
+	 * DataTables processed the returned data, so it can also be used to pre-
+	 * process the data returned from the server, if needed.
+	 *
+	 * Note that this trigger is called in `fnServerData`, if you override
+	 * `fnServerData` and which to use this event, you need to trigger it in you
+	 * success function.
+	 *  @name DataTable#xhr.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 *  @param {object} json JSON returned from the server
+	 *
+	 *  @example
+	 *     // Use a custom property returned from the server in another DOM element
+	 *     $('#table').dataTable().on('xhr.dt', function (e, settings, json) {
+	 *       $('#status').html( json.status );
+	 *     } );
+	 *
+	 *  @example
+	 *     // Pre-process the data returned from the server
+	 *     $('#table').dataTable().on('xhr.dt', function (e, settings, json) {
+	 *       for ( var i=0, ien=json.aaData.length ; i<ien ; i++ ) {
+	 *         json.aaData[i].sum = json.aaData[i].one + json.aaData[i].two;
+	 *       }
+	 *       // Note no return - manipulate the data directly in the JSON object.
+	 *     } );
+	 */
+
+	/**
+	 * Destroy event, fired when the DataTable is destroyed by calling fnDestroy
+	 * or passing the bDestroy:true parameter in the initialisation object. This
+	 * can be used to remove bound events, added DOM nodes, etc.
+	 *  @name DataTable#destroy.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Page length change event, fired when number of records to show on each
+	 * page (the length) is changed.
+	 *  @name DataTable#length.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 *  @param {integer} len New length
+	 */
+
+	/**
+	 * Column sizing has changed.
+	 *  @name DataTable#column-sizing.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Column visibility has changed.
+	 *  @name DataTable#column-visibility.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 *  @param {int} column Column index
+	 *  @param {bool} vis `false` if column now hidden, or `true` if visible
+	 */
+
+	return $.fn.dataTable;
+}));
diff --git a/static/DataTables/DataTables-1.10.15/js/jquery.dataTables.min.js b/static/DataTables/DataTables-1.10.15/js/jquery.dataTables.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc969eefbb0f11750978439689a0d9771064e121
--- /dev/null
+++ b/static/DataTables/DataTables-1.10.15/js/jquery.dataTables.min.js
@@ -0,0 +1,167 @@
+/*!
+ DataTables 1.10.15
+ ©2008-2017 SpryMedia Ltd - datatables.net/license
+*/
+(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(E){return h(E,window,document)}):"object"===typeof exports?module.exports=function(E,H){E||(E=window);H||(H="undefined"!==typeof window?require("jquery"):require("jquery")(E));return h(H,E,E.document)}:h(jQuery,window,document)})(function(h,E,H,k){function Y(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()),
+d[c]=e,"o"===b[1]&&Y(a[e])});a._hungarianMap=d}function J(a,b,c){a._hungarianMap||Y(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),J(a[d],b[d],c)):b[d]=b[e]})}function Fa(a){var b=m.defaults.oLanguage,c=a.sZeroRecords;!a.sEmptyTable&&(c&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&F(a,a,"sZeroRecords","sLoadingRecords");
+a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&fb(a)}function gb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":"");"boolean"===typeof a.scrollX&&(a.scrollX=
+a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b<c;b++)a[b]&&J(m.models.oSearch,a[b])}function hb(a){A(a,"orderable","bSortable");A(a,"orderData","aDataSort");A(a,"orderSequence","asSorting");A(a,"orderDataType","sortDataType");var b=a.aDataSort;"number"===typeof b&&!h.isArray(b)&&(a.aDataSort=[b])}function ib(a){if(!m.__browser){var b={};m.__browser=b;var c=h("<div/>").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1,overflow:"hidden"}).append(h("<div/>").css({position:"absolute",
+top:1,left:1,width:100,overflow:"scroll"}).append(h("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,m.__browser);a.oScroll.iBarWidth=m.__browser.barWidth}function jb(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==
+e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ga(a,b){var c=m.defaults.column,d=a.aoColumns.length,c=h.extend({},m.models.oColumn,c,{nTh:b?b:H.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},m.models.oSearch,c[d]);la(a,d,h(b).data())}function la(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=
+e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(hb(c),J(m.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=R(g),i=b.mRender?R(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};
+b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return S(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=
+d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function Z(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ha(a);for(var c=0,d=b.length;c<d;c++)b[c].nTh.style.width=b[c].sWidth}b=a.oScroll;(""!==b.sY||""!==b.sX)&&ma(a);s(a,null,"column-sizing",[a])}function $(a,b){var c=na(a,"bVisible");return"number"===typeof c[b]?c[b]:null}function aa(a,b){var c=na(a,"bVisible"),c=h.inArray(b,
+c);return-1!==c?c:null}function ba(a){var b=0;h.each(a.aoColumns,function(a,d){d.bVisible&&"none"!==h(d.nTh).css("display")&&b++});return b}function na(a,b){var c=[];h.map(a.aoColumns,function(a,e){a[b]&&c.push(e)});return c}function Ia(a){var b=a.aoColumns,c=a.aoData,d=m.ext.type.detect,e,f,g,j,i,h,l,q,r;e=0;for(f=b.length;e<f;e++)if(l=b[e],r=[],!l.sType&&l._sManualType)l.sType=l._sManualType;else if(!l.sType){g=0;for(j=d.length;g<j;g++){i=0;for(h=c.length;i<h;i++){r[i]===k&&(r[i]=B(a,i,e,"type"));
+q=d[g](r[i],a);if(!q&&g!==d.length-1)break;if("html"===q)break}if(q){l.sType=q;break}}l.sType||(l.sType="string")}}function kb(a,b,c,d){var e,f,g,j,i,n,l=a.aoColumns;if(b)for(e=b.length-1;0<=e;e--){n=b[e];var q=n.targets!==k?n.targets:n.aTargets;h.isArray(q)||(q=[q]);f=0;for(g=q.length;f<g;f++)if("number"===typeof q[f]&&0<=q[f]){for(;l.length<=q[f];)Ga(a);d(q[f],n)}else if("number"===typeof q[f]&&0>q[f])d(l.length+q[f],n);else if("string"===typeof q[f]){j=0;for(i=l.length;j<i;j++)("_all"==q[f]||h(l[j].nTh).hasClass(q[f]))&&
+d(j,n)}}if(c){e=0;for(a=c.length;e<a;e++)d(e,c[e])}}function N(a,b,c,d){var e=a.aoData.length,f=h.extend(!0,{},m.models.oRow,{src:c?"dom":"data",idx:e});f._aData=b;a.aoData.push(f);for(var g=a.aoColumns,j=0,i=g.length;j<i;j++)g[j].sType=null;a.aiDisplayMaster.push(e);b=a.rowIdFn(b);b!==k&&(a.aIds[b]=f);(c||!a.oFeatures.bDeferRender)&&Ja(a,e,c,d);return e}function oa(a,b){var c;b instanceof h||(b=h(b));return b.map(function(b,e){c=Ka(a,e);return N(a,c.data,e,c.cells)})}function B(a,b,c,d){var e=a.iDraw,
+f=a.aoColumns[c],g=a.aoData[b]._aData,j=f.sDefaultContent,i=f.fnGetData(g,d,{settings:a,row:b,col:c});if(i===k)return a.iDrawError!=e&&null===j&&(K(a,0,"Requested unknown parameter "+("function"==typeof f.mData?"{function}":"'"+f.mData+"'")+" for row "+b+", column "+c,4),a.iDrawError=e),j;if((i===g||null===i)&&null!==j&&d!==k)i=j;else if("function"===typeof i)return i.call(g);return null===i&&"display"==d?"":i}function lb(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,d,{settings:a,row:b,col:c})}
+function La(a){return h.map(a.match(/(\\.|[^\.])+/g)||[""],function(a){return a.replace(/\\\./g,".")})}function R(a){if(h.isPlainObject(a)){var b={};h.each(a,function(a,c){c&&(b[a]=R(c))});return function(a,c,f,g){var j=b[c]||b._;return j!==k?j(a,c,f,g):a}}if(null===a)return function(a){return a};if("function"===typeof a)return function(b,c,f,g){return a(b,c,f,g)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var c=function(a,b,f){var g,j;if(""!==f){j=La(f);
+for(var i=0,n=j.length;i<n;i++){f=j[i].match(ca);g=j[i].match(V);if(f){j[i]=j[i].replace(ca,"");""!==j[i]&&(a=a[j[i]]);g=[];j.splice(0,i+1);j=j.join(".");if(h.isArray(a)){i=0;for(n=a.length;i<n;i++)g.push(c(a[i],b,j))}a=f[0].substring(1,f[0].length-1);a=""===a?g:g.join(a);break}else if(g){j[i]=j[i].replace(V,"");a=a[j[i]]();continue}if(null===a||a[j[i]]===k)return k;a=a[j[i]]}}return a};return function(b,e){return c(b,e,a)}}return function(b){return b[a]}}function S(a){if(h.isPlainObject(a))return S(a._);
+if(null===a)return function(){};if("function"===typeof a)return function(b,d,e){a(b,"set",d,e)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var b=function(a,d,e){var e=La(e),f;f=e[e.length-1];for(var g,j,i=0,n=e.length-1;i<n;i++){g=e[i].match(ca);j=e[i].match(V);if(g){e[i]=e[i].replace(ca,"");a[e[i]]=[];f=e.slice();f.splice(0,i+1);g=f.join(".");if(h.isArray(d)){j=0;for(n=d.length;j<n;j++)f={},b(f,d[j],g),a[e[i]].push(f)}else a[e[i]]=d;return}j&&(e[i]=e[i].replace(V,
+""),a=a[e[i]](d));if(null===a[e[i]]||a[e[i]]===k)a[e[i]]={};a=a[e[i]]}if(f.match(V))a[f.replace(V,"")](d);else a[f.replace(ca,"")]=d};return function(c,d){return b(c,d,a)}}return function(b,d){b[a]=d}}function Ma(a){return D(a.aoData,"_aData")}function pa(a){a.aoData.length=0;a.aiDisplayMaster.length=0;a.aiDisplay.length=0;a.aIds={}}function qa(a,b,c){for(var d=-1,e=0,f=a.length;e<f;e++)a[e]==b?d=e:a[e]>b&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function da(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);
+c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ka(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c<f;c++)g(j[c],c)}}e._aSortData=null;e._aFilterData=null;g=a.aoColumns;if(d!==k)g[d].sType=null;else{c=0;for(f=g.length;c<f;c++)g[c].sType=null;Na(a,e)}}function Ka(a,b,c,d){var e=[],f=b.firstChild,g,j,i=0,n,l=a.aoColumns,q=a._rowReadObject,d=d!==k?d:q?{}:[],r=function(a,b){if("string"===typeof a){var c=a.indexOf("@");
+-1!==c&&(c=a.substring(c+1),S(a)(d,b.getAttribute(c)))}},m=function(a){if(c===k||c===i)j=l[i],n=h.trim(a.innerHTML),j&&j._bAttrSrc?(S(j.mData._)(d,n),r(j.mData.sort,a),r(j.mData.type,a),r(j.mData.filter,a)):q?(j._setter||(j._setter=S(j.mData)),j._setter(d,n)):d[i]=n;i++};if(f)for(;f;){g=f.nodeName.toUpperCase();if("TD"==g||"TH"==g)m(f),e.push(f);f=f.nextSibling}else{e=b.anCells;f=0;for(g=e.length;f<g;f++)m(e[f])}if(b=b.firstChild?b:b.nTr)(b=b.getAttribute("id"))&&S(a.rowId)(d,b);return{data:d,cells:e}}
+function Ja(a,b,c,d){var e=a.aoData[b],f=e._aData,g=[],j,i,n,l,q;if(null===e.nTr){j=c||H.createElement("tr");e.nTr=j;e.anCells=g;j._DT_RowIndex=b;Na(a,e);l=0;for(q=a.aoColumns.length;l<q;l++){n=a.aoColumns[l];i=c?d[l]:H.createElement(n.sCellType);i._DT_CellIndex={row:b,column:l};g.push(i);if((!c||n.mRender||n.mData!==l)&&(!h.isPlainObject(n.mData)||n.mData._!==l+".display"))i.innerHTML=B(a,b,l,"display");n.sClass&&(i.className+=" "+n.sClass);n.bVisible&&!c?j.appendChild(i):!n.bVisible&&c&&i.parentNode.removeChild(i);
+n.fnCreatedCell&&n.fnCreatedCell.call(a.oInstance,i,B(a,b,l),f,b,l)}s(a,"aoRowCreatedCallback",null,[j,f,b])}e.nTr.setAttribute("role","row")}function Na(a,b){var c=b.nTr,d=b._aData;if(c){var e=a.rowIdFn(d);e&&(c.id=e);d.DT_RowClass&&(e=d.DT_RowClass.split(" "),b.__rowc=b.__rowc?sa(b.__rowc.concat(e)):e,h(c).removeClass(b.__rowc.join(" ")).addClass(d.DT_RowClass));d.DT_RowAttr&&h(c).attr(d.DT_RowAttr);d.DT_RowData&&h(c).data(d.DT_RowData)}}function mb(a){var b,c,d,e,f,g=a.nTHead,j=a.nTFoot,i=0===
+h("th, td",g).length,n=a.oClasses,l=a.aoColumns;i&&(e=h("<tr/>").appendTo(g));b=0;for(c=l.length;b<c;b++)f=l[b],d=h(f.nTh).addClass(f.sClass),i&&d.appendTo(e),a.oFeatures.bSort&&(d.addClass(f.sSortingClass),!1!==f.bSortable&&(d.attr("tabindex",a.iTabIndex).attr("aria-controls",a.sTableId),Oa(a,f.nTh,b))),f.sTitle!=d[0].innerHTML&&d.html(f.sTitle),Pa(a,"header")(a,d,f,n);i&&ea(a.aoHeader,g);h(g).find(">tr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(n.sFooterTH);
+if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b<c;b++)f=l[b],f.nTf=a[b].cell,f.sClass&&h(f.nTf).addClass(f.sClass)}}function fa(a,b,c){var d,e,f,g=[],j=[],i=a.aoColumns.length,n;if(b){c===k&&(c=!1);d=0;for(e=b.length;d<e;d++){g[d]=b[d].slice();g[d].nTr=b[d].nTr;for(f=i-1;0<=f;f--)!a.aoColumns[f].bVisible&&!c&&g[d].splice(f,1);j.push([])}d=0;for(e=g.length;d<e;d++){if(a=g[d].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=g[d].length;f<b;f++)if(n=i=1,j[d][f]===k){a.appendChild(g[d][f].cell);
+for(j[d][f]=1;g[d+i]!==k&&g[d][f].cell==g[d+i][f].cell;)j[d+i][f]=1,i++;for(;g[d][f+n]!==k&&g[d][f].cell==g[d][f+n].cell;){for(c=0;c<i;c++)j[d+c][f+n]=1;n++}h(g[d][f].cell).attr("rowspan",i).attr("colspan",n)}}}}function O(a){var b=s(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==h.inArray(!1,b))C(a,!1);else{var b=[],c=0,d=a.asStripeClasses,e=d.length,f=a.oLanguage,g=a.iInitDisplayStart,j="ssp"==y(a),i=a.aiDisplay;a.bDrawing=!0;g!==k&&-1!==g&&(a._iDisplayStart=j?g:g>=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=
+-1);var g=a._iDisplayStart,n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!nb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:n;for(j=j?0:g;j<f;j++){var l=i[j],q=a.aoData[l];null===q.nTr&&Ja(a,l);l=q.nTr;if(0!==e){var r=d[c%e];q._sRowStripe!=r&&(h(l).removeClass(q._sRowStripe).addClass(r),q._sRowStripe=r)}s(a,"aoRowCallback",null,[l,q._aData,c,j]);b.push(l);c++}}else c=f.sZeroRecords,1==a.iDraw&&"ajax"==y(a)?c=f.sLoadingRecords:
+f.sEmptyTable&&0===a.fnRecordsTotal()&&(c=f.sEmptyTable),b[0]=h("<tr/>",{"class":e?d[0]:""}).append(h("<td />",{valign:"top",colSpan:ba(a),"class":a.oClasses.sRowEmpty}).html(c))[0];s(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ma(a),g,n,i]);s(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ma(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));s(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter;
+c.bSort&&ob(a);d?ga(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;O(a);a._drawHold=!1}function pb(a){var b=a.oClasses,c=h(a.nTable),c=h("<div/>").insertBefore(c),d=a.oFeatures,e=h("<div/>",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,k=0;k<f.length;k++){g=null;j=f[k];if("<"==j){i=h("<div/>")[0];
+n=f[k+1];if("'"==n||'"'==n){l="";for(q=2;f[k+q]!=n;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=qb(a);else if("f"==j&&d.bFilter)g=rb(a);else if("r"==j&&d.bProcessing)g=sb(a);else if("t"==j)g=tb(a);else if("i"==j&&d.bInfo)g=ub(a);else if("p"==
+j&&d.bPaginate)g=vb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q<n;q++)if(j==i[q].cFeature){g=i[q].fnInit(a);break}}g&&(i=a.aanFeatures,i[j]||(i[j]=[]),i[j].push(g),e.append(g))}c.replaceWith(e);a.nHolding=null}function ea(a,b){var c=h(b).children("tr"),d,e,f,g,j,i,n,l,q,k;a.splice(0,a.length);f=0;for(i=c.length;f<i;f++)a.push([]);f=0;for(i=c.length;f<i;f++){d=c[f];for(e=d.firstChild;e;){if("TD"==e.nodeName.toUpperCase()||"TH"==e.nodeName.toUpperCase()){l=1*e.getAttribute("colspan");
+q=1*e.getAttribute("rowspan");l=!l||0===l||1===l?1:l;q=!q||0===q||1===q?1:q;g=0;for(j=a[f];j[g];)g++;n=g;k=1===l?!0:!1;for(j=0;j<l;j++)for(g=0;g<q;g++)a[f+g][n+j]={cell:e,unique:k},a[f+g].nTr=d}e=e.nextSibling}}}function ta(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],ea(c,b)));for(var b=0,e=c.length;b<e;b++)for(var f=0,g=c[b].length;f<g;f++)if(c[b][f].unique&&(!d[f]||!a.bSortCellsTop))d[f]=c[b][f].cell;return d}function ua(a,b,c){s(a,"aoServerParams","serverParams",[b]);if(b&&h.isArray(b)){var d={},
+e=/(.*?)\[\]$/;h.each(b,function(a,b){var c=b.name.match(e);c?(c=c[0],d[c]||(d[c]=[]),d[c].push(b.value)):d[b.name]=b.value});b=d}var f,g=a.ajax,j=a.oInstance,i=function(b){s(a,null,"xhr",[a,b,a.jqXHR]);c(b)};if(h.isPlainObject(g)&&g.data){f=g.data;var n=h.isFunction(f)?f(b,a):f,b=h.isFunction(f)&&n?n:h.extend(!0,b,n);delete g.data}n={data:b,success:function(b){var c=b.error||b.sError;c&&K(a,0,c);a.json=b;i(b)},dataType:"json",cache:!1,type:a.sServerMethod,error:function(b,c){var d=s(a,null,"xhr",
+[a,null,a.jqXHR]);-1===h.inArray(!0,d)&&("parsererror"==c?K(a,0,"Invalid JSON response",1):4===b.readyState&&K(a,0,"Ajax error",7));C(a,!1)}};a.oAjaxData=b;s(a,null,"preXhr",[a,b]);a.fnServerData?a.fnServerData.call(j,a.sAjaxSource,h.map(b,function(a,b){return{name:b,value:a}}),i,a):a.sAjaxSource||"string"===typeof g?a.jqXHR=h.ajax(h.extend(n,{url:g||a.sAjaxSource})):h.isFunction(g)?a.jqXHR=g.call(j,b,i,a):(a.jqXHR=h.ajax(h.extend(n,g)),g.data=f)}function nb(a){return a.bAjaxDataGet?(a.iDraw++,C(a,
+!0),ua(a,wb(a),function(b){xb(a,b)}),!1):!0}function wb(a){var b=a.aoColumns,c=b.length,d=a.oFeatures,e=a.oPreviousSearch,f=a.aoPreSearchCols,g,j=[],i,n,l,k=W(a);g=a._iDisplayStart;i=!1!==d.bPaginate?a._iDisplayLength:-1;var r=function(a,b){j.push({name:a,value:b})};r("sEcho",a.iDraw);r("iColumns",c);r("sColumns",D(b,"sName").join(","));r("iDisplayStart",g);r("iDisplayLength",i);var ra={draw:a.iDraw,columns:[],order:[],start:g,length:i,search:{value:e.sSearch,regex:e.bRegex}};for(g=0;g<c;g++)n=b[g],
+l=f[g],i="function"==typeof n.mData?"function":n.mData,ra.columns.push({data:i,name:n.sName,searchable:n.bSearchable,orderable:n.bSortable,search:{value:l.sSearch,regex:l.bRegex}}),r("mDataProp_"+g,i),d.bFilter&&(r("sSearch_"+g,l.sSearch),r("bRegex_"+g,l.bRegex),r("bSearchable_"+g,n.bSearchable)),d.bSort&&r("bSortable_"+g,n.bSortable);d.bFilter&&(r("sSearch",e.sSearch),r("bRegex",e.bRegex));d.bSort&&(h.each(k,function(a,b){ra.order.push({column:b.col,dir:b.dir});r("iSortCol_"+a,b.col);r("sSortDir_"+
+a,b.dir)}),r("iSortingCols",k.length));b=m.ext.legacy.ajax;return null===b?a.sAjaxSource?j:ra:b?j:ra}function xb(a,b){var c=va(a,b),d=b.sEcho!==k?b.sEcho:b.draw,e=b.iTotalRecords!==k?b.iTotalRecords:b.recordsTotal,f=b.iTotalDisplayRecords!==k?b.iTotalDisplayRecords:b.recordsFiltered;if(d){if(1*d<a.iDraw)return;a.iDraw=1*d}pa(a);a._iRecordsTotal=parseInt(e,10);a._iRecordsDisplay=parseInt(f,10);d=0;for(e=c.length;d<e;d++)N(a,c[d]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;O(a);a._bInitComplete||
+wa(a,b);a.bAjaxDataGet=!0;C(a,!1)}function va(a,b){var c=h.isPlainObject(a.ajax)&&a.ajax.dataSrc!==k?a.ajax.dataSrc:a.sAjaxDataProp;return"data"===c?b.aaData||b[c]:""!==c?R(c)(b):b}function rb(a){var b=a.oClasses,c=a.sTableId,d=a.oLanguage,e=a.oPreviousSearch,f=a.aanFeatures,g='<input type="search" class="'+b.sFilterInput+'"/>',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("<div/>",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("<label/>").append(j)),f=function(){var b=!this.value?
+"":this.value;b!=e.sSearch&&(ga(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,O(a))},g=null!==a.searchDelay?a.searchDelay:"ssp"===y(a)?400:0,i=h("input",b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).on("keyup.DT search.DT input.DT paste.DT cut.DT",g?Qa(f,g):f).on("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);h(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{i[0]!==H.activeElement&&i.val(e.sSearch)}catch(d){}});
+return b[0]}function ga(a,b,c){var d=a.oPreviousSearch,e=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};Ia(a);if("ssp"!=y(a)){yb(a,b.sSearch,c,b.bEscapeRegex!==k?!b.bEscapeRegex:b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b<e.length;b++)zb(a,e[b].sSearch,b,e[b].bEscapeRegex!==k?!e[b].bEscapeRegex:e[b].bRegex,e[b].bSmart,e[b].bCaseInsensitive);Ab(a)}else f(b);a.bFiltered=!0;s(a,null,"search",[a])}function Ab(a){for(var b=
+m.ext.search,c=a.aiDisplay,d,e,f=0,g=b.length;f<g;f++){for(var j=[],i=0,n=c.length;i<n;i++)e=c[i],d=a.aoData[e],b[f](a,d._aFilterData,e,d._aData,i)&&j.push(e);c.length=0;h.merge(c,j)}}function zb(a,b,c,d,e,f){if(""!==b){for(var g=[],j=a.aiDisplay,d=Ra(b,d,e,f),e=0;e<j.length;e++)b=a.aoData[j[e]]._aFilterData[c],d.test(b)&&g.push(j[e]);a.aiDisplay=g}}function yb(a,b,c,d,e,f){var d=Ra(b,d,e,f),f=a.oPreviousSearch.sSearch,g=a.aiDisplayMaster,j,e=[];0!==m.ext.search.length&&(c=!0);j=Bb(a);if(0>=b.length)a.aiDisplay=
+g.slice();else{if(j||c||f.length>b.length||0!==b.indexOf(f)||a.bSorted)a.aiDisplay=g.slice();b=a.aiDisplay;for(c=0;c<b.length;c++)d.test(a.aoData[b[c]]._sFilterRow)&&e.push(b[c]);a.aiDisplay=e}}function Ra(a,b,c,d){a=b?a:Sa(a);c&&(a="^(?=.*?"+h.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(a){if('"'===a.charAt(0))var b=a.match(/^"(.*)"$/),a=b?b[1]:a;return a.replace('"',"")}).join(")(?=.*?")+").*$");return RegExp(a,d?"i":"")}function Bb(a){var b=a.aoColumns,c,d,e,f,g,j,i,h,l=m.ext.type.search;c=!1;
+d=0;for(f=a.aoData.length;d<f;d++)if(h=a.aoData[d],!h._aFilterData){j=[];e=0;for(g=b.length;e<g;e++)c=b[e],c.bSearchable?(i=B(a,d,e,"filter"),l[c.sType]&&(i=l[c.sType](i)),null===i&&(i=""),"string"!==typeof i&&i.toString&&(i=i.toString())):i="",i.indexOf&&-1!==i.indexOf("&")&&(xa.innerHTML=i,i=$b?xa.textContent:xa.innerText),i.replace&&(i=i.replace(/[\r\n]/g,"")),j.push(i);h._aFilterData=j;h._sFilterRow=j.join("  ");c=!0}return c}function Cb(a){return{search:a.sSearch,smart:a.bSmart,regex:a.bRegex,
+caseInsensitive:a.bCaseInsensitive}}function Db(a){return{sSearch:a.search,bSmart:a.smart,bRegex:a.regex,bCaseInsensitive:a.caseInsensitive}}function ub(a){var b=a.sTableId,c=a.aanFeatures.i,d=h("<div/>",{"class":a.oClasses.sInfo,id:!c?b+"_info":null});c||(a.aoDrawCallback.push({fn:Eb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),h(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Eb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+
+1,e=a.fnDisplayEnd(),f=a.fnRecordsTotal(),g=a.fnRecordsDisplay(),j=g?c.sInfo:c.sInfoEmpty;g!==f&&(j+=" "+c.sInfoFiltered);j+=c.sInfoPostFix;j=Fb(a,j);c=c.fnInfoCallback;null!==c&&(j=c.call(a.oInstance,a,d,e,f,g,j));h(b).html(j)}}function Fb(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,f=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,
+f)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(f/e)))}function ha(a){var b,c,d=a.iInitDisplayStart,e=a.aoColumns,f;c=a.oFeatures;var g=a.bDeferLoading;if(a.bInitialised){pb(a);mb(a);fa(a,a.aoHeader);fa(a,a.aoFooter);C(a,!0);c.bAutoWidth&&Ha(a);b=0;for(c=e.length;b<c;b++)f=e[b],f.sWidth&&(f.nTh.style.width=v(f.sWidth));s(a,null,"preInit",[a]);T(a);e=y(a);if("ssp"!=e||g)"ajax"==e?ua(a,[],function(c){var f=va(a,c);for(b=0;b<f.length;b++)N(a,f[b]);a.iInitDisplayStart=
+d;T(a);C(a,!1);wa(a,c)},a):(C(a,!1),wa(a))}else setTimeout(function(){ha(a)},200)}function wa(a,b){a._bInitComplete=!0;(b||a.oInit.aaData)&&Z(a);s(a,null,"plugin-init",[a,b]);s(a,"aoInitComplete","init",[a,b])}function Ta(a,b){var c=parseInt(b,10);a._iDisplayLength=c;Ua(a);s(a,null,"length",[a,c])}function qb(a){for(var b=a.oClasses,c=a.sTableId,d=a.aLengthMenu,e=h.isArray(d[0]),f=e?d[0]:d,d=e?d[1]:d,e=h("<select/>",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect}),g=0,j=f.length;g<j;g++)e[0][g]=
+new Option(d[g],f[g]);var i=h("<div><label/></div>").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Ta(a,h(this).val());O(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function vb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){O(a)},b=h("<div/>").addClass(a.oClasses.sPaging+
+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;l<k;l++)Pa(a,"pageButton")(a,f.p[l],l,h,b,i)}else c.fnUpdate(a,e)},sName:"pagination"}));return b}function Va(a,b,c){var d=a._iDisplayStart,e=a._iDisplayLength,f=a.fnRecordsDisplay();0===f||-1===e?d=0:"number"===typeof b?(d=b*e,d>f&&
+(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e<f&&(d+=e):"last"==b?d=Math.floor((f-1)/e)*e:K(a,0,"Unknown paging action: "+b,5);b=a._iDisplayStart!==d;a._iDisplayStart=d;b&&(s(a,null,"page",[a]),c&&O(a));return b}function sb(a){return h("<div/>",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");
+s(a,null,"processing",[a,b])}function tb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("<div/>",{"class":f.sScrollWrapper}).append(h("<div/>",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("<div/>",
+{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("<div/>",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("<div/>",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("<div/>",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",
+0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],r=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(r.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=r;a.aoDrawCallback.push({fn:ma,sName:"scrolling"});return i[0]}function ma(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,n=j.children("table"),
+j=a.nScrollBody,l=h(j),q=j.style,r=h(a.nScrollFoot).children("div"),m=r.children("table"),p=h(a.nTHead),o=h(a.nTable),t=o[0],s=t.style,u=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,U=x.bScrollOversize,ac=D(a.aoColumns,"nTh"),P,L,Q,w,Wa=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};L=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!==L&&a.scrollBarVis!==k)a.scrollBarVis=L,Z(a);else{a.scrollBarVis=L;o.children("thead, tfoot").remove();
+u&&(Q=u.clone().prependTo(o),P=u.find("tr"),Q=Q.find("tr"));w=p.clone().prependTo(o);p=p.find("tr");L=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ta(a,w),function(b,c){B=$(a,b);c.style.width=a.aoColumns[B].sWidth});u&&I(function(a){a.style.width=""},Q);f=o.outerWidth();if(""===c){s.width="100%";if(U&&(o.find("tbody").height()>j.offsetHeight||"scroll"==l.css("overflow-y")))s.width=v(o.outerWidth()-b);f=o.outerWidth()}else""!==d&&(s.width=
+v(d),f=o.outerWidth());I(C,L);I(function(a){z.push(a.innerHTML);Wa.push(v(h(a).css("width")))},L);I(function(a,b){if(h.inArray(a,ac)!==-1)a.style.width=Wa[b]},p);h(L).height(0);u&&(I(C,Q),I(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},Q),I(function(a,b){a.style.width=y[b]},P),h(Q).height(0));I(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+z[b]+"</div>";a.style.width=Wa[b]},L);u&&I(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+
+A[b]+"</div>";a.style.width=y[b]},Q);if(o.outerWidth()<f){P=j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(U&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))s.width=v(P-b);(""===c||""!==d)&&K(a,1,"Possible column misalignment",6)}else P="100%";q.width=v(P);g.width=v(P);u&&(a.nScrollFoot.style.width=v(P));!e&&U&&(q.height=v(t.offsetHeight+b));c=o.outerWidth();n[0].style.width=v(c);i.width=v(c);d=o.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+
+(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";u&&(m[0].style.width=v(c),r[0].style.width=v(c),r[0].style[e]=d?b+"px":"0px");o.children("colgroup").insertBefore(o.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function I(a,b,c){for(var d=0,e=0,f=b.length,g,j;e<f;){g=b[e].firstChild;for(j=c?c[e].firstChild:null;g;)1===g.nodeType&&(c?a(g,j,d):a(g,d),d++),g=g.nextSibling,j=c?j.nextSibling:null;e++}}function Ha(a){var b=a.nTable,c=a.aoColumns,d=a.oScroll,
+e=d.sY,f=d.sX,g=d.sXInner,j=c.length,i=na(a,"bVisible"),n=h("th",a.nTHead),l=b.getAttribute("width"),k=b.parentNode,r=!1,m,p,o=a.oBrowser,d=o.bScrollOversize;(m=b.style.width)&&-1!==m.indexOf("%")&&(l=m);for(m=0;m<i.length;m++)p=c[i[m]],null!==p.sWidth&&(p.sWidth=Gb(p.sWidthOrig,k),r=!0);if(d||!r&&!f&&!e&&j==ba(a)&&j==n.length)for(m=0;m<j;m++)i=$(a,m),null!==i&&(c[i].sWidth=v(n.eq(m).width()));else{j=h(b).clone().css("visibility","hidden").removeAttr("id");j.find("tbody tr").remove();var t=h("<tr/>").appendTo(j.find("tbody"));
+j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");n=ta(a,j.find("thead")[0]);for(m=0;m<i.length;m++)p=c[i[m]],n[m].style.width=null!==p.sWidthOrig&&""!==p.sWidthOrig?v(p.sWidthOrig):"",p.sWidthOrig&&f&&h(n[m]).append(h("<div/>").css({width:p.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(m=0;m<i.length;m++)r=i[m],p=c[r],h(Hb(a,r)).clone(!1).append(p.sContentPadding).appendTo(t);h("[name]",
+j).removeAttr("name");p=h("<div/>").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()<k.clientWidth&&l&&j.width(k.clientWidth)):e?j.width(k.clientWidth):l&&j.width(l);for(m=e=0;m<i.length;m++)k=h(n[m]),g=k.outerWidth()-k.width(),k=o.bBounding?Math.ceil(n[m].getBoundingClientRect().width):k.outerWidth(),e+=k,c[i[m]].sWidth=v(k-g);b.style.width=v(e);p.remove()}l&&(b.style.width=
+v(l));if((l||f)&&!a._reszEvt)b=function(){h(E).on("resize.DT-"+a.sInstance,Qa(function(){Z(a)}))},d?setTimeout(b,1E3):b(),a._reszEvt=!0}function Gb(a,b){if(!a)return 0;var c=h("<div/>").css("width",v(a)).appendTo(b||H.body),d=c[0].offsetWidth;c.remove();return d}function Hb(a,b){var c=Ib(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("<td/>").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Ib(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;f<g;f++)c=B(a,f,b,"display")+"",c=c.replace(bc,
+""),c=c.replace(/&nbsp;/g," "),c.length>d&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function W(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):h.merge(n,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;a<n.length;a++){i=n[a][0];f=e[i].aDataSort;b=0;for(c=f.length;b<c;b++)g=f[b],j=e[g].sType||
+"string",n[a]._idx===k&&(n[a]._idx=h.inArray(n[a][1],e[g].asSorting)),d.push({src:i,col:g,dir:n[a][1],index:n[a]._idx,type:j,formatter:m.ext.type.order[j+"-pre"]})}return d}function ob(a){var b,c,d=[],e=m.ext.type.order,f=a.aoData,g=0,j,i=a.aiDisplayMaster,h;Ia(a);h=W(a);b=0;for(c=h.length;b<c;b++)j=h[b],j.formatter&&g++,Jb(a,j.col);if("ssp"!=y(a)&&0!==h.length){b=0;for(c=i.length;b<c;b++)d[i[b]]=b;g===h.length?i.sort(function(a,b){var c,e,g,j,i=h.length,k=f[a]._aSortData,m=f[b]._aSortData;for(g=
+0;g<i;g++)if(j=h[g],c=k[j.col],e=m[j.col],c=c<e?-1:c>e?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return c<e?-1:c>e?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,m=f[a]._aSortData,p=f[b]._aSortData;for(j=0;j<k;j++)if(i=h[j],c=m[i.col],g=p[i.col],i=e[i.type+"-"+i.dir]||e["string-"+i.dir],c=i(c,g),0!==c)return c;c=d[a];g=d[b];return c<g?-1:c>g?1:0})}a.bSorted=!0}function Kb(a){for(var b,c,d=a.aoColumns,e=W(a),a=a.oLanguage.oAria,f=0,g=d.length;f<g;f++){c=d[f];var j=c.asSorting;b=c.sTitle.replace(/<.*?>/g,
+"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0<e.length&&e[0].col==f?(i.setAttribute("aria-sort","asc"==e[0].dir?"ascending":"descending"),c=j[e[0].index+1]||j[0]):c=j[0],b+="asc"===c?a.sSortAscending:a.sSortDescending);i.setAttribute("aria-label",b)}}function Xa(a,b,c,d){var e=a.aaSorting,f=a.aoColumns[b].asSorting,g=function(a,b){var c=a._idx;c===k&&(c=h.inArray(a[1],f));return c+1<f.length?c+1:b?null:0};"number"===typeof e[0]&&(e=a.aaSorting=[e]);c&&a.oFeatures.bSortMulti?(c=h.inArray(b,
+D(e,"0")),-1!==c?(b=g(e[c],!0),null===b&&1===e.length&&(b=0),null===b?e.splice(c,1):(e[c][1]=f[b],e[c]._idx=b)):(e.push([b,f[0],0]),e[e.length-1]._idx=0)):e.length&&e[0][0]==b?(b=g(e[0]),e.length=1,e[0][1]=f[b],e[0]._idx=b):(e.length=0,e.push([b,f[0]]),e[0]._idx=0);T(a);"function"==typeof d&&d(a)}function Oa(a,b,c,d){var e=a.aoColumns[c];Ya(b,{},function(b){!1!==e.bSortable&&(a.oFeatures.bProcessing?(C(a,!0),setTimeout(function(){Xa(a,c,b.shiftKey,d);"ssp"!==y(a)&&C(a,!1)},0)):Xa(a,c,b.shiftKey,d))})}
+function ya(a){var b=a.aLastSort,c=a.oClasses.sSortColumn,d=W(a),e=a.oFeatures,f,g;if(e.bSort&&e.bSortClasses){e=0;for(f=b.length;e<f;e++)g=b[e].src,h(D(a.aoData,"anCells",g)).removeClass(c+(2>e?e+1:3));e=0;for(f=d.length;e<f;e++)g=d[e].src,h(D(a.aoData,"anCells",g)).addClass(c+(2>e?e+1:3))}a.aLastSort=d}function Jb(a,b){var c=a.aoColumns[b],d=m.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,aa(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j<i;j++)if(c=a.aoData[j],
+c._aSortData||(c._aSortData=[]),!c._aSortData[b]||d)f=d?e[j]:B(a,j,b,"sort"),c._aSortData[b]=g?g(f):f}function za(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b={time:+new Date,start:a._iDisplayStart,length:a._iDisplayLength,order:h.extend(!0,[],a.aaSorting),search:Cb(a.oPreviousSearch),columns:h.map(a.aoColumns,function(b,d){return{visible:b.bVisible,search:Cb(a.aoPreSearchCols[d])}})};s(a,"aoStateSaveParams","stateSaveParams",[a,b]);a.oSavedState=b;a.fnStateSaveCallback.call(a.oInstance,a,
+b)}}function Lb(a,b,c){var d,e,f=a.aoColumns,b=function(b){if(b&&b.time){var g=s(a,"aoStateLoadParams","stateLoadParams",[a,b]);if(-1===h.inArray(!1,g)&&(g=a.iStateDuration,!(0<g&&b.time<+new Date-1E3*g)&&!(b.columns&&f.length!==b.columns.length))){a.oLoadedState=h.extend(!0,{},b);b.start!==k&&(a._iDisplayStart=b.start,a.iInitDisplayStart=b.start);b.length!==k&&(a._iDisplayLength=b.length);b.order!==k&&(a.aaSorting=[],h.each(b.order,function(b,c){a.aaSorting.push(c[0]>=f.length?[0,c[1]]:c)}));b.search!==
+k&&h.extend(a.oPreviousSearch,Db(b.search));if(b.columns){d=0;for(e=b.columns.length;d<e;d++)g=b.columns[d],g.visible!==k&&(f[d].bVisible=g.visible),g.search!==k&&h.extend(a.aoPreSearchCols[d],Db(g.search))}s(a,"aoStateLoaded","stateLoaded",[a,b])}}c()};if(a.oFeatures.bStateSave){var g=a.fnStateLoadCallback.call(a.oInstance,a,b);g!==k&&b(g)}else c()}function Aa(a){var b=m.settings,a=h.inArray(a,D(b,"nTable"));return-1!==a?b[a]:null}function K(a,b,c,d){c="DataTables warning: "+(a?"table id="+a.sTableId+
+" - ":"")+c;d&&(c+=". For more information about this error, please see http://datatables.net/tn/"+d);if(b)E.console&&console.log&&console.log(c);else if(b=m.ext,b=b.sErrMode||b.errMode,a&&s(a,null,"error",[a,d,c]),"alert"==b)alert(c);else{if("throw"==b)throw Error(c);"function"==typeof b&&b(a,d,c)}}function F(a,b,c,d){h.isArray(c)?h.each(c,function(c,d){h.isArray(d)?F(a,b,d[0],d[1]):F(a,b,d)}):(d===k&&(d=c),b[c]!==k&&(a[d]=b[c]))}function Mb(a,b,c){var d,e;for(e in b)b.hasOwnProperty(e)&&(d=b[e],
+h.isPlainObject(d)?(h.isPlainObject(a[e])||(a[e]={}),h.extend(!0,a[e],d)):a[e]=c&&"data"!==e&&"aaData"!==e&&h.isArray(d)?d.slice():d);return a}function Ya(a,b,c){h(a).on("click.DT",b,function(b){a.blur();c(b)}).on("keypress.DT",b,function(a){13===a.which&&(a.preventDefault(),c(a))}).on("selectstart.DT",function(){return!1})}function z(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function s(a,b,c,d){var e=[];b&&(e=h.map(a[b].slice().reverse(),function(b){return b.fn.apply(a.oInstance,d)}));null!==c&&(b=h.Event(c+
+".dt"),h(a.nTable).trigger(b,d),e.push(b.result));return e}function Ua(a){var b=a._iDisplayStart,c=a.fnDisplayEnd(),d=a._iDisplayLength;b>=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Pa(a,b){var c=a.renderer,d=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ia(a,b){var c=[],c=Nb.numbers_length,d=Math.floor(c/2);b<=c?c=X(0,b):a<=d?(c=X(0,
+c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=X(b-(c-2),b):(c=X(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function fb(a){h.each({num:function(b){return Ba(b,a)},"num-fmt":function(b){return Ba(b,a,Za)},"html-num":function(b){return Ba(b,a,Ca)},"html-num-fmt":function(b){return Ba(b,a,Ca,Za)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Ob(a){return function(){var b=
+[Aa(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,b)}}var m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new t(Aa(this[x.iApiIndex])):new t(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=
+function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&ma(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};
+this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();
+return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return Aa(this[x.iApiIndex])};
+this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in m.ext.internal)e&&(this[e]=Ob(e));this.each(function(){var e={},g=1<d?Mb(e,a,!0):
+a,j=0,i,e=this.getAttribute("id"),n=!1,l=m.defaults,q=h(this);if("table"!=this.nodeName.toLowerCase())K(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{gb(l);hb(l.column);J(l,l,!0);J(l.column,l.column,!0);J(l,h.extend(g,q.data()));var r=m.settings,j=0;for(i=r.length;j<i;j++){var p=r[j];if(p.nTable==this||p.nTHead.parentNode==this||p.nTFoot&&p.nTFoot.parentNode==this){var t=g.bRetrieve!==k?g.bRetrieve:l.bRetrieve;if(c||t)return p.oInstance;if(g.bDestroy!==k?g.bDestroy:l.bDestroy){p.oInstance.fnDestroy();
+break}else{K(p,0,"Cannot reinitialise DataTable",3);return}}if(p.sTableId==this.id){r.splice(j,1);break}}if(null===e||""===e)this.id=e="DataTables_Table_"+m.ext._unique++;var o=h.extend(!0,{},m.models.oSettings,{sDestroyWidth:q[0].style.width,sInstance:e,sTableId:e});o.nTable=this;o.oApi=b.internal;o.oInit=g;r.push(o);o.oInstance=1===b.length?b:q.dataTable();gb(g);g.oLanguage&&Fa(g.oLanguage);g.aLengthMenu&&!g.iDisplayLength&&(g.iDisplayLength=h.isArray(g.aLengthMenu[0])?g.aLengthMenu[0][0]:g.aLengthMenu[0]);
+g=Mb(h.extend(!0,{},l),g);F(o.oFeatures,g,"bPaginate bLengthChange bFilter bSort bSortMulti bInfo bProcessing bAutoWidth bSortClasses bServerSide bDeferRender".split(" "));F(o,g,["asStripeClasses","ajax","fnServerData","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed","aLengthMenu","sPaginationType","sAjaxSource","sAjaxDataProp","iStateDuration","sDom","bSortCellsTop","iTabIndex","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId",["iCookieDuration","iStateDuration"],
+["oSearch","oPreviousSearch"],["aoSearchCols","aoPreSearchCols"],["iDisplayLength","_iDisplayLength"],["bJQueryUI","bJUI"]]);F(o.oScroll,g,[["sScrollX","sX"],["sScrollXInner","sXInner"],["sScrollY","sY"],["bScrollCollapse","bCollapse"]]);F(o.oLanguage,g,"fnInfoCallback");z(o,"aoDrawCallback",g.fnDrawCallback,"user");z(o,"aoServerParams",g.fnServerParams,"user");z(o,"aoStateSaveParams",g.fnStateSaveParams,"user");z(o,"aoStateLoadParams",g.fnStateLoadParams,"user");z(o,"aoStateLoaded",g.fnStateLoaded,
+"user");z(o,"aoRowCallback",g.fnRowCallback,"user");z(o,"aoRowCreatedCallback",g.fnCreatedRow,"user");z(o,"aoHeaderCallback",g.fnHeaderCallback,"user");z(o,"aoFooterCallback",g.fnFooterCallback,"user");z(o,"aoInitComplete",g.fnInitComplete,"user");z(o,"aoPreDrawCallback",g.fnPreDrawCallback,"user");o.rowIdFn=R(g.rowId);ib(o);var u=o.oClasses;g.bJQueryUI?(h.extend(u,m.ext.oJUIClasses,g.oClasses),g.sDom===l.sDom&&"lfrtip"===l.sDom&&(o.sDom='<"H"lfr>t<"F"ip>'),o.renderer)?h.isPlainObject(o.renderer)&&
+!o.renderer.header&&(o.renderer.header="jqueryui"):o.renderer="jqueryui":h.extend(u,m.ext.classes,g.oClasses);q.addClass(u.sTable);o.iInitDisplayStart===k&&(o.iInitDisplayStart=g.iDisplayStart,o._iDisplayStart=g.iDisplayStart);null!==g.iDeferLoading&&(o.bDeferLoading=!0,e=h.isArray(g.iDeferLoading),o._iRecordsDisplay=e?g.iDeferLoading[0]:g.iDeferLoading,o._iRecordsTotal=e?g.iDeferLoading[1]:g.iDeferLoading);var v=o.oLanguage;h.extend(!0,v,g.oLanguage);v.sUrl&&(h.ajax({dataType:"json",url:v.sUrl,success:function(a){Fa(a);
+J(l.oLanguage,a);h.extend(true,v,a);ha(o)},error:function(){ha(o)}}),n=!0);null===g.asStripeClasses&&(o.asStripeClasses=[u.sStripeOdd,u.sStripeEven]);var e=o.asStripeClasses,x=q.children("tbody").find("tr").eq(0);-1!==h.inArray(!0,h.map(e,function(a){return x.hasClass(a)}))&&(h("tbody tr",this).removeClass(e.join(" ")),o.asDestroyStripes=e.slice());e=[];r=this.getElementsByTagName("thead");0!==r.length&&(ea(o.aoHeader,r[0]),e=ta(o));if(null===g.aoColumns){r=[];j=0;for(i=e.length;j<i;j++)r.push(null)}else r=
+g.aoColumns;j=0;for(i=r.length;j<i;j++)Ga(o,e?e[j]:null);kb(o,g.aoColumnDefs,r,function(a,b){la(o,a,b)});if(x.length){var w=function(a,b){return a.getAttribute("data-"+b)!==null?b:null};h(x[0]).children("th, td").each(function(a,b){var c=o.aoColumns[a];if(c.mData===a){var d=w(b,"sort")||w(b,"order"),e=w(b,"filter")||w(b,"search");if(d!==null||e!==null){c.mData={_:a+".display",sort:d!==null?a+".@data-"+d:k,type:d!==null?a+".@data-"+d:k,filter:e!==null?a+".@data-"+e:k};la(o,a)}}})}var U=o.oFeatures,
+e=function(){if(g.aaSorting===k){var a=o.aaSorting;j=0;for(i=a.length;j<i;j++)a[j][1]=o.aoColumns[j].asSorting[0]}ya(o);U.bSort&&z(o,"aoDrawCallback",function(){if(o.bSorted){var a=W(o),b={};h.each(a,function(a,c){b[c.src]=c.dir});s(o,null,"order",[o,a,b]);Kb(o)}});z(o,"aoDrawCallback",function(){(o.bSorted||y(o)==="ssp"||U.bDeferRender)&&ya(o)},"sc");var a=q.children("caption").each(function(){this._captionSide=h(this).css("caption-side")}),b=q.children("thead");b.length===0&&(b=h("<thead/>").appendTo(q));
+o.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("<tbody/>").appendTo(q));o.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(o.oScroll.sX!==""||o.oScroll.sY!==""))b=h("<tfoot/>").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(u.sNoFooter);else if(b.length>0){o.nTFoot=b[0];ea(o.aoFooter,o.nTFoot)}if(g.aaData)for(j=0;j<g.aaData.length;j++)N(o,g.aaData[j]);else(o.bDeferLoading||y(o)=="dom")&&oa(o,h(o.nTBody).children("tr"));o.aiDisplay=o.aiDisplayMaster.slice();
+o.bInitialised=true;n===false&&ha(o)};g.bStateSave?(U.bStateSave=!0,z(o,"aoDrawCallback",za,"state_save"),Lb(o,g,e)):e()}});b=null;return this},x,t,p,u,$a={},Pb=/[\r\n]/g,Ca=/<.*?>/g,cc=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,dc=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Za=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,M=function(a){return!a||!0===a||"-"===a?!0:!1},Qb=function(a){var b=parseInt(a,10);return!isNaN(b)&&
+isFinite(a)?b:null},Rb=function(a,b){$a[b]||($a[b]=RegExp(Sa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace($a[b],"."):a},ab=function(a,b,c){var d="string"===typeof a;if(M(a))return!0;b&&d&&(a=Rb(a,b));c&&d&&(a=a.replace(Za,""));return!isNaN(parseFloat(a))&&isFinite(a)},Sb=function(a,b,c){return M(a)?!0:!(M(a)||"string"===typeof a)?null:ab(a.replace(Ca,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;e<f;e++)a[e]&&a[e][b]&&d.push(a[e][b][c]);else for(;e<
+f;e++)a[e]&&d.push(a[e][b]);return d},ja=function(a,b,c,d){var e=[],f=0,g=b.length;if(d!==k)for(;f<g;f++)a[b[f]][c]&&e.push(a[b[f]][c][d]);else for(;f<g;f++)e.push(a[b[f]][c]);return e},X=function(a,b){var c=[],d;b===k?(b=0,d=a):(d=b,b=a);for(var e=b;e<d;e++)c.push(e);return c},Tb=function(a){for(var b=[],c=0,d=a.length;c<d;c++)a[c]&&b.push(a[c]);return b},sa=function(a){var b;a:{if(!(2>a.length)){b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d<e;d++){if(b[d]===c){b=!1;break a}c=b[d]}}b=!0}if(b)return a.slice();
+b=[];var e=a.length,f,g=0,d=0;a:for(;d<e;d++){c=a[d];for(f=0;f<g;f++)if(b[f]===c)continue a;b.push(c);g++}return b};m.util={throttle:function(a,b){var c=b!==k?b:200,d,e;return function(){var b=this,g=+new Date,h=arguments;d&&g<d+c?(clearTimeout(e),e=setTimeout(function(){d=k;a.apply(b,h)},c)):(d=g,a.apply(b,h))}},escapeRegex:function(a){return a.replace(dc,"\\$1")}};var A=function(a,b,c){a[b]!==k&&(a[c]=a[b])},ca=/\[.*?\]$/,V=/\(\)$/,Sa=m.util.escapeRegex,xa=h("<div>")[0],$b=xa.textContent!==k,bc=
+/<.*?>/g,Qa=m.util.throttle,Ub=[],w=Array.prototype,ec=function(a){var b,c,d=m.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};t=function(a,b){if(!(this instanceof
+t))return new t(a,b);var c=[],d=function(a){(a=ec(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;e<f;e++)d(a[e]);else d(a);this.context=sa(c);b&&h.merge(this,b);this.selector={rows:null,cols:null,opts:null};t.extend(this,this,Ub)};m.Api=t;h.extend(t.prototype,{any:function(){return 0!==this.count()},concat:w.concat,context:[],count:function(){return this.flatten().length},each:function(a){for(var b=0,c=this.length;b<c;b++)a.call(this,this[b],b,this);return this},eq:function(a){var b=
+this.context;return b.length>a?new t(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)a.call(this,this[c],c,this)&&b.push(this[c]);return new t(this.context,b)},flatten:function(){var a=[];return new t(this.context,a.concat.apply(a,this.toArray()))},join:w.join,indexOf:w.indexOf||function(a,b){for(var c=b||0,d=this.length;c<d;c++)if(this[c]===a)return c;return-1},iterator:function(a,b,c,d){var e=[],f,g,h,i,n,l=this.context,
+m,p,u=this.selector;"string"===typeof a&&(d=c,c=b,b=a,a=!1);g=0;for(h=l.length;g<h;g++){var s=new t(l[g]);if("table"===b)f=c.call(s,l[g],g),f!==k&&e.push(f);else if("columns"===b||"rows"===b)f=c.call(s,l[g],this[g],g),f!==k&&e.push(f);else if("column"===b||"column-rows"===b||"row"===b||"cell"===b){p=this[g];"column-rows"===b&&(m=Da(l[g],u.opts));i=0;for(n=p.length;i<n;i++)f=p[i],f="cell"===b?c.call(s,l[g],f.row,f.column,g,i):c.call(s,l[g],f,g,i,m),f!==k&&e.push(f)}}return e.length||d?(a=new t(l,a?
+e.concat.apply([],e):e),b=a.selector,b.rows=u.rows,b.cols=u.cols,b.opts=u.opts,a):this},lastIndexOf:w.lastIndexOf||function(a,b){return this.indexOf.apply(this.toArray.reverse(),arguments)},length:0,map:function(a){var b=[];if(w.map)b=w.map.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)b.push(a.call(this,this[c],c));return new t(this.context,b)},pluck:function(a){return this.map(function(b){return b[a]})},pop:w.pop,push:w.push,reduce:w.reduce||function(a,b){return jb(this,a,b,0,this.length,
+1)},reduceRight:w.reduceRight||function(a,b){return jb(this,a,b,this.length-1,-1,-1)},reverse:w.reverse,selector:null,shift:w.shift,slice:function(){return new t(this.context,this)},sort:w.sort,splice:w.splice,toArray:function(){return w.slice.call(this)},to$:function(){return h(this)},toJQuery:function(){return h(this)},unique:function(){return new t(this.context,sa(this))},unshift:w.unshift});t.extend=function(a,b,c){if(c.length&&b&&(b instanceof t||b.__dt_wrapper)){var d,e,f,g=function(a,b,c){return function(){var d=
+b.apply(a,arguments);t.extend(d,d,c.methodExt);return d}};d=0;for(e=c.length;d<e;d++)f=c[d],b[f.name]="function"===typeof f.val?g(a,f.val,f):h.isPlainObject(f.val)?{}:f.val,b[f.name].__dt_wrapper=!0,t.extend(a,b[f.name],f.propExt)}};t.register=p=function(a,b){if(h.isArray(a))for(var c=0,d=a.length;c<d;c++)t.register(a[c],b);else for(var e=a.split("."),f=Ub,g,j,c=0,d=e.length;c<d;c++){g=(j=-1!==e[c].indexOf("()"))?e[c].replace("()",""):e[c];var i;a:{i=0;for(var n=f.length;i<n;i++)if(f[i].name===g){i=
+f[i];break a}i=null}i||(i={name:g,val:{},methodExt:[],propExt:[]},f.push(i));c===d-1?i.val=b:f=j?i.methodExt:i.propExt}};t.registerPlural=u=function(a,b,c){t.register(a,c);t.register(b,function(){var a=c.apply(this,arguments);return a===this?this:a instanceof t?a.length?h.isArray(a[0])?new t(a.context,a[0]):a[0]:k:a})};p("tables()",function(a){var b;if(a){b=t;var c=this.context;if("number"===typeof a)a=[c[a]];else var d=h.map(c,function(a){return a.nTable}),a=h(d).filter(a).map(function(){var a=h.inArray(this,
+d);return c[a]}).toArray();b=new b(a)}else b=this;return b});p("table()",function(a){var a=this.tables(a),b=a.context;return b.length?new t(b[0]):a});u("tables().nodes()","table().node()",function(){return this.iterator("table",function(a){return a.nTable},1)});u("tables().body()","table().body()",function(){return this.iterator("table",function(a){return a.nTBody},1)});u("tables().header()","table().header()",function(){return this.iterator("table",function(a){return a.nTHead},1)});u("tables().footer()",
+"table().footer()",function(){return this.iterator("table",function(a){return a.nTFoot},1)});u("tables().containers()","table().container()",function(){return this.iterator("table",function(a){return a.nTableWrapper},1)});p("draw()",function(a){return this.iterator("table",function(b){"page"===a?O(b):("string"===typeof a&&(a="full-hold"===a?!1:!0),T(b,!1===a))})});p("page()",function(a){return a===k?this.page.info().page:this.iterator("table",function(b){Va(b,a)})});p("page.info()",function(){if(0===
+this.context.length)return k;var a=this.context[0],b=a._iDisplayStart,c=a.oFeatures.bPaginate?a._iDisplayLength:-1,d=a.fnRecordsDisplay(),e=-1===c;return{page:e?0:Math.floor(b/c),pages:e?1:Math.ceil(d/c),start:b,end:a.fnDisplayEnd(),length:c,recordsTotal:a.fnRecordsTotal(),recordsDisplay:d,serverSide:"ssp"===y(a)}});p("page.len()",function(a){return a===k?0!==this.context.length?this.context[0]._iDisplayLength:k:this.iterator("table",function(b){Ta(b,a)})});var Vb=function(a,b,c){if(c){var d=new t(a);
+d.one("draw",function(){c(d.ajax.json())})}if("ssp"==y(a))T(a,b);else{C(a,!0);var e=a.jqXHR;e&&4!==e.readyState&&e.abort();ua(a,[],function(c){pa(a);for(var c=va(a,c),d=0,e=c.length;d<e;d++)N(a,c[d]);T(a,b);C(a,!1)})}};p("ajax.json()",function(){var a=this.context;if(0<a.length)return a[0].json});p("ajax.params()",function(){var a=this.context;if(0<a.length)return a[0].oAjaxData});p("ajax.reload()",function(a,b){return this.iterator("table",function(c){Vb(c,!1===b,a)})});p("ajax.url()",function(a){var b=
+this.context;if(a===k){if(0===b.length)return k;b=b[0];return b.ajax?h.isPlainObject(b.ajax)?b.ajax.url:b.ajax:b.sAjaxSource}return this.iterator("table",function(b){h.isPlainObject(b.ajax)?b.ajax.url=a:b.ajax=a})});p("ajax.url().load()",function(a,b){return this.iterator("table",function(c){Vb(c,!1===b,a)})});var bb=function(a,b,c,d,e){var f=[],g,j,i,n,l,m;i=typeof b;if(!b||"string"===i||"function"===i||b.length===k)b=[b];i=0;for(n=b.length;i<n;i++){j=b[i]&&b[i].split&&!b[i].match(/[\[\(:]/)?b[i].split(","):
+[b[i]];l=0;for(m=j.length;l<m;l++)(g=c("string"===typeof j[l]?h.trim(j[l]):j[l]))&&g.length&&(f=f.concat(g))}a=x.selector[a];if(a.length){i=0;for(n=a.length;i<n;i++)f=a[i](d,e,f)}return sa(f)},cb=function(a){a||(a={});a.filter&&a.search===k&&(a.search=a.filter);return h.extend({search:"none",order:"current",page:"all"},a)},db=function(a){for(var b=0,c=a.length;b<c;b++)if(0<a[b].length)return a[0]=a[b],a[0].length=1,a.length=1,a.context=[a.context[b]],a;a.length=0;return a},Da=function(a,b){var c,
+d,e,f=[],g=a.aiDisplay;c=a.aiDisplayMaster;var j=b.search;d=b.order;e=b.page;if("ssp"==y(a))return"removed"===j?[]:X(0,c.length);if("current"==e){c=a._iDisplayStart;for(d=a.fnDisplayEnd();c<d;c++)f.push(g[c])}else if("current"==d||"applied"==d)f="none"==j?c.slice():"applied"==j?g.slice():h.map(c,function(a){return-1===h.inArray(a,g)?a:null});else if("index"==d||"original"==d){c=0;for(d=a.aoData.length;c<d;c++)"none"==j?f.push(c):(e=h.inArray(c,g),(-1===e&&"removed"==j||0<=e&&"applied"==j)&&f.push(c))}return f};
+p("rows()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=cb(b),c=this.iterator("table",function(c){var e=b,f;return bb("row",a,function(a){var b=Qb(a);if(b!==null&&!e)return[b];f||(f=Da(c,e));if(b!==null&&h.inArray(b,f)!==-1)return[b];if(a===null||a===k||a==="")return f;if(typeof a==="function")return h.map(f,function(b){var e=c.aoData[b];return a(b,e._aData,e.nTr)?b:null});b=Tb(ja(c.aoData,f,"nTr"));if(a.nodeName){if(a._DT_RowIndex!==k)return[a._DT_RowIndex];if(a._DT_CellIndex)return[a._DT_CellIndex.row];
+b=h(a).closest("*[data-dt-row]");return b.length?[b.data("dt-row")]:[]}if(typeof a==="string"&&a.charAt(0)==="#"){var i=c.aIds[a.replace(/^#/,"")];if(i!==k)return[i.idx]}return h(b).filter(a).map(function(){return this._DT_RowIndex}).toArray()},c,e)},1);c.selector.rows=a;c.selector.opts=b;return c});p("rows().nodes()",function(){return this.iterator("row",function(a,b){return a.aoData[b].nTr||k},1)});p("rows().data()",function(){return this.iterator(!0,"rows",function(a,b){return ja(a.aoData,b,"_aData")},
+1)});u("rows().cache()","row().cache()",function(a){return this.iterator("row",function(b,c){var d=b.aoData[c];return"search"===a?d._aFilterData:d._aSortData},1)});u("rows().invalidate()","row().invalidate()",function(a){return this.iterator("row",function(b,c){da(b,c,a)})});u("rows().indexes()","row().index()",function(){return this.iterator("row",function(a,b){return b},1)});u("rows().ids()","row().id()",function(a){for(var b=[],c=this.context,d=0,e=c.length;d<e;d++)for(var f=0,g=this[d].length;f<
+g;f++){var h=c[d].rowIdFn(c[d].aoData[this[d][f]]._aData);b.push((!0===a?"#":"")+h)}return new t(c,b)});u("rows().remove()","row().remove()",function(){var a=this;this.iterator("row",function(b,c,d){var e=b.aoData,f=e[c],g,h,i,n,l;e.splice(c,1);g=0;for(h=e.length;g<h;g++)if(i=e[g],l=i.anCells,null!==i.nTr&&(i.nTr._DT_RowIndex=g),null!==l){i=0;for(n=l.length;i<n;i++)l[i]._DT_CellIndex.row=g}qa(b.aiDisplayMaster,c);qa(b.aiDisplay,c);qa(a[d],c,!1);Ua(b);c=b.rowIdFn(f._aData);c!==k&&delete b.aIds[c]});
+this.iterator("table",function(a){for(var c=0,d=a.aoData.length;c<d;c++)a.aoData[c].idx=c});return this});p("rows.add()",function(a){var b=this.iterator("table",function(b){var c,f,g,h=[];f=0;for(g=a.length;f<g;f++)c=a[f],c.nodeName&&"TR"===c.nodeName.toUpperCase()?h.push(oa(b,c)[0]):h.push(N(b,c));return h},1),c=this.rows(-1);c.pop();h.merge(c,b);return c});p("row()",function(a,b){return db(this.rows(a,b))});p("row().data()",function(a){var b=this.context;if(a===k)return b.length&&this.length?b[0].aoData[this[0]]._aData:
+k;b[0].aoData[this[0]]._aData=a;da(b[0],this[0],"data");return this});p("row().node()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]].nTr||null:null});p("row.add()",function(a){a instanceof h&&a.length&&(a=a[0]);var b=this.iterator("table",function(b){return a.nodeName&&"TR"===a.nodeName.toUpperCase()?oa(b,a)[0]:N(b,a)});return this.row(b[0])});var eb=function(a,b){var c=a.context;if(c.length&&(c=c[0].aoData[b!==k?b:a[0]])&&c._details)c._details.remove(),c._detailsShow=
+k,c._details=k},Wb=function(a,b){var c=a.context;if(c.length&&a.length){var d=c[0].aoData[a[0]];if(d._details){(d._detailsShow=b)?d._details.insertAfter(d.nTr):d._details.detach();var e=c[0],f=new t(e),g=e.aoData;f.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0<D(g,"_details").length&&(f.on("draw.dt.DT_details",function(a,b){e===b&&f.rows({page:"current"}).eq(0).each(function(a){a=g[a];a._detailsShow&&a._details.insertAfter(a.nTr)})}),f.on("column-visibility.dt.DT_details",
+function(a,b){if(e===b)for(var c,d=ba(b),f=0,h=g.length;f<h;f++)c=g[f],c._details&&c._details.children("td[colspan]").attr("colspan",d)}),f.on("destroy.dt.DT_details",function(a,b){if(e===b)for(var c=0,d=g.length;c<d;c++)g[c]._details&&eb(f,c)}))}}};p("row().child()",function(a,b){var c=this.context;if(a===k)return c.length&&this.length?c[0].aoData[this[0]]._details:k;if(!0===a)this.child.show();else if(!1===a)eb(this);else if(c.length&&this.length){var d=c[0],c=c[0].aoData[this[0]],e=[],f=function(a,
+b){if(h.isArray(a)||a instanceof h)for(var c=0,k=a.length;c<k;c++)f(a[c],b);else a.nodeName&&"tr"===a.nodeName.toLowerCase()?e.push(a):(c=h("<tr><td/></tr>").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=ba(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e);c._detailsShow&&c._details.insertAfter(c.nTr)}return this});p(["row().child.show()","row().child().show()"],function(){Wb(this,!0);return this});p(["row().child.hide()","row().child().hide()"],function(){Wb(this,!1);
+return this});p(["row().child.remove()","row().child().remove()"],function(){eb(this);return this});p("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var fc=/^([^:]+):(name|visIdx|visible)$/,Xb=function(a,b,c,d,e){for(var c=[],d=0,f=e.length;d<f;d++)c.push(B(a,e[d],b));return c};p("columns()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=cb(b),c=this.iterator("table",function(c){var e=a,f=b,g=c.aoColumns,
+j=D(g,"sName"),i=D(g,"nTh");return bb("column",e,function(a){var b=Qb(a);if(a==="")return X(g.length);if(b!==null)return[b>=0?b:g.length+b];if(typeof a==="function"){var e=Da(c,f);return h.map(g,function(b,f){return a(f,Xb(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(fc):"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[$(c,b)];case "name":return h.map(j,function(a,b){return a===
+k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)},1);c.selector.cols=a;c.selector.opts=b;return c});u("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});u("columns().footer()","column().footer()",
+function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});u("columns().data()","column().data()",function(){return this.iterator("column-rows",Xb,1)});u("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});u("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ja(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});u("columns().nodes()",
+"column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ja(a.aoData,e,"anCells",b)},1)});u("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData,i,n,l;if(a!==k&&g.bVisible!==a){if(a){var m=h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(n=j.length;i<n;i++)l=j[i].nTr,f=j[i].anCells,l&&l.insertBefore(f[c],f[m]||null)}else h(D(b.aoData,"anCells",c)).detach();
+g.bVisible=a;fa(b,b.aoHeader);fa(b,b.aoFooter);za(b)}});a!==k&&(this.iterator("column",function(c,e){s(c,null,"column-visibility",[c,e,a,b])}),(b===k||b)&&this.columns.adjust());return c});u("columns().indexes()","column().index()",function(a){return this.iterator("column",function(b,c){return"visible"===a?aa(b,c):c},1)});p("columns.adjust()",function(){return this.iterator("table",function(a){Z(a)},1)});p("column.index()",function(a,b){if(0!==this.context.length){var c=this.context[0];if("fromVisible"===
+a||"toData"===a)return $(c,b);if("fromData"===a||"toVisible"===a)return aa(c,b)}});p("column()",function(a,b){return db(this.columns(a,b))});p("cells()",function(a,b,c){h.isPlainObject(a)&&(a.row===k?(c=a,a=null):(c=b,b=null));h.isPlainObject(b)&&(c=b,b=null);if(null===b||b===k)return this.iterator("table",function(b){var d=a,e=cb(c),f=b.aoData,g=Da(b,e),j=Tb(ja(f,g,"anCells")),i=h([].concat.apply([],j)),l,n=b.aoColumns.length,m,p,u,t,s,v;return bb("cell",d,function(a){var c=typeof a==="function";
+if(a===null||a===k||c){m=[];p=0;for(u=g.length;p<u;p++){l=g[p];for(t=0;t<n;t++){s={row:l,column:t};if(c){v=f[l];a(s,B(b,l,t),v.anCells?v.anCells[t]:null)&&m.push(s)}else m.push(s)}}return m}if(h.isPlainObject(a))return[a];c=i.filter(a).map(function(a,b){return{row:b._DT_CellIndex.row,column:b._DT_CellIndex.column}}).toArray();if(c.length||!a.nodeName)return c;v=h(a).closest("*[data-dt-row]");return v.length?[{row:v.data("dt-row"),column:v.data("dt-column")}]:[]},b,e)});var d=this.columns(b,c),e=this.rows(a,
+c),f,g,j,i,n,l=this.iterator("table",function(a,b){f=[];g=0;for(j=e[b].length;g<j;g++){i=0;for(n=d[b].length;i<n;i++)f.push({row:e[b][g],column:d[b][i]})}return f},1);h.extend(l.selector,{cols:b,rows:a,opts:c});return l});u("cells().nodes()","cell().node()",function(){return this.iterator("cell",function(a,b,c){return(a=a.aoData[b])&&a.anCells?a.anCells[c]:k},1)});p("cells().data()",function(){return this.iterator("cell",function(a,b,c){return B(a,b,c)},1)});u("cells().cache()","cell().cache()",function(a){a=
+"search"===a?"_aFilterData":"_aSortData";return this.iterator("cell",function(b,c,d){return b.aoData[c][a][d]},1)});u("cells().render()","cell().render()",function(a){return this.iterator("cell",function(b,c,d){return B(b,c,d,a)},1)});u("cells().indexes()","cell().index()",function(){return this.iterator("cell",function(a,b,c){return{row:b,column:c,columnVisible:aa(a,c)}},1)});u("cells().invalidate()","cell().invalidate()",function(a){return this.iterator("cell",function(b,c,d){da(b,c,a,d)})});p("cell()",
+function(a,b,c){return db(this.cells(a,b,c))});p("cell().data()",function(a){var b=this.context,c=this[0];if(a===k)return b.length&&c.length?B(b[0],c[0].row,c[0].column):k;lb(b[0],c[0].row,c[0].column,a);da(b[0],c[0].row,"data",c[0].column);return this});p("order()",function(a,b){var c=this.context;if(a===k)return 0!==c.length?c[0].aaSorting:k;"number"===typeof a?a=[[a,b]]:a.length&&!h.isArray(a[0])&&(a=Array.prototype.slice.call(arguments));return this.iterator("table",function(b){b.aaSorting=a.slice()})});
+p("order.listener()",function(a,b,c){return this.iterator("table",function(d){Oa(d,a,b,c)})});p("order.fixed()",function(a){if(!a){var b=this.context,b=b.length?b[0].aaSortingFixed:k;return h.isArray(b)?{pre:b}:b}return this.iterator("table",function(b){b.aaSortingFixed=h.extend(!0,{},a)})});p(["columns().order()","column().order()"],function(a){var b=this;return this.iterator("table",function(c,d){var e=[];h.each(b[d],function(b,c){e.push([c,a])});c.aaSorting=e})});p("search()",function(a,b,c,d){var e=
+this.context;return a===k?0!==e.length?e[0].oPreviousSearch.sSearch:k:this.iterator("table",function(e){e.oFeatures.bFilter&&ga(e,h.extend({},e.oPreviousSearch,{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),1)})});u("columns().search()","column().search()",function(a,b,c,d){return this.iterator("column",function(e,f){var g=e.aoPreSearchCols;if(a===k)return g[f].sSearch;e.oFeatures.bFilter&&(h.extend(g[f],{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?
+!0:c,bCaseInsensitive:null===d?!0:d}),ga(e,e.oPreviousSearch,1))})});p("state()",function(){return this.context.length?this.context[0].oSavedState:null});p("state.clear()",function(){return this.iterator("table",function(a){a.fnStateSaveCallback.call(a.oInstance,a,{})})});p("state.loaded()",function(){return this.context.length?this.context[0].oLoadedState:null});p("state.save()",function(){return this.iterator("table",function(a){za(a)})});m.versionCheck=m.fnVersionCheck=function(a){for(var b=m.version.split("."),
+a=a.split("."),c,d,e=0,f=a.length;e<f;e++)if(c=parseInt(b[e],10)||0,d=parseInt(a[e],10)||0,c!==d)return c>d;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof m.Api)return!0;h.each(m.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(m.settings,
+function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new t(c):c};m.camelToHungarian=J;p("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){p(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});p("clear()",function(){return this.iterator("table",
+function(a){pa(a)})});p("settings()",function(){return new t(this.context,this.context)});p("init()",function(){var a=this.context;return a.length?a[0].oInit:null});p("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});p("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),
+p;b.bDestroying=!0;s(b,"aoDestroyCallback","destroy",[b]);a||(new t(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT");h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));b.aaSorting=[];b.aaSortingFixed=[];ya(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);b.bJUI&&(h("th span."+d.sSortIcon+
+", td span."+d.sSortIcon,g).detach(),h("th, td",g).each(function(){var a=h("div."+d.sSortJUIWrapper,this);h(this).append(a.contents());a.detach()}));f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%p])}));c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c,1)})});h.each(["column",
+"row","cell"],function(a,b){p(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,m){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,m)})})});p("i18n()",function(a,b,c){var d=this.context[0],a=R(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.15";m.settings=[];m.models={};m.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow={nTr:null,anCells:null,
+_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};m.defaults=
+{aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,
+this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+
+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",
+sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};
+Y(m.defaults);m.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};Y(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,
+bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],
+aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,
+aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=
+this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};m.ext=x={buttons:{},classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},
+header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",
+sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",
+sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Ea="",Ea="",G=Ea+"ui-state-default",ka=Ea+"css_right ui-icon ui-icon-",Yb=Ea+"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix";h.extend(m.ext.oJUIClasses,
+m.ext.classes,{sPageButton:"fg-button ui-button "+G,sPageButtonActive:"ui-state-disabled",sPageButtonDisabled:"ui-state-disabled",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",sSortAsc:G+" sorting_asc",sSortDesc:G+" sorting_desc",sSortable:G+" sorting",sSortableAsc:G+" sorting_asc_disabled",sSortableDesc:G+" sorting_desc_disabled",sSortableNone:G+" sorting_disabled",sSortJUIAsc:ka+"triangle-1-n",sSortJUIDesc:ka+"triangle-1-s",sSortJUI:ka+"carat-2-n-s",
+sSortJUIAscAllowed:ka+"carat-1-n",sSortJUIDescAllowed:ka+"carat-1-s",sSortJUIWrapper:"DataTables_sort_wrapper",sSortIcon:"DataTables_sort_icon",sScrollHead:"dataTables_scrollHead "+G,sScrollFoot:"dataTables_scrollFoot "+G,sHeaderTH:G,sFooterTH:G,sJUIHeader:Yb+" ui-corner-tl ui-corner-tr",sJUIFooter:Yb+" ui-corner-bl ui-corner-br"});var Nb=m.ext.pager;h.extend(Nb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ia(a,
+b)]},simple_numbers:function(a,b){return["previous",ia(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ia(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ia(a,b),"last"]},_numbers:ia,numbers_length:7});h.extend(!0,m.ext.renderer,{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},m,l,p=0,r=function(b,d){var k,t,u,s,v=function(b){Va(a,b.data.action,true)};k=0;for(t=d.length;k<t;k++){s=d[k];if(h.isArray(s)){u=
+h("<"+(s.DT_el||"div")+"/>").appendTo(b);r(u,s)}else{m=null;l="";switch(s){case "ellipsis":b.append('<span class="ellipsis">&#x2026;</span>');break;case "first":m=j.sFirst;l=s+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":m=j.sPrevious;l=s+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":m=j.sNext;l=s+(e<f-1?"":" "+g.sPageButtonDisabled);break;case "last":m=j.sLast;l=s+(e<f-1?"":" "+g.sPageButtonDisabled);break;default:m=s+1;l=e===s?g.sPageButtonActive:""}if(m!==null){u=h("<a>",{"class":g.sPageButton+
+" "+l,"aria-controls":a.sTableId,"aria-label":i[s],"data-dt-idx":p,tabindex:a.iTabIndex,id:c===0&&typeof s==="string"?a.sTableId+"_"+s:null}).html(m).appendTo(b);Ya(u,{action:s},v);p++}}}},t;try{t=h(b).find(H.activeElement).data("dt-idx")}catch(u){}r(h(b).empty(),d);t!==k&&h(b).find("[data-dt-idx="+t+"]").focus()}}});h.extend(m.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return ab(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!cc.test(a))return null;var b=Date.parse(a);
+return null!==b&&!isNaN(b)||M(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return ab(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Sb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Sb(a,c,!0)?"html-num-fmt"+c:null},function(a){return M(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(m.ext.type.search,{html:function(a){return M(a)?a:"string"===typeof a?a.replace(Pb," ").replace(Ca,""):""},string:function(a){return M(a)?
+a:"string"===typeof a?a.replace(Pb," "):a}});var Ba=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Rb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){return Date.parse(a)||-Infinity},"html-pre":function(a){return M(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return M(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return a<
+b?-1:a>b?1:0},"string-desc":function(a,b){return a<b?1:a>b?-1:0}});fb("");h.extend(!0,m.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("<div/>").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("<span/>").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);
+h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var Zb=function(a){return"string"===typeof a?a.replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,
+"&quot;"):a};m.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return Zb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}},text:function(){return{display:Zb}}};h.extend(m.ext.internal,{_fnExternApiFunc:Ob,_fnBuildAjax:ua,_fnAjaxUpdate:nb,_fnAjaxParameters:wb,_fnAjaxUpdateDraw:xb,
+_fnAjaxDataSrc:va,_fnAddColumn:Ga,_fnColumnOptions:la,_fnAdjustColumnSizing:Z,_fnVisibleToColumnIndex:$,_fnColumnIndexToVisible:aa,_fnVisbleColumns:ba,_fnGetColumns:na,_fnColumnTypes:Ia,_fnApplyColumnDefs:kb,_fnHungarianMap:Y,_fnCamelToHungarian:J,_fnLanguageCompat:Fa,_fnBrowserDetect:ib,_fnAddData:N,_fnAddTr:oa,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:lb,
+_fnSplitObjNotation:La,_fnGetObjectDataFn:R,_fnSetObjectDataFn:S,_fnGetDataMaster:Ma,_fnClearTable:pa,_fnDeleteIndex:qa,_fnInvalidate:da,_fnGetRowElements:Ka,_fnCreateTr:Ja,_fnBuildHead:mb,_fnDrawHead:fa,_fnDraw:O,_fnReDraw:T,_fnAddOptionsHtml:pb,_fnDetectHeader:ea,_fnGetUniqueThs:ta,_fnFeatureHtmlFilter:rb,_fnFilterComplete:ga,_fnFilterCustom:Ab,_fnFilterColumn:zb,_fnFilter:yb,_fnFilterCreateSearch:Ra,_fnEscapeRegex:Sa,_fnFilterData:Bb,_fnFeatureHtmlInfo:ub,_fnUpdateInfo:Eb,_fnInfoMacros:Fb,_fnInitialise:ha,
+_fnInitComplete:wa,_fnLengthChange:Ta,_fnFeatureHtmlLength:qb,_fnFeatureHtmlPaginate:vb,_fnPageChange:Va,_fnFeatureHtmlProcessing:sb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:tb,_fnScrollDraw:ma,_fnApplyToChildren:I,_fnCalculateColumnWidths:Ha,_fnThrottle:Qa,_fnConvertToWidth:Gb,_fnGetWidestNode:Hb,_fnGetMaxLenString:Ib,_fnStringToCss:v,_fnSortFlatten:W,_fnSort:ob,_fnSortAria:Kb,_fnSortListener:Xa,_fnSortAttachListener:Oa,_fnSortingClasses:ya,_fnSortData:Jb,_fnSaveState:za,_fnLoadState:Lb,_fnSettingsFromNode:Aa,
+_fnLog:K,_fnMap:F,_fnBindAction:Ya,_fnCallbackReg:z,_fnCallbackFire:s,_fnLengthOverflow:Ua,_fnRenderer:Pa,_fnDataSource:y,_fnRowAttributes:Na,_fnCalculateEnd:function(){}});h.fn.dataTable=m;m.$=h;h.fn.dataTableSettings=m.settings;h.fn.dataTableExt=m.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(m,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable});
diff --git a/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.bootstrap.css b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.bootstrap.css
new file mode 100644
index 0000000000000000000000000000000000000000..28ee2abe6d07994b9dd45409d7ee6234ebba68be
--- /dev/null
+++ b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.bootstrap.css
@@ -0,0 +1,44 @@
+table.DTFC_Cloned tr {
+  background-color: white;
+  margin-bottom: 0;
+}
+
+div.DTFC_LeftHeadWrapper table,
+div.DTFC_RightHeadWrapper table {
+  border-bottom: none !important;
+  margin-bottom: 0 !important;
+  background-color: white;
+}
+
+div.DTFC_LeftBodyWrapper table,
+div.DTFC_RightBodyWrapper table {
+  border-top: none;
+  margin: 0 !important;
+}
+div.DTFC_LeftBodyWrapper table thead .sorting:after,
+div.DTFC_LeftBodyWrapper table thead .sorting_asc:after,
+div.DTFC_LeftBodyWrapper table thead .sorting_desc:after,
+div.DTFC_LeftBodyWrapper table thead .sorting:after,
+div.DTFC_LeftBodyWrapper table thead .sorting_asc:after,
+div.DTFC_LeftBodyWrapper table thead .sorting_desc:after,
+div.DTFC_RightBodyWrapper table thead .sorting:after,
+div.DTFC_RightBodyWrapper table thead .sorting_asc:after,
+div.DTFC_RightBodyWrapper table thead .sorting_desc:after,
+div.DTFC_RightBodyWrapper table thead .sorting:after,
+div.DTFC_RightBodyWrapper table thead .sorting_asc:after,
+div.DTFC_RightBodyWrapper table thead .sorting_desc:after {
+  display: none;
+}
+div.DTFC_LeftBodyWrapper table tbody tr:first-child th,
+div.DTFC_LeftBodyWrapper table tbody tr:first-child td,
+div.DTFC_RightBodyWrapper table tbody tr:first-child th,
+div.DTFC_RightBodyWrapper table tbody tr:first-child td {
+  border-top: none;
+}
+
+div.DTFC_LeftFootWrapper table,
+div.DTFC_RightFootWrapper table {
+  border-top: none;
+  margin-top: 0 !important;
+  background-color: white;
+}
diff --git a/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.bootstrap.min.css b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.bootstrap.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..d85ab4df775349d3b6540d85aac77ea2dad6abe0
--- /dev/null
+++ b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.bootstrap.min.css
@@ -0,0 +1 @@
+table.DTFC_Cloned tr{background-color:white;margin-bottom:0}div.DTFC_LeftHeadWrapper table,div.DTFC_RightHeadWrapper table{border-bottom:none !important;margin-bottom:0 !important;background-color:white}div.DTFC_LeftBodyWrapper table,div.DTFC_RightBodyWrapper table{border-top:none;margin:0 !important}div.DTFC_LeftBodyWrapper table thead .sorting:after,div.DTFC_LeftBodyWrapper table thead .sorting_asc:after,div.DTFC_LeftBodyWrapper table thead .sorting_desc:after,div.DTFC_LeftBodyWrapper table thead .sorting:after,div.DTFC_LeftBodyWrapper table thead .sorting_asc:after,div.DTFC_LeftBodyWrapper table thead .sorting_desc:after,div.DTFC_RightBodyWrapper table thead .sorting:after,div.DTFC_RightBodyWrapper table thead .sorting_asc:after,div.DTFC_RightBodyWrapper table thead .sorting_desc:after,div.DTFC_RightBodyWrapper table thead .sorting:after,div.DTFC_RightBodyWrapper table thead .sorting_asc:after,div.DTFC_RightBodyWrapper table thead .sorting_desc:after{display:none}div.DTFC_LeftBodyWrapper table tbody tr:first-child th,div.DTFC_LeftBodyWrapper table tbody tr:first-child td,div.DTFC_RightBodyWrapper table tbody tr:first-child th,div.DTFC_RightBodyWrapper table tbody tr:first-child td{border-top:none}div.DTFC_LeftFootWrapper table,div.DTFC_RightFootWrapper table{border-top:none;margin-top:0 !important;background-color:white}
diff --git a/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.dataTables.css b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.dataTables.css
new file mode 100644
index 0000000000000000000000000000000000000000..9b94ffa1334b918e1322cb8a5e0b13f6649caf18
--- /dev/null
+++ b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.dataTables.css
@@ -0,0 +1,18 @@
+table.DTFC_Cloned thead,
+table.DTFC_Cloned tfoot {
+  background-color: white;
+}
+
+div.DTFC_Blocker {
+  background-color: white;
+}
+
+div.DTFC_LeftWrapper table.dataTable,
+div.DTFC_RightWrapper table.dataTable {
+  margin-bottom: 0;
+  z-index: 2;
+}
+div.DTFC_LeftWrapper table.dataTable.no-footer,
+div.DTFC_RightWrapper table.dataTable.no-footer {
+  border-bottom: none;
+}
diff --git a/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.dataTables.min.css b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.dataTables.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..71e801b53e7d693d1e00f3e58779e499b11c2ed9
--- /dev/null
+++ b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.dataTables.min.css
@@ -0,0 +1 @@
+table.DTFC_Cloned thead,table.DTFC_Cloned tfoot{background-color:white}div.DTFC_Blocker{background-color:white}div.DTFC_LeftWrapper table.dataTable,div.DTFC_RightWrapper table.dataTable{margin-bottom:0;z-index:2}div.DTFC_LeftWrapper table.dataTable.no-footer,div.DTFC_RightWrapper table.dataTable.no-footer{border-bottom:none}
diff --git a/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.foundation.css b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.foundation.css
new file mode 100644
index 0000000000000000000000000000000000000000..fc717eef507b39454187da1f84595823675b6fcf
--- /dev/null
+++ b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.foundation.css
@@ -0,0 +1,27 @@
+div.DTFC_LeftHeadWrapper table,
+div.DTFC_LeftBodyWrapper table,
+div.DTFC_LeftFootWrapper table {
+  border-right-width: 0;
+}
+
+div.DTFC_RightHeadWrapper table,
+div.DTFC_RightBodyWrapper table,
+div.DTFC_RightFootWrapper table {
+  border-left-width: 0;
+}
+
+div.DTFC_LeftHeadWrapper table,
+div.DTFC_RightHeadWrapper table {
+  margin-bottom: 0 !important;
+}
+
+div.DTFC_LeftBodyWrapper table,
+div.DTFC_RightBodyWrapper table {
+  border-top: none;
+  margin: 0 !important;
+}
+
+div.DTFC_LeftFootWrapper table,
+div.DTFC_RightFootWrapper table {
+  margin-top: 0 !important;
+}
diff --git a/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.foundation.min.css b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.foundation.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..c9937f8186795bab7d1d6fc5068068ffe1f6644e
--- /dev/null
+++ b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.foundation.min.css
@@ -0,0 +1 @@
+div.DTFC_LeftHeadWrapper table,div.DTFC_LeftBodyWrapper table,div.DTFC_LeftFootWrapper table{border-right-width:0}div.DTFC_RightHeadWrapper table,div.DTFC_RightBodyWrapper table,div.DTFC_RightFootWrapper table{border-left-width:0}div.DTFC_LeftHeadWrapper table,div.DTFC_RightHeadWrapper table{margin-bottom:0 !important}div.DTFC_LeftBodyWrapper table,div.DTFC_RightBodyWrapper table{border-top:none;margin:0 !important}div.DTFC_LeftFootWrapper table,div.DTFC_RightFootWrapper table{margin-top:0 !important}
diff --git a/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.jqueryui.css b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.jqueryui.css
new file mode 100644
index 0000000000000000000000000000000000000000..d6a7ec9c78db2639df771a77aa7848e24552830f
--- /dev/null
+++ b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.jqueryui.css
@@ -0,0 +1,8 @@
+div.DTFC_LeftWrapper table.dataTable,
+div.DTFC_RightWrapper table.dataTable {
+  z-index: 2;
+}
+div.DTFC_LeftWrapper table.dataTable.no-footer,
+div.DTFC_RightWrapper table.dataTable.no-footer {
+  border-bottom: none;
+}
diff --git a/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.jqueryui.min.css b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.jqueryui.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..9926742c8431102f67e354e893334c4e59dd4447
--- /dev/null
+++ b/static/DataTables/FixedColumns-3.2.2/css/fixedColumns.jqueryui.min.css
@@ -0,0 +1 @@
+div.DTFC_LeftWrapper table.dataTable,div.DTFC_RightWrapper table.dataTable{z-index:2}div.DTFC_LeftWrapper table.dataTable.no-footer,div.DTFC_RightWrapper table.dataTable.no-footer{border-bottom:none}
diff --git a/static/DataTables/FixedColumns-3.2.2/js/dataTables.fixedColumns.js b/static/DataTables/FixedColumns-3.2.2/js/dataTables.fixedColumns.js
new file mode 100644
index 0000000000000000000000000000000000000000..0ac001bfddb8188c6f5ac060559e4c0381cc7924
--- /dev/null
+++ b/static/DataTables/FixedColumns-3.2.2/js/dataTables.fixedColumns.js
@@ -0,0 +1,1623 @@
+/*! FixedColumns 3.2.2
+ * ©2010-2016 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary     FixedColumns
+ * @description Freeze columns in place on a scrolling DataTable
+ * @version     3.2.2
+ * @file        dataTables.fixedColumns.js
+ * @author      SpryMedia Ltd (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ * @copyright   Copyright 2010-2016 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ *   MIT license - http://datatables.net/license/mit
+ *
+ * This source file 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 license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+var _firefoxScroll;
+
+/**
+ * When making use of DataTables' x-axis scrolling feature, you may wish to
+ * fix the left most column in place. This plug-in for DataTables provides
+ * exactly this option (note for non-scrolling tables, please use the
+ * FixedHeader plug-in, which can fix headers, footers and columns). Key
+ * features include:
+ *
+ * * Freezes the left or right most columns to the side of the table
+ * * Option to freeze two or more columns
+ * * Full integration with DataTables' scrolling options
+ * * Speed - FixedColumns is fast in its operation
+ *
+ *  @class
+ *  @constructor
+ *  @global
+ *  @param {object} dt DataTables instance. With DataTables 1.10 this can also
+ *    be a jQuery collection, a jQuery selector, DataTables API instance or
+ *    settings object.
+ *  @param {object} [init={}] Configuration object for FixedColumns. Options are
+ *    defined by {@link FixedColumns.defaults}
+ *
+ *  @requires jQuery 1.7+
+ *  @requires DataTables 1.8.0+
+ *
+ *  @example
+ *      var table = $('#example').dataTable( {
+ *        "scrollX": "100%"
+ *      } );
+ *      new $.fn.dataTable.fixedColumns( table );
+ */
+var FixedColumns = function ( dt, init ) {
+	var that = this;
+
+	/* Sanity check - you just know it will happen */
+	if ( ! ( this instanceof FixedColumns ) ) {
+		alert( "FixedColumns warning: FixedColumns must be initialised with the 'new' keyword." );
+		return;
+	}
+
+	if ( init === undefined || init === true ) {
+		init = {};
+	}
+
+	// Use the DataTables Hungarian notation mapping method, if it exists to
+	// provide forwards compatibility for camel case variables
+	var camelToHungarian = $.fn.dataTable.camelToHungarian;
+	if ( camelToHungarian ) {
+		camelToHungarian( FixedColumns.defaults, FixedColumns.defaults, true );
+		camelToHungarian( FixedColumns.defaults, init );
+	}
+
+	// v1.10 allows the settings object to be got form a number of sources
+	var dtSettings = new $.fn.dataTable.Api( dt ).settings()[0];
+
+	/**
+	 * Settings object which contains customisable information for FixedColumns instance
+	 * @namespace
+	 * @extends FixedColumns.defaults
+	 * @private
+	 */
+	this.s = {
+		/**
+		 * DataTables settings objects
+		 *  @type     object
+		 *  @default  Obtained from DataTables instance
+		 */
+		"dt": dtSettings,
+
+		/**
+		 * Number of columns in the DataTable - stored for quick access
+		 *  @type     int
+		 *  @default  Obtained from DataTables instance
+		 */
+		"iTableColumns": dtSettings.aoColumns.length,
+
+		/**
+		 * Original outer widths of the columns as rendered by DataTables - used to calculate
+		 * the FixedColumns grid bounding box
+		 *  @type     array.<int>
+		 *  @default  []
+		 */
+		"aiOuterWidths": [],
+
+		/**
+		 * Original inner widths of the columns as rendered by DataTables - used to apply widths
+		 * to the columns
+		 *  @type     array.<int>
+		 *  @default  []
+		 */
+		"aiInnerWidths": [],
+
+
+		/**
+		 * Is the document layout right-to-left
+		 * @type boolean
+		 */
+		rtl: $(dtSettings.nTable).css('direction') === 'rtl'
+	};
+
+
+	/**
+	 * DOM elements used by the class instance
+	 * @namespace
+	 * @private
+	 *
+	 */
+	this.dom = {
+		/**
+		 * DataTables scrolling element
+		 *  @type     node
+		 *  @default  null
+		 */
+		"scroller": null,
+
+		/**
+		 * DataTables header table
+		 *  @type     node
+		 *  @default  null
+		 */
+		"header": null,
+
+		/**
+		 * DataTables body table
+		 *  @type     node
+		 *  @default  null
+		 */
+		"body": null,
+
+		/**
+		 * DataTables footer table
+		 *  @type     node
+		 *  @default  null
+		 */
+		"footer": null,
+
+		/**
+		 * Display grid elements
+		 * @namespace
+		 */
+		"grid": {
+			/**
+			 * Grid wrapper. This is the container element for the 3x3 grid
+			 *  @type     node
+			 *  @default  null
+			 */
+			"wrapper": null,
+
+			/**
+			 * DataTables scrolling element. This element is the DataTables
+			 * component in the display grid (making up the main table - i.e.
+			 * not the fixed columns).
+			 *  @type     node
+			 *  @default  null
+			 */
+			"dt": null,
+
+			/**
+			 * Left fixed column grid components
+			 * @namespace
+			 */
+			"left": {
+				"wrapper": null,
+				"head": null,
+				"body": null,
+				"foot": null
+			},
+
+			/**
+			 * Right fixed column grid components
+			 * @namespace
+			 */
+			"right": {
+				"wrapper": null,
+				"head": null,
+				"body": null,
+				"foot": null
+			}
+		},
+
+		/**
+		 * Cloned table nodes
+		 * @namespace
+		 */
+		"clone": {
+			/**
+			 * Left column cloned table nodes
+			 * @namespace
+			 */
+			"left": {
+				/**
+				 * Cloned header table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"header": null,
+
+				/**
+				 * Cloned body table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"body": null,
+
+				/**
+				 * Cloned footer table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"footer": null
+			},
+
+			/**
+			 * Right column cloned table nodes
+			 * @namespace
+			 */
+			"right": {
+				/**
+				 * Cloned header table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"header": null,
+
+				/**
+				 * Cloned body table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"body": null,
+
+				/**
+				 * Cloned footer table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"footer": null
+			}
+		}
+	};
+
+	if ( dtSettings._oFixedColumns ) {
+		throw 'FixedColumns already initialised on this table';
+	}
+
+	/* Attach the instance to the DataTables instance so it can be accessed easily */
+	dtSettings._oFixedColumns = this;
+
+	/* Let's do it */
+	if ( ! dtSettings._bInitComplete )
+	{
+		dtSettings.oApi._fnCallbackReg( dtSettings, 'aoInitComplete', function () {
+			that._fnConstruct( init );
+		}, 'FixedColumns' );
+	}
+	else
+	{
+		this._fnConstruct( init );
+	}
+};
+
+
+
+$.extend( FixedColumns.prototype , {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public methods
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+	/**
+	 * Update the fixed columns - including headers and footers. Note that FixedColumns will
+	 * automatically update the display whenever the host DataTable redraws.
+	 *  @returns {void}
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      var fc = new $.fn.dataTable.fixedColumns( table );
+	 *
+	 *      // at some later point when the table has been manipulated....
+	 *      fc.fnUpdate();
+	 */
+	"fnUpdate": function ()
+	{
+		this._fnDraw( true );
+	},
+
+
+	/**
+	 * Recalculate the resizes of the 3x3 grid that FixedColumns uses for display of the table.
+	 * This is useful if you update the width of the table container. Note that FixedColumns will
+	 * perform this function automatically when the window.resize event is fired.
+	 *  @returns {void}
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      var fc = new $.fn.dataTable.fixedColumns( table );
+	 *
+	 *      // Resize the table container and then have FixedColumns adjust its layout....
+	 *      $('#content').width( 1200 );
+	 *      fc.fnRedrawLayout();
+	 */
+	"fnRedrawLayout": function ()
+	{
+		this._fnColCalc();
+		this._fnGridLayout();
+		this.fnUpdate();
+	},
+
+
+	/**
+	 * Mark a row such that it's height should be recalculated when using 'semiauto' row
+	 * height matching. This function will have no effect when 'none' or 'auto' row height
+	 * matching is used.
+	 *  @param   {Node} nTr TR element that should have it's height recalculated
+	 *  @returns {void}
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      var fc = new $.fn.dataTable.fixedColumns( table );
+	 *
+	 *      // manipulate the table - mark the row as needing an update then update the table
+	 *      // this allows the redraw performed by DataTables fnUpdate to recalculate the row
+	 *      // height
+	 *      fc.fnRecalculateHeight();
+	 *      table.fnUpdate( $('#example tbody tr:eq(0)')[0], ["insert date", 1, 2, 3 ... ]);
+	 */
+	"fnRecalculateHeight": function ( nTr )
+	{
+		delete nTr._DTTC_iHeight;
+		nTr.style.height = 'auto';
+	},
+
+
+	/**
+	 * Set the height of a given row - provides cross browser compatibility
+	 *  @param   {Node} nTarget TR element that should have it's height recalculated
+	 *  @param   {int} iHeight Height in pixels to set
+	 *  @returns {void}
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      var fc = new $.fn.dataTable.fixedColumns( table );
+	 *
+	 *      // You may want to do this after manipulating a row in the fixed column
+	 *      fc.fnSetRowHeight( $('#example tbody tr:eq(0)')[0], 50 );
+	 */
+	"fnSetRowHeight": function ( nTarget, iHeight )
+	{
+		nTarget.style.height = iHeight+"px";
+	},
+
+
+	/**
+	 * Get data index information about a row or cell in the table body.
+	 * This function is functionally identical to fnGetPosition in DataTables,
+	 * taking the same parameter (TH, TD or TR node) and returning exactly the
+	 * the same information (data index information). THe difference between
+	 * the two is that this method takes into account the fixed columns in the
+	 * table, so you can pass in nodes from the master table, or the cloned
+	 * tables and get the index position for the data in the main table.
+	 *  @param {node} node TR, TH or TD element to get the information about
+	 *  @returns {int} If nNode is given as a TR, then a single index is 
+	 *    returned, or if given as a cell, an array of [row index, column index
+	 *    (visible), column index (all)] is given.
+	 */
+	"fnGetPosition": function ( node )
+	{
+		var idx;
+		var inst = this.s.dt.oInstance;
+
+		if ( ! $(node).parents('.DTFC_Cloned').length )
+		{
+			// Not in a cloned table
+			return inst.fnGetPosition( node );
+		}
+		else
+		{
+			// Its in the cloned table, so need to look up position
+			if ( node.nodeName.toLowerCase() === 'tr' ) {
+				idx = $(node).index();
+				return inst.fnGetPosition( $('tr', this.s.dt.nTBody)[ idx ] );
+			}
+			else
+			{
+				var colIdx = $(node).index();
+				idx = $(node.parentNode).index();
+				var row = inst.fnGetPosition( $('tr', this.s.dt.nTBody)[ idx ] );
+
+				return [
+					row,
+					colIdx,
+					inst.oApi._fnVisibleToColumnIndex( this.s.dt, colIdx )
+				];
+			}
+		}
+	},
+
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods (they are of course public in JS, but recommended as private)
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+	/**
+	 * Initialisation for FixedColumns
+	 *  @param   {Object} oInit User settings for initialisation
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnConstruct": function ( oInit )
+	{
+		var i, iLen, iWidth,
+			that = this;
+
+		/* Sanity checking */
+		if ( typeof this.s.dt.oInstance.fnVersionCheck != 'function' ||
+		     this.s.dt.oInstance.fnVersionCheck( '1.8.0' ) !== true )
+		{
+			alert( "FixedColumns "+FixedColumns.VERSION+" required DataTables 1.8.0 or later. "+
+				"Please upgrade your DataTables installation" );
+			return;
+		}
+
+		if ( this.s.dt.oScroll.sX === "" )
+		{
+			this.s.dt.oInstance.oApi._fnLog( this.s.dt, 1, "FixedColumns is not needed (no "+
+				"x-scrolling in DataTables enabled), so no action will be taken. Use 'FixedHeader' for "+
+				"column fixing when scrolling is not enabled" );
+			return;
+		}
+
+		/* Apply the settings from the user / defaults */
+		this.s = $.extend( true, this.s, FixedColumns.defaults, oInit );
+
+		/* Set up the DOM as we need it and cache nodes */
+		var classes = this.s.dt.oClasses;
+		this.dom.grid.dt = $(this.s.dt.nTable).parents('div.'+classes.sScrollWrapper)[0];
+		this.dom.scroller = $('div.'+classes.sScrollBody, this.dom.grid.dt )[0];
+
+		/* Set up the DOM that we want for the fixed column layout grid */
+		this._fnColCalc();
+		this._fnGridSetup();
+
+		/* Event handlers */
+		var mouseController;
+		var mouseDown = false;
+
+		// When the mouse is down (drag scroll) the mouse controller cannot
+		// change, as the browser keeps the original element as the scrolling one
+		$(this.s.dt.nTableWrapper).on( 'mousedown.DTFC', function () {
+			mouseDown = true;
+
+			$(document).one( 'mouseup', function () {
+				mouseDown = false;
+			} );
+		} );
+
+		// When the body is scrolled - scroll the left and right columns
+		$(this.dom.scroller)
+			.on( 'mouseover.DTFC touchstart.DTFC', function () {
+				if ( ! mouseDown ) {
+					mouseController = 'main';
+				}
+			} )
+			.on( 'scroll.DTFC', function (e) {
+				if ( ! mouseController && e.originalEvent ) {
+					mouseController = 'main';
+				}
+
+				if ( mouseController === 'main' ) {
+					if ( that.s.iLeftColumns > 0 ) {
+						that.dom.grid.left.liner.scrollTop = that.dom.scroller.scrollTop;
+					}
+					if ( that.s.iRightColumns > 0 ) {
+						that.dom.grid.right.liner.scrollTop = that.dom.scroller.scrollTop;
+					}
+				}
+			} );
+
+		var wheelType = 'onwheel' in document.createElement('div') ?
+			'wheel.DTFC' :
+			'mousewheel.DTFC';
+
+		if ( that.s.iLeftColumns > 0 ) {
+			// When scrolling the left column, scroll the body and right column
+			$(that.dom.grid.left.liner)
+				.on( 'mouseover.DTFC touchstart.DTFC', function () {
+					if ( ! mouseDown ) {
+						mouseController = 'left';
+					}
+				} )
+				.on( 'scroll.DTFC', function ( e ) {
+					if ( ! mouseController && e.originalEvent ) {
+						mouseController = 'left';
+					}
+
+					if ( mouseController === 'left' ) {
+						that.dom.scroller.scrollTop = that.dom.grid.left.liner.scrollTop;
+						if ( that.s.iRightColumns > 0 ) {
+							that.dom.grid.right.liner.scrollTop = that.dom.grid.left.liner.scrollTop;
+						}
+					}
+				} )
+				.on( wheelType, function(e) {
+					// Pass horizontal scrolling through
+					var xDelta = e.type === 'wheel' ?
+						-e.originalEvent.deltaX :
+						e.originalEvent.wheelDeltaX;
+					that.dom.scroller.scrollLeft -= xDelta;
+				} );
+		}
+
+		if ( that.s.iRightColumns > 0 ) {
+			// When scrolling the right column, scroll the body and the left column
+			$(that.dom.grid.right.liner)
+				.on( 'mouseover.DTFC touchstart.DTFC', function () {
+					if ( ! mouseDown ) {
+						mouseController = 'right';
+					}
+				} )
+				.on( 'scroll.DTFC', function ( e ) {
+					if ( ! mouseController && e.originalEvent ) {
+						mouseController = 'right';
+					}
+
+					if ( mouseController === 'right' ) {
+						that.dom.scroller.scrollTop = that.dom.grid.right.liner.scrollTop;
+						if ( that.s.iLeftColumns > 0 ) {
+							that.dom.grid.left.liner.scrollTop = that.dom.grid.right.liner.scrollTop;
+						}
+					}
+				} )
+				.on( wheelType, function(e) {
+					// Pass horizontal scrolling through
+					var xDelta = e.type === 'wheel' ?
+						-e.originalEvent.deltaX :
+						e.originalEvent.wheelDeltaX;
+					that.dom.scroller.scrollLeft -= xDelta;
+				} );
+		}
+
+		$(window).on( 'resize.DTFC', function () {
+			that._fnGridLayout.call( that );
+		} );
+
+		var bFirstDraw = true;
+		var jqTable = $(this.s.dt.nTable);
+
+		jqTable
+			.on( 'draw.dt.DTFC', function () {
+				that._fnColCalc();
+				that._fnDraw.call( that, bFirstDraw );
+				bFirstDraw = false;
+			} )
+			.on( 'column-sizing.dt.DTFC', function () {
+				that._fnColCalc();
+				that._fnGridLayout( that );
+			} )
+			.on( 'column-visibility.dt.DTFC', function ( e, settings, column, vis, recalc ) {
+				if ( recalc === undefined || recalc ) {
+					that._fnColCalc();
+					that._fnGridLayout( that );
+					that._fnDraw( true );
+				}
+			} )
+			.on( 'select.dt.DTFC deselect.dt.DTFC', function ( e, dt, type, indexes ) {
+				if ( e.namespace === 'dt' ) {
+					that._fnDraw( false );
+				}
+			} )
+			.on( 'destroy.dt.DTFC', function () {
+				jqTable.off( '.DTFC' );
+
+				$(that.dom.scroller).off( '.DTFC' );
+				$(window).off( '.DTFC' );
+				$(that.s.dt.nTableWrapper).off( '.DTFC' );
+
+				$(that.dom.grid.left.liner).off( '.DTFC '+wheelType );
+				$(that.dom.grid.left.wrapper).remove();
+
+				$(that.dom.grid.right.liner).off( '.DTFC '+wheelType );
+				$(that.dom.grid.right.wrapper).remove();
+			} );
+
+		/* Get things right to start with - note that due to adjusting the columns, there must be
+		 * another redraw of the main table. It doesn't need to be a full redraw however.
+		 */
+		this._fnGridLayout();
+		this.s.dt.oInstance.fnDraw(false);
+	},
+
+
+	/**
+	 * Calculate the column widths for the grid layout
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnColCalc": function ()
+	{
+		var that = this;
+		var iLeftWidth = 0;
+		var iRightWidth = 0;
+
+		this.s.aiInnerWidths = [];
+		this.s.aiOuterWidths = [];
+
+		$.each( this.s.dt.aoColumns, function (i, col) {
+			var th = $(col.nTh);
+			var border;
+
+			if ( ! th.filter(':visible').length ) {
+				that.s.aiInnerWidths.push( 0 );
+				that.s.aiOuterWidths.push( 0 );
+			}
+			else
+			{
+				// Inner width is used to assign widths to cells
+				// Outer width is used to calculate the container
+				var iWidth = th.outerWidth();
+
+				// When working with the left most-cell, need to add on the
+				// table's border to the outerWidth, since we need to take
+				// account of it, but it isn't in any cell
+				if ( that.s.aiOuterWidths.length === 0 ) {
+					border = $(that.s.dt.nTable).css('border-left-width');
+					iWidth += typeof border === 'string' ? 1 : parseInt( border, 10 );
+				}
+
+				// Likewise with the final column on the right
+				if ( that.s.aiOuterWidths.length === that.s.dt.aoColumns.length-1 ) {
+					border = $(that.s.dt.nTable).css('border-right-width');
+					iWidth += typeof border === 'string' ? 1 : parseInt( border, 10 );
+				}
+
+				that.s.aiOuterWidths.push( iWidth );
+				that.s.aiInnerWidths.push( th.width() );
+
+				if ( i < that.s.iLeftColumns )
+				{
+					iLeftWidth += iWidth;
+				}
+
+				if ( that.s.iTableColumns-that.s.iRightColumns <= i )
+				{
+					iRightWidth += iWidth;
+				}
+			}
+		} );
+
+		this.s.iLeftWidth = iLeftWidth;
+		this.s.iRightWidth = iRightWidth;
+	},
+
+
+	/**
+	 * Set up the DOM for the fixed column. The way the layout works is to create a 1x3 grid
+	 * for the left column, the DataTable (for which we just reuse the scrolling element DataTable
+	 * puts into the DOM) and the right column. In each of he two fixed column elements there is a
+	 * grouping wrapper element and then a head, body and footer wrapper. In each of these we then
+	 * place the cloned header, body or footer tables. This effectively gives as 3x3 grid structure.
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnGridSetup": function ()
+	{
+		var that = this;
+		var oOverflow = this._fnDTOverflow();
+		var block;
+
+		this.dom.body = this.s.dt.nTable;
+		this.dom.header = this.s.dt.nTHead.parentNode;
+		this.dom.header.parentNode.parentNode.style.position = "relative";
+
+		var nSWrapper =
+			$('<div class="DTFC_ScrollWrapper" style="position:relative; clear:both;">'+
+				'<div class="DTFC_LeftWrapper" style="position:absolute; top:0; left:0;">'+
+					'<div class="DTFC_LeftHeadWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div>'+
+					'<div class="DTFC_LeftBodyWrapper" style="position:relative; top:0; left:0; overflow:hidden;">'+
+						'<div class="DTFC_LeftBodyLiner" style="position:relative; top:0; left:0; overflow-y:scroll;"></div>'+
+					'</div>'+
+					'<div class="DTFC_LeftFootWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div>'+
+				'</div>'+
+				'<div class="DTFC_RightWrapper" style="position:absolute; top:0; right:0;">'+
+					'<div class="DTFC_RightHeadWrapper" style="position:relative; top:0; left:0;">'+
+						'<div class="DTFC_RightHeadBlocker DTFC_Blocker" style="position:absolute; top:0; bottom:0;"></div>'+
+					'</div>'+
+					'<div class="DTFC_RightBodyWrapper" style="position:relative; top:0; left:0; overflow:hidden;">'+
+						'<div class="DTFC_RightBodyLiner" style="position:relative; top:0; left:0; overflow-y:scroll;"></div>'+
+					'</div>'+
+					'<div class="DTFC_RightFootWrapper" style="position:relative; top:0; left:0;">'+
+						'<div class="DTFC_RightFootBlocker DTFC_Blocker" style="position:absolute; top:0; bottom:0;"></div>'+
+					'</div>'+
+				'</div>'+
+			'</div>')[0];
+		var nLeft = nSWrapper.childNodes[0];
+		var nRight = nSWrapper.childNodes[1];
+
+		this.dom.grid.dt.parentNode.insertBefore( nSWrapper, this.dom.grid.dt );
+		nSWrapper.appendChild( this.dom.grid.dt );
+
+		this.dom.grid.wrapper = nSWrapper;
+
+		if ( this.s.iLeftColumns > 0 )
+		{
+			this.dom.grid.left.wrapper = nLeft;
+			this.dom.grid.left.head = nLeft.childNodes[0];
+			this.dom.grid.left.body = nLeft.childNodes[1];
+			this.dom.grid.left.liner = $('div.DTFC_LeftBodyLiner', nSWrapper)[0];
+
+			nSWrapper.appendChild( nLeft );
+		}
+
+		if ( this.s.iRightColumns > 0 )
+		{
+			this.dom.grid.right.wrapper = nRight;
+			this.dom.grid.right.head = nRight.childNodes[0];
+			this.dom.grid.right.body = nRight.childNodes[1];
+			this.dom.grid.right.liner = $('div.DTFC_RightBodyLiner', nSWrapper)[0];
+
+			nRight.style.right = oOverflow.bar+"px";
+
+			block = $('div.DTFC_RightHeadBlocker', nSWrapper)[0];
+			block.style.width = oOverflow.bar+"px";
+			block.style.right = -oOverflow.bar+"px";
+			this.dom.grid.right.headBlock = block;
+
+			block = $('div.DTFC_RightFootBlocker', nSWrapper)[0];
+			block.style.width = oOverflow.bar+"px";
+			block.style.right = -oOverflow.bar+"px";
+			this.dom.grid.right.footBlock = block;
+
+			nSWrapper.appendChild( nRight );
+		}
+
+		if ( this.s.dt.nTFoot )
+		{
+			this.dom.footer = this.s.dt.nTFoot.parentNode;
+			if ( this.s.iLeftColumns > 0 )
+			{
+				this.dom.grid.left.foot = nLeft.childNodes[2];
+			}
+			if ( this.s.iRightColumns > 0 )
+			{
+				this.dom.grid.right.foot = nRight.childNodes[2];
+			}
+		}
+
+		// RTL support - swap the position of the left and right columns (#48)
+		if ( this.s.rtl ) {
+			$('div.DTFC_RightHeadBlocker', nSWrapper).css( {
+				left: -oOverflow.bar+'px',
+				right: ''
+			} );
+		}
+	},
+
+
+	/**
+	 * Style and position the grid used for the FixedColumns layout
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnGridLayout": function ()
+	{
+		var that = this;
+		var oGrid = this.dom.grid;
+		var iWidth = $(oGrid.wrapper).width();
+		var iBodyHeight = $(this.s.dt.nTable.parentNode).outerHeight();
+		var iFullHeight = $(this.s.dt.nTable.parentNode.parentNode).outerHeight();
+		var oOverflow = this._fnDTOverflow();
+		var iLeftWidth = this.s.iLeftWidth;
+		var iRightWidth = this.s.iRightWidth;
+		var rtl = $(this.dom.body).css('direction') === 'rtl';
+		var wrapper;
+		var scrollbarAdjust = function ( node, width ) {
+			if ( ! oOverflow.bar ) {
+				// If there is no scrollbar (Macs) we need to hide the auto scrollbar
+				node.style.width = (width+20)+"px";
+				node.style.paddingRight = "20px";
+				node.style.boxSizing = "border-box";
+			}
+			else if ( that._firefoxScrollError() ) {
+				// See the above function for why this is required
+				if ( $(node).height() > 34 ) {
+					node.style.width = (width+oOverflow.bar)+"px";
+				}
+			}
+			else {
+				// Otherwise just overflow by the scrollbar
+				node.style.width = (width+oOverflow.bar)+"px";
+			}
+		};
+
+		// When x scrolling - don't paint the fixed columns over the x scrollbar
+		if ( oOverflow.x )
+		{
+			iBodyHeight -= oOverflow.bar;
+		}
+
+		oGrid.wrapper.style.height = iFullHeight+"px";
+
+		if ( this.s.iLeftColumns > 0 )
+		{
+			wrapper = oGrid.left.wrapper;
+			wrapper.style.width = iLeftWidth+'px';
+			wrapper.style.height = '1px';
+
+			// Swap the position of the left and right columns for rtl (#48)
+			// This is always up against the edge, scrollbar on the far side
+			if ( rtl ) {
+				wrapper.style.left = '';
+				wrapper.style.right = 0;
+			}
+			else {
+				wrapper.style.left = 0;
+				wrapper.style.right = '';
+			}
+
+			oGrid.left.body.style.height = iBodyHeight+"px";
+			if ( oGrid.left.foot ) {
+				oGrid.left.foot.style.top = (oOverflow.x ? oOverflow.bar : 0)+"px"; // shift footer for scrollbar
+			}
+
+			scrollbarAdjust( oGrid.left.liner, iLeftWidth );
+			oGrid.left.liner.style.height = iBodyHeight+"px";
+		}
+
+		if ( this.s.iRightColumns > 0 )
+		{
+			wrapper = oGrid.right.wrapper;
+			wrapper.style.width = iRightWidth+'px';
+			wrapper.style.height = '1px';
+
+			// Need to take account of the vertical scrollbar
+			if ( this.s.rtl ) {
+				wrapper.style.left = oOverflow.y ? oOverflow.bar+'px' : 0;
+				wrapper.style.right = '';
+			}
+			else {
+				wrapper.style.left = '';
+				wrapper.style.right = oOverflow.y ? oOverflow.bar+'px' : 0;
+			}
+
+			oGrid.right.body.style.height = iBodyHeight+"px";
+			if ( oGrid.right.foot ) {
+				oGrid.right.foot.style.top = (oOverflow.x ? oOverflow.bar : 0)+"px";
+			}
+
+			scrollbarAdjust( oGrid.right.liner, iRightWidth );
+			oGrid.right.liner.style.height = iBodyHeight+"px";
+
+			oGrid.right.headBlock.style.display = oOverflow.y ? 'block' : 'none';
+			oGrid.right.footBlock.style.display = oOverflow.y ? 'block' : 'none';
+		}
+	},
+
+
+	/**
+	 * Get information about the DataTable's scrolling state - specifically if the table is scrolling
+	 * on either the x or y axis, and also the scrollbar width.
+	 *  @returns {object} Information about the DataTables scrolling state with the properties:
+	 *    'x', 'y' and 'bar'
+	 *  @private
+	 */
+	"_fnDTOverflow": function ()
+	{
+		var nTable = this.s.dt.nTable;
+		var nTableScrollBody = nTable.parentNode;
+		var out = {
+			"x": false,
+			"y": false,
+			"bar": this.s.dt.oScroll.iBarWidth
+		};
+
+		if ( nTable.offsetWidth > nTableScrollBody.clientWidth )
+		{
+			out.x = true;
+		}
+
+		if ( nTable.offsetHeight > nTableScrollBody.clientHeight )
+		{
+			out.y = true;
+		}
+
+		return out;
+	},
+
+
+	/**
+	 * Clone and position the fixed columns
+	 *  @returns {void}
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnDraw": function ( bAll )
+	{
+		this._fnGridLayout();
+		this._fnCloneLeft( bAll );
+		this._fnCloneRight( bAll );
+
+		/* Draw callback function */
+		if ( this.s.fnDrawCallback !== null )
+		{
+			this.s.fnDrawCallback.call( this, this.dom.clone.left, this.dom.clone.right );
+		}
+
+		/* Event triggering */
+		$(this).trigger( 'draw.dtfc', {
+			"leftClone": this.dom.clone.left,
+			"rightClone": this.dom.clone.right
+		} );
+	},
+
+
+	/**
+	 * Clone the right columns
+	 *  @returns {void}
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnCloneRight": function ( bAll )
+	{
+		if ( this.s.iRightColumns <= 0 ) {
+			return;
+		}
+
+		var that = this,
+			i, jq,
+			aiColumns = [];
+
+		for ( i=this.s.iTableColumns-this.s.iRightColumns ; i<this.s.iTableColumns ; i++ ) {
+			if ( this.s.dt.aoColumns[i].bVisible ) {
+				aiColumns.push( i );
+			}
+		}
+
+		this._fnClone( this.dom.clone.right, this.dom.grid.right, aiColumns, bAll );
+	},
+
+
+	/**
+	 * Clone the left columns
+	 *  @returns {void}
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnCloneLeft": function ( bAll )
+	{
+		if ( this.s.iLeftColumns <= 0 ) {
+			return;
+		}
+
+		var that = this,
+			i, jq,
+			aiColumns = [];
+
+		for ( i=0 ; i<this.s.iLeftColumns ; i++ ) {
+			if ( this.s.dt.aoColumns[i].bVisible ) {
+				aiColumns.push( i );
+			}
+		}
+
+		this._fnClone( this.dom.clone.left, this.dom.grid.left, aiColumns, bAll );
+	},
+
+
+	/**
+	 * Make a copy of the layout object for a header or footer element from DataTables. Note that
+	 * this method will clone the nodes in the layout object.
+	 *  @returns {Array} Copy of the layout array
+	 *  @param   {Object} aoOriginal Layout array from DataTables (aoHeader or aoFooter)
+	 *  @param   {Object} aiColumns Columns to copy
+	 *  @param   {boolean} events Copy cell events or not
+	 *  @private
+	 */
+	"_fnCopyLayout": function ( aoOriginal, aiColumns, events )
+	{
+		var aReturn = [];
+		var aClones = [];
+		var aCloned = [];
+
+		for ( var i=0, iLen=aoOriginal.length ; i<iLen ; i++ )
+		{
+			var aRow = [];
+			aRow.nTr = $(aoOriginal[i].nTr).clone(events, false)[0];
+
+			for ( var j=0, jLen=this.s.iTableColumns ; j<jLen ; j++ )
+			{
+				if ( $.inArray( j, aiColumns ) === -1 )
+				{
+					continue;
+				}
+
+				var iCloned = $.inArray( aoOriginal[i][j].cell, aCloned );
+				if ( iCloned === -1 )
+				{
+					var nClone = $(aoOriginal[i][j].cell).clone(events, false)[0];
+					aClones.push( nClone );
+					aCloned.push( aoOriginal[i][j].cell );
+
+					aRow.push( {
+						"cell": nClone,
+						"unique": aoOriginal[i][j].unique
+					} );
+				}
+				else
+				{
+					aRow.push( {
+						"cell": aClones[ iCloned ],
+						"unique": aoOriginal[i][j].unique
+					} );
+				}
+			}
+
+			aReturn.push( aRow );
+		}
+
+		return aReturn;
+	},
+
+
+	/**
+	 * Clone the DataTable nodes and place them in the DOM (sized correctly)
+	 *  @returns {void}
+	 *  @param   {Object} oClone Object containing the header, footer and body cloned DOM elements
+	 *  @param   {Object} oGrid Grid object containing the display grid elements for the cloned
+	 *                    column (left or right)
+	 *  @param   {Array} aiColumns Column indexes which should be operated on from the DataTable
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnClone": function ( oClone, oGrid, aiColumns, bAll )
+	{
+		var that = this,
+			i, iLen, j, jLen, jq, nTarget, iColumn, nClone, iIndex, aoCloneLayout,
+			jqCloneThead, aoFixedHeader,
+			dt = this.s.dt;
+
+		/*
+		 * Header
+		 */
+		if ( bAll )
+		{
+			$(oClone.header).remove();
+
+			oClone.header = $(this.dom.header).clone(true, false)[0];
+			oClone.header.className += " DTFC_Cloned";
+			oClone.header.style.width = "100%";
+			oGrid.head.appendChild( oClone.header );
+
+			/* Copy the DataTables layout cache for the header for our floating column */
+			aoCloneLayout = this._fnCopyLayout( dt.aoHeader, aiColumns, true );
+			jqCloneThead = $('>thead', oClone.header);
+			jqCloneThead.empty();
+
+			/* Add the created cloned TR elements to the table */
+			for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+			{
+				jqCloneThead[0].appendChild( aoCloneLayout[i].nTr );
+			}
+
+			/* Use the handy _fnDrawHead function in DataTables to do the rowspan/colspan
+			 * calculations for us
+			 */
+			dt.oApi._fnDrawHead( dt, aoCloneLayout, true );
+		}
+		else
+		{
+			/* To ensure that we copy cell classes exactly, regardless of colspan, multiple rows
+			 * etc, we make a copy of the header from the DataTable again, but don't insert the
+			 * cloned cells, just copy the classes across. To get the matching layout for the
+			 * fixed component, we use the DataTables _fnDetectHeader method, allowing 1:1 mapping
+			 */
+			aoCloneLayout = this._fnCopyLayout( dt.aoHeader, aiColumns, false );
+			aoFixedHeader=[];
+
+			dt.oApi._fnDetectHeader( aoFixedHeader, $('>thead', oClone.header)[0] );
+
+			for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+			{
+				for ( j=0, jLen=aoCloneLayout[i].length ; j<jLen ; j++ )
+				{
+					aoFixedHeader[i][j].cell.className = aoCloneLayout[i][j].cell.className;
+
+					// If jQuery UI theming is used we need to copy those elements as well
+					$('span.DataTables_sort_icon', aoFixedHeader[i][j].cell).each( function () {
+						this.className = $('span.DataTables_sort_icon', aoCloneLayout[i][j].cell)[0].className;
+					} );
+				}
+			}
+		}
+		this._fnEqualiseHeights( 'thead', this.dom.header, oClone.header );
+
+		/*
+		 * Body
+		 */
+		if ( this.s.sHeightMatch == 'auto' )
+		{
+			/* Remove any heights which have been applied already and let the browser figure it out */
+			$('>tbody>tr', that.dom.body).css('height', 'auto');
+		}
+
+		if ( oClone.body !== null )
+		{
+			$(oClone.body).remove();
+			oClone.body = null;
+		}
+
+		oClone.body = $(this.dom.body).clone(true)[0];
+		oClone.body.className += " DTFC_Cloned";
+		oClone.body.style.paddingBottom = dt.oScroll.iBarWidth+"px";
+		oClone.body.style.marginBottom = (dt.oScroll.iBarWidth*2)+"px"; /* For IE */
+		if ( oClone.body.getAttribute('id') !== null )
+		{
+			oClone.body.removeAttribute('id');
+		}
+
+		$('>thead>tr', oClone.body).empty();
+		$('>tfoot', oClone.body).remove();
+
+		var nBody = $('tbody', oClone.body)[0];
+		$(nBody).empty();
+		if ( dt.aiDisplay.length > 0 )
+		{
+			/* Copy the DataTables' header elements to force the column width in exactly the
+			 * same way that DataTables does it - have the header element, apply the width and
+			 * colapse it down
+			 */
+			var nInnerThead = $('>thead>tr', oClone.body)[0];
+			for ( iIndex=0 ; iIndex<aiColumns.length ; iIndex++ )
+			{
+				iColumn = aiColumns[iIndex];
+
+				nClone = $(dt.aoColumns[iColumn].nTh).clone(true)[0];
+				nClone.innerHTML = "";
+
+				var oStyle = nClone.style;
+				oStyle.paddingTop = "0";
+				oStyle.paddingBottom = "0";
+				oStyle.borderTopWidth = "0";
+				oStyle.borderBottomWidth = "0";
+				oStyle.height = 0;
+				oStyle.width = that.s.aiInnerWidths[iColumn]+"px";
+
+				nInnerThead.appendChild( nClone );
+			}
+
+			/* Add in the tbody elements, cloning form the master table */
+			$('>tbody>tr', that.dom.body).each( function (z) {
+				var i = that.s.dt.oFeatures.bServerSide===false ?
+					that.s.dt.aiDisplay[ that.s.dt._iDisplayStart+z ] : z;
+				var aTds = that.s.dt.aoData[ i ].anCells || $(this).children('td, th');
+
+				var n = this.cloneNode(false);
+				n.removeAttribute('id');
+				n.setAttribute( 'data-dt-row', i );
+
+				for ( iIndex=0 ; iIndex<aiColumns.length ; iIndex++ )
+				{
+					iColumn = aiColumns[iIndex];
+
+					if ( aTds.length > 0 )
+					{
+						nClone = $( aTds[iColumn] ).clone(true, true)[0];
+						nClone.setAttribute( 'data-dt-row', i );
+						nClone.setAttribute( 'data-dt-column', iIndex );
+						n.appendChild( nClone );
+					}
+				}
+				nBody.appendChild( n );
+			} );
+		}
+		else
+		{
+			$('>tbody>tr', that.dom.body).each( function (z) {
+				nClone = this.cloneNode(true);
+				nClone.className += ' DTFC_NoData';
+				$('td', nClone).html('');
+				nBody.appendChild( nClone );
+			} );
+		}
+
+		oClone.body.style.width = "100%";
+		oClone.body.style.margin = "0";
+		oClone.body.style.padding = "0";
+
+		// Interop with Scroller - need to use a height forcing element in the
+		// scrolling area in the same way that Scroller does in the body scroll.
+		if ( dt.oScroller !== undefined )
+		{
+			var scrollerForcer = dt.oScroller.dom.force;
+
+			if ( ! oGrid.forcer ) {
+				oGrid.forcer = scrollerForcer.cloneNode( true );
+				oGrid.liner.appendChild( oGrid.forcer );
+			}
+			else {
+				oGrid.forcer.style.height = scrollerForcer.style.height;
+			}
+		}
+
+		oGrid.liner.appendChild( oClone.body );
+
+		this._fnEqualiseHeights( 'tbody', that.dom.body, oClone.body );
+
+		/*
+		 * Footer
+		 */
+		if ( dt.nTFoot !== null )
+		{
+			if ( bAll )
+			{
+				if ( oClone.footer !== null )
+				{
+					oClone.footer.parentNode.removeChild( oClone.footer );
+				}
+				oClone.footer = $(this.dom.footer).clone(true, true)[0];
+				oClone.footer.className += " DTFC_Cloned";
+				oClone.footer.style.width = "100%";
+				oGrid.foot.appendChild( oClone.footer );
+
+				/* Copy the footer just like we do for the header */
+				aoCloneLayout = this._fnCopyLayout( dt.aoFooter, aiColumns, true );
+				var jqCloneTfoot = $('>tfoot', oClone.footer);
+				jqCloneTfoot.empty();
+
+				for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+				{
+					jqCloneTfoot[0].appendChild( aoCloneLayout[i].nTr );
+				}
+				dt.oApi._fnDrawHead( dt, aoCloneLayout, true );
+			}
+			else
+			{
+				aoCloneLayout = this._fnCopyLayout( dt.aoFooter, aiColumns, false );
+				var aoCurrFooter=[];
+
+				dt.oApi._fnDetectHeader( aoCurrFooter, $('>tfoot', oClone.footer)[0] );
+
+				for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+				{
+					for ( j=0, jLen=aoCloneLayout[i].length ; j<jLen ; j++ )
+					{
+						aoCurrFooter[i][j].cell.className = aoCloneLayout[i][j].cell.className;
+					}
+				}
+			}
+			this._fnEqualiseHeights( 'tfoot', this.dom.footer, oClone.footer );
+		}
+
+		/* Equalise the column widths between the header footer and body - body get's priority */
+		var anUnique = dt.oApi._fnGetUniqueThs( dt, $('>thead', oClone.header)[0] );
+		$(anUnique).each( function (i) {
+			iColumn = aiColumns[i];
+			this.style.width = that.s.aiInnerWidths[iColumn]+"px";
+		} );
+
+		if ( that.s.dt.nTFoot !== null )
+		{
+			anUnique = dt.oApi._fnGetUniqueThs( dt, $('>tfoot', oClone.footer)[0] );
+			$(anUnique).each( function (i) {
+				iColumn = aiColumns[i];
+				this.style.width = that.s.aiInnerWidths[iColumn]+"px";
+			} );
+		}
+	},
+
+
+	/**
+	 * From a given table node (THEAD etc), get a list of TR direct child elements
+	 *  @param   {Node} nIn Table element to search for TR elements (THEAD, TBODY or TFOOT element)
+	 *  @returns {Array} List of TR elements found
+	 *  @private
+	 */
+	"_fnGetTrNodes": function ( nIn )
+	{
+		var aOut = [];
+		for ( var i=0, iLen=nIn.childNodes.length ; i<iLen ; i++ )
+		{
+			if ( nIn.childNodes[i].nodeName.toUpperCase() == "TR" )
+			{
+				aOut.push( nIn.childNodes[i] );
+			}
+		}
+		return aOut;
+	},
+
+
+	/**
+	 * Equalise the heights of the rows in a given table node in a cross browser way
+	 *  @returns {void}
+	 *  @param   {String} nodeName Node type - thead, tbody or tfoot
+	 *  @param   {Node} original Original node to take the heights from
+	 *  @param   {Node} clone Copy the heights to
+	 *  @private
+	 */
+	"_fnEqualiseHeights": function ( nodeName, original, clone )
+	{
+		if ( this.s.sHeightMatch == 'none' && nodeName !== 'thead' && nodeName !== 'tfoot' )
+		{
+			return;
+		}
+
+		var that = this,
+			i, iLen, iHeight, iHeight2, iHeightOriginal, iHeightClone,
+			rootOriginal = original.getElementsByTagName(nodeName)[0],
+			rootClone    = clone.getElementsByTagName(nodeName)[0],
+			jqBoxHack    = $('>'+nodeName+'>tr:eq(0)', original).children(':first'),
+			iBoxHack     = jqBoxHack.outerHeight() - jqBoxHack.height(),
+			anOriginal   = this._fnGetTrNodes( rootOriginal ),
+			anClone      = this._fnGetTrNodes( rootClone ),
+			heights      = [];
+
+		for ( i=0, iLen=anClone.length ; i<iLen ; i++ )
+		{
+			iHeightOriginal = anOriginal[i].offsetHeight;
+			iHeightClone = anClone[i].offsetHeight;
+			iHeight = iHeightClone > iHeightOriginal ? iHeightClone : iHeightOriginal;
+
+			if ( this.s.sHeightMatch == 'semiauto' )
+			{
+				anOriginal[i]._DTTC_iHeight = iHeight;
+			}
+
+			heights.push( iHeight );
+		}
+
+		for ( i=0, iLen=anClone.length ; i<iLen ; i++ )
+		{
+			anClone[i].style.height = heights[i]+"px";
+			anOriginal[i].style.height = heights[i]+"px";
+		}
+	},
+
+	/**
+	 * Determine if the UA suffers from Firefox's overflow:scroll scrollbars
+	 * not being shown bug.
+	 *
+	 * Firefox doesn't draw scrollbars, even if it is told to using
+	 * overflow:scroll, if the div is less than 34px height. See bugs 292284 and
+	 * 781885. Using UA detection here since this is particularly hard to detect
+	 * using objects - its a straight up rendering error in Firefox.
+	 *
+	 * @return {boolean} True if Firefox error is present, false otherwise
+	 */
+	_firefoxScrollError: function () {
+		if ( _firefoxScroll === undefined ) {
+			var test = $('<div/>')
+				.css( {
+					position: 'absolute',
+					top: 0,
+					left: 0,
+					height: 10,
+					width: 50,
+					overflow: 'scroll'
+				} )
+				.appendTo( 'body' );
+
+			// Make sure this doesn't apply on Macs with 0 width scrollbars
+			_firefoxScroll = (
+				test[0].clientWidth === test[0].offsetWidth && this._fnDTOverflow().bar !== 0
+			);
+
+			test.remove();
+		}
+
+		return _firefoxScroll;
+	}
+} );
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Statics
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * FixedColumns default settings for initialisation
+ *  @name FixedColumns.defaults
+ *  @namespace
+ *  @static
+ */
+FixedColumns.defaults = /** @lends FixedColumns.defaults */{
+	/**
+	 * Number of left hand columns to fix in position
+	 *  @type     int
+	 *  @default  1
+	 *  @static
+	 *  @example
+	 *      var  = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      new $.fn.dataTable.fixedColumns( table, {
+	 *          "leftColumns": 2
+	 *      } );
+	 */
+	"iLeftColumns": 1,
+
+	/**
+	 * Number of right hand columns to fix in position
+	 *  @type     int
+	 *  @default  0
+	 *  @static
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      new $.fn.dataTable.fixedColumns( table, {
+	 *          "rightColumns": 1
+	 *      } );
+	 */
+	"iRightColumns": 0,
+
+	/**
+	 * Draw callback function which is called when FixedColumns has redrawn the fixed assets
+	 *  @type     function(object, object):void
+	 *  @default  null
+	 *  @static
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      new $.fn.dataTable.fixedColumns( table, {
+	 *          "drawCallback": function () {
+	 *	            alert( "FixedColumns redraw" );
+	 *	        }
+	 *      } );
+	 */
+	"fnDrawCallback": null,
+
+	/**
+	 * Height matching algorthim to use. This can be "none" which will result in no height
+	 * matching being applied by FixedColumns (height matching could be forced by CSS in this
+	 * case), "semiauto" whereby the height calculation will be performed once, and the result
+	 * cached to be used again (fnRecalculateHeight can be used to force recalculation), or
+	 * "auto" when height matching is performed on every draw (slowest but must accurate)
+	 *  @type     string
+	 *  @default  semiauto
+	 *  @static
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      new $.fn.dataTable.fixedColumns( table, {
+	 *          "heightMatch": "auto"
+	 *      } );
+	 */
+	"sHeightMatch": "semiauto"
+};
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Constants
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * FixedColumns version
+ *  @name      FixedColumns.version
+ *  @type      String
+ *  @default   See code
+ *  @static
+ */
+FixedColumns.version = "3.2.2";
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables API integration
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+DataTable.Api.register( 'fixedColumns()', function () {
+	return this;
+} );
+
+DataTable.Api.register( 'fixedColumns().update()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		if ( ctx._oFixedColumns ) {
+			ctx._oFixedColumns.fnUpdate();
+		}
+	} );
+} );
+
+DataTable.Api.register( 'fixedColumns().relayout()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		if ( ctx._oFixedColumns ) {
+			ctx._oFixedColumns.fnRedrawLayout();
+		}
+	} );
+} );
+
+DataTable.Api.register( 'rows().recalcHeight()', function () {
+	return this.iterator( 'row', function ( ctx, idx ) {
+		if ( ctx._oFixedColumns ) {
+			ctx._oFixedColumns.fnRecalculateHeight( this.row(idx).node() );
+		}
+	} );
+} );
+
+DataTable.Api.register( 'fixedColumns().rowIndex()', function ( row ) {
+	row = $(row);
+
+	return row.parents('.DTFC_Cloned').length ?
+		this.rows( { page: 'current' } ).indexes()[ row.index() ] :
+		this.row( row ).index();
+} );
+
+DataTable.Api.register( 'fixedColumns().cellIndex()', function ( cell ) {
+	cell = $(cell);
+
+	if ( cell.parents('.DTFC_Cloned').length ) {
+		var rowClonedIdx = cell.parent().index();
+		var rowIdx = this.rows( { page: 'current' } ).indexes()[ rowClonedIdx ];
+		var columnIdx;
+
+		if ( cell.parents('.DTFC_LeftWrapper').length ) {
+			columnIdx = cell.index();
+		}
+		else {
+			var columns = this.columns().flatten().length;
+			columnIdx = columns - this.context[0]._oFixedColumns.s.iRightColumns + cell.index();
+		}
+
+		return {
+			row: rowIdx,
+			column: this.column.index( 'toData', columnIdx ),
+			columnVisible: columnIdx
+		};
+	}
+	else {
+		return this.cell( cell ).index();
+	}
+} );
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Initialisation
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+// Attach a listener to the document which listens for DataTables initialisation
+// events so we can automatically initialise
+$(document).on( 'init.dt.fixedColumns', function (e, settings) {
+	if ( e.namespace !== 'dt' ) {
+		return;
+	}
+
+	var init = settings.oInit.fixedColumns;
+	var defaults = DataTable.defaults.fixedColumns;
+
+	if ( init || defaults ) {
+		var opts = $.extend( {}, init, defaults );
+
+		if ( init !== false ) {
+			new FixedColumns( settings, opts );
+		}
+	}
+} );
+
+
+
+// Make FixedColumns accessible from the DataTables instance
+$.fn.dataTable.FixedColumns = FixedColumns;
+$.fn.DataTable.FixedColumns = FixedColumns;
+
+return FixedColumns;
+}));
diff --git a/static/DataTables/FixedColumns-3.2.2/js/dataTables.fixedColumns.min.js b/static/DataTables/FixedColumns-3.2.2/js/dataTables.fixedColumns.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..6e3a593c9cca2e859c1f0bc336d564221b285870
--- /dev/null
+++ b/static/DataTables/FixedColumns-3.2.2/js/dataTables.fixedColumns.min.js
@@ -0,0 +1,35 @@
+/*!
+ FixedColumns 3.2.2
+ ©2010-2016 SpryMedia Ltd - datatables.net/license
+*/
+(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(q){return d(q,window,document)}):"object"===typeof exports?module.exports=function(q,r){q||(q=window);if(!r||!r.fn.dataTable)r=require("datatables.net")(q,r).$;return d(r,q,q.document)}:d(jQuery,window,document)})(function(d,q,r,t){var s=d.fn.dataTable,u,m=function(a,b){var c=this;if(this instanceof m){if(b===t||!0===b)b={};var e=d.fn.dataTable.camelToHungarian;e&&(e(m.defaults,m.defaults,!0),e(m.defaults,
+b));e=(new d.fn.dataTable.Api(a)).settings()[0];this.s={dt:e,iTableColumns:e.aoColumns.length,aiOuterWidths:[],aiInnerWidths:[],rtl:"rtl"===d(e.nTable).css("direction")};this.dom={scroller:null,header:null,body:null,footer:null,grid:{wrapper:null,dt:null,left:{wrapper:null,head:null,body:null,foot:null},right:{wrapper:null,head:null,body:null,foot:null}},clone:{left:{header:null,body:null,footer:null},right:{header:null,body:null,footer:null}}};if(e._oFixedColumns)throw"FixedColumns already initialised on this table";
+e._oFixedColumns=this;e._bInitComplete?this._fnConstruct(b):e.oApi._fnCallbackReg(e,"aoInitComplete",function(){c._fnConstruct(b)},"FixedColumns")}else alert("FixedColumns warning: FixedColumns must be initialised with the 'new' keyword.")};d.extend(m.prototype,{fnUpdate:function(){this._fnDraw(!0)},fnRedrawLayout:function(){this._fnColCalc();this._fnGridLayout();this.fnUpdate()},fnRecalculateHeight:function(a){delete a._DTTC_iHeight;a.style.height="auto"},fnSetRowHeight:function(a,b){a.style.height=
+b+"px"},fnGetPosition:function(a){var b=this.s.dt.oInstance;if(d(a).parents(".DTFC_Cloned").length){if("tr"===a.nodeName.toLowerCase())return a=d(a).index(),b.fnGetPosition(d("tr",this.s.dt.nTBody)[a]);var c=d(a).index(),a=d(a.parentNode).index();return[b.fnGetPosition(d("tr",this.s.dt.nTBody)[a]),c,b.oApi._fnVisibleToColumnIndex(this.s.dt,c)]}return b.fnGetPosition(a)},_fnConstruct:function(a){var b=this;if("function"!=typeof this.s.dt.oInstance.fnVersionCheck||!0!==this.s.dt.oInstance.fnVersionCheck("1.8.0"))alert("FixedColumns "+
+m.VERSION+" required DataTables 1.8.0 or later. Please upgrade your DataTables installation");else if(""===this.s.dt.oScroll.sX)this.s.dt.oInstance.oApi._fnLog(this.s.dt,1,"FixedColumns is not needed (no x-scrolling in DataTables enabled), so no action will be taken. Use 'FixedHeader' for column fixing when scrolling is not enabled");else{this.s=d.extend(!0,this.s,m.defaults,a);a=this.s.dt.oClasses;this.dom.grid.dt=d(this.s.dt.nTable).parents("div."+a.sScrollWrapper)[0];this.dom.scroller=d("div."+
+a.sScrollBody,this.dom.grid.dt)[0];this._fnColCalc();this._fnGridSetup();var c,e=!1;d(this.s.dt.nTableWrapper).on("mousedown.DTFC",function(){e=!0;d(r).one("mouseup",function(){e=!1})});d(this.dom.scroller).on("mouseover.DTFC touchstart.DTFC",function(){e||(c="main")}).on("scroll.DTFC",function(a){!c&&a.originalEvent&&(c="main");if("main"===c&&(0<b.s.iLeftColumns&&(b.dom.grid.left.liner.scrollTop=b.dom.scroller.scrollTop),0<b.s.iRightColumns))b.dom.grid.right.liner.scrollTop=b.dom.scroller.scrollTop});
+var f="onwheel"in r.createElement("div")?"wheel.DTFC":"mousewheel.DTFC";if(0<b.s.iLeftColumns)d(b.dom.grid.left.liner).on("mouseover.DTFC touchstart.DTFC",function(){e||(c="left")}).on("scroll.DTFC",function(a){!c&&a.originalEvent&&(c="left");"left"===c&&(b.dom.scroller.scrollTop=b.dom.grid.left.liner.scrollTop,0<b.s.iRightColumns&&(b.dom.grid.right.liner.scrollTop=b.dom.grid.left.liner.scrollTop))}).on(f,function(a){b.dom.scroller.scrollLeft-="wheel"===a.type?-a.originalEvent.deltaX:a.originalEvent.wheelDeltaX});
+if(0<b.s.iRightColumns)d(b.dom.grid.right.liner).on("mouseover.DTFC touchstart.DTFC",function(){e||(c="right")}).on("scroll.DTFC",function(a){!c&&a.originalEvent&&(c="right");"right"===c&&(b.dom.scroller.scrollTop=b.dom.grid.right.liner.scrollTop,0<b.s.iLeftColumns&&(b.dom.grid.left.liner.scrollTop=b.dom.grid.right.liner.scrollTop))}).on(f,function(a){b.dom.scroller.scrollLeft-="wheel"===a.type?-a.originalEvent.deltaX:a.originalEvent.wheelDeltaX});d(q).on("resize.DTFC",function(){b._fnGridLayout.call(b)});
+var g=!0,h=d(this.s.dt.nTable);h.on("draw.dt.DTFC",function(){b._fnColCalc();b._fnDraw.call(b,g);g=!1}).on("column-sizing.dt.DTFC",function(){b._fnColCalc();b._fnGridLayout(b)}).on("column-visibility.dt.DTFC",function(a,c,d,e,f){if(f===t||f)b._fnColCalc(),b._fnGridLayout(b),b._fnDraw(!0)}).on("select.dt.DTFC deselect.dt.DTFC",function(a){"dt"===a.namespace&&b._fnDraw(!1)}).on("destroy.dt.DTFC",function(){h.off(".DTFC");d(b.dom.scroller).off(".DTFC");d(q).off(".DTFC");d(b.s.dt.nTableWrapper).off(".DTFC");
+d(b.dom.grid.left.liner).off(".DTFC "+f);d(b.dom.grid.left.wrapper).remove();d(b.dom.grid.right.liner).off(".DTFC "+f);d(b.dom.grid.right.wrapper).remove()});this._fnGridLayout();this.s.dt.oInstance.fnDraw(!1)}},_fnColCalc:function(){var a=this,b=0,c=0;this.s.aiInnerWidths=[];this.s.aiOuterWidths=[];d.each(this.s.dt.aoColumns,function(e,f){var g=d(f.nTh),h;if(g.filter(":visible").length){var i=g.outerWidth();0===a.s.aiOuterWidths.length&&(h=d(a.s.dt.nTable).css("border-left-width"),i+="string"===
+typeof h?1:parseInt(h,10));a.s.aiOuterWidths.length===a.s.dt.aoColumns.length-1&&(h=d(a.s.dt.nTable).css("border-right-width"),i+="string"===typeof h?1:parseInt(h,10));a.s.aiOuterWidths.push(i);a.s.aiInnerWidths.push(g.width());e<a.s.iLeftColumns&&(b+=i);a.s.iTableColumns-a.s.iRightColumns<=e&&(c+=i)}else a.s.aiInnerWidths.push(0),a.s.aiOuterWidths.push(0)});this.s.iLeftWidth=b;this.s.iRightWidth=c},_fnGridSetup:function(){var a=this._fnDTOverflow(),b;this.dom.body=this.s.dt.nTable;this.dom.header=
+this.s.dt.nTHead.parentNode;this.dom.header.parentNode.parentNode.style.position="relative";var c=d('<div class="DTFC_ScrollWrapper" style="position:relative; clear:both;"><div class="DTFC_LeftWrapper" style="position:absolute; top:0; left:0;"><div class="DTFC_LeftHeadWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div><div class="DTFC_LeftBodyWrapper" style="position:relative; top:0; left:0; overflow:hidden;"><div class="DTFC_LeftBodyLiner" style="position:relative; top:0; left:0; overflow-y:scroll;"></div></div><div class="DTFC_LeftFootWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div></div><div class="DTFC_RightWrapper" style="position:absolute; top:0; right:0;"><div class="DTFC_RightHeadWrapper" style="position:relative; top:0; left:0;"><div class="DTFC_RightHeadBlocker DTFC_Blocker" style="position:absolute; top:0; bottom:0;"></div></div><div class="DTFC_RightBodyWrapper" style="position:relative; top:0; left:0; overflow:hidden;"><div class="DTFC_RightBodyLiner" style="position:relative; top:0; left:0; overflow-y:scroll;"></div></div><div class="DTFC_RightFootWrapper" style="position:relative; top:0; left:0;"><div class="DTFC_RightFootBlocker DTFC_Blocker" style="position:absolute; top:0; bottom:0;"></div></div></div></div>')[0],
+e=c.childNodes[0],f=c.childNodes[1];this.dom.grid.dt.parentNode.insertBefore(c,this.dom.grid.dt);c.appendChild(this.dom.grid.dt);this.dom.grid.wrapper=c;0<this.s.iLeftColumns&&(this.dom.grid.left.wrapper=e,this.dom.grid.left.head=e.childNodes[0],this.dom.grid.left.body=e.childNodes[1],this.dom.grid.left.liner=d("div.DTFC_LeftBodyLiner",c)[0],c.appendChild(e));0<this.s.iRightColumns&&(this.dom.grid.right.wrapper=f,this.dom.grid.right.head=f.childNodes[0],this.dom.grid.right.body=f.childNodes[1],this.dom.grid.right.liner=
+d("div.DTFC_RightBodyLiner",c)[0],f.style.right=a.bar+"px",b=d("div.DTFC_RightHeadBlocker",c)[0],b.style.width=a.bar+"px",b.style.right=-a.bar+"px",this.dom.grid.right.headBlock=b,b=d("div.DTFC_RightFootBlocker",c)[0],b.style.width=a.bar+"px",b.style.right=-a.bar+"px",this.dom.grid.right.footBlock=b,c.appendChild(f));if(this.s.dt.nTFoot&&(this.dom.footer=this.s.dt.nTFoot.parentNode,0<this.s.iLeftColumns&&(this.dom.grid.left.foot=e.childNodes[2]),0<this.s.iRightColumns))this.dom.grid.right.foot=f.childNodes[2];
+this.s.rtl&&d("div.DTFC_RightHeadBlocker",c).css({left:-a.bar+"px",right:""})},_fnGridLayout:function(){var a=this,b=this.dom.grid;d(b.wrapper).width();var c=d(this.s.dt.nTable.parentNode).outerHeight(),e=d(this.s.dt.nTable.parentNode.parentNode).outerHeight(),f=this._fnDTOverflow(),g=this.s.iLeftWidth,h=this.s.iRightWidth,i="rtl"===d(this.dom.body).css("direction"),j=function(b,c){f.bar?a._firefoxScrollError()?34<d(b).height()&&(b.style.width=c+f.bar+"px"):b.style.width=c+f.bar+"px":(b.style.width=
+c+20+"px",b.style.paddingRight="20px",b.style.boxSizing="border-box")};f.x&&(c-=f.bar);b.wrapper.style.height=e+"px";0<this.s.iLeftColumns&&(e=b.left.wrapper,e.style.width=g+"px",e.style.height="1px",i?(e.style.left="",e.style.right=0):(e.style.left=0,e.style.right=""),b.left.body.style.height=c+"px",b.left.foot&&(b.left.foot.style.top=(f.x?f.bar:0)+"px"),j(b.left.liner,g),b.left.liner.style.height=c+"px");0<this.s.iRightColumns&&(e=b.right.wrapper,e.style.width=h+"px",e.style.height="1px",this.s.rtl?
+(e.style.left=f.y?f.bar+"px":0,e.style.right=""):(e.style.left="",e.style.right=f.y?f.bar+"px":0),b.right.body.style.height=c+"px",b.right.foot&&(b.right.foot.style.top=(f.x?f.bar:0)+"px"),j(b.right.liner,h),b.right.liner.style.height=c+"px",b.right.headBlock.style.display=f.y?"block":"none",b.right.footBlock.style.display=f.y?"block":"none")},_fnDTOverflow:function(){var a=this.s.dt.nTable,b=a.parentNode,c={x:!1,y:!1,bar:this.s.dt.oScroll.iBarWidth};a.offsetWidth>b.clientWidth&&(c.x=!0);a.offsetHeight>
+b.clientHeight&&(c.y=!0);return c},_fnDraw:function(a){this._fnGridLayout();this._fnCloneLeft(a);this._fnCloneRight(a);null!==this.s.fnDrawCallback&&this.s.fnDrawCallback.call(this,this.dom.clone.left,this.dom.clone.right);d(this).trigger("draw.dtfc",{leftClone:this.dom.clone.left,rightClone:this.dom.clone.right})},_fnCloneRight:function(a){if(!(0>=this.s.iRightColumns)){var b,c=[];for(b=this.s.iTableColumns-this.s.iRightColumns;b<this.s.iTableColumns;b++)this.s.dt.aoColumns[b].bVisible&&c.push(b);
+this._fnClone(this.dom.clone.right,this.dom.grid.right,c,a)}},_fnCloneLeft:function(a){if(!(0>=this.s.iLeftColumns)){var b,c=[];for(b=0;b<this.s.iLeftColumns;b++)this.s.dt.aoColumns[b].bVisible&&c.push(b);this._fnClone(this.dom.clone.left,this.dom.grid.left,c,a)}},_fnCopyLayout:function(a,b,c){for(var e=[],f=[],g=[],h=0,i=a.length;h<i;h++){var j=[];j.nTr=d(a[h].nTr).clone(c,!1)[0];for(var l=0,o=this.s.iTableColumns;l<o;l++)if(-1!==d.inArray(l,b)){var p=d.inArray(a[h][l].cell,g);-1===p?(p=d(a[h][l].cell).clone(c,
+!1)[0],f.push(p),g.push(a[h][l].cell),j.push({cell:p,unique:a[h][l].unique})):j.push({cell:f[p],unique:a[h][l].unique})}e.push(j)}return e},_fnClone:function(a,b,c,e){var f=this,g,h,i,j,l,o,p,n,m,k=this.s.dt;if(e){d(a.header).remove();a.header=d(this.dom.header).clone(!0,!1)[0];a.header.className+=" DTFC_Cloned";a.header.style.width="100%";b.head.appendChild(a.header);n=this._fnCopyLayout(k.aoHeader,c,!0);j=d(">thead",a.header);j.empty();g=0;for(h=n.length;g<h;g++)j[0].appendChild(n[g].nTr);k.oApi._fnDrawHead(k,
+n,!0)}else{n=this._fnCopyLayout(k.aoHeader,c,!1);m=[];k.oApi._fnDetectHeader(m,d(">thead",a.header)[0]);g=0;for(h=n.length;g<h;g++){i=0;for(j=n[g].length;i<j;i++)m[g][i].cell.className=n[g][i].cell.className,d("span.DataTables_sort_icon",m[g][i].cell).each(function(){this.className=d("span.DataTables_sort_icon",n[g][i].cell)[0].className})}}this._fnEqualiseHeights("thead",this.dom.header,a.header);"auto"==this.s.sHeightMatch&&d(">tbody>tr",f.dom.body).css("height","auto");null!==a.body&&(d(a.body).remove(),
+a.body=null);a.body=d(this.dom.body).clone(!0)[0];a.body.className+=" DTFC_Cloned";a.body.style.paddingBottom=k.oScroll.iBarWidth+"px";a.body.style.marginBottom=2*k.oScroll.iBarWidth+"px";null!==a.body.getAttribute("id")&&a.body.removeAttribute("id");d(">thead>tr",a.body).empty();d(">tfoot",a.body).remove();var q=d("tbody",a.body)[0];d(q).empty();if(0<k.aiDisplay.length){h=d(">thead>tr",a.body)[0];for(p=0;p<c.length;p++)l=c[p],o=d(k.aoColumns[l].nTh).clone(!0)[0],o.innerHTML="",j=o.style,j.paddingTop=
+"0",j.paddingBottom="0",j.borderTopWidth="0",j.borderBottomWidth="0",j.height=0,j.width=f.s.aiInnerWidths[l]+"px",h.appendChild(o);d(">tbody>tr",f.dom.body).each(function(a){var a=f.s.dt.oFeatures.bServerSide===false?f.s.dt.aiDisplay[f.s.dt._iDisplayStart+a]:a,b=f.s.dt.aoData[a].anCells||d(this).children("td, th"),e=this.cloneNode(false);e.removeAttribute("id");e.setAttribute("data-dt-row",a);for(p=0;p<c.length;p++){l=c[p];if(b.length>0){o=d(b[l]).clone(true,true)[0];o.setAttribute("data-dt-row",
+a);o.setAttribute("data-dt-column",p);e.appendChild(o)}}q.appendChild(e)})}else d(">tbody>tr",f.dom.body).each(function(){o=this.cloneNode(true);o.className=o.className+" DTFC_NoData";d("td",o).html("");q.appendChild(o)});a.body.style.width="100%";a.body.style.margin="0";a.body.style.padding="0";k.oScroller!==t&&(h=k.oScroller.dom.force,b.forcer?b.forcer.style.height=h.style.height:(b.forcer=h.cloneNode(!0),b.liner.appendChild(b.forcer)));b.liner.appendChild(a.body);this._fnEqualiseHeights("tbody",
+f.dom.body,a.body);if(null!==k.nTFoot){if(e){null!==a.footer&&a.footer.parentNode.removeChild(a.footer);a.footer=d(this.dom.footer).clone(!0,!0)[0];a.footer.className+=" DTFC_Cloned";a.footer.style.width="100%";b.foot.appendChild(a.footer);n=this._fnCopyLayout(k.aoFooter,c,!0);b=d(">tfoot",a.footer);b.empty();g=0;for(h=n.length;g<h;g++)b[0].appendChild(n[g].nTr);k.oApi._fnDrawHead(k,n,!0)}else{n=this._fnCopyLayout(k.aoFooter,c,!1);b=[];k.oApi._fnDetectHeader(b,d(">tfoot",a.footer)[0]);g=0;for(h=n.length;g<
+h;g++){i=0;for(j=n[g].length;i<j;i++)b[g][i].cell.className=n[g][i].cell.className}}this._fnEqualiseHeights("tfoot",this.dom.footer,a.footer)}b=k.oApi._fnGetUniqueThs(k,d(">thead",a.header)[0]);d(b).each(function(a){l=c[a];this.style.width=f.s.aiInnerWidths[l]+"px"});null!==f.s.dt.nTFoot&&(b=k.oApi._fnGetUniqueThs(k,d(">tfoot",a.footer)[0]),d(b).each(function(a){l=c[a];this.style.width=f.s.aiInnerWidths[l]+"px"}))},_fnGetTrNodes:function(a){for(var b=[],c=0,d=a.childNodes.length;c<d;c++)"TR"==a.childNodes[c].nodeName.toUpperCase()&&
+b.push(a.childNodes[c]);return b},_fnEqualiseHeights:function(a,b,c){if(!("none"==this.s.sHeightMatch&&"thead"!==a&&"tfoot"!==a)){var e,f,g=b.getElementsByTagName(a)[0],c=c.getElementsByTagName(a)[0],a=d(">"+a+">tr:eq(0)",b).children(":first");a.outerHeight();a.height();for(var g=this._fnGetTrNodes(g),b=this._fnGetTrNodes(c),h=[],c=0,a=b.length;c<a;c++)e=g[c].offsetHeight,f=b[c].offsetHeight,e=f>e?f:e,"semiauto"==this.s.sHeightMatch&&(g[c]._DTTC_iHeight=e),h.push(e);c=0;for(a=b.length;c<a;c++)b[c].style.height=
+h[c]+"px",g[c].style.height=h[c]+"px"}},_firefoxScrollError:function(){if(u===t){var a=d("<div/>").css({position:"absolute",top:0,left:0,height:10,width:50,overflow:"scroll"}).appendTo("body");u=a[0].clientWidth===a[0].offsetWidth&&0!==this._fnDTOverflow().bar;a.remove()}return u}});m.defaults={iLeftColumns:1,iRightColumns:0,fnDrawCallback:null,sHeightMatch:"semiauto"};m.version="3.2.2";s.Api.register("fixedColumns()",function(){return this});s.Api.register("fixedColumns().update()",function(){return this.iterator("table",
+function(a){a._oFixedColumns&&a._oFixedColumns.fnUpdate()})});s.Api.register("fixedColumns().relayout()",function(){return this.iterator("table",function(a){a._oFixedColumns&&a._oFixedColumns.fnRedrawLayout()})});s.Api.register("rows().recalcHeight()",function(){return this.iterator("row",function(a,b){a._oFixedColumns&&a._oFixedColumns.fnRecalculateHeight(this.row(b).node())})});s.Api.register("fixedColumns().rowIndex()",function(a){a=d(a);return a.parents(".DTFC_Cloned").length?this.rows({page:"current"}).indexes()[a.index()]:
+this.row(a).index()});s.Api.register("fixedColumns().cellIndex()",function(a){a=d(a);if(a.parents(".DTFC_Cloned").length){var b=a.parent().index(),b=this.rows({page:"current"}).indexes()[b],a=a.parents(".DTFC_LeftWrapper").length?a.index():this.columns().flatten().length-this.context[0]._oFixedColumns.s.iRightColumns+a.index();return{row:b,column:this.column.index("toData",a),columnVisible:a}}return this.cell(a).index()});d(r).on("init.dt.fixedColumns",function(a,b){if("dt"===a.namespace){var c=b.oInit.fixedColumns,
+e=s.defaults.fixedColumns;if(c||e)e=d.extend({},c,e),!1!==c&&new m(b,e)}});d.fn.dataTable.FixedColumns=m;return d.fn.DataTable.FixedColumns=m});
diff --git a/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.bootstrap.css b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.bootstrap.css
new file mode 100644
index 0000000000000000000000000000000000000000..98b09b07ade2026c6eb4972a0b3523c75c06dad9
--- /dev/null
+++ b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.bootstrap.css
@@ -0,0 +1,20 @@
+table.dataTable.fixedHeader-floating,
+table.dataTable.fixedHeader-locked {
+  background-color: white;
+  margin-top: 0 !important;
+  margin-bottom: 0 !important;
+}
+
+table.dataTable.fixedHeader-floating {
+  position: fixed !important;
+}
+
+table.dataTable.fixedHeader-locked {
+  position: absolute !important;
+}
+
+@media print {
+  table.fixedHeader-floating {
+    display: none;
+  }
+}
diff --git a/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.bootstrap.min.css b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.bootstrap.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..68f816759272a0a80dcf22dcbdd27f657c1ffbe4
--- /dev/null
+++ b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.bootstrap.min.css
@@ -0,0 +1 @@
+table.dataTable.fixedHeader-floating,table.dataTable.fixedHeader-locked{background-color:white;margin-top:0 !important;margin-bottom:0 !important}table.dataTable.fixedHeader-floating{position:fixed !important}table.dataTable.fixedHeader-locked{position:absolute !important}@media print{table.fixedHeader-floating{display:none}}
diff --git a/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.dataTables.css b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.dataTables.css
new file mode 100644
index 0000000000000000000000000000000000000000..77509717e2a2322bdde1b92e3ec1188228386d16
--- /dev/null
+++ b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.dataTables.css
@@ -0,0 +1,19 @@
+table.fixedHeader-floating {
+  position: fixed !important;
+  background-color: white;
+}
+
+table.fixedHeader-floating.no-footer {
+  border-bottom-width: 0;
+}
+
+table.fixedHeader-locked {
+  position: absolute !important;
+  background-color: white;
+}
+
+@media print {
+  table.fixedHeader-floating {
+    display: none;
+  }
+}
diff --git a/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.dataTables.min.css b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.dataTables.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..7113963e43075a9c1b390ec447ada7b5b6c51ea6
--- /dev/null
+++ b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.dataTables.min.css
@@ -0,0 +1 @@
+table.fixedHeader-floating{position:fixed !important;background-color:white}table.fixedHeader-floating.no-footer{border-bottom-width:0}table.fixedHeader-locked{position:absolute !important;background-color:white}@media print{table.fixedHeader-floating{display:none}}
diff --git a/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.foundation.css b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.foundation.css
new file mode 100644
index 0000000000000000000000000000000000000000..98b09b07ade2026c6eb4972a0b3523c75c06dad9
--- /dev/null
+++ b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.foundation.css
@@ -0,0 +1,20 @@
+table.dataTable.fixedHeader-floating,
+table.dataTable.fixedHeader-locked {
+  background-color: white;
+  margin-top: 0 !important;
+  margin-bottom: 0 !important;
+}
+
+table.dataTable.fixedHeader-floating {
+  position: fixed !important;
+}
+
+table.dataTable.fixedHeader-locked {
+  position: absolute !important;
+}
+
+@media print {
+  table.fixedHeader-floating {
+    display: none;
+  }
+}
diff --git a/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.foundation.min.css b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.foundation.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..68f816759272a0a80dcf22dcbdd27f657c1ffbe4
--- /dev/null
+++ b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.foundation.min.css
@@ -0,0 +1 @@
+table.dataTable.fixedHeader-floating,table.dataTable.fixedHeader-locked{background-color:white;margin-top:0 !important;margin-bottom:0 !important}table.dataTable.fixedHeader-floating{position:fixed !important}table.dataTable.fixedHeader-locked{position:absolute !important}@media print{table.fixedHeader-floating{display:none}}
diff --git a/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.jqueryui.css b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.jqueryui.css
new file mode 100644
index 0000000000000000000000000000000000000000..a453aa96d559c85e69641619396b520a2b1bcba2
--- /dev/null
+++ b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.jqueryui.css
@@ -0,0 +1,15 @@
+table.fixedHeader-floating {
+  position: fixed !important;
+  background-color: white;
+}
+
+table.fixedHeader-locked {
+  position: absolute !important;
+  background-color: white;
+}
+
+@media print {
+  table.fixedHeader-floating {
+    display: none;
+  }
+}
diff --git a/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.jqueryui.min.css b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.jqueryui.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..c89d8145bcf5961f2e7643a45437e2a10c39387f
--- /dev/null
+++ b/static/DataTables/FixedHeader-3.1.2/css/fixedHeader.jqueryui.min.css
@@ -0,0 +1 @@
+table.fixedHeader-floating{position:fixed !important;background-color:white}table.fixedHeader-locked{position:absolute !important;background-color:white}@media print{table.fixedHeader-floating{display:none}}
diff --git a/static/DataTables/FixedHeader-3.1.2/js/dataTables.fixedHeader.js b/static/DataTables/FixedHeader-3.1.2/js/dataTables.fixedHeader.js
new file mode 100644
index 0000000000000000000000000000000000000000..7c236da694b97e31dc7e164faaf6d36fd2580c2f
--- /dev/null
+++ b/static/DataTables/FixedHeader-3.1.2/js/dataTables.fixedHeader.js
@@ -0,0 +1,672 @@
+/*! FixedHeader 3.1.2
+ * ©2009-2016 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary     FixedHeader
+ * @description Fix a table's header or footer, so it is always visible while
+ *              scrolling
+ * @version     3.1.2
+ * @file        dataTables.fixedHeader.js
+ * @author      SpryMedia Ltd (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ * @copyright   Copyright 2009-2016 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ *   MIT license - http://datatables.net/license/mit
+ *
+ * This source file 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 license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+var _instCounter = 0;
+
+var FixedHeader = function ( dt, config ) {
+	// Sanity check - you just know it will happen
+	if ( ! (this instanceof FixedHeader) ) {
+		throw "FixedHeader must be initialised with the 'new' keyword.";
+	}
+
+	// Allow a boolean true for defaults
+	if ( config === true ) {
+		config = {};
+	}
+
+	dt = new DataTable.Api( dt );
+
+	this.c = $.extend( true, {}, FixedHeader.defaults, config );
+
+	this.s = {
+		dt: dt,
+		position: {
+			theadTop: 0,
+			tbodyTop: 0,
+			tfootTop: 0,
+			tfootBottom: 0,
+			width: 0,
+			left: 0,
+			tfootHeight: 0,
+			theadHeight: 0,
+			windowHeight: $(window).height(),
+			visible: true
+		},
+		headerMode: null,
+		footerMode: null,
+		autoWidth: dt.settings()[0].oFeatures.bAutoWidth,
+		namespace: '.dtfc'+(_instCounter++),
+		scrollLeft: {
+			header: -1,
+			footer: -1
+		},
+		enable: true
+	};
+
+	this.dom = {
+		floatingHeader: null,
+		thead: $(dt.table().header()),
+		tbody: $(dt.table().body()),
+		tfoot: $(dt.table().footer()),
+		header: {
+			host: null,
+			floating: null,
+			placeholder: null
+		},
+		footer: {
+			host: null,
+			floating: null,
+			placeholder: null
+		}
+	};
+
+	this.dom.header.host = this.dom.thead.parent();
+	this.dom.footer.host = this.dom.tfoot.parent();
+
+	var dtSettings = dt.settings()[0];
+	if ( dtSettings._fixedHeader ) {
+		throw "FixedHeader already initialised on table "+dtSettings.nTable.id;
+	}
+
+	dtSettings._fixedHeader = this;
+
+	this._constructor();
+};
+
+
+/*
+ * Variable: FixedHeader
+ * Purpose:  Prototype for FixedHeader
+ * Scope:    global
+ */
+$.extend( FixedHeader.prototype, {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * API methods
+	 */
+	
+	/**
+	 * Enable / disable the fixed elements
+	 *
+	 * @param  {boolean} enable `true` to enable, `false` to disable
+	 */
+	enable: function ( enable )
+	{
+		this.s.enable = enable;
+
+		if ( this.c.header ) {
+			this._modeChange( 'in-place', 'header', true );
+		}
+
+		if ( this.c.footer && this.dom.tfoot.length ) {
+			this._modeChange( 'in-place', 'footer', true );
+		}
+
+		this.update();
+	},
+	
+	/**
+	 * Set header offset 
+	 *
+	 * @param  {int} new value for headerOffset
+	 */
+	headerOffset: function ( offset )
+	{
+		if ( offset !== undefined ) {
+			this.c.headerOffset = offset;
+			this.update();
+		}
+
+		return this.c.headerOffset;
+	},
+	
+	/**
+	 * Set footer offset
+	 *
+	 * @param  {int} new value for footerOffset
+	 */
+	footerOffset: function ( offset )
+	{
+		if ( offset !== undefined ) {
+			this.c.footerOffset = offset;
+			this.update();
+		}
+
+		return this.c.footerOffset;
+	},
+
+	
+	/**
+	 * Recalculate the position of the fixed elements and force them into place
+	 */
+	update: function ()
+	{
+		this._positions();
+		this._scroll( true );
+	},
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Constructor
+	 */
+	
+	/**
+	 * FixedHeader constructor - adding the required event listeners and
+	 * simple initialisation
+	 *
+	 * @private
+	 */
+	_constructor: function ()
+	{
+		var that = this;
+		var dt = this.s.dt;
+
+		$(window)
+			.on( 'scroll'+this.s.namespace, function () {
+				that._scroll();
+			} )
+			.on( 'resize'+this.s.namespace, function () {
+				that.s.position.windowHeight = $(window).height();
+				that.update();
+			} );
+
+		var autoHeader = $('.fh-fixedHeader');
+		if ( ! this.c.headerOffset && autoHeader.length ) {
+			this.c.headerOffset = autoHeader.outerHeight();
+		}
+
+		var autoFooter = $('.fh-fixedFooter');
+		if ( ! this.c.footerOffset && autoFooter.length ) {
+			this.c.footerOffset = autoFooter.outerHeight();
+		}
+
+		dt.on( 'column-reorder.dt.dtfc column-visibility.dt.dtfc draw.dt.dtfc column-sizing.dt.dtfc', function () {
+			that.update();
+		} );
+
+		dt.on( 'destroy.dtfc', function () {
+			dt.off( '.dtfc' );
+			$(window).off( that.s.namespace );
+		} );
+
+		this._positions();
+		this._scroll();
+	},
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods
+	 */
+
+	/**
+	 * Clone a fixed item to act as a place holder for the original element
+	 * which is moved into a clone of the table element, and moved around the
+	 * document to give the fixed effect.
+	 *
+	 * @param  {string}  item  'header' or 'footer'
+	 * @param  {boolean} force Force the clone to happen, or allow automatic
+	 *   decision (reuse existing if available)
+	 * @private
+	 */
+	_clone: function ( item, force )
+	{
+		var dt = this.s.dt;
+		var itemDom = this.dom[ item ];
+		var itemElement = item === 'header' ?
+			this.dom.thead :
+			this.dom.tfoot;
+
+		if ( ! force && itemDom.floating ) {
+			// existing floating element - reuse it
+			itemDom.floating.removeClass( 'fixedHeader-floating fixedHeader-locked' );
+		}
+		else {
+			if ( itemDom.floating ) {
+				itemDom.placeholder.remove();
+				this._unsize( item );
+				itemDom.floating.children().detach();
+				itemDom.floating.remove();
+			}
+
+			itemDom.floating = $( dt.table().node().cloneNode( false ) )
+				.css( 'table-layout', 'fixed' )
+				.removeAttr( 'id' )
+				.append( itemElement )
+				.appendTo( 'body' );
+
+			// Insert a fake thead/tfoot into the DataTable to stop it jumping around
+			itemDom.placeholder = itemElement.clone( false );
+			itemDom.host.prepend( itemDom.placeholder );
+
+			// Clone widths
+			this._matchWidths( itemDom.placeholder, itemDom.floating );
+		}
+	},
+
+	/**
+	 * Copy widths from the cells in one element to another. This is required
+	 * for the footer as the footer in the main table takes its sizes from the
+	 * header columns. That isn't present in the footer so to have it still
+	 * align correctly, the sizes need to be copied over. It is also required
+	 * for the header when auto width is not enabled
+	 *
+	 * @param  {jQuery} from Copy widths from
+	 * @param  {jQuery} to   Copy widths to
+	 * @private
+	 */
+	_matchWidths: function ( from, to ) {
+		var get = function ( name ) {
+			return $(name, from)
+				.map( function () {
+					return $(this).width();
+				} ).toArray();
+		};
+
+		var set = function ( name, toWidths ) {
+			$(name, to).each( function ( i ) {
+				$(this).css( {
+					width: toWidths[i],
+					minWidth: toWidths[i]
+				} );
+			} );
+		};
+
+		var thWidths = get( 'th' );
+		var tdWidths = get( 'td' );
+
+		set( 'th', thWidths );
+		set( 'td', tdWidths );
+	},
+
+	/**
+	 * Remove assigned widths from the cells in an element. This is required
+	 * when inserting the footer back into the main table so the size is defined
+	 * by the header columns and also when auto width is disabled in the
+	 * DataTable.
+	 *
+	 * @param  {string} item The `header` or `footer`
+	 * @private
+	 */
+	_unsize: function ( item ) {
+		var el = this.dom[ item ].floating;
+
+		if ( el && (item === 'footer' || (item === 'header' && ! this.s.autoWidth)) ) {
+			$('th, td', el).css( {
+				width: '',
+				minWidth: ''
+			} );
+		}
+		else if ( el && item === 'header' ) {
+			$('th, td', el).css( 'min-width', '' );
+		}
+	},
+
+	/**
+	 * Reposition the floating elements to take account of horizontal page
+	 * scroll
+	 *
+	 * @param  {string} item       The `header` or `footer`
+	 * @param  {int}    scrollLeft Document scrollLeft
+	 * @private
+	 */
+	_horizontal: function ( item, scrollLeft )
+	{
+		var itemDom = this.dom[ item ];
+		var position = this.s.position;
+		var lastScrollLeft = this.s.scrollLeft;
+
+		if ( itemDom.floating && lastScrollLeft[ item ] !== scrollLeft ) {
+			itemDom.floating.css( 'left', position.left - scrollLeft );
+
+			lastScrollLeft[ item ] = scrollLeft;
+		}
+	},
+
+	/**
+	 * Change from one display mode to another. Each fixed item can be in one
+	 * of:
+	 *
+	 * * `in-place` - In the main DataTable
+	 * * `in` - Floating over the DataTable
+	 * * `below` - (Header only) Fixed to the bottom of the table body
+	 * * `above` - (Footer only) Fixed to the top of the table body
+	 * 
+	 * @param  {string}  mode        Mode that the item should be shown in
+	 * @param  {string}  item        'header' or 'footer'
+	 * @param  {boolean} forceChange Force a redraw of the mode, even if already
+	 *     in that mode.
+	 * @private
+	 */
+	_modeChange: function ( mode, item, forceChange )
+	{
+		var dt = this.s.dt;
+		var itemDom = this.dom[ item ];
+		var position = this.s.position;
+
+		// Record focus. Browser's will cause input elements to loose focus if
+		// they are inserted else where in the doc
+		var tablePart = this.dom[ item==='footer' ? 'tfoot' : 'thead' ];
+		var focus = $.contains( tablePart[0], document.activeElement ) ?
+			document.activeElement :
+			null;
+
+		if ( mode === 'in-place' ) {
+			// Insert the header back into the table's real header
+			if ( itemDom.placeholder ) {
+				itemDom.placeholder.remove();
+				itemDom.placeholder = null;
+			}
+
+			this._unsize( item );
+
+			if ( item === 'header' ) {
+				itemDom.host.prepend( this.dom.thead );
+			}
+			else {
+				itemDom.host.append( this.dom.tfoot );
+			}
+
+			if ( itemDom.floating ) {
+				itemDom.floating.remove();
+				itemDom.floating = null;
+			}
+		}
+		else if ( mode === 'in' ) {
+			// Remove the header from the read header and insert into a fixed
+			// positioned floating table clone
+			this._clone( item, forceChange );
+
+			itemDom.floating
+				.addClass( 'fixedHeader-floating' )
+				.css( item === 'header' ? 'top' : 'bottom', this.c[item+'Offset'] )
+				.css( 'left', position.left+'px' )
+				.css( 'width', position.width+'px' );
+
+			if ( item === 'footer' ) {
+				itemDom.floating.css( 'top', '' );
+			}
+		}
+		else if ( mode === 'below' ) { // only used for the header
+			// Fix the position of the floating header at base of the table body
+			this._clone( item, forceChange );
+
+			itemDom.floating
+				.addClass( 'fixedHeader-locked' )
+				.css( 'top', position.tfootTop - position.theadHeight )
+				.css( 'left', position.left+'px' )
+				.css( 'width', position.width+'px' );
+		}
+		else if ( mode === 'above' ) { // only used for the footer
+			// Fix the position of the floating footer at top of the table body
+			this._clone( item, forceChange );
+
+			itemDom.floating
+				.addClass( 'fixedHeader-locked' )
+				.css( 'top', position.tbodyTop )
+				.css( 'left', position.left+'px' )
+				.css( 'width', position.width+'px' );
+		}
+
+		// Restore focus if it was lost
+		if ( focus && focus !== document.activeElement ) {
+			focus.focus();
+		}
+
+		this.s.scrollLeft.header = -1;
+		this.s.scrollLeft.footer = -1;
+		this.s[item+'Mode'] = mode;
+	},
+
+	/**
+	 * Cache the positional information that is required for the mode
+	 * calculations that FixedHeader performs.
+	 *
+	 * @private
+	 */
+	_positions: function ()
+	{
+		var dt = this.s.dt;
+		var table = dt.table();
+		var position = this.s.position;
+		var dom = this.dom;
+		var tableNode = $(table.node());
+
+		// Need to use the header and footer that are in the main table,
+		// regardless of if they are clones, since they hold the positions we
+		// want to measure from
+		var thead = tableNode.children('thead');
+		var tfoot = tableNode.children('tfoot');
+		var tbody = dom.tbody;
+
+		position.visible = tableNode.is(':visible');
+		position.width = tableNode.outerWidth();
+		position.left = tableNode.offset().left;
+		position.theadTop = thead.offset().top;
+		position.tbodyTop = tbody.offset().top;
+		position.theadHeight = position.tbodyTop - position.theadTop;
+
+		if ( tfoot.length ) {
+			position.tfootTop = tfoot.offset().top;
+			position.tfootBottom = position.tfootTop + tfoot.outerHeight();
+			position.tfootHeight = position.tfootBottom - position.tfootTop;
+		}
+		else {
+			position.tfootTop = position.tbodyTop + tbody.outerHeight();
+			position.tfootBottom = position.tfootTop;
+			position.tfootHeight = position.tfootTop;
+		}
+	},
+
+
+	/**
+	 * Mode calculation - determine what mode the fixed items should be placed
+	 * into.
+	 *
+	 * @param  {boolean} forceChange Force a redraw of the mode, even if already
+	 *     in that mode.
+	 * @private
+	 */
+	_scroll: function ( forceChange )
+	{
+		var windowTop = $(document).scrollTop();
+		var windowLeft = $(document).scrollLeft();
+		var position = this.s.position;
+		var headerMode, footerMode;
+
+		if ( ! this.s.enable ) {
+			return;
+		}
+
+		if ( this.c.header ) {
+			if ( ! position.visible || windowTop <= position.theadTop - this.c.headerOffset ) {
+				headerMode = 'in-place';
+			}
+			else if ( windowTop <= position.tfootTop - position.theadHeight - this.c.headerOffset ) {
+				headerMode = 'in';
+			}
+			else {
+				headerMode = 'below';
+			}
+
+			if ( forceChange || headerMode !== this.s.headerMode ) {
+				this._modeChange( headerMode, 'header', forceChange );
+			}
+
+			this._horizontal( 'header', windowLeft );
+		}
+
+		if ( this.c.footer && this.dom.tfoot.length ) {
+			if ( ! position.visible || windowTop + position.windowHeight >= position.tfootBottom + this.c.footerOffset ) {
+				footerMode = 'in-place';
+			}
+			else if ( position.windowHeight + windowTop > position.tbodyTop + position.tfootHeight + this.c.footerOffset ) {
+				footerMode = 'in';
+			}
+			else {
+				footerMode = 'above';
+			}
+
+			if ( forceChange || footerMode !== this.s.footerMode ) {
+				this._modeChange( footerMode, 'footer', forceChange );
+			}
+
+			this._horizontal( 'footer', windowLeft );
+		}
+	}
+} );
+
+
+/**
+ * Version
+ * @type {String}
+ * @static
+ */
+FixedHeader.version = "3.1.2";
+
+/**
+ * Defaults
+ * @type {Object}
+ * @static
+ */
+FixedHeader.defaults = {
+	header: true,
+	footer: false,
+	headerOffset: 0,
+	footerOffset: 0
+};
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables interfaces
+ */
+
+// Attach for constructor access
+$.fn.dataTable.FixedHeader = FixedHeader;
+$.fn.DataTable.FixedHeader = FixedHeader;
+
+
+// DataTables creation - check if the FixedHeader option has been defined on the
+// table and if so, initialise
+$(document).on( 'init.dt.dtfh', function (e, settings, json) {
+	if ( e.namespace !== 'dt' ) {
+		return;
+	}
+
+	var init = settings.oInit.fixedHeader;
+	var defaults = DataTable.defaults.fixedHeader;
+
+	if ( (init || defaults) && ! settings._fixedHeader ) {
+		var opts = $.extend( {}, defaults, init );
+
+		if ( init !== false ) {
+			new FixedHeader( settings, opts );
+		}
+	}
+} );
+
+// DataTables API methods
+DataTable.Api.register( 'fixedHeader()', function () {} );
+
+DataTable.Api.register( 'fixedHeader.adjust()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		var fh = ctx._fixedHeader;
+
+		if ( fh ) {
+			fh.update();
+		}
+	} );
+} );
+
+DataTable.Api.register( 'fixedHeader.enable()', function ( flag ) {
+	return this.iterator( 'table', function ( ctx ) {
+		var fh = ctx._fixedHeader;
+
+		if ( fh ) {
+			fh.enable( flag !== undefined ? flag : true );
+		}
+	} );
+} );
+
+DataTable.Api.register( 'fixedHeader.disable()', function ( ) {
+	return this.iterator( 'table', function ( ctx ) {
+		var fh = ctx._fixedHeader;
+
+		if ( fh ) {
+			fh.enable( false );
+		}
+	} );
+} );
+
+$.each( ['header', 'footer'], function ( i, el ) {
+	DataTable.Api.register( 'fixedHeader.'+el+'Offset()', function ( offset ) {
+		var ctx = this.context;
+
+		if ( offset === undefined ) {
+			return ctx.length && ctx[0]._fixedHeader ?
+				ctx[0]._fixedHeader[el +'Offset']() :
+				undefined;
+		}
+
+		return this.iterator( 'table', function ( ctx ) {
+			var fh = ctx._fixedHeader;
+
+			if ( fh ) {
+				fh[ el +'Offset' ]( offset );
+			}
+		} );
+	} );
+} );
+
+
+return FixedHeader;
+}));
diff --git a/static/DataTables/FixedHeader-3.1.2/js/dataTables.fixedHeader.min.js b/static/DataTables/FixedHeader-3.1.2/js/dataTables.fixedHeader.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..80a8a4cbae36b99a33b8a792645b4c241503ff55
--- /dev/null
+++ b/static/DataTables/FixedHeader-3.1.2/js/dataTables.fixedHeader.min.js
@@ -0,0 +1,17 @@
+/*!
+ FixedHeader 3.1.2
+ ©2009-2016 SpryMedia Ltd - datatables.net/license
+*/
+(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(g){return d(g,window,document)}):"object"===typeof exports?module.exports=function(g,h){g||(g=window);if(!h||!h.fn.dataTable)h=require("datatables.net")(g,h).$;return d(h,g,g.document)}:d(jQuery,window,document)})(function(d,g,h,k){var j=d.fn.dataTable,l=0,i=function(b,a){if(!(this instanceof i))throw"FixedHeader must be initialised with the 'new' keyword.";!0===a&&(a={});b=new j.Api(b);this.c=d.extend(!0,
+{},i.defaults,a);this.s={dt:b,position:{theadTop:0,tbodyTop:0,tfootTop:0,tfootBottom:0,width:0,left:0,tfootHeight:0,theadHeight:0,windowHeight:d(g).height(),visible:!0},headerMode:null,footerMode:null,autoWidth:b.settings()[0].oFeatures.bAutoWidth,namespace:".dtfc"+l++,scrollLeft:{header:-1,footer:-1},enable:!0};this.dom={floatingHeader:null,thead:d(b.table().header()),tbody:d(b.table().body()),tfoot:d(b.table().footer()),header:{host:null,floating:null,placeholder:null},footer:{host:null,floating:null,
+placeholder:null}};this.dom.header.host=this.dom.thead.parent();this.dom.footer.host=this.dom.tfoot.parent();var e=b.settings()[0];if(e._fixedHeader)throw"FixedHeader already initialised on table "+e.nTable.id;e._fixedHeader=this;this._constructor()};d.extend(i.prototype,{enable:function(b){this.s.enable=b;this.c.header&&this._modeChange("in-place","header",!0);this.c.footer&&this.dom.tfoot.length&&this._modeChange("in-place","footer",!0);this.update()},headerOffset:function(b){b!==k&&(this.c.headerOffset=
+b,this.update());return this.c.headerOffset},footerOffset:function(b){b!==k&&(this.c.footerOffset=b,this.update());return this.c.footerOffset},update:function(){this._positions();this._scroll(!0)},_constructor:function(){var b=this,a=this.s.dt;d(g).on("scroll"+this.s.namespace,function(){b._scroll()}).on("resize"+this.s.namespace,function(){b.s.position.windowHeight=d(g).height();b.update()});var e=d(".fh-fixedHeader");!this.c.headerOffset&&e.length&&(this.c.headerOffset=e.outerHeight());e=d(".fh-fixedFooter");
+!this.c.footerOffset&&e.length&&(this.c.footerOffset=e.outerHeight());a.on("column-reorder.dt.dtfc column-visibility.dt.dtfc draw.dt.dtfc column-sizing.dt.dtfc",function(){b.update()});a.on("destroy.dtfc",function(){a.off(".dtfc");d(g).off(b.s.namespace)});this._positions();this._scroll()},_clone:function(b,a){var e=this.s.dt,c=this.dom[b],f="header"===b?this.dom.thead:this.dom.tfoot;!a&&c.floating?c.floating.removeClass("fixedHeader-floating fixedHeader-locked"):(c.floating&&(c.placeholder.remove(),
+this._unsize(b),c.floating.children().detach(),c.floating.remove()),c.floating=d(e.table().node().cloneNode(!1)).css("table-layout","fixed").removeAttr("id").append(f).appendTo("body"),c.placeholder=f.clone(!1),c.host.prepend(c.placeholder),this._matchWidths(c.placeholder,c.floating))},_matchWidths:function(b,a){var e=function(a){return d(a,b).map(function(){return d(this).width()}).toArray()},c=function(b,c){d(b,a).each(function(a){d(this).css({width:c[a],minWidth:c[a]})})},f=e("th"),e=e("td");c("th",
+f);c("td",e)},_unsize:function(b){var a=this.dom[b].floating;a&&("footer"===b||"header"===b&&!this.s.autoWidth)?d("th, td",a).css({width:"",minWidth:""}):a&&"header"===b&&d("th, td",a).css("min-width","")},_horizontal:function(b,a){var e=this.dom[b],c=this.s.position,d=this.s.scrollLeft;e.floating&&d[b]!==a&&(e.floating.css("left",c.left-a),d[b]=a)},_modeChange:function(b,a,e){var c=this.dom[a],f=this.s.position,g=d.contains(this.dom["footer"===a?"tfoot":"thead"][0],h.activeElement)?h.activeElement:
+null;if("in-place"===b){if(c.placeholder&&(c.placeholder.remove(),c.placeholder=null),this._unsize(a),"header"===a?c.host.prepend(this.dom.thead):c.host.append(this.dom.tfoot),c.floating)c.floating.remove(),c.floating=null}else"in"===b?(this._clone(a,e),c.floating.addClass("fixedHeader-floating").css("header"===a?"top":"bottom",this.c[a+"Offset"]).css("left",f.left+"px").css("width",f.width+"px"),"footer"===a&&c.floating.css("top","")):"below"===b?(this._clone(a,e),c.floating.addClass("fixedHeader-locked").css("top",
+f.tfootTop-f.theadHeight).css("left",f.left+"px").css("width",f.width+"px")):"above"===b&&(this._clone(a,e),c.floating.addClass("fixedHeader-locked").css("top",f.tbodyTop).css("left",f.left+"px").css("width",f.width+"px"));g&&g!==h.activeElement&&g.focus();this.s.scrollLeft.header=-1;this.s.scrollLeft.footer=-1;this.s[a+"Mode"]=b},_positions:function(){var b=this.s.dt.table(),a=this.s.position,e=this.dom,b=d(b.node()),c=b.children("thead"),f=b.children("tfoot"),e=e.tbody;a.visible=b.is(":visible");
+a.width=b.outerWidth();a.left=b.offset().left;a.theadTop=c.offset().top;a.tbodyTop=e.offset().top;a.theadHeight=a.tbodyTop-a.theadTop;f.length?(a.tfootTop=f.offset().top,a.tfootBottom=a.tfootTop+f.outerHeight(),a.tfootHeight=a.tfootBottom-a.tfootTop):(a.tfootTop=a.tbodyTop+e.outerHeight(),a.tfootBottom=a.tfootTop,a.tfootHeight=a.tfootTop)},_scroll:function(b){var a=d(h).scrollTop(),e=d(h).scrollLeft(),c=this.s.position,f;if(this.s.enable&&(this.c.header&&(f=!c.visible||a<=c.theadTop-this.c.headerOffset?
+"in-place":a<=c.tfootTop-c.theadHeight-this.c.headerOffset?"in":"below",(b||f!==this.s.headerMode)&&this._modeChange(f,"header",b),this._horizontal("header",e)),this.c.footer&&this.dom.tfoot.length))a=!c.visible||a+c.windowHeight>=c.tfootBottom+this.c.footerOffset?"in-place":c.windowHeight+a>c.tbodyTop+c.tfootHeight+this.c.footerOffset?"in":"above",(b||a!==this.s.footerMode)&&this._modeChange(a,"footer",b),this._horizontal("footer",e)}});i.version="3.1.2";i.defaults={header:!0,footer:!1,headerOffset:0,
+footerOffset:0};d.fn.dataTable.FixedHeader=i;d.fn.DataTable.FixedHeader=i;d(h).on("init.dt.dtfh",function(b,a){if("dt"===b.namespace){var e=a.oInit.fixedHeader,c=j.defaults.fixedHeader;if((e||c)&&!a._fixedHeader)c=d.extend({},c,e),!1!==e&&new i(a,c)}});j.Api.register("fixedHeader()",function(){});j.Api.register("fixedHeader.adjust()",function(){return this.iterator("table",function(b){(b=b._fixedHeader)&&b.update()})});j.Api.register("fixedHeader.enable()",function(b){return this.iterator("table",
+function(a){(a=a._fixedHeader)&&a.enable(b!==k?b:!0)})});j.Api.register("fixedHeader.disable()",function(){return this.iterator("table",function(b){(b=b._fixedHeader)&&b.enable(!1)})});d.each(["header","footer"],function(b,a){j.Api.register("fixedHeader."+a+"Offset()",function(b){var c=this.context;return b===k?c.length&&c[0]._fixedHeader?c[0]._fixedHeader[a+"Offset"]():k:this.iterator("table",function(c){if(c=c._fixedHeader)c[a+"Offset"](b)})})});return i});
diff --git a/static/DataTables/Responsive-2.1.1/css/responsive.bootstrap.css b/static/DataTables/Responsive-2.1.1/css/responsive.bootstrap.css
new file mode 100644
index 0000000000000000000000000000000000000000..aa2fb3e0fc1a8841151403e27638f4155feb39fc
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/css/responsive.bootstrap.css
@@ -0,0 +1,181 @@
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th.child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty {
+  cursor: default !important;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th.child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty:before {
+  display: none !important;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td:first-child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th:first-child {
+  position: relative;
+  padding-left: 30px;
+  cursor: pointer;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td:first-child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th:first-child:before {
+  top: 9px;
+  left: 4px;
+  height: 14px;
+  width: 14px;
+  display: block;
+  position: absolute;
+  color: white;
+  border: 2px solid white;
+  border-radius: 14px;
+  box-shadow: 0 0 3px #444;
+  box-sizing: content-box;
+  text-align: center;
+  font-family: 'Courier New', Courier, monospace;
+  line-height: 14px;
+  content: '+';
+  background-color: #337ab7;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr.parent > td:first-child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr.parent > th:first-child:before {
+  content: '-';
+  background-color: #d33333;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr.child td:before {
+  display: none;
+}
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td:first-child,
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th:first-child {
+  padding-left: 27px;
+}
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td:first-child:before,
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th:first-child:before {
+  top: 5px;
+  left: 4px;
+  height: 14px;
+  width: 14px;
+  border-radius: 14px;
+  line-height: 14px;
+  text-indent: 3px;
+}
+table.dataTable.dtr-column > tbody > tr > td.control,
+table.dataTable.dtr-column > tbody > tr > th.control {
+  position: relative;
+  cursor: pointer;
+}
+table.dataTable.dtr-column > tbody > tr > td.control:before,
+table.dataTable.dtr-column > tbody > tr > th.control:before {
+  top: 50%;
+  left: 50%;
+  height: 16px;
+  width: 16px;
+  margin-top: -10px;
+  margin-left: -10px;
+  display: block;
+  position: absolute;
+  color: white;
+  border: 2px solid white;
+  border-radius: 14px;
+  box-shadow: 0 0 3px #444;
+  box-sizing: content-box;
+  text-align: center;
+  font-family: 'Courier New', Courier, monospace;
+  line-height: 14px;
+  content: '+';
+  background-color: #337ab7;
+}
+table.dataTable.dtr-column > tbody > tr.parent td.control:before,
+table.dataTable.dtr-column > tbody > tr.parent th.control:before {
+  content: '-';
+  background-color: #d33333;
+}
+table.dataTable > tbody > tr.child {
+  padding: 0.5em 1em;
+}
+table.dataTable > tbody > tr.child:hover {
+  background: transparent !important;
+}
+table.dataTable > tbody > tr.child ul.dtr-details {
+  display: inline-block;
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li {
+  border-bottom: 1px solid #efefef;
+  padding: 0.5em 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li:first-child {
+  padding-top: 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li:last-child {
+  border-bottom: none;
+}
+table.dataTable > tbody > tr.child span.dtr-title {
+  display: inline-block;
+  min-width: 75px;
+  font-weight: bold;
+}
+
+div.dtr-modal {
+  position: fixed;
+  box-sizing: border-box;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+  z-index: 100;
+  padding: 10em 1em;
+}
+div.dtr-modal div.dtr-modal-display {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  width: 50%;
+  height: 50%;
+  overflow: auto;
+  margin: auto;
+  z-index: 102;
+  overflow: auto;
+  background-color: #f5f5f7;
+  border: 1px solid black;
+  border-radius: 0.5em;
+  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
+}
+div.dtr-modal div.dtr-modal-content {
+  position: relative;
+  padding: 1em;
+}
+div.dtr-modal div.dtr-modal-close {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  width: 22px;
+  height: 22px;
+  border: 1px solid #eaeaea;
+  background-color: #f9f9f9;
+  text-align: center;
+  border-radius: 3px;
+  cursor: pointer;
+  z-index: 12;
+}
+div.dtr-modal div.dtr-modal-close:hover {
+  background-color: #eaeaea;
+}
+div.dtr-modal div.dtr-modal-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 101;
+  background: rgba(0, 0, 0, 0.6);
+}
+
+@media screen and (max-width: 767px) {
+  div.dtr-modal div.dtr-modal-display {
+    width: 95%;
+  }
+}
+div.dtr-bs-modal table.table tr:first-child td {
+  border-top: none;
+}
diff --git a/static/DataTables/Responsive-2.1.1/css/responsive.bootstrap.min.css b/static/DataTables/Responsive-2.1.1/css/responsive.bootstrap.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..b53db0e0fb8601755ab1bda31d16863ce25cc4c4
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/css/responsive.bootstrap.min.css
@@ -0,0 +1 @@
+table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child:before{top:9px;left:4px;height:14px;width:14px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#337ab7}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed>tbody>tr.child td:before{display:none}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child:before{top:5px;left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#337ab7}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable>tbody>tr.child{padding:0.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable>tbody>tr.child ul.dtr-details li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:0.5em;box-shadow:0 12px 30px rgba(0,0,0,0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0,0,0,0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}div.dtr-bs-modal table.table tr:first-child td{border-top:none}
diff --git a/static/DataTables/Responsive-2.1.1/css/responsive.dataTables.css b/static/DataTables/Responsive-2.1.1/css/responsive.dataTables.css
new file mode 100644
index 0000000000000000000000000000000000000000..72353d2afc63b4d0f96b059527da0cadf05bc7e1
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/css/responsive.dataTables.css
@@ -0,0 +1,178 @@
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th.child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty {
+  cursor: default !important;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th.child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty:before {
+  display: none !important;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td:first-child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th:first-child {
+  position: relative;
+  padding-left: 30px;
+  cursor: pointer;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td:first-child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th:first-child:before {
+  top: 9px;
+  left: 4px;
+  height: 14px;
+  width: 14px;
+  display: block;
+  position: absolute;
+  color: white;
+  border: 2px solid white;
+  border-radius: 14px;
+  box-shadow: 0 0 3px #444;
+  box-sizing: content-box;
+  text-align: center;
+  font-family: 'Courier New', Courier, monospace;
+  line-height: 14px;
+  content: '+';
+  background-color: #31b131;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr.parent > td:first-child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr.parent > th:first-child:before {
+  content: '-';
+  background-color: #d33333;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr.child td:before {
+  display: none;
+}
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td:first-child,
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th:first-child {
+  padding-left: 27px;
+}
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td:first-child:before,
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th:first-child:before {
+  top: 5px;
+  left: 4px;
+  height: 14px;
+  width: 14px;
+  border-radius: 14px;
+  line-height: 14px;
+  text-indent: 3px;
+}
+table.dataTable.dtr-column > tbody > tr > td.control,
+table.dataTable.dtr-column > tbody > tr > th.control {
+  position: relative;
+  cursor: pointer;
+}
+table.dataTable.dtr-column > tbody > tr > td.control:before,
+table.dataTable.dtr-column > tbody > tr > th.control:before {
+  top: 50%;
+  left: 50%;
+  height: 16px;
+  width: 16px;
+  margin-top: -10px;
+  margin-left: -10px;
+  display: block;
+  position: absolute;
+  color: white;
+  border: 2px solid white;
+  border-radius: 14px;
+  box-shadow: 0 0 3px #444;
+  box-sizing: content-box;
+  text-align: center;
+  font-family: 'Courier New', Courier, monospace;
+  line-height: 14px;
+  content: '+';
+  background-color: #31b131;
+}
+table.dataTable.dtr-column > tbody > tr.parent td.control:before,
+table.dataTable.dtr-column > tbody > tr.parent th.control:before {
+  content: '-';
+  background-color: #d33333;
+}
+table.dataTable > tbody > tr.child {
+  padding: 0.5em 1em;
+}
+table.dataTable > tbody > tr.child:hover {
+  background: transparent !important;
+}
+table.dataTable > tbody > tr.child ul.dtr-details {
+  display: inline-block;
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li {
+  border-bottom: 1px solid #efefef;
+  padding: 0.5em 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li:first-child {
+  padding-top: 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li:last-child {
+  border-bottom: none;
+}
+table.dataTable > tbody > tr.child span.dtr-title {
+  display: inline-block;
+  min-width: 75px;
+  font-weight: bold;
+}
+
+div.dtr-modal {
+  position: fixed;
+  box-sizing: border-box;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+  z-index: 100;
+  padding: 10em 1em;
+}
+div.dtr-modal div.dtr-modal-display {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  width: 50%;
+  height: 50%;
+  overflow: auto;
+  margin: auto;
+  z-index: 102;
+  overflow: auto;
+  background-color: #f5f5f7;
+  border: 1px solid black;
+  border-radius: 0.5em;
+  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
+}
+div.dtr-modal div.dtr-modal-content {
+  position: relative;
+  padding: 1em;
+}
+div.dtr-modal div.dtr-modal-close {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  width: 22px;
+  height: 22px;
+  border: 1px solid #eaeaea;
+  background-color: #f9f9f9;
+  text-align: center;
+  border-radius: 3px;
+  cursor: pointer;
+  z-index: 12;
+}
+div.dtr-modal div.dtr-modal-close:hover {
+  background-color: #eaeaea;
+}
+div.dtr-modal div.dtr-modal-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 101;
+  background: rgba(0, 0, 0, 0.6);
+}
+
+@media screen and (max-width: 767px) {
+  div.dtr-modal div.dtr-modal-display {
+    width: 95%;
+  }
+}
diff --git a/static/DataTables/Responsive-2.1.1/css/responsive.dataTables.min.css b/static/DataTables/Responsive-2.1.1/css/responsive.dataTables.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..db2f7d9c4eefd6e0f180309b99060361a096a25b
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/css/responsive.dataTables.min.css
@@ -0,0 +1 @@
+table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child:before{top:9px;left:4px;height:14px;width:14px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#31b131}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed>tbody>tr.child td:before{display:none}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child:before{top:5px;left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#31b131}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable>tbody>tr.child{padding:0.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable>tbody>tr.child ul.dtr-details li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:0.5em;box-shadow:0 12px 30px rgba(0,0,0,0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0,0,0,0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}
diff --git a/static/DataTables/Responsive-2.1.1/css/responsive.foundation.css b/static/DataTables/Responsive-2.1.1/css/responsive.foundation.css
new file mode 100644
index 0000000000000000000000000000000000000000..9d70328037bfc17695ffbe57826737f9fda26666
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/css/responsive.foundation.css
@@ -0,0 +1,181 @@
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th.child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty {
+  cursor: default !important;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th.child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty:before {
+  display: none !important;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td:first-child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th:first-child {
+  position: relative;
+  padding-left: 30px;
+  cursor: pointer;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td:first-child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th:first-child:before {
+  top: 9px;
+  left: 4px;
+  height: 14px;
+  width: 14px;
+  display: block;
+  position: absolute;
+  color: white;
+  border: 2px solid white;
+  border-radius: 14px;
+  box-shadow: 0 0 3px #444;
+  box-sizing: content-box;
+  text-align: center;
+  font-family: 'Courier New', Courier, monospace;
+  line-height: 14px;
+  content: '+';
+  background-color: #008CBA;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr.parent > td:first-child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr.parent > th:first-child:before {
+  content: '-';
+  background-color: #d33333;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr.child td:before {
+  display: none;
+}
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td:first-child,
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th:first-child {
+  padding-left: 27px;
+}
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td:first-child:before,
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th:first-child:before {
+  top: 5px;
+  left: 4px;
+  height: 14px;
+  width: 14px;
+  border-radius: 14px;
+  line-height: 14px;
+  text-indent: 3px;
+}
+table.dataTable.dtr-column > tbody > tr > td.control,
+table.dataTable.dtr-column > tbody > tr > th.control {
+  position: relative;
+  cursor: pointer;
+}
+table.dataTable.dtr-column > tbody > tr > td.control:before,
+table.dataTable.dtr-column > tbody > tr > th.control:before {
+  top: 50%;
+  left: 50%;
+  height: 16px;
+  width: 16px;
+  margin-top: -10px;
+  margin-left: -10px;
+  display: block;
+  position: absolute;
+  color: white;
+  border: 2px solid white;
+  border-radius: 14px;
+  box-shadow: 0 0 3px #444;
+  box-sizing: content-box;
+  text-align: center;
+  font-family: 'Courier New', Courier, monospace;
+  line-height: 14px;
+  content: '+';
+  background-color: #008CBA;
+}
+table.dataTable.dtr-column > tbody > tr.parent td.control:before,
+table.dataTable.dtr-column > tbody > tr.parent th.control:before {
+  content: '-';
+  background-color: #d33333;
+}
+table.dataTable > tbody > tr.child {
+  padding: 0.5em 1em;
+}
+table.dataTable > tbody > tr.child:hover {
+  background: transparent !important;
+}
+table.dataTable > tbody > tr.child ul.dtr-details {
+  display: inline-block;
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li {
+  border-bottom: 1px solid #efefef;
+  padding: 0.5em 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li:first-child {
+  padding-top: 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li:last-child {
+  border-bottom: none;
+}
+table.dataTable > tbody > tr.child span.dtr-title {
+  display: inline-block;
+  min-width: 75px;
+  font-weight: bold;
+}
+
+div.dtr-modal {
+  position: fixed;
+  box-sizing: border-box;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+  z-index: 100;
+  padding: 10em 1em;
+}
+div.dtr-modal div.dtr-modal-display {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  width: 50%;
+  height: 50%;
+  overflow: auto;
+  margin: auto;
+  z-index: 102;
+  overflow: auto;
+  background-color: #f5f5f7;
+  border: 1px solid black;
+  border-radius: 0.5em;
+  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
+}
+div.dtr-modal div.dtr-modal-content {
+  position: relative;
+  padding: 1em;
+}
+div.dtr-modal div.dtr-modal-close {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  width: 22px;
+  height: 22px;
+  border: 1px solid #eaeaea;
+  background-color: #f9f9f9;
+  text-align: center;
+  border-radius: 3px;
+  cursor: pointer;
+  z-index: 12;
+}
+div.dtr-modal div.dtr-modal-close:hover {
+  background-color: #eaeaea;
+}
+div.dtr-modal div.dtr-modal-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 101;
+  background: rgba(0, 0, 0, 0.6);
+}
+
+@media screen and (max-width: 767px) {
+  div.dtr-modal div.dtr-modal-display {
+    width: 95%;
+  }
+}
+table.dataTable > tbody > tr.child ul {
+  font-size: 1em;
+}
diff --git a/static/DataTables/Responsive-2.1.1/css/responsive.foundation.min.css b/static/DataTables/Responsive-2.1.1/css/responsive.foundation.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..e8842be3c600fdc1b8ffeee1c0db9c1cf7c54ab8
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/css/responsive.foundation.min.css
@@ -0,0 +1 @@
+table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child:before{top:9px;left:4px;height:14px;width:14px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#008CBA}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed>tbody>tr.child td:before{display:none}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child:before{top:5px;left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#008CBA}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable>tbody>tr.child{padding:0.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable>tbody>tr.child ul.dtr-details li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:0.5em;box-shadow:0 12px 30px rgba(0,0,0,0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0,0,0,0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}table.dataTable>tbody>tr.child ul{font-size:1em}
diff --git a/static/DataTables/Responsive-2.1.1/css/responsive.jqueryui.css b/static/DataTables/Responsive-2.1.1/css/responsive.jqueryui.css
new file mode 100644
index 0000000000000000000000000000000000000000..72353d2afc63b4d0f96b059527da0cadf05bc7e1
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/css/responsive.jqueryui.css
@@ -0,0 +1,178 @@
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th.child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty {
+  cursor: default !important;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th.child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty:before {
+  display: none !important;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td:first-child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th:first-child {
+  position: relative;
+  padding-left: 30px;
+  cursor: pointer;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td:first-child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th:first-child:before {
+  top: 9px;
+  left: 4px;
+  height: 14px;
+  width: 14px;
+  display: block;
+  position: absolute;
+  color: white;
+  border: 2px solid white;
+  border-radius: 14px;
+  box-shadow: 0 0 3px #444;
+  box-sizing: content-box;
+  text-align: center;
+  font-family: 'Courier New', Courier, monospace;
+  line-height: 14px;
+  content: '+';
+  background-color: #31b131;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr.parent > td:first-child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr.parent > th:first-child:before {
+  content: '-';
+  background-color: #d33333;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr.child td:before {
+  display: none;
+}
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td:first-child,
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th:first-child {
+  padding-left: 27px;
+}
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td:first-child:before,
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th:first-child:before {
+  top: 5px;
+  left: 4px;
+  height: 14px;
+  width: 14px;
+  border-radius: 14px;
+  line-height: 14px;
+  text-indent: 3px;
+}
+table.dataTable.dtr-column > tbody > tr > td.control,
+table.dataTable.dtr-column > tbody > tr > th.control {
+  position: relative;
+  cursor: pointer;
+}
+table.dataTable.dtr-column > tbody > tr > td.control:before,
+table.dataTable.dtr-column > tbody > tr > th.control:before {
+  top: 50%;
+  left: 50%;
+  height: 16px;
+  width: 16px;
+  margin-top: -10px;
+  margin-left: -10px;
+  display: block;
+  position: absolute;
+  color: white;
+  border: 2px solid white;
+  border-radius: 14px;
+  box-shadow: 0 0 3px #444;
+  box-sizing: content-box;
+  text-align: center;
+  font-family: 'Courier New', Courier, monospace;
+  line-height: 14px;
+  content: '+';
+  background-color: #31b131;
+}
+table.dataTable.dtr-column > tbody > tr.parent td.control:before,
+table.dataTable.dtr-column > tbody > tr.parent th.control:before {
+  content: '-';
+  background-color: #d33333;
+}
+table.dataTable > tbody > tr.child {
+  padding: 0.5em 1em;
+}
+table.dataTable > tbody > tr.child:hover {
+  background: transparent !important;
+}
+table.dataTable > tbody > tr.child ul.dtr-details {
+  display: inline-block;
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li {
+  border-bottom: 1px solid #efefef;
+  padding: 0.5em 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li:first-child {
+  padding-top: 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li:last-child {
+  border-bottom: none;
+}
+table.dataTable > tbody > tr.child span.dtr-title {
+  display: inline-block;
+  min-width: 75px;
+  font-weight: bold;
+}
+
+div.dtr-modal {
+  position: fixed;
+  box-sizing: border-box;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+  z-index: 100;
+  padding: 10em 1em;
+}
+div.dtr-modal div.dtr-modal-display {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  width: 50%;
+  height: 50%;
+  overflow: auto;
+  margin: auto;
+  z-index: 102;
+  overflow: auto;
+  background-color: #f5f5f7;
+  border: 1px solid black;
+  border-radius: 0.5em;
+  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
+}
+div.dtr-modal div.dtr-modal-content {
+  position: relative;
+  padding: 1em;
+}
+div.dtr-modal div.dtr-modal-close {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  width: 22px;
+  height: 22px;
+  border: 1px solid #eaeaea;
+  background-color: #f9f9f9;
+  text-align: center;
+  border-radius: 3px;
+  cursor: pointer;
+  z-index: 12;
+}
+div.dtr-modal div.dtr-modal-close:hover {
+  background-color: #eaeaea;
+}
+div.dtr-modal div.dtr-modal-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 101;
+  background: rgba(0, 0, 0, 0.6);
+}
+
+@media screen and (max-width: 767px) {
+  div.dtr-modal div.dtr-modal-display {
+    width: 95%;
+  }
+}
diff --git a/static/DataTables/Responsive-2.1.1/css/responsive.jqueryui.min.css b/static/DataTables/Responsive-2.1.1/css/responsive.jqueryui.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..db2f7d9c4eefd6e0f180309b99060361a096a25b
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/css/responsive.jqueryui.min.css
@@ -0,0 +1 @@
+table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child:before{top:9px;left:4px;height:14px;width:14px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#31b131}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed>tbody>tr.child td:before{display:none}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child:before{top:5px;left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#31b131}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable>tbody>tr.child{padding:0.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable>tbody>tr.child ul.dtr-details li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:0.5em;box-shadow:0 12px 30px rgba(0,0,0,0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0,0,0,0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}
diff --git a/static/DataTables/Responsive-2.1.1/js/dataTables.responsive.js b/static/DataTables/Responsive-2.1.1/js/dataTables.responsive.js
new file mode 100644
index 0000000000000000000000000000000000000000..a1bd5b66884e767db079b22f63ed409c30343e63
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/js/dataTables.responsive.js
@@ -0,0 +1,1255 @@
+/*! Responsive 2.1.1
+ * 2014-2016 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary     Responsive
+ * @description Responsive tables plug-in for DataTables
+ * @version     2.1.1
+ * @file        dataTables.responsive.js
+ * @author      SpryMedia Ltd (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ * @copyright   Copyright 2014-2016 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ *   MIT license - http://datatables.net/license/mit
+ *
+ * This source file 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 license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+/**
+ * Responsive is a plug-in for the DataTables library that makes use of
+ * DataTables' ability to change the visibility of columns, changing the
+ * visibility of columns so the displayed columns fit into the table container.
+ * The end result is that complex tables will be dynamically adjusted to fit
+ * into the viewport, be it on a desktop, tablet or mobile browser.
+ *
+ * Responsive for DataTables has two modes of operation, which can used
+ * individually or combined:
+ *
+ * * Class name based control - columns assigned class names that match the
+ *   breakpoint logic can be shown / hidden as required for each breakpoint.
+ * * Automatic control - columns are automatically hidden when there is no
+ *   room left to display them. Columns removed from the right.
+ *
+ * In additional to column visibility control, Responsive also has built into
+ * options to use DataTables' child row display to show / hide the information
+ * from the table that has been hidden. There are also two modes of operation
+ * for this child row display:
+ *
+ * * Inline - when the control element that the user can use to show / hide
+ *   child rows is displayed inside the first column of the table.
+ * * Column - where a whole column is dedicated to be the show / hide control.
+ *
+ * Initialisation of Responsive is performed by:
+ *
+ * * Adding the class `responsive` or `dt-responsive` to the table. In this case
+ *   Responsive will automatically be initialised with the default configuration
+ *   options when the DataTable is created.
+ * * Using the `responsive` option in the DataTables configuration options. This
+ *   can also be used to specify the configuration options, or simply set to
+ *   `true` to use the defaults.
+ *
+ *  @class
+ *  @param {object} settings DataTables settings object for the host table
+ *  @param {object} [opts] Configuration options
+ *  @requires jQuery 1.7+
+ *  @requires DataTables 1.10.3+
+ *
+ *  @example
+ *      $('#example').DataTable( {
+ *        responsive: true
+ *      } );
+ *    } );
+ */
+var Responsive = function ( settings, opts ) {
+	// Sanity check that we are using DataTables 1.10 or newer
+	if ( ! DataTable.versionCheck || ! DataTable.versionCheck( '1.10.3' ) ) {
+		throw 'DataTables Responsive requires DataTables 1.10.3 or newer';
+	}
+
+	this.s = {
+		dt: new DataTable.Api( settings ),
+		columns: [],
+		current: []
+	};
+
+	// Check if responsive has already been initialised on this table
+	if ( this.s.dt.settings()[0].responsive ) {
+		return;
+	}
+
+	// details is an object, but for simplicity the user can give it as a string
+	// or a boolean
+	if ( opts && typeof opts.details === 'string' ) {
+		opts.details = { type: opts.details };
+	}
+	else if ( opts && opts.details === false ) {
+		opts.details = { type: false };
+	}
+	else if ( opts && opts.details === true ) {
+		opts.details = { type: 'inline' };
+	}
+
+	this.c = $.extend( true, {}, Responsive.defaults, DataTable.defaults.responsive, opts );
+	settings.responsive = this;
+	this._constructor();
+};
+
+$.extend( Responsive.prototype, {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Constructor
+	 */
+
+	/**
+	 * Initialise the Responsive instance
+	 *
+	 * @private
+	 */
+	_constructor: function ()
+	{
+		var that = this;
+		var dt = this.s.dt;
+		var dtPrivateSettings = dt.settings()[0];
+		var oldWindowWidth = $(window).width();
+
+		dt.settings()[0]._responsive = this;
+
+		// Use DataTables' throttle function to avoid processor thrashing on
+		// resize
+		$(window).on( 'resize.dtr orientationchange.dtr', DataTable.util.throttle( function () {
+			// iOS has a bug whereby resize can fire when only scrolling
+			// See: http://stackoverflow.com/questions/8898412
+			var width = $(window).width();
+
+			if ( width !== oldWindowWidth ) {
+				that._resize();
+				oldWindowWidth = width;
+			}
+		} ) );
+
+		// DataTables doesn't currently trigger an event when a row is added, so
+		// we need to hook into its private API to enforce the hidden rows when
+		// new data is added
+		dtPrivateSettings.oApi._fnCallbackReg( dtPrivateSettings, 'aoRowCreatedCallback', function (tr, data, idx) {
+			if ( $.inArray( false, that.s.current ) !== -1 ) {
+				$('>td, >th', tr).each( function ( i ) {
+					var idx = dt.column.index( 'toData', i );
+
+					if ( that.s.current[idx] === false ) {
+						$(this).css('display', 'none');
+					}
+				} );
+			}
+		} );
+
+		// Destroy event handler
+		dt.on( 'destroy.dtr', function () {
+			dt.off( '.dtr' );
+			$( dt.table().body() ).off( '.dtr' );
+			$(window).off( 'resize.dtr orientationchange.dtr' );
+
+			// Restore the columns that we've hidden
+			$.each( that.s.current, function ( i, val ) {
+				if ( val === false ) {
+					that._setColumnVis( i, true );
+				}
+			} );
+		} );
+
+		// Reorder the breakpoints array here in case they have been added out
+		// of order
+		this.c.breakpoints.sort( function (a, b) {
+			return a.width < b.width ? 1 :
+				a.width > b.width ? -1 : 0;
+		} );
+
+		this._classLogic();
+		this._resizeAuto();
+
+		// Details handler
+		var details = this.c.details;
+
+		if ( details.type !== false ) {
+			that._detailsInit();
+
+			// DataTables will trigger this event on every column it shows and
+			// hides individually
+			dt.on( 'column-visibility.dtr', function (e, ctx, col, vis) {
+				that._classLogic();
+				that._resizeAuto();
+				that._resize();
+			} );
+
+			// Redraw the details box on each draw which will happen if the data
+			// has changed. This is used until DataTables implements a native
+			// `updated` event for rows
+			dt.on( 'draw.dtr', function () {
+				that._redrawChildren();
+			} );
+
+			$(dt.table().node()).addClass( 'dtr-'+details.type );
+		}
+
+		dt.on( 'column-reorder.dtr', function (e, settings, details) {
+			that._classLogic();
+			that._resizeAuto();
+			that._resize();
+		} );
+
+		// Change in column sizes means we need to calc
+		dt.on( 'column-sizing.dtr', function () {
+			that._resizeAuto();
+			that._resize();
+		});
+
+		// On Ajax reload we want to reopen any child rows which are displayed
+		// by responsive
+		dt.on( 'preXhr.dtr', function () {
+			var rowIds = [];
+			dt.rows().every( function () {
+				if ( this.child.isShown() ) {
+					rowIds.push( this.id(true) );
+				}
+			} );
+
+			dt.one( 'draw.dtr', function () {
+				dt.rows( rowIds ).every( function () {
+					that._detailsDisplay( this, false );
+				} );
+			} );
+		});
+
+		dt.on( 'init.dtr', function (e, settings, details) {
+			that._resizeAuto();
+			that._resize();
+
+			// If columns were hidden, then DataTables needs to adjust the
+			// column sizing
+			if ( $.inArray( false, that.s.current ) ) {
+				dt.columns.adjust();
+			}
+		} );
+
+		// First pass - draw the table for the current viewport size
+		this._resize();
+	},
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods
+	 */
+
+	/**
+	 * Calculate the visibility for the columns in a table for a given
+	 * breakpoint. The result is pre-determined based on the class logic if
+	 * class names are used to control all columns, but the width of the table
+	 * is also used if there are columns which are to be automatically shown
+	 * and hidden.
+	 *
+	 * @param  {string} breakpoint Breakpoint name to use for the calculation
+	 * @return {array} Array of boolean values initiating the visibility of each
+	 *   column.
+	 *  @private
+	 */
+	_columnsVisiblity: function ( breakpoint )
+	{
+		var dt = this.s.dt;
+		var columns = this.s.columns;
+		var i, ien;
+
+		// Create an array that defines the column ordering based first on the
+		// column's priority, and secondly the column index. This allows the
+		// columns to be removed from the right if the priority matches
+		var order = columns
+			.map( function ( col, idx ) {
+				return {
+					columnIdx: idx,
+					priority: col.priority
+				};
+			} )
+			.sort( function ( a, b ) {
+				if ( a.priority !== b.priority ) {
+					return a.priority - b.priority;
+				}
+				return a.columnIdx - b.columnIdx;
+			} );
+
+		// Class logic - determine which columns are in this breakpoint based
+		// on the classes. If no class control (i.e. `auto`) then `-` is used
+		// to indicate this to the rest of the function
+		var display = $.map( columns, function ( col ) {
+			return col.auto && col.minWidth === null ?
+				false :
+				col.auto === true ?
+					'-' :
+					$.inArray( breakpoint, col.includeIn ) !== -1;
+		} );
+
+		// Auto column control - first pass: how much width is taken by the
+		// ones that must be included from the non-auto columns
+		var requiredWidth = 0;
+		for ( i=0, ien=display.length ; i<ien ; i++ ) {
+			if ( display[i] === true ) {
+				requiredWidth += columns[i].minWidth;
+			}
+		}
+
+		// Second pass, use up any remaining width for other columns. For
+		// scrolling tables we need to subtract the width of the scrollbar. It
+		// may not be requires which makes this sub-optimal, but it would
+		// require another full redraw to make complete use of those extra few
+		// pixels
+		var scrolling = dt.settings()[0].oScroll;
+		var bar = scrolling.sY || scrolling.sX ? scrolling.iBarWidth : 0;
+		var widthAvailable = dt.table().container().offsetWidth - bar;
+		var usedWidth = widthAvailable - requiredWidth;
+
+		// Control column needs to always be included. This makes it sub-
+		// optimal in terms of using the available with, but to stop layout
+		// thrashing or overflow. Also we need to account for the control column
+		// width first so we know how much width is available for the other
+		// columns, since the control column might not be the first one shown
+		for ( i=0, ien=display.length ; i<ien ; i++ ) {
+			if ( columns[i].control ) {
+				usedWidth -= columns[i].minWidth;
+			}
+		}
+
+		// Allow columns to be shown (counting by priority and then right to
+		// left) until we run out of room
+		var empty = false;
+		for ( i=0, ien=order.length ; i<ien ; i++ ) {
+			var colIdx = order[i].columnIdx;
+
+			if ( display[colIdx] === '-' && ! columns[colIdx].control && columns[colIdx].minWidth ) {
+				// Once we've found a column that won't fit we don't let any
+				// others display either, or columns might disappear in the
+				// middle of the table
+				if ( empty || usedWidth - columns[colIdx].minWidth < 0 ) {
+					empty = true;
+					display[colIdx] = false;
+				}
+				else {
+					display[colIdx] = true;
+				}
+
+				usedWidth -= columns[colIdx].minWidth;
+			}
+		}
+
+		// Determine if the 'control' column should be shown (if there is one).
+		// This is the case when there is a hidden column (that is not the
+		// control column). The two loops look inefficient here, but they are
+		// trivial and will fly through. We need to know the outcome from the
+		// first , before the action in the second can be taken
+		var showControl = false;
+
+		for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+			if ( ! columns[i].control && ! columns[i].never && ! display[i] ) {
+				showControl = true;
+				break;
+			}
+		}
+
+		for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+			if ( columns[i].control ) {
+				display[i] = showControl;
+			}
+		}
+
+		// Finally we need to make sure that there is at least one column that
+		// is visible
+		if ( $.inArray( true, display ) === -1 ) {
+			display[0] = true;
+		}
+
+		return display;
+	},
+
+
+	/**
+	 * Create the internal `columns` array with information about the columns
+	 * for the table. This includes determining which breakpoints the column
+	 * will appear in, based upon class names in the column, which makes up the
+	 * vast majority of this method.
+	 *
+	 * @private
+	 */
+	_classLogic: function ()
+	{
+		var that = this;
+		var calc = {};
+		var breakpoints = this.c.breakpoints;
+		var dt = this.s.dt;
+		var columns = dt.columns().eq(0).map( function (i) {
+			var column = this.column(i);
+			var className = column.header().className;
+			var priority = dt.settings()[0].aoColumns[i].responsivePriority;
+
+			if ( priority === undefined ) {
+				var dataPriority = $(column.header()).data('priority');
+
+				priority = dataPriority !== undefined ?
+					dataPriority * 1 :
+					10000;
+			}
+
+			return {
+				className: className,
+				includeIn: [],
+				auto:      false,
+				control:   false,
+				never:     className.match(/\bnever\b/) ? true : false,
+				priority:  priority
+			};
+		} );
+
+		// Simply add a breakpoint to `includeIn` array, ensuring that there are
+		// no duplicates
+		var add = function ( colIdx, name ) {
+			var includeIn = columns[ colIdx ].includeIn;
+
+			if ( $.inArray( name, includeIn ) === -1 ) {
+				includeIn.push( name );
+			}
+		};
+
+		var column = function ( colIdx, name, operator, matched ) {
+			var size, i, ien;
+
+			if ( ! operator ) {
+				columns[ colIdx ].includeIn.push( name );
+			}
+			else if ( operator === 'max-' ) {
+				// Add this breakpoint and all smaller
+				size = that._find( name ).width;
+
+				for ( i=0, ien=breakpoints.length ; i<ien ; i++ ) {
+					if ( breakpoints[i].width <= size ) {
+						add( colIdx, breakpoints[i].name );
+					}
+				}
+			}
+			else if ( operator === 'min-' ) {
+				// Add this breakpoint and all larger
+				size = that._find( name ).width;
+
+				for ( i=0, ien=breakpoints.length ; i<ien ; i++ ) {
+					if ( breakpoints[i].width >= size ) {
+						add( colIdx, breakpoints[i].name );
+					}
+				}
+			}
+			else if ( operator === 'not-' ) {
+				// Add all but this breakpoint
+				for ( i=0, ien=breakpoints.length ; i<ien ; i++ ) {
+					if ( breakpoints[i].name.indexOf( matched ) === -1 ) {
+						add( colIdx, breakpoints[i].name );
+					}
+				}
+			}
+		};
+
+		// Loop over each column and determine if it has a responsive control
+		// class
+		columns.each( function ( col, i ) {
+			var classNames = col.className.split(' ');
+			var hasClass = false;
+
+			// Split the class name up so multiple rules can be applied if needed
+			for ( var k=0, ken=classNames.length ; k<ken ; k++ ) {
+				var className = $.trim( classNames[k] );
+
+				if ( className === 'all' ) {
+					// Include in all
+					hasClass = true;
+					col.includeIn = $.map( breakpoints, function (a) {
+						return a.name;
+					} );
+					return;
+				}
+				else if ( className === 'none' || col.never ) {
+					// Include in none (default) and no auto
+					hasClass = true;
+					return;
+				}
+				else if ( className === 'control' ) {
+					// Special column that is only visible, when one of the other
+					// columns is hidden. This is used for the details control
+					hasClass = true;
+					col.control = true;
+					return;
+				}
+
+				$.each( breakpoints, function ( j, breakpoint ) {
+					// Does this column have a class that matches this breakpoint?
+					var brokenPoint = breakpoint.name.split('-');
+					var re = new RegExp( '(min\\-|max\\-|not\\-)?('+brokenPoint[0]+')(\\-[_a-zA-Z0-9])?' );
+					var match = className.match( re );
+
+					if ( match ) {
+						hasClass = true;
+
+						if ( match[2] === brokenPoint[0] && match[3] === '-'+brokenPoint[1] ) {
+							// Class name matches breakpoint name fully
+							column( i, breakpoint.name, match[1], match[2]+match[3] );
+						}
+						else if ( match[2] === brokenPoint[0] && ! match[3] ) {
+							// Class name matched primary breakpoint name with no qualifier
+							column( i, breakpoint.name, match[1], match[2] );
+						}
+					}
+				} );
+			}
+
+			// If there was no control class, then automatic sizing is used
+			if ( ! hasClass ) {
+				col.auto = true;
+			}
+		} );
+
+		this.s.columns = columns;
+	},
+
+
+	/**
+	 * Show the details for the child row
+	 *
+	 * @param  {DataTables.Api} row    API instance for the row
+	 * @param  {boolean}        update Update flag
+	 * @private
+	 */
+	_detailsDisplay: function ( row, update )
+	{
+		var that = this;
+		var dt = this.s.dt;
+		var details = this.c.details;
+
+		if ( details && details.type !== false ) {
+			var res = details.display( row, update, function () {
+				return details.renderer(
+					dt, row[0], that._detailsObj(row[0])
+				);
+			} );
+
+			if ( res === true || res === false ) {
+				$(dt.table().node()).triggerHandler( 'responsive-display.dt', [dt, row, res, update] );
+			}
+		}
+	},
+
+
+	/**
+	 * Initialisation for the details handler
+	 *
+	 * @private
+	 */
+	_detailsInit: function ()
+	{
+		var that    = this;
+		var dt      = this.s.dt;
+		var details = this.c.details;
+
+		// The inline type always uses the first child as the target
+		if ( details.type === 'inline' ) {
+			details.target = 'td:first-child, th:first-child';
+		}
+
+		// Keyboard accessibility
+		dt.on( 'draw.dtr', function () {
+			that._tabIndexes();
+		} );
+		that._tabIndexes(); // Initial draw has already happened
+
+		$( dt.table().body() ).on( 'keyup.dtr', 'td, th', function (e) {
+			if ( e.keyCode === 13 && $(this).data('dtr-keyboard') ) {
+				$(this).click();
+			}
+		} );
+
+		// type.target can be a string jQuery selector or a column index
+		var target   = details.target;
+		var selector = typeof target === 'string' ? target : 'td, th';
+
+		// Click handler to show / hide the details rows when they are available
+		$( dt.table().body() )
+			.on( 'click.dtr mousedown.dtr mouseup.dtr', selector, function (e) {
+				// If the table is not collapsed (i.e. there is no hidden columns)
+				// then take no action
+				if ( ! $(dt.table().node()).hasClass('collapsed' ) ) {
+					return;
+				}
+
+				// Check that the row is actually a DataTable's controlled node
+				if ( $.inArray( $(this).closest('tr').get(0), dt.rows().nodes().toArray() ) === -1 ) {
+					return;
+				}
+
+				// For column index, we determine if we should act or not in the
+				// handler - otherwise it is already okay
+				if ( typeof target === 'number' ) {
+					var targetIdx = target < 0 ?
+						dt.columns().eq(0).length + target :
+						target;
+
+					if ( dt.cell( this ).index().column !== targetIdx ) {
+						return;
+					}
+				}
+
+				// $().closest() includes itself in its check
+				var row = dt.row( $(this).closest('tr') );
+
+				// Check event type to do an action
+				if ( e.type === 'click' ) {
+					// The renderer is given as a function so the caller can execute it
+					// only when they need (i.e. if hiding there is no point is running
+					// the renderer)
+					that._detailsDisplay( row, false );
+				}
+				else if ( e.type === 'mousedown' ) {
+					// For mouse users, prevent the focus ring from showing
+					$(this).css('outline', 'none');
+				}
+				else if ( e.type === 'mouseup' ) {
+					// And then re-allow at the end of the click
+					$(this).blur().css('outline', '');
+				}
+			} );
+	},
+
+
+	/**
+	 * Get the details to pass to a renderer for a row
+	 * @param  {int} rowIdx Row index
+	 * @private
+	 */
+	_detailsObj: function ( rowIdx )
+	{
+		var that = this;
+		var dt = this.s.dt;
+
+		return $.map( this.s.columns, function( col, i ) {
+			// Never and control columns should not be passed to the renderer
+			if ( col.never || col.control ) {
+				return;
+			}
+
+			return {
+				title:       dt.settings()[0].aoColumns[ i ].sTitle,
+				data:        dt.cell( rowIdx, i ).render( that.c.orthogonal ),
+				hidden:      dt.column( i ).visible() && !that.s.current[ i ],
+				columnIndex: i,
+				rowIndex:    rowIdx
+			};
+		} );
+	},
+
+
+	/**
+	 * Find a breakpoint object from a name
+	 *
+	 * @param  {string} name Breakpoint name to find
+	 * @return {object}      Breakpoint description object
+	 * @private
+	 */
+	_find: function ( name )
+	{
+		var breakpoints = this.c.breakpoints;
+
+		for ( var i=0, ien=breakpoints.length ; i<ien ; i++ ) {
+			if ( breakpoints[i].name === name ) {
+				return breakpoints[i];
+			}
+		}
+	},
+
+
+	/**
+	 * Re-create the contents of the child rows as the display has changed in
+	 * some way.
+	 *
+	 * @private
+	 */
+	_redrawChildren: function ()
+	{
+		var that = this;
+		var dt = this.s.dt;
+
+		dt.rows( {page: 'current'} ).iterator( 'row', function ( settings, idx ) {
+			var row = dt.row( idx );
+
+			that._detailsDisplay( dt.row( idx ), true );
+		} );
+	},
+
+
+	/**
+	 * Alter the table display for a resized viewport. This involves first
+	 * determining what breakpoint the window currently is in, getting the
+	 * column visibilities to apply and then setting them.
+	 *
+	 * @private
+	 */
+	_resize: function ()
+	{
+		var that = this;
+		var dt = this.s.dt;
+		var width = $(window).width();
+		var breakpoints = this.c.breakpoints;
+		var breakpoint = breakpoints[0].name;
+		var columns = this.s.columns;
+		var i, ien;
+		var oldVis = this.s.current.slice();
+
+		// Determine what breakpoint we are currently at
+		for ( i=breakpoints.length-1 ; i>=0 ; i-- ) {
+			if ( width <= breakpoints[i].width ) {
+				breakpoint = breakpoints[i].name;
+				break;
+			}
+		}
+		
+		// Show the columns for that break point
+		var columnsVis = this._columnsVisiblity( breakpoint );
+		this.s.current = columnsVis;
+
+		// Set the class before the column visibility is changed so event
+		// listeners know what the state is. Need to determine if there are
+		// any columns that are not visible but can be shown
+		var collapsedClass = false;
+		for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+			if ( columnsVis[i] === false && ! columns[i].never && ! columns[i].control ) {
+				collapsedClass = true;
+				break;
+			}
+		}
+
+		$( dt.table().node() ).toggleClass( 'collapsed', collapsedClass );
+
+		var changed = false;
+
+		dt.columns().eq(0).each( function ( colIdx, i ) {
+			if ( columnsVis[i] !== oldVis[i] ) {
+				changed = true;
+				that._setColumnVis( colIdx, columnsVis[i] );
+			}
+		} );
+
+		if ( changed ) {
+			this._redrawChildren();
+
+			// Inform listeners of the change
+			$(dt.table().node()).trigger( 'responsive-resize.dt', [dt, this.s.current] );
+		}
+	},
+
+
+	/**
+	 * Determine the width of each column in the table so the auto column hiding
+	 * has that information to work with. This method is never going to be 100%
+	 * perfect since column widths can change slightly per page, but without
+	 * seriously compromising performance this is quite effective.
+	 *
+	 * @private
+	 */
+	_resizeAuto: function ()
+	{
+		var dt = this.s.dt;
+		var columns = this.s.columns;
+
+		// Are we allowed to do auto sizing?
+		if ( ! this.c.auto ) {
+			return;
+		}
+
+		// Are there any columns that actually need auto-sizing, or do they all
+		// have classes defined
+		if ( $.inArray( true, $.map( columns, function (c) { return c.auto; } ) ) === -1 ) {
+			return;
+		}
+
+		// Clone the table with the current data in it
+		var tableWidth   = dt.table().node().offsetWidth;
+		var columnWidths = dt.columns;
+		var clonedTable  = dt.table().node().cloneNode( false );
+		var clonedHeader = $( dt.table().header().cloneNode( false ) ).appendTo( clonedTable );
+		var clonedBody   = $( dt.table().body() ).clone( false, false ).empty().appendTo( clonedTable ); // use jQuery because of IE8
+
+		// Header
+		var headerCells = dt.columns()
+			.header()
+			.filter( function (idx) {
+				return dt.column(idx).visible();
+			} )
+			.to$()
+			.clone( false )
+			.css( 'display', 'table-cell' );
+
+		// Body rows - we don't need to take account of DataTables' column
+		// visibility since we implement our own here (hence the `display` set)
+		$(clonedBody)
+			.append( $(dt.rows( { page: 'current' } ).nodes()).clone( false ) )
+			.find( 'th, td' ).css( 'display', '' );
+
+		// Footer
+		var footer = dt.table().footer();
+		if ( footer ) {
+			var clonedFooter = $( footer.cloneNode( false ) ).appendTo( clonedTable );
+			var footerCells = dt.columns()
+				.footer()
+				.filter( function (idx) {
+					return dt.column(idx).visible();
+				} )
+				.to$()
+				.clone( false )
+				.css( 'display', 'table-cell' );
+
+			$('<tr/>')
+				.append( footerCells )
+				.appendTo( clonedFooter );
+		}
+
+		$('<tr/>')
+			.append( headerCells )
+			.appendTo( clonedHeader );
+
+		// In the inline case extra padding is applied to the first column to
+		// give space for the show / hide icon. We need to use this in the
+		// calculation
+		if ( this.c.details.type === 'inline' ) {
+			$(clonedTable).addClass( 'dtr-inline collapsed' );
+		}
+		
+		// It is unsafe to insert elements with the same name into the DOM
+		// multiple times. For example, cloning and inserting a checked radio
+		// clears the chcecked state of the original radio.
+		$( clonedTable ).find( '[name]' ).removeAttr( 'name' );
+		
+		var inserted = $('<div/>')
+			.css( {
+				width: 1,
+				height: 1,
+				overflow: 'hidden'
+			} )
+			.append( clonedTable );
+
+		inserted.insertBefore( dt.table().node() );
+
+		// The cloned header now contains the smallest that each column can be
+		headerCells.each( function (i) {
+			var idx = dt.column.index( 'fromVisible', i );
+			columns[ idx ].minWidth =  this.offsetWidth || 0;
+		} );
+
+		inserted.remove();
+	},
+
+	/**
+	 * Set a column's visibility.
+	 *
+	 * We don't use DataTables' column visibility controls in order to ensure
+	 * that column visibility can Responsive can no-exist. Since only IE8+ is
+	 * supported (and all evergreen browsers of course) the control of the
+	 * display attribute works well.
+	 *
+	 * @param {integer} col      Column index
+	 * @param {boolean} showHide Show or hide (true or false)
+	 * @private
+	 */
+	_setColumnVis: function ( col, showHide )
+	{
+		var dt = this.s.dt;
+		var display = showHide ? '' : 'none'; // empty string will remove the attr
+
+		$( dt.column( col ).header() ).css( 'display', display );
+		$( dt.column( col ).footer() ).css( 'display', display );
+		dt.column( col ).nodes().to$().css( 'display', display );
+	},
+
+
+	/**
+	 * Update the cell tab indexes for keyboard accessibility. This is called on
+	 * every table draw - that is potentially inefficient, but also the least
+	 * complex option given that column visibility can change on the fly. Its a
+	 * shame user-focus was removed from CSS 3 UI, as it would have solved this
+	 * issue with a single CSS statement.
+	 *
+	 * @private
+	 */
+	_tabIndexes: function ()
+	{
+		var dt = this.s.dt;
+		var cells = dt.cells( { page: 'current' } ).nodes().to$();
+		var ctx = dt.settings()[0];
+		var target = this.c.details.target;
+
+		cells.filter( '[data-dtr-keyboard]' ).removeData( '[data-dtr-keyboard]' );
+
+		var selector = typeof target === 'number' ?
+			':eq('+target+')' :
+			target;
+
+		// This is a bit of a hack - we need to limit the selected nodes to just
+		// those of this table
+		if ( selector === 'td:first-child, th:first-child' ) {
+			selector = '>td:first-child, >th:first-child';
+		}
+
+		$( selector, dt.rows( { page: 'current' } ).nodes() )
+			.attr( 'tabIndex', ctx.iTabIndex )
+			.data( 'dtr-keyboard', 1 );
+	}
+} );
+
+
+/**
+ * List of default breakpoints. Each item in the array is an object with two
+ * properties:
+ *
+ * * `name` - the breakpoint name.
+ * * `width` - the breakpoint width
+ *
+ * @name Responsive.breakpoints
+ * @static
+ */
+Responsive.breakpoints = [
+	{ name: 'desktop',  width: Infinity },
+	{ name: 'tablet-l', width: 1024 },
+	{ name: 'tablet-p', width: 768 },
+	{ name: 'mobile-l', width: 480 },
+	{ name: 'mobile-p', width: 320 }
+];
+
+
+/**
+ * Display methods - functions which define how the hidden data should be shown
+ * in the table.
+ *
+ * @namespace
+ * @name Responsive.defaults
+ * @static
+ */
+Responsive.display = {
+	childRow: function ( row, update, render ) {
+		if ( update ) {
+			if ( $(row.node()).hasClass('parent') ) {
+				row.child( render(), 'child' ).show();
+
+				return true;
+			}
+		}
+		else {
+			if ( ! row.child.isShown()  ) {
+				row.child( render(), 'child' ).show();
+				$( row.node() ).addClass( 'parent' );
+
+				return true;
+			}
+			else {
+				row.child( false );
+				$( row.node() ).removeClass( 'parent' );
+
+				return false;
+			}
+		}
+	},
+
+	childRowImmediate: function ( row, update, render ) {
+		if ( (! update && row.child.isShown()) || ! row.responsive.hasHidden() ) {
+			// User interaction and the row is show, or nothing to show
+			row.child( false );
+			$( row.node() ).removeClass( 'parent' );
+
+			return false;
+		}
+		else {
+			// Display
+			row.child( render(), 'child' ).show();
+			$( row.node() ).addClass( 'parent' );
+
+			return true;
+		}
+	},
+
+	// This is a wrapper so the modal options for Bootstrap and jQuery UI can
+	// have options passed into them. This specific one doesn't need to be a
+	// function but it is for consistency in the `modal` name
+	modal: function ( options ) {
+		return function ( row, update, render ) {
+			if ( ! update ) {
+				// Show a modal
+				var close = function () {
+					modal.remove(); // will tidy events for us
+					$(document).off( 'keypress.dtr' );
+				};
+
+				var modal = $('<div class="dtr-modal"/>')
+					.append( $('<div class="dtr-modal-display"/>')
+						.append( $('<div class="dtr-modal-content"/>')
+							.append( render() )
+						)
+						.append( $('<div class="dtr-modal-close">&times;</div>' )
+							.click( function () {
+								close();
+							} )
+						)
+					)
+					.append( $('<div class="dtr-modal-background"/>')
+						.click( function () {
+							close();
+						} )
+					)
+					.appendTo( 'body' );
+
+				$(document).on( 'keyup.dtr', function (e) {
+					if ( e.keyCode === 27 ) {
+						e.stopPropagation();
+
+						close();
+					}
+				} );
+			}
+			else {
+				$('div.dtr-modal-content')
+					.empty()
+					.append( render() );
+			}
+
+			if ( options && options.header ) {
+				$('div.dtr-modal-content').prepend(
+					'<h2>'+options.header( row )+'</h2>'
+				);
+			}
+		};
+	}
+};
+
+
+/**
+ * Display methods - functions which define how the hidden data should be shown
+ * in the table.
+ *
+ * @namespace
+ * @name Responsive.defaults
+ * @static
+ */
+Responsive.renderer = {
+	listHidden: function () {
+		return function ( api, rowIdx, columns ) {
+			var data = $.map( columns, function ( col ) {
+				return col.hidden ?
+					'<li data-dtr-index="'+col.columnIndex+'" data-dt-row="'+col.rowIndex+'" data-dt-column="'+col.columnIndex+'">'+
+						'<span class="dtr-title">'+
+							col.title+
+						'</span> '+
+						'<span class="dtr-data">'+
+							col.data+
+						'</span>'+
+					'</li>' :
+					'';
+			} ).join('');
+
+			return data ?
+				$('<ul data-dtr-index="'+rowIdx+'" class="dtr-details"/>').append( data ) :
+				false;
+		}
+	},
+
+	tableAll: function ( options ) {
+		options = $.extend( {
+			tableClass: ''
+		}, options );
+
+		return function ( api, rowIdx, columns ) {
+			var data = $.map( columns, function ( col ) {
+				return '<tr data-dt-row="'+col.rowIndex+'" data-dt-column="'+col.columnIndex+'">'+
+						'<td>'+col.title+':'+'</td> '+
+						'<td>'+col.data+'</td>'+
+					'</tr>';
+			} ).join('');
+
+			return $('<table class="'+options.tableClass+' dtr-details" width="100%"/>').append( data );
+		}
+	}
+};
+
+/**
+ * Responsive default settings for initialisation
+ *
+ * @namespace
+ * @name Responsive.defaults
+ * @static
+ */
+Responsive.defaults = {
+	/**
+	 * List of breakpoints for the instance. Note that this means that each
+	 * instance can have its own breakpoints. Additionally, the breakpoints
+	 * cannot be changed once an instance has been creased.
+	 *
+	 * @type {Array}
+	 * @default Takes the value of `Responsive.breakpoints`
+	 */
+	breakpoints: Responsive.breakpoints,
+
+	/**
+	 * Enable / disable auto hiding calculations. It can help to increase
+	 * performance slightly if you disable this option, but all columns would
+	 * need to have breakpoint classes assigned to them
+	 *
+	 * @type {Boolean}
+	 * @default  `true`
+	 */
+	auto: true,
+
+	/**
+	 * Details control. If given as a string value, the `type` property of the
+	 * default object is set to that value, and the defaults used for the rest
+	 * of the object - this is for ease of implementation.
+	 *
+	 * The object consists of the following properties:
+	 *
+	 * * `display` - A function that is used to show and hide the hidden details
+	 * * `renderer` - function that is called for display of the child row data.
+	 *   The default function will show the data from the hidden columns
+	 * * `target` - Used as the selector for what objects to attach the child
+	 *   open / close to
+	 * * `type` - `false` to disable the details display, `inline` or `column`
+	 *   for the two control types
+	 *
+	 * @type {Object|string}
+	 */
+	details: {
+		display: Responsive.display.childRow,
+
+		renderer: Responsive.renderer.listHidden(),
+
+		target: 0,
+
+		type: 'inline'
+	},
+
+	/**
+	 * Orthogonal data request option. This is used to define the data type
+	 * requested when Responsive gets the data to show in the child row.
+	 *
+	 * @type {String}
+	 */
+	orthogonal: 'display'
+};
+
+
+/*
+ * API
+ */
+var Api = $.fn.dataTable.Api;
+
+// Doesn't do anything - work around for a bug in DT... Not documented
+Api.register( 'responsive()', function () {
+	return this;
+} );
+
+Api.register( 'responsive.index()', function ( li ) {
+	li = $(li);
+
+	return {
+		column: li.data('dtr-index'),
+		row:    li.parent().data('dtr-index')
+	};
+} );
+
+Api.register( 'responsive.rebuild()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		if ( ctx._responsive ) {
+			ctx._responsive._classLogic();
+		}
+	} );
+} );
+
+Api.register( 'responsive.recalc()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		if ( ctx._responsive ) {
+			ctx._responsive._resizeAuto();
+			ctx._responsive._resize();
+		}
+	} );
+} );
+
+Api.register( 'responsive.hasHidden()', function () {
+	var ctx = this.context[0];
+
+	return ctx._responsive ?
+		$.inArray( false, ctx._responsive.s.current ) !== -1 :
+		false;
+} );
+
+
+/**
+ * Version information
+ *
+ * @name Responsive.version
+ * @static
+ */
+Responsive.version = '2.1.1';
+
+
+$.fn.dataTable.Responsive = Responsive;
+$.fn.DataTable.Responsive = Responsive;
+
+// Attach a listener to the document which listens for DataTables initialisation
+// events so we can automatically initialise
+$(document).on( 'preInit.dt.dtr', function (e, settings, json) {
+	if ( e.namespace !== 'dt' ) {
+		return;
+	}
+
+	if ( $(settings.nTable).hasClass( 'responsive' ) ||
+		 $(settings.nTable).hasClass( 'dt-responsive' ) ||
+		 settings.oInit.responsive ||
+		 DataTable.defaults.responsive
+	) {
+		var init = settings.oInit.responsive;
+
+		if ( init !== false ) {
+			new Responsive( settings, $.isPlainObject( init ) ? init : {}  );
+		}
+	}
+} );
+
+
+return Responsive;
+}));
diff --git a/static/DataTables/Responsive-2.1.1/js/dataTables.responsive.min.js b/static/DataTables/Responsive-2.1.1/js/dataTables.responsive.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..2bbfabbbbd4a308bab4b617fc065e4c0b20d742c
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/js/dataTables.responsive.min.js
@@ -0,0 +1,26 @@
+/*!
+ Responsive 2.1.1
+ 2014-2016 SpryMedia Ltd - datatables.net/license
+*/
+(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(l){return c(l,window,document)}):"object"===typeof exports?module.exports=function(l,k){l||(l=window);if(!k||!k.fn.dataTable)k=require("datatables.net")(l,k).$;return c(k,l,l.document)}:c(jQuery,window,document)})(function(c,l,k,p){var m=c.fn.dataTable,j=function(b,a){if(!m.versionCheck||!m.versionCheck("1.10.3"))throw"DataTables Responsive requires DataTables 1.10.3 or newer";this.s={dt:new m.Api(b),columns:[],
+current:[]};this.s.dt.settings()[0].responsive||(a&&"string"===typeof a.details?a.details={type:a.details}:a&&!1===a.details?a.details={type:!1}:a&&!0===a.details&&(a.details={type:"inline"}),this.c=c.extend(!0,{},j.defaults,m.defaults.responsive,a),b.responsive=this,this._constructor())};c.extend(j.prototype,{_constructor:function(){var b=this,a=this.s.dt,d=a.settings()[0],e=c(l).width();a.settings()[0]._responsive=this;c(l).on("resize.dtr orientationchange.dtr",m.util.throttle(function(){var a=
+c(l).width();a!==e&&(b._resize(),e=a)}));d.oApi._fnCallbackReg(d,"aoRowCreatedCallback",function(e){-1!==c.inArray(!1,b.s.current)&&c(">td, >th",e).each(function(e){e=a.column.index("toData",e);!1===b.s.current[e]&&c(this).css("display","none")})});a.on("destroy.dtr",function(){a.off(".dtr");c(a.table().body()).off(".dtr");c(l).off("resize.dtr orientationchange.dtr");c.each(b.s.current,function(a,e){!1===e&&b._setColumnVis(a,!0)})});this.c.breakpoints.sort(function(a,b){return a.width<b.width?1:a.width>
+b.width?-1:0});this._classLogic();this._resizeAuto();d=this.c.details;!1!==d.type&&(b._detailsInit(),a.on("column-visibility.dtr",function(){b._classLogic();b._resizeAuto();b._resize()}),a.on("draw.dtr",function(){b._redrawChildren()}),c(a.table().node()).addClass("dtr-"+d.type));a.on("column-reorder.dtr",function(){b._classLogic();b._resizeAuto();b._resize()});a.on("column-sizing.dtr",function(){b._resizeAuto();b._resize()});a.on("preXhr.dtr",function(){var e=[];a.rows().every(function(){this.child.isShown()&&
+e.push(this.id(true))});a.one("draw.dtr",function(){a.rows(e).every(function(){b._detailsDisplay(this,false)})})});a.on("init.dtr",function(){b._resizeAuto();b._resize();c.inArray(false,b.s.current)&&a.columns.adjust()});this._resize()},_columnsVisiblity:function(b){var a=this.s.dt,d=this.s.columns,e,f,g=d.map(function(a,b){return{columnIdx:b,priority:a.priority}}).sort(function(a,b){return a.priority!==b.priority?a.priority-b.priority:a.columnIdx-b.columnIdx}),h=c.map(d,function(a){return a.auto&&
+null===a.minWidth?!1:!0===a.auto?"-":-1!==c.inArray(b,a.includeIn)}),n=0;e=0;for(f=h.length;e<f;e++)!0===h[e]&&(n+=d[e].minWidth);e=a.settings()[0].oScroll;e=e.sY||e.sX?e.iBarWidth:0;a=a.table().container().offsetWidth-e-n;e=0;for(f=h.length;e<f;e++)d[e].control&&(a-=d[e].minWidth);n=!1;e=0;for(f=g.length;e<f;e++){var i=g[e].columnIdx;"-"===h[i]&&(!d[i].control&&d[i].minWidth)&&(n||0>a-d[i].minWidth?(n=!0,h[i]=!1):h[i]=!0,a-=d[i].minWidth)}g=!1;e=0;for(f=d.length;e<f;e++)if(!d[e].control&&!d[e].never&&
+!h[e]){g=!0;break}e=0;for(f=d.length;e<f;e++)d[e].control&&(h[e]=g);-1===c.inArray(!0,h)&&(h[0]=!0);return h},_classLogic:function(){var b=this,a=this.c.breakpoints,d=this.s.dt,e=d.columns().eq(0).map(function(a){var b=this.column(a),e=b.header().className,a=d.settings()[0].aoColumns[a].responsivePriority;a===p&&(b=c(b.header()).data("priority"),a=b!==p?1*b:1E4);return{className:e,includeIn:[],auto:!1,control:!1,never:e.match(/\bnever\b/)?!0:!1,priority:a}}),f=function(a,b){var d=e[a].includeIn;-1===
+c.inArray(b,d)&&d.push(b)},g=function(c,d,i,g){if(i)if("max-"===i){g=b._find(d).width;d=0;for(i=a.length;d<i;d++)a[d].width<=g&&f(c,a[d].name)}else if("min-"===i){g=b._find(d).width;d=0;for(i=a.length;d<i;d++)a[d].width>=g&&f(c,a[d].name)}else{if("not-"===i){d=0;for(i=a.length;d<i;d++)-1===a[d].name.indexOf(g)&&f(c,a[d].name)}}else e[c].includeIn.push(d)};e.each(function(b,e){for(var d=b.className.split(" "),f=!1,j=0,l=d.length;j<l;j++){var k=c.trim(d[j]);if("all"===k){f=!0;b.includeIn=c.map(a,function(a){return a.name});
+return}if("none"===k||b.never){f=!0;return}if("control"===k){f=!0;b.control=!0;return}c.each(a,function(a,b){var d=b.name.split("-"),c=k.match(RegExp("(min\\-|max\\-|not\\-)?("+d[0]+")(\\-[_a-zA-Z0-9])?"));c&&(f=!0,c[2]===d[0]&&c[3]==="-"+d[1]?g(e,b.name,c[1],c[2]+c[3]):c[2]===d[0]&&!c[3]&&g(e,b.name,c[1],c[2]))})}f||(b.auto=!0)});this.s.columns=e},_detailsDisplay:function(b,a){var d=this,e=this.s.dt,f=this.c.details;if(f&&!1!==f.type){var g=f.display(b,a,function(){return f.renderer(e,b[0],d._detailsObj(b[0]))});
+(!0===g||!1===g)&&c(e.table().node()).triggerHandler("responsive-display.dt",[e,b,g,a])}},_detailsInit:function(){var b=this,a=this.s.dt,d=this.c.details;"inline"===d.type&&(d.target="td:first-child, th:first-child");a.on("draw.dtr",function(){b._tabIndexes()});b._tabIndexes();c(a.table().body()).on("keyup.dtr","td, th",function(b){b.keyCode===13&&c(this).data("dtr-keyboard")&&c(this).click()});var e=d.target;c(a.table().body()).on("click.dtr mousedown.dtr mouseup.dtr","string"===typeof e?e:"td, th",
+function(d){if(c(a.table().node()).hasClass("collapsed")&&c.inArray(c(this).closest("tr").get(0),a.rows().nodes().toArray())!==-1){if(typeof e==="number"){var g=e<0?a.columns().eq(0).length+e:e;if(a.cell(this).index().column!==g)return}g=a.row(c(this).closest("tr"));d.type==="click"?b._detailsDisplay(g,false):d.type==="mousedown"?c(this).css("outline","none"):d.type==="mouseup"&&c(this).blur().css("outline","")}})},_detailsObj:function(b){var a=this,d=this.s.dt;return c.map(this.s.columns,function(e,
+c){if(!e.never&&!e.control)return{title:d.settings()[0].aoColumns[c].sTitle,data:d.cell(b,c).render(a.c.orthogonal),hidden:d.column(c).visible()&&!a.s.current[c],columnIndex:c,rowIndex:b}})},_find:function(b){for(var a=this.c.breakpoints,d=0,c=a.length;d<c;d++)if(a[d].name===b)return a[d]},_redrawChildren:function(){var b=this,a=this.s.dt;a.rows({page:"current"}).iterator("row",function(c,e){a.row(e);b._detailsDisplay(a.row(e),!0)})},_resize:function(){var b=this,a=this.s.dt,d=c(l).width(),e=this.c.breakpoints,
+f=e[0].name,g=this.s.columns,h,j=this.s.current.slice();for(h=e.length-1;0<=h;h--)if(d<=e[h].width){f=e[h].name;break}var i=this._columnsVisiblity(f);this.s.current=i;e=!1;h=0;for(d=g.length;h<d;h++)if(!1===i[h]&&!g[h].never&&!g[h].control){e=!0;break}c(a.table().node()).toggleClass("collapsed",e);var k=!1;a.columns().eq(0).each(function(a,c){i[c]!==j[c]&&(k=!0,b._setColumnVis(a,i[c]))});k&&(this._redrawChildren(),c(a.table().node()).trigger("responsive-resize.dt",[a,this.s.current]))},_resizeAuto:function(){var b=
+this.s.dt,a=this.s.columns;if(this.c.auto&&-1!==c.inArray(!0,c.map(a,function(b){return b.auto}))){b.table().node();var d=b.table().node().cloneNode(!1),e=c(b.table().header().cloneNode(!1)).appendTo(d),f=c(b.table().body()).clone(!1,!1).empty().appendTo(d),g=b.columns().header().filter(function(a){return b.column(a).visible()}).to$().clone(!1).css("display","table-cell");c(f).append(c(b.rows({page:"current"}).nodes()).clone(!1)).find("th, td").css("display","");if(f=b.table().footer()){var f=c(f.cloneNode(!1)).appendTo(d),
+h=b.columns().footer().filter(function(a){return b.column(a).visible()}).to$().clone(!1).css("display","table-cell");c("<tr/>").append(h).appendTo(f)}c("<tr/>").append(g).appendTo(e);"inline"===this.c.details.type&&c(d).addClass("dtr-inline collapsed");c(d).find("[name]").removeAttr("name");d=c("<div/>").css({width:1,height:1,overflow:"hidden"}).append(d);d.insertBefore(b.table().node());g.each(function(c){c=b.column.index("fromVisible",c);a[c].minWidth=this.offsetWidth||0});d.remove()}},_setColumnVis:function(b,
+a){var d=this.s.dt,e=a?"":"none";c(d.column(b).header()).css("display",e);c(d.column(b).footer()).css("display",e);d.column(b).nodes().to$().css("display",e)},_tabIndexes:function(){var b=this.s.dt,a=b.cells({page:"current"}).nodes().to$(),d=b.settings()[0],e=this.c.details.target;a.filter("[data-dtr-keyboard]").removeData("[data-dtr-keyboard]");a="number"===typeof e?":eq("+e+")":e;"td:first-child, th:first-child"===a&&(a=">td:first-child, >th:first-child");c(a,b.rows({page:"current"}).nodes()).attr("tabIndex",
+d.iTabIndex).data("dtr-keyboard",1)}});j.breakpoints=[{name:"desktop",width:Infinity},{name:"tablet-l",width:1024},{name:"tablet-p",width:768},{name:"mobile-l",width:480},{name:"mobile-p",width:320}];j.display={childRow:function(b,a,d){if(a){if(c(b.node()).hasClass("parent"))return b.child(d(),"child").show(),!0}else{if(b.child.isShown())return b.child(!1),c(b.node()).removeClass("parent"),!1;b.child(d(),"child").show();c(b.node()).addClass("parent");return!0}},childRowImmediate:function(b,a,d){if(!a&&
+b.child.isShown()||!b.responsive.hasHidden())return b.child(!1),c(b.node()).removeClass("parent"),!1;b.child(d(),"child").show();c(b.node()).addClass("parent");return!0},modal:function(b){return function(a,d,e){if(d)c("div.dtr-modal-content").empty().append(e());else{var f=function(){g.remove();c(k).off("keypress.dtr")},g=c('<div class="dtr-modal"/>').append(c('<div class="dtr-modal-display"/>').append(c('<div class="dtr-modal-content"/>').append(e())).append(c('<div class="dtr-modal-close">&times;</div>').click(function(){f()}))).append(c('<div class="dtr-modal-background"/>').click(function(){f()})).appendTo("body");
+c(k).on("keyup.dtr",function(a){27===a.keyCode&&(a.stopPropagation(),f())})}b&&b.header&&c("div.dtr-modal-content").prepend("<h2>"+b.header(a)+"</h2>")}}};j.renderer={listHidden:function(){return function(b,a,d){return(b=c.map(d,function(a){return a.hidden?'<li data-dtr-index="'+a.columnIndex+'" data-dt-row="'+a.rowIndex+'" data-dt-column="'+a.columnIndex+'"><span class="dtr-title">'+a.title+'</span> <span class="dtr-data">'+a.data+"</span></li>":""}).join(""))?c('<ul data-dtr-index="'+a+'" class="dtr-details"/>').append(b):
+!1}},tableAll:function(b){b=c.extend({tableClass:""},b);return function(a,d,e){a=c.map(e,function(a){return'<tr data-dt-row="'+a.rowIndex+'" data-dt-column="'+a.columnIndex+'"><td>'+a.title+":</td> <td>"+a.data+"</td></tr>"}).join("");return c('<table class="'+b.tableClass+' dtr-details" width="100%"/>').append(a)}}};j.defaults={breakpoints:j.breakpoints,auto:!0,details:{display:j.display.childRow,renderer:j.renderer.listHidden(),target:0,type:"inline"},orthogonal:"display"};var o=c.fn.dataTable.Api;
+o.register("responsive()",function(){return this});o.register("responsive.index()",function(b){b=c(b);return{column:b.data("dtr-index"),row:b.parent().data("dtr-index")}});o.register("responsive.rebuild()",function(){return this.iterator("table",function(b){b._responsive&&b._responsive._classLogic()})});o.register("responsive.recalc()",function(){return this.iterator("table",function(b){b._responsive&&(b._responsive._resizeAuto(),b._responsive._resize())})});o.register("responsive.hasHidden()",function(){var b=
+this.context[0];return b._responsive?-1!==c.inArray(!1,b._responsive.s.current):!1});j.version="2.1.1";c.fn.dataTable.Responsive=j;c.fn.DataTable.Responsive=j;c(k).on("preInit.dt.dtr",function(b,a){if("dt"===b.namespace&&(c(a.nTable).hasClass("responsive")||c(a.nTable).hasClass("dt-responsive")||a.oInit.responsive||m.defaults.responsive)){var d=a.oInit.responsive;!1!==d&&new j(a,c.isPlainObject(d)?d:{})}});return j});
diff --git a/static/DataTables/Responsive-2.1.1/js/responsive.bootstrap.js b/static/DataTables/Responsive-2.1.1/js/responsive.bootstrap.js
new file mode 100644
index 0000000000000000000000000000000000000000..c275a0e0b2e34a108cede37c9b44073aa9443e68
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/js/responsive.bootstrap.js
@@ -0,0 +1,85 @@
+/*! Bootstrap integration for DataTables' Responsive
+ * ©2015-2016 SpryMedia Ltd - datatables.net/license
+ */
+
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net-bs', 'datatables.net-responsive'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net-bs')(root, $).$;
+			}
+
+			if ( ! $.fn.dataTable.Responsive ) {
+				require('datatables.net-responsive')(root, $);
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+var _display = DataTable.Responsive.display;
+var _original = _display.modal;
+var _modal = $(
+	'<div class="modal fade dtr-bs-modal" role="dialog">'+
+		'<div class="modal-dialog" role="document">'+
+			'<div class="modal-content">'+
+				'<div class="modal-header">'+
+					'<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>'+
+				'</div>'+
+				'<div class="modal-body"/>'+
+			'</div>'+
+		'</div>'+
+	'</div>'
+);
+
+_display.modal = function ( options ) {
+	return function ( row, update, render ) {
+		if ( ! $.fn.modal ) {
+			_original( row, update, render );
+		}
+		else {
+			if ( ! update ) {
+				if ( options && options.header ) {
+					var header = _modal.find('div.modal-header');
+					var button = header.find('button').detach();
+					
+					header
+						.empty()
+						.append( '<h4 class="modal-title">'+options.header( row )+'</h4>' )
+						.prepend( button );
+				}
+
+				_modal.find( 'div.modal-body' )
+					.empty()
+					.append( render() );
+
+				_modal
+					.appendTo( 'body' )
+					.modal();
+			}
+		}
+	};
+};
+
+
+return DataTable.Responsive;
+}));
diff --git a/static/DataTables/Responsive-2.1.1/js/responsive.bootstrap.min.js b/static/DataTables/Responsive-2.1.1/js/responsive.bootstrap.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..e77a6da5e4d0685f48412353bb171f088579ea81
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/js/responsive.bootstrap.min.js
@@ -0,0 +1,6 @@
+/*!
+ Bootstrap integration for DataTables' Responsive
+ ©2015-2016 SpryMedia Ltd - datatables.net/license
+*/
+(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs","datatables.net-responsive"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);if(!b||!b.fn.dataTable)b=require("datatables.net-bs")(a,b).$;b.fn.dataTable.Responsive||require("datatables.net-responsive")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c){var a=c.fn.dataTable,b=a.Responsive.display,g=b.modal,e=c('<div class="modal fade dtr-bs-modal" role="dialog"><div class="modal-dialog" role="document"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><div class="modal-body"/></div></div></div>');
+b.modal=function(a){return function(b,d,f){if(c.fn.modal){if(!d){if(a&&a.header){var d=e.find("div.modal-header"),h=d.find("button").detach();d.empty().append('<h4 class="modal-title">'+a.header(b)+"</h4>").prepend(h)}e.find("div.modal-body").empty().append(f());e.appendTo("body").modal()}}else g(b,d,f)}};return a.Responsive});
diff --git a/static/DataTables/Responsive-2.1.1/js/responsive.foundation.js b/static/DataTables/Responsive-2.1.1/js/responsive.foundation.js
new file mode 100644
index 0000000000000000000000000000000000000000..99e8689c02a615e108ab3eb3c32e842194eee60b
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/js/responsive.foundation.js
@@ -0,0 +1,62 @@
+/*! Foundation integration for DataTables' Responsive
+ * ©2015 SpryMedia Ltd - datatables.net/license
+ */
+
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net-zf', 'datatables.net-responsive'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net-zf')(root, $).$;
+			}
+
+			if ( ! $.fn.dataTable.Responsive ) {
+				require('datatables.net-responsive')(root, $);
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+var _display = DataTable.Responsive.display;
+var _original = _display.modal;
+
+_display.modal = function ( options ) {
+	return function ( row, update, render ) {
+		if ( ! $.fn.foundation ) {
+			_original( row, update, render );
+		}
+		else {
+			if ( ! update ) {
+				$( '<div class="reveal-modal" data-reveal/>' )
+					.append( '<a class="close-reveal-modal" aria-label="Close">&#215;</a>' )
+					.append( options && options.header ? '<h4>'+options.header( row )+'</h4>' : null )
+					.append( render() )
+					.appendTo( 'body' )
+					.foundation( 'reveal', 'open' );
+			}
+		}
+	};
+};
+
+
+return DataTable.Responsive;
+}));
diff --git a/static/DataTables/Responsive-2.1.1/js/responsive.foundation.min.js b/static/DataTables/Responsive-2.1.1/js/responsive.foundation.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..e48fea2191c9d94cf9d196931e1dac606b165c94
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/js/responsive.foundation.min.js
@@ -0,0 +1,6 @@
+/*!
+ Foundation integration for DataTables' Responsive
+ ©2015 SpryMedia Ltd - datatables.net/license
+*/
+(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-zf","datatables.net-responsive"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);if(!b||!b.fn.dataTable)b=require("datatables.net-zf")(a,b).$;b.fn.dataTable.Responsive||require("datatables.net-responsive")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c){var a=c.fn.dataTable,b=a.Responsive.display,f=b.modal;b.modal=function(a){return function(b,
+d,e){c.fn.foundation?d||c('<div class="reveal-modal" data-reveal/>').append('<a class="close-reveal-modal" aria-label="Close">&#215;</a>').append(a&&a.header?"<h4>"+a.header(b)+"</h4>":null).append(e()).appendTo("body").foundation("reveal","open"):f(b,d,e)}};return a.Responsive});
diff --git a/static/DataTables/Responsive-2.1.1/js/responsive.jqueryui.js b/static/DataTables/Responsive-2.1.1/js/responsive.jqueryui.js
new file mode 100644
index 0000000000000000000000000000000000000000..2c1d23d35df86b02db43785082ed6a23f3117cfa
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/js/responsive.jqueryui.js
@@ -0,0 +1,63 @@
+/*! jQuery UI integration for DataTables' Responsive
+ * ©2015 SpryMedia Ltd - datatables.net/license
+ */
+
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net-jqui', 'datatables.net-responsive'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net-jqui')(root, $).$;
+			}
+
+			if ( ! $.fn.dataTable.Responsive ) {
+				require('datatables.net-responsive')(root, $);
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+var _display = DataTable.Responsive.display;
+var _original = _display.modal;
+
+_display.modal = function ( options ) {
+	return function ( row, update, render ) {
+		if ( ! $.fn.dialog ) {
+			_original( row, update, render );
+		}
+		else {
+			if ( ! update ) {
+				$( '<div/>' )
+					.append( render() )
+					.appendTo( 'body' )
+					.dialog( $.extend( true, {
+						title: options && options.header ? options.header( row ) : '',
+						width: 500
+					}, options.dialog ) );
+			}
+		}
+	};
+};
+
+
+return DataTable.Responsive;
+}));
diff --git a/static/DataTables/Responsive-2.1.1/js/responsive.jqueryui.min.js b/static/DataTables/Responsive-2.1.1/js/responsive.jqueryui.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..e8f98dda4984b489e991961126bcdb5619392792
--- /dev/null
+++ b/static/DataTables/Responsive-2.1.1/js/responsive.jqueryui.min.js
@@ -0,0 +1,6 @@
+/*!
+ jQuery UI integration for DataTables' Responsive
+ ©2015 SpryMedia Ltd - datatables.net/license
+*/
+(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-jqui","datatables.net-responsive"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);if(!b||!b.fn.dataTable)b=require("datatables.net-jqui")(a,b).$;b.fn.dataTable.Responsive||require("datatables.net-responsive")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c){var a=c.fn.dataTable,b=a.Responsive.display,f=b.modal;b.modal=function(a){return function(b,
+d,e){c.fn.dialog?d||c("<div/>").append(e()).appendTo("body").dialog(c.extend(!0,{title:a&&a.header?a.header(b):"",width:500},a.dialog)):f(b,d,e)}};return a.Responsive});
diff --git a/static/DataTables/datatables.css b/static/DataTables/datatables.css
new file mode 100644
index 0000000000000000000000000000000000000000..44ad668c6fdc4f1a403813be528f1126346f70a5
--- /dev/null
+++ b/static/DataTables/datatables.css
@@ -0,0 +1,703 @@
+/*
+ * This combined file was created by the DataTables downloader builder:
+ *   https://datatables.net/download
+ *
+ * To rebuild or modify this file with the latest versions of the included
+ * software please visit:
+ *   https://datatables.net/download/#dt/dt-1.10.15/cr-1.3.3/fc-3.2.2/fh-3.1.2/r-2.1.1
+ *
+ * Included libraries:
+ *   DataTables 1.10.15, ColReorder 1.3.3, FixedColumns 3.2.2, FixedHeader 3.1.2, Responsive 2.1.1
+ */
+
+/*
+ * Table styles
+ */
+table.dataTable {
+  width: 100%;
+  margin: 0 auto;
+  clear: both;
+  border-collapse: separate;
+  border-spacing: 0;
+  /*
+   * Header and footer styles
+   */
+  /*
+   * Body styles
+   */
+}
+table.dataTable thead th,
+table.dataTable tfoot th {
+  font-weight: bold;
+}
+table.dataTable thead th,
+table.dataTable thead td {
+  padding: 10px 18px;
+  border-bottom: 1px solid #111;
+}
+table.dataTable thead th:active,
+table.dataTable thead td:active {
+  outline: none;
+}
+table.dataTable tfoot th,
+table.dataTable tfoot td {
+  padding: 10px 18px 6px 18px;
+  border-top: 1px solid #111;
+}
+table.dataTable thead .sorting,
+table.dataTable thead .sorting_asc,
+table.dataTable thead .sorting_desc,
+table.dataTable thead .sorting_asc_disabled,
+table.dataTable thead .sorting_desc_disabled {
+  cursor: pointer;
+  *cursor: hand;
+}
+table.dataTable thead .sorting,
+table.dataTable thead .sorting_asc,
+table.dataTable thead .sorting_desc,
+table.dataTable thead .sorting_asc_disabled,
+table.dataTable thead .sorting_desc_disabled {
+  background-repeat: no-repeat;
+  background-position: center right;
+}
+table.dataTable thead .sorting {
+  background-image: url("DataTables-1.10.15/images/sort_both.png");
+}
+table.dataTable thead .sorting_asc {
+  background-image: url("DataTables-1.10.15/images/sort_asc.png");
+}
+table.dataTable thead .sorting_desc {
+  background-image: url("DataTables-1.10.15/images/sort_desc.png");
+}
+table.dataTable thead .sorting_asc_disabled {
+  background-image: url("DataTables-1.10.15/images/sort_asc_disabled.png");
+}
+table.dataTable thead .sorting_desc_disabled {
+  background-image: url("DataTables-1.10.15/images/sort_desc_disabled.png");
+}
+table.dataTable tbody tr {
+  background-color: #ffffff;
+}
+table.dataTable tbody tr.selected {
+  background-color: #B0BED9;
+}
+table.dataTable tbody th,
+table.dataTable tbody td {
+  padding: 8px 10px;
+}
+table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td {
+  border-top: 1px solid #ddd;
+}
+table.dataTable.row-border tbody tr:first-child th,
+table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th,
+table.dataTable.display tbody tr:first-child td {
+  border-top: none;
+}
+table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td {
+  border-top: 1px solid #ddd;
+  border-right: 1px solid #ddd;
+}
+table.dataTable.cell-border tbody tr th:first-child,
+table.dataTable.cell-border tbody tr td:first-child {
+  border-left: 1px solid #ddd;
+}
+table.dataTable.cell-border tbody tr:first-child th,
+table.dataTable.cell-border tbody tr:first-child td {
+  border-top: none;
+}
+table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd {
+  background-color: #f9f9f9;
+}
+table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected {
+  background-color: #acbad4;
+}
+table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover {
+  background-color: #f6f6f6;
+}
+table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected {
+  background-color: #aab7d1;
+}
+table.dataTable.order-column tbody tr > .sorting_1,
+table.dataTable.order-column tbody tr > .sorting_2,
+table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1,
+table.dataTable.display tbody tr > .sorting_2,
+table.dataTable.display tbody tr > .sorting_3 {
+  background-color: #fafafa;
+}
+table.dataTable.order-column tbody tr.selected > .sorting_1,
+table.dataTable.order-column tbody tr.selected > .sorting_2,
+table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1,
+table.dataTable.display tbody tr.selected > .sorting_2,
+table.dataTable.display tbody tr.selected > .sorting_3 {
+  background-color: #acbad5;
+}
+table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
+  background-color: #f1f1f1;
+}
+table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
+  background-color: #f3f3f3;
+}
+table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
+  background-color: whitesmoke;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
+  background-color: #a6b4cd;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
+  background-color: #a8b5cf;
+}
+table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
+  background-color: #a9b7d1;
+}
+table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
+  background-color: #fafafa;
+}
+table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
+  background-color: #fcfcfc;
+}
+table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
+  background-color: #fefefe;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
+  background-color: #acbad5;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
+  background-color: #aebcd6;
+}
+table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
+  background-color: #afbdd8;
+}
+table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
+  background-color: #eaeaea;
+}
+table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
+  background-color: #ececec;
+}
+table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
+  background-color: #efefef;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
+  background-color: #a2aec7;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
+  background-color: #a3b0c9;
+}
+table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
+  background-color: #a5b2cb;
+}
+table.dataTable.no-footer {
+  border-bottom: 1px solid #111;
+}
+table.dataTable.nowrap th, table.dataTable.nowrap td {
+  white-space: nowrap;
+}
+table.dataTable.compact thead th,
+table.dataTable.compact thead td {
+  padding: 4px 17px 4px 4px;
+}
+table.dataTable.compact tfoot th,
+table.dataTable.compact tfoot td {
+  padding: 4px;
+}
+table.dataTable.compact tbody th,
+table.dataTable.compact tbody td {
+  padding: 4px;
+}
+table.dataTable th.dt-left,
+table.dataTable td.dt-left {
+  text-align: left;
+}
+table.dataTable th.dt-center,
+table.dataTable td.dt-center,
+table.dataTable td.dataTables_empty {
+  text-align: center;
+}
+table.dataTable th.dt-right,
+table.dataTable td.dt-right {
+  text-align: right;
+}
+table.dataTable th.dt-justify,
+table.dataTable td.dt-justify {
+  text-align: justify;
+}
+table.dataTable th.dt-nowrap,
+table.dataTable td.dt-nowrap {
+  white-space: nowrap;
+}
+table.dataTable thead th.dt-head-left,
+table.dataTable thead td.dt-head-left,
+table.dataTable tfoot th.dt-head-left,
+table.dataTable tfoot td.dt-head-left {
+  text-align: left;
+}
+table.dataTable thead th.dt-head-center,
+table.dataTable thead td.dt-head-center,
+table.dataTable tfoot th.dt-head-center,
+table.dataTable tfoot td.dt-head-center {
+  text-align: center;
+}
+table.dataTable thead th.dt-head-right,
+table.dataTable thead td.dt-head-right,
+table.dataTable tfoot th.dt-head-right,
+table.dataTable tfoot td.dt-head-right {
+  text-align: right;
+}
+table.dataTable thead th.dt-head-justify,
+table.dataTable thead td.dt-head-justify,
+table.dataTable tfoot th.dt-head-justify,
+table.dataTable tfoot td.dt-head-justify {
+  text-align: justify;
+}
+table.dataTable thead th.dt-head-nowrap,
+table.dataTable thead td.dt-head-nowrap,
+table.dataTable tfoot th.dt-head-nowrap,
+table.dataTable tfoot td.dt-head-nowrap {
+  white-space: nowrap;
+}
+table.dataTable tbody th.dt-body-left,
+table.dataTable tbody td.dt-body-left {
+  text-align: left;
+}
+table.dataTable tbody th.dt-body-center,
+table.dataTable tbody td.dt-body-center {
+  text-align: center;
+}
+table.dataTable tbody th.dt-body-right,
+table.dataTable tbody td.dt-body-right {
+  text-align: right;
+}
+table.dataTable tbody th.dt-body-justify,
+table.dataTable tbody td.dt-body-justify {
+  text-align: justify;
+}
+table.dataTable tbody th.dt-body-nowrap,
+table.dataTable tbody td.dt-body-nowrap {
+  white-space: nowrap;
+}
+
+table.dataTable,
+table.dataTable th,
+table.dataTable td {
+  -webkit-box-sizing: content-box;
+  box-sizing: content-box;
+}
+
+/*
+ * Control feature layout
+ */
+.dataTables_wrapper {
+  position: relative;
+  clear: both;
+  *zoom: 1;
+  zoom: 1;
+}
+.dataTables_wrapper .dataTables_length {
+  float: left;
+}
+.dataTables_wrapper .dataTables_filter {
+  float: right;
+  text-align: right;
+}
+.dataTables_wrapper .dataTables_filter input {
+  margin-left: 0.5em;
+}
+.dataTables_wrapper .dataTables_info {
+  clear: both;
+  float: left;
+  padding-top: 0.755em;
+}
+.dataTables_wrapper .dataTables_paginate {
+  float: right;
+  text-align: right;
+  padding-top: 0.25em;
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button {
+  box-sizing: border-box;
+  display: inline-block;
+  min-width: 1.5em;
+  padding: 0.5em 1em;
+  margin-left: 2px;
+  text-align: center;
+  text-decoration: none !important;
+  cursor: pointer;
+  *cursor: hand;
+  color: #333 !important;
+  border: 1px solid transparent;
+  border-radius: 2px;
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
+  color: #333 !important;
+  border: 1px solid #979797;
+  background-color: white;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc));
+  /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* Chrome10+,Safari5.1+ */
+  background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* FF3.6+ */
+  background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* IE10+ */
+  background: -o-linear-gradient(top, white 0%, #dcdcdc 100%);
+  /* Opera 11.10+ */
+  background: linear-gradient(to bottom, white 0%, #dcdcdc 100%);
+  /* W3C */
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active {
+  cursor: default;
+  color: #666 !important;
+  border: 1px solid transparent;
+  background: transparent;
+  box-shadow: none;
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
+  color: white !important;
+  border: 1px solid #111;
+  background-color: #585858;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));
+  /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(top, #585858 0%, #111 100%);
+  /* Chrome10+,Safari5.1+ */
+  background: -moz-linear-gradient(top, #585858 0%, #111 100%);
+  /* FF3.6+ */
+  background: -ms-linear-gradient(top, #585858 0%, #111 100%);
+  /* IE10+ */
+  background: -o-linear-gradient(top, #585858 0%, #111 100%);
+  /* Opera 11.10+ */
+  background: linear-gradient(to bottom, #585858 0%, #111 100%);
+  /* W3C */
+}
+.dataTables_wrapper .dataTables_paginate .paginate_button:active {
+  outline: none;
+  background-color: #2b2b2b;
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));
+  /* Chrome,Safari4+ */
+  background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* Chrome10+,Safari5.1+ */
+  background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* FF3.6+ */
+  background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* IE10+ */
+  background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+  /* Opera 11.10+ */
+  background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);
+  /* W3C */
+  box-shadow: inset 0 0 3px #111;
+}
+.dataTables_wrapper .dataTables_paginate .ellipsis {
+  padding: 0 1em;
+}
+.dataTables_wrapper .dataTables_processing {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 100%;
+  height: 40px;
+  margin-left: -50%;
+  margin-top: -25px;
+  padding-top: 20px;
+  text-align: center;
+  font-size: 1.2em;
+  background-color: white;
+  background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
+  background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+  background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+}
+.dataTables_wrapper .dataTables_length,
+.dataTables_wrapper .dataTables_filter,
+.dataTables_wrapper .dataTables_info,
+.dataTables_wrapper .dataTables_processing,
+.dataTables_wrapper .dataTables_paginate {
+  color: #333;
+}
+.dataTables_wrapper .dataTables_scroll {
+  clear: both;
+}
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody {
+  *margin-top: -1px;
+  -webkit-overflow-scrolling: touch;
+}
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td {
+  vertical-align: middle;
+}
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing,
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing,
+.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing {
+  height: 0;
+  overflow: hidden;
+  margin: 0 !important;
+  padding: 0 !important;
+}
+.dataTables_wrapper.no-footer .dataTables_scrollBody {
+  border-bottom: 1px solid #111;
+}
+.dataTables_wrapper.no-footer div.dataTables_scrollHead > table,
+.dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
+  border-bottom: none;
+}
+.dataTables_wrapper:after {
+  visibility: hidden;
+  display: block;
+  content: "";
+  clear: both;
+  height: 0;
+}
+
+@media screen and (max-width: 767px) {
+  .dataTables_wrapper .dataTables_info,
+  .dataTables_wrapper .dataTables_paginate {
+    float: none;
+    text-align: center;
+  }
+  .dataTables_wrapper .dataTables_paginate {
+    margin-top: 0.5em;
+  }
+}
+@media screen and (max-width: 640px) {
+  .dataTables_wrapper .dataTables_length,
+  .dataTables_wrapper .dataTables_filter {
+    float: none;
+    text-align: center;
+  }
+  .dataTables_wrapper .dataTables_filter {
+    margin-top: 0.5em;
+  }
+}
+
+
+table.DTCR_clonedTable.dataTable {
+  position: absolute !important;
+  background-color: rgba(255, 255, 255, 0.7);
+  z-index: 202;
+}
+
+div.DTCR_pointer {
+  width: 1px;
+  background-color: #0259C4;
+  z-index: 201;
+}
+
+
+table.DTFC_Cloned thead,
+table.DTFC_Cloned tfoot {
+  background-color: white;
+}
+
+div.DTFC_Blocker {
+  background-color: white;
+}
+
+div.DTFC_LeftWrapper table.dataTable,
+div.DTFC_RightWrapper table.dataTable {
+  margin-bottom: 0;
+  z-index: 2;
+}
+div.DTFC_LeftWrapper table.dataTable.no-footer,
+div.DTFC_RightWrapper table.dataTable.no-footer {
+  border-bottom: none;
+}
+
+
+table.fixedHeader-floating {
+  position: fixed !important;
+  background-color: white;
+}
+
+table.fixedHeader-floating.no-footer {
+  border-bottom-width: 0;
+}
+
+table.fixedHeader-locked {
+  position: absolute !important;
+  background-color: white;
+}
+
+@media print {
+  table.fixedHeader-floating {
+    display: none;
+  }
+}
+
+
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th.child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty {
+  cursor: default !important;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th.child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty:before {
+  display: none !important;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td:first-child,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th:first-child {
+  position: relative;
+  padding-left: 30px;
+  cursor: pointer;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr > td:first-child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr > th:first-child:before {
+  top: 9px;
+  left: 4px;
+  height: 14px;
+  width: 14px;
+  display: block;
+  position: absolute;
+  color: white;
+  border: 2px solid white;
+  border-radius: 14px;
+  box-shadow: 0 0 3px #444;
+  box-sizing: content-box;
+  text-align: center;
+  font-family: 'Courier New', Courier, monospace;
+  line-height: 14px;
+  content: '+';
+  background-color: #31b131;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr.parent > td:first-child:before,
+table.dataTable.dtr-inline.collapsed > tbody > tr.parent > th:first-child:before {
+  content: '-';
+  background-color: #d33333;
+}
+table.dataTable.dtr-inline.collapsed > tbody > tr.child td:before {
+  display: none;
+}
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td:first-child,
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th:first-child {
+  padding-left: 27px;
+}
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td:first-child:before,
+table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th:first-child:before {
+  top: 5px;
+  left: 4px;
+  height: 14px;
+  width: 14px;
+  border-radius: 14px;
+  line-height: 14px;
+  text-indent: 3px;
+}
+table.dataTable.dtr-column > tbody > tr > td.control,
+table.dataTable.dtr-column > tbody > tr > th.control {
+  position: relative;
+  cursor: pointer;
+}
+table.dataTable.dtr-column > tbody > tr > td.control:before,
+table.dataTable.dtr-column > tbody > tr > th.control:before {
+  top: 50%;
+  left: 50%;
+  height: 16px;
+  width: 16px;
+  margin-top: -10px;
+  margin-left: -10px;
+  display: block;
+  position: absolute;
+  color: white;
+  border: 2px solid white;
+  border-radius: 14px;
+  box-shadow: 0 0 3px #444;
+  box-sizing: content-box;
+  text-align: center;
+  font-family: 'Courier New', Courier, monospace;
+  line-height: 14px;
+  content: '+';
+  background-color: #31b131;
+}
+table.dataTable.dtr-column > tbody > tr.parent td.control:before,
+table.dataTable.dtr-column > tbody > tr.parent th.control:before {
+  content: '-';
+  background-color: #d33333;
+}
+table.dataTable > tbody > tr.child {
+  padding: 0.5em 1em;
+}
+table.dataTable > tbody > tr.child:hover {
+  background: transparent !important;
+}
+table.dataTable > tbody > tr.child ul.dtr-details {
+  display: inline-block;
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li {
+  border-bottom: 1px solid #efefef;
+  padding: 0.5em 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li:first-child {
+  padding-top: 0;
+}
+table.dataTable > tbody > tr.child ul.dtr-details li:last-child {
+  border-bottom: none;
+}
+table.dataTable > tbody > tr.child span.dtr-title {
+  display: inline-block;
+  min-width: 75px;
+  font-weight: bold;
+}
+
+div.dtr-modal {
+  position: fixed;
+  box-sizing: border-box;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+  z-index: 100;
+  padding: 10em 1em;
+}
+div.dtr-modal div.dtr-modal-display {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  width: 50%;
+  height: 50%;
+  overflow: auto;
+  margin: auto;
+  z-index: 102;
+  overflow: auto;
+  background-color: #f5f5f7;
+  border: 1px solid black;
+  border-radius: 0.5em;
+  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
+}
+div.dtr-modal div.dtr-modal-content {
+  position: relative;
+  padding: 1em;
+}
+div.dtr-modal div.dtr-modal-close {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  width: 22px;
+  height: 22px;
+  border: 1px solid #eaeaea;
+  background-color: #f9f9f9;
+  text-align: center;
+  border-radius: 3px;
+  cursor: pointer;
+  z-index: 12;
+}
+div.dtr-modal div.dtr-modal-close:hover {
+  background-color: #eaeaea;
+}
+div.dtr-modal div.dtr-modal-background {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 101;
+  background: rgba(0, 0, 0, 0.6);
+}
+
+@media screen and (max-width: 767px) {
+  div.dtr-modal div.dtr-modal-display {
+    width: 95%;
+  }
+}
+
+
diff --git a/static/DataTables/datatables.js b/static/DataTables/datatables.js
new file mode 100644
index 0000000000000000000000000000000000000000..f3accba4df4b8503ef7ca8af0e56ddc6692ee12b
--- /dev/null
+++ b/static/DataTables/datatables.js
@@ -0,0 +1,20270 @@
+/*
+ * This combined file was created by the DataTables downloader builder:
+ *   https://datatables.net/download
+ *
+ * To rebuild or modify this file with the latest versions of the included
+ * software please visit:
+ *   https://datatables.net/download/#dt/dt-1.10.15/cr-1.3.3/fc-3.2.2/fh-3.1.2/r-2.1.1
+ *
+ * Included libraries:
+ *   DataTables 1.10.15, ColReorder 1.3.3, FixedColumns 3.2.2, FixedHeader 3.1.2, Responsive 2.1.1
+ */
+
+/*! DataTables 1.10.15
+ * ©2008-2017 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary     DataTables
+ * @description Paginate, search and order HTML tables
+ * @version     1.10.15
+ * @file        jquery.dataTables.js
+ * @author      SpryMedia Ltd
+ * @contact     www.datatables.net
+ * @copyright   Copyright 2008-2017 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ *   MIT license - http://datatables.net/license
+ *
+ * This source file 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 license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+
+/*jslint evil: true, undef: true, browser: true */
+/*globals $,require,jQuery,define,_selector_run,_selector_opts,_selector_first,_selector_row_indexes,_ext,_Api,_api_register,_api_registerPlural,_re_new_lines,_re_html,_re_formatted_numeric,_re_escape_regex,_empty,_intVal,_numToDecimal,_isNumber,_isHtml,_htmlNumeric,_pluck,_pluck_order,_range,_stripHtml,_unique,_fnBuildAjax,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnAjaxDataSrc,_fnAddColumn,_fnColumnOptions,_fnAdjustColumnSizing,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnVisbleColumns,_fnGetColumns,_fnColumnTypes,_fnApplyColumnDefs,_fnHungarianMap,_fnCamelToHungarian,_fnLanguageCompat,_fnBrowserDetect,_fnAddData,_fnAddTr,_fnNodeToDataIndex,_fnNodeToColumnIndex,_fnGetCellData,_fnSetCellData,_fnSplitObjNotation,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnGetDataMaster,_fnClearTable,_fnDeleteIndex,_fnInvalidate,_fnGetRowElements,_fnCreateTr,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAddOptionsHtml,_fnDetectHeader,_fnGetUniqueThs,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnFilterCreateSearch,_fnEscapeRegex,_fnFilterData,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnInfoMacros,_fnInitialise,_fnInitComplete,_fnLengthChange,_fnFeatureHtmlLength,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnFeatureHtmlTable,_fnScrollDraw,_fnApplyToChildren,_fnCalculateColumnWidths,_fnThrottle,_fnConvertToWidth,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnSortFlatten,_fnSort,_fnSortAria,_fnSortListener,_fnSortAttachListener,_fnSortingClasses,_fnSortData,_fnSaveState,_fnLoadState,_fnSettingsFromNode,_fnLog,_fnMap,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnLengthOverflow,_fnRenderer,_fnDataSource,_fnRowAttributes*/
+
+(function( factory ) {
+	"use strict";
+
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				// CommonJS environments without a window global must pass a
+				// root. This will give an error otherwise
+				root = window;
+			}
+
+			if ( ! $ ) {
+				$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
+					require('jquery') :
+					require('jquery')( root );
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}
+(function( $, window, document, undefined ) {
+	"use strict";
+
+	/**
+	 * DataTables is a plug-in for the jQuery Javascript library. It is a highly
+	 * flexible tool, based upon the foundations of progressive enhancement,
+	 * which will add advanced interaction controls to any HTML table. For a
+	 * full list of features please refer to
+	 * [DataTables.net](href="http://datatables.net).
+	 *
+	 * Note that the `DataTable` object is not a global variable but is aliased
+	 * to `jQuery.fn.DataTable` and `jQuery.fn.dataTable` through which it may
+	 * be  accessed.
+	 *
+	 *  @class
+	 *  @param {object} [init={}] Configuration object for DataTables. Options
+	 *    are defined by {@link DataTable.defaults}
+	 *  @requires jQuery 1.7+
+	 *
+	 *  @example
+	 *    // Basic initialisation
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable();
+	 *    } );
+	 *
+	 *  @example
+	 *    // Initialisation with configuration options - in this case, disable
+	 *    // pagination and sorting.
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable( {
+	 *        "paginate": false,
+	 *        "sort": false
+	 *      } );
+	 *    } );
+	 */
+	var DataTable = function ( options )
+	{
+		/**
+		 * Perform a jQuery selector action on the table's TR elements (from the tbody) and
+		 * return the resulting jQuery object.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select TR elements that meet the current filter
+		 *    criterion ("applied") or all TR elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the TR elements in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {object} jQuery object, filtered by the given selector.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Highlight every second row
+		 *      oTable.$('tr:odd').css('backgroundColor', 'blue');
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to rows with 'Webkit' in them, add a background colour and then
+		 *      // remove the filter, thus highlighting the 'Webkit' rows only.
+		 *      oTable.fnFilter('Webkit');
+		 *      oTable.$('tr', {"search": "applied"}).css('backgroundColor', 'blue');
+		 *      oTable.fnFilter('');
+		 *    } );
+		 */
+		this.$ = function ( sSelector, oOpts )
+		{
+			return this.api(true).$( sSelector, oOpts );
+		};
+		
+		
+		/**
+		 * Almost identical to $ in operation, but in this case returns the data for the matched
+		 * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes
+		 * rather than any descendants, so the data can be obtained for the row/cell. If matching
+		 * rows are found, the data returned is the original data array/object that was used to
+		 * create the row (or a generated array if from a DOM source).
+		 *
+		 * This method is often useful in-combination with $ where both functions are given the
+		 * same parameters and the array indexes will match identically.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select elements that meet the current filter
+		 *    criterion ("applied") or all elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the data in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {array} Data for the matched elements. If any elements, as a result of the
+		 *    selector, were not TR, TD or TH elements in the DataTable, they will have a null
+		 *    entry in the array.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Get the data from the first row in the table
+		 *      var data = oTable._('tr:first');
+		 *
+		 *      // Do something useful with the data
+		 *      alert( "First cell is: "+data[0] );
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to 'Webkit' and get all data for
+		 *      oTable.fnFilter('Webkit');
+		 *      var data = oTable._('tr', {"search": "applied"});
+		 *
+		 *      // Do something with the data
+		 *      alert( data.length+" rows matched the search" );
+		 *    } );
+		 */
+		this._ = function ( sSelector, oOpts )
+		{
+			return this.api(true).rows( sSelector, oOpts ).data();
+		};
+		
+		
+		/**
+		 * Create a DataTables Api instance, with the currently selected tables for
+		 * the Api's context.
+		 * @param {boolean} [traditional=false] Set the API instance's context to be
+		 *   only the table referred to by the `DataTable.ext.iApiIndex` option, as was
+		 *   used in the API presented by DataTables 1.9- (i.e. the traditional mode),
+		 *   or if all tables captured in the jQuery object should be used.
+		 * @return {DataTables.Api}
+		 */
+		this.api = function ( traditional )
+		{
+			return traditional ?
+				new _Api(
+					_fnSettingsFromNode( this[ _ext.iApiIndex ] )
+				) :
+				new _Api( this );
+		};
+		
+		
+		/**
+		 * Add a single new row or multiple rows of data to the table. Please note
+		 * that this is suitable for client-side processing only - if you are using
+		 * server-side processing (i.e. "bServerSide": true), then to add data, you
+		 * must add it to the data source, i.e. the server-side, through an Ajax call.
+		 *  @param {array|object} data The data to be added to the table. This can be:
+		 *    <ul>
+		 *      <li>1D array of data - add a single row with the data provided</li>
+		 *      <li>2D array of arrays - add multiple rows in a single call</li>
+		 *      <li>object - data object when using <i>mData</i></li>
+		 *      <li>array of objects - multiple data objects when using <i>mData</i></li>
+		 *    </ul>
+		 *  @param {bool} [redraw=true] redraw the table or not
+		 *  @returns {array} An array of integers, representing the list of indexes in
+		 *    <i>aoData</i> ({@link DataTable.models.oSettings}) that have been added to
+		 *    the table.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    // Global var for counter
+		 *    var giCount = 2;
+		 *
+		 *    $(document).ready(function() {
+		 *      $('#example').dataTable();
+		 *    } );
+		 *
+		 *    function fnClickAddRow() {
+		 *      $('#example').dataTable().fnAddData( [
+		 *        giCount+".1",
+		 *        giCount+".2",
+		 *        giCount+".3",
+		 *        giCount+".4" ]
+		 *      );
+		 *
+		 *      giCount++;
+		 *    }
+		 */
+		this.fnAddData = function( data, redraw )
+		{
+			var api = this.api( true );
+		
+			/* Check if we want to add multiple rows or not */
+			var rows = $.isArray(data) && ( $.isArray(data[0]) || $.isPlainObject(data[0]) ) ?
+				api.rows.add( data ) :
+				api.row.add( data );
+		
+			if ( redraw === undefined || redraw ) {
+				api.draw();
+			}
+		
+			return rows.flatten().toArray();
+		};
+		
+		
+		/**
+		 * This function will make DataTables recalculate the column sizes, based on the data
+		 * contained in the table and the sizes applied to the columns (in the DOM, CSS or
+		 * through the sWidth parameter). This can be useful when the width of the table's
+		 * parent element changes (for example a window resize).
+		 *  @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *
+		 *      $(window).on('resize', function () {
+		 *        oTable.fnAdjustColumnSizing();
+		 *      } );
+		 *    } );
+		 */
+		this.fnAdjustColumnSizing = function ( bRedraw )
+		{
+			var api = this.api( true ).columns.adjust();
+			var settings = api.settings()[0];
+			var scroll = settings.oScroll;
+		
+			if ( bRedraw === undefined || bRedraw ) {
+				api.draw( false );
+			}
+			else if ( scroll.sX !== "" || scroll.sY !== "" ) {
+				/* If not redrawing, but scrolling, we want to apply the new column sizes anyway */
+				_fnScrollDraw( settings );
+			}
+		};
+		
+		
+		/**
+		 * Quickly and simply clear a table
+		 *  @param {bool} [bRedraw=true] redraw the table or not
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...)
+		 *      oTable.fnClearTable();
+		 *    } );
+		 */
+		this.fnClearTable = function( bRedraw )
+		{
+			var api = this.api( true ).clear();
+		
+			if ( bRedraw === undefined || bRedraw ) {
+				api.draw();
+			}
+		};
+		
+		
+		/**
+		 * The exact opposite of 'opening' a row, this function will close any rows which
+		 * are currently 'open'.
+		 *  @param {node} nTr the table row to 'close'
+		 *  @returns {int} 0 on success, or 1 if failed (can't find the row)
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnClose = function( nTr )
+		{
+			this.api( true ).row( nTr ).child.hide();
+		};
+		
+		
+		/**
+		 * Remove a row for the table
+		 *  @param {mixed} target The index of the row from aoData to be deleted, or
+		 *    the TR element you want to delete
+		 *  @param {function|null} [callBack] Callback function
+		 *  @param {bool} [redraw=true] Redraw the table or not
+		 *  @returns {array} The row that was deleted
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Immediately remove the first row
+		 *      oTable.fnDeleteRow( 0 );
+		 *    } );
+		 */
+		this.fnDeleteRow = function( target, callback, redraw )
+		{
+			var api = this.api( true );
+			var rows = api.rows( target );
+			var settings = rows.settings()[0];
+			var data = settings.aoData[ rows[0][0] ];
+		
+			rows.remove();
+		
+			if ( callback ) {
+				callback.call( this, settings, data );
+			}
+		
+			if ( redraw === undefined || redraw ) {
+				api.draw();
+			}
+		
+			return data;
+		};
+		
+		
+		/**
+		 * Restore the table to it's original state in the DOM by removing all of DataTables
+		 * enhancements, alterations to the DOM structure of the table and event listeners.
+		 *  @param {boolean} [remove=false] Completely remove the table from the DOM
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      // This example is fairly pointless in reality, but shows how fnDestroy can be used
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnDestroy();
+		 *    } );
+		 */
+		this.fnDestroy = function ( remove )
+		{
+			this.api( true ).destroy( remove );
+		};
+		
+		
+		/**
+		 * Redraw the table
+		 *  @param {bool} [complete=true] Re-filter and resort (if enabled) the table before the draw.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Re-draw the table - you wouldn't want to do it here, but it's an example :-)
+		 *      oTable.fnDraw();
+		 *    } );
+		 */
+		this.fnDraw = function( complete )
+		{
+			// Note that this isn't an exact match to the old call to _fnDraw - it takes
+			// into account the new data, but can hold position.
+			this.api( true ).draw( complete );
+		};
+		
+		
+		/**
+		 * Filter the input based on data
+		 *  @param {string} sInput String to filter the table on
+		 *  @param {int|null} [iColumn] Column to limit filtering to
+		 *  @param {bool} [bRegex=false] Treat as regular expression or not
+		 *  @param {bool} [bSmart=true] Perform smart filtering or not
+		 *  @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es)
+		 *  @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false)
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Sometime later - filter...
+		 *      oTable.fnFilter( 'test string' );
+		 *    } );
+		 */
+		this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive )
+		{
+			var api = this.api( true );
+		
+			if ( iColumn === null || iColumn === undefined ) {
+				api.search( sInput, bRegex, bSmart, bCaseInsensitive );
+			}
+			else {
+				api.column( iColumn ).search( sInput, bRegex, bSmart, bCaseInsensitive );
+			}
+		
+			api.draw();
+		};
+		
+		
+		/**
+		 * Get the data for the whole table, an individual row or an individual cell based on the
+		 * provided parameters.
+		 *  @param {int|node} [src] A TR row node, TD/TH cell node or an integer. If given as
+		 *    a TR node then the data source for the whole row will be returned. If given as a
+		 *    TD/TH cell node then iCol will be automatically calculated and the data for the
+		 *    cell returned. If given as an integer, then this is treated as the aoData internal
+		 *    data index for the row (see fnGetPosition) and the data for that row used.
+		 *  @param {int} [col] Optional column index that you want the data of.
+		 *  @returns {array|object|string} If mRow is undefined, then the data for all rows is
+		 *    returned. If mRow is defined, just data for that row, and is iCol is
+		 *    defined, only data for the designated cell is returned.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    // Row data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('tr').click( function () {
+		 *        var data = oTable.fnGetData( this );
+		 *        // ... do something with the array / object of data for the row
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Individual cell data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('td').click( function () {
+		 *        var sData = oTable.fnGetData( this );
+		 *        alert( 'The cell clicked on had the value of '+sData );
+		 *      } );
+		 *    } );
+		 */
+		this.fnGetData = function( src, col )
+		{
+			var api = this.api( true );
+		
+			if ( src !== undefined ) {
+				var type = src.nodeName ? src.nodeName.toLowerCase() : '';
+		
+				return col !== undefined || type == 'td' || type == 'th' ?
+					api.cell( src, col ).data() :
+					api.row( src ).data() || null;
+			}
+		
+			return api.data().toArray();
+		};
+		
+		
+		/**
+		 * Get an array of the TR nodes that are used in the table's body. Note that you will
+		 * typically want to use the '$' API method in preference to this as it is more
+		 * flexible.
+		 *  @param {int} [iRow] Optional row index for the TR element you want
+		 *  @returns {array|node} If iRow is undefined, returns an array of all TR elements
+		 *    in the table's body, or iRow is defined, just the TR element requested.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Get the nodes from the table
+		 *      var nNodes = oTable.fnGetNodes( );
+		 *    } );
+		 */
+		this.fnGetNodes = function( iRow )
+		{
+			var api = this.api( true );
+		
+			return iRow !== undefined ?
+				api.row( iRow ).node() :
+				api.rows().nodes().flatten().toArray();
+		};
+		
+		
+		/**
+		 * Get the array indexes of a particular cell from it's DOM element
+		 * and column index including hidden columns
+		 *  @param {node} node this can either be a TR, TD or TH in the table's body
+		 *  @returns {int} If nNode is given as a TR, then a single index is returned, or
+		 *    if given as a cell, an array of [row index, column index (visible),
+		 *    column index (all)] is given.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      $('#example tbody td').click( function () {
+		 *        // Get the position of the current data from the node
+		 *        var aPos = oTable.fnGetPosition( this );
+		 *
+		 *        // Get the data array for this row
+		 *        var aData = oTable.fnGetData( aPos[0] );
+		 *
+		 *        // Update the data array and return the value
+		 *        aData[ aPos[1] ] = 'clicked';
+		 *        this.innerHTML = 'clicked';
+		 *      } );
+		 *
+		 *      // Init DataTables
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnGetPosition = function( node )
+		{
+			var api = this.api( true );
+			var nodeName = node.nodeName.toUpperCase();
+		
+			if ( nodeName == 'TR' ) {
+				return api.row( node ).index();
+			}
+			else if ( nodeName == 'TD' || nodeName == 'TH' ) {
+				var cell = api.cell( node ).index();
+		
+				return [
+					cell.row,
+					cell.columnVisible,
+					cell.column
+				];
+			}
+			return null;
+		};
+		
+		
+		/**
+		 * Check to see if a row is 'open' or not.
+		 *  @param {node} nTr the table row to check
+		 *  @returns {boolean} true if the row is currently open, false otherwise
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnIsOpen = function( nTr )
+		{
+			return this.api( true ).row( nTr ).child.isShown();
+		};
+		
+		
+		/**
+		 * This function will place a new row directly after a row which is currently
+		 * on display on the page, with the HTML contents that is passed into the
+		 * function. This can be used, for example, to ask for confirmation that a
+		 * particular record should be deleted.
+		 *  @param {node} nTr The table row to 'open'
+		 *  @param {string|node|jQuery} mHtml The HTML to put into the row
+		 *  @param {string} sClass Class to give the new TD cell
+		 *  @returns {node} The row opened. Note that if the table row passed in as the
+		 *    first parameter, is not found in the table, this method will silently
+		 *    return.
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnOpen = function( nTr, mHtml, sClass )
+		{
+			return this.api( true )
+				.row( nTr )
+				.child( mHtml, sClass )
+				.show()
+				.child()[0];
+		};
+		
+		
+		/**
+		 * Change the pagination - provides the internal logic for pagination in a simple API
+		 * function. With this function you can have a DataTables table go to the next,
+		 * previous, first or last pages.
+		 *  @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+		 *    or page number to jump to (integer), note that page 0 is the first page.
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnPageChange( 'next' );
+		 *    } );
+		 */
+		this.fnPageChange = function ( mAction, bRedraw )
+		{
+			var api = this.api( true ).page( mAction );
+		
+			if ( bRedraw === undefined || bRedraw ) {
+				api.draw(false);
+			}
+		};
+		
+		
+		/**
+		 * Show a particular column
+		 *  @param {int} iCol The column whose display should be changed
+		 *  @param {bool} bShow Show (true) or hide (false) the column
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Hide the second column after initialisation
+		 *      oTable.fnSetColumnVis( 1, false );
+		 *    } );
+		 */
+		this.fnSetColumnVis = function ( iCol, bShow, bRedraw )
+		{
+			var api = this.api( true ).column( iCol ).visible( bShow );
+		
+			if ( bRedraw === undefined || bRedraw ) {
+				api.columns.adjust().draw();
+			}
+		};
+		
+		
+		/**
+		 * Get the settings for a particular table for external manipulation
+		 *  @returns {object} DataTables settings object. See
+		 *    {@link DataTable.models.oSettings}
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      var oSettings = oTable.fnSettings();
+		 *
+		 *      // Show an example parameter from the settings
+		 *      alert( oSettings._iDisplayStart );
+		 *    } );
+		 */
+		this.fnSettings = function()
+		{
+			return _fnSettingsFromNode( this[_ext.iApiIndex] );
+		};
+		
+		
+		/**
+		 * Sort the table by a particular column
+		 *  @param {int} iCol the data index to sort on. Note that this will not match the
+		 *    'display index' if you have hidden data entries
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Sort immediately with columns 0 and 1
+		 *      oTable.fnSort( [ [0,'asc'], [1,'asc'] ] );
+		 *    } );
+		 */
+		this.fnSort = function( aaSort )
+		{
+			this.api( true ).order( aaSort ).draw();
+		};
+		
+		
+		/**
+		 * Attach a sort listener to an element for a given column
+		 *  @param {node} nNode the element to attach the sort listener to
+		 *  @param {int} iColumn the column that a click on this node will sort on
+		 *  @param {function} [fnCallback] callback function when sort is run
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Sort on column 1, when 'sorter' is clicked on
+		 *      oTable.fnSortListener( document.getElementById('sorter'), 1 );
+		 *    } );
+		 */
+		this.fnSortListener = function( nNode, iColumn, fnCallback )
+		{
+			this.api( true ).order.listener( nNode, iColumn, fnCallback );
+		};
+		
+		
+		/**
+		 * Update a table cell or row - this method will accept either a single value to
+		 * update the cell with, an array of values with one element for each column or
+		 * an object in the same format as the original data source. The function is
+		 * self-referencing in order to make the multi column updates easier.
+		 *  @param {object|array|string} mData Data to update the cell/row with
+		 *  @param {node|int} mRow TR element you want to update or the aoData index
+		 *  @param {int} [iColumn] The column to update, give as null or undefined to
+		 *    update a whole row.
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @param {bool} [bAction=true] Perform pre-draw actions or not
+		 *  @returns {int} 0 on success, 1 on error
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell
+		 *      oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], $('tbody tr')[0] ); // Row
+		 *    } );
+		 */
+		this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )
+		{
+			var api = this.api( true );
+		
+			if ( iColumn === undefined || iColumn === null ) {
+				api.row( mRow ).data( mData );
+			}
+			else {
+				api.cell( mRow, iColumn ).data( mData );
+			}
+		
+			if ( bAction === undefined || bAction ) {
+				api.columns.adjust();
+			}
+		
+			if ( bRedraw === undefined || bRedraw ) {
+				api.draw();
+			}
+			return 0;
+		};
+		
+		
+		/**
+		 * Provide a common method for plug-ins to check the version of DataTables being used, in order
+		 * to ensure compatibility.
+		 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+		 *    formats "X" and "X.Y" are also acceptable.
+		 *  @returns {boolean} true if this version of DataTables is greater or equal to the required
+		 *    version, or false if this version of DataTales is not suitable
+		 *  @method
+		 *  @dtopt API
+		 *  @deprecated Since v1.10
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      alert( oTable.fnVersionCheck( '1.9.0' ) );
+		 *    } );
+		 */
+		this.fnVersionCheck = _ext.fnVersionCheck;
+		
+
+		var _that = this;
+		var emptyInit = options === undefined;
+		var len = this.length;
+
+		if ( emptyInit ) {
+			options = {};
+		}
+
+		this.oApi = this.internal = _ext.internal;
+
+		// Extend with old style plug-in API methods
+		for ( var fn in DataTable.ext.internal ) {
+			if ( fn ) {
+				this[fn] = _fnExternApiFunc(fn);
+			}
+		}
+
+		this.each(function() {
+			// For each initialisation we want to give it a clean initialisation
+			// object that can be bashed around
+			var o = {};
+			var oInit = len > 1 ? // optimisation for single table case
+				_fnExtend( o, options, true ) :
+				options;
+
+			/*global oInit,_that,emptyInit*/
+			var i=0, iLen, j, jLen, k, kLen;
+			var sId = this.getAttribute( 'id' );
+			var bInitHandedOff = false;
+			var defaults = DataTable.defaults;
+			var $this = $(this);
+			
+			
+			/* Sanity check */
+			if ( this.nodeName.toLowerCase() != 'table' )
+			{
+				_fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 );
+				return;
+			}
+			
+			/* Backwards compatibility for the defaults */
+			_fnCompatOpts( defaults );
+			_fnCompatCols( defaults.column );
+			
+			/* Convert the camel-case defaults to Hungarian */
+			_fnCamelToHungarian( defaults, defaults, true );
+			_fnCamelToHungarian( defaults.column, defaults.column, true );
+			
+			/* Setting up the initialisation object */
+			_fnCamelToHungarian( defaults, $.extend( oInit, $this.data() ) );
+			
+			
+			
+			/* Check to see if we are re-initialising a table */
+			var allSettings = DataTable.settings;
+			for ( i=0, iLen=allSettings.length ; i<iLen ; i++ )
+			{
+				var s = allSettings[i];
+			
+				/* Base check on table node */
+				if ( s.nTable == this || s.nTHead.parentNode == this || (s.nTFoot && s.nTFoot.parentNode == this) )
+				{
+					var bRetrieve = oInit.bRetrieve !== undefined ? oInit.bRetrieve : defaults.bRetrieve;
+					var bDestroy = oInit.bDestroy !== undefined ? oInit.bDestroy : defaults.bDestroy;
+			
+					if ( emptyInit || bRetrieve )
+					{
+						return s.oInstance;
+					}
+					else if ( bDestroy )
+					{
+						s.oInstance.fnDestroy();
+						break;
+					}
+					else
+					{
+						_fnLog( s, 0, 'Cannot reinitialise DataTable', 3 );
+						return;
+					}
+				}
+			
+				/* If the element we are initialising has the same ID as a table which was previously
+				 * initialised, but the table nodes don't match (from before) then we destroy the old
+				 * instance by simply deleting it. This is under the assumption that the table has been
+				 * destroyed by other methods. Anyone using non-id selectors will need to do this manually
+				 */
+				if ( s.sTableId == this.id )
+				{
+					allSettings.splice( i, 1 );
+					break;
+				}
+			}
+			
+			/* Ensure the table has an ID - required for accessibility */
+			if ( sId === null || sId === "" )
+			{
+				sId = "DataTables_Table_"+(DataTable.ext._unique++);
+				this.id = sId;
+			}
+			
+			/* Create the settings object for this table and set some of the default parameters */
+			var oSettings = $.extend( true, {}, DataTable.models.oSettings, {
+				"sDestroyWidth": $this[0].style.width,
+				"sInstance":     sId,
+				"sTableId":      sId
+			} );
+			oSettings.nTable = this;
+			oSettings.oApi   = _that.internal;
+			oSettings.oInit  = oInit;
+			
+			allSettings.push( oSettings );
+			
+			// Need to add the instance after the instance after the settings object has been added
+			// to the settings array, so we can self reference the table instance if more than one
+			oSettings.oInstance = (_that.length===1) ? _that : $this.dataTable();
+			
+			// Backwards compatibility, before we apply all the defaults
+			_fnCompatOpts( oInit );
+			
+			if ( oInit.oLanguage )
+			{
+				_fnLanguageCompat( oInit.oLanguage );
+			}
+			
+			// If the length menu is given, but the init display length is not, use the length menu
+			if ( oInit.aLengthMenu && ! oInit.iDisplayLength )
+			{
+				oInit.iDisplayLength = $.isArray( oInit.aLengthMenu[0] ) ?
+					oInit.aLengthMenu[0][0] : oInit.aLengthMenu[0];
+			}
+			
+			// Apply the defaults and init options to make a single init object will all
+			// options defined from defaults and instance options.
+			oInit = _fnExtend( $.extend( true, {}, defaults ), oInit );
+			
+			
+			// Map the initialisation options onto the settings object
+			_fnMap( oSettings.oFeatures, oInit, [
+				"bPaginate",
+				"bLengthChange",
+				"bFilter",
+				"bSort",
+				"bSortMulti",
+				"bInfo",
+				"bProcessing",
+				"bAutoWidth",
+				"bSortClasses",
+				"bServerSide",
+				"bDeferRender"
+			] );
+			_fnMap( oSettings, oInit, [
+				"asStripeClasses",
+				"ajax",
+				"fnServerData",
+				"fnFormatNumber",
+				"sServerMethod",
+				"aaSorting",
+				"aaSortingFixed",
+				"aLengthMenu",
+				"sPaginationType",
+				"sAjaxSource",
+				"sAjaxDataProp",
+				"iStateDuration",
+				"sDom",
+				"bSortCellsTop",
+				"iTabIndex",
+				"fnStateLoadCallback",
+				"fnStateSaveCallback",
+				"renderer",
+				"searchDelay",
+				"rowId",
+				[ "iCookieDuration", "iStateDuration" ], // backwards compat
+				[ "oSearch", "oPreviousSearch" ],
+				[ "aoSearchCols", "aoPreSearchCols" ],
+				[ "iDisplayLength", "_iDisplayLength" ],
+				[ "bJQueryUI", "bJUI" ]
+			] );
+			_fnMap( oSettings.oScroll, oInit, [
+				[ "sScrollX", "sX" ],
+				[ "sScrollXInner", "sXInner" ],
+				[ "sScrollY", "sY" ],
+				[ "bScrollCollapse", "bCollapse" ]
+			] );
+			_fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" );
+			
+			/* Callback functions which are array driven */
+			_fnCallbackReg( oSettings, 'aoDrawCallback',       oInit.fnDrawCallback,      'user' );
+			_fnCallbackReg( oSettings, 'aoServerParams',       oInit.fnServerParams,      'user' );
+			_fnCallbackReg( oSettings, 'aoStateSaveParams',    oInit.fnStateSaveParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoadParams',    oInit.fnStateLoadParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoaded',        oInit.fnStateLoaded,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCallback',        oInit.fnRowCallback,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow,        'user' );
+			_fnCallbackReg( oSettings, 'aoHeaderCallback',     oInit.fnHeaderCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoFooterCallback',     oInit.fnFooterCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoInitComplete',       oInit.fnInitComplete,      'user' );
+			_fnCallbackReg( oSettings, 'aoPreDrawCallback',    oInit.fnPreDrawCallback,   'user' );
+			
+			oSettings.rowIdFn = _fnGetObjectDataFn( oInit.rowId );
+			
+			/* Browser support detection */
+			_fnBrowserDetect( oSettings );
+			
+			var oClasses = oSettings.oClasses;
+			
+			// @todo Remove in 1.11
+			if ( oInit.bJQueryUI )
+			{
+				/* Use the JUI classes object for display. You could clone the oStdClasses object if
+				 * you want to have multiple tables with multiple independent classes
+				 */
+				$.extend( oClasses, DataTable.ext.oJUIClasses, oInit.oClasses );
+			
+				if ( oInit.sDom === defaults.sDom && defaults.sDom === "lfrtip" )
+				{
+					/* Set the DOM to use a layout suitable for jQuery UI's theming */
+					oSettings.sDom = '<"H"lfr>t<"F"ip>';
+				}
+			
+				if ( ! oSettings.renderer ) {
+					oSettings.renderer = 'jqueryui';
+				}
+				else if ( $.isPlainObject( oSettings.renderer ) && ! oSettings.renderer.header ) {
+					oSettings.renderer.header = 'jqueryui';
+				}
+			}
+			else
+			{
+				$.extend( oClasses, DataTable.ext.classes, oInit.oClasses );
+			}
+			$this.addClass( oClasses.sTable );
+			
+			
+			if ( oSettings.iInitDisplayStart === undefined )
+			{
+				/* Display start point, taking into account the save saving */
+				oSettings.iInitDisplayStart = oInit.iDisplayStart;
+				oSettings._iDisplayStart = oInit.iDisplayStart;
+			}
+			
+			if ( oInit.iDeferLoading !== null )
+			{
+				oSettings.bDeferLoading = true;
+				var tmp = $.isArray( oInit.iDeferLoading );
+				oSettings._iRecordsDisplay = tmp ? oInit.iDeferLoading[0] : oInit.iDeferLoading;
+				oSettings._iRecordsTotal = tmp ? oInit.iDeferLoading[1] : oInit.iDeferLoading;
+			}
+			
+			/* Language definitions */
+			var oLanguage = oSettings.oLanguage;
+			$.extend( true, oLanguage, oInit.oLanguage );
+			
+			if ( oLanguage.sUrl )
+			{
+				/* Get the language definitions from a file - because this Ajax call makes the language
+				 * get async to the remainder of this function we use bInitHandedOff to indicate that
+				 * _fnInitialise will be fired by the returned Ajax handler, rather than the constructor
+				 */
+				$.ajax( {
+					dataType: 'json',
+					url: oLanguage.sUrl,
+					success: function ( json ) {
+						_fnLanguageCompat( json );
+						_fnCamelToHungarian( defaults.oLanguage, json );
+						$.extend( true, oLanguage, json );
+						_fnInitialise( oSettings );
+					},
+					error: function () {
+						// Error occurred loading language file, continue on as best we can
+						_fnInitialise( oSettings );
+					}
+				} );
+				bInitHandedOff = true;
+			}
+			
+			/*
+			 * Stripes
+			 */
+			if ( oInit.asStripeClasses === null )
+			{
+				oSettings.asStripeClasses =[
+					oClasses.sStripeOdd,
+					oClasses.sStripeEven
+				];
+			}
+			
+			/* Remove row stripe classes if they are already on the table row */
+			var stripeClasses = oSettings.asStripeClasses;
+			var rowOne = $this.children('tbody').find('tr').eq(0);
+			if ( $.inArray( true, $.map( stripeClasses, function(el, i) {
+				return rowOne.hasClass(el);
+			} ) ) !== -1 ) {
+				$('tbody tr', this).removeClass( stripeClasses.join(' ') );
+				oSettings.asDestroyStripes = stripeClasses.slice();
+			}
+			
+			/*
+			 * Columns
+			 * See if we should load columns automatically or use defined ones
+			 */
+			var anThs = [];
+			var aoColumnsInit;
+			var nThead = this.getElementsByTagName('thead');
+			if ( nThead.length !== 0 )
+			{
+				_fnDetectHeader( oSettings.aoHeader, nThead[0] );
+				anThs = _fnGetUniqueThs( oSettings );
+			}
+			
+			/* If not given a column array, generate one with nulls */
+			if ( oInit.aoColumns === null )
+			{
+				aoColumnsInit = [];
+				for ( i=0, iLen=anThs.length ; i<iLen ; i++ )
+				{
+					aoColumnsInit.push( null );
+				}
+			}
+			else
+			{
+				aoColumnsInit = oInit.aoColumns;
+			}
+			
+			/* Add the columns */
+			for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ )
+			{
+				_fnAddColumn( oSettings, anThs ? anThs[i] : null );
+			}
+			
+			/* Apply the column definitions */
+			_fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, aoColumnsInit, function (iCol, oDef) {
+				_fnColumnOptions( oSettings, iCol, oDef );
+			} );
+			
+			/* HTML5 attribute detection - build an mData object automatically if the
+			 * attributes are found
+			 */
+			if ( rowOne.length ) {
+				var a = function ( cell, name ) {
+					return cell.getAttribute( 'data-'+name ) !== null ? name : null;
+				};
+			
+				$( rowOne[0] ).children('th, td').each( function (i, cell) {
+					var col = oSettings.aoColumns[i];
+			
+					if ( col.mData === i ) {
+						var sort = a( cell, 'sort' ) || a( cell, 'order' );
+						var filter = a( cell, 'filter' ) || a( cell, 'search' );
+			
+						if ( sort !== null || filter !== null ) {
+							col.mData = {
+								_:      i+'.display',
+								sort:   sort !== null   ? i+'.@data-'+sort   : undefined,
+								type:   sort !== null   ? i+'.@data-'+sort   : undefined,
+								filter: filter !== null ? i+'.@data-'+filter : undefined
+							};
+			
+							_fnColumnOptions( oSettings, i );
+						}
+					}
+				} );
+			}
+			
+			var features = oSettings.oFeatures;
+			var loadedInit = function () {
+				/*
+				 * Sorting
+				 * @todo For modularisation (1.11) this needs to do into a sort start up handler
+				 */
+			
+				// If aaSorting is not defined, then we use the first indicator in asSorting
+				// in case that has been altered, so the default sort reflects that option
+				if ( oInit.aaSorting === undefined ) {
+					var sorting = oSettings.aaSorting;
+					for ( i=0, iLen=sorting.length ; i<iLen ; i++ ) {
+						sorting[i][1] = oSettings.aoColumns[ i ].asSorting[0];
+					}
+				}
+			
+				/* Do a first pass on the sorting classes (allows any size changes to be taken into
+				 * account, and also will apply sorting disabled classes if disabled
+				 */
+				_fnSortingClasses( oSettings );
+			
+				if ( features.bSort ) {
+					_fnCallbackReg( oSettings, 'aoDrawCallback', function () {
+						if ( oSettings.bSorted ) {
+							var aSort = _fnSortFlatten( oSettings );
+							var sortedColumns = {};
+			
+							$.each( aSort, function (i, val) {
+								sortedColumns[ val.src ] = val.dir;
+							} );
+			
+							_fnCallbackFire( oSettings, null, 'order', [oSettings, aSort, sortedColumns] );
+							_fnSortAria( oSettings );
+						}
+					} );
+				}
+			
+				_fnCallbackReg( oSettings, 'aoDrawCallback', function () {
+					if ( oSettings.bSorted || _fnDataSource( oSettings ) === 'ssp' || features.bDeferRender ) {
+						_fnSortingClasses( oSettings );
+					}
+				}, 'sc' );
+			
+			
+				/*
+				 * Final init
+				 * Cache the header, body and footer as required, creating them if needed
+				 */
+			
+				// Work around for Webkit bug 83867 - store the caption-side before removing from doc
+				var captions = $this.children('caption').each( function () {
+					this._captionSide = $(this).css('caption-side');
+				} );
+			
+				var thead = $this.children('thead');
+				if ( thead.length === 0 ) {
+					thead = $('<thead/>').appendTo($this);
+				}
+				oSettings.nTHead = thead[0];
+			
+				var tbody = $this.children('tbody');
+				if ( tbody.length === 0 ) {
+					tbody = $('<tbody/>').appendTo($this);
+				}
+				oSettings.nTBody = tbody[0];
+			
+				var tfoot = $this.children('tfoot');
+				if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") ) {
+					// If we are a scrolling table, and no footer has been given, then we need to create
+					// a tfoot element for the caption element to be appended to
+					tfoot = $('<tfoot/>').appendTo($this);
+				}
+			
+				if ( tfoot.length === 0 || tfoot.children().length === 0 ) {
+					$this.addClass( oClasses.sNoFooter );
+				}
+				else if ( tfoot.length > 0 ) {
+					oSettings.nTFoot = tfoot[0];
+					_fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot );
+				}
+			
+				/* Check if there is data passing into the constructor */
+				if ( oInit.aaData ) {
+					for ( i=0 ; i<oInit.aaData.length ; i++ ) {
+						_fnAddData( oSettings, oInit.aaData[ i ] );
+					}
+				}
+				else if ( oSettings.bDeferLoading || _fnDataSource( oSettings ) == 'dom' ) {
+					/* Grab the data from the page - only do this when deferred loading or no Ajax
+					 * source since there is no point in reading the DOM data if we are then going
+					 * to replace it with Ajax data
+					 */
+					_fnAddTr( oSettings, $(oSettings.nTBody).children('tr') );
+				}
+			
+				/* Copy the data index array */
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+				/* Initialisation complete - table can be drawn */
+				oSettings.bInitialised = true;
+			
+				/* Check if we need to initialise the table (it might not have been handed off to the
+				 * language processor)
+				 */
+				if ( bInitHandedOff === false ) {
+					_fnInitialise( oSettings );
+				}
+			};
+			
+			/* Must be done after everything which can be overridden by the state saving! */
+			if ( oInit.bStateSave )
+			{
+				features.bStateSave = true;
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' );
+				_fnLoadState( oSettings, oInit, loadedInit );
+			}
+			else {
+				loadedInit();
+			}
+			
+		} );
+		_that = null;
+		return this;
+	};
+
+	
+	/*
+	 * It is useful to have variables which are scoped locally so only the
+	 * DataTables functions can access them and they don't leak into global space.
+	 * At the same time these functions are often useful over multiple files in the
+	 * core and API, so we list, or at least document, all variables which are used
+	 * by DataTables as private variables here. This also ensures that there is no
+	 * clashing of variable names and that they can easily referenced for reuse.
+	 */
+	
+	
+	// Defined else where
+	//  _selector_run
+	//  _selector_opts
+	//  _selector_first
+	//  _selector_row_indexes
+	
+	var _ext; // DataTable.ext
+	var _Api; // DataTable.Api
+	var _api_register; // DataTable.Api.register
+	var _api_registerPlural; // DataTable.Api.registerPlural
+	
+	var _re_dic = {};
+	var _re_new_lines = /[\r\n]/g;
+	var _re_html = /<.*?>/g;
+	
+	// This is not strict ISO8601 - Date.parse() is quite lax, although
+	// implementations differ between browsers.
+	var _re_date = /^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/;
+	
+	// Escape regular expression special characters
+	var _re_escape_regex = new RegExp( '(\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ].join('|\\') + ')', 'g' );
+	
+	// http://en.wikipedia.org/wiki/Foreign_exchange_market
+	// - \u20BD - Russian ruble.
+	// - \u20a9 - South Korean Won
+	// - \u20BA - Turkish Lira
+	// - \u20B9 - Indian Rupee
+	// - R - Brazil (R$) and South Africa
+	// - fr - Swiss Franc
+	// - kr - Swedish krona, Norwegian krone and Danish krone
+	// - \u2009 is thin space and \u202F is narrow no-break space, both used in many
+	//   standards as thousands separators.
+	var _re_formatted_numeric = /[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi;
+	
+	
+	var _empty = function ( d ) {
+		return !d || d === true || d === '-' ? true : false;
+	};
+	
+	
+	var _intVal = function ( s ) {
+		var integer = parseInt( s, 10 );
+		return !isNaN(integer) && isFinite(s) ? integer : null;
+	};
+	
+	// Convert from a formatted number with characters other than `.` as the
+	// decimal place, to a Javascript number
+	var _numToDecimal = function ( num, decimalPoint ) {
+		// Cache created regular expressions for speed as this function is called often
+		if ( ! _re_dic[ decimalPoint ] ) {
+			_re_dic[ decimalPoint ] = new RegExp( _fnEscapeRegex( decimalPoint ), 'g' );
+		}
+		return typeof num === 'string' && decimalPoint !== '.' ?
+			num.replace( /\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) :
+			num;
+	};
+	
+	
+	var _isNumber = function ( d, decimalPoint, formatted ) {
+		var strType = typeof d === 'string';
+	
+		// If empty return immediately so there must be a number if it is a
+		// formatted string (this stops the string "k", or "kr", etc being detected
+		// as a formatted number for currency
+		if ( _empty( d ) ) {
+			return true;
+		}
+	
+		if ( decimalPoint && strType ) {
+			d = _numToDecimal( d, decimalPoint );
+		}
+	
+		if ( formatted && strType ) {
+			d = d.replace( _re_formatted_numeric, '' );
+		}
+	
+		return !isNaN( parseFloat(d) ) && isFinite( d );
+	};
+	
+	
+	// A string without HTML in it can be considered to be HTML still
+	var _isHtml = function ( d ) {
+		return _empty( d ) || typeof d === 'string';
+	};
+	
+	
+	var _htmlNumeric = function ( d, decimalPoint, formatted ) {
+		if ( _empty( d ) ) {
+			return true;
+		}
+	
+		var html = _isHtml( d );
+		return ! html ?
+			null :
+			_isNumber( _stripHtml( d ), decimalPoint, formatted ) ?
+				true :
+				null;
+	};
+	
+	
+	var _pluck = function ( a, prop, prop2 ) {
+		var out = [];
+		var i=0, ien=a.length;
+	
+		// Could have the test in the loop for slightly smaller code, but speed
+		// is essential here
+		if ( prop2 !== undefined ) {
+			for ( ; i<ien ; i++ ) {
+				if ( a[i] && a[i][ prop ] ) {
+					out.push( a[i][ prop ][ prop2 ] );
+				}
+			}
+		}
+		else {
+			for ( ; i<ien ; i++ ) {
+				if ( a[i] ) {
+					out.push( a[i][ prop ] );
+				}
+			}
+		}
+	
+		return out;
+	};
+	
+	
+	// Basically the same as _pluck, but rather than looping over `a` we use `order`
+	// as the indexes to pick from `a`
+	var _pluck_order = function ( a, order, prop, prop2 )
+	{
+		var out = [];
+		var i=0, ien=order.length;
+	
+		// Could have the test in the loop for slightly smaller code, but speed
+		// is essential here
+		if ( prop2 !== undefined ) {
+			for ( ; i<ien ; i++ ) {
+				if ( a[ order[i] ][ prop ] ) {
+					out.push( a[ order[i] ][ prop ][ prop2 ] );
+				}
+			}
+		}
+		else {
+			for ( ; i<ien ; i++ ) {
+				out.push( a[ order[i] ][ prop ] );
+			}
+		}
+	
+		return out;
+	};
+	
+	
+	var _range = function ( len, start )
+	{
+		var out = [];
+		var end;
+	
+		if ( start === undefined ) {
+			start = 0;
+			end = len;
+		}
+		else {
+			end = start;
+			start = len;
+		}
+	
+		for ( var i=start ; i<end ; i++ ) {
+			out.push( i );
+		}
+	
+		return out;
+	};
+	
+	
+	var _removeEmpty = function ( a )
+	{
+		var out = [];
+	
+		for ( var i=0, ien=a.length ; i<ien ; i++ ) {
+			if ( a[i] ) { // careful - will remove all falsy values!
+				out.push( a[i] );
+			}
+		}
+	
+		return out;
+	};
+	
+	
+	var _stripHtml = function ( d ) {
+		return d.replace( _re_html, '' );
+	};
+	
+	
+	/**
+	 * Determine if all values in the array are unique. This means we can short
+	 * cut the _unique method at the cost of a single loop. A sorted array is used
+	 * to easily check the values.
+	 *
+	 * @param  {array} src Source array
+	 * @return {boolean} true if all unique, false otherwise
+	 * @ignore
+	 */
+	var _areAllUnique = function ( src ) {
+		if ( src.length < 2 ) {
+			return true;
+		}
+	
+		var sorted = src.slice().sort();
+		var last = sorted[0];
+	
+		for ( var i=1, ien=sorted.length ; i<ien ; i++ ) {
+			if ( sorted[i] === last ) {
+				return false;
+			}
+	
+			last = sorted[i];
+		}
+	
+		return true;
+	};
+	
+	
+	/**
+	 * Find the unique elements in a source array.
+	 *
+	 * @param  {array} src Source array
+	 * @return {array} Array of unique items
+	 * @ignore
+	 */
+	var _unique = function ( src )
+	{
+		if ( _areAllUnique( src ) ) {
+			return src.slice();
+		}
+	
+		// A faster unique method is to use object keys to identify used values,
+		// but this doesn't work with arrays or objects, which we must also
+		// consider. See jsperf.com/compare-array-unique-versions/4 for more
+		// information.
+		var
+			out = [],
+			val,
+			i, ien=src.length,
+			j, k=0;
+	
+		again: for ( i=0 ; i<ien ; i++ ) {
+			val = src[i];
+	
+			for ( j=0 ; j<k ; j++ ) {
+				if ( out[j] === val ) {
+					continue again;
+				}
+			}
+	
+			out.push( val );
+			k++;
+		}
+	
+		return out;
+	};
+	
+	
+	/**
+	 * DataTables utility methods
+	 * 
+	 * This namespace provides helper methods that DataTables uses internally to
+	 * create a DataTable, but which are not exclusively used only for DataTables.
+	 * These methods can be used by extension authors to save the duplication of
+	 * code.
+	 *
+	 *  @namespace
+	 */
+	DataTable.util = {
+		/**
+		 * Throttle the calls to a function. Arguments and context are maintained
+		 * for the throttled function.
+		 *
+		 * @param {function} fn Function to be called
+		 * @param {integer} freq Call frequency in mS
+		 * @return {function} Wrapped function
+		 */
+		throttle: function ( fn, freq ) {
+			var
+				frequency = freq !== undefined ? freq : 200,
+				last,
+				timer;
+	
+			return function () {
+				var
+					that = this,
+					now  = +new Date(),
+					args = arguments;
+	
+				if ( last && now < last + frequency ) {
+					clearTimeout( timer );
+	
+					timer = setTimeout( function () {
+						last = undefined;
+						fn.apply( that, args );
+					}, frequency );
+				}
+				else {
+					last = now;
+					fn.apply( that, args );
+				}
+			};
+		},
+	
+	
+		/**
+		 * Escape a string such that it can be used in a regular expression
+		 *
+		 *  @param {string} val string to escape
+		 *  @returns {string} escaped string
+		 */
+		escapeRegex: function ( val ) {
+			return val.replace( _re_escape_regex, '\\$1' );
+		}
+	};
+	
+	
+	
+	/**
+	 * Create a mapping object that allows camel case parameters to be looked up
+	 * for their Hungarian counterparts. The mapping is stored in a private
+	 * parameter called `_hungarianMap` which can be accessed on the source object.
+	 *  @param {object} o
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnHungarianMap ( o )
+	{
+		var
+			hungarian = 'a aa ai ao as b fn i m o s ',
+			match,
+			newKey,
+			map = {};
+	
+		$.each( o, function (key, val) {
+			match = key.match(/^([^A-Z]+?)([A-Z])/);
+	
+			if ( match && hungarian.indexOf(match[1]+' ') !== -1 )
+			{
+				newKey = key.replace( match[0], match[2].toLowerCase() );
+				map[ newKey ] = key;
+	
+				if ( match[1] === 'o' )
+				{
+					_fnHungarianMap( o[key] );
+				}
+			}
+		} );
+	
+		o._hungarianMap = map;
+	}
+	
+	
+	/**
+	 * Convert from camel case parameters to Hungarian, based on a Hungarian map
+	 * created by _fnHungarianMap.
+	 *  @param {object} src The model object which holds all parameters that can be
+	 *    mapped.
+	 *  @param {object} user The object to convert from camel case to Hungarian.
+	 *  @param {boolean} force When set to `true`, properties which already have a
+	 *    Hungarian value in the `user` object will be overwritten. Otherwise they
+	 *    won't be.
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnCamelToHungarian ( src, user, force )
+	{
+		if ( ! src._hungarianMap ) {
+			_fnHungarianMap( src );
+		}
+	
+		var hungarianKey;
+	
+		$.each( user, function (key, val) {
+			hungarianKey = src._hungarianMap[ key ];
+	
+			if ( hungarianKey !== undefined && (force || user[hungarianKey] === undefined) )
+			{
+				// For objects, we need to buzz down into the object to copy parameters
+				if ( hungarianKey.charAt(0) === 'o' )
+				{
+					// Copy the camelCase options over to the hungarian
+					if ( ! user[ hungarianKey ] ) {
+						user[ hungarianKey ] = {};
+					}
+					$.extend( true, user[hungarianKey], user[key] );
+	
+					_fnCamelToHungarian( src[hungarianKey], user[hungarianKey], force );
+				}
+				else {
+					user[hungarianKey] = user[ key ];
+				}
+			}
+		} );
+	}
+	
+	
+	/**
+	 * Language compatibility - when certain options are given, and others aren't, we
+	 * need to duplicate the values over, in order to provide backwards compatibility
+	 * with older language files.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnLanguageCompat( lang )
+	{
+		var defaults = DataTable.defaults.oLanguage;
+		var zeroRecords = lang.sZeroRecords;
+	
+		/* Backwards compatibility - if there is no sEmptyTable given, then use the same as
+		 * sZeroRecords - assuming that is given.
+		 */
+		if ( ! lang.sEmptyTable && zeroRecords &&
+			defaults.sEmptyTable === "No data available in table" )
+		{
+			_fnMap( lang, lang, 'sZeroRecords', 'sEmptyTable' );
+		}
+	
+		/* Likewise with loading records */
+		if ( ! lang.sLoadingRecords && zeroRecords &&
+			defaults.sLoadingRecords === "Loading..." )
+		{
+			_fnMap( lang, lang, 'sZeroRecords', 'sLoadingRecords' );
+		}
+	
+		// Old parameter name of the thousands separator mapped onto the new
+		if ( lang.sInfoThousands ) {
+			lang.sThousands = lang.sInfoThousands;
+		}
+	
+		var decimal = lang.sDecimal;
+		if ( decimal ) {
+			_addNumericSort( decimal );
+		}
+	}
+	
+	
+	/**
+	 * Map one parameter onto another
+	 *  @param {object} o Object to map
+	 *  @param {*} knew The new parameter name
+	 *  @param {*} old The old parameter name
+	 */
+	var _fnCompatMap = function ( o, knew, old ) {
+		if ( o[ knew ] !== undefined ) {
+			o[ old ] = o[ knew ];
+		}
+	};
+	
+	
+	/**
+	 * Provide backwards compatibility for the main DT options. Note that the new
+	 * options are mapped onto the old parameters, so this is an external interface
+	 * change only.
+	 *  @param {object} init Object to map
+	 */
+	function _fnCompatOpts ( init )
+	{
+		_fnCompatMap( init, 'ordering',      'bSort' );
+		_fnCompatMap( init, 'orderMulti',    'bSortMulti' );
+		_fnCompatMap( init, 'orderClasses',  'bSortClasses' );
+		_fnCompatMap( init, 'orderCellsTop', 'bSortCellsTop' );
+		_fnCompatMap( init, 'order',         'aaSorting' );
+		_fnCompatMap( init, 'orderFixed',    'aaSortingFixed' );
+		_fnCompatMap( init, 'paging',        'bPaginate' );
+		_fnCompatMap( init, 'pagingType',    'sPaginationType' );
+		_fnCompatMap( init, 'pageLength',    'iDisplayLength' );
+		_fnCompatMap( init, 'searching',     'bFilter' );
+	
+		// Boolean initialisation of x-scrolling
+		if ( typeof init.sScrollX === 'boolean' ) {
+			init.sScrollX = init.sScrollX ? '100%' : '';
+		}
+		if ( typeof init.scrollX === 'boolean' ) {
+			init.scrollX = init.scrollX ? '100%' : '';
+		}
+	
+		// Column search objects are in an array, so it needs to be converted
+		// element by element
+		var searchCols = init.aoSearchCols;
+	
+		if ( searchCols ) {
+			for ( var i=0, ien=searchCols.length ; i<ien ; i++ ) {
+				if ( searchCols[i] ) {
+					_fnCamelToHungarian( DataTable.models.oSearch, searchCols[i] );
+				}
+			}
+		}
+	}
+	
+	
+	/**
+	 * Provide backwards compatibility for column options. Note that the new options
+	 * are mapped onto the old parameters, so this is an external interface change
+	 * only.
+	 *  @param {object} init Object to map
+	 */
+	function _fnCompatCols ( init )
+	{
+		_fnCompatMap( init, 'orderable',     'bSortable' );
+		_fnCompatMap( init, 'orderData',     'aDataSort' );
+		_fnCompatMap( init, 'orderSequence', 'asSorting' );
+		_fnCompatMap( init, 'orderDataType', 'sortDataType' );
+	
+		// orderData can be given as an integer
+		var dataSort = init.aDataSort;
+		if ( typeof dataSort === 'number' && ! $.isArray( dataSort ) ) {
+			init.aDataSort = [ dataSort ];
+		}
+	}
+	
+	
+	/**
+	 * Browser feature detection for capabilities, quirks
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnBrowserDetect( settings )
+	{
+		// We don't need to do this every time DataTables is constructed, the values
+		// calculated are specific to the browser and OS configuration which we
+		// don't expect to change between initialisations
+		if ( ! DataTable.__browser ) {
+			var browser = {};
+			DataTable.__browser = browser;
+	
+			// Scrolling feature / quirks detection
+			var n = $('<div/>')
+				.css( {
+					position: 'fixed',
+					top: 0,
+					left: $(window).scrollLeft()*-1, // allow for scrolling
+					height: 1,
+					width: 1,
+					overflow: 'hidden'
+				} )
+				.append(
+					$('<div/>')
+						.css( {
+							position: 'absolute',
+							top: 1,
+							left: 1,
+							width: 100,
+							overflow: 'scroll'
+						} )
+						.append(
+							$('<div/>')
+								.css( {
+									width: '100%',
+									height: 10
+								} )
+						)
+				)
+				.appendTo( 'body' );
+	
+			var outer = n.children();
+			var inner = outer.children();
+	
+			// Numbers below, in order, are:
+			// inner.offsetWidth, inner.clientWidth, outer.offsetWidth, outer.clientWidth
+			//
+			// IE6 XP:                           100 100 100  83
+			// IE7 Vista:                        100 100 100  83
+			// IE 8+ Windows:                     83  83 100  83
+			// Evergreen Windows:                 83  83 100  83
+			// Evergreen Mac with scrollbars:     85  85 100  85
+			// Evergreen Mac without scrollbars: 100 100 100 100
+	
+			// Get scrollbar width
+			browser.barWidth = outer[0].offsetWidth - outer[0].clientWidth;
+	
+			// IE6/7 will oversize a width 100% element inside a scrolling element, to
+			// include the width of the scrollbar, while other browsers ensure the inner
+			// element is contained without forcing scrolling
+			browser.bScrollOversize = inner[0].offsetWidth === 100 && outer[0].clientWidth !== 100;
+	
+			// In rtl text layout, some browsers (most, but not all) will place the
+			// scrollbar on the left, rather than the right.
+			browser.bScrollbarLeft = Math.round( inner.offset().left ) !== 1;
+	
+			// IE8- don't provide height and width for getBoundingClientRect
+			browser.bBounding = n[0].getBoundingClientRect().width ? true : false;
+	
+			n.remove();
+		}
+	
+		$.extend( settings.oBrowser, DataTable.__browser );
+		settings.oScroll.iBarWidth = DataTable.__browser.barWidth;
+	}
+	
+	
+	/**
+	 * Array.prototype reduce[Right] method, used for browsers which don't support
+	 * JS 1.6. Done this way to reduce code size, since we iterate either way
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnReduce ( that, fn, init, start, end, inc )
+	{
+		var
+			i = start,
+			value,
+			isSet = false;
+	
+		if ( init !== undefined ) {
+			value = init;
+			isSet = true;
+		}
+	
+		while ( i !== end ) {
+			if ( ! that.hasOwnProperty(i) ) {
+				continue;
+			}
+	
+			value = isSet ?
+				fn( value, that[i], i, that ) :
+				that[i];
+	
+			isSet = true;
+			i += inc;
+		}
+	
+		return value;
+	}
+	
+	/**
+	 * Add a column to the list used for the table with default values
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {node} nTh The th element for this column
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAddColumn( oSettings, nTh )
+	{
+		// Add column to aoColumns array
+		var oDefaults = DataTable.defaults.column;
+		var iCol = oSettings.aoColumns.length;
+		var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, {
+			"nTh": nTh ? nTh : document.createElement('th'),
+			"sTitle":    oDefaults.sTitle    ? oDefaults.sTitle    : nTh ? nTh.innerHTML : '',
+			"aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol],
+			"mData": oDefaults.mData ? oDefaults.mData : iCol,
+			idx: iCol
+		} );
+		oSettings.aoColumns.push( oCol );
+	
+		// Add search object for column specific search. Note that the `searchCols[ iCol ]`
+		// passed into extend can be undefined. This allows the user to give a default
+		// with only some of the parameters defined, and also not give a default
+		var searchCols = oSettings.aoPreSearchCols;
+		searchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] );
+	
+		// Use the default column options function to initialise classes etc
+		_fnColumnOptions( oSettings, iCol, $(nTh).data() );
+	}
+	
+	
+	/**
+	 * Apply options for a column
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {int} iCol column index to consider
+	 *  @param {object} oOptions object with sType, bVisible and bSearchable etc
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnColumnOptions( oSettings, iCol, oOptions )
+	{
+		var oCol = oSettings.aoColumns[ iCol ];
+		var oClasses = oSettings.oClasses;
+		var th = $(oCol.nTh);
+	
+		// Try to get width information from the DOM. We can't get it from CSS
+		// as we'd need to parse the CSS stylesheet. `width` option can override
+		if ( ! oCol.sWidthOrig ) {
+			// Width attribute
+			oCol.sWidthOrig = th.attr('width') || null;
+	
+			// Style attribute
+			var t = (th.attr('style') || '').match(/width:\s*(\d+[pxem%]+)/);
+			if ( t ) {
+				oCol.sWidthOrig = t[1];
+			}
+		}
+	
+		/* User specified column options */
+		if ( oOptions !== undefined && oOptions !== null )
+		{
+			// Backwards compatibility
+			_fnCompatCols( oOptions );
+	
+			// Map camel case parameters to their Hungarian counterparts
+			_fnCamelToHungarian( DataTable.defaults.column, oOptions );
+	
+			/* Backwards compatibility for mDataProp */
+			if ( oOptions.mDataProp !== undefined && !oOptions.mData )
+			{
+				oOptions.mData = oOptions.mDataProp;
+			}
+	
+			if ( oOptions.sType )
+			{
+				oCol._sManualType = oOptions.sType;
+			}
+	
+			// `class` is a reserved word in Javascript, so we need to provide
+			// the ability to use a valid name for the camel case input
+			if ( oOptions.className && ! oOptions.sClass )
+			{
+				oOptions.sClass = oOptions.className;
+			}
+	
+			$.extend( oCol, oOptions );
+			_fnMap( oCol, oOptions, "sWidth", "sWidthOrig" );
+	
+			/* iDataSort to be applied (backwards compatibility), but aDataSort will take
+			 * priority if defined
+			 */
+			if ( oOptions.iDataSort !== undefined )
+			{
+				oCol.aDataSort = [ oOptions.iDataSort ];
+			}
+			_fnMap( oCol, oOptions, "aDataSort" );
+		}
+	
+		/* Cache the data get and set functions for speed */
+		var mDataSrc = oCol.mData;
+		var mData = _fnGetObjectDataFn( mDataSrc );
+		var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null;
+	
+		var attrTest = function( src ) {
+			return typeof src === 'string' && src.indexOf('@') !== -1;
+		};
+		oCol._bAttrSrc = $.isPlainObject( mDataSrc ) && (
+			attrTest(mDataSrc.sort) || attrTest(mDataSrc.type) || attrTest(mDataSrc.filter)
+		);
+		oCol._setter = null;
+	
+		oCol.fnGetData = function (rowData, type, meta) {
+			var innerData = mData( rowData, type, undefined, meta );
+	
+			return mRender && type ?
+				mRender( innerData, type, rowData, meta ) :
+				innerData;
+		};
+		oCol.fnSetData = function ( rowData, val, meta ) {
+			return _fnSetObjectDataFn( mDataSrc )( rowData, val, meta );
+		};
+	
+		// Indicate if DataTables should read DOM data as an object or array
+		// Used in _fnGetRowElements
+		if ( typeof mDataSrc !== 'number' ) {
+			oSettings._rowReadObject = true;
+		}
+	
+		/* Feature sorting overrides column specific when off */
+		if ( !oSettings.oFeatures.bSort )
+		{
+			oCol.bSortable = false;
+			th.addClass( oClasses.sSortableNone ); // Have to add class here as order event isn't called
+		}
+	
+		/* Check that the class assignment is correct for sorting */
+		var bAsc = $.inArray('asc', oCol.asSorting) !== -1;
+		var bDesc = $.inArray('desc', oCol.asSorting) !== -1;
+		if ( !oCol.bSortable || (!bAsc && !bDesc) )
+		{
+			oCol.sSortingClass = oClasses.sSortableNone;
+			oCol.sSortingClassJUI = "";
+		}
+		else if ( bAsc && !bDesc )
+		{
+			oCol.sSortingClass = oClasses.sSortableAsc;
+			oCol.sSortingClassJUI = oClasses.sSortJUIAscAllowed;
+		}
+		else if ( !bAsc && bDesc )
+		{
+			oCol.sSortingClass = oClasses.sSortableDesc;
+			oCol.sSortingClassJUI = oClasses.sSortJUIDescAllowed;
+		}
+		else
+		{
+			oCol.sSortingClass = oClasses.sSortable;
+			oCol.sSortingClassJUI = oClasses.sSortJUI;
+		}
+	}
+	
+	
+	/**
+	 * Adjust the table column widths for new data. Note: you would probably want to
+	 * do a redraw after calling this function!
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAdjustColumnSizing ( settings )
+	{
+		/* Not interested in doing column width calculation if auto-width is disabled */
+		if ( settings.oFeatures.bAutoWidth !== false )
+		{
+			var columns = settings.aoColumns;
+	
+			_fnCalculateColumnWidths( settings );
+			for ( var i=0 , iLen=columns.length ; i<iLen ; i++ )
+			{
+				columns[i].nTh.style.width = columns[i].sWidth;
+			}
+		}
+	
+		var scroll = settings.oScroll;
+		if ( scroll.sY !== '' || scroll.sX !== '')
+		{
+			_fnScrollDraw( settings );
+		}
+	
+		_fnCallbackFire( settings, null, 'column-sizing', [settings] );
+	}
+	
+	
+	/**
+	 * Covert the index of a visible column to the index in the data array (take account
+	 * of hidden columns)
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {int} iMatch Visible column index to lookup
+	 *  @returns {int} i the data index
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnVisibleToColumnIndex( oSettings, iMatch )
+	{
+		var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+	
+		return typeof aiVis[iMatch] === 'number' ?
+			aiVis[iMatch] :
+			null;
+	}
+	
+	
+	/**
+	 * Covert the index of an index in the data array and convert it to the visible
+	 *   column index (take account of hidden columns)
+	 *  @param {int} iMatch Column index to lookup
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns {int} i the data index
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnColumnIndexToVisible( oSettings, iMatch )
+	{
+		var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+		var iPos = $.inArray( iMatch, aiVis );
+	
+		return iPos !== -1 ? iPos : null;
+	}
+	
+	
+	/**
+	 * Get the number of visible columns
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns {int} i the number of visible columns
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnVisbleColumns( oSettings )
+	{
+		var vis = 0;
+	
+		// No reduce in IE8, use a loop for now
+		$.each( oSettings.aoColumns, function ( i, col ) {
+			if ( col.bVisible && $(col.nTh).css('display') !== 'none' ) {
+				vis++;
+			}
+		} );
+	
+		return vis;
+	}
+	
+	
+	/**
+	 * Get an array of column indexes that match a given property
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {string} sParam Parameter in aoColumns to look for - typically
+	 *    bVisible or bSearchable
+	 *  @returns {array} Array of indexes with matched properties
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetColumns( oSettings, sParam )
+	{
+		var a = [];
+	
+		$.map( oSettings.aoColumns, function(val, i) {
+			if ( val[sParam] ) {
+				a.push( i );
+			}
+		} );
+	
+		return a;
+	}
+	
+	
+	/**
+	 * Calculate the 'type' of a column
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnColumnTypes ( settings )
+	{
+		var columns = settings.aoColumns;
+		var data = settings.aoData;
+		var types = DataTable.ext.type.detect;
+		var i, ien, j, jen, k, ken;
+		var col, cell, detectedType, cache;
+	
+		// For each column, spin over the 
+		for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+			col = columns[i];
+			cache = [];
+	
+			if ( ! col.sType && col._sManualType ) {
+				col.sType = col._sManualType;
+			}
+			else if ( ! col.sType ) {
+				for ( j=0, jen=types.length ; j<jen ; j++ ) {
+					for ( k=0, ken=data.length ; k<ken ; k++ ) {
+						// Use a cache array so we only need to get the type data
+						// from the formatter once (when using multiple detectors)
+						if ( cache[k] === undefined ) {
+							cache[k] = _fnGetCellData( settings, k, i, 'type' );
+						}
+	
+						detectedType = types[j]( cache[k], settings );
+	
+						// If null, then this type can't apply to this column, so
+						// rather than testing all cells, break out. There is an
+						// exception for the last type which is `html`. We need to
+						// scan all rows since it is possible to mix string and HTML
+						// types
+						if ( ! detectedType && j !== types.length-1 ) {
+							break;
+						}
+	
+						// Only a single match is needed for html type since it is
+						// bottom of the pile and very similar to string
+						if ( detectedType === 'html' ) {
+							break;
+						}
+					}
+	
+					// Type is valid for all data points in the column - use this
+					// type
+					if ( detectedType ) {
+						col.sType = detectedType;
+						break;
+					}
+				}
+	
+				// Fall back - if no type was detected, always use string
+				if ( ! col.sType ) {
+					col.sType = 'string';
+				}
+			}
+		}
+	}
+	
+	
+	/**
+	 * Take the column definitions and static columns arrays and calculate how
+	 * they relate to column indexes. The callback function will then apply the
+	 * definition found for a column to a suitable configuration object.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {array} aoColDefs The aoColumnDefs array that is to be applied
+	 *  @param {array} aoCols The aoColumns array that defines columns individually
+	 *  @param {function} fn Callback function - takes two parameters, the calculated
+	 *    column index and the definition for that column.
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, fn )
+	{
+		var i, iLen, j, jLen, k, kLen, def;
+		var columns = oSettings.aoColumns;
+	
+		// Column definitions with aTargets
+		if ( aoColDefs )
+		{
+			/* Loop over the definitions array - loop in reverse so first instance has priority */
+			for ( i=aoColDefs.length-1 ; i>=0 ; i-- )
+			{
+				def = aoColDefs[i];
+	
+				/* Each definition can target multiple columns, as it is an array */
+				var aTargets = def.targets !== undefined ?
+					def.targets :
+					def.aTargets;
+	
+				if ( ! $.isArray( aTargets ) )
+				{
+					aTargets = [ aTargets ];
+				}
+	
+				for ( j=0, jLen=aTargets.length ; j<jLen ; j++ )
+				{
+					if ( typeof aTargets[j] === 'number' && aTargets[j] >= 0 )
+					{
+						/* Add columns that we don't yet know about */
+						while( columns.length <= aTargets[j] )
+						{
+							_fnAddColumn( oSettings );
+						}
+	
+						/* Integer, basic index */
+						fn( aTargets[j], def );
+					}
+					else if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 )
+					{
+						/* Negative integer, right to left column counting */
+						fn( columns.length+aTargets[j], def );
+					}
+					else if ( typeof aTargets[j] === 'string' )
+					{
+						/* Class name matching on TH element */
+						for ( k=0, kLen=columns.length ; k<kLen ; k++ )
+						{
+							if ( aTargets[j] == "_all" ||
+							     $(columns[k].nTh).hasClass( aTargets[j] ) )
+							{
+								fn( k, def );
+							}
+						}
+					}
+				}
+			}
+		}
+	
+		// Statically defined columns array
+		if ( aoCols )
+		{
+			for ( i=0, iLen=aoCols.length ; i<iLen ; i++ )
+			{
+				fn( i, aoCols[i] );
+			}
+		}
+	}
+	
+	/**
+	 * Add a data array to the table, creating DOM node etc. This is the parallel to
+	 * _fnGatherData, but for adding rows from a Javascript source, rather than a
+	 * DOM source.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {array} aData data array to be added
+	 *  @param {node} [nTr] TR element to add to the table - optional. If not given,
+	 *    DataTables will create a row automatically
+	 *  @param {array} [anTds] Array of TD|TH elements for the row - must be given
+	 *    if nTr is.
+	 *  @returns {int} >=0 if successful (index of new aoData entry), -1 if failed
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAddData ( oSettings, aDataIn, nTr, anTds )
+	{
+		/* Create the object for storing information about this new row */
+		var iRow = oSettings.aoData.length;
+		var oData = $.extend( true, {}, DataTable.models.oRow, {
+			src: nTr ? 'dom' : 'data',
+			idx: iRow
+		} );
+	
+		oData._aData = aDataIn;
+		oSettings.aoData.push( oData );
+	
+		/* Create the cells */
+		var nTd, sThisType;
+		var columns = oSettings.aoColumns;
+	
+		// Invalidate the column types as the new data needs to be revalidated
+		for ( var i=0, iLen=columns.length ; i<iLen ; i++ )
+		{
+			columns[i].sType = null;
+		}
+	
+		/* Add to the display array */
+		oSettings.aiDisplayMaster.push( iRow );
+	
+		var id = oSettings.rowIdFn( aDataIn );
+		if ( id !== undefined ) {
+			oSettings.aIds[ id ] = oData;
+		}
+	
+		/* Create the DOM information, or register it if already present */
+		if ( nTr || ! oSettings.oFeatures.bDeferRender )
+		{
+			_fnCreateTr( oSettings, iRow, nTr, anTds );
+		}
+	
+		return iRow;
+	}
+	
+	
+	/**
+	 * Add one or more TR elements to the table. Generally we'd expect to
+	 * use this for reading data from a DOM sourced table, but it could be
+	 * used for an TR element. Note that if a TR is given, it is used (i.e.
+	 * it is not cloned).
+	 *  @param {object} settings dataTables settings object
+	 *  @param {array|node|jQuery} trs The TR element(s) to add to the table
+	 *  @returns {array} Array of indexes for the added rows
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAddTr( settings, trs )
+	{
+		var row;
+	
+		// Allow an individual node to be passed in
+		if ( ! (trs instanceof $) ) {
+			trs = $(trs);
+		}
+	
+		return trs.map( function (i, el) {
+			row = _fnGetRowElements( settings, el );
+			return _fnAddData( settings, row.data, el, row.cells );
+		} );
+	}
+	
+	
+	/**
+	 * Take a TR element and convert it to an index in aoData
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {node} n the TR element to find
+	 *  @returns {int} index if the node is found, null if not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnNodeToDataIndex( oSettings, n )
+	{
+		return (n._DT_RowIndex!==undefined) ? n._DT_RowIndex : null;
+	}
+	
+	
+	/**
+	 * Take a TD element and convert it into a column data index (not the visible index)
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {int} iRow The row number the TD/TH can be found in
+	 *  @param {node} n The TD/TH element to find
+	 *  @returns {int} index if the node is found, -1 if not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnNodeToColumnIndex( oSettings, iRow, n )
+	{
+		return $.inArray( n, oSettings.aoData[ iRow ].anCells );
+	}
+	
+	
+	/**
+	 * Get the data for a given cell from the internal cache, taking into account data mapping
+	 *  @param {object} settings dataTables settings object
+	 *  @param {int} rowIdx aoData row id
+	 *  @param {int} colIdx Column index
+	 *  @param {string} type data get type ('display', 'type' 'filter' 'sort')
+	 *  @returns {*} Cell data
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetCellData( settings, rowIdx, colIdx, type )
+	{
+		var draw           = settings.iDraw;
+		var col            = settings.aoColumns[colIdx];
+		var rowData        = settings.aoData[rowIdx]._aData;
+		var defaultContent = col.sDefaultContent;
+		var cellData       = col.fnGetData( rowData, type, {
+			settings: settings,
+			row:      rowIdx,
+			col:      colIdx
+		} );
+	
+		if ( cellData === undefined ) {
+			if ( settings.iDrawError != draw && defaultContent === null ) {
+				_fnLog( settings, 0, "Requested unknown parameter "+
+					(typeof col.mData=='function' ? '{function}' : "'"+col.mData+"'")+
+					" for row "+rowIdx+", column "+colIdx, 4 );
+				settings.iDrawError = draw;
+			}
+			return defaultContent;
+		}
+	
+		// When the data source is null and a specific data type is requested (i.e.
+		// not the original data), we can use default column data
+		if ( (cellData === rowData || cellData === null) && defaultContent !== null && type !== undefined ) {
+			cellData = defaultContent;
+		}
+		else if ( typeof cellData === 'function' ) {
+			// If the data source is a function, then we run it and use the return,
+			// executing in the scope of the data object (for instances)
+			return cellData.call( rowData );
+		}
+	
+		if ( cellData === null && type == 'display' ) {
+			return '';
+		}
+		return cellData;
+	}
+	
+	
+	/**
+	 * Set the value for a specific cell, into the internal data cache
+	 *  @param {object} settings dataTables settings object
+	 *  @param {int} rowIdx aoData row id
+	 *  @param {int} colIdx Column index
+	 *  @param {*} val Value to set
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSetCellData( settings, rowIdx, colIdx, val )
+	{
+		var col     = settings.aoColumns[colIdx];
+		var rowData = settings.aoData[rowIdx]._aData;
+	
+		col.fnSetData( rowData, val, {
+			settings: settings,
+			row:      rowIdx,
+			col:      colIdx
+		}  );
+	}
+	
+	
+	// Private variable that is used to match action syntax in the data property object
+	var __reArray = /\[.*?\]$/;
+	var __reFn = /\(\)$/;
+	
+	/**
+	 * Split string on periods, taking into account escaped periods
+	 * @param  {string} str String to split
+	 * @return {array} Split string
+	 */
+	function _fnSplitObjNotation( str )
+	{
+		return $.map( str.match(/(\\.|[^\.])+/g) || [''], function ( s ) {
+			return s.replace(/\\\./g, '.');
+		} );
+	}
+	
+	
+	/**
+	 * Return a function that can be used to get data from a source object, taking
+	 * into account the ability to use nested objects as a source
+	 *  @param {string|int|function} mSource The data source for the object
+	 *  @returns {function} Data get function
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetObjectDataFn( mSource )
+	{
+		if ( $.isPlainObject( mSource ) )
+		{
+			/* Build an object of get functions, and wrap them in a single call */
+			var o = {};
+			$.each( mSource, function (key, val) {
+				if ( val ) {
+					o[key] = _fnGetObjectDataFn( val );
+				}
+			} );
+	
+			return function (data, type, row, meta) {
+				var t = o[type] || o._;
+				return t !== undefined ?
+					t(data, type, row, meta) :
+					data;
+			};
+		}
+		else if ( mSource === null )
+		{
+			/* Give an empty string for rendering / sorting etc */
+			return function (data) { // type, row and meta also passed, but not used
+				return data;
+			};
+		}
+		else if ( typeof mSource === 'function' )
+		{
+			return function (data, type, row, meta) {
+				return mSource( data, type, row, meta );
+			};
+		}
+		else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 ||
+			      mSource.indexOf('[') !== -1 || mSource.indexOf('(') !== -1) )
+		{
+			/* If there is a . in the source string then the data source is in a
+			 * nested object so we loop over the data for each level to get the next
+			 * level down. On each loop we test for undefined, and if found immediately
+			 * return. This allows entire objects to be missing and sDefaultContent to
+			 * be used if defined, rather than throwing an error
+			 */
+			var fetchData = function (data, type, src) {
+				var arrayNotation, funcNotation, out, innerSrc;
+	
+				if ( src !== "" )
+				{
+					var a = _fnSplitObjNotation( src );
+	
+					for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+					{
+						// Check if we are dealing with special notation
+						arrayNotation = a[i].match(__reArray);
+						funcNotation = a[i].match(__reFn);
+	
+						if ( arrayNotation )
+						{
+							// Array notation
+							a[i] = a[i].replace(__reArray, '');
+	
+							// Condition allows simply [] to be passed in
+							if ( a[i] !== "" ) {
+								data = data[ a[i] ];
+							}
+							out = [];
+	
+							// Get the remainder of the nested object to get
+							a.splice( 0, i+1 );
+							innerSrc = a.join('.');
+	
+							// Traverse each entry in the array getting the properties requested
+							if ( $.isArray( data ) ) {
+								for ( var j=0, jLen=data.length ; j<jLen ; j++ ) {
+									out.push( fetchData( data[j], type, innerSrc ) );
+								}
+							}
+	
+							// If a string is given in between the array notation indicators, that
+							// is used to join the strings together, otherwise an array is returned
+							var join = arrayNotation[0].substring(1, arrayNotation[0].length-1);
+							data = (join==="") ? out : out.join(join);
+	
+							// The inner call to fetchData has already traversed through the remainder
+							// of the source requested, so we exit from the loop
+							break;
+						}
+						else if ( funcNotation )
+						{
+							// Function call
+							a[i] = a[i].replace(__reFn, '');
+							data = data[ a[i] ]();
+							continue;
+						}
+	
+						if ( data === null || data[ a[i] ] === undefined )
+						{
+							return undefined;
+						}
+						data = data[ a[i] ];
+					}
+				}
+	
+				return data;
+			};
+	
+			return function (data, type) { // row and meta also passed, but not used
+				return fetchData( data, type, mSource );
+			};
+		}
+		else
+		{
+			/* Array or flat object mapping */
+			return function (data, type) { // row and meta also passed, but not used
+				return data[mSource];
+			};
+		}
+	}
+	
+	
+	/**
+	 * Return a function that can be used to set data from a source object, taking
+	 * into account the ability to use nested objects as a source
+	 *  @param {string|int|function} mSource The data source for the object
+	 *  @returns {function} Data set function
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSetObjectDataFn( mSource )
+	{
+		if ( $.isPlainObject( mSource ) )
+		{
+			/* Unlike get, only the underscore (global) option is used for for
+			 * setting data since we don't know the type here. This is why an object
+			 * option is not documented for `mData` (which is read/write), but it is
+			 * for `mRender` which is read only.
+			 */
+			return _fnSetObjectDataFn( mSource._ );
+		}
+		else if ( mSource === null )
+		{
+			/* Nothing to do when the data source is null */
+			return function () {};
+		}
+		else if ( typeof mSource === 'function' )
+		{
+			return function (data, val, meta) {
+				mSource( data, 'set', val, meta );
+			};
+		}
+		else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 ||
+			      mSource.indexOf('[') !== -1 || mSource.indexOf('(') !== -1) )
+		{
+			/* Like the get, we need to get data from a nested object */
+			var setData = function (data, val, src) {
+				var a = _fnSplitObjNotation( src ), b;
+				var aLast = a[a.length-1];
+				var arrayNotation, funcNotation, o, innerSrc;
+	
+				for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ )
+				{
+					// Check if we are dealing with an array notation request
+					arrayNotation = a[i].match(__reArray);
+					funcNotation = a[i].match(__reFn);
+	
+					if ( arrayNotation )
+					{
+						a[i] = a[i].replace(__reArray, '');
+						data[ a[i] ] = [];
+	
+						// Get the remainder of the nested object to set so we can recurse
+						b = a.slice();
+						b.splice( 0, i+1 );
+						innerSrc = b.join('.');
+	
+						// Traverse each entry in the array setting the properties requested
+						if ( $.isArray( val ) )
+						{
+							for ( var j=0, jLen=val.length ; j<jLen ; j++ )
+							{
+								o = {};
+								setData( o, val[j], innerSrc );
+								data[ a[i] ].push( o );
+							}
+						}
+						else
+						{
+							// We've been asked to save data to an array, but it
+							// isn't array data to be saved. Best that can be done
+							// is to just save the value.
+							data[ a[i] ] = val;
+						}
+	
+						// The inner call to setData has already traversed through the remainder
+						// of the source and has set the data, thus we can exit here
+						return;
+					}
+					else if ( funcNotation )
+					{
+						// Function call
+						a[i] = a[i].replace(__reFn, '');
+						data = data[ a[i] ]( val );
+					}
+	
+					// If the nested object doesn't currently exist - since we are
+					// trying to set the value - create it
+					if ( data[ a[i] ] === null || data[ a[i] ] === undefined )
+					{
+						data[ a[i] ] = {};
+					}
+					data = data[ a[i] ];
+				}
+	
+				// Last item in the input - i.e, the actual set
+				if ( aLast.match(__reFn ) )
+				{
+					// Function call
+					data = data[ aLast.replace(__reFn, '') ]( val );
+				}
+				else
+				{
+					// If array notation is used, we just want to strip it and use the property name
+					// and assign the value. If it isn't used, then we get the result we want anyway
+					data[ aLast.replace(__reArray, '') ] = val;
+				}
+			};
+	
+			return function (data, val) { // meta is also passed in, but not used
+				return setData( data, val, mSource );
+			};
+		}
+		else
+		{
+			/* Array or flat object mapping */
+			return function (data, val) { // meta is also passed in, but not used
+				data[mSource] = val;
+			};
+		}
+	}
+	
+	
+	/**
+	 * Return an array with the full table data
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns array {array} aData Master data array
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetDataMaster ( settings )
+	{
+		return _pluck( settings.aoData, '_aData' );
+	}
+	
+	
+	/**
+	 * Nuke the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnClearTable( settings )
+	{
+		settings.aoData.length = 0;
+		settings.aiDisplayMaster.length = 0;
+		settings.aiDisplay.length = 0;
+		settings.aIds = {};
+	}
+	
+	
+	 /**
+	 * Take an array of integers (index array) and remove a target integer (value - not
+	 * the key!)
+	 *  @param {array} a Index array to target
+	 *  @param {int} iTarget value to find
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnDeleteIndex( a, iTarget, splice )
+	{
+		var iTargetIndex = -1;
+	
+		for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+		{
+			if ( a[i] == iTarget )
+			{
+				iTargetIndex = i;
+			}
+			else if ( a[i] > iTarget )
+			{
+				a[i]--;
+			}
+		}
+	
+		if ( iTargetIndex != -1 && splice === undefined )
+		{
+			a.splice( iTargetIndex, 1 );
+		}
+	}
+	
+	
+	/**
+	 * Mark cached data as invalid such that a re-read of the data will occur when
+	 * the cached data is next requested. Also update from the data source object.
+	 *
+	 * @param {object} settings DataTables settings object
+	 * @param {int}    rowIdx   Row index to invalidate
+	 * @param {string} [src]    Source to invalidate from: undefined, 'auto', 'dom'
+	 *     or 'data'
+	 * @param {int}    [colIdx] Column index to invalidate. If undefined the whole
+	 *     row will be invalidated
+	 * @memberof DataTable#oApi
+	 *
+	 * @todo For the modularisation of v1.11 this will need to become a callback, so
+	 *   the sort and filter methods can subscribe to it. That will required
+	 *   initialisation options for sorting, which is why it is not already baked in
+	 */
+	function _fnInvalidate( settings, rowIdx, src, colIdx )
+	{
+		var row = settings.aoData[ rowIdx ];
+		var i, ien;
+		var cellWrite = function ( cell, col ) {
+			// This is very frustrating, but in IE if you just write directly
+			// to innerHTML, and elements that are overwritten are GC'ed,
+			// even if there is a reference to them elsewhere
+			while ( cell.childNodes.length ) {
+				cell.removeChild( cell.firstChild );
+			}
+	
+			cell.innerHTML = _fnGetCellData( settings, rowIdx, col, 'display' );
+		};
+	
+		// Are we reading last data from DOM or the data object?
+		if ( src === 'dom' || ((! src || src === 'auto') && row.src === 'dom') ) {
+			// Read the data from the DOM
+			row._aData = _fnGetRowElements(
+					settings, row, colIdx, colIdx === undefined ? undefined : row._aData
+				)
+				.data;
+		}
+		else {
+			// Reading from data object, update the DOM
+			var cells = row.anCells;
+	
+			if ( cells ) {
+				if ( colIdx !== undefined ) {
+					cellWrite( cells[colIdx], colIdx );
+				}
+				else {
+					for ( i=0, ien=cells.length ; i<ien ; i++ ) {
+						cellWrite( cells[i], i );
+					}
+				}
+			}
+		}
+	
+		// For both row and cell invalidation, the cached data for sorting and
+		// filtering is nulled out
+		row._aSortData = null;
+		row._aFilterData = null;
+	
+		// Invalidate the type for a specific column (if given) or all columns since
+		// the data might have changed
+		var cols = settings.aoColumns;
+		if ( colIdx !== undefined ) {
+			cols[ colIdx ].sType = null;
+		}
+		else {
+			for ( i=0, ien=cols.length ; i<ien ; i++ ) {
+				cols[i].sType = null;
+			}
+	
+			// Update DataTables special `DT_*` attributes for the row
+			_fnRowAttributes( settings, row );
+		}
+	}
+	
+	
+	/**
+	 * Build a data source object from an HTML row, reading the contents of the
+	 * cells that are in the row.
+	 *
+	 * @param {object} settings DataTables settings object
+	 * @param {node|object} TR element from which to read data or existing row
+	 *   object from which to re-read the data from the cells
+	 * @param {int} [colIdx] Optional column index
+	 * @param {array|object} [d] Data source object. If `colIdx` is given then this
+	 *   parameter should also be given and will be used to write the data into.
+	 *   Only the column in question will be written
+	 * @returns {object} Object with two parameters: `data` the data read, in
+	 *   document order, and `cells` and array of nodes (they can be useful to the
+	 *   caller, so rather than needing a second traversal to get them, just return
+	 *   them from here).
+	 * @memberof DataTable#oApi
+	 */
+	function _fnGetRowElements( settings, row, colIdx, d )
+	{
+		var
+			tds = [],
+			td = row.firstChild,
+			name, col, o, i=0, contents,
+			columns = settings.aoColumns,
+			objectRead = settings._rowReadObject;
+	
+		// Allow the data object to be passed in, or construct
+		d = d !== undefined ?
+			d :
+			objectRead ?
+				{} :
+				[];
+	
+		var attr = function ( str, td  ) {
+			if ( typeof str === 'string' ) {
+				var idx = str.indexOf('@');
+	
+				if ( idx !== -1 ) {
+					var attr = str.substring( idx+1 );
+					var setter = _fnSetObjectDataFn( str );
+					setter( d, td.getAttribute( attr ) );
+				}
+			}
+		};
+	
+		// Read data from a cell and store into the data object
+		var cellProcess = function ( cell ) {
+			if ( colIdx === undefined || colIdx === i ) {
+				col = columns[i];
+				contents = $.trim(cell.innerHTML);
+	
+				if ( col && col._bAttrSrc ) {
+					var setter = _fnSetObjectDataFn( col.mData._ );
+					setter( d, contents );
+	
+					attr( col.mData.sort, cell );
+					attr( col.mData.type, cell );
+					attr( col.mData.filter, cell );
+				}
+				else {
+					// Depending on the `data` option for the columns the data can
+					// be read to either an object or an array.
+					if ( objectRead ) {
+						if ( ! col._setter ) {
+							// Cache the setter function
+							col._setter = _fnSetObjectDataFn( col.mData );
+						}
+						col._setter( d, contents );
+					}
+					else {
+						d[i] = contents;
+					}
+				}
+			}
+	
+			i++;
+		};
+	
+		if ( td ) {
+			// `tr` element was passed in
+			while ( td ) {
+				name = td.nodeName.toUpperCase();
+	
+				if ( name == "TD" || name == "TH" ) {
+					cellProcess( td );
+					tds.push( td );
+				}
+	
+				td = td.nextSibling;
+			}
+		}
+		else {
+			// Existing row object passed in
+			tds = row.anCells;
+	
+			for ( var j=0, jen=tds.length ; j<jen ; j++ ) {
+				cellProcess( tds[j] );
+			}
+		}
+	
+		// Read the ID from the DOM if present
+		var rowNode = row.firstChild ? row : row.nTr;
+	
+		if ( rowNode ) {
+			var id = rowNode.getAttribute( 'id' );
+	
+			if ( id ) {
+				_fnSetObjectDataFn( settings.rowId )( d, id );
+			}
+		}
+	
+		return {
+			data: d,
+			cells: tds
+		};
+	}
+	/**
+	 * Create a new TR element (and it's TD children) for a row
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {int} iRow Row to consider
+	 *  @param {node} [nTrIn] TR element to add to the table - optional. If not given,
+	 *    DataTables will create a row automatically
+	 *  @param {array} [anTds] Array of TD|TH elements for the row - must be given
+	 *    if nTr is.
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnCreateTr ( oSettings, iRow, nTrIn, anTds )
+	{
+		var
+			row = oSettings.aoData[iRow],
+			rowData = row._aData,
+			cells = [],
+			nTr, nTd, oCol,
+			i, iLen;
+	
+		if ( row.nTr === null )
+		{
+			nTr = nTrIn || document.createElement('tr');
+	
+			row.nTr = nTr;
+			row.anCells = cells;
+	
+			/* Use a private property on the node to allow reserve mapping from the node
+			 * to the aoData array for fast look up
+			 */
+			nTr._DT_RowIndex = iRow;
+	
+			/* Special parameters can be given by the data source to be used on the row */
+			_fnRowAttributes( oSettings, row );
+	
+			/* Process each column */
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oCol = oSettings.aoColumns[i];
+	
+				nTd = nTrIn ? anTds[i] : document.createElement( oCol.sCellType );
+				nTd._DT_CellIndex = {
+					row: iRow,
+					column: i
+				};
+				
+				cells.push( nTd );
+	
+				// Need to create the HTML if new, or if a rendering function is defined
+				if ( (!nTrIn || oCol.mRender || oCol.mData !== i) &&
+					 (!$.isPlainObject(oCol.mData) || oCol.mData._ !== i+'.display')
+				) {
+					nTd.innerHTML = _fnGetCellData( oSettings, iRow, i, 'display' );
+				}
+	
+				/* Add user defined class */
+				if ( oCol.sClass )
+				{
+					nTd.className += ' '+oCol.sClass;
+				}
+	
+				// Visibility - add or remove as required
+				if ( oCol.bVisible && ! nTrIn )
+				{
+					nTr.appendChild( nTd );
+				}
+				else if ( ! oCol.bVisible && nTrIn )
+				{
+					nTd.parentNode.removeChild( nTd );
+				}
+	
+				if ( oCol.fnCreatedCell )
+				{
+					oCol.fnCreatedCell.call( oSettings.oInstance,
+						nTd, _fnGetCellData( oSettings, iRow, i ), rowData, iRow, i
+					);
+				}
+			}
+	
+			_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [nTr, rowData, iRow] );
+		}
+	
+		// Remove once webkit bug 131819 and Chromium bug 365619 have been resolved
+		// and deployed
+		row.nTr.setAttribute( 'role', 'row' );
+	}
+	
+	
+	/**
+	 * Add attributes to a row based on the special `DT_*` parameters in a data
+	 * source object.
+	 *  @param {object} settings DataTables settings object
+	 *  @param {object} DataTables row object for the row to be modified
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnRowAttributes( settings, row )
+	{
+		var tr = row.nTr;
+		var data = row._aData;
+	
+		if ( tr ) {
+			var id = settings.rowIdFn( data );
+	
+			if ( id ) {
+				tr.id = id;
+			}
+	
+			if ( data.DT_RowClass ) {
+				// Remove any classes added by DT_RowClass before
+				var a = data.DT_RowClass.split(' ');
+				row.__rowc = row.__rowc ?
+					_unique( row.__rowc.concat( a ) ) :
+					a;
+	
+				$(tr)
+					.removeClass( row.__rowc.join(' ') )
+					.addClass( data.DT_RowClass );
+			}
+	
+			if ( data.DT_RowAttr ) {
+				$(tr).attr( data.DT_RowAttr );
+			}
+	
+			if ( data.DT_RowData ) {
+				$(tr).data( data.DT_RowData );
+			}
+		}
+	}
+	
+	
+	/**
+	 * Create the HTML header for the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnBuildHead( oSettings )
+	{
+		var i, ien, cell, row, column;
+		var thead = oSettings.nTHead;
+		var tfoot = oSettings.nTFoot;
+		var createHeader = $('th, td', thead).length === 0;
+		var classes = oSettings.oClasses;
+		var columns = oSettings.aoColumns;
+	
+		if ( createHeader ) {
+			row = $('<tr/>').appendTo( thead );
+		}
+	
+		for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+			column = columns[i];
+			cell = $( column.nTh ).addClass( column.sClass );
+	
+			if ( createHeader ) {
+				cell.appendTo( row );
+			}
+	
+			// 1.11 move into sorting
+			if ( oSettings.oFeatures.bSort ) {
+				cell.addClass( column.sSortingClass );
+	
+				if ( column.bSortable !== false ) {
+					cell
+						.attr( 'tabindex', oSettings.iTabIndex )
+						.attr( 'aria-controls', oSettings.sTableId );
+	
+					_fnSortAttachListener( oSettings, column.nTh, i );
+				}
+			}
+	
+			if ( column.sTitle != cell[0].innerHTML ) {
+				cell.html( column.sTitle );
+			}
+	
+			_fnRenderer( oSettings, 'header' )(
+				oSettings, cell, column, classes
+			);
+		}
+	
+		if ( createHeader ) {
+			_fnDetectHeader( oSettings.aoHeader, thead );
+		}
+		
+		/* ARIA role for the rows */
+	 	$(thead).find('>tr').attr('role', 'row');
+	
+		/* Deal with the footer - add classes if required */
+		$(thead).find('>tr>th, >tr>td').addClass( classes.sHeaderTH );
+		$(tfoot).find('>tr>th, >tr>td').addClass( classes.sFooterTH );
+	
+		// Cache the footer cells. Note that we only take the cells from the first
+		// row in the footer. If there is more than one row the user wants to
+		// interact with, they need to use the table().foot() method. Note also this
+		// allows cells to be used for multiple columns using colspan
+		if ( tfoot !== null ) {
+			var cells = oSettings.aoFooter[0];
+	
+			for ( i=0, ien=cells.length ; i<ien ; i++ ) {
+				column = columns[i];
+				column.nTf = cells[i].cell;
+	
+				if ( column.sClass ) {
+					$(column.nTf).addClass( column.sClass );
+				}
+			}
+		}
+	}
+	
+	
+	/**
+	 * Draw the header (or footer) element based on the column visibility states. The
+	 * methodology here is to use the layout array from _fnDetectHeader, modified for
+	 * the instantaneous column visibility, to construct the new layout. The grid is
+	 * traversed over cell at a time in a rows x columns grid fashion, although each
+	 * cell insert can cover multiple elements in the grid - which is tracks using the
+	 * aApplied array. Cell inserts in the grid will only occur where there isn't
+	 * already a cell in that position.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param array {objects} aoSource Layout array from _fnDetectHeader
+	 *  @param {boolean} [bIncludeHidden=false] If true then include the hidden columns in the calc,
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnDrawHead( oSettings, aoSource, bIncludeHidden )
+	{
+		var i, iLen, j, jLen, k, kLen, n, nLocalTr;
+		var aoLocal = [];
+		var aApplied = [];
+		var iColumns = oSettings.aoColumns.length;
+		var iRowspan, iColspan;
+	
+		if ( ! aoSource )
+		{
+			return;
+		}
+	
+		if (  bIncludeHidden === undefined )
+		{
+			bIncludeHidden = false;
+		}
+	
+		/* Make a copy of the master layout array, but without the visible columns in it */
+		for ( i=0, iLen=aoSource.length ; i<iLen ; i++ )
+		{
+			aoLocal[i] = aoSource[i].slice();
+			aoLocal[i].nTr = aoSource[i].nTr;
+	
+			/* Remove any columns which are currently hidden */
+			for ( j=iColumns-1 ; j>=0 ; j-- )
+			{
+				if ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden )
+				{
+					aoLocal[i].splice( j, 1 );
+				}
+			}
+	
+			/* Prep the applied array - it needs an element for each row */
+			aApplied.push( [] );
+		}
+	
+		for ( i=0, iLen=aoLocal.length ; i<iLen ; i++ )
+		{
+			nLocalTr = aoLocal[i].nTr;
+	
+			/* All cells are going to be replaced, so empty out the row */
+			if ( nLocalTr )
+			{
+				while( (n = nLocalTr.firstChild) )
+				{
+					nLocalTr.removeChild( n );
+				}
+			}
+	
+			for ( j=0, jLen=aoLocal[i].length ; j<jLen ; j++ )
+			{
+				iRowspan = 1;
+				iColspan = 1;
+	
+				/* Check to see if there is already a cell (row/colspan) covering our target
+				 * insert point. If there is, then there is nothing to do.
+				 */
+				if ( aApplied[i][j] === undefined )
+				{
+					nLocalTr.appendChild( aoLocal[i][j].cell );
+					aApplied[i][j] = 1;
+	
+					/* Expand the cell to cover as many rows as needed */
+					while ( aoLocal[i+iRowspan] !== undefined &&
+					        aoLocal[i][j].cell == aoLocal[i+iRowspan][j].cell )
+					{
+						aApplied[i+iRowspan][j] = 1;
+						iRowspan++;
+					}
+	
+					/* Expand the cell to cover as many columns as needed */
+					while ( aoLocal[i][j+iColspan] !== undefined &&
+					        aoLocal[i][j].cell == aoLocal[i][j+iColspan].cell )
+					{
+						/* Must update the applied array over the rows for the columns */
+						for ( k=0 ; k<iRowspan ; k++ )
+						{
+							aApplied[i+k][j+iColspan] = 1;
+						}
+						iColspan++;
+					}
+	
+					/* Do the actual expansion in the DOM */
+					$(aoLocal[i][j].cell)
+						.attr('rowspan', iRowspan)
+						.attr('colspan', iColspan);
+				}
+			}
+		}
+	}
+	
+	
+	/**
+	 * Insert the required TR nodes into the table for display
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnDraw( oSettings )
+	{
+		/* Provide a pre-callback function which can be used to cancel the draw is false is returned */
+		var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] );
+		if ( $.inArray( false, aPreDraw ) !== -1 )
+		{
+			_fnProcessingDisplay( oSettings, false );
+			return;
+		}
+	
+		var i, iLen, n;
+		var anRows = [];
+		var iRowCount = 0;
+		var asStripeClasses = oSettings.asStripeClasses;
+		var iStripes = asStripeClasses.length;
+		var iOpenRows = oSettings.aoOpenRows.length;
+		var oLang = oSettings.oLanguage;
+		var iInitDisplayStart = oSettings.iInitDisplayStart;
+		var bServerSide = _fnDataSource( oSettings ) == 'ssp';
+		var aiDisplay = oSettings.aiDisplay;
+	
+		oSettings.bDrawing = true;
+	
+		/* Check and see if we have an initial draw position from state saving */
+		if ( iInitDisplayStart !== undefined && iInitDisplayStart !== -1 )
+		{
+			oSettings._iDisplayStart = bServerSide ?
+				iInitDisplayStart :
+				iInitDisplayStart >= oSettings.fnRecordsDisplay() ?
+					0 :
+					iInitDisplayStart;
+	
+			oSettings.iInitDisplayStart = -1;
+		}
+	
+		var iDisplayStart = oSettings._iDisplayStart;
+		var iDisplayEnd = oSettings.fnDisplayEnd();
+	
+		/* Server-side processing draw intercept */
+		if ( oSettings.bDeferLoading )
+		{
+			oSettings.bDeferLoading = false;
+			oSettings.iDraw++;
+			_fnProcessingDisplay( oSettings, false );
+		}
+		else if ( !bServerSide )
+		{
+			oSettings.iDraw++;
+		}
+		else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) )
+		{
+			return;
+		}
+	
+		if ( aiDisplay.length !== 0 )
+		{
+			var iStart = bServerSide ? 0 : iDisplayStart;
+			var iEnd = bServerSide ? oSettings.aoData.length : iDisplayEnd;
+	
+			for ( var j=iStart ; j<iEnd ; j++ )
+			{
+				var iDataIndex = aiDisplay[j];
+				var aoData = oSettings.aoData[ iDataIndex ];
+				if ( aoData.nTr === null )
+				{
+					_fnCreateTr( oSettings, iDataIndex );
+				}
+	
+				var nRow = aoData.nTr;
+	
+				/* Remove the old striping classes and then add the new one */
+				if ( iStripes !== 0 )
+				{
+					var sStripe = asStripeClasses[ iRowCount % iStripes ];
+					if ( aoData._sRowStripe != sStripe )
+					{
+						$(nRow).removeClass( aoData._sRowStripe ).addClass( sStripe );
+						aoData._sRowStripe = sStripe;
+					}
+				}
+	
+				// Row callback functions - might want to manipulate the row
+				// iRowCount and j are not currently documented. Are they at all
+				// useful?
+				_fnCallbackFire( oSettings, 'aoRowCallback', null,
+					[nRow, aoData._aData, iRowCount, j] );
+	
+				anRows.push( nRow );
+				iRowCount++;
+			}
+		}
+		else
+		{
+			/* Table is empty - create a row with an empty message in it */
+			var sZero = oLang.sZeroRecords;
+			if ( oSettings.iDraw == 1 &&  _fnDataSource( oSettings ) == 'ajax' )
+			{
+				sZero = oLang.sLoadingRecords;
+			}
+			else if ( oLang.sEmptyTable && oSettings.fnRecordsTotal() === 0 )
+			{
+				sZero = oLang.sEmptyTable;
+			}
+	
+			anRows[ 0 ] = $( '<tr/>', { 'class': iStripes ? asStripeClasses[0] : '' } )
+				.append( $('<td />', {
+					'valign':  'top',
+					'colSpan': _fnVisbleColumns( oSettings ),
+					'class':   oSettings.oClasses.sRowEmpty
+				} ).html( sZero ) )[0];
+		}
+	
+		/* Header and footer callbacks */
+		_fnCallbackFire( oSettings, 'aoHeaderCallback', 'header', [ $(oSettings.nTHead).children('tr')[0],
+			_fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] );
+	
+		_fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0],
+			_fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] );
+	
+		var body = $(oSettings.nTBody);
+	
+		body.children().detach();
+		body.append( $(anRows) );
+	
+		/* Call all required callback functions for the end of a draw */
+		_fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] );
+	
+		/* Draw is complete, sorting and filtering must be as well */
+		oSettings.bSorted = false;
+		oSettings.bFiltered = false;
+		oSettings.bDrawing = false;
+	}
+	
+	
+	/**
+	 * Redraw the table - taking account of the various features which are enabled
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {boolean} [holdPosition] Keep the current paging position. By default
+	 *    the paging is reset to the first page
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnReDraw( settings, holdPosition )
+	{
+		var
+			features = settings.oFeatures,
+			sort     = features.bSort,
+			filter   = features.bFilter;
+	
+		if ( sort ) {
+			_fnSort( settings );
+		}
+	
+		if ( filter ) {
+			_fnFilterComplete( settings, settings.oPreviousSearch );
+		}
+		else {
+			// No filtering, so we want to just use the display master
+			settings.aiDisplay = settings.aiDisplayMaster.slice();
+		}
+	
+		if ( holdPosition !== true ) {
+			settings._iDisplayStart = 0;
+		}
+	
+		// Let any modules know about the draw hold position state (used by
+		// scrolling internally)
+		settings._drawHold = holdPosition;
+	
+		_fnDraw( settings );
+	
+		settings._drawHold = false;
+	}
+	
+	
+	/**
+	 * Add the options to the page HTML for the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAddOptionsHtml ( oSettings )
+	{
+		var classes = oSettings.oClasses;
+		var table = $(oSettings.nTable);
+		var holding = $('<div/>').insertBefore( table ); // Holding element for speed
+		var features = oSettings.oFeatures;
+	
+		// All DataTables are wrapped in a div
+		var insert = $('<div/>', {
+			id:      oSettings.sTableId+'_wrapper',
+			'class': classes.sWrapper + (oSettings.nTFoot ? '' : ' '+classes.sNoFooter)
+		} );
+	
+		oSettings.nHolding = holding[0];
+		oSettings.nTableWrapper = insert[0];
+		oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling;
+	
+		/* Loop over the user set positioning and place the elements as needed */
+		var aDom = oSettings.sDom.split('');
+		var featureNode, cOption, nNewNode, cNext, sAttr, j;
+		for ( var i=0 ; i<aDom.length ; i++ )
+		{
+			featureNode = null;
+			cOption = aDom[i];
+	
+			if ( cOption == '<' )
+			{
+				/* New container div */
+				nNewNode = $('<div/>')[0];
+	
+				/* Check to see if we should append an id and/or a class name to the container */
+				cNext = aDom[i+1];
+				if ( cNext == "'" || cNext == '"' )
+				{
+					sAttr = "";
+					j = 2;
+					while ( aDom[i+j] != cNext )
+					{
+						sAttr += aDom[i+j];
+						j++;
+					}
+	
+					/* Replace jQuery UI constants @todo depreciated */
+					if ( sAttr == "H" )
+					{
+						sAttr = classes.sJUIHeader;
+					}
+					else if ( sAttr == "F" )
+					{
+						sAttr = classes.sJUIFooter;
+					}
+	
+					/* The attribute can be in the format of "#id.class", "#id" or "class" This logic
+					 * breaks the string into parts and applies them as needed
+					 */
+					if ( sAttr.indexOf('.') != -1 )
+					{
+						var aSplit = sAttr.split('.');
+						nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1);
+						nNewNode.className = aSplit[1];
+					}
+					else if ( sAttr.charAt(0) == "#" )
+					{
+						nNewNode.id = sAttr.substr(1, sAttr.length-1);
+					}
+					else
+					{
+						nNewNode.className = sAttr;
+					}
+	
+					i += j; /* Move along the position array */
+				}
+	
+				insert.append( nNewNode );
+				insert = $(nNewNode);
+			}
+			else if ( cOption == '>' )
+			{
+				/* End container div */
+				insert = insert.parent();
+			}
+			// @todo Move options into their own plugins?
+			else if ( cOption == 'l' && features.bPaginate && features.bLengthChange )
+			{
+				/* Length */
+				featureNode = _fnFeatureHtmlLength( oSettings );
+			}
+			else if ( cOption == 'f' && features.bFilter )
+			{
+				/* Filter */
+				featureNode = _fnFeatureHtmlFilter( oSettings );
+			}
+			else if ( cOption == 'r' && features.bProcessing )
+			{
+				/* pRocessing */
+				featureNode = _fnFeatureHtmlProcessing( oSettings );
+			}
+			else if ( cOption == 't' )
+			{
+				/* Table */
+				featureNode = _fnFeatureHtmlTable( oSettings );
+			}
+			else if ( cOption ==  'i' && features.bInfo )
+			{
+				/* Info */
+				featureNode = _fnFeatureHtmlInfo( oSettings );
+			}
+			else if ( cOption == 'p' && features.bPaginate )
+			{
+				/* Pagination */
+				featureNode = _fnFeatureHtmlPaginate( oSettings );
+			}
+			else if ( DataTable.ext.feature.length !== 0 )
+			{
+				/* Plug-in features */
+				var aoFeatures = DataTable.ext.feature;
+				for ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ )
+				{
+					if ( cOption == aoFeatures[k].cFeature )
+					{
+						featureNode = aoFeatures[k].fnInit( oSettings );
+						break;
+					}
+				}
+			}
+	
+			/* Add to the 2D features array */
+			if ( featureNode )
+			{
+				var aanFeatures = oSettings.aanFeatures;
+	
+				if ( ! aanFeatures[cOption] )
+				{
+					aanFeatures[cOption] = [];
+				}
+	
+				aanFeatures[cOption].push( featureNode );
+				insert.append( featureNode );
+			}
+		}
+	
+		/* Built our DOM structure - replace the holding div with what we want */
+		holding.replaceWith( insert );
+		oSettings.nHolding = null;
+	}
+	
+	
+	/**
+	 * Use the DOM source to create up an array of header cells. The idea here is to
+	 * create a layout grid (array) of rows x columns, which contains a reference
+	 * to the cell that that point in the grid (regardless of col/rowspan), such that
+	 * any column / row could be removed and the new grid constructed
+	 *  @param array {object} aLayout Array to store the calculated layout in
+	 *  @param {node} nThead The header/footer element for the table
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnDetectHeader ( aLayout, nThead )
+	{
+		var nTrs = $(nThead).children('tr');
+		var nTr, nCell;
+		var i, k, l, iLen, jLen, iColShifted, iColumn, iColspan, iRowspan;
+		var bUnique;
+		var fnShiftCol = function ( a, i, j ) {
+			var k = a[i];
+	                while ( k[j] ) {
+				j++;
+			}
+			return j;
+		};
+	
+		aLayout.splice( 0, aLayout.length );
+	
+		/* We know how many rows there are in the layout - so prep it */
+		for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+		{
+			aLayout.push( [] );
+		}
+	
+		/* Calculate a layout array */
+		for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+		{
+			nTr = nTrs[i];
+			iColumn = 0;
+	
+			/* For every cell in the row... */
+			nCell = nTr.firstChild;
+			while ( nCell ) {
+				if ( nCell.nodeName.toUpperCase() == "TD" ||
+				     nCell.nodeName.toUpperCase() == "TH" )
+				{
+					/* Get the col and rowspan attributes from the DOM and sanitise them */
+					iColspan = nCell.getAttribute('colspan') * 1;
+					iRowspan = nCell.getAttribute('rowspan') * 1;
+					iColspan = (!iColspan || iColspan===0 || iColspan===1) ? 1 : iColspan;
+					iRowspan = (!iRowspan || iRowspan===0 || iRowspan===1) ? 1 : iRowspan;
+	
+					/* There might be colspan cells already in this row, so shift our target
+					 * accordingly
+					 */
+					iColShifted = fnShiftCol( aLayout, i, iColumn );
+	
+					/* Cache calculation for unique columns */
+					bUnique = iColspan === 1 ? true : false;
+	
+					/* If there is col / rowspan, copy the information into the layout grid */
+					for ( l=0 ; l<iColspan ; l++ )
+					{
+						for ( k=0 ; k<iRowspan ; k++ )
+						{
+							aLayout[i+k][iColShifted+l] = {
+								"cell": nCell,
+								"unique": bUnique
+							};
+							aLayout[i+k].nTr = nTr;
+						}
+					}
+				}
+				nCell = nCell.nextSibling;
+			}
+		}
+	}
+	
+	
+	/**
+	 * Get an array of unique th elements, one for each column
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {node} nHeader automatically detect the layout from this node - optional
+	 *  @param {array} aLayout thead/tfoot layout from _fnDetectHeader - optional
+	 *  @returns array {node} aReturn list of unique th's
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetUniqueThs ( oSettings, nHeader, aLayout )
+	{
+		var aReturn = [];
+		if ( !aLayout )
+		{
+			aLayout = oSettings.aoHeader;
+			if ( nHeader )
+			{
+				aLayout = [];
+				_fnDetectHeader( aLayout, nHeader );
+			}
+		}
+	
+		for ( var i=0, iLen=aLayout.length ; i<iLen ; i++ )
+		{
+			for ( var j=0, jLen=aLayout[i].length ; j<jLen ; j++ )
+			{
+				if ( aLayout[i][j].unique &&
+					 (!aReturn[j] || !oSettings.bSortCellsTop) )
+				{
+					aReturn[j] = aLayout[i][j].cell;
+				}
+			}
+		}
+	
+		return aReturn;
+	}
+	
+	/**
+	 * Create an Ajax call based on the table's settings, taking into account that
+	 * parameters can have multiple forms, and backwards compatibility.
+	 *
+	 * @param {object} oSettings dataTables settings object
+	 * @param {array} data Data to send to the server, required by
+	 *     DataTables - may be augmented by developer callbacks
+	 * @param {function} fn Callback function to run when data is obtained
+	 */
+	function _fnBuildAjax( oSettings, data, fn )
+	{
+		// Compatibility with 1.9-, allow fnServerData and event to manipulate
+		_fnCallbackFire( oSettings, 'aoServerParams', 'serverParams', [data] );
+	
+		// Convert to object based for 1.10+ if using the old array scheme which can
+		// come from server-side processing or serverParams
+		if ( data && $.isArray(data) ) {
+			var tmp = {};
+			var rbracket = /(.*?)\[\]$/;
+	
+			$.each( data, function (key, val) {
+				var match = val.name.match(rbracket);
+	
+				if ( match ) {
+					// Support for arrays
+					var name = match[0];
+	
+					if ( ! tmp[ name ] ) {
+						tmp[ name ] = [];
+					}
+					tmp[ name ].push( val.value );
+				}
+				else {
+					tmp[val.name] = val.value;
+				}
+			} );
+			data = tmp;
+		}
+	
+		var ajaxData;
+		var ajax = oSettings.ajax;
+		var instance = oSettings.oInstance;
+		var callback = function ( json ) {
+			_fnCallbackFire( oSettings, null, 'xhr', [oSettings, json, oSettings.jqXHR] );
+			fn( json );
+		};
+	
+		if ( $.isPlainObject( ajax ) && ajax.data )
+		{
+			ajaxData = ajax.data;
+	
+			var newData = $.isFunction( ajaxData ) ?
+				ajaxData( data, oSettings ) :  // fn can manipulate data or return
+				ajaxData;                      // an object object or array to merge
+	
+			// If the function returned something, use that alone
+			data = $.isFunction( ajaxData ) && newData ?
+				newData :
+				$.extend( true, data, newData );
+	
+			// Remove the data property as we've resolved it already and don't want
+			// jQuery to do it again (it is restored at the end of the function)
+			delete ajax.data;
+		}
+	
+		var baseAjax = {
+			"data": data,
+			"success": function (json) {
+				var error = json.error || json.sError;
+				if ( error ) {
+					_fnLog( oSettings, 0, error );
+				}
+	
+				oSettings.json = json;
+				callback( json );
+			},
+			"dataType": "json",
+			"cache": false,
+			"type": oSettings.sServerMethod,
+			"error": function (xhr, error, thrown) {
+				var ret = _fnCallbackFire( oSettings, null, 'xhr', [oSettings, null, oSettings.jqXHR] );
+	
+				if ( $.inArray( true, ret ) === -1 ) {
+					if ( error == "parsererror" ) {
+						_fnLog( oSettings, 0, 'Invalid JSON response', 1 );
+					}
+					else if ( xhr.readyState === 4 ) {
+						_fnLog( oSettings, 0, 'Ajax error', 7 );
+					}
+				}
+	
+				_fnProcessingDisplay( oSettings, false );
+			}
+		};
+	
+		// Store the data submitted for the API
+		oSettings.oAjaxData = data;
+	
+		// Allow plug-ins and external processes to modify the data
+		_fnCallbackFire( oSettings, null, 'preXhr', [oSettings, data] );
+	
+		if ( oSettings.fnServerData )
+		{
+			// DataTables 1.9- compatibility
+			oSettings.fnServerData.call( instance,
+				oSettings.sAjaxSource,
+				$.map( data, function (val, key) { // Need to convert back to 1.9 trad format
+					return { name: key, value: val };
+				} ),
+				callback,
+				oSettings
+			);
+		}
+		else if ( oSettings.sAjaxSource || typeof ajax === 'string' )
+		{
+			// DataTables 1.9- compatibility
+			oSettings.jqXHR = $.ajax( $.extend( baseAjax, {
+				url: ajax || oSettings.sAjaxSource
+			} ) );
+		}
+		else if ( $.isFunction( ajax ) )
+		{
+			// Is a function - let the caller define what needs to be done
+			oSettings.jqXHR = ajax.call( instance, data, callback, oSettings );
+		}
+		else
+		{
+			// Object to extend the base settings
+			oSettings.jqXHR = $.ajax( $.extend( baseAjax, ajax ) );
+	
+			// Restore for next time around
+			ajax.data = ajaxData;
+		}
+	}
+	
+	
+	/**
+	 * Update the table using an Ajax call
+	 *  @param {object} settings dataTables settings object
+	 *  @returns {boolean} Block the table drawing or not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAjaxUpdate( settings )
+	{
+		if ( settings.bAjaxDataGet ) {
+			settings.iDraw++;
+			_fnProcessingDisplay( settings, true );
+	
+			_fnBuildAjax(
+				settings,
+				_fnAjaxParameters( settings ),
+				function(json) {
+					_fnAjaxUpdateDraw( settings, json );
+				}
+			);
+	
+			return false;
+		}
+		return true;
+	}
+	
+	
+	/**
+	 * Build up the parameters in an object needed for a server-side processing
+	 * request. Note that this is basically done twice, is different ways - a modern
+	 * method which is used by default in DataTables 1.10 which uses objects and
+	 * arrays, or the 1.9- method with is name / value pairs. 1.9 method is used if
+	 * the sAjaxSource option is used in the initialisation, or the legacyAjax
+	 * option is set.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns {bool} block the table drawing or not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAjaxParameters( settings )
+	{
+		var
+			columns = settings.aoColumns,
+			columnCount = columns.length,
+			features = settings.oFeatures,
+			preSearch = settings.oPreviousSearch,
+			preColSearch = settings.aoPreSearchCols,
+			i, data = [], dataProp, column, columnSearch,
+			sort = _fnSortFlatten( settings ),
+			displayStart = settings._iDisplayStart,
+			displayLength = features.bPaginate !== false ?
+				settings._iDisplayLength :
+				-1;
+	
+		var param = function ( name, value ) {
+			data.push( { 'name': name, 'value': value } );
+		};
+	
+		// DataTables 1.9- compatible method
+		param( 'sEcho',          settings.iDraw );
+		param( 'iColumns',       columnCount );
+		param( 'sColumns',       _pluck( columns, 'sName' ).join(',') );
+		param( 'iDisplayStart',  displayStart );
+		param( 'iDisplayLength', displayLength );
+	
+		// DataTables 1.10+ method
+		var d = {
+			draw:    settings.iDraw,
+			columns: [],
+			order:   [],
+			start:   displayStart,
+			length:  displayLength,
+			search:  {
+				value: preSearch.sSearch,
+				regex: preSearch.bRegex
+			}
+		};
+	
+		for ( i=0 ; i<columnCount ; i++ ) {
+			column = columns[i];
+			columnSearch = preColSearch[i];
+			dataProp = typeof column.mData=="function" ? 'function' : column.mData ;
+	
+			d.columns.push( {
+				data:       dataProp,
+				name:       column.sName,
+				searchable: column.bSearchable,
+				orderable:  column.bSortable,
+				search:     {
+					value: columnSearch.sSearch,
+					regex: columnSearch.bRegex
+				}
+			} );
+	
+			param( "mDataProp_"+i, dataProp );
+	
+			if ( features.bFilter ) {
+				param( 'sSearch_'+i,     columnSearch.sSearch );
+				param( 'bRegex_'+i,      columnSearch.bRegex );
+				param( 'bSearchable_'+i, column.bSearchable );
+			}
+	
+			if ( features.bSort ) {
+				param( 'bSortable_'+i, column.bSortable );
+			}
+		}
+	
+		if ( features.bFilter ) {
+			param( 'sSearch', preSearch.sSearch );
+			param( 'bRegex', preSearch.bRegex );
+		}
+	
+		if ( features.bSort ) {
+			$.each( sort, function ( i, val ) {
+				d.order.push( { column: val.col, dir: val.dir } );
+	
+				param( 'iSortCol_'+i, val.col );
+				param( 'sSortDir_'+i, val.dir );
+			} );
+	
+			param( 'iSortingCols', sort.length );
+		}
+	
+		// If the legacy.ajax parameter is null, then we automatically decide which
+		// form to use, based on sAjaxSource
+		var legacy = DataTable.ext.legacy.ajax;
+		if ( legacy === null ) {
+			return settings.sAjaxSource ? data : d;
+		}
+	
+		// Otherwise, if legacy has been specified then we use that to decide on the
+		// form
+		return legacy ? data : d;
+	}
+	
+	
+	/**
+	 * Data the data from the server (nuking the old) and redraw the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {object} json json data return from the server.
+	 *  @param {string} json.sEcho Tracking flag for DataTables to match requests
+	 *  @param {int} json.iTotalRecords Number of records in the data set, not accounting for filtering
+	 *  @param {int} json.iTotalDisplayRecords Number of records in the data set, accounting for filtering
+	 *  @param {array} json.aaData The data to display on this page
+	 *  @param {string} [json.sColumns] Column ordering (sName, comma separated)
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnAjaxUpdateDraw ( settings, json )
+	{
+		// v1.10 uses camelCase variables, while 1.9 uses Hungarian notation.
+		// Support both
+		var compat = function ( old, modern ) {
+			return json[old] !== undefined ? json[old] : json[modern];
+		};
+	
+		var data = _fnAjaxDataSrc( settings, json );
+		var draw            = compat( 'sEcho',                'draw' );
+		var recordsTotal    = compat( 'iTotalRecords',        'recordsTotal' );
+		var recordsFiltered = compat( 'iTotalDisplayRecords', 'recordsFiltered' );
+	
+		if ( draw ) {
+			// Protect against out of sequence returns
+			if ( draw*1 < settings.iDraw ) {
+				return;
+			}
+			settings.iDraw = draw * 1;
+		}
+	
+		_fnClearTable( settings );
+		settings._iRecordsTotal   = parseInt(recordsTotal, 10);
+		settings._iRecordsDisplay = parseInt(recordsFiltered, 10);
+	
+		for ( var i=0, ien=data.length ; i<ien ; i++ ) {
+			_fnAddData( settings, data[i] );
+		}
+		settings.aiDisplay = settings.aiDisplayMaster.slice();
+	
+		settings.bAjaxDataGet = false;
+		_fnDraw( settings );
+	
+		if ( ! settings._bInitComplete ) {
+			_fnInitComplete( settings, json );
+		}
+	
+		settings.bAjaxDataGet = true;
+		_fnProcessingDisplay( settings, false );
+	}
+	
+	
+	/**
+	 * Get the data from the JSON data source to use for drawing a table. Using
+	 * `_fnGetObjectDataFn` allows the data to be sourced from a property of the
+	 * source object, or from a processing function.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param  {object} json Data source object / array from the server
+	 *  @return {array} Array of data to use
+	 */
+	function _fnAjaxDataSrc ( oSettings, json )
+	{
+		var dataSrc = $.isPlainObject( oSettings.ajax ) && oSettings.ajax.dataSrc !== undefined ?
+			oSettings.ajax.dataSrc :
+			oSettings.sAjaxDataProp; // Compatibility with 1.9-.
+	
+		// Compatibility with 1.9-. In order to read from aaData, check if the
+		// default has been changed, if not, check for aaData
+		if ( dataSrc === 'data' ) {
+			return json.aaData || json[dataSrc];
+		}
+	
+		return dataSrc !== "" ?
+			_fnGetObjectDataFn( dataSrc )( json ) :
+			json;
+	}
+	
+	/**
+	 * Generate the node required for filtering text
+	 *  @returns {node} Filter control element
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlFilter ( settings )
+	{
+		var classes = settings.oClasses;
+		var tableId = settings.sTableId;
+		var language = settings.oLanguage;
+		var previousSearch = settings.oPreviousSearch;
+		var features = settings.aanFeatures;
+		var input = '<input type="search" class="'+classes.sFilterInput+'"/>';
+	
+		var str = language.sSearch;
+		str = str.match(/_INPUT_/) ?
+			str.replace('_INPUT_', input) :
+			str+input;
+	
+		var filter = $('<div/>', {
+				'id': ! features.f ? tableId+'_filter' : null,
+				'class': classes.sFilter
+			} )
+			.append( $('<label/>' ).append( str ) );
+	
+		var searchFn = function() {
+			/* Update all other filter input elements for the new display */
+			var n = features.f;
+			var val = !this.value ? "" : this.value; // mental IE8 fix :-(
+	
+			/* Now do the filter */
+			if ( val != previousSearch.sSearch ) {
+				_fnFilterComplete( settings, {
+					"sSearch": val,
+					"bRegex": previousSearch.bRegex,
+					"bSmart": previousSearch.bSmart ,
+					"bCaseInsensitive": previousSearch.bCaseInsensitive
+				} );
+	
+				// Need to redraw, without resorting
+				settings._iDisplayStart = 0;
+				_fnDraw( settings );
+			}
+		};
+	
+		var searchDelay = settings.searchDelay !== null ?
+			settings.searchDelay :
+			_fnDataSource( settings ) === 'ssp' ?
+				400 :
+				0;
+	
+		var jqFilter = $('input', filter)
+			.val( previousSearch.sSearch )
+			.attr( 'placeholder', language.sSearchPlaceholder )
+			.on(
+				'keyup.DT search.DT input.DT paste.DT cut.DT',
+				searchDelay ?
+					_fnThrottle( searchFn, searchDelay ) :
+					searchFn
+			)
+			.on( 'keypress.DT', function(e) {
+				/* Prevent form submission */
+				if ( e.keyCode == 13 ) {
+					return false;
+				}
+			} )
+			.attr('aria-controls', tableId);
+	
+		// Update the input elements whenever the table is filtered
+		$(settings.nTable).on( 'search.dt.DT', function ( ev, s ) {
+			if ( settings === s ) {
+				// IE9 throws an 'unknown error' if document.activeElement is used
+				// inside an iframe or frame...
+				try {
+					if ( jqFilter[0] !== document.activeElement ) {
+						jqFilter.val( previousSearch.sSearch );
+					}
+				}
+				catch ( e ) {}
+			}
+		} );
+	
+		return filter[0];
+	}
+	
+	
+	/**
+	 * Filter the table using both the global filter and column based filtering
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {object} oSearch search information
+	 *  @param {int} [iForce] force a research of the master array (1) or not (undefined or 0)
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFilterComplete ( oSettings, oInput, iForce )
+	{
+		var oPrevSearch = oSettings.oPreviousSearch;
+		var aoPrevSearch = oSettings.aoPreSearchCols;
+		var fnSaveFilter = function ( oFilter ) {
+			/* Save the filtering values */
+			oPrevSearch.sSearch = oFilter.sSearch;
+			oPrevSearch.bRegex = oFilter.bRegex;
+			oPrevSearch.bSmart = oFilter.bSmart;
+			oPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive;
+		};
+		var fnRegex = function ( o ) {
+			// Backwards compatibility with the bEscapeRegex option
+			return o.bEscapeRegex !== undefined ? !o.bEscapeRegex : o.bRegex;
+		};
+	
+		// Resolve any column types that are unknown due to addition or invalidation
+		// @todo As per sort - can this be moved into an event handler?
+		_fnColumnTypes( oSettings );
+	
+		/* In server-side processing all filtering is done by the server, so no point hanging around here */
+		if ( _fnDataSource( oSettings ) != 'ssp' )
+		{
+			/* Global filter */
+			_fnFilter( oSettings, oInput.sSearch, iForce, fnRegex(oInput), oInput.bSmart, oInput.bCaseInsensitive );
+			fnSaveFilter( oInput );
+	
+			/* Now do the individual column filter */
+			for ( var i=0 ; i<aoPrevSearch.length ; i++ )
+			{
+				_fnFilterColumn( oSettings, aoPrevSearch[i].sSearch, i, fnRegex(aoPrevSearch[i]),
+					aoPrevSearch[i].bSmart, aoPrevSearch[i].bCaseInsensitive );
+			}
+	
+			/* Custom filtering */
+			_fnFilterCustom( oSettings );
+		}
+		else
+		{
+			fnSaveFilter( oInput );
+		}
+	
+		/* Tell the draw function we have been filtering */
+		oSettings.bFiltered = true;
+		_fnCallbackFire( oSettings, null, 'search', [oSettings] );
+	}
+	
+	
+	/**
+	 * Apply custom filtering functions
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFilterCustom( settings )
+	{
+		var filters = DataTable.ext.search;
+		var displayRows = settings.aiDisplay;
+		var row, rowIdx;
+	
+		for ( var i=0, ien=filters.length ; i<ien ; i++ ) {
+			var rows = [];
+	
+			// Loop over each row and see if it should be included
+			for ( var j=0, jen=displayRows.length ; j<jen ; j++ ) {
+				rowIdx = displayRows[ j ];
+				row = settings.aoData[ rowIdx ];
+	
+				if ( filters[i]( settings, row._aFilterData, rowIdx, row._aData, j ) ) {
+					rows.push( rowIdx );
+				}
+			}
+	
+			// So the array reference doesn't break set the results into the
+			// existing array
+			displayRows.length = 0;
+			$.merge( displayRows, rows );
+		}
+	}
+	
+	
+	/**
+	 * Filter the table on a per-column basis
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {string} sInput string to filter on
+	 *  @param {int} iColumn column to filter
+	 *  @param {bool} bRegex treat search string as a regular expression or not
+	 *  @param {bool} bSmart use smart filtering or not
+	 *  @param {bool} bCaseInsensitive Do case insenstive matching or not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFilterColumn ( settings, searchStr, colIdx, regex, smart, caseInsensitive )
+	{
+		if ( searchStr === '' ) {
+			return;
+		}
+	
+		var data;
+		var out = [];
+		var display = settings.aiDisplay;
+		var rpSearch = _fnFilterCreateSearch( searchStr, regex, smart, caseInsensitive );
+	
+		for ( var i=0 ; i<display.length ; i++ ) {
+			data = settings.aoData[ display[i] ]._aFilterData[ colIdx ];
+	
+			if ( rpSearch.test( data ) ) {
+				out.push( display[i] );
+			}
+		}
+	
+		settings.aiDisplay = out;
+	}
+	
+	
+	/**
+	 * Filter the data table based on user input and draw the table
+	 *  @param {object} settings dataTables settings object
+	 *  @param {string} input string to filter on
+	 *  @param {int} force optional - force a research of the master array (1) or not (undefined or 0)
+	 *  @param {bool} regex treat as a regular expression or not
+	 *  @param {bool} smart perform smart filtering or not
+	 *  @param {bool} caseInsensitive Do case insenstive matching or not
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFilter( settings, input, force, regex, smart, caseInsensitive )
+	{
+		var rpSearch = _fnFilterCreateSearch( input, regex, smart, caseInsensitive );
+		var prevSearch = settings.oPreviousSearch.sSearch;
+		var displayMaster = settings.aiDisplayMaster;
+		var display, invalidated, i;
+		var filtered = [];
+	
+		// Need to take account of custom filtering functions - always filter
+		if ( DataTable.ext.search.length !== 0 ) {
+			force = true;
+		}
+	
+		// Check if any of the rows were invalidated
+		invalidated = _fnFilterData( settings );
+	
+		// If the input is blank - we just want the full data set
+		if ( input.length <= 0 ) {
+			settings.aiDisplay = displayMaster.slice();
+		}
+		else {
+			// New search - start from the master array
+			if ( invalidated ||
+				 force ||
+				 prevSearch.length > input.length ||
+				 input.indexOf(prevSearch) !== 0 ||
+				 settings.bSorted // On resort, the display master needs to be
+				                  // re-filtered since indexes will have changed
+			) {
+				settings.aiDisplay = displayMaster.slice();
+			}
+	
+			// Search the display array
+			display = settings.aiDisplay;
+	
+			for ( i=0 ; i<display.length ; i++ ) {
+				if ( rpSearch.test( settings.aoData[ display[i] ]._sFilterRow ) ) {
+					filtered.push( display[i] );
+				}
+			}
+	
+			settings.aiDisplay = filtered;
+		}
+	}
+	
+	
+	/**
+	 * Build a regular expression object suitable for searching a table
+	 *  @param {string} sSearch string to search for
+	 *  @param {bool} bRegex treat as a regular expression or not
+	 *  @param {bool} bSmart perform smart filtering or not
+	 *  @param {bool} bCaseInsensitive Do case insensitive matching or not
+	 *  @returns {RegExp} constructed object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFilterCreateSearch( search, regex, smart, caseInsensitive )
+	{
+		search = regex ?
+			search :
+			_fnEscapeRegex( search );
+		
+		if ( smart ) {
+			/* For smart filtering we want to allow the search to work regardless of
+			 * word order. We also want double quoted text to be preserved, so word
+			 * order is important - a la google. So this is what we want to
+			 * generate:
+			 * 
+			 * ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$
+			 */
+			var a = $.map( search.match( /"[^"]+"|[^ ]+/g ) || [''], function ( word ) {
+				if ( word.charAt(0) === '"' ) {
+					var m = word.match( /^"(.*)"$/ );
+					word = m ? m[1] : word;
+				}
+	
+				return word.replace('"', '');
+			} );
+	
+			search = '^(?=.*?'+a.join( ')(?=.*?' )+').*$';
+		}
+	
+		return new RegExp( search, caseInsensitive ? 'i' : '' );
+	}
+	
+	
+	/**
+	 * Escape a string such that it can be used in a regular expression
+	 *  @param {string} sVal string to escape
+	 *  @returns {string} escaped string
+	 *  @memberof DataTable#oApi
+	 */
+	var _fnEscapeRegex = DataTable.util.escapeRegex;
+	
+	var __filter_div = $('<div>')[0];
+	var __filter_div_textContent = __filter_div.textContent !== undefined;
+	
+	// Update the filtering data for each row if needed (by invalidation or first run)
+	function _fnFilterData ( settings )
+	{
+		var columns = settings.aoColumns;
+		var column;
+		var i, j, ien, jen, filterData, cellData, row;
+		var fomatters = DataTable.ext.type.search;
+		var wasInvalidated = false;
+	
+		for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
+			row = settings.aoData[i];
+	
+			if ( ! row._aFilterData ) {
+				filterData = [];
+	
+				for ( j=0, jen=columns.length ; j<jen ; j++ ) {
+					column = columns[j];
+	
+					if ( column.bSearchable ) {
+						cellData = _fnGetCellData( settings, i, j, 'filter' );
+	
+						if ( fomatters[ column.sType ] ) {
+							cellData = fomatters[ column.sType ]( cellData );
+						}
+	
+						// Search in DataTables 1.10 is string based. In 1.11 this
+						// should be altered to also allow strict type checking.
+						if ( cellData === null ) {
+							cellData = '';
+						}
+	
+						if ( typeof cellData !== 'string' && cellData.toString ) {
+							cellData = cellData.toString();
+						}
+					}
+					else {
+						cellData = '';
+					}
+	
+					// If it looks like there is an HTML entity in the string,
+					// attempt to decode it so sorting works as expected. Note that
+					// we could use a single line of jQuery to do this, but the DOM
+					// method used here is much faster http://jsperf.com/html-decode
+					if ( cellData.indexOf && cellData.indexOf('&') !== -1 ) {
+						__filter_div.innerHTML = cellData;
+						cellData = __filter_div_textContent ?
+							__filter_div.textContent :
+							__filter_div.innerText;
+					}
+	
+					if ( cellData.replace ) {
+						cellData = cellData.replace(/[\r\n]/g, '');
+					}
+	
+					filterData.push( cellData );
+				}
+	
+				row._aFilterData = filterData;
+				row._sFilterRow = filterData.join('  ');
+				wasInvalidated = true;
+			}
+		}
+	
+		return wasInvalidated;
+	}
+	
+	
+	/**
+	 * Convert from the internal Hungarian notation to camelCase for external
+	 * interaction
+	 *  @param {object} obj Object to convert
+	 *  @returns {object} Inverted object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSearchToCamel ( obj )
+	{
+		return {
+			search:          obj.sSearch,
+			smart:           obj.bSmart,
+			regex:           obj.bRegex,
+			caseInsensitive: obj.bCaseInsensitive
+		};
+	}
+	
+	
+	
+	/**
+	 * Convert from camelCase notation to the internal Hungarian. We could use the
+	 * Hungarian convert function here, but this is cleaner
+	 *  @param {object} obj Object to convert
+	 *  @returns {object} Inverted object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSearchToHung ( obj )
+	{
+		return {
+			sSearch:          obj.search,
+			bSmart:           obj.smart,
+			bRegex:           obj.regex,
+			bCaseInsensitive: obj.caseInsensitive
+		};
+	}
+	
+	/**
+	 * Generate the node required for the info display
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns {node} Information element
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlInfo ( settings )
+	{
+		var
+			tid = settings.sTableId,
+			nodes = settings.aanFeatures.i,
+			n = $('<div/>', {
+				'class': settings.oClasses.sInfo,
+				'id': ! nodes ? tid+'_info' : null
+			} );
+	
+		if ( ! nodes ) {
+			// Update display on each draw
+			settings.aoDrawCallback.push( {
+				"fn": _fnUpdateInfo,
+				"sName": "information"
+			} );
+	
+			n
+				.attr( 'role', 'status' )
+				.attr( 'aria-live', 'polite' );
+	
+			// Table is described by our info div
+			$(settings.nTable).attr( 'aria-describedby', tid+'_info' );
+		}
+	
+		return n[0];
+	}
+	
+	
+	/**
+	 * Update the information elements in the display
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnUpdateInfo ( settings )
+	{
+		/* Show information about the table */
+		var nodes = settings.aanFeatures.i;
+		if ( nodes.length === 0 ) {
+			return;
+		}
+	
+		var
+			lang  = settings.oLanguage,
+			start = settings._iDisplayStart+1,
+			end   = settings.fnDisplayEnd(),
+			max   = settings.fnRecordsTotal(),
+			total = settings.fnRecordsDisplay(),
+			out   = total ?
+				lang.sInfo :
+				lang.sInfoEmpty;
+	
+		if ( total !== max ) {
+			/* Record set after filtering */
+			out += ' ' + lang.sInfoFiltered;
+		}
+	
+		// Convert the macros
+		out += lang.sInfoPostFix;
+		out = _fnInfoMacros( settings, out );
+	
+		var callback = lang.fnInfoCallback;
+		if ( callback !== null ) {
+			out = callback.call( settings.oInstance,
+				settings, start, end, max, total, out
+			);
+		}
+	
+		$(nodes).html( out );
+	}
+	
+	
+	function _fnInfoMacros ( settings, str )
+	{
+		// When infinite scrolling, we are always starting at 1. _iDisplayStart is used only
+		// internally
+		var
+			formatter  = settings.fnFormatNumber,
+			start      = settings._iDisplayStart+1,
+			len        = settings._iDisplayLength,
+			vis        = settings.fnRecordsDisplay(),
+			all        = len === -1;
+	
+		return str.
+			replace(/_START_/g, formatter.call( settings, start ) ).
+			replace(/_END_/g,   formatter.call( settings, settings.fnDisplayEnd() ) ).
+			replace(/_MAX_/g,   formatter.call( settings, settings.fnRecordsTotal() ) ).
+			replace(/_TOTAL_/g, formatter.call( settings, vis ) ).
+			replace(/_PAGE_/g,  formatter.call( settings, all ? 1 : Math.ceil( start / len ) ) ).
+			replace(/_PAGES_/g, formatter.call( settings, all ? 1 : Math.ceil( vis / len ) ) );
+	}
+	
+	
+	
+	/**
+	 * Draw the table for the first time, adding all required features
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnInitialise ( settings )
+	{
+		var i, iLen, iAjaxStart=settings.iInitDisplayStart;
+		var columns = settings.aoColumns, column;
+		var features = settings.oFeatures;
+		var deferLoading = settings.bDeferLoading; // value modified by the draw
+	
+		/* Ensure that the table data is fully initialised */
+		if ( ! settings.bInitialised ) {
+			setTimeout( function(){ _fnInitialise( settings ); }, 200 );
+			return;
+		}
+	
+		/* Show the display HTML options */
+		_fnAddOptionsHtml( settings );
+	
+		/* Build and draw the header / footer for the table */
+		_fnBuildHead( settings );
+		_fnDrawHead( settings, settings.aoHeader );
+		_fnDrawHead( settings, settings.aoFooter );
+	
+		/* Okay to show that something is going on now */
+		_fnProcessingDisplay( settings, true );
+	
+		/* Calculate sizes for columns */
+		if ( features.bAutoWidth ) {
+			_fnCalculateColumnWidths( settings );
+		}
+	
+		for ( i=0, iLen=columns.length ; i<iLen ; i++ ) {
+			column = columns[i];
+	
+			if ( column.sWidth ) {
+				column.nTh.style.width = _fnStringToCss( column.sWidth );
+			}
+		}
+	
+		_fnCallbackFire( settings, null, 'preInit', [settings] );
+	
+		// If there is default sorting required - let's do it. The sort function
+		// will do the drawing for us. Otherwise we draw the table regardless of the
+		// Ajax source - this allows the table to look initialised for Ajax sourcing
+		// data (show 'loading' message possibly)
+		_fnReDraw( settings );
+	
+		// Server-side processing init complete is done by _fnAjaxUpdateDraw
+		var dataSrc = _fnDataSource( settings );
+		if ( dataSrc != 'ssp' || deferLoading ) {
+			// if there is an ajax source load the data
+			if ( dataSrc == 'ajax' ) {
+				_fnBuildAjax( settings, [], function(json) {
+					var aData = _fnAjaxDataSrc( settings, json );
+	
+					// Got the data - add it to the table
+					for ( i=0 ; i<aData.length ; i++ ) {
+						_fnAddData( settings, aData[i] );
+					}
+	
+					// Reset the init display for cookie saving. We've already done
+					// a filter, and therefore cleared it before. So we need to make
+					// it appear 'fresh'
+					settings.iInitDisplayStart = iAjaxStart;
+	
+					_fnReDraw( settings );
+	
+					_fnProcessingDisplay( settings, false );
+					_fnInitComplete( settings, json );
+				}, settings );
+			}
+			else {
+				_fnProcessingDisplay( settings, false );
+				_fnInitComplete( settings );
+			}
+		}
+	}
+	
+	
+	/**
+	 * Draw the table for the first time, adding all required features
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {object} [json] JSON from the server that completed the table, if using Ajax source
+	 *    with client-side processing (optional)
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnInitComplete ( settings, json )
+	{
+		settings._bInitComplete = true;
+	
+		// When data was added after the initialisation (data or Ajax) we need to
+		// calculate the column sizing
+		if ( json || settings.oInit.aaData ) {
+			_fnAdjustColumnSizing( settings );
+		}
+	
+		_fnCallbackFire( settings, null, 'plugin-init', [settings, json] );
+		_fnCallbackFire( settings, 'aoInitComplete', 'init', [settings, json] );
+	}
+	
+	
+	function _fnLengthChange ( settings, val )
+	{
+		var len = parseInt( val, 10 );
+		settings._iDisplayLength = len;
+	
+		_fnLengthOverflow( settings );
+	
+		// Fire length change event
+		_fnCallbackFire( settings, null, 'length', [settings, len] );
+	}
+	
+	
+	/**
+	 * Generate the node required for user display length changing
+	 *  @param {object} settings dataTables settings object
+	 *  @returns {node} Display length feature node
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlLength ( settings )
+	{
+		var
+			classes  = settings.oClasses,
+			tableId  = settings.sTableId,
+			menu     = settings.aLengthMenu,
+			d2       = $.isArray( menu[0] ),
+			lengths  = d2 ? menu[0] : menu,
+			language = d2 ? menu[1] : menu;
+	
+		var select = $('<select/>', {
+			'name':          tableId+'_length',
+			'aria-controls': tableId,
+			'class':         classes.sLengthSelect
+		} );
+	
+		for ( var i=0, ien=lengths.length ; i<ien ; i++ ) {
+			select[0][ i ] = new Option( language[i], lengths[i] );
+		}
+	
+		var div = $('<div><label/></div>').addClass( classes.sLength );
+		if ( ! settings.aanFeatures.l ) {
+			div[0].id = tableId+'_length';
+		}
+	
+		div.children().append(
+			settings.oLanguage.sLengthMenu.replace( '_MENU_', select[0].outerHTML )
+		);
+	
+		// Can't use `select` variable as user might provide their own and the
+		// reference is broken by the use of outerHTML
+		$('select', div)
+			.val( settings._iDisplayLength )
+			.on( 'change.DT', function(e) {
+				_fnLengthChange( settings, $(this).val() );
+				_fnDraw( settings );
+			} );
+	
+		// Update node value whenever anything changes the table's length
+		$(settings.nTable).on( 'length.dt.DT', function (e, s, len) {
+			if ( settings === s ) {
+				$('select', div).val( len );
+			}
+		} );
+	
+		return div[0];
+	}
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Note that most of the paging logic is done in
+	 * DataTable.ext.pager
+	 */
+	
+	/**
+	 * Generate the node required for default pagination
+	 *  @param {object} oSettings dataTables settings object
+	 *  @returns {node} Pagination feature node
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlPaginate ( settings )
+	{
+		var
+			type   = settings.sPaginationType,
+			plugin = DataTable.ext.pager[ type ],
+			modern = typeof plugin === 'function',
+			redraw = function( settings ) {
+				_fnDraw( settings );
+			},
+			node = $('<div/>').addClass( settings.oClasses.sPaging + type )[0],
+			features = settings.aanFeatures;
+	
+		if ( ! modern ) {
+			plugin.fnInit( settings, node, redraw );
+		}
+	
+		/* Add a draw callback for the pagination on first instance, to update the paging display */
+		if ( ! features.p )
+		{
+			node.id = settings.sTableId+'_paginate';
+	
+			settings.aoDrawCallback.push( {
+				"fn": function( settings ) {
+					if ( modern ) {
+						var
+							start      = settings._iDisplayStart,
+							len        = settings._iDisplayLength,
+							visRecords = settings.fnRecordsDisplay(),
+							all        = len === -1,
+							page = all ? 0 : Math.ceil( start / len ),
+							pages = all ? 1 : Math.ceil( visRecords / len ),
+							buttons = plugin(page, pages),
+							i, ien;
+	
+						for ( i=0, ien=features.p.length ; i<ien ; i++ ) {
+							_fnRenderer( settings, 'pageButton' )(
+								settings, features.p[i], i, buttons, page, pages
+							);
+						}
+					}
+					else {
+						plugin.fnUpdate( settings, redraw );
+					}
+				},
+				"sName": "pagination"
+			} );
+		}
+	
+		return node;
+	}
+	
+	
+	/**
+	 * Alter the display settings to change the page
+	 *  @param {object} settings DataTables settings object
+	 *  @param {string|int} action Paging action to take: "first", "previous",
+	 *    "next" or "last" or page number to jump to (integer)
+	 *  @param [bool] redraw Automatically draw the update or not
+	 *  @returns {bool} true page has changed, false - no change
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnPageChange ( settings, action, redraw )
+	{
+		var
+			start     = settings._iDisplayStart,
+			len       = settings._iDisplayLength,
+			records   = settings.fnRecordsDisplay();
+	
+		if ( records === 0 || len === -1 )
+		{
+			start = 0;
+		}
+		else if ( typeof action === "number" )
+		{
+			start = action * len;
+	
+			if ( start > records )
+			{
+				start = 0;
+			}
+		}
+		else if ( action == "first" )
+		{
+			start = 0;
+		}
+		else if ( action == "previous" )
+		{
+			start = len >= 0 ?
+				start - len :
+				0;
+	
+			if ( start < 0 )
+			{
+			  start = 0;
+			}
+		}
+		else if ( action == "next" )
+		{
+			if ( start + len < records )
+			{
+				start += len;
+			}
+		}
+		else if ( action == "last" )
+		{
+			start = Math.floor( (records-1) / len) * len;
+		}
+		else
+		{
+			_fnLog( settings, 0, "Unknown paging action: "+action, 5 );
+		}
+	
+		var changed = settings._iDisplayStart !== start;
+		settings._iDisplayStart = start;
+	
+		if ( changed ) {
+			_fnCallbackFire( settings, null, 'page', [settings] );
+	
+			if ( redraw ) {
+				_fnDraw( settings );
+			}
+		}
+	
+		return changed;
+	}
+	
+	
+	
+	/**
+	 * Generate the node required for the processing node
+	 *  @param {object} settings dataTables settings object
+	 *  @returns {node} Processing element
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlProcessing ( settings )
+	{
+		return $('<div/>', {
+				'id': ! settings.aanFeatures.r ? settings.sTableId+'_processing' : null,
+				'class': settings.oClasses.sProcessing
+			} )
+			.html( settings.oLanguage.sProcessing )
+			.insertBefore( settings.nTable )[0];
+	}
+	
+	
+	/**
+	 * Display or hide the processing indicator
+	 *  @param {object} settings dataTables settings object
+	 *  @param {bool} show Show the processing indicator (true) or not (false)
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnProcessingDisplay ( settings, show )
+	{
+		if ( settings.oFeatures.bProcessing ) {
+			$(settings.aanFeatures.r).css( 'display', show ? 'block' : 'none' );
+		}
+	
+		_fnCallbackFire( settings, null, 'processing', [settings, show] );
+	}
+	
+	/**
+	 * Add any control elements for the table - specifically scrolling
+	 *  @param {object} settings dataTables settings object
+	 *  @returns {node} Node to add to the DOM
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnFeatureHtmlTable ( settings )
+	{
+		var table = $(settings.nTable);
+	
+		// Add the ARIA grid role to the table
+		table.attr( 'role', 'grid' );
+	
+		// Scrolling from here on in
+		var scroll = settings.oScroll;
+	
+		if ( scroll.sX === '' && scroll.sY === '' ) {
+			return settings.nTable;
+		}
+	
+		var scrollX = scroll.sX;
+		var scrollY = scroll.sY;
+		var classes = settings.oClasses;
+		var caption = table.children('caption');
+		var captionSide = caption.length ? caption[0]._captionSide : null;
+		var headerClone = $( table[0].cloneNode(false) );
+		var footerClone = $( table[0].cloneNode(false) );
+		var footer = table.children('tfoot');
+		var _div = '<div/>';
+		var size = function ( s ) {
+			return !s ? null : _fnStringToCss( s );
+		};
+	
+		if ( ! footer.length ) {
+			footer = null;
+		}
+	
+		/*
+		 * The HTML structure that we want to generate in this function is:
+		 *  div - scroller
+		 *    div - scroll head
+		 *      div - scroll head inner
+		 *        table - scroll head table
+		 *          thead - thead
+		 *    div - scroll body
+		 *      table - table (master table)
+		 *        thead - thead clone for sizing
+		 *        tbody - tbody
+		 *    div - scroll foot
+		 *      div - scroll foot inner
+		 *        table - scroll foot table
+		 *          tfoot - tfoot
+		 */
+		var scroller = $( _div, { 'class': classes.sScrollWrapper } )
+			.append(
+				$(_div, { 'class': classes.sScrollHead } )
+					.css( {
+						overflow: 'hidden',
+						position: 'relative',
+						border: 0,
+						width: scrollX ? size(scrollX) : '100%'
+					} )
+					.append(
+						$(_div, { 'class': classes.sScrollHeadInner } )
+							.css( {
+								'box-sizing': 'content-box',
+								width: scroll.sXInner || '100%'
+							} )
+							.append(
+								headerClone
+									.removeAttr('id')
+									.css( 'margin-left', 0 )
+									.append( captionSide === 'top' ? caption : null )
+									.append(
+										table.children('thead')
+									)
+							)
+					)
+			)
+			.append(
+				$(_div, { 'class': classes.sScrollBody } )
+					.css( {
+						position: 'relative',
+						overflow: 'auto',
+						width: size( scrollX )
+					} )
+					.append( table )
+			);
+	
+		if ( footer ) {
+			scroller.append(
+				$(_div, { 'class': classes.sScrollFoot } )
+					.css( {
+						overflow: 'hidden',
+						border: 0,
+						width: scrollX ? size(scrollX) : '100%'
+					} )
+					.append(
+						$(_div, { 'class': classes.sScrollFootInner } )
+							.append(
+								footerClone
+									.removeAttr('id')
+									.css( 'margin-left', 0 )
+									.append( captionSide === 'bottom' ? caption : null )
+									.append(
+										table.children('tfoot')
+									)
+							)
+					)
+			);
+		}
+	
+		var children = scroller.children();
+		var scrollHead = children[0];
+		var scrollBody = children[1];
+		var scrollFoot = footer ? children[2] : null;
+	
+		// When the body is scrolled, then we also want to scroll the headers
+		if ( scrollX ) {
+			$(scrollBody).on( 'scroll.DT', function (e) {
+				var scrollLeft = this.scrollLeft;
+	
+				scrollHead.scrollLeft = scrollLeft;
+	
+				if ( footer ) {
+					scrollFoot.scrollLeft = scrollLeft;
+				}
+			} );
+		}
+	
+		$(scrollBody).css(
+			scrollY && scroll.bCollapse ? 'max-height' : 'height', 
+			scrollY
+		);
+	
+		settings.nScrollHead = scrollHead;
+		settings.nScrollBody = scrollBody;
+		settings.nScrollFoot = scrollFoot;
+	
+		// On redraw - align columns
+		settings.aoDrawCallback.push( {
+			"fn": _fnScrollDraw,
+			"sName": "scrolling"
+		} );
+	
+		return scroller[0];
+	}
+	
+	
+	
+	/**
+	 * Update the header, footer and body tables for resizing - i.e. column
+	 * alignment.
+	 *
+	 * Welcome to the most horrible function DataTables. The process that this
+	 * function follows is basically:
+	 *   1. Re-create the table inside the scrolling div
+	 *   2. Take live measurements from the DOM
+	 *   3. Apply the measurements to align the columns
+	 *   4. Clean up
+	 *
+	 *  @param {object} settings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnScrollDraw ( settings )
+	{
+		// Given that this is such a monster function, a lot of variables are use
+		// to try and keep the minimised size as small as possible
+		var
+			scroll         = settings.oScroll,
+			scrollX        = scroll.sX,
+			scrollXInner   = scroll.sXInner,
+			scrollY        = scroll.sY,
+			barWidth       = scroll.iBarWidth,
+			divHeader      = $(settings.nScrollHead),
+			divHeaderStyle = divHeader[0].style,
+			divHeaderInner = divHeader.children('div'),
+			divHeaderInnerStyle = divHeaderInner[0].style,
+			divHeaderTable = divHeaderInner.children('table'),
+			divBodyEl      = settings.nScrollBody,
+			divBody        = $(divBodyEl),
+			divBodyStyle   = divBodyEl.style,
+			divFooter      = $(settings.nScrollFoot),
+			divFooterInner = divFooter.children('div'),
+			divFooterTable = divFooterInner.children('table'),
+			header         = $(settings.nTHead),
+			table          = $(settings.nTable),
+			tableEl        = table[0],
+			tableStyle     = tableEl.style,
+			footer         = settings.nTFoot ? $(settings.nTFoot) : null,
+			browser        = settings.oBrowser,
+			ie67           = browser.bScrollOversize,
+			dtHeaderCells  = _pluck( settings.aoColumns, 'nTh' ),
+			headerTrgEls, footerTrgEls,
+			headerSrcEls, footerSrcEls,
+			headerCopy, footerCopy,
+			headerWidths=[], footerWidths=[],
+			headerContent=[], footerContent=[],
+			idx, correction, sanityWidth,
+			zeroOut = function(nSizer) {
+				var style = nSizer.style;
+				style.paddingTop = "0";
+				style.paddingBottom = "0";
+				style.borderTopWidth = "0";
+				style.borderBottomWidth = "0";
+				style.height = 0;
+			};
+	
+		// If the scrollbar visibility has changed from the last draw, we need to
+		// adjust the column sizes as the table width will have changed to account
+		// for the scrollbar
+		var scrollBarVis = divBodyEl.scrollHeight > divBodyEl.clientHeight;
+		
+		if ( settings.scrollBarVis !== scrollBarVis && settings.scrollBarVis !== undefined ) {
+			settings.scrollBarVis = scrollBarVis;
+			_fnAdjustColumnSizing( settings );
+			return; // adjust column sizing will call this function again
+		}
+		else {
+			settings.scrollBarVis = scrollBarVis;
+		}
+	
+		/*
+		 * 1. Re-create the table inside the scrolling div
+		 */
+	
+		// Remove the old minimised thead and tfoot elements in the inner table
+		table.children('thead, tfoot').remove();
+	
+		if ( footer ) {
+			footerCopy = footer.clone().prependTo( table );
+			footerTrgEls = footer.find('tr'); // the original tfoot is in its own table and must be sized
+			footerSrcEls = footerCopy.find('tr');
+		}
+	
+		// Clone the current header and footer elements and then place it into the inner table
+		headerCopy = header.clone().prependTo( table );
+		headerTrgEls = header.find('tr'); // original header is in its own table
+		headerSrcEls = headerCopy.find('tr');
+		headerCopy.find('th, td').removeAttr('tabindex');
+	
+	
+		/*
+		 * 2. Take live measurements from the DOM - do not alter the DOM itself!
+		 */
+	
+		// Remove old sizing and apply the calculated column widths
+		// Get the unique column headers in the newly created (cloned) header. We want to apply the
+		// calculated sizes to this header
+		if ( ! scrollX )
+		{
+			divBodyStyle.width = '100%';
+			divHeader[0].style.width = '100%';
+		}
+	
+		$.each( _fnGetUniqueThs( settings, headerCopy ), function ( i, el ) {
+			idx = _fnVisibleToColumnIndex( settings, i );
+			el.style.width = settings.aoColumns[idx].sWidth;
+		} );
+	
+		if ( footer ) {
+			_fnApplyToChildren( function(n) {
+				n.style.width = "";
+			}, footerSrcEls );
+		}
+	
+		// Size the table as a whole
+		sanityWidth = table.outerWidth();
+		if ( scrollX === "" ) {
+			// No x scrolling
+			tableStyle.width = "100%";
+	
+			// IE7 will make the width of the table when 100% include the scrollbar
+			// - which is shouldn't. When there is a scrollbar we need to take this
+			// into account.
+			if ( ie67 && (table.find('tbody').height() > divBodyEl.offsetHeight ||
+				divBody.css('overflow-y') == "scroll")
+			) {
+				tableStyle.width = _fnStringToCss( table.outerWidth() - barWidth);
+			}
+	
+			// Recalculate the sanity width
+			sanityWidth = table.outerWidth();
+		}
+		else if ( scrollXInner !== "" ) {
+			// legacy x scroll inner has been given - use it
+			tableStyle.width = _fnStringToCss(scrollXInner);
+	
+			// Recalculate the sanity width
+			sanityWidth = table.outerWidth();
+		}
+	
+		// Hidden header should have zero height, so remove padding and borders. Then
+		// set the width based on the real headers
+	
+		// Apply all styles in one pass
+		_fnApplyToChildren( zeroOut, headerSrcEls );
+	
+		// Read all widths in next pass
+		_fnApplyToChildren( function(nSizer) {
+			headerContent.push( nSizer.innerHTML );
+			headerWidths.push( _fnStringToCss( $(nSizer).css('width') ) );
+		}, headerSrcEls );
+	
+		// Apply all widths in final pass
+		_fnApplyToChildren( function(nToSize, i) {
+			// Only apply widths to the DataTables detected header cells - this
+			// prevents complex headers from having contradictory sizes applied
+			if ( $.inArray( nToSize, dtHeaderCells ) !== -1 ) {
+				nToSize.style.width = headerWidths[i];
+			}
+		}, headerTrgEls );
+	
+		$(headerSrcEls).height(0);
+	
+		/* Same again with the footer if we have one */
+		if ( footer )
+		{
+			_fnApplyToChildren( zeroOut, footerSrcEls );
+	
+			_fnApplyToChildren( function(nSizer) {
+				footerContent.push( nSizer.innerHTML );
+				footerWidths.push( _fnStringToCss( $(nSizer).css('width') ) );
+			}, footerSrcEls );
+	
+			_fnApplyToChildren( function(nToSize, i) {
+				nToSize.style.width = footerWidths[i];
+			}, footerTrgEls );
+	
+			$(footerSrcEls).height(0);
+		}
+	
+	
+		/*
+		 * 3. Apply the measurements
+		 */
+	
+		// "Hide" the header and footer that we used for the sizing. We need to keep
+		// the content of the cell so that the width applied to the header and body
+		// both match, but we want to hide it completely. We want to also fix their
+		// width to what they currently are
+		_fnApplyToChildren( function(nSizer, i) {
+			nSizer.innerHTML = '<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+headerContent[i]+'</div>';
+			nSizer.style.width = headerWidths[i];
+		}, headerSrcEls );
+	
+		if ( footer )
+		{
+			_fnApplyToChildren( function(nSizer, i) {
+				nSizer.innerHTML = '<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+footerContent[i]+'</div>';
+				nSizer.style.width = footerWidths[i];
+			}, footerSrcEls );
+		}
+	
+		// Sanity check that the table is of a sensible width. If not then we are going to get
+		// misalignment - try to prevent this by not allowing the table to shrink below its min width
+		if ( table.outerWidth() < sanityWidth )
+		{
+			// The min width depends upon if we have a vertical scrollbar visible or not */
+			correction = ((divBodyEl.scrollHeight > divBodyEl.offsetHeight ||
+				divBody.css('overflow-y') == "scroll")) ?
+					sanityWidth+barWidth :
+					sanityWidth;
+	
+			// IE6/7 are a law unto themselves...
+			if ( ie67 && (divBodyEl.scrollHeight >
+				divBodyEl.offsetHeight || divBody.css('overflow-y') == "scroll")
+			) {
+				tableStyle.width = _fnStringToCss( correction-barWidth );
+			}
+	
+			// And give the user a warning that we've stopped the table getting too small
+			if ( scrollX === "" || scrollXInner !== "" ) {
+				_fnLog( settings, 1, 'Possible column misalignment', 6 );
+			}
+		}
+		else
+		{
+			correction = '100%';
+		}
+	
+		// Apply to the container elements
+		divBodyStyle.width = _fnStringToCss( correction );
+		divHeaderStyle.width = _fnStringToCss( correction );
+	
+		if ( footer ) {
+			settings.nScrollFoot.style.width = _fnStringToCss( correction );
+		}
+	
+	
+		/*
+		 * 4. Clean up
+		 */
+		if ( ! scrollY ) {
+			/* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting
+			 * the scrollbar height from the visible display, rather than adding it on. We need to
+			 * set the height in order to sort this. Don't want to do it in any other browsers.
+			 */
+			if ( ie67 ) {
+				divBodyStyle.height = _fnStringToCss( tableEl.offsetHeight+barWidth );
+			}
+		}
+	
+		/* Finally set the width's of the header and footer tables */
+		var iOuterWidth = table.outerWidth();
+		divHeaderTable[0].style.width = _fnStringToCss( iOuterWidth );
+		divHeaderInnerStyle.width = _fnStringToCss( iOuterWidth );
+	
+		// Figure out if there are scrollbar present - if so then we need a the header and footer to
+		// provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar)
+		var bScrolling = table.height() > divBodyEl.clientHeight || divBody.css('overflow-y') == "scroll";
+		var padding = 'padding' + (browser.bScrollbarLeft ? 'Left' : 'Right' );
+		divHeaderInnerStyle[ padding ] = bScrolling ? barWidth+"px" : "0px";
+	
+		if ( footer ) {
+			divFooterTable[0].style.width = _fnStringToCss( iOuterWidth );
+			divFooterInner[0].style.width = _fnStringToCss( iOuterWidth );
+			divFooterInner[0].style[padding] = bScrolling ? barWidth+"px" : "0px";
+		}
+	
+		// Correct DOM ordering for colgroup - comes before the thead
+		table.children('colgroup').insertBefore( table.children('thead') );
+	
+		/* Adjust the position of the header in case we loose the y-scrollbar */
+		divBody.scroll();
+	
+		// If sorting or filtering has occurred, jump the scrolling back to the top
+		// only if we aren't holding the position
+		if ( (settings.bSorted || settings.bFiltered) && ! settings._drawHold ) {
+			divBodyEl.scrollTop = 0;
+		}
+	}
+	
+	
+	
+	/**
+	 * Apply a given function to the display child nodes of an element array (typically
+	 * TD children of TR rows
+	 *  @param {function} fn Method to apply to the objects
+	 *  @param array {nodes} an1 List of elements to look through for display children
+	 *  @param array {nodes} an2 Another list (identical structure to the first) - optional
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnApplyToChildren( fn, an1, an2 )
+	{
+		var index=0, i=0, iLen=an1.length;
+		var nNode1, nNode2;
+	
+		while ( i < iLen ) {
+			nNode1 = an1[i].firstChild;
+			nNode2 = an2 ? an2[i].firstChild : null;
+	
+			while ( nNode1 ) {
+				if ( nNode1.nodeType === 1 ) {
+					if ( an2 ) {
+						fn( nNode1, nNode2, index );
+					}
+					else {
+						fn( nNode1, index );
+					}
+	
+					index++;
+				}
+	
+				nNode1 = nNode1.nextSibling;
+				nNode2 = an2 ? nNode2.nextSibling : null;
+			}
+	
+			i++;
+		}
+	}
+	
+	
+	
+	var __re_html_remove = /<.*?>/g;
+	
+	
+	/**
+	 * Calculate the width of columns for the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnCalculateColumnWidths ( oSettings )
+	{
+		var
+			table = oSettings.nTable,
+			columns = oSettings.aoColumns,
+			scroll = oSettings.oScroll,
+			scrollY = scroll.sY,
+			scrollX = scroll.sX,
+			scrollXInner = scroll.sXInner,
+			columnCount = columns.length,
+			visibleColumns = _fnGetColumns( oSettings, 'bVisible' ),
+			headerCells = $('th', oSettings.nTHead),
+			tableWidthAttr = table.getAttribute('width'), // from DOM element
+			tableContainer = table.parentNode,
+			userInputs = false,
+			i, column, columnIdx, width, outerWidth,
+			browser = oSettings.oBrowser,
+			ie67 = browser.bScrollOversize;
+	
+		var styleWidth = table.style.width;
+		if ( styleWidth && styleWidth.indexOf('%') !== -1 ) {
+			tableWidthAttr = styleWidth;
+		}
+	
+		/* Convert any user input sizes into pixel sizes */
+		for ( i=0 ; i<visibleColumns.length ; i++ ) {
+			column = columns[ visibleColumns[i] ];
+	
+			if ( column.sWidth !== null ) {
+				column.sWidth = _fnConvertToWidth( column.sWidthOrig, tableContainer );
+	
+				userInputs = true;
+			}
+		}
+	
+		/* If the number of columns in the DOM equals the number that we have to
+		 * process in DataTables, then we can use the offsets that are created by
+		 * the web- browser. No custom sizes can be set in order for this to happen,
+		 * nor scrolling used
+		 */
+		if ( ie67 || ! userInputs && ! scrollX && ! scrollY &&
+		     columnCount == _fnVisbleColumns( oSettings ) &&
+		     columnCount == headerCells.length
+		) {
+			for ( i=0 ; i<columnCount ; i++ ) {
+				var colIdx = _fnVisibleToColumnIndex( oSettings, i );
+	
+				if ( colIdx !== null ) {
+					columns[ colIdx ].sWidth = _fnStringToCss( headerCells.eq(i).width() );
+				}
+			}
+		}
+		else
+		{
+			// Otherwise construct a single row, worst case, table with the widest
+			// node in the data, assign any user defined widths, then insert it into
+			// the DOM and allow the browser to do all the hard work of calculating
+			// table widths
+			var tmpTable = $(table).clone() // don't use cloneNode - IE8 will remove events on the main table
+				.css( 'visibility', 'hidden' )
+				.removeAttr( 'id' );
+	
+			// Clean up the table body
+			tmpTable.find('tbody tr').remove();
+			var tr = $('<tr/>').appendTo( tmpTable.find('tbody') );
+	
+			// Clone the table header and footer - we can't use the header / footer
+			// from the cloned table, since if scrolling is active, the table's
+			// real header and footer are contained in different table tags
+			tmpTable.find('thead, tfoot').remove();
+			tmpTable
+				.append( $(oSettings.nTHead).clone() )
+				.append( $(oSettings.nTFoot).clone() );
+	
+			// Remove any assigned widths from the footer (from scrolling)
+			tmpTable.find('tfoot th, tfoot td').css('width', '');
+	
+			// Apply custom sizing to the cloned header
+			headerCells = _fnGetUniqueThs( oSettings, tmpTable.find('thead')[0] );
+	
+			for ( i=0 ; i<visibleColumns.length ; i++ ) {
+				column = columns[ visibleColumns[i] ];
+	
+				headerCells[i].style.width = column.sWidthOrig !== null && column.sWidthOrig !== '' ?
+					_fnStringToCss( column.sWidthOrig ) :
+					'';
+	
+				// For scrollX we need to force the column width otherwise the
+				// browser will collapse it. If this width is smaller than the
+				// width the column requires, then it will have no effect
+				if ( column.sWidthOrig && scrollX ) {
+					$( headerCells[i] ).append( $('<div/>').css( {
+						width: column.sWidthOrig,
+						margin: 0,
+						padding: 0,
+						border: 0,
+						height: 1
+					} ) );
+				}
+			}
+	
+			// Find the widest cell for each column and put it into the table
+			if ( oSettings.aoData.length ) {
+				for ( i=0 ; i<visibleColumns.length ; i++ ) {
+					columnIdx = visibleColumns[i];
+					column = columns[ columnIdx ];
+	
+					$( _fnGetWidestNode( oSettings, columnIdx ) )
+						.clone( false )
+						.append( column.sContentPadding )
+						.appendTo( tr );
+				}
+			}
+	
+			// Tidy the temporary table - remove name attributes so there aren't
+			// duplicated in the dom (radio elements for example)
+			$('[name]', tmpTable).removeAttr('name');
+	
+			// Table has been built, attach to the document so we can work with it.
+			// A holding element is used, positioned at the top of the container
+			// with minimal height, so it has no effect on if the container scrolls
+			// or not. Otherwise it might trigger scrolling when it actually isn't
+			// needed
+			var holder = $('<div/>').css( scrollX || scrollY ?
+					{
+						position: 'absolute',
+						top: 0,
+						left: 0,
+						height: 1,
+						right: 0,
+						overflow: 'hidden'
+					} :
+					{}
+				)
+				.append( tmpTable )
+				.appendTo( tableContainer );
+	
+			// When scrolling (X or Y) we want to set the width of the table as 
+			// appropriate. However, when not scrolling leave the table width as it
+			// is. This results in slightly different, but I think correct behaviour
+			if ( scrollX && scrollXInner ) {
+				tmpTable.width( scrollXInner );
+			}
+			else if ( scrollX ) {
+				tmpTable.css( 'width', 'auto' );
+				tmpTable.removeAttr('width');
+	
+				// If there is no width attribute or style, then allow the table to
+				// collapse
+				if ( tmpTable.width() < tableContainer.clientWidth && tableWidthAttr ) {
+					tmpTable.width( tableContainer.clientWidth );
+				}
+			}
+			else if ( scrollY ) {
+				tmpTable.width( tableContainer.clientWidth );
+			}
+			else if ( tableWidthAttr ) {
+				tmpTable.width( tableWidthAttr );
+			}
+	
+			// Get the width of each column in the constructed table - we need to
+			// know the inner width (so it can be assigned to the other table's
+			// cells) and the outer width so we can calculate the full width of the
+			// table. This is safe since DataTables requires a unique cell for each
+			// column, but if ever a header can span multiple columns, this will
+			// need to be modified.
+			var total = 0;
+			for ( i=0 ; i<visibleColumns.length ; i++ ) {
+				var cell = $(headerCells[i]);
+				var border = cell.outerWidth() - cell.width();
+	
+				// Use getBounding... where possible (not IE8-) because it can give
+				// sub-pixel accuracy, which we then want to round up!
+				var bounding = browser.bBounding ?
+					Math.ceil( headerCells[i].getBoundingClientRect().width ) :
+					cell.outerWidth();
+	
+				// Total is tracked to remove any sub-pixel errors as the outerWidth
+				// of the table might not equal the total given here (IE!).
+				total += bounding;
+	
+				// Width for each column to use
+				columns[ visibleColumns[i] ].sWidth = _fnStringToCss( bounding - border );
+			}
+	
+			table.style.width = _fnStringToCss( total );
+	
+			// Finished with the table - ditch it
+			holder.remove();
+		}
+	
+		// If there is a width attr, we want to attach an event listener which
+		// allows the table sizing to automatically adjust when the window is
+		// resized. Use the width attr rather than CSS, since we can't know if the
+		// CSS is a relative value or absolute - DOM read is always px.
+		if ( tableWidthAttr ) {
+			table.style.width = _fnStringToCss( tableWidthAttr );
+		}
+	
+		if ( (tableWidthAttr || scrollX) && ! oSettings._reszEvt ) {
+			var bindResize = function () {
+				$(window).on('resize.DT-'+oSettings.sInstance, _fnThrottle( function () {
+					_fnAdjustColumnSizing( oSettings );
+				} ) );
+			};
+	
+			// IE6/7 will crash if we bind a resize event handler on page load.
+			// To be removed in 1.11 which drops IE6/7 support
+			if ( ie67 ) {
+				setTimeout( bindResize, 1000 );
+			}
+			else {
+				bindResize();
+			}
+	
+			oSettings._reszEvt = true;
+		}
+	}
+	
+	
+	/**
+	 * Throttle the calls to a function. Arguments and context are maintained for
+	 * the throttled function
+	 *  @param {function} fn Function to be called
+	 *  @param {int} [freq=200] call frequency in mS
+	 *  @returns {function} wrapped function
+	 *  @memberof DataTable#oApi
+	 */
+	var _fnThrottle = DataTable.util.throttle;
+	
+	
+	/**
+	 * Convert a CSS unit width to pixels (e.g. 2em)
+	 *  @param {string} width width to be converted
+	 *  @param {node} parent parent to get the with for (required for relative widths) - optional
+	 *  @returns {int} width in pixels
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnConvertToWidth ( width, parent )
+	{
+		if ( ! width ) {
+			return 0;
+		}
+	
+		var n = $('<div/>')
+			.css( 'width', _fnStringToCss( width ) )
+			.appendTo( parent || document.body );
+	
+		var val = n[0].offsetWidth;
+		n.remove();
+	
+		return val;
+	}
+	
+	
+	/**
+	 * Get the widest node
+	 *  @param {object} settings dataTables settings object
+	 *  @param {int} colIdx column of interest
+	 *  @returns {node} widest table node
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetWidestNode( settings, colIdx )
+	{
+		var idx = _fnGetMaxLenString( settings, colIdx );
+		if ( idx < 0 ) {
+			return null;
+		}
+	
+		var data = settings.aoData[ idx ];
+		return ! data.nTr ? // Might not have been created when deferred rendering
+			$('<td/>').html( _fnGetCellData( settings, idx, colIdx, 'display' ) )[0] :
+			data.anCells[ colIdx ];
+	}
+	
+	
+	/**
+	 * Get the maximum strlen for each data column
+	 *  @param {object} settings dataTables settings object
+	 *  @param {int} colIdx column of interest
+	 *  @returns {string} max string length for each column
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnGetMaxLenString( settings, colIdx )
+	{
+		var s, max=-1, maxIdx = -1;
+	
+		for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
+			s = _fnGetCellData( settings, i, colIdx, 'display' )+'';
+			s = s.replace( __re_html_remove, '' );
+			s = s.replace( /&nbsp;/g, ' ' );
+	
+			if ( s.length > max ) {
+				max = s.length;
+				maxIdx = i;
+			}
+		}
+	
+		return maxIdx;
+	}
+	
+	
+	/**
+	 * Append a CSS unit (only if required) to a string
+	 *  @param {string} value to css-ify
+	 *  @returns {string} value with css unit
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnStringToCss( s )
+	{
+		if ( s === null ) {
+			return '0px';
+		}
+	
+		if ( typeof s == 'number' ) {
+			return s < 0 ?
+				'0px' :
+				s+'px';
+		}
+	
+		// Check it has a unit character already
+		return s.match(/\d$/) ?
+			s+'px' :
+			s;
+	}
+	
+	
+	
+	function _fnSortFlatten ( settings )
+	{
+		var
+			i, iLen, k, kLen,
+			aSort = [],
+			aiOrig = [],
+			aoColumns = settings.aoColumns,
+			aDataSort, iCol, sType, srcCol,
+			fixed = settings.aaSortingFixed,
+			fixedObj = $.isPlainObject( fixed ),
+			nestedSort = [],
+			add = function ( a ) {
+				if ( a.length && ! $.isArray( a[0] ) ) {
+					// 1D array
+					nestedSort.push( a );
+				}
+				else {
+					// 2D array
+					$.merge( nestedSort, a );
+				}
+			};
+	
+		// Build the sort array, with pre-fix and post-fix options if they have been
+		// specified
+		if ( $.isArray( fixed ) ) {
+			add( fixed );
+		}
+	
+		if ( fixedObj && fixed.pre ) {
+			add( fixed.pre );
+		}
+	
+		add( settings.aaSorting );
+	
+		if (fixedObj && fixed.post ) {
+			add( fixed.post );
+		}
+	
+		for ( i=0 ; i<nestedSort.length ; i++ )
+		{
+			srcCol = nestedSort[i][0];
+			aDataSort = aoColumns[ srcCol ].aDataSort;
+	
+			for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ )
+			{
+				iCol = aDataSort[k];
+				sType = aoColumns[ iCol ].sType || 'string';
+	
+				if ( nestedSort[i]._idx === undefined ) {
+					nestedSort[i]._idx = $.inArray( nestedSort[i][1], aoColumns[iCol].asSorting );
+				}
+	
+				aSort.push( {
+					src:       srcCol,
+					col:       iCol,
+					dir:       nestedSort[i][1],
+					index:     nestedSort[i]._idx,
+					type:      sType,
+					formatter: DataTable.ext.type.order[ sType+"-pre" ]
+				} );
+			}
+		}
+	
+		return aSort;
+	}
+	
+	/**
+	 * Change the order of the table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 *  @todo This really needs split up!
+	 */
+	function _fnSort ( oSettings )
+	{
+		var
+			i, ien, iLen, j, jLen, k, kLen,
+			sDataType, nTh,
+			aiOrig = [],
+			oExtSort = DataTable.ext.type.order,
+			aoData = oSettings.aoData,
+			aoColumns = oSettings.aoColumns,
+			aDataSort, data, iCol, sType, oSort,
+			formatters = 0,
+			sortCol,
+			displayMaster = oSettings.aiDisplayMaster,
+			aSort;
+	
+		// Resolve any column types that are unknown due to addition or invalidation
+		// @todo Can this be moved into a 'data-ready' handler which is called when
+		//   data is going to be used in the table?
+		_fnColumnTypes( oSettings );
+	
+		aSort = _fnSortFlatten( oSettings );
+	
+		for ( i=0, ien=aSort.length ; i<ien ; i++ ) {
+			sortCol = aSort[i];
+	
+			// Track if we can use the fast sort algorithm
+			if ( sortCol.formatter ) {
+				formatters++;
+			}
+	
+			// Load the data needed for the sort, for each cell
+			_fnSortData( oSettings, sortCol.col );
+		}
+	
+		/* No sorting required if server-side or no sorting array */
+		if ( _fnDataSource( oSettings ) != 'ssp' && aSort.length !== 0 )
+		{
+			// Create a value - key array of the current row positions such that we can use their
+			// current position during the sort, if values match, in order to perform stable sorting
+			for ( i=0, iLen=displayMaster.length ; i<iLen ; i++ ) {
+				aiOrig[ displayMaster[i] ] = i;
+			}
+	
+			/* Do the sort - here we want multi-column sorting based on a given data source (column)
+			 * and sorting function (from oSort) in a certain direction. It's reasonably complex to
+			 * follow on it's own, but this is what we want (example two column sorting):
+			 *  fnLocalSorting = function(a,b){
+			 *    var iTest;
+			 *    iTest = oSort['string-asc']('data11', 'data12');
+			 *      if (iTest !== 0)
+			 *        return iTest;
+			 *    iTest = oSort['numeric-desc']('data21', 'data22');
+			 *    if (iTest !== 0)
+			 *      return iTest;
+			 *    return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+			 *  }
+			 * Basically we have a test for each sorting column, if the data in that column is equal,
+			 * test the next column. If all columns match, then we use a numeric sort on the row
+			 * positions in the original data array to provide a stable sort.
+			 *
+			 * Note - I know it seems excessive to have two sorting methods, but the first is around
+			 * 15% faster, so the second is only maintained for backwards compatibility with sorting
+			 * methods which do not have a pre-sort formatting function.
+			 */
+			if ( formatters === aSort.length ) {
+				// All sort types have formatting functions
+				displayMaster.sort( function ( a, b ) {
+					var
+						x, y, k, test, sort,
+						len=aSort.length,
+						dataA = aoData[a]._aSortData,
+						dataB = aoData[b]._aSortData;
+	
+					for ( k=0 ; k<len ; k++ ) {
+						sort = aSort[k];
+	
+						x = dataA[ sort.col ];
+						y = dataB[ sort.col ];
+	
+						test = x<y ? -1 : x>y ? 1 : 0;
+						if ( test !== 0 ) {
+							return sort.dir === 'asc' ? test : -test;
+						}
+					}
+	
+					x = aiOrig[a];
+					y = aiOrig[b];
+					return x<y ? -1 : x>y ? 1 : 0;
+				} );
+			}
+			else {
+				// Depreciated - remove in 1.11 (providing a plug-in option)
+				// Not all sort types have formatting methods, so we have to call their sorting
+				// methods.
+				displayMaster.sort( function ( a, b ) {
+					var
+						x, y, k, l, test, sort, fn,
+						len=aSort.length,
+						dataA = aoData[a]._aSortData,
+						dataB = aoData[b]._aSortData;
+	
+					for ( k=0 ; k<len ; k++ ) {
+						sort = aSort[k];
+	
+						x = dataA[ sort.col ];
+						y = dataB[ sort.col ];
+	
+						fn = oExtSort[ sort.type+"-"+sort.dir ] || oExtSort[ "string-"+sort.dir ];
+						test = fn( x, y );
+						if ( test !== 0 ) {
+							return test;
+						}
+					}
+	
+					x = aiOrig[a];
+					y = aiOrig[b];
+					return x<y ? -1 : x>y ? 1 : 0;
+				} );
+			}
+		}
+	
+		/* Tell the draw function that we have sorted the data */
+		oSettings.bSorted = true;
+	}
+	
+	
+	function _fnSortAria ( settings )
+	{
+		var label;
+		var nextSort;
+		var columns = settings.aoColumns;
+		var aSort = _fnSortFlatten( settings );
+		var oAria = settings.oLanguage.oAria;
+	
+		// ARIA attributes - need to loop all columns, to update all (removing old
+		// attributes as needed)
+		for ( var i=0, iLen=columns.length ; i<iLen ; i++ )
+		{
+			var col = columns[i];
+			var asSorting = col.asSorting;
+			var sTitle = col.sTitle.replace( /<.*?>/g, "" );
+			var th = col.nTh;
+	
+			// IE7 is throwing an error when setting these properties with jQuery's
+			// attr() and removeAttr() methods...
+			th.removeAttribute('aria-sort');
+	
+			/* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */
+			if ( col.bSortable ) {
+				if ( aSort.length > 0 && aSort[0].col == i ) {
+					th.setAttribute('aria-sort', aSort[0].dir=="asc" ? "ascending" : "descending" );
+					nextSort = asSorting[ aSort[0].index+1 ] || asSorting[0];
+				}
+				else {
+					nextSort = asSorting[0];
+				}
+	
+				label = sTitle + ( nextSort === "asc" ?
+					oAria.sSortAscending :
+					oAria.sSortDescending
+				);
+			}
+			else {
+				label = sTitle;
+			}
+	
+			th.setAttribute('aria-label', label);
+		}
+	}
+	
+	
+	/**
+	 * Function to run on user sort request
+	 *  @param {object} settings dataTables settings object
+	 *  @param {node} attachTo node to attach the handler to
+	 *  @param {int} colIdx column sorting index
+	 *  @param {boolean} [append=false] Append the requested sort to the existing
+	 *    sort if true (i.e. multi-column sort)
+	 *  @param {function} [callback] callback function
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSortListener ( settings, colIdx, append, callback )
+	{
+		var col = settings.aoColumns[ colIdx ];
+		var sorting = settings.aaSorting;
+		var asSorting = col.asSorting;
+		var nextSortIdx;
+		var next = function ( a, overflow ) {
+			var idx = a._idx;
+			if ( idx === undefined ) {
+				idx = $.inArray( a[1], asSorting );
+			}
+	
+			return idx+1 < asSorting.length ?
+				idx+1 :
+				overflow ?
+					null :
+					0;
+		};
+	
+		// Convert to 2D array if needed
+		if ( typeof sorting[0] === 'number' ) {
+			sorting = settings.aaSorting = [ sorting ];
+		}
+	
+		// If appending the sort then we are multi-column sorting
+		if ( append && settings.oFeatures.bSortMulti ) {
+			// Are we already doing some kind of sort on this column?
+			var sortIdx = $.inArray( colIdx, _pluck(sorting, '0') );
+	
+			if ( sortIdx !== -1 ) {
+				// Yes, modify the sort
+				nextSortIdx = next( sorting[sortIdx], true );
+	
+				if ( nextSortIdx === null && sorting.length === 1 ) {
+					nextSortIdx = 0; // can't remove sorting completely
+				}
+	
+				if ( nextSortIdx === null ) {
+					sorting.splice( sortIdx, 1 );
+				}
+				else {
+					sorting[sortIdx][1] = asSorting[ nextSortIdx ];
+					sorting[sortIdx]._idx = nextSortIdx;
+				}
+			}
+			else {
+				// No sort on this column yet
+				sorting.push( [ colIdx, asSorting[0], 0 ] );
+				sorting[sorting.length-1]._idx = 0;
+			}
+		}
+		else if ( sorting.length && sorting[0][0] == colIdx ) {
+			// Single column - already sorting on this column, modify the sort
+			nextSortIdx = next( sorting[0] );
+	
+			sorting.length = 1;
+			sorting[0][1] = asSorting[ nextSortIdx ];
+			sorting[0]._idx = nextSortIdx;
+		}
+		else {
+			// Single column - sort only on this column
+			sorting.length = 0;
+			sorting.push( [ colIdx, asSorting[0] ] );
+			sorting[0]._idx = 0;
+		}
+	
+		// Run the sort by calling a full redraw
+		_fnReDraw( settings );
+	
+		// callback used for async user interaction
+		if ( typeof callback == 'function' ) {
+			callback( settings );
+		}
+	}
+	
+	
+	/**
+	 * Attach a sort handler (click) to a node
+	 *  @param {object} settings dataTables settings object
+	 *  @param {node} attachTo node to attach the handler to
+	 *  @param {int} colIdx column sorting index
+	 *  @param {function} [callback] callback function
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSortAttachListener ( settings, attachTo, colIdx, callback )
+	{
+		var col = settings.aoColumns[ colIdx ];
+	
+		_fnBindAction( attachTo, {}, function (e) {
+			/* If the column is not sortable - don't to anything */
+			if ( col.bSortable === false ) {
+				return;
+			}
+	
+			// If processing is enabled use a timeout to allow the processing
+			// display to be shown - otherwise to it synchronously
+			if ( settings.oFeatures.bProcessing ) {
+				_fnProcessingDisplay( settings, true );
+	
+				setTimeout( function() {
+					_fnSortListener( settings, colIdx, e.shiftKey, callback );
+	
+					// In server-side processing, the draw callback will remove the
+					// processing display
+					if ( _fnDataSource( settings ) !== 'ssp' ) {
+						_fnProcessingDisplay( settings, false );
+					}
+				}, 0 );
+			}
+			else {
+				_fnSortListener( settings, colIdx, e.shiftKey, callback );
+			}
+		} );
+	}
+	
+	
+	/**
+	 * Set the sorting classes on table's body, Note: it is safe to call this function
+	 * when bSort and bSortClasses are false
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSortingClasses( settings )
+	{
+		var oldSort = settings.aLastSort;
+		var sortClass = settings.oClasses.sSortColumn;
+		var sort = _fnSortFlatten( settings );
+		var features = settings.oFeatures;
+		var i, ien, colIdx;
+	
+		if ( features.bSort && features.bSortClasses ) {
+			// Remove old sorting classes
+			for ( i=0, ien=oldSort.length ; i<ien ; i++ ) {
+				colIdx = oldSort[i].src;
+	
+				// Remove column sorting
+				$( _pluck( settings.aoData, 'anCells', colIdx ) )
+					.removeClass( sortClass + (i<2 ? i+1 : 3) );
+			}
+	
+			// Add new column sorting
+			for ( i=0, ien=sort.length ; i<ien ; i++ ) {
+				colIdx = sort[i].src;
+	
+				$( _pluck( settings.aoData, 'anCells', colIdx ) )
+					.addClass( sortClass + (i<2 ? i+1 : 3) );
+			}
+		}
+	
+		settings.aLastSort = sort;
+	}
+	
+	
+	// Get the data to sort a column, be it from cache, fresh (populating the
+	// cache), or from a sort formatter
+	function _fnSortData( settings, idx )
+	{
+		// Custom sorting function - provided by the sort data type
+		var column = settings.aoColumns[ idx ];
+		var customSort = DataTable.ext.order[ column.sSortDataType ];
+		var customData;
+	
+		if ( customSort ) {
+			customData = customSort.call( settings.oInstance, settings, idx,
+				_fnColumnIndexToVisible( settings, idx )
+			);
+		}
+	
+		// Use / populate cache
+		var row, cellData;
+		var formatter = DataTable.ext.type.order[ column.sType+"-pre" ];
+	
+		for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
+			row = settings.aoData[i];
+	
+			if ( ! row._aSortData ) {
+				row._aSortData = [];
+			}
+	
+			if ( ! row._aSortData[idx] || customSort ) {
+				cellData = customSort ?
+					customData[i] : // If there was a custom sort function, use data from there
+					_fnGetCellData( settings, i, idx, 'sort' );
+	
+				row._aSortData[ idx ] = formatter ?
+					formatter( cellData ) :
+					cellData;
+			}
+		}
+	}
+	
+	
+	
+	/**
+	 * Save the state of a table
+	 *  @param {object} oSettings dataTables settings object
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSaveState ( settings )
+	{
+		if ( !settings.oFeatures.bStateSave || settings.bDestroying )
+		{
+			return;
+		}
+	
+		/* Store the interesting variables */
+		var state = {
+			time:    +new Date(),
+			start:   settings._iDisplayStart,
+			length:  settings._iDisplayLength,
+			order:   $.extend( true, [], settings.aaSorting ),
+			search:  _fnSearchToCamel( settings.oPreviousSearch ),
+			columns: $.map( settings.aoColumns, function ( col, i ) {
+				return {
+					visible: col.bVisible,
+					search: _fnSearchToCamel( settings.aoPreSearchCols[i] )
+				};
+			} )
+		};
+	
+		_fnCallbackFire( settings, "aoStateSaveParams", 'stateSaveParams', [settings, state] );
+	
+		settings.oSavedState = state;
+		settings.fnStateSaveCallback.call( settings.oInstance, settings, state );
+	}
+	
+	
+	/**
+	 * Attempt to load a saved table state
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {object} oInit DataTables init object so we can override settings
+	 *  @param {function} callback Callback to execute when the state has been loaded
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnLoadState ( settings, oInit, callback )
+	{
+		var i, ien;
+		var columns = settings.aoColumns;
+		var loaded = function ( s ) {
+			if ( ! s || ! s.time ) {
+				callback();
+				return;
+			}
+	
+			// Allow custom and plug-in manipulation functions to alter the saved data set and
+			// cancelling of loading by returning false
+			var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] );
+			if ( $.inArray( false, abStateLoad ) !== -1 ) {
+				callback();
+				return;
+			}
+	
+			// Reject old data
+			var duration = settings.iStateDuration;
+			if ( duration > 0 && s.time < +new Date() - (duration*1000) ) {
+				callback();
+				return;
+			}
+	
+			// Number of columns have changed - all bets are off, no restore of settings
+			if ( s.columns && columns.length !== s.columns.length ) {
+				callback();
+				return;
+			}
+	
+			// Store the saved state so it might be accessed at any time
+			settings.oLoadedState = $.extend( true, {}, s );
+	
+			// Restore key features - todo - for 1.11 this needs to be done by
+			// subscribed events
+			if ( s.start !== undefined ) {
+				settings._iDisplayStart    = s.start;
+				settings.iInitDisplayStart = s.start;
+			}
+			if ( s.length !== undefined ) {
+				settings._iDisplayLength   = s.length;
+			}
+	
+			// Order
+			if ( s.order !== undefined ) {
+				settings.aaSorting = [];
+				$.each( s.order, function ( i, col ) {
+					settings.aaSorting.push( col[0] >= columns.length ?
+						[ 0, col[1] ] :
+						col
+					);
+				} );
+			}
+	
+			// Search
+			if ( s.search !== undefined ) {
+				$.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) );
+			}
+	
+			// Columns
+			//
+			if ( s.columns ) {
+				for ( i=0, ien=s.columns.length ; i<ien ; i++ ) {
+					var col = s.columns[i];
+	
+					// Visibility
+					if ( col.visible !== undefined ) {
+						columns[i].bVisible = col.visible;
+					}
+	
+					// Search
+					if ( col.search !== undefined ) {
+						$.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) );
+					}
+				}
+			}
+	
+			_fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] );
+			callback();
+		}
+	
+		if ( ! settings.oFeatures.bStateSave ) {
+			callback();
+			return;
+		}
+	
+		var state = settings.fnStateLoadCallback.call( settings.oInstance, settings, loaded );
+	
+		if ( state !== undefined ) {
+			loaded( state );
+		}
+		// otherwise, wait for the loaded callback to be executed
+	}
+	
+	
+	/**
+	 * Return the settings object for a particular table
+	 *  @param {node} table table we are using as a dataTable
+	 *  @returns {object} Settings object - or null if not found
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnSettingsFromNode ( table )
+	{
+		var settings = DataTable.settings;
+		var idx = $.inArray( table, _pluck( settings, 'nTable' ) );
+	
+		return idx !== -1 ?
+			settings[ idx ] :
+			null;
+	}
+	
+	
+	/**
+	 * Log an error message
+	 *  @param {object} settings dataTables settings object
+	 *  @param {int} level log error messages, or display them to the user
+	 *  @param {string} msg error message
+	 *  @param {int} tn Technical note id to get more information about the error.
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnLog( settings, level, msg, tn )
+	{
+		msg = 'DataTables warning: '+
+			(settings ? 'table id='+settings.sTableId+' - ' : '')+msg;
+	
+		if ( tn ) {
+			msg += '. For more information about this error, please see '+
+			'http://datatables.net/tn/'+tn;
+		}
+	
+		if ( ! level  ) {
+			// Backwards compatibility pre 1.10
+			var ext = DataTable.ext;
+			var type = ext.sErrMode || ext.errMode;
+	
+			if ( settings ) {
+				_fnCallbackFire( settings, null, 'error', [ settings, tn, msg ] );
+			}
+	
+			if ( type == 'alert' ) {
+				alert( msg );
+			}
+			else if ( type == 'throw' ) {
+				throw new Error(msg);
+			}
+			else if ( typeof type == 'function' ) {
+				type( settings, tn, msg );
+			}
+		}
+		else if ( window.console && console.log ) {
+			console.log( msg );
+		}
+	}
+	
+	
+	/**
+	 * See if a property is defined on one object, if so assign it to the other object
+	 *  @param {object} ret target object
+	 *  @param {object} src source object
+	 *  @param {string} name property
+	 *  @param {string} [mappedName] name to map too - optional, name used if not given
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnMap( ret, src, name, mappedName )
+	{
+		if ( $.isArray( name ) ) {
+			$.each( name, function (i, val) {
+				if ( $.isArray( val ) ) {
+					_fnMap( ret, src, val[0], val[1] );
+				}
+				else {
+					_fnMap( ret, src, val );
+				}
+			} );
+	
+			return;
+		}
+	
+		if ( mappedName === undefined ) {
+			mappedName = name;
+		}
+	
+		if ( src[name] !== undefined ) {
+			ret[mappedName] = src[name];
+		}
+	}
+	
+	
+	/**
+	 * Extend objects - very similar to jQuery.extend, but deep copy objects, and
+	 * shallow copy arrays. The reason we need to do this, is that we don't want to
+	 * deep copy array init values (such as aaSorting) since the dev wouldn't be
+	 * able to override them, but we do want to deep copy arrays.
+	 *  @param {object} out Object to extend
+	 *  @param {object} extender Object from which the properties will be applied to
+	 *      out
+	 *  @param {boolean} breakRefs If true, then arrays will be sliced to take an
+	 *      independent copy with the exception of the `data` or `aaData` parameters
+	 *      if they are present. This is so you can pass in a collection to
+	 *      DataTables and have that used as your data source without breaking the
+	 *      references
+	 *  @returns {object} out Reference, just for convenience - out === the return.
+	 *  @memberof DataTable#oApi
+	 *  @todo This doesn't take account of arrays inside the deep copied objects.
+	 */
+	function _fnExtend( out, extender, breakRefs )
+	{
+		var val;
+	
+		for ( var prop in extender ) {
+			if ( extender.hasOwnProperty(prop) ) {
+				val = extender[prop];
+	
+				if ( $.isPlainObject( val ) ) {
+					if ( ! $.isPlainObject( out[prop] ) ) {
+						out[prop] = {};
+					}
+					$.extend( true, out[prop], val );
+				}
+				else if ( breakRefs && prop !== 'data' && prop !== 'aaData' && $.isArray(val) ) {
+					out[prop] = val.slice();
+				}
+				else {
+					out[prop] = val;
+				}
+			}
+		}
+	
+		return out;
+	}
+	
+	
+	/**
+	 * Bind an event handers to allow a click or return key to activate the callback.
+	 * This is good for accessibility since a return on the keyboard will have the
+	 * same effect as a click, if the element has focus.
+	 *  @param {element} n Element to bind the action to
+	 *  @param {object} oData Data object to pass to the triggered function
+	 *  @param {function} fn Callback function for when the event is triggered
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnBindAction( n, oData, fn )
+	{
+		$(n)
+			.on( 'click.DT', oData, function (e) {
+					n.blur(); // Remove focus outline for mouse users
+					fn(e);
+				} )
+			.on( 'keypress.DT', oData, function (e){
+					if ( e.which === 13 ) {
+						e.preventDefault();
+						fn(e);
+					}
+				} )
+			.on( 'selectstart.DT', function () {
+					/* Take the brutal approach to cancelling text selection */
+					return false;
+				} );
+	}
+	
+	
+	/**
+	 * Register a callback function. Easily allows a callback function to be added to
+	 * an array store of callback functions that can then all be called together.
+	 *  @param {object} oSettings dataTables settings object
+	 *  @param {string} sStore Name of the array storage for the callbacks in oSettings
+	 *  @param {function} fn Function to be called back
+	 *  @param {string} sName Identifying name for the callback (i.e. a label)
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnCallbackReg( oSettings, sStore, fn, sName )
+	{
+		if ( fn )
+		{
+			oSettings[sStore].push( {
+				"fn": fn,
+				"sName": sName
+			} );
+		}
+	}
+	
+	
+	/**
+	 * Fire callback functions and trigger events. Note that the loop over the
+	 * callback array store is done backwards! Further note that you do not want to
+	 * fire off triggers in time sensitive applications (for example cell creation)
+	 * as its slow.
+	 *  @param {object} settings dataTables settings object
+	 *  @param {string} callbackArr Name of the array storage for the callbacks in
+	 *      oSettings
+	 *  @param {string} eventName Name of the jQuery custom event to trigger. If
+	 *      null no trigger is fired
+	 *  @param {array} args Array of arguments to pass to the callback function /
+	 *      trigger
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnCallbackFire( settings, callbackArr, eventName, args )
+	{
+		var ret = [];
+	
+		if ( callbackArr ) {
+			ret = $.map( settings[callbackArr].slice().reverse(), function (val, i) {
+				return val.fn.apply( settings.oInstance, args );
+			} );
+		}
+	
+		if ( eventName !== null ) {
+			var e = $.Event( eventName+'.dt' );
+	
+			$(settings.nTable).trigger( e, args );
+	
+			ret.push( e.result );
+		}
+	
+		return ret;
+	}
+	
+	
+	function _fnLengthOverflow ( settings )
+	{
+		var
+			start = settings._iDisplayStart,
+			end = settings.fnDisplayEnd(),
+			len = settings._iDisplayLength;
+	
+		/* If we have space to show extra rows (backing up from the end point - then do so */
+		if ( start >= end )
+		{
+			start = end - len;
+		}
+	
+		// Keep the start record on the current page
+		start -= (start % len);
+	
+		if ( len === -1 || start < 0 )
+		{
+			start = 0;
+		}
+	
+		settings._iDisplayStart = start;
+	}
+	
+	
+	function _fnRenderer( settings, type )
+	{
+		var renderer = settings.renderer;
+		var host = DataTable.ext.renderer[type];
+	
+		if ( $.isPlainObject( renderer ) && renderer[type] ) {
+			// Specific renderer for this type. If available use it, otherwise use
+			// the default.
+			return host[renderer[type]] || host._;
+		}
+		else if ( typeof renderer === 'string' ) {
+			// Common renderer - if there is one available for this type use it,
+			// otherwise use the default
+			return host[renderer] || host._;
+		}
+	
+		// Use the default
+		return host._;
+	}
+	
+	
+	/**
+	 * Detect the data source being used for the table. Used to simplify the code
+	 * a little (ajax) and to make it compress a little smaller.
+	 *
+	 *  @param {object} settings dataTables settings object
+	 *  @returns {string} Data source
+	 *  @memberof DataTable#oApi
+	 */
+	function _fnDataSource ( settings )
+	{
+		if ( settings.oFeatures.bServerSide ) {
+			return 'ssp';
+		}
+		else if ( settings.ajax || settings.sAjaxSource ) {
+			return 'ajax';
+		}
+		return 'dom';
+	}
+	
+
+	
+	
+	/**
+	 * Computed structure of the DataTables API, defined by the options passed to
+	 * `DataTable.Api.register()` when building the API.
+	 *
+	 * The structure is built in order to speed creation and extension of the Api
+	 * objects since the extensions are effectively pre-parsed.
+	 *
+	 * The array is an array of objects with the following structure, where this
+	 * base array represents the Api prototype base:
+	 *
+	 *     [
+	 *       {
+	 *         name:      'data'                -- string   - Property name
+	 *         val:       function () {},       -- function - Api method (or undefined if just an object
+	 *         methodExt: [ ... ],              -- array    - Array of Api object definitions to extend the method result
+	 *         propExt:   [ ... ]               -- array    - Array of Api object definitions to extend the property
+	 *       },
+	 *       {
+	 *         name:     'row'
+	 *         val:       {},
+	 *         methodExt: [ ... ],
+	 *         propExt:   [
+	 *           {
+	 *             name:      'data'
+	 *             val:       function () {},
+	 *             methodExt: [ ... ],
+	 *             propExt:   [ ... ]
+	 *           },
+	 *           ...
+	 *         ]
+	 *       }
+	 *     ]
+	 *
+	 * @type {Array}
+	 * @ignore
+	 */
+	var __apiStruct = [];
+	
+	
+	/**
+	 * `Array.prototype` reference.
+	 *
+	 * @type object
+	 * @ignore
+	 */
+	var __arrayProto = Array.prototype;
+	
+	
+	/**
+	 * Abstraction for `context` parameter of the `Api` constructor to allow it to
+	 * take several different forms for ease of use.
+	 *
+	 * Each of the input parameter types will be converted to a DataTables settings
+	 * object where possible.
+	 *
+	 * @param  {string|node|jQuery|object} mixed DataTable identifier. Can be one
+	 *   of:
+	 *
+	 *   * `string` - jQuery selector. Any DataTables' matching the given selector
+	 *     with be found and used.
+	 *   * `node` - `TABLE` node which has already been formed into a DataTable.
+	 *   * `jQuery` - A jQuery object of `TABLE` nodes.
+	 *   * `object` - DataTables settings object
+	 *   * `DataTables.Api` - API instance
+	 * @return {array|null} Matching DataTables settings objects. `null` or
+	 *   `undefined` is returned if no matching DataTable is found.
+	 * @ignore
+	 */
+	var _toSettings = function ( mixed )
+	{
+		var idx, jq;
+		var settings = DataTable.settings;
+		var tables = $.map( settings, function (el, i) {
+			return el.nTable;
+		} );
+	
+		if ( ! mixed ) {
+			return [];
+		}
+		else if ( mixed.nTable && mixed.oApi ) {
+			// DataTables settings object
+			return [ mixed ];
+		}
+		else if ( mixed.nodeName && mixed.nodeName.toLowerCase() === 'table' ) {
+			// Table node
+			idx = $.inArray( mixed, tables );
+			return idx !== -1 ? [ settings[idx] ] : null;
+		}
+		else if ( mixed && typeof mixed.settings === 'function' ) {
+			return mixed.settings().toArray();
+		}
+		else if ( typeof mixed === 'string' ) {
+			// jQuery selector
+			jq = $(mixed);
+		}
+		else if ( mixed instanceof $ ) {
+			// jQuery object (also DataTables instance)
+			jq = mixed;
+		}
+	
+		if ( jq ) {
+			return jq.map( function(i) {
+				idx = $.inArray( this, tables );
+				return idx !== -1 ? settings[idx] : null;
+			} ).toArray();
+		}
+	};
+	
+	
+	/**
+	 * DataTables API class - used to control and interface with  one or more
+	 * DataTables enhanced tables.
+	 *
+	 * The API class is heavily based on jQuery, presenting a chainable interface
+	 * that you can use to interact with tables. Each instance of the API class has
+	 * a "context" - i.e. the tables that it will operate on. This could be a single
+	 * table, all tables on a page or a sub-set thereof.
+	 *
+	 * Additionally the API is designed to allow you to easily work with the data in
+	 * the tables, retrieving and manipulating it as required. This is done by
+	 * presenting the API class as an array like interface. The contents of the
+	 * array depend upon the actions requested by each method (for example
+	 * `rows().nodes()` will return an array of nodes, while `rows().data()` will
+	 * return an array of objects or arrays depending upon your table's
+	 * configuration). The API object has a number of array like methods (`push`,
+	 * `pop`, `reverse` etc) as well as additional helper methods (`each`, `pluck`,
+	 * `unique` etc) to assist your working with the data held in a table.
+	 *
+	 * Most methods (those which return an Api instance) are chainable, which means
+	 * the return from a method call also has all of the methods available that the
+	 * top level object had. For example, these two calls are equivalent:
+	 *
+	 *     // Not chained
+	 *     api.row.add( {...} );
+	 *     api.draw();
+	 *
+	 *     // Chained
+	 *     api.row.add( {...} ).draw();
+	 *
+	 * @class DataTable.Api
+	 * @param {array|object|string|jQuery} context DataTable identifier. This is
+	 *   used to define which DataTables enhanced tables this API will operate on.
+	 *   Can be one of:
+	 *
+	 *   * `string` - jQuery selector. Any DataTables' matching the given selector
+	 *     with be found and used.
+	 *   * `node` - `TABLE` node which has already been formed into a DataTable.
+	 *   * `jQuery` - A jQuery object of `TABLE` nodes.
+	 *   * `object` - DataTables settings object
+	 * @param {array} [data] Data to initialise the Api instance with.
+	 *
+	 * @example
+	 *   // Direct initialisation during DataTables construction
+	 *   var api = $('#example').DataTable();
+	 *
+	 * @example
+	 *   // Initialisation using a DataTables jQuery object
+	 *   var api = $('#example').dataTable().api();
+	 *
+	 * @example
+	 *   // Initialisation as a constructor
+	 *   var api = new $.fn.DataTable.Api( 'table.dataTable' );
+	 */
+	_Api = function ( context, data )
+	{
+		if ( ! (this instanceof _Api) ) {
+			return new _Api( context, data );
+		}
+	
+		var settings = [];
+		var ctxSettings = function ( o ) {
+			var a = _toSettings( o );
+			if ( a ) {
+				settings = settings.concat( a );
+			}
+		};
+	
+		if ( $.isArray( context ) ) {
+			for ( var i=0, ien=context.length ; i<ien ; i++ ) {
+				ctxSettings( context[i] );
+			}
+		}
+		else {
+			ctxSettings( context );
+		}
+	
+		// Remove duplicates
+		this.context = _unique( settings );
+	
+		// Initial data
+		if ( data ) {
+			$.merge( this, data );
+		}
+	
+		// selector
+		this.selector = {
+			rows: null,
+			cols: null,
+			opts: null
+		};
+	
+		_Api.extend( this, this, __apiStruct );
+	};
+	
+	DataTable.Api = _Api;
+	
+	// Don't destroy the existing prototype, just extend it. Required for jQuery 2's
+	// isPlainObject.
+	$.extend( _Api.prototype, {
+		any: function ()
+		{
+			return this.count() !== 0;
+		},
+	
+	
+		concat:  __arrayProto.concat,
+	
+	
+		context: [], // array of table settings objects
+	
+	
+		count: function ()
+		{
+			return this.flatten().length;
+		},
+	
+	
+		each: function ( fn )
+		{
+			for ( var i=0, ien=this.length ; i<ien; i++ ) {
+				fn.call( this, this[i], i, this );
+			}
+	
+			return this;
+		},
+	
+	
+		eq: function ( idx )
+		{
+			var ctx = this.context;
+	
+			return ctx.length > idx ?
+				new _Api( ctx[idx], this[idx] ) :
+				null;
+		},
+	
+	
+		filter: function ( fn )
+		{
+			var a = [];
+	
+			if ( __arrayProto.filter ) {
+				a = __arrayProto.filter.call( this, fn, this );
+			}
+			else {
+				// Compatibility for browsers without EMCA-252-5 (JS 1.6)
+				for ( var i=0, ien=this.length ; i<ien ; i++ ) {
+					if ( fn.call( this, this[i], i, this ) ) {
+						a.push( this[i] );
+					}
+				}
+			}
+	
+			return new _Api( this.context, a );
+		},
+	
+	
+		flatten: function ()
+		{
+			var a = [];
+			return new _Api( this.context, a.concat.apply( a, this.toArray() ) );
+		},
+	
+	
+		join:    __arrayProto.join,
+	
+	
+		indexOf: __arrayProto.indexOf || function (obj, start)
+		{
+			for ( var i=(start || 0), ien=this.length ; i<ien ; i++ ) {
+				if ( this[i] === obj ) {
+					return i;
+				}
+			}
+			return -1;
+		},
+	
+		iterator: function ( flatten, type, fn, alwaysNew ) {
+			var
+				a = [], ret,
+				i, ien, j, jen,
+				context = this.context,
+				rows, items, item,
+				selector = this.selector;
+	
+			// Argument shifting
+			if ( typeof flatten === 'string' ) {
+				alwaysNew = fn;
+				fn = type;
+				type = flatten;
+				flatten = false;
+			}
+	
+			for ( i=0, ien=context.length ; i<ien ; i++ ) {
+				var apiInst = new _Api( context[i] );
+	
+				if ( type === 'table' ) {
+					ret = fn.call( apiInst, context[i], i );
+	
+					if ( ret !== undefined ) {
+						a.push( ret );
+					}
+				}
+				else if ( type === 'columns' || type === 'rows' ) {
+					// this has same length as context - one entry for each table
+					ret = fn.call( apiInst, context[i], this[i], i );
+	
+					if ( ret !== undefined ) {
+						a.push( ret );
+					}
+				}
+				else if ( type === 'column' || type === 'column-rows' || type === 'row' || type === 'cell' ) {
+					// columns and rows share the same structure.
+					// 'this' is an array of column indexes for each context
+					items = this[i];
+	
+					if ( type === 'column-rows' ) {
+						rows = _selector_row_indexes( context[i], selector.opts );
+					}
+	
+					for ( j=0, jen=items.length ; j<jen ; j++ ) {
+						item = items[j];
+	
+						if ( type === 'cell' ) {
+							ret = fn.call( apiInst, context[i], item.row, item.column, i, j );
+						}
+						else {
+							ret = fn.call( apiInst, context[i], item, i, j, rows );
+						}
+	
+						if ( ret !== undefined ) {
+							a.push( ret );
+						}
+					}
+				}
+			}
+	
+			if ( a.length || alwaysNew ) {
+				var api = new _Api( context, flatten ? a.concat.apply( [], a ) : a );
+				var apiSelector = api.selector;
+				apiSelector.rows = selector.rows;
+				apiSelector.cols = selector.cols;
+				apiSelector.opts = selector.opts;
+				return api;
+			}
+			return this;
+		},
+	
+	
+		lastIndexOf: __arrayProto.lastIndexOf || function (obj, start)
+		{
+			// Bit cheeky...
+			return this.indexOf.apply( this.toArray.reverse(), arguments );
+		},
+	
+	
+		length:  0,
+	
+	
+		map: function ( fn )
+		{
+			var a = [];
+	
+			if ( __arrayProto.map ) {
+				a = __arrayProto.map.call( this, fn, this );
+			}
+			else {
+				// Compatibility for browsers without EMCA-252-5 (JS 1.6)
+				for ( var i=0, ien=this.length ; i<ien ; i++ ) {
+					a.push( fn.call( this, this[i], i ) );
+				}
+			}
+	
+			return new _Api( this.context, a );
+		},
+	
+	
+		pluck: function ( prop )
+		{
+			return this.map( function ( el ) {
+				return el[ prop ];
+			} );
+		},
+	
+		pop:     __arrayProto.pop,
+	
+	
+		push:    __arrayProto.push,
+	
+	
+		// Does not return an API instance
+		reduce: __arrayProto.reduce || function ( fn, init )
+		{
+			return _fnReduce( this, fn, init, 0, this.length, 1 );
+		},
+	
+	
+		reduceRight: __arrayProto.reduceRight || function ( fn, init )
+		{
+			return _fnReduce( this, fn, init, this.length-1, -1, -1 );
+		},
+	
+	
+		reverse: __arrayProto.reverse,
+	
+	
+		// Object with rows, columns and opts
+		selector: null,
+	
+	
+		shift:   __arrayProto.shift,
+	
+	
+		slice: function () {
+			return new _Api( this.context, this );
+		},
+	
+	
+		sort:    __arrayProto.sort, // ? name - order?
+	
+	
+		splice:  __arrayProto.splice,
+	
+	
+		toArray: function ()
+		{
+			return __arrayProto.slice.call( this );
+		},
+	
+	
+		to$: function ()
+		{
+			return $( this );
+		},
+	
+	
+		toJQuery: function ()
+		{
+			return $( this );
+		},
+	
+	
+		unique: function ()
+		{
+			return new _Api( this.context, _unique(this) );
+		},
+	
+	
+		unshift: __arrayProto.unshift
+	} );
+	
+	
+	_Api.extend = function ( scope, obj, ext )
+	{
+		// Only extend API instances and static properties of the API
+		if ( ! ext.length || ! obj || ( ! (obj instanceof _Api) && ! obj.__dt_wrapper ) ) {
+			return;
+		}
+	
+		var
+			i, ien,
+			j, jen,
+			struct, inner,
+			methodScoping = function ( scope, fn, struc ) {
+				return function () {
+					var ret = fn.apply( scope, arguments );
+	
+					// Method extension
+					_Api.extend( ret, ret, struc.methodExt );
+					return ret;
+				};
+			};
+	
+		for ( i=0, ien=ext.length ; i<ien ; i++ ) {
+			struct = ext[i];
+	
+			// Value
+			obj[ struct.name ] = typeof struct.val === 'function' ?
+				methodScoping( scope, struct.val, struct ) :
+				$.isPlainObject( struct.val ) ?
+					{} :
+					struct.val;
+	
+			obj[ struct.name ].__dt_wrapper = true;
+	
+			// Property extension
+			_Api.extend( scope, obj[ struct.name ], struct.propExt );
+		}
+	};
+	
+	
+	// @todo - Is there need for an augment function?
+	// _Api.augment = function ( inst, name )
+	// {
+	// 	// Find src object in the structure from the name
+	// 	var parts = name.split('.');
+	
+	// 	_Api.extend( inst, obj );
+	// };
+	
+	
+	//     [
+	//       {
+	//         name:      'data'                -- string   - Property name
+	//         val:       function () {},       -- function - Api method (or undefined if just an object
+	//         methodExt: [ ... ],              -- array    - Array of Api object definitions to extend the method result
+	//         propExt:   [ ... ]               -- array    - Array of Api object definitions to extend the property
+	//       },
+	//       {
+	//         name:     'row'
+	//         val:       {},
+	//         methodExt: [ ... ],
+	//         propExt:   [
+	//           {
+	//             name:      'data'
+	//             val:       function () {},
+	//             methodExt: [ ... ],
+	//             propExt:   [ ... ]
+	//           },
+	//           ...
+	//         ]
+	//       }
+	//     ]
+	
+	_Api.register = _api_register = function ( name, val )
+	{
+		if ( $.isArray( name ) ) {
+			for ( var j=0, jen=name.length ; j<jen ; j++ ) {
+				_Api.register( name[j], val );
+			}
+			return;
+		}
+	
+		var
+			i, ien,
+			heir = name.split('.'),
+			struct = __apiStruct,
+			key, method;
+	
+		var find = function ( src, name ) {
+			for ( var i=0, ien=src.length ; i<ien ; i++ ) {
+				if ( src[i].name === name ) {
+					return src[i];
+				}
+			}
+			return null;
+		};
+	
+		for ( i=0, ien=heir.length ; i<ien ; i++ ) {
+			method = heir[i].indexOf('()') !== -1;
+			key = method ?
+				heir[i].replace('()', '') :
+				heir[i];
+	
+			var src = find( struct, key );
+			if ( ! src ) {
+				src = {
+					name:      key,
+					val:       {},
+					methodExt: [],
+					propExt:   []
+				};
+				struct.push( src );
+			}
+	
+			if ( i === ien-1 ) {
+				src.val = val;
+			}
+			else {
+				struct = method ?
+					src.methodExt :
+					src.propExt;
+			}
+		}
+	};
+	
+	
+	_Api.registerPlural = _api_registerPlural = function ( pluralName, singularName, val ) {
+		_Api.register( pluralName, val );
+	
+		_Api.register( singularName, function () {
+			var ret = val.apply( this, arguments );
+	
+			if ( ret === this ) {
+				// Returned item is the API instance that was passed in, return it
+				return this;
+			}
+			else if ( ret instanceof _Api ) {
+				// New API instance returned, want the value from the first item
+				// in the returned array for the singular result.
+				return ret.length ?
+					$.isArray( ret[0] ) ?
+						new _Api( ret.context, ret[0] ) : // Array results are 'enhanced'
+						ret[0] :
+					undefined;
+			}
+	
+			// Non-API return - just fire it back
+			return ret;
+		} );
+	};
+	
+	
+	/**
+	 * Selector for HTML tables. Apply the given selector to the give array of
+	 * DataTables settings objects.
+	 *
+	 * @param {string|integer} [selector] jQuery selector string or integer
+	 * @param  {array} Array of DataTables settings objects to be filtered
+	 * @return {array}
+	 * @ignore
+	 */
+	var __table_selector = function ( selector, a )
+	{
+		// Integer is used to pick out a table by index
+		if ( typeof selector === 'number' ) {
+			return [ a[ selector ] ];
+		}
+	
+		// Perform a jQuery selector on the table nodes
+		var nodes = $.map( a, function (el, i) {
+			return el.nTable;
+		} );
+	
+		return $(nodes)
+			.filter( selector )
+			.map( function (i) {
+				// Need to translate back from the table node to the settings
+				var idx = $.inArray( this, nodes );
+				return a[ idx ];
+			} )
+			.toArray();
+	};
+	
+	
+	
+	/**
+	 * Context selector for the API's context (i.e. the tables the API instance
+	 * refers to.
+	 *
+	 * @name    DataTable.Api#tables
+	 * @param {string|integer} [selector] Selector to pick which tables the iterator
+	 *   should operate on. If not given, all tables in the current context are
+	 *   used. This can be given as a jQuery selector (for example `':gt(0)'`) to
+	 *   select multiple tables or as an integer to select a single table.
+	 * @returns {DataTable.Api} Returns a new API instance if a selector is given.
+	 */
+	_api_register( 'tables()', function ( selector ) {
+		// A new instance is created if there was a selector specified
+		return selector ?
+			new _Api( __table_selector( selector, this.context ) ) :
+			this;
+	} );
+	
+	
+	_api_register( 'table()', function ( selector ) {
+		var tables = this.tables( selector );
+		var ctx = tables.context;
+	
+		// Truncate to the first matched table
+		return ctx.length ?
+			new _Api( ctx[0] ) :
+			tables;
+	} );
+	
+	
+	_api_registerPlural( 'tables().nodes()', 'table().node()' , function () {
+		return this.iterator( 'table', function ( ctx ) {
+			return ctx.nTable;
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'tables().body()', 'table().body()' , function () {
+		return this.iterator( 'table', function ( ctx ) {
+			return ctx.nTBody;
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'tables().header()', 'table().header()' , function () {
+		return this.iterator( 'table', function ( ctx ) {
+			return ctx.nTHead;
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'tables().footer()', 'table().footer()' , function () {
+		return this.iterator( 'table', function ( ctx ) {
+			return ctx.nTFoot;
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'tables().containers()', 'table().container()' , function () {
+		return this.iterator( 'table', function ( ctx ) {
+			return ctx.nTableWrapper;
+		}, 1 );
+	} );
+	
+	
+	
+	/**
+	 * Redraw the tables in the current context.
+	 */
+	_api_register( 'draw()', function ( paging ) {
+		return this.iterator( 'table', function ( settings ) {
+			if ( paging === 'page' ) {
+				_fnDraw( settings );
+			}
+			else {
+				if ( typeof paging === 'string' ) {
+					paging = paging === 'full-hold' ?
+						false :
+						true;
+				}
+	
+				_fnReDraw( settings, paging===false );
+			}
+		} );
+	} );
+	
+	
+	
+	/**
+	 * Get the current page index.
+	 *
+	 * @return {integer} Current page index (zero based)
+	 *//**
+	 * Set the current page.
+	 *
+	 * Note that if you attempt to show a page which does not exist, DataTables will
+	 * not throw an error, but rather reset the paging.
+	 *
+	 * @param {integer|string} action The paging action to take. This can be one of:
+	 *  * `integer` - The page index to jump to
+	 *  * `string` - An action to take:
+	 *    * `first` - Jump to first page.
+	 *    * `next` - Jump to the next page
+	 *    * `previous` - Jump to previous page
+	 *    * `last` - Jump to the last page.
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'page()', function ( action ) {
+		if ( action === undefined ) {
+			return this.page.info().page; // not an expensive call
+		}
+	
+		// else, have an action to take on all tables
+		return this.iterator( 'table', function ( settings ) {
+			_fnPageChange( settings, action );
+		} );
+	} );
+	
+	
+	/**
+	 * Paging information for the first table in the current context.
+	 *
+	 * If you require paging information for another table, use the `table()` method
+	 * with a suitable selector.
+	 *
+	 * @return {object} Object with the following properties set:
+	 *  * `page` - Current page index (zero based - i.e. the first page is `0`)
+	 *  * `pages` - Total number of pages
+	 *  * `start` - Display index for the first record shown on the current page
+	 *  * `end` - Display index for the last record shown on the current page
+	 *  * `length` - Display length (number of records). Note that generally `start
+	 *    + length = end`, but this is not always true, for example if there are
+	 *    only 2 records to show on the final page, with a length of 10.
+	 *  * `recordsTotal` - Full data set length
+	 *  * `recordsDisplay` - Data set length once the current filtering criterion
+	 *    are applied.
+	 */
+	_api_register( 'page.info()', function ( action ) {
+		if ( this.context.length === 0 ) {
+			return undefined;
+		}
+	
+		var
+			settings   = this.context[0],
+			start      = settings._iDisplayStart,
+			len        = settings.oFeatures.bPaginate ? settings._iDisplayLength : -1,
+			visRecords = settings.fnRecordsDisplay(),
+			all        = len === -1;
+	
+		return {
+			"page":           all ? 0 : Math.floor( start / len ),
+			"pages":          all ? 1 : Math.ceil( visRecords / len ),
+			"start":          start,
+			"end":            settings.fnDisplayEnd(),
+			"length":         len,
+			"recordsTotal":   settings.fnRecordsTotal(),
+			"recordsDisplay": visRecords,
+			"serverSide":     _fnDataSource( settings ) === 'ssp'
+		};
+	} );
+	
+	
+	/**
+	 * Get the current page length.
+	 *
+	 * @return {integer} Current page length. Note `-1` indicates that all records
+	 *   are to be shown.
+	 *//**
+	 * Set the current page length.
+	 *
+	 * @param {integer} Page length to set. Use `-1` to show all records.
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'page.len()', function ( len ) {
+		// Note that we can't call this function 'length()' because `length`
+		// is a Javascript property of functions which defines how many arguments
+		// the function expects.
+		if ( len === undefined ) {
+			return this.context.length !== 0 ?
+				this.context[0]._iDisplayLength :
+				undefined;
+		}
+	
+		// else, set the page length
+		return this.iterator( 'table', function ( settings ) {
+			_fnLengthChange( settings, len );
+		} );
+	} );
+	
+	
+	
+	var __reload = function ( settings, holdPosition, callback ) {
+		// Use the draw event to trigger a callback
+		if ( callback ) {
+			var api = new _Api( settings );
+	
+			api.one( 'draw', function () {
+				callback( api.ajax.json() );
+			} );
+		}
+	
+		if ( _fnDataSource( settings ) == 'ssp' ) {
+			_fnReDraw( settings, holdPosition );
+		}
+		else {
+			_fnProcessingDisplay( settings, true );
+	
+			// Cancel an existing request
+			var xhr = settings.jqXHR;
+			if ( xhr && xhr.readyState !== 4 ) {
+				xhr.abort();
+			}
+	
+			// Trigger xhr
+			_fnBuildAjax( settings, [], function( json ) {
+				_fnClearTable( settings );
+	
+				var data = _fnAjaxDataSrc( settings, json );
+				for ( var i=0, ien=data.length ; i<ien ; i++ ) {
+					_fnAddData( settings, data[i] );
+				}
+	
+				_fnReDraw( settings, holdPosition );
+				_fnProcessingDisplay( settings, false );
+			} );
+		}
+	};
+	
+	
+	/**
+	 * Get the JSON response from the last Ajax request that DataTables made to the
+	 * server. Note that this returns the JSON from the first table in the current
+	 * context.
+	 *
+	 * @return {object} JSON received from the server.
+	 */
+	_api_register( 'ajax.json()', function () {
+		var ctx = this.context;
+	
+		if ( ctx.length > 0 ) {
+			return ctx[0].json;
+		}
+	
+		// else return undefined;
+	} );
+	
+	
+	/**
+	 * Get the data submitted in the last Ajax request
+	 */
+	_api_register( 'ajax.params()', function () {
+		var ctx = this.context;
+	
+		if ( ctx.length > 0 ) {
+			return ctx[0].oAjaxData;
+		}
+	
+		// else return undefined;
+	} );
+	
+	
+	/**
+	 * Reload tables from the Ajax data source. Note that this function will
+	 * automatically re-draw the table when the remote data has been loaded.
+	 *
+	 * @param {boolean} [reset=true] Reset (default) or hold the current paging
+	 *   position. A full re-sort and re-filter is performed when this method is
+	 *   called, which is why the pagination reset is the default action.
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'ajax.reload()', function ( callback, resetPaging ) {
+		return this.iterator( 'table', function (settings) {
+			__reload( settings, resetPaging===false, callback );
+		} );
+	} );
+	
+	
+	/**
+	 * Get the current Ajax URL. Note that this returns the URL from the first
+	 * table in the current context.
+	 *
+	 * @return {string} Current Ajax source URL
+	 *//**
+	 * Set the Ajax URL. Note that this will set the URL for all tables in the
+	 * current context.
+	 *
+	 * @param {string} url URL to set.
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'ajax.url()', function ( url ) {
+		var ctx = this.context;
+	
+		if ( url === undefined ) {
+			// get
+			if ( ctx.length === 0 ) {
+				return undefined;
+			}
+			ctx = ctx[0];
+	
+			return ctx.ajax ?
+				$.isPlainObject( ctx.ajax ) ?
+					ctx.ajax.url :
+					ctx.ajax :
+				ctx.sAjaxSource;
+		}
+	
+		// set
+		return this.iterator( 'table', function ( settings ) {
+			if ( $.isPlainObject( settings.ajax ) ) {
+				settings.ajax.url = url;
+			}
+			else {
+				settings.ajax = url;
+			}
+			// No need to consider sAjaxSource here since DataTables gives priority
+			// to `ajax` over `sAjaxSource`. So setting `ajax` here, renders any
+			// value of `sAjaxSource` redundant.
+		} );
+	} );
+	
+	
+	/**
+	 * Load data from the newly set Ajax URL. Note that this method is only
+	 * available when `ajax.url()` is used to set a URL. Additionally, this method
+	 * has the same effect as calling `ajax.reload()` but is provided for
+	 * convenience when setting a new URL. Like `ajax.reload()` it will
+	 * automatically redraw the table once the remote data has been loaded.
+	 *
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'ajax.url().load()', function ( callback, resetPaging ) {
+		// Same as a reload, but makes sense to present it for easy access after a
+		// url change
+		return this.iterator( 'table', function ( ctx ) {
+			__reload( ctx, resetPaging===false, callback );
+		} );
+	} );
+	
+	
+	
+	
+	var _selector_run = function ( type, selector, selectFn, settings, opts )
+	{
+		var
+			out = [], res,
+			a, i, ien, j, jen,
+			selectorType = typeof selector;
+	
+		// Can't just check for isArray here, as an API or jQuery instance might be
+		// given with their array like look
+		if ( ! selector || selectorType === 'string' || selectorType === 'function' || selector.length === undefined ) {
+			selector = [ selector ];
+		}
+	
+		for ( i=0, ien=selector.length ; i<ien ; i++ ) {
+			// Only split on simple strings - complex expressions will be jQuery selectors
+			a = selector[i] && selector[i].split && ! selector[i].match(/[\[\(:]/) ?
+				selector[i].split(',') :
+				[ selector[i] ];
+	
+			for ( j=0, jen=a.length ; j<jen ; j++ ) {
+				res = selectFn( typeof a[j] === 'string' ? $.trim(a[j]) : a[j] );
+	
+				if ( res && res.length ) {
+					out = out.concat( res );
+				}
+			}
+		}
+	
+		// selector extensions
+		var ext = _ext.selector[ type ];
+		if ( ext.length ) {
+			for ( i=0, ien=ext.length ; i<ien ; i++ ) {
+				out = ext[i]( settings, opts, out );
+			}
+		}
+	
+		return _unique( out );
+	};
+	
+	
+	var _selector_opts = function ( opts )
+	{
+		if ( ! opts ) {
+			opts = {};
+		}
+	
+		// Backwards compatibility for 1.9- which used the terminology filter rather
+		// than search
+		if ( opts.filter && opts.search === undefined ) {
+			opts.search = opts.filter;
+		}
+	
+		return $.extend( {
+			search: 'none',
+			order: 'current',
+			page: 'all'
+		}, opts );
+	};
+	
+	
+	var _selector_first = function ( inst )
+	{
+		// Reduce the API instance to the first item found
+		for ( var i=0, ien=inst.length ; i<ien ; i++ ) {
+			if ( inst[i].length > 0 ) {
+				// Assign the first element to the first item in the instance
+				// and truncate the instance and context
+				inst[0] = inst[i];
+				inst[0].length = 1;
+				inst.length = 1;
+				inst.context = [ inst.context[i] ];
+	
+				return inst;
+			}
+		}
+	
+		// Not found - return an empty instance
+		inst.length = 0;
+		return inst;
+	};
+	
+	
+	var _selector_row_indexes = function ( settings, opts )
+	{
+		var
+			i, ien, tmp, a=[],
+			displayFiltered = settings.aiDisplay,
+			displayMaster = settings.aiDisplayMaster;
+	
+		var
+			search = opts.search,  // none, applied, removed
+			order  = opts.order,   // applied, current, index (original - compatibility with 1.9)
+			page   = opts.page;    // all, current
+	
+		if ( _fnDataSource( settings ) == 'ssp' ) {
+			// In server-side processing mode, most options are irrelevant since
+			// rows not shown don't exist and the index order is the applied order
+			// Removed is a special case - for consistency just return an empty
+			// array
+			return search === 'removed' ?
+				[] :
+				_range( 0, displayMaster.length );
+		}
+		else if ( page == 'current' ) {
+			// Current page implies that order=current and fitler=applied, since it is
+			// fairly senseless otherwise, regardless of what order and search actually
+			// are
+			for ( i=settings._iDisplayStart, ien=settings.fnDisplayEnd() ; i<ien ; i++ ) {
+				a.push( displayFiltered[i] );
+			}
+		}
+		else if ( order == 'current' || order == 'applied' ) {
+			a = search == 'none' ?
+				displayMaster.slice() :                      // no search
+				search == 'applied' ?
+					displayFiltered.slice() :                // applied search
+					$.map( displayMaster, function (el, i) { // removed search
+						return $.inArray( el, displayFiltered ) === -1 ? el : null;
+					} );
+		}
+		else if ( order == 'index' || order == 'original' ) {
+			for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
+				if ( search == 'none' ) {
+					a.push( i );
+				}
+				else { // applied | removed
+					tmp = $.inArray( i, displayFiltered );
+	
+					if ((tmp === -1 && search == 'removed') ||
+						(tmp >= 0   && search == 'applied') )
+					{
+						a.push( i );
+					}
+				}
+			}
+		}
+	
+		return a;
+	};
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Rows
+	 *
+	 * {}          - no selector - use all available rows
+	 * {integer}   - row aoData index
+	 * {node}      - TR node
+	 * {string}    - jQuery selector to apply to the TR elements
+	 * {array}     - jQuery array of nodes, or simply an array of TR nodes
+	 *
+	 */
+	
+	
+	var __row_selector = function ( settings, selector, opts )
+	{
+		var rows;
+		var run = function ( sel ) {
+			var selInt = _intVal( sel );
+			var i, ien;
+	
+			// Short cut - selector is a number and no options provided (default is
+			// all records, so no need to check if the index is in there, since it
+			// must be - dev error if the index doesn't exist).
+			if ( selInt !== null && ! opts ) {
+				return [ selInt ];
+			}
+	
+			if ( ! rows ) {
+				rows = _selector_row_indexes( settings, opts );
+			}
+	
+			if ( selInt !== null && $.inArray( selInt, rows ) !== -1 ) {
+				// Selector - integer
+				return [ selInt ];
+			}
+			else if ( sel === null || sel === undefined || sel === '' ) {
+				// Selector - none
+				return rows;
+			}
+	
+			// Selector - function
+			if ( typeof sel === 'function' ) {
+				return $.map( rows, function (idx) {
+					var row = settings.aoData[ idx ];
+					return sel( idx, row._aData, row.nTr ) ? idx : null;
+				} );
+			}
+	
+			// Get nodes in the order from the `rows` array with null values removed
+			var nodes = _removeEmpty(
+				_pluck_order( settings.aoData, rows, 'nTr' )
+			);
+	
+			// Selector - node
+			if ( sel.nodeName ) {
+				if ( sel._DT_RowIndex !== undefined ) {
+					return [ sel._DT_RowIndex ]; // Property added by DT for fast lookup
+				}
+				else if ( sel._DT_CellIndex ) {
+					return [ sel._DT_CellIndex.row ];
+				}
+				else {
+					var host = $(sel).closest('*[data-dt-row]');
+					return host.length ?
+						[ host.data('dt-row') ] :
+						[];
+				}
+			}
+	
+			// ID selector. Want to always be able to select rows by id, regardless
+			// of if the tr element has been created or not, so can't rely upon
+			// jQuery here - hence a custom implementation. This does not match
+			// Sizzle's fast selector or HTML4 - in HTML5 the ID can be anything,
+			// but to select it using a CSS selector engine (like Sizzle or
+			// querySelect) it would need to need to be escaped for some characters.
+			// DataTables simplifies this for row selectors since you can select
+			// only a row. A # indicates an id any anything that follows is the id -
+			// unescaped.
+			if ( typeof sel === 'string' && sel.charAt(0) === '#' ) {
+				// get row index from id
+				var rowObj = settings.aIds[ sel.replace( /^#/, '' ) ];
+				if ( rowObj !== undefined ) {
+					return [ rowObj.idx ];
+				}
+	
+				// need to fall through to jQuery in case there is DOM id that
+				// matches
+			}
+	
+			// Selector - jQuery selector string, array of nodes or jQuery object/
+			// As jQuery's .filter() allows jQuery objects to be passed in filter,
+			// it also allows arrays, so this will cope with all three options
+			return $(nodes)
+				.filter( sel )
+				.map( function () {
+					return this._DT_RowIndex;
+				} )
+				.toArray();
+		};
+	
+		return _selector_run( 'row', selector, run, settings, opts );
+	};
+	
+	
+	_api_register( 'rows()', function ( selector, opts ) {
+		// argument shifting
+		if ( selector === undefined ) {
+			selector = '';
+		}
+		else if ( $.isPlainObject( selector ) ) {
+			opts = selector;
+			selector = '';
+		}
+	
+		opts = _selector_opts( opts );
+	
+		var inst = this.iterator( 'table', function ( settings ) {
+			return __row_selector( settings, selector, opts );
+		}, 1 );
+	
+		// Want argument shifting here and in __row_selector?
+		inst.selector.rows = selector;
+		inst.selector.opts = opts;
+	
+		return inst;
+	} );
+	
+	_api_register( 'rows().nodes()', function () {
+		return this.iterator( 'row', function ( settings, row ) {
+			return settings.aoData[ row ].nTr || undefined;
+		}, 1 );
+	} );
+	
+	_api_register( 'rows().data()', function () {
+		return this.iterator( true, 'rows', function ( settings, rows ) {
+			return _pluck_order( settings.aoData, rows, '_aData' );
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'rows().cache()', 'row().cache()', function ( type ) {
+		return this.iterator( 'row', function ( settings, row ) {
+			var r = settings.aoData[ row ];
+			return type === 'search' ? r._aFilterData : r._aSortData;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'rows().invalidate()', 'row().invalidate()', function ( src ) {
+		return this.iterator( 'row', function ( settings, row ) {
+			_fnInvalidate( settings, row, src );
+		} );
+	} );
+	
+	_api_registerPlural( 'rows().indexes()', 'row().index()', function () {
+		return this.iterator( 'row', function ( settings, row ) {
+			return row;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'rows().ids()', 'row().id()', function ( hash ) {
+		var a = [];
+		var context = this.context;
+	
+		// `iterator` will drop undefined values, but in this case we want them
+		for ( var i=0, ien=context.length ; i<ien ; i++ ) {
+			for ( var j=0, jen=this[i].length ; j<jen ; j++ ) {
+				var id = context[i].rowIdFn( context[i].aoData[ this[i][j] ]._aData );
+				a.push( (hash === true ? '#' : '' )+ id );
+			}
+		}
+	
+		return new _Api( context, a );
+	} );
+	
+	_api_registerPlural( 'rows().remove()', 'row().remove()', function () {
+		var that = this;
+	
+		this.iterator( 'row', function ( settings, row, thatIdx ) {
+			var data = settings.aoData;
+			var rowData = data[ row ];
+			var i, ien, j, jen;
+			var loopRow, loopCells;
+	
+			data.splice( row, 1 );
+	
+			// Update the cached indexes
+			for ( i=0, ien=data.length ; i<ien ; i++ ) {
+				loopRow = data[i];
+				loopCells = loopRow.anCells;
+	
+				// Rows
+				if ( loopRow.nTr !== null ) {
+					loopRow.nTr._DT_RowIndex = i;
+				}
+	
+				// Cells
+				if ( loopCells !== null ) {
+					for ( j=0, jen=loopCells.length ; j<jen ; j++ ) {
+						loopCells[j]._DT_CellIndex.row = i;
+					}
+				}
+			}
+	
+			// Delete from the display arrays
+			_fnDeleteIndex( settings.aiDisplayMaster, row );
+			_fnDeleteIndex( settings.aiDisplay, row );
+			_fnDeleteIndex( that[ thatIdx ], row, false ); // maintain local indexes
+	
+			// Check for an 'overflow' they case for displaying the table
+			_fnLengthOverflow( settings );
+	
+			// Remove the row's ID reference if there is one
+			var id = settings.rowIdFn( rowData._aData );
+			if ( id !== undefined ) {
+				delete settings.aIds[ id ];
+			}
+		} );
+	
+		this.iterator( 'table', function ( settings ) {
+			for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) {
+				settings.aoData[i].idx = i;
+			}
+		} );
+	
+		return this;
+	} );
+	
+	
+	_api_register( 'rows.add()', function ( rows ) {
+		var newRows = this.iterator( 'table', function ( settings ) {
+				var row, i, ien;
+				var out = [];
+	
+				for ( i=0, ien=rows.length ; i<ien ; i++ ) {
+					row = rows[i];
+	
+					if ( row.nodeName && row.nodeName.toUpperCase() === 'TR' ) {
+						out.push( _fnAddTr( settings, row )[0] );
+					}
+					else {
+						out.push( _fnAddData( settings, row ) );
+					}
+				}
+	
+				return out;
+			}, 1 );
+	
+		// Return an Api.rows() extended instance, so rows().nodes() etc can be used
+		var modRows = this.rows( -1 );
+		modRows.pop();
+		$.merge( modRows, newRows );
+	
+		return modRows;
+	} );
+	
+	
+	
+	
+	
+	/**
+	 *
+	 */
+	_api_register( 'row()', function ( selector, opts ) {
+		return _selector_first( this.rows( selector, opts ) );
+	} );
+	
+	
+	_api_register( 'row().data()', function ( data ) {
+		var ctx = this.context;
+	
+		if ( data === undefined ) {
+			// Get
+			return ctx.length && this.length ?
+				ctx[0].aoData[ this[0] ]._aData :
+				undefined;
+		}
+	
+		// Set
+		ctx[0].aoData[ this[0] ]._aData = data;
+	
+		// Automatically invalidate
+		_fnInvalidate( ctx[0], this[0], 'data' );
+	
+		return this;
+	} );
+	
+	
+	_api_register( 'row().node()', function () {
+		var ctx = this.context;
+	
+		return ctx.length && this.length ?
+			ctx[0].aoData[ this[0] ].nTr || null :
+			null;
+	} );
+	
+	
+	_api_register( 'row.add()', function ( row ) {
+		// Allow a jQuery object to be passed in - only a single row is added from
+		// it though - the first element in the set
+		if ( row instanceof $ && row.length ) {
+			row = row[0];
+		}
+	
+		var rows = this.iterator( 'table', function ( settings ) {
+			if ( row.nodeName && row.nodeName.toUpperCase() === 'TR' ) {
+				return _fnAddTr( settings, row )[0];
+			}
+			return _fnAddData( settings, row );
+		} );
+	
+		// Return an Api.rows() extended instance, with the newly added row selected
+		return this.row( rows[0] );
+	} );
+	
+	
+	
+	var __details_add = function ( ctx, row, data, klass )
+	{
+		// Convert to array of TR elements
+		var rows = [];
+		var addRow = function ( r, k ) {
+			// Recursion to allow for arrays of jQuery objects
+			if ( $.isArray( r ) || r instanceof $ ) {
+				for ( var i=0, ien=r.length ; i<ien ; i++ ) {
+					addRow( r[i], k );
+				}
+				return;
+			}
+	
+			// If we get a TR element, then just add it directly - up to the dev
+			// to add the correct number of columns etc
+			if ( r.nodeName && r.nodeName.toLowerCase() === 'tr' ) {
+				rows.push( r );
+			}
+			else {
+				// Otherwise create a row with a wrapper
+				var created = $('<tr><td/></tr>').addClass( k );
+				$('td', created)
+					.addClass( k )
+					.html( r )
+					[0].colSpan = _fnVisbleColumns( ctx );
+	
+				rows.push( created[0] );
+			}
+		};
+	
+		addRow( data, klass );
+	
+		if ( row._details ) {
+			row._details.detach();
+		}
+	
+		row._details = $(rows);
+	
+		// If the children were already shown, that state should be retained
+		if ( row._detailsShow ) {
+			row._details.insertAfter( row.nTr );
+		}
+	};
+	
+	
+	var __details_remove = function ( api, idx )
+	{
+		var ctx = api.context;
+	
+		if ( ctx.length ) {
+			var row = ctx[0].aoData[ idx !== undefined ? idx : api[0] ];
+	
+			if ( row && row._details ) {
+				row._details.remove();
+	
+				row._detailsShow = undefined;
+				row._details = undefined;
+			}
+		}
+	};
+	
+	
+	var __details_display = function ( api, show ) {
+		var ctx = api.context;
+	
+		if ( ctx.length && api.length ) {
+			var row = ctx[0].aoData[ api[0] ];
+	
+			if ( row._details ) {
+				row._detailsShow = show;
+	
+				if ( show ) {
+					row._details.insertAfter( row.nTr );
+				}
+				else {
+					row._details.detach();
+				}
+	
+				__details_events( ctx[0] );
+			}
+		}
+	};
+	
+	
+	var __details_events = function ( settings )
+	{
+		var api = new _Api( settings );
+		var namespace = '.dt.DT_details';
+		var drawEvent = 'draw'+namespace;
+		var colvisEvent = 'column-visibility'+namespace;
+		var destroyEvent = 'destroy'+namespace;
+		var data = settings.aoData;
+	
+		api.off( drawEvent +' '+ colvisEvent +' '+ destroyEvent );
+	
+		if ( _pluck( data, '_details' ).length > 0 ) {
+			// On each draw, insert the required elements into the document
+			api.on( drawEvent, function ( e, ctx ) {
+				if ( settings !== ctx ) {
+					return;
+				}
+	
+				api.rows( {page:'current'} ).eq(0).each( function (idx) {
+					// Internal data grab
+					var row = data[ idx ];
+	
+					if ( row._detailsShow ) {
+						row._details.insertAfter( row.nTr );
+					}
+				} );
+			} );
+	
+			// Column visibility change - update the colspan
+			api.on( colvisEvent, function ( e, ctx, idx, vis ) {
+				if ( settings !== ctx ) {
+					return;
+				}
+	
+				// Update the colspan for the details rows (note, only if it already has
+				// a colspan)
+				var row, visible = _fnVisbleColumns( ctx );
+	
+				for ( var i=0, ien=data.length ; i<ien ; i++ ) {
+					row = data[i];
+	
+					if ( row._details ) {
+						row._details.children('td[colspan]').attr('colspan', visible );
+					}
+				}
+			} );
+	
+			// Table destroyed - nuke any child rows
+			api.on( destroyEvent, function ( e, ctx ) {
+				if ( settings !== ctx ) {
+					return;
+				}
+	
+				for ( var i=0, ien=data.length ; i<ien ; i++ ) {
+					if ( data[i]._details ) {
+						__details_remove( api, i );
+					}
+				}
+			} );
+		}
+	};
+	
+	// Strings for the method names to help minification
+	var _emp = '';
+	var _child_obj = _emp+'row().child';
+	var _child_mth = _child_obj+'()';
+	
+	// data can be:
+	//  tr
+	//  string
+	//  jQuery or array of any of the above
+	_api_register( _child_mth, function ( data, klass ) {
+		var ctx = this.context;
+	
+		if ( data === undefined ) {
+			// get
+			return ctx.length && this.length ?
+				ctx[0].aoData[ this[0] ]._details :
+				undefined;
+		}
+		else if ( data === true ) {
+			// show
+			this.child.show();
+		}
+		else if ( data === false ) {
+			// remove
+			__details_remove( this );
+		}
+		else if ( ctx.length && this.length ) {
+			// set
+			__details_add( ctx[0], ctx[0].aoData[ this[0] ], data, klass );
+		}
+	
+		return this;
+	} );
+	
+	
+	_api_register( [
+		_child_obj+'.show()',
+		_child_mth+'.show()' // only when `child()` was called with parameters (without
+	], function ( show ) {   // it returns an object and this method is not executed)
+		__details_display( this, true );
+		return this;
+	} );
+	
+	
+	_api_register( [
+		_child_obj+'.hide()',
+		_child_mth+'.hide()' // only when `child()` was called with parameters (without
+	], function () {         // it returns an object and this method is not executed)
+		__details_display( this, false );
+		return this;
+	} );
+	
+	
+	_api_register( [
+		_child_obj+'.remove()',
+		_child_mth+'.remove()' // only when `child()` was called with parameters (without
+	], function () {           // it returns an object and this method is not executed)
+		__details_remove( this );
+		return this;
+	} );
+	
+	
+	_api_register( _child_obj+'.isShown()', function () {
+		var ctx = this.context;
+	
+		if ( ctx.length && this.length ) {
+			// _detailsShown as false or undefined will fall through to return false
+			return ctx[0].aoData[ this[0] ]._detailsShow || false;
+		}
+		return false;
+	} );
+	
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Columns
+	 *
+	 * {integer}           - column index (>=0 count from left, <0 count from right)
+	 * "{integer}:visIdx"  - visible column index (i.e. translate to column index)  (>=0 count from left, <0 count from right)
+	 * "{integer}:visible" - alias for {integer}:visIdx  (>=0 count from left, <0 count from right)
+	 * "{string}:name"     - column name
+	 * "{string}"          - jQuery selector on column header nodes
+	 *
+	 */
+	
+	// can be an array of these items, comma separated list, or an array of comma
+	// separated lists
+	
+	var __re_column_selector = /^([^:]+):(name|visIdx|visible)$/;
+	
+	
+	// r1 and r2 are redundant - but it means that the parameters match for the
+	// iterator callback in columns().data()
+	var __columnData = function ( settings, column, r1, r2, rows ) {
+		var a = [];
+		for ( var row=0, ien=rows.length ; row<ien ; row++ ) {
+			a.push( _fnGetCellData( settings, rows[row], column ) );
+		}
+		return a;
+	};
+	
+	
+	var __column_selector = function ( settings, selector, opts )
+	{
+		var
+			columns = settings.aoColumns,
+			names = _pluck( columns, 'sName' ),
+			nodes = _pluck( columns, 'nTh' );
+	
+		var run = function ( s ) {
+			var selInt = _intVal( s );
+	
+			// Selector - all
+			if ( s === '' ) {
+				return _range( columns.length );
+			}
+	
+			// Selector - index
+			if ( selInt !== null ) {
+				return [ selInt >= 0 ?
+					selInt : // Count from left
+					columns.length + selInt // Count from right (+ because its a negative value)
+				];
+			}
+	
+			// Selector = function
+			if ( typeof s === 'function' ) {
+				var rows = _selector_row_indexes( settings, opts );
+	
+				return $.map( columns, function (col, idx) {
+					return s(
+							idx,
+							__columnData( settings, idx, 0, 0, rows ),
+							nodes[ idx ]
+						) ? idx : null;
+				} );
+			}
+	
+			// jQuery or string selector
+			var match = typeof s === 'string' ?
+				s.match( __re_column_selector ) :
+				'';
+	
+			if ( match ) {
+				switch( match[2] ) {
+					case 'visIdx':
+					case 'visible':
+						var idx = parseInt( match[1], 10 );
+						// Visible index given, convert to column index
+						if ( idx < 0 ) {
+							// Counting from the right
+							var visColumns = $.map( columns, function (col,i) {
+								return col.bVisible ? i : null;
+							} );
+							return [ visColumns[ visColumns.length + idx ] ];
+						}
+						// Counting from the left
+						return [ _fnVisibleToColumnIndex( settings, idx ) ];
+	
+					case 'name':
+						// match by name. `names` is column index complete and in order
+						return $.map( names, function (name, i) {
+							return name === match[1] ? i : null;
+						} );
+	
+					default:
+						return [];
+				}
+			}
+	
+			// Cell in the table body
+			if ( s.nodeName && s._DT_CellIndex ) {
+				return [ s._DT_CellIndex.column ];
+			}
+	
+			// jQuery selector on the TH elements for the columns
+			var jqResult = $( nodes )
+				.filter( s )
+				.map( function () {
+					return $.inArray( this, nodes ); // `nodes` is column index complete and in order
+				} )
+				.toArray();
+	
+			if ( jqResult.length || ! s.nodeName ) {
+				return jqResult;
+			}
+	
+			// Otherwise a node which might have a `dt-column` data attribute, or be
+			// a child or such an element
+			var host = $(s).closest('*[data-dt-column]');
+			return host.length ?
+				[ host.data('dt-column') ] :
+				[];
+		};
+	
+		return _selector_run( 'column', selector, run, settings, opts );
+	};
+	
+	
+	var __setColumnVis = function ( settings, column, vis ) {
+		var
+			cols = settings.aoColumns,
+			col  = cols[ column ],
+			data = settings.aoData,
+			row, cells, i, ien, tr;
+	
+		// Get
+		if ( vis === undefined ) {
+			return col.bVisible;
+		}
+	
+		// Set
+		// No change
+		if ( col.bVisible === vis ) {
+			return;
+		}
+	
+		if ( vis ) {
+			// Insert column
+			// Need to decide if we should use appendChild or insertBefore
+			var insertBefore = $.inArray( true, _pluck(cols, 'bVisible'), column+1 );
+	
+			for ( i=0, ien=data.length ; i<ien ; i++ ) {
+				tr = data[i].nTr;
+				cells = data[i].anCells;
+	
+				if ( tr ) {
+					// insertBefore can act like appendChild if 2nd arg is null
+					tr.insertBefore( cells[ column ], cells[ insertBefore ] || null );
+				}
+			}
+		}
+		else {
+			// Remove column
+			$( _pluck( settings.aoData, 'anCells', column ) ).detach();
+		}
+	
+		// Common actions
+		col.bVisible = vis;
+		_fnDrawHead( settings, settings.aoHeader );
+		_fnDrawHead( settings, settings.aoFooter );
+	
+		_fnSaveState( settings );
+	};
+	
+	
+	_api_register( 'columns()', function ( selector, opts ) {
+		// argument shifting
+		if ( selector === undefined ) {
+			selector = '';
+		}
+		else if ( $.isPlainObject( selector ) ) {
+			opts = selector;
+			selector = '';
+		}
+	
+		opts = _selector_opts( opts );
+	
+		var inst = this.iterator( 'table', function ( settings ) {
+			return __column_selector( settings, selector, opts );
+		}, 1 );
+	
+		// Want argument shifting here and in _row_selector?
+		inst.selector.cols = selector;
+		inst.selector.opts = opts;
+	
+		return inst;
+	} );
+	
+	_api_registerPlural( 'columns().header()', 'column().header()', function ( selector, opts ) {
+		return this.iterator( 'column', function ( settings, column ) {
+			return settings.aoColumns[column].nTh;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().footer()', 'column().footer()', function ( selector, opts ) {
+		return this.iterator( 'column', function ( settings, column ) {
+			return settings.aoColumns[column].nTf;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().data()', 'column().data()', function () {
+		return this.iterator( 'column-rows', __columnData, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().dataSrc()', 'column().dataSrc()', function () {
+		return this.iterator( 'column', function ( settings, column ) {
+			return settings.aoColumns[column].mData;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().cache()', 'column().cache()', function ( type ) {
+		return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {
+			return _pluck_order( settings.aoData, rows,
+				type === 'search' ? '_aFilterData' : '_aSortData', column
+			);
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().nodes()', 'column().nodes()', function () {
+		return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) {
+			return _pluck_order( settings.aoData, rows, 'anCells', column ) ;
+		}, 1 );
+	} );
+	
+	_api_registerPlural( 'columns().visible()', 'column().visible()', function ( vis, calc ) {
+		var ret = this.iterator( 'column', function ( settings, column ) {
+			if ( vis === undefined ) {
+				return settings.aoColumns[ column ].bVisible;
+			} // else
+			__setColumnVis( settings, column, vis );
+		} );
+	
+		// Group the column visibility changes
+		if ( vis !== undefined ) {
+			// Second loop once the first is done for events
+			this.iterator( 'column', function ( settings, column ) {
+				_fnCallbackFire( settings, null, 'column-visibility', [settings, column, vis, calc] );
+			} );
+	
+			if ( calc === undefined || calc ) {
+				this.columns.adjust();
+			}
+		}
+	
+		return ret;
+	} );
+	
+	_api_registerPlural( 'columns().indexes()', 'column().index()', function ( type ) {
+		return this.iterator( 'column', function ( settings, column ) {
+			return type === 'visible' ?
+				_fnColumnIndexToVisible( settings, column ) :
+				column;
+		}, 1 );
+	} );
+	
+	_api_register( 'columns.adjust()', function () {
+		return this.iterator( 'table', function ( settings ) {
+			_fnAdjustColumnSizing( settings );
+		}, 1 );
+	} );
+	
+	_api_register( 'column.index()', function ( type, idx ) {
+		if ( this.context.length !== 0 ) {
+			var ctx = this.context[0];
+	
+			if ( type === 'fromVisible' || type === 'toData' ) {
+				return _fnVisibleToColumnIndex( ctx, idx );
+			}
+			else if ( type === 'fromData' || type === 'toVisible' ) {
+				return _fnColumnIndexToVisible( ctx, idx );
+			}
+		}
+	} );
+	
+	_api_register( 'column()', function ( selector, opts ) {
+		return _selector_first( this.columns( selector, opts ) );
+	} );
+	
+	
+	
+	var __cell_selector = function ( settings, selector, opts )
+	{
+		var data = settings.aoData;
+		var rows = _selector_row_indexes( settings, opts );
+		var cells = _removeEmpty( _pluck_order( data, rows, 'anCells' ) );
+		var allCells = $( [].concat.apply([], cells) );
+		var row;
+		var columns = settings.aoColumns.length;
+		var a, i, ien, j, o, host;
+	
+		var run = function ( s ) {
+			var fnSelector = typeof s === 'function';
+	
+			if ( s === null || s === undefined || fnSelector ) {
+				// All cells and function selectors
+				a = [];
+	
+				for ( i=0, ien=rows.length ; i<ien ; i++ ) {
+					row = rows[i];
+	
+					for ( j=0 ; j<columns ; j++ ) {
+						o = {
+							row: row,
+							column: j
+						};
+	
+						if ( fnSelector ) {
+							// Selector - function
+							host = data[ row ];
+	
+							if ( s( o, _fnGetCellData(settings, row, j), host.anCells ? host.anCells[j] : null ) ) {
+								a.push( o );
+							}
+						}
+						else {
+							// Selector - all
+							a.push( o );
+						}
+					}
+				}
+	
+				return a;
+			}
+			
+			// Selector - index
+			if ( $.isPlainObject( s ) ) {
+				return [s];
+			}
+	
+			// Selector - jQuery filtered cells
+			var jqResult = allCells
+				.filter( s )
+				.map( function (i, el) {
+					return { // use a new object, in case someone changes the values
+						row:    el._DT_CellIndex.row,
+						column: el._DT_CellIndex.column
+	 				};
+				} )
+				.toArray();
+	
+			if ( jqResult.length || ! s.nodeName ) {
+				return jqResult;
+			}
+	
+			// Otherwise the selector is a node, and there is one last option - the
+			// element might be a child of an element which has dt-row and dt-column
+			// data attributes
+			host = $(s).closest('*[data-dt-row]');
+			return host.length ?
+				[ {
+					row: host.data('dt-row'),
+					column: host.data('dt-column')
+				} ] :
+				[];
+		};
+	
+		return _selector_run( 'cell', selector, run, settings, opts );
+	};
+	
+	
+	
+	
+	_api_register( 'cells()', function ( rowSelector, columnSelector, opts ) {
+		// Argument shifting
+		if ( $.isPlainObject( rowSelector ) ) {
+			// Indexes
+			if ( rowSelector.row === undefined ) {
+				// Selector options in first parameter
+				opts = rowSelector;
+				rowSelector = null;
+			}
+			else {
+				// Cell index objects in first parameter
+				opts = columnSelector;
+				columnSelector = null;
+			}
+		}
+		if ( $.isPlainObject( columnSelector ) ) {
+			opts = columnSelector;
+			columnSelector = null;
+		}
+	
+		// Cell selector
+		if ( columnSelector === null || columnSelector === undefined ) {
+			return this.iterator( 'table', function ( settings ) {
+				return __cell_selector( settings, rowSelector, _selector_opts( opts ) );
+			} );
+		}
+	
+		// Row + column selector
+		var columns = this.columns( columnSelector, opts );
+		var rows = this.rows( rowSelector, opts );
+		var a, i, ien, j, jen;
+	
+		var cells = this.iterator( 'table', function ( settings, idx ) {
+			a = [];
+	
+			for ( i=0, ien=rows[idx].length ; i<ien ; i++ ) {
+				for ( j=0, jen=columns[idx].length ; j<jen ; j++ ) {
+					a.push( {
+						row:    rows[idx][i],
+						column: columns[idx][j]
+					} );
+				}
+			}
+	
+			return a;
+		}, 1 );
+	
+		$.extend( cells.selector, {
+			cols: columnSelector,
+			rows: rowSelector,
+			opts: opts
+		} );
+	
+		return cells;
+	} );
+	
+	
+	_api_registerPlural( 'cells().nodes()', 'cell().node()', function () {
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			var data = settings.aoData[ row ];
+	
+			return data && data.anCells ?
+				data.anCells[ column ] :
+				undefined;
+		}, 1 );
+	} );
+	
+	
+	_api_register( 'cells().data()', function () {
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			return _fnGetCellData( settings, row, column );
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'cells().cache()', 'cell().cache()', function ( type ) {
+		type = type === 'search' ? '_aFilterData' : '_aSortData';
+	
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			return settings.aoData[ row ][ type ][ column ];
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'cells().render()', 'cell().render()', function ( type ) {
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			return _fnGetCellData( settings, row, column, type );
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'cells().indexes()', 'cell().index()', function () {
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			return {
+				row: row,
+				column: column,
+				columnVisible: _fnColumnIndexToVisible( settings, column )
+			};
+		}, 1 );
+	} );
+	
+	
+	_api_registerPlural( 'cells().invalidate()', 'cell().invalidate()', function ( src ) {
+		return this.iterator( 'cell', function ( settings, row, column ) {
+			_fnInvalidate( settings, row, src, column );
+		} );
+	} );
+	
+	
+	
+	_api_register( 'cell()', function ( rowSelector, columnSelector, opts ) {
+		return _selector_first( this.cells( rowSelector, columnSelector, opts ) );
+	} );
+	
+	
+	_api_register( 'cell().data()', function ( data ) {
+		var ctx = this.context;
+		var cell = this[0];
+	
+		if ( data === undefined ) {
+			// Get
+			return ctx.length && cell.length ?
+				_fnGetCellData( ctx[0], cell[0].row, cell[0].column ) :
+				undefined;
+		}
+	
+		// Set
+		_fnSetCellData( ctx[0], cell[0].row, cell[0].column, data );
+		_fnInvalidate( ctx[0], cell[0].row, 'data', cell[0].column );
+	
+		return this;
+	} );
+	
+	
+	
+	/**
+	 * Get current ordering (sorting) that has been applied to the table.
+	 *
+	 * @returns {array} 2D array containing the sorting information for the first
+	 *   table in the current context. Each element in the parent array represents
+	 *   a column being sorted upon (i.e. multi-sorting with two columns would have
+	 *   2 inner arrays). The inner arrays may have 2 or 3 elements. The first is
+	 *   the column index that the sorting condition applies to, the second is the
+	 *   direction of the sort (`desc` or `asc`) and, optionally, the third is the
+	 *   index of the sorting order from the `column.sorting` initialisation array.
+	 *//**
+	 * Set the ordering for the table.
+	 *
+	 * @param {integer} order Column index to sort upon.
+	 * @param {string} direction Direction of the sort to be applied (`asc` or `desc`)
+	 * @returns {DataTables.Api} this
+	 *//**
+	 * Set the ordering for the table.
+	 *
+	 * @param {array} order 1D array of sorting information to be applied.
+	 * @param {array} [...] Optional additional sorting conditions
+	 * @returns {DataTables.Api} this
+	 *//**
+	 * Set the ordering for the table.
+	 *
+	 * @param {array} order 2D array of sorting information to be applied.
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'order()', function ( order, dir ) {
+		var ctx = this.context;
+	
+		if ( order === undefined ) {
+			// get
+			return ctx.length !== 0 ?
+				ctx[0].aaSorting :
+				undefined;
+		}
+	
+		// set
+		if ( typeof order === 'number' ) {
+			// Simple column / direction passed in
+			order = [ [ order, dir ] ];
+		}
+		else if ( order.length && ! $.isArray( order[0] ) ) {
+			// Arguments passed in (list of 1D arrays)
+			order = Array.prototype.slice.call( arguments );
+		}
+		// otherwise a 2D array was passed in
+	
+		return this.iterator( 'table', function ( settings ) {
+			settings.aaSorting = order.slice();
+		} );
+	} );
+	
+	
+	/**
+	 * Attach a sort listener to an element for a given column
+	 *
+	 * @param {node|jQuery|string} node Identifier for the element(s) to attach the
+	 *   listener to. This can take the form of a single DOM node, a jQuery
+	 *   collection of nodes or a jQuery selector which will identify the node(s).
+	 * @param {integer} column the column that a click on this node will sort on
+	 * @param {function} [callback] callback function when sort is run
+	 * @returns {DataTables.Api} this
+	 */
+	_api_register( 'order.listener()', function ( node, column, callback ) {
+		return this.iterator( 'table', function ( settings ) {
+			_fnSortAttachListener( settings, node, column, callback );
+		} );
+	} );
+	
+	
+	_api_register( 'order.fixed()', function ( set ) {
+		if ( ! set ) {
+			var ctx = this.context;
+			var fixed = ctx.length ?
+				ctx[0].aaSortingFixed :
+				undefined;
+	
+			return $.isArray( fixed ) ?
+				{ pre: fixed } :
+				fixed;
+		}
+	
+		return this.iterator( 'table', function ( settings ) {
+			settings.aaSortingFixed = $.extend( true, {}, set );
+		} );
+	} );
+	
+	
+	// Order by the selected column(s)
+	_api_register( [
+		'columns().order()',
+		'column().order()'
+	], function ( dir ) {
+		var that = this;
+	
+		return this.iterator( 'table', function ( settings, i ) {
+			var sort = [];
+	
+			$.each( that[i], function (j, col) {
+				sort.push( [ col, dir ] );
+			} );
+	
+			settings.aaSorting = sort;
+		} );
+	} );
+	
+	
+	
+	_api_register( 'search()', function ( input, regex, smart, caseInsen ) {
+		var ctx = this.context;
+	
+		if ( input === undefined ) {
+			// get
+			return ctx.length !== 0 ?
+				ctx[0].oPreviousSearch.sSearch :
+				undefined;
+		}
+	
+		// set
+		return this.iterator( 'table', function ( settings ) {
+			if ( ! settings.oFeatures.bFilter ) {
+				return;
+			}
+	
+			_fnFilterComplete( settings, $.extend( {}, settings.oPreviousSearch, {
+				"sSearch": input+"",
+				"bRegex":  regex === null ? false : regex,
+				"bSmart":  smart === null ? true  : smart,
+				"bCaseInsensitive": caseInsen === null ? true : caseInsen
+			} ), 1 );
+		} );
+	} );
+	
+	
+	_api_registerPlural(
+		'columns().search()',
+		'column().search()',
+		function ( input, regex, smart, caseInsen ) {
+			return this.iterator( 'column', function ( settings, column ) {
+				var preSearch = settings.aoPreSearchCols;
+	
+				if ( input === undefined ) {
+					// get
+					return preSearch[ column ].sSearch;
+				}
+	
+				// set
+				if ( ! settings.oFeatures.bFilter ) {
+					return;
+				}
+	
+				$.extend( preSearch[ column ], {
+					"sSearch": input+"",
+					"bRegex":  regex === null ? false : regex,
+					"bSmart":  smart === null ? true  : smart,
+					"bCaseInsensitive": caseInsen === null ? true : caseInsen
+				} );
+	
+				_fnFilterComplete( settings, settings.oPreviousSearch, 1 );
+			} );
+		}
+	);
+	
+	/*
+	 * State API methods
+	 */
+	
+	_api_register( 'state()', function () {
+		return this.context.length ?
+			this.context[0].oSavedState :
+			null;
+	} );
+	
+	
+	_api_register( 'state.clear()', function () {
+		return this.iterator( 'table', function ( settings ) {
+			// Save an empty object
+			settings.fnStateSaveCallback.call( settings.oInstance, settings, {} );
+		} );
+	} );
+	
+	
+	_api_register( 'state.loaded()', function () {
+		return this.context.length ?
+			this.context[0].oLoadedState :
+			null;
+	} );
+	
+	
+	_api_register( 'state.save()', function () {
+		return this.iterator( 'table', function ( settings ) {
+			_fnSaveState( settings );
+		} );
+	} );
+	
+	
+	
+	/**
+	 * Provide a common method for plug-ins to check the version of DataTables being
+	 * used, in order to ensure compatibility.
+	 *
+	 *  @param {string} version Version string to check for, in the format "X.Y.Z".
+	 *    Note that the formats "X" and "X.Y" are also acceptable.
+	 *  @returns {boolean} true if this version of DataTables is greater or equal to
+	 *    the required version, or false if this version of DataTales is not
+	 *    suitable
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    alert( $.fn.dataTable.versionCheck( '1.9.0' ) );
+	 */
+	DataTable.versionCheck = DataTable.fnVersionCheck = function( version )
+	{
+		var aThis = DataTable.version.split('.');
+		var aThat = version.split('.');
+		var iThis, iThat;
+	
+		for ( var i=0, iLen=aThat.length ; i<iLen ; i++ ) {
+			iThis = parseInt( aThis[i], 10 ) || 0;
+			iThat = parseInt( aThat[i], 10 ) || 0;
+	
+			// Parts are the same, keep comparing
+			if (iThis === iThat) {
+				continue;
+			}
+	
+			// Parts are different, return immediately
+			return iThis > iThat;
+		}
+	
+		return true;
+	};
+	
+	
+	/**
+	 * Check if a `<table>` node is a DataTable table already or not.
+	 *
+	 *  @param {node|jquery|string} table Table node, jQuery object or jQuery
+	 *      selector for the table to test. Note that if more than more than one
+	 *      table is passed on, only the first will be checked
+	 *  @returns {boolean} true the table given is a DataTable, or false otherwise
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    if ( ! $.fn.DataTable.isDataTable( '#example' ) ) {
+	 *      $('#example').dataTable();
+	 *    }
+	 */
+	DataTable.isDataTable = DataTable.fnIsDataTable = function ( table )
+	{
+		var t = $(table).get(0);
+		var is = false;
+	
+		if ( table instanceof DataTable.Api ) {
+			return true;
+		}
+	
+		$.each( DataTable.settings, function (i, o) {
+			var head = o.nScrollHead ? $('table', o.nScrollHead)[0] : null;
+			var foot = o.nScrollFoot ? $('table', o.nScrollFoot)[0] : null;
+	
+			if ( o.nTable === t || head === t || foot === t ) {
+				is = true;
+			}
+		} );
+	
+		return is;
+	};
+	
+	
+	/**
+	 * Get all DataTable tables that have been initialised - optionally you can
+	 * select to get only currently visible tables.
+	 *
+	 *  @param {boolean} [visible=false] Flag to indicate if you want all (default)
+	 *    or visible tables only.
+	 *  @returns {array} Array of `table` nodes (not DataTable instances) which are
+	 *    DataTables
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    $.each( $.fn.dataTable.tables(true), function () {
+	 *      $(table).DataTable().columns.adjust();
+	 *    } );
+	 */
+	DataTable.tables = DataTable.fnTables = function ( visible )
+	{
+		var api = false;
+	
+		if ( $.isPlainObject( visible ) ) {
+			api = visible.api;
+			visible = visible.visible;
+		}
+	
+		var a = $.map( DataTable.settings, function (o) {
+			if ( !visible || (visible && $(o.nTable).is(':visible')) ) {
+				return o.nTable;
+			}
+		} );
+	
+		return api ?
+			new _Api( a ) :
+			a;
+	};
+	
+	
+	/**
+	 * Convert from camel case parameters to Hungarian notation. This is made public
+	 * for the extensions to provide the same ability as DataTables core to accept
+	 * either the 1.9 style Hungarian notation, or the 1.10+ style camelCase
+	 * parameters.
+	 *
+	 *  @param {object} src The model object which holds all parameters that can be
+	 *    mapped.
+	 *  @param {object} user The object to convert from camel case to Hungarian.
+	 *  @param {boolean} force When set to `true`, properties which already have a
+	 *    Hungarian value in the `user` object will be overwritten. Otherwise they
+	 *    won't be.
+	 */
+	DataTable.camelToHungarian = _fnCamelToHungarian;
+	
+	
+	
+	/**
+	 *
+	 */
+	_api_register( '$()', function ( selector, opts ) {
+		var
+			rows   = this.rows( opts ).nodes(), // Get all rows
+			jqRows = $(rows);
+	
+		return $( [].concat(
+			jqRows.filter( selector ).toArray(),
+			jqRows.find( selector ).toArray()
+		) );
+	} );
+	
+	
+	// jQuery functions to operate on the tables
+	$.each( [ 'on', 'one', 'off' ], function (i, key) {
+		_api_register( key+'()', function ( /* event, handler */ ) {
+			var args = Array.prototype.slice.call(arguments);
+	
+			// Add the `dt` namespace automatically if it isn't already present
+			args[0] = $.map( args[0].split( /\s/ ), function ( e ) {
+				return ! e.match(/\.dt\b/) ?
+					e+'.dt' :
+					e;
+				} ).join( ' ' );
+	
+			var inst = $( this.tables().nodes() );
+			inst[key].apply( inst, args );
+			return this;
+		} );
+	} );
+	
+	
+	_api_register( 'clear()', function () {
+		return this.iterator( 'table', function ( settings ) {
+			_fnClearTable( settings );
+		} );
+	} );
+	
+	
+	_api_register( 'settings()', function () {
+		return new _Api( this.context, this.context );
+	} );
+	
+	
+	_api_register( 'init()', function () {
+		var ctx = this.context;
+		return ctx.length ? ctx[0].oInit : null;
+	} );
+	
+	
+	_api_register( 'data()', function () {
+		return this.iterator( 'table', function ( settings ) {
+			return _pluck( settings.aoData, '_aData' );
+		} ).flatten();
+	} );
+	
+	
+	_api_register( 'destroy()', function ( remove ) {
+		remove = remove || false;
+	
+		return this.iterator( 'table', function ( settings ) {
+			var orig      = settings.nTableWrapper.parentNode;
+			var classes   = settings.oClasses;
+			var table     = settings.nTable;
+			var tbody     = settings.nTBody;
+			var thead     = settings.nTHead;
+			var tfoot     = settings.nTFoot;
+			var jqTable   = $(table);
+			var jqTbody   = $(tbody);
+			var jqWrapper = $(settings.nTableWrapper);
+			var rows      = $.map( settings.aoData, function (r) { return r.nTr; } );
+			var i, ien;
+	
+			// Flag to note that the table is currently being destroyed - no action
+			// should be taken
+			settings.bDestroying = true;
+	
+			// Fire off the destroy callbacks for plug-ins etc
+			_fnCallbackFire( settings, "aoDestroyCallback", "destroy", [settings] );
+	
+			// If not being removed from the document, make all columns visible
+			if ( ! remove ) {
+				new _Api( settings ).columns().visible( true );
+			}
+	
+			// Blitz all `DT` namespaced events (these are internal events, the
+			// lowercase, `dt` events are user subscribed and they are responsible
+			// for removing them
+			jqWrapper.off('.DT').find(':not(tbody *)').off('.DT');
+			$(window).off('.DT-'+settings.sInstance);
+	
+			// When scrolling we had to break the table up - restore it
+			if ( table != thead.parentNode ) {
+				jqTable.children('thead').detach();
+				jqTable.append( thead );
+			}
+	
+			if ( tfoot && table != tfoot.parentNode ) {
+				jqTable.children('tfoot').detach();
+				jqTable.append( tfoot );
+			}
+	
+			settings.aaSorting = [];
+			settings.aaSortingFixed = [];
+			_fnSortingClasses( settings );
+	
+			$( rows ).removeClass( settings.asStripeClasses.join(' ') );
+	
+			$('th, td', thead).removeClass( classes.sSortable+' '+
+				classes.sSortableAsc+' '+classes.sSortableDesc+' '+classes.sSortableNone
+			);
+	
+			if ( settings.bJUI ) {
+				$('th span.'+classes.sSortIcon+ ', td span.'+classes.sSortIcon, thead).detach();
+				$('th, td', thead).each( function () {
+					var wrapper = $('div.'+classes.sSortJUIWrapper, this);
+					$(this).append( wrapper.contents() );
+					wrapper.detach();
+				} );
+			}
+	
+			// Add the TR elements back into the table in their original order
+			jqTbody.children().detach();
+			jqTbody.append( rows );
+	
+			// Remove the DataTables generated nodes, events and classes
+			var removedMethod = remove ? 'remove' : 'detach';
+			jqTable[ removedMethod ]();
+			jqWrapper[ removedMethod ]();
+	
+			// If we need to reattach the table to the document
+			if ( ! remove && orig ) {
+				// insertBefore acts like appendChild if !arg[1]
+				orig.insertBefore( table, settings.nTableReinsertBefore );
+	
+				// Restore the width of the original table - was read from the style property,
+				// so we can restore directly to that
+				jqTable
+					.css( 'width', settings.sDestroyWidth )
+					.removeClass( classes.sTable );
+	
+				// If the were originally stripe classes - then we add them back here.
+				// Note this is not fool proof (for example if not all rows had stripe
+				// classes - but it's a good effort without getting carried away
+				ien = settings.asDestroyStripes.length;
+	
+				if ( ien ) {
+					jqTbody.children().each( function (i) {
+						$(this).addClass( settings.asDestroyStripes[i % ien] );
+					} );
+				}
+			}
+	
+			/* Remove the settings object from the settings array */
+			var idx = $.inArray( settings, DataTable.settings );
+			if ( idx !== -1 ) {
+				DataTable.settings.splice( idx, 1 );
+			}
+		} );
+	} );
+	
+	
+	// Add the `every()` method for rows, columns and cells in a compact form
+	$.each( [ 'column', 'row', 'cell' ], function ( i, type ) {
+		_api_register( type+'s().every()', function ( fn ) {
+			var opts = this.selector.opts;
+			var api = this;
+	
+			return this.iterator( type, function ( settings, arg1, arg2, arg3, arg4 ) {
+				// Rows and columns:
+				//  arg1 - index
+				//  arg2 - table counter
+				//  arg3 - loop counter
+				//  arg4 - undefined
+				// Cells:
+				//  arg1 - row index
+				//  arg2 - column index
+				//  arg3 - table counter
+				//  arg4 - loop counter
+				fn.call(
+					api[ type ](
+						arg1,
+						type==='cell' ? arg2 : opts,
+						type==='cell' ? opts : undefined
+					),
+					arg1, arg2, arg3, arg4
+				);
+			} );
+		} );
+	} );
+	
+	
+	// i18n method for extensions to be able to use the language object from the
+	// DataTable
+	_api_register( 'i18n()', function ( token, def, plural ) {
+		var ctx = this.context[0];
+		var resolved = _fnGetObjectDataFn( token )( ctx.oLanguage );
+	
+		if ( resolved === undefined ) {
+			resolved = def;
+		}
+	
+		if ( plural !== undefined && $.isPlainObject( resolved ) ) {
+			resolved = resolved[ plural ] !== undefined ?
+				resolved[ plural ] :
+				resolved._;
+		}
+	
+		return resolved.replace( '%d', plural ); // nb: plural might be undefined,
+	} );
+
+	/**
+	 * Version string for plug-ins to check compatibility. Allowed format is
+	 * `a.b.c-d` where: a:int, b:int, c:int, d:string(dev|beta|alpha). `d` is used
+	 * only for non-release builds. See http://semver.org/ for more information.
+	 *  @member
+	 *  @type string
+	 *  @default Version number
+	 */
+	DataTable.version = "1.10.15";
+
+	/**
+	 * Private data store, containing all of the settings objects that are
+	 * created for the tables on a given page.
+	 *
+	 * Note that the `DataTable.settings` object is aliased to
+	 * `jQuery.fn.dataTableExt` through which it may be accessed and
+	 * manipulated, or `jQuery.fn.dataTable.settings`.
+	 *  @member
+	 *  @type array
+	 *  @default []
+	 *  @private
+	 */
+	DataTable.settings = [];
+
+	/**
+	 * Object models container, for the various models that DataTables has
+	 * available to it. These models define the objects that are used to hold
+	 * the active state and configuration of the table.
+	 *  @namespace
+	 */
+	DataTable.models = {};
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * search information for the global filter and individual column filters.
+	 *  @namespace
+	 */
+	DataTable.models.oSearch = {
+		/**
+		 * Flag to indicate if the filtering should be case insensitive or not
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bCaseInsensitive": true,
+	
+		/**
+		 * Applied search term
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sSearch": "",
+	
+		/**
+		 * Flag to indicate if the search term should be interpreted as a
+		 * regular expression (true) or not (false) and therefore and special
+		 * regex characters escaped.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bRegex": false,
+	
+		/**
+		 * Flag to indicate if DataTables is to use its smart filtering or not.
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bSmart": true
+	};
+	
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * each individual row. This is the object format used for the settings
+	 * aoData array.
+	 *  @namespace
+	 */
+	DataTable.models.oRow = {
+		/**
+		 * TR element for the row
+		 *  @type node
+		 *  @default null
+		 */
+		"nTr": null,
+	
+		/**
+		 * Array of TD elements for each row. This is null until the row has been
+		 * created.
+		 *  @type array nodes
+		 *  @default []
+		 */
+		"anCells": null,
+	
+		/**
+		 * Data object from the original data source for the row. This is either
+		 * an array if using the traditional form of DataTables, or an object if
+		 * using mData options. The exact type will depend on the passed in
+		 * data from the data source, or will be an array if using DOM a data
+		 * source.
+		 *  @type array|object
+		 *  @default []
+		 */
+		"_aData": [],
+	
+		/**
+		 * Sorting data cache - this array is ostensibly the same length as the
+		 * number of columns (although each index is generated only as it is
+		 * needed), and holds the data that is used for sorting each column in the
+		 * row. We do this cache generation at the start of the sort in order that
+		 * the formatting of the sort data need be done only once for each cell
+		 * per sort. This array should not be read from or written to by anything
+		 * other than the master sorting methods.
+		 *  @type array
+		 *  @default null
+		 *  @private
+		 */
+		"_aSortData": null,
+	
+		/**
+		 * Per cell filtering data cache. As per the sort data cache, used to
+		 * increase the performance of the filtering in DataTables
+		 *  @type array
+		 *  @default null
+		 *  @private
+		 */
+		"_aFilterData": null,
+	
+		/**
+		 * Filtering data cache. This is the same as the cell filtering cache, but
+		 * in this case a string rather than an array. This is easily computed with
+		 * a join on `_aFilterData`, but is provided as a cache so the join isn't
+		 * needed on every search (memory traded for performance)
+		 *  @type array
+		 *  @default null
+		 *  @private
+		 */
+		"_sFilterRow": null,
+	
+		/**
+		 * Cache of the class name that DataTables has applied to the row, so we
+		 * can quickly look at this variable rather than needing to do a DOM check
+		 * on className for the nTr property.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @private
+		 */
+		"_sRowStripe": "",
+	
+		/**
+		 * Denote if the original data source was from the DOM, or the data source
+		 * object. This is used for invalidating data, so DataTables can
+		 * automatically read data from the original source, unless uninstructed
+		 * otherwise.
+		 *  @type string
+		 *  @default null
+		 *  @private
+		 */
+		"src": null,
+	
+		/**
+		 * Index in the aoData array. This saves an indexOf lookup when we have the
+		 * object, but want to know the index
+		 *  @type integer
+		 *  @default -1
+		 *  @private
+		 */
+		"idx": -1
+	};
+	
+	
+	/**
+	 * Template object for the column information object in DataTables. This object
+	 * is held in the settings aoColumns array and contains all the information that
+	 * DataTables needs about each individual column.
+	 *
+	 * Note that this object is related to {@link DataTable.defaults.column}
+	 * but this one is the internal data store for DataTables's cache of columns.
+	 * It should NOT be manipulated outside of DataTables. Any configuration should
+	 * be done through the initialisation options.
+	 *  @namespace
+	 */
+	DataTable.models.oColumn = {
+		/**
+		 * Column index. This could be worked out on-the-fly with $.inArray, but it
+		 * is faster to just hold it as a variable
+		 *  @type integer
+		 *  @default null
+		 */
+		"idx": null,
+	
+		/**
+		 * A list of the columns that sorting should occur on when this column
+		 * is sorted. That this property is an array allows multi-column sorting
+		 * to be defined for a column (for example first name / last name columns
+		 * would benefit from this). The values are integers pointing to the
+		 * columns to be sorted on (typically it will be a single integer pointing
+		 * at itself, but that doesn't need to be the case).
+		 *  @type array
+		 */
+		"aDataSort": null,
+	
+		/**
+		 * Define the sorting directions that are applied to the column, in sequence
+		 * as the column is repeatedly sorted upon - i.e. the first value is used
+		 * as the sorting direction when the column if first sorted (clicked on).
+		 * Sort it again (click again) and it will move on to the next index.
+		 * Repeat until loop.
+		 *  @type array
+		 */
+		"asSorting": null,
+	
+		/**
+		 * Flag to indicate if the column is searchable, and thus should be included
+		 * in the filtering or not.
+		 *  @type boolean
+		 */
+		"bSearchable": null,
+	
+		/**
+		 * Flag to indicate if the column is sortable or not.
+		 *  @type boolean
+		 */
+		"bSortable": null,
+	
+		/**
+		 * Flag to indicate if the column is currently visible in the table or not
+		 *  @type boolean
+		 */
+		"bVisible": null,
+	
+		/**
+		 * Store for manual type assignment using the `column.type` option. This
+		 * is held in store so we can manipulate the column's `sType` property.
+		 *  @type string
+		 *  @default null
+		 *  @private
+		 */
+		"_sManualType": null,
+	
+		/**
+		 * Flag to indicate if HTML5 data attributes should be used as the data
+		 * source for filtering or sorting. True is either are.
+		 *  @type boolean
+		 *  @default false
+		 *  @private
+		 */
+		"_bAttrSrc": false,
+	
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} nTd The TD node that has been created
+		 *  @param {*} sData The Data for the cell
+		 *  @param {array|object} oData The data for the whole row
+		 *  @param {int} iRow The row index for the aoData data store
+		 *  @default null
+		 */
+		"fnCreatedCell": null,
+	
+		/**
+		 * Function to get data from a cell in a column. You should <b>never</b>
+		 * access data directly through _aData internally in DataTables - always use
+		 * the method attached to this property. It allows mData to function as
+		 * required. This function is automatically assigned by the column
+		 * initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array
+		 *    (i.e. aoData[]._aData)
+		 *  @param {string} sSpecific The specific data type you want to get -
+		 *    'display', 'type' 'filter' 'sort'
+		 *  @returns {*} The data for the cell from the given row's data
+		 *  @default null
+		 */
+		"fnGetData": null,
+	
+		/**
+		 * Function to set data for a cell in the column. You should <b>never</b>
+		 * set the data directly to _aData internally in DataTables - always use
+		 * this method. It allows mData to function as required. This function
+		 * is automatically assigned by the column initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array
+		 *    (i.e. aoData[]._aData)
+		 *  @param {*} sValue Value to set
+		 *  @default null
+		 */
+		"fnSetData": null,
+	
+		/**
+		 * Property to read the value for the cells in the column from the data
+		 * source array / object. If null, then the default content is used, if a
+		 * function is given then the return from the function is used.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mData": null,
+	
+		/**
+		 * Partner property to mData which is used (only when defined) to get
+		 * the data - i.e. it is basically the same as mData, but without the
+		 * 'set' option, and also the data fed to it is the result from mData.
+		 * This is the rendering method to match the data method of mData.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mRender": null,
+	
+		/**
+		 * Unique header TH/TD element for this column - this is what the sorting
+		 * listener is attached to (if sorting is enabled.)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTh": null,
+	
+		/**
+		 * Unique footer TH/TD element for this column (if there is one). Not used
+		 * in DataTables as such, but can be used for plug-ins to reference the
+		 * footer for each column.
+		 *  @type node
+		 *  @default null
+		 */
+		"nTf": null,
+	
+		/**
+		 * The class to apply to all TD elements in the table's TBODY for the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sClass": null,
+	
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 *  @type string
+		 */
+		"sContentPadding": null,
+	
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because mData
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 */
+		"sDefaultContent": null,
+	
+		/**
+		 * Name for the column, allowing reference to the column by name as well as
+		 * by index (needs a lookup to work by name).
+		 *  @type string
+		 */
+		"sName": null,
+	
+		/**
+		 * Custom sorting data type - defines which of the available plug-ins in
+		 * afnSortData the custom sorting will use - if any is defined.
+		 *  @type string
+		 *  @default std
+		 */
+		"sSortDataType": 'std',
+	
+		/**
+		 * Class to be applied to the header element when sorting on this column
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClass": null,
+	
+		/**
+		 * Class to be applied to the header element when sorting on this column -
+		 * when jQuery UI theming is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClassJUI": null,
+	
+		/**
+		 * Title of the column - what is seen in the TH element (nTh).
+		 *  @type string
+		 */
+		"sTitle": null,
+	
+		/**
+		 * Column sorting and filtering type
+		 *  @type string
+		 *  @default null
+		 */
+		"sType": null,
+	
+		/**
+		 * Width of the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidth": null,
+	
+		/**
+		 * Width of the column when it was first "encountered"
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidthOrig": null
+	};
+	
+	
+	/*
+	 * Developer note: The properties of the object below are given in Hungarian
+	 * notation, that was used as the interface for DataTables prior to v1.10, however
+	 * from v1.10 onwards the primary interface is camel case. In order to avoid
+	 * breaking backwards compatibility utterly with this change, the Hungarian
+	 * version is still, internally the primary interface, but is is not documented
+	 * - hence the @name tags in each doc comment. This allows a Javascript function
+	 * to create a map from Hungarian notation to camel case (going the other direction
+	 * would require each property to be listed, which would at around 3K to the size
+	 * of DataTables, while this method is about a 0.5K hit.
+	 *
+	 * Ultimately this does pave the way for Hungarian notation to be dropped
+	 * completely, but that is a massive amount of work and will break current
+	 * installs (therefore is on-hold until v2).
+	 */
+	
+	/**
+	 * Initialisation options that can be given to DataTables at initialisation
+	 * time.
+	 *  @namespace
+	 */
+	DataTable.defaults = {
+		/**
+		 * An array of data to use for the table, passed in at initialisation which
+		 * will be used in preference to any data which is already in the DOM. This is
+		 * particularly useful for constructing tables purely in Javascript, for
+		 * example with a custom Ajax call.
+		 *  @type array
+		 *  @default null
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.data
+		 *
+		 *  @example
+		 *    // Using a 2D array data source
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "data": [
+		 *          ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'],
+		 *          ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'],
+		 *        ],
+		 *        "columns": [
+		 *          { "title": "Engine" },
+		 *          { "title": "Browser" },
+		 *          { "title": "Platform" },
+		 *          { "title": "Version" },
+		 *          { "title": "Grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using an array of objects as a data source (`data`)
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "data": [
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 4.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  4,
+		 *            "grade":    "X"
+		 *          },
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 5.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  5,
+		 *            "grade":    "C"
+		 *          }
+		 *        ],
+		 *        "columns": [
+		 *          { "title": "Engine",   "data": "engine" },
+		 *          { "title": "Browser",  "data": "browser" },
+		 *          { "title": "Platform", "data": "platform" },
+		 *          { "title": "Version",  "data": "version" },
+		 *          { "title": "Grade",    "data": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aaData": null,
+	
+	
+		/**
+		 * If ordering is enabled, then DataTables will perform a first pass sort on
+		 * initialisation. You can define which column(s) the sort is performed
+		 * upon, and the sorting direction, with this variable. The `sorting` array
+		 * should contain an array for each column to be sorted initially containing
+		 * the column's index and a direction string ('asc' or 'desc').
+		 *  @type array
+		 *  @default [[0,'asc']]
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.order
+		 *
+		 *  @example
+		 *    // Sort by 3rd column first, and then 4th column
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "order": [[2,'asc'], [3,'desc']]
+		 *      } );
+		 *    } );
+		 *
+		 *    // No initial sorting
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "order": []
+		 *      } );
+		 *    } );
+		 */
+		"aaSorting": [[0,'asc']],
+	
+	
+		/**
+		 * This parameter is basically identical to the `sorting` parameter, but
+		 * cannot be overridden by user interaction with the table. What this means
+		 * is that you could have a column (visible or hidden) which the sorting
+		 * will always be forced on first - any sorting after that (from the user)
+		 * will then be performed as required. This can be useful for grouping rows
+		 * together.
+		 *  @type array
+		 *  @default null
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.orderFixed
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "orderFixed": [[0,'asc']]
+		 *      } );
+		 *    } )
+		 */
+		"aaSortingFixed": [],
+	
+	
+		/**
+		 * DataTables can be instructed to load data to display in the table from a
+		 * Ajax source. This option defines how that Ajax call is made and where to.
+		 *
+		 * The `ajax` property has three different modes of operation, depending on
+		 * how it is defined. These are:
+		 *
+		 * * `string` - Set the URL from where the data should be loaded from.
+		 * * `object` - Define properties for `jQuery.ajax`.
+		 * * `function` - Custom data get function
+		 *
+		 * `string`
+		 * --------
+		 *
+		 * As a string, the `ajax` property simply defines the URL from which
+		 * DataTables will load data.
+		 *
+		 * `object`
+		 * --------
+		 *
+		 * As an object, the parameters in the object are passed to
+		 * [jQuery.ajax](http://api.jquery.com/jQuery.ajax/) allowing fine control
+		 * of the Ajax request. DataTables has a number of default parameters which
+		 * you can override using this option. Please refer to the jQuery
+		 * documentation for a full description of the options available, although
+		 * the following parameters provide additional options in DataTables or
+		 * require special consideration:
+		 *
+		 * * `data` - As with jQuery, `data` can be provided as an object, but it
+		 *   can also be used as a function to manipulate the data DataTables sends
+		 *   to the server. The function takes a single parameter, an object of
+		 *   parameters with the values that DataTables has readied for sending. An
+		 *   object may be returned which will be merged into the DataTables
+		 *   defaults, or you can add the items to the object that was passed in and
+		 *   not return anything from the function. This supersedes `fnServerParams`
+		 *   from DataTables 1.9-.
+		 *
+		 * * `dataSrc` - By default DataTables will look for the property `data` (or
+		 *   `aaData` for compatibility with DataTables 1.9-) when obtaining data
+		 *   from an Ajax source or for server-side processing - this parameter
+		 *   allows that property to be changed. You can use Javascript dotted
+		 *   object notation to get a data source for multiple levels of nesting, or
+		 *   it my be used as a function. As a function it takes a single parameter,
+		 *   the JSON returned from the server, which can be manipulated as
+		 *   required, with the returned value being that used by DataTables as the
+		 *   data source for the table. This supersedes `sAjaxDataProp` from
+		 *   DataTables 1.9-.
+		 *
+		 * * `success` - Should not be overridden it is used internally in
+		 *   DataTables. To manipulate / transform the data returned by the server
+		 *   use `ajax.dataSrc`, or use `ajax` as a function (see below).
+		 *
+		 * `function`
+		 * ----------
+		 *
+		 * As a function, making the Ajax call is left up to yourself allowing
+		 * complete control of the Ajax request. Indeed, if desired, a method other
+		 * than Ajax could be used to obtain the required data, such as Web storage
+		 * or an AIR database.
+		 *
+		 * The function is given four parameters and no return is required. The
+		 * parameters are:
+		 *
+		 * 1. _object_ - Data to send to the server
+		 * 2. _function_ - Callback function that must be executed when the required
+		 *    data has been obtained. That data should be passed into the callback
+		 *    as the only parameter
+		 * 3. _object_ - DataTables settings object for the table
+		 *
+		 * Note that this supersedes `fnServerData` from DataTables 1.9-.
+		 *
+		 *  @type string|object|function
+		 *  @default null
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.ajax
+		 *  @since 1.10.0
+		 *
+		 * @example
+		 *   // Get JSON data from a file via Ajax.
+		 *   // Note DataTables expects data in the form `{ data: [ ...data... ] }` by default).
+		 *   $('#example').dataTable( {
+		 *     "ajax": "data.json"
+		 *   } );
+		 *
+		 * @example
+		 *   // Get JSON data from a file via Ajax, using `dataSrc` to change
+		 *   // `data` to `tableData` (i.e. `{ tableData: [ ...data... ] }`)
+		 *   $('#example').dataTable( {
+		 *     "ajax": {
+		 *       "url": "data.json",
+		 *       "dataSrc": "tableData"
+		 *     }
+		 *   } );
+		 *
+		 * @example
+		 *   // Get JSON data from a file via Ajax, using `dataSrc` to read data
+		 *   // from a plain array rather than an array in an object
+		 *   $('#example').dataTable( {
+		 *     "ajax": {
+		 *       "url": "data.json",
+		 *       "dataSrc": ""
+		 *     }
+		 *   } );
+		 *
+		 * @example
+		 *   // Manipulate the data returned from the server - add a link to data
+		 *   // (note this can, should, be done using `render` for the column - this
+		 *   // is just a simple example of how the data can be manipulated).
+		 *   $('#example').dataTable( {
+		 *     "ajax": {
+		 *       "url": "data.json",
+		 *       "dataSrc": function ( json ) {
+		 *         for ( var i=0, ien=json.length ; i<ien ; i++ ) {
+		 *           json[i][0] = '<a href="/message/'+json[i][0]+'>View message</a>';
+		 *         }
+		 *         return json;
+		 *       }
+		 *     }
+		 *   } );
+		 *
+		 * @example
+		 *   // Add data to the request
+		 *   $('#example').dataTable( {
+		 *     "ajax": {
+		 *       "url": "data.json",
+		 *       "data": function ( d ) {
+		 *         return {
+		 *           "extra_search": $('#extra').val()
+		 *         };
+		 *       }
+		 *     }
+		 *   } );
+		 *
+		 * @example
+		 *   // Send request as POST
+		 *   $('#example').dataTable( {
+		 *     "ajax": {
+		 *       "url": "data.json",
+		 *       "type": "POST"
+		 *     }
+		 *   } );
+		 *
+		 * @example
+		 *   // Get the data from localStorage (could interface with a form for
+		 *   // adding, editing and removing rows).
+		 *   $('#example').dataTable( {
+		 *     "ajax": function (data, callback, settings) {
+		 *       callback(
+		 *         JSON.parse( localStorage.getItem('dataTablesData') )
+		 *       );
+		 *     }
+		 *   } );
+		 */
+		"ajax": null,
+	
+	
+		/**
+		 * This parameter allows you to readily specify the entries in the length drop
+		 * down menu that DataTables shows when pagination is enabled. It can be
+		 * either a 1D array of options which will be used for both the displayed
+		 * option and the value, or a 2D array which will use the array in the first
+		 * position as the value, and the array in the second position as the
+		 * displayed options (useful for language strings such as 'All').
+		 *
+		 * Note that the `pageLength` property will be automatically set to the
+		 * first value given in this array, unless `pageLength` is also provided.
+		 *  @type array
+		 *  @default [ 10, 25, 50, 100 ]
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.lengthMenu
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "lengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]]
+		 *      } );
+		 *    } );
+		 */
+		"aLengthMenu": [ 10, 25, 50, 100 ],
+	
+	
+		/**
+		 * The `columns` option in the initialisation parameter allows you to define
+		 * details about the way individual columns behave. For a full list of
+		 * column options that can be set, please see
+		 * {@link DataTable.defaults.column}. Note that if you use `columns` to
+		 * define your columns, you must have an entry in the array for every single
+		 * column that you have in your table (these can be null if you don't which
+		 * to specify any options).
+		 *  @member
+		 *
+		 *  @name DataTable.defaults.column
+		 */
+		"aoColumns": null,
+	
+		/**
+		 * Very similar to `columns`, `columnDefs` allows you to target a specific
+		 * column, multiple columns, or all columns, using the `targets` property of
+		 * each object in the array. This allows great flexibility when creating
+		 * tables, as the `columnDefs` arrays can be of any length, targeting the
+		 * columns you specifically want. `columnDefs` may use any of the column
+		 * options available: {@link DataTable.defaults.column}, but it _must_
+		 * have `targets` defined in each object in the array. Values in the `targets`
+		 * array may be:
+		 *   <ul>
+		 *     <li>a string - class name will be matched on the TH for the column</li>
+		 *     <li>0 or a positive integer - column index counting from the left</li>
+		 *     <li>a negative integer - column index counting from the right</li>
+		 *     <li>the string "_all" - all columns (i.e. assign a default)</li>
+		 *   </ul>
+		 *  @member
+		 *
+		 *  @name DataTable.defaults.columnDefs
+		 */
+		"aoColumnDefs": null,
+	
+	
+		/**
+		 * Basically the same as `search`, this parameter defines the individual column
+		 * filtering state at initialisation time. The array must be of the same size
+		 * as the number of columns, and each element be an object with the parameters
+		 * `search` and `escapeRegex` (the latter is optional). 'null' is also
+		 * accepted and the default will be used.
+		 *  @type array
+		 *  @default []
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.searchCols
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "searchCols": [
+		 *          null,
+		 *          { "search": "My filter" },
+		 *          null,
+		 *          { "search": "^[0-9]", "escapeRegex": false }
+		 *        ]
+		 *      } );
+		 *    } )
+		 */
+		"aoSearchCols": [],
+	
+	
+		/**
+		 * An array of CSS classes that should be applied to displayed rows. This
+		 * array may be of any length, and DataTables will apply each class
+		 * sequentially, looping when required.
+		 *  @type array
+		 *  @default null <i>Will take the values determined by the `oClasses.stripe*`
+		 *    options</i>
+		 *
+		 *  @dtopt Option
+		 *  @name DataTable.defaults.stripeClasses
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stripeClasses": [ 'strip1', 'strip2', 'strip3' ]
+		 *      } );
+		 *    } )
+		 */
+		"asStripeClasses": null,
+	
+	
+		/**
+		 * Enable or disable automatic column width calculation. This can be disabled
+		 * as an optimisation (it takes some time to calculate the widths) if the
+		 * tables widths are passed in using `columns`.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.autoWidth
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "autoWidth": false
+		 *      } );
+		 *    } );
+		 */
+		"bAutoWidth": true,
+	
+	
+		/**
+		 * Deferred rendering can provide DataTables with a huge speed boost when you
+		 * are using an Ajax or JS data source for the table. This option, when set to
+		 * true, will cause DataTables to defer the creation of the table elements for
+		 * each row until they are needed for a draw - saving a significant amount of
+		 * time.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.deferRender
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "ajax": "sources/arrays.txt",
+		 *        "deferRender": true
+		 *      } );
+		 *    } );
+		 */
+		"bDeferRender": false,
+	
+	
+		/**
+		 * Replace a DataTable which matches the given selector and replace it with
+		 * one which has the properties of the new initialisation object passed. If no
+		 * table matches the selector, then the new DataTable will be constructed as
+		 * per normal.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.destroy
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "srollY": "200px",
+		 *        "paginate": false
+		 *      } );
+		 *
+		 *      // Some time later....
+		 *      $('#example').dataTable( {
+		 *        "filter": false,
+		 *        "destroy": true
+		 *      } );
+		 *    } );
+		 */
+		"bDestroy": false,
+	
+	
+		/**
+		 * Enable or disable filtering of data. Filtering in DataTables is "smart" in
+		 * that it allows the end user to input multiple words (space separated) and
+		 * will match a row containing those words, even if not in the order that was
+		 * specified (this allow matching across multiple columns). Note that if you
+		 * wish to use filtering in DataTables this must remain 'true' - to remove the
+		 * default filtering input box and retain filtering abilities, please use
+		 * {@link DataTable.defaults.dom}.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.searching
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "searching": false
+		 *      } );
+		 *    } );
+		 */
+		"bFilter": true,
+	
+	
+		/**
+		 * Enable or disable the table information display. This shows information
+		 * about the data that is currently visible on the page, including information
+		 * about filtered data if that action is being performed.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.info
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "info": false
+		 *      } );
+		 *    } );
+		 */
+		"bInfo": true,
+	
+	
+		/**
+		 * Enable jQuery UI ThemeRoller support (required as ThemeRoller requires some
+		 * slightly different and additional mark-up from what DataTables has
+		 * traditionally used).
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.jQueryUI
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "jQueryUI": true
+		 *      } );
+		 *    } );
+		 */
+		"bJQueryUI": false,
+	
+	
+		/**
+		 * Allows the end user to select the size of a formatted page from a select
+		 * menu (sizes are 10, 25, 50 and 100). Requires pagination (`paginate`).
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.lengthChange
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "lengthChange": false
+		 *      } );
+		 *    } );
+		 */
+		"bLengthChange": true,
+	
+	
+		/**
+		 * Enable or disable pagination.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.paging
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "paging": false
+		 *      } );
+		 *    } );
+		 */
+		"bPaginate": true,
+	
+	
+		/**
+		 * Enable or disable the display of a 'processing' indicator when the table is
+		 * being processed (e.g. a sort). This is particularly useful for tables with
+		 * large amounts of data where it can take a noticeable amount of time to sort
+		 * the entries.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.processing
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "processing": true
+		 *      } );
+		 *    } );
+		 */
+		"bProcessing": false,
+	
+	
+		/**
+		 * Retrieve the DataTables object for the given selector. Note that if the
+		 * table has already been initialised, this parameter will cause DataTables
+		 * to simply return the object that has already been set up - it will not take
+		 * account of any changes you might have made to the initialisation object
+		 * passed to DataTables (setting this parameter to true is an acknowledgement
+		 * that you understand this). `destroy` can be used to reinitialise a table if
+		 * you need.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.retrieve
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      initTable();
+		 *      tableActions();
+		 *    } );
+		 *
+		 *    function initTable ()
+		 *    {
+		 *      return $('#example').dataTable( {
+		 *        "scrollY": "200px",
+		 *        "paginate": false,
+		 *        "retrieve": true
+		 *      } );
+		 *    }
+		 *
+		 *    function tableActions ()
+		 *    {
+		 *      var table = initTable();
+		 *      // perform API operations with oTable
+		 *    }
+		 */
+		"bRetrieve": false,
+	
+	
+		/**
+		 * When vertical (y) scrolling is enabled, DataTables will force the height of
+		 * the table's viewport to the given height at all times (useful for layout).
+		 * However, this can look odd when filtering data down to a small data set,
+		 * and the footer is left "floating" further down. This parameter (when
+		 * enabled) will cause DataTables to collapse the table's viewport down when
+		 * the result set will fit within the given Y height.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.scrollCollapse
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "scrollY": "200",
+		 *        "scrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"bScrollCollapse": false,
+	
+	
+		/**
+		 * Configure DataTables to use server-side processing. Note that the
+		 * `ajax` parameter must also be given in order to give DataTables a
+		 * source to obtain the required data for each draw.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Features
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.serverSide
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "serverSide": true,
+		 *        "ajax": "xhr.php"
+		 *      } );
+		 *    } );
+		 */
+		"bServerSide": false,
+	
+	
+		/**
+		 * Enable or disable sorting of columns. Sorting of individual columns can be
+		 * disabled by the `sortable` option for each column.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.ordering
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "ordering": false
+		 *      } );
+		 *    } );
+		 */
+		"bSort": true,
+	
+	
+		/**
+		 * Enable or display DataTables' ability to sort multiple columns at the
+		 * same time (activated by shift-click by the user).
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.orderMulti
+		 *
+		 *  @example
+		 *    // Disable multiple column sorting ability
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "orderMulti": false
+		 *      } );
+		 *    } );
+		 */
+		"bSortMulti": true,
+	
+	
+		/**
+		 * Allows control over whether DataTables should use the top (true) unique
+		 * cell that is found for a single column, or the bottom (false - default).
+		 * This is useful when using complex headers.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.orderCellsTop
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "orderCellsTop": true
+		 *      } );
+		 *    } );
+		 */
+		"bSortCellsTop": false,
+	
+	
+		/**
+		 * Enable or disable the addition of the classes `sorting\_1`, `sorting\_2` and
+		 * `sorting\_3` to the columns which are currently being sorted on. This is
+		 * presented as a feature switch as it can increase processing time (while
+		 * classes are removed and added) so for large data sets you might want to
+		 * turn this off.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.orderClasses
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "orderClasses": false
+		 *      } );
+		 *    } );
+		 */
+		"bSortClasses": true,
+	
+	
+		/**
+		 * Enable or disable state saving. When enabled HTML5 `localStorage` will be
+		 * used to save table display information such as pagination information,
+		 * display length, filtering and sorting. As such when the end user reloads
+		 * the page the display display will match what thy had previously set up.
+		 *
+		 * Due to the use of `localStorage` the default state saving is not supported
+		 * in IE6 or 7. If state saving is required in those browsers, use
+		 * `stateSaveCallback` to provide a storage solution such as cookies.
+		 *  @type boolean
+		 *  @default false
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.stateSave
+		 *
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true
+		 *      } );
+		 *    } );
+		 */
+		"bStateSave": false,
+	
+	
+		/**
+		 * This function is called when a TR element is created (and all TD child
+		 * elements have been inserted), or registered if using a DOM source, allowing
+		 * manipulation of the TR element (adding classes etc).
+		 *  @type function
+		 *  @param {node} row "TR" element for the current row
+		 *  @param {array} data Raw data array for this row
+		 *  @param {int} dataIndex The index of this row in the internal aoData array
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.createdRow
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "createdRow": function( row, data, dataIndex ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( data[4] == "A" )
+		 *          {
+		 *            $('td:eq(4)', row).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnCreatedRow": null,
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify any aspect you want about the created DOM.
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.drawCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "drawCallback": function( settings ) {
+		 *          alert( 'DataTables has redrawn the table' );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnDrawCallback": null,
+	
+	
+		/**
+		 * Identical to fnHeaderCallback() but for the table footer this function
+		 * allows you to modify the table footer on every 'draw' event.
+		 *  @type function
+		 *  @param {node} foot "TR" element for the footer
+		 *  @param {array} data Full table data (as derived from the original HTML)
+		 *  @param {int} start Index for the current display starting point in the
+		 *    display array
+		 *  @param {int} end Index for the current display ending point in the
+		 *    display array
+		 *  @param {array int} display Index array to translate the visual position
+		 *    to the full data array
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.footerCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "footerCallback": function( tfoot, data, start, end, display ) {
+		 *          tfoot.getElementsByTagName('th')[0].innerHTML = "Starting index is "+start;
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnFooterCallback": null,
+	
+	
+		/**
+		 * When rendering large numbers in the information element for the table
+		 * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers
+		 * to have a comma separator for the 'thousands' units (e.g. 1 million is
+		 * rendered as "1,000,000") to help readability for the end user. This
+		 * function will override the default method DataTables uses.
+		 *  @type function
+		 *  @member
+		 *  @param {int} toFormat number to be formatted
+		 *  @returns {string} formatted string for DataTables to show the number
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.formatNumber
+		 *
+		 *  @example
+		 *    // Format a number using a single quote for the separator (note that
+		 *    // this can also be done with the language.thousands option)
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "formatNumber": function ( toFormat ) {
+		 *          return toFormat.toString().replace(
+		 *            /\B(?=(\d{3})+(?!\d))/g, "'"
+		 *          );
+		 *        };
+		 *      } );
+		 *    } );
+		 */
+		"fnFormatNumber": function ( toFormat ) {
+			return toFormat.toString().replace(
+				/\B(?=(\d{3})+(?!\d))/g,
+				this.oLanguage.sThousands
+			);
+		},
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify the header row. This can be used to calculate and
+		 * display useful information about the table.
+		 *  @type function
+		 *  @param {node} head "TR" element for the header
+		 *  @param {array} data Full table data (as derived from the original HTML)
+		 *  @param {int} start Index for the current display starting point in the
+		 *    display array
+		 *  @param {int} end Index for the current display ending point in the
+		 *    display array
+		 *  @param {array int} display Index array to translate the visual position
+		 *    to the full data array
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.headerCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fheaderCallback": function( head, data, start, end, display ) {
+		 *          head.getElementsByTagName('th')[0].innerHTML = "Displaying "+(end-start)+" records";
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnHeaderCallback": null,
+	
+	
+		/**
+		 * The information element can be used to convey information about the current
+		 * state of the table. Although the internationalisation options presented by
+		 * DataTables are quite capable of dealing with most customisations, there may
+		 * be times where you wish to customise the string further. This callback
+		 * allows you to do exactly that.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {int} start Starting position in data for the draw
+		 *  @param {int} end End position in data for the draw
+		 *  @param {int} max Total number of rows in the table (regardless of
+		 *    filtering)
+		 *  @param {int} total Total number of rows in the data set, after filtering
+		 *  @param {string} pre The string that DataTables has formatted using it's
+		 *    own rules
+		 *  @returns {string} The string to be displayed in the information element.
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.infoCallback
+		 *
+		 *  @example
+		 *    $('#example').dataTable( {
+		 *      "infoCallback": function( settings, start, end, max, total, pre ) {
+		 *        return start +" to "+ end;
+		 *      }
+		 *    } );
+		 */
+		"fnInfoCallback": null,
+	
+	
+		/**
+		 * Called when the table has been initialised. Normally DataTables will
+		 * initialise sequentially and there will be no need for this function,
+		 * however, this does not hold true when using external language information
+		 * since that is obtained using an async XHR call.
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} json The JSON object request from the server - only
+		 *    present if client-side Ajax sourced data is used
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.initComplete
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "initComplete": function(settings, json) {
+		 *          alert( 'DataTables has finished its initialisation.' );
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnInitComplete": null,
+	
+	
+		/**
+		 * Called at the very start of each table draw and can be used to cancel the
+		 * draw by returning false, any other return (including undefined) results in
+		 * the full draw occurring).
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *  @returns {boolean} False will cancel the draw, anything else (including no
+		 *    return) will allow it to complete.
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.preDrawCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "preDrawCallback": function( settings ) {
+		 *          if ( $('#test').val() == 1 ) {
+		 *            return false;
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnPreDrawCallback": null,
+	
+	
+		/**
+		 * This function allows you to 'post process' each row after it have been
+		 * generated for each table draw, but before it is rendered on screen. This
+		 * function might be used for setting the row class name etc.
+		 *  @type function
+		 *  @param {node} row "TR" element for the current row
+		 *  @param {array} data Raw data array for this row
+		 *  @param {int} displayIndex The display index for the current table draw
+		 *  @param {int} displayIndexFull The index of the data in the full list of
+		 *    rows (after filtering)
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.rowCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "rowCallback": function( row, data, displayIndex, displayIndexFull ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( data[4] == "A" ) {
+		 *            $('td:eq(4)', row).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnRowCallback": null,
+	
+	
+		/**
+		 * __Deprecated__ The functionality provided by this parameter has now been
+		 * superseded by that provided through `ajax`, which should be used instead.
+		 *
+		 * This parameter allows you to override the default function which obtains
+		 * the data from the server so something more suitable for your application.
+		 * For example you could use POST data, or pull information from a Gears or
+		 * AIR database.
+		 *  @type function
+		 *  @member
+		 *  @param {string} source HTTP source to obtain the data from (`ajax`)
+		 *  @param {array} data A key/value pair object containing the data to send
+		 *    to the server
+		 *  @param {function} callback to be called on completion of the data get
+		 *    process that will draw the data on the page.
+		 *  @param {object} settings DataTables settings object
+		 *
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.serverData
+		 *
+		 *  @deprecated 1.10. Please use `ajax` for this functionality now.
+		 */
+		"fnServerData": null,
+	
+	
+		/**
+		 * __Deprecated__ The functionality provided by this parameter has now been
+		 * superseded by that provided through `ajax`, which should be used instead.
+		 *
+		 *  It is often useful to send extra data to the server when making an Ajax
+		 * request - for example custom filtering information, and this callback
+		 * function makes it trivial to send extra information to the server. The
+		 * passed in parameter is the data set that has been constructed by
+		 * DataTables, and you can add to this or modify it as you require.
+		 *  @type function
+		 *  @param {array} data Data array (array of objects which are name/value
+		 *    pairs) that has been constructed by DataTables and will be sent to the
+		 *    server. In the case of Ajax sourced data with server-side processing
+		 *    this will be an empty array, for server-side processing there will be a
+		 *    significant number of parameters!
+		 *  @returns {undefined} Ensure that you modify the data array passed in,
+		 *    as this is passed by reference.
+		 *
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.serverParams
+		 *
+		 *  @deprecated 1.10. Please use `ajax` for this functionality now.
+		 */
+		"fnServerParams": null,
+	
+	
+		/**
+		 * Load the table state. With this function you can define from where, and how, the
+		 * state of a table is loaded. By default DataTables will load from `localStorage`
+		 * but you might wish to use a server-side database or cookies.
+		 *  @type function
+		 *  @member
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} callback Callback that can be executed when done. It
+		 *    should be passed the loaded state object.
+		 *  @return {object} The DataTables state object to be loaded
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.stateLoadCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateLoadCallback": function (settings, callback) {
+		 *          $.ajax( {
+		 *            "url": "/state_load",
+		 *            "dataType": "json",
+		 *            "success": function (json) {
+		 *              callback( json );
+		 *            }
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoadCallback": function ( settings ) {
+			try {
+				return JSON.parse(
+					(settings.iStateDuration === -1 ? sessionStorage : localStorage).getItem(
+						'DataTables_'+settings.sInstance+'_'+location.pathname
+					)
+				);
+			} catch (e) {}
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the saved state prior to loading that state.
+		 * This callback is called when the table is loading state from the stored data, but
+		 * prior to the settings object being modified by the saved state. Note that for
+		 * plug-in authors, you should use the `stateLoadParams` event to load parameters for
+		 * a plug-in.
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} data The state object that is to be loaded
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.stateLoadParams
+		 *
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never loaded
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateLoadParams": function (settings, data) {
+		 *          data.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Disallow state loading by returning false
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateLoadParams": function (settings, data) {
+		 *          return false;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoadParams": null,
+	
+	
+		/**
+		 * Callback that is called when the state has been loaded from the state saving method
+		 * and the DataTables settings object has been modified as a result of the loaded state.
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} data The state object that was loaded
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.stateLoaded
+		 *
+		 *  @example
+		 *    // Show an alert with the filtering value that was saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateLoaded": function (settings, data) {
+		 *          alert( 'Saved filter was: '+data.oSearch.sSearch );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoaded": null,
+	
+	
+		/**
+		 * Save the table state. This function allows you to define where and how the state
+		 * information for the table is stored By default DataTables will use `localStorage`
+		 * but you might wish to use a server-side database or cookies.
+		 *  @type function
+		 *  @member
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} data The state object to be saved
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.stateSaveCallback
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateSaveCallback": function (settings, data) {
+		 *          // Send an Ajax request to the server with the state object
+		 *          $.ajax( {
+		 *            "url": "/state_save",
+		 *            "data": data,
+		 *            "dataType": "json",
+		 *            "method": "POST"
+		 *            "success": function () {}
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSaveCallback": function ( settings, data ) {
+			try {
+				(settings.iStateDuration === -1 ? sessionStorage : localStorage).setItem(
+					'DataTables_'+settings.sInstance+'_'+location.pathname,
+					JSON.stringify( data )
+				);
+			} catch (e) {}
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the state to be saved. Called when the table
+		 * has changed state a new state save is required. This method allows modification of
+		 * the state saving object prior to actually doing the save, including addition or
+		 * other state properties or modification. Note that for plug-in authors, you should
+		 * use the `stateSaveParams` event to save parameters for a plug-in.
+		 *  @type function
+		 *  @param {object} settings DataTables settings object
+		 *  @param {object} data The state object to be saved
+		 *
+		 *  @dtopt Callbacks
+		 *  @name DataTable.defaults.stateSaveParams
+		 *
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateSave": true,
+		 *        "stateSaveParams": function (settings, data) {
+		 *          data.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSaveParams": null,
+	
+	
+		/**
+		 * Duration for which the saved state information is considered valid. After this period
+		 * has elapsed the state will be returned to the default.
+		 * Value is given in seconds.
+		 *  @type int
+		 *  @default 7200 <i>(2 hours)</i>
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.stateDuration
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "stateDuration": 60*60*24; // 1 day
+		 *      } );
+		 *    } )
+		 */
+		"iStateDuration": 7200,
+	
+	
+		/**
+		 * When enabled DataTables will not make a request to the server for the first
+		 * page draw - rather it will use the data already on the page (no sorting etc
+		 * will be applied to it), thus saving on an XHR at load time. `deferLoading`
+		 * is used to indicate that deferred loading is required, but it is also used
+		 * to tell DataTables how many records there are in the full table (allowing
+		 * the information element and pagination to be displayed correctly). In the case
+		 * where a filtering is applied to the table on initial load, this can be
+		 * indicated by giving the parameter as an array, where the first element is
+		 * the number of records available after filtering and the second element is the
+		 * number of records without filtering (allowing the table information element
+		 * to be shown correctly).
+		 *  @type int | array
+		 *  @default null
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.deferLoading
+		 *
+		 *  @example
+		 *    // 57 records available in the table, no filtering applied
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "serverSide": true,
+		 *        "ajax": "scripts/server_processing.php",
+		 *        "deferLoading": 57
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // 57 records after filtering, 100 without filtering (an initial filter applied)
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "serverSide": true,
+		 *        "ajax": "scripts/server_processing.php",
+		 *        "deferLoading": [ 57, 100 ],
+		 *        "search": {
+		 *          "search": "my_filter"
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"iDeferLoading": null,
+	
+	
+		/**
+		 * Number of rows to display on a single page when using pagination. If
+		 * feature enabled (`lengthChange`) then the end user will be able to override
+		 * this to a custom setting using a pop-up menu.
+		 *  @type int
+		 *  @default 10
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.pageLength
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "pageLength": 50
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayLength": 10,
+	
+	
+		/**
+		 * Define the starting point for data display when using DataTables with
+		 * pagination. Note that this parameter is the number of records, rather than
+		 * the page number, so if you have 10 records per page and want to start on
+		 * the third page, it should be "20".
+		 *  @type int
+		 *  @default 0
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.displayStart
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "displayStart": 20
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayStart": 0,
+	
+	
+		/**
+		 * By default DataTables allows keyboard navigation of the table (sorting, paging,
+		 * and filtering) by adding a `tabindex` attribute to the required elements. This
+		 * allows you to tab through the controls and press the enter key to activate them.
+		 * The tabindex is default 0, meaning that the tab follows the flow of the document.
+		 * You can overrule this using this parameter if you wish. Use a value of -1 to
+		 * disable built-in keyboard navigation.
+		 *  @type int
+		 *  @default 0
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.tabIndex
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "tabIndex": 1
+		 *      } );
+		 *    } );
+		 */
+		"iTabIndex": 0,
+	
+	
+		/**
+		 * Classes that DataTables assigns to the various components and features
+		 * that it adds to the HTML table. This allows classes to be configured
+		 * during initialisation in addition to through the static
+		 * {@link DataTable.ext.oStdClasses} object).
+		 *  @namespace
+		 *  @name DataTable.defaults.classes
+		 */
+		"oClasses": {},
+	
+	
+		/**
+		 * All strings that DataTables uses in the user interface that it creates
+		 * are defined in this object, allowing you to modified them individually or
+		 * completely replace them all as required.
+		 *  @namespace
+		 *  @name DataTable.defaults.language
+		 */
+		"oLanguage": {
+			/**
+			 * Strings that are used for WAI-ARIA labels and controls only (these are not
+			 * actually visible on the page, but will be read by screenreaders, and thus
+			 * must be internationalised as well).
+			 *  @namespace
+			 *  @name DataTable.defaults.language.aria
+			 */
+			"oAria": {
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted ascending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.aria.sortAscending
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "aria": {
+				 *            "sortAscending": " - click/return to sort ascending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortAscending": ": activate to sort column ascending",
+	
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted descending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.aria.sortDescending
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "aria": {
+				 *            "sortDescending": " - click/return to sort descending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortDescending": ": activate to sort column descending"
+			},
+	
+			/**
+			 * Pagination string used by DataTables for the built-in pagination
+			 * control types.
+			 *  @namespace
+			 *  @name DataTable.defaults.language.paginate
+			 */
+			"oPaginate": {
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the first page.
+				 *  @type string
+				 *  @default First
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.paginate.first
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "paginate": {
+				 *            "first": "First page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sFirst": "First",
+	
+	
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the last page.
+				 *  @type string
+				 *  @default Last
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.paginate.last
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "paginate": {
+				 *            "last": "Last page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sLast": "Last",
+	
+	
+				/**
+				 * Text to use for the 'next' pagination button (to take the user to the
+				 * next page).
+				 *  @type string
+				 *  @default Next
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.paginate.next
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "paginate": {
+				 *            "next": "Next page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sNext": "Next",
+	
+	
+				/**
+				 * Text to use for the 'previous' pagination button (to take the user to
+				 * the previous page).
+				 *  @type string
+				 *  @default Previous
+				 *
+				 *  @dtopt Language
+				 *  @name DataTable.defaults.language.paginate.previous
+				 *
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "language": {
+				 *          "paginate": {
+				 *            "previous": "Previous page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sPrevious": "Previous"
+			},
+	
+			/**
+			 * This string is shown in preference to `zeroRecords` when the table is
+			 * empty of data (regardless of filtering). Note that this is an optional
+			 * parameter - if it is not given, the value of `zeroRecords` will be used
+			 * instead (either the default or given value).
+			 *  @type string
+			 *  @default No data available in table
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.emptyTable
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "emptyTable": "No data available in table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sEmptyTable": "No data available in table",
+	
+	
+			/**
+			 * This string gives information to the end user about the information
+			 * that is current on display on the page. The following tokens can be
+			 * used in the string and will be dynamically replaced as the table
+			 * display updates. This tokens can be placed anywhere in the string, or
+			 * removed as needed by the language requires:
+			 *
+			 * * `\_START\_` - Display index of the first record on the current page
+			 * * `\_END\_` - Display index of the last record on the current page
+			 * * `\_TOTAL\_` - Number of records in the table after filtering
+			 * * `\_MAX\_` - Number of records in the table without filtering
+			 * * `\_PAGE\_` - Current page number
+			 * * `\_PAGES\_` - Total number of pages of data in the table
+			 *
+			 *  @type string
+			 *  @default Showing _START_ to _END_ of _TOTAL_ entries
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.info
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "info": "Showing page _PAGE_ of _PAGES_"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
+	
+	
+			/**
+			 * Display information string for when the table is empty. Typically the
+			 * format of this string should match `info`.
+			 *  @type string
+			 *  @default Showing 0 to 0 of 0 entries
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.infoEmpty
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "infoEmpty": "No entries to show"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoEmpty": "Showing 0 to 0 of 0 entries",
+	
+	
+			/**
+			 * When a user filters the information in a table, this string is appended
+			 * to the information (`info`) to give an idea of how strong the filtering
+			 * is. The variable _MAX_ is dynamically updated.
+			 *  @type string
+			 *  @default (filtered from _MAX_ total entries)
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.infoFiltered
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "infoFiltered": " - filtering from _MAX_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoFiltered": "(filtered from _MAX_ total entries)",
+	
+	
+			/**
+			 * If can be useful to append extra information to the info string at times,
+			 * and this variable does exactly that. This information will be appended to
+			 * the `info` (`infoEmpty` and `infoFiltered` in whatever combination they are
+			 * being used) at all times.
+			 *  @type string
+			 *  @default <i>Empty string</i>
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.infoPostFix
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "infoPostFix": "All records shown are derived from real information."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoPostFix": "",
+	
+	
+			/**
+			 * This decimal place operator is a little different from the other
+			 * language options since DataTables doesn't output floating point
+			 * numbers, so it won't ever use this for display of a number. Rather,
+			 * what this parameter does is modify the sort methods of the table so
+			 * that numbers which are in a format which has a character other than
+			 * a period (`.`) as a decimal place will be sorted numerically.
+			 *
+			 * Note that numbers with different decimal places cannot be shown in
+			 * the same table and still be sortable, the table must be consistent.
+			 * However, multiple different tables on the page can use different
+			 * decimal place characters.
+			 *  @type string
+			 *  @default 
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.decimal
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "decimal": ","
+			 *          "thousands": "."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sDecimal": "",
+	
+	
+			/**
+			 * DataTables has a build in number formatter (`formatNumber`) which is
+			 * used to format large numbers that are used in the table information.
+			 * By default a comma is used, but this can be trivially changed to any
+			 * character you wish with this parameter.
+			 *  @type string
+			 *  @default ,
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.thousands
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "thousands": "'"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sThousands": ",",
+	
+	
+			/**
+			 * Detail the action that will be taken when the drop down menu for the
+			 * pagination length option is changed. The '_MENU_' variable is replaced
+			 * with a default select list of 10, 25, 50 and 100, and can be replaced
+			 * with a custom select box if required.
+			 *  @type string
+			 *  @default Show _MENU_ entries
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.lengthMenu
+			 *
+			 *  @example
+			 *    // Language change only
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "lengthMenu": "Display _MENU_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 *
+			 *  @example
+			 *    // Language and options change
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "lengthMenu": 'Display <select>'+
+			 *            '<option value="10">10</option>'+
+			 *            '<option value="20">20</option>'+
+			 *            '<option value="30">30</option>'+
+			 *            '<option value="40">40</option>'+
+			 *            '<option value="50">50</option>'+
+			 *            '<option value="-1">All</option>'+
+			 *            '</select> records'
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLengthMenu": "Show _MENU_ entries",
+	
+	
+			/**
+			 * When using Ajax sourced data and during the first draw when DataTables is
+			 * gathering the data, this message is shown in an empty row in the table to
+			 * indicate to the end user the the data is being loaded. Note that this
+			 * parameter is not used when loading data by server-side processing, just
+			 * Ajax sourced data with client-side processing.
+			 *  @type string
+			 *  @default Loading...
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.loadingRecords
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "loadingRecords": "Please wait - loading..."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLoadingRecords": "Loading...",
+	
+	
+			/**
+			 * Text which is displayed when the table is processing a user action
+			 * (usually a sort command or similar).
+			 *  @type string
+			 *  @default Processing...
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.processing
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "processing": "DataTables is currently busy"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sProcessing": "Processing...",
+	
+	
+			/**
+			 * Details the actions that will be taken when the user types into the
+			 * filtering input text box. The variable "_INPUT_", if used in the string,
+			 * is replaced with the HTML text box for the filtering input allowing
+			 * control over where it appears in the string. If "_INPUT_" is not given
+			 * then the input box is appended to the string automatically.
+			 *  @type string
+			 *  @default Search:
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.search
+			 *
+			 *  @example
+			 *    // Input text box will be appended at the end automatically
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "search": "Filter records:"
+			 *        }
+			 *      } );
+			 *    } );
+			 *
+			 *  @example
+			 *    // Specify where the filter should appear
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "search": "Apply filter _INPUT_ to table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sSearch": "Search:",
+	
+	
+			/**
+			 * Assign a `placeholder` attribute to the search `input` element
+			 *  @type string
+			 *  @default 
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.searchPlaceholder
+			 */
+			"sSearchPlaceholder": "",
+	
+	
+			/**
+			 * All of the language information can be stored in a file on the
+			 * server-side, which DataTables will look up if this parameter is passed.
+			 * It must store the URL of the language file, which is in a JSON format,
+			 * and the object has the same properties as the oLanguage object in the
+			 * initialiser object (i.e. the above parameters). Please refer to one of
+			 * the example language files to see how this works in action.
+			 *  @type string
+			 *  @default <i>Empty string - i.e. disabled</i>
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.url
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "url": "http://www.sprymedia.co.uk/dataTables/lang.txt"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sUrl": "",
+	
+	
+			/**
+			 * Text shown inside the table records when the is no information to be
+			 * displayed after filtering. `emptyTable` is shown when there is simply no
+			 * information in the table at all (regardless of filtering).
+			 *  @type string
+			 *  @default No matching records found
+			 *
+			 *  @dtopt Language
+			 *  @name DataTable.defaults.language.zeroRecords
+			 *
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "language": {
+			 *          "zeroRecords": "No records to display"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sZeroRecords": "No matching records found"
+		},
+	
+	
+		/**
+		 * This parameter allows you to have define the global filtering state at
+		 * initialisation time. As an object the `search` parameter must be
+		 * defined, but all other parameters are optional. When `regex` is true,
+		 * the search string will be treated as a regular expression, when false
+		 * (default) it will be treated as a straight string. When `smart`
+		 * DataTables will use it's smart filtering methods (to word match at
+		 * any point in the data), when false this will not be done.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.search
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "search": {"search": "Initial search"}
+		 *      } );
+		 *    } )
+		 */
+		"oSearch": $.extend( {}, DataTable.models.oSearch ),
+	
+	
+		/**
+		 * __Deprecated__ The functionality provided by this parameter has now been
+		 * superseded by that provided through `ajax`, which should be used instead.
+		 *
+		 * By default DataTables will look for the property `data` (or `aaData` for
+		 * compatibility with DataTables 1.9-) when obtaining data from an Ajax
+		 * source or for server-side processing - this parameter allows that
+		 * property to be changed. You can use Javascript dotted object notation to
+		 * get a data source for multiple levels of nesting.
+		 *  @type string
+		 *  @default data
+		 *
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.ajaxDataProp
+		 *
+		 *  @deprecated 1.10. Please use `ajax` for this functionality now.
+		 */
+		"sAjaxDataProp": "data",
+	
+	
+		/**
+		 * __Deprecated__ The functionality provided by this parameter has now been
+		 * superseded by that provided through `ajax`, which should be used instead.
+		 *
+		 * You can instruct DataTables to load data from an external
+		 * source using this parameter (use aData if you want to pass data in you
+		 * already have). Simply provide a url a JSON object can be obtained from.
+		 *  @type string
+		 *  @default null
+		 *
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.ajaxSource
+		 *
+		 *  @deprecated 1.10. Please use `ajax` for this functionality now.
+		 */
+		"sAjaxSource": null,
+	
+	
+		/**
+		 * This initialisation variable allows you to specify exactly where in the
+		 * DOM you want DataTables to inject the various controls it adds to the page
+		 * (for example you might want the pagination controls at the top of the
+		 * table). DIV elements (with or without a custom class) can also be added to
+		 * aid styling. The follow syntax is used:
+		 *   <ul>
+		 *     <li>The following options are allowed:
+		 *       <ul>
+		 *         <li>'l' - Length changing</li>
+		 *         <li>'f' - Filtering input</li>
+		 *         <li>'t' - The table!</li>
+		 *         <li>'i' - Information</li>
+		 *         <li>'p' - Pagination</li>
+		 *         <li>'r' - pRocessing</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following constants are allowed:
+		 *       <ul>
+		 *         <li>'H' - jQueryUI theme "header" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')</li>
+		 *         <li>'F' - jQueryUI theme "footer" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following syntax is expected:
+		 *       <ul>
+		 *         <li>'&lt;' and '&gt;' - div elements</li>
+		 *         <li>'&lt;"class" and '&gt;' - div with a class</li>
+		 *         <li>'&lt;"#id" and '&gt;' - div with an ID</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>Examples:
+		 *       <ul>
+		 *         <li>'&lt;"wrapper"flipt&gt;'</li>
+		 *         <li>'&lt;lf&lt;t&gt;ip&gt;'</li>
+		 *       </ul>
+		 *     </li>
+		 *   </ul>
+		 *  @type string
+		 *  @default lfrtip <i>(when `jQueryUI` is false)</i> <b>or</b>
+		 *    <"H"lfr>t<"F"ip> <i>(when `jQueryUI` is true)</i>
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.dom
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "dom": '&lt;"top"i&gt;rt&lt;"bottom"flp&gt;&lt;"clear"&gt;'
+		 *      } );
+		 *    } );
+		 */
+		"sDom": "lfrtip",
+	
+	
+		/**
+		 * Search delay option. This will throttle full table searches that use the
+		 * DataTables provided search input element (it does not effect calls to
+		 * `dt-api search()`, providing a delay before the search is made.
+		 *  @type integer
+		 *  @default 0
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.searchDelay
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "searchDelay": 200
+		 *      } );
+		 *    } )
+		 */
+		"searchDelay": null,
+	
+	
+		/**
+		 * DataTables features six different built-in options for the buttons to
+		 * display for pagination control:
+		 *
+		 * * `numbers` - Page number buttons only
+		 * * `simple` - 'Previous' and 'Next' buttons only
+		 * * 'simple_numbers` - 'Previous' and 'Next' buttons, plus page numbers
+		 * * `full` - 'First', 'Previous', 'Next' and 'Last' buttons
+		 * * `full_numbers` - 'First', 'Previous', 'Next' and 'Last' buttons, plus page numbers
+		 * * `first_last_numbers` - 'First' and 'Last' buttons, plus page numbers
+		 *  
+		 * Further methods can be added using {@link DataTable.ext.oPagination}.
+		 *  @type string
+		 *  @default simple_numbers
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.pagingType
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "pagingType": "full_numbers"
+		 *      } );
+		 *    } )
+		 */
+		"sPaginationType": "simple_numbers",
+	
+	
+		/**
+		 * Enable horizontal scrolling. When a table is too wide to fit into a
+		 * certain layout, or you have a large number of columns in the table, you
+		 * can enable x-scrolling to show the table in a viewport, which can be
+		 * scrolled. This property can be `true` which will allow the table to
+		 * scroll horizontally when needed, or any CSS unit, or a number (in which
+		 * case it will be treated as a pixel measurement). Setting as simply `true`
+		 * is recommended.
+		 *  @type boolean|string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.scrollX
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "scrollX": true,
+		 *        "scrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"sScrollX": "",
+	
+	
+		/**
+		 * This property can be used to force a DataTable to use more width than it
+		 * might otherwise do when x-scrolling is enabled. For example if you have a
+		 * table which requires to be well spaced, this parameter is useful for
+		 * "over-sizing" the table, and thus forcing scrolling. This property can by
+		 * any CSS unit, or a number (in which case it will be treated as a pixel
+		 * measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *
+		 *  @dtopt Options
+		 *  @name DataTable.defaults.scrollXInner
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "scrollX": "100%",
+		 *        "scrollXInner": "110%"
+		 *      } );
+		 *    } );
+		 */
+		"sScrollXInner": "",
+	
+	
+		/**
+		 * Enable vertical scrolling. Vertical scrolling will constrain the DataTable
+		 * to the given height, and enable scrolling for any data which overflows the
+		 * current viewport. This can be used as an alternative to paging to display
+		 * a lot of data in a small area (although paging and scrolling can both be
+		 * enabled at the same time). This property can be any CSS unit, or a number
+		 * (in which case it will be treated as a pixel measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *
+		 *  @dtopt Features
+		 *  @name DataTable.defaults.scrollY
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "scrollY": "200px",
+		 *        "paginate": false
+		 *      } );
+		 *    } );
+		 */
+		"sScrollY": "",
+	
+	
+		/**
+		 * __Deprecated__ The functionality provided by this parameter has now been
+		 * superseded by that provided through `ajax`, which should be used instead.
+		 *
+		 * Set the HTTP method that is used to make the Ajax call for server-side
+		 * processing or Ajax sourced data.
+		 *  @type string
+		 *  @default GET
+		 *
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 *  @name DataTable.defaults.serverMethod
+		 *
+		 *  @deprecated 1.10. Please use `ajax` for this functionality now.
+		 */
+		"sServerMethod": "GET",
+	
+	
+		/**
+		 * DataTables makes use of renderers when displaying HTML elements for
+		 * a table. These renderers can be added or modified by plug-ins to
+		 * generate suitable mark-up for a site. For example the Bootstrap
+		 * integration plug-in for DataTables uses a paging button renderer to
+		 * display pagination buttons in the mark-up required by Bootstrap.
+		 *
+		 * For further information about the renderers available see
+		 * DataTable.ext.renderer
+		 *  @type string|object
+		 *  @default null
+		 *
+		 *  @name DataTable.defaults.renderer
+		 *
+		 */
+		"renderer": null,
+	
+	
+		/**
+		 * Set the data property name that DataTables should use to get a row's id
+		 * to set as the `id` property in the node.
+		 *  @type string
+		 *  @default DT_RowId
+		 *
+		 *  @name DataTable.defaults.rowId
+		 */
+		"rowId": "DT_RowId"
+	};
+	
+	_fnHungarianMap( DataTable.defaults );
+	
+	
+	
+	/*
+	 * Developer note - See note in model.defaults.js about the use of Hungarian
+	 * notation and camel case.
+	 */
+	
+	/**
+	 * Column options that can be given to DataTables at initialisation time.
+	 *  @namespace
+	 */
+	DataTable.defaults.column = {
+		/**
+		 * Define which column(s) an order will occur on for this column. This
+		 * allows a column's ordering to take multiple columns into account when
+		 * doing a sort or use the data from a different column. For example first
+		 * name / last name columns make sense to do a multi-column sort over the
+		 * two columns.
+		 *  @type array|int
+		 *  @default null <i>Takes the value of the column index automatically</i>
+		 *
+		 *  @name DataTable.defaults.column.orderData
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "orderData": [ 0, 1 ], "targets": [ 0 ] },
+		 *          { "orderData": [ 1, 0 ], "targets": [ 1 ] },
+		 *          { "orderData": 2, "targets": [ 2 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "orderData": [ 0, 1 ] },
+		 *          { "orderData": [ 1, 0 ] },
+		 *          { "orderData": 2 },
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aDataSort": null,
+		"iDataSort": -1,
+	
+	
+		/**
+		 * You can control the default ordering direction, and even alter the
+		 * behaviour of the sort handler (i.e. only allow ascending ordering etc)
+		 * using this parameter.
+		 *  @type array
+		 *  @default [ 'asc', 'desc' ]
+		 *
+		 *  @name DataTable.defaults.column.orderSequence
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "orderSequence": [ "asc" ], "targets": [ 1 ] },
+		 *          { "orderSequence": [ "desc", "asc", "asc" ], "targets": [ 2 ] },
+		 *          { "orderSequence": [ "desc" ], "targets": [ 3 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          null,
+		 *          { "orderSequence": [ "asc" ] },
+		 *          { "orderSequence": [ "desc", "asc", "asc" ] },
+		 *          { "orderSequence": [ "desc" ] },
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"asSorting": [ 'asc', 'desc' ],
+	
+	
+		/**
+		 * Enable or disable filtering on the data in this column.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @name DataTable.defaults.column.searchable
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "searchable": false, "targets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "searchable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSearchable": true,
+	
+	
+		/**
+		 * Enable or disable ordering on this column.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @name DataTable.defaults.column.orderable
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "orderable": false, "targets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "orderable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSortable": true,
+	
+	
+		/**
+		 * Enable or disable the display of this column.
+		 *  @type boolean
+		 *  @default true
+		 *
+		 *  @name DataTable.defaults.column.visible
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "visible": false, "targets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "visible": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bVisible": true,
+	
+	
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} td The TD node that has been created
+		 *  @param {*} cellData The Data for the cell
+		 *  @param {array|object} rowData The data for the whole row
+		 *  @param {int} row The row index for the aoData data store
+		 *  @param {int} col The column index for aoColumns
+		 *
+		 *  @name DataTable.defaults.column.createdCell
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [3],
+		 *          "createdCell": function (td, cellData, rowData, row, col) {
+		 *            if ( cellData == "1.7" ) {
+		 *              $(td).css('color', 'blue')
+		 *            }
+		 *          }
+		 *        } ]
+		 *      });
+		 *    } );
+		 */
+		"fnCreatedCell": null,
+	
+	
+		/**
+		 * This parameter has been replaced by `data` in DataTables to ensure naming
+		 * consistency. `dataProp` can still be used, as there is backwards
+		 * compatibility in DataTables for this option, but it is strongly
+		 * recommended that you use `data` in preference to `dataProp`.
+		 *  @name DataTable.defaults.column.dataProp
+		 */
+	
+	
+		/**
+		 * This property can be used to read data from any data source property,
+		 * including deeply nested objects / properties. `data` can be given in a
+		 * number of different ways which effect its behaviour:
+		 *
+		 * * `integer` - treated as an array index for the data source. This is the
+		 *   default that DataTables uses (incrementally increased for each column).
+		 * * `string` - read an object property from the data source. There are
+		 *   three 'special' options that can be used in the string to alter how
+		 *   DataTables reads the data from the source object:
+		 *    * `.` - Dotted Javascript notation. Just as you use a `.` in
+		 *      Javascript to read from nested objects, so to can the options
+		 *      specified in `data`. For example: `browser.version` or
+		 *      `browser.name`. If your object parameter name contains a period, use
+		 *      `\\` to escape it - i.e. `first\\.name`.
+		 *    * `[]` - Array notation. DataTables can automatically combine data
+		 *      from and array source, joining the data with the characters provided
+		 *      between the two brackets. For example: `name[, ]` would provide a
+		 *      comma-space separated list from the source array. If no characters
+		 *      are provided between the brackets, the original array source is
+		 *      returned.
+		 *    * `()` - Function notation. Adding `()` to the end of a parameter will
+		 *      execute a function of the name given. For example: `browser()` for a
+		 *      simple function on the data source, `browser.version()` for a
+		 *      function in a nested property or even `browser().version` to get an
+		 *      object property if the function called returns an object. Note that
+		 *      function notation is recommended for use in `render` rather than
+		 *      `data` as it is much simpler to use as a renderer.
+		 * * `null` - use the original data source for the row rather than plucking
+		 *   data directly from it. This action has effects on two other
+		 *   initialisation options:
+		 *    * `defaultContent` - When null is given as the `data` option and
+		 *      `defaultContent` is specified for the column, the value defined by
+		 *      `defaultContent` will be used for the cell.
+		 *    * `render` - When null is used for the `data` option and the `render`
+		 *      option is specified for the column, the whole data source for the
+		 *      row is used for the renderer.
+		 * * `function` - the function given will be executed whenever DataTables
+		 *   needs to set or get the data for a cell in the column. The function
+		 *   takes three parameters:
+		 *    * Parameters:
+		 *      * `{array|object}` The data source for the row
+		 *      * `{string}` The type call data requested - this will be 'set' when
+		 *        setting data or 'filter', 'display', 'type', 'sort' or undefined
+		 *        when gathering data. Note that when `undefined` is given for the
+		 *        type DataTables expects to get the raw data for the object back<
+		 *      * `{*}` Data to set when the second parameter is 'set'.
+		 *    * Return:
+		 *      * The return value from the function is not required when 'set' is
+		 *        the type of call, but otherwise the return is what will be used
+		 *        for the data requested.
+		 *
+		 * Note that `data` is a getter and setter option. If you just require
+		 * formatting of data for output, you will likely want to use `render` which
+		 * is simply a getter and thus simpler to use.
+		 *
+		 * Note that prior to DataTables 1.9.2 `data` was called `mDataProp`. The
+		 * name change reflects the flexibility of this property and is consistent
+		 * with the naming of mRender. If 'mDataProp' is given, then it will still
+		 * be used by DataTables, as it automatically maps the old name to the new
+		 * if required.
+		 *
+		 *  @type string|int|function|null
+		 *  @default null <i>Use automatically calculated column index</i>
+		 *
+		 *  @name DataTable.defaults.column.data
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Read table data from objects
+		 *    // JSON structure for each row:
+		 *    //   {
+		 *    //      "engine": {value},
+		 *    //      "browser": {value},
+		 *    //      "platform": {value},
+		 *    //      "version": {value},
+		 *    //      "grade": {value}
+		 *    //   }
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "ajaxSource": "sources/objects.txt",
+		 *        "columns": [
+		 *          { "data": "engine" },
+		 *          { "data": "browser" },
+		 *          { "data": "platform" },
+		 *          { "data": "version" },
+		 *          { "data": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Read information from deeply nested objects
+		 *    // JSON structure for each row:
+		 *    //   {
+		 *    //      "engine": {value},
+		 *    //      "browser": {value},
+		 *    //      "platform": {
+		 *    //         "inner": {value}
+		 *    //      },
+		 *    //      "details": [
+		 *    //         {value}, {value}
+		 *    //      ]
+		 *    //   }
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "ajaxSource": "sources/deep.txt",
+		 *        "columns": [
+		 *          { "data": "engine" },
+		 *          { "data": "browser" },
+		 *          { "data": "platform.inner" },
+		 *          { "data": "platform.details.0" },
+		 *          { "data": "platform.details.1" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `data` as a function to provide different information for
+		 *    // sorting, filtering and display. In this case, currency (price)
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": function ( source, type, val ) {
+		 *            if (type === 'set') {
+		 *              source.price = val;
+		 *              // Store the computed dislay and filter values for efficiency
+		 *              source.price_display = val=="" ? "" : "$"+numberFormat(val);
+		 *              source.price_filter  = val=="" ? "" : "$"+numberFormat(val)+" "+val;
+		 *              return;
+		 *            }
+		 *            else if (type === 'display') {
+		 *              return source.price_display;
+		 *            }
+		 *            else if (type === 'filter') {
+		 *              return source.price_filter;
+		 *            }
+		 *            // 'sort', 'type' and undefined all just use the integer
+		 *            return source.price;
+		 *          }
+		 *        } ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using default content
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": null,
+		 *          "defaultContent": "Click to edit"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using array notation - outputting a list from an array
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": "name[, ]"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 *
+		 */
+		"mData": null,
+	
+	
+		/**
+		 * This property is the rendering partner to `data` and it is suggested that
+		 * when you want to manipulate data for display (including filtering,
+		 * sorting etc) without altering the underlying data for the table, use this
+		 * property. `render` can be considered to be the the read only companion to
+		 * `data` which is read / write (then as such more complex). Like `data`
+		 * this option can be given in a number of different ways to effect its
+		 * behaviour:
+		 *
+		 * * `integer` - treated as an array index for the data source. This is the
+		 *   default that DataTables uses (incrementally increased for each column).
+		 * * `string` - read an object property from the data source. There are
+		 *   three 'special' options that can be used in the string to alter how
+		 *   DataTables reads the data from the source object:
+		 *    * `.` - Dotted Javascript notation. Just as you use a `.` in
+		 *      Javascript to read from nested objects, so to can the options
+		 *      specified in `data`. For example: `browser.version` or
+		 *      `browser.name`. If your object parameter name contains a period, use
+		 *      `\\` to escape it - i.e. `first\\.name`.
+		 *    * `[]` - Array notation. DataTables can automatically combine data
+		 *      from and array source, joining the data with the characters provided
+		 *      between the two brackets. For example: `name[, ]` would provide a
+		 *      comma-space separated list from the source array. If no characters
+		 *      are provided between the brackets, the original array source is
+		 *      returned.
+		 *    * `()` - Function notation. Adding `()` to the end of a parameter will
+		 *      execute a function of the name given. For example: `browser()` for a
+		 *      simple function on the data source, `browser.version()` for a
+		 *      function in a nested property or even `browser().version` to get an
+		 *      object property if the function called returns an object.
+		 * * `object` - use different data for the different data types requested by
+		 *   DataTables ('filter', 'display', 'type' or 'sort'). The property names
+		 *   of the object is the data type the property refers to and the value can
+		 *   defined using an integer, string or function using the same rules as
+		 *   `render` normally does. Note that an `_` option _must_ be specified.
+		 *   This is the default value to use if you haven't specified a value for
+		 *   the data type requested by DataTables.
+		 * * `function` - the function given will be executed whenever DataTables
+		 *   needs to set or get the data for a cell in the column. The function
+		 *   takes three parameters:
+		 *    * Parameters:
+		 *      * {array|object} The data source for the row (based on `data`)
+		 *      * {string} The type call data requested - this will be 'filter',
+		 *        'display', 'type' or 'sort'.
+		 *      * {array|object} The full data source for the row (not based on
+		 *        `data`)
+		 *    * Return:
+		 *      * The return value from the function is what will be used for the
+		 *        data requested.
+		 *
+		 *  @type string|int|function|object|null
+		 *  @default null Use the data source value.
+		 *
+		 *  @name DataTable.defaults.column.render
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Create a comma separated list from an array of objects
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "ajaxSource": "sources/deep.txt",
+		 *        "columns": [
+		 *          { "data": "engine" },
+		 *          { "data": "browser" },
+		 *          {
+		 *            "data": "platform",
+		 *            "render": "[, ].name"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Execute a function to obtain data
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": null, // Use the full data source object for the renderer's source
+		 *          "render": "browserName()"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // As an object, extracting different data for the different types
+		 *    // This would be used with a data source such as:
+		 *    //   { "phone": 5552368, "phone_filter": "5552368 555-2368", "phone_display": "555-2368" }
+		 *    // Here the `phone` integer is used for sorting and type detection, while `phone_filter`
+		 *    // (which has both forms) is used for filtering for if a user inputs either format, while
+		 *    // the formatted phone number is the one that is shown in the table.
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": null, // Use the full data source object for the renderer's source
+		 *          "render": {
+		 *            "_": "phone",
+		 *            "filter": "phone_filter",
+		 *            "display": "phone_display"
+		 *          }
+		 *        } ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Use as a function to create a link from the data source
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "data": "download_link",
+		 *          "render": function ( data, type, full ) {
+		 *            return '<a href="'+data+'">Download</a>';
+		 *          }
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"mRender": null,
+	
+	
+		/**
+		 * Change the cell type created for the column - either TD cells or TH cells. This
+		 * can be useful as TH cells have semantic meaning in the table body, allowing them
+		 * to act as a header for a row (you may wish to add scope='row' to the TH elements).
+		 *  @type string
+		 *  @default td
+		 *
+		 *  @name DataTable.defaults.column.cellType
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Make the first column use TH cells
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [ {
+		 *          "targets": [ 0 ],
+		 *          "cellType": "th"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"sCellType": "td",
+	
+	
+		/**
+		 * Class to give to each cell in this column.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *
+		 *  @name DataTable.defaults.column.class
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "class": "my_class", "targets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "class": "my_class" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sClass": "",
+	
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 * Generally you shouldn't need this!
+		 *  @type string
+		 *  @default <i>Empty string<i>
+		 *
+		 *  @name DataTable.defaults.column.contentPadding
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "contentPadding": "mmm"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sContentPadding": "",
+	
+	
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because `data`
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 *
+		 *  @name DataTable.defaults.column.defaultContent
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          {
+		 *            "data": null,
+		 *            "defaultContent": "Edit",
+		 *            "targets": [ -1 ]
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "data": null,
+		 *            "defaultContent": "Edit"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sDefaultContent": null,
+	
+	
+		/**
+		 * This parameter is only used in DataTables' server-side processing. It can
+		 * be exceptionally useful to know what columns are being displayed on the
+		 * client side, and to map these to database fields. When defined, the names
+		 * also allow DataTables to reorder information from the server if it comes
+		 * back in an unexpected order (i.e. if you switch your columns around on the
+		 * client-side, your server-side code does not also need updating).
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *
+		 *  @name DataTable.defaults.column.name
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "name": "engine", "targets": [ 0 ] },
+		 *          { "name": "browser", "targets": [ 1 ] },
+		 *          { "name": "platform", "targets": [ 2 ] },
+		 *          { "name": "version", "targets": [ 3 ] },
+		 *          { "name": "grade", "targets": [ 4 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "name": "engine" },
+		 *          { "name": "browser" },
+		 *          { "name": "platform" },
+		 *          { "name": "version" },
+		 *          { "name": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sName": "",
+	
+	
+		/**
+		 * Defines a data source type for the ordering which can be used to read
+		 * real-time information from the table (updating the internally cached
+		 * version) prior to ordering. This allows ordering to occur on user
+		 * editable elements such as form inputs.
+		 *  @type string
+		 *  @default std
+		 *
+		 *  @name DataTable.defaults.column.orderDataType
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "orderDataType": "dom-text", "targets": [ 2, 3 ] },
+		 *          { "type": "numeric", "targets": [ 3 ] },
+		 *          { "orderDataType": "dom-select", "targets": [ 4 ] },
+		 *          { "orderDataType": "dom-checkbox", "targets": [ 5 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          null,
+		 *          null,
+		 *          { "orderDataType": "dom-text" },
+		 *          { "orderDataType": "dom-text", "type": "numeric" },
+		 *          { "orderDataType": "dom-select" },
+		 *          { "orderDataType": "dom-checkbox" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sSortDataType": "std",
+	
+	
+		/**
+		 * The title of this column.
+		 *  @type string
+		 *  @default null <i>Derived from the 'TH' value for this column in the
+		 *    original HTML table.</i>
+		 *
+		 *  @name DataTable.defaults.column.title
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "title": "My column title", "targets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "title": "My column title" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sTitle": null,
+	
+	
+		/**
+		 * The type allows you to specify how the data for this column will be
+		 * ordered. Four types (string, numeric, date and html (which will strip
+		 * HTML tags before ordering)) are currently available. Note that only date
+		 * formats understood by Javascript's Date() object will be accepted as type
+		 * date. For example: "Mar 26, 2008 5:03 PM". May take the values: 'string',
+		 * 'numeric', 'date' or 'html' (by default). Further types can be adding
+		 * through plug-ins.
+		 *  @type string
+		 *  @default null <i>Auto-detected from raw data</i>
+		 *
+		 *  @name DataTable.defaults.column.type
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "type": "html", "targets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "type": "html" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sType": null,
+	
+	
+		/**
+		 * Defining the width of the column, this parameter may take any CSS value
+		 * (3em, 20px etc). DataTables applies 'smart' widths to columns which have not
+		 * been given a specific width through this interface ensuring that the table
+		 * remains readable.
+		 *  @type string
+		 *  @default null <i>Automatic</i>
+		 *
+		 *  @name DataTable.defaults.column.width
+		 *  @dtopt Columns
+		 *
+		 *  @example
+		 *    // Using `columnDefs`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columnDefs": [
+		 *          { "width": "20%", "targets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Using `columns`
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "columns": [
+		 *          { "width": "20%" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sWidth": null
+	};
+	
+	_fnHungarianMap( DataTable.defaults.column );
+	
+	
+	
+	/**
+	 * DataTables settings object - this holds all the information needed for a
+	 * given table, including configuration, data and current application of the
+	 * table options. DataTables does not have a single instance for each DataTable
+	 * with the settings attached to that instance, but rather instances of the
+	 * DataTable "class" are created on-the-fly as needed (typically by a
+	 * $().dataTable() call) and the settings object is then applied to that
+	 * instance.
+	 *
+	 * Note that this object is related to {@link DataTable.defaults} but this
+	 * one is the internal data store for DataTables's cache of columns. It should
+	 * NOT be manipulated outside of DataTables. Any configuration should be done
+	 * through the initialisation options.
+	 *  @namespace
+	 *  @todo Really should attach the settings object to individual instances so we
+	 *    don't need to create new instances on each $().dataTable() call (if the
+	 *    table already exists). It would also save passing oSettings around and
+	 *    into every single function. However, this is a very significant
+	 *    architecture change for DataTables and will almost certainly break
+	 *    backwards compatibility with older installations. This is something that
+	 *    will be done in 2.0.
+	 */
+	DataTable.models.oSettings = {
+		/**
+		 * Primary features of DataTables and their enablement state.
+		 *  @namespace
+		 */
+		"oFeatures": {
+	
+			/**
+			 * Flag to say if DataTables should automatically try to calculate the
+			 * optimum table and columns widths (true) or not (false).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bAutoWidth": null,
+	
+			/**
+			 * Delay the creation of TR and TD elements until they are actually
+			 * needed by a driven page draw. This can give a significant speed
+			 * increase for Ajax source and Javascript source data, but makes no
+			 * difference at all fro DOM and server-side processing tables.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bDeferRender": null,
+	
+			/**
+			 * Enable filtering on the table or not. Note that if this is disabled
+			 * then there is no filtering at all on the table, including fnFilter.
+			 * To just remove the filtering input use sDom and remove the 'f' option.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bFilter": null,
+	
+			/**
+			 * Table information element (the 'Showing x of y records' div) enable
+			 * flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bInfo": null,
+	
+			/**
+			 * Present a user control allowing the end user to change the page size
+			 * when pagination is enabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bLengthChange": null,
+	
+			/**
+			 * Pagination enabled or not. Note that if this is disabled then length
+			 * changing must also be disabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bPaginate": null,
+	
+			/**
+			 * Processing indicator enable flag whenever DataTables is enacting a
+			 * user request - typically an Ajax request for server-side processing.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bProcessing": null,
+	
+			/**
+			 * Server-side processing enabled flag - when enabled DataTables will
+			 * get all data from the server for every draw - there is no filtering,
+			 * sorting or paging done on the client-side.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bServerSide": null,
+	
+			/**
+			 * Sorting enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSort": null,
+	
+			/**
+			 * Multi-column sorting
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSortMulti": null,
+	
+			/**
+			 * Apply a class to the columns which are being sorted to provide a
+			 * visual highlight or not. This can slow things down when enabled since
+			 * there is a lot of DOM interaction.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSortClasses": null,
+	
+			/**
+			 * State saving enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bStateSave": null
+		},
+	
+	
+		/**
+		 * Scrolling settings for a table.
+		 *  @namespace
+		 */
+		"oScroll": {
+			/**
+			 * When the table is shorter in height than sScrollY, collapse the
+			 * table container down to the height of the table (when true).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bCollapse": null,
+	
+			/**
+			 * Width of the scrollbar for the web-browser's platform. Calculated
+			 * during table initialisation.
+			 *  @type int
+			 *  @default 0
+			 */
+			"iBarWidth": 0,
+	
+			/**
+			 * Viewport width for horizontal scrolling. Horizontal scrolling is
+			 * disabled if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sX": null,
+	
+			/**
+			 * Width to expand the table to when using x-scrolling. Typically you
+			 * should not need to use this.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 *  @deprecated
+			 */
+			"sXInner": null,
+	
+			/**
+			 * Viewport height for vertical scrolling. Vertical scrolling is disabled
+			 * if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sY": null
+		},
+	
+		/**
+		 * Language information for the table.
+		 *  @namespace
+		 *  @extends DataTable.defaults.oLanguage
+		 */
+		"oLanguage": {
+			/**
+			 * Information callback function. See
+			 * {@link DataTable.defaults.fnInfoCallback}
+			 *  @type function
+			 *  @default null
+			 */
+			"fnInfoCallback": null
+		},
+	
+		/**
+		 * Browser support parameters
+		 *  @namespace
+		 */
+		"oBrowser": {
+			/**
+			 * Indicate if the browser incorrectly calculates width:100% inside a
+			 * scrolling element (IE6/7)
+			 *  @type boolean
+			 *  @default false
+			 */
+			"bScrollOversize": false,
+	
+			/**
+			 * Determine if the vertical scrollbar is on the right or left of the
+			 * scrolling container - needed for rtl language layout, although not
+			 * all browsers move the scrollbar (Safari).
+			 *  @type boolean
+			 *  @default false
+			 */
+			"bScrollbarLeft": false,
+	
+			/**
+			 * Flag for if `getBoundingClientRect` is fully supported or not
+			 *  @type boolean
+			 *  @default false
+			 */
+			"bBounding": false,
+	
+			/**
+			 * Browser scrollbar width
+			 *  @type integer
+			 *  @default 0
+			 */
+			"barWidth": 0
+		},
+	
+	
+		"ajax": null,
+	
+	
+		/**
+		 * Array referencing the nodes which are used for the features. The
+		 * parameters of this object match what is allowed by sDom - i.e.
+		 *   <ul>
+		 *     <li>'l' - Length changing</li>
+		 *     <li>'f' - Filtering input</li>
+		 *     <li>'t' - The table!</li>
+		 *     <li>'i' - Information</li>
+		 *     <li>'p' - Pagination</li>
+		 *     <li>'r' - pRocessing</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aanFeatures": [],
+	
+		/**
+		 * Store data information - see {@link DataTable.models.oRow} for detailed
+		 * information.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoData": [],
+	
+		/**
+		 * Array of indexes which are in the current display (after filtering etc)
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplay": [],
+	
+		/**
+		 * Array of indexes for display - no filtering
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplayMaster": [],
+	
+		/**
+		 * Map of row ids to data indexes
+		 *  @type object
+		 *  @default {}
+		 */
+		"aIds": {},
+	
+		/**
+		 * Store information about each column that is in use
+		 *  @type array
+		 *  @default []
+		 */
+		"aoColumns": [],
+	
+		/**
+		 * Store information about the table's header
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeader": [],
+	
+		/**
+		 * Store information about the table's footer
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooter": [],
+	
+		/**
+		 * Store the applied global search information in case we want to force a
+		 * research or compare the old search to a new one.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 */
+		"oPreviousSearch": {},
+	
+		/**
+		 * Store the applied search for each column - see
+		 * {@link DataTable.models.oSearch} for the format that is used for the
+		 * filtering information for each column.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreSearchCols": [],
+	
+		/**
+		 * Sorting that is applied to the table. Note that the inner arrays are
+		 * used in the following manner:
+		 * <ul>
+		 *   <li>Index 0 - column number</li>
+		 *   <li>Index 1 - current sorting direction</li>
+		 * </ul>
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @todo These inner arrays should really be objects
+		 */
+		"aaSorting": null,
+	
+		/**
+		 * Sorting that is always applied to the table (i.e. prefixed in front of
+		 * aaSorting).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"aaSortingFixed": [],
+	
+		/**
+		 * Classes to use for the striping of a table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"asStripeClasses": null,
+	
+		/**
+		 * If restoring a table - we should restore its striping classes as well
+		 *  @type array
+		 *  @default []
+		 */
+		"asDestroyStripes": [],
+	
+		/**
+		 * If restoring a table - we should restore its width
+		 *  @type int
+		 *  @default 0
+		 */
+		"sDestroyWidth": 0,
+	
+		/**
+		 * Callback functions array for every time a row is inserted (i.e. on a draw).
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCallback": [],
+	
+		/**
+		 * Callback functions for the header on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeaderCallback": [],
+	
+		/**
+		 * Callback function for the footer on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooterCallback": [],
+	
+		/**
+		 * Array of callback functions for draw callback functions
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDrawCallback": [],
+	
+		/**
+		 * Array of callback functions for row created function
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCreatedCallback": [],
+	
+		/**
+		 * Callback functions for just before the table is redrawn. A return of
+		 * false will be used to cancel the draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreDrawCallback": [],
+	
+		/**
+		 * Callback functions for when the table has been initialised.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoInitComplete": [],
+	
+	
+		/**
+		 * Callbacks for modifying the settings to be stored for state saving, prior to
+		 * saving state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSaveParams": [],
+	
+		/**
+		 * Callbacks for modifying the settings that have been stored for state saving
+		 * prior to using the stored values to restore the state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoadParams": [],
+	
+		/**
+		 * Callbacks for operating on the settings object once the saved state has been
+		 * loaded
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoaded": [],
+	
+		/**
+		 * Cache the table ID for quick access
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sTableId": "",
+	
+		/**
+		 * The TABLE node for the main table
+		 *  @type node
+		 *  @default null
+		 */
+		"nTable": null,
+	
+		/**
+		 * Permanent ref to the thead element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTHead": null,
+	
+		/**
+		 * Permanent ref to the tfoot element - if it exists
+		 *  @type node
+		 *  @default null
+		 */
+		"nTFoot": null,
+	
+		/**
+		 * Permanent ref to the tbody element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTBody": null,
+	
+		/**
+		 * Cache the wrapper node (contains all DataTables controlled elements)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTableWrapper": null,
+	
+		/**
+		 * Indicate if when using server-side processing the loading of data
+		 * should be deferred until the second draw.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDeferLoading": false,
+	
+		/**
+		 * Indicate if all required information has been read in
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bInitialised": false,
+	
+		/**
+		 * Information about open rows. Each object in the array has the parameters
+		 * 'nTr' and 'nParent'
+		 *  @type array
+		 *  @default []
+		 */
+		"aoOpenRows": [],
+	
+		/**
+		 * Dictate the positioning of DataTables' control elements - see
+		 * {@link DataTable.model.oInit.sDom}.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sDom": null,
+	
+		/**
+		 * Search delay (in mS)
+		 *  @type integer
+		 *  @default null
+		 */
+		"searchDelay": null,
+	
+		/**
+		 * Which type of pagination should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default two_button
+		 */
+		"sPaginationType": "two_button",
+	
+		/**
+		 * The state duration (for `stateSave`) in seconds.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type int
+		 *  @default 0
+		 */
+		"iStateDuration": 0,
+	
+		/**
+		 * Array of callback functions for state saving. Each array element is an
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings
+		 *       and the JSON string to save that has been thus far created. Returns
+		 *       a JSON string to be inserted into a json object
+		 *       (i.e. '"param": [ 0, 1, 2]')</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSave": [],
+	
+		/**
+		 * Array of callback functions for state loading. Each array element is an
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings
+		 *       and the object stored. May return false to cancel state loading</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoad": [],
+	
+		/**
+		 * State that was saved. Useful for back reference
+		 *  @type object
+		 *  @default null
+		 */
+		"oSavedState": null,
+	
+		/**
+		 * State that was loaded. Useful for back reference
+		 *  @type object
+		 *  @default null
+		 */
+		"oLoadedState": null,
+	
+		/**
+		 * Source url for AJAX data for the table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sAjaxSource": null,
+	
+		/**
+		 * Property from a given object from which to read the table data from. This
+		 * can be an empty string (when not server-side processing), in which case
+		 * it is  assumed an an array is given directly.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sAjaxDataProp": null,
+	
+		/**
+		 * Note if draw should be blocked while getting data
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bAjaxDataGet": true,
+	
+		/**
+		 * The last jQuery XHR object that was used for server-side data gathering.
+		 * This can be used for working with the XHR information in one of the
+		 * callbacks
+		 *  @type object
+		 *  @default null
+		 */
+		"jqXHR": null,
+	
+		/**
+		 * JSON returned from the server in the last Ajax request
+		 *  @type object
+		 *  @default undefined
+		 */
+		"json": undefined,
+	
+		/**
+		 * Data submitted as part of the last Ajax request
+		 *  @type object
+		 *  @default undefined
+		 */
+		"oAjaxData": undefined,
+	
+		/**
+		 * Function to get the server-side data.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnServerData": null,
+	
+		/**
+		 * Functions which are called prior to sending an Ajax request so extra
+		 * parameters can easily be sent to the server
+		 *  @type array
+		 *  @default []
+		 */
+		"aoServerParams": [],
+	
+		/**
+		 * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if
+		 * required).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sServerMethod": null,
+	
+		/**
+		 * Format numbers for display.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnFormatNumber": null,
+	
+		/**
+		 * List of options that can be used for the user selectable length menu.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"aLengthMenu": null,
+	
+		/**
+		 * Counter for the draws that the table does. Also used as a tracker for
+		 * server-side processing
+		 *  @type int
+		 *  @default 0
+		 */
+		"iDraw": 0,
+	
+		/**
+		 * Indicate if a redraw is being done - useful for Ajax
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDrawing": false,
+	
+		/**
+		 * Draw index (iDraw) of the last error when parsing the returned data
+		 *  @type int
+		 *  @default -1
+		 */
+		"iDrawError": -1,
+	
+		/**
+		 * Paging display length
+		 *  @type int
+		 *  @default 10
+		 */
+		"_iDisplayLength": 10,
+	
+		/**
+		 * Paging start point - aiDisplay index
+		 *  @type int
+		 *  @default 0
+		 */
+		"_iDisplayStart": 0,
+	
+		/**
+		 * Server-side processing - number of records in the result set
+		 * (i.e. before filtering), Use fnRecordsTotal rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type int
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsTotal": 0,
+	
+		/**
+		 * Server-side processing - number of records in the current display set
+		 * (i.e. after filtering). Use fnRecordsDisplay rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type boolean
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsDisplay": 0,
+	
+		/**
+		 * Flag to indicate if jQuery UI marking and classes should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bJUI": null,
+	
+		/**
+		 * The classes to use for the table
+		 *  @type object
+		 *  @default {}
+		 */
+		"oClasses": {},
+	
+		/**
+		 * Flag attached to the settings object so you can check in the draw
+		 * callback if filtering has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bFiltered": false,
+	
+		/**
+		 * Flag attached to the settings object so you can check in the draw
+		 * callback if sorting has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bSorted": false,
+	
+		/**
+		 * Indicate that if multiple rows are in the header and there is more than
+		 * one unique cell per column, if the top one (true) or bottom one (false)
+		 * should be used for sorting / title by DataTables.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bSortCellsTop": null,
+	
+		/**
+		 * Initialisation object that is used for the table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInit": null,
+	
+		/**
+		 * Destroy callback functions - for plug-ins to attach themselves to the
+		 * destroy so they can clean up markup and events.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDestroyCallback": [],
+	
+	
+		/**
+		 * Get the number of records in the current record set, before filtering
+		 *  @type function
+		 */
+		"fnRecordsTotal": function ()
+		{
+			return _fnDataSource( this ) == 'ssp' ?
+				this._iRecordsTotal * 1 :
+				this.aiDisplayMaster.length;
+		},
+	
+		/**
+		 * Get the number of records in the current record set, after filtering
+		 *  @type function
+		 */
+		"fnRecordsDisplay": function ()
+		{
+			return _fnDataSource( this ) == 'ssp' ?
+				this._iRecordsDisplay * 1 :
+				this.aiDisplay.length;
+		},
+	
+		/**
+		 * Get the display end point - aiDisplay index
+		 *  @type function
+		 */
+		"fnDisplayEnd": function ()
+		{
+			var
+				len      = this._iDisplayLength,
+				start    = this._iDisplayStart,
+				calc     = start + len,
+				records  = this.aiDisplay.length,
+				features = this.oFeatures,
+				paginate = features.bPaginate;
+	
+			if ( features.bServerSide ) {
+				return paginate === false || len === -1 ?
+					start + records :
+					Math.min( start+len, this._iRecordsDisplay );
+			}
+			else {
+				return ! paginate || calc>records || len===-1 ?
+					records :
+					calc;
+			}
+		},
+	
+		/**
+		 * The DataTables object for this table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInstance": null,
+	
+		/**
+		 * Unique identifier for each instance of the DataTables object. If there
+		 * is an ID on the table node, then it takes that value, otherwise an
+		 * incrementing internal counter is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sInstance": null,
+	
+		/**
+		 * tabindex attribute value that is added to DataTables control elements, allowing
+		 * keyboard navigation of the table and its controls.
+		 */
+		"iTabIndex": 0,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollHead": null,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollFoot": null,
+	
+		/**
+		 * Last applied sort
+		 *  @type array
+		 *  @default []
+		 */
+		"aLastSort": [],
+	
+		/**
+		 * Stored plug-in instances
+		 *  @type object
+		 *  @default {}
+		 */
+		"oPlugins": {},
+	
+		/**
+		 * Function used to get a row's id from the row's data
+		 *  @type function
+		 *  @default null
+		 */
+		"rowIdFn": null,
+	
+		/**
+		 * Data location where to store a row's id
+		 *  @type string
+		 *  @default null
+		 */
+		"rowId": null
+	};
+
+	/**
+	 * Extension object for DataTables that is used to provide all extension
+	 * options.
+	 *
+	 * Note that the `DataTable.ext` object is available through
+	 * `jQuery.fn.dataTable.ext` where it may be accessed and manipulated. It is
+	 * also aliased to `jQuery.fn.dataTableExt` for historic reasons.
+	 *  @namespace
+	 *  @extends DataTable.models.ext
+	 */
+	
+	
+	/**
+	 * DataTables extensions
+	 * 
+	 * This namespace acts as a collection area for plug-ins that can be used to
+	 * extend DataTables capabilities. Indeed many of the build in methods
+	 * use this method to provide their own capabilities (sorting methods for
+	 * example).
+	 *
+	 * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy
+	 * reasons
+	 *
+	 *  @namespace
+	 */
+	DataTable.ext = _ext = {
+		/**
+		 * Buttons. For use with the Buttons extension for DataTables. This is
+		 * defined here so other extensions can define buttons regardless of load
+		 * order. It is _not_ used by DataTables core.
+		 *
+		 *  @type object
+		 *  @default {}
+		 */
+		buttons: {},
+	
+	
+		/**
+		 * Element class names
+		 *
+		 *  @type object
+		 *  @default {}
+		 */
+		classes: {},
+	
+	
+		/**
+		 * DataTables build type (expanded by the download builder)
+		 *
+		 *  @type string
+		 */
+		build:"dt/dt-1.10.15/cr-1.3.3/fc-3.2.2/fh-3.1.2/r-2.1.1",
+	
+	
+		/**
+		 * Error reporting.
+		 * 
+		 * How should DataTables report an error. Can take the value 'alert',
+		 * 'throw', 'none' or a function.
+		 *
+		 *  @type string|function
+		 *  @default alert
+		 */
+		errMode: "alert",
+	
+	
+		/**
+		 * Feature plug-ins.
+		 * 
+		 * This is an array of objects which describe the feature plug-ins that are
+		 * available to DataTables. These feature plug-ins are then available for
+		 * use through the `dom` initialisation option.
+		 * 
+		 * Each feature plug-in is described by an object which must have the
+		 * following properties:
+		 * 
+		 * * `fnInit` - function that is used to initialise the plug-in,
+		 * * `cFeature` - a character so the feature can be enabled by the `dom`
+		 *   instillation option. This is case sensitive.
+		 *
+		 * The `fnInit` function has the following input parameters:
+		 *
+		 * 1. `{object}` DataTables settings object: see
+		 *    {@link DataTable.models.oSettings}
+		 *
+		 * And the following return is expected:
+		 * 
+		 * * {node|null} The element which contains your feature. Note that the
+		 *   return may also be void if your plug-in does not require to inject any
+		 *   DOM elements into DataTables control (`dom`) - for example this might
+		 *   be useful when developing a plug-in which allows table control via
+		 *   keyboard entry
+		 *
+		 *  @type array
+		 *
+		 *  @example
+		 *    $.fn.dataTable.ext.features.push( {
+		 *      "fnInit": function( oSettings ) {
+		 *        return new TableTools( { "oDTSettings": oSettings } );
+		 *      },
+		 *      "cFeature": "T"
+		 *    } );
+		 */
+		feature: [],
+	
+	
+		/**
+		 * Row searching.
+		 * 
+		 * This method of searching is complimentary to the default type based
+		 * searching, and a lot more comprehensive as it allows you complete control
+		 * over the searching logic. Each element in this array is a function
+		 * (parameters described below) that is called for every row in the table,
+		 * and your logic decides if it should be included in the searching data set
+		 * or not.
+		 *
+		 * Searching functions have the following input parameters:
+		 *
+		 * 1. `{object}` DataTables settings object: see
+		 *    {@link DataTable.models.oSettings}
+		 * 2. `{array|object}` Data for the row to be processed (same as the
+		 *    original format that was passed in as the data source, or an array
+		 *    from a DOM data source
+		 * 3. `{int}` Row index ({@link DataTable.models.oSettings.aoData}), which
+		 *    can be useful to retrieve the `TR` element if you need DOM interaction.
+		 *
+		 * And the following return is expected:
+		 *
+		 * * {boolean} Include the row in the searched result set (true) or not
+		 *   (false)
+		 *
+		 * Note that as with the main search ability in DataTables, technically this
+		 * is "filtering", since it is subtractive. However, for consistency in
+		 * naming we call it searching here.
+		 *
+		 *  @type array
+		 *  @default []
+		 *
+		 *  @example
+		 *    // The following example shows custom search being applied to the
+		 *    // fourth column (i.e. the data[3] index) based on two input values
+		 *    // from the end-user, matching the data in a certain range.
+		 *    $.fn.dataTable.ext.search.push(
+		 *      function( settings, data, dataIndex ) {
+		 *        var min = document.getElementById('min').value * 1;
+		 *        var max = document.getElementById('max').value * 1;
+		 *        var version = data[3] == "-" ? 0 : data[3]*1;
+		 *
+		 *        if ( min == "" && max == "" ) {
+		 *          return true;
+		 *        }
+		 *        else if ( min == "" && version < max ) {
+		 *          return true;
+		 *        }
+		 *        else if ( min < version && "" == max ) {
+		 *          return true;
+		 *        }
+		 *        else if ( min < version && version < max ) {
+		 *          return true;
+		 *        }
+		 *        return false;
+		 *      }
+		 *    );
+		 */
+		search: [],
+	
+	
+		/**
+		 * Selector extensions
+		 *
+		 * The `selector` option can be used to extend the options available for the
+		 * selector modifier options (`selector-modifier` object data type) that
+		 * each of the three built in selector types offer (row, column and cell +
+		 * their plural counterparts). For example the Select extension uses this
+		 * mechanism to provide an option to select only rows, columns and cells
+		 * that have been marked as selected by the end user (`{selected: true}`),
+		 * which can be used in conjunction with the existing built in selector
+		 * options.
+		 *
+		 * Each property is an array to which functions can be pushed. The functions
+		 * take three attributes:
+		 *
+		 * * Settings object for the host table
+		 * * Options object (`selector-modifier` object type)
+		 * * Array of selected item indexes
+		 *
+		 * The return is an array of the resulting item indexes after the custom
+		 * selector has been applied.
+		 *
+		 *  @type object
+		 */
+		selector: {
+			cell: [],
+			column: [],
+			row: []
+		},
+	
+	
+		/**
+		 * Internal functions, exposed for used in plug-ins.
+		 * 
+		 * Please note that you should not need to use the internal methods for
+		 * anything other than a plug-in (and even then, try to avoid if possible).
+		 * The internal function may change between releases.
+		 *
+		 *  @type object
+		 *  @default {}
+		 */
+		internal: {},
+	
+	
+		/**
+		 * Legacy configuration options. Enable and disable legacy options that
+		 * are available in DataTables.
+		 *
+		 *  @type object
+		 */
+		legacy: {
+			/**
+			 * Enable / disable DataTables 1.9 compatible server-side processing
+			 * requests
+			 *
+			 *  @type boolean
+			 *  @default null
+			 */
+			ajax: null
+		},
+	
+	
+		/**
+		 * Pagination plug-in methods.
+		 * 
+		 * Each entry in this object is a function and defines which buttons should
+		 * be shown by the pagination rendering method that is used for the table:
+		 * {@link DataTable.ext.renderer.pageButton}. The renderer addresses how the
+		 * buttons are displayed in the document, while the functions here tell it
+		 * what buttons to display. This is done by returning an array of button
+		 * descriptions (what each button will do).
+		 *
+		 * Pagination types (the four built in options and any additional plug-in
+		 * options defined here) can be used through the `paginationType`
+		 * initialisation parameter.
+		 *
+		 * The functions defined take two parameters:
+		 *
+		 * 1. `{int} page` The current page index
+		 * 2. `{int} pages` The number of pages in the table
+		 *
+		 * Each function is expected to return an array where each element of the
+		 * array can be one of:
+		 *
+		 * * `first` - Jump to first page when activated
+		 * * `last` - Jump to last page when activated
+		 * * `previous` - Show previous page when activated
+		 * * `next` - Show next page when activated
+		 * * `{int}` - Show page of the index given
+		 * * `{array}` - A nested array containing the above elements to add a
+		 *   containing 'DIV' element (might be useful for styling).
+		 *
+		 * Note that DataTables v1.9- used this object slightly differently whereby
+		 * an object with two functions would be defined for each plug-in. That
+		 * ability is still supported by DataTables 1.10+ to provide backwards
+		 * compatibility, but this option of use is now decremented and no longer
+		 * documented in DataTables 1.10+.
+		 *
+		 *  @type object
+		 *  @default {}
+		 *
+		 *  @example
+		 *    // Show previous, next and current page buttons only
+		 *    $.fn.dataTableExt.oPagination.current = function ( page, pages ) {
+		 *      return [ 'previous', page, 'next' ];
+		 *    };
+		 */
+		pager: {},
+	
+	
+		renderer: {
+			pageButton: {},
+			header: {}
+		},
+	
+	
+		/**
+		 * Ordering plug-ins - custom data source
+		 * 
+		 * The extension options for ordering of data available here is complimentary
+		 * to the default type based ordering that DataTables typically uses. It
+		 * allows much greater control over the the data that is being used to
+		 * order a column, but is necessarily therefore more complex.
+		 * 
+		 * This type of ordering is useful if you want to do ordering based on data
+		 * live from the DOM (for example the contents of an 'input' element) rather
+		 * than just the static string that DataTables knows of.
+		 * 
+		 * The way these plug-ins work is that you create an array of the values you
+		 * wish to be ordering for the column in question and then return that
+		 * array. The data in the array much be in the index order of the rows in
+		 * the table (not the currently ordering order!). Which order data gathering
+		 * function is run here depends on the `dt-init columns.orderDataType`
+		 * parameter that is used for the column (if any).
+		 *
+		 * The functions defined take two parameters:
+		 *
+		 * 1. `{object}` DataTables settings object: see
+		 *    {@link DataTable.models.oSettings}
+		 * 2. `{int}` Target column index
+		 *
+		 * Each function is expected to return an array:
+		 *
+		 * * `{array}` Data for the column to be ordering upon
+		 *
+		 *  @type array
+		 *
+		 *  @example
+		 *    // Ordering using `input` node values
+		 *    $.fn.dataTable.ext.order['dom-text'] = function  ( settings, col )
+		 *    {
+		 *      return this.api().column( col, {order:'index'} ).nodes().map( function ( td, i ) {
+		 *        return $('input', td).val();
+		 *      } );
+		 *    }
+		 */
+		order: {},
+	
+	
+		/**
+		 * Type based plug-ins.
+		 *
+		 * Each column in DataTables has a type assigned to it, either by automatic
+		 * detection or by direct assignment using the `type` option for the column.
+		 * The type of a column will effect how it is ordering and search (plug-ins
+		 * can also make use of the column type if required).
+		 *
+		 * @namespace
+		 */
+		type: {
+			/**
+			 * Type detection functions.
+			 *
+			 * The functions defined in this object are used to automatically detect
+			 * a column's type, making initialisation of DataTables super easy, even
+			 * when complex data is in the table.
+			 *
+			 * The functions defined take two parameters:
+			 *
+		     *  1. `{*}` Data from the column cell to be analysed
+		     *  2. `{settings}` DataTables settings object. This can be used to
+		     *     perform context specific type detection - for example detection
+		     *     based on language settings such as using a comma for a decimal
+		     *     place. Generally speaking the options from the settings will not
+		     *     be required
+			 *
+			 * Each function is expected to return:
+			 *
+			 * * `{string|null}` Data type detected, or null if unknown (and thus
+			 *   pass it on to the other type detection functions.
+			 *
+			 *  @type array
+			 *
+			 *  @example
+			 *    // Currency type detection plug-in:
+			 *    $.fn.dataTable.ext.type.detect.push(
+			 *      function ( data, settings ) {
+			 *        // Check the numeric part
+			 *        if ( ! $.isNumeric( data.substring(1) ) ) {
+			 *          return null;
+			 *        }
+			 *
+			 *        // Check prefixed by currency
+			 *        if ( data.charAt(0) == '$' || data.charAt(0) == '&pound;' ) {
+			 *          return 'currency';
+			 *        }
+			 *        return null;
+			 *      }
+			 *    );
+			 */
+			detect: [],
+	
+	
+			/**
+			 * Type based search formatting.
+			 *
+			 * The type based searching functions can be used to pre-format the
+			 * data to be search on. For example, it can be used to strip HTML
+			 * tags or to de-format telephone numbers for numeric only searching.
+			 *
+			 * Note that is a search is not defined for a column of a given type,
+			 * no search formatting will be performed.
+			 * 
+			 * Pre-processing of searching data plug-ins - When you assign the sType
+			 * for a column (or have it automatically detected for you by DataTables
+			 * or a type detection plug-in), you will typically be using this for
+			 * custom sorting, but it can also be used to provide custom searching
+			 * by allowing you to pre-processing the data and returning the data in
+			 * the format that should be searched upon. This is done by adding
+			 * functions this object with a parameter name which matches the sType
+			 * for that target column. This is the corollary of <i>afnSortData</i>
+			 * for searching data.
+			 *
+			 * The functions defined take a single parameter:
+			 *
+		     *  1. `{*}` Data from the column cell to be prepared for searching
+			 *
+			 * Each function is expected to return:
+			 *
+			 * * `{string|null}` Formatted string that will be used for the searching.
+			 *
+			 *  @type object
+			 *  @default {}
+			 *
+			 *  @example
+			 *    $.fn.dataTable.ext.type.search['title-numeric'] = function ( d ) {
+			 *      return d.replace(/\n/g," ").replace( /<.*?>/g, "" );
+			 *    }
+			 */
+			search: {},
+	
+	
+			/**
+			 * Type based ordering.
+			 *
+			 * The column type tells DataTables what ordering to apply to the table
+			 * when a column is sorted upon. The order for each type that is defined,
+			 * is defined by the functions available in this object.
+			 *
+			 * Each ordering option can be described by three properties added to
+			 * this object:
+			 *
+			 * * `{type}-pre` - Pre-formatting function
+			 * * `{type}-asc` - Ascending order function
+			 * * `{type}-desc` - Descending order function
+			 *
+			 * All three can be used together, only `{type}-pre` or only
+			 * `{type}-asc` and `{type}-desc` together. It is generally recommended
+			 * that only `{type}-pre` is used, as this provides the optimal
+			 * implementation in terms of speed, although the others are provided
+			 * for compatibility with existing Javascript sort functions.
+			 *
+			 * `{type}-pre`: Functions defined take a single parameter:
+			 *
+		     *  1. `{*}` Data from the column cell to be prepared for ordering
+			 *
+			 * And return:
+			 *
+			 * * `{*}` Data to be sorted upon
+			 *
+			 * `{type}-asc` and `{type}-desc`: Functions are typical Javascript sort
+			 * functions, taking two parameters:
+			 *
+		     *  1. `{*}` Data to compare to the second parameter
+		     *  2. `{*}` Data to compare to the first parameter
+			 *
+			 * And returning:
+			 *
+			 * * `{*}` Ordering match: <0 if first parameter should be sorted lower
+			 *   than the second parameter, ===0 if the two parameters are equal and
+			 *   >0 if the first parameter should be sorted height than the second
+			 *   parameter.
+			 * 
+			 *  @type object
+			 *  @default {}
+			 *
+			 *  @example
+			 *    // Numeric ordering of formatted numbers with a pre-formatter
+			 *    $.extend( $.fn.dataTable.ext.type.order, {
+			 *      "string-pre": function(x) {
+			 *        a = (a === "-" || a === "") ? 0 : a.replace( /[^\d\-\.]/g, "" );
+			 *        return parseFloat( a );
+			 *      }
+			 *    } );
+			 *
+			 *  @example
+			 *    // Case-sensitive string ordering, with no pre-formatting method
+			 *    $.extend( $.fn.dataTable.ext.order, {
+			 *      "string-case-asc": function(x,y) {
+			 *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+			 *      },
+			 *      "string-case-desc": function(x,y) {
+			 *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+			 *      }
+			 *    } );
+			 */
+			order: {}
+		},
+	
+		/**
+		 * Unique DataTables instance counter
+		 *
+		 * @type int
+		 * @private
+		 */
+		_unique: 0,
+	
+	
+		//
+		// Depreciated
+		// The following properties are retained for backwards compatiblity only.
+		// The should not be used in new projects and will be removed in a future
+		// version
+		//
+	
+		/**
+		 * Version check function.
+		 *  @type function
+		 *  @depreciated Since 1.10
+		 */
+		fnVersionCheck: DataTable.fnVersionCheck,
+	
+	
+		/**
+		 * Index for what 'this' index API functions should use
+		 *  @type int
+		 *  @deprecated Since v1.10
+		 */
+		iApiIndex: 0,
+	
+	
+		/**
+		 * jQuery UI class container
+		 *  @type object
+		 *  @deprecated Since v1.10
+		 */
+		oJUIClasses: {},
+	
+	
+		/**
+		 * Software version
+		 *  @type string
+		 *  @deprecated Since v1.10
+		 */
+		sVersion: DataTable.version
+	};
+	
+	
+	//
+	// Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts
+	//
+	$.extend( _ext, {
+		afnFiltering: _ext.search,
+		aTypes:       _ext.type.detect,
+		ofnSearch:    _ext.type.search,
+		oSort:        _ext.type.order,
+		afnSortData:  _ext.order,
+		aoFeatures:   _ext.feature,
+		oApi:         _ext.internal,
+		oStdClasses:  _ext.classes,
+		oPagination:  _ext.pager
+	} );
+	
+	
+	$.extend( DataTable.ext.classes, {
+		"sTable": "dataTable",
+		"sNoFooter": "no-footer",
+	
+		/* Paging buttons */
+		"sPageButton": "paginate_button",
+		"sPageButtonActive": "current",
+		"sPageButtonDisabled": "disabled",
+	
+		/* Striping classes */
+		"sStripeOdd": "odd",
+		"sStripeEven": "even",
+	
+		/* Empty row */
+		"sRowEmpty": "dataTables_empty",
+	
+		/* Features */
+		"sWrapper": "dataTables_wrapper",
+		"sFilter": "dataTables_filter",
+		"sInfo": "dataTables_info",
+		"sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */
+		"sLength": "dataTables_length",
+		"sProcessing": "dataTables_processing",
+	
+		/* Sorting */
+		"sSortAsc": "sorting_asc",
+		"sSortDesc": "sorting_desc",
+		"sSortable": "sorting", /* Sortable in both directions */
+		"sSortableAsc": "sorting_asc_disabled",
+		"sSortableDesc": "sorting_desc_disabled",
+		"sSortableNone": "sorting_disabled",
+		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
+	
+		/* Filtering */
+		"sFilterInput": "",
+	
+		/* Page length */
+		"sLengthSelect": "",
+	
+		/* Scrolling */
+		"sScrollWrapper": "dataTables_scroll",
+		"sScrollHead": "dataTables_scrollHead",
+		"sScrollHeadInner": "dataTables_scrollHeadInner",
+		"sScrollBody": "dataTables_scrollBody",
+		"sScrollFoot": "dataTables_scrollFoot",
+		"sScrollFootInner": "dataTables_scrollFootInner",
+	
+		/* Misc */
+		"sHeaderTH": "",
+		"sFooterTH": "",
+	
+		// Deprecated
+		"sSortJUIAsc": "",
+		"sSortJUIDesc": "",
+		"sSortJUI": "",
+		"sSortJUIAscAllowed": "",
+		"sSortJUIDescAllowed": "",
+		"sSortJUIWrapper": "",
+		"sSortIcon": "",
+		"sJUIHeader": "",
+		"sJUIFooter": ""
+	} );
+	
+	
+	(function() {
+	
+	// Reused strings for better compression. Closure compiler appears to have a
+	// weird edge case where it is trying to expand strings rather than use the
+	// variable version. This results in about 200 bytes being added, for very
+	// little preference benefit since it this run on script load only.
+	var _empty = '';
+	_empty = '';
+	
+	var _stateDefault = _empty + 'ui-state-default';
+	var _sortIcon     = _empty + 'css_right ui-icon ui-icon-';
+	var _headerFooter = _empty + 'fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix';
+	
+	$.extend( DataTable.ext.oJUIClasses, DataTable.ext.classes, {
+		/* Full numbers paging buttons */
+		"sPageButton":         "fg-button ui-button "+_stateDefault,
+		"sPageButtonActive":   "ui-state-disabled",
+		"sPageButtonDisabled": "ui-state-disabled",
+	
+		/* Features */
+		"sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+
+			"ui-buttonset-multi paging_", /* Note that the type is postfixed */
+	
+		/* Sorting */
+		"sSortAsc":            _stateDefault+" sorting_asc",
+		"sSortDesc":           _stateDefault+" sorting_desc",
+		"sSortable":           _stateDefault+" sorting",
+		"sSortableAsc":        _stateDefault+" sorting_asc_disabled",
+		"sSortableDesc":       _stateDefault+" sorting_desc_disabled",
+		"sSortableNone":       _stateDefault+" sorting_disabled",
+		"sSortJUIAsc":         _sortIcon+"triangle-1-n",
+		"sSortJUIDesc":        _sortIcon+"triangle-1-s",
+		"sSortJUI":            _sortIcon+"carat-2-n-s",
+		"sSortJUIAscAllowed":  _sortIcon+"carat-1-n",
+		"sSortJUIDescAllowed": _sortIcon+"carat-1-s",
+		"sSortJUIWrapper":     "DataTables_sort_wrapper",
+		"sSortIcon":           "DataTables_sort_icon",
+	
+		/* Scrolling */
+		"sScrollHead": "dataTables_scrollHead "+_stateDefault,
+		"sScrollFoot": "dataTables_scrollFoot "+_stateDefault,
+	
+		/* Misc */
+		"sHeaderTH":  _stateDefault,
+		"sFooterTH":  _stateDefault,
+		"sJUIHeader": _headerFooter+" ui-corner-tl ui-corner-tr",
+		"sJUIFooter": _headerFooter+" ui-corner-bl ui-corner-br"
+	} );
+	
+	}());
+	
+	
+	
+	var extPagination = DataTable.ext.pager;
+	
+	function _numbers ( page, pages ) {
+		var
+			numbers = [],
+			buttons = extPagination.numbers_length,
+			half = Math.floor( buttons / 2 ),
+			i = 1;
+	
+		if ( pages <= buttons ) {
+			numbers = _range( 0, pages );
+		}
+		else if ( page <= half ) {
+			numbers = _range( 0, buttons-2 );
+			numbers.push( 'ellipsis' );
+			numbers.push( pages-1 );
+		}
+		else if ( page >= pages - 1 - half ) {
+			numbers = _range( pages-(buttons-2), pages );
+			numbers.splice( 0, 0, 'ellipsis' ); // no unshift in ie6
+			numbers.splice( 0, 0, 0 );
+		}
+		else {
+			numbers = _range( page-half+2, page+half-1 );
+			numbers.push( 'ellipsis' );
+			numbers.push( pages-1 );
+			numbers.splice( 0, 0, 'ellipsis' );
+			numbers.splice( 0, 0, 0 );
+		}
+	
+		numbers.DT_el = 'span';
+		return numbers;
+	}
+	
+	
+	$.extend( extPagination, {
+		simple: function ( page, pages ) {
+			return [ 'previous', 'next' ];
+		},
+	
+		full: function ( page, pages ) {
+			return [  'first', 'previous', 'next', 'last' ];
+		},
+	
+		numbers: function ( page, pages ) {
+			return [ _numbers(page, pages) ];
+		},
+	
+		simple_numbers: function ( page, pages ) {
+			return [ 'previous', _numbers(page, pages), 'next' ];
+		},
+	
+		full_numbers: function ( page, pages ) {
+			return [ 'first', 'previous', _numbers(page, pages), 'next', 'last' ];
+		},
+		
+		first_last_numbers: function (page, pages) {
+	 		return ['first', _numbers(page, pages), 'last'];
+	 	},
+	
+		// For testing and plug-ins to use
+		_numbers: _numbers,
+	
+		// Number of number buttons (including ellipsis) to show. _Must be odd!_
+		numbers_length: 7
+	} );
+	
+	
+	$.extend( true, DataTable.ext.renderer, {
+		pageButton: {
+			_: function ( settings, host, idx, buttons, page, pages ) {
+				var classes = settings.oClasses;
+				var lang = settings.oLanguage.oPaginate;
+				var aria = settings.oLanguage.oAria.paginate || {};
+				var btnDisplay, btnClass, counter=0;
+	
+				var attach = function( container, buttons ) {
+					var i, ien, node, button;
+					var clickHandler = function ( e ) {
+						_fnPageChange( settings, e.data.action, true );
+					};
+	
+					for ( i=0, ien=buttons.length ; i<ien ; i++ ) {
+						button = buttons[i];
+	
+						if ( $.isArray( button ) ) {
+							var inner = $( '<'+(button.DT_el || 'div')+'/>' )
+								.appendTo( container );
+							attach( inner, button );
+						}
+						else {
+							btnDisplay = null;
+							btnClass = '';
+	
+							switch ( button ) {
+								case 'ellipsis':
+									container.append('<span class="ellipsis">&#x2026;</span>');
+									break;
+	
+								case 'first':
+									btnDisplay = lang.sFirst;
+									btnClass = button + (page > 0 ?
+										'' : ' '+classes.sPageButtonDisabled);
+									break;
+	
+								case 'previous':
+									btnDisplay = lang.sPrevious;
+									btnClass = button + (page > 0 ?
+										'' : ' '+classes.sPageButtonDisabled);
+									break;
+	
+								case 'next':
+									btnDisplay = lang.sNext;
+									btnClass = button + (page < pages-1 ?
+										'' : ' '+classes.sPageButtonDisabled);
+									break;
+	
+								case 'last':
+									btnDisplay = lang.sLast;
+									btnClass = button + (page < pages-1 ?
+										'' : ' '+classes.sPageButtonDisabled);
+									break;
+	
+								default:
+									btnDisplay = button + 1;
+									btnClass = page === button ?
+										classes.sPageButtonActive : '';
+									break;
+							}
+	
+							if ( btnDisplay !== null ) {
+								node = $('<a>', {
+										'class': classes.sPageButton+' '+btnClass,
+										'aria-controls': settings.sTableId,
+										'aria-label': aria[ button ],
+										'data-dt-idx': counter,
+										'tabindex': settings.iTabIndex,
+										'id': idx === 0 && typeof button === 'string' ?
+											settings.sTableId +'_'+ button :
+											null
+									} )
+									.html( btnDisplay )
+									.appendTo( container );
+	
+								_fnBindAction(
+									node, {action: button}, clickHandler
+								);
+	
+								counter++;
+							}
+						}
+					}
+				};
+	
+				// IE9 throws an 'unknown error' if document.activeElement is used
+				// inside an iframe or frame. Try / catch the error. Not good for
+				// accessibility, but neither are frames.
+				var activeEl;
+	
+				try {
+					// Because this approach is destroying and recreating the paging
+					// elements, focus is lost on the select button which is bad for
+					// accessibility. So we want to restore focus once the draw has
+					// completed
+					activeEl = $(host).find(document.activeElement).data('dt-idx');
+				}
+				catch (e) {}
+	
+				attach( $(host).empty(), buttons );
+	
+				if ( activeEl !== undefined ) {
+					$(host).find( '[data-dt-idx='+activeEl+']' ).focus();
+				}
+			}
+		}
+	} );
+	
+	
+	
+	// Built in type detection. See model.ext.aTypes for information about
+	// what is required from this methods.
+	$.extend( DataTable.ext.type.detect, [
+		// Plain numbers - first since V8 detects some plain numbers as dates
+		// e.g. Date.parse('55') (but not all, e.g. Date.parse('22')...).
+		function ( d, settings )
+		{
+			var decimal = settings.oLanguage.sDecimal;
+			return _isNumber( d, decimal ) ? 'num'+decimal : null;
+		},
+	
+		// Dates (only those recognised by the browser's Date.parse)
+		function ( d, settings )
+		{
+			// V8 tries _very_ hard to make a string passed into `Date.parse()`
+			// valid, so we need to use a regex to restrict date formats. Use a
+			// plug-in for anything other than ISO8601 style strings
+			if ( d && !(d instanceof Date) && ! _re_date.test(d) ) {
+				return null;
+			}
+			var parsed = Date.parse(d);
+			return (parsed !== null && !isNaN(parsed)) || _empty(d) ? 'date' : null;
+		},
+	
+		// Formatted numbers
+		function ( d, settings )
+		{
+			var decimal = settings.oLanguage.sDecimal;
+			return _isNumber( d, decimal, true ) ? 'num-fmt'+decimal : null;
+		},
+	
+		// HTML numeric
+		function ( d, settings )
+		{
+			var decimal = settings.oLanguage.sDecimal;
+			return _htmlNumeric( d, decimal ) ? 'html-num'+decimal : null;
+		},
+	
+		// HTML numeric, formatted
+		function ( d, settings )
+		{
+			var decimal = settings.oLanguage.sDecimal;
+			return _htmlNumeric( d, decimal, true ) ? 'html-num-fmt'+decimal : null;
+		},
+	
+		// HTML (this is strict checking - there must be html)
+		function ( d, settings )
+		{
+			return _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1) ?
+				'html' : null;
+		}
+	] );
+	
+	
+	
+	// Filter formatting functions. See model.ext.ofnSearch for information about
+	// what is required from these methods.
+	// 
+	// Note that additional search methods are added for the html numbers and
+	// html formatted numbers by `_addNumericSort()` when we know what the decimal
+	// place is
+	
+	
+	$.extend( DataTable.ext.type.search, {
+		html: function ( data ) {
+			return _empty(data) ?
+				data :
+				typeof data === 'string' ?
+					data
+						.replace( _re_new_lines, " " )
+						.replace( _re_html, "" ) :
+					'';
+		},
+	
+		string: function ( data ) {
+			return _empty(data) ?
+				data :
+				typeof data === 'string' ?
+					data.replace( _re_new_lines, " " ) :
+					data;
+		}
+	} );
+	
+	
+	
+	var __numericReplace = function ( d, decimalPlace, re1, re2 ) {
+		if ( d !== 0 && (!d || d === '-') ) {
+			return -Infinity;
+		}
+	
+		// If a decimal place other than `.` is used, it needs to be given to the
+		// function so we can detect it and replace with a `.` which is the only
+		// decimal place Javascript recognises - it is not locale aware.
+		if ( decimalPlace ) {
+			d = _numToDecimal( d, decimalPlace );
+		}
+	
+		if ( d.replace ) {
+			if ( re1 ) {
+				d = d.replace( re1, '' );
+			}
+	
+			if ( re2 ) {
+				d = d.replace( re2, '' );
+			}
+		}
+	
+		return d * 1;
+	};
+	
+	
+	// Add the numeric 'deformatting' functions for sorting and search. This is done
+	// in a function to provide an easy ability for the language options to add
+	// additional methods if a non-period decimal place is used.
+	function _addNumericSort ( decimalPlace ) {
+		$.each(
+			{
+				// Plain numbers
+				"num": function ( d ) {
+					return __numericReplace( d, decimalPlace );
+				},
+	
+				// Formatted numbers
+				"num-fmt": function ( d ) {
+					return __numericReplace( d, decimalPlace, _re_formatted_numeric );
+				},
+	
+				// HTML numeric
+				"html-num": function ( d ) {
+					return __numericReplace( d, decimalPlace, _re_html );
+				},
+	
+				// HTML numeric, formatted
+				"html-num-fmt": function ( d ) {
+					return __numericReplace( d, decimalPlace, _re_html, _re_formatted_numeric );
+				}
+			},
+			function ( key, fn ) {
+				// Add the ordering method
+				_ext.type.order[ key+decimalPlace+'-pre' ] = fn;
+	
+				// For HTML types add a search formatter that will strip the HTML
+				if ( key.match(/^html\-/) ) {
+					_ext.type.search[ key+decimalPlace ] = _ext.type.search.html;
+				}
+			}
+		);
+	}
+	
+	
+	// Default sort methods
+	$.extend( _ext.type.order, {
+		// Dates
+		"date-pre": function ( d ) {
+			return Date.parse( d ) || -Infinity;
+		},
+	
+		// html
+		"html-pre": function ( a ) {
+			return _empty(a) ?
+				'' :
+				a.replace ?
+					a.replace( /<.*?>/g, "" ).toLowerCase() :
+					a+'';
+		},
+	
+		// string
+		"string-pre": function ( a ) {
+			// This is a little complex, but faster than always calling toString,
+			// http://jsperf.com/tostring-v-check
+			return _empty(a) ?
+				'' :
+				typeof a === 'string' ?
+					a.toLowerCase() :
+					! a.toString ?
+						'' :
+						a.toString();
+		},
+	
+		// string-asc and -desc are retained only for compatibility with the old
+		// sort methods
+		"string-asc": function ( x, y ) {
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+	
+		"string-desc": function ( x, y ) {
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		}
+	} );
+	
+	
+	// Numeric sorting types - order doesn't matter here
+	_addNumericSort( '' );
+	
+	
+	$.extend( true, DataTable.ext.renderer, {
+		header: {
+			_: function ( settings, cell, column, classes ) {
+				// No additional mark-up required
+				// Attach a sort listener to update on sort - note that using the
+				// `DT` namespace will allow the event to be removed automatically
+				// on destroy, while the `dt` namespaced event is the one we are
+				// listening for
+				$(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting, columns ) {
+					if ( settings !== ctx ) { // need to check this this is the host
+						return;               // table, not a nested one
+					}
+	
+					var colIdx = column.idx;
+	
+					cell
+						.removeClass(
+							column.sSortingClass +' '+
+							classes.sSortAsc +' '+
+							classes.sSortDesc
+						)
+						.addClass( columns[ colIdx ] == 'asc' ?
+							classes.sSortAsc : columns[ colIdx ] == 'desc' ?
+								classes.sSortDesc :
+								column.sSortingClass
+						);
+				} );
+			},
+	
+			jqueryui: function ( settings, cell, column, classes ) {
+				$('<div/>')
+					.addClass( classes.sSortJUIWrapper )
+					.append( cell.contents() )
+					.append( $('<span/>')
+						.addClass( classes.sSortIcon+' '+column.sSortingClassJUI )
+					)
+					.appendTo( cell );
+	
+				// Attach a sort listener to update on sort
+				$(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting, columns ) {
+					if ( settings !== ctx ) {
+						return;
+					}
+	
+					var colIdx = column.idx;
+	
+					cell
+						.removeClass( classes.sSortAsc +" "+classes.sSortDesc )
+						.addClass( columns[ colIdx ] == 'asc' ?
+							classes.sSortAsc : columns[ colIdx ] == 'desc' ?
+								classes.sSortDesc :
+								column.sSortingClass
+						);
+	
+					cell
+						.find( 'span.'+classes.sSortIcon )
+						.removeClass(
+							classes.sSortJUIAsc +" "+
+							classes.sSortJUIDesc +" "+
+							classes.sSortJUI +" "+
+							classes.sSortJUIAscAllowed +" "+
+							classes.sSortJUIDescAllowed
+						)
+						.addClass( columns[ colIdx ] == 'asc' ?
+							classes.sSortJUIAsc : columns[ colIdx ] == 'desc' ?
+								classes.sSortJUIDesc :
+								column.sSortingClassJUI
+						);
+				} );
+			}
+		}
+	} );
+	
+	/*
+	 * Public helper functions. These aren't used internally by DataTables, or
+	 * called by any of the options passed into DataTables, but they can be used
+	 * externally by developers working with DataTables. They are helper functions
+	 * to make working with DataTables a little bit easier.
+	 */
+	
+	var __htmlEscapeEntities = function ( d ) {
+		return typeof d === 'string' ?
+			d.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') :
+			d;
+	};
+	
+	/**
+	 * Helpers for `columns.render`.
+	 *
+	 * The options defined here can be used with the `columns.render` initialisation
+	 * option to provide a display renderer. The following functions are defined:
+	 *
+	 * * `number` - Will format numeric data (defined by `columns.data`) for
+	 *   display, retaining the original unformatted data for sorting and filtering.
+	 *   It takes 5 parameters:
+	 *   * `string` - Thousands grouping separator
+	 *   * `string` - Decimal point indicator
+	 *   * `integer` - Number of decimal points to show
+	 *   * `string` (optional) - Prefix.
+	 *   * `string` (optional) - Postfix (/suffix).
+	 * * `text` - Escape HTML to help prevent XSS attacks. It has no optional
+	 *   parameters.
+	 *
+	 * @example
+	 *   // Column definition using the number renderer
+	 *   {
+	 *     data: "salary",
+	 *     render: $.fn.dataTable.render.number( '\'', '.', 0, '$' )
+	 *   }
+	 *
+	 * @namespace
+	 */
+	DataTable.render = {
+		number: function ( thousands, decimal, precision, prefix, postfix ) {
+			return {
+				display: function ( d ) {
+					if ( typeof d !== 'number' && typeof d !== 'string' ) {
+						return d;
+					}
+	
+					var negative = d < 0 ? '-' : '';
+					var flo = parseFloat( d );
+	
+					// If NaN then there isn't much formatting that we can do - just
+					// return immediately, escaping any HTML (this was supposed to
+					// be a number after all)
+					if ( isNaN( flo ) ) {
+						return __htmlEscapeEntities( d );
+					}
+	
+					flo = flo.toFixed( precision );
+					d = Math.abs( flo );
+	
+					var intPart = parseInt( d, 10 );
+					var floatPart = precision ?
+						decimal+(d - intPart).toFixed( precision ).substring( 2 ):
+						'';
+	
+					return negative + (prefix||'') +
+						intPart.toString().replace(
+							/\B(?=(\d{3})+(?!\d))/g, thousands
+						) +
+						floatPart +
+						(postfix||'');
+				}
+			};
+		},
+	
+		text: function () {
+			return {
+				display: __htmlEscapeEntities
+			};
+		}
+	};
+	
+	
+	/*
+	 * This is really a good bit rubbish this method of exposing the internal methods
+	 * publicly... - To be fixed in 2.0 using methods on the prototype
+	 */
+	
+	
+	/**
+	 * Create a wrapper function for exporting an internal functions to an external API.
+	 *  @param {string} fn API function name
+	 *  @returns {function} wrapped function
+	 *  @memberof DataTable#internal
+	 */
+	function _fnExternApiFunc (fn)
+	{
+		return function() {
+			var args = [_fnSettingsFromNode( this[DataTable.ext.iApiIndex] )].concat(
+				Array.prototype.slice.call(arguments)
+			);
+			return DataTable.ext.internal[fn].apply( this, args );
+		};
+	}
+	
+	
+	/**
+	 * Reference to internal functions for use by plug-in developers. Note that
+	 * these methods are references to internal functions and are considered to be
+	 * private. If you use these methods, be aware that they are liable to change
+	 * between versions.
+	 *  @namespace
+	 */
+	$.extend( DataTable.ext.internal, {
+		_fnExternApiFunc: _fnExternApiFunc,
+		_fnBuildAjax: _fnBuildAjax,
+		_fnAjaxUpdate: _fnAjaxUpdate,
+		_fnAjaxParameters: _fnAjaxParameters,
+		_fnAjaxUpdateDraw: _fnAjaxUpdateDraw,
+		_fnAjaxDataSrc: _fnAjaxDataSrc,
+		_fnAddColumn: _fnAddColumn,
+		_fnColumnOptions: _fnColumnOptions,
+		_fnAdjustColumnSizing: _fnAdjustColumnSizing,
+		_fnVisibleToColumnIndex: _fnVisibleToColumnIndex,
+		_fnColumnIndexToVisible: _fnColumnIndexToVisible,
+		_fnVisbleColumns: _fnVisbleColumns,
+		_fnGetColumns: _fnGetColumns,
+		_fnColumnTypes: _fnColumnTypes,
+		_fnApplyColumnDefs: _fnApplyColumnDefs,
+		_fnHungarianMap: _fnHungarianMap,
+		_fnCamelToHungarian: _fnCamelToHungarian,
+		_fnLanguageCompat: _fnLanguageCompat,
+		_fnBrowserDetect: _fnBrowserDetect,
+		_fnAddData: _fnAddData,
+		_fnAddTr: _fnAddTr,
+		_fnNodeToDataIndex: _fnNodeToDataIndex,
+		_fnNodeToColumnIndex: _fnNodeToColumnIndex,
+		_fnGetCellData: _fnGetCellData,
+		_fnSetCellData: _fnSetCellData,
+		_fnSplitObjNotation: _fnSplitObjNotation,
+		_fnGetObjectDataFn: _fnGetObjectDataFn,
+		_fnSetObjectDataFn: _fnSetObjectDataFn,
+		_fnGetDataMaster: _fnGetDataMaster,
+		_fnClearTable: _fnClearTable,
+		_fnDeleteIndex: _fnDeleteIndex,
+		_fnInvalidate: _fnInvalidate,
+		_fnGetRowElements: _fnGetRowElements,
+		_fnCreateTr: _fnCreateTr,
+		_fnBuildHead: _fnBuildHead,
+		_fnDrawHead: _fnDrawHead,
+		_fnDraw: _fnDraw,
+		_fnReDraw: _fnReDraw,
+		_fnAddOptionsHtml: _fnAddOptionsHtml,
+		_fnDetectHeader: _fnDetectHeader,
+		_fnGetUniqueThs: _fnGetUniqueThs,
+		_fnFeatureHtmlFilter: _fnFeatureHtmlFilter,
+		_fnFilterComplete: _fnFilterComplete,
+		_fnFilterCustom: _fnFilterCustom,
+		_fnFilterColumn: _fnFilterColumn,
+		_fnFilter: _fnFilter,
+		_fnFilterCreateSearch: _fnFilterCreateSearch,
+		_fnEscapeRegex: _fnEscapeRegex,
+		_fnFilterData: _fnFilterData,
+		_fnFeatureHtmlInfo: _fnFeatureHtmlInfo,
+		_fnUpdateInfo: _fnUpdateInfo,
+		_fnInfoMacros: _fnInfoMacros,
+		_fnInitialise: _fnInitialise,
+		_fnInitComplete: _fnInitComplete,
+		_fnLengthChange: _fnLengthChange,
+		_fnFeatureHtmlLength: _fnFeatureHtmlLength,
+		_fnFeatureHtmlPaginate: _fnFeatureHtmlPaginate,
+		_fnPageChange: _fnPageChange,
+		_fnFeatureHtmlProcessing: _fnFeatureHtmlProcessing,
+		_fnProcessingDisplay: _fnProcessingDisplay,
+		_fnFeatureHtmlTable: _fnFeatureHtmlTable,
+		_fnScrollDraw: _fnScrollDraw,
+		_fnApplyToChildren: _fnApplyToChildren,
+		_fnCalculateColumnWidths: _fnCalculateColumnWidths,
+		_fnThrottle: _fnThrottle,
+		_fnConvertToWidth: _fnConvertToWidth,
+		_fnGetWidestNode: _fnGetWidestNode,
+		_fnGetMaxLenString: _fnGetMaxLenString,
+		_fnStringToCss: _fnStringToCss,
+		_fnSortFlatten: _fnSortFlatten,
+		_fnSort: _fnSort,
+		_fnSortAria: _fnSortAria,
+		_fnSortListener: _fnSortListener,
+		_fnSortAttachListener: _fnSortAttachListener,
+		_fnSortingClasses: _fnSortingClasses,
+		_fnSortData: _fnSortData,
+		_fnSaveState: _fnSaveState,
+		_fnLoadState: _fnLoadState,
+		_fnSettingsFromNode: _fnSettingsFromNode,
+		_fnLog: _fnLog,
+		_fnMap: _fnMap,
+		_fnBindAction: _fnBindAction,
+		_fnCallbackReg: _fnCallbackReg,
+		_fnCallbackFire: _fnCallbackFire,
+		_fnLengthOverflow: _fnLengthOverflow,
+		_fnRenderer: _fnRenderer,
+		_fnDataSource: _fnDataSource,
+		_fnRowAttributes: _fnRowAttributes,
+		_fnCalculateEnd: function () {} // Used by a lot of plug-ins, but redundant
+		                                // in 1.10, so this dead-end function is
+		                                // added to prevent errors
+	} );
+	
+
+	// jQuery access
+	$.fn.dataTable = DataTable;
+
+	// Provide access to the host jQuery object (circular reference)
+	DataTable.$ = $;
+
+	// Legacy aliases
+	$.fn.dataTableSettings = DataTable.settings;
+	$.fn.dataTableExt = DataTable.ext;
+
+	// With a capital `D` we return a DataTables API instance rather than a
+	// jQuery object
+	$.fn.DataTable = function ( opts ) {
+		return $(this).dataTable( opts ).api();
+	};
+
+	// All properties that are available to $.fn.dataTable should also be
+	// available on $.fn.DataTable
+	$.each( DataTable, function ( prop, val ) {
+		$.fn.DataTable[ prop ] = val;
+	} );
+
+
+	// Information about events fired by DataTables - for documentation.
+	/**
+	 * Draw event, fired whenever the table is redrawn on the page, at the same
+	 * point as fnDrawCallback. This may be useful for binding events or
+	 * performing calculations when the table is altered at all.
+	 *  @name DataTable#draw.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Search event, fired when the searching applied to the table (using the
+	 * built-in global search, or column filters) is altered.
+	 *  @name DataTable#search.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Page change event, fired when the paging of the table is altered.
+	 *  @name DataTable#page.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Order event, fired when the ordering applied to the table is altered.
+	 *  @name DataTable#order.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * DataTables initialisation complete event, fired when the table is fully
+	 * drawn, including Ajax data loaded, if Ajax data is required.
+	 *  @name DataTable#init.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The JSON object request from the server - only
+	 *    present if client-side Ajax sourced data is used</li></ol>
+	 */
+
+	/**
+	 * State save event, fired when the table has changed state a new state save
+	 * is required. This event allows modification of the state saving object
+	 * prior to actually doing the save, including addition or other state
+	 * properties (for plug-ins) or modification of a DataTables core property.
+	 *  @name DataTable#stateSaveParams.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The state information to be saved
+	 */
+
+	/**
+	 * State load event, fired when the table is loading state from the stored
+	 * data, but prior to the settings object being modified by the saved state
+	 * - allowing modification of the saved state is required or loading of
+	 * state for a plug-in.
+	 *  @name DataTable#stateLoadParams.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * State loaded event, fired when state has been loaded from stored data and
+	 * the settings object has been modified by the loaded data.
+	 *  @name DataTable#stateLoaded.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * Processing event, fired when DataTables is doing some kind of processing
+	 * (be it, order, searcg or anything else). It can be used to indicate to
+	 * the end user that there is something happening, or that something has
+	 * finished.
+	 *  @name DataTable#processing.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {boolean} bShow Flag for if DataTables is doing processing or not
+	 */
+
+	/**
+	 * Ajax (XHR) event, fired whenever an Ajax request is completed from a
+	 * request to made to the server for new data. This event is called before
+	 * DataTables processed the returned data, so it can also be used to pre-
+	 * process the data returned from the server, if needed.
+	 *
+	 * Note that this trigger is called in `fnServerData`, if you override
+	 * `fnServerData` and which to use this event, you need to trigger it in you
+	 * success function.
+	 *  @name DataTable#xhr.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 *  @param {object} json JSON returned from the server
+	 *
+	 *  @example
+	 *     // Use a custom property returned from the server in another DOM element
+	 *     $('#table').dataTable().on('xhr.dt', function (e, settings, json) {
+	 *       $('#status').html( json.status );
+	 *     } );
+	 *
+	 *  @example
+	 *     // Pre-process the data returned from the server
+	 *     $('#table').dataTable().on('xhr.dt', function (e, settings, json) {
+	 *       for ( var i=0, ien=json.aaData.length ; i<ien ; i++ ) {
+	 *         json.aaData[i].sum = json.aaData[i].one + json.aaData[i].two;
+	 *       }
+	 *       // Note no return - manipulate the data directly in the JSON object.
+	 *     } );
+	 */
+
+	/**
+	 * Destroy event, fired when the DataTable is destroyed by calling fnDestroy
+	 * or passing the bDestroy:true parameter in the initialisation object. This
+	 * can be used to remove bound events, added DOM nodes, etc.
+	 *  @name DataTable#destroy.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Page length change event, fired when number of records to show on each
+	 * page (the length) is changed.
+	 *  @name DataTable#length.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 *  @param {integer} len New length
+	 */
+
+	/**
+	 * Column sizing has changed.
+	 *  @name DataTable#column-sizing.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Column visibility has changed.
+	 *  @name DataTable#column-visibility.dt
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 *  @param {int} column Column index
+	 *  @param {bool} vis `false` if column now hidden, or `true` if visible
+	 */
+
+	return $.fn.dataTable;
+}));
+
+
+/*! ColReorder 1.3.3
+ * ©2010-2015 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary     ColReorder
+ * @description Provide the ability to reorder columns in a DataTable
+ * @version     1.3.3
+ * @file        dataTables.colReorder.js
+ * @author      SpryMedia Ltd (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ * @copyright   Copyright 2010-2014 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ *   MIT license - http://datatables.net/license/mit
+ *
+ * This source file 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 license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+/**
+ * Switch the key value pairing of an index array to be value key (i.e. the old value is now the
+ * key). For example consider [ 2, 0, 1 ] this would be returned as [ 1, 2, 0 ].
+ *  @method  fnInvertKeyValues
+ *  @param   array aIn Array to switch around
+ *  @returns array
+ */
+function fnInvertKeyValues( aIn )
+{
+	var aRet=[];
+	for ( var i=0, iLen=aIn.length ; i<iLen ; i++ )
+	{
+		aRet[ aIn[i] ] = i;
+	}
+	return aRet;
+}
+
+
+/**
+ * Modify an array by switching the position of two elements
+ *  @method  fnArraySwitch
+ *  @param   array aArray Array to consider, will be modified by reference (i.e. no return)
+ *  @param   int iFrom From point
+ *  @param   int iTo Insert point
+ *  @returns void
+ */
+function fnArraySwitch( aArray, iFrom, iTo )
+{
+	var mStore = aArray.splice( iFrom, 1 )[0];
+	aArray.splice( iTo, 0, mStore );
+}
+
+
+/**
+ * Switch the positions of nodes in a parent node (note this is specifically designed for
+ * table rows). Note this function considers all element nodes under the parent!
+ *  @method  fnDomSwitch
+ *  @param   string sTag Tag to consider
+ *  @param   int iFrom Element to move
+ *  @param   int Point to element the element to (before this point), can be null for append
+ *  @returns void
+ */
+function fnDomSwitch( nParent, iFrom, iTo )
+{
+	var anTags = [];
+	for ( var i=0, iLen=nParent.childNodes.length ; i<iLen ; i++ )
+	{
+		if ( nParent.childNodes[i].nodeType == 1 )
+		{
+			anTags.push( nParent.childNodes[i] );
+		}
+	}
+	var nStore = anTags[ iFrom ];
+
+	if ( iTo !== null )
+	{
+		nParent.insertBefore( nStore, anTags[iTo] );
+	}
+	else
+	{
+		nParent.appendChild( nStore );
+	}
+}
+
+
+/**
+ * Plug-in for DataTables which will reorder the internal column structure by taking the column
+ * from one position (iFrom) and insert it into a given point (iTo).
+ *  @method  $.fn.dataTableExt.oApi.fnColReorder
+ *  @param   object oSettings DataTables settings object - automatically added by DataTables!
+ *  @param   int iFrom Take the column to be repositioned from this point
+ *  @param   int iTo and insert it into this point
+ *  @param   bool drop Indicate if the reorder is the final one (i.e. a drop)
+ *    not a live reorder
+ *  @param   bool invalidateRows speeds up processing if false passed
+ *  @returns void
+ */
+$.fn.dataTableExt.oApi.fnColReorder = function ( oSettings, iFrom, iTo, drop, invalidateRows )
+{
+	var i, iLen, j, jLen, jen, iCols=oSettings.aoColumns.length, nTrs, oCol;
+	var attrMap = function ( obj, prop, mapping ) {
+		if ( ! obj[ prop ] || typeof obj[ prop ] === 'function' ) {
+			return;
+		}
+
+		var a = obj[ prop ].split('.');
+		var num = a.shift();
+
+		if ( isNaN( num*1 ) ) {
+			return;
+		}
+
+		obj[ prop ] = mapping[ num*1 ]+'.'+a.join('.');
+	};
+
+	/* Sanity check in the input */
+	if ( iFrom == iTo )
+	{
+		/* Pointless reorder */
+		return;
+	}
+
+	if ( iFrom < 0 || iFrom >= iCols )
+	{
+		this.oApi._fnLog( oSettings, 1, "ColReorder 'from' index is out of bounds: "+iFrom );
+		return;
+	}
+
+	if ( iTo < 0 || iTo >= iCols )
+	{
+		this.oApi._fnLog( oSettings, 1, "ColReorder 'to' index is out of bounds: "+iTo );
+		return;
+	}
+
+	/*
+	 * Calculate the new column array index, so we have a mapping between the old and new
+	 */
+	var aiMapping = [];
+	for ( i=0, iLen=iCols ; i<iLen ; i++ )
+	{
+		aiMapping[i] = i;
+	}
+	fnArraySwitch( aiMapping, iFrom, iTo );
+	var aiInvertMapping = fnInvertKeyValues( aiMapping );
+
+
+	/*
+	 * Convert all internal indexing to the new column order indexes
+	 */
+	/* Sorting */
+	for ( i=0, iLen=oSettings.aaSorting.length ; i<iLen ; i++ )
+	{
+		oSettings.aaSorting[i][0] = aiInvertMapping[ oSettings.aaSorting[i][0] ];
+	}
+
+	/* Fixed sorting */
+	if ( oSettings.aaSortingFixed !== null )
+	{
+		for ( i=0, iLen=oSettings.aaSortingFixed.length ; i<iLen ; i++ )
+		{
+			oSettings.aaSortingFixed[i][0] = aiInvertMapping[ oSettings.aaSortingFixed[i][0] ];
+		}
+	}
+
+	/* Data column sorting (the column which the sort for a given column should take place on) */
+	for ( i=0, iLen=iCols ; i<iLen ; i++ )
+	{
+		oCol = oSettings.aoColumns[i];
+		for ( j=0, jLen=oCol.aDataSort.length ; j<jLen ; j++ )
+		{
+			oCol.aDataSort[j] = aiInvertMapping[ oCol.aDataSort[j] ];
+		}
+
+		// Update the column indexes
+		oCol.idx = aiInvertMapping[ oCol.idx ];
+	}
+
+	// Update 1.10 optimised sort class removal variable
+	$.each( oSettings.aLastSort, function (i, val) {
+		oSettings.aLastSort[i].src = aiInvertMapping[ val.src ];
+	} );
+
+	/* Update the Get and Set functions for each column */
+	for ( i=0, iLen=iCols ; i<iLen ; i++ )
+	{
+		oCol = oSettings.aoColumns[i];
+
+		if ( typeof oCol.mData == 'number' ) {
+			oCol.mData = aiInvertMapping[ oCol.mData ];
+		}
+		else if ( $.isPlainObject( oCol.mData ) ) {
+			// HTML5 data sourced
+			attrMap( oCol.mData, '_',      aiInvertMapping );
+			attrMap( oCol.mData, 'filter', aiInvertMapping );
+			attrMap( oCol.mData, 'sort',   aiInvertMapping );
+			attrMap( oCol.mData, 'type',   aiInvertMapping );
+		}
+	}
+
+	/*
+	 * Move the DOM elements
+	 */
+	if ( oSettings.aoColumns[iFrom].bVisible )
+	{
+		/* Calculate the current visible index and the point to insert the node before. The insert
+		 * before needs to take into account that there might not be an element to insert before,
+		 * in which case it will be null, and an appendChild should be used
+		 */
+		var iVisibleIndex = this.oApi._fnColumnIndexToVisible( oSettings, iFrom );
+		var iInsertBeforeIndex = null;
+
+		i = iTo < iFrom ? iTo : iTo + 1;
+		while ( iInsertBeforeIndex === null && i < iCols )
+		{
+			iInsertBeforeIndex = this.oApi._fnColumnIndexToVisible( oSettings, i );
+			i++;
+		}
+
+		/* Header */
+		nTrs = oSettings.nTHead.getElementsByTagName('tr');
+		for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+		{
+			fnDomSwitch( nTrs[i], iVisibleIndex, iInsertBeforeIndex );
+		}
+
+		/* Footer */
+		if ( oSettings.nTFoot !== null )
+		{
+			nTrs = oSettings.nTFoot.getElementsByTagName('tr');
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				fnDomSwitch( nTrs[i], iVisibleIndex, iInsertBeforeIndex );
+			}
+		}
+
+		/* Body */
+		for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+		{
+			if ( oSettings.aoData[i].nTr !== null )
+			{
+				fnDomSwitch( oSettings.aoData[i].nTr, iVisibleIndex, iInsertBeforeIndex );
+			}
+		}
+	}
+
+	/*
+	 * Move the internal array elements
+	 */
+	/* Columns */
+	fnArraySwitch( oSettings.aoColumns, iFrom, iTo );
+
+	// regenerate the get / set functions
+	for ( i=0, iLen=iCols ; i<iLen ; i++ ) {
+		oSettings.oApi._fnColumnOptions( oSettings, i, {} );
+	}
+
+	/* Search columns */
+	fnArraySwitch( oSettings.aoPreSearchCols, iFrom, iTo );
+
+	/* Array array - internal data anodes cache */
+	for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+	{
+		var data = oSettings.aoData[i];
+		var cells = data.anCells;
+
+		if ( cells ) {
+			fnArraySwitch( cells, iFrom, iTo );
+
+			// Longer term, should this be moved into the DataTables' invalidate
+			// methods?
+			for ( j=0, jen=cells.length ; j<jen ; j++ ) {
+				if ( cells[j] && cells[j]._DT_CellIndex ) {
+					cells[j]._DT_CellIndex.column = j;
+				}
+			}
+		}
+
+		// For DOM sourced data, the invalidate will reread the cell into
+		// the data array, but for data sources as an array, they need to
+		// be flipped
+		if ( data.src !== 'dom' && $.isArray( data._aData ) ) {
+			fnArraySwitch( data._aData, iFrom, iTo );
+		}
+	}
+
+	/* Reposition the header elements in the header layout array */
+	for ( i=0, iLen=oSettings.aoHeader.length ; i<iLen ; i++ )
+	{
+		fnArraySwitch( oSettings.aoHeader[i], iFrom, iTo );
+	}
+
+	if ( oSettings.aoFooter !== null )
+	{
+		for ( i=0, iLen=oSettings.aoFooter.length ; i<iLen ; i++ )
+		{
+			fnArraySwitch( oSettings.aoFooter[i], iFrom, iTo );
+		}
+	}
+
+	if ( invalidateRows || invalidateRows === undefined )
+	{
+		$.fn.dataTable.Api( oSettings ).rows().invalidate();
+	}
+
+	/*
+	 * Update DataTables' event handlers
+	 */
+
+	/* Sort listener */
+	for ( i=0, iLen=iCols ; i<iLen ; i++ )
+	{
+		$(oSettings.aoColumns[i].nTh).off('click.DT');
+		this.oApi._fnSortAttachListener( oSettings, oSettings.aoColumns[i].nTh, i );
+	}
+
+
+	/* Fire an event so other plug-ins can update */
+	$(oSettings.oInstance).trigger( 'column-reorder.dt', [ oSettings, {
+		from: iFrom,
+		to: iTo,
+		mapping: aiInvertMapping,
+		drop: drop,
+
+		// Old style parameters for compatibility
+		iFrom: iFrom,
+		iTo: iTo,
+		aiInvertMapping: aiInvertMapping
+	} ] );
+};
+
+/**
+ * ColReorder provides column visibility control for DataTables
+ * @class ColReorder
+ * @constructor
+ * @param {object} dt DataTables settings object
+ * @param {object} opts ColReorder options
+ */
+var ColReorder = function( dt, opts )
+{
+	var settings = new $.fn.dataTable.Api( dt ).settings()[0];
+
+	// Ensure that we can't initialise on the same table twice
+	if ( settings._colReorder ) {
+		return settings._colReorder;
+	}
+
+	// Allow the options to be a boolean for defaults
+	if ( opts === true ) {
+		opts = {};
+	}
+
+	// Convert from camelCase to Hungarian, just as DataTables does
+	var camelToHungarian = $.fn.dataTable.camelToHungarian;
+	if ( camelToHungarian ) {
+		camelToHungarian( ColReorder.defaults, ColReorder.defaults, true );
+		camelToHungarian( ColReorder.defaults, opts || {} );
+	}
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public class variables
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+	/**
+	 * @namespace Settings object which contains customisable information for ColReorder instance
+	 */
+	this.s = {
+		/**
+		 * DataTables settings object
+		 *  @property dt
+		 *  @type     Object
+		 *  @default  null
+		 */
+		"dt": null,
+
+		/**
+		 * Initialisation object used for this instance
+		 *  @property init
+		 *  @type     object
+		 *  @default  {}
+		 */
+		"init": $.extend( true, {}, ColReorder.defaults, opts ),
+
+		/**
+		 * Number of columns to fix (not allow to be reordered)
+		 *  @property fixed
+		 *  @type     int
+		 *  @default  0
+		 */
+		"fixed": 0,
+
+		/**
+		 * Number of columns to fix counting from right (not allow to be reordered)
+		 *  @property fixedRight
+		 *  @type     int
+		 *  @default  0
+		 */
+		"fixedRight": 0,
+
+		/**
+		 * Callback function for once the reorder has been done
+		 *  @property reorderCallback
+		 *  @type     function
+		 *  @default  null
+		 */
+		"reorderCallback": null,
+
+		/**
+		 * @namespace Information used for the mouse drag
+		 */
+		"mouse": {
+			"startX": -1,
+			"startY": -1,
+			"offsetX": -1,
+			"offsetY": -1,
+			"target": -1,
+			"targetIndex": -1,
+			"fromIndex": -1
+		},
+
+		/**
+		 * Information which is used for positioning the insert cusor and knowing where to do the
+		 * insert. Array of objects with the properties:
+		 *   x: x-axis position
+		 *   to: insert point
+		 *  @property aoTargets
+		 *  @type     array
+		 *  @default  []
+		 */
+		"aoTargets": []
+	};
+
+
+	/**
+	 * @namespace Common and useful DOM elements for the class instance
+	 */
+	this.dom = {
+		/**
+		 * Dragging element (the one the mouse is moving)
+		 *  @property drag
+		 *  @type     element
+		 *  @default  null
+		 */
+		"drag": null,
+
+		/**
+		 * The insert cursor
+		 *  @property pointer
+		 *  @type     element
+		 *  @default  null
+		 */
+		"pointer": null
+	};
+
+
+	/* Constructor logic */
+	this.s.dt = settings;
+	this.s.dt._colReorder = this;
+	this._fnConstruct();
+
+	return this;
+};
+
+
+
+$.extend( ColReorder.prototype, {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public methods
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+	/**
+	 * Reset the column ordering to the original ordering that was detected on
+	 * start up.
+	 *  @return {this} Returns `this` for chaining.
+	 *
+	 *  @example
+	 *    // DataTables initialisation with ColReorder
+	 *    var table = $('#example').dataTable( {
+	 *        "sDom": 'Rlfrtip'
+	 *    } );
+	 *
+	 *    // Add click event to a button to reset the ordering
+	 *    $('#resetOrdering').click( function (e) {
+	 *        e.preventDefault();
+	 *        $.fn.dataTable.ColReorder( table ).fnReset();
+	 *    } );
+	 */
+	"fnReset": function ()
+	{
+		this._fnOrderColumns( this.fnOrder() );
+
+		return this;
+	},
+
+	/**
+	 * `Deprecated` - Get the current order of the columns, as an array.
+	 *  @return {array} Array of column identifiers
+	 *  @deprecated `fnOrder` should be used in preference to this method.
+	 *      `fnOrder` acts as a getter/setter.
+	 */
+	"fnGetCurrentOrder": function ()
+	{
+		return this.fnOrder();
+	},
+
+	/**
+	 * Get the current order of the columns, as an array. Note that the values
+	 * given in the array are unique identifiers for each column. Currently
+	 * these are the original ordering of the columns that was detected on
+	 * start up, but this could potentially change in future.
+	 *  @return {array} Array of column identifiers
+	 *
+	 *  @example
+	 *    // Get column ordering for the table
+	 *    var order = $.fn.dataTable.ColReorder( dataTable ).fnOrder();
+	 *//**
+	 * Set the order of the columns, from the positions identified in the
+	 * ordering array given. Note that ColReorder takes a brute force approach
+	 * to reordering, so it is possible multiple reordering events will occur
+	 * before the final order is settled upon.
+	 *  @param {array} [set] Array of column identifiers in the new order. Note
+	 *    that every column must be included, uniquely, in this array.
+	 *  @return {this} Returns `this` for chaining.
+	 *
+	 *  @example
+	 *    // Swap the first and second columns
+	 *    $.fn.dataTable.ColReorder( dataTable ).fnOrder( [1, 0, 2, 3, 4] );
+	 *
+	 *  @example
+	 *    // Move the first column to the end for the table `#example`
+	 *    var curr = $.fn.dataTable.ColReorder( '#example' ).fnOrder();
+	 *    var first = curr.shift();
+	 *    curr.push( first );
+	 *    $.fn.dataTable.ColReorder( '#example' ).fnOrder( curr );
+	 *
+	 *  @example
+	 *    // Reverse the table's order
+	 *    $.fn.dataTable.ColReorder( '#example' ).fnOrder(
+	 *      $.fn.dataTable.ColReorder( '#example' ).fnOrder().reverse()
+	 *    );
+	 */
+	"fnOrder": function ( set, original )
+	{
+		var a = [], i, ien, j, jen;
+		var columns = this.s.dt.aoColumns;
+
+		if ( set === undefined ){
+			for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+				a.push( columns[i]._ColReorder_iOrigCol );
+			}
+
+			return a;
+		}
+
+		// The order given is based on the original indexes, rather than the
+		// existing ones, so we need to translate from the original to current
+		// before then doing the order
+		if ( original ) {
+			var order = this.fnOrder();
+
+			for ( i=0, ien=set.length ; i<ien ; i++ ) {
+				a.push( $.inArray( set[i], order ) );
+			}
+
+			set = a;
+		}
+
+		this._fnOrderColumns( fnInvertKeyValues( set ) );
+
+		return this;
+	},
+
+
+	/**
+	 * Convert from the original column index, to the original
+	 *
+	 * @param  {int|array} idx Index(es) to convert
+	 * @param  {string} dir Transpose direction - `fromOriginal` / `toCurrent`
+	 *   or `'toOriginal` / `fromCurrent`
+	 * @return {int|array}     Converted values
+	 */
+	fnTranspose: function ( idx, dir )
+	{
+		if ( ! dir ) {
+			dir = 'toCurrent';
+		}
+
+		var order = this.fnOrder();
+		var columns = this.s.dt.aoColumns;
+
+		if ( dir === 'toCurrent' ) {
+			// Given an original index, want the current
+			return ! $.isArray( idx ) ?
+				$.inArray( idx, order ) :
+				$.map( idx, function ( index ) {
+					return $.inArray( index, order );
+				} );
+		}
+		else {
+			// Given a current index, want the original
+			return ! $.isArray( idx ) ?
+				columns[idx]._ColReorder_iOrigCol :
+				$.map( idx, function ( index ) {
+					return columns[index]._ColReorder_iOrigCol;
+				} );
+		}
+	},
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods (they are of course public in JS, but recommended as private)
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+	/**
+	 * Constructor logic
+	 *  @method  _fnConstruct
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnConstruct": function ()
+	{
+		var that = this;
+		var iLen = this.s.dt.aoColumns.length;
+		var table = this.s.dt.nTable;
+		var i;
+
+		/* Columns discounted from reordering - counting left to right */
+		if ( this.s.init.iFixedColumns )
+		{
+			this.s.fixed = this.s.init.iFixedColumns;
+		}
+
+		if ( this.s.init.iFixedColumnsLeft )
+		{
+			this.s.fixed = this.s.init.iFixedColumnsLeft;
+		}
+
+		/* Columns discounted from reordering - counting right to left */
+		this.s.fixedRight = this.s.init.iFixedColumnsRight ?
+			this.s.init.iFixedColumnsRight :
+			0;
+
+		/* Drop callback initialisation option */
+		if ( this.s.init.fnReorderCallback )
+		{
+			this.s.reorderCallback = this.s.init.fnReorderCallback;
+		}
+
+		/* Add event handlers for the drag and drop, and also mark the original column order */
+		for ( i = 0; i < iLen; i++ )
+		{
+			if ( i > this.s.fixed-1 && i < iLen - this.s.fixedRight )
+			{
+				this._fnMouseListener( i, this.s.dt.aoColumns[i].nTh );
+			}
+
+			/* Mark the original column order for later reference */
+			this.s.dt.aoColumns[i]._ColReorder_iOrigCol = i;
+		}
+
+		/* State saving */
+		this.s.dt.oApi._fnCallbackReg( this.s.dt, 'aoStateSaveParams', function (oS, oData) {
+			that._fnStateSave.call( that, oData );
+		}, "ColReorder_State" );
+
+		/* An initial column order has been specified */
+		var aiOrder = null;
+		if ( this.s.init.aiOrder )
+		{
+			aiOrder = this.s.init.aiOrder.slice();
+		}
+
+		/* State loading, overrides the column order given */
+		if ( this.s.dt.oLoadedState && typeof this.s.dt.oLoadedState.ColReorder != 'undefined' &&
+		  this.s.dt.oLoadedState.ColReorder.length == this.s.dt.aoColumns.length )
+		{
+			aiOrder = this.s.dt.oLoadedState.ColReorder;
+		}
+
+		/* If we have an order to apply - do so */
+		if ( aiOrder )
+		{
+			/* We might be called during or after the DataTables initialisation. If before, then we need
+			 * to wait until the draw is done, if after, then do what we need to do right away
+			 */
+			if ( !that.s.dt._bInitComplete )
+			{
+				var bDone = false;
+				$(table).on( 'draw.dt.colReorder', function () {
+					if ( !that.s.dt._bInitComplete && !bDone )
+					{
+						bDone = true;
+						var resort = fnInvertKeyValues( aiOrder );
+						that._fnOrderColumns.call( that, resort );
+					}
+				} );
+			}
+			else
+			{
+				var resort = fnInvertKeyValues( aiOrder );
+				that._fnOrderColumns.call( that, resort );
+			}
+		}
+		else {
+			this._fnSetColumnIndexes();
+		}
+
+		// Destroy clean up
+		$(table).on( 'destroy.dt.colReorder', function () {
+			$(table).off( 'destroy.dt.colReorder draw.dt.colReorder' );
+			$(that.s.dt.nTHead).find( '*' ).off( '.ColReorder' );
+
+			$.each( that.s.dt.aoColumns, function (i, column) {
+				$(column.nTh).removeAttr('data-column-index');
+			} );
+
+			that.s.dt._colReorder = null;
+			that.s = null;
+		} );
+	},
+
+
+	/**
+	 * Set the column order from an array
+	 *  @method  _fnOrderColumns
+	 *  @param   array a An array of integers which dictate the column order that should be applied
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnOrderColumns": function ( a )
+	{
+		var changed = false;
+
+		if ( a.length != this.s.dt.aoColumns.length )
+		{
+			this.s.dt.oInstance.oApi._fnLog( this.s.dt, 1, "ColReorder - array reorder does not "+
+				"match known number of columns. Skipping." );
+			return;
+		}
+
+		for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+		{
+			var currIndex = $.inArray( i, a );
+			if ( i != currIndex )
+			{
+				/* Reorder our switching array */
+				fnArraySwitch( a, currIndex, i );
+
+				/* Do the column reorder in the table */
+				this.s.dt.oInstance.fnColReorder( currIndex, i, true, false );
+
+				changed = true;
+			}
+		}
+
+		$.fn.dataTable.Api( this.s.dt ).rows().invalidate();
+
+		this._fnSetColumnIndexes();
+
+		// Has anything actually changed? If not, then nothing else to do
+		if ( ! changed ) {
+			return;
+		}
+
+		/* When scrolling we need to recalculate the column sizes to allow for the shift */
+		if ( this.s.dt.oScroll.sX !== "" || this.s.dt.oScroll.sY !== "" )
+		{
+			this.s.dt.oInstance.fnAdjustColumnSizing( false );
+		}
+
+		/* Save the state */
+		this.s.dt.oInstance.oApi._fnSaveState( this.s.dt );
+
+		if ( this.s.reorderCallback !== null )
+		{
+			this.s.reorderCallback.call( this );
+		}
+	},
+
+
+	/**
+	 * Because we change the indexes of columns in the table, relative to their starting point
+	 * we need to reorder the state columns to what they are at the starting point so we can
+	 * then rearrange them again on state load!
+	 *  @method  _fnStateSave
+	 *  @param   object oState DataTables state
+	 *  @returns string JSON encoded cookie string for DataTables
+	 *  @private
+	 */
+	"_fnStateSave": function ( oState )
+	{
+		var i, iLen, aCopy, iOrigColumn;
+		var oSettings = this.s.dt;
+		var columns = oSettings.aoColumns;
+
+		oState.ColReorder = [];
+
+		/* Sorting */
+		if ( oState.aaSorting ) {
+			// 1.10.0-
+			for ( i=0 ; i<oState.aaSorting.length ; i++ ) {
+				oState.aaSorting[i][0] = columns[ oState.aaSorting[i][0] ]._ColReorder_iOrigCol;
+			}
+
+			var aSearchCopy = $.extend( true, [], oState.aoSearchCols );
+
+			for ( i=0, iLen=columns.length ; i<iLen ; i++ )
+			{
+				iOrigColumn = columns[i]._ColReorder_iOrigCol;
+
+				/* Column filter */
+				oState.aoSearchCols[ iOrigColumn ] = aSearchCopy[i];
+
+				/* Visibility */
+				oState.abVisCols[ iOrigColumn ] = columns[i].bVisible;
+
+				/* Column reordering */
+				oState.ColReorder.push( iOrigColumn );
+			}
+		}
+		else if ( oState.order ) {
+			// 1.10.1+
+			for ( i=0 ; i<oState.order.length ; i++ ) {
+				oState.order[i][0] = columns[ oState.order[i][0] ]._ColReorder_iOrigCol;
+			}
+
+			var stateColumnsCopy = $.extend( true, [], oState.columns );
+
+			for ( i=0, iLen=columns.length ; i<iLen ; i++ )
+			{
+				iOrigColumn = columns[i]._ColReorder_iOrigCol;
+
+				/* Columns */
+				oState.columns[ iOrigColumn ] = stateColumnsCopy[i];
+
+				/* Column reordering */
+				oState.ColReorder.push( iOrigColumn );
+			}
+		}
+	},
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Mouse drop and drag
+	 */
+
+	/**
+	 * Add a mouse down listener to a particluar TH element
+	 *  @method  _fnMouseListener
+	 *  @param   int i Column index
+	 *  @param   element nTh TH element clicked on
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnMouseListener": function ( i, nTh )
+	{
+		var that = this;
+		$(nTh)
+			.on( 'mousedown.ColReorder', function (e) {
+				e.preventDefault();
+				that._fnMouseDown.call( that, e, nTh );
+			} )
+			.on( 'touchstart.ColReorder', function (e) {
+				that._fnMouseDown.call( that, e, nTh );
+			} );
+	},
+
+
+	/**
+	 * Mouse down on a TH element in the table header
+	 *  @method  _fnMouseDown
+	 *  @param   event e Mouse event
+	 *  @param   element nTh TH element to be dragged
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnMouseDown": function ( e, nTh )
+	{
+		var that = this;
+
+		/* Store information about the mouse position */
+		var target = $(e.target).closest('th, td');
+		var offset = target.offset();
+		var idx = parseInt( $(nTh).attr('data-column-index'), 10 );
+
+		if ( idx === undefined ) {
+			return;
+		}
+
+		this.s.mouse.startX = this._fnCursorPosition( e, 'pageX' );
+		this.s.mouse.startY = this._fnCursorPosition( e, 'pageY' );
+		this.s.mouse.offsetX = this._fnCursorPosition( e, 'pageX' ) - offset.left;
+		this.s.mouse.offsetY = this._fnCursorPosition( e, 'pageY' ) - offset.top;
+		this.s.mouse.target = this.s.dt.aoColumns[ idx ].nTh;//target[0];
+		this.s.mouse.targetIndex = idx;
+		this.s.mouse.fromIndex = idx;
+
+		this._fnRegions();
+
+		/* Add event handlers to the document */
+		$(document)
+			.on( 'mousemove.ColReorder touchmove.ColReorder', function (e) {
+				that._fnMouseMove.call( that, e );
+			} )
+			.on( 'mouseup.ColReorder touchend.ColReorder', function (e) {
+				that._fnMouseUp.call( that, e );
+			} );
+	},
+
+
+	/**
+	 * Deal with a mouse move event while dragging a node
+	 *  @method  _fnMouseMove
+	 *  @param   event e Mouse event
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnMouseMove": function ( e )
+	{
+		var that = this;
+
+		if ( this.dom.drag === null )
+		{
+			/* Only create the drag element if the mouse has moved a specific distance from the start
+			 * point - this allows the user to make small mouse movements when sorting and not have a
+			 * possibly confusing drag element showing up
+			 */
+			if ( Math.pow(
+				Math.pow(this._fnCursorPosition( e, 'pageX') - this.s.mouse.startX, 2) +
+				Math.pow(this._fnCursorPosition( e, 'pageY') - this.s.mouse.startY, 2), 0.5 ) < 5 )
+			{
+				return;
+			}
+			this._fnCreateDragNode();
+		}
+
+		/* Position the element - we respect where in the element the click occured */
+		this.dom.drag.css( {
+			left: this._fnCursorPosition( e, 'pageX' ) - this.s.mouse.offsetX,
+			top: this._fnCursorPosition( e, 'pageY' ) - this.s.mouse.offsetY
+		} );
+
+		/* Based on the current mouse position, calculate where the insert should go */
+		var bSet = false;
+		var lastToIndex = this.s.mouse.toIndex;
+
+		for ( var i=1, iLen=this.s.aoTargets.length ; i<iLen ; i++ )
+		{
+			if ( this._fnCursorPosition(e, 'pageX') < this.s.aoTargets[i-1].x + ((this.s.aoTargets[i].x-this.s.aoTargets[i-1].x)/2) )
+			{
+				this.dom.pointer.css( 'left', this.s.aoTargets[i-1].x );
+				this.s.mouse.toIndex = this.s.aoTargets[i-1].to;
+				bSet = true;
+				break;
+			}
+		}
+
+		// The insert element wasn't positioned in the array (less than
+		// operator), so we put it at the end
+		if ( !bSet )
+		{
+			this.dom.pointer.css( 'left', this.s.aoTargets[this.s.aoTargets.length-1].x );
+			this.s.mouse.toIndex = this.s.aoTargets[this.s.aoTargets.length-1].to;
+		}
+
+		// Perform reordering if realtime updating is on and the column has moved
+		if ( this.s.init.bRealtime && lastToIndex !== this.s.mouse.toIndex ) {
+			this.s.dt.oInstance.fnColReorder( this.s.mouse.fromIndex, this.s.mouse.toIndex, false );
+			this.s.mouse.fromIndex = this.s.mouse.toIndex;
+			this._fnRegions();
+		}
+	},
+
+
+	/**
+	 * Finish off the mouse drag and insert the column where needed
+	 *  @method  _fnMouseUp
+	 *  @param   event e Mouse event
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnMouseUp": function ( e )
+	{
+		var that = this;
+
+		$(document).off( '.ColReorder' );
+
+		if ( this.dom.drag !== null )
+		{
+			/* Remove the guide elements */
+			this.dom.drag.remove();
+			this.dom.pointer.remove();
+			this.dom.drag = null;
+			this.dom.pointer = null;
+
+			/* Actually do the reorder */
+			this.s.dt.oInstance.fnColReorder( this.s.mouse.fromIndex, this.s.mouse.toIndex, true );
+			this._fnSetColumnIndexes();
+
+			/* When scrolling we need to recalculate the column sizes to allow for the shift */
+			if ( this.s.dt.oScroll.sX !== "" || this.s.dt.oScroll.sY !== "" )
+			{
+				this.s.dt.oInstance.fnAdjustColumnSizing( false );
+			}
+
+			/* Save the state */
+			this.s.dt.oInstance.oApi._fnSaveState( this.s.dt );
+
+			if ( this.s.reorderCallback !== null )
+			{
+				this.s.reorderCallback.call( this );
+			}
+		}
+	},
+
+
+	/**
+	 * Calculate a cached array with the points of the column inserts, and the
+	 * 'to' points
+	 *  @method  _fnRegions
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnRegions": function ()
+	{
+		var aoColumns = this.s.dt.aoColumns;
+
+		this.s.aoTargets.splice( 0, this.s.aoTargets.length );
+
+		this.s.aoTargets.push( {
+			"x":  $(this.s.dt.nTable).offset().left,
+			"to": 0
+		} );
+
+		var iToPoint = 0;
+		var total = this.s.aoTargets[0].x;
+
+		for ( var i=0, iLen=aoColumns.length ; i<iLen ; i++ )
+		{
+			/* For the column / header in question, we want it's position to remain the same if the
+			 * position is just to it's immediate left or right, so we only increment the counter for
+			 * other columns
+			 */
+			if ( i != this.s.mouse.fromIndex )
+			{
+				iToPoint++;
+			}
+
+			if ( aoColumns[i].bVisible && aoColumns[i].nTh.style.display !=='none' )
+			{
+				total += $(aoColumns[i].nTh).outerWidth();
+
+				this.s.aoTargets.push( {
+					"x":  total,
+					"to": iToPoint
+				} );
+			}
+		}
+
+		/* Disallow columns for being reordered by drag and drop, counting right to left */
+		if ( this.s.fixedRight !== 0 )
+		{
+			this.s.aoTargets.splice( this.s.aoTargets.length - this.s.fixedRight );
+		}
+
+		/* Disallow columns for being reordered by drag and drop, counting left to right */
+		if ( this.s.fixed !== 0 )
+		{
+			this.s.aoTargets.splice( 0, this.s.fixed );
+		}
+	},
+
+
+	/**
+	 * Copy the TH element that is being drags so the user has the idea that they are actually
+	 * moving it around the page.
+	 *  @method  _fnCreateDragNode
+	 *  @returns void
+	 *  @private
+	 */
+	"_fnCreateDragNode": function ()
+	{
+		var scrolling = this.s.dt.oScroll.sX !== "" || this.s.dt.oScroll.sY !== "";
+
+		var origCell = this.s.dt.aoColumns[ this.s.mouse.targetIndex ].nTh;
+		var origTr = origCell.parentNode;
+		var origThead = origTr.parentNode;
+		var origTable = origThead.parentNode;
+		var cloneCell = $(origCell).clone();
+
+		// This is a slightly odd combination of jQuery and DOM, but it is the
+		// fastest and least resource intensive way I could think of cloning
+		// the table with just a single header cell in it.
+		this.dom.drag = $(origTable.cloneNode(false))
+			.addClass( 'DTCR_clonedTable' )
+			.append(
+				$(origThead.cloneNode(false)).append(
+					$(origTr.cloneNode(false)).append(
+						cloneCell[0]
+					)
+				)
+			)
+			.css( {
+				position: 'absolute',
+				top: 0,
+				left: 0,
+				width: $(origCell).outerWidth(),
+				height: $(origCell).outerHeight()
+			} )
+			.appendTo( 'body' );
+
+		this.dom.pointer = $('<div></div>')
+			.addClass( 'DTCR_pointer' )
+			.css( {
+				position: 'absolute',
+				top: scrolling ?
+					$('div.dataTables_scroll', this.s.dt.nTableWrapper).offset().top :
+					$(this.s.dt.nTable).offset().top,
+				height : scrolling ?
+					$('div.dataTables_scroll', this.s.dt.nTableWrapper).height() :
+					$(this.s.dt.nTable).height()
+			} )
+			.appendTo( 'body' );
+	},
+
+
+	/**
+	 * Add a data attribute to the column headers, so we know the index of
+	 * the row to be reordered. This allows fast detection of the index, and
+	 * for this plug-in to work with FixedHeader which clones the nodes.
+	 *  @private
+	 */
+	"_fnSetColumnIndexes": function ()
+	{
+		$.each( this.s.dt.aoColumns, function (i, column) {
+			$(column.nTh).attr('data-column-index', i);
+		} );
+	},
+
+
+	/**
+	 * Get cursor position regardless of mouse or touch input
+	 * @param  {Event}  e    jQuery Event
+	 * @param  {string} prop Property to get
+	 * @return {number}      Value
+	 */
+	_fnCursorPosition: function ( e, prop ) {
+		if ( e.type.indexOf('touch') !== -1 ) {
+			return e.originalEvent.touches[0][ prop ];
+		}
+		return e[ prop ];
+	}
+} );
+
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Static parameters
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+/**
+ * ColReorder default settings for initialisation
+ *  @namespace
+ *  @static
+ */
+ColReorder.defaults = {
+	/**
+	 * Predefined ordering for the columns that will be applied automatically
+	 * on initialisation. If not specified then the order that the columns are
+	 * found to be in the HTML is the order used.
+	 *  @type array
+	 *  @default null
+	 *  @static
+	 */
+	aiOrder: null,
+
+	/**
+	 * Redraw the table's column ordering as the end user draws the column
+	 * (`true`) or wait until the mouse is released (`false` - default). Note
+	 * that this will perform a redraw on each reordering, which involves an
+	 * Ajax request each time if you are using server-side processing in
+	 * DataTables.
+	 *  @type boolean
+	 *  @default false
+	 *  @static
+	 */
+	bRealtime: true,
+
+	/**
+	 * Indicate how many columns should be fixed in position (counting from the
+	 * left). This will typically be 1 if used, but can be as high as you like.
+	 *  @type int
+	 *  @default 0
+	 *  @static
+	 */
+	iFixedColumnsLeft: 0,
+
+	/**
+	 * As `iFixedColumnsRight` but counting from the right.
+	 *  @type int
+	 *  @default 0
+	 *  @static
+	 */
+	iFixedColumnsRight: 0,
+
+	/**
+	 * Callback function that is fired when columns are reordered. The `column-
+	 * reorder` event is preferred over this callback
+	 *  @type function():void
+	 *  @default null
+	 *  @static
+	 */
+	fnReorderCallback: null
+};
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Constants
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * ColReorder version
+ *  @constant  version
+ *  @type      String
+ *  @default   As code
+ */
+ColReorder.version = "1.3.3";
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables interfaces
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+// Expose
+$.fn.dataTable.ColReorder = ColReorder;
+$.fn.DataTable.ColReorder = ColReorder;
+
+
+// Register a new feature with DataTables
+if ( typeof $.fn.dataTable == "function" &&
+     typeof $.fn.dataTableExt.fnVersionCheck == "function" &&
+     $.fn.dataTableExt.fnVersionCheck('1.10.8') )
+{
+	$.fn.dataTableExt.aoFeatures.push( {
+		"fnInit": function( settings ) {
+			var table = settings.oInstance;
+
+			if ( ! settings._colReorder ) {
+				var dtInit = settings.oInit;
+				var opts = dtInit.colReorder || dtInit.oColReorder || {};
+
+				new ColReorder( settings, opts );
+			}
+			else {
+				table.oApi._fnLog( settings, 1, "ColReorder attempted to initialise twice. Ignoring second" );
+			}
+
+			return null; /* No node for DataTables to insert */
+		},
+		"cFeature": "R",
+		"sFeature": "ColReorder"
+	} );
+}
+else {
+	alert( "Warning: ColReorder requires DataTables 1.10.8 or greater - www.datatables.net/download");
+}
+
+
+// Attach a listener to the document which listens for DataTables initialisation
+// events so we can automatically initialise
+$(document).on( 'preInit.dt.colReorder', function (e, settings) {
+	if ( e.namespace !== 'dt' ) {
+		return;
+	}
+
+	var init = settings.oInit.colReorder;
+	var defaults = DataTable.defaults.colReorder;
+
+	if ( init || defaults ) {
+		var opts = $.extend( {}, init, defaults );
+
+		if ( init !== false ) {
+			new ColReorder( settings, opts  );
+		}
+	}
+} );
+
+
+// API augmentation
+$.fn.dataTable.Api.register( 'colReorder.reset()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		ctx._colReorder.fnReset();
+	} );
+} );
+
+$.fn.dataTable.Api.register( 'colReorder.order()', function ( set, original ) {
+	if ( set ) {
+		return this.iterator( 'table', function ( ctx ) {
+			ctx._colReorder.fnOrder( set, original );
+		} );
+	}
+
+	return this.context.length ?
+		this.context[0]._colReorder.fnOrder() :
+		null;
+} );
+
+$.fn.dataTable.Api.register( 'colReorder.transpose()', function ( idx, dir ) {
+	return this.context.length && this.context[0]._colReorder ?
+		this.context[0]._colReorder.fnTranspose( idx, dir ) :
+		idx;
+} );
+
+
+return ColReorder;
+}));
+
+
+/*! FixedColumns 3.2.2
+ * ©2010-2016 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary     FixedColumns
+ * @description Freeze columns in place on a scrolling DataTable
+ * @version     3.2.2
+ * @file        dataTables.fixedColumns.js
+ * @author      SpryMedia Ltd (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ * @copyright   Copyright 2010-2016 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ *   MIT license - http://datatables.net/license/mit
+ *
+ * This source file 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 license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+var _firefoxScroll;
+
+/**
+ * When making use of DataTables' x-axis scrolling feature, you may wish to
+ * fix the left most column in place. This plug-in for DataTables provides
+ * exactly this option (note for non-scrolling tables, please use the
+ * FixedHeader plug-in, which can fix headers, footers and columns). Key
+ * features include:
+ *
+ * * Freezes the left or right most columns to the side of the table
+ * * Option to freeze two or more columns
+ * * Full integration with DataTables' scrolling options
+ * * Speed - FixedColumns is fast in its operation
+ *
+ *  @class
+ *  @constructor
+ *  @global
+ *  @param {object} dt DataTables instance. With DataTables 1.10 this can also
+ *    be a jQuery collection, a jQuery selector, DataTables API instance or
+ *    settings object.
+ *  @param {object} [init={}] Configuration object for FixedColumns. Options are
+ *    defined by {@link FixedColumns.defaults}
+ *
+ *  @requires jQuery 1.7+
+ *  @requires DataTables 1.8.0+
+ *
+ *  @example
+ *      var table = $('#example').dataTable( {
+ *        "scrollX": "100%"
+ *      } );
+ *      new $.fn.dataTable.fixedColumns( table );
+ */
+var FixedColumns = function ( dt, init ) {
+	var that = this;
+
+	/* Sanity check - you just know it will happen */
+	if ( ! ( this instanceof FixedColumns ) ) {
+		alert( "FixedColumns warning: FixedColumns must be initialised with the 'new' keyword." );
+		return;
+	}
+
+	if ( init === undefined || init === true ) {
+		init = {};
+	}
+
+	// Use the DataTables Hungarian notation mapping method, if it exists to
+	// provide forwards compatibility for camel case variables
+	var camelToHungarian = $.fn.dataTable.camelToHungarian;
+	if ( camelToHungarian ) {
+		camelToHungarian( FixedColumns.defaults, FixedColumns.defaults, true );
+		camelToHungarian( FixedColumns.defaults, init );
+	}
+
+	// v1.10 allows the settings object to be got form a number of sources
+	var dtSettings = new $.fn.dataTable.Api( dt ).settings()[0];
+
+	/**
+	 * Settings object which contains customisable information for FixedColumns instance
+	 * @namespace
+	 * @extends FixedColumns.defaults
+	 * @private
+	 */
+	this.s = {
+		/**
+		 * DataTables settings objects
+		 *  @type     object
+		 *  @default  Obtained from DataTables instance
+		 */
+		"dt": dtSettings,
+
+		/**
+		 * Number of columns in the DataTable - stored for quick access
+		 *  @type     int
+		 *  @default  Obtained from DataTables instance
+		 */
+		"iTableColumns": dtSettings.aoColumns.length,
+
+		/**
+		 * Original outer widths of the columns as rendered by DataTables - used to calculate
+		 * the FixedColumns grid bounding box
+		 *  @type     array.<int>
+		 *  @default  []
+		 */
+		"aiOuterWidths": [],
+
+		/**
+		 * Original inner widths of the columns as rendered by DataTables - used to apply widths
+		 * to the columns
+		 *  @type     array.<int>
+		 *  @default  []
+		 */
+		"aiInnerWidths": [],
+
+
+		/**
+		 * Is the document layout right-to-left
+		 * @type boolean
+		 */
+		rtl: $(dtSettings.nTable).css('direction') === 'rtl'
+	};
+
+
+	/**
+	 * DOM elements used by the class instance
+	 * @namespace
+	 * @private
+	 *
+	 */
+	this.dom = {
+		/**
+		 * DataTables scrolling element
+		 *  @type     node
+		 *  @default  null
+		 */
+		"scroller": null,
+
+		/**
+		 * DataTables header table
+		 *  @type     node
+		 *  @default  null
+		 */
+		"header": null,
+
+		/**
+		 * DataTables body table
+		 *  @type     node
+		 *  @default  null
+		 */
+		"body": null,
+
+		/**
+		 * DataTables footer table
+		 *  @type     node
+		 *  @default  null
+		 */
+		"footer": null,
+
+		/**
+		 * Display grid elements
+		 * @namespace
+		 */
+		"grid": {
+			/**
+			 * Grid wrapper. This is the container element for the 3x3 grid
+			 *  @type     node
+			 *  @default  null
+			 */
+			"wrapper": null,
+
+			/**
+			 * DataTables scrolling element. This element is the DataTables
+			 * component in the display grid (making up the main table - i.e.
+			 * not the fixed columns).
+			 *  @type     node
+			 *  @default  null
+			 */
+			"dt": null,
+
+			/**
+			 * Left fixed column grid components
+			 * @namespace
+			 */
+			"left": {
+				"wrapper": null,
+				"head": null,
+				"body": null,
+				"foot": null
+			},
+
+			/**
+			 * Right fixed column grid components
+			 * @namespace
+			 */
+			"right": {
+				"wrapper": null,
+				"head": null,
+				"body": null,
+				"foot": null
+			}
+		},
+
+		/**
+		 * Cloned table nodes
+		 * @namespace
+		 */
+		"clone": {
+			/**
+			 * Left column cloned table nodes
+			 * @namespace
+			 */
+			"left": {
+				/**
+				 * Cloned header table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"header": null,
+
+				/**
+				 * Cloned body table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"body": null,
+
+				/**
+				 * Cloned footer table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"footer": null
+			},
+
+			/**
+			 * Right column cloned table nodes
+			 * @namespace
+			 */
+			"right": {
+				/**
+				 * Cloned header table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"header": null,
+
+				/**
+				 * Cloned body table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"body": null,
+
+				/**
+				 * Cloned footer table
+				 *  @type     node
+				 *  @default  null
+				 */
+				"footer": null
+			}
+		}
+	};
+
+	if ( dtSettings._oFixedColumns ) {
+		throw 'FixedColumns already initialised on this table';
+	}
+
+	/* Attach the instance to the DataTables instance so it can be accessed easily */
+	dtSettings._oFixedColumns = this;
+
+	/* Let's do it */
+	if ( ! dtSettings._bInitComplete )
+	{
+		dtSettings.oApi._fnCallbackReg( dtSettings, 'aoInitComplete', function () {
+			that._fnConstruct( init );
+		}, 'FixedColumns' );
+	}
+	else
+	{
+		this._fnConstruct( init );
+	}
+};
+
+
+
+$.extend( FixedColumns.prototype , {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Public methods
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+	/**
+	 * Update the fixed columns - including headers and footers. Note that FixedColumns will
+	 * automatically update the display whenever the host DataTable redraws.
+	 *  @returns {void}
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      var fc = new $.fn.dataTable.fixedColumns( table );
+	 *
+	 *      // at some later point when the table has been manipulated....
+	 *      fc.fnUpdate();
+	 */
+	"fnUpdate": function ()
+	{
+		this._fnDraw( true );
+	},
+
+
+	/**
+	 * Recalculate the resizes of the 3x3 grid that FixedColumns uses for display of the table.
+	 * This is useful if you update the width of the table container. Note that FixedColumns will
+	 * perform this function automatically when the window.resize event is fired.
+	 *  @returns {void}
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      var fc = new $.fn.dataTable.fixedColumns( table );
+	 *
+	 *      // Resize the table container and then have FixedColumns adjust its layout....
+	 *      $('#content').width( 1200 );
+	 *      fc.fnRedrawLayout();
+	 */
+	"fnRedrawLayout": function ()
+	{
+		this._fnColCalc();
+		this._fnGridLayout();
+		this.fnUpdate();
+	},
+
+
+	/**
+	 * Mark a row such that it's height should be recalculated when using 'semiauto' row
+	 * height matching. This function will have no effect when 'none' or 'auto' row height
+	 * matching is used.
+	 *  @param   {Node} nTr TR element that should have it's height recalculated
+	 *  @returns {void}
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      var fc = new $.fn.dataTable.fixedColumns( table );
+	 *
+	 *      // manipulate the table - mark the row as needing an update then update the table
+	 *      // this allows the redraw performed by DataTables fnUpdate to recalculate the row
+	 *      // height
+	 *      fc.fnRecalculateHeight();
+	 *      table.fnUpdate( $('#example tbody tr:eq(0)')[0], ["insert date", 1, 2, 3 ... ]);
+	 */
+	"fnRecalculateHeight": function ( nTr )
+	{
+		delete nTr._DTTC_iHeight;
+		nTr.style.height = 'auto';
+	},
+
+
+	/**
+	 * Set the height of a given row - provides cross browser compatibility
+	 *  @param   {Node} nTarget TR element that should have it's height recalculated
+	 *  @param   {int} iHeight Height in pixels to set
+	 *  @returns {void}
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      var fc = new $.fn.dataTable.fixedColumns( table );
+	 *
+	 *      // You may want to do this after manipulating a row in the fixed column
+	 *      fc.fnSetRowHeight( $('#example tbody tr:eq(0)')[0], 50 );
+	 */
+	"fnSetRowHeight": function ( nTarget, iHeight )
+	{
+		nTarget.style.height = iHeight+"px";
+	},
+
+
+	/**
+	 * Get data index information about a row or cell in the table body.
+	 * This function is functionally identical to fnGetPosition in DataTables,
+	 * taking the same parameter (TH, TD or TR node) and returning exactly the
+	 * the same information (data index information). THe difference between
+	 * the two is that this method takes into account the fixed columns in the
+	 * table, so you can pass in nodes from the master table, or the cloned
+	 * tables and get the index position for the data in the main table.
+	 *  @param {node} node TR, TH or TD element to get the information about
+	 *  @returns {int} If nNode is given as a TR, then a single index is 
+	 *    returned, or if given as a cell, an array of [row index, column index
+	 *    (visible), column index (all)] is given.
+	 */
+	"fnGetPosition": function ( node )
+	{
+		var idx;
+		var inst = this.s.dt.oInstance;
+
+		if ( ! $(node).parents('.DTFC_Cloned').length )
+		{
+			// Not in a cloned table
+			return inst.fnGetPosition( node );
+		}
+		else
+		{
+			// Its in the cloned table, so need to look up position
+			if ( node.nodeName.toLowerCase() === 'tr' ) {
+				idx = $(node).index();
+				return inst.fnGetPosition( $('tr', this.s.dt.nTBody)[ idx ] );
+			}
+			else
+			{
+				var colIdx = $(node).index();
+				idx = $(node.parentNode).index();
+				var row = inst.fnGetPosition( $('tr', this.s.dt.nTBody)[ idx ] );
+
+				return [
+					row,
+					colIdx,
+					inst.oApi._fnVisibleToColumnIndex( this.s.dt, colIdx )
+				];
+			}
+		}
+	},
+
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods (they are of course public in JS, but recommended as private)
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+	/**
+	 * Initialisation for FixedColumns
+	 *  @param   {Object} oInit User settings for initialisation
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnConstruct": function ( oInit )
+	{
+		var i, iLen, iWidth,
+			that = this;
+
+		/* Sanity checking */
+		if ( typeof this.s.dt.oInstance.fnVersionCheck != 'function' ||
+		     this.s.dt.oInstance.fnVersionCheck( '1.8.0' ) !== true )
+		{
+			alert( "FixedColumns "+FixedColumns.VERSION+" required DataTables 1.8.0 or later. "+
+				"Please upgrade your DataTables installation" );
+			return;
+		}
+
+		if ( this.s.dt.oScroll.sX === "" )
+		{
+			this.s.dt.oInstance.oApi._fnLog( this.s.dt, 1, "FixedColumns is not needed (no "+
+				"x-scrolling in DataTables enabled), so no action will be taken. Use 'FixedHeader' for "+
+				"column fixing when scrolling is not enabled" );
+			return;
+		}
+
+		/* Apply the settings from the user / defaults */
+		this.s = $.extend( true, this.s, FixedColumns.defaults, oInit );
+
+		/* Set up the DOM as we need it and cache nodes */
+		var classes = this.s.dt.oClasses;
+		this.dom.grid.dt = $(this.s.dt.nTable).parents('div.'+classes.sScrollWrapper)[0];
+		this.dom.scroller = $('div.'+classes.sScrollBody, this.dom.grid.dt )[0];
+
+		/* Set up the DOM that we want for the fixed column layout grid */
+		this._fnColCalc();
+		this._fnGridSetup();
+
+		/* Event handlers */
+		var mouseController;
+		var mouseDown = false;
+
+		// When the mouse is down (drag scroll) the mouse controller cannot
+		// change, as the browser keeps the original element as the scrolling one
+		$(this.s.dt.nTableWrapper).on( 'mousedown.DTFC', function () {
+			mouseDown = true;
+
+			$(document).one( 'mouseup', function () {
+				mouseDown = false;
+			} );
+		} );
+
+		// When the body is scrolled - scroll the left and right columns
+		$(this.dom.scroller)
+			.on( 'mouseover.DTFC touchstart.DTFC', function () {
+				if ( ! mouseDown ) {
+					mouseController = 'main';
+				}
+			} )
+			.on( 'scroll.DTFC', function (e) {
+				if ( ! mouseController && e.originalEvent ) {
+					mouseController = 'main';
+				}
+
+				if ( mouseController === 'main' ) {
+					if ( that.s.iLeftColumns > 0 ) {
+						that.dom.grid.left.liner.scrollTop = that.dom.scroller.scrollTop;
+					}
+					if ( that.s.iRightColumns > 0 ) {
+						that.dom.grid.right.liner.scrollTop = that.dom.scroller.scrollTop;
+					}
+				}
+			} );
+
+		var wheelType = 'onwheel' in document.createElement('div') ?
+			'wheel.DTFC' :
+			'mousewheel.DTFC';
+
+		if ( that.s.iLeftColumns > 0 ) {
+			// When scrolling the left column, scroll the body and right column
+			$(that.dom.grid.left.liner)
+				.on( 'mouseover.DTFC touchstart.DTFC', function () {
+					if ( ! mouseDown ) {
+						mouseController = 'left';
+					}
+				} )
+				.on( 'scroll.DTFC', function ( e ) {
+					if ( ! mouseController && e.originalEvent ) {
+						mouseController = 'left';
+					}
+
+					if ( mouseController === 'left' ) {
+						that.dom.scroller.scrollTop = that.dom.grid.left.liner.scrollTop;
+						if ( that.s.iRightColumns > 0 ) {
+							that.dom.grid.right.liner.scrollTop = that.dom.grid.left.liner.scrollTop;
+						}
+					}
+				} )
+				.on( wheelType, function(e) {
+					// Pass horizontal scrolling through
+					var xDelta = e.type === 'wheel' ?
+						-e.originalEvent.deltaX :
+						e.originalEvent.wheelDeltaX;
+					that.dom.scroller.scrollLeft -= xDelta;
+				} );
+		}
+
+		if ( that.s.iRightColumns > 0 ) {
+			// When scrolling the right column, scroll the body and the left column
+			$(that.dom.grid.right.liner)
+				.on( 'mouseover.DTFC touchstart.DTFC', function () {
+					if ( ! mouseDown ) {
+						mouseController = 'right';
+					}
+				} )
+				.on( 'scroll.DTFC', function ( e ) {
+					if ( ! mouseController && e.originalEvent ) {
+						mouseController = 'right';
+					}
+
+					if ( mouseController === 'right' ) {
+						that.dom.scroller.scrollTop = that.dom.grid.right.liner.scrollTop;
+						if ( that.s.iLeftColumns > 0 ) {
+							that.dom.grid.left.liner.scrollTop = that.dom.grid.right.liner.scrollTop;
+						}
+					}
+				} )
+				.on( wheelType, function(e) {
+					// Pass horizontal scrolling through
+					var xDelta = e.type === 'wheel' ?
+						-e.originalEvent.deltaX :
+						e.originalEvent.wheelDeltaX;
+					that.dom.scroller.scrollLeft -= xDelta;
+				} );
+		}
+
+		$(window).on( 'resize.DTFC', function () {
+			that._fnGridLayout.call( that );
+		} );
+
+		var bFirstDraw = true;
+		var jqTable = $(this.s.dt.nTable);
+
+		jqTable
+			.on( 'draw.dt.DTFC', function () {
+				that._fnColCalc();
+				that._fnDraw.call( that, bFirstDraw );
+				bFirstDraw = false;
+			} )
+			.on( 'column-sizing.dt.DTFC', function () {
+				that._fnColCalc();
+				that._fnGridLayout( that );
+			} )
+			.on( 'column-visibility.dt.DTFC', function ( e, settings, column, vis, recalc ) {
+				if ( recalc === undefined || recalc ) {
+					that._fnColCalc();
+					that._fnGridLayout( that );
+					that._fnDraw( true );
+				}
+			} )
+			.on( 'select.dt.DTFC deselect.dt.DTFC', function ( e, dt, type, indexes ) {
+				if ( e.namespace === 'dt' ) {
+					that._fnDraw( false );
+				}
+			} )
+			.on( 'destroy.dt.DTFC', function () {
+				jqTable.off( '.DTFC' );
+
+				$(that.dom.scroller).off( '.DTFC' );
+				$(window).off( '.DTFC' );
+				$(that.s.dt.nTableWrapper).off( '.DTFC' );
+
+				$(that.dom.grid.left.liner).off( '.DTFC '+wheelType );
+				$(that.dom.grid.left.wrapper).remove();
+
+				$(that.dom.grid.right.liner).off( '.DTFC '+wheelType );
+				$(that.dom.grid.right.wrapper).remove();
+			} );
+
+		/* Get things right to start with - note that due to adjusting the columns, there must be
+		 * another redraw of the main table. It doesn't need to be a full redraw however.
+		 */
+		this._fnGridLayout();
+		this.s.dt.oInstance.fnDraw(false);
+	},
+
+
+	/**
+	 * Calculate the column widths for the grid layout
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnColCalc": function ()
+	{
+		var that = this;
+		var iLeftWidth = 0;
+		var iRightWidth = 0;
+
+		this.s.aiInnerWidths = [];
+		this.s.aiOuterWidths = [];
+
+		$.each( this.s.dt.aoColumns, function (i, col) {
+			var th = $(col.nTh);
+			var border;
+
+			if ( ! th.filter(':visible').length ) {
+				that.s.aiInnerWidths.push( 0 );
+				that.s.aiOuterWidths.push( 0 );
+			}
+			else
+			{
+				// Inner width is used to assign widths to cells
+				// Outer width is used to calculate the container
+				var iWidth = th.outerWidth();
+
+				// When working with the left most-cell, need to add on the
+				// table's border to the outerWidth, since we need to take
+				// account of it, but it isn't in any cell
+				if ( that.s.aiOuterWidths.length === 0 ) {
+					border = $(that.s.dt.nTable).css('border-left-width');
+					iWidth += typeof border === 'string' ? 1 : parseInt( border, 10 );
+				}
+
+				// Likewise with the final column on the right
+				if ( that.s.aiOuterWidths.length === that.s.dt.aoColumns.length-1 ) {
+					border = $(that.s.dt.nTable).css('border-right-width');
+					iWidth += typeof border === 'string' ? 1 : parseInt( border, 10 );
+				}
+
+				that.s.aiOuterWidths.push( iWidth );
+				that.s.aiInnerWidths.push( th.width() );
+
+				if ( i < that.s.iLeftColumns )
+				{
+					iLeftWidth += iWidth;
+				}
+
+				if ( that.s.iTableColumns-that.s.iRightColumns <= i )
+				{
+					iRightWidth += iWidth;
+				}
+			}
+		} );
+
+		this.s.iLeftWidth = iLeftWidth;
+		this.s.iRightWidth = iRightWidth;
+	},
+
+
+	/**
+	 * Set up the DOM for the fixed column. The way the layout works is to create a 1x3 grid
+	 * for the left column, the DataTable (for which we just reuse the scrolling element DataTable
+	 * puts into the DOM) and the right column. In each of he two fixed column elements there is a
+	 * grouping wrapper element and then a head, body and footer wrapper. In each of these we then
+	 * place the cloned header, body or footer tables. This effectively gives as 3x3 grid structure.
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnGridSetup": function ()
+	{
+		var that = this;
+		var oOverflow = this._fnDTOverflow();
+		var block;
+
+		this.dom.body = this.s.dt.nTable;
+		this.dom.header = this.s.dt.nTHead.parentNode;
+		this.dom.header.parentNode.parentNode.style.position = "relative";
+
+		var nSWrapper =
+			$('<div class="DTFC_ScrollWrapper" style="position:relative; clear:both;">'+
+				'<div class="DTFC_LeftWrapper" style="position:absolute; top:0; left:0;">'+
+					'<div class="DTFC_LeftHeadWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div>'+
+					'<div class="DTFC_LeftBodyWrapper" style="position:relative; top:0; left:0; overflow:hidden;">'+
+						'<div class="DTFC_LeftBodyLiner" style="position:relative; top:0; left:0; overflow-y:scroll;"></div>'+
+					'</div>'+
+					'<div class="DTFC_LeftFootWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div>'+
+				'</div>'+
+				'<div class="DTFC_RightWrapper" style="position:absolute; top:0; right:0;">'+
+					'<div class="DTFC_RightHeadWrapper" style="position:relative; top:0; left:0;">'+
+						'<div class="DTFC_RightHeadBlocker DTFC_Blocker" style="position:absolute; top:0; bottom:0;"></div>'+
+					'</div>'+
+					'<div class="DTFC_RightBodyWrapper" style="position:relative; top:0; left:0; overflow:hidden;">'+
+						'<div class="DTFC_RightBodyLiner" style="position:relative; top:0; left:0; overflow-y:scroll;"></div>'+
+					'</div>'+
+					'<div class="DTFC_RightFootWrapper" style="position:relative; top:0; left:0;">'+
+						'<div class="DTFC_RightFootBlocker DTFC_Blocker" style="position:absolute; top:0; bottom:0;"></div>'+
+					'</div>'+
+				'</div>'+
+			'</div>')[0];
+		var nLeft = nSWrapper.childNodes[0];
+		var nRight = nSWrapper.childNodes[1];
+
+		this.dom.grid.dt.parentNode.insertBefore( nSWrapper, this.dom.grid.dt );
+		nSWrapper.appendChild( this.dom.grid.dt );
+
+		this.dom.grid.wrapper = nSWrapper;
+
+		if ( this.s.iLeftColumns > 0 )
+		{
+			this.dom.grid.left.wrapper = nLeft;
+			this.dom.grid.left.head = nLeft.childNodes[0];
+			this.dom.grid.left.body = nLeft.childNodes[1];
+			this.dom.grid.left.liner = $('div.DTFC_LeftBodyLiner', nSWrapper)[0];
+
+			nSWrapper.appendChild( nLeft );
+		}
+
+		if ( this.s.iRightColumns > 0 )
+		{
+			this.dom.grid.right.wrapper = nRight;
+			this.dom.grid.right.head = nRight.childNodes[0];
+			this.dom.grid.right.body = nRight.childNodes[1];
+			this.dom.grid.right.liner = $('div.DTFC_RightBodyLiner', nSWrapper)[0];
+
+			nRight.style.right = oOverflow.bar+"px";
+
+			block = $('div.DTFC_RightHeadBlocker', nSWrapper)[0];
+			block.style.width = oOverflow.bar+"px";
+			block.style.right = -oOverflow.bar+"px";
+			this.dom.grid.right.headBlock = block;
+
+			block = $('div.DTFC_RightFootBlocker', nSWrapper)[0];
+			block.style.width = oOverflow.bar+"px";
+			block.style.right = -oOverflow.bar+"px";
+			this.dom.grid.right.footBlock = block;
+
+			nSWrapper.appendChild( nRight );
+		}
+
+		if ( this.s.dt.nTFoot )
+		{
+			this.dom.footer = this.s.dt.nTFoot.parentNode;
+			if ( this.s.iLeftColumns > 0 )
+			{
+				this.dom.grid.left.foot = nLeft.childNodes[2];
+			}
+			if ( this.s.iRightColumns > 0 )
+			{
+				this.dom.grid.right.foot = nRight.childNodes[2];
+			}
+		}
+
+		// RTL support - swap the position of the left and right columns (#48)
+		if ( this.s.rtl ) {
+			$('div.DTFC_RightHeadBlocker', nSWrapper).css( {
+				left: -oOverflow.bar+'px',
+				right: ''
+			} );
+		}
+	},
+
+
+	/**
+	 * Style and position the grid used for the FixedColumns layout
+	 *  @returns {void}
+	 *  @private
+	 */
+	"_fnGridLayout": function ()
+	{
+		var that = this;
+		var oGrid = this.dom.grid;
+		var iWidth = $(oGrid.wrapper).width();
+		var iBodyHeight = $(this.s.dt.nTable.parentNode).outerHeight();
+		var iFullHeight = $(this.s.dt.nTable.parentNode.parentNode).outerHeight();
+		var oOverflow = this._fnDTOverflow();
+		var iLeftWidth = this.s.iLeftWidth;
+		var iRightWidth = this.s.iRightWidth;
+		var rtl = $(this.dom.body).css('direction') === 'rtl';
+		var wrapper;
+		var scrollbarAdjust = function ( node, width ) {
+			if ( ! oOverflow.bar ) {
+				// If there is no scrollbar (Macs) we need to hide the auto scrollbar
+				node.style.width = (width+20)+"px";
+				node.style.paddingRight = "20px";
+				node.style.boxSizing = "border-box";
+			}
+			else if ( that._firefoxScrollError() ) {
+				// See the above function for why this is required
+				if ( $(node).height() > 34 ) {
+					node.style.width = (width+oOverflow.bar)+"px";
+				}
+			}
+			else {
+				// Otherwise just overflow by the scrollbar
+				node.style.width = (width+oOverflow.bar)+"px";
+			}
+		};
+
+		// When x scrolling - don't paint the fixed columns over the x scrollbar
+		if ( oOverflow.x )
+		{
+			iBodyHeight -= oOverflow.bar;
+		}
+
+		oGrid.wrapper.style.height = iFullHeight+"px";
+
+		if ( this.s.iLeftColumns > 0 )
+		{
+			wrapper = oGrid.left.wrapper;
+			wrapper.style.width = iLeftWidth+'px';
+			wrapper.style.height = '1px';
+
+			// Swap the position of the left and right columns for rtl (#48)
+			// This is always up against the edge, scrollbar on the far side
+			if ( rtl ) {
+				wrapper.style.left = '';
+				wrapper.style.right = 0;
+			}
+			else {
+				wrapper.style.left = 0;
+				wrapper.style.right = '';
+			}
+
+			oGrid.left.body.style.height = iBodyHeight+"px";
+			if ( oGrid.left.foot ) {
+				oGrid.left.foot.style.top = (oOverflow.x ? oOverflow.bar : 0)+"px"; // shift footer for scrollbar
+			}
+
+			scrollbarAdjust( oGrid.left.liner, iLeftWidth );
+			oGrid.left.liner.style.height = iBodyHeight+"px";
+		}
+
+		if ( this.s.iRightColumns > 0 )
+		{
+			wrapper = oGrid.right.wrapper;
+			wrapper.style.width = iRightWidth+'px';
+			wrapper.style.height = '1px';
+
+			// Need to take account of the vertical scrollbar
+			if ( this.s.rtl ) {
+				wrapper.style.left = oOverflow.y ? oOverflow.bar+'px' : 0;
+				wrapper.style.right = '';
+			}
+			else {
+				wrapper.style.left = '';
+				wrapper.style.right = oOverflow.y ? oOverflow.bar+'px' : 0;
+			}
+
+			oGrid.right.body.style.height = iBodyHeight+"px";
+			if ( oGrid.right.foot ) {
+				oGrid.right.foot.style.top = (oOverflow.x ? oOverflow.bar : 0)+"px";
+			}
+
+			scrollbarAdjust( oGrid.right.liner, iRightWidth );
+			oGrid.right.liner.style.height = iBodyHeight+"px";
+
+			oGrid.right.headBlock.style.display = oOverflow.y ? 'block' : 'none';
+			oGrid.right.footBlock.style.display = oOverflow.y ? 'block' : 'none';
+		}
+	},
+
+
+	/**
+	 * Get information about the DataTable's scrolling state - specifically if the table is scrolling
+	 * on either the x or y axis, and also the scrollbar width.
+	 *  @returns {object} Information about the DataTables scrolling state with the properties:
+	 *    'x', 'y' and 'bar'
+	 *  @private
+	 */
+	"_fnDTOverflow": function ()
+	{
+		var nTable = this.s.dt.nTable;
+		var nTableScrollBody = nTable.parentNode;
+		var out = {
+			"x": false,
+			"y": false,
+			"bar": this.s.dt.oScroll.iBarWidth
+		};
+
+		if ( nTable.offsetWidth > nTableScrollBody.clientWidth )
+		{
+			out.x = true;
+		}
+
+		if ( nTable.offsetHeight > nTableScrollBody.clientHeight )
+		{
+			out.y = true;
+		}
+
+		return out;
+	},
+
+
+	/**
+	 * Clone and position the fixed columns
+	 *  @returns {void}
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnDraw": function ( bAll )
+	{
+		this._fnGridLayout();
+		this._fnCloneLeft( bAll );
+		this._fnCloneRight( bAll );
+
+		/* Draw callback function */
+		if ( this.s.fnDrawCallback !== null )
+		{
+			this.s.fnDrawCallback.call( this, this.dom.clone.left, this.dom.clone.right );
+		}
+
+		/* Event triggering */
+		$(this).trigger( 'draw.dtfc', {
+			"leftClone": this.dom.clone.left,
+			"rightClone": this.dom.clone.right
+		} );
+	},
+
+
+	/**
+	 * Clone the right columns
+	 *  @returns {void}
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnCloneRight": function ( bAll )
+	{
+		if ( this.s.iRightColumns <= 0 ) {
+			return;
+		}
+
+		var that = this,
+			i, jq,
+			aiColumns = [];
+
+		for ( i=this.s.iTableColumns-this.s.iRightColumns ; i<this.s.iTableColumns ; i++ ) {
+			if ( this.s.dt.aoColumns[i].bVisible ) {
+				aiColumns.push( i );
+			}
+		}
+
+		this._fnClone( this.dom.clone.right, this.dom.grid.right, aiColumns, bAll );
+	},
+
+
+	/**
+	 * Clone the left columns
+	 *  @returns {void}
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnCloneLeft": function ( bAll )
+	{
+		if ( this.s.iLeftColumns <= 0 ) {
+			return;
+		}
+
+		var that = this,
+			i, jq,
+			aiColumns = [];
+
+		for ( i=0 ; i<this.s.iLeftColumns ; i++ ) {
+			if ( this.s.dt.aoColumns[i].bVisible ) {
+				aiColumns.push( i );
+			}
+		}
+
+		this._fnClone( this.dom.clone.left, this.dom.grid.left, aiColumns, bAll );
+	},
+
+
+	/**
+	 * Make a copy of the layout object for a header or footer element from DataTables. Note that
+	 * this method will clone the nodes in the layout object.
+	 *  @returns {Array} Copy of the layout array
+	 *  @param   {Object} aoOriginal Layout array from DataTables (aoHeader or aoFooter)
+	 *  @param   {Object} aiColumns Columns to copy
+	 *  @param   {boolean} events Copy cell events or not
+	 *  @private
+	 */
+	"_fnCopyLayout": function ( aoOriginal, aiColumns, events )
+	{
+		var aReturn = [];
+		var aClones = [];
+		var aCloned = [];
+
+		for ( var i=0, iLen=aoOriginal.length ; i<iLen ; i++ )
+		{
+			var aRow = [];
+			aRow.nTr = $(aoOriginal[i].nTr).clone(events, false)[0];
+
+			for ( var j=0, jLen=this.s.iTableColumns ; j<jLen ; j++ )
+			{
+				if ( $.inArray( j, aiColumns ) === -1 )
+				{
+					continue;
+				}
+
+				var iCloned = $.inArray( aoOriginal[i][j].cell, aCloned );
+				if ( iCloned === -1 )
+				{
+					var nClone = $(aoOriginal[i][j].cell).clone(events, false)[0];
+					aClones.push( nClone );
+					aCloned.push( aoOriginal[i][j].cell );
+
+					aRow.push( {
+						"cell": nClone,
+						"unique": aoOriginal[i][j].unique
+					} );
+				}
+				else
+				{
+					aRow.push( {
+						"cell": aClones[ iCloned ],
+						"unique": aoOriginal[i][j].unique
+					} );
+				}
+			}
+
+			aReturn.push( aRow );
+		}
+
+		return aReturn;
+	},
+
+
+	/**
+	 * Clone the DataTable nodes and place them in the DOM (sized correctly)
+	 *  @returns {void}
+	 *  @param   {Object} oClone Object containing the header, footer and body cloned DOM elements
+	 *  @param   {Object} oGrid Grid object containing the display grid elements for the cloned
+	 *                    column (left or right)
+	 *  @param   {Array} aiColumns Column indexes which should be operated on from the DataTable
+	 *  @param   {Boolean} bAll Indicate if the header and footer should be updated as well (true)
+	 *  @private
+	 */
+	"_fnClone": function ( oClone, oGrid, aiColumns, bAll )
+	{
+		var that = this,
+			i, iLen, j, jLen, jq, nTarget, iColumn, nClone, iIndex, aoCloneLayout,
+			jqCloneThead, aoFixedHeader,
+			dt = this.s.dt;
+
+		/*
+		 * Header
+		 */
+		if ( bAll )
+		{
+			$(oClone.header).remove();
+
+			oClone.header = $(this.dom.header).clone(true, false)[0];
+			oClone.header.className += " DTFC_Cloned";
+			oClone.header.style.width = "100%";
+			oGrid.head.appendChild( oClone.header );
+
+			/* Copy the DataTables layout cache for the header for our floating column */
+			aoCloneLayout = this._fnCopyLayout( dt.aoHeader, aiColumns, true );
+			jqCloneThead = $('>thead', oClone.header);
+			jqCloneThead.empty();
+
+			/* Add the created cloned TR elements to the table */
+			for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+			{
+				jqCloneThead[0].appendChild( aoCloneLayout[i].nTr );
+			}
+
+			/* Use the handy _fnDrawHead function in DataTables to do the rowspan/colspan
+			 * calculations for us
+			 */
+			dt.oApi._fnDrawHead( dt, aoCloneLayout, true );
+		}
+		else
+		{
+			/* To ensure that we copy cell classes exactly, regardless of colspan, multiple rows
+			 * etc, we make a copy of the header from the DataTable again, but don't insert the
+			 * cloned cells, just copy the classes across. To get the matching layout for the
+			 * fixed component, we use the DataTables _fnDetectHeader method, allowing 1:1 mapping
+			 */
+			aoCloneLayout = this._fnCopyLayout( dt.aoHeader, aiColumns, false );
+			aoFixedHeader=[];
+
+			dt.oApi._fnDetectHeader( aoFixedHeader, $('>thead', oClone.header)[0] );
+
+			for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+			{
+				for ( j=0, jLen=aoCloneLayout[i].length ; j<jLen ; j++ )
+				{
+					aoFixedHeader[i][j].cell.className = aoCloneLayout[i][j].cell.className;
+
+					// If jQuery UI theming is used we need to copy those elements as well
+					$('span.DataTables_sort_icon', aoFixedHeader[i][j].cell).each( function () {
+						this.className = $('span.DataTables_sort_icon', aoCloneLayout[i][j].cell)[0].className;
+					} );
+				}
+			}
+		}
+		this._fnEqualiseHeights( 'thead', this.dom.header, oClone.header );
+
+		/*
+		 * Body
+		 */
+		if ( this.s.sHeightMatch == 'auto' )
+		{
+			/* Remove any heights which have been applied already and let the browser figure it out */
+			$('>tbody>tr', that.dom.body).css('height', 'auto');
+		}
+
+		if ( oClone.body !== null )
+		{
+			$(oClone.body).remove();
+			oClone.body = null;
+		}
+
+		oClone.body = $(this.dom.body).clone(true)[0];
+		oClone.body.className += " DTFC_Cloned";
+		oClone.body.style.paddingBottom = dt.oScroll.iBarWidth+"px";
+		oClone.body.style.marginBottom = (dt.oScroll.iBarWidth*2)+"px"; /* For IE */
+		if ( oClone.body.getAttribute('id') !== null )
+		{
+			oClone.body.removeAttribute('id');
+		}
+
+		$('>thead>tr', oClone.body).empty();
+		$('>tfoot', oClone.body).remove();
+
+		var nBody = $('tbody', oClone.body)[0];
+		$(nBody).empty();
+		if ( dt.aiDisplay.length > 0 )
+		{
+			/* Copy the DataTables' header elements to force the column width in exactly the
+			 * same way that DataTables does it - have the header element, apply the width and
+			 * colapse it down
+			 */
+			var nInnerThead = $('>thead>tr', oClone.body)[0];
+			for ( iIndex=0 ; iIndex<aiColumns.length ; iIndex++ )
+			{
+				iColumn = aiColumns[iIndex];
+
+				nClone = $(dt.aoColumns[iColumn].nTh).clone(true)[0];
+				nClone.innerHTML = "";
+
+				var oStyle = nClone.style;
+				oStyle.paddingTop = "0";
+				oStyle.paddingBottom = "0";
+				oStyle.borderTopWidth = "0";
+				oStyle.borderBottomWidth = "0";
+				oStyle.height = 0;
+				oStyle.width = that.s.aiInnerWidths[iColumn]+"px";
+
+				nInnerThead.appendChild( nClone );
+			}
+
+			/* Add in the tbody elements, cloning form the master table */
+			$('>tbody>tr', that.dom.body).each( function (z) {
+				var i = that.s.dt.oFeatures.bServerSide===false ?
+					that.s.dt.aiDisplay[ that.s.dt._iDisplayStart+z ] : z;
+				var aTds = that.s.dt.aoData[ i ].anCells || $(this).children('td, th');
+
+				var n = this.cloneNode(false);
+				n.removeAttribute('id');
+				n.setAttribute( 'data-dt-row', i );
+
+				for ( iIndex=0 ; iIndex<aiColumns.length ; iIndex++ )
+				{
+					iColumn = aiColumns[iIndex];
+
+					if ( aTds.length > 0 )
+					{
+						nClone = $( aTds[iColumn] ).clone(true, true)[0];
+						nClone.setAttribute( 'data-dt-row', i );
+						nClone.setAttribute( 'data-dt-column', iIndex );
+						n.appendChild( nClone );
+					}
+				}
+				nBody.appendChild( n );
+			} );
+		}
+		else
+		{
+			$('>tbody>tr', that.dom.body).each( function (z) {
+				nClone = this.cloneNode(true);
+				nClone.className += ' DTFC_NoData';
+				$('td', nClone).html('');
+				nBody.appendChild( nClone );
+			} );
+		}
+
+		oClone.body.style.width = "100%";
+		oClone.body.style.margin = "0";
+		oClone.body.style.padding = "0";
+
+		// Interop with Scroller - need to use a height forcing element in the
+		// scrolling area in the same way that Scroller does in the body scroll.
+		if ( dt.oScroller !== undefined )
+		{
+			var scrollerForcer = dt.oScroller.dom.force;
+
+			if ( ! oGrid.forcer ) {
+				oGrid.forcer = scrollerForcer.cloneNode( true );
+				oGrid.liner.appendChild( oGrid.forcer );
+			}
+			else {
+				oGrid.forcer.style.height = scrollerForcer.style.height;
+			}
+		}
+
+		oGrid.liner.appendChild( oClone.body );
+
+		this._fnEqualiseHeights( 'tbody', that.dom.body, oClone.body );
+
+		/*
+		 * Footer
+		 */
+		if ( dt.nTFoot !== null )
+		{
+			if ( bAll )
+			{
+				if ( oClone.footer !== null )
+				{
+					oClone.footer.parentNode.removeChild( oClone.footer );
+				}
+				oClone.footer = $(this.dom.footer).clone(true, true)[0];
+				oClone.footer.className += " DTFC_Cloned";
+				oClone.footer.style.width = "100%";
+				oGrid.foot.appendChild( oClone.footer );
+
+				/* Copy the footer just like we do for the header */
+				aoCloneLayout = this._fnCopyLayout( dt.aoFooter, aiColumns, true );
+				var jqCloneTfoot = $('>tfoot', oClone.footer);
+				jqCloneTfoot.empty();
+
+				for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+				{
+					jqCloneTfoot[0].appendChild( aoCloneLayout[i].nTr );
+				}
+				dt.oApi._fnDrawHead( dt, aoCloneLayout, true );
+			}
+			else
+			{
+				aoCloneLayout = this._fnCopyLayout( dt.aoFooter, aiColumns, false );
+				var aoCurrFooter=[];
+
+				dt.oApi._fnDetectHeader( aoCurrFooter, $('>tfoot', oClone.footer)[0] );
+
+				for ( i=0, iLen=aoCloneLayout.length ; i<iLen ; i++ )
+				{
+					for ( j=0, jLen=aoCloneLayout[i].length ; j<jLen ; j++ )
+					{
+						aoCurrFooter[i][j].cell.className = aoCloneLayout[i][j].cell.className;
+					}
+				}
+			}
+			this._fnEqualiseHeights( 'tfoot', this.dom.footer, oClone.footer );
+		}
+
+		/* Equalise the column widths between the header footer and body - body get's priority */
+		var anUnique = dt.oApi._fnGetUniqueThs( dt, $('>thead', oClone.header)[0] );
+		$(anUnique).each( function (i) {
+			iColumn = aiColumns[i];
+			this.style.width = that.s.aiInnerWidths[iColumn]+"px";
+		} );
+
+		if ( that.s.dt.nTFoot !== null )
+		{
+			anUnique = dt.oApi._fnGetUniqueThs( dt, $('>tfoot', oClone.footer)[0] );
+			$(anUnique).each( function (i) {
+				iColumn = aiColumns[i];
+				this.style.width = that.s.aiInnerWidths[iColumn]+"px";
+			} );
+		}
+	},
+
+
+	/**
+	 * From a given table node (THEAD etc), get a list of TR direct child elements
+	 *  @param   {Node} nIn Table element to search for TR elements (THEAD, TBODY or TFOOT element)
+	 *  @returns {Array} List of TR elements found
+	 *  @private
+	 */
+	"_fnGetTrNodes": function ( nIn )
+	{
+		var aOut = [];
+		for ( var i=0, iLen=nIn.childNodes.length ; i<iLen ; i++ )
+		{
+			if ( nIn.childNodes[i].nodeName.toUpperCase() == "TR" )
+			{
+				aOut.push( nIn.childNodes[i] );
+			}
+		}
+		return aOut;
+	},
+
+
+	/**
+	 * Equalise the heights of the rows in a given table node in a cross browser way
+	 *  @returns {void}
+	 *  @param   {String} nodeName Node type - thead, tbody or tfoot
+	 *  @param   {Node} original Original node to take the heights from
+	 *  @param   {Node} clone Copy the heights to
+	 *  @private
+	 */
+	"_fnEqualiseHeights": function ( nodeName, original, clone )
+	{
+		if ( this.s.sHeightMatch == 'none' && nodeName !== 'thead' && nodeName !== 'tfoot' )
+		{
+			return;
+		}
+
+		var that = this,
+			i, iLen, iHeight, iHeight2, iHeightOriginal, iHeightClone,
+			rootOriginal = original.getElementsByTagName(nodeName)[0],
+			rootClone    = clone.getElementsByTagName(nodeName)[0],
+			jqBoxHack    = $('>'+nodeName+'>tr:eq(0)', original).children(':first'),
+			iBoxHack     = jqBoxHack.outerHeight() - jqBoxHack.height(),
+			anOriginal   = this._fnGetTrNodes( rootOriginal ),
+			anClone      = this._fnGetTrNodes( rootClone ),
+			heights      = [];
+
+		for ( i=0, iLen=anClone.length ; i<iLen ; i++ )
+		{
+			iHeightOriginal = anOriginal[i].offsetHeight;
+			iHeightClone = anClone[i].offsetHeight;
+			iHeight = iHeightClone > iHeightOriginal ? iHeightClone : iHeightOriginal;
+
+			if ( this.s.sHeightMatch == 'semiauto' )
+			{
+				anOriginal[i]._DTTC_iHeight = iHeight;
+			}
+
+			heights.push( iHeight );
+		}
+
+		for ( i=0, iLen=anClone.length ; i<iLen ; i++ )
+		{
+			anClone[i].style.height = heights[i]+"px";
+			anOriginal[i].style.height = heights[i]+"px";
+		}
+	},
+
+	/**
+	 * Determine if the UA suffers from Firefox's overflow:scroll scrollbars
+	 * not being shown bug.
+	 *
+	 * Firefox doesn't draw scrollbars, even if it is told to using
+	 * overflow:scroll, if the div is less than 34px height. See bugs 292284 and
+	 * 781885. Using UA detection here since this is particularly hard to detect
+	 * using objects - its a straight up rendering error in Firefox.
+	 *
+	 * @return {boolean} True if Firefox error is present, false otherwise
+	 */
+	_firefoxScrollError: function () {
+		if ( _firefoxScroll === undefined ) {
+			var test = $('<div/>')
+				.css( {
+					position: 'absolute',
+					top: 0,
+					left: 0,
+					height: 10,
+					width: 50,
+					overflow: 'scroll'
+				} )
+				.appendTo( 'body' );
+
+			// Make sure this doesn't apply on Macs with 0 width scrollbars
+			_firefoxScroll = (
+				test[0].clientWidth === test[0].offsetWidth && this._fnDTOverflow().bar !== 0
+			);
+
+			test.remove();
+		}
+
+		return _firefoxScroll;
+	}
+} );
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Statics
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * FixedColumns default settings for initialisation
+ *  @name FixedColumns.defaults
+ *  @namespace
+ *  @static
+ */
+FixedColumns.defaults = /** @lends FixedColumns.defaults */{
+	/**
+	 * Number of left hand columns to fix in position
+	 *  @type     int
+	 *  @default  1
+	 *  @static
+	 *  @example
+	 *      var  = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      new $.fn.dataTable.fixedColumns( table, {
+	 *          "leftColumns": 2
+	 *      } );
+	 */
+	"iLeftColumns": 1,
+
+	/**
+	 * Number of right hand columns to fix in position
+	 *  @type     int
+	 *  @default  0
+	 *  @static
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      new $.fn.dataTable.fixedColumns( table, {
+	 *          "rightColumns": 1
+	 *      } );
+	 */
+	"iRightColumns": 0,
+
+	/**
+	 * Draw callback function which is called when FixedColumns has redrawn the fixed assets
+	 *  @type     function(object, object):void
+	 *  @default  null
+	 *  @static
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      new $.fn.dataTable.fixedColumns( table, {
+	 *          "drawCallback": function () {
+	 *	            alert( "FixedColumns redraw" );
+	 *	        }
+	 *      } );
+	 */
+	"fnDrawCallback": null,
+
+	/**
+	 * Height matching algorthim to use. This can be "none" which will result in no height
+	 * matching being applied by FixedColumns (height matching could be forced by CSS in this
+	 * case), "semiauto" whereby the height calculation will be performed once, and the result
+	 * cached to be used again (fnRecalculateHeight can be used to force recalculation), or
+	 * "auto" when height matching is performed on every draw (slowest but must accurate)
+	 *  @type     string
+	 *  @default  semiauto
+	 *  @static
+	 *  @example
+	 *      var table = $('#example').dataTable( {
+	 *          "scrollX": "100%"
+	 *      } );
+	 *      new $.fn.dataTable.fixedColumns( table, {
+	 *          "heightMatch": "auto"
+	 *      } );
+	 */
+	"sHeightMatch": "semiauto"
+};
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Constants
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * FixedColumns version
+ *  @name      FixedColumns.version
+ *  @type      String
+ *  @default   See code
+ *  @static
+ */
+FixedColumns.version = "3.2.2";
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables API integration
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+DataTable.Api.register( 'fixedColumns()', function () {
+	return this;
+} );
+
+DataTable.Api.register( 'fixedColumns().update()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		if ( ctx._oFixedColumns ) {
+			ctx._oFixedColumns.fnUpdate();
+		}
+	} );
+} );
+
+DataTable.Api.register( 'fixedColumns().relayout()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		if ( ctx._oFixedColumns ) {
+			ctx._oFixedColumns.fnRedrawLayout();
+		}
+	} );
+} );
+
+DataTable.Api.register( 'rows().recalcHeight()', function () {
+	return this.iterator( 'row', function ( ctx, idx ) {
+		if ( ctx._oFixedColumns ) {
+			ctx._oFixedColumns.fnRecalculateHeight( this.row(idx).node() );
+		}
+	} );
+} );
+
+DataTable.Api.register( 'fixedColumns().rowIndex()', function ( row ) {
+	row = $(row);
+
+	return row.parents('.DTFC_Cloned').length ?
+		this.rows( { page: 'current' } ).indexes()[ row.index() ] :
+		this.row( row ).index();
+} );
+
+DataTable.Api.register( 'fixedColumns().cellIndex()', function ( cell ) {
+	cell = $(cell);
+
+	if ( cell.parents('.DTFC_Cloned').length ) {
+		var rowClonedIdx = cell.parent().index();
+		var rowIdx = this.rows( { page: 'current' } ).indexes()[ rowClonedIdx ];
+		var columnIdx;
+
+		if ( cell.parents('.DTFC_LeftWrapper').length ) {
+			columnIdx = cell.index();
+		}
+		else {
+			var columns = this.columns().flatten().length;
+			columnIdx = columns - this.context[0]._oFixedColumns.s.iRightColumns + cell.index();
+		}
+
+		return {
+			row: rowIdx,
+			column: this.column.index( 'toData', columnIdx ),
+			columnVisible: columnIdx
+		};
+	}
+	else {
+		return this.cell( cell ).index();
+	}
+} );
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Initialisation
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+// Attach a listener to the document which listens for DataTables initialisation
+// events so we can automatically initialise
+$(document).on( 'init.dt.fixedColumns', function (e, settings) {
+	if ( e.namespace !== 'dt' ) {
+		return;
+	}
+
+	var init = settings.oInit.fixedColumns;
+	var defaults = DataTable.defaults.fixedColumns;
+
+	if ( init || defaults ) {
+		var opts = $.extend( {}, init, defaults );
+
+		if ( init !== false ) {
+			new FixedColumns( settings, opts );
+		}
+	}
+} );
+
+
+
+// Make FixedColumns accessible from the DataTables instance
+$.fn.dataTable.FixedColumns = FixedColumns;
+$.fn.DataTable.FixedColumns = FixedColumns;
+
+return FixedColumns;
+}));
+
+
+/*! FixedHeader 3.1.2
+ * ©2009-2016 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary     FixedHeader
+ * @description Fix a table's header or footer, so it is always visible while
+ *              scrolling
+ * @version     3.1.2
+ * @file        dataTables.fixedHeader.js
+ * @author      SpryMedia Ltd (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ * @copyright   Copyright 2009-2016 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ *   MIT license - http://datatables.net/license/mit
+ *
+ * This source file 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 license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+var _instCounter = 0;
+
+var FixedHeader = function ( dt, config ) {
+	// Sanity check - you just know it will happen
+	if ( ! (this instanceof FixedHeader) ) {
+		throw "FixedHeader must be initialised with the 'new' keyword.";
+	}
+
+	// Allow a boolean true for defaults
+	if ( config === true ) {
+		config = {};
+	}
+
+	dt = new DataTable.Api( dt );
+
+	this.c = $.extend( true, {}, FixedHeader.defaults, config );
+
+	this.s = {
+		dt: dt,
+		position: {
+			theadTop: 0,
+			tbodyTop: 0,
+			tfootTop: 0,
+			tfootBottom: 0,
+			width: 0,
+			left: 0,
+			tfootHeight: 0,
+			theadHeight: 0,
+			windowHeight: $(window).height(),
+			visible: true
+		},
+		headerMode: null,
+		footerMode: null,
+		autoWidth: dt.settings()[0].oFeatures.bAutoWidth,
+		namespace: '.dtfc'+(_instCounter++),
+		scrollLeft: {
+			header: -1,
+			footer: -1
+		},
+		enable: true
+	};
+
+	this.dom = {
+		floatingHeader: null,
+		thead: $(dt.table().header()),
+		tbody: $(dt.table().body()),
+		tfoot: $(dt.table().footer()),
+		header: {
+			host: null,
+			floating: null,
+			placeholder: null
+		},
+		footer: {
+			host: null,
+			floating: null,
+			placeholder: null
+		}
+	};
+
+	this.dom.header.host = this.dom.thead.parent();
+	this.dom.footer.host = this.dom.tfoot.parent();
+
+	var dtSettings = dt.settings()[0];
+	if ( dtSettings._fixedHeader ) {
+		throw "FixedHeader already initialised on table "+dtSettings.nTable.id;
+	}
+
+	dtSettings._fixedHeader = this;
+
+	this._constructor();
+};
+
+
+/*
+ * Variable: FixedHeader
+ * Purpose:  Prototype for FixedHeader
+ * Scope:    global
+ */
+$.extend( FixedHeader.prototype, {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * API methods
+	 */
+	
+	/**
+	 * Enable / disable the fixed elements
+	 *
+	 * @param  {boolean} enable `true` to enable, `false` to disable
+	 */
+	enable: function ( enable )
+	{
+		this.s.enable = enable;
+
+		if ( this.c.header ) {
+			this._modeChange( 'in-place', 'header', true );
+		}
+
+		if ( this.c.footer && this.dom.tfoot.length ) {
+			this._modeChange( 'in-place', 'footer', true );
+		}
+
+		this.update();
+	},
+	
+	/**
+	 * Set header offset 
+	 *
+	 * @param  {int} new value for headerOffset
+	 */
+	headerOffset: function ( offset )
+	{
+		if ( offset !== undefined ) {
+			this.c.headerOffset = offset;
+			this.update();
+		}
+
+		return this.c.headerOffset;
+	},
+	
+	/**
+	 * Set footer offset
+	 *
+	 * @param  {int} new value for footerOffset
+	 */
+	footerOffset: function ( offset )
+	{
+		if ( offset !== undefined ) {
+			this.c.footerOffset = offset;
+			this.update();
+		}
+
+		return this.c.footerOffset;
+	},
+
+	
+	/**
+	 * Recalculate the position of the fixed elements and force them into place
+	 */
+	update: function ()
+	{
+		this._positions();
+		this._scroll( true );
+	},
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Constructor
+	 */
+	
+	/**
+	 * FixedHeader constructor - adding the required event listeners and
+	 * simple initialisation
+	 *
+	 * @private
+	 */
+	_constructor: function ()
+	{
+		var that = this;
+		var dt = this.s.dt;
+
+		$(window)
+			.on( 'scroll'+this.s.namespace, function () {
+				that._scroll();
+			} )
+			.on( 'resize'+this.s.namespace, function () {
+				that.s.position.windowHeight = $(window).height();
+				that.update();
+			} );
+
+		var autoHeader = $('.fh-fixedHeader');
+		if ( ! this.c.headerOffset && autoHeader.length ) {
+			this.c.headerOffset = autoHeader.outerHeight();
+		}
+
+		var autoFooter = $('.fh-fixedFooter');
+		if ( ! this.c.footerOffset && autoFooter.length ) {
+			this.c.footerOffset = autoFooter.outerHeight();
+		}
+
+		dt.on( 'column-reorder.dt.dtfc column-visibility.dt.dtfc draw.dt.dtfc column-sizing.dt.dtfc', function () {
+			that.update();
+		} );
+
+		dt.on( 'destroy.dtfc', function () {
+			dt.off( '.dtfc' );
+			$(window).off( that.s.namespace );
+		} );
+
+		this._positions();
+		this._scroll();
+	},
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods
+	 */
+
+	/**
+	 * Clone a fixed item to act as a place holder for the original element
+	 * which is moved into a clone of the table element, and moved around the
+	 * document to give the fixed effect.
+	 *
+	 * @param  {string}  item  'header' or 'footer'
+	 * @param  {boolean} force Force the clone to happen, or allow automatic
+	 *   decision (reuse existing if available)
+	 * @private
+	 */
+	_clone: function ( item, force )
+	{
+		var dt = this.s.dt;
+		var itemDom = this.dom[ item ];
+		var itemElement = item === 'header' ?
+			this.dom.thead :
+			this.dom.tfoot;
+
+		if ( ! force && itemDom.floating ) {
+			// existing floating element - reuse it
+			itemDom.floating.removeClass( 'fixedHeader-floating fixedHeader-locked' );
+		}
+		else {
+			if ( itemDom.floating ) {
+				itemDom.placeholder.remove();
+				this._unsize( item );
+				itemDom.floating.children().detach();
+				itemDom.floating.remove();
+			}
+
+			itemDom.floating = $( dt.table().node().cloneNode( false ) )
+				.css( 'table-layout', 'fixed' )
+				.removeAttr( 'id' )
+				.append( itemElement )
+				.appendTo( 'body' );
+
+			// Insert a fake thead/tfoot into the DataTable to stop it jumping around
+			itemDom.placeholder = itemElement.clone( false );
+			itemDom.host.prepend( itemDom.placeholder );
+
+			// Clone widths
+			this._matchWidths( itemDom.placeholder, itemDom.floating );
+		}
+	},
+
+	/**
+	 * Copy widths from the cells in one element to another. This is required
+	 * for the footer as the footer in the main table takes its sizes from the
+	 * header columns. That isn't present in the footer so to have it still
+	 * align correctly, the sizes need to be copied over. It is also required
+	 * for the header when auto width is not enabled
+	 *
+	 * @param  {jQuery} from Copy widths from
+	 * @param  {jQuery} to   Copy widths to
+	 * @private
+	 */
+	_matchWidths: function ( from, to ) {
+		var get = function ( name ) {
+			return $(name, from)
+				.map( function () {
+					return $(this).width();
+				} ).toArray();
+		};
+
+		var set = function ( name, toWidths ) {
+			$(name, to).each( function ( i ) {
+				$(this).css( {
+					width: toWidths[i],
+					minWidth: toWidths[i]
+				} );
+			} );
+		};
+
+		var thWidths = get( 'th' );
+		var tdWidths = get( 'td' );
+
+		set( 'th', thWidths );
+		set( 'td', tdWidths );
+	},
+
+	/**
+	 * Remove assigned widths from the cells in an element. This is required
+	 * when inserting the footer back into the main table so the size is defined
+	 * by the header columns and also when auto width is disabled in the
+	 * DataTable.
+	 *
+	 * @param  {string} item The `header` or `footer`
+	 * @private
+	 */
+	_unsize: function ( item ) {
+		var el = this.dom[ item ].floating;
+
+		if ( el && (item === 'footer' || (item === 'header' && ! this.s.autoWidth)) ) {
+			$('th, td', el).css( {
+				width: '',
+				minWidth: ''
+			} );
+		}
+		else if ( el && item === 'header' ) {
+			$('th, td', el).css( 'min-width', '' );
+		}
+	},
+
+	/**
+	 * Reposition the floating elements to take account of horizontal page
+	 * scroll
+	 *
+	 * @param  {string} item       The `header` or `footer`
+	 * @param  {int}    scrollLeft Document scrollLeft
+	 * @private
+	 */
+	_horizontal: function ( item, scrollLeft )
+	{
+		var itemDom = this.dom[ item ];
+		var position = this.s.position;
+		var lastScrollLeft = this.s.scrollLeft;
+
+		if ( itemDom.floating && lastScrollLeft[ item ] !== scrollLeft ) {
+			itemDom.floating.css( 'left', position.left - scrollLeft );
+
+			lastScrollLeft[ item ] = scrollLeft;
+		}
+	},
+
+	/**
+	 * Change from one display mode to another. Each fixed item can be in one
+	 * of:
+	 *
+	 * * `in-place` - In the main DataTable
+	 * * `in` - Floating over the DataTable
+	 * * `below` - (Header only) Fixed to the bottom of the table body
+	 * * `above` - (Footer only) Fixed to the top of the table body
+	 * 
+	 * @param  {string}  mode        Mode that the item should be shown in
+	 * @param  {string}  item        'header' or 'footer'
+	 * @param  {boolean} forceChange Force a redraw of the mode, even if already
+	 *     in that mode.
+	 * @private
+	 */
+	_modeChange: function ( mode, item, forceChange )
+	{
+		var dt = this.s.dt;
+		var itemDom = this.dom[ item ];
+		var position = this.s.position;
+
+		// Record focus. Browser's will cause input elements to loose focus if
+		// they are inserted else where in the doc
+		var tablePart = this.dom[ item==='footer' ? 'tfoot' : 'thead' ];
+		var focus = $.contains( tablePart[0], document.activeElement ) ?
+			document.activeElement :
+			null;
+
+		if ( mode === 'in-place' ) {
+			// Insert the header back into the table's real header
+			if ( itemDom.placeholder ) {
+				itemDom.placeholder.remove();
+				itemDom.placeholder = null;
+			}
+
+			this._unsize( item );
+
+			if ( item === 'header' ) {
+				itemDom.host.prepend( this.dom.thead );
+			}
+			else {
+				itemDom.host.append( this.dom.tfoot );
+			}
+
+			if ( itemDom.floating ) {
+				itemDom.floating.remove();
+				itemDom.floating = null;
+			}
+		}
+		else if ( mode === 'in' ) {
+			// Remove the header from the read header and insert into a fixed
+			// positioned floating table clone
+			this._clone( item, forceChange );
+
+			itemDom.floating
+				.addClass( 'fixedHeader-floating' )
+				.css( item === 'header' ? 'top' : 'bottom', this.c[item+'Offset'] )
+				.css( 'left', position.left+'px' )
+				.css( 'width', position.width+'px' );
+
+			if ( item === 'footer' ) {
+				itemDom.floating.css( 'top', '' );
+			}
+		}
+		else if ( mode === 'below' ) { // only used for the header
+			// Fix the position of the floating header at base of the table body
+			this._clone( item, forceChange );
+
+			itemDom.floating
+				.addClass( 'fixedHeader-locked' )
+				.css( 'top', position.tfootTop - position.theadHeight )
+				.css( 'left', position.left+'px' )
+				.css( 'width', position.width+'px' );
+		}
+		else if ( mode === 'above' ) { // only used for the footer
+			// Fix the position of the floating footer at top of the table body
+			this._clone( item, forceChange );
+
+			itemDom.floating
+				.addClass( 'fixedHeader-locked' )
+				.css( 'top', position.tbodyTop )
+				.css( 'left', position.left+'px' )
+				.css( 'width', position.width+'px' );
+		}
+
+		// Restore focus if it was lost
+		if ( focus && focus !== document.activeElement ) {
+			focus.focus();
+		}
+
+		this.s.scrollLeft.header = -1;
+		this.s.scrollLeft.footer = -1;
+		this.s[item+'Mode'] = mode;
+	},
+
+	/**
+	 * Cache the positional information that is required for the mode
+	 * calculations that FixedHeader performs.
+	 *
+	 * @private
+	 */
+	_positions: function ()
+	{
+		var dt = this.s.dt;
+		var table = dt.table();
+		var position = this.s.position;
+		var dom = this.dom;
+		var tableNode = $(table.node());
+
+		// Need to use the header and footer that are in the main table,
+		// regardless of if they are clones, since they hold the positions we
+		// want to measure from
+		var thead = tableNode.children('thead');
+		var tfoot = tableNode.children('tfoot');
+		var tbody = dom.tbody;
+
+		position.visible = tableNode.is(':visible');
+		position.width = tableNode.outerWidth();
+		position.left = tableNode.offset().left;
+		position.theadTop = thead.offset().top;
+		position.tbodyTop = tbody.offset().top;
+		position.theadHeight = position.tbodyTop - position.theadTop;
+
+		if ( tfoot.length ) {
+			position.tfootTop = tfoot.offset().top;
+			position.tfootBottom = position.tfootTop + tfoot.outerHeight();
+			position.tfootHeight = position.tfootBottom - position.tfootTop;
+		}
+		else {
+			position.tfootTop = position.tbodyTop + tbody.outerHeight();
+			position.tfootBottom = position.tfootTop;
+			position.tfootHeight = position.tfootTop;
+		}
+	},
+
+
+	/**
+	 * Mode calculation - determine what mode the fixed items should be placed
+	 * into.
+	 *
+	 * @param  {boolean} forceChange Force a redraw of the mode, even if already
+	 *     in that mode.
+	 * @private
+	 */
+	_scroll: function ( forceChange )
+	{
+		var windowTop = $(document).scrollTop();
+		var windowLeft = $(document).scrollLeft();
+		var position = this.s.position;
+		var headerMode, footerMode;
+
+		if ( ! this.s.enable ) {
+			return;
+		}
+
+		if ( this.c.header ) {
+			if ( ! position.visible || windowTop <= position.theadTop - this.c.headerOffset ) {
+				headerMode = 'in-place';
+			}
+			else if ( windowTop <= position.tfootTop - position.theadHeight - this.c.headerOffset ) {
+				headerMode = 'in';
+			}
+			else {
+				headerMode = 'below';
+			}
+
+			if ( forceChange || headerMode !== this.s.headerMode ) {
+				this._modeChange( headerMode, 'header', forceChange );
+			}
+
+			this._horizontal( 'header', windowLeft );
+		}
+
+		if ( this.c.footer && this.dom.tfoot.length ) {
+			if ( ! position.visible || windowTop + position.windowHeight >= position.tfootBottom + this.c.footerOffset ) {
+				footerMode = 'in-place';
+			}
+			else if ( position.windowHeight + windowTop > position.tbodyTop + position.tfootHeight + this.c.footerOffset ) {
+				footerMode = 'in';
+			}
+			else {
+				footerMode = 'above';
+			}
+
+			if ( forceChange || footerMode !== this.s.footerMode ) {
+				this._modeChange( footerMode, 'footer', forceChange );
+			}
+
+			this._horizontal( 'footer', windowLeft );
+		}
+	}
+} );
+
+
+/**
+ * Version
+ * @type {String}
+ * @static
+ */
+FixedHeader.version = "3.1.2";
+
+/**
+ * Defaults
+ * @type {Object}
+ * @static
+ */
+FixedHeader.defaults = {
+	header: true,
+	footer: false,
+	headerOffset: 0,
+	footerOffset: 0
+};
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables interfaces
+ */
+
+// Attach for constructor access
+$.fn.dataTable.FixedHeader = FixedHeader;
+$.fn.DataTable.FixedHeader = FixedHeader;
+
+
+// DataTables creation - check if the FixedHeader option has been defined on the
+// table and if so, initialise
+$(document).on( 'init.dt.dtfh', function (e, settings, json) {
+	if ( e.namespace !== 'dt' ) {
+		return;
+	}
+
+	var init = settings.oInit.fixedHeader;
+	var defaults = DataTable.defaults.fixedHeader;
+
+	if ( (init || defaults) && ! settings._fixedHeader ) {
+		var opts = $.extend( {}, defaults, init );
+
+		if ( init !== false ) {
+			new FixedHeader( settings, opts );
+		}
+	}
+} );
+
+// DataTables API methods
+DataTable.Api.register( 'fixedHeader()', function () {} );
+
+DataTable.Api.register( 'fixedHeader.adjust()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		var fh = ctx._fixedHeader;
+
+		if ( fh ) {
+			fh.update();
+		}
+	} );
+} );
+
+DataTable.Api.register( 'fixedHeader.enable()', function ( flag ) {
+	return this.iterator( 'table', function ( ctx ) {
+		var fh = ctx._fixedHeader;
+
+		if ( fh ) {
+			fh.enable( flag !== undefined ? flag : true );
+		}
+	} );
+} );
+
+DataTable.Api.register( 'fixedHeader.disable()', function ( ) {
+	return this.iterator( 'table', function ( ctx ) {
+		var fh = ctx._fixedHeader;
+
+		if ( fh ) {
+			fh.enable( false );
+		}
+	} );
+} );
+
+$.each( ['header', 'footer'], function ( i, el ) {
+	DataTable.Api.register( 'fixedHeader.'+el+'Offset()', function ( offset ) {
+		var ctx = this.context;
+
+		if ( offset === undefined ) {
+			return ctx.length && ctx[0]._fixedHeader ?
+				ctx[0]._fixedHeader[el +'Offset']() :
+				undefined;
+		}
+
+		return this.iterator( 'table', function ( ctx ) {
+			var fh = ctx._fixedHeader;
+
+			if ( fh ) {
+				fh[ el +'Offset' ]( offset );
+			}
+		} );
+	} );
+} );
+
+
+return FixedHeader;
+}));
+
+
+/*! Responsive 2.1.1
+ * 2014-2016 SpryMedia Ltd - datatables.net/license
+ */
+
+/**
+ * @summary     Responsive
+ * @description Responsive tables plug-in for DataTables
+ * @version     2.1.1
+ * @file        dataTables.responsive.js
+ * @author      SpryMedia Ltd (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ * @copyright   Copyright 2014-2016 SpryMedia Ltd.
+ *
+ * This source file is free software, available under the following license:
+ *   MIT license - http://datatables.net/license/mit
+ *
+ * This source file 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 license files for details.
+ *
+ * For details please refer to: http://www.datatables.net
+ */
+(function( factory ){
+	if ( typeof define === 'function' && define.amd ) {
+		// AMD
+		define( ['jquery', 'datatables.net'], function ( $ ) {
+			return factory( $, window, document );
+		} );
+	}
+	else if ( typeof exports === 'object' ) {
+		// CommonJS
+		module.exports = function (root, $) {
+			if ( ! root ) {
+				root = window;
+			}
+
+			if ( ! $ || ! $.fn.dataTable ) {
+				$ = require('datatables.net')(root, $).$;
+			}
+
+			return factory( $, root, root.document );
+		};
+	}
+	else {
+		// Browser
+		factory( jQuery, window, document );
+	}
+}(function( $, window, document, undefined ) {
+'use strict';
+var DataTable = $.fn.dataTable;
+
+
+/**
+ * Responsive is a plug-in for the DataTables library that makes use of
+ * DataTables' ability to change the visibility of columns, changing the
+ * visibility of columns so the displayed columns fit into the table container.
+ * The end result is that complex tables will be dynamically adjusted to fit
+ * into the viewport, be it on a desktop, tablet or mobile browser.
+ *
+ * Responsive for DataTables has two modes of operation, which can used
+ * individually or combined:
+ *
+ * * Class name based control - columns assigned class names that match the
+ *   breakpoint logic can be shown / hidden as required for each breakpoint.
+ * * Automatic control - columns are automatically hidden when there is no
+ *   room left to display them. Columns removed from the right.
+ *
+ * In additional to column visibility control, Responsive also has built into
+ * options to use DataTables' child row display to show / hide the information
+ * from the table that has been hidden. There are also two modes of operation
+ * for this child row display:
+ *
+ * * Inline - when the control element that the user can use to show / hide
+ *   child rows is displayed inside the first column of the table.
+ * * Column - where a whole column is dedicated to be the show / hide control.
+ *
+ * Initialisation of Responsive is performed by:
+ *
+ * * Adding the class `responsive` or `dt-responsive` to the table. In this case
+ *   Responsive will automatically be initialised with the default configuration
+ *   options when the DataTable is created.
+ * * Using the `responsive` option in the DataTables configuration options. This
+ *   can also be used to specify the configuration options, or simply set to
+ *   `true` to use the defaults.
+ *
+ *  @class
+ *  @param {object} settings DataTables settings object for the host table
+ *  @param {object} [opts] Configuration options
+ *  @requires jQuery 1.7+
+ *  @requires DataTables 1.10.3+
+ *
+ *  @example
+ *      $('#example').DataTable( {
+ *        responsive: true
+ *      } );
+ *    } );
+ */
+var Responsive = function ( settings, opts ) {
+	// Sanity check that we are using DataTables 1.10 or newer
+	if ( ! DataTable.versionCheck || ! DataTable.versionCheck( '1.10.3' ) ) {
+		throw 'DataTables Responsive requires DataTables 1.10.3 or newer';
+	}
+
+	this.s = {
+		dt: new DataTable.Api( settings ),
+		columns: [],
+		current: []
+	};
+
+	// Check if responsive has already been initialised on this table
+	if ( this.s.dt.settings()[0].responsive ) {
+		return;
+	}
+
+	// details is an object, but for simplicity the user can give it as a string
+	// or a boolean
+	if ( opts && typeof opts.details === 'string' ) {
+		opts.details = { type: opts.details };
+	}
+	else if ( opts && opts.details === false ) {
+		opts.details = { type: false };
+	}
+	else if ( opts && opts.details === true ) {
+		opts.details = { type: 'inline' };
+	}
+
+	this.c = $.extend( true, {}, Responsive.defaults, DataTable.defaults.responsive, opts );
+	settings.responsive = this;
+	this._constructor();
+};
+
+$.extend( Responsive.prototype, {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Constructor
+	 */
+
+	/**
+	 * Initialise the Responsive instance
+	 *
+	 * @private
+	 */
+	_constructor: function ()
+	{
+		var that = this;
+		var dt = this.s.dt;
+		var dtPrivateSettings = dt.settings()[0];
+		var oldWindowWidth = $(window).width();
+
+		dt.settings()[0]._responsive = this;
+
+		// Use DataTables' throttle function to avoid processor thrashing on
+		// resize
+		$(window).on( 'resize.dtr orientationchange.dtr', DataTable.util.throttle( function () {
+			// iOS has a bug whereby resize can fire when only scrolling
+			// See: http://stackoverflow.com/questions/8898412
+			var width = $(window).width();
+
+			if ( width !== oldWindowWidth ) {
+				that._resize();
+				oldWindowWidth = width;
+			}
+		} ) );
+
+		// DataTables doesn't currently trigger an event when a row is added, so
+		// we need to hook into its private API to enforce the hidden rows when
+		// new data is added
+		dtPrivateSettings.oApi._fnCallbackReg( dtPrivateSettings, 'aoRowCreatedCallback', function (tr, data, idx) {
+			if ( $.inArray( false, that.s.current ) !== -1 ) {
+				$('>td, >th', tr).each( function ( i ) {
+					var idx = dt.column.index( 'toData', i );
+
+					if ( that.s.current[idx] === false ) {
+						$(this).css('display', 'none');
+					}
+				} );
+			}
+		} );
+
+		// Destroy event handler
+		dt.on( 'destroy.dtr', function () {
+			dt.off( '.dtr' );
+			$( dt.table().body() ).off( '.dtr' );
+			$(window).off( 'resize.dtr orientationchange.dtr' );
+
+			// Restore the columns that we've hidden
+			$.each( that.s.current, function ( i, val ) {
+				if ( val === false ) {
+					that._setColumnVis( i, true );
+				}
+			} );
+		} );
+
+		// Reorder the breakpoints array here in case they have been added out
+		// of order
+		this.c.breakpoints.sort( function (a, b) {
+			return a.width < b.width ? 1 :
+				a.width > b.width ? -1 : 0;
+		} );
+
+		this._classLogic();
+		this._resizeAuto();
+
+		// Details handler
+		var details = this.c.details;
+
+		if ( details.type !== false ) {
+			that._detailsInit();
+
+			// DataTables will trigger this event on every column it shows and
+			// hides individually
+			dt.on( 'column-visibility.dtr', function (e, ctx, col, vis) {
+				that._classLogic();
+				that._resizeAuto();
+				that._resize();
+			} );
+
+			// Redraw the details box on each draw which will happen if the data
+			// has changed. This is used until DataTables implements a native
+			// `updated` event for rows
+			dt.on( 'draw.dtr', function () {
+				that._redrawChildren();
+			} );
+
+			$(dt.table().node()).addClass( 'dtr-'+details.type );
+		}
+
+		dt.on( 'column-reorder.dtr', function (e, settings, details) {
+			that._classLogic();
+			that._resizeAuto();
+			that._resize();
+		} );
+
+		// Change in column sizes means we need to calc
+		dt.on( 'column-sizing.dtr', function () {
+			that._resizeAuto();
+			that._resize();
+		});
+
+		// On Ajax reload we want to reopen any child rows which are displayed
+		// by responsive
+		dt.on( 'preXhr.dtr', function () {
+			var rowIds = [];
+			dt.rows().every( function () {
+				if ( this.child.isShown() ) {
+					rowIds.push( this.id(true) );
+				}
+			} );
+
+			dt.one( 'draw.dtr', function () {
+				dt.rows( rowIds ).every( function () {
+					that._detailsDisplay( this, false );
+				} );
+			} );
+		});
+
+		dt.on( 'init.dtr', function (e, settings, details) {
+			that._resizeAuto();
+			that._resize();
+
+			// If columns were hidden, then DataTables needs to adjust the
+			// column sizing
+			if ( $.inArray( false, that.s.current ) ) {
+				dt.columns.adjust();
+			}
+		} );
+
+		// First pass - draw the table for the current viewport size
+		this._resize();
+	},
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Private methods
+	 */
+
+	/**
+	 * Calculate the visibility for the columns in a table for a given
+	 * breakpoint. The result is pre-determined based on the class logic if
+	 * class names are used to control all columns, but the width of the table
+	 * is also used if there are columns which are to be automatically shown
+	 * and hidden.
+	 *
+	 * @param  {string} breakpoint Breakpoint name to use for the calculation
+	 * @return {array} Array of boolean values initiating the visibility of each
+	 *   column.
+	 *  @private
+	 */
+	_columnsVisiblity: function ( breakpoint )
+	{
+		var dt = this.s.dt;
+		var columns = this.s.columns;
+		var i, ien;
+
+		// Create an array that defines the column ordering based first on the
+		// column's priority, and secondly the column index. This allows the
+		// columns to be removed from the right if the priority matches
+		var order = columns
+			.map( function ( col, idx ) {
+				return {
+					columnIdx: idx,
+					priority: col.priority
+				};
+			} )
+			.sort( function ( a, b ) {
+				if ( a.priority !== b.priority ) {
+					return a.priority - b.priority;
+				}
+				return a.columnIdx - b.columnIdx;
+			} );
+
+		// Class logic - determine which columns are in this breakpoint based
+		// on the classes. If no class control (i.e. `auto`) then `-` is used
+		// to indicate this to the rest of the function
+		var display = $.map( columns, function ( col ) {
+			return col.auto && col.minWidth === null ?
+				false :
+				col.auto === true ?
+					'-' :
+					$.inArray( breakpoint, col.includeIn ) !== -1;
+		} );
+
+		// Auto column control - first pass: how much width is taken by the
+		// ones that must be included from the non-auto columns
+		var requiredWidth = 0;
+		for ( i=0, ien=display.length ; i<ien ; i++ ) {
+			if ( display[i] === true ) {
+				requiredWidth += columns[i].minWidth;
+			}
+		}
+
+		// Second pass, use up any remaining width for other columns. For
+		// scrolling tables we need to subtract the width of the scrollbar. It
+		// may not be requires which makes this sub-optimal, but it would
+		// require another full redraw to make complete use of those extra few
+		// pixels
+		var scrolling = dt.settings()[0].oScroll;
+		var bar = scrolling.sY || scrolling.sX ? scrolling.iBarWidth : 0;
+		var widthAvailable = dt.table().container().offsetWidth - bar;
+		var usedWidth = widthAvailable - requiredWidth;
+
+		// Control column needs to always be included. This makes it sub-
+		// optimal in terms of using the available with, but to stop layout
+		// thrashing or overflow. Also we need to account for the control column
+		// width first so we know how much width is available for the other
+		// columns, since the control column might not be the first one shown
+		for ( i=0, ien=display.length ; i<ien ; i++ ) {
+			if ( columns[i].control ) {
+				usedWidth -= columns[i].minWidth;
+			}
+		}
+
+		// Allow columns to be shown (counting by priority and then right to
+		// left) until we run out of room
+		var empty = false;
+		for ( i=0, ien=order.length ; i<ien ; i++ ) {
+			var colIdx = order[i].columnIdx;
+
+			if ( display[colIdx] === '-' && ! columns[colIdx].control && columns[colIdx].minWidth ) {
+				// Once we've found a column that won't fit we don't let any
+				// others display either, or columns might disappear in the
+				// middle of the table
+				if ( empty || usedWidth - columns[colIdx].minWidth < 0 ) {
+					empty = true;
+					display[colIdx] = false;
+				}
+				else {
+					display[colIdx] = true;
+				}
+
+				usedWidth -= columns[colIdx].minWidth;
+			}
+		}
+
+		// Determine if the 'control' column should be shown (if there is one).
+		// This is the case when there is a hidden column (that is not the
+		// control column). The two loops look inefficient here, but they are
+		// trivial and will fly through. We need to know the outcome from the
+		// first , before the action in the second can be taken
+		var showControl = false;
+
+		for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+			if ( ! columns[i].control && ! columns[i].never && ! display[i] ) {
+				showControl = true;
+				break;
+			}
+		}
+
+		for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+			if ( columns[i].control ) {
+				display[i] = showControl;
+			}
+		}
+
+		// Finally we need to make sure that there is at least one column that
+		// is visible
+		if ( $.inArray( true, display ) === -1 ) {
+			display[0] = true;
+		}
+
+		return display;
+	},
+
+
+	/**
+	 * Create the internal `columns` array with information about the columns
+	 * for the table. This includes determining which breakpoints the column
+	 * will appear in, based upon class names in the column, which makes up the
+	 * vast majority of this method.
+	 *
+	 * @private
+	 */
+	_classLogic: function ()
+	{
+		var that = this;
+		var calc = {};
+		var breakpoints = this.c.breakpoints;
+		var dt = this.s.dt;
+		var columns = dt.columns().eq(0).map( function (i) {
+			var column = this.column(i);
+			var className = column.header().className;
+			var priority = dt.settings()[0].aoColumns[i].responsivePriority;
+
+			if ( priority === undefined ) {
+				var dataPriority = $(column.header()).data('priority');
+
+				priority = dataPriority !== undefined ?
+					dataPriority * 1 :
+					10000;
+			}
+
+			return {
+				className: className,
+				includeIn: [],
+				auto:      false,
+				control:   false,
+				never:     className.match(/\bnever\b/) ? true : false,
+				priority:  priority
+			};
+		} );
+
+		// Simply add a breakpoint to `includeIn` array, ensuring that there are
+		// no duplicates
+		var add = function ( colIdx, name ) {
+			var includeIn = columns[ colIdx ].includeIn;
+
+			if ( $.inArray( name, includeIn ) === -1 ) {
+				includeIn.push( name );
+			}
+		};
+
+		var column = function ( colIdx, name, operator, matched ) {
+			var size, i, ien;
+
+			if ( ! operator ) {
+				columns[ colIdx ].includeIn.push( name );
+			}
+			else if ( operator === 'max-' ) {
+				// Add this breakpoint and all smaller
+				size = that._find( name ).width;
+
+				for ( i=0, ien=breakpoints.length ; i<ien ; i++ ) {
+					if ( breakpoints[i].width <= size ) {
+						add( colIdx, breakpoints[i].name );
+					}
+				}
+			}
+			else if ( operator === 'min-' ) {
+				// Add this breakpoint and all larger
+				size = that._find( name ).width;
+
+				for ( i=0, ien=breakpoints.length ; i<ien ; i++ ) {
+					if ( breakpoints[i].width >= size ) {
+						add( colIdx, breakpoints[i].name );
+					}
+				}
+			}
+			else if ( operator === 'not-' ) {
+				// Add all but this breakpoint
+				for ( i=0, ien=breakpoints.length ; i<ien ; i++ ) {
+					if ( breakpoints[i].name.indexOf( matched ) === -1 ) {
+						add( colIdx, breakpoints[i].name );
+					}
+				}
+			}
+		};
+
+		// Loop over each column and determine if it has a responsive control
+		// class
+		columns.each( function ( col, i ) {
+			var classNames = col.className.split(' ');
+			var hasClass = false;
+
+			// Split the class name up so multiple rules can be applied if needed
+			for ( var k=0, ken=classNames.length ; k<ken ; k++ ) {
+				var className = $.trim( classNames[k] );
+
+				if ( className === 'all' ) {
+					// Include in all
+					hasClass = true;
+					col.includeIn = $.map( breakpoints, function (a) {
+						return a.name;
+					} );
+					return;
+				}
+				else if ( className === 'none' || col.never ) {
+					// Include in none (default) and no auto
+					hasClass = true;
+					return;
+				}
+				else if ( className === 'control' ) {
+					// Special column that is only visible, when one of the other
+					// columns is hidden. This is used for the details control
+					hasClass = true;
+					col.control = true;
+					return;
+				}
+
+				$.each( breakpoints, function ( j, breakpoint ) {
+					// Does this column have a class that matches this breakpoint?
+					var brokenPoint = breakpoint.name.split('-');
+					var re = new RegExp( '(min\\-|max\\-|not\\-)?('+brokenPoint[0]+')(\\-[_a-zA-Z0-9])?' );
+					var match = className.match( re );
+
+					if ( match ) {
+						hasClass = true;
+
+						if ( match[2] === brokenPoint[0] && match[3] === '-'+brokenPoint[1] ) {
+							// Class name matches breakpoint name fully
+							column( i, breakpoint.name, match[1], match[2]+match[3] );
+						}
+						else if ( match[2] === brokenPoint[0] && ! match[3] ) {
+							// Class name matched primary breakpoint name with no qualifier
+							column( i, breakpoint.name, match[1], match[2] );
+						}
+					}
+				} );
+			}
+
+			// If there was no control class, then automatic sizing is used
+			if ( ! hasClass ) {
+				col.auto = true;
+			}
+		} );
+
+		this.s.columns = columns;
+	},
+
+
+	/**
+	 * Show the details for the child row
+	 *
+	 * @param  {DataTables.Api} row    API instance for the row
+	 * @param  {boolean}        update Update flag
+	 * @private
+	 */
+	_detailsDisplay: function ( row, update )
+	{
+		var that = this;
+		var dt = this.s.dt;
+		var details = this.c.details;
+
+		if ( details && details.type !== false ) {
+			var res = details.display( row, update, function () {
+				return details.renderer(
+					dt, row[0], that._detailsObj(row[0])
+				);
+			} );
+
+			if ( res === true || res === false ) {
+				$(dt.table().node()).triggerHandler( 'responsive-display.dt', [dt, row, res, update] );
+			}
+		}
+	},
+
+
+	/**
+	 * Initialisation for the details handler
+	 *
+	 * @private
+	 */
+	_detailsInit: function ()
+	{
+		var that    = this;
+		var dt      = this.s.dt;
+		var details = this.c.details;
+
+		// The inline type always uses the first child as the target
+		if ( details.type === 'inline' ) {
+			details.target = 'td:first-child, th:first-child';
+		}
+
+		// Keyboard accessibility
+		dt.on( 'draw.dtr', function () {
+			that._tabIndexes();
+		} );
+		that._tabIndexes(); // Initial draw has already happened
+
+		$( dt.table().body() ).on( 'keyup.dtr', 'td, th', function (e) {
+			if ( e.keyCode === 13 && $(this).data('dtr-keyboard') ) {
+				$(this).click();
+			}
+		} );
+
+		// type.target can be a string jQuery selector or a column index
+		var target   = details.target;
+		var selector = typeof target === 'string' ? target : 'td, th';
+
+		// Click handler to show / hide the details rows when they are available
+		$( dt.table().body() )
+			.on( 'click.dtr mousedown.dtr mouseup.dtr', selector, function (e) {
+				// If the table is not collapsed (i.e. there is no hidden columns)
+				// then take no action
+				if ( ! $(dt.table().node()).hasClass('collapsed' ) ) {
+					return;
+				}
+
+				// Check that the row is actually a DataTable's controlled node
+				if ( $.inArray( $(this).closest('tr').get(0), dt.rows().nodes().toArray() ) === -1 ) {
+					return;
+				}
+
+				// For column index, we determine if we should act or not in the
+				// handler - otherwise it is already okay
+				if ( typeof target === 'number' ) {
+					var targetIdx = target < 0 ?
+						dt.columns().eq(0).length + target :
+						target;
+
+					if ( dt.cell( this ).index().column !== targetIdx ) {
+						return;
+					}
+				}
+
+				// $().closest() includes itself in its check
+				var row = dt.row( $(this).closest('tr') );
+
+				// Check event type to do an action
+				if ( e.type === 'click' ) {
+					// The renderer is given as a function so the caller can execute it
+					// only when they need (i.e. if hiding there is no point is running
+					// the renderer)
+					that._detailsDisplay( row, false );
+				}
+				else if ( e.type === 'mousedown' ) {
+					// For mouse users, prevent the focus ring from showing
+					$(this).css('outline', 'none');
+				}
+				else if ( e.type === 'mouseup' ) {
+					// And then re-allow at the end of the click
+					$(this).blur().css('outline', '');
+				}
+			} );
+	},
+
+
+	/**
+	 * Get the details to pass to a renderer for a row
+	 * @param  {int} rowIdx Row index
+	 * @private
+	 */
+	_detailsObj: function ( rowIdx )
+	{
+		var that = this;
+		var dt = this.s.dt;
+
+		return $.map( this.s.columns, function( col, i ) {
+			// Never and control columns should not be passed to the renderer
+			if ( col.never || col.control ) {
+				return;
+			}
+
+			return {
+				title:       dt.settings()[0].aoColumns[ i ].sTitle,
+				data:        dt.cell( rowIdx, i ).render( that.c.orthogonal ),
+				hidden:      dt.column( i ).visible() && !that.s.current[ i ],
+				columnIndex: i,
+				rowIndex:    rowIdx
+			};
+		} );
+	},
+
+
+	/**
+	 * Find a breakpoint object from a name
+	 *
+	 * @param  {string} name Breakpoint name to find
+	 * @return {object}      Breakpoint description object
+	 * @private
+	 */
+	_find: function ( name )
+	{
+		var breakpoints = this.c.breakpoints;
+
+		for ( var i=0, ien=breakpoints.length ; i<ien ; i++ ) {
+			if ( breakpoints[i].name === name ) {
+				return breakpoints[i];
+			}
+		}
+	},
+
+
+	/**
+	 * Re-create the contents of the child rows as the display has changed in
+	 * some way.
+	 *
+	 * @private
+	 */
+	_redrawChildren: function ()
+	{
+		var that = this;
+		var dt = this.s.dt;
+
+		dt.rows( {page: 'current'} ).iterator( 'row', function ( settings, idx ) {
+			var row = dt.row( idx );
+
+			that._detailsDisplay( dt.row( idx ), true );
+		} );
+	},
+
+
+	/**
+	 * Alter the table display for a resized viewport. This involves first
+	 * determining what breakpoint the window currently is in, getting the
+	 * column visibilities to apply and then setting them.
+	 *
+	 * @private
+	 */
+	_resize: function ()
+	{
+		var that = this;
+		var dt = this.s.dt;
+		var width = $(window).width();
+		var breakpoints = this.c.breakpoints;
+		var breakpoint = breakpoints[0].name;
+		var columns = this.s.columns;
+		var i, ien;
+		var oldVis = this.s.current.slice();
+
+		// Determine what breakpoint we are currently at
+		for ( i=breakpoints.length-1 ; i>=0 ; i-- ) {
+			if ( width <= breakpoints[i].width ) {
+				breakpoint = breakpoints[i].name;
+				break;
+			}
+		}
+		
+		// Show the columns for that break point
+		var columnsVis = this._columnsVisiblity( breakpoint );
+		this.s.current = columnsVis;
+
+		// Set the class before the column visibility is changed so event
+		// listeners know what the state is. Need to determine if there are
+		// any columns that are not visible but can be shown
+		var collapsedClass = false;
+		for ( i=0, ien=columns.length ; i<ien ; i++ ) {
+			if ( columnsVis[i] === false && ! columns[i].never && ! columns[i].control ) {
+				collapsedClass = true;
+				break;
+			}
+		}
+
+		$( dt.table().node() ).toggleClass( 'collapsed', collapsedClass );
+
+		var changed = false;
+
+		dt.columns().eq(0).each( function ( colIdx, i ) {
+			if ( columnsVis[i] !== oldVis[i] ) {
+				changed = true;
+				that._setColumnVis( colIdx, columnsVis[i] );
+			}
+		} );
+
+		if ( changed ) {
+			this._redrawChildren();
+
+			// Inform listeners of the change
+			$(dt.table().node()).trigger( 'responsive-resize.dt', [dt, this.s.current] );
+		}
+	},
+
+
+	/**
+	 * Determine the width of each column in the table so the auto column hiding
+	 * has that information to work with. This method is never going to be 100%
+	 * perfect since column widths can change slightly per page, but without
+	 * seriously compromising performance this is quite effective.
+	 *
+	 * @private
+	 */
+	_resizeAuto: function ()
+	{
+		var dt = this.s.dt;
+		var columns = this.s.columns;
+
+		// Are we allowed to do auto sizing?
+		if ( ! this.c.auto ) {
+			return;
+		}
+
+		// Are there any columns that actually need auto-sizing, or do they all
+		// have classes defined
+		if ( $.inArray( true, $.map( columns, function (c) { return c.auto; } ) ) === -1 ) {
+			return;
+		}
+
+		// Clone the table with the current data in it
+		var tableWidth   = dt.table().node().offsetWidth;
+		var columnWidths = dt.columns;
+		var clonedTable  = dt.table().node().cloneNode( false );
+		var clonedHeader = $( dt.table().header().cloneNode( false ) ).appendTo( clonedTable );
+		var clonedBody   = $( dt.table().body() ).clone( false, false ).empty().appendTo( clonedTable ); // use jQuery because of IE8
+
+		// Header
+		var headerCells = dt.columns()
+			.header()
+			.filter( function (idx) {
+				return dt.column(idx).visible();
+			} )
+			.to$()
+			.clone( false )
+			.css( 'display', 'table-cell' );
+
+		// Body rows - we don't need to take account of DataTables' column
+		// visibility since we implement our own here (hence the `display` set)
+		$(clonedBody)
+			.append( $(dt.rows( { page: 'current' } ).nodes()).clone( false ) )
+			.find( 'th, td' ).css( 'display', '' );
+
+		// Footer
+		var footer = dt.table().footer();
+		if ( footer ) {
+			var clonedFooter = $( footer.cloneNode( false ) ).appendTo( clonedTable );
+			var footerCells = dt.columns()
+				.footer()
+				.filter( function (idx) {
+					return dt.column(idx).visible();
+				} )
+				.to$()
+				.clone( false )
+				.css( 'display', 'table-cell' );
+
+			$('<tr/>')
+				.append( footerCells )
+				.appendTo( clonedFooter );
+		}
+
+		$('<tr/>')
+			.append( headerCells )
+			.appendTo( clonedHeader );
+
+		// In the inline case extra padding is applied to the first column to
+		// give space for the show / hide icon. We need to use this in the
+		// calculation
+		if ( this.c.details.type === 'inline' ) {
+			$(clonedTable).addClass( 'dtr-inline collapsed' );
+		}
+		
+		// It is unsafe to insert elements with the same name into the DOM
+		// multiple times. For example, cloning and inserting a checked radio
+		// clears the chcecked state of the original radio.
+		$( clonedTable ).find( '[name]' ).removeAttr( 'name' );
+		
+		var inserted = $('<div/>')
+			.css( {
+				width: 1,
+				height: 1,
+				overflow: 'hidden'
+			} )
+			.append( clonedTable );
+
+		inserted.insertBefore( dt.table().node() );
+
+		// The cloned header now contains the smallest that each column can be
+		headerCells.each( function (i) {
+			var idx = dt.column.index( 'fromVisible', i );
+			columns[ idx ].minWidth =  this.offsetWidth || 0;
+		} );
+
+		inserted.remove();
+	},
+
+	/**
+	 * Set a column's visibility.
+	 *
+	 * We don't use DataTables' column visibility controls in order to ensure
+	 * that column visibility can Responsive can no-exist. Since only IE8+ is
+	 * supported (and all evergreen browsers of course) the control of the
+	 * display attribute works well.
+	 *
+	 * @param {integer} col      Column index
+	 * @param {boolean} showHide Show or hide (true or false)
+	 * @private
+	 */
+	_setColumnVis: function ( col, showHide )
+	{
+		var dt = this.s.dt;
+		var display = showHide ? '' : 'none'; // empty string will remove the attr
+
+		$( dt.column( col ).header() ).css( 'display', display );
+		$( dt.column( col ).footer() ).css( 'display', display );
+		dt.column( col ).nodes().to$().css( 'display', display );
+	},
+
+
+	/**
+	 * Update the cell tab indexes for keyboard accessibility. This is called on
+	 * every table draw - that is potentially inefficient, but also the least
+	 * complex option given that column visibility can change on the fly. Its a
+	 * shame user-focus was removed from CSS 3 UI, as it would have solved this
+	 * issue with a single CSS statement.
+	 *
+	 * @private
+	 */
+	_tabIndexes: function ()
+	{
+		var dt = this.s.dt;
+		var cells = dt.cells( { page: 'current' } ).nodes().to$();
+		var ctx = dt.settings()[0];
+		var target = this.c.details.target;
+
+		cells.filter( '[data-dtr-keyboard]' ).removeData( '[data-dtr-keyboard]' );
+
+		var selector = typeof target === 'number' ?
+			':eq('+target+')' :
+			target;
+
+		// This is a bit of a hack - we need to limit the selected nodes to just
+		// those of this table
+		if ( selector === 'td:first-child, th:first-child' ) {
+			selector = '>td:first-child, >th:first-child';
+		}
+
+		$( selector, dt.rows( { page: 'current' } ).nodes() )
+			.attr( 'tabIndex', ctx.iTabIndex )
+			.data( 'dtr-keyboard', 1 );
+	}
+} );
+
+
+/**
+ * List of default breakpoints. Each item in the array is an object with two
+ * properties:
+ *
+ * * `name` - the breakpoint name.
+ * * `width` - the breakpoint width
+ *
+ * @name Responsive.breakpoints
+ * @static
+ */
+Responsive.breakpoints = [
+	{ name: 'desktop',  width: Infinity },
+	{ name: 'tablet-l', width: 1024 },
+	{ name: 'tablet-p', width: 768 },
+	{ name: 'mobile-l', width: 480 },
+	{ name: 'mobile-p', width: 320 }
+];
+
+
+/**
+ * Display methods - functions which define how the hidden data should be shown
+ * in the table.
+ *
+ * @namespace
+ * @name Responsive.defaults
+ * @static
+ */
+Responsive.display = {
+	childRow: function ( row, update, render ) {
+		if ( update ) {
+			if ( $(row.node()).hasClass('parent') ) {
+				row.child( render(), 'child' ).show();
+
+				return true;
+			}
+		}
+		else {
+			if ( ! row.child.isShown()  ) {
+				row.child( render(), 'child' ).show();
+				$( row.node() ).addClass( 'parent' );
+
+				return true;
+			}
+			else {
+				row.child( false );
+				$( row.node() ).removeClass( 'parent' );
+
+				return false;
+			}
+		}
+	},
+
+	childRowImmediate: function ( row, update, render ) {
+		if ( (! update && row.child.isShown()) || ! row.responsive.hasHidden() ) {
+			// User interaction and the row is show, or nothing to show
+			row.child( false );
+			$( row.node() ).removeClass( 'parent' );
+
+			return false;
+		}
+		else {
+			// Display
+			row.child( render(), 'child' ).show();
+			$( row.node() ).addClass( 'parent' );
+
+			return true;
+		}
+	},
+
+	// This is a wrapper so the modal options for Bootstrap and jQuery UI can
+	// have options passed into them. This specific one doesn't need to be a
+	// function but it is for consistency in the `modal` name
+	modal: function ( options ) {
+		return function ( row, update, render ) {
+			if ( ! update ) {
+				// Show a modal
+				var close = function () {
+					modal.remove(); // will tidy events for us
+					$(document).off( 'keypress.dtr' );
+				};
+
+				var modal = $('<div class="dtr-modal"/>')
+					.append( $('<div class="dtr-modal-display"/>')
+						.append( $('<div class="dtr-modal-content"/>')
+							.append( render() )
+						)
+						.append( $('<div class="dtr-modal-close">&times;</div>' )
+							.click( function () {
+								close();
+							} )
+						)
+					)
+					.append( $('<div class="dtr-modal-background"/>')
+						.click( function () {
+							close();
+						} )
+					)
+					.appendTo( 'body' );
+
+				$(document).on( 'keyup.dtr', function (e) {
+					if ( e.keyCode === 27 ) {
+						e.stopPropagation();
+
+						close();
+					}
+				} );
+			}
+			else {
+				$('div.dtr-modal-content')
+					.empty()
+					.append( render() );
+			}
+
+			if ( options && options.header ) {
+				$('div.dtr-modal-content').prepend(
+					'<h2>'+options.header( row )+'</h2>'
+				);
+			}
+		};
+	}
+};
+
+
+/**
+ * Display methods - functions which define how the hidden data should be shown
+ * in the table.
+ *
+ * @namespace
+ * @name Responsive.defaults
+ * @static
+ */
+Responsive.renderer = {
+	listHidden: function () {
+		return function ( api, rowIdx, columns ) {
+			var data = $.map( columns, function ( col ) {
+				return col.hidden ?
+					'<li data-dtr-index="'+col.columnIndex+'" data-dt-row="'+col.rowIndex+'" data-dt-column="'+col.columnIndex+'">'+
+						'<span class="dtr-title">'+
+							col.title+
+						'</span> '+
+						'<span class="dtr-data">'+
+							col.data+
+						'</span>'+
+					'</li>' :
+					'';
+			} ).join('');
+
+			return data ?
+				$('<ul data-dtr-index="'+rowIdx+'" class="dtr-details"/>').append( data ) :
+				false;
+		}
+	},
+
+	tableAll: function ( options ) {
+		options = $.extend( {
+			tableClass: ''
+		}, options );
+
+		return function ( api, rowIdx, columns ) {
+			var data = $.map( columns, function ( col ) {
+				return '<tr data-dt-row="'+col.rowIndex+'" data-dt-column="'+col.columnIndex+'">'+
+						'<td>'+col.title+':'+'</td> '+
+						'<td>'+col.data+'</td>'+
+					'</tr>';
+			} ).join('');
+
+			return $('<table class="'+options.tableClass+' dtr-details" width="100%"/>').append( data );
+		}
+	}
+};
+
+/**
+ * Responsive default settings for initialisation
+ *
+ * @namespace
+ * @name Responsive.defaults
+ * @static
+ */
+Responsive.defaults = {
+	/**
+	 * List of breakpoints for the instance. Note that this means that each
+	 * instance can have its own breakpoints. Additionally, the breakpoints
+	 * cannot be changed once an instance has been creased.
+	 *
+	 * @type {Array}
+	 * @default Takes the value of `Responsive.breakpoints`
+	 */
+	breakpoints: Responsive.breakpoints,
+
+	/**
+	 * Enable / disable auto hiding calculations. It can help to increase
+	 * performance slightly if you disable this option, but all columns would
+	 * need to have breakpoint classes assigned to them
+	 *
+	 * @type {Boolean}
+	 * @default  `true`
+	 */
+	auto: true,
+
+	/**
+	 * Details control. If given as a string value, the `type` property of the
+	 * default object is set to that value, and the defaults used for the rest
+	 * of the object - this is for ease of implementation.
+	 *
+	 * The object consists of the following properties:
+	 *
+	 * * `display` - A function that is used to show and hide the hidden details
+	 * * `renderer` - function that is called for display of the child row data.
+	 *   The default function will show the data from the hidden columns
+	 * * `target` - Used as the selector for what objects to attach the child
+	 *   open / close to
+	 * * `type` - `false` to disable the details display, `inline` or `column`
+	 *   for the two control types
+	 *
+	 * @type {Object|string}
+	 */
+	details: {
+		display: Responsive.display.childRow,
+
+		renderer: Responsive.renderer.listHidden(),
+
+		target: 0,
+
+		type: 'inline'
+	},
+
+	/**
+	 * Orthogonal data request option. This is used to define the data type
+	 * requested when Responsive gets the data to show in the child row.
+	 *
+	 * @type {String}
+	 */
+	orthogonal: 'display'
+};
+
+
+/*
+ * API
+ */
+var Api = $.fn.dataTable.Api;
+
+// Doesn't do anything - work around for a bug in DT... Not documented
+Api.register( 'responsive()', function () {
+	return this;
+} );
+
+Api.register( 'responsive.index()', function ( li ) {
+	li = $(li);
+
+	return {
+		column: li.data('dtr-index'),
+		row:    li.parent().data('dtr-index')
+	};
+} );
+
+Api.register( 'responsive.rebuild()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		if ( ctx._responsive ) {
+			ctx._responsive._classLogic();
+		}
+	} );
+} );
+
+Api.register( 'responsive.recalc()', function () {
+	return this.iterator( 'table', function ( ctx ) {
+		if ( ctx._responsive ) {
+			ctx._responsive._resizeAuto();
+			ctx._responsive._resize();
+		}
+	} );
+} );
+
+Api.register( 'responsive.hasHidden()', function () {
+	var ctx = this.context[0];
+
+	return ctx._responsive ?
+		$.inArray( false, ctx._responsive.s.current ) !== -1 :
+		false;
+} );
+
+
+/**
+ * Version information
+ *
+ * @name Responsive.version
+ * @static
+ */
+Responsive.version = '2.1.1';
+
+
+$.fn.dataTable.Responsive = Responsive;
+$.fn.DataTable.Responsive = Responsive;
+
+// Attach a listener to the document which listens for DataTables initialisation
+// events so we can automatically initialise
+$(document).on( 'preInit.dt.dtr', function (e, settings, json) {
+	if ( e.namespace !== 'dt' ) {
+		return;
+	}
+
+	if ( $(settings.nTable).hasClass( 'responsive' ) ||
+		 $(settings.nTable).hasClass( 'dt-responsive' ) ||
+		 settings.oInit.responsive ||
+		 DataTable.defaults.responsive
+	) {
+		var init = settings.oInit.responsive;
+
+		if ( init !== false ) {
+			new Responsive( settings, $.isPlainObject( init ) ? init : {}  );
+		}
+	}
+} );
+
+
+return Responsive;
+}));
+
+
diff --git a/static/DataTables/datatables.min.css b/static/DataTables/datatables.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..e244e540af9976007d1483debedbdab72c421492
--- /dev/null
+++ b/static/DataTables/datatables.min.css
@@ -0,0 +1,27 @@
+/*
+ * This combined file was created by the DataTables downloader builder:
+ *   https://datatables.net/download
+ *
+ * To rebuild or modify this file with the latest versions of the included
+ * software please visit:
+ *   https://datatables.net/download/#dt/dt-1.10.15/cr-1.3.3/fc-3.2.2/fh-3.1.2/r-2.1.1
+ *
+ * Included libraries:
+ *   DataTables 1.10.15, ColReorder 1.3.3, FixedColumns 3.2.2, FixedHeader 3.1.2, Responsive 2.1.1
+ */
+
+table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;*cursor:hand}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("DataTables-1.10.15/images/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("DataTables-1.10.15/images/sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("DataTables-1.10.15/images/sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("DataTables-1.10.15/images/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("DataTables-1.10.15/images/sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#ffffff}table.dataTable tbody tr.selected{background-color:#B0BED9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{-webkit-box-sizing:content-box;box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead>table,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}}
+
+
+table.DTCR_clonedTable.dataTable{position:absolute !important;background-color:rgba(255,255,255,0.7);z-index:202}div.DTCR_pointer{width:1px;background-color:#0259C4;z-index:201}
+
+
+table.DTFC_Cloned thead,table.DTFC_Cloned tfoot{background-color:white}div.DTFC_Blocker{background-color:white}div.DTFC_LeftWrapper table.dataTable,div.DTFC_RightWrapper table.dataTable{margin-bottom:0;z-index:2}div.DTFC_LeftWrapper table.dataTable.no-footer,div.DTFC_RightWrapper table.dataTable.no-footer{border-bottom:none}
+
+
+table.fixedHeader-floating{position:fixed !important;background-color:white}table.fixedHeader-floating.no-footer{border-bottom-width:0}table.fixedHeader-locked{position:absolute !important;background-color:white}@media print{table.fixedHeader-floating{display:none}}
+
+
+table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child:before{top:9px;left:4px;height:14px;width:14px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#31b131}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed>tbody>tr.child td:before{display:none}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child:before{top:5px;left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#31b131}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable>tbody>tr.child{padding:0.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable>tbody>tr.child ul.dtr-details li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:0.5em;box-shadow:0 12px 30px rgba(0,0,0,0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0,0,0,0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}
+
+
diff --git a/static/DataTables/datatables.min.js b/static/DataTables/datatables.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..55416ec99be89cda21c9616d307f6f7f8c4197aa
--- /dev/null
+++ b/static/DataTables/datatables.min.js
@@ -0,0 +1,294 @@
+/*
+ * This combined file was created by the DataTables downloader builder:
+ *   https://datatables.net/download
+ *
+ * To rebuild or modify this file with the latest versions of the included
+ * software please visit:
+ *   https://datatables.net/download/#dt/dt-1.10.15/cr-1.3.3/fc-3.2.2/fh-3.1.2/r-2.1.1
+ *
+ * Included libraries:
+ *   DataTables 1.10.15, ColReorder 1.3.3, FixedColumns 3.2.2, FixedHeader 3.1.2, Responsive 2.1.1
+ */
+
+/*!
+ DataTables 1.10.15
+ ©2008-2017 SpryMedia Ltd - datatables.net/license
+*/
+(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(E){return h(E,window,document)}):"object"===typeof exports?module.exports=function(E,H){E||(E=window);H||(H="undefined"!==typeof window?require("jquery"):require("jquery")(E));return h(H,E,E.document)}:h(jQuery,window,document)})(function(h,E,H,k){function Y(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()),
+d[c]=e,"o"===b[1]&&Y(a[e])});a._hungarianMap=d}function J(a,b,c){a._hungarianMap||Y(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),J(a[d],b[d],c)):b[d]=b[e]})}function Fa(a){var b=m.defaults.oLanguage,c=a.sZeroRecords;!a.sEmptyTable&&(c&&"No data available in table"===b.sEmptyTable)&&F(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&F(a,a,"sZeroRecords","sLoadingRecords");
+a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&fb(a)}function gb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":"");"boolean"===typeof a.scrollX&&(a.scrollX=
+a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b<c;b++)a[b]&&J(m.models.oSearch,a[b])}function hb(a){A(a,"orderable","bSortable");A(a,"orderData","aDataSort");A(a,"orderSequence","asSorting");A(a,"orderDataType","sortDataType");var b=a.aDataSort;"number"===typeof b&&!h.isArray(b)&&(a.aDataSort=[b])}function ib(a){if(!m.__browser){var b={};m.__browser=b;var c=h("<div/>").css({position:"fixed",top:0,left:-1*h(E).scrollLeft(),height:1,width:1,overflow:"hidden"}).append(h("<div/>").css({position:"absolute",
+top:1,left:1,width:100,overflow:"scroll"}).append(h("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,m.__browser);a.oScroll.iBarWidth=m.__browser.barWidth}function jb(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==
+e;)a.hasOwnProperty(d)&&(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ga(a,b){var c=m.defaults.column,d=a.aoColumns.length,c=h.extend({},m.models.oColumn,c,{nTh:b?b:H.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},m.models.oSearch,c[d]);la(a,d,h(b).data())}function la(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=
+e.attr("width")||null;var f=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(hb(c),J(m.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),h.extend(b,c),F(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),F(b,c,"aDataSort"));var g=b.mData,j=R(g),i=b.mRender?R(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};
+b._bAttrSrc=h.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return S(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=
+d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function Z(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ha(a);for(var c=0,d=b.length;c<d;c++)b[c].nTh.style.width=b[c].sWidth}b=a.oScroll;(""!==b.sY||""!==b.sX)&&ma(a);s(a,null,"column-sizing",[a])}function $(a,b){var c=na(a,"bVisible");return"number"===typeof c[b]?c[b]:null}function aa(a,b){var c=na(a,"bVisible"),c=h.inArray(b,
+c);return-1!==c?c:null}function ba(a){var b=0;h.each(a.aoColumns,function(a,d){d.bVisible&&"none"!==h(d.nTh).css("display")&&b++});return b}function na(a,b){var c=[];h.map(a.aoColumns,function(a,e){a[b]&&c.push(e)});return c}function Ia(a){var b=a.aoColumns,c=a.aoData,d=m.ext.type.detect,e,f,g,j,i,h,l,q,r;e=0;for(f=b.length;e<f;e++)if(l=b[e],r=[],!l.sType&&l._sManualType)l.sType=l._sManualType;else if(!l.sType){g=0;for(j=d.length;g<j;g++){i=0;for(h=c.length;i<h;i++){r[i]===k&&(r[i]=B(a,i,e,"type"));
+q=d[g](r[i],a);if(!q&&g!==d.length-1)break;if("html"===q)break}if(q){l.sType=q;break}}l.sType||(l.sType="string")}}function kb(a,b,c,d){var e,f,g,j,i,n,l=a.aoColumns;if(b)for(e=b.length-1;0<=e;e--){n=b[e];var q=n.targets!==k?n.targets:n.aTargets;h.isArray(q)||(q=[q]);f=0;for(g=q.length;f<g;f++)if("number"===typeof q[f]&&0<=q[f]){for(;l.length<=q[f];)Ga(a);d(q[f],n)}else if("number"===typeof q[f]&&0>q[f])d(l.length+q[f],n);else if("string"===typeof q[f]){j=0;for(i=l.length;j<i;j++)("_all"==q[f]||h(l[j].nTh).hasClass(q[f]))&&
+d(j,n)}}if(c){e=0;for(a=c.length;e<a;e++)d(e,c[e])}}function N(a,b,c,d){var e=a.aoData.length,f=h.extend(!0,{},m.models.oRow,{src:c?"dom":"data",idx:e});f._aData=b;a.aoData.push(f);for(var g=a.aoColumns,j=0,i=g.length;j<i;j++)g[j].sType=null;a.aiDisplayMaster.push(e);b=a.rowIdFn(b);b!==k&&(a.aIds[b]=f);(c||!a.oFeatures.bDeferRender)&&Ja(a,e,c,d);return e}function oa(a,b){var c;b instanceof h||(b=h(b));return b.map(function(b,e){c=Ka(a,e);return N(a,c.data,e,c.cells)})}function B(a,b,c,d){var e=a.iDraw,
+f=a.aoColumns[c],g=a.aoData[b]._aData,j=f.sDefaultContent,i=f.fnGetData(g,d,{settings:a,row:b,col:c});if(i===k)return a.iDrawError!=e&&null===j&&(K(a,0,"Requested unknown parameter "+("function"==typeof f.mData?"{function}":"'"+f.mData+"'")+" for row "+b+", column "+c,4),a.iDrawError=e),j;if((i===g||null===i)&&null!==j&&d!==k)i=j;else if("function"===typeof i)return i.call(g);return null===i&&"display"==d?"":i}function lb(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,d,{settings:a,row:b,col:c})}
+function La(a){return h.map(a.match(/(\\.|[^\.])+/g)||[""],function(a){return a.replace(/\\\./g,".")})}function R(a){if(h.isPlainObject(a)){var b={};h.each(a,function(a,c){c&&(b[a]=R(c))});return function(a,c,f,g){var j=b[c]||b._;return j!==k?j(a,c,f,g):a}}if(null===a)return function(a){return a};if("function"===typeof a)return function(b,c,f,g){return a(b,c,f,g)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var c=function(a,b,f){var g,j;if(""!==f){j=La(f);
+for(var i=0,n=j.length;i<n;i++){f=j[i].match(ca);g=j[i].match(V);if(f){j[i]=j[i].replace(ca,"");""!==j[i]&&(a=a[j[i]]);g=[];j.splice(0,i+1);j=j.join(".");if(h.isArray(a)){i=0;for(n=a.length;i<n;i++)g.push(c(a[i],b,j))}a=f[0].substring(1,f[0].length-1);a=""===a?g:g.join(a);break}else if(g){j[i]=j[i].replace(V,"");a=a[j[i]]();continue}if(null===a||a[j[i]]===k)return k;a=a[j[i]]}}return a};return function(b,e){return c(b,e,a)}}return function(b){return b[a]}}function S(a){if(h.isPlainObject(a))return S(a._);
+if(null===a)return function(){};if("function"===typeof a)return function(b,d,e){a(b,"set",d,e)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var b=function(a,d,e){var e=La(e),f;f=e[e.length-1];for(var g,j,i=0,n=e.length-1;i<n;i++){g=e[i].match(ca);j=e[i].match(V);if(g){e[i]=e[i].replace(ca,"");a[e[i]]=[];f=e.slice();f.splice(0,i+1);g=f.join(".");if(h.isArray(d)){j=0;for(n=d.length;j<n;j++)f={},b(f,d[j],g),a[e[i]].push(f)}else a[e[i]]=d;return}j&&(e[i]=e[i].replace(V,
+""),a=a[e[i]](d));if(null===a[e[i]]||a[e[i]]===k)a[e[i]]={};a=a[e[i]]}if(f.match(V))a[f.replace(V,"")](d);else a[f.replace(ca,"")]=d};return function(c,d){return b(c,d,a)}}return function(b,d){b[a]=d}}function Ma(a){return D(a.aoData,"_aData")}function pa(a){a.aoData.length=0;a.aiDisplayMaster.length=0;a.aiDisplay.length=0;a.aIds={}}function qa(a,b,c){for(var d=-1,e=0,f=a.length;e<f;e++)a[e]==b?d=e:a[e]>b&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function da(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);
+c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ka(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c<f;c++)g(j[c],c)}}e._aSortData=null;e._aFilterData=null;g=a.aoColumns;if(d!==k)g[d].sType=null;else{c=0;for(f=g.length;c<f;c++)g[c].sType=null;Na(a,e)}}function Ka(a,b,c,d){var e=[],f=b.firstChild,g,j,i=0,n,l=a.aoColumns,q=a._rowReadObject,d=d!==k?d:q?{}:[],r=function(a,b){if("string"===typeof a){var c=a.indexOf("@");
+-1!==c&&(c=a.substring(c+1),S(a)(d,b.getAttribute(c)))}},m=function(a){if(c===k||c===i)j=l[i],n=h.trim(a.innerHTML),j&&j._bAttrSrc?(S(j.mData._)(d,n),r(j.mData.sort,a),r(j.mData.type,a),r(j.mData.filter,a)):q?(j._setter||(j._setter=S(j.mData)),j._setter(d,n)):d[i]=n;i++};if(f)for(;f;){g=f.nodeName.toUpperCase();if("TD"==g||"TH"==g)m(f),e.push(f);f=f.nextSibling}else{e=b.anCells;f=0;for(g=e.length;f<g;f++)m(e[f])}if(b=b.firstChild?b:b.nTr)(b=b.getAttribute("id"))&&S(a.rowId)(d,b);return{data:d,cells:e}}
+function Ja(a,b,c,d){var e=a.aoData[b],f=e._aData,g=[],j,i,n,l,q;if(null===e.nTr){j=c||H.createElement("tr");e.nTr=j;e.anCells=g;j._DT_RowIndex=b;Na(a,e);l=0;for(q=a.aoColumns.length;l<q;l++){n=a.aoColumns[l];i=c?d[l]:H.createElement(n.sCellType);i._DT_CellIndex={row:b,column:l};g.push(i);if((!c||n.mRender||n.mData!==l)&&(!h.isPlainObject(n.mData)||n.mData._!==l+".display"))i.innerHTML=B(a,b,l,"display");n.sClass&&(i.className+=" "+n.sClass);n.bVisible&&!c?j.appendChild(i):!n.bVisible&&c&&i.parentNode.removeChild(i);
+n.fnCreatedCell&&n.fnCreatedCell.call(a.oInstance,i,B(a,b,l),f,b,l)}s(a,"aoRowCreatedCallback",null,[j,f,b])}e.nTr.setAttribute("role","row")}function Na(a,b){var c=b.nTr,d=b._aData;if(c){var e=a.rowIdFn(d);e&&(c.id=e);d.DT_RowClass&&(e=d.DT_RowClass.split(" "),b.__rowc=b.__rowc?sa(b.__rowc.concat(e)):e,h(c).removeClass(b.__rowc.join(" ")).addClass(d.DT_RowClass));d.DT_RowAttr&&h(c).attr(d.DT_RowAttr);d.DT_RowData&&h(c).data(d.DT_RowData)}}function mb(a){var b,c,d,e,f,g=a.nTHead,j=a.nTFoot,i=0===
+h("th, td",g).length,n=a.oClasses,l=a.aoColumns;i&&(e=h("<tr/>").appendTo(g));b=0;for(c=l.length;b<c;b++)f=l[b],d=h(f.nTh).addClass(f.sClass),i&&d.appendTo(e),a.oFeatures.bSort&&(d.addClass(f.sSortingClass),!1!==f.bSortable&&(d.attr("tabindex",a.iTabIndex).attr("aria-controls",a.sTableId),Oa(a,f.nTh,b))),f.sTitle!=d[0].innerHTML&&d.html(f.sTitle),Pa(a,"header")(a,d,f,n);i&&ea(a.aoHeader,g);h(g).find(">tr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(n.sFooterTH);
+if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b<c;b++)f=l[b],f.nTf=a[b].cell,f.sClass&&h(f.nTf).addClass(f.sClass)}}function fa(a,b,c){var d,e,f,g=[],j=[],i=a.aoColumns.length,n;if(b){c===k&&(c=!1);d=0;for(e=b.length;d<e;d++){g[d]=b[d].slice();g[d].nTr=b[d].nTr;for(f=i-1;0<=f;f--)!a.aoColumns[f].bVisible&&!c&&g[d].splice(f,1);j.push([])}d=0;for(e=g.length;d<e;d++){if(a=g[d].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=g[d].length;f<b;f++)if(n=i=1,j[d][f]===k){a.appendChild(g[d][f].cell);
+for(j[d][f]=1;g[d+i]!==k&&g[d][f].cell==g[d+i][f].cell;)j[d+i][f]=1,i++;for(;g[d][f+n]!==k&&g[d][f].cell==g[d][f+n].cell;){for(c=0;c<i;c++)j[d+c][f+n]=1;n++}h(g[d][f].cell).attr("rowspan",i).attr("colspan",n)}}}}function O(a){var b=s(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==h.inArray(!1,b))C(a,!1);else{var b=[],c=0,d=a.asStripeClasses,e=d.length,f=a.oLanguage,g=a.iInitDisplayStart,j="ssp"==y(a),i=a.aiDisplay;a.bDrawing=!0;g!==k&&-1!==g&&(a._iDisplayStart=j?g:g>=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=
+-1);var g=a._iDisplayStart,n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!nb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:n;for(j=j?0:g;j<f;j++){var l=i[j],q=a.aoData[l];null===q.nTr&&Ja(a,l);l=q.nTr;if(0!==e){var r=d[c%e];q._sRowStripe!=r&&(h(l).removeClass(q._sRowStripe).addClass(r),q._sRowStripe=r)}s(a,"aoRowCallback",null,[l,q._aData,c,j]);b.push(l);c++}}else c=f.sZeroRecords,1==a.iDraw&&"ajax"==y(a)?c=f.sLoadingRecords:
+f.sEmptyTable&&0===a.fnRecordsTotal()&&(c=f.sEmptyTable),b[0]=h("<tr/>",{"class":e?d[0]:""}).append(h("<td />",{valign:"top",colSpan:ba(a),"class":a.oClasses.sRowEmpty}).html(c))[0];s(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ma(a),g,n,i]);s(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ma(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));s(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter;
+c.bSort&&ob(a);d?ga(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;O(a);a._drawHold=!1}function pb(a){var b=a.oClasses,c=h(a.nTable),c=h("<div/>").insertBefore(c),d=a.oFeatures,e=h("<div/>",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,k=0;k<f.length;k++){g=null;j=f[k];if("<"==j){i=h("<div/>")[0];
+n=f[k+1];if("'"==n||'"'==n){l="";for(q=2;f[k+q]!=n;)l+=f[k+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;k+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=qb(a);else if("f"==j&&d.bFilter)g=rb(a);else if("r"==j&&d.bProcessing)g=sb(a);else if("t"==j)g=tb(a);else if("i"==j&&d.bInfo)g=ub(a);else if("p"==
+j&&d.bPaginate)g=vb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q<n;q++)if(j==i[q].cFeature){g=i[q].fnInit(a);break}}g&&(i=a.aanFeatures,i[j]||(i[j]=[]),i[j].push(g),e.append(g))}c.replaceWith(e);a.nHolding=null}function ea(a,b){var c=h(b).children("tr"),d,e,f,g,j,i,n,l,q,k;a.splice(0,a.length);f=0;for(i=c.length;f<i;f++)a.push([]);f=0;for(i=c.length;f<i;f++){d=c[f];for(e=d.firstChild;e;){if("TD"==e.nodeName.toUpperCase()||"TH"==e.nodeName.toUpperCase()){l=1*e.getAttribute("colspan");
+q=1*e.getAttribute("rowspan");l=!l||0===l||1===l?1:l;q=!q||0===q||1===q?1:q;g=0;for(j=a[f];j[g];)g++;n=g;k=1===l?!0:!1;for(j=0;j<l;j++)for(g=0;g<q;g++)a[f+g][n+j]={cell:e,unique:k},a[f+g].nTr=d}e=e.nextSibling}}}function ta(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],ea(c,b)));for(var b=0,e=c.length;b<e;b++)for(var f=0,g=c[b].length;f<g;f++)if(c[b][f].unique&&(!d[f]||!a.bSortCellsTop))d[f]=c[b][f].cell;return d}function ua(a,b,c){s(a,"aoServerParams","serverParams",[b]);if(b&&h.isArray(b)){var d={},
+e=/(.*?)\[\]$/;h.each(b,function(a,b){var c=b.name.match(e);c?(c=c[0],d[c]||(d[c]=[]),d[c].push(b.value)):d[b.name]=b.value});b=d}var f,g=a.ajax,j=a.oInstance,i=function(b){s(a,null,"xhr",[a,b,a.jqXHR]);c(b)};if(h.isPlainObject(g)&&g.data){f=g.data;var n=h.isFunction(f)?f(b,a):f,b=h.isFunction(f)&&n?n:h.extend(!0,b,n);delete g.data}n={data:b,success:function(b){var c=b.error||b.sError;c&&K(a,0,c);a.json=b;i(b)},dataType:"json",cache:!1,type:a.sServerMethod,error:function(b,c){var d=s(a,null,"xhr",
+[a,null,a.jqXHR]);-1===h.inArray(!0,d)&&("parsererror"==c?K(a,0,"Invalid JSON response",1):4===b.readyState&&K(a,0,"Ajax error",7));C(a,!1)}};a.oAjaxData=b;s(a,null,"preXhr",[a,b]);a.fnServerData?a.fnServerData.call(j,a.sAjaxSource,h.map(b,function(a,b){return{name:b,value:a}}),i,a):a.sAjaxSource||"string"===typeof g?a.jqXHR=h.ajax(h.extend(n,{url:g||a.sAjaxSource})):h.isFunction(g)?a.jqXHR=g.call(j,b,i,a):(a.jqXHR=h.ajax(h.extend(n,g)),g.data=f)}function nb(a){return a.bAjaxDataGet?(a.iDraw++,C(a,
+!0),ua(a,wb(a),function(b){xb(a,b)}),!1):!0}function wb(a){var b=a.aoColumns,c=b.length,d=a.oFeatures,e=a.oPreviousSearch,f=a.aoPreSearchCols,g,j=[],i,n,l,k=W(a);g=a._iDisplayStart;i=!1!==d.bPaginate?a._iDisplayLength:-1;var r=function(a,b){j.push({name:a,value:b})};r("sEcho",a.iDraw);r("iColumns",c);r("sColumns",D(b,"sName").join(","));r("iDisplayStart",g);r("iDisplayLength",i);var ra={draw:a.iDraw,columns:[],order:[],start:g,length:i,search:{value:e.sSearch,regex:e.bRegex}};for(g=0;g<c;g++)n=b[g],
+l=f[g],i="function"==typeof n.mData?"function":n.mData,ra.columns.push({data:i,name:n.sName,searchable:n.bSearchable,orderable:n.bSortable,search:{value:l.sSearch,regex:l.bRegex}}),r("mDataProp_"+g,i),d.bFilter&&(r("sSearch_"+g,l.sSearch),r("bRegex_"+g,l.bRegex),r("bSearchable_"+g,n.bSearchable)),d.bSort&&r("bSortable_"+g,n.bSortable);d.bFilter&&(r("sSearch",e.sSearch),r("bRegex",e.bRegex));d.bSort&&(h.each(k,function(a,b){ra.order.push({column:b.col,dir:b.dir});r("iSortCol_"+a,b.col);r("sSortDir_"+
+a,b.dir)}),r("iSortingCols",k.length));b=m.ext.legacy.ajax;return null===b?a.sAjaxSource?j:ra:b?j:ra}function xb(a,b){var c=va(a,b),d=b.sEcho!==k?b.sEcho:b.draw,e=b.iTotalRecords!==k?b.iTotalRecords:b.recordsTotal,f=b.iTotalDisplayRecords!==k?b.iTotalDisplayRecords:b.recordsFiltered;if(d){if(1*d<a.iDraw)return;a.iDraw=1*d}pa(a);a._iRecordsTotal=parseInt(e,10);a._iRecordsDisplay=parseInt(f,10);d=0;for(e=c.length;d<e;d++)N(a,c[d]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;O(a);a._bInitComplete||
+wa(a,b);a.bAjaxDataGet=!0;C(a,!1)}function va(a,b){var c=h.isPlainObject(a.ajax)&&a.ajax.dataSrc!==k?a.ajax.dataSrc:a.sAjaxDataProp;return"data"===c?b.aaData||b[c]:""!==c?R(c)(b):b}function rb(a){var b=a.oClasses,c=a.sTableId,d=a.oLanguage,e=a.oPreviousSearch,f=a.aanFeatures,g='<input type="search" class="'+b.sFilterInput+'"/>',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("<div/>",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("<label/>").append(j)),f=function(){var b=!this.value?
+"":this.value;b!=e.sSearch&&(ga(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,O(a))},g=null!==a.searchDelay?a.searchDelay:"ssp"===y(a)?400:0,i=h("input",b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).on("keyup.DT search.DT input.DT paste.DT cut.DT",g?Qa(f,g):f).on("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);h(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{i[0]!==H.activeElement&&i.val(e.sSearch)}catch(d){}});
+return b[0]}function ga(a,b,c){var d=a.oPreviousSearch,e=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};Ia(a);if("ssp"!=y(a)){yb(a,b.sSearch,c,b.bEscapeRegex!==k?!b.bEscapeRegex:b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b<e.length;b++)zb(a,e[b].sSearch,b,e[b].bEscapeRegex!==k?!e[b].bEscapeRegex:e[b].bRegex,e[b].bSmart,e[b].bCaseInsensitive);Ab(a)}else f(b);a.bFiltered=!0;s(a,null,"search",[a])}function Ab(a){for(var b=
+m.ext.search,c=a.aiDisplay,d,e,f=0,g=b.length;f<g;f++){for(var j=[],i=0,n=c.length;i<n;i++)e=c[i],d=a.aoData[e],b[f](a,d._aFilterData,e,d._aData,i)&&j.push(e);c.length=0;h.merge(c,j)}}function zb(a,b,c,d,e,f){if(""!==b){for(var g=[],j=a.aiDisplay,d=Ra(b,d,e,f),e=0;e<j.length;e++)b=a.aoData[j[e]]._aFilterData[c],d.test(b)&&g.push(j[e]);a.aiDisplay=g}}function yb(a,b,c,d,e,f){var d=Ra(b,d,e,f),f=a.oPreviousSearch.sSearch,g=a.aiDisplayMaster,j,e=[];0!==m.ext.search.length&&(c=!0);j=Bb(a);if(0>=b.length)a.aiDisplay=
+g.slice();else{if(j||c||f.length>b.length||0!==b.indexOf(f)||a.bSorted)a.aiDisplay=g.slice();b=a.aiDisplay;for(c=0;c<b.length;c++)d.test(a.aoData[b[c]]._sFilterRow)&&e.push(b[c]);a.aiDisplay=e}}function Ra(a,b,c,d){a=b?a:Sa(a);c&&(a="^(?=.*?"+h.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(a){if('"'===a.charAt(0))var b=a.match(/^"(.*)"$/),a=b?b[1]:a;return a.replace('"',"")}).join(")(?=.*?")+").*$");return RegExp(a,d?"i":"")}function Bb(a){var b=a.aoColumns,c,d,e,f,g,j,i,h,l=m.ext.type.search;c=!1;
+d=0;for(f=a.aoData.length;d<f;d++)if(h=a.aoData[d],!h._aFilterData){j=[];e=0;for(g=b.length;e<g;e++)c=b[e],c.bSearchable?(i=B(a,d,e,"filter"),l[c.sType]&&(i=l[c.sType](i)),null===i&&(i=""),"string"!==typeof i&&i.toString&&(i=i.toString())):i="",i.indexOf&&-1!==i.indexOf("&")&&(xa.innerHTML=i,i=$b?xa.textContent:xa.innerText),i.replace&&(i=i.replace(/[\r\n]/g,"")),j.push(i);h._aFilterData=j;h._sFilterRow=j.join("  ");c=!0}return c}function Cb(a){return{search:a.sSearch,smart:a.bSmart,regex:a.bRegex,
+caseInsensitive:a.bCaseInsensitive}}function Db(a){return{sSearch:a.search,bSmart:a.smart,bRegex:a.regex,bCaseInsensitive:a.caseInsensitive}}function ub(a){var b=a.sTableId,c=a.aanFeatures.i,d=h("<div/>",{"class":a.oClasses.sInfo,id:!c?b+"_info":null});c||(a.aoDrawCallback.push({fn:Eb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),h(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Eb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+
+1,e=a.fnDisplayEnd(),f=a.fnRecordsTotal(),g=a.fnRecordsDisplay(),j=g?c.sInfo:c.sInfoEmpty;g!==f&&(j+=" "+c.sInfoFiltered);j+=c.sInfoPostFix;j=Fb(a,j);c=c.fnInfoCallback;null!==c&&(j=c.call(a.oInstance,a,d,e,f,g,j));h(b).html(j)}}function Fb(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,f=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,
+f)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(f/e)))}function ha(a){var b,c,d=a.iInitDisplayStart,e=a.aoColumns,f;c=a.oFeatures;var g=a.bDeferLoading;if(a.bInitialised){pb(a);mb(a);fa(a,a.aoHeader);fa(a,a.aoFooter);C(a,!0);c.bAutoWidth&&Ha(a);b=0;for(c=e.length;b<c;b++)f=e[b],f.sWidth&&(f.nTh.style.width=v(f.sWidth));s(a,null,"preInit",[a]);T(a);e=y(a);if("ssp"!=e||g)"ajax"==e?ua(a,[],function(c){var f=va(a,c);for(b=0;b<f.length;b++)N(a,f[b]);a.iInitDisplayStart=
+d;T(a);C(a,!1);wa(a,c)},a):(C(a,!1),wa(a))}else setTimeout(function(){ha(a)},200)}function wa(a,b){a._bInitComplete=!0;(b||a.oInit.aaData)&&Z(a);s(a,null,"plugin-init",[a,b]);s(a,"aoInitComplete","init",[a,b])}function Ta(a,b){var c=parseInt(b,10);a._iDisplayLength=c;Ua(a);s(a,null,"length",[a,c])}function qb(a){for(var b=a.oClasses,c=a.sTableId,d=a.aLengthMenu,e=h.isArray(d[0]),f=e?d[0]:d,d=e?d[1]:d,e=h("<select/>",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect}),g=0,j=f.length;g<j;g++)e[0][g]=
+new Option(d[g],f[g]);var i=h("<div><label/></div>").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).on("change.DT",function(){Ta(a,h(this).val());O(a)});h(a.nTable).on("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function vb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){O(a)},b=h("<div/>").addClass(a.oClasses.sPaging+
+b)[0],f=a.aanFeatures;d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;l<k;l++)Pa(a,"pageButton")(a,f.p[l],l,h,b,i)}else c.fnUpdate(a,e)},sName:"pagination"}));return b}function Va(a,b,c){var d=a._iDisplayStart,e=a._iDisplayLength,f=a.fnRecordsDisplay();0===f||-1===e?d=0:"number"===typeof b?(d=b*e,d>f&&
+(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e<f&&(d+=e):"last"==b?d=Math.floor((f-1)/e)*e:K(a,0,"Unknown paging action: "+b,5);b=a._iDisplayStart!==d;a._iDisplayStart=d;b&&(s(a,null,"page",[a]),c&&O(a));return b}function sb(a){return h("<div/>",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");
+s(a,null,"processing",[a,b])}function tb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("<div/>",{"class":f.sScrollWrapper}).append(h("<div/>",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:v(d):"100%"}).append(h("<div/>",
+{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("<div/>",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:v(d)}).append(b));l&&i.append(h("<div/>",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:v(d):"100%"}).append(h("<div/>",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",
+0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=i.children(),k=b[0],f=b[1],r=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(r.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=r;a.aoDrawCallback.push({fn:ma,sName:"scrolling"});return i[0]}function ma(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,n=j.children("table"),
+j=a.nScrollBody,l=h(j),q=j.style,r=h(a.nScrollFoot).children("div"),m=r.children("table"),p=h(a.nTHead),o=h(a.nTable),t=o[0],s=t.style,u=a.nTFoot?h(a.nTFoot):null,x=a.oBrowser,U=x.bScrollOversize,ac=D(a.aoColumns,"nTh"),P,L,Q,w,Wa=[],y=[],z=[],A=[],B,C=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};L=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!==L&&a.scrollBarVis!==k)a.scrollBarVis=L,Z(a);else{a.scrollBarVis=L;o.children("thead, tfoot").remove();
+u&&(Q=u.clone().prependTo(o),P=u.find("tr"),Q=Q.find("tr"));w=p.clone().prependTo(o);p=p.find("tr");L=w.find("tr");w.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(ta(a,w),function(b,c){B=$(a,b);c.style.width=a.aoColumns[B].sWidth});u&&I(function(a){a.style.width=""},Q);f=o.outerWidth();if(""===c){s.width="100%";if(U&&(o.find("tbody").height()>j.offsetHeight||"scroll"==l.css("overflow-y")))s.width=v(o.outerWidth()-b);f=o.outerWidth()}else""!==d&&(s.width=
+v(d),f=o.outerWidth());I(C,L);I(function(a){z.push(a.innerHTML);Wa.push(v(h(a).css("width")))},L);I(function(a,b){if(h.inArray(a,ac)!==-1)a.style.width=Wa[b]},p);h(L).height(0);u&&(I(C,Q),I(function(a){A.push(a.innerHTML);y.push(v(h(a).css("width")))},Q),I(function(a,b){a.style.width=y[b]},P),h(Q).height(0));I(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+z[b]+"</div>";a.style.width=Wa[b]},L);u&&I(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+
+A[b]+"</div>";a.style.width=y[b]},Q);if(o.outerWidth()<f){P=j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(U&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))s.width=v(P-b);(""===c||""!==d)&&K(a,1,"Possible column misalignment",6)}else P="100%";q.width=v(P);g.width=v(P);u&&(a.nScrollFoot.style.width=v(P));!e&&U&&(q.height=v(t.offsetHeight+b));c=o.outerWidth();n[0].style.width=v(c);i.width=v(c);d=o.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+
+(x.bScrollbarLeft?"Left":"Right");i[e]=d?b+"px":"0px";u&&(m[0].style.width=v(c),r[0].style.width=v(c),r[0].style[e]=d?b+"px":"0px");o.children("colgroup").insertBefore(o.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function I(a,b,c){for(var d=0,e=0,f=b.length,g,j;e<f;){g=b[e].firstChild;for(j=c?c[e].firstChild:null;g;)1===g.nodeType&&(c?a(g,j,d):a(g,d),d++),g=g.nextSibling,j=c?j.nextSibling:null;e++}}function Ha(a){var b=a.nTable,c=a.aoColumns,d=a.oScroll,
+e=d.sY,f=d.sX,g=d.sXInner,j=c.length,i=na(a,"bVisible"),n=h("th",a.nTHead),l=b.getAttribute("width"),k=b.parentNode,r=!1,m,p,o=a.oBrowser,d=o.bScrollOversize;(m=b.style.width)&&-1!==m.indexOf("%")&&(l=m);for(m=0;m<i.length;m++)p=c[i[m]],null!==p.sWidth&&(p.sWidth=Gb(p.sWidthOrig,k),r=!0);if(d||!r&&!f&&!e&&j==ba(a)&&j==n.length)for(m=0;m<j;m++)i=$(a,m),null!==i&&(c[i].sWidth=v(n.eq(m).width()));else{j=h(b).clone().css("visibility","hidden").removeAttr("id");j.find("tbody tr").remove();var t=h("<tr/>").appendTo(j.find("tbody"));
+j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");n=ta(a,j.find("thead")[0]);for(m=0;m<i.length;m++)p=c[i[m]],n[m].style.width=null!==p.sWidthOrig&&""!==p.sWidthOrig?v(p.sWidthOrig):"",p.sWidthOrig&&f&&h(n[m]).append(h("<div/>").css({width:p.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(m=0;m<i.length;m++)r=i[m],p=c[r],h(Hb(a,r)).clone(!1).append(p.sContentPadding).appendTo(t);h("[name]",
+j).removeAttr("name");p=h("<div/>").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()<k.clientWidth&&l&&j.width(k.clientWidth)):e?j.width(k.clientWidth):l&&j.width(l);for(m=e=0;m<i.length;m++)k=h(n[m]),g=k.outerWidth()-k.width(),k=o.bBounding?Math.ceil(n[m].getBoundingClientRect().width):k.outerWidth(),e+=k,c[i[m]].sWidth=v(k-g);b.style.width=v(e);p.remove()}l&&(b.style.width=
+v(l));if((l||f)&&!a._reszEvt)b=function(){h(E).on("resize.DT-"+a.sInstance,Qa(function(){Z(a)}))},d?setTimeout(b,1E3):b(),a._reszEvt=!0}function Gb(a,b){if(!a)return 0;var c=h("<div/>").css("width",v(a)).appendTo(b||H.body),d=c[0].offsetWidth;c.remove();return d}function Hb(a,b){var c=Ib(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("<td/>").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Ib(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;f<g;f++)c=B(a,f,b,"display")+"",c=c.replace(bc,
+""),c=c.replace(/&nbsp;/g," "),c.length>d&&(d=c.length,e=f);return e}function v(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function W(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):h.merge(n,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;a<n.length;a++){i=n[a][0];f=e[i].aDataSort;b=0;for(c=f.length;b<c;b++)g=f[b],j=e[g].sType||
+"string",n[a]._idx===k&&(n[a]._idx=h.inArray(n[a][1],e[g].asSorting)),d.push({src:i,col:g,dir:n[a][1],index:n[a]._idx,type:j,formatter:m.ext.type.order[j+"-pre"]})}return d}function ob(a){var b,c,d=[],e=m.ext.type.order,f=a.aoData,g=0,j,i=a.aiDisplayMaster,h;Ia(a);h=W(a);b=0;for(c=h.length;b<c;b++)j=h[b],j.formatter&&g++,Jb(a,j.col);if("ssp"!=y(a)&&0!==h.length){b=0;for(c=i.length;b<c;b++)d[i[b]]=b;g===h.length?i.sort(function(a,b){var c,e,g,j,i=h.length,k=f[a]._aSortData,m=f[b]._aSortData;for(g=
+0;g<i;g++)if(j=h[g],c=k[j.col],e=m[j.col],c=c<e?-1:c>e?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return c<e?-1:c>e?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,m=f[a]._aSortData,p=f[b]._aSortData;for(j=0;j<k;j++)if(i=h[j],c=m[i.col],g=p[i.col],i=e[i.type+"-"+i.dir]||e["string-"+i.dir],c=i(c,g),0!==c)return c;c=d[a];g=d[b];return c<g?-1:c>g?1:0})}a.bSorted=!0}function Kb(a){for(var b,c,d=a.aoColumns,e=W(a),a=a.oLanguage.oAria,f=0,g=d.length;f<g;f++){c=d[f];var j=c.asSorting;b=c.sTitle.replace(/<.*?>/g,
+"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0<e.length&&e[0].col==f?(i.setAttribute("aria-sort","asc"==e[0].dir?"ascending":"descending"),c=j[e[0].index+1]||j[0]):c=j[0],b+="asc"===c?a.sSortAscending:a.sSortDescending);i.setAttribute("aria-label",b)}}function Xa(a,b,c,d){var e=a.aaSorting,f=a.aoColumns[b].asSorting,g=function(a,b){var c=a._idx;c===k&&(c=h.inArray(a[1],f));return c+1<f.length?c+1:b?null:0};"number"===typeof e[0]&&(e=a.aaSorting=[e]);c&&a.oFeatures.bSortMulti?(c=h.inArray(b,
+D(e,"0")),-1!==c?(b=g(e[c],!0),null===b&&1===e.length&&(b=0),null===b?e.splice(c,1):(e[c][1]=f[b],e[c]._idx=b)):(e.push([b,f[0],0]),e[e.length-1]._idx=0)):e.length&&e[0][0]==b?(b=g(e[0]),e.length=1,e[0][1]=f[b],e[0]._idx=b):(e.length=0,e.push([b,f[0]]),e[0]._idx=0);T(a);"function"==typeof d&&d(a)}function Oa(a,b,c,d){var e=a.aoColumns[c];Ya(b,{},function(b){!1!==e.bSortable&&(a.oFeatures.bProcessing?(C(a,!0),setTimeout(function(){Xa(a,c,b.shiftKey,d);"ssp"!==y(a)&&C(a,!1)},0)):Xa(a,c,b.shiftKey,d))})}
+function ya(a){var b=a.aLastSort,c=a.oClasses.sSortColumn,d=W(a),e=a.oFeatures,f,g;if(e.bSort&&e.bSortClasses){e=0;for(f=b.length;e<f;e++)g=b[e].src,h(D(a.aoData,"anCells",g)).removeClass(c+(2>e?e+1:3));e=0;for(f=d.length;e<f;e++)g=d[e].src,h(D(a.aoData,"anCells",g)).addClass(c+(2>e?e+1:3))}a.aLastSort=d}function Jb(a,b){var c=a.aoColumns[b],d=m.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,aa(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j<i;j++)if(c=a.aoData[j],
+c._aSortData||(c._aSortData=[]),!c._aSortData[b]||d)f=d?e[j]:B(a,j,b,"sort"),c._aSortData[b]=g?g(f):f}function za(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b={time:+new Date,start:a._iDisplayStart,length:a._iDisplayLength,order:h.extend(!0,[],a.aaSorting),search:Cb(a.oPreviousSearch),columns:h.map(a.aoColumns,function(b,d){return{visible:b.bVisible,search:Cb(a.aoPreSearchCols[d])}})};s(a,"aoStateSaveParams","stateSaveParams",[a,b]);a.oSavedState=b;a.fnStateSaveCallback.call(a.oInstance,a,
+b)}}function Lb(a,b,c){var d,e,f=a.aoColumns,b=function(b){if(b&&b.time){var g=s(a,"aoStateLoadParams","stateLoadParams",[a,b]);if(-1===h.inArray(!1,g)&&(g=a.iStateDuration,!(0<g&&b.time<+new Date-1E3*g)&&!(b.columns&&f.length!==b.columns.length))){a.oLoadedState=h.extend(!0,{},b);b.start!==k&&(a._iDisplayStart=b.start,a.iInitDisplayStart=b.start);b.length!==k&&(a._iDisplayLength=b.length);b.order!==k&&(a.aaSorting=[],h.each(b.order,function(b,c){a.aaSorting.push(c[0]>=f.length?[0,c[1]]:c)}));b.search!==
+k&&h.extend(a.oPreviousSearch,Db(b.search));if(b.columns){d=0;for(e=b.columns.length;d<e;d++)g=b.columns[d],g.visible!==k&&(f[d].bVisible=g.visible),g.search!==k&&h.extend(a.aoPreSearchCols[d],Db(g.search))}s(a,"aoStateLoaded","stateLoaded",[a,b])}}c()};if(a.oFeatures.bStateSave){var g=a.fnStateLoadCallback.call(a.oInstance,a,b);g!==k&&b(g)}else c()}function Aa(a){var b=m.settings,a=h.inArray(a,D(b,"nTable"));return-1!==a?b[a]:null}function K(a,b,c,d){c="DataTables warning: "+(a?"table id="+a.sTableId+
+" - ":"")+c;d&&(c+=". For more information about this error, please see http://datatables.net/tn/"+d);if(b)E.console&&console.log&&console.log(c);else if(b=m.ext,b=b.sErrMode||b.errMode,a&&s(a,null,"error",[a,d,c]),"alert"==b)alert(c);else{if("throw"==b)throw Error(c);"function"==typeof b&&b(a,d,c)}}function F(a,b,c,d){h.isArray(c)?h.each(c,function(c,d){h.isArray(d)?F(a,b,d[0],d[1]):F(a,b,d)}):(d===k&&(d=c),b[c]!==k&&(a[d]=b[c]))}function Mb(a,b,c){var d,e;for(e in b)b.hasOwnProperty(e)&&(d=b[e],
+h.isPlainObject(d)?(h.isPlainObject(a[e])||(a[e]={}),h.extend(!0,a[e],d)):a[e]=c&&"data"!==e&&"aaData"!==e&&h.isArray(d)?d.slice():d);return a}function Ya(a,b,c){h(a).on("click.DT",b,function(b){a.blur();c(b)}).on("keypress.DT",b,function(a){13===a.which&&(a.preventDefault(),c(a))}).on("selectstart.DT",function(){return!1})}function z(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function s(a,b,c,d){var e=[];b&&(e=h.map(a[b].slice().reverse(),function(b){return b.fn.apply(a.oInstance,d)}));null!==c&&(b=h.Event(c+
+".dt"),h(a.nTable).trigger(b,d),e.push(b.result));return e}function Ua(a){var b=a._iDisplayStart,c=a.fnDisplayEnd(),d=a._iDisplayLength;b>=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Pa(a,b){var c=a.renderer,d=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ia(a,b){var c=[],c=Nb.numbers_length,d=Math.floor(c/2);b<=c?c=X(0,b):a<=d?(c=X(0,
+c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=X(b-(c-2),b):(c=X(a-d+2,a+d-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function fb(a){h.each({num:function(b){return Ba(b,a)},"num-fmt":function(b){return Ba(b,a,Za)},"html-num":function(b){return Ba(b,a,Ca)},"html-num-fmt":function(b){return Ba(b,a,Ca,Za)}},function(b,c){x.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(x.type.search[b+a]=x.type.search.html)})}function Ob(a){return function(){var b=
+[Aa(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,b)}}var m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new t(Aa(this[x.iApiIndex])):new t(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=
+function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):(""!==d.sX||""!==d.sY)&&ma(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};
+this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();
+return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return Aa(this[x.iApiIndex])};
+this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=x.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=x.internal;for(var e in m.ext.internal)e&&(this[e]=Ob(e));this.each(function(){var e={},g=1<d?Mb(e,a,!0):
+a,j=0,i,e=this.getAttribute("id"),n=!1,l=m.defaults,q=h(this);if("table"!=this.nodeName.toLowerCase())K(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{gb(l);hb(l.column);J(l,l,!0);J(l.column,l.column,!0);J(l,h.extend(g,q.data()));var r=m.settings,j=0;for(i=r.length;j<i;j++){var p=r[j];if(p.nTable==this||p.nTHead.parentNode==this||p.nTFoot&&p.nTFoot.parentNode==this){var t=g.bRetrieve!==k?g.bRetrieve:l.bRetrieve;if(c||t)return p.oInstance;if(g.bDestroy!==k?g.bDestroy:l.bDestroy){p.oInstance.fnDestroy();
+break}else{K(p,0,"Cannot reinitialise DataTable",3);return}}if(p.sTableId==this.id){r.splice(j,1);break}}if(null===e||""===e)this.id=e="DataTables_Table_"+m.ext._unique++;var o=h.extend(!0,{},m.models.oSettings,{sDestroyWidth:q[0].style.width,sInstance:e,sTableId:e});o.nTable=this;o.oApi=b.internal;o.oInit=g;r.push(o);o.oInstance=1===b.length?b:q.dataTable();gb(g);g.oLanguage&&Fa(g.oLanguage);g.aLengthMenu&&!g.iDisplayLength&&(g.iDisplayLength=h.isArray(g.aLengthMenu[0])?g.aLengthMenu[0][0]:g.aLengthMenu[0]);
+g=Mb(h.extend(!0,{},l),g);F(o.oFeatures,g,"bPaginate bLengthChange bFilter bSort bSortMulti bInfo bProcessing bAutoWidth bSortClasses bServerSide bDeferRender".split(" "));F(o,g,["asStripeClasses","ajax","fnServerData","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed","aLengthMenu","sPaginationType","sAjaxSource","sAjaxDataProp","iStateDuration","sDom","bSortCellsTop","iTabIndex","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId",["iCookieDuration","iStateDuration"],
+["oSearch","oPreviousSearch"],["aoSearchCols","aoPreSearchCols"],["iDisplayLength","_iDisplayLength"],["bJQueryUI","bJUI"]]);F(o.oScroll,g,[["sScrollX","sX"],["sScrollXInner","sXInner"],["sScrollY","sY"],["bScrollCollapse","bCollapse"]]);F(o.oLanguage,g,"fnInfoCallback");z(o,"aoDrawCallback",g.fnDrawCallback,"user");z(o,"aoServerParams",g.fnServerParams,"user");z(o,"aoStateSaveParams",g.fnStateSaveParams,"user");z(o,"aoStateLoadParams",g.fnStateLoadParams,"user");z(o,"aoStateLoaded",g.fnStateLoaded,
+"user");z(o,"aoRowCallback",g.fnRowCallback,"user");z(o,"aoRowCreatedCallback",g.fnCreatedRow,"user");z(o,"aoHeaderCallback",g.fnHeaderCallback,"user");z(o,"aoFooterCallback",g.fnFooterCallback,"user");z(o,"aoInitComplete",g.fnInitComplete,"user");z(o,"aoPreDrawCallback",g.fnPreDrawCallback,"user");o.rowIdFn=R(g.rowId);ib(o);var u=o.oClasses;g.bJQueryUI?(h.extend(u,m.ext.oJUIClasses,g.oClasses),g.sDom===l.sDom&&"lfrtip"===l.sDom&&(o.sDom='<"H"lfr>t<"F"ip>'),o.renderer)?h.isPlainObject(o.renderer)&&
+!o.renderer.header&&(o.renderer.header="jqueryui"):o.renderer="jqueryui":h.extend(u,m.ext.classes,g.oClasses);q.addClass(u.sTable);o.iInitDisplayStart===k&&(o.iInitDisplayStart=g.iDisplayStart,o._iDisplayStart=g.iDisplayStart);null!==g.iDeferLoading&&(o.bDeferLoading=!0,e=h.isArray(g.iDeferLoading),o._iRecordsDisplay=e?g.iDeferLoading[0]:g.iDeferLoading,o._iRecordsTotal=e?g.iDeferLoading[1]:g.iDeferLoading);var v=o.oLanguage;h.extend(!0,v,g.oLanguage);v.sUrl&&(h.ajax({dataType:"json",url:v.sUrl,success:function(a){Fa(a);
+J(l.oLanguage,a);h.extend(true,v,a);ha(o)},error:function(){ha(o)}}),n=!0);null===g.asStripeClasses&&(o.asStripeClasses=[u.sStripeOdd,u.sStripeEven]);var e=o.asStripeClasses,x=q.children("tbody").find("tr").eq(0);-1!==h.inArray(!0,h.map(e,function(a){return x.hasClass(a)}))&&(h("tbody tr",this).removeClass(e.join(" ")),o.asDestroyStripes=e.slice());e=[];r=this.getElementsByTagName("thead");0!==r.length&&(ea(o.aoHeader,r[0]),e=ta(o));if(null===g.aoColumns){r=[];j=0;for(i=e.length;j<i;j++)r.push(null)}else r=
+g.aoColumns;j=0;for(i=r.length;j<i;j++)Ga(o,e?e[j]:null);kb(o,g.aoColumnDefs,r,function(a,b){la(o,a,b)});if(x.length){var w=function(a,b){return a.getAttribute("data-"+b)!==null?b:null};h(x[0]).children("th, td").each(function(a,b){var c=o.aoColumns[a];if(c.mData===a){var d=w(b,"sort")||w(b,"order"),e=w(b,"filter")||w(b,"search");if(d!==null||e!==null){c.mData={_:a+".display",sort:d!==null?a+".@data-"+d:k,type:d!==null?a+".@data-"+d:k,filter:e!==null?a+".@data-"+e:k};la(o,a)}}})}var U=o.oFeatures,
+e=function(){if(g.aaSorting===k){var a=o.aaSorting;j=0;for(i=a.length;j<i;j++)a[j][1]=o.aoColumns[j].asSorting[0]}ya(o);U.bSort&&z(o,"aoDrawCallback",function(){if(o.bSorted){var a=W(o),b={};h.each(a,function(a,c){b[c.src]=c.dir});s(o,null,"order",[o,a,b]);Kb(o)}});z(o,"aoDrawCallback",function(){(o.bSorted||y(o)==="ssp"||U.bDeferRender)&&ya(o)},"sc");var a=q.children("caption").each(function(){this._captionSide=h(this).css("caption-side")}),b=q.children("thead");b.length===0&&(b=h("<thead/>").appendTo(q));
+o.nTHead=b[0];b=q.children("tbody");b.length===0&&(b=h("<tbody/>").appendTo(q));o.nTBody=b[0];b=q.children("tfoot");if(b.length===0&&a.length>0&&(o.oScroll.sX!==""||o.oScroll.sY!==""))b=h("<tfoot/>").appendTo(q);if(b.length===0||b.children().length===0)q.addClass(u.sNoFooter);else if(b.length>0){o.nTFoot=b[0];ea(o.aoFooter,o.nTFoot)}if(g.aaData)for(j=0;j<g.aaData.length;j++)N(o,g.aaData[j]);else(o.bDeferLoading||y(o)=="dom")&&oa(o,h(o.nTBody).children("tr"));o.aiDisplay=o.aiDisplayMaster.slice();
+o.bInitialised=true;n===false&&ha(o)};g.bStateSave?(U.bStateSave=!0,z(o,"aoDrawCallback",za,"state_save"),Lb(o,g,e)):e()}});b=null;return this},x,t,p,u,$a={},Pb=/[\r\n]/g,Ca=/<.*?>/g,cc=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,dc=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Za=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,M=function(a){return!a||!0===a||"-"===a?!0:!1},Qb=function(a){var b=parseInt(a,10);return!isNaN(b)&&
+isFinite(a)?b:null},Rb=function(a,b){$a[b]||($a[b]=RegExp(Sa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace($a[b],"."):a},ab=function(a,b,c){var d="string"===typeof a;if(M(a))return!0;b&&d&&(a=Rb(a,b));c&&d&&(a=a.replace(Za,""));return!isNaN(parseFloat(a))&&isFinite(a)},Sb=function(a,b,c){return M(a)?!0:!(M(a)||"string"===typeof a)?null:ab(a.replace(Ca,""),b,c)?!0:null},D=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;e<f;e++)a[e]&&a[e][b]&&d.push(a[e][b][c]);else for(;e<
+f;e++)a[e]&&d.push(a[e][b]);return d},ja=function(a,b,c,d){var e=[],f=0,g=b.length;if(d!==k)for(;f<g;f++)a[b[f]][c]&&e.push(a[b[f]][c][d]);else for(;f<g;f++)e.push(a[b[f]][c]);return e},X=function(a,b){var c=[],d;b===k?(b=0,d=a):(d=b,b=a);for(var e=b;e<d;e++)c.push(e);return c},Tb=function(a){for(var b=[],c=0,d=a.length;c<d;c++)a[c]&&b.push(a[c]);return b},sa=function(a){var b;a:{if(!(2>a.length)){b=a.slice().sort();for(var c=b[0],d=1,e=b.length;d<e;d++){if(b[d]===c){b=!1;break a}c=b[d]}}b=!0}if(b)return a.slice();
+b=[];var e=a.length,f,g=0,d=0;a:for(;d<e;d++){c=a[d];for(f=0;f<g;f++)if(b[f]===c)continue a;b.push(c);g++}return b};m.util={throttle:function(a,b){var c=b!==k?b:200,d,e;return function(){var b=this,g=+new Date,h=arguments;d&&g<d+c?(clearTimeout(e),e=setTimeout(function(){d=k;a.apply(b,h)},c)):(d=g,a.apply(b,h))}},escapeRegex:function(a){return a.replace(dc,"\\$1")}};var A=function(a,b,c){a[b]!==k&&(a[c]=a[b])},ca=/\[.*?\]$/,V=/\(\)$/,Sa=m.util.escapeRegex,xa=h("<div>")[0],$b=xa.textContent!==k,bc=
+/<.*?>/g,Qa=m.util.throttle,Ub=[],w=Array.prototype,ec=function(a){var b,c,d=m.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};t=function(a,b){if(!(this instanceof
+t))return new t(a,b);var c=[],d=function(a){(a=ec(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;e<f;e++)d(a[e]);else d(a);this.context=sa(c);b&&h.merge(this,b);this.selector={rows:null,cols:null,opts:null};t.extend(this,this,Ub)};m.Api=t;h.extend(t.prototype,{any:function(){return 0!==this.count()},concat:w.concat,context:[],count:function(){return this.flatten().length},each:function(a){for(var b=0,c=this.length;b<c;b++)a.call(this,this[b],b,this);return this},eq:function(a){var b=
+this.context;return b.length>a?new t(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)a.call(this,this[c],c,this)&&b.push(this[c]);return new t(this.context,b)},flatten:function(){var a=[];return new t(this.context,a.concat.apply(a,this.toArray()))},join:w.join,indexOf:w.indexOf||function(a,b){for(var c=b||0,d=this.length;c<d;c++)if(this[c]===a)return c;return-1},iterator:function(a,b,c,d){var e=[],f,g,h,i,n,l=this.context,
+m,p,u=this.selector;"string"===typeof a&&(d=c,c=b,b=a,a=!1);g=0;for(h=l.length;g<h;g++){var s=new t(l[g]);if("table"===b)f=c.call(s,l[g],g),f!==k&&e.push(f);else if("columns"===b||"rows"===b)f=c.call(s,l[g],this[g],g),f!==k&&e.push(f);else if("column"===b||"column-rows"===b||"row"===b||"cell"===b){p=this[g];"column-rows"===b&&(m=Da(l[g],u.opts));i=0;for(n=p.length;i<n;i++)f=p[i],f="cell"===b?c.call(s,l[g],f.row,f.column,g,i):c.call(s,l[g],f,g,i,m),f!==k&&e.push(f)}}return e.length||d?(a=new t(l,a?
+e.concat.apply([],e):e),b=a.selector,b.rows=u.rows,b.cols=u.cols,b.opts=u.opts,a):this},lastIndexOf:w.lastIndexOf||function(a,b){return this.indexOf.apply(this.toArray.reverse(),arguments)},length:0,map:function(a){var b=[];if(w.map)b=w.map.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)b.push(a.call(this,this[c],c));return new t(this.context,b)},pluck:function(a){return this.map(function(b){return b[a]})},pop:w.pop,push:w.push,reduce:w.reduce||function(a,b){return jb(this,a,b,0,this.length,
+1)},reduceRight:w.reduceRight||function(a,b){return jb(this,a,b,this.length-1,-1,-1)},reverse:w.reverse,selector:null,shift:w.shift,slice:function(){return new t(this.context,this)},sort:w.sort,splice:w.splice,toArray:function(){return w.slice.call(this)},to$:function(){return h(this)},toJQuery:function(){return h(this)},unique:function(){return new t(this.context,sa(this))},unshift:w.unshift});t.extend=function(a,b,c){if(c.length&&b&&(b instanceof t||b.__dt_wrapper)){var d,e,f,g=function(a,b,c){return function(){var d=
+b.apply(a,arguments);t.extend(d,d,c.methodExt);return d}};d=0;for(e=c.length;d<e;d++)f=c[d],b[f.name]="function"===typeof f.val?g(a,f.val,f):h.isPlainObject(f.val)?{}:f.val,b[f.name].__dt_wrapper=!0,t.extend(a,b[f.name],f.propExt)}};t.register=p=function(a,b){if(h.isArray(a))for(var c=0,d=a.length;c<d;c++)t.register(a[c],b);else for(var e=a.split("."),f=Ub,g,j,c=0,d=e.length;c<d;c++){g=(j=-1!==e[c].indexOf("()"))?e[c].replace("()",""):e[c];var i;a:{i=0;for(var n=f.length;i<n;i++)if(f[i].name===g){i=
+f[i];break a}i=null}i||(i={name:g,val:{},methodExt:[],propExt:[]},f.push(i));c===d-1?i.val=b:f=j?i.methodExt:i.propExt}};t.registerPlural=u=function(a,b,c){t.register(a,c);t.register(b,function(){var a=c.apply(this,arguments);return a===this?this:a instanceof t?a.length?h.isArray(a[0])?new t(a.context,a[0]):a[0]:k:a})};p("tables()",function(a){var b;if(a){b=t;var c=this.context;if("number"===typeof a)a=[c[a]];else var d=h.map(c,function(a){return a.nTable}),a=h(d).filter(a).map(function(){var a=h.inArray(this,
+d);return c[a]}).toArray();b=new b(a)}else b=this;return b});p("table()",function(a){var a=this.tables(a),b=a.context;return b.length?new t(b[0]):a});u("tables().nodes()","table().node()",function(){return this.iterator("table",function(a){return a.nTable},1)});u("tables().body()","table().body()",function(){return this.iterator("table",function(a){return a.nTBody},1)});u("tables().header()","table().header()",function(){return this.iterator("table",function(a){return a.nTHead},1)});u("tables().footer()",
+"table().footer()",function(){return this.iterator("table",function(a){return a.nTFoot},1)});u("tables().containers()","table().container()",function(){return this.iterator("table",function(a){return a.nTableWrapper},1)});p("draw()",function(a){return this.iterator("table",function(b){"page"===a?O(b):("string"===typeof a&&(a="full-hold"===a?!1:!0),T(b,!1===a))})});p("page()",function(a){return a===k?this.page.info().page:this.iterator("table",function(b){Va(b,a)})});p("page.info()",function(){if(0===
+this.context.length)return k;var a=this.context[0],b=a._iDisplayStart,c=a.oFeatures.bPaginate?a._iDisplayLength:-1,d=a.fnRecordsDisplay(),e=-1===c;return{page:e?0:Math.floor(b/c),pages:e?1:Math.ceil(d/c),start:b,end:a.fnDisplayEnd(),length:c,recordsTotal:a.fnRecordsTotal(),recordsDisplay:d,serverSide:"ssp"===y(a)}});p("page.len()",function(a){return a===k?0!==this.context.length?this.context[0]._iDisplayLength:k:this.iterator("table",function(b){Ta(b,a)})});var Vb=function(a,b,c){if(c){var d=new t(a);
+d.one("draw",function(){c(d.ajax.json())})}if("ssp"==y(a))T(a,b);else{C(a,!0);var e=a.jqXHR;e&&4!==e.readyState&&e.abort();ua(a,[],function(c){pa(a);for(var c=va(a,c),d=0,e=c.length;d<e;d++)N(a,c[d]);T(a,b);C(a,!1)})}};p("ajax.json()",function(){var a=this.context;if(0<a.length)return a[0].json});p("ajax.params()",function(){var a=this.context;if(0<a.length)return a[0].oAjaxData});p("ajax.reload()",function(a,b){return this.iterator("table",function(c){Vb(c,!1===b,a)})});p("ajax.url()",function(a){var b=
+this.context;if(a===k){if(0===b.length)return k;b=b[0];return b.ajax?h.isPlainObject(b.ajax)?b.ajax.url:b.ajax:b.sAjaxSource}return this.iterator("table",function(b){h.isPlainObject(b.ajax)?b.ajax.url=a:b.ajax=a})});p("ajax.url().load()",function(a,b){return this.iterator("table",function(c){Vb(c,!1===b,a)})});var bb=function(a,b,c,d,e){var f=[],g,j,i,n,l,m;i=typeof b;if(!b||"string"===i||"function"===i||b.length===k)b=[b];i=0;for(n=b.length;i<n;i++){j=b[i]&&b[i].split&&!b[i].match(/[\[\(:]/)?b[i].split(","):
+[b[i]];l=0;for(m=j.length;l<m;l++)(g=c("string"===typeof j[l]?h.trim(j[l]):j[l]))&&g.length&&(f=f.concat(g))}a=x.selector[a];if(a.length){i=0;for(n=a.length;i<n;i++)f=a[i](d,e,f)}return sa(f)},cb=function(a){a||(a={});a.filter&&a.search===k&&(a.search=a.filter);return h.extend({search:"none",order:"current",page:"all"},a)},db=function(a){for(var b=0,c=a.length;b<c;b++)if(0<a[b].length)return a[0]=a[b],a[0].length=1,a.length=1,a.context=[a.context[b]],a;a.length=0;return a},Da=function(a,b){var c,
+d,e,f=[],g=a.aiDisplay;c=a.aiDisplayMaster;var j=b.search;d=b.order;e=b.page;if("ssp"==y(a))return"removed"===j?[]:X(0,c.length);if("current"==e){c=a._iDisplayStart;for(d=a.fnDisplayEnd();c<d;c++)f.push(g[c])}else if("current"==d||"applied"==d)f="none"==j?c.slice():"applied"==j?g.slice():h.map(c,function(a){return-1===h.inArray(a,g)?a:null});else if("index"==d||"original"==d){c=0;for(d=a.aoData.length;c<d;c++)"none"==j?f.push(c):(e=h.inArray(c,g),(-1===e&&"removed"==j||0<=e&&"applied"==j)&&f.push(c))}return f};
+p("rows()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=cb(b),c=this.iterator("table",function(c){var e=b,f;return bb("row",a,function(a){var b=Qb(a);if(b!==null&&!e)return[b];f||(f=Da(c,e));if(b!==null&&h.inArray(b,f)!==-1)return[b];if(a===null||a===k||a==="")return f;if(typeof a==="function")return h.map(f,function(b){var e=c.aoData[b];return a(b,e._aData,e.nTr)?b:null});b=Tb(ja(c.aoData,f,"nTr"));if(a.nodeName){if(a._DT_RowIndex!==k)return[a._DT_RowIndex];if(a._DT_CellIndex)return[a._DT_CellIndex.row];
+b=h(a).closest("*[data-dt-row]");return b.length?[b.data("dt-row")]:[]}if(typeof a==="string"&&a.charAt(0)==="#"){var i=c.aIds[a.replace(/^#/,"")];if(i!==k)return[i.idx]}return h(b).filter(a).map(function(){return this._DT_RowIndex}).toArray()},c,e)},1);c.selector.rows=a;c.selector.opts=b;return c});p("rows().nodes()",function(){return this.iterator("row",function(a,b){return a.aoData[b].nTr||k},1)});p("rows().data()",function(){return this.iterator(!0,"rows",function(a,b){return ja(a.aoData,b,"_aData")},
+1)});u("rows().cache()","row().cache()",function(a){return this.iterator("row",function(b,c){var d=b.aoData[c];return"search"===a?d._aFilterData:d._aSortData},1)});u("rows().invalidate()","row().invalidate()",function(a){return this.iterator("row",function(b,c){da(b,c,a)})});u("rows().indexes()","row().index()",function(){return this.iterator("row",function(a,b){return b},1)});u("rows().ids()","row().id()",function(a){for(var b=[],c=this.context,d=0,e=c.length;d<e;d++)for(var f=0,g=this[d].length;f<
+g;f++){var h=c[d].rowIdFn(c[d].aoData[this[d][f]]._aData);b.push((!0===a?"#":"")+h)}return new t(c,b)});u("rows().remove()","row().remove()",function(){var a=this;this.iterator("row",function(b,c,d){var e=b.aoData,f=e[c],g,h,i,n,l;e.splice(c,1);g=0;for(h=e.length;g<h;g++)if(i=e[g],l=i.anCells,null!==i.nTr&&(i.nTr._DT_RowIndex=g),null!==l){i=0;for(n=l.length;i<n;i++)l[i]._DT_CellIndex.row=g}qa(b.aiDisplayMaster,c);qa(b.aiDisplay,c);qa(a[d],c,!1);Ua(b);c=b.rowIdFn(f._aData);c!==k&&delete b.aIds[c]});
+this.iterator("table",function(a){for(var c=0,d=a.aoData.length;c<d;c++)a.aoData[c].idx=c});return this});p("rows.add()",function(a){var b=this.iterator("table",function(b){var c,f,g,h=[];f=0;for(g=a.length;f<g;f++)c=a[f],c.nodeName&&"TR"===c.nodeName.toUpperCase()?h.push(oa(b,c)[0]):h.push(N(b,c));return h},1),c=this.rows(-1);c.pop();h.merge(c,b);return c});p("row()",function(a,b){return db(this.rows(a,b))});p("row().data()",function(a){var b=this.context;if(a===k)return b.length&&this.length?b[0].aoData[this[0]]._aData:
+k;b[0].aoData[this[0]]._aData=a;da(b[0],this[0],"data");return this});p("row().node()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]].nTr||null:null});p("row.add()",function(a){a instanceof h&&a.length&&(a=a[0]);var b=this.iterator("table",function(b){return a.nodeName&&"TR"===a.nodeName.toUpperCase()?oa(b,a)[0]:N(b,a)});return this.row(b[0])});var eb=function(a,b){var c=a.context;if(c.length&&(c=c[0].aoData[b!==k?b:a[0]])&&c._details)c._details.remove(),c._detailsShow=
+k,c._details=k},Wb=function(a,b){var c=a.context;if(c.length&&a.length){var d=c[0].aoData[a[0]];if(d._details){(d._detailsShow=b)?d._details.insertAfter(d.nTr):d._details.detach();var e=c[0],f=new t(e),g=e.aoData;f.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0<D(g,"_details").length&&(f.on("draw.dt.DT_details",function(a,b){e===b&&f.rows({page:"current"}).eq(0).each(function(a){a=g[a];a._detailsShow&&a._details.insertAfter(a.nTr)})}),f.on("column-visibility.dt.DT_details",
+function(a,b){if(e===b)for(var c,d=ba(b),f=0,h=g.length;f<h;f++)c=g[f],c._details&&c._details.children("td[colspan]").attr("colspan",d)}),f.on("destroy.dt.DT_details",function(a,b){if(e===b)for(var c=0,d=g.length;c<d;c++)g[c]._details&&eb(f,c)}))}}};p("row().child()",function(a,b){var c=this.context;if(a===k)return c.length&&this.length?c[0].aoData[this[0]]._details:k;if(!0===a)this.child.show();else if(!1===a)eb(this);else if(c.length&&this.length){var d=c[0],c=c[0].aoData[this[0]],e=[],f=function(a,
+b){if(h.isArray(a)||a instanceof h)for(var c=0,k=a.length;c<k;c++)f(a[c],b);else a.nodeName&&"tr"===a.nodeName.toLowerCase()?e.push(a):(c=h("<tr><td/></tr>").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=ba(d),e.push(c[0]))};f(a,b);c._details&&c._details.detach();c._details=h(e);c._detailsShow&&c._details.insertAfter(c.nTr)}return this});p(["row().child.show()","row().child().show()"],function(){Wb(this,!0);return this});p(["row().child.hide()","row().child().hide()"],function(){Wb(this,!1);
+return this});p(["row().child.remove()","row().child().remove()"],function(){eb(this);return this});p("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var fc=/^([^:]+):(name|visIdx|visible)$/,Xb=function(a,b,c,d,e){for(var c=[],d=0,f=e.length;d<f;d++)c.push(B(a,e[d],b));return c};p("columns()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=cb(b),c=this.iterator("table",function(c){var e=a,f=b,g=c.aoColumns,
+j=D(g,"sName"),i=D(g,"nTh");return bb("column",e,function(a){var b=Qb(a);if(a==="")return X(g.length);if(b!==null)return[b>=0?b:g.length+b];if(typeof a==="function"){var e=Da(c,f);return h.map(g,function(b,f){return a(f,Xb(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(fc):"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[$(c,b)];case "name":return h.map(j,function(a,b){return a===
+k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)},1);c.selector.cols=a;c.selector.opts=b;return c});u("columns().header()","column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});u("columns().footer()","column().footer()",
+function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});u("columns().data()","column().data()",function(){return this.iterator("column-rows",Xb,1)});u("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});u("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ja(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});u("columns().nodes()",
+"column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ja(a.aoData,e,"anCells",b)},1)});u("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData,i,n,l;if(a!==k&&g.bVisible!==a){if(a){var m=h.inArray(!0,D(f,"bVisible"),c+1);i=0;for(n=j.length;i<n;i++)l=j[i].nTr,f=j[i].anCells,l&&l.insertBefore(f[c],f[m]||null)}else h(D(b.aoData,"anCells",c)).detach();
+g.bVisible=a;fa(b,b.aoHeader);fa(b,b.aoFooter);za(b)}});a!==k&&(this.iterator("column",function(c,e){s(c,null,"column-visibility",[c,e,a,b])}),(b===k||b)&&this.columns.adjust());return c});u("columns().indexes()","column().index()",function(a){return this.iterator("column",function(b,c){return"visible"===a?aa(b,c):c},1)});p("columns.adjust()",function(){return this.iterator("table",function(a){Z(a)},1)});p("column.index()",function(a,b){if(0!==this.context.length){var c=this.context[0];if("fromVisible"===
+a||"toData"===a)return $(c,b);if("fromData"===a||"toVisible"===a)return aa(c,b)}});p("column()",function(a,b){return db(this.columns(a,b))});p("cells()",function(a,b,c){h.isPlainObject(a)&&(a.row===k?(c=a,a=null):(c=b,b=null));h.isPlainObject(b)&&(c=b,b=null);if(null===b||b===k)return this.iterator("table",function(b){var d=a,e=cb(c),f=b.aoData,g=Da(b,e),j=Tb(ja(f,g,"anCells")),i=h([].concat.apply([],j)),l,n=b.aoColumns.length,m,p,u,t,s,v;return bb("cell",d,function(a){var c=typeof a==="function";
+if(a===null||a===k||c){m=[];p=0;for(u=g.length;p<u;p++){l=g[p];for(t=0;t<n;t++){s={row:l,column:t};if(c){v=f[l];a(s,B(b,l,t),v.anCells?v.anCells[t]:null)&&m.push(s)}else m.push(s)}}return m}if(h.isPlainObject(a))return[a];c=i.filter(a).map(function(a,b){return{row:b._DT_CellIndex.row,column:b._DT_CellIndex.column}}).toArray();if(c.length||!a.nodeName)return c;v=h(a).closest("*[data-dt-row]");return v.length?[{row:v.data("dt-row"),column:v.data("dt-column")}]:[]},b,e)});var d=this.columns(b,c),e=this.rows(a,
+c),f,g,j,i,n,l=this.iterator("table",function(a,b){f=[];g=0;for(j=e[b].length;g<j;g++){i=0;for(n=d[b].length;i<n;i++)f.push({row:e[b][g],column:d[b][i]})}return f},1);h.extend(l.selector,{cols:b,rows:a,opts:c});return l});u("cells().nodes()","cell().node()",function(){return this.iterator("cell",function(a,b,c){return(a=a.aoData[b])&&a.anCells?a.anCells[c]:k},1)});p("cells().data()",function(){return this.iterator("cell",function(a,b,c){return B(a,b,c)},1)});u("cells().cache()","cell().cache()",function(a){a=
+"search"===a?"_aFilterData":"_aSortData";return this.iterator("cell",function(b,c,d){return b.aoData[c][a][d]},1)});u("cells().render()","cell().render()",function(a){return this.iterator("cell",function(b,c,d){return B(b,c,d,a)},1)});u("cells().indexes()","cell().index()",function(){return this.iterator("cell",function(a,b,c){return{row:b,column:c,columnVisible:aa(a,c)}},1)});u("cells().invalidate()","cell().invalidate()",function(a){return this.iterator("cell",function(b,c,d){da(b,c,a,d)})});p("cell()",
+function(a,b,c){return db(this.cells(a,b,c))});p("cell().data()",function(a){var b=this.context,c=this[0];if(a===k)return b.length&&c.length?B(b[0],c[0].row,c[0].column):k;lb(b[0],c[0].row,c[0].column,a);da(b[0],c[0].row,"data",c[0].column);return this});p("order()",function(a,b){var c=this.context;if(a===k)return 0!==c.length?c[0].aaSorting:k;"number"===typeof a?a=[[a,b]]:a.length&&!h.isArray(a[0])&&(a=Array.prototype.slice.call(arguments));return this.iterator("table",function(b){b.aaSorting=a.slice()})});
+p("order.listener()",function(a,b,c){return this.iterator("table",function(d){Oa(d,a,b,c)})});p("order.fixed()",function(a){if(!a){var b=this.context,b=b.length?b[0].aaSortingFixed:k;return h.isArray(b)?{pre:b}:b}return this.iterator("table",function(b){b.aaSortingFixed=h.extend(!0,{},a)})});p(["columns().order()","column().order()"],function(a){var b=this;return this.iterator("table",function(c,d){var e=[];h.each(b[d],function(b,c){e.push([c,a])});c.aaSorting=e})});p("search()",function(a,b,c,d){var e=
+this.context;return a===k?0!==e.length?e[0].oPreviousSearch.sSearch:k:this.iterator("table",function(e){e.oFeatures.bFilter&&ga(e,h.extend({},e.oPreviousSearch,{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),1)})});u("columns().search()","column().search()",function(a,b,c,d){return this.iterator("column",function(e,f){var g=e.aoPreSearchCols;if(a===k)return g[f].sSearch;e.oFeatures.bFilter&&(h.extend(g[f],{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?
+!0:c,bCaseInsensitive:null===d?!0:d}),ga(e,e.oPreviousSearch,1))})});p("state()",function(){return this.context.length?this.context[0].oSavedState:null});p("state.clear()",function(){return this.iterator("table",function(a){a.fnStateSaveCallback.call(a.oInstance,a,{})})});p("state.loaded()",function(){return this.context.length?this.context[0].oLoadedState:null});p("state.save()",function(){return this.iterator("table",function(a){za(a)})});m.versionCheck=m.fnVersionCheck=function(a){for(var b=m.version.split("."),
+a=a.split("."),c,d,e=0,f=a.length;e<f;e++)if(c=parseInt(b[e],10)||0,d=parseInt(a[e],10)||0,c!==d)return c>d;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;if(a instanceof m.Api)return!0;h.each(m.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?h("table",e.nScrollFoot)[0]:null;if(e.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(m.settings,
+function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new t(c):c};m.camelToHungarian=J;p("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){p(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0]=h.map(a[0].split(/\s/),function(a){return!a.match(/\.dt\b/)?a+".dt":a}).join(" ");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});p("clear()",function(){return this.iterator("table",
+function(a){pa(a)})});p("settings()",function(){return new t(this.context,this.context)});p("init()",function(){var a=this.context;return a.length?a[0].oInit:null});p("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});p("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),
+p;b.bDestroying=!0;s(b,"aoDestroyCallback","destroy",[b]);a||(new t(b)).columns().visible(!0);k.off(".DT").find(":not(tbody *)").off(".DT");h(E).off(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));b.aaSorting=[];b.aaSortingFixed=[];ya(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);b.bJUI&&(h("th span."+d.sSortIcon+
+", td span."+d.sSortIcon,g).detach(),h("th, td",g).each(function(){var a=h("div."+d.sSortJUIWrapper,this);h(this).append(a.contents());a.detach()}));f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%p])}));c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c,1)})});h.each(["column",
+"row","cell"],function(a,b){p(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,m){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,m)})})});p("i18n()",function(a,b,c){var d=this.context[0],a=R(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.15";m.settings=[];m.models={};m.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow={nTr:null,anCells:null,
+_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};m.defaults=
+{aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,
+this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+
+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",
+sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};
+Y(m.defaults);m.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};Y(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,
+bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],
+aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,
+aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=
+this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};m.ext=x={buttons:{},classes:{},build:"dt/dt-1.10.15/cr-1.3.3/fc-3.2.2/fh-3.1.2/r-2.1.1",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},
+header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",
+sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",
+sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Ea="",Ea="",G=Ea+"ui-state-default",ka=Ea+"css_right ui-icon ui-icon-",Yb=Ea+"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix";h.extend(m.ext.oJUIClasses,
+m.ext.classes,{sPageButton:"fg-button ui-button "+G,sPageButtonActive:"ui-state-disabled",sPageButtonDisabled:"ui-state-disabled",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",sSortAsc:G+" sorting_asc",sSortDesc:G+" sorting_desc",sSortable:G+" sorting",sSortableAsc:G+" sorting_asc_disabled",sSortableDesc:G+" sorting_desc_disabled",sSortableNone:G+" sorting_disabled",sSortJUIAsc:ka+"triangle-1-n",sSortJUIDesc:ka+"triangle-1-s",sSortJUI:ka+"carat-2-n-s",
+sSortJUIAscAllowed:ka+"carat-1-n",sSortJUIDescAllowed:ka+"carat-1-s",sSortJUIWrapper:"DataTables_sort_wrapper",sSortIcon:"DataTables_sort_icon",sScrollHead:"dataTables_scrollHead "+G,sScrollFoot:"dataTables_scrollFoot "+G,sHeaderTH:G,sFooterTH:G,sJUIHeader:Yb+" ui-corner-tl ui-corner-tr",sJUIFooter:Yb+" ui-corner-bl ui-corner-br"});var Nb=m.ext.pager;h.extend(Nb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ia(a,
+b)]},simple_numbers:function(a,b){return["previous",ia(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ia(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ia(a,b),"last"]},_numbers:ia,numbers_length:7});h.extend(!0,m.ext.renderer,{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},m,l,p=0,r=function(b,d){var k,t,u,s,v=function(b){Va(a,b.data.action,true)};k=0;for(t=d.length;k<t;k++){s=d[k];if(h.isArray(s)){u=
+h("<"+(s.DT_el||"div")+"/>").appendTo(b);r(u,s)}else{m=null;l="";switch(s){case "ellipsis":b.append('<span class="ellipsis">&#x2026;</span>');break;case "first":m=j.sFirst;l=s+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":m=j.sPrevious;l=s+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":m=j.sNext;l=s+(e<f-1?"":" "+g.sPageButtonDisabled);break;case "last":m=j.sLast;l=s+(e<f-1?"":" "+g.sPageButtonDisabled);break;default:m=s+1;l=e===s?g.sPageButtonActive:""}if(m!==null){u=h("<a>",{"class":g.sPageButton+
+" "+l,"aria-controls":a.sTableId,"aria-label":i[s],"data-dt-idx":p,tabindex:a.iTabIndex,id:c===0&&typeof s==="string"?a.sTableId+"_"+s:null}).html(m).appendTo(b);Ya(u,{action:s},v);p++}}}},t;try{t=h(b).find(H.activeElement).data("dt-idx")}catch(u){}r(h(b).empty(),d);t!==k&&h(b).find("[data-dt-idx="+t+"]").focus()}}});h.extend(m.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return ab(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&!cc.test(a))return null;var b=Date.parse(a);
+return null!==b&&!isNaN(b)||M(a)?"date":null},function(a,b){var c=b.oLanguage.sDecimal;return ab(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Sb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Sb(a,c,!0)?"html-num-fmt"+c:null},function(a){return M(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(m.ext.type.search,{html:function(a){return M(a)?a:"string"===typeof a?a.replace(Pb," ").replace(Ca,""):""},string:function(a){return M(a)?
+a:"string"===typeof a?a.replace(Pb," "):a}});var Ba=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Rb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(x.type.order,{"date-pre":function(a){return Date.parse(a)||-Infinity},"html-pre":function(a){return M(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return M(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return a<
+b?-1:a>b?1:0},"string-desc":function(a,b){return a<b?1:a>b?-1:0}});fb("");h.extend(!0,m.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("<div/>").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("<span/>").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);
+h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var Zb=function(a){return"string"===typeof a?a.replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,
+"&quot;"):a};m.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return Zb(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}},text:function(){return{display:Zb}}};h.extend(m.ext.internal,{_fnExternApiFunc:Ob,_fnBuildAjax:ua,_fnAjaxUpdate:nb,_fnAjaxParameters:wb,_fnAjaxUpdateDraw:xb,
+_fnAjaxDataSrc:va,_fnAddColumn:Ga,_fnColumnOptions:la,_fnAdjustColumnSizing:Z,_fnVisibleToColumnIndex:$,_fnColumnIndexToVisible:aa,_fnVisbleColumns:ba,_fnGetColumns:na,_fnColumnTypes:Ia,_fnApplyColumnDefs:kb,_fnHungarianMap:Y,_fnCamelToHungarian:J,_fnLanguageCompat:Fa,_fnBrowserDetect:ib,_fnAddData:N,_fnAddTr:oa,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:lb,
+_fnSplitObjNotation:La,_fnGetObjectDataFn:R,_fnSetObjectDataFn:S,_fnGetDataMaster:Ma,_fnClearTable:pa,_fnDeleteIndex:qa,_fnInvalidate:da,_fnGetRowElements:Ka,_fnCreateTr:Ja,_fnBuildHead:mb,_fnDrawHead:fa,_fnDraw:O,_fnReDraw:T,_fnAddOptionsHtml:pb,_fnDetectHeader:ea,_fnGetUniqueThs:ta,_fnFeatureHtmlFilter:rb,_fnFilterComplete:ga,_fnFilterCustom:Ab,_fnFilterColumn:zb,_fnFilter:yb,_fnFilterCreateSearch:Ra,_fnEscapeRegex:Sa,_fnFilterData:Bb,_fnFeatureHtmlInfo:ub,_fnUpdateInfo:Eb,_fnInfoMacros:Fb,_fnInitialise:ha,
+_fnInitComplete:wa,_fnLengthChange:Ta,_fnFeatureHtmlLength:qb,_fnFeatureHtmlPaginate:vb,_fnPageChange:Va,_fnFeatureHtmlProcessing:sb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:tb,_fnScrollDraw:ma,_fnApplyToChildren:I,_fnCalculateColumnWidths:Ha,_fnThrottle:Qa,_fnConvertToWidth:Gb,_fnGetWidestNode:Hb,_fnGetMaxLenString:Ib,_fnStringToCss:v,_fnSortFlatten:W,_fnSort:ob,_fnSortAria:Kb,_fnSortListener:Xa,_fnSortAttachListener:Oa,_fnSortingClasses:ya,_fnSortData:Jb,_fnSaveState:za,_fnLoadState:Lb,_fnSettingsFromNode:Aa,
+_fnLog:K,_fnMap:F,_fnBindAction:Ya,_fnCallbackReg:z,_fnCallbackFire:s,_fnLengthOverflow:Ua,_fnRenderer:Pa,_fnDataSource:y,_fnRowAttributes:Na,_fnCalculateEnd:function(){}});h.fn.dataTable=m;m.$=h;h.fn.dataTableSettings=m.settings;h.fn.dataTableExt=m.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(m,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable});
+
+
+/*!
+ ColReorder 1.3.3
+ ©2010-2015 SpryMedia Ltd - datatables.net/license
+*/
+(function(f){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(o){return f(o,window,document)}):"object"===typeof exports?module.exports=function(o,l){o||(o=window);if(!l||!l.fn.dataTable)l=require("datatables.net")(o,l).$;return f(l,o,o.document)}:f(jQuery,window,document)})(function(f,o,l,r){function q(a){for(var b=[],d=0,e=a.length;d<e;d++)b[a[d]]=d;return b}function p(a,b,d){b=a.splice(b,1)[0];a.splice(d,0,b)}function s(a,b,d){for(var e=[],f=0,c=a.childNodes.length;f<
+c;f++)1==a.childNodes[f].nodeType&&e.push(a.childNodes[f]);b=e[b];null!==d?a.insertBefore(b,e[d]):a.appendChild(b)}var t=f.fn.dataTable;f.fn.dataTableExt.oApi.fnColReorder=function(a,b,d,e,g){var c,h,j,m,i,l=a.aoColumns.length,k;i=function(a,b,c){if(a[b]&&"function"!==typeof a[b]){var d=a[b].split("."),e=d.shift();isNaN(1*e)||(a[b]=c[1*e]+"."+d.join("."))}};if(b!=d)if(0>b||b>=l)this.oApi._fnLog(a,1,"ColReorder 'from' index is out of bounds: "+b);else if(0>d||d>=l)this.oApi._fnLog(a,1,"ColReorder 'to' index is out of bounds: "+
+d);else{j=[];c=0;for(h=l;c<h;c++)j[c]=c;p(j,b,d);var n=q(j);c=0;for(h=a.aaSorting.length;c<h;c++)a.aaSorting[c][0]=n[a.aaSorting[c][0]];if(null!==a.aaSortingFixed){c=0;for(h=a.aaSortingFixed.length;c<h;c++)a.aaSortingFixed[c][0]=n[a.aaSortingFixed[c][0]]}c=0;for(h=l;c<h;c++){k=a.aoColumns[c];j=0;for(m=k.aDataSort.length;j<m;j++)k.aDataSort[j]=n[k.aDataSort[j]];k.idx=n[k.idx]}f.each(a.aLastSort,function(b,c){a.aLastSort[b].src=n[c.src]});c=0;for(h=l;c<h;c++)k=a.aoColumns[c],"number"==typeof k.mData?
+k.mData=n[k.mData]:f.isPlainObject(k.mData)&&(i(k.mData,"_",n),i(k.mData,"filter",n),i(k.mData,"sort",n),i(k.mData,"type",n));if(a.aoColumns[b].bVisible){i=this.oApi._fnColumnIndexToVisible(a,b);m=null;for(c=d<b?d:d+1;null===m&&c<l;)m=this.oApi._fnColumnIndexToVisible(a,c),c++;j=a.nTHead.getElementsByTagName("tr");c=0;for(h=j.length;c<h;c++)s(j[c],i,m);if(null!==a.nTFoot){j=a.nTFoot.getElementsByTagName("tr");c=0;for(h=j.length;c<h;c++)s(j[c],i,m)}c=0;for(h=a.aoData.length;c<h;c++)null!==a.aoData[c].nTr&&
+s(a.aoData[c].nTr,i,m)}p(a.aoColumns,b,d);c=0;for(h=l;c<h;c++)a.oApi._fnColumnOptions(a,c,{});p(a.aoPreSearchCols,b,d);c=0;for(h=a.aoData.length;c<h;c++){m=a.aoData[c];if(k=m.anCells){p(k,b,d);j=0;for(i=k.length;j<i;j++)k[j]&&k[j]._DT_CellIndex&&(k[j]._DT_CellIndex.column=j)}"dom"!==m.src&&f.isArray(m._aData)&&p(m._aData,b,d)}c=0;for(h=a.aoHeader.length;c<h;c++)p(a.aoHeader[c],b,d);if(null!==a.aoFooter){c=0;for(h=a.aoFooter.length;c<h;c++)p(a.aoFooter[c],b,d)}(g||g===r)&&f.fn.dataTable.Api(a).rows().invalidate();
+c=0;for(h=l;c<h;c++)f(a.aoColumns[c].nTh).off("click.DT"),this.oApi._fnSortAttachListener(a,a.aoColumns[c].nTh,c);f(a.oInstance).trigger("column-reorder.dt",[a,{from:b,to:d,mapping:n,drop:e,iFrom:b,iTo:d,aiInvertMapping:n}])}};var i=function(a,b){var d=(new f.fn.dataTable.Api(a)).settings()[0];if(d._colReorder)return d._colReorder;!0===b&&(b={});var e=f.fn.dataTable.camelToHungarian;e&&(e(i.defaults,i.defaults,!0),e(i.defaults,b||{}));this.s={dt:null,init:f.extend(!0,{},i.defaults,b),fixed:0,fixedRight:0,
+reorderCallback:null,mouse:{startX:-1,startY:-1,offsetX:-1,offsetY:-1,target:-1,targetIndex:-1,fromIndex:-1},aoTargets:[]};this.dom={drag:null,pointer:null};this.s.dt=d;this.s.dt._colReorder=this;this._fnConstruct();return this};f.extend(i.prototype,{fnReset:function(){this._fnOrderColumns(this.fnOrder());return this},fnGetCurrentOrder:function(){return this.fnOrder()},fnOrder:function(a,b){var d=[],e,g,c=this.s.dt.aoColumns;if(a===r){e=0;for(g=c.length;e<g;e++)d.push(c[e]._ColReorder_iOrigCol);return d}if(b){c=
+this.fnOrder();e=0;for(g=a.length;e<g;e++)d.push(f.inArray(a[e],c));a=d}this._fnOrderColumns(q(a));return this},fnTranspose:function(a,b){b||(b="toCurrent");var d=this.fnOrder(),e=this.s.dt.aoColumns;return"toCurrent"===b?!f.isArray(a)?f.inArray(a,d):f.map(a,function(a){return f.inArray(a,d)}):!f.isArray(a)?e[a]._ColReorder_iOrigCol:f.map(a,function(a){return e[a]._ColReorder_iOrigCol})},_fnConstruct:function(){var a=this,b=this.s.dt.aoColumns.length,d=this.s.dt.nTable,e;this.s.init.iFixedColumns&&
+(this.s.fixed=this.s.init.iFixedColumns);this.s.init.iFixedColumnsLeft&&(this.s.fixed=this.s.init.iFixedColumnsLeft);this.s.fixedRight=this.s.init.iFixedColumnsRight?this.s.init.iFixedColumnsRight:0;this.s.init.fnReorderCallback&&(this.s.reorderCallback=this.s.init.fnReorderCallback);for(e=0;e<b;e++)e>this.s.fixed-1&&e<b-this.s.fixedRight&&this._fnMouseListener(e,this.s.dt.aoColumns[e].nTh),this.s.dt.aoColumns[e]._ColReorder_iOrigCol=e;this.s.dt.oApi._fnCallbackReg(this.s.dt,"aoStateSaveParams",function(b,
+c){a._fnStateSave.call(a,c)},"ColReorder_State");var g=null;this.s.init.aiOrder&&(g=this.s.init.aiOrder.slice());this.s.dt.oLoadedState&&("undefined"!=typeof this.s.dt.oLoadedState.ColReorder&&this.s.dt.oLoadedState.ColReorder.length==this.s.dt.aoColumns.length)&&(g=this.s.dt.oLoadedState.ColReorder);if(g)if(a.s.dt._bInitComplete)b=q(g),a._fnOrderColumns.call(a,b);else{var c=!1;f(d).on("draw.dt.colReorder",function(){if(!a.s.dt._bInitComplete&&!c){c=true;var b=q(g);a._fnOrderColumns.call(a,b)}})}else this._fnSetColumnIndexes();
+f(d).on("destroy.dt.colReorder",function(){f(d).off("destroy.dt.colReorder draw.dt.colReorder");f(a.s.dt.nTHead).find("*").off(".ColReorder");f.each(a.s.dt.aoColumns,function(a,b){f(b.nTh).removeAttr("data-column-index")});a.s.dt._colReorder=null;a.s=null})},_fnOrderColumns:function(a){var b=!1;if(a.length!=this.s.dt.aoColumns.length)this.s.dt.oInstance.oApi._fnLog(this.s.dt,1,"ColReorder - array reorder does not match known number of columns. Skipping.");else{for(var d=0,e=a.length;d<e;d++){var g=
+f.inArray(d,a);d!=g&&(p(a,g,d),this.s.dt.oInstance.fnColReorder(g,d,!0,!1),b=!0)}f.fn.dataTable.Api(this.s.dt).rows().invalidate();this._fnSetColumnIndexes();b&&((""!==this.s.dt.oScroll.sX||""!==this.s.dt.oScroll.sY)&&this.s.dt.oInstance.fnAdjustColumnSizing(!1),this.s.dt.oInstance.oApi._fnSaveState(this.s.dt),null!==this.s.reorderCallback&&this.s.reorderCallback.call(this))}},_fnStateSave:function(a){var b,d,e,g=this.s.dt.aoColumns;a.ColReorder=[];if(a.aaSorting){for(b=0;b<a.aaSorting.length;b++)a.aaSorting[b][0]=
+g[a.aaSorting[b][0]]._ColReorder_iOrigCol;var c=f.extend(!0,[],a.aoSearchCols);b=0;for(d=g.length;b<d;b++)e=g[b]._ColReorder_iOrigCol,a.aoSearchCols[e]=c[b],a.abVisCols[e]=g[b].bVisible,a.ColReorder.push(e)}else if(a.order){for(b=0;b<a.order.length;b++)a.order[b][0]=g[a.order[b][0]]._ColReorder_iOrigCol;c=f.extend(!0,[],a.columns);b=0;for(d=g.length;b<d;b++)e=g[b]._ColReorder_iOrigCol,a.columns[e]=c[b],a.ColReorder.push(e)}},_fnMouseListener:function(a,b){var d=this;f(b).on("mousedown.ColReorder",
+function(a){a.preventDefault();d._fnMouseDown.call(d,a,b)}).on("touchstart.ColReorder",function(a){d._fnMouseDown.call(d,a,b)})},_fnMouseDown:function(a,b){var d=this,e=f(a.target).closest("th, td").offset(),g=parseInt(f(b).attr("data-column-index"),10);g!==r&&(this.s.mouse.startX=this._fnCursorPosition(a,"pageX"),this.s.mouse.startY=this._fnCursorPosition(a,"pageY"),this.s.mouse.offsetX=this._fnCursorPosition(a,"pageX")-e.left,this.s.mouse.offsetY=this._fnCursorPosition(a,"pageY")-e.top,this.s.mouse.target=
+this.s.dt.aoColumns[g].nTh,this.s.mouse.targetIndex=g,this.s.mouse.fromIndex=g,this._fnRegions(),f(l).on("mousemove.ColReorder touchmove.ColReorder",function(a){d._fnMouseMove.call(d,a)}).on("mouseup.ColReorder touchend.ColReorder",function(a){d._fnMouseUp.call(d,a)}))},_fnMouseMove:function(a){if(null===this.dom.drag){if(5>Math.pow(Math.pow(this._fnCursorPosition(a,"pageX")-this.s.mouse.startX,2)+Math.pow(this._fnCursorPosition(a,"pageY")-this.s.mouse.startY,2),0.5))return;this._fnCreateDragNode()}this.dom.drag.css({left:this._fnCursorPosition(a,
+"pageX")-this.s.mouse.offsetX,top:this._fnCursorPosition(a,"pageY")-this.s.mouse.offsetY});for(var b=!1,d=this.s.mouse.toIndex,e=1,f=this.s.aoTargets.length;e<f;e++)if(this._fnCursorPosition(a,"pageX")<this.s.aoTargets[e-1].x+(this.s.aoTargets[e].x-this.s.aoTargets[e-1].x)/2){this.dom.pointer.css("left",this.s.aoTargets[e-1].x);this.s.mouse.toIndex=this.s.aoTargets[e-1].to;b=!0;break}b||(this.dom.pointer.css("left",this.s.aoTargets[this.s.aoTargets.length-1].x),this.s.mouse.toIndex=this.s.aoTargets[this.s.aoTargets.length-
+1].to);this.s.init.bRealtime&&d!==this.s.mouse.toIndex&&(this.s.dt.oInstance.fnColReorder(this.s.mouse.fromIndex,this.s.mouse.toIndex,!1),this.s.mouse.fromIndex=this.s.mouse.toIndex,this._fnRegions())},_fnMouseUp:function(){f(l).off(".ColReorder");null!==this.dom.drag&&(this.dom.drag.remove(),this.dom.pointer.remove(),this.dom.drag=null,this.dom.pointer=null,this.s.dt.oInstance.fnColReorder(this.s.mouse.fromIndex,this.s.mouse.toIndex,!0),this._fnSetColumnIndexes(),(""!==this.s.dt.oScroll.sX||""!==
+this.s.dt.oScroll.sY)&&this.s.dt.oInstance.fnAdjustColumnSizing(!1),this.s.dt.oInstance.oApi._fnSaveState(this.s.dt),null!==this.s.reorderCallback&&this.s.reorderCallback.call(this))},_fnRegions:function(){var a=this.s.dt.aoColumns;this.s.aoTargets.splice(0,this.s.aoTargets.length);this.s.aoTargets.push({x:f(this.s.dt.nTable).offset().left,to:0});for(var b=0,d=this.s.aoTargets[0].x,e=0,g=a.length;e<g;e++)e!=this.s.mouse.fromIndex&&b++,a[e].bVisible&&"none"!==a[e].nTh.style.display&&(d+=f(a[e].nTh).outerWidth(),
+this.s.aoTargets.push({x:d,to:b}));0!==this.s.fixedRight&&this.s.aoTargets.splice(this.s.aoTargets.length-this.s.fixedRight);0!==this.s.fixed&&this.s.aoTargets.splice(0,this.s.fixed)},_fnCreateDragNode:function(){var a=""!==this.s.dt.oScroll.sX||""!==this.s.dt.oScroll.sY,b=this.s.dt.aoColumns[this.s.mouse.targetIndex].nTh,d=b.parentNode,e=d.parentNode,g=e.parentNode,c=f(b).clone();this.dom.drag=f(g.cloneNode(!1)).addClass("DTCR_clonedTable").append(f(e.cloneNode(!1)).append(f(d.cloneNode(!1)).append(c[0]))).css({position:"absolute",
+top:0,left:0,width:f(b).outerWidth(),height:f(b).outerHeight()}).appendTo("body");this.dom.pointer=f("<div></div>").addClass("DTCR_pointer").css({position:"absolute",top:a?f("div.dataTables_scroll",this.s.dt.nTableWrapper).offset().top:f(this.s.dt.nTable).offset().top,height:a?f("div.dataTables_scroll",this.s.dt.nTableWrapper).height():f(this.s.dt.nTable).height()}).appendTo("body")},_fnSetColumnIndexes:function(){f.each(this.s.dt.aoColumns,function(a,b){f(b.nTh).attr("data-column-index",a)})},_fnCursorPosition:function(a,
+b){return-1!==a.type.indexOf("touch")?a.originalEvent.touches[0][b]:a[b]}});i.defaults={aiOrder:null,bRealtime:!0,iFixedColumnsLeft:0,iFixedColumnsRight:0,fnReorderCallback:null};i.version="1.3.3";f.fn.dataTable.ColReorder=i;f.fn.DataTable.ColReorder=i;"function"==typeof f.fn.dataTable&&"function"==typeof f.fn.dataTableExt.fnVersionCheck&&f.fn.dataTableExt.fnVersionCheck("1.10.8")?f.fn.dataTableExt.aoFeatures.push({fnInit:function(a){var b=a.oInstance;a._colReorder?b.oApi._fnLog(a,1,"ColReorder attempted to initialise twice. Ignoring second"):
+(b=a.oInit,new i(a,b.colReorder||b.oColReorder||{}));return null},cFeature:"R",sFeature:"ColReorder"}):alert("Warning: ColReorder requires DataTables 1.10.8 or greater - www.datatables.net/download");f(l).on("preInit.dt.colReorder",function(a,b){if("dt"===a.namespace){var d=b.oInit.colReorder,e=t.defaults.colReorder;if(d||e)e=f.extend({},d,e),!1!==d&&new i(b,e)}});f.fn.dataTable.Api.register("colReorder.reset()",function(){return this.iterator("table",function(a){a._colReorder.fnReset()})});f.fn.dataTable.Api.register("colReorder.order()",
+function(a,b){return a?this.iterator("table",function(d){d._colReorder.fnOrder(a,b)}):this.context.length?this.context[0]._colReorder.fnOrder():null});f.fn.dataTable.Api.register("colReorder.transpose()",function(a,b){return this.context.length&&this.context[0]._colReorder?this.context[0]._colReorder.fnTranspose(a,b):a});return i});
+
+
+/*!
+ FixedColumns 3.2.2
+ ©2010-2016 SpryMedia Ltd - datatables.net/license
+*/
+(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(q){return d(q,window,document)}):"object"===typeof exports?module.exports=function(q,r){q||(q=window);if(!r||!r.fn.dataTable)r=require("datatables.net")(q,r).$;return d(r,q,q.document)}:d(jQuery,window,document)})(function(d,q,r,t){var s=d.fn.dataTable,u,m=function(a,b){var c=this;if(this instanceof m){if(b===t||!0===b)b={};var e=d.fn.dataTable.camelToHungarian;e&&(e(m.defaults,m.defaults,!0),e(m.defaults,
+b));e=(new d.fn.dataTable.Api(a)).settings()[0];this.s={dt:e,iTableColumns:e.aoColumns.length,aiOuterWidths:[],aiInnerWidths:[],rtl:"rtl"===d(e.nTable).css("direction")};this.dom={scroller:null,header:null,body:null,footer:null,grid:{wrapper:null,dt:null,left:{wrapper:null,head:null,body:null,foot:null},right:{wrapper:null,head:null,body:null,foot:null}},clone:{left:{header:null,body:null,footer:null},right:{header:null,body:null,footer:null}}};if(e._oFixedColumns)throw"FixedColumns already initialised on this table";
+e._oFixedColumns=this;e._bInitComplete?this._fnConstruct(b):e.oApi._fnCallbackReg(e,"aoInitComplete",function(){c._fnConstruct(b)},"FixedColumns")}else alert("FixedColumns warning: FixedColumns must be initialised with the 'new' keyword.")};d.extend(m.prototype,{fnUpdate:function(){this._fnDraw(!0)},fnRedrawLayout:function(){this._fnColCalc();this._fnGridLayout();this.fnUpdate()},fnRecalculateHeight:function(a){delete a._DTTC_iHeight;a.style.height="auto"},fnSetRowHeight:function(a,b){a.style.height=
+b+"px"},fnGetPosition:function(a){var b=this.s.dt.oInstance;if(d(a).parents(".DTFC_Cloned").length){if("tr"===a.nodeName.toLowerCase())return a=d(a).index(),b.fnGetPosition(d("tr",this.s.dt.nTBody)[a]);var c=d(a).index(),a=d(a.parentNode).index();return[b.fnGetPosition(d("tr",this.s.dt.nTBody)[a]),c,b.oApi._fnVisibleToColumnIndex(this.s.dt,c)]}return b.fnGetPosition(a)},_fnConstruct:function(a){var b=this;if("function"!=typeof this.s.dt.oInstance.fnVersionCheck||!0!==this.s.dt.oInstance.fnVersionCheck("1.8.0"))alert("FixedColumns "+
+m.VERSION+" required DataTables 1.8.0 or later. Please upgrade your DataTables installation");else if(""===this.s.dt.oScroll.sX)this.s.dt.oInstance.oApi._fnLog(this.s.dt,1,"FixedColumns is not needed (no x-scrolling in DataTables enabled), so no action will be taken. Use 'FixedHeader' for column fixing when scrolling is not enabled");else{this.s=d.extend(!0,this.s,m.defaults,a);a=this.s.dt.oClasses;this.dom.grid.dt=d(this.s.dt.nTable).parents("div."+a.sScrollWrapper)[0];this.dom.scroller=d("div."+
+a.sScrollBody,this.dom.grid.dt)[0];this._fnColCalc();this._fnGridSetup();var c,e=!1;d(this.s.dt.nTableWrapper).on("mousedown.DTFC",function(){e=!0;d(r).one("mouseup",function(){e=!1})});d(this.dom.scroller).on("mouseover.DTFC touchstart.DTFC",function(){e||(c="main")}).on("scroll.DTFC",function(a){!c&&a.originalEvent&&(c="main");if("main"===c&&(0<b.s.iLeftColumns&&(b.dom.grid.left.liner.scrollTop=b.dom.scroller.scrollTop),0<b.s.iRightColumns))b.dom.grid.right.liner.scrollTop=b.dom.scroller.scrollTop});
+var f="onwheel"in r.createElement("div")?"wheel.DTFC":"mousewheel.DTFC";if(0<b.s.iLeftColumns)d(b.dom.grid.left.liner).on("mouseover.DTFC touchstart.DTFC",function(){e||(c="left")}).on("scroll.DTFC",function(a){!c&&a.originalEvent&&(c="left");"left"===c&&(b.dom.scroller.scrollTop=b.dom.grid.left.liner.scrollTop,0<b.s.iRightColumns&&(b.dom.grid.right.liner.scrollTop=b.dom.grid.left.liner.scrollTop))}).on(f,function(a){b.dom.scroller.scrollLeft-="wheel"===a.type?-a.originalEvent.deltaX:a.originalEvent.wheelDeltaX});
+if(0<b.s.iRightColumns)d(b.dom.grid.right.liner).on("mouseover.DTFC touchstart.DTFC",function(){e||(c="right")}).on("scroll.DTFC",function(a){!c&&a.originalEvent&&(c="right");"right"===c&&(b.dom.scroller.scrollTop=b.dom.grid.right.liner.scrollTop,0<b.s.iLeftColumns&&(b.dom.grid.left.liner.scrollTop=b.dom.grid.right.liner.scrollTop))}).on(f,function(a){b.dom.scroller.scrollLeft-="wheel"===a.type?-a.originalEvent.deltaX:a.originalEvent.wheelDeltaX});d(q).on("resize.DTFC",function(){b._fnGridLayout.call(b)});
+var g=!0,h=d(this.s.dt.nTable);h.on("draw.dt.DTFC",function(){b._fnColCalc();b._fnDraw.call(b,g);g=!1}).on("column-sizing.dt.DTFC",function(){b._fnColCalc();b._fnGridLayout(b)}).on("column-visibility.dt.DTFC",function(a,c,d,e,f){if(f===t||f)b._fnColCalc(),b._fnGridLayout(b),b._fnDraw(!0)}).on("select.dt.DTFC deselect.dt.DTFC",function(a){"dt"===a.namespace&&b._fnDraw(!1)}).on("destroy.dt.DTFC",function(){h.off(".DTFC");d(b.dom.scroller).off(".DTFC");d(q).off(".DTFC");d(b.s.dt.nTableWrapper).off(".DTFC");
+d(b.dom.grid.left.liner).off(".DTFC "+f);d(b.dom.grid.left.wrapper).remove();d(b.dom.grid.right.liner).off(".DTFC "+f);d(b.dom.grid.right.wrapper).remove()});this._fnGridLayout();this.s.dt.oInstance.fnDraw(!1)}},_fnColCalc:function(){var a=this,b=0,c=0;this.s.aiInnerWidths=[];this.s.aiOuterWidths=[];d.each(this.s.dt.aoColumns,function(e,f){var g=d(f.nTh),h;if(g.filter(":visible").length){var i=g.outerWidth();0===a.s.aiOuterWidths.length&&(h=d(a.s.dt.nTable).css("border-left-width"),i+="string"===
+typeof h?1:parseInt(h,10));a.s.aiOuterWidths.length===a.s.dt.aoColumns.length-1&&(h=d(a.s.dt.nTable).css("border-right-width"),i+="string"===typeof h?1:parseInt(h,10));a.s.aiOuterWidths.push(i);a.s.aiInnerWidths.push(g.width());e<a.s.iLeftColumns&&(b+=i);a.s.iTableColumns-a.s.iRightColumns<=e&&(c+=i)}else a.s.aiInnerWidths.push(0),a.s.aiOuterWidths.push(0)});this.s.iLeftWidth=b;this.s.iRightWidth=c},_fnGridSetup:function(){var a=this._fnDTOverflow(),b;this.dom.body=this.s.dt.nTable;this.dom.header=
+this.s.dt.nTHead.parentNode;this.dom.header.parentNode.parentNode.style.position="relative";var c=d('<div class="DTFC_ScrollWrapper" style="position:relative; clear:both;"><div class="DTFC_LeftWrapper" style="position:absolute; top:0; left:0;"><div class="DTFC_LeftHeadWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div><div class="DTFC_LeftBodyWrapper" style="position:relative; top:0; left:0; overflow:hidden;"><div class="DTFC_LeftBodyLiner" style="position:relative; top:0; left:0; overflow-y:scroll;"></div></div><div class="DTFC_LeftFootWrapper" style="position:relative; top:0; left:0; overflow:hidden;"></div></div><div class="DTFC_RightWrapper" style="position:absolute; top:0; right:0;"><div class="DTFC_RightHeadWrapper" style="position:relative; top:0; left:0;"><div class="DTFC_RightHeadBlocker DTFC_Blocker" style="position:absolute; top:0; bottom:0;"></div></div><div class="DTFC_RightBodyWrapper" style="position:relative; top:0; left:0; overflow:hidden;"><div class="DTFC_RightBodyLiner" style="position:relative; top:0; left:0; overflow-y:scroll;"></div></div><div class="DTFC_RightFootWrapper" style="position:relative; top:0; left:0;"><div class="DTFC_RightFootBlocker DTFC_Blocker" style="position:absolute; top:0; bottom:0;"></div></div></div></div>')[0],
+e=c.childNodes[0],f=c.childNodes[1];this.dom.grid.dt.parentNode.insertBefore(c,this.dom.grid.dt);c.appendChild(this.dom.grid.dt);this.dom.grid.wrapper=c;0<this.s.iLeftColumns&&(this.dom.grid.left.wrapper=e,this.dom.grid.left.head=e.childNodes[0],this.dom.grid.left.body=e.childNodes[1],this.dom.grid.left.liner=d("div.DTFC_LeftBodyLiner",c)[0],c.appendChild(e));0<this.s.iRightColumns&&(this.dom.grid.right.wrapper=f,this.dom.grid.right.head=f.childNodes[0],this.dom.grid.right.body=f.childNodes[1],this.dom.grid.right.liner=
+d("div.DTFC_RightBodyLiner",c)[0],f.style.right=a.bar+"px",b=d("div.DTFC_RightHeadBlocker",c)[0],b.style.width=a.bar+"px",b.style.right=-a.bar+"px",this.dom.grid.right.headBlock=b,b=d("div.DTFC_RightFootBlocker",c)[0],b.style.width=a.bar+"px",b.style.right=-a.bar+"px",this.dom.grid.right.footBlock=b,c.appendChild(f));if(this.s.dt.nTFoot&&(this.dom.footer=this.s.dt.nTFoot.parentNode,0<this.s.iLeftColumns&&(this.dom.grid.left.foot=e.childNodes[2]),0<this.s.iRightColumns))this.dom.grid.right.foot=f.childNodes[2];
+this.s.rtl&&d("div.DTFC_RightHeadBlocker",c).css({left:-a.bar+"px",right:""})},_fnGridLayout:function(){var a=this,b=this.dom.grid;d(b.wrapper).width();var c=d(this.s.dt.nTable.parentNode).outerHeight(),e=d(this.s.dt.nTable.parentNode.parentNode).outerHeight(),f=this._fnDTOverflow(),g=this.s.iLeftWidth,h=this.s.iRightWidth,i="rtl"===d(this.dom.body).css("direction"),j=function(b,c){f.bar?a._firefoxScrollError()?34<d(b).height()&&(b.style.width=c+f.bar+"px"):b.style.width=c+f.bar+"px":(b.style.width=
+c+20+"px",b.style.paddingRight="20px",b.style.boxSizing="border-box")};f.x&&(c-=f.bar);b.wrapper.style.height=e+"px";0<this.s.iLeftColumns&&(e=b.left.wrapper,e.style.width=g+"px",e.style.height="1px",i?(e.style.left="",e.style.right=0):(e.style.left=0,e.style.right=""),b.left.body.style.height=c+"px",b.left.foot&&(b.left.foot.style.top=(f.x?f.bar:0)+"px"),j(b.left.liner,g),b.left.liner.style.height=c+"px");0<this.s.iRightColumns&&(e=b.right.wrapper,e.style.width=h+"px",e.style.height="1px",this.s.rtl?
+(e.style.left=f.y?f.bar+"px":0,e.style.right=""):(e.style.left="",e.style.right=f.y?f.bar+"px":0),b.right.body.style.height=c+"px",b.right.foot&&(b.right.foot.style.top=(f.x?f.bar:0)+"px"),j(b.right.liner,h),b.right.liner.style.height=c+"px",b.right.headBlock.style.display=f.y?"block":"none",b.right.footBlock.style.display=f.y?"block":"none")},_fnDTOverflow:function(){var a=this.s.dt.nTable,b=a.parentNode,c={x:!1,y:!1,bar:this.s.dt.oScroll.iBarWidth};a.offsetWidth>b.clientWidth&&(c.x=!0);a.offsetHeight>
+b.clientHeight&&(c.y=!0);return c},_fnDraw:function(a){this._fnGridLayout();this._fnCloneLeft(a);this._fnCloneRight(a);null!==this.s.fnDrawCallback&&this.s.fnDrawCallback.call(this,this.dom.clone.left,this.dom.clone.right);d(this).trigger("draw.dtfc",{leftClone:this.dom.clone.left,rightClone:this.dom.clone.right})},_fnCloneRight:function(a){if(!(0>=this.s.iRightColumns)){var b,c=[];for(b=this.s.iTableColumns-this.s.iRightColumns;b<this.s.iTableColumns;b++)this.s.dt.aoColumns[b].bVisible&&c.push(b);
+this._fnClone(this.dom.clone.right,this.dom.grid.right,c,a)}},_fnCloneLeft:function(a){if(!(0>=this.s.iLeftColumns)){var b,c=[];for(b=0;b<this.s.iLeftColumns;b++)this.s.dt.aoColumns[b].bVisible&&c.push(b);this._fnClone(this.dom.clone.left,this.dom.grid.left,c,a)}},_fnCopyLayout:function(a,b,c){for(var e=[],f=[],g=[],h=0,i=a.length;h<i;h++){var j=[];j.nTr=d(a[h].nTr).clone(c,!1)[0];for(var l=0,o=this.s.iTableColumns;l<o;l++)if(-1!==d.inArray(l,b)){var p=d.inArray(a[h][l].cell,g);-1===p?(p=d(a[h][l].cell).clone(c,
+!1)[0],f.push(p),g.push(a[h][l].cell),j.push({cell:p,unique:a[h][l].unique})):j.push({cell:f[p],unique:a[h][l].unique})}e.push(j)}return e},_fnClone:function(a,b,c,e){var f=this,g,h,i,j,l,o,p,n,m,k=this.s.dt;if(e){d(a.header).remove();a.header=d(this.dom.header).clone(!0,!1)[0];a.header.className+=" DTFC_Cloned";a.header.style.width="100%";b.head.appendChild(a.header);n=this._fnCopyLayout(k.aoHeader,c,!0);j=d(">thead",a.header);j.empty();g=0;for(h=n.length;g<h;g++)j[0].appendChild(n[g].nTr);k.oApi._fnDrawHead(k,
+n,!0)}else{n=this._fnCopyLayout(k.aoHeader,c,!1);m=[];k.oApi._fnDetectHeader(m,d(">thead",a.header)[0]);g=0;for(h=n.length;g<h;g++){i=0;for(j=n[g].length;i<j;i++)m[g][i].cell.className=n[g][i].cell.className,d("span.DataTables_sort_icon",m[g][i].cell).each(function(){this.className=d("span.DataTables_sort_icon",n[g][i].cell)[0].className})}}this._fnEqualiseHeights("thead",this.dom.header,a.header);"auto"==this.s.sHeightMatch&&d(">tbody>tr",f.dom.body).css("height","auto");null!==a.body&&(d(a.body).remove(),
+a.body=null);a.body=d(this.dom.body).clone(!0)[0];a.body.className+=" DTFC_Cloned";a.body.style.paddingBottom=k.oScroll.iBarWidth+"px";a.body.style.marginBottom=2*k.oScroll.iBarWidth+"px";null!==a.body.getAttribute("id")&&a.body.removeAttribute("id");d(">thead>tr",a.body).empty();d(">tfoot",a.body).remove();var q=d("tbody",a.body)[0];d(q).empty();if(0<k.aiDisplay.length){h=d(">thead>tr",a.body)[0];for(p=0;p<c.length;p++)l=c[p],o=d(k.aoColumns[l].nTh).clone(!0)[0],o.innerHTML="",j=o.style,j.paddingTop=
+"0",j.paddingBottom="0",j.borderTopWidth="0",j.borderBottomWidth="0",j.height=0,j.width=f.s.aiInnerWidths[l]+"px",h.appendChild(o);d(">tbody>tr",f.dom.body).each(function(a){var a=f.s.dt.oFeatures.bServerSide===false?f.s.dt.aiDisplay[f.s.dt._iDisplayStart+a]:a,b=f.s.dt.aoData[a].anCells||d(this).children("td, th"),e=this.cloneNode(false);e.removeAttribute("id");e.setAttribute("data-dt-row",a);for(p=0;p<c.length;p++){l=c[p];if(b.length>0){o=d(b[l]).clone(true,true)[0];o.setAttribute("data-dt-row",
+a);o.setAttribute("data-dt-column",p);e.appendChild(o)}}q.appendChild(e)})}else d(">tbody>tr",f.dom.body).each(function(){o=this.cloneNode(true);o.className=o.className+" DTFC_NoData";d("td",o).html("");q.appendChild(o)});a.body.style.width="100%";a.body.style.margin="0";a.body.style.padding="0";k.oScroller!==t&&(h=k.oScroller.dom.force,b.forcer?b.forcer.style.height=h.style.height:(b.forcer=h.cloneNode(!0),b.liner.appendChild(b.forcer)));b.liner.appendChild(a.body);this._fnEqualiseHeights("tbody",
+f.dom.body,a.body);if(null!==k.nTFoot){if(e){null!==a.footer&&a.footer.parentNode.removeChild(a.footer);a.footer=d(this.dom.footer).clone(!0,!0)[0];a.footer.className+=" DTFC_Cloned";a.footer.style.width="100%";b.foot.appendChild(a.footer);n=this._fnCopyLayout(k.aoFooter,c,!0);b=d(">tfoot",a.footer);b.empty();g=0;for(h=n.length;g<h;g++)b[0].appendChild(n[g].nTr);k.oApi._fnDrawHead(k,n,!0)}else{n=this._fnCopyLayout(k.aoFooter,c,!1);b=[];k.oApi._fnDetectHeader(b,d(">tfoot",a.footer)[0]);g=0;for(h=n.length;g<
+h;g++){i=0;for(j=n[g].length;i<j;i++)b[g][i].cell.className=n[g][i].cell.className}}this._fnEqualiseHeights("tfoot",this.dom.footer,a.footer)}b=k.oApi._fnGetUniqueThs(k,d(">thead",a.header)[0]);d(b).each(function(a){l=c[a];this.style.width=f.s.aiInnerWidths[l]+"px"});null!==f.s.dt.nTFoot&&(b=k.oApi._fnGetUniqueThs(k,d(">tfoot",a.footer)[0]),d(b).each(function(a){l=c[a];this.style.width=f.s.aiInnerWidths[l]+"px"}))},_fnGetTrNodes:function(a){for(var b=[],c=0,d=a.childNodes.length;c<d;c++)"TR"==a.childNodes[c].nodeName.toUpperCase()&&
+b.push(a.childNodes[c]);return b},_fnEqualiseHeights:function(a,b,c){if(!("none"==this.s.sHeightMatch&&"thead"!==a&&"tfoot"!==a)){var e,f,g=b.getElementsByTagName(a)[0],c=c.getElementsByTagName(a)[0],a=d(">"+a+">tr:eq(0)",b).children(":first");a.outerHeight();a.height();for(var g=this._fnGetTrNodes(g),b=this._fnGetTrNodes(c),h=[],c=0,a=b.length;c<a;c++)e=g[c].offsetHeight,f=b[c].offsetHeight,e=f>e?f:e,"semiauto"==this.s.sHeightMatch&&(g[c]._DTTC_iHeight=e),h.push(e);c=0;for(a=b.length;c<a;c++)b[c].style.height=
+h[c]+"px",g[c].style.height=h[c]+"px"}},_firefoxScrollError:function(){if(u===t){var a=d("<div/>").css({position:"absolute",top:0,left:0,height:10,width:50,overflow:"scroll"}).appendTo("body");u=a[0].clientWidth===a[0].offsetWidth&&0!==this._fnDTOverflow().bar;a.remove()}return u}});m.defaults={iLeftColumns:1,iRightColumns:0,fnDrawCallback:null,sHeightMatch:"semiauto"};m.version="3.2.2";s.Api.register("fixedColumns()",function(){return this});s.Api.register("fixedColumns().update()",function(){return this.iterator("table",
+function(a){a._oFixedColumns&&a._oFixedColumns.fnUpdate()})});s.Api.register("fixedColumns().relayout()",function(){return this.iterator("table",function(a){a._oFixedColumns&&a._oFixedColumns.fnRedrawLayout()})});s.Api.register("rows().recalcHeight()",function(){return this.iterator("row",function(a,b){a._oFixedColumns&&a._oFixedColumns.fnRecalculateHeight(this.row(b).node())})});s.Api.register("fixedColumns().rowIndex()",function(a){a=d(a);return a.parents(".DTFC_Cloned").length?this.rows({page:"current"}).indexes()[a.index()]:
+this.row(a).index()});s.Api.register("fixedColumns().cellIndex()",function(a){a=d(a);if(a.parents(".DTFC_Cloned").length){var b=a.parent().index(),b=this.rows({page:"current"}).indexes()[b],a=a.parents(".DTFC_LeftWrapper").length?a.index():this.columns().flatten().length-this.context[0]._oFixedColumns.s.iRightColumns+a.index();return{row:b,column:this.column.index("toData",a),columnVisible:a}}return this.cell(a).index()});d(r).on("init.dt.fixedColumns",function(a,b){if("dt"===a.namespace){var c=b.oInit.fixedColumns,
+e=s.defaults.fixedColumns;if(c||e)e=d.extend({},c,e),!1!==c&&new m(b,e)}});d.fn.dataTable.FixedColumns=m;return d.fn.DataTable.FixedColumns=m});
+
+
+/*!
+ FixedHeader 3.1.2
+ ©2009-2016 SpryMedia Ltd - datatables.net/license
+*/
+(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(g){return d(g,window,document)}):"object"===typeof exports?module.exports=function(g,h){g||(g=window);if(!h||!h.fn.dataTable)h=require("datatables.net")(g,h).$;return d(h,g,g.document)}:d(jQuery,window,document)})(function(d,g,h,k){var j=d.fn.dataTable,l=0,i=function(b,a){if(!(this instanceof i))throw"FixedHeader must be initialised with the 'new' keyword.";!0===a&&(a={});b=new j.Api(b);this.c=d.extend(!0,
+{},i.defaults,a);this.s={dt:b,position:{theadTop:0,tbodyTop:0,tfootTop:0,tfootBottom:0,width:0,left:0,tfootHeight:0,theadHeight:0,windowHeight:d(g).height(),visible:!0},headerMode:null,footerMode:null,autoWidth:b.settings()[0].oFeatures.bAutoWidth,namespace:".dtfc"+l++,scrollLeft:{header:-1,footer:-1},enable:!0};this.dom={floatingHeader:null,thead:d(b.table().header()),tbody:d(b.table().body()),tfoot:d(b.table().footer()),header:{host:null,floating:null,placeholder:null},footer:{host:null,floating:null,
+placeholder:null}};this.dom.header.host=this.dom.thead.parent();this.dom.footer.host=this.dom.tfoot.parent();var e=b.settings()[0];if(e._fixedHeader)throw"FixedHeader already initialised on table "+e.nTable.id;e._fixedHeader=this;this._constructor()};d.extend(i.prototype,{enable:function(b){this.s.enable=b;this.c.header&&this._modeChange("in-place","header",!0);this.c.footer&&this.dom.tfoot.length&&this._modeChange("in-place","footer",!0);this.update()},headerOffset:function(b){b!==k&&(this.c.headerOffset=
+b,this.update());return this.c.headerOffset},footerOffset:function(b){b!==k&&(this.c.footerOffset=b,this.update());return this.c.footerOffset},update:function(){this._positions();this._scroll(!0)},_constructor:function(){var b=this,a=this.s.dt;d(g).on("scroll"+this.s.namespace,function(){b._scroll()}).on("resize"+this.s.namespace,function(){b.s.position.windowHeight=d(g).height();b.update()});var e=d(".fh-fixedHeader");!this.c.headerOffset&&e.length&&(this.c.headerOffset=e.outerHeight());e=d(".fh-fixedFooter");
+!this.c.footerOffset&&e.length&&(this.c.footerOffset=e.outerHeight());a.on("column-reorder.dt.dtfc column-visibility.dt.dtfc draw.dt.dtfc column-sizing.dt.dtfc",function(){b.update()});a.on("destroy.dtfc",function(){a.off(".dtfc");d(g).off(b.s.namespace)});this._positions();this._scroll()},_clone:function(b,a){var e=this.s.dt,c=this.dom[b],f="header"===b?this.dom.thead:this.dom.tfoot;!a&&c.floating?c.floating.removeClass("fixedHeader-floating fixedHeader-locked"):(c.floating&&(c.placeholder.remove(),
+this._unsize(b),c.floating.children().detach(),c.floating.remove()),c.floating=d(e.table().node().cloneNode(!1)).css("table-layout","fixed").removeAttr("id").append(f).appendTo("body"),c.placeholder=f.clone(!1),c.host.prepend(c.placeholder),this._matchWidths(c.placeholder,c.floating))},_matchWidths:function(b,a){var e=function(a){return d(a,b).map(function(){return d(this).width()}).toArray()},c=function(b,c){d(b,a).each(function(a){d(this).css({width:c[a],minWidth:c[a]})})},f=e("th"),e=e("td");c("th",
+f);c("td",e)},_unsize:function(b){var a=this.dom[b].floating;a&&("footer"===b||"header"===b&&!this.s.autoWidth)?d("th, td",a).css({width:"",minWidth:""}):a&&"header"===b&&d("th, td",a).css("min-width","")},_horizontal:function(b,a){var e=this.dom[b],c=this.s.position,d=this.s.scrollLeft;e.floating&&d[b]!==a&&(e.floating.css("left",c.left-a),d[b]=a)},_modeChange:function(b,a,e){var c=this.dom[a],f=this.s.position,g=d.contains(this.dom["footer"===a?"tfoot":"thead"][0],h.activeElement)?h.activeElement:
+null;if("in-place"===b){if(c.placeholder&&(c.placeholder.remove(),c.placeholder=null),this._unsize(a),"header"===a?c.host.prepend(this.dom.thead):c.host.append(this.dom.tfoot),c.floating)c.floating.remove(),c.floating=null}else"in"===b?(this._clone(a,e),c.floating.addClass("fixedHeader-floating").css("header"===a?"top":"bottom",this.c[a+"Offset"]).css("left",f.left+"px").css("width",f.width+"px"),"footer"===a&&c.floating.css("top","")):"below"===b?(this._clone(a,e),c.floating.addClass("fixedHeader-locked").css("top",
+f.tfootTop-f.theadHeight).css("left",f.left+"px").css("width",f.width+"px")):"above"===b&&(this._clone(a,e),c.floating.addClass("fixedHeader-locked").css("top",f.tbodyTop).css("left",f.left+"px").css("width",f.width+"px"));g&&g!==h.activeElement&&g.focus();this.s.scrollLeft.header=-1;this.s.scrollLeft.footer=-1;this.s[a+"Mode"]=b},_positions:function(){var b=this.s.dt.table(),a=this.s.position,e=this.dom,b=d(b.node()),c=b.children("thead"),f=b.children("tfoot"),e=e.tbody;a.visible=b.is(":visible");
+a.width=b.outerWidth();a.left=b.offset().left;a.theadTop=c.offset().top;a.tbodyTop=e.offset().top;a.theadHeight=a.tbodyTop-a.theadTop;f.length?(a.tfootTop=f.offset().top,a.tfootBottom=a.tfootTop+f.outerHeight(),a.tfootHeight=a.tfootBottom-a.tfootTop):(a.tfootTop=a.tbodyTop+e.outerHeight(),a.tfootBottom=a.tfootTop,a.tfootHeight=a.tfootTop)},_scroll:function(b){var a=d(h).scrollTop(),e=d(h).scrollLeft(),c=this.s.position,f;if(this.s.enable&&(this.c.header&&(f=!c.visible||a<=c.theadTop-this.c.headerOffset?
+"in-place":a<=c.tfootTop-c.theadHeight-this.c.headerOffset?"in":"below",(b||f!==this.s.headerMode)&&this._modeChange(f,"header",b),this._horizontal("header",e)),this.c.footer&&this.dom.tfoot.length))a=!c.visible||a+c.windowHeight>=c.tfootBottom+this.c.footerOffset?"in-place":c.windowHeight+a>c.tbodyTop+c.tfootHeight+this.c.footerOffset?"in":"above",(b||a!==this.s.footerMode)&&this._modeChange(a,"footer",b),this._horizontal("footer",e)}});i.version="3.1.2";i.defaults={header:!0,footer:!1,headerOffset:0,
+footerOffset:0};d.fn.dataTable.FixedHeader=i;d.fn.DataTable.FixedHeader=i;d(h).on("init.dt.dtfh",function(b,a){if("dt"===b.namespace){var e=a.oInit.fixedHeader,c=j.defaults.fixedHeader;if((e||c)&&!a._fixedHeader)c=d.extend({},c,e),!1!==e&&new i(a,c)}});j.Api.register("fixedHeader()",function(){});j.Api.register("fixedHeader.adjust()",function(){return this.iterator("table",function(b){(b=b._fixedHeader)&&b.update()})});j.Api.register("fixedHeader.enable()",function(b){return this.iterator("table",
+function(a){(a=a._fixedHeader)&&a.enable(b!==k?b:!0)})});j.Api.register("fixedHeader.disable()",function(){return this.iterator("table",function(b){(b=b._fixedHeader)&&b.enable(!1)})});d.each(["header","footer"],function(b,a){j.Api.register("fixedHeader."+a+"Offset()",function(b){var c=this.context;return b===k?c.length&&c[0]._fixedHeader?c[0]._fixedHeader[a+"Offset"]():k:this.iterator("table",function(c){if(c=c._fixedHeader)c[a+"Offset"](b)})})});return i});
+
+
+/*!
+ Responsive 2.1.1
+ 2014-2016 SpryMedia Ltd - datatables.net/license
+*/
+(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(l){return c(l,window,document)}):"object"===typeof exports?module.exports=function(l,k){l||(l=window);if(!k||!k.fn.dataTable)k=require("datatables.net")(l,k).$;return c(k,l,l.document)}:c(jQuery,window,document)})(function(c,l,k,p){var m=c.fn.dataTable,j=function(b,a){if(!m.versionCheck||!m.versionCheck("1.10.3"))throw"DataTables Responsive requires DataTables 1.10.3 or newer";this.s={dt:new m.Api(b),columns:[],
+current:[]};this.s.dt.settings()[0].responsive||(a&&"string"===typeof a.details?a.details={type:a.details}:a&&!1===a.details?a.details={type:!1}:a&&!0===a.details&&(a.details={type:"inline"}),this.c=c.extend(!0,{},j.defaults,m.defaults.responsive,a),b.responsive=this,this._constructor())};c.extend(j.prototype,{_constructor:function(){var b=this,a=this.s.dt,d=a.settings()[0],e=c(l).width();a.settings()[0]._responsive=this;c(l).on("resize.dtr orientationchange.dtr",m.util.throttle(function(){var a=
+c(l).width();a!==e&&(b._resize(),e=a)}));d.oApi._fnCallbackReg(d,"aoRowCreatedCallback",function(e){-1!==c.inArray(!1,b.s.current)&&c(">td, >th",e).each(function(e){e=a.column.index("toData",e);!1===b.s.current[e]&&c(this).css("display","none")})});a.on("destroy.dtr",function(){a.off(".dtr");c(a.table().body()).off(".dtr");c(l).off("resize.dtr orientationchange.dtr");c.each(b.s.current,function(a,e){!1===e&&b._setColumnVis(a,!0)})});this.c.breakpoints.sort(function(a,b){return a.width<b.width?1:a.width>
+b.width?-1:0});this._classLogic();this._resizeAuto();d=this.c.details;!1!==d.type&&(b._detailsInit(),a.on("column-visibility.dtr",function(){b._classLogic();b._resizeAuto();b._resize()}),a.on("draw.dtr",function(){b._redrawChildren()}),c(a.table().node()).addClass("dtr-"+d.type));a.on("column-reorder.dtr",function(){b._classLogic();b._resizeAuto();b._resize()});a.on("column-sizing.dtr",function(){b._resizeAuto();b._resize()});a.on("preXhr.dtr",function(){var e=[];a.rows().every(function(){this.child.isShown()&&
+e.push(this.id(true))});a.one("draw.dtr",function(){a.rows(e).every(function(){b._detailsDisplay(this,false)})})});a.on("init.dtr",function(){b._resizeAuto();b._resize();c.inArray(false,b.s.current)&&a.columns.adjust()});this._resize()},_columnsVisiblity:function(b){var a=this.s.dt,d=this.s.columns,e,f,g=d.map(function(a,b){return{columnIdx:b,priority:a.priority}}).sort(function(a,b){return a.priority!==b.priority?a.priority-b.priority:a.columnIdx-b.columnIdx}),h=c.map(d,function(a){return a.auto&&
+null===a.minWidth?!1:!0===a.auto?"-":-1!==c.inArray(b,a.includeIn)}),n=0;e=0;for(f=h.length;e<f;e++)!0===h[e]&&(n+=d[e].minWidth);e=a.settings()[0].oScroll;e=e.sY||e.sX?e.iBarWidth:0;a=a.table().container().offsetWidth-e-n;e=0;for(f=h.length;e<f;e++)d[e].control&&(a-=d[e].minWidth);n=!1;e=0;for(f=g.length;e<f;e++){var i=g[e].columnIdx;"-"===h[i]&&(!d[i].control&&d[i].minWidth)&&(n||0>a-d[i].minWidth?(n=!0,h[i]=!1):h[i]=!0,a-=d[i].minWidth)}g=!1;e=0;for(f=d.length;e<f;e++)if(!d[e].control&&!d[e].never&&
+!h[e]){g=!0;break}e=0;for(f=d.length;e<f;e++)d[e].control&&(h[e]=g);-1===c.inArray(!0,h)&&(h[0]=!0);return h},_classLogic:function(){var b=this,a=this.c.breakpoints,d=this.s.dt,e=d.columns().eq(0).map(function(a){var b=this.column(a),e=b.header().className,a=d.settings()[0].aoColumns[a].responsivePriority;a===p&&(b=c(b.header()).data("priority"),a=b!==p?1*b:1E4);return{className:e,includeIn:[],auto:!1,control:!1,never:e.match(/\bnever\b/)?!0:!1,priority:a}}),f=function(a,b){var d=e[a].includeIn;-1===
+c.inArray(b,d)&&d.push(b)},g=function(c,d,i,g){if(i)if("max-"===i){g=b._find(d).width;d=0;for(i=a.length;d<i;d++)a[d].width<=g&&f(c,a[d].name)}else if("min-"===i){g=b._find(d).width;d=0;for(i=a.length;d<i;d++)a[d].width>=g&&f(c,a[d].name)}else{if("not-"===i){d=0;for(i=a.length;d<i;d++)-1===a[d].name.indexOf(g)&&f(c,a[d].name)}}else e[c].includeIn.push(d)};e.each(function(b,e){for(var d=b.className.split(" "),f=!1,j=0,l=d.length;j<l;j++){var k=c.trim(d[j]);if("all"===k){f=!0;b.includeIn=c.map(a,function(a){return a.name});
+return}if("none"===k||b.never){f=!0;return}if("control"===k){f=!0;b.control=!0;return}c.each(a,function(a,b){var d=b.name.split("-"),c=k.match(RegExp("(min\\-|max\\-|not\\-)?("+d[0]+")(\\-[_a-zA-Z0-9])?"));c&&(f=!0,c[2]===d[0]&&c[3]==="-"+d[1]?g(e,b.name,c[1],c[2]+c[3]):c[2]===d[0]&&!c[3]&&g(e,b.name,c[1],c[2]))})}f||(b.auto=!0)});this.s.columns=e},_detailsDisplay:function(b,a){var d=this,e=this.s.dt,f=this.c.details;if(f&&!1!==f.type){var g=f.display(b,a,function(){return f.renderer(e,b[0],d._detailsObj(b[0]))});
+(!0===g||!1===g)&&c(e.table().node()).triggerHandler("responsive-display.dt",[e,b,g,a])}},_detailsInit:function(){var b=this,a=this.s.dt,d=this.c.details;"inline"===d.type&&(d.target="td:first-child, th:first-child");a.on("draw.dtr",function(){b._tabIndexes()});b._tabIndexes();c(a.table().body()).on("keyup.dtr","td, th",function(b){b.keyCode===13&&c(this).data("dtr-keyboard")&&c(this).click()});var e=d.target;c(a.table().body()).on("click.dtr mousedown.dtr mouseup.dtr","string"===typeof e?e:"td, th",
+function(d){if(c(a.table().node()).hasClass("collapsed")&&c.inArray(c(this).closest("tr").get(0),a.rows().nodes().toArray())!==-1){if(typeof e==="number"){var g=e<0?a.columns().eq(0).length+e:e;if(a.cell(this).index().column!==g)return}g=a.row(c(this).closest("tr"));d.type==="click"?b._detailsDisplay(g,false):d.type==="mousedown"?c(this).css("outline","none"):d.type==="mouseup"&&c(this).blur().css("outline","")}})},_detailsObj:function(b){var a=this,d=this.s.dt;return c.map(this.s.columns,function(e,
+c){if(!e.never&&!e.control)return{title:d.settings()[0].aoColumns[c].sTitle,data:d.cell(b,c).render(a.c.orthogonal),hidden:d.column(c).visible()&&!a.s.current[c],columnIndex:c,rowIndex:b}})},_find:function(b){for(var a=this.c.breakpoints,d=0,c=a.length;d<c;d++)if(a[d].name===b)return a[d]},_redrawChildren:function(){var b=this,a=this.s.dt;a.rows({page:"current"}).iterator("row",function(c,e){a.row(e);b._detailsDisplay(a.row(e),!0)})},_resize:function(){var b=this,a=this.s.dt,d=c(l).width(),e=this.c.breakpoints,
+f=e[0].name,g=this.s.columns,h,j=this.s.current.slice();for(h=e.length-1;0<=h;h--)if(d<=e[h].width){f=e[h].name;break}var i=this._columnsVisiblity(f);this.s.current=i;e=!1;h=0;for(d=g.length;h<d;h++)if(!1===i[h]&&!g[h].never&&!g[h].control){e=!0;break}c(a.table().node()).toggleClass("collapsed",e);var k=!1;a.columns().eq(0).each(function(a,c){i[c]!==j[c]&&(k=!0,b._setColumnVis(a,i[c]))});k&&(this._redrawChildren(),c(a.table().node()).trigger("responsive-resize.dt",[a,this.s.current]))},_resizeAuto:function(){var b=
+this.s.dt,a=this.s.columns;if(this.c.auto&&-1!==c.inArray(!0,c.map(a,function(b){return b.auto}))){b.table().node();var d=b.table().node().cloneNode(!1),e=c(b.table().header().cloneNode(!1)).appendTo(d),f=c(b.table().body()).clone(!1,!1).empty().appendTo(d),g=b.columns().header().filter(function(a){return b.column(a).visible()}).to$().clone(!1).css("display","table-cell");c(f).append(c(b.rows({page:"current"}).nodes()).clone(!1)).find("th, td").css("display","");if(f=b.table().footer()){var f=c(f.cloneNode(!1)).appendTo(d),
+h=b.columns().footer().filter(function(a){return b.column(a).visible()}).to$().clone(!1).css("display","table-cell");c("<tr/>").append(h).appendTo(f)}c("<tr/>").append(g).appendTo(e);"inline"===this.c.details.type&&c(d).addClass("dtr-inline collapsed");c(d).find("[name]").removeAttr("name");d=c("<div/>").css({width:1,height:1,overflow:"hidden"}).append(d);d.insertBefore(b.table().node());g.each(function(c){c=b.column.index("fromVisible",c);a[c].minWidth=this.offsetWidth||0});d.remove()}},_setColumnVis:function(b,
+a){var d=this.s.dt,e=a?"":"none";c(d.column(b).header()).css("display",e);c(d.column(b).footer()).css("display",e);d.column(b).nodes().to$().css("display",e)},_tabIndexes:function(){var b=this.s.dt,a=b.cells({page:"current"}).nodes().to$(),d=b.settings()[0],e=this.c.details.target;a.filter("[data-dtr-keyboard]").removeData("[data-dtr-keyboard]");a="number"===typeof e?":eq("+e+")":e;"td:first-child, th:first-child"===a&&(a=">td:first-child, >th:first-child");c(a,b.rows({page:"current"}).nodes()).attr("tabIndex",
+d.iTabIndex).data("dtr-keyboard",1)}});j.breakpoints=[{name:"desktop",width:Infinity},{name:"tablet-l",width:1024},{name:"tablet-p",width:768},{name:"mobile-l",width:480},{name:"mobile-p",width:320}];j.display={childRow:function(b,a,d){if(a){if(c(b.node()).hasClass("parent"))return b.child(d(),"child").show(),!0}else{if(b.child.isShown())return b.child(!1),c(b.node()).removeClass("parent"),!1;b.child(d(),"child").show();c(b.node()).addClass("parent");return!0}},childRowImmediate:function(b,a,d){if(!a&&
+b.child.isShown()||!b.responsive.hasHidden())return b.child(!1),c(b.node()).removeClass("parent"),!1;b.child(d(),"child").show();c(b.node()).addClass("parent");return!0},modal:function(b){return function(a,d,e){if(d)c("div.dtr-modal-content").empty().append(e());else{var f=function(){g.remove();c(k).off("keypress.dtr")},g=c('<div class="dtr-modal"/>').append(c('<div class="dtr-modal-display"/>').append(c('<div class="dtr-modal-content"/>').append(e())).append(c('<div class="dtr-modal-close">&times;</div>').click(function(){f()}))).append(c('<div class="dtr-modal-background"/>').click(function(){f()})).appendTo("body");
+c(k).on("keyup.dtr",function(a){27===a.keyCode&&(a.stopPropagation(),f())})}b&&b.header&&c("div.dtr-modal-content").prepend("<h2>"+b.header(a)+"</h2>")}}};j.renderer={listHidden:function(){return function(b,a,d){return(b=c.map(d,function(a){return a.hidden?'<li data-dtr-index="'+a.columnIndex+'" data-dt-row="'+a.rowIndex+'" data-dt-column="'+a.columnIndex+'"><span class="dtr-title">'+a.title+'</span> <span class="dtr-data">'+a.data+"</span></li>":""}).join(""))?c('<ul data-dtr-index="'+a+'" class="dtr-details"/>').append(b):
+!1}},tableAll:function(b){b=c.extend({tableClass:""},b);return function(a,d,e){a=c.map(e,function(a){return'<tr data-dt-row="'+a.rowIndex+'" data-dt-column="'+a.columnIndex+'"><td>'+a.title+":</td> <td>"+a.data+"</td></tr>"}).join("");return c('<table class="'+b.tableClass+' dtr-details" width="100%"/>').append(a)}}};j.defaults={breakpoints:j.breakpoints,auto:!0,details:{display:j.display.childRow,renderer:j.renderer.listHidden(),target:0,type:"inline"},orthogonal:"display"};var o=c.fn.dataTable.Api;
+o.register("responsive()",function(){return this});o.register("responsive.index()",function(b){b=c(b);return{column:b.data("dtr-index"),row:b.parent().data("dtr-index")}});o.register("responsive.rebuild()",function(){return this.iterator("table",function(b){b._responsive&&b._responsive._classLogic()})});o.register("responsive.recalc()",function(){return this.iterator("table",function(b){b._responsive&&(b._responsive._resizeAuto(),b._responsive._resize())})});o.register("responsive.hasHidden()",function(){var b=
+this.context[0];return b._responsive?-1!==c.inArray(!1,b._responsive.s.current):!1});j.version="2.1.1";c.fn.dataTable.Responsive=j;c.fn.DataTable.Responsive=j;c(k).on("preInit.dt.dtr",function(b,a){if("dt"===b.namespace&&(c(a.nTable).hasClass("responsive")||c(a.nTable).hasClass("dt-responsive")||a.oInit.responsive||m.defaults.responsive)){var d=a.oInit.responsive;!1!==d&&new j(a,c.isPlainObject(d)?d:{})}});return j});
+
+
diff --git a/static/README b/static/README
new file mode 100644
index 0000000000000000000000000000000000000000..42b1b717c8a5b7f32873ebb44fe86e6e622d7020
--- /dev/null
+++ b/static/README
@@ -0,0 +1 @@
+Static content served directly by Apache.
diff --git a/static/css/autosuggest_inquisitor.css b/static/css/autosuggest_inquisitor.css
new file mode 100644
index 0000000000000000000000000000000000000000..f737fb61b0e8bd1eb21ed468a1e2750eccc6b85e
--- /dev/null
+++ b/static/css/autosuggest_inquisitor.css
@@ -0,0 +1,177 @@
+/* 
+================================================
+autosuggest, inquisitor style
+================================================
+*/
+
+body
+{
+	position: relative;
+}
+
+
+div.autosuggest
+{
+	position: absolute;
+	background-image: url(/ScoDoc/static/icons/as_pointer.png);
+	background-position: top;
+	background-repeat: no-repeat;
+	padding: 10px 0 0 0;
+}
+
+div.autosuggest div.as_header,
+div.autosuggest div.as_footer
+{
+	position: relative;
+	height: 6px;
+	padding: 0 6px;
+	background-image: url(/ScoDoc/static/icons/ul_corner_tr.png);
+	background-position: top right;
+	background-repeat: no-repeat;
+	overflow: hidden;
+}
+div.autosuggest div.as_footer
+{
+	background-image: url(/ScoDoc/static/icons/ul_corner_br.png);
+}
+
+div.autosuggest div.as_header div.as_corner,
+div.autosuggest div.as_footer div.as_corner
+{
+	position: absolute;
+	top: 0;
+	left: 0;
+	height: 6px;
+	width: 6px;
+	background-image: url(/ScoDoc/static/icons/ul_corner_tl.png);
+	background-position: top left;
+	background-repeat: no-repeat;
+}
+div.autosuggest div.as_footer div.as_corner
+{
+	background-image: url(/ScoDoc/static/icons/ul_corner_bl.png);
+}
+div.autosuggest div.as_header div.as_bar,
+div.autosuggest div.as_footer div.as_bar
+{
+	height: 6px;
+	overflow: hidden;
+	background-color: #333;
+}
+
+
+div.autosuggest ul
+{
+	list-style: none;
+	margin: 0 0 -4px 0;
+	padding: 0;
+	overflow: hidden;
+	background-color: #333;
+}
+
+div.autosuggest ul li
+{
+	color: #ccc;
+	padding: 0;
+	margin: 0 4px 4px;
+	text-align: left;
+}
+
+div.autosuggest ul li a
+{
+	color: #ccc;
+	display: block;
+	text-decoration: none;
+	background-color: transparent;
+	text-shadow: #000 0px 0px 5px;
+	position: relative;
+	padding: 0;
+	width: 100%;
+}
+div.autosuggest ul li a:hover
+{
+	background-color: #444;
+}
+div.autosuggest ul li.as_highlight a:hover
+{
+	background-color: #1B5CCD;
+}
+
+div.autosuggest ul li a span
+{
+	display: block;
+	padding: 3px 6px;
+	font-weight: bold;
+}
+
+div.autosuggest ul li a span small
+{
+	font-weight: normal;
+	color: #999;
+}
+
+div.autosuggest ul li.as_highlight a span small
+{
+	color: #ccc;
+}
+
+div.autosuggest ul li.as_highlight a
+{
+	color: #fff;
+	background-color: #1B5CCD;
+	background-image: url(/ScoDoc/static/icons/hl_corner_br.png);
+	background-position: bottom right;
+	background-repeat: no-repeat;
+}
+
+div.autosuggest ul li.as_highlight a span
+{
+	background-image: url(/ScoDoc/static/icons/hl_corner_bl.png);
+	background-position: bottom left;
+	background-repeat: no-repeat;
+}
+
+div.autosuggest ul li a .tl,
+div.autosuggest ul li a .tr
+{
+	background-image: transparent;
+	background-repeat: no-repeat;
+	width: 6px;
+	height: 6px;
+	position: absolute;
+	top: 0;
+	padding: 0;
+	margin: 0;
+}
+div.autosuggest ul li a .tr
+{
+	right: 0;
+}
+
+div.autosuggest ul li.as_highlight a .tl
+{
+	left: 0;
+	background-image: url(/ScoDoc/static/icons/hl_corner_tl.png);
+	background-position: bottom left;
+}
+
+div.autosuggest ul li.as_highlight a .tr
+{
+	right: 0;
+	background-image: url(/ScoDoc/static/icons/hl_corner_tr.png);
+	background-position: bottom right;
+}
+
+
+
+div.autosuggest ul li.as_warning
+{
+	font-weight: bold;
+	text-align: center;
+}
+
+div.autosuggest ul em
+{
+	font-style: normal;
+	color: #6EADE7;
+}
\ No newline at end of file
diff --git a/static/css/calabs.css b/static/css/calabs.css
new file mode 100644
index 0000000000000000000000000000000000000000..b6e52af491e38c21dee551eb89c1feab781bbaf1
--- /dev/null
+++ b/static/css/calabs.css
@@ -0,0 +1,13 @@
+
+/* feuille style calendrier absences */
+
+table.tcellevt { border-spacing: 0;  border: 0px; padding: 0; border-spacing: 0; border-style: none}
+table.tr.rcellevt { border: 0px; padding: 0  }
+table.td.cellevtleft   { padding: 0; border-style: none; text-align: left; width: 50%; }
+table.td.cellevtright  { padding: 0; border-style: none; text-align: right; width: 50%; }
+
+.maincalendar, .monthcalendar { font-family: monospace; }
+
+tr.currentweek {
+ background-color: yellow;
+}
diff --git a/static/css/groups.css b/static/css/groups.css
new file mode 100644
index 0000000000000000000000000000000000000000..36779c7b8f654370a8030676d7751594917b0249
--- /dev/null
+++ b/static/css/groups.css
@@ -0,0 +1,91 @@
+/* # -*- mode: css -*- 
+           ScoDoc, (c) Emmanuel Viennet 1998 - 2010
+ */
+
+.simpleDropPanel { 
+    float: left;
+    border-width: 2px;
+    border-style: solid;
+    width: 18em;
+    margin: 5px;
+    padding: 3px;
+    -moz-border-radius: 10px;
+    -webkit-border-radius: 10px;
+    border-radius: 10px;
+    background-color: rgb(212, 248, 252);
+}
+
+.activatedPanel {
+    border-color: red;
+}
+
+#_none_ { /* boite des sans groupes */
+    background-color: rgb(190, 190, 190);
+}
+
+#_none_ div.groupTitle0 {
+    background-color: rgb(190, 190, 190);
+}
+
+.groupTitle {
+    font-weight: bold;
+}
+
+div.groupTitle {
+    background-color: rgb(212, 248, 252);
+    padding-left: 2px;
+}
+
+div.groupTitle0 {
+    background-color:rgb(212, 248, 252); 
+}
+
+
+
+#groups {
+    overflow:hidden;
+    background-color: rgb(255, 248, 220);
+    margin-right: 4px;
+    border-width: 2px;
+    border: 2px solid rgb(150, 220, 220);
+}
+
+#groups .barrenav ul li a.custommenu {
+    /* background-color: rgb(212, 248, 252);  */
+    font-size: 12pt;
+    height: 18px;
+    padding-left: 3px;
+    padding-right: 3px;
+    padding-top: 0px;
+    padding-bottom: 0px;
+    text-transform: none;
+}
+
+div.box {
+    margin-left:4px;
+}
+
+#ginfo {
+    margin: 1em;
+}
+
+#savedinfo {
+    text-align: center;
+    font-style: italic; 
+    font-weight: bold;
+    color: red;
+    
+}
+
+#formGroup {
+  margin: 1em;
+}
+
+span.groupDelete {
+    padding-left:3em;
+}
+
+span.newgroup {
+    font-style: italic; 
+    color: red;
+}
diff --git a/static/css/gt_table.css b/static/css/gt_table.css
new file mode 100644
index 0000000000000000000000000000000000000000..934e461421f3c4a3c14fba50e9bbf4bc625f0b8d
--- /dev/null
+++ b/static/css/gt_table.css
@@ -0,0 +1,417 @@
+/* 
+ * DataTables style for ScoDoc gen_tables
+ * generated using https://datatables.net/manual/styling/theme-creator
+ */
+
+/*
+ * Table styles
+ */
+table.dataTable {
+  width: 100%;
+  margin: 0 auto;
+  clear: both;
+  border-collapse: separate;
+  border-spacing: 0;
+  /*
+   * Header and footer styles
+   */
+  /*
+   * Body styles
+   */ }
+  table.dataTable thead th,
+  table.dataTable tfoot th {
+    font-weight: bold; }
+  table.dataTable thead th,
+  table.dataTable thead td {
+    padding: 10px 18px;
+    border-bottom: 1px solid #111111; }
+    table.dataTable thead th:active,
+    table.dataTable thead td:active {
+      outline: none; }
+  table.dataTable tfoot th,
+  table.dataTable tfoot td {
+    padding: 10px 18px 6px 18px;
+    border-top: 1px solid #111111; }
+  table.dataTable thead .sorting,
+  table.dataTable thead .sorting_asc,
+  table.dataTable thead .sorting_desc,
+  table.dataTable thead .sorting_asc_disabled,
+  table.dataTable thead .sorting_desc_disabled {
+    cursor: pointer;
+    *cursor: hand;
+    background-repeat: no-repeat;
+    background-position: center right; }
+  table.dataTable thead .sorting {
+    background-image: url("../images/sort_both.png"); }
+  table.dataTable thead .sorting_asc {
+    background-image: url("../images/sort_asc.png"); }
+  table.dataTable thead .sorting_desc {
+    background-image: url("../images/sort_desc.png"); }
+  table.dataTable thead .sorting_asc_disabled {
+    background-image: url("../images/sort_asc_disabled.png"); }
+  table.dataTable thead .sorting_desc_disabled {
+    background-image: url("../images/sort_desc_disabled.png"); }
+  table.dataTable tbody tr {
+    background-color: white; }
+    table.dataTable tbody tr.selected {
+      background-color: #b0bed9; }
+  table.dataTable tbody th,
+  table.dataTable tbody td {
+    padding: 8px 10px; }
+  table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td {
+    border-top: 1px solid #dddddd; }
+  table.dataTable.row-border tbody tr:first-child th,
+  table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th,
+  table.dataTable.display tbody tr:first-child td {
+    border-top: none; }
+  table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td {
+    border-top: 1px solid #dddddd;
+    border-right: 1px solid #dddddd; }
+  table.dataTable.cell-border tbody tr th:first-child,
+  table.dataTable.cell-border tbody tr td:first-child {
+    border-left: 1px solid #dddddd; }
+  table.dataTable.cell-border tbody tr:first-child th,
+  table.dataTable.cell-border tbody tr:first-child td {
+    border-top: none; }
+  table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd {
+    background-color: #f9f9f9;
+  }
+    table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected {
+      background-color: #abb9d3; }
+  table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover {
+    background-color: whitesmoke; }
+    table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected {
+      background-color: #a9b7d1; }
+  table.dataTable.order-column tbody tr > .sorting_1,
+  table.dataTable.order-column tbody tr > .sorting_2,
+  table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1,
+  table.dataTable.display tbody tr > .sorting_2,
+  table.dataTable.display tbody tr > .sorting_3 {
+    background-color: #f9f9f9;
+   }
+  table.dataTable.order-column tbody tr.selected > .sorting_1,
+  table.dataTable.order-column tbody tr.selected > .sorting_2,
+  table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1,
+  table.dataTable.display tbody tr.selected > .sorting_2,
+  table.dataTable.display tbody tr.selected > .sorting_3 {
+    background-color: #acbad4; }
+  table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
+    background-color: #f1f1f1; }
+  table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
+    background-color: #f3f3f3; }
+  table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
+    background-color: whitesmoke; }
+  table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
+    background-color: #a6b3cd; }
+  table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
+    background-color: #a7b5ce; }
+  table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
+    background-color: #a9b6d0; }
+  table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
+    background-color: #f9f9f9;
+ }
+  table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
+    background-color: #fbfbfb; }
+  table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
+    background-color: #fdfdfd; }
+  table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
+    background-color: #acbad4; }
+  table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
+    background-color: #adbbd6; }
+  table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
+    background-color: #afbdd8; }
+  table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
+    background-color: #eaeaea; }
+  table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
+    background-color: #ebebeb; }
+  table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
+    background-color: #eeeeee; }
+  table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
+    background-color: #a1aec7; }
+  table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
+    background-color: #a2afc8; }
+  table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
+    background-color: #a4b2cb; }
+  table.dataTable.no-footer {
+    border-bottom: 1px solid #111111; }
+  table.dataTable.nowrap th, table.dataTable.nowrap td {
+    white-space: nowrap; }
+  table.dataTable.compact thead th,
+  table.dataTable.compact thead td {
+    padding: 4px 17px 4px 4px; }
+  table.dataTable.compact tfoot th,
+  table.dataTable.compact tfoot td {
+    padding: 4px; }
+  table.dataTable.compact tbody th,
+  table.dataTable.compact tbody td {
+    padding: 4px; }
+  table.dataTable th.dt-left,
+  table.dataTable td.dt-left {
+    text-align: left; }
+  table.dataTable th.dt-center,
+  table.dataTable td.dt-center,
+  table.dataTable td.dataTables_empty {
+    text-align: center; }
+  table.dataTable th.dt-right,
+  table.dataTable td.dt-right {
+    text-align: right; }
+  table.dataTable th.dt-justify,
+  table.dataTable td.dt-justify {
+    text-align: justify; }
+  table.dataTable th.dt-nowrap,
+  table.dataTable td.dt-nowrap {
+    white-space: nowrap; }
+  table.dataTable thead th.dt-head-left,
+  table.dataTable thead td.dt-head-left,
+  table.dataTable tfoot th.dt-head-left,
+  table.dataTable tfoot td.dt-head-left {
+    text-align: left; }
+  table.dataTable thead th.dt-head-center,
+  table.dataTable thead td.dt-head-center,
+  table.dataTable tfoot th.dt-head-center,
+  table.dataTable tfoot td.dt-head-center {
+    text-align: center; }
+  table.dataTable thead th.dt-head-right,
+  table.dataTable thead td.dt-head-right,
+  table.dataTable tfoot th.dt-head-right,
+  table.dataTable tfoot td.dt-head-right {
+    text-align: right; }
+  table.dataTable thead th.dt-head-justify,
+  table.dataTable thead td.dt-head-justify,
+  table.dataTable tfoot th.dt-head-justify,
+  table.dataTable tfoot td.dt-head-justify {
+    text-align: justify; }
+  table.dataTable thead th.dt-head-nowrap,
+  table.dataTable thead td.dt-head-nowrap,
+  table.dataTable tfoot th.dt-head-nowrap,
+  table.dataTable tfoot td.dt-head-nowrap {
+    white-space: nowrap; }
+  table.dataTable tbody th.dt-body-left,
+  table.dataTable tbody td.dt-body-left {
+    text-align: left; }
+  table.dataTable tbody th.dt-body-center,
+  table.dataTable tbody td.dt-body-center {
+    text-align: center; }
+  table.dataTable tbody th.dt-body-right,
+  table.dataTable tbody td.dt-body-right {
+    text-align: right; }
+  table.dataTable tbody th.dt-body-justify,
+  table.dataTable tbody td.dt-body-justify {
+    text-align: justify; }
+  table.dataTable tbody th.dt-body-nowrap,
+  table.dataTable tbody td.dt-body-nowrap {
+    white-space: nowrap; }
+ 
+table.dataTable,
+table.dataTable th,
+table.dataTable td {
+  box-sizing: content-box; }
+ 
+/*
+ * Control feature layout
+ */
+.dataTables_wrapper {
+  position: relative;
+  clear: both;
+  *zoom: 1;
+  zoom: 1; }
+  .dataTables_wrapper .dataTables_length {
+    float: left; }
+  .dataTables_wrapper .dataTables_filter {
+    float: right;
+    text-align: right; }
+    .dataTables_wrapper .dataTables_filter input {
+      margin-left: 0.5em; }
+  .dataTables_wrapper .dataTables_info {
+    clear: both;
+    float: left;
+    padding-top: 0.755em; }
+  .dataTables_wrapper .dataTables_paginate {
+    float: right;
+    text-align: right;
+    padding-top: 0.25em; }
+    .dataTables_wrapper .dataTables_paginate .paginate_button {
+      box-sizing: border-box;
+      display: inline-block;
+      min-width: 1.5em;
+      padding: 0.5em 1em;
+      margin-left: 2px;
+      text-align: center;
+      text-decoration: none !important;
+      cursor: pointer;
+      *cursor: hand;
+      color: #333333 !important;
+      border: 1px solid transparent;
+      border-radius: 2px; }
+      .dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
+        color: #333333 !important;
+        border: 1px solid #979797;
+        background-color: white;
+        background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, gainsboro));
+        /* Chrome,Safari4+ */
+        background: -webkit-linear-gradient(top, white 0%, gainsboro 100%);
+        /* Chrome10+,Safari5.1+ */
+        background: -moz-linear-gradient(top, white 0%, gainsboro 100%);
+        /* FF3.6+ */
+        background: -ms-linear-gradient(top, white 0%, gainsboro 100%);
+        /* IE10+ */
+        background: -o-linear-gradient(top, white 0%, gainsboro 100%);
+        /* Opera 11.10+ */
+        background: linear-gradient(to bottom, white 0%, gainsboro 100%);
+        /* W3C */ }
+      .dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active {
+        cursor: default;
+        color: #666 !important;
+        border: 1px solid transparent;
+        background: transparent;
+        box-shadow: none; }
+      .dataTables_wrapper .dataTables_paginate .paginate_button:hover {
+        color: white !important;
+        border: 1px solid #111111;
+        background-color: #585858;
+        background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111111));
+        /* Chrome,Safari4+ */
+        background: -webkit-linear-gradient(top, #585858 0%, #111111 100%);
+        /* Chrome10+,Safari5.1+ */
+        background: -moz-linear-gradient(top, #585858 0%, #111111 100%);
+        /* FF3.6+ */
+        background: -ms-linear-gradient(top, #585858 0%, #111111 100%);
+        /* IE10+ */
+        background: -o-linear-gradient(top, #585858 0%, #111111 100%);
+        /* Opera 11.10+ */
+        background: linear-gradient(to bottom, #585858 0%, #111111 100%);
+        /* W3C */ }
+      .dataTables_wrapper .dataTables_paginate .paginate_button:active {
+        outline: none;
+        background-color: #2b2b2b;
+        background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));
+        /* Chrome,Safari4+ */
+        background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+        /* Chrome10+,Safari5.1+ */
+        background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+        /* FF3.6+ */
+        background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+        /* IE10+ */
+        background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
+        /* Opera 11.10+ */
+        background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);
+        /* W3C */
+        box-shadow: inset 0 0 3px #111; }
+    .dataTables_wrapper .dataTables_paginate .ellipsis {
+      padding: 0 1em; }
+  .dataTables_wrapper .dataTables_processing {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 100%;
+    height: 40px;
+    margin-left: -50%;
+    margin-top: -25px;
+    padding-top: 20px;
+    text-align: center;
+    font-size: 1.2em;
+    background-color: white;
+    background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
+    background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+    background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+    background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+    background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
+    background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); }
+  .dataTables_wrapper .dataTables_length,
+  .dataTables_wrapper .dataTables_filter,
+  .dataTables_wrapper .dataTables_info,
+  .dataTables_wrapper .dataTables_processing,
+  .dataTables_wrapper .dataTables_paginate {
+    color: #333333; }
+  .dataTables_wrapper .dataTables_scroll {
+    clear: both; }
+    .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody {
+      *margin-top: -1px;
+      -webkit-overflow-scrolling: touch; }
+      .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td {
+        vertical-align: middle; }
+      .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing,
+      .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing,
+      .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing {
+        height: 0;
+        overflow: hidden;
+        margin: 0 !important;
+        padding: 0 !important; }
+  .dataTables_wrapper.no-footer .dataTables_scrollBody {
+    border-bottom: 1px solid #111111; }
+  .dataTables_wrapper.no-footer div.dataTables_scrollHead > table,
+  .dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
+    border-bottom: none; }
+  .dataTables_wrapper:after {
+    visibility: hidden;
+    display: block;
+    content: "";
+    clear: both;
+    height: 0; }
+ 
+@media screen and (max-width: 767px) {
+  .dataTables_wrapper .dataTables_info,
+  .dataTables_wrapper .dataTables_paginate {
+    float: none;
+    text-align: center; }
+  .dataTables_wrapper .dataTables_paginate {
+    margin-top: 0.5em; } }
+@media screen and (max-width: 640px) {
+  .dataTables_wrapper .dataTables_length,
+  .dataTables_wrapper .dataTables_filter {
+    float: none;
+    text-align: center; }
+  .dataTables_wrapper .dataTables_filter {
+    margin-top: 0.5em; } }
+
+
+/* ------ Ajouts spécifiques pour ScoDoc: 
+ */
+
+table.gt_table td { 
+ text-align: right;
+}
+table.gt_table tbody th {
+ font-weight: normal;
+ text-align: left;
+}
+table.table_leftalign tr td {  
+  text-align: left;
+}
+
+/* Ligne(s) de titre */
+table.dataTable thead tr th {
+ background-color: rgb(90%,90%,90%);
+}
+table.dataTable thead tr td {
+ background-color: rgb(95%,95%,95%);
+ border-top: none;
+ border-bottom: none;
+ border-right: 1px solid #dddddd; 
+}
+table.dataTable thead tr td:first-child {
+ border-left:  1px solid #dddddd; 
+}
+
+/* Lignes impaires des tableaux "stripe" */
+table.dataTable.stripe tbody tr.odd td, table.dataTable.stripe tbody tr.odd td.sorting_1, table.dataTable.order-column.stripe tbody tr.odd td.sorting_1 {
+ background-color: rgb(90%,95%,90%);   
+}
+
+table.dataTable.stripe.hover tbody tr.odd:hover td, table.dataTable.stripe.hover tbody tr.odd:hover td.sorting_1, table.dataTable.order-column.stripe.hover tbody tr.odd:hover td.sorting_1 {
+ background-color: rgb(80%,85%,80%);;
+}
+/* Lignes paires */
+table.dataTable.stripe tbody tr.even td, table.dataTable.stripe tbody tr.even td.sorting_1, table.dataTable.order-column.stripe tbody tr.even td.sorting_1 {
+ background-color: rgb(95%,95%,95%);   
+}
+table.dataTable.stripe.hover tbody tr.even:hover td, table.dataTable.stripe.hover tbody tr.even:hover td.sorting_1, table.dataTable.order-column.stripe.hover tbody tr.even:hover td.sorting_1 {
+ background-color: rgb(85%,85%,85%);;
+}
+
+/* Reglage largeur de la table */
+table.dataTable.gt_table {
+ width: auto;
+ padding-right: 5px;
+}
+
diff --git a/static/css/menu.css b/static/css/menu.css
new file mode 100644
index 0000000000000000000000000000000000000000..a4a8ef19d661333d7854cf89444d7b43e640561d
--- /dev/null
+++ b/static/css/menu.css
@@ -0,0 +1,153 @@
+/* # -*- mode: css -*- 
+           ScoDoc, (c) Emmanuel Viennet 1998 - 2014
+ */
+
+.ui-menu {
+    z-index: 1000;
+}
+
+ul.nav {
+    padding: 0px 0px 0px 0px;
+    margin: 0px; 
+}
+
+.barrenav {
+    background-color: rgb(176,176,176) ;
+}
+
+.barrenav ul {
+    list-style-type: none ;
+}
+
+.barrenav ul li {
+    float: left;
+    position: relative ;
+}
+	
+.barrenav ul li img {
+    display: inline ;		
+}
+
+.barrenav ul li a.menu {
+    display: block;
+    text-align: center;
+    text-transform: uppercase;
+    height: 15px;
+    border-style: solid;
+    border-width: 0px 0px 0px 0px;
+    border-color: black;
+    /* color: rgb(255,255,255); */
+    font-size: 10px ;
+    font-weight: bold ;
+    font-family: Arial, Helvetica, sans-serif;
+    padding: 2px 5px 0px 5px ;
+}
+
+.barrenav ul li a.accueil {
+    background-color: rgb(133,000,000) ;
+    width: 102px;
+}
+
+.barrenav ul li a.secretariat {
+    background-color:rgb(183,000,000) ;
+}
+
+.barrenav ul li a.etudiant {
+    background-color:rgb(026,160,101) ;
+}
+
+.barrenav ul li a.direction_etud {
+    background-color:rgb(183,0,0) ;
+}
+
+.barrenav ul li a.bulletin {
+    background-color:rgb(026,160,101) ;
+}
+
+/* inutile avec nouveaux menus jquery-ui
+.barrenav ul li a.custommenu {
+    background-color:rgb(150,230,151);
+    color: black;
+    padding-left: 15px;
+    padding-right: 15px;
+}
+*/
+.barrenav ul li a.menu:after {
+    content:" "url(/ScoDoc/static/icons/menuarrow1_img.png);
+}
+.formsemestre_menubar .barrenav ul li a.menu:after {
+     content:"";
+}
+.formsemestre_menubar .barrenav ul li a.menu {
+    background-color:  #D6E9F8;
+}
+
+.barrenav ul li ul {
+ /* width:350px; */
+ white-space: nowrap;
+ list-style-type: none ;
+  display: none ;
+  z-index: 1000;
+  position: absolute ;
+ margin:0px;
+ padding:0px 0px 0px 0px ;
+ border-style: solid ;
+ border-width:  1px 1px 1px 1px ;
+ border-color: rgb(0,0,0) ;
+ background-color: rgb(255,255,255) ;
+}
+
+.barrenav ul li ul li {
+ display:inline;
+ margin:0px;
+ padding: 0px;
+ font-size: 90%;
+ font-style: normal;
+ font-family: Arial, Helvetica, sans-serif;
+ font-weight: normal;
+ color: black;
+/* ce float none permet d'annuler l'heritage du float left precedent */
+ float: none ;
+/* permet de contrebalancer l'heritage precedent */
+ text-align: left ;
+}
+
+.barrenav ul li ul li a {
+ margin:0px;
+ padding: 3px 3px 0px 3px;
+ display: block ;
+ width: 100% ;
+ padding: 2px 6px 2px -3px ;
+ background-color: rgb(255,255,255) ;
+
+ border-bottom-style: dotted ;
+ border-bottom-width: 1px ;
+ border-bottom-color: rgb(153,153,153) ;
+}
+.barrenav ul li ul li .disabled_menu_item {
+ margin:0px;
+ padding: 3px 3px 0px 3px;
+ display: block ;
+ width: 100% ;
+ padding: 2px 6px 2px -3px ;
+ color: rgb(128,128,128);
+ background-color: rgb(255,255,255) ;
+
+ border-bottom-style: dotted ;
+ border-bottom-width: 1px ;
+ border-bottom-color: rgb(153,153,153) ;
+}
+
+.barrenav ul li ul li a:hover {
+    background: rgb(255,215,0);
+}
+
+.barrenav ul li:hover ul {
+    display: block ;
+}
+
+.barrenav a , .barrenav a:visited , .barrenav a:active , .barrenav a:hover , .barrenav a:focus {
+    text-decoration: none;
+    color: black;
+}
+
diff --git a/static/css/multiple-select.css b/static/css/multiple-select.css
new file mode 100644
index 0000000000000000000000000000000000000000..9727c8241f6bdff74d9bda1100d93c9c522740ce
--- /dev/null
+++ b/static/css/multiple-select.css
@@ -0,0 +1,170 @@
+/**
+ * @author zhixin wen <wenzhixin2010@gmail.com>
+ */
+
+.ms-parent {
+	display: inline-block;
+	position: relative;
+	vertical-align: middle;
+}
+
+.ms-choice {
+	display: block;
+	height: 26px;
+	padding: 0;
+	overflow: hidden;
+	cursor: pointer;
+	border: 1px solid #aaa;
+	text-align: left;
+	white-space: nowrap;
+	line-height: 26px;
+	color: #444;
+	text-decoration: none;
+	-webkit-border-radius: 4px;
+	-moz-border-radius: 4px;
+	border-radius: 4px;
+	background-color: #fff;
+}
+
+.ms-choice.disabled {
+	background-color: #f4f4f4;
+	background-image: none;
+	border: 1px solid #ddd;
+	cursor: default;
+}
+
+.ms-choice > span {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 20px;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: block;
+	padding-left: 8px;
+}
+
+.ms-choice > span.placeholder {
+	color: #999;
+}
+
+.ms-choice > div {
+	position: absolute;
+	top: 0;
+	right: 0;
+	width: 20px;
+	height: 25px;
+	background: url('multiple-select.png') right top no-repeat;
+}
+
+.ms-choice > div.open {
+	background: url('multiple-select.png') left top no-repeat;
+}
+
+.ms-drop {
+	overflow: hidden;
+	display: none;
+	margin-top: -1px;
+	padding: 0;
+	position: absolute;
+	z-index: 1000;
+	top: 100%;
+	background: #fff;
+	color: #000;
+	border: 1px solid #aaa;
+	-webkit-border-radius: 4px;
+	-moz-border-radius: 4px;
+	border-radius: 4px;
+	-webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+	-moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+	box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+}
+
+
+.ms-search {
+	display: inline-block;
+	margin: 0;
+	min-height: 26px;
+	padding: 4px;
+	position: relative;
+	white-space: nowrap;
+	width: 100%;
+	z-index: 10000;
+}
+
+.ms-search input {
+	width: 100%;
+	height: auto !important;
+	min-height: 24px;
+	padding: 0 20px 0 5px;
+	margin: 0;
+	outline: 0;
+	font-family: sans-serif;
+	font-size: 1em;
+	border: 1px solid #aaa;
+	-webkit-border-radius: 0;
+	-moz-border-radius: 0;
+	border-radius: 0;
+	-webkit-box-shadow: none;
+	-moz-box-shadow: none;
+	box-shadow: none;
+	background: #fff url('multiple-select.png') no-repeat 100% -22px;
+	background: url('multiple-select.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
+	background: url('multiple-select.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+	background: url('multiple-select.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+	background: url('multiple-select.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
+	background: url('multiple-select.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+	background: url('multiple-select.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+}
+
+.ms-search, .ms-search input {
+	-webkit-box-sizing: border-box;
+	-khtml-box-sizing: border-box;
+	-moz-box-sizing: border-box;
+	-ms-box-sizing: border-box;
+	box-sizing: border-box;
+}
+
+.ms-drop ul {
+	overflow: auto;
+	margin: 0;
+	padding: 5px 8px;
+}
+
+.ms-drop ul > li {
+	list-style: none;
+	display: list-item;
+	background-image: none;
+	position: static;
+}
+
+.ms-drop ul > li .disabled {
+	opacity: .35;
+	filter: Alpha(Opacity=35);
+}
+
+.ms-drop ul > li.multiple {
+	display: block;
+	float: left;
+}
+
+.ms-drop ul > li.group {
+	clear: both;
+}
+
+.ms-drop ul > li.multiple label {
+	width: 100%;
+	display: block;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.ms-drop ul > li label.optgroup {
+	font-weight: bold;
+}
+
+.ms-drop input[type="checkbox"] {
+	vertical-align: middle;
+}
diff --git a/static/css/multiple-select.png b/static/css/multiple-select.png
new file mode 100644
index 0000000000000000000000000000000000000000..4fd036d38d5bf503158a46ae8e7f77b7777934b8
--- /dev/null
+++ b/static/css/multiple-select.png
@@ -0,0 +1,600 @@
+
+
+
+
+
+<!DOCTYPE html>
+<html>
+  <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# object: http://ogp.me/ns/object# article: http://ogp.me/ns/article# profile: http://ogp.me/ns/profile#">
+    <meta charset='utf-8'>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+        <title>multiple-select/multiple-select.png at master · wenzhixin/multiple-select · GitHub</title>
+    <link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="GitHub" />
+    <link rel="fluid-icon" href="https://github.com/fluidicon.png" title="GitHub" />
+    <link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-114.png" />
+    <link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114.png" />
+    <link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-144.png" />
+    <link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144.png" />
+    <meta property="fb:app_id" content="1401488693436528"/>
+
+      <meta content="@github" name="twitter:site" /><meta content="summary" name="twitter:card" /><meta content="wenzhixin/multiple-select" name="twitter:title" /><meta content="multiple-select - A jQuery plugin to select multiple elements with checkboxes :)" name="twitter:description" /><meta content="https://2.gravatar.com/avatar/385b62f540e4bc0888f34ddd3f555949?d=https%3A%2F%2Fidenticons.github.com%2F5977911f8b7723464c75f4fbfb7fd2c2.png&amp;r=x&amp;s=400" name="twitter:image:src" />
+<meta content="GitHub" property="og:site_name" /><meta content="object" property="og:type" /><meta content="https://2.gravatar.com/avatar/385b62f540e4bc0888f34ddd3f555949?d=https%3A%2F%2Fidenticons.github.com%2F5977911f8b7723464c75f4fbfb7fd2c2.png&amp;r=x&amp;s=400" property="og:image" /><meta content="wenzhixin/multiple-select" property="og:title" /><meta content="https://github.com/wenzhixin/multiple-select" property="og:url" /><meta content="multiple-select - A jQuery plugin to select multiple elements with checkboxes :)" property="og:description" />
+
+    <meta name="hostname" content="github-fe118-cp1-prd.iad.github.net">
+    <meta name="ruby" content="ruby 2.1.0p0-github-tcmalloc (87d8860372) [x86_64-linux]">
+    <link rel="assets" href="https://github.global.ssl.fastly.net/">
+    <link rel="conduit-xhr" href="https://ghconduit.com:25035/">
+    <link rel="xhr-socket" href="/_sockets" />
+
+
+    <meta name="msapplication-TileImage" content="/windows-tile.png" />
+    <meta name="msapplication-TileColor" content="#ffffff" />
+    <meta name="selected-link" value="repo_source" data-pjax-transient />
+    <meta content="collector.githubapp.com" name="octolytics-host" /><meta content="collector-cdn.github.com" name="octolytics-script-host" /><meta content="github" name="octolytics-app-id" /><meta content="51392B97:47D4:713F89:52FCFE6D" name="octolytics-dimension-request_id" />
+    
+
+    
+    
+    <link rel="icon" type="image/x-icon" href="/favicon.ico" />
+
+    <meta content="authenticity_token" name="csrf-param" />
+<meta content="D09AqUY7ZgVI44xfxMnTIHtRF+H5rVKtIiNQ0VQwkgI=" name="csrf-token" />
+
+    <link href="https://github.global.ssl.fastly.net/assets/github-1d6e450b54230462761f7f0fb692c60039b0e67c.css" media="all" rel="stylesheet" type="text/css" />
+    <link href="https://github.global.ssl.fastly.net/assets/github2-b4dcbb73b0e43228627f42f62e0587eed9d692fb.css" media="all" rel="stylesheet" type="text/css" />
+    
+
+
+      <script src="https://github.global.ssl.fastly.net/assets/frameworks-693e11922dcacc3a7408a911fe1647da4febd3bd.js" type="text/javascript"></script>
+      <script async="async" defer="defer" src="https://github.global.ssl.fastly.net/assets/github-b741f7e70431fc287ccebb131cd57d59fee617b5.js" type="text/javascript"></script>
+      
+      <meta http-equiv="x-pjax-version" content="8a2e8fb661b4aa49c088cdc4b6f4622b">
+
+        <link data-pjax-transient rel='permalink' href='/wenzhixin/multiple-select/blob/5a4c3b7292f8c3f861ef76c6271f40cd6d51aba6/multiple-select.png'>
+
+  <meta name="description" content="multiple-select - A jQuery plugin to select multiple elements with checkboxes :)" />
+
+  <meta content="2117018" name="octolytics-dimension-user_id" /><meta content="wenzhixin" name="octolytics-dimension-user_login" /><meta content="9517907" name="octolytics-dimension-repository_id" /><meta content="wenzhixin/multiple-select" name="octolytics-dimension-repository_nwo" /><meta content="true" name="octolytics-dimension-repository_public" /><meta content="false" name="octolytics-dimension-repository_is_fork" /><meta content="9517907" name="octolytics-dimension-repository_network_root_id" /><meta content="wenzhixin/multiple-select" name="octolytics-dimension-repository_network_root_nwo" />
+  <link href="https://github.com/wenzhixin/multiple-select/commits/master.atom" rel="alternate" title="Recent Commits to multiple-select:master" type="application/atom+xml" />
+
+  </head>
+
+
+  <body class="logged_out  env-production macintosh vis-public page-blob">
+    <div class="wrapper">
+      
+      
+      
+      
+
+
+      
+      <div class="header header-logged-out">
+  <div class="container clearfix">
+
+    <a class="header-logo-wordmark" href="https://github.com/">
+      <span class="mega-octicon octicon-logo-github"></span>
+    </a>
+
+    <div class="header-actions">
+        <a class="button primary" href="/join">Sign up</a>
+      <a class="button signin" href="/login?return_to=%2Fwenzhixin%2Fmultiple-select%2Fblob%2Fmaster%2Fmultiple-select.png">Sign in</a>
+    </div>
+
+    <div class="command-bar js-command-bar  in-repository">
+
+      <ul class="top-nav">
+          <li class="explore"><a href="/explore">Explore</a></li>
+        <li class="features"><a href="/features">Features</a></li>
+          <li class="enterprise"><a href="https://enterprise.github.com/">Enterprise</a></li>
+          <li class="blog"><a href="/blog">Blog</a></li>
+      </ul>
+        <form accept-charset="UTF-8" action="/search" class="command-bar-form" id="top_search_form" method="get">
+
+<input type="text" data-hotkey="/ s" name="q" id="js-command-bar-field" placeholder="Search or type a command" tabindex="1" autocapitalize="off"
+    
+    
+      data-repo="wenzhixin/multiple-select"
+      data-branch="master"
+      data-sha="cf7abf3e705edc88334591c1898efcf20b9e522f"
+  >
+
+    <input type="hidden" name="nwo" value="wenzhixin/multiple-select" />
+
+    <div class="select-menu js-menu-container js-select-menu search-context-select-menu">
+      <span class="minibutton select-menu-button js-menu-target">
+        <span class="js-select-button">This repository</span>
+      </span>
+
+      <div class="select-menu-modal-holder js-menu-content js-navigation-container">
+        <div class="select-menu-modal">
+
+          <div class="select-menu-item js-navigation-item js-this-repository-navigation-item selected">
+            <span class="select-menu-item-icon octicon octicon-check"></span>
+            <input type="radio" class="js-search-this-repository" name="search_target" value="repository" checked="checked" />
+            <div class="select-menu-item-text js-select-button-text">This repository</div>
+          </div> <!-- /.select-menu-item -->
+
+          <div class="select-menu-item js-navigation-item js-all-repositories-navigation-item">
+            <span class="select-menu-item-icon octicon octicon-check"></span>
+            <input type="radio" name="search_target" value="global" />
+            <div class="select-menu-item-text js-select-button-text">All repositories</div>
+          </div> <!-- /.select-menu-item -->
+
+        </div>
+      </div>
+    </div>
+
+  <span class="octicon help tooltipped downwards" aria-label="Show command bar help">
+    <span class="octicon octicon-question"></span>
+  </span>
+
+
+  <input type="hidden" name="ref" value="cmdform">
+
+</form>
+    </div>
+
+  </div>
+</div>
+
+
+
+
+          <div class="site" itemscope itemtype="http://schema.org/WebPage">
+    
+    <div class="pagehead repohead instapaper_ignore readability-menu">
+      <div class="container">
+        
+
+<ul class="pagehead-actions">
+
+
+  <li>
+    <a href="/login?return_to=%2Fwenzhixin%2Fmultiple-select"
+    class="minibutton with-count js-toggler-target star-button tooltipped upwards"
+    aria-label="You must be signed in to use this feature" rel="nofollow">
+    <span class="octicon octicon-star"></span>Star
+  </a>
+
+    <a class="social-count js-social-count" href="/wenzhixin/multiple-select/stargazers">
+      81
+    </a>
+
+  </li>
+
+    <li>
+      <a href="/login?return_to=%2Fwenzhixin%2Fmultiple-select"
+        class="minibutton with-count js-toggler-target fork-button tooltipped upwards"
+        aria-label="You must be signed in to fork a repository" rel="nofollow">
+        <span class="octicon octicon-git-branch"></span>Fork
+      </a>
+      <a href="/wenzhixin/multiple-select/network" class="social-count">
+        50
+      </a>
+    </li>
+</ul>
+
+        <h1 itemscope itemtype="http://data-vocabulary.org/Breadcrumb" class="entry-title public">
+          <span class="repo-label"><span>public</span></span>
+          <span class="mega-octicon octicon-repo"></span>
+          <span class="author">
+            <a href="/wenzhixin" class="url fn" itemprop="url" rel="author"><span itemprop="title">wenzhixin</span></a>
+          </span>
+          <span class="repohead-name-divider">/</span>
+          <strong><a href="/wenzhixin/multiple-select" class="js-current-repository js-repo-home-link">multiple-select</a></strong>
+
+          <span class="page-context-loader">
+            <img alt="Octocat-spinner-32" height="16" src="https://github.global.ssl.fastly.net/images/spinners/octocat-spinner-32.gif" width="16" />
+          </span>
+
+        </h1>
+      </div><!-- /.container -->
+    </div><!-- /.repohead -->
+
+    <div class="container">
+      
+
+      <div class="repository-with-sidebar repo-container new-discussion-timeline js-new-discussion-timeline  ">
+        <div class="repository-sidebar">
+            
+
+<div class="sunken-menu vertical-right repo-nav js-repo-nav js-repository-container-pjax js-octicon-loaders">
+  <div class="sunken-menu-contents">
+    <ul class="sunken-menu-group">
+      <li class="tooltipped leftwards" aria-label="Code">
+        <a href="/wenzhixin/multiple-select" aria-label="Code" class="selected js-selected-navigation-item sunken-menu-item" data-gotokey="c" data-pjax="true" data-selected-links="repo_source repo_downloads repo_commits repo_tags repo_branches /wenzhixin/multiple-select">
+          <span class="octicon octicon-code"></span> <span class="full-word">Code</span>
+          <img alt="Octocat-spinner-32" class="mini-loader" height="16" src="https://github.global.ssl.fastly.net/images/spinners/octocat-spinner-32.gif" width="16" />
+</a>      </li>
+
+        <li class="tooltipped leftwards" aria-label="Issues">
+          <a href="/wenzhixin/multiple-select/issues" aria-label="Issues" class="js-selected-navigation-item sunken-menu-item js-disable-pjax" data-gotokey="i" data-selected-links="repo_issues /wenzhixin/multiple-select/issues">
+            <span class="octicon octicon-issue-opened"></span> <span class="full-word">Issues</span>
+            <span class='counter'>10</span>
+            <img alt="Octocat-spinner-32" class="mini-loader" height="16" src="https://github.global.ssl.fastly.net/images/spinners/octocat-spinner-32.gif" width="16" />
+</a>        </li>
+
+      <li class="tooltipped leftwards" aria-label="Pull Requests">
+        <a href="/wenzhixin/multiple-select/pulls" aria-label="Pull Requests" class="js-selected-navigation-item sunken-menu-item js-disable-pjax" data-gotokey="p" data-selected-links="repo_pulls /wenzhixin/multiple-select/pulls">
+            <span class="octicon octicon-git-pull-request"></span> <span class="full-word">Pull Requests</span>
+            <span class='counter'>0</span>
+            <img alt="Octocat-spinner-32" class="mini-loader" height="16" src="https://github.global.ssl.fastly.net/images/spinners/octocat-spinner-32.gif" width="16" />
+</a>      </li>
+
+
+        <li class="tooltipped leftwards" aria-label="Wiki">
+          <a href="/wenzhixin/multiple-select/wiki" aria-label="Wiki" class="js-selected-navigation-item sunken-menu-item" data-pjax="true" data-selected-links="repo_wiki /wenzhixin/multiple-select/wiki">
+            <span class="octicon octicon-book"></span> <span class="full-word">Wiki</span>
+            <img alt="Octocat-spinner-32" class="mini-loader" height="16" src="https://github.global.ssl.fastly.net/images/spinners/octocat-spinner-32.gif" width="16" />
+</a>        </li>
+    </ul>
+    <div class="sunken-menu-separator"></div>
+    <ul class="sunken-menu-group">
+
+      <li class="tooltipped leftwards" aria-label="Pulse">
+        <a href="/wenzhixin/multiple-select/pulse" aria-label="Pulse" class="js-selected-navigation-item sunken-menu-item" data-pjax="true" data-selected-links="pulse /wenzhixin/multiple-select/pulse">
+          <span class="octicon octicon-pulse"></span> <span class="full-word">Pulse</span>
+          <img alt="Octocat-spinner-32" class="mini-loader" height="16" src="https://github.global.ssl.fastly.net/images/spinners/octocat-spinner-32.gif" width="16" />
+</a>      </li>
+
+      <li class="tooltipped leftwards" aria-label="Graphs">
+        <a href="/wenzhixin/multiple-select/graphs" aria-label="Graphs" class="js-selected-navigation-item sunken-menu-item" data-pjax="true" data-selected-links="repo_graphs repo_contributors /wenzhixin/multiple-select/graphs">
+          <span class="octicon octicon-graph"></span> <span class="full-word">Graphs</span>
+          <img alt="Octocat-spinner-32" class="mini-loader" height="16" src="https://github.global.ssl.fastly.net/images/spinners/octocat-spinner-32.gif" width="16" />
+</a>      </li>
+
+      <li class="tooltipped leftwards" aria-label="Network">
+        <a href="/wenzhixin/multiple-select/network" aria-label="Network" class="js-selected-navigation-item sunken-menu-item js-disable-pjax" data-selected-links="repo_network /wenzhixin/multiple-select/network">
+          <span class="octicon octicon-git-branch"></span> <span class="full-word">Network</span>
+          <img alt="Octocat-spinner-32" class="mini-loader" height="16" src="https://github.global.ssl.fastly.net/images/spinners/octocat-spinner-32.gif" width="16" />
+</a>      </li>
+    </ul>
+
+
+  </div>
+</div>
+
+              <div class="only-with-full-nav">
+                
+
+  
+
+<div class="clone-url open"
+  data-protocol-type="http"
+  data-url="/users/set_protocol?protocol_selector=http&amp;protocol_type=clone">
+  <h3><strong>HTTPS</strong> clone URL</h3>
+  <div class="clone-url-box">
+    <input type="text" class="clone js-url-field"
+           value="https://github.com/wenzhixin/multiple-select.git" readonly="readonly">
+
+    <span class="js-zeroclipboard url-box-clippy minibutton zeroclipboard-button" data-clipboard-text="https://github.com/wenzhixin/multiple-select.git" data-copied-hint="copied!" title="copy to clipboard"><span class="octicon octicon-clippy"></span></span>
+  </div>
+</div>
+
+  
+
+<div class="clone-url "
+  data-protocol-type="subversion"
+  data-url="/users/set_protocol?protocol_selector=subversion&amp;protocol_type=clone">
+  <h3><strong>Subversion</strong> checkout URL</h3>
+  <div class="clone-url-box">
+    <input type="text" class="clone js-url-field"
+           value="https://github.com/wenzhixin/multiple-select" readonly="readonly">
+
+    <span class="js-zeroclipboard url-box-clippy minibutton zeroclipboard-button" data-clipboard-text="https://github.com/wenzhixin/multiple-select" data-copied-hint="copied!" title="copy to clipboard"><span class="octicon octicon-clippy"></span></span>
+  </div>
+</div>
+
+
+<p class="clone-options">You can clone with
+      <a href="#" class="js-clone-selector" data-protocol="http">HTTPS</a>,
+      or <a href="#" class="js-clone-selector" data-protocol="subversion">Subversion</a>.
+  <span class="octicon help tooltipped upwards" aria-label="Get help on which URL is right for you.">
+    <a href="https://help.github.com/articles/which-remote-url-should-i-use">
+    <span class="octicon octicon-question"></span>
+    </a>
+  </span>
+</p>
+
+  <a href="http://mac.github.com" data-url="github-mac://openRepo/https://github.com/wenzhixin/multiple-select" class="minibutton sidebar-button js-conduit-rewrite-url">
+    <span class="octicon octicon-device-desktop"></span>
+    Clone in Desktop
+  </a>
+
+
+                <a href="/wenzhixin/multiple-select/archive/master.zip"
+                   class="minibutton sidebar-button"
+                   title="Download this repository as a zip file"
+                   rel="nofollow">
+                  <span class="octicon octicon-cloud-download"></span>
+                  Download ZIP
+                </a>
+              </div>
+        </div><!-- /.repository-sidebar -->
+
+        <div id="js-repo-pjax-container" class="repository-content context-loader-container" data-pjax-container>
+          
+
+
+<!-- blob contrib key: blob_contributors:v21:aad7b6119ad03b593f2c4e9a13481ee5 -->
+
+<p title="This is a placeholder element" class="js-history-link-replace hidden"></p>
+
+<a href="/wenzhixin/multiple-select/find/master" data-pjax data-hotkey="t" class="js-show-file-finder" style="display:none">Show File Finder</a>
+
+<div class="file-navigation">
+  
+
+<div class="select-menu js-menu-container js-select-menu" >
+  <span class="minibutton select-menu-button js-menu-target" data-hotkey="w"
+    data-master-branch="master"
+    data-ref="master"
+    role="button" aria-label="Switch branches or tags" tabindex="0">
+    <span class="octicon octicon-git-branch"></span>
+    <i>branch:</i>
+    <span class="js-select-button">master</span>
+  </span>
+
+  <div class="select-menu-modal-holder js-menu-content js-navigation-container" data-pjax>
+
+    <div class="select-menu-modal">
+      <div class="select-menu-header">
+        <span class="select-menu-title">Switch branches/tags</span>
+        <span class="octicon octicon-remove-close js-menu-close"></span>
+      </div> <!-- /.select-menu-header -->
+
+      <div class="select-menu-filters">
+        <div class="select-menu-text-filter">
+          <input type="text" aria-label="Filter branches/tags" id="context-commitish-filter-field" class="js-filterable-field js-navigation-enable" placeholder="Filter branches/tags">
+        </div>
+        <div class="select-menu-tabs">
+          <ul>
+            <li class="select-menu-tab">
+              <a href="#" data-tab-filter="branches" class="js-select-menu-tab">Branches</a>
+            </li>
+            <li class="select-menu-tab">
+              <a href="#" data-tab-filter="tags" class="js-select-menu-tab">Tags</a>
+            </li>
+          </ul>
+        </div><!-- /.select-menu-tabs -->
+      </div><!-- /.select-menu-filters -->
+
+      <div class="select-menu-list select-menu-tab-bucket js-select-menu-tab-bucket" data-tab-filter="branches">
+
+        <div data-filterable-for="context-commitish-filter-field" data-filterable-type="substring">
+
+
+            <div class="select-menu-item js-navigation-item ">
+              <span class="select-menu-item-icon octicon octicon-check"></span>
+              <a href="/wenzhixin/multiple-select/blob/gh-pages/multiple-select.png"
+                 data-name="gh-pages"
+                 data-skip-pjax="true"
+                 rel="nofollow"
+                 class="js-navigation-open select-menu-item-text js-select-button-text css-truncate-target"
+                 title="gh-pages">gh-pages</a>
+            </div> <!-- /.select-menu-item -->
+            <div class="select-menu-item js-navigation-item selected">
+              <span class="select-menu-item-icon octicon octicon-check"></span>
+              <a href="/wenzhixin/multiple-select/blob/master/multiple-select.png"
+                 data-name="master"
+                 data-skip-pjax="true"
+                 rel="nofollow"
+                 class="js-navigation-open select-menu-item-text js-select-button-text css-truncate-target"
+                 title="master">master</a>
+            </div> <!-- /.select-menu-item -->
+        </div>
+
+          <div class="select-menu-no-results">Nothing to show</div>
+      </div> <!-- /.select-menu-list -->
+
+      <div class="select-menu-list select-menu-tab-bucket js-select-menu-tab-bucket" data-tab-filter="tags">
+        <div data-filterable-for="context-commitish-filter-field" data-filterable-type="substring">
+
+
+            <div class="select-menu-item js-navigation-item ">
+              <span class="select-menu-item-icon octicon octicon-check"></span>
+              <a href="/wenzhixin/multiple-select/tree/1.0.5/multiple-select.png"
+                 data-name="1.0.5"
+                 data-skip-pjax="true"
+                 rel="nofollow"
+                 class="js-navigation-open select-menu-item-text js-select-button-text css-truncate-target"
+                 title="1.0.5">1.0.5</a>
+            </div> <!-- /.select-menu-item -->
+            <div class="select-menu-item js-navigation-item ">
+              <span class="select-menu-item-icon octicon octicon-check"></span>
+              <a href="/wenzhixin/multiple-select/tree/1.0.4/multiple-select.png"
+                 data-name="1.0.4"
+                 data-skip-pjax="true"
+                 rel="nofollow"
+                 class="js-navigation-open select-menu-item-text js-select-button-text css-truncate-target"
+                 title="1.0.4">1.0.4</a>
+            </div> <!-- /.select-menu-item -->
+            <div class="select-menu-item js-navigation-item ">
+              <span class="select-menu-item-icon octicon octicon-check"></span>
+              <a href="/wenzhixin/multiple-select/tree/1.0.3/multiple-select.png"
+                 data-name="1.0.3"
+                 data-skip-pjax="true"
+                 rel="nofollow"
+                 class="js-navigation-open select-menu-item-text js-select-button-text css-truncate-target"
+                 title="1.0.3">1.0.3</a>
+            </div> <!-- /.select-menu-item -->
+            <div class="select-menu-item js-navigation-item ">
+              <span class="select-menu-item-icon octicon octicon-check"></span>
+              <a href="/wenzhixin/multiple-select/tree/1.0.2/multiple-select.png"
+                 data-name="1.0.2"
+                 data-skip-pjax="true"
+                 rel="nofollow"
+                 class="js-navigation-open select-menu-item-text js-select-button-text css-truncate-target"
+                 title="1.0.2">1.0.2</a>
+            </div> <!-- /.select-menu-item -->
+            <div class="select-menu-item js-navigation-item ">
+              <span class="select-menu-item-icon octicon octicon-check"></span>
+              <a href="/wenzhixin/multiple-select/tree/1.0.1/multiple-select.png"
+                 data-name="1.0.1"
+                 data-skip-pjax="true"
+                 rel="nofollow"
+                 class="js-navigation-open select-menu-item-text js-select-button-text css-truncate-target"
+                 title="1.0.1">1.0.1</a>
+            </div> <!-- /.select-menu-item -->
+            <div class="select-menu-item js-navigation-item ">
+              <span class="select-menu-item-icon octicon octicon-check"></span>
+              <a href="/wenzhixin/multiple-select/tree/1.0.0/multiple-select.png"
+                 data-name="1.0.0"
+                 data-skip-pjax="true"
+                 rel="nofollow"
+                 class="js-navigation-open select-menu-item-text js-select-button-text css-truncate-target"
+                 title="1.0.0">1.0.0</a>
+            </div> <!-- /.select-menu-item -->
+        </div>
+
+        <div class="select-menu-no-results">Nothing to show</div>
+      </div> <!-- /.select-menu-list -->
+
+    </div> <!-- /.select-menu-modal -->
+  </div> <!-- /.select-menu-modal-holder -->
+</div> <!-- /.select-menu -->
+
+  <div class="breadcrumb">
+    <span class='repo-root js-repo-root'><span itemscope="" itemtype="http://data-vocabulary.org/Breadcrumb"><a href="/wenzhixin/multiple-select" data-branch="master" data-direction="back" data-pjax="true" itemscope="url"><span itemprop="title">multiple-select</span></a></span></span><span class="separator"> / </span><strong class="final-path">multiple-select.png</strong> <span class="js-zeroclipboard minibutton zeroclipboard-button" data-clipboard-text="multiple-select.png" data-copied-hint="copied!" title="copy to clipboard"><span class="octicon octicon-clippy"></span></span>
+  </div>
+</div>
+
+
+  <div class="commit file-history-tease">
+    <img class="main-avatar" height="24" src="https://0.gravatar.com/avatar/385b62f540e4bc0888f34ddd3f555949?d=https%3A%2F%2Fidenticons.github.com%2F5977911f8b7723464c75f4fbfb7fd2c2.png&amp;r=x&amp;s=140" width="24" />
+    <span class="author"><a href="/wenzhixin" rel="author">wenzhixin</a></span>
+    <time class="js-relative-date" datetime="2013-06-04T23:02:40-07:00" title="2013-06-04 23:02:40">June 04, 2013</time>
+    <div class="commit-title">
+        <a href="/wenzhixin/multiple-select/commit/36e583847a9486fd9b9decce339e72959235cf5f" class="message" data-pjax="true" title="Fix #4: Add filter property to allow search through checkbox items.">Fix</a> <a href="https://github.com/wenzhixin/multiple-select/issues/4" class="issue-link" title="Allow search through checkbox items">#4</a><a href="/wenzhixin/multiple-select/commit/36e583847a9486fd9b9decce339e72959235cf5f" class="message" data-pjax="true" title="Fix #4: Add filter property to allow search through checkbox items.">: Add filter property to allow search through checkbox items.</a>
+    </div>
+
+    <div class="participation">
+      <p class="quickstat"><a href="#blob_contributors_box" rel="facebox"><strong>1</strong> contributor</a></p>
+      
+    </div>
+    <div id="blob_contributors_box" style="display:none">
+      <h2 class="facebox-header">Users who have contributed to this file</h2>
+      <ul class="facebox-user-list">
+          <li class="facebox-user-list-item">
+            <img height="24" src="https://0.gravatar.com/avatar/385b62f540e4bc0888f34ddd3f555949?d=https%3A%2F%2Fidenticons.github.com%2F5977911f8b7723464c75f4fbfb7fd2c2.png&amp;r=x&amp;s=140" width="24" />
+            <a href="/wenzhixin">wenzhixin</a>
+          </li>
+      </ul>
+    </div>
+  </div>
+
+<div class="file-box">
+  <div class="file">
+    <div class="meta">
+      <div class="info file-name">
+        <span class="icon"><b class="octicon octicon-file-text"></b></span>
+        <span class="mode" title="File Mode">file</span>
+        <span class="meta-divider"></span>
+        <span>3.342 kb</span>
+      </div>
+      <div class="actions">
+        <div class="button-group">
+            <a class="minibutton tooltipped leftwards js-conduit-openfile-check"
+               href="http://mac.github.com"
+               data-url="github-mac://openRepo/https://github.com/wenzhixin/multiple-select?branch=master&amp;filepath=multiple-select.png"
+               aria-label="Open this file in GitHub for Mac"
+               data-failed-title="Your version of GitHub for Mac is too old to open this file. Try checking for updates.">
+                <span class="octicon octicon-device-desktop"></span> Open
+            </a>
+          <a href="/wenzhixin/multiple-select/raw/master/multiple-select.png" class="button minibutton " id="raw-url">Raw</a>
+          <a href="/wenzhixin/multiple-select/commits/master/multiple-select.png" class="button minibutton " rel="nofollow">History</a>
+        </div><!-- /.button-group -->
+          <a class="minibutton danger disabled empty-icon tooltipped leftwards" href="#"
+             aria-label="You must be signed in to make or propose changes">
+          Delete
+        </a>
+      </div><!-- /.actions -->
+    </div>
+        <div class="blob-wrapper data type-text js-blob-data">
+      <div class="image js-image">
+          <span class="border-wrap"><img src="/wenzhixin/multiple-select/blob/master/multiple-select.png?raw=true" /></span>
+      </div>
+  </div>
+
+  </div>
+</div>
+
+<a href="#jump-to-line" rel="facebox[.linejump]" data-hotkey="l" class="js-jump-to-line" style="display:none">Jump to Line</a>
+<div id="jump-to-line" style="display:none">
+  <form accept-charset="UTF-8" class="js-jump-to-line-form">
+    <input class="linejump-input js-jump-to-line-field" type="text" placeholder="Jump to line&hellip;" autofocus>
+    <button type="submit" class="button">Go</button>
+  </form>
+</div>
+
+        </div>
+
+      </div><!-- /.repo-container -->
+      <div class="modal-backdrop"></div>
+    </div><!-- /.container -->
+  </div><!-- /.site -->
+
+
+    </div><!-- /.wrapper -->
+
+      <div class="container">
+  <div class="site-footer">
+    <ul class="site-footer-links right">
+      <li><a href="https://status.github.com/">Status</a></li>
+      <li><a href="http://developer.github.com">API</a></li>
+      <li><a href="http://training.github.com">Training</a></li>
+      <li><a href="http://shop.github.com">Shop</a></li>
+      <li><a href="/blog">Blog</a></li>
+      <li><a href="/about">About</a></li>
+
+    </ul>
+
+    <a href="/">
+      <span class="mega-octicon octicon-mark-github" title="GitHub"></span>
+    </a>
+
+    <ul class="site-footer-links">
+      <li>&copy; 2014 <span title="0.03915s from github-fe118-cp1-prd.iad.github.net">GitHub</span>, Inc.</li>
+        <li><a href="/site/terms">Terms</a></li>
+        <li><a href="/site/privacy">Privacy</a></li>
+        <li><a href="/security">Security</a></li>
+        <li><a href="/contact">Contact</a></li>
+    </ul>
+  </div><!-- /.site-footer -->
+</div><!-- /.container -->
+
+
+    <div class="fullscreen-overlay js-fullscreen-overlay" id="fullscreen_overlay">
+  <div class="fullscreen-container js-fullscreen-container">
+    <div class="textarea-wrap">
+      <textarea name="fullscreen-contents" id="fullscreen-contents" class="js-fullscreen-contents" placeholder="" data-suggester="fullscreen_suggester"></textarea>
+          <div class="suggester-container">
+              <div class="suggester fullscreen-suggester js-navigation-container" id="fullscreen_suggester"
+                 data-url="/wenzhixin/multiple-select/suggestions/commit">
+              </div>
+          </div>
+    </div>
+  </div>
+  <div class="fullscreen-sidebar">
+    <a href="#" class="exit-fullscreen js-exit-fullscreen tooltipped leftwards" aria-label="Exit Zen Mode">
+      <span class="mega-octicon octicon-screen-normal"></span>
+    </a>
+    <a href="#" class="theme-switcher js-theme-switcher tooltipped leftwards"
+      aria-label="Switch themes">
+      <span class="octicon octicon-color-mode"></span>
+    </a>
+  </div>
+</div>
+
+
+
+    <div id="ajax-error-message" class="flash flash-error">
+      <span class="octicon octicon-alert"></span>
+      <a href="#" class="octicon octicon-remove-close close js-ajax-error-dismiss"></a>
+      Something went wrong with that request. Please try again.
+    </div>
+
+  </body>
+</html>
+
diff --git a/static/css/radar_bulletin.css b/static/css/radar_bulletin.css
new file mode 100644
index 0000000000000000000000000000000000000000..b0d7df0b7d3cfca1a7354c4519bd43e7e677c40a
--- /dev/null
+++ b/static/css/radar_bulletin.css
@@ -0,0 +1,90 @@
+/* # -*- mode: css -*- 
+           ScoDoc, (c) Emmanuel Viennet 1998 - 2012
+ */
+
+div#radar_bulletin {
+    width: 500px;
+    margin-left: auto ;
+    margin-right: auto ;
+}
+
+.radar {
+    font-family: sans-serif;
+    font-size: 12pt;
+}
+
+.radar rect {
+    stroke: white;
+    fill: steelblue;
+}
+
+path {
+    fill:none;
+    stroke:gray;
+}
+
+path.radarnoteslines {
+    stroke: darkblue;
+    stroke-width: 1.5px;
+}
+
+path.radarmoylines {
+    stroke: rgb(20,90,50);;
+    stroke-width: 1.25px;
+    stroke-dasharray: 8, 4;
+}
+
+line.radarrad {
+    stroke: rgb(150, 150, 150);
+}
+
+circle.radar_disk_tick {
+    stroke-width: 1;
+    stroke : rgb(150,150,150);
+    fill : none;
+}
+circle.radar_disk_red {
+    stroke-width: 0px;
+    fill: #FFC0CB;
+    fill-opacity: 0.5;
+}
+
+path#radar_disk_tic_8 {
+    fill: #FF493C;
+    fill-opacity: 0.5;
+}
+
+path#radar_disk_tic_10 {
+    fill: #FFC0CB;
+    fill-opacity: 0.5;
+    stroke-width: 1.5px;
+    stroke: #F44646;
+}
+
+path#radar_disk_tic_20 {
+    fill: grey;
+    fill-opacity: 0.1;
+}
+
+.radar_center_mark {
+    stroke: red;
+    stroke-width : 2;
+    fill: red;
+}
+
+rect.radartip {
+    fill: #F5FB84;
+    font-size: 10pt;
+}
+
+text.textaxis {
+    fill: red;
+    font-size: 8pt;
+    text-anchor: end;
+}
+
+text.note_label {
+    fill: darkblue;
+    font-size: 10pt;
+    font-weight: bold;
+}
diff --git a/static/css/rickshaw.min.css b/static/css/rickshaw.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..89f258caf5f2faa1b9a894e29052e94d69c5ec56
--- /dev/null
+++ b/static/css/rickshaw.min.css
@@ -0,0 +1 @@
+.rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-2px;margin-top:-2px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px}
\ No newline at end of file
diff --git a/static/css/scodoc.css b/static/css/scodoc.css
new file mode 100644
index 0000000000000000000000000000000000000000..9b7868748e8cd3a2a5afbbcdc5abe1914a5565af
--- /dev/null
+++ b/static/css/scodoc.css
@@ -0,0 +1,2895 @@
+/* # -*- mode: css -*- 
+           ScoDoc, (c) Emmanuel Viennet 1998 - 2020
+ */
+
+html,body {
+  margin:0;
+  padding:0;
+  width: 100%;
+  background-color: rgb(242,242,238);
+  font-family:   "Helvetica Neue",Helvetica,Arial,sans-serif;
+  font-size: 12pt;
+}
+
+
+@media print {
+.noprint {
+  display:none;
+}
+}
+
+h1, h2, h3 {
+ font-family : "Helvetica Neue",Helvetica,Arial,sans-serif;
+}
+
+h3 {
+ font-size: 14pt;
+ font-weight: bold;
+}
+
+.scotext {
+ font-family : TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia, serif;
+}
+
+.sco-hidden {
+    display: None;
+}
+
+div.tab-content {
+  margin-top: 10px;
+  margin-left: 15px;
+}
+div.tab-content ul {
+  padding-bottom: 3px;
+}
+div.tab-content h3 {
+  font-size: 1.17em;
+}
+
+form#group_selector {
+    display: inline;
+}
+
+#group_selector button {
+  padding-top: 3px;
+  padding-bottom: 3px;
+  margin-bottom: 3px;
+}
+
+/* ----- bandeau haut ------ */
+span.bandeaugtr {
+  width: 100%;
+  margin: 0;
+  border-width: 0;
+  padding-left: 160px;
+/*  background-color: rgb(17,51,85); */
+}
+@media print {
+span.bandeaugtr {
+   display:none;
+}
+}
+tr.bandeaugtr {
+/*  background-color: rgb(17,51,85); */
+  color: rgb(255,215,0);
+ /* font-style: italic; */
+  font-weight: bold;
+  border-width: 0;
+  margin: 0;
+}
+
+#authuser {
+    margin-top: 16px;
+}
+
+#authuserlink {
+  color: rgb(255,0,0);
+  text-decoration: none;
+}
+#authuserlink:hover {
+  text-decoration: underline;
+}
+#deconnectlink {
+  font-size: 75%;
+  font-style: normal;
+  color: rgb(255,0,0);
+  text-decoration: underline;
+}
+
+/* ----- page content ------ */
+
+div.about-logo {
+  text-align: center;
+  padding-top: 10px;
+}
+
+
+div.head_message {
+   margin-top: 2px;
+   margin-bottom: 8px;
+   padding:  5px;
+   margin-left: auto;
+   margin-right: auto;
+   background-color: rgba(255, 255, 115, 0.9);
+   -moz-border-radius: 8px;
+   -khtml-border-radius: 8px;
+   border-radius: 8px;
+   font-family : arial, verdana, sans-serif ;
+   font-weight: bold;
+   width: 40%;
+   text-align: center;
+}
+
+#sco_msg {
+    padding:  0px;
+    position: fixed;
+    top: 0px;
+    right: 0px;
+    color: green;
+}
+
+
+div.passwd_warn {
+    font-weight: bold;
+    font-size: 200%;
+    background-color: #feb199;
+    width: 80%;
+    height: 200px;
+    text-align: center;
+    padding:  20px;
+    margin-left: auto;
+    margin-right: auto;
+    margin-top: 10px;
+}
+
+div.scovalueerror {
+    padding-left: 20px;
+    padding-bottom: 100px;
+}
+
+p.footer {
+    font-size: 80%;
+    color: rgb(60,60,60);
+    margin-top: 10px;
+    border-top: 1px solid rgb(60,60,60);
+}
+
+/* ---- (left) SIDEBAR  ----- */
+
+div.sidebar {
+  position: absolute;
+  top: 5px; left: 5px;
+  width: 130px;
+  border: black 1px;
+  /* debug background-color: rgb(245,245,245); */
+   border-right: 1px solid rgb(210,210,210);
+}
+@media print {
+div.sidebar {
+   display:none;
+}
+}
+a.sidebar:link { 
+  color: rgb(4,16,159);
+  text-decoration:none;
+} 
+a.sidebar:visited { 
+   color: rgb(4,16,159);
+   text-decoration: none;
+}
+a.sidebar:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+a.scodoc_title {
+    color: rgb(102,102,102);
+    font-family: arial,verdana,sans-serif;
+    font-size: large;
+    font-weight: bold;
+    text-transform: uppercase;
+    text-decoration: none;
+}
+h2.insidebar {
+    color: rgb(102,102,102);
+    font-weight: bold;
+    font-size: large;
+    margin-bottom: 0;
+}
+
+h3.insidebar {
+  color: rgb(102,102,102);
+  font-weight: bold;
+  font-size: medium;
+  margin-bottom: 0;
+  margin-top: 0;
+}
+
+ul.insidebar {
+  padding-left: 1em;
+  list-style: circle;
+}
+
+div.box-chercheetud {
+  margin-top: 12px;
+}
+
+div.table_etud_in_accessible_depts {
+  margin-left: 3em;
+  margin-bottom: 2em;
+}
+div.table_etud_in_dept {
+  margin-bottom: 2em;
+}
+
+div.table_etud_in_dept  table.gt_table {
+  width: 600px;
+}
+
+.etud-insidebar {
+   font-size: small;
+   background-color: rgb(220,220,220);
+   width: 100%;
+   -moz-border-radius: 6px;
+   -khtml-border-radius: 6px;
+   border-radius: 6px;
+}
+
+.etud-insidebar h2 {
+  color: rgb(153,51,51);
+  font-size: medium;
+}
+
+
+.etud-insidebar ul {
+   padding-left: 1.5em;
+   margin-left: 0;
+}
+
+div.logo-insidebar {
+   margin-left: 0px;
+   width: 75px; /* la marge fait 130px */
+}
+div.logo-logo {
+   text-align: center ;
+}
+
+div.etud_info_div {
+    border: 2px solid gray;
+    height: 94px;
+    background-color: #f7f7ff;
+}
+
+div.eid_left {
+    display: inline-block;
+    
+    padding: 2px; 
+    border: 0px;
+    vertical-align: top;
+    margin-right: 100px;
+}
+
+span.eid_right {
+    padding: 0px;
+    border: 0px;
+    position: absolute;
+    right: 2px;
+    top: 2px;
+}
+
+div.eid_nom {
+    display: inline;
+    color: navy;
+    font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
+    font-size: 120%;
+}
+
+div.eid_nom div {
+    margin-top: 4px;
+}
+
+div.eid_info {
+    margin-left: 2px;
+    margin-top: 3px;
+}
+div.eid_bac {
+    margin-top: 5px;
+}
+div.eid_bac span.eid_bac {
+    font-weight: bold;
+}
+div.eid_parcours {
+    margin-top: 3px;
+}
+
+.qtip-etud {
+    border-width: 0px;
+    margin: 0px;
+    padding: 0px;
+}
+
+.qtip-etud .qtip-content {
+	padding: 0px 0px;
+}
+
+table.listesems th { 
+  text-align: left;  
+  padding-top: 0.5em;
+  padding-left: 0.5em;
+}
+
+table.listesems td.semicon { 
+  padding-left: 1.5em;
+}
+
+table.listesems tr.firstsem td {
+ padding-top: 0.8em;
+}
+
+h2.listesems { 
+  padding-top: 10px;
+  padding-bottom: 0px;
+  margin-bottom: 0px;
+}
+
+table.semlist tr.gt_firstrow th {
+}
+
+table.semlist tr td {
+ border: none;
+}
+table.semlist tr a.stdlink, table.semlist tr a.stdlink:visited {
+ color: navy;
+ text-decoration: none;
+}
+
+table.semlist tr a.stdlink:hover {
+ color: red; 
+ text-decoration: underline;
+}
+
+table.semlist tr td.semestre_id {
+ text-align: right;
+}
+table.semlist tr td.modalite {
+ text-align: left;
+ padding-right: 1em;
+}
+div.gtrcontent table.semlist tr.css_S-1 {
+ background-color: rgb(251, 250, 216);
+}
+
+div.gtrcontent table.semlist tr.css_S1 {
+ background-color: rgb(92%,95%,94%);
+}
+div.gtrcontent table.semlist tr.css_S2 {
+ background-color: rgb(214, 223, 236);
+}
+div.gtrcontent table.semlist tr.css_S3 {
+ background-color: rgb(167, 216, 201);
+}
+div.gtrcontent table.semlist tr.css_S4 {
+ background-color: rgb(131, 225, 140);
+}
+
+div.gtrcontent table.semlist tr.css_MEXT {
+  color: #0b6e08;
+}
+
+/* ----- Liste des news ----- */
+
+div.news {
+   font-family :  "Helvetica Neue",Helvetica,Arial,sans-serif;
+   font-size: 10pt;
+   margin-top: 1em;
+   margin-bottom: 0px;
+   margin-right: 16px;
+   margin-left: 16px;
+   padding:  0.5em;
+   background-color: rgb(255,235,170);
+   -moz-border-radius: 8px;
+   -khtml-border-radius: 8px;
+   border-radius: 8px;
+}
+
+span.newstitle {
+   font-weight: bold;
+}
+
+ul.newslist {
+  padding-left: 1em;
+  padding-bottom: 0em;
+  list-style: circle;
+}
+
+span.newsdate {
+  padding-right: 2em;
+  font-family : monospace;
+}
+
+span.newstext {
+    font-style: normal; 
+}
+
+/* --- infos sur premiere page Sco --- */
+div.scoinfos {
+   margin-top: 0.5em;
+   margin-bottom: 0px;
+   padding: 2px;
+   padding-bottom: 0px;
+   background-color: #F4F4B2;
+}
+
+/* ----- fiches etudiants ------ */
+
+div.ficheEtud {
+   background-color: #f5edc8; /* rgb(255,240,128); */
+   border: 1px solid gray;
+   width: 910px;
+   padding: 10px;
+   margin-top: 10px;
+}
+
+div.menus_etud {
+   position: absolute;
+   margin-left: 1px;
+   margin-top: 1px;
+}
+div.ficheEtud h2 {
+   padding-top: 10px;
+}
+
+div.code_nip {
+   padding-top: 10px;
+   font-family: "Andale Mono", "Courier";
+}
+
+div.fichesituation {
+  background-color: rgb( 231, 234, 218 ); /* E7EADA */   
+  margin: 0.5em 0 0.5em 0;
+  padding: 0.5em;
+  -moz-border-radius: 8px;
+  -khtml-border-radius: 8px;
+  border-radius: 8px;
+}
+
+div.ficheadmission { 
+  background-color: rgb( 231, 234, 218 ); /* E7EADA */
+  
+  margin: 0.5em 0 0.5em 0;
+  padding: 0.5em;
+  -moz-border-radius: 8px;
+  -khtml-border-radius: 8px;
+  border-radius: 8px;
+}
+
+div#adm_table_description_format table.gt_table td {
+   font-size: 80%;
+}
+
+div.ficheadmission div.note_rapporteur {
+   font-size: 80%;
+   font-style: italic;
+}
+
+div.etudarchive ul {
+    padding:0; 
+    margin:0; 
+    margin-left: 1em;
+    list-style-type:none; 
+}
+
+div.etudarchive ul li {
+    background-image: url(/ScoDoc/static/icons/bullet_arrow.png);
+    background-repeat: no-repeat;
+    background-position: 0 .4em;
+    padding-left: .6em;
+}
+div.etudarchive ul li.addetudarchive {
+    background-image: url(/ScoDoc/static/icons/bullet_plus.png);
+    padding-left: 1.2em
+}
+span.etudarchive_descr {
+    margin-right: .4em;
+}
+span.deletudarchive {
+    margin-left: 0.5em;
+}
+div#fichedebouche { 
+  background-color: rgb(183, 227, 254); /* bleu clair */
+  color: navy;
+  width: 910px;
+  margin: 0.5em 0 0.5em 0;
+  padding: 0.5em;
+  -moz-border-radius: 8px;
+  -khtml-border-radius: 8px;
+  border-radius: 8px;
+}
+
+div#fichedebouche .ui-accordion-content {
+  background-color: rgb(183, 227, 254); /* bleu clair */
+  padding: 0px 10px 0px 0px;  
+}
+
+span.debouche_tit {
+  font-weight: bold;
+  padding-right: 1em;
+}
+
+/* li.itemsuivi {} */
+
+span.itemsuivi_tag_edit {
+    border: 2px;
+}
+.listdebouches .itemsuivi_tag_edit .tag-editor {
+    background-color: rgb(183, 227, 254);
+    border: 0px;
+}
+.itemsuivi_tag_edit ul.tag-editor {
+    display: inline-block;
+    width: 100%;
+}
+/* .itemsuivi_tag_edit ul.tag-editor li {} */
+
+.itemsuivi_tag_edit .tag-editor-delete {
+    height: 20px;
+}
+
+.itemsuivi_suppress {
+    float: right;
+    padding-top: 9px;
+    padding-right: 5px;
+}
+
+div.itemsituation {
+    background-color: rgb(224, 234, 241);
+    /* height: 2em;*/
+    border: 1px solid rgb(204,204,204);
+    -moz-border-radius: 4px;
+   -khtml-border-radius: 4px;
+   border-radius: 4px;
+   padding-top: 1px;
+   padding-bottom: 1px;
+   padding-left: 10px;
+   padding-right: 10px;
+}
+div.itemsituation em {
+    color: #bbb;
+}
+
+/* tags readonly */
+span.ro_tag {
+    display: inline-block;
+    background-color: rgb(224, 234, 241);
+    color: #46799b;
+    margin-top: 3px;
+    margin-left: 5px;
+    margin-right: 3px;
+    padding-left: 3px;
+    padding-right: 3px;
+    border: 1px solid rgb(204,204,204);
+}
+
+div.ficheinscriptions {
+   background-color: #eae3e2; /* was EADDDA */
+   margin: 0.5em 0 0.5em 0;
+   padding: 0.5em;
+   -moz-border-radius: 8px;
+   -khtml-border-radius: 8px;
+   border-radius: 8px;
+   overflow-x: scroll;
+}
+
+.ficheinscriptions a.sem {
+   text-decoration: none;
+   font-weight: bold;
+   color: blue;
+}
+
+.ficheinscriptions a.sem:hover {
+   color: red;
+}
+
+
+td.photocell {
+   padding-left: 32px;
+}
+
+div.fichetitre {
+   font-weight: bold;
+}
+
+span.etud_type_admission {
+   color: rgb(0,0,128);
+   font-style: normal;
+}
+
+td.fichetitre2 {
+   font-weight: bold;
+   vertical-align: top;
+}
+
+td.fichetitre2 .formula {
+   font-weight: normal;
+   color: rgb(0,64,0);
+   border: 1px solid red;
+   padding-left: 1em;
+   padding-right: 1em;
+   padding-top: 3px;
+   padding-bottom: 3px;
+   margin-right: 1em;
+}
+
+span.formula {
+  font-size: 80%;
+  font-family: Courier, monospace;
+  font-weight: normal;
+}
+
+td.fichetitre2 .fl {
+  font-weight: normal;
+}
+
+.ficheannotations {
+  background-color: #f7d892;
+  width: 910px;
+  
+  margin: 0.5em 0 0.5em 0;
+  padding: 0.5em;
+  -moz-border-radius: 8px;
+  -khtml-border-radius: 8px;
+  border-radius: 8px;
+}
+
+.ficheannotations table#etudannotations {
+    width: 100%;
+    border-collapse: collapse;
+}
+.ficheannotations table#etudannotations tr:nth-child(odd) {
+    background: rgb(240, 240, 240);
+}
+.ficheannotations table#etudannotations tr:nth-child(even) {
+    background: rgb(230, 230, 230);
+}
+
+.ficheannotations span.annodate {
+    color: rgb(200, 50, 50);
+    font-size: 80%;
+}
+.ficheannotations span.annoc {
+    color: navy;
+}
+.ficheannotations td.annodel {
+    text-align: right;
+}
+
+span.link_bul_pdf {
+  font-size: 80%;
+}
+
+/* Page accueil Sco */
+span.infostitresem {
+   font-weight: normal;
+}
+span.linktitresem {
+   font-weight: normal;
+}
+span.linktitresem a:link {color: red;}
+span.linktitresem a:visited {color: red;}
+
+.listegroupelink a:link { color: blue; }
+.listegroupelink a:visited { color: blue; }
+.listegroupelink a:hover { color: red; }
+
+a.stdlink, a.stdlink:visited {
+  color: blue; 
+  text-decoration: underline;
+}
+a.stdlink:hover { 
+  color: red; 
+  text-decoration: underline;
+}
+
+a.link_accessible {
+}
+a.link_unauthorized, a.link_unauthorized:visited {
+  color: rgb(75,75,75);
+}
+
+span.spanlink {
+  color: rgb(0, 0, 255);
+  text-decoration: underline;
+}
+
+span.spanlink:hover {
+  color: red;
+}
+
+/* Trombinoscope */
+
+.trombi_legend {
+ font-size: 80%;
+ margin-bottom: 3px;
+ -ms-word-break: break-all;
+ word-break: break-all;
+ // non std for webkit:
+ work-break: break-word; 
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ hyphens: auto;
+}
+
+.trombi_box {
+  display: inline-block;
+  width: 110px;
+  vertical-align: top;
+  margin-left: 5px;
+  margin-top: 5px;
+}
+
+span.trombi_legend {
+  display: inline-block;
+}
+span.trombi-photo {
+ display: inline-block;
+}
+span.trombi_box a {
+  display: inline-block;
+}
+span.trombi_box a img {
+  display: inline-block;
+}
+
+.trombi_nom {
+  display: block;
+  padding-top: 0px;
+  padding-bottom: 0px;
+  margin-top: -5px;
+  margin-bottom: 0px;
+}
+
+.trombi_prenom {
+  display: inline-block;
+  padding-top: 0px;
+  padding-bottom: 0px;
+  margin-top: -2px;
+  margin-bottom: 0px;
+}
+
+
+/* markup non semantique pour les cas simples */
+
+.fontred { color: red; }
+.fontorange { color: rgb(215, 90, 0); }
+.fontitalic { font-style: italic;  }
+.redboldtext {
+   font-weight: bold;
+   color: red;
+}
+
+.greenboldtext {
+   font-weight: bold;
+   color: green;
+} 
+
+a.redlink {
+   color: red;
+}
+a.redlink:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+a.discretelink {
+   color: black;
+   text-decoration: none;
+}
+
+a.discretelink:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+.rightcell {
+    text-align: right;
+}
+
+.rightjust {
+  padding-left: 2em;
+}
+
+.centercell { 
+    text-align: center;
+}
+
+.help {
+  font-style: italic; 
+}
+
+.help_important {
+  font-style: italic; 
+  color: red;
+}
+
+div.sco_help {
+  margin-top: 12px;
+  font-style: italic; 
+  color: navy;
+  background-color: rgb(200,200,220);
+}
+
+p.indent {
+  padding-left: 2em;
+}
+
+.blacktt { 
+  font-family: Courier, monospace;
+  font-weight: normal;
+  color: black;
+}
+
+p.msg { 
+  color: red;
+  font-weight: bold;
+  border: 1px solid blue;
+  background-color: rgb(140,230,250);
+  padding: 10px;
+}
+
+table.tablegrid {
+  border-color: black;
+  border-width: 0 0 1px 1px;
+  border-style: solid;
+  border-collapse: collapse;
+}
+table.tablegrid td, table.tablegrid th {
+  border-color: black;
+  border-width: 1px 1px 0 0;
+  border-style: solid;
+  margin: 0;
+  padding-left: 4px;
+  padding-right: 4px;
+}
+
+/* ----- Notes ------ */
+a.smallbutton {
+    border-width: 0;
+    margin: 0;
+    margin-left: 2px;
+    text-decoration: none;
+}
+span.evallink {
+    font-size: 80%;
+    font-weight: normal;
+}
+.boldredmsg {
+    color: red;
+    font-weight: bold;
+}
+
+tr.etuddem td {
+    color: rgb(100,100,100);
+    font-style: italic; 
+}
+
+td.etudabs, td.etudabs a.discretelink, tr.etudabs td.moyenne a.discretelink {
+    color: rgb(180,0,0);
+}
+tr.moyenne td {
+    font-weight: bold; 
+}
+
+table.notes_evaluation th.eval_complete a.sortheader {
+    color: green;
+}
+table.notes_evaluation th.eval_incomplete a.sortheader {
+    color: red;
+}
+table.notes_evaluation th.eval_attente a.sortheader {
+    color: rgb(215, 90, 0);;
+}
+table.notes_evaluation tr td a.discretelink:hover {
+    text-decoration: none;
+}
+table.notes_evaluation tr td.tdlink a.discretelink:hover {
+    color: red; 
+    text-decoration: underline;
+}
+table.notes_evaluation tr td.tdlink a.discretelink, table.notes_evaluation tr td.tdlink a.discretelink:visited {
+    color: blue; 
+    text-decoration: underline;
+}
+
+table.notes_evaluation tr td {
+    padding-left: 0.5em;
+    padding-right: 0.5em;
+}
+
+div.notes_evaluation_stats {
+    margin-top: -15px;
+}
+
+span.eval_title {
+    font-weight: bold; 
+    font-size: 14pt;
+}
+#saisie_notes span.eval_title {
+    /* border-bottom: 1px solid rgb(100,100,100); */
+}
+
+span.jurylink {
+    margin-left: 1.5em;
+}
+span.jurylink a {
+    color: red;
+    text-decoration: underline;
+}
+
+.eval_description p {
+    margin-left: 15px;
+    margin-bottom: 2px;
+    margin-top: 0px;
+}
+
+.eval_description span.resp {
+    font-weight: normal;
+}
+.eval_description span.resp a {
+    font-weight: normal;
+}
+.eval_description span.eval_malus {
+    font-weight: bold;
+    color: red;
+}
+
+
+span.eval_info {
+    font-style: italic;
+}
+span.eval_complete {
+    color: green;
+}
+span.eval_incomplete {
+    color: red;
+}
+span.eval_attente {
+    color:  rgb(215, 90, 0);
+}
+
+table.tablenote {
+  border-collapse: collapse;
+  border: 2px solid blue;
+/*  width: 100%;*/
+  margin-bottom: 20px;
+  margin-right: 20px;
+}
+table.tablenote th {  
+  padding-left: 1em;
+}
+.tablenote a {
+   text-decoration: none;
+   color: black;
+}
+.tablenote a:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+table.tablenote_anonyme {
+   border-collapse: collapse;
+   border: 2px solid blue;
+}
+
+tr.tablenote {
+    border: solid blue 1px;
+}
+td.colnote {
+  text-align : right;
+  padding-right: 0.5em;  
+  border: solid blue 1px;
+}
+td.colnotemoy {
+  text-align : right;
+  padding-right: 0.5em;
+  font-weight: bold; 
+}
+td.colcomment, span.colcomment {
+  text-align: left;
+  padding-left: 2em;
+  font-style: italic; 
+  color: rgb(80,100,80);
+}
+
+h2.formsemestre, .gtrcontent h2 {
+    margin-top: 2px;
+    font-size: 130%;
+}
+
+.formsemestre_page_title table.semtitle, .formsemestre_page_title table.semtitle td {
+    padding: 0px;
+    margin-top: 0px; 
+    margin-bottom: 0px; 
+    border-width: 0;
+    border-collapse: collapse;
+}
+.formsemestre_page_title table.semtitle {
+    /* width: 100%; */
+}
+
+.formsemestre_page_title { 
+    width: 100%;
+    padding-top:5px;
+    padding-bottom: 10px;
+}
+
+.formsemestre_page_title table.semtitle td.infos table {
+    padding-top: 10px;
+}
+
+.formsemestre_page_title a {
+    color: black;
+}
+
+.formsemestre_page_title .eye,  formsemestre_page_title .eye img {
+    display: inline-block;
+    vertical-align: middle;
+    margin-bottom: 2px;
+}
+.formsemestre_page_title .infos span.lock,  formsemestre_page_title .lock img {
+  display: inline-block;
+  vertical-align: middle;
+  margin-bottom: 5px;
+  padding-right: 5px;
+}
+#formnotes .tf-explanation {
+    font-size: 80%;
+}
+#formnotes .tf-explanation .sn_abs {
+    color: red;
+}
+
+#formnotes .tf-ro-fieldlabel.formnote_bareme {
+    text-align: right;
+    font-weight: bold;
+}
+#formnotes td.tf-ro-fieldlabel:after {
+   content: '';
+}
+#formnotes .tf-ro-field.formnote_bareme {
+    font-weight: bold;
+}
+/*
+.formsemestre_menubar {
+    border-top: 3px solid #67A7E3;
+    background-color: #D6E9F8;
+    margin-top: 8px;
+}
+
+.formsemestre_menubar .barrenav ul li a.menu {
+    font-size: 12px;
+}
+*/
+/* Barre menu semestre */
+#sco_menu {
+  overflow : hidden;
+  background: rgb(214, 233, 248);
+  border-top-color: rgb(103, 167, 227);
+  border-top-style: solid;
+  border-top-width: 3px;
+  margin-top: 5px;
+  margin-right: 3px;
+  margin-left: -1px;
+}
+
+#sco_menu > li {
+  float : left;
+  width : auto; /* 120px !important; */
+  font-size: 12px;
+  font-family: Arial, Helvetica, sans-serif;
+  text-transform: uppercase;
+}
+
+#sco_menu > li li {
+    text-transform: none;
+    font-size: 14px;
+    font-family: Arial, Helvetica, sans-serif;
+}
+
+#sco_menu > li > a {
+    font-weight: bold !important;
+    padding-left: 15px;
+    padding-right: 15px;
+}
+
+#sco_menu ul .ui-menu {
+  width : 200px;
+}
+
+.sco_dropdown_menu {
+}
+.sco_dropdown_menu > li {
+  width : auto; /* 120px !important; */
+  font-size: 12px;
+  font-family: Arial, Helvetica, sans-serif;
+}
+
+span.inscr_addremove_menu {
+  width: 150px;
+}
+
+.formsemestre_page_title  .infos span {
+    padding-right: 25px;
+}
+
+.formsemestre_page_title span.semtitle {
+    font-size: 12pt;
+}
+.formsemestre_page_title span.resp, span.resp a {
+    color: red;
+    font-weight: bold;
+}
+.formsemestre_page_title span.nbinscrits {
+    text-align: right;
+    font-weight: bold;
+    padding-right: 1px;
+}
+.formsemestre_page_title span.lock {
+
+}
+
+div.formsemestre_status {
+    -moz-border-radius: 8px;
+    -khtml-border-radius: 8px;
+    border-radius: 8px;
+    padding: 2px 6px 2px 16px;
+    margin-right: 10px;
+}
+
+table.formsemestre_status {
+    border-collapse: collapse;
+}
+tr.formsemestre_status { background-color: rgb(90%,90%,90%); }
+tr.formsemestre_status_green { background-color: #EFF7F2; }
+tr.formsemestre_status_ue { background-color: rgb(90%,90%,90%); }
+table.formsemestre_status td {
+    border-top: 1px solid rgb(80%,80%,80%);
+    border-bottom: 1px solid rgb(80%,80%,80%);
+    border-left: 0px;
+}
+table.formsemestre_status td.evals, table.formsemestre_status th.evals, table.formsemestre_status td.resp, table.formsemestre_status th.resp, table.formsemestre_status td.malus {
+    padding-left: 1em;
+}
+
+table.formsemestre_status th {
+    font-weight: bold;
+    text-align: left;
+}
+th.formsemestre_status_inscrits {
+    font-weight: bold;
+    text-align: center;
+}
+td.formsemestre_status_code {
+    width: 2em;
+    padding-right: 1em;
+}
+
+table.formsemestre_status td.malus a {
+    color: red;
+}
+
+a.formsemestre_status_link {
+    text-decoration:none;
+    color: black;
+}
+a.formsemestre_status_link:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+td.formsemestre_status_inscrits {
+  text-align: center;
+}
+td.formsemestre_status_cell {
+  white-space: nowrap;
+}
+
+span.status_ue_acro { font-weight: bold; }
+span.status_ue_title { font-style: italic; padding-left: 1cm;}
+
+table.formsemestre_inscr td {
+    padding-right: 1.25em;
+}
+
+ul.ue_inscr_list li span.tit {
+    font-weight: bold;
+}
+ul.ue_inscr_list li.tit {
+    padding-top: 1ex;
+}
+ul.ue_inscr_list li.etud {
+    padding-top: 0.7ex;
+}
+
+/* Liste des groupes sur tableau bord semestre */
+.formsemestre_status h3 {
+  border: 0px solid black;
+  margin-bottom: 5px;
+}
+
+#grouplists h4 {
+   font-style: italic;
+   margin-bottom: 0px;
+   margin-top: 5px;
+}
+
+#grouplists table {
+   //border: 1px solid black;
+   border-spacing: 1px;
+}
+
+#grouplists td {
+}
+
+
+/* Modules */
+div.moduleimpl_tableaubord { 
+  padding: 7px;
+  border: 2px solid gray;
+}
+
+.moduleimpl_evaluations_top_links {
+  font-size: 80%;
+  margin-bottom: 3px;
+}
+
+table.moduleimpl_evaluations {
+  width: 100%;
+  border-spacing: 0px;
+}
+
+th.moduleimpl_evaluations {
+  font-weight: normal;
+  text-align: left;
+  color: rgb(0,0,128);
+}
+
+th.moduleimpl_evaluations a, th.moduleimpl_evaluations a:visited {
+  font-weight: normal;
+  color: red;
+  text-decoration: none;
+}
+th.moduleimpl_evaluations a:hover {
+   text-decoration: underline;
+}
+
+tr.moduleimpl_evaluation_row {
+}
+
+td.moduleimpl_evaluation_row {
+}
+
+tr.mievr {
+   background-color:#eeeeee;
+}
+
+tr.mievr_spaced {
+   /* margin-top: 20px; */
+}
+
+tr.mievr_rattr {
+   background-color:#dddddd;
+}
+span.mievr_rattr {
+    font-weight: bold;
+    color: blue;
+    margin-left: 2em;
+    border: 1px solid red;
+    padding: 1px 3px 1px 3px;
+}
+
+tr.mievr td.mievr_tit { 
+  font-weight: bold;
+  background-color: #cccccc;
+}
+tr.mievr td {
+  text-align: left;
+  background-color:white;
+}
+tr.mievr th {
+  background-color:white;
+}
+tr.mievr td.mievr {
+  width: 90px;
+}
+tr.mievr td.mievr_menu {
+  width: 110px;
+}
+tr.mievr td.mievr_dur {
+  width: 60px;
+}
+tr.mievr td.mievr_coef {
+  width: 60px;
+}
+tr.mievr td.mievr_nbnotes {
+  width: 90px;
+}
+tr td.mievr_grtit {
+ vertical-align: top;
+ text-align: right;
+ font-weight: bold;
+}
+span.mievr_lastmodif { 
+ padding-left: 2em;
+ font-weight: normal;
+ font-style: italic; 
+}
+a.mievr_evalnodate {
+ color: rgb(215, 90, 0);
+ font-style: italic;
+ text-decoration: none;
+}
+a.mievr_evalnodate:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+span.evalindex_cont {
+ float: right;
+}
+span.evalindex {
+ font-weight: normal;
+ margin-right: 5px;
+}
+.eval_arrows_chld {
+ margin-right: 3px;
+ margin-top: 3px;
+}
+.eval_arrows_chld a {
+  margin-left: 3px;
+}
+
+
+/* Formulaire edition des partitions */
+form#editpart table {
+   border: 1px solid gray;
+   border-collapse: collapse;
+}
+form#editpart tr.eptit th {
+   font-size: 110%;
+   border-bottom: 1px solid gray;
+}
+form#editpart td {
+   border-bottom: 1px dashed gray;
+}
+form#editpart table td {
+   padding-left: 1em;
+}
+form#editpart table td.epnav {
+   padding-left: 0;
+}
+
+/* Liste des formations */
+ul.notes_formation_list { 
+  list-style-type: none;
+  font-size: 110%;
+}
+li.notes_formation_list { 
+  padding-top: 10px;
+}
+
+table.formation_list_table {
+    width:100%;
+    border-collapse:collapse;
+    background-color: rgb(0%,90%,90%);
+}
+table#formation_list_table tr.gt_hl {
+    background-color: rgb(96%,96%,96%);
+}
+.formation_list_table img.delete_small_img {
+  width: 16px;
+  height:16px;
+}
+.formation_list_table td.acronyme {
+  width: 10%;
+  font-weight: bold;
+}
+
+.formation_list_table td.formation_code {
+  font-family: Courier, monospace;
+  font-weight: normal;
+  color: black;
+  font-size: 100%;
+}
+.formation_list_table td.version {
+  text-align: center;
+}
+.formation_list_table td.titre {
+  width: 50%;
+}
+
+.formation_list_table td.sems_list_txt {
+  font-size: 90%;
+}
+
+/* Presentation formation (ue_list) */
+div.formation_descr {
+  background-color: rgb(250,250,240);
+  border: 1px solid rgb(128,128,128);
+  padding-left: 5px;
+  padding-bottom: 5px;
+  margin-right: 12px;
+}
+div.formation_descr span.fd_t {
+  font-weight: bold;
+  margin-right: 5px;
+}
+div.formation_descr span.fd_n {
+  font-weight: bold;
+  font-style: italic;
+  color: green;
+  margin-left: 6em;
+}
+
+div.formation_ue_list {
+  border: 1px solid black;
+  margin-top: 5px;
+  margin-right: 12px;
+  padding-left: 5px;
+}
+
+li.module_malus span.formation_module_tit {
+  color: red;
+  font-weight: bold;
+  text-decoration: underline;
+}
+
+span.notes_module_list_buts {
+  margin-right: 5px;
+}
+
+div.ue_list_tit {
+  font-weight: bold;
+  margin-top: 5px;
+}
+
+ul.notes_ue_list {
+  background-color: rgb(240,240,240);
+  font-weight: bold;
+  margin-top: 4px;
+  margin-right: 1em;
+}
+
+li.notes_ue_list {
+  margin-top: 9px;
+}
+
+span.ue_code {
+  font-family: Courier, monospace;
+  font-weight: normal;
+  color: black;
+  font-size: 80%;
+}
+
+span.ue_type {
+  color: green;
+  margin-left: 1.5em;
+  margin-right: 1.5em;
+}
+
+li.notes_matiere_list {
+  margin-top: 2px;
+}
+
+ul.notes_matiere_list {
+  background-color: rgb(220,220,220);
+  font-weight: normal;
+  font-style: italic; 
+}
+
+ul.notes_module_list {
+   background-color: rgb(210,210,210);
+   font-weight: normal;
+   font-style: normal; 
+}
+
+.notes_ue_list a.discretelink, .notes_ue_list a.stdlink  {
+    color: #001084;
+    text-decoration: underline;
+}
+.notes_ue_list span.locked {
+    font-weight: normal;
+}
+
+.notes_ue_list a.smallbutton img {
+    position: relative;
+    top: 2px;
+}
+
+div#ue_list_code {
+    background-color: rgb(220,220,220);
+    font-size: small;
+    padding-left: 4px;
+    padding-bottom: 1px;
+    margin-top: 10px;
+    margin: 3ex;
+}
+
+ul.notes_module_list {
+    list-style-type: none;
+}
+
+div#ue_list_etud_validations {
+    background-color: rgb(220,250,220);
+    padding-left: 4px;
+    padding-bottom: 1px;
+    margin: 3ex;
+}
+div#ue_list_etud_validations span {
+    font-weight: bold;
+}
+
+span.ue_share {
+    font-weight: bold;
+}
+
+div.ue_warning {
+    border: 1px solid red;
+    background-color: rgb(250,220,220);
+    margin: 3ex;
+    padding-left: 1ex;
+    padding-right: 1ex;
+}
+
+div.ue_warning span:before {
+    content: url(/ScoDoc/static/icons/warning_img.png);
+    vertical-align: -80%;  
+}
+
+div.ue_warning span {
+    font-weight: bold;
+}
+
+/* tableau recap notes */
+table.notes_recapcomplet {
+   border: 2px solid blue;
+   border-spacing: 0px 0px;
+   border-collapse: collapse;
+   white-space: nowrap;   
+}
+tr.recap_row_even {
+   background-color: rgb(210,210,210);
+}
+@media print {
+tr.recap_row_even { /* bordures noires pour impression */
+   border-top: 1px solid black;
+   border-bottom: 1px solid black;
+}
+}
+tr.recap_row_min {
+   border-top: 1px solid blue;
+}
+tr.recap_row_min, tr.recap_row_max {
+   font-weight: normal;
+   font-style: italic;
+}
+tr.recap_row_coef {   
+}
+tr.recap_row_moy {
+    font-weight: bold;
+}
+tr.recap_row_nbeval {
+    color: green;
+}
+tr.recap_row_ects {
+  color: rgb(160, 86, 3);
+  border-bottom: 1px solid blue;
+}
+td.recap_tit {
+   font-weight: bold;
+   text-align: left;
+   padding-right: 1.2em;
+}
+td.recap_tit_ue {
+   font-weight: bold;
+   text-align: left;
+   padding-right: 1.2em;
+   padding-left: 2px;
+   border-left: 1px solid blue;
+}
+td.recap_col {
+   padding-right: 1.2em;
+   text-align: left;
+}
+td.recap_col_moy {
+   padding-right: 1.5em;
+   text-align: left;
+   font-weight: bold;
+   color: rgb(80,0,0);
+}
+td.recap_col_moy_inf {
+   padding-right: 1.5em;
+   text-align: left;
+   font-weight: bold;
+   color: rgb(255,0,0);
+}
+td.recap_col_ue {
+   padding-right: 1.2em;
+   padding-left: 4px;
+   text-align: left;
+   font-weight: bold;
+   border-left: 1px solid blue;
+}
+td.recap_col_ue_inf {
+   padding-right: 1.2em;
+   padding-left: 4px;
+   text-align: left;
+   font-weight: bold;
+   color: rgb(255,0,0);
+   border-left: 1px solid blue;
+}
+td.recap_col_ue_val {
+   padding-right: 1.2em;
+   padding-left: 4px;
+   text-align: left;
+   font-weight: bold;
+   color: rgb(0,140,0);
+   border-left: 1px solid blue;
+}
+/* noms des etudiants sur recap complet */
+table.notes_recapcomplet a:link, table.notes_recapcomplet a:visited {
+   text-decoration: none;
+   color: black;
+}
+
+table.notes_recapcomplet a:hover {
+   color: red;
+   text-decoration: underline;
+}
+
+/* bulletin */
+div.notes_bulletin {
+  margin-right: 5px;
+}
+
+table.notes_bulletin {
+   border-collapse: collapse;
+   border: 2px solid rgb(100,100,240);
+   width: 100%;
+   margin-right: 100px;
+   background-color: rgb(240,250,255);
+   font-family : arial, verdana, sans-serif ;
+   font-size: 13px;
+}
+
+tr.notes_bulletin_row_gen {
+   border-top: 1px solid black;
+   font-weight: bold;
+}
+tr.notes_bulletin_row_rang {
+   font-weight: bold;
+}
+
+tr.notes_bulletin_row_ue {
+   /* background-color: rgb(170,187,204); voir sco_utils.UE_COLORS */
+   font-weight: bold;
+   border-top: 1px solid black;
+}
+
+tr.bul_row_ue_cur {
+   background-color: rgb(180,180,180);
+   color: rgb(50,50,50);
+}
+
+tr.bul_row_ue_cap {
+   background-color: rgb(150,170,200);
+   color: rgb(50,50,50);
+}
+
+tr.notes_bulletin_row_mat {
+  border-top: 2px solid rgb(140,140,140);
+  color: blue;
+}
+
+tr.notes_bulletin_row_mod {
+  border-top: 1px solid rgb(140,140,140);
+}
+tr.notes_bulletin_row_sum_ects {
+  border-top: 1px solid black;
+  font-weight: bold;
+  background-color: rgb(170,190,200);
+}
+
+tr.notes_bulletin_row_mod td.titre, tr.notes_bulletin_row_mat td.titre {
+  padding-left: 1em;
+}
+
+tr.notes_bulletin_row_eval {
+  font-style: italic; 
+  color: rgb(60,60,80);
+}
+tr.notes_bulletin_row_eval_incomplete .discretelink {
+  color: rgb(200,0,0);
+}
+
+tr.b_eval_first td {
+  border-top: 1px dashed rgb(170,170,170);
+}
+tr.b_eval_first td.titre {
+  border-top: 0px;
+}
+tr.notes_bulletin_row_eval td.module {
+  padding-left: 5px;
+  border-left: 1px dashed rgb(170,170,170);
+}
+
+table.notes_bulletin td.note {
+  padding-left: 1em;
+}
+table.notes_bulletin td.min, table.notes_bulletin td.max, table.notes_bulletin td.moy {
+  font-size: 80%;  
+}
+
+table.notes_bulletin tr.notes_bulletin_row_ue_cur td.note, table.notes_bulletin tr.notes_bulletin_row_ue_cur td.min, table.notes_bulletin tr.notes_bulletin_row_ue_cur td.max {
+  color: rgb(80,80,80);
+  font-style: italic; 
+}
+
+.note_bold {
+   font-weight: bold; 
+}
+td.bull_coef_eval, td.bull_nom_eval {
+   font-style: italic; 
+   color: rgb(60,60,80);
+}
+tr.notes_bulletin_row_eval td.note {
+   font-style: italic; 
+   color: rgb(40,40,40);
+   font-size: 90%;
+}
+
+tr.notes_bulletin_row_eval td.note .note_nd {
+   font-weight: bold; 
+   color: red;
+}
+
+/* --- Bulletins UCAC */
+tr.bul_ucac_row_tit, tr.bul_ucac_row_ue, tr.bul_ucac_row_total, tr.bul_ucac_row_decision, tr.bul_ucac_row_mention {
+   font-weight: bold;
+   border: 1px solid black;
+}
+tr.bul_ucac_row_tit {
+   background-color: rgb(170,187,204);
+}
+tr.bul_ucac_row_total, tr.bul_ucac_row_decision, tr.bul_ucac_row_mention {
+   background-color: rgb(220,220,220);
+}
+
+/* ---- /ucac */
+
+span.bul_minmax {
+   font-weight: normal;
+   font-size: 66%;   
+}
+span.bul_minmax:before {
+   content: " ";
+}
+
+a.bull_link {
+   text-decoration:none;
+   color: rgb(20,30,30);
+}
+a.bull_link:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+table.bull_head {
+   width: 100%;
+}
+td.bull_photo {
+   text-align: right;
+}
+
+div.bulletin_menubar {
+   padding-left: 25px;
+}
+
+.bull_liensemestre {
+   font-weight: bold; 
+}
+.bull_liensemestre a {
+   color: rgb(255,0,0);
+   text-decoration: none;
+}
+.bull_liensemestre a:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+div.bull_appreciations {
+}
+
+.bull_appreciations p {
+   margin: 0;
+   font-style: italic; 
+}
+.bull_appreciations_link {
+   margin-left: 1em;
+}
+span.bull_appreciations_date {
+   margin-right: 1em;
+   font-style: normal;
+   font-size: 75%; 
+}
+
+div.eval_description {
+    color: rgb(20,20,20);
+    /* border: 1px solid rgb(30,100,0); */
+    padding: 3px;
+}
+
+
+/* Saisie des notes */
+div.saisienote_etape1 {
+  border: 2px solid blue;
+  padding: 5px;
+  background-color: rgb( 231, 234, 218 ); /* E7EADA */ 
+}
+div.saisienote_etape2 {
+  border: 2px solid green; 
+  margin-top: 1em;
+  padding: 5px;
+  background-color: rgb(234,221,218); /* EADDDA */
+}
+span.titredivsaisienote { 
+  font-weight: bold;
+  font-size: 115%;
+}
+
+
+.etud_dem {
+  color: rgb(130,130,130);
+}
+
+input.note_invalid {
+ color: red;
+ background-color: yellow;
+}
+
+input.note_valid_new {
+ color: blue;
+}
+
+input.note_saved {
+ color: green;
+}
+
+input.note {
+}
+
+span.history {
+  font-style: italic;
+}
+span.histcomment {
+  font-style: italic;
+}
+
+/* ----- Absences ------ */
+td.matin {
+  background-color: rgb(203,242,255);
+}
+
+td.absent {
+  background-color: rgb(255,156,156);
+}
+
+td.present {
+  background-color: rgb(230,230,230);
+}
+
+span.capstr {
+  color: red;
+}
+
+
+tr.row_1 {
+    background-color: white;
+}
+tr.row_2 {
+    background-color: white;
+}
+tr.row_3 {
+    background-color: #dfdfdf;
+}
+td.matin_1 {
+    background-color: #e1f7ff;
+}
+td.matin_2 {
+    background-color: #e1f7ff;
+}
+td.matin_3 {
+    background-color: #c1efff;
+}
+
+table.abs_form_table tr:hover td {
+    border: 1px solid red;
+}
+
+
+/* ----- Formulator  ------- */
+ul.tf-msg { 
+  color: rgb(6,80,18);
+  border: 1px solid red;
+}
+
+li.tf-msg {  
+  list-style-image: url(/ScoDoc/static/icons/warning16_img.png);
+  padding-top: 5px;
+  padding-bottom: 5px;
+}
+
+.warning {
+  font-weight: bold;
+  color: red;
+}
+.warning::before { 
+  content: url(/ScoDoc/static/icons/warning_img.png);
+  vertical-align: -80%;  
+}
+.infop {
+  font-weight: normal;
+  color: rgb(26, 150, 26);
+  font-style: italic;
+}
+.infop::before { 
+  content: url(/ScoDoc/static/icons/info-16_img.png);
+  vertical-align: -15%;  
+  padding-right: 5px;
+}
+
+
+form.sco_pref table.tf {
+  border-spacing: 5px 15px;
+}
+
+td.tf-ro-fieldlabel { 
+  /* font-weight: bold; */
+  vertical-align:top;
+  margin-top: 20px;
+}
+td.tf-ro-fieldlabel:after {
+   content: ' :';
+}
+td.tf-ro-field {
+  vertical-align:top;
+}
+span.tf-ro-value {
+  background-color: white;
+  color: grey;
+  margin-right: 2em;
+}
+div.tf-ro-textarea {
+  border: 1px solid grey;
+  padding-left: 8px;
+}
+select.tf-selglobal {
+  margin-left: 10px;
+}
+
+td.tf-fieldlabel { 
+  /* font-weight: bold; */
+  vertical-align:top;
+}
+
+.tf-comment {
+    font-size: 80%;
+    font-style: italic;
+}
+
+.tf-explanation {
+    font-style: italic;
+}
+
+.radio_green { 
+   background-color: green;
+}
+
+.radio_red {
+   background-color: red;
+}
+
+td.fvs_val {
+ border-left: 1px solid rgb(80,80,80);
+ text-align: center;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+td.fvs_val_inf {
+ border-left: 1px solid rgb(80,80,80);
+ text-align: center;
+ padding-left: 1em;
+ padding-right: 1em;
+ color: red;
+}
+
+td.fvs_chk {
+}
+
+td.fvs_tit {
+ font-weight: bold;
+ text-align: left;
+ border-left: 1px solid rgb(80,80,80);
+ text-align: center;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+td.fvs_tit_chk {
+ font-weight: bold;
+}
+
+/* ----- Entreprises ------- */
+
+table.entreprise_list, table.corr_list, table.contact_list {
+   width : 100%;
+   border-width: 0px;
+   /* border-style: solid; */
+   border-spacing: 0px 0px;
+   padding: 0px;
+}
+tr.entreprise_list_even, tr.corr_list_even, tr.contact_list_even {
+  background-color: rgb(85%,85%,95%);
+}
+tr.entreprise_list_odd, tr.corr_list_odd, tr.contact_list_odd {
+  background-color: rgb(90%,90%, 90%);
+}
+
+td.entreprise_descr, td.corr_descr, td.contact_descr { 
+  padding-left: 2em;
+}
+td.entreprise_descr_link { 
+  padding-left: 2em;
+   white-space: nowrap;
+}
+td.entreprise_descr_name { white-space: nowrap; }
+
+a.entreprise_delete {  color: black; text-decoration: underline; }
+a.entreprise_delete:visited { color: black; }
+
+a.entreprise_edit { 
+  text-decoration: underline; 
+  color : rgb(0,0,204);
+  font-weight: normal;
+}
+a.entreprise_edit:visited { color :  rgb(0,0,204); }
+a.entreprise_edit:hover {
+   color: rgb(153,51,51);
+   text-decoration: underline;
+}
+
+p.entreprise_create { padding-top: 2em; }
+a.entreprise_create { color : black; font-weight: bold; }
+a.entreprise_create:visited { color : black; }
+
+table.entreprise_list_title { 
+  width: 100%; 
+  border-top: 1px solid rgb(51,102,204); 
+  border-spacing: 0px 0px; 
+  padding: 0px;
+}
+tr.entreprise_list_title {
+  background-color: rgb(229,236,249);
+  font-weight: bold;
+}
+td.entreprise_list_title {
+  padding-left: 1em;
+}
+td.entreprise_list_title_res {
+  font-weight: normal;
+  text-align : right;
+}
+
+h2.entreprise { 
+  color: rgb(6,102,18);
+  border: 1px solid blue;
+}
+
+h2.entreprise_contact:before { 
+  content: url(/ScoDoc/static/icons/contact_img.png);
+  vertical-align: -80%;  
+  padding-right: 1em; 
+}
+h2.entreprise_correspondant:before { 
+  content: url(/ScoDoc/static/icons/correspondant_img.png);
+  vertical-align: -80%;  
+  padding-right: 1em; 
+}
+
+h2.entreprise_new:before { 
+  content: url(/ScoDoc/static/icons/entreprise_img.png);
+  vertical-align: -80%; 
+  padding-right: 2em; 
+}
+
+p.entreprise_warning, p.gtr_warning, p.gtr_interdit, p.gtr_devel { 
+  color: red; 
+  font-style: italic; 
+  margin-top: -1em;
+}
+P.entreprise_warning:before { 
+  content: url(/ScoDoc/static/icons/warning_img.png);
+  vertical-align: -80%;  
+}
+P.gtr_interdit:before {
+  content: url(/ScoDoc/static/icons/interdit_img.png);
+  vertical-align: -80%;  
+}
+P.gtr_devel:before {
+  content: url(/ScoDoc/static/icons/devel_img.png);
+  vertical-align: -80%;  
+}
+div.entreprise-insidebar { 
+  border: 1px solid blue;
+}
+
+/* ---- Sortable tables --- */
+/* Sortable tables */
+table.sortable a.sortheader {
+    background-color:#E6E6E6;
+    color: black;
+    font-weight: bold;
+    text-decoration: none;
+    display: block;
+}
+table.sortable span.sortarrow {
+    color: black;
+    text-decoration: none;
+}
+
+/* Horizontal bar graph */
+.graph {
+  width: 100px;
+  height: 12px;
+  /* background-color: rgb(200, 200, 250); */
+  padding-bottom: 0px;  
+  margin-bottom: 0;
+  margin-right: 0px;
+  margin-top: 3px;
+  margin-left: 10px;
+  border:1px solid black;
+  position: absolute;
+}
+
+.bar {
+     background-color: rgb(100, 150, 255);
+     margin: 0; 
+     padding: 0; 
+     position: absolute;
+     left: 0;
+     top: 2px;
+     height: 8px;
+     z-index: 2;
+}
+
+.mark {
+     background-color: rgb(0, 150, 0);
+     margin: 0; 
+     padding: 0; 
+     position: absolute;
+     top: 0;
+     width: 2px;
+     height: 100%;
+     z-index: 2;
+}
+
+td.cell_graph {
+    width: 170px;
+}
+
+/* ------------------ Formulaire validation semestre ---------- */
+table.recap_parcours {
+  color: black;
+  border-collapse: collapse;
+}
+
+table.recap_parcours td { 
+  padding-left: 8px;
+  padding-right: 8px;
+}
+
+.recap_parcours tr.sem_courant {
+    background-color: rgb(255, 241, 118);
+}
+
+.recap_parcours tr.sem_precedent {
+  background-color: rgb(90%,95%,90%); 
+}
+
+.recap_parcours tr.sem_autre {
+  background-color: rgb(90%,90%,90%);
+}
+
+.rcp_l1 td {
+  padding-top: 5px;
+  border-top: 3px solid rgb(50%,50%,50%);
+  border-right: 0px;
+  border-left: 0px;
+  color: blue;
+  vertical-align: top;
+}
+td.rcp_dec {
+  color: rgb(0%,0%,50%);;
+}
+td.rcp_nonass {
+  color: red;
+}
+
+.recap_hide_details tr.rcp_l2 { display: none; } 
+table.recap_hide_details td.ue_acro span { display: none; }
+
+.sco_hide { display: none; }
+
+table.recap_hide_details tr.sem_courant,  table.recap_hide_details tr.sem_precedent { 
+    display: table-row; 
+}
+table.recap_hide_details tr.sem_courant td.ue_acro span, table.recap_hide_details tr.sem_precedent td.ue_acro span { 
+    display: inline; 
+}
+
+.recap_parcours tr.sem_courant td.rcp_type_sem {
+    font-weight: bold;
+}
+.recap_parcours tr.sem_autre td.rcp_type_sem {
+    color: rgb(100%,70%,70%);
+}
+
+.recap_parcours tr.sem_autre_formation td.rcp_titre_sem {
+  background-image: repeating-linear-gradient(-45deg, rgb(100, 205, 193), rgb(100, 205, 193) 2px, transparent 5px, transparent 40px);
+}
+
+.rcp_l2 td {
+  padding-bottom: 5px;
+}
+
+td.rcp_moy {  
+}
+td.sem_ects_tit {
+ text-align: right;
+}
+span.ects_fond {
+ text-decoration: underline;
+}
+span.ects_fond:before {
+ content: "(";
+}
+span.ects_fond:after {
+ content: ")";
+}
+table.recap_parcours td.datedebut {
+    color: rgb(0,0,128);
+}
+table.recap_parcours td.datefin {
+    color: rgb(0,0,128);
+}
+table.recap_parcours td.rcp_type_sem {  
+  padding-left: 4px;
+  padding-right: 4px;
+  color: red;
+}
+td.ue_adm {
+  color: green;
+  font-weight: bold;
+}
+
+td.ue_cmp {
+  color: green;
+}
+
+td.ue { 
+}
+
+td.ue_capitalized {
+  text-decoration: underline;
+}
+
+h3.sfv {   
+  margin-top: 0px;
+}
+
+form.sfv_decisions { 
+  border:1px solid blue;
+  padding: 6px;
+  margin-right: 2px;
+}
+form.sfv_decisions_manuelles { 
+  margin-top: 10px;
+}
+th.sfv_subtitle { 
+  text-align: left;
+  font-style: italic;  
+}
+
+
+tr.sfv_ass { 
+  background-color: rgb(90%,90%,80%);
+}
+
+tr.sfv_pbass { 
+  background-color: rgb(90%,70%,80%);
+}
+
+div.link_defaillance {
+  padding-top: 8px;
+  font-weight: bold;
+}
+
+div.pas_sembox {
+  margin-top: 10px;
+  border: 2px solid #a0522d;
+  padding: 5px;
+  margin-right: 10px;
+  font-family: arial,verdana,sans-serif;
+}
+
+div.pas_empty_sems {
+}
+
+span.sp_etape {
+  display: inline-block;
+  width: 4em;
+  font-family: "Andale Mono", "Courier";
+  font-size: 75%;
+  color: black;
+}
+
+.inscrit {
+/*  font-weight: bold; */
+}
+
+.inscrailleurs {
+  font-weight: bold;
+  color: red !important;
+}
+
+span.paspaye, span.paspaye a {
+  color:  #9400d3 !important;
+}
+
+span.finalisationinscription {
+    color: green;
+}
+
+.pas_sembox_title a {
+  font-weight: bold;
+  font-size: 100%;
+  color: #1C721C;
+}
+
+.pas_sembox_subtitle {
+  font-weight: normal;
+  font-size: 100%;
+  color: blue;
+  border-bottom: 1px solid  rgb(50%,50%,50%);
+  margin-bottom: 8px;
+}
+
+.pas_recap {  
+  font-weight: bold;
+  font-size: 110%;
+  margin-top: 10px;
+}
+
+div.pas_help {
+  width: 80%;
+  font-size: 80%;
+  background-color: rgb(90%,90%,90%);
+  color: rgb(40%,20%,0%);
+  margin-top: 30px;
+  margin-bottom: 30px;
+}
+div.pas_help_left {
+  float: left;
+}
+span.libelle {
+    font-weight: bold;
+}  
+span.anomalie {
+    font-style: italic;
+}
+
+/* ---- check absences / evaluations ---- */
+div.module_check_absences h2 { 
+  font-size: 100%;
+  color: blue;
+  margin-bottom:0px;
+}
+
+div.module_check_absences h2.eval_check_absences {  
+  font-size: 80%;
+  color: black;
+  margin-left: 20px;
+  margin-top:0px;
+  margin-bottom:5px;
+}
+
+div.module_check_absences h3 {  
+  font-size: 80%;
+  color: rgb(133,0,0);
+  margin-left: 40px;
+  margin-top:0px;
+  margin-bottom:0px;
+}
+
+div.module_check_absences ul { 
+  margin-left: 60px;
+  font-size: 80%;
+  margin-top:0px;
+  margin-bottom:0px;
+}
+
+/* ----------------------------------------------- */
+/*    Help bubbles (aka tooltips)                  */
+/* ----------------------------------------------- */
+.tooltip{
+  width: 200px;
+  color:#000;
+  font:lighter 11px/1.3 Arial,sans-serif;
+  text-decoration:none;
+  text-align:center;
+}
+
+.tooltip span.top{
+  padding: 30px 8px 0;
+  background: url(/ScoDoc/static/icons/bt_gif.png) no-repeat top;
+}
+
+.tooltip b.bottom{
+  padding:3px 8px 15px;
+  color: #548912;
+  background: url(/ScoDoc/static/icons/bt_gif.png) no-repeat bottom;
+}
+/* ----------------------------------------------- */
+
+/* ----------------------------- */
+/* TABLES generees par gen_table */
+/* ----------------------------- */
+/* Voir gt_table.css les definitions s'appliquant à toutes les tables
+ */
+
+table.table_cohorte tfoot tr td, table.table_cohorte tfoot tr th { 
+  background-color: rgb(90%,95%,100%);
+  border-right:  1px solid #dddddd; 
+}
+table.table_cohorte tfoot tr th {
+  text-align: left;
+  border-left:  1px solid #dddddd; 
+  font-weight: normal;
+}
+
+table.table_coldate tr td:first-child { /* largeur col. date/time */
+  width: 12em;
+  color: rgb(0%,0%,50%);
+}
+
+
+table.table_listegroupe tr td { 
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+}
+
+table.list_users td.roles {
+  width: 22em;
+}
+
+table.list_users td.date_modif_passwd {
+  white-space: nowrap;
+}
+
+table.formsemestre_description tr.table_row_ue td {
+  font-weight: bold;
+}
+
+/* --- */
+tr#tf_extue_decl > td, tr#tf_extue_note > td {
+   padding-top: 20px;
+}
+
+tr#tf_extue_titre > td, tr#tf_extue_acronyme > td, tr#tf_extue_type > td, tr#tf_extue_ects > td {
+    padding-left: 20px;
+}
+
+/* ----------------------------- */
+
+div.form_rename_partition {
+  margin-top: 2em;
+  margin-bottom: 2em;
+}
+
+
+td.calday {
+  text-align: right;
+  vertical-align: top;
+}
+
+div.cal_evaluations table.monthcalendar td.calcol {
+}
+
+div.cal_evaluations table.monthcalendar td.calcell {
+  padding-left: 0.6em;
+  width: 6em;
+}
+
+
+div.cal_evaluations table.monthcalendar td a {
+  color: rgb(128,0,0);
+}
+
+#lyc_map_canvas { 
+   width:  900px;
+   height: 600px; 
+}
+
+div.othersemlist {
+    margin-bottom: 10px;
+    margin-right: 5px;
+    padding-bottom: 4px;
+    padding-left: 5px;
+    border: 1px solid gray;
+}
+div.othersemlist p {
+   font-weight: bold;
+   margin-top: 0px;
+}
+div.othersemlist input {
+   margin-left: 20px;
+}
+
+
+div.update_warning {
+    border: 1px solid red;
+    background-color: rgb(250,220,220);
+    margin: 3ex;
+    padding-left: 1ex;
+    padding-right: 1ex;
+    padding-bottom: 1ex;
+}
+div.update_warning span:before {
+    content: url(/ScoDoc/static/icons/warning_img.png);
+    vertical-align: -80%;  
+}
+div.update_warning_sub {
+    font-size: 80%;
+    padding-left: 8ex;
+}
+
+/* Titres des tabs: */
+.nav-tabs li a { 
+    /* font-variant: small-caps; */
+    /*font-size: 13pt;*/
+}
+
+/*
+#group-tabs {
+ clear: both;
+}
+
+#group-tabs ul {
+ display: inline;
+}
+
+#group-tabs ul li {
+ display: inline;
+}
+*/
+
+/* Page accueil */
+#scodoc_attribution p {
+   font-size:75%;
+}
+div.maindiv {
+   margin: 1em;
+}
+ul.main {
+   list-style-type: square;
+}
+
+ul.main li {
+   padding-bottom: 2ex;
+}
+
+
+#scodoc_admin {
+   background-color: #EEFFFF;
+}
+#message {
+   margin-top: 2px;
+   margin-bottom: 0px;
+   padding:  0.1em;
+   margin-left: auto;
+   margin-right: auto;
+   background-color: #ffff73;
+   -moz-border-radius: 8px;
+   -khtml-border-radius: 8px;
+   border-radius: 8px;
+   font-family : arial, verdana, sans-serif ;
+   font-weight: bold;
+   width: 40%;
+   text-align: center;
+   color: red;
+}
+h4.scodoc {
+   padding-top: 20px;
+   padding-bottom: 0px;
+}
+
+tr#erroneous_ue td {
+   color: red;
+}
+
+/* Export Apogee */
+
+div.apo_csv_infos {
+   margin-bottom: 12px;
+}
+
+div.apo_csv_infos span:first-of-type {
+   font-weight: bold;
+   margin-right: 2ex;
+}
+
+div.apo_csv_infos span:last-of-type {
+   font-weight: bold;
+   font-family: "Andale Mono", "Courier";
+   margin-right: 2ex;
+}
+
+div.apo_csv_1 {
+  margin-bottom: 10px;
+}
+
+div.apo_csv_status {
+   margin-top: 20px; 
+   padding-left: 22px;
+}
+
+div.apo_csv_status li {
+   margin: 10px 0;
+}
+
+div.apo_csv_status span {
+   font-family : arial, verdana, sans-serif ;
+   font-weight: bold;
+}
+
+div.apo_csv_status_nok {
+   background: url(/ScoDoc/static/icons/bullet_warning_img.png) no-repeat left top 0px;
+}
+
+div.apo_csv_status_missing_elems {
+   background: url(/ScoDoc/static/icons/bullet_warning_img.png) no-repeat left top 0px;
+   padding-left: 22px;
+}
+
+div#param_export_res {
+    padding-top: 1em;
+}
+
+div#apo_elements span.apo_elems {
+    font-family: "Andale Mono", "Courier";
+    font-weight: normal;
+    font-size: 9pt;
+}
+
+div#apo_elements span.apo_elems .missing {
+    color: red;
+    font-weight: normal;
+}
+
+div.apo_csv_jury_nok li {
+  color: red;
+}
+
+
+pre.small_pre_acc {
+  font-size: 60%;
+  width: 90%;
+  height: 20em;
+  background-color: #fffff0;
+  overflow: scroll;
+}
+
+.apo_csv_jury_ok input[type=submit] {
+  color: green;
+}
+
+li.apo_csv_warning, .apo_csv_problems li.apo_csv_warning {
+  color: darkorange;
+}
+
+.apo_csv_problems li {
+ color: red;
+}
+
+table.apo_maq_list {
+  margin-bottom: 12px;
+}
+
+table.apo_maq_table tr.apo_not_scodoc td:last-of-type {
+ color: red;
+}
+table.apo_maq_table th:last-of-type {
+ width: 4em;
+ text-align: left;
+}
+
+div.apo_csv_list {
+  margin-top: 4px;
+  padding-left: 5px;
+  padding-bottom: 5px;
+  border: 1px dashed rgb(150,10,40);
+}
+#apo_csv_download {
+   margin-top: 5px;
+}
+
+div.apo_compare_csv_form_but {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+div.apo_compare_csv_form_submit {
+  
+  
+}
+div.apo_compare_csv_form_submit input {
+  margin-top: 2ex;
+  margin-left: 5em;
+  font-size: 120%;
+}
+.apo_compare_csv div.section .tit {
+  margin-top: 10px;
+  font-size: 120%;
+  font-weight: bold;
+}
+
+.apo_compare_csv div.section .key {
+  font-size: 110%;
+}
+.apo_compare_csv div.section .val_ok {
+  font-size: 110%;
+  color: green;
+  font-weight: bold;
+  font-family: "Courier New", Courier, monospace;
+}
+.apo_compare_csv div.section .val_dif {
+  font-size: 110%;
+  color: red;
+  font-weight: bold;
+  font-family: "Courier New", Courier, monospace;
+}
+.apo_compare_csv div.section .p_ok {
+  font-size: 100%;
+  font-style: italic;
+  color: green;
+  margin-left: 4em;
+}
+.apo_compare_csv div.section .s_ok {
+  font-size: 100%;
+  font-style: italic;
+  color: green;
+}
+.apo_compare_csv div.sec_table {
+  margin-bottom: 10px;
+  margin-top: 20px;
+}
+.apo_compare_csv div.sec_table .gt_table {
+  font-size: 100%;
+}
+.apo_compare_csv div.sec_table .gt_table td.val_A, .apo_compare_csv div.sec_table .gt_table td.val_B  {
+  color: red;
+  font-weight: bold;
+  text-align: center;
+}
+.apo_compare_csv div.sec_table .gt_table td.type_res {
+  text-align: center;
+}
+
+div.semset_descr {
+  border: 1px dashed rgb(10,150,40);
+  padding-left: 5px;
+}
+
+div.semset_descr p {
+  margin: 5px;
+}
+
+ul.semset_listsems li {
+  margin-top: 10px;
+}
+ul.semset_listsems li:first-child {
+    margin-top:0;
+}
+span.box_title {
+  font-weight: bold;
+  font-size: 115%;
+}
+
+div.apo_csv_status {
+  border: 1px dashed red;
+  padding-bottom: 5px;
+}
+
+.form_apo_export input[type="submit"]
+{
+    -webkit-appearance: button;
+    font-size:150%;
+    font-weight: bold;
+    color: green;
+    margin: 10px;
+}
+
+span.vdi_label {
+  padding-left: 2em;
+}
+
+/* Poursuites edtude PE */
+form#pe_view_sem_recap_form div.pe_template_up {
+    margin-top: 20px;
+    margin-bottom: 30px;
+}
+
+/* Editable */
+span.span_apo_edit {
+    border-bottom: 1px dashed #84ae84;
+}
+
+/* Tags */
+.notes_module_list span.sco_tag_edit {    
+    display: none;
+}
+span.sco_tag_edit .tag-editor {
+    background-color: rgb(210,210,210);
+    border: 0px;
+    margin-left: 40px;
+    margin-top: 2px;
+}
+
+span.sco_tag_edit .tag-editor-delete {
+    height: 20px;
+}
+
+/* Autocomplete noms */
+.ui-menu-item {
+    font-family:   "Helvetica Neue",Helvetica,Arial,sans-serif;
+    font-size: 11pt;
+}
+
+div#formsemestre_ext_edit_ue_validations {
+  margin-bottom: 2ex;
+}
+form.tf_ext_edit_ue_validations table {
+  border-collapse: collapse;
+  width: 97%;
+  margin-left: 1em;ext
+  margin-right: 1em;
+}
+form.tf_ext_edit_ue_validations table, form.tf_ext_edit_ue_validations table th, form.tf_ext_edit_ue_validations table td {
+  /* border: 1px solid black; */
+}
+form.tf_ext_edit_ue_validations table th, form.tf_ext_edit_ue_validations table td {
+  border-bottom: 1px solid rgb(168, 168, 168);
+}
+form.tf_ext_edit_ue_validations table th {
+  padding-left: 1em;
+  padding-right: 1em;
+}
+form.tf_ext_edit_ue_validations table td.tf_field_note, form.tf_ext_edit_ue_validations table td.tf_field_coef {
+  text-align: center;
+}
+form.tf_ext_edit_ue_validations table td.tf_field_note input {
+  margin-left: 1em;
+}
+span.ext_sem_moy {
+  font-weight: bold;
+  color: rgb(122, 40, 2);
+  font-size: 120%;
+}
\ No newline at end of file
diff --git a/static/css/verticalhisto.css b/static/css/verticalhisto.css
new file mode 100644
index 0000000000000000000000000000000000000000..57dc34bf8b70a77bd7978dc3f353b89cd119acb5
--- /dev/null
+++ b/static/css/verticalhisto.css
@@ -0,0 +1,88 @@
+
+#vhist-q-graph {
+/* Ceci est la classe du UL principal qui contient toute sles autres donn�es */
+/* D�finir un Width et un Height est essentiel, c'est ca qui donne les dimension du graphique */
+  width: 428px;
+  height: 200px;
+/* Ne pas oublier d'enlever le point qui se trouve a cot� des LI */
+  list-style: none;
+/* le reste n'est que positionnement et esth�tique */
+  position: relative; 
+  margin: 1.1em 0 3.5em;
+  padding: 0;
+  background: #DDD;
+  border: 2px solid gray;
+  font: 9px Helvetica, Geneva, sans-serif;
+}
+#vhist-q-graph ul {
+/* Une colonne �tant un UL dans un UL, on d�fini les param�tres des colonnes */
+  margin: 0; 
+  padding: 0; 
+  list-style: none;
+}
+#vhist-q-graph li {
+/* Ceci d�fini les colonnes (largeur, positionnement des b�ton a l'int�rieur,position du titre...) */
+  position: absolute; 
+  bottom: 0; 
+  width: 150px; 
+  z-index: 2;
+  margin: 0; 
+  padding: 0;
+  text-align: center; 
+  list-style: none;
+}
+#vhist-q-graph li.vhist-qtr { /* colonnes */
+  height: 198px; 
+  padding-top: 2px;
+  /* border-right: 1px dotted #C4C4C4; */
+  color: #AAA;
+  border:0; /* sans colonnes */
+}
+#vhist-q-graph li.vhist-bar {
+/* esth�tique des batons */
+  width: 16px;  
+  border: 1px solid;  
+  border-bottom: none;  
+  color: #000;
+}
+#vhist-q-graph li.vhist-bar p { 
+/* esth�tique de titre dans les b�tons */
+  margin: 0px 0 0;  
+  padding: 0; 
+}
+
+p.leg { /* legende (sous l'axe horizontal) */
+   position: absolute;
+   bottom: -18px;
+   left: 2px;
+   width: 10px;
+   color: red;
+   text-align: right;
+}
+
+#vhist-q-graph li.vhist-sent {
+/* la position left est importante car le b�ton 2 a comme left : left(baton1) + espacement. Il faut donc pens� a espacer les b�ton !! */
+  left: 5px;
+/* esth�tique d'un baton en particulier */
+  background: #DCA;
+  border-color: #EDC #BA9 #000 #EDC;
+}
+#vhist-q-graph li.vhist-paid {
+/* la position left est importante car le b�ton 2 a comme left : left(baton1) + espacement. Il faut donc pens� a espacer les b�ton !! */
+  left: 77px;  
+/* esth�tique d'un b�ton en particulier */
+  background: #9D9;
+  border-color: #CDC #9B9 #000 #BFB;
+}
+/* position de la colonne 1 */
+#vhist-q-graph #q1 {left: 0;}
+/* position de la colonne 2 */
+#vhist-q-graph #q2 {left: 150px;}
+/* position de la colonne 3 */
+#vhist-q-graph #q3 {left: 300px;}
+/* position de la colonne 4 */
+#vhist-q-graph #q4 {left: 450px; border-right: none;}
+
+.vhist-bar { 
+  background: yellow;
+}
diff --git a/static/icons/arrow_down.png b/static/icons/arrow_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..722baeda21d19512815b3f5580c51f647b7ee7e9
Binary files /dev/null and b/static/icons/arrow_down.png differ
diff --git a/static/icons/arrow_none.png b/static/icons/arrow_none.png
new file mode 100644
index 0000000000000000000000000000000000000000..73c1df996e3a00fa763b1799ea119c4bae1fa4c2
Binary files /dev/null and b/static/icons/arrow_none.png differ
diff --git a/static/icons/arrow_up.png b/static/icons/arrow_up.png
new file mode 100644
index 0000000000000000000000000000000000000000..f1464f347f042fa4ddfe2e41e16691d7978f2188
Binary files /dev/null and b/static/icons/arrow_up.png differ
diff --git a/static/icons/as_pointer.png b/static/icons/as_pointer.png
new file mode 100644
index 0000000000000000000000000000000000000000..698f20d2409e96fe04d3e2f99666be6128ef0dff
Binary files /dev/null and b/static/icons/as_pointer.png differ
diff --git a/static/icons/borgne_img.png b/static/icons/borgne_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..758b4b07dadc9819d8addf6645c706b3a6dc18b8
Binary files /dev/null and b/static/icons/borgne_img.png differ
diff --git a/static/icons/bt_gif.png b/static/icons/bt_gif.png
new file mode 100644
index 0000000000000000000000000000000000000000..2149b59b4addac1efdfd8586d4ba28f28b3bb195
Binary files /dev/null and b/static/icons/bt_gif.png differ
diff --git a/static/icons/bullet_arrow.png b/static/icons/bullet_arrow.png
new file mode 100644
index 0000000000000000000000000000000000000000..683fc83c8f336e3148953f7d99989fd12170c021
Binary files /dev/null and b/static/icons/bullet_arrow.png differ
diff --git a/static/icons/bullet_plus.png b/static/icons/bullet_plus.png
new file mode 100644
index 0000000000000000000000000000000000000000..24f060dfcd628dbc95e37aef9678ebf758b57a49
Binary files /dev/null and b/static/icons/bullet_plus.png differ
diff --git a/static/icons/bullet_warning_img.png b/static/icons/bullet_warning_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..62fcfed0ca0ae26cb5123cf26faed03314592677
Binary files /dev/null and b/static/icons/bullet_warning_img.png differ
diff --git a/static/icons/calendar_img.png b/static/icons/calendar_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d552d050813dcf916098b07954ca7d24b087d2a
Binary files /dev/null and b/static/icons/calendar_img.png differ
diff --git a/static/icons/contact_img.png b/static/icons/contact_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..d4d8e1f3fbcdf698e6cf13d0bacbdad527214591
Binary files /dev/null and b/static/icons/contact_img.png differ
diff --git a/static/icons/correspondant_img.png b/static/icons/correspondant_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..40c2997d72b64f35f095d3b4aa313148bca91b57
Binary files /dev/null and b/static/icons/correspondant_img.png differ
diff --git a/static/icons/delete_img.png b/static/icons/delete_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..2415dfc78b0ca4a93995be5b71ef00d2f25784d9
Binary files /dev/null and b/static/icons/delete_img.png differ
diff --git a/static/icons/delete_small_dis_img.png b/static/icons/delete_small_dis_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..9e2bb1fc056e59f060558e9bb35ad9adf5e07d87
Binary files /dev/null and b/static/icons/delete_small_dis_img.png differ
diff --git a/static/icons/delete_small_img.png b/static/icons/delete_small_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e016bbf29c6377699ffcd3b62ebc920a1c35b79
Binary files /dev/null and b/static/icons/delete_small_img.png differ
diff --git a/static/icons/devel_img.png b/static/icons/devel_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..09d11ab11aed0ac080417ffbe7d36d0204f113ef
Binary files /dev/null and b/static/icons/devel_img.png differ
diff --git a/static/icons/edit_img.png b/static/icons/edit_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..fc9884bf3e6213a50912d2e1c2f40bfccecbef2d
Binary files /dev/null and b/static/icons/edit_img.png differ
diff --git a/static/icons/emptygroupicon_img.png b/static/icons/emptygroupicon_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..0043d8d17db591552b65abf5aeef9c675b1065a2
Binary files /dev/null and b/static/icons/emptygroupicon_img.png differ
diff --git a/static/icons/entreprise_img.png b/static/icons/entreprise_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..8eeab05604d4a61afc63ab5651d8c64c5285eff1
Binary files /dev/null and b/static/icons/entreprise_img.png differ
diff --git a/static/icons/entreprise_side_img.png b/static/icons/entreprise_side_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..b3d990b80d110a5a4828ee0f9d327ee0aa1dcd5e
Binary files /dev/null and b/static/icons/entreprise_side_img.png differ
diff --git a/static/icons/eye_img.png b/static/icons/eye_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..8d68bac0f17cfcdfd6340c9e931ed117e263bab4
Binary files /dev/null and b/static/icons/eye_img.png differ
diff --git a/static/icons/firefox_fr.png b/static/icons/firefox_fr.png
new file mode 100644
index 0000000000000000000000000000000000000000..aeafb5a4d35120a76e957a629cdadf8da3965ee3
Binary files /dev/null and b/static/icons/firefox_fr.png differ
diff --git a/static/icons/flag_uk_img.png b/static/icons/flag_uk_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..b83c20457abf7a73682f59d297849a508b17bb17
Binary files /dev/null and b/static/icons/flag_uk_img.png differ
diff --git a/static/icons/formula.png b/static/icons/formula.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3ed8daafb8b0970a7962eaddbe3ff330054f219
Binary files /dev/null and b/static/icons/formula.png differ
diff --git a/static/icons/groupicon_img.png b/static/icons/groupicon_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..31b6f339912deb2f421694490b45bad9f67ecf68
Binary files /dev/null and b/static/icons/groupicon_img.png differ
diff --git a/static/icons/hide_img.png b/static/icons/hide_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..c8e97e96a8bea30a1f762114fd0cb894111e8173
Binary files /dev/null and b/static/icons/hide_img.png differ
diff --git a/static/icons/hl_corner_bl.png b/static/icons/hl_corner_bl.png
new file mode 100644
index 0000000000000000000000000000000000000000..ce560e78d5db1d54337e69c0a9ad8b04d0e12af6
Binary files /dev/null and b/static/icons/hl_corner_bl.png differ
diff --git a/static/icons/hl_corner_br.png b/static/icons/hl_corner_br.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef6ce44492af4f31644c994157d190a9c64e6ddc
Binary files /dev/null and b/static/icons/hl_corner_br.png differ
diff --git a/static/icons/hl_corner_tl.png b/static/icons/hl_corner_tl.png
new file mode 100644
index 0000000000000000000000000000000000000000..92085768702178bc7eaeb66a36547385d9908be2
Binary files /dev/null and b/static/icons/hl_corner_tl.png differ
diff --git a/static/icons/hl_corner_tr.png b/static/icons/hl_corner_tr.png
new file mode 100644
index 0000000000000000000000000000000000000000..7f1bdfbcc77698e6e0dedf1a63092edd7775408a
Binary files /dev/null and b/static/icons/hl_corner_tr.png differ
diff --git a/static/icons/info-16_img.png b/static/icons/info-16_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..25e71d2a872822f3d0f27a625e0a4b6ef7ab5d04
Binary files /dev/null and b/static/icons/info-16_img.png differ
diff --git a/static/icons/info-24_img.png b/static/icons/info-24_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..df733178251661c6bba479f68a7216411ec84fe3
Binary files /dev/null and b/static/icons/info-24_img.png differ
diff --git a/static/icons/interdit_img.png b/static/icons/interdit_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc7830e26ef2b67b23587061997543e4eeadeb21
Binary files /dev/null and b/static/icons/interdit_img.png differ
diff --git a/static/icons/loading.jpg b/static/icons/loading.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..beb660d72df6c49f2994e89c1d99ee3cf407e757
Binary files /dev/null and b/static/icons/loading.jpg differ
diff --git a/static/icons/lock32_img.png b/static/icons/lock32_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d19ffc69dc0fb98d8153d0a78019dba3cbf1dab
Binary files /dev/null and b/static/icons/lock32_img.png differ
diff --git a/static/icons/lock_img.png b/static/icons/lock_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..77ae0f24e19d43474e9ec1f37b583a52ec5b2457
Binary files /dev/null and b/static/icons/lock_img.png differ
diff --git a/static/icons/menuarrow1_img.png b/static/icons/menuarrow1_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..90c6b6fe90b087730de50ccb32557d71144b1326
Binary files /dev/null and b/static/icons/menuarrow1_img.png differ
diff --git a/static/icons/minus18_img.png b/static/icons/minus18_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..409a394d67329cf171795115d7a0acfa16ef78f1
Binary files /dev/null and b/static/icons/minus18_img.png differ
diff --git a/static/icons/minus_img.png b/static/icons/minus_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..9648a2189fc9ce4a0de2a194c2b9c1d09b9ac9c5
Binary files /dev/null and b/static/icons/minus_img.png differ
diff --git a/static/icons/next_img.png b/static/icons/next_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..8ff1c77662f7afe1a33f4685c12516974bf8cffa
Binary files /dev/null and b/static/icons/next_img.png differ
diff --git a/static/icons/notes_icon.png b/static/icons/notes_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..22220d872b83ed2901f3237d1d673a119ca333c1
Binary files /dev/null and b/static/icons/notes_icon.png differ
diff --git a/static/icons/notes_img.png b/static/icons/notes_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..71962cfcc1adf6abf58a7d84ef6e0589979e08e6
Binary files /dev/null and b/static/icons/notes_img.png differ
diff --git a/static/icons/nouv_img.png b/static/icons/nouv_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..c149258509b2a836308a377df4760d8311cc95e6
Binary files /dev/null and b/static/icons/nouv_img.png differ
diff --git a/static/icons/pdficon16x20_img.png b/static/icons/pdficon16x20_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..f4fd5923bc341d99c1e795a77fef48cf70010e5a
Binary files /dev/null and b/static/icons/pdficon16x20_img.png differ
diff --git a/static/icons/pdficon_img.png b/static/icons/pdficon_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..6438be6b2bbab9c97d9c3c12cd5781b15a4dd458
Binary files /dev/null and b/static/icons/pdficon_img.png differ
diff --git a/static/icons/plus18_img.png b/static/icons/plus18_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..ca3a05c564634d88362cb948094f4e7186193e1a
Binary files /dev/null and b/static/icons/plus18_img.png differ
diff --git a/static/icons/plus_img.png b/static/icons/plus_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9ee6407b2a67b6fdffdf31a495e8eea271c9c16
Binary files /dev/null and b/static/icons/plus_img.png differ
diff --git a/static/icons/prev_img.png b/static/icons/prev_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..647cd315039c6749c9c4fb0ee5508735e614148b
Binary files /dev/null and b/static/icons/prev_img.png differ
diff --git a/static/icons/pythonpoweredsmall_img.png b/static/icons/pythonpoweredsmall_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..e4763bee15c4a870e2edee8e82b0bc67736c697f
Binary files /dev/null and b/static/icons/pythonpoweredsmall_img.png differ
diff --git a/static/icons/roundedge_img.png b/static/icons/roundedge_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..dafc292b8796e51f288f7736a536a6bf935411ae
Binary files /dev/null and b/static/icons/roundedge_img.png differ
diff --git a/static/icons/rssscodoc_img.png b/static/icons/rssscodoc_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..b3c949d2244f2c0c81d65e74719af2a1b56d06a3
Binary files /dev/null and b/static/icons/rssscodoc_img.png differ
diff --git a/static/icons/sco_icon.png b/static/icons/sco_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..96427f2634cd22a7b847cfe7ade619d2f88731d1
Binary files /dev/null and b/static/icons/sco_icon.png differ
diff --git a/static/icons/scologo_img.png b/static/icons/scologo_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..525f5d34cddab3fc27deacd1e75a1dca703eb5e2
Binary files /dev/null and b/static/icons/scologo_img.png differ
diff --git a/static/icons/smallpolar_img.png b/static/icons/smallpolar_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..250babf43c49134b4c5631d9a405b96f8b21939a
Binary files /dev/null and b/static/icons/smallpolar_img.png differ
diff --git a/static/icons/status_green_img.png b/static/icons/status_green_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..698c8d95926b597f6cd7e7015da1a1aa46fbad45
Binary files /dev/null and b/static/icons/status_green_img.png differ
diff --git a/static/icons/status_greenorange_img.gif b/static/icons/status_greenorange_img.gif
new file mode 100644
index 0000000000000000000000000000000000000000..e0b90afa2fa4feb4c080cc6f09a79def34e02775
Binary files /dev/null and b/static/icons/status_greenorange_img.gif differ
diff --git a/static/icons/status_orange_img.png b/static/icons/status_orange_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..f142bb6c337d8e8d62bbe4c27511d1b9e2d3da1d
Binary files /dev/null and b/static/icons/status_orange_img.png differ
diff --git a/static/icons/status_visible_img.png b/static/icons/status_visible_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..d686c7f4eaa8fce4c75812c39d0fef10e2815638
Binary files /dev/null and b/static/icons/status_visible_img.png differ
diff --git a/static/icons/table_brushed_jpg.png b/static/icons/table_brushed_jpg.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d9ce77193fc4a36cba49f909212207ac3cccd22
Binary files /dev/null and b/static/icons/table_brushed_jpg.png differ
diff --git a/static/icons/takebacktheweb_small.png b/static/icons/takebacktheweb_small.png
new file mode 100644
index 0000000000000000000000000000000000000000..e5c700862de9b37f94bb3607dc967d5681e5a338
Binary files /dev/null and b/static/icons/takebacktheweb_small.png differ
diff --git a/static/icons/trust_firefox_gif.png b/static/icons/trust_firefox_gif.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f6abf5fcd8eeea7f2b54ce4218d633f979f23a8
Binary files /dev/null and b/static/icons/trust_firefox_gif.png differ
diff --git a/static/icons/ul_corner_bl.png b/static/icons/ul_corner_bl.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f9b08060c4b3a9f69e98f6889ccefbb4646d346
Binary files /dev/null and b/static/icons/ul_corner_bl.png differ
diff --git a/static/icons/ul_corner_br.png b/static/icons/ul_corner_br.png
new file mode 100644
index 0000000000000000000000000000000000000000..360871bffb416d41f92022169e997cf26be6971c
Binary files /dev/null and b/static/icons/ul_corner_br.png differ
diff --git a/static/icons/ul_corner_tl.png b/static/icons/ul_corner_tl.png
new file mode 100644
index 0000000000000000000000000000000000000000..63620def9c62bccf18eb3935fa2ce86f878aeca4
Binary files /dev/null and b/static/icons/ul_corner_tl.png differ
diff --git a/static/icons/ul_corner_tr.png b/static/icons/ul_corner_tr.png
new file mode 100644
index 0000000000000000000000000000000000000000..38dbb9f6db3816e4652f3888a8b4cdcef91fb59c
Binary files /dev/null and b/static/icons/ul_corner_tr.png differ
diff --git a/static/icons/unknown.jpg b/static/icons/unknown.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..4f8f3904fec6a6afdff978e0b0db2fdf6bf3e0ec
Binary files /dev/null and b/static/icons/unknown.jpg differ
diff --git a/static/icons/unknown_img.png b/static/icons/unknown_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..a2a4b75b680b30f1e35a115a432e65c34120b694
Binary files /dev/null and b/static/icons/unknown_img.png differ
diff --git a/static/icons/warning16_img.png b/static/icons/warning16_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad2ca6ee2997bdde82cf019b5dc7f4ae8ebad026
Binary files /dev/null and b/static/icons/warning16_img.png differ
diff --git a/static/icons/warning21_img.png b/static/icons/warning21_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..b728713bf5aa9804f281f38b51f4df023888a960
Binary files /dev/null and b/static/icons/warning21_img.png differ
diff --git a/static/icons/warning_img.png b/static/icons/warning_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..d6f2306dba4ece5c3880ef9e0f4ad3eb0c438bff
Binary files /dev/null and b/static/icons/warning_img.png differ
diff --git a/static/icons/xlsicon_img.png b/static/icons/xlsicon_img.png
new file mode 100644
index 0000000000000000000000000000000000000000..961979d6d5b17d4f44d87f54cc5f074d8b47d2b3
Binary files /dev/null and b/static/icons/xlsicon_img.png differ
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/static/jQuery/jquery-1.12.4.min.js b/static/jQuery/jquery-1.12.4.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..e836475870da67f3c72f64777c6e0f37d9f4c87b
--- /dev/null
+++ b/static/jQuery/jquery-1.12.4.min.js
@@ -0,0 +1,5 @@
+/*! jQuery v1.12.4 | (c) jQuery Foundation | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=la(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=ma(b);function pa(){}pa.prototype=d.filters=d.pseudos,d.setFilters=new pa,g=fa.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=R.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=S.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(Q," ")}),h=h.slice(c.length));for(g in d.filter)!(e=W[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fa.error(a):z(a,i).slice(0)};function qa(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){n.each(b,function(b,c){n.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==n.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return n.each(arguments,function(a,b){var c;while((c=n.inArray(b,f,c))>-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0;
+}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=n._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}}),function(){var a;l.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,e;return c=d.getElementsByTagName("body")[0],c&&c.style?(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(d.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(e),a):void 0}}();var T=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,U=new RegExp("^(?:([+-])=|)("+T+")([a-z%]*)$","i"),V=["Top","Right","Bottom","Left"],W=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)};function X(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return n.css(a,b,"")},i=h(),j=c&&c[3]||(n.cssNumber[b]?"":"px"),k=(n.cssNumber[b]||"px"!==j&&+i)&&U.exec(n.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,n.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var Y=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)Y(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav></:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="<textarea>x</textarea>",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:l.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/<tbody/i;function ia(a){Z.test(a.type)&&(a.defaultChecked=a.checked)}function ja(a,b,c,d,e){for(var f,g,h,i,j,k,m,o=a.length,p=ca(b),q=[],r=0;o>r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?"<table>"!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[n.expando])return a;var b,c,e,f=a.type,g=a,h=this.fixHooks[f];h||(this.fixHooks[f]=h=ma.test(f)?this.mouseHooks:la.test(f)?this.keyHooks:{}),e=h.props?this.props.concat(h.props):this.props,a=new n.Event(g),b=e.length;while(b--)c=e[b],a[c]=g[c];return a.target||(a.target=g.srcElement||d),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,h.filter?h.filter(a,g):a},props:"altKey bubbles cancelable ctrlKey currentTarget detail eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,e,f,g=b.button,h=b.fromElement;return null==a.pageX&&null!=b.clientX&&(e=a.target.ownerDocument||d,f=e.documentElement,c=e.body,a.pageX=b.clientX+(f&&f.scrollLeft||c&&c.scrollLeft||0)-(f&&f.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(f&&f.scrollTop||c&&c.scrollTop||0)-(f&&f.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&h&&(a.relatedTarget=h===a.target?b.toElement:h),a.which||void 0===g||(a.which=1&g?1:2&g?3:4&g?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==ra()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:"focusin"},blur:{trigger:function(){return this===ra()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return n.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c){var d=n.extend(new n.Event,c,{type:a,isSimulated:!0});n.event.trigger(d,null,b),d.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=d.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)}:function(a,b,c){var d="on"+b;a.detachEvent&&("undefined"==typeof a[d]&&(a[d]=null),a.detachEvent(d,c))},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?pa:qa):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={constructor:n.Event,isDefaultPrevented:qa,isPropagationStopped:qa,isImmediatePropagationStopped:qa,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=pa,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=pa,a&&!this.isSimulated&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=pa,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||n.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),l.submit||(n.event.special.submit={setup:function(){return n.nodeName(this,"form")?!1:void n.event.add(this,"click._submit keypress._submit",function(a){var b=a.target,c=n.nodeName(b,"input")||n.nodeName(b,"button")?n.prop(b,"form"):void 0;c&&!n._data(c,"submit")&&(n.event.add(c,"submit._submit",function(a){a._submitBubble=!0}),n._data(c,"submit",!0))})},postDispatch:function(a){a._submitBubble&&(delete a._submitBubble,this.parentNode&&!a.isTrigger&&n.event.simulate("submit",this.parentNode,a))},teardown:function(){return n.nodeName(this,"form")?!1:void n.event.remove(this,"._submit")}}),l.change||(n.event.special.change={setup:function(){return ka.test(this.nodeName)?("checkbox"!==this.type&&"radio"!==this.type||(n.event.add(this,"propertychange._change",function(a){"checked"===a.originalEvent.propertyName&&(this._justChanged=!0)}),n.event.add(this,"click._change",function(a){this._justChanged&&!a.isTrigger&&(this._justChanged=!1),n.event.simulate("change",this,a)})),!1):void n.event.add(this,"beforeactivate._change",function(a){var b=a.target;ka.test(b.nodeName)&&!n._data(b,"change")&&(n.event.add(b,"change._change",function(a){!this.parentNode||a.isSimulated||a.isTrigger||n.event.simulate("change",this.parentNode,a)}),n._data(b,"change",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||"radio"!==b.type&&"checkbox"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return n.event.remove(this,"._change"),!ka.test(this.nodeName)}}),l.focusin||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a))};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=n._data(d,b);e||d.addEventListener(a,c,!0),n._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=n._data(d,b)-1;e?n._data(d,b,e):(d.removeEventListener(a,c,!0),n._removeData(d,b))}}}),n.fn.extend({on:function(a,b,c,d){return sa(this,a,b,c,d)},one:function(a,b,c,d){return sa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=qa),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ta=/ jQuery\d+="(?:null|\d+)"/g,ua=new RegExp("<(?:"+ba+")[\\s/>]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/<script|<style|<link/i,xa=/checked\s*(?:[^=]|=\s*.checked.)/i,ya=/^true\/(.*)/,za=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=(Ja[0].contentWindow||Ja[0].contentDocument).document,b.write(),b.close(),c=La(a,b),Ja.detach()),Ka[a]=c),c}var Na=/^margin/,Oa=new RegExp("^("+T+")(?!px)[a-z%]+$","i"),Pa=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e},Qa=d.documentElement;!function(){var b,c,e,f,g,h,i=d.createElement("div"),j=d.createElement("div");if(j.style){j.style.cssText="float:left;opacity:.5",l.opacity="0.5"===j.style.opacity,l.cssFloat=!!j.style.cssFloat,j.style.backgroundClip="content-box",j.cloneNode(!0).style.backgroundClip="",l.clearCloneStyle="content-box"===j.style.backgroundClip,i=d.createElement("div"),i.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",j.innerHTML="",i.appendChild(j),l.boxSizing=""===j.style.boxSizing||""===j.style.MozBoxSizing||""===j.style.WebkitBoxSizing,n.extend(l,{reliableHiddenOffsets:function(){return null==b&&k(),f},boxSizingReliable:function(){return null==b&&k(),e},pixelMarginRight:function(){return null==b&&k(),c},pixelPosition:function(){return null==b&&k(),b},reliableMarginRight:function(){return null==b&&k(),g},reliableMarginLeft:function(){return null==b&&k(),h}});function k(){var k,l,m=d.documentElement;m.appendChild(i),j.style.cssText="-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",b=e=h=!1,c=g=!0,a.getComputedStyle&&(l=a.getComputedStyle(j),b="1%"!==(l||{}).top,h="2px"===(l||{}).marginLeft,e="4px"===(l||{width:"4px"}).width,j.style.marginRight="50%",c="4px"===(l||{marginRight:"4px"}).marginRight,k=j.appendChild(d.createElement("div")),k.style.cssText=j.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",k.style.marginRight=k.style.width="0",j.style.width="1px",g=!parseFloat((a.getComputedStyle(k)||{}).marginRight),j.removeChild(k)),j.style.display="none",f=0===j.getClientRects().length,f&&(j.style.display="",j.innerHTML="<table><tr><td></td><td>t</td></tr></table>",j.childNodes[0].style.borderCollapse="separate",k=j.getElementsByTagName("td"),k[0].style.cssText="margin:0;border:0;padding:0;display:none",f=0===k[0].offsetHeight,f&&(k[0].style.display="",k[1].style.display="none",f=0===k[0].offsetHeight)),m.removeChild(i)}}}();var Ra,Sa,Ta=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ra=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)},Sa=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ra(a),g=c?c.getPropertyValue(b)||c[b]:void 0,""!==g&&void 0!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),c&&!l.pixelMarginRight()&&Oa.test(g)&&Na.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f),void 0===g?g:g+""}):Qa.currentStyle&&(Ra=function(a){return a.currentStyle},Sa=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ra(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Oa.test(g)&&!Ta.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Ua(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Va=/alpha\([^)]*\)/i,Wa=/opacity\s*=\s*([^)]*)/i,Xa=/^(none|table(?!-c[ea]).+)/,Ya=new RegExp("^("+T+")(.*)$","i"),Za={position:"absolute",visibility:"hidden",display:"block"},$a={letterSpacing:"0",fontWeight:"400"},_a=["Webkit","O","Moz","ms"],ab=d.createElement("div").style;function bb(a){if(a in ab)return a;var b=a.charAt(0).toUpperCase()+a.slice(1),c=_a.length;while(c--)if(a=_a[c]+b,a in ab)return a}function cb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=n._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&W(d)&&(f[g]=n._data(d,"olddisplay",Ma(d.nodeName)))):(e=W(d),(c&&"none"!==c||!e)&&n._data(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function db(a,b,c){var d=Ya.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function eb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+V[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+V[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+V[f]+"Width",!0,e))):(g+=n.css(a,"padding"+V[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+V[f]+"Width",!0,e)));return g}function fb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ra(a),g=l.boxSizing&&"border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Sa(a,b,f),(0>e||null==e)&&(e=a.style[b]),Oa.test(e))return e;d=g&&(l.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+eb(a,b,c||(g?"border":"content"),d,f)+"px"}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Sa(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":l.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;if(b=n.cssProps[h]||(n.cssProps[h]=bb(h)||h),g=n.cssHooks[b]||n.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=U.exec(c))&&e[1]&&(c=X(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(n.cssNumber[h]?"":"px")),l.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=bb(h)||h),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Sa(a,b,d)),"normal"===f&&b in $a&&(f=$a[b]),""===c||c?(e=parseFloat(f),c===!0||isFinite(e)?e||0:f):f}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?Xa.test(n.css(a,"display"))&&0===a.offsetWidth?Pa(a,Za,function(){return fb(a,b,d)}):fb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ra(a);return db(a,c,d?eb(a,b,d,l.boxSizing&&"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),l.opacity||(n.cssHooks.opacity={get:function(a,b){return Wa.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=n.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===n.trim(f.replace(Va,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Va.test(f)?f.replace(Va,e):f+" "+e)}}),n.cssHooks.marginRight=Ua(l.reliableMarginRight,function(a,b){return b?Pa(a,{display:"inline-block"},Sa,[a,"marginRight"]):void 0}),n.cssHooks.marginLeft=Ua(l.reliableMarginLeft,function(a,b){return b?(parseFloat(Sa(a,"marginLeft"))||(n.contains(a.ownerDocument,a)?a.getBoundingClientRect().left-Pa(a,{
+marginLeft:0},function(){return a.getBoundingClientRect().left}):0))+"px":void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+V[d]+b]=f[d]||f[d-2]||f[0];return e}},Na.test(a)||(n.cssHooks[a+b].set=db)}),n.fn.extend({css:function(a,b){return Y(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=Ra(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return cb(this,!0)},hide:function(){return cb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){W(this)?n(this).show():n(this).hide()})}});function gb(a,b,c,d,e){return new gb.prototype.init(a,b,c,d,e)}n.Tween=gb,gb.prototype={constructor:gb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||n.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=gb.propHooks[this.prop];return a&&a.get?a.get(this):gb.propHooks._default.get(this)},run:function(a){var b,c=gb.propHooks[this.prop];return this.options.duration?this.pos=b=n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):gb.propHooks._default.set(this),this}},gb.prototype.init.prototype=gb.prototype,gb.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[n.cssProps[a.prop]]&&!n.cssHooks[a.prop]?a.elem[a.prop]=a.now:n.style(a.elem,a.prop,a.now+a.unit)}}},gb.propHooks.scrollTop=gb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},n.fx=gb.prototype.init,n.fx.step={};var hb,ib,jb=/^(?:toggle|show|hide)$/,kb=/queueHooks$/;function lb(){return a.setTimeout(function(){hb=void 0}),hb=n.now()}function mb(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=V[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function nb(a,b,c){for(var d,e=(qb.tweeners[b]||[]).concat(qb.tweeners["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ob(a,b,c){var d,e,f,g,h,i,j,k,m=this,o={},p=a.style,q=a.nodeType&&W(a),r=n._data(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,m.always(function(){m.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=n.css(a,"display"),k="none"===j?n._data(a,"olddisplay")||Ma(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(l.inlineBlockNeedsLayout&&"inline"!==Ma(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",l.shrinkWrapBlocks()||m.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],jb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(o))"inline"===("none"===j?Ma(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=n._data(a,"fxshow",{}),f&&(r.hidden=!q),q?n(a).show():m.done(function(){n(a).hide()}),m.done(function(){var b;n._removeData(a,"fxshow");for(b in o)n.style(a,b,o[b])});for(d in o)g=nb(q?r[d]:0,d,m),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function pb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function qb(a,b,c){var d,e,f=0,g=qb.prefilters.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=hb||lb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{},easing:n.easing._default},c),originalProperties:b,originalOptions:c,startTime:hb||lb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(pb(k,j.opts.specialEasing);g>f;f++)if(d=qb.prefilters[f].call(j,a,k,j.opts))return n.isFunction(d.stop)&&(n._queueHooks(j.elem,j.opts.queue).stop=n.proxy(d.stop,d)),d;return n.map(k,nb,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(qb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return X(c.elem,a,U.exec(b),c),c}]},tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.match(G);for(var c,d=0,e=a.length;e>d;d++)c=a[d],qb.tweeners[c]=qb.tweeners[c]||[],qb.tweeners[c].unshift(b)},prefilters:[ob],prefilter:function(a,b){b?qb.prefilters.unshift(a):qb.prefilters.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,null!=d.queue&&d.queue!==!0||(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(W).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=qb(this,n.extend({},a),f);(e||n._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=n._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&kb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=n._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(mb(b,!0),a,d,e)}}),n.each({slideDown:mb("show"),slideUp:mb("hide"),slideToggle:mb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=n.timers,c=0;for(hb=n.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||n.fx.stop(),hb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){ib||(ib=a.setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){a.clearInterval(ib),ib=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(b,c){return b=n.fx?n.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a,b=d.createElement("input"),c=d.createElement("div"),e=d.createElement("select"),f=e.appendChild(d.createElement("option"));c=d.createElement("div"),c.setAttribute("className","t"),c.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",a=c.getElementsByTagName("a")[0],b.setAttribute("type","checkbox"),c.appendChild(b),a=c.getElementsByTagName("a")[0],a.style.cssText="top:1px",l.getSetAttribute="t"!==c.className,l.style=/top/.test(a.getAttribute("style")),l.hrefNormalized="/a"===a.getAttribute("href"),l.checkOn=!!b.value,l.optSelected=f.selected,l.enctype=!!d.createElement("form").enctype,e.disabled=!0,l.optDisabled=!f.disabled,b=d.createElement("input"),b.setAttribute("value",""),l.input=""===b.getAttribute("value"),b.value="t",b.setAttribute("type","radio"),l.radioValue="t"===b.value}();var rb=/\r/g,sb=/[\x20\t\r\n\f]+/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a)).replace(sb," ")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],(c.selected||i===e)&&(l.optDisabled?!c.disabled:null===c.getAttribute("disabled"))&&(!c.parentNode.disabled||!n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)if(d=e[g],n.inArray(n.valHooks.option.get(d),f)>-1)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>-1:void 0}},l.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var tb,ub,vb=n.expr.attrHandle,wb=/^(?:checked|selected)$/i,xb=l.getSetAttribute,yb=l.input;n.fn.extend({attr:function(a,b){return Y(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),e=n.attrHooks[b]||(n.expr.match.bool.test(b)?ub:tb)),void 0!==c?null===c?void n.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=n.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!l.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(G);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)?yb&&xb||!wb.test(c)?a[d]=!1:a[n.camelCase("default-"+c)]=a[d]=!1:n.attr(a,c,""),a.removeAttribute(xb?c:d)}}),ub={set:function(a,b,c){return b===!1?n.removeAttr(a,c):yb&&xb||!wb.test(c)?a.setAttribute(!xb&&n.propFix[c]||c,c):a[n.camelCase("default-"+c)]=a[c]=!0,c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=vb[b]||n.find.attr;yb&&xb||!wb.test(b)?vb[b]=function(a,b,d){var e,f;return d||(f=vb[b],vb[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,vb[b]=f),e}:vb[b]=function(a,b,c){return c?void 0:a[n.camelCase("default-"+b)]?b.toLowerCase():null}}),yb&&xb||(n.attrHooks.value={set:function(a,b,c){return n.nodeName(a,"input")?void(a.defaultValue=b):tb&&tb.set(a,b,c)}}),xb||(tb={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},vb.id=vb.name=vb.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},n.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:tb.set},n.attrHooks.contenteditable={set:function(a,b,c){tb.set(a,""===b?!1:b,c)}},n.each(["width","height"],function(a,b){n.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),l.style||(n.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var zb=/^(?:input|select|textarea|button|object)$/i,Ab=/^(?:a|area)$/i;n.fn.extend({prop:function(a,b){return Y(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return a=n.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),n.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&n.isXMLDoc(a)||(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=n.find.attr(a,"tabindex");return b?parseInt(b,10):zb.test(a.nodeName)||Ab.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),l.hrefNormalized||n.each(["href","src"],function(a,b){n.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),l.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this}),l.enctype||(n.propFix.enctype="encoding");var Bb=/[\t\r\n\f]/g;function Cb(a){return n.attr(a,"class")||""}n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,Cb(this)))});if("string"==typeof a&&a){b=a.match(G)||[];while(c=this[i++])if(e=Cb(c),d=1===c.nodeType&&(" "+e+" ").replace(Bb," ")){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=n.trim(d),e!==h&&n.attr(c,"class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,Cb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(G)||[];while(c=this[i++])if(e=Cb(c),d=1===c.nodeType&&(" "+e+" ").replace(Bb," ")){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=n.trim(d),e!==h&&n.attr(c,"class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):n.isFunction(a)?this.each(function(c){n(this).toggleClass(a.call(this,c,Cb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=n(this),f=a.match(G)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=Cb(this),b&&n._data(this,"__className__",b),n.attr(this,"class",b||a===!1?"":n._data(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+Cb(c)+" ").replace(Bb," ").indexOf(b)>-1)return!0;return!1}}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var Db=a.location,Eb=n.now(),Fb=/\?/,Gb=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;n.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=n.trim(b+"");return e&&!n.trim(e.replace(Gb,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():n.error("Invalid JSON: "+b)},n.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new a.DOMParser,c=d.parseFromString(b,"text/xml")):(c=new a.ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||n.error("Invalid XML: "+b),c};var Hb=/#.*$/,Ib=/([?&])_=[^&]*/,Jb=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Kb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Lb=/^(?:GET|HEAD)$/,Mb=/^\/\//,Nb=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Ob={},Pb={},Qb="*/".concat("*"),Rb=Db.href,Sb=Nb.exec(Rb.toLowerCase())||[];function Tb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(G)||[];if(n.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Ub(a,b,c,d){var e={},f=a===Pb;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Vb(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&n.extend(!0,a,c),a}function Wb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Xb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Rb,type:"GET",isLocal:Kb.test(Sb[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Qb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Vb(Vb(a,n.ajaxSettings),b):Vb(n.ajaxSettings,a)},ajaxPrefilter:Tb(Ob),ajaxTransport:Tb(Pb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var d,e,f,g,h,i,j,k,l=n.ajaxSetup({},c),m=l.context||l,o=l.context&&(m.nodeType||m.jquery)?n(m):n.event,p=n.Deferred(),q=n.Callbacks("once memory"),r=l.statusCode||{},s={},t={},u=0,v="canceled",w={readyState:0,getResponseHeader:function(a){var b;if(2===u){if(!k){k={};while(b=Jb.exec(g))k[b[1].toLowerCase()]=b[2]}b=k[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===u?g:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return u||(a=t[c]=t[c]||a,s[a]=b),this},overrideMimeType:function(a){return u||(l.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>u)for(b in a)r[b]=[r[b],a[b]];else w.always(a[w.status]);return this},abort:function(a){var b=a||v;return j&&j.abort(b),y(0,b),this}};if(p.promise(w).complete=q.add,w.success=w.done,w.error=w.fail,l.url=((b||l.url||Rb)+"").replace(Hb,"").replace(Mb,Sb[1]+"//"),l.type=c.method||c.type||l.method||l.type,l.dataTypes=n.trim(l.dataType||"*").toLowerCase().match(G)||[""],null==l.crossDomain&&(d=Nb.exec(l.url.toLowerCase()),l.crossDomain=!(!d||d[1]===Sb[1]&&d[2]===Sb[2]&&(d[3]||("http:"===d[1]?"80":"443"))===(Sb[3]||("http:"===Sb[1]?"80":"443")))),l.data&&l.processData&&"string"!=typeof l.data&&(l.data=n.param(l.data,l.traditional)),Ub(Ob,l,c,w),2===u)return w;i=n.event&&l.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),l.type=l.type.toUpperCase(),l.hasContent=!Lb.test(l.type),f=l.url,l.hasContent||(l.data&&(f=l.url+=(Fb.test(f)?"&":"?")+l.data,delete l.data),l.cache===!1&&(l.url=Ib.test(f)?f.replace(Ib,"$1_="+Eb++):f+(Fb.test(f)?"&":"?")+"_="+Eb++)),l.ifModified&&(n.lastModified[f]&&w.setRequestHeader("If-Modified-Since",n.lastModified[f]),n.etag[f]&&w.setRequestHeader("If-None-Match",n.etag[f])),(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&w.setRequestHeader("Content-Type",l.contentType),w.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+("*"!==l.dataTypes[0]?", "+Qb+"; q=0.01":""):l.accepts["*"]);for(e in l.headers)w.setRequestHeader(e,l.headers[e]);if(l.beforeSend&&(l.beforeSend.call(m,w,l)===!1||2===u))return w.abort();v="abort";for(e in{success:1,error:1,complete:1})w[e](l[e]);if(j=Ub(Pb,l,c,w)){if(w.readyState=1,i&&o.trigger("ajaxSend",[w,l]),2===u)return w;l.async&&l.timeout>0&&(h=a.setTimeout(function(){w.abort("timeout")},l.timeout));try{u=1,j.send(s,y)}catch(x){if(!(2>u))throw x;y(-1,x)}}else y(-1,"No Transport");function y(b,c,d,e){var k,s,t,v,x,y=c;2!==u&&(u=2,h&&a.clearTimeout(h),j=void 0,g=e||"",w.readyState=b>0?4:0,k=b>=200&&300>b||304===b,d&&(v=Wb(l,w,d)),v=Xb(l,v,w,k),k?(l.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(n.lastModified[f]=x),x=w.getResponseHeader("etag"),x&&(n.etag[f]=x)),204===b||"HEAD"===l.type?y="nocontent":304===b?y="notmodified":(y=v.state,s=v.data,t=v.error,k=!t)):(t=y,!b&&y||(y="error",0>b&&(b=0))),w.status=b,w.statusText=(c||y)+"",k?p.resolveWith(m,[s,y,w]):p.rejectWith(m,[w,y,t]),w.statusCode(r),r=void 0,i&&o.trigger(k?"ajaxSuccess":"ajaxError",[w,l,k?s:t]),q.fireWith(m,[w,y]),i&&(o.trigger("ajaxComplete",[w,l]),--n.active||n.event.trigger("ajaxStop")))}return w},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax(n.extend({url:a,type:b,dataType:e,data:c,success:d},n.isPlainObject(a)&&a))}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){if(n.isFunction(a))return this.each(function(b){n(this).wrapAll(a.call(this,b))});if(this[0]){var b=n(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return n.isFunction(a)?this.each(function(b){n(this).wrapInner(a.call(this,b))}):this.each(function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}});function Yb(a){return a.style&&a.style.display||n.css(a,"display")}function Zb(a){if(!n.contains(a.ownerDocument||d,a))return!0;while(a&&1===a.nodeType){if("none"===Yb(a)||"hidden"===a.type)return!0;a=a.parentNode}return!1}n.expr.filters.hidden=function(a){return l.reliableHiddenOffsets()?a.offsetWidth<=0&&a.offsetHeight<=0&&!a.getClientRects().length:Zb(a)},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var $b=/%20/g,_b=/\[\]$/,ac=/\r?\n/g,bc=/^(?:submit|button|image|reset|file)$/i,cc=/^(?:input|select|textarea|keygen)/i;function dc(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||_b.test(a)?d(a,e):dc(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)dc(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)dc(c,a[c],b,e);return d.join("&").replace($b,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&cc.test(this.nodeName)&&!bc.test(a)&&(this.checked||!Z.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(ac,"\r\n")}}):{name:b.name,value:c.replace(ac,"\r\n")}}).get()}}),n.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return this.isLocal?ic():d.documentMode>8?hc():/^(get|post|head|put|delete|options)$/i.test(this.type)&&hc()||ic()}:hc;var ec=0,fc={},gc=n.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in fc)fc[a](void 0,!0)}),l.cors=!!gc&&"withCredentials"in gc,gc=l.ajax=!!gc,gc&&n.ajaxTransport(function(b){if(!b.crossDomain||l.cors){var c;return{send:function(d,e){var f,g=b.xhr(),h=++ec;if(g.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(f in b.xhrFields)g[f]=b.xhrFields[f];b.mimeType&&g.overrideMimeType&&g.overrideMimeType(b.mimeType),b.crossDomain||d["X-Requested-With"]||(d["X-Requested-With"]="XMLHttpRequest");for(f in d)void 0!==d[f]&&g.setRequestHeader(f,d[f]+"");g.send(b.hasContent&&b.data||null),c=function(a,d){var f,i,j;if(c&&(d||4===g.readyState))if(delete fc[h],c=void 0,g.onreadystatechange=n.noop,d)4!==g.readyState&&g.abort();else{j={},f=g.status,"string"==typeof g.responseText&&(j.text=g.responseText);try{i=g.statusText}catch(k){i=""}f||!b.isLocal||b.crossDomain?1223===f&&(f=204):f=j.text?200:404}j&&e(f,i,j,g.getAllResponseHeaders())},b.async?4===g.readyState?a.setTimeout(c):g.onreadystatechange=fc[h]=c:c()},abort:function(){c&&c(void 0,!0)}}}});function hc(){try{return new a.XMLHttpRequest}catch(b){}}function ic(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=d.head||n("head")[0]||d.documentElement;return{send:function(e,f){b=d.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||f(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var jc=[],kc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=jc.pop()||n.expando+"_"+Eb++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(kc.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&kc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(kc,"$1"+e):b.jsonp!==!1&&(b.url+=(Fb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?n(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,jc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||d;var e=x.exec(a),f=!c&&[];return e?[b.createElement(e[1])]:(e=ja([a],b,f),f&&f.length&&n(f).remove(),n.merge([],e.childNodes))};var lc=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&lc)return lc.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=n.trim(a.slice(h,a.length)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};function mc(a){return n.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&n.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,n.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,n.contains(b,e)?("undefined"!=typeof e.getBoundingClientRect&&(d=e.getBoundingClientRect()),c=mc(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===n.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(c=a.offset()),c.top+=n.css(a[0],"borderTopWidth",!0),c.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-n.css(d,"marginTop",!0),left:b.left-c.left-n.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Qa})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);n.fn[a]=function(d){return Y(this,function(a,d,e){var f=mc(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?n(f).scrollLeft():e,c?e:n(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=Ua(l.pixelPosition,function(a,c){return c?(c=Sa(a,b),Oa.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({
+padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return Y(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var nc=a.jQuery,oc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=oc),b&&a.jQuery===n&&(a.jQuery=nc),n},b||(a.jQuery=a.$=n),n});
diff --git a/static/jQuery/jquery-migrate-1.2.0.min.js b/static/jQuery/jquery-migrate-1.2.0.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..9cc81a68bf789a963cae97278d0bb3d6a272268d
--- /dev/null
+++ b/static/jQuery/jquery-migrate-1.2.0.min.js
@@ -0,0 +1,2 @@
+/*! jQuery Migrate v1.2.0 | (c) 2005, 2013 jQuery Foundation, Inc. and other contributors | jquery.org/license */
+jQuery.migrateMute===void 0&&(jQuery.migrateMute=!0),function(e,t,n){function r(n){var r=t.console;i[n]||(i[n]=!0,e.migrateWarnings.push(n),r&&r.warn&&!e.migrateMute&&(r.warn("JQMIGRATE: "+n),e.migrateTrace&&r.trace&&r.trace()))}function a(t,a,i,o){if(Object.defineProperty)try{return Object.defineProperty(t,a,{configurable:!0,enumerable:!0,get:function(){return r(o),i},set:function(e){r(o),i=e}}),n}catch(s){}e._definePropertyBroken=!0,t[a]=i}var i={};e.migrateWarnings=[],!e.migrateMute&&t.console&&t.console.log&&t.console.log("JQMIGRATE: Logging is active"),e.migrateTrace===n&&(e.migrateTrace=!0),e.migrateReset=function(){i={},e.migrateWarnings.length=0},"BackCompat"===document.compatMode&&r("jQuery is not compatible with Quirks Mode");var o=e("<input/>",{size:1}).attr("size")&&e.attrFn,s=e.attr,u=e.attrHooks.value&&e.attrHooks.value.get||function(){return null},c=e.attrHooks.value&&e.attrHooks.value.set||function(){return n},l=/^(?:input|button)$/i,d=/^[238]$/,p=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,f=/^(?:checked|selected)$/i;a(e,"attrFn",o||{},"jQuery.attrFn is deprecated"),e.attr=function(t,a,i,u){var c=a.toLowerCase(),g=t&&t.nodeType;return u&&(4>s.length&&r("jQuery.fn.attr( props, pass ) is deprecated"),t&&!d.test(g)&&(o?a in o:e.isFunction(e.fn[a])))?e(t)[a](i):("type"===a&&i!==n&&l.test(t.nodeName)&&t.parentNode&&r("Can't change the 'type' of an input or button in IE 6/7/8"),!e.attrHooks[c]&&p.test(c)&&(e.attrHooks[c]={get:function(t,r){var a,i=e.prop(t,r);return i===!0||"boolean"!=typeof i&&(a=t.getAttributeNode(r))&&a.nodeValue!==!1?r.toLowerCase():n},set:function(t,n,r){var a;return n===!1?e.removeAttr(t,r):(a=e.propFix[r]||r,a in t&&(t[a]=!0),t.setAttribute(r,r.toLowerCase())),r}},f.test(c)&&r("jQuery.fn.attr('"+c+"') may use property instead of attribute")),s.call(e,t,a,i))},e.attrHooks.value={get:function(e,t){var n=(e.nodeName||"").toLowerCase();return"button"===n?u.apply(this,arguments):("input"!==n&&"option"!==n&&r("jQuery.fn.attr('value') no longer gets properties"),t in e?e.value:null)},set:function(e,t){var a=(e.nodeName||"").toLowerCase();return"button"===a?c.apply(this,arguments):("input"!==a&&"option"!==a&&r("jQuery.fn.attr('value', val) no longer sets properties"),e.value=t,n)}};var g,h,v=e.fn.init,m=e.parseJSON,y=/^[^<]*(.*?)[^>]*$/,b=/^[^<]*<[\w\W]+>[^>]*$/;e.fn.init=function(t,n,a){var i;return t&&"string"==typeof t&&!e.isPlainObject(n)&&(i=b.exec(t))&&i[0]&&("<"!==t.charAt(0)&&r("$(html) HTML strings must start with '<' character"),">"!==t.charAt(t.length-1)&&r("$(html) HTML text after last tag is ignored"),"#"===e.trim(t).charAt(0)&&(r("HTML string cannot start with a '#' character"),e.error("JQMIGRATE: Invalid selector string (XSS)")),n&&n.context&&(n=n.context),e.parseHTML)?(i=y.exec(t),v.call(this,e.parseHTML(i[1]||t,n,!0),n,a)):v.apply(this,arguments)},e.fn.init.prototype=e.fn,e.parseJSON=function(e){return e||null===e?m.apply(this,arguments):(r("jQuery.parseJSON requires a valid JSON string"),null)},e.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||0>e.indexOf("compatible")&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e.browser||(g=e.uaMatch(navigator.userAgent),h={},g.browser&&(h[g.browser]=!0,h.version=g.version),h.chrome?h.webkit=!0:h.webkit&&(h.safari=!0),e.browser=h),a(e,"browser",e.browser,"jQuery.browser is deprecated"),e.sub=function(){function t(e,n){return new t.fn.init(e,n)}e.extend(!0,t,this),t.superclass=this,t.fn=t.prototype=this(),t.fn.constructor=t,t.sub=this.sub,t.fn.init=function(r,a){return a&&a instanceof e&&!(a instanceof t)&&(a=t(a)),e.fn.init.call(this,r,a,n)},t.fn.init.prototype=t.fn;var n=t(document);return r("jQuery.sub() is deprecated"),t},e.ajaxSetup({converters:{"text json":e.parseJSON}});var j=e.fn.data;e.fn.data=function(t){var a,i,o=this[0];return!o||"events"!==t||1!==arguments.length||(a=e.data(o,t),i=e._data(o,t),a!==n&&a!==i||i===n)?j.apply(this,arguments):(r("Use of jQuery.fn.data('events') is deprecated"),i)};var w=/\/(java|ecma)script/i,Q=e.fn.andSelf||e.fn.addBack;e.fn.andSelf=function(){return r("jQuery.fn.andSelf() replaced by jQuery.fn.addBack()"),Q.apply(this,arguments)},e.clean||(e.clean=function(t,a,i,o){a=a||document,a=!a.nodeType&&a[0]||a,a=a.ownerDocument||a,r("jQuery.clean() is deprecated");var s,u,c,l,d=[];if(e.merge(d,e.buildFragment(t,a).childNodes),i)for(c=function(e){return!e.type||w.test(e.type)?o?o.push(e.parentNode?e.parentNode.removeChild(e):e):i.appendChild(e):n},s=0;null!=(u=d[s]);s++)e.nodeName(u,"script")&&c(u)||(i.appendChild(u),u.getElementsByTagName!==n&&(l=e.grep(e.merge([],u.getElementsByTagName("script")),c),d.splice.apply(d,[s+1,0].concat(l)),s+=l.length));return d});var x=e.event.add,k=e.event.remove,N=e.event.trigger,T=e.fn.toggle,M=e.fn.live,S=e.fn.die,C="ajaxStart|ajaxStop|ajaxSend|ajaxComplete|ajaxError|ajaxSuccess",A=RegExp("\\b(?:"+C+")\\b"),H=/(?:^|\s)hover(\.\S+|)\b/,L=function(t){return"string"!=typeof t||e.event.special.hover?t:(H.test(t)&&r("'hover' pseudo-event is deprecated, use 'mouseenter mouseleave'"),t&&t.replace(H,"mouseenter$1 mouseleave$1"))};e.event.props&&"attrChange"!==e.event.props[0]&&e.event.props.unshift("attrChange","attrName","relatedNode","srcElement"),e.event.dispatch&&a(e.event,"handle",e.event.dispatch,"jQuery.event.handle is undocumented and deprecated"),e.event.add=function(e,t,n,a,i){e!==document&&A.test(t)&&r("AJAX events should be attached to document: "+t),x.call(this,e,L(t||""),n,a,i)},e.event.remove=function(e,t,n,r,a){k.call(this,e,L(t)||"",n,r,a)},e.fn.error=function(){var e=Array.prototype.slice.call(arguments,0);return r("jQuery.fn.error() is deprecated"),e.splice(0,0,"error"),arguments.length?this.bind.apply(this,e):(this.triggerHandler.apply(this,e),this)},e.fn.toggle=function(t,n){if(!e.isFunction(t)||!e.isFunction(n))return T.apply(this,arguments);r("jQuery.fn.toggle(handler, handler...) is deprecated");var a=arguments,i=t.guid||e.guid++,o=0,s=function(n){var r=(e._data(this,"lastToggle"+t.guid)||0)%o;return e._data(this,"lastToggle"+t.guid,r+1),n.preventDefault(),a[r].apply(this,arguments)||!1};for(s.guid=i;a.length>o;)a[o++].guid=i;return this.click(s)},e.fn.live=function(t,n,a){return r("jQuery.fn.live() is deprecated"),M?M.apply(this,arguments):(e(this.context).on(t,this.selector,n,a),this)},e.fn.die=function(t,n){return r("jQuery.fn.die() is deprecated"),S?S.apply(this,arguments):(e(this.context).off(t,this.selector||"**",n),this)},e.event.trigger=function(e,t,n,a){return n||A.test(e)||r("Global events are undocumented and deprecated"),N.call(this,e,t,n||document,a)},e.each(C.split("|"),function(t,n){e.event.special[n]={setup:function(){var t=this;return t!==document&&(e.event.add(document,n+"."+e.guid,function(){e.event.trigger(n,null,t,!0)}),e._data(this,n,e.guid++)),!1},teardown:function(){return this!==document&&e.event.remove(document,n+"."+e._data(this,n)),!1}}})}(jQuery,window);
\ No newline at end of file
diff --git a/static/jQuery/jquery.js b/static/jQuery/jquery.js
new file mode 120000
index 0000000000000000000000000000000000000000..5049035950ce61ed9a8cef1a7b7bf46036193156
--- /dev/null
+++ b/static/jQuery/jquery.js
@@ -0,0 +1 @@
+jquery-1.12.4.min.js
\ No newline at end of file
diff --git a/static/js/abs_ajax.js b/static/js/abs_ajax.js
new file mode 100644
index 0000000000000000000000000000000000000000..5b8f030f2d0c1a9fa20b8cf3bd6dcc5ba5c8abb5
--- /dev/null
+++ b/static/js/abs_ajax.js
@@ -0,0 +1,44 @@
+
+// JS Ajax code for SignaleAbsenceGrSemestre
+// Contributed by YLB
+
+function ajaxFunction(mod, etudid, dat){
+	var ajaxRequest;  // The variable that makes Ajax possible!
+	
+	try{
+		// Opera 8.0+, Firefox, Safari
+		ajaxRequest = new XMLHttpRequest();
+	} catch (e){
+		// Internet Explorer Browsers
+		try{
+			ajaxRequest = new ActiveXObject("Msxml2.XMLHTTP");
+		} catch (e) {
+			try{
+				ajaxRequest = new ActiveXObject("Microsoft.XMLHTTP");
+			} catch (e){
+				// Something went wrong
+				alert("Your browser broke!");
+				return false;
+			}
+		}
+	}
+	// Create a function that will receive data sent from the server
+	ajaxRequest.onreadystatechange = function(){
+		if(ajaxRequest.readyState == 4 && ajaxRequest.status == 200){
+			document.getElementById("AjaxDiv").innerHTML=ajaxRequest.responseText;
+		}
+	}
+	ajaxRequest.open("POST", "doSignaleAbsenceGrSemestre", true);
+	ajaxRequest.setRequestHeader("Content-type","application/x-www-form-urlencoded");
+	oForm = document.forms[0];
+	oSelectOne = oForm.elements["moduleimpl_id"];
+	index = oSelectOne.selectedIndex;
+        modul_id = oSelectOne.options[index].value;
+	if (mod == 'add') {
+		ajaxRequest.send("reply=0&moduleimpl_id=" + modul_id +"&abslist:list=" + etudid + ":" + dat);
+	}
+	if (mod == 'remove') {
+		ajaxRequest.send("reply=0&moduleimpl_id=" + modul_id +"&etudids=" + etudid + "&dates=" + dat);
+	}
+}
+
diff --git a/static/js/apo_semset_maq_status.js b/static/js/apo_semset_maq_status.js
new file mode 100644
index 0000000000000000000000000000000000000000..3b2a3487beda8fce54c5c6d7e9b7b8fdb5af182b
--- /dev/null
+++ b/static/js/apo_semset_maq_status.js
@@ -0,0 +1,90 @@
+
+$(function() {
+    $( "div#export_help" ).accordion( {
+        heightStyle: "content",
+        collapsible: true,
+        active: false,
+    });
+});
+
+// Affichage des listes par type
+// routine de traitement d'évènement javascript à associé au lien
+// présents dans le tableau effectifs
+// -> filtre la liste étudiant sur critère de classe
+// -> surligne le cas sélectionné
+
+function display(r, c, row, col) {
+    if ((row != r) && (row != '*')) return 'none';
+    if ((col != c) && (col != '*')) return 'none';
+    return '';
+}
+
+function show_tag(all_rows, all_cols, tag) {
+    // Filtrer tous les étudiants
+    all_rows.split(',').forEach(function(r) {
+        all_cols.split(',').forEach(function(c) {
+            etudiants = r + c.substring(1);
+            $(etudiants).css("display", "none");
+        })
+    })
+    // sauf le tag
+    $('.' + tag).css('display', '');
+}
+
+function show_filtres(effectifs, filtre_row, filtre_col) {
+    $("#compte").html(effectifs);
+    if ((filtre_row == '') && (filtre_col == '')) {
+            $("#sans_filtre").css("display", "");
+            $("#filtre_row").css("display", "none");
+            $("#filtre_col").css("display", "none");
+    } else {
+            $("#sans_filtre").css("display", "none");
+            if (filtre_row == '') {
+                $("#filtre_row").css("display", "none");
+                $("#filtre_col").css("display", "");
+                $("#filtre_col").html("Filtre sur code étape: " + filtre_col);
+            } else if (filtre_col == '') {
+                $("#filtre_row").css("display", "");
+                $("#filtre_col").css("display", "none");
+                $("#filtre_row").html("Filtre sur semestre: " + filtre_row);
+            } else {
+                $("#filtre_row").css("display", "");
+                $("#filtre_col").css("display", "");
+                $("#filtre_row").html("Filtre sur semestre: " + filtre_row);
+                $("#filtre_col").html("Filtre sur code étape: " + filtre_col);
+            }
+    }
+}
+
+function doFiltrage(all_rows, all_cols, row, col, effectifs, filtre_row, filtre_col) {
+    show_filtres(effectifs, filtre_row, filtre_col)
+    all_rows.split(',').forEach(function(r) {
+        all_cols.split(',').forEach(function(c) {
+            etudiants = r + c.substring(1);
+            $(etudiants).css("display", display(r, c, row, col));
+        })
+    })
+
+    $('.repartition td').css("background-color", "");
+    $('.repartition th').css("background-color", "");
+
+    if (row == '*' && col == '*') {     // Aucun filtre
+    } else if (row == '*') {            // filtrage sur 1 colonne
+        $(col).css("background-color", "lightblue");
+    } else if (col == '*') {            // Filtrage sur 1 ligne
+        $(row + '>td').css("background-color", "lightblue");
+        $(row + '>th').css("background-color", "lightblue");
+    } else {                            // filtrage sur 1 case
+        $(row + '>td' + col).css("background-color", "lightblue");
+    }
+
+    // Modifie le titre de la section pour indiquer la sélection:
+    // elt est le lien cliqué
+    // var td_class = elt.parentNode.className.trim();
+    // if (td_class) {
+    //     var titre_col = $("table.repartition th.")[0].textContent.trim();
+    //     if (titre_col) {
+    //         $("h4#effectifs").html("Liste des étudiants de " + titre_col);
+    //     }
+    // }
+}
diff --git a/static/js/apo_semset_maq_status.js.rej b/static/js/apo_semset_maq_status.js.rej
new file mode 100644
index 0000000000000000000000000000000000000000..1512800d8b0089f353516b1b43e36fdb3def2345
--- /dev/null
+++ b/static/js/apo_semset_maq_status.js.rej
@@ -0,0 +1,154 @@
+--- static/js/apo_semset_maq_status.js	(revision 1865)
++++ static/js/apo_semset_maq_status.js	(working copy)
+@@ -1,61 +1,90 @@
+-
+-$(function() {
+-    $( "div#export_help" ).accordion( {
+-        heightStyle: "content",
+-        collapsible: true,
+-        active: false,
+-    });
+-});
+-
+-// Affichage des listes par type
+-// routine de traitement d'évènement javascript à associé au lien
+-// présents dans le tableau effectifs
+-// -> filtre la liste étudiant sur critère de classe
+-// -> surligne le cas sélectionné
+-
+-function display(r, c, row, col) {
+-    if ((row != r) && (row != '*')) return 'none';
+-    if ((col != c) && (col != '*')) return 'none';
+-    return '';
+-}
+-
+-function show_tag(all_rows, all_cols, tag) {
+-    all_rows.split(',').forEach(function(r) {
+-        all_cols.split(',').forEach(function(c) {
+-            etudiants = r + c.substring(1);
+-            $(etudiants).css("display", "none");
+-        })
+-    })
+-    $('.' + tag).css('display', '');
+-}
+-
+-function show_css(elt, all_rows, all_cols, row, col, precision) {
+-    all_rows.split(',').forEach(function(r) {
+-        all_cols.split(',').forEach(function(c) {
+-            etudiants = r + c.substring(1);
+-            $(etudiants).css("display", display(r, c, row, col));
+-        })
+-    })
+-    $('.repartition td').css("background-color", "");
+-    $('.repartition th').css("background-color", "");
+-    if (row == '*' && col == '*') {
+-    } else if (row == '*') {
+-        $(col).css("background-color", "lightblue");
+-    } else if (col == '*') {
+-        $(row + '>td').css("background-color", "lightblue");
+-        $(row + '>th').css("background-color", "lightblue");
+-    } else {
+-        $(row + '>td' + col).css("background-color", "lightblue");
+-    }
+-    $("#effectifs").html("Liste des étudiants" +  precision);
+-
+-    // Modifie le titre de la section pour indiquer la sélection:
+-    // elt est le lien cliqué
+-    var td_class = elt.parentNode.className.trim();
+-    if (td_class) {
+-        var titre_col = $("table.repartition th." + td_class)[0].textContent.trim();
+-        if (titre_col) {
+-            $("h4#effectifs").html("Liste des étudiants de " + titre_col);
+-        }
+-    }
+-}
++
++$(function() {
++    $( "div#export_help" ).accordion( {
++        heightStyle: "content",
++        collapsible: true,
++        active: false,
++    });
++});
++
++// Affichage des listes par type
++// routine de traitement d'évènement javascript à associé au lien
++// présents dans le tableau effectifs
++// -> filtre la liste étudiant sur critère de classe
++// -> surligne le cas sélectionné
++
++function display(r, c, row, col) {
++    if ((row != r) && (row != '*')) return 'none';
++    if ((col != c) && (col != '*')) return 'none';
++    return '';
++}
++
++function show_tag(all_rows, all_cols, tag) {
++    // Filtrer tous les étudiants
++    all_rows.split(',').forEach(function(r) {
++        all_cols.split(',').forEach(function(c) {
++            etudiants = r + c.substring(1);
++            $(etudiants).css("display", "none");
++        })
++    })
++    // sauf le tag
++    $('.' + tag).css('display', '');
++}
++
++function show_filtres(effectifs, filtre_row, filtre_col) {
++    $("#compte").html(effectifs);
++    if ((filtre_row == '') && (filtre_col == '')) {
++            $("#sans_filtre").css("display", "");
++            $("#filtre_row").css("display", "none");
++            $("#filtre_col").css("display", "none");
++    } else {
++            $("#sans_filtre").css("display", "none");
++            if (filtre_row == '') {
++                $("#filtre_row").css("display", "none");
++                $("#filtre_col").css("display", "");
++                $("#filtre_col").html("Filtre sur code étape: " + filtre_col);
++            } else if (filtre_col == '') {
++                $("#filtre_row").css("display", "");
++                $("#filtre_col").css("display", "none");
++                $("#filtre_row").html("Filtre sur semestre: " + filtre_row);
++            } else {
++                $("#filtre_row").css("display", "");
++                $("#filtre_col").css("display", "");
++                $("#filtre_row").html("Filtre sur semestre: " + filtre_row);
++                $("#filtre_col").html("Filtre sur code étape: " + filtre_col);
++            }
++    }
++}
++
++function doFiltrage(all_rows, all_cols, row, col, effectifs, filtre_row, filtre_col) {
++    show_filtres(effectifs, filtre_row, filtre_col)
++    all_rows.split(',').forEach(function(r) {
++        all_cols.split(',').forEach(function(c) {
++            etudiants = r + c.substring(1);
++            $(etudiants).css("display", display(r, c, row, col));
++        })
++    })
++
++    $('.repartition td').css("background-color", "");
++    $('.repartition th').css("background-color", "");
++
++    if (row == '*' && col == '*') {     // Aucun filtre
++    } else if (row == '*') {            // filtrage sur 1 colonne
++        $(col).css("background-color", "lightblue");
++    } else if (col == '*') {            // Filtrage sur 1 ligne
++        $(row + '>td').css("background-color", "lightblue");
++        $(row + '>th').css("background-color", "lightblue");
++    } else {                            // filtrage sur 1 case
++        $(row + '>td' + col).css("background-color", "lightblue");
++    }
++
++    // Modifie le titre de la section pour indiquer la sélection:
++    // elt est le lien cliqué
++    // var td_class = elt.parentNode.className.trim();
++    // if (td_class) {
++    //     var titre_col = $("table.repartition th.")[0].textContent.trim();
++    //     if (titre_col) {
++    //         $("h4#effectifs").html("Liste des étudiants de " + titre_col);
++    //     }
++    // }
++}
diff --git a/static/js/bulletin.js b/static/js/bulletin.js
new file mode 100644
index 0000000000000000000000000000000000000000..a46b603b8d91bb27d160a920f706b06480cab9aa
--- /dev/null
+++ b/static/js/bulletin.js
@@ -0,0 +1,50 @@
+// Affichage bulletin de notes
+// (uses jQuery)
+
+
+// Change visibility of UE details (les <tr> de classe "notes_bulletin_row_mod" suivant)
+// La table a la structure suivante:
+//  <tr class="notes_bulletin_row_ue"><td><span class="toggle_ue">+/-</span>...</td>...</tr>
+//  <tr class="notes_bulletin_row_mod">...</tr>
+//  <tr class="notes_bulletin_row_eval">...</tr>
+//
+// On change la visi de tous les <tr> jusqu'au notes_bulletin_row_ue suivant.
+//
+function toggle_vis_ue(e, new_state) { 
+    // e is the span containg the clicked +/- icon
+    var tr = e.parentNode.parentNode;
+    if (new_state == undefined) {
+	// current state: use alt attribute of current image
+	if (e.childNodes[0].alt == '+') {
+            new_state=false;
+	} else {
+            new_state=true;
+	}
+    } 
+    // find next tr in siblings
+    var tr = tr.nextSibling;
+    //while ((tr != null) && sibl.tagName == 'TR') {
+    var current = true;
+    while ((tr != null) && current) {
+	    if ((tr.nodeType==1) && (tr.tagName == 'TR')) {
+	        for (var i=0; i < tr.classList.length; i++) {
+		        if ((tr.classList[i] == 'notes_bulletin_row_ue') || (tr.classList[i] == 'notes_bulletin_row_sum_ects'))
+		            current = false;
+ 	        }
+	        if (current) {
+		        if (new_state) {
+		            tr.style.display = 'none';
+		        } else {
+		            tr.style.display = 'table-row';
+		        }
+	        }
+        }
+        tr = tr.nextSibling;	
+    }
+    if (new_state) {
+	e.innerHTML = '<img width="13" height="13" border="0" title="" alt="+" src="/ScoDoc/static/icons/plus_img.png"/>';
+    } else {
+	e.innerHTML = '<img width="13" height="13" border="0" title="" alt="-" src="/ScoDoc/static/icons/minus_img.png"/>';
+    }
+}
+
diff --git a/static/js/calabs.js b/static/js/calabs.js
new file mode 100644
index 0000000000000000000000000000000000000000..50f4ff184a5dce07c631ff239a961bc65236df87
--- /dev/null
+++ b/static/js/calabs.js
@@ -0,0 +1,64 @@
+/* -*- mode: javascript -*-
+ *
+ * Selection semaine sur calendrier Absences
+ *
+ * E. Viennet, Oct 2006
+ */
+
+var WEEKDAYCOLOR = "#EEEEEE";
+var WEEKENDCOLOR = "#99CC99";
+var DAYHIGHLIGHT = "red";
+var CURRENTWEEKCOLOR = "yellow";
+
+// get all tr elements from this class
+// (no getElementBuClassName)
+function getTRweek( week ) { 
+  var tablecal = document.getElementById('maincalendar');
+  var all = tablecal.getElementsByTagName('tr');
+  var res = [] ;
+  for(var i=0; i < all.length; i++) {
+    if (all[i].className == week)
+      res[res.length] = all[i];
+  }
+  return res;
+}
+
+var HIGHLIGHTEDCELLS = [];
+
+function deselectweeks() {
+  
+  for(var i=0; i < HIGHLIGHTEDCELLS.length; i++) {
+    var row = rows[i];
+    if (row) {
+      if (row.className.match('currentweek')) {
+	row.style.backgroundColor = CURRENTWEEKCOLOR;
+      } else {
+	row.style.backgroundColor = WEEKDAYCOLOR;
+      }
+      rows[i] = null;
+    }
+  }
+}
+
+// highlight 5 days
+function highlightweek(el) {
+  deselectweeks();
+  var week = el.className;
+  if ((week == 'wkend') || (week.substring(0,2) != 'wk')) {
+    return; /* does not hightlight weekends */
+  }
+  rows = getTRweek(week);
+  for (var i=0; i < rows.length; i++) {
+    var row = rows[i];
+    row.style.backgroundColor = DAYHIGHLIGHT;
+    HIGHLIGHTEDCELLS[HIGHLIGHTEDCELLS.length] = row;
+  }
+}
+
+// click on a day
+function wclick(el) {
+  monday = el.className;
+  form = document.getElementById('formw');
+  form.datelundi.value = monday.substr(2).replace(/_/g,'/').split(' ')[0];
+  form.submit();
+}
diff --git a/static/js/editPartitionForm.js b/static/js/editPartitionForm.js
new file mode 100644
index 0000000000000000000000000000000000000000..242c539dcbc0b917ef7f9ded766dfc73e6e9b73c
--- /dev/null
+++ b/static/js/editPartitionForm.js
@@ -0,0 +1,37 @@
+
+
+function _partition_set_attr(partition_id, attr_name, attr_value) {
+    $.post('partition_set_attr',
+           { 'partition_id' :  partition_id,
+             'attr' : attr_name,
+             'value' : attr_value
+           },
+           function(result) {
+               sco_message(result);
+           });
+    return;
+} 
+
+// Met à jour bul_show_rank lorsque checkbox modifiees:
+function update_rk(e) {
+    var partition_id = $(e).attr('data-partition_id');
+    var v;
+    if (e.checked)
+	    v='1';
+    else
+	    v='0';
+    _partition_set_attr(partition_id, 'bul_show_rank', v);
+}
+
+
+function update_show_in_list(e) {
+    var partition_id = $(e).attr('data-partition_id');
+    var v;
+    if (e.checked)
+	    v='1';
+    else
+	    v='0';
+    
+    _partition_set_attr(partition_id, 'show_in_lists', v);
+} 
+
diff --git a/static/js/edit_ue.js b/static/js/edit_ue.js
new file mode 100644
index 0000000000000000000000000000000000000000..6d41fa5363fcd902c41715a3cfb2a62f7d8487b0
--- /dev/null
+++ b/static/js/edit_ue.js
@@ -0,0 +1,16 @@
+// Affiche et met a jour la liste des UE partageant le meme code
+
+$().ready(function(){
+    update_ue_list();
+    $("#tf_ue_code").bind("keyup", update_ue_list);
+});
+
+
+function update_ue_list() {
+    var ue_id = $("#tf_ue_id")[0].value;
+    var ue_code = $("#tf_ue_code")[0].value;
+    var query = "ue_sharing_code?ue_code=" + ue_code +"&hide_ue_id=" + ue_id + "&ue_id=" + ue_id;
+    $.get( query, '',  function(data){ 
+	$("#ue_list_code").html(data);
+    });
+} 
diff --git a/static/js/etud_debouche.js b/static/js/etud_debouche.js
new file mode 100644
index 0000000000000000000000000000000000000000..a72dc6f86eb8d45d9241afefcf8f3f759609e137
--- /dev/null
+++ b/static/js/etud_debouche.js
@@ -0,0 +1,138 @@
+// Cadre "debouchés" sur fiche etudiant
+// affichage et saisie des informations sur l'avenir de l'étudiant.
+
+// console.log('etud_debouche.js loaded');
+
+$(function() {
+    display_itemsuivis(false);
+});
+
+
+function display_itemsuivis(active) {
+    var etudid = $('div#fichedebouche').data("etudid");
+    var readonly = $('div#fichedebouche').data('readonly'); // present ro interface
+    
+    if (!readonly) {
+	$('#adddebouchelink').off("click").click(function(e){
+	    e.preventDefault();
+	    $.post( "itemsuivi_create", { etudid: etudid, format:'json' } ).done( item_insert_new );
+	
+	    return false;
+	});
+    }
+    // add existing items
+    $.get( 'itemsuivi_list_etud', { etudid: etudid, format: 'json' }, function(L) {
+	for (var i in L) {
+	    item_insert( L[i]['itemsuivi_id'], L[i]['item_date'], L[i]['situation'], L[i]['tags'], readonly );
+	}
+    });
+
+    $( "div#fichedebouche" ).accordion( {
+	heightStyle: "content",
+	collapsible: true,
+	active: active,
+    });
+}
+
+function item_insert_new( it ) {
+    item_insert( it.itemsuivi_id, it.item_date, it.situation, '', false );
+}
+
+function item_insert( itemsuivi_id, item_date, situation, tags, readonly ) {
+    if ( item_date === undefined )
+	item_date = Date2DMY(new Date());
+    if ( situation === undefined )
+	situation = '';
+    if ( tags === undefined )
+	tags = '';
+
+    var nodes = item_nodes(itemsuivi_id, item_date, situation, tags, readonly);
+    // insert just before last li:
+    if ($('ul.listdebouches li.adddebouche').length > 0) {
+	$('ul.listdebouches').children(':last').before(nodes);
+    } else {
+	// mode readonly, pas de li "ajouter"
+	$('ul.listdebouches').append(nodes);
+    }
+};
+
+function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
+    // console.log('item_nodes: itemsuivi_id=' + itemsuivi_id);
+    var sel_mois = 'Situation à la date du <input type="text" class="itemsuividatepicker" size="10" value="' + item_date + '"/><span class="itemsuivi_suppress" onclick="itemsuivi_suppress(\''+itemsuivi_id+'\')"><img width="10" height="9" border="0" title="" alt="supprimer cet item" src="/ScoDoc/static/icons/delete_small_img.png"/></span>';
+    
+    var h = sel_mois;
+    // situation
+    h += '<div class="itemsituation editable" data-type="textarea" data-url="itemsuivi_set_situation" data-placeholder="<em>décrire situation...</em>" data-object="' + itemsuivi_id + '">' + situation + '</div>';
+    // tags:
+    h += '<div class="itemsuivi_tag_edit"><textarea class="itemsuivi_tag_editor">' + tags + '</textarea></div>';
+    
+    var nodes = $($.parseHTML( '<li class="itemsuivi">' + h + '</li>' ));
+    var dp = nodes.find('.itemsuividatepicker');
+    dp.blur( function(e) {
+	var date = this.value;
+	// console.log('selected text: ' + date);
+	$.post( "itemsuivi_set_date", { item_date: date, itemsuivi_id: itemsuivi_id  } );
+    });
+    dp.datepicker({
+	onSelect: function(date, instance) {
+	    // console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id);
+	    $.post( "itemsuivi_set_date", { item_date: date, itemsuivi_id: itemsuivi_id  } );
+	},
+	showOn: 'button', 
+        buttonImage: '/ScoDoc/static/icons/calendar_img.png', 
+        buttonImageOnly: true,
+        dateFormat: 'dd/mm/yy',   
+        duration : 'fast',
+	disabled: readonly
+    });
+    dp.datepicker('option', $.extend({showMonthAfterYear: false},
+                                     $.datepicker.regional['fr']));
+    
+    if (readonly) {
+	// show tags read-only
+	readOnlyTags(nodes.find('.itemsuivi_tag_editor'));
+    }
+    else {
+	// bind tag editor
+	nodes.find('.itemsuivi_tag_editor').tagEditor({
+            initialTags: '',	
+            placeholder: 'Tags...',
+            onChange: function(field, editor, tags) {
+		$.post('itemsuivi_tag_set', 
+                       {
+			   itemsuivi_id: itemsuivi_id,
+			   taglist: tags.join()
+                       });
+            },
+            autocomplete: {
+		delay: 200, // ms before suggest
+		position: { collision: 'flip' }, // automatic menu position up/down
+		source: "itemsuivi_tag_search"
+            },
+	});
+
+	// bind inplace editor
+	nodes.find('div.itemsituation').jinplace();
+    }
+    
+    return nodes;
+};
+
+function Date2DMY(date) {
+  var year = date.getFullYear();
+
+  var month = (1 + date.getMonth()).toString();
+  month = month.length > 1 ? month : '0' + month;
+
+  var day = date.getDate().toString();
+  day = day.length > 1 ? day : '0' + day;
+  
+  return day + '/' + month + '/' + year;
+}
+
+function itemsuivi_suppress(itemsuivi_id) {
+    $.post( "itemsuivi_suppress", { itemsuivi_id: itemsuivi_id } );
+    // Clear items and rebuild:
+    $("ul.listdebouches li.itemsuivi").remove();
+    display_itemsuivis(0);
+}
diff --git a/static/js/etud_info.js b/static/js/etud_info.js
new file mode 100644
index 0000000000000000000000000000000000000000..1b2a7e5005fed46a260e6d668d63abe03a647a4e
--- /dev/null
+++ b/static/js/etud_info.js
@@ -0,0 +1,61 @@
+// Affiche popup avec info sur etudiant (pour les listes d'etudiants)
+// affecte les elements de classe "etudinfo" portant l'id d'un etudiant
+// utilise jQuery / qTip
+
+function get_etudid_from_elem(e) {
+    // renvoie l'etudid, obtenu a partir de l'id de l'element
+    // qui est soit de la forme xxxx-etudid, soit tout simplement etudid
+    var etudid = e.id.split("-")[1];
+    if (etudid == undefined) {
+        return e.id;
+    } else {
+        return etudid;
+    }
+}
+
+$().ready(function(){
+
+    var elems = $(".etudinfo");
+
+    var q_args = get_query_args();
+    var args_to_pass = new Set(
+        [ "formsemestre_id", "group_ids","group_id", "partition_id",
+          "moduleimpl_id", "evaluation_id"
+        ]);
+    var qs = "";
+    for (var k in q_args) {
+        if (args_to_pass.has(k)) {
+            qs += '&' + k + '=' + q_args[k];
+        }
+    }
+    for (var i=0; i < elems.length; i++) {
+	    $(elems[i]).qtip({
+	        content: {
+		        ajax: {
+			        url: "etud_info_html?etudid=" + get_etudid_from_elem(elems[i]) + qs,
+                    type: "GET"
+                    //success: function(data, status) {
+                    //    this.set('content.text', data);
+                    //    xxx called twice on each success ???
+                    //    console.log(status);
+                }
+            },
+		    text: "Loading...",	    
+	        position: {
+		        at: "right bottom",
+		        my: "left top"
+	        },
+	        style: {
+		        classes: 'qtip-etud'
+	        },
+            hide: {
+                fixed: true,
+                delay: 300
+            }
+            // utile pour debugguer le css: 
+            // hide: { event: 'unfocus' }
+        });
+    }
+});
+
+
diff --git a/static/js/export_results.js b/static/js/export_results.js
new file mode 100644
index 0000000000000000000000000000000000000000..ba117fa4aec11928a925194c813a117d6007d4a8
--- /dev/null
+++ b/static/js/export_results.js
@@ -0,0 +1,14 @@
+// Export table tous les resultats
+
+// Menu choix parcours:
+$(function() {
+    $('#parcours_sel').multiselect(
+	{
+	    includeSelectAllOption: true,
+	    nonSelectedText:'Choisir le(s) parcours...',
+	    selectAllValue: '',
+	    numberDisplayed: 3,
+	}
+    );
+});
+
diff --git a/static/js/formsemestre_ext_edit_ue_validations.js b/static/js/formsemestre_ext_edit_ue_validations.js
new file mode 100644
index 0000000000000000000000000000000000000000..b0e08d32d0ac1f14ceda83793e79e633356b85d5
--- /dev/null
+++ b/static/js/formsemestre_ext_edit_ue_validations.js
@@ -0,0 +1,67 @@
+
+
+
+function compute_moyenne() {
+    var notes = $(".tf_field_note input").map( 
+        function() {  return parseFloat($(this).val());  } 
+    ).get();
+    var coefs = $(".tf_field_coef input").map( 
+        function() {  return parseFloat($(this).val());  } 
+    ).get();
+    var N = notes.length;
+    var dp = 0.;
+    var sum_coefs = 0.;
+    for (var i=0; i < N; i++) {
+        if (!(isNaN(notes[i]) || isNaN(coefs[i]))) {
+            dp += notes[i] * coefs[i];
+            sum_coefs += coefs[i];
+        }
+    }
+    return dp / sum_coefs;    
+}
+
+// Callback select menu (UE code)
+function enable_disable_fields_cb() {
+    enable_disable_fields(this);
+}
+function enable_disable_fields(select_elt) {    
+    // input fields controled by this menu
+    var input_fields = $(select_elt).parent().parent().find('input');
+    var disabled = false;
+    if ($(select_elt).val() === "None") {
+        disabled = true;
+    }
+    console.log('disabled=', disabled);
+    input_fields.each( function () {
+        var old_state = this.disabled;
+        console.log("old_state=", old_state)
+        if (old_state == disabled) {
+            return; /* state unchanged */
+        }
+        var saved_value = $(this).data('saved-value');
+        if (typeof saved_value == 'undefined') {
+            saved_value = '';
+        }
+        var cur_value = $(this).val();
+        // swap
+        $(this).data('saved-value', cur_value);
+        $(this).val(saved_value);
+    });
+    input_fields.prop('disabled', disabled);
+}
+function setup_text_fields() {
+    $(".ueext_valid_select").each(
+        function() {
+            enable_disable_fields(this);
+        }
+    );
+}
+
+$().ready(function(){
+    $(".tf_ext_edit_ue_validations").change(function (){
+        $(".ext_sem_moy_val")[0].innerHTML=compute_moyenne();
+    });
+    $(".ueext_valid_select").change( enable_disable_fields_cb );
+
+    setup_text_fields();
+});
diff --git a/static/js/groupmgr.js b/static/js/groupmgr.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f89361d4ff56df5f4089f03dafb2d1fb702ef76
--- /dev/null
+++ b/static/js/groupmgr.js
@@ -0,0 +1,446 @@
+/* -*- mode: javascript -*-
+ *
+ * ScoDoc: Affectation des groupes de TD
+ * re-ecriture utilisant jQuery de l'ancien code
+ */
+
+
+/* --- Globals ---- */
+var EtudColors = [ "#E8EEF7", "#ffffff" ]; // [ "#E8EEF7", "#E0ECFF", "#E5E6BE", "#F3EAE2", "#E3EAE1" ];
+var EtudColorsIdx = 0;
+var NbEtuds = 0;
+var ETUDS = new Object(); // { etudid : etud }
+var ETUD_GROUP = new Object(); // { etudid : group_id }
+var groups_unsaved = false;
+
+var groups = new Object(); // Liste des groupes
+
+function loadGroupes() {
+    $("#gmsg")[0].innerHTML = 'Chargement des groupes en cours...';
+    var partition_id = document.formGroup.partition_id.value;
+
+    $.get('XMLgetGroupsInPartition', { partition_id : partition_id } )
+        .done(
+          function( data ) {
+              var nodes = data.getElementsByTagName('group');
+              if (nodes) {
+                  var nbgroups = nodes.length;
+                  // put last group at first (etudiants sans groupes)
+                  if (nodes.length > 1 && nodes[nbgroups-1].attributes.getNamedItem("group_id").value == '_none_') {
+                      populateGroup(nodes[nodes.length-1]);
+	                  nbgroups -= 1;
+                  }
+                  // then standard groups
+                  for (var i=0; i < nbgroups; i++) {
+	                  populateGroup(nodes[i]);
+                  }
+              }              
+              $("#gmsg")[0].innerHTML = '';
+              updateginfo();
+          }
+        )
+}
+
+function populateGroup( node ) {
+    var group_id = node.attributes.getNamedItem("group_id").value;
+    var group_name =  node.attributes.getNamedItem("group_name").value;
+
+    // CREE LA BOITE POUR CE GROUPE
+    if (group_id) {
+        var gbox = new CGroupBox( group_id, group_name );
+        var etuds = node.getElementsByTagName('etud');
+        var x='';
+        gbox.sorting = false; // disable to speedup
+        EtudColorsIdx = 0; // repart de la premiere couleur
+        for (var j=0; j < etuds.length; j++) {
+            var nom = etuds[j].attributes.getNamedItem("nom").value;
+            var prenom = etuds[j].attributes.getNamedItem("prenom").value;
+            var sexe = etuds[j].attributes.getNamedItem("sexe").value;
+            var etudid = etuds[j].attributes.getNamedItem("etudid").value;
+            var origin = etuds[j].attributes.getNamedItem("origin").value;
+            var etud = new CDraggableEtud( nom, prenom, sexe, origin, etudid );
+            gbox.createEtudInGroup(etud, group_id);
+        }
+        gbox.sorting = true;
+        gbox.updateTitle(); // sort 
+    }
+}
+
+
+/* --- Boite pour un groupe --- */
+
+var groupBoxes = new Object(); // assoc group_id : groupBox
+var groupsToDelete = new Object(); // list of group_id to be supressed
+
+var CGroupBox = function(group_id, group_name) {
+    group_id = $.trim(group_id);
+    var regex = /^\w+$/;
+    if (! regex.test(group_id) ) {
+        alert("Id de groupe invalide");
+        return;
+    }
+    if ( group_id in groups ) {
+        alert("Le groupe " + group_id + " existe déjà !");
+        return;
+    }
+    groups[group_id] = 1;
+    this.group_id = group_id;
+    this.group_name = group_name;
+    this.etuds = new Object();
+    this.nbetuds = 0;
+    this.isNew = false; // true for newly user-created groups
+    this.sorting = true; // false to disable sorting
+    
+    this.groupBox = document.createElement("div");
+    this.groupBox.className = "simpleDropPanel";
+    this.groupBox.id = group_id;
+    var titleDiv = document.createElement("div");
+    titleDiv.className = "groupTitle0";
+    titleDiv.appendChild(this.groupTitle());
+    this.groupBox.appendChild(titleDiv);
+    var gdiv = document.getElementById('groups');
+    gdiv.appendChild(this.groupBox);
+    this.updateTitle();
+    $(this.groupBox).droppable(
+        {
+            accept : ".box", 
+            activeClass: "activatedPanel",
+            drop: function( event, ui ) {
+                // alert("drop on " + this.group_name);
+                var etudid = ui.draggable[0].id;
+                var etud = ETUDS[etudid];
+                var newGroupName = this.id;              
+                var oldGroupName = ETUD_GROUP[etudid];
+                $(groupBoxes[newGroupName].groupBox).append(ui.draggable)
+                ui.draggable[0].style.left = ""; // fix style (?)
+                ui.draggable[0].style.top = "";
+                etud.changeGroup(oldGroupName, newGroupName);
+                etud.htmlElement.style.fontStyle = 'italic'; // italic pour les etudiants deplaces                
+            }
+        });
+    /* On peut s'amuser a deplacer tout un groupe (visuellement: pas droppable) */
+    $(this.groupBox).draggable( {
+        cursor: 'move',
+        containment: '#groups'
+    });
+
+    groupBoxes[group_id] = this; // register
+    updateginfo();
+}
+
+$.extend(CGroupBox.prototype, {
+    // menu for group title
+    groupTitle : function() {
+        var menuSpan = document.createElement("span");
+        menuSpan.className = "barrenav";
+        var h = "<table><tr><td><ul class=\"nav\"><li onmouseover=\"MenuDisplay(this)\" onmouseout=\"MenuHide(this)\"><a href=\"#\" class=\"menu custommenu\"><span id=\"titleSpan" + this.group_id + "\" class=\"groupTitle\">menu</span></a><ul>";
+        if (this.group_id != '_none_') {
+	        h += "<li><a href=\"#\" onClick=\"suppressGroup('" + this.group_id + "');\">Supprimer</a></li>";    
+	        h += "<li><a href=\"#\" onClick=\"renameGroup('" + this.group_id + "');\">Renommer</a></li>";
+        }
+        h += "</ul></li></ul></td></tr></table>";
+        menuSpan.innerHTML = h;
+        return menuSpan;
+    },
+    // add etud to group, attach to DOM 
+    createEtudInGroup: function(etud) {
+        this.addEtudToGroup(etud);
+        this.groupBox.appendChild(etud.htmlElement);
+    },
+    // add existing etud to group (does not affect DOM)
+    addEtudToGroup: function(etud) {
+        etud.group_id = this.group_id;
+        this.etuds[etud.etudid] = etud;
+        this.nbetuds++;
+        ETUD_GROUP[etud.etudid] = this.group_id;
+        this.updateTitle();
+    },
+    // remove etud
+    removeEtud: function(etud) {
+        delete this.etuds[etud.etudid];
+        this.nbetuds--;
+        this.updateTitle();
+    },
+    // Update counter display
+    updateTitle: function() {
+        var tclass = '';
+        if (this.isNew) {
+            tclass = ' class="newgroup"'
+        }
+        var titleSpan = document.getElementById('titleSpan'+this.group_id);
+        if (this.group_id != '_none_') 
+            titleSpan.innerHTML = '<span'+tclass+'>Groupe ' + this.group_name + ' (' + this.nbetuds + ')</span>';
+        else
+            titleSpan.innerHTML = '<span'+tclass+'>Etudiants sans groupe' + ' (' + this.nbetuds + ')</span>';
+        this.sortList(); // maintient toujours la liste triee
+    },
+    // Tri de la boite par nom
+    sortList: function() {
+        if (!this.sorting) 
+            return;
+        var newRows = new Array();
+        for (var i=1; i < this.groupBox.childNodes.length; i++) { // 1 car div titre
+            newRows[i-1] = this.groupBox.childNodes[i];
+        }
+        var sortfn = function(a,b) {
+            // recupere les noms qui sont dans un span
+            var nom_a = a.childNodes[1].childNodes[0].nodeValue;
+            var nom_b = b.childNodes[1].childNodes[0].nodeValue;
+            // console.log( 'comp( %s, %s )', nom_a, nom_b );
+            if (nom_a == nom_b) 
+                return 0;
+            if (nom_a < nom_b) 
+                return -1;
+            return 1;
+        };
+        newRows.sort(sortfn); 
+        for (var i=0;i<newRows.length;i++) {
+            this.groupBox.appendChild(newRows[i]);
+            newRows[i].style.backgroundColor = EtudColors[EtudColorsIdx];
+            EtudColorsIdx = (EtudColorsIdx + 1) % EtudColors.length;
+        }
+    }
+});
+
+
+function suppressGroup( group_id ) {
+    // 1- associate all members to group _none_
+    if (!groupBoxes['_none_']) {
+        // create group _none_
+        var gbox = new CGroupBox( '_none_', 'Etudiants sans groupe' );    
+    }
+    var dst_group_id = groupBoxes['_none_'].group_id;
+    var src_box_etuds = groupBoxes[group_id].etuds;
+    for (var etudid in src_box_etuds) {
+        var etud = src_box_etuds[etudid];
+        etud.changeGroup(group_id, dst_group_id);
+        groupBoxes['_none_'].groupBox.appendChild(etud.htmlElement);
+    }
+    groupBoxes['_none_'].updateTitle();
+    // 2- add group to list of groups to be removed (unless it's a new group)
+    if (!groupBoxes[group_id].isNew)
+        groupsToDelete[group_id] = true;
+    // 3- delete objects and remove from DOM
+    var div = document.getElementById(group_id);
+    div.remove();
+    delete groupBoxes[group_id];
+    groups_unsaved = true;
+    updateginfo();
+}
+
+
+function renameGroup( group_id ) {
+    // 1-- save modifications
+    if (groups_unsaved) {
+	    alert("Enregistrez ou annulez vos changement avant !");
+    } else {
+	    // 2- form rename
+	    document.location='group_rename?group_id=' + group_id; 
+    }
+}
+
+var createdGroupId = 0;
+function newGroupId() {
+    var gid;
+    do {
+        gid = 'NG' + createdGroupId.toString();
+        createdGroupId += 1;
+    } while (gid in groupBoxes);
+    return gid;
+}
+
+// Creation d'un groupe
+function createGroup() {
+    var group_name = document.formGroup.groupName.value.trim();
+    if (!group_name) {
+        alert("Nom de groupe vide !");
+        return false;
+    }
+    // check name:
+    for (var group_id in groupBoxes) { 
+        if (group_id != 'extend') {
+            if (groupBoxes[group_id].group_name == group_name) {
+	            alert("Nom de groupe déja existant !");
+	            return false;
+            }
+        }
+    }
+    var group_id = newGroupId();
+    groups_unsaved = true;
+    var gbox = new CGroupBox( group_id, group_name );
+    gbox.isNew = true;
+    gbox.updateTitle();
+    return true;
+}
+
+/* --- Etudiant draggable --- */
+var CDraggableEtud = function(nom, prenom, sexe, origin, etudid) {
+    this.type        = 'Custom';
+    this.name        = etudid;
+    this.etudid = etudid;
+    this.nom = nom;
+    this.prenom = prenom;
+    this.sexe = sexe;
+    this.origin = origin;
+    this.createNode();
+    ETUDS[etudid] = this;
+    NbEtuds ++;
+}
+
+$.extend(CDraggableEtud.prototype, {
+    repr: function() {
+        return this.sexe + ' ' + this.prenom + ' <span class="nom">' + this.nom + '</span> ' + '<b>'+this.origin+'</b>';
+    },
+    createNode: function() {
+        // Create DOM element for student
+        var e = document.createElement("div");
+        this.htmlElement = e;
+        e.className = "box";
+        e.id = this.etudid;
+        // e.style.backgroundColor = EtudColors[EtudColorsIdx];
+        // EtudColorsIdx = (EtudColorsIdx + 1) % EtudColors.length;
+        //var txtNode = document.createTextNode( this.repr() );
+        //e.appendChild(txtNode);
+        e.innerHTML = this.repr();
+        // declare as draggable
+        $(e).draggable( {
+            cursor: 'move',
+            stack: '#groups div',
+            containment: '#groups',
+            revert: 'invalid'
+        });
+    },
+    endDrag: function() {
+        var el = this.htmlElement;
+        var p = el.parentNode;
+        // alert("endDrag: [" + this.name +"] " + p.id );
+        this.changeGroup( this.group_id, p.id  );
+        this.htmlElement.style.fontStyle = 'italic'; // italic pour les etudiants deplaces
+    },
+    // Move a student from a group to another
+    changeGroup: function(  oldGroupName, newGroupName ) {
+        if (oldGroupName==newGroupName) {
+	        // drop on original group, just sort
+	        groupBoxes[oldGroupName].updateTitle();
+	        return;
+        }
+        var oldGroupBox = null;
+        if (oldGroupName) {
+	        oldGroupBox = groupBoxes[oldGroupName];
+        }
+        var newGroupBox = groupBoxes[newGroupName];
+        newGroupBox.addEtudToGroup(this);
+        if (oldGroupBox)
+	        oldGroupBox.removeEtud(this);
+        groups_unsaved = true;
+        updatesavedinfo();
+    }
+}); 
+
+
+/* --- Upload du resultat --- */
+function processResponse(value) {
+    location.reload(); // necessaire pour reinitialiser les id des groupes créés
+}
+
+function handleError( msg ) {
+  alert( 'Error: ' + msg );
+  console.log( 'Error: ' + msg );
+}
+
+function submitGroups() {
+    var url = 'setGroups';
+    // build post request body: groupname \n etudid; ...
+    var groupsLists = '';
+    var groupsToCreate='';
+    for (var group_id in groupBoxes) {    
+        if (group_id != 'extend') { // je ne sais pas ce dont il s'agit ???
+            if (group_id != '_none_') { // ne renvoie pas le groupe des sans-groupes
+	            groupBox = groupBoxes[group_id];
+	            if (groupBox.isNew) {
+	                groupsToCreate += groupBox.group_name + ';';
+	                for (var etudid in groupBox.etuds) {
+	                    if (etudid != 'extend')
+	                        groupsToCreate += etudid + ';';
+	                }
+	                groupsToCreate += '\n';
+	                groupBox.isNew = false; // is no more new !
+	            } else {
+	                groupsLists += group_id + ';';
+	                for (var etudid in groupBox.etuds) {
+	                    if (etudid != 'extend')
+	                        groupsLists += etudid + ';';
+	                }
+	                groupsLists += '\n';
+	            }
+            }
+        }
+    }
+    var todel = '';
+    for (var group_id in groupsToDelete) {
+        todel += group_id + ';';    
+    }
+    groupsToDelete = new Object(); // empty
+    var partition_id = document.formGroup.partition_id.value;
+    // Send to server
+    $.get( url, {
+        groupsLists : groupsLists, // encodeURIComponent
+        partition_id : partition_id,
+        groupsToDelete : todel,
+        groupsToCreate : groupsToCreate
+    })           
+        .done( function (data) {
+            processResponse(data); 
+        })
+        .fail(function() {
+            handleError("Erreur lors de l'enregistrement de groupes");
+        });
+}    
+
+// Move to another partition (specified by menu)
+function GotoAnother() {
+    if (groups_unsaved) {
+        alert("Enregistrez ou annulez vos changement avant !");
+    } else
+        document.location='affectGroups?partition_id='+document.formGroup.other_partition_id.value;
+}
+
+
+// Boite information haut de page 
+function updateginfo() {
+    var g = document.getElementById('ginfo');
+    var group_names = new Array();
+    for (var group_id in groupBoxes) {
+        if ((group_id != 'extend') && (groupBoxes[group_id].group_name)){ 
+            group_names.push(groupBoxes[group_id].group_name);
+        }
+    }
+    g.innerHTML = '<b>Groupes définis: ' + group_names.join(', ') + '<br/>'
+        + "Nombre d'etudiants: " + NbEtuds + '</b>';
+    
+    updatesavedinfo();
+}
+
+// Boite indiquant si modifications non enregistrees ou non
+function updatesavedinfo() {
+    var g = document.getElementById('savedinfo');
+    if (groups_unsaved) {
+        g.innerHTML = 'modifications non enregistrées';
+        g.style.visibility='visible';
+    } else {
+        g.innerHTML = '';
+        g.style.visibility='hidden';
+    }
+    return true;
+}
+
+
+$(function() {
+    loadGroupes();
+});
+
+/* debug:
+
+var g = new CGroupBox('G0', 'Toto');
+
+*/
\ No newline at end of file
diff --git a/static/js/groups_view.js b/static/js/groups_view.js
new file mode 100644
index 0000000000000000000000000000000000000000..e050e40ed6c7a075824fcce1b91ea6207c41e7a4
--- /dev/null
+++ b/static/js/groups_view.js
@@ -0,0 +1,179 @@
+// Affichage progressif du trombinoscope html
+
+$().ready(function(){
+    var spans = $(".unloaded_img");
+    for (var i=0; i < spans.length; i++) {
+	var sp = spans[i];
+	var etudid = sp.id;
+	$(sp).load('etud_photo_html?etudid='+etudid);
+    }
+});
+
+
+// L'URL pour recharger l'état courant de la page (groupes et tab selectionnes)
+// (ne fonctionne que pour les requetes GET: manipule la query string)
+
+function groups_view_url() {
+    var url = $.url();
+    delete url.param()['group_ids']; // retire anciens groupes de l'URL
+    delete url.param()['curtab']; // retire ancien tab actif
+    if (CURRENT_TAB_HASH) {
+        url.param()['curtab'] = CURRENT_TAB_HASH;
+    }
+    delete url.param()['formsemestre_id'];
+    url.param()['formsemestre_id'] = $("#group_selector")[0].formsemestre_id.value;
+
+    var selected_groups = $("#group_selector select").val();
+    url.param()['group_ids'] = selected_groups;    // remplace par groupes selectionnes
+ 
+    return url;
+}
+
+// Selectionne tous les etudiants et recharge la page:
+function select_tous() {
+    var url = groups_view_url();
+    var default_group_id = $("#group_selector")[0].default_group_id.value;
+    delete url.param()['group_ids'];
+    url.param()['group_ids'] = [ default_group_id ];
+
+    var query_string = $.param(url.param(), traditional=true );
+    window.location = url.attr('base') + url.attr('path') + '?' + query_string;
+}
+
+// L'URL pour l'état courant de la page:
+function get_current_url() {
+    var url = groups_view_url();
+    var query_string = $.param(url.param(), traditional=true );
+    return url.attr('base') + url.attr('path') + '?' + query_string;
+}
+
+// Recharge la page en changeant les groupes selectionnés et en conservant le tab actif:
+function submit_group_selector() {
+    window.location = get_current_url();
+}
+
+function show_current_tab() {
+    $('.nav-tabs [href="#'+CURRENT_TAB_HASH+'"]').tab('show');
+}
+
+var CURRENT_TAB_HASH = $.url().param()['curtab'];
+
+$().ready(function(){
+    $('.nav-tabs a').on('shown.bs.tab', function (e) {
+        CURRENT_TAB_HASH = e.target.hash.slice(1); // sans le #
+    });
+
+    show_current_tab();
+});
+
+function change_list_options() {
+    var url = groups_view_url();
+    var selected_options = $("#group_list_options").val();
+    var options = [ "with_paiement", "with_archives", "with_annotations", "with_codes" ];
+    for (var i=0; i<options.length; i++) {
+        var option = options[i];
+        delete url.param()[option];
+        if ($.inArray( option, selected_options ) >= 0) {
+            url.param()[option] = 1;
+        }
+    }
+    var query_string = $.param(url.param(), traditional=true );
+    window.location = url.attr('base') + url.attr('path') + '?' + query_string;
+}
+
+// Menu choix groupe:
+function toggle_visible_etuds() {
+    //
+    $(".etud_elem").hide();
+    var qargs = "";
+    $("#group_ids_sel option:selected").each( function(index, opt) {
+        var group_id = opt.value;
+        $("."+group_id).show();
+        qargs += "&group_ids=" + group_id;
+    });
+    // Update url saisie tableur:
+    var input_eval = $("#formnotes_evaluation_id");
+    if (input_eval.length > 0) {
+        var evaluation_id = input_eval[0].value;
+        $("#menu_saisie_tableur a").attr("href", "saisie_notes_tableur?evaluation_id=" + evaluation_id + qargs );
+        // lien feuille excel:
+        $("#lnk_feuille_saisie").attr("href", "feuille_saisie_notes?evaluation_id=" + evaluation_id + qargs );
+    }
+}
+
+$().ready(function() {
+    $('#group_ids_sel').multiselect(
+        {
+            includeSelectAllOption: false,
+            nonSelectedText:'choisir...',
+            // buttonContainer: '<div id="group_ids_sel_container"/>',
+            onChange: function(element, checked){
+                if (checked == true) {
+                    var default_group_id = $(".default_group")[0].value;
+                    
+                    if (element.hasClass("default_group")) {
+                        // click sur groupe "tous"
+                        // deselectionne les autres
+                        $("#group_ids_sel option:selected").each( function(index, opt) {
+                            if (opt.value != default_group_id) {
+                                $("#group_ids_sel").multiselect('deselect', opt.value);
+                            }
+                        });
+                    
+                    } else {
+                        // click sur un autre item
+                        // si le groupe "tous" est selectionne et que l'on coche un autre, le deselectionner
+                        var default_is_selected = false;
+                        $("#group_ids_sel option:selected").each( function(index, opt) {
+                            if (opt.value == default_group_id) {
+                                default_is_selected = true;
+                                return false;
+                            }
+                        });
+                        if (default_is_selected) {
+                            $("#group_ids_sel").multiselect('deselect', default_group_id);
+                        }
+                    }        
+                }    
+
+                toggle_visible_etuds();
+                // referme le menu apres chaque choix:
+                $("#group_selector .btn-group").removeClass('open');
+                
+                if ($("#group_ids_sel").hasClass("submit_on_change")) {
+                    submit_group_selector();
+                }
+            }
+        }
+    );
+
+    // initial setup
+    toggle_visible_etuds();
+});
+
+// Trombinoscope
+$().ready(function(){
+
+    var elems = $(".trombi-photo");
+    for (var i=0; i < elems.length; i++) {
+	$(elems[i]).qtip(
+        {
+	        content: {
+		        ajax: {
+			        url: "etud_info_html?with_photo=0&etudid=" + get_etudid_from_elem(elems[i])
+		        },
+		        text: "Loading..."
+	        },
+	        position: {
+		        at : "right",
+                my : "left top"
+	        },
+	        style: {
+		        classes: 'qtip-etud'
+	        },
+            // utile pour debugguer le css: 
+            // hide: { event: 'unfocus' }
+        }
+    );
+    }
+});
diff --git a/static/js/map_lycees.js b/static/js/map_lycees.js
new file mode 100644
index 0000000000000000000000000000000000000000..dae9f433774118dfd2e7578078818c7718dd711d
--- /dev/null
+++ b/static/js/map_lycees.js
@@ -0,0 +1,34 @@
+// Affiche carte google map des lycees
+
+var ScoMarkerIcons = {};
+
+$().ready(function(){
+    $('#lyc_map_canvas').gmap(
+	{ 'center': '48.955741,2.34141', 
+	  'zoom' : 8,
+	  'mapTypeId': google.maps.MapTypeId.ROADMAP
+	}).bind('init', function(event, map) {
+	    for (var i =0; i < lycees_coords.length; i++) {
+		var lycee = lycees_coords[i];
+		var nb = lycee['number'];
+		var icon;
+		if (nb in ScoMarkerIcons) {
+		    icon = ScoMarkerIcons[nb];
+		} else {
+		    icon = new google.maps.MarkerImage( 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=' + nb + '|FF0000|000000' );
+		    ScoMarkerIcons[nb] = icon; // cache
+		}
+		$('#lyc_map_canvas').gmap(
+		    'addMarker', 
+		    {'position': lycee['position'], 'bounds': true, 'nomlycee' : lycee['name'], 'icon' : icon } 
+		).click(
+		    function() {
+			$('#lyc_map_canvas').gmap('openInfoWindow', {'content': this.nomlycee}, this);
+		    }
+		);
+	    }
+	});
+});
+
+
+
diff --git a/static/js/module_tag_editor.js b/static/js/module_tag_editor.js
new file mode 100644
index 0000000000000000000000000000000000000000..13ca64d7d24241853d410946990a7b7855b2778e
--- /dev/null
+++ b/static/js/module_tag_editor.js
@@ -0,0 +1,35 @@
+// Edition tags sur modules
+
+
+$(function() {
+    $('.module_tag_editor').tagEditor({
+        initialTags: '',
+        placeholder: 'Tags du module ...',
+	forceLowercase: false,
+        onChange: function(field, editor, tags) {
+            $.post('module_tag_set', 
+                   {
+                       module_id: field.data("module_id"),
+                       taglist: tags.join()
+                   });
+        },
+        autocomplete: {
+            delay: 200, // ms before suggest
+            position: { collision: 'flip' }, // automatic menu position up/down
+            source: "module_tag_search"
+        },
+    });
+
+    // version readonly
+    readOnlyTags($('.module_tag_editor_ro'));
+
+    
+    $('.sco_tag_checkbox').click(function() {
+        if( $(this).is(':checked')) {
+            $(".sco_tag_edit").show();
+        } else {
+            $(".sco_tag_edit").hide();
+        }
+    }); 
+
+});
diff --git a/static/js/radar_bulletin.js b/static/js/radar_bulletin.js
new file mode 100644
index 0000000000000000000000000000000000000000..231399ca64d3b9d2e47799ea74124e9a8a824a73
--- /dev/null
+++ b/static/js/radar_bulletin.js
@@ -0,0 +1,278 @@
+//
+// Diagramme "radar" montrant les notes d'un étudiant
+//
+// ScoDoc, (c) E. Viennet 2012
+//
+// Ce code utilise d3.js
+
+$().ready(function(){
+    var etudid = $("#etudid")[0].value;
+    var formsemestre_id = $("#formsemestre_id")[0].value;
+    get_notes_and_draw(formsemestre_id, etudid);
+});
+
+
+var WIDTH = 460; // taille du canvas SVG
+var HEIGHT = WIDTH;
+var CX = WIDTH/2; // coordonnees centre du cercle
+var CY = HEIGHT/2; 
+var RR = 0.4*WIDTH; // Rayon du cercle exterieur
+
+/* Emplacements des marques (polygones et axe gradué) */
+var R_TICS = [ 8, 10, 20 ]; /* [6, 8, 10, 12, 14, 16, 18, 20]; */
+var R_AXIS_TICS = [4, 6, 8, 10, 12, 14, 16, 18, 20];
+var NB_TICS = R_TICS.length;
+
+function get_notes_and_draw(formsemestre_id, etudid) {
+    console.log("get_notes(" + formsemestre_id + ", " + etudid + " )");
+    /* Recupère le bulletin de note et extrait tableau de notes */
+    /* 
+    var notes = [
+        { 'module' : 'E1',
+          'note' : 13,
+          'moy' : 16 },
+    ];
+    */
+    var query = "formsemestre_bulletinetud?formsemestre_id=" + formsemestre_id + "&etudid=" + etudid + "&format=json&version=selectedevals&force_publishing=1"
+    
+    $.get( query, '',  function(bul){ 
+        var notes = [];
+        bul.ue.forEach( 
+            function(ue, i, ues) { 
+                ue['module'].forEach( function(m, i) {
+                    notes.push( { 'code': m['code'], 
+                                  'titre' : m['titre'],
+                                  'note':m['note']['value'], 
+                                  'moy':m['note']['moy'] } ); 
+                }); } ); 
+        draw_radar(notes); 
+    });
+}
+
+function draw_radar(notes) {
+    /* Calcul coordonnées des éléments */
+    var nmod = notes.length;
+    var angle = 2*Math.PI/nmod;
+
+    for (var i=0; i<notes.length; i++) {
+        var d = notes[i];
+        var cx = Math.sin(i*angle);
+        var cy = - Math.cos(i*angle);
+        d["x_v"] = CX + RR * d.note/20 * cx;
+        d["y_v"] = CY + RR * d.note/20 * cy;
+        d["x_moy"] = CX + RR * d.moy/20 * cx;
+        d["y_moy"] = CY + RR * d.moy/20 * cy;
+        d["x_20"] = CX + RR * cx;
+        d["y_20"] = CY + RR * cy;
+        d["x_label"] = CX + (RR + 25) * cx - 10
+        d["y_label"] = CY + (RR + 25) * cy + 10;
+        d["tics"] = [];
+        // Coords des tics sur chaque axe
+        for (var j=0; j < NB_TICS; j++) {
+            var r = R_TICS[j]/20 * RR;
+            d["tics"][j] = { "x" : CX + r * cx, "y" : CY + r * cy };        
+        }
+    }
+
+    var notes_circ = notes.slice(0);
+    notes_circ.push(notes[0])
+    var notes_circ_valid = notes_circ.filter( function(e,i,a) { return e.note != 'NA' && e.note != '-'; } );
+    var notes_valid = notes.filter( function(e,i,a) { return e.note != 'NA' && e.note != '-'; } )
+
+    /* Crée l'élément SVG */
+    g = d3.select("#radar_bulletin").append("svg")
+        .attr("class", "radar")
+        .attr("width", WIDTH+100)
+        .attr("height", HEIGHT);
+
+    /* Centre */
+    g.append( "circle" ).attr("cy", CY)
+        .attr("cx", CX)
+        .attr("r", 2)
+        .attr("class", "radar_center_mark");
+
+    /* Lignes "tics" */
+    for (var j=0; j < NB_TICS; j++) {
+        var ligne_tics = d3.svg.line() 
+            .x(function(d) { return d["tics"][j]["x"]; })
+            .y(function(d) { return d["tics"][j]["y"]; });
+        g.append( "svg:path" )
+            .attr("class", "radar_disk_tic")
+            .attr("id", "radar_disk_tic_" +  R_TICS[j])
+            .attr("d", ligne_tics(notes_circ));
+    }
+
+    /* Lignes radiales pour chaque module */
+    g.selectAll("radar_rad")
+        .data(notes)
+        .enter().append("line")
+        .attr("x1", CX)
+        .attr("y1", CY)
+        .attr("x2", function(d) { return d["x_20"]; })
+        .attr("y2", function(d) { return d["y_20"]; })
+        .attr("class", "radarrad");
+
+
+    /* Lignes entre notes */
+    var ligne = d3.svg.line() 
+        .x(function(d) { return d["x_v"]; })
+        .y(function(d) { return d["y_v"]; });
+
+    g.append( "svg:path" )
+        .attr("class", "radarnoteslines")
+        .attr("d", ligne(notes_circ_valid));
+
+    var ligne_moy = d3.svg.line() 
+        .x(function(d) { return d["x_moy"]; })
+        .y(function(d) { return d["y_moy"]; })
+
+    g.append( "svg:path" )
+        .attr("class", "radarmoylines")
+        .attr("d", ligne_moy(notes_circ_valid));
+
+    /* Points (notes) */
+    g.selectAll("circle1")
+        .data(notes_valid)
+        .enter().append("circle")
+        .attr("cx", function(d) { return d["x_v"]; })
+        .attr("cy", function(d) { return d["y_v"]; })
+        .attr("r", function(x, i) { return 3; } )
+        .style("stroke-width", 1)
+        .style("stroke", "black")
+        .style("fill", "blue")
+        .on("mouseover", function(d) {
+	        var rwidth = 310;
+	        var x = d["x_v"];
+            if ((x - CX) < 0) {
+                x = x + 5;
+                if (x + rwidth + 12 > WIDTH) {
+                    x = WIDTH - rwidth - 12;
+                }
+            }
+            else {
+                if ((x - CX) > 0) {
+                    x = x - rwidth - 5;
+                    if (x < 12) {
+                        x = 12;
+                    }
+                }
+                else {
+                    x = CX - rwidth/2;
+                }
+            }
+            var yrect = d["y_v"];
+            var ytext = d["y_v"];
+            if ((yrect - CY) > 0) {
+                yrect = yrect - 5 - 20;
+                ytext = ytext - 5 - 20 + 16;
+            }
+            else {
+                yrect = yrect + 5;
+                ytext = ytext + 5 + 16;
+            }            
+	        var r = g.append("rect")
+	            .attr('class','radartip')
+	            .attr("x", x)
+                .attr("y", yrect );
+	        
+	        var txt = g.append("text").text("Note: " +  d.note + "/20, moyenne promo: " + d.moy + "/20")
+	            .attr('class','radartip')
+	            .attr("x", x + 5)
+                .attr("y", ytext);
+	        r.attr("width", rwidth).attr("height", 20);
+        })
+        .on("mouseout", function(d){
+            d3.selectAll(".radartip").remove()
+        });
+
+    /* Valeurs des notes */
+    g.selectAll("notes_labels")
+        .data(notes_valid)
+        .enter().append("text")
+        .text(function(d) { return d["note"]; })
+        .attr("x", function(d) { 
+            return d["x_v"]; 
+        })
+        .attr("y", function(d) { 
+            if (d["y_v"] > CY)
+                return d["y_v"] + 16;
+            else
+                return d["y_v"] - 8;
+        })
+        .attr("class", "note_label");
+
+    /* Petits points sur les moyennes */
+    g.selectAll("circle2")
+        .data(notes_valid)
+        .enter().append("circle")
+        .attr("cx", function(d) { return d["x_moy"]; })
+        .attr("cy", function(d) { return d["y_moy"]; })
+        .attr("r", function(x, i) { return 2; } )
+        .style("stroke-width", 0)
+        .style("stroke", "black")
+        .style("fill", "rgb(20,90,50)");
+
+    /* Valeurs sur axe */
+    g.selectAll("textaxis")
+        .data( R_AXIS_TICS )
+        .enter().append("text")
+        .text(String)
+        .attr("x", CX - 10)
+        .attr("y", function(x, i) { return CY - x*RR/20 + 6; })
+        .attr("class", "textaxis");
+
+    /* Noms des modules */
+    g.selectAll("text_modules")
+        .data(notes)
+        .enter().append("text")
+        .text( function(d) { return d['code']; } )
+        .attr("x", function(d) { return d['x_label']; } )
+        .attr("y", function(d) { return d['y_label']; })
+        .attr("dx", 0)
+        .attr("dy", 0)
+        .on("mouseover", function(d) {
+            var x = d["x_label"];
+            var yrect = d["y_label"];
+            var ytext = d["y_label"];
+            var titre = d['titre'].replace("&apos;", "'").substring(0,64);
+            var rwidth = titre.length * 9; // rough estimate of string width in pixels
+            if ((x - CX) < 0) {
+                x = x + 5;
+                if (x + rwidth + 12 > WIDTH) {
+                    x = WIDTH - rwidth - 12;
+                }
+            }
+            else {
+                if ((x - CX) > 0) {
+                    x = x - rwidth - 5;
+                    if (x < 12) {
+                        x = 12;
+                    }
+                }
+                else {
+                    x = CX - rwidth/2;
+                }
+            }
+            if ((yrect - CY) > 0) {
+                yrect = yrect - 5 - 20;
+                ytext = ytext - 5 - 20 + 16;
+            }
+            else {
+                yrect = yrect + 5;
+                ytext = ytext + 5 + 16;
+            }      
+            var r = g.append("rect")
+	            .attr('class','radartip')
+	            .attr("x", x)
+                .attr("y", yrect)
+                .attr("height", 20)
+                .attr("width", rwidth);
+            var txt = g.append("text").text( titre )
+                .attr('class','radartip')
+	            .attr("x", x + 5)
+                .attr("y", ytext);
+        })
+        .on("mouseout", function(d){
+            d3.selectAll(".radartip").remove()
+        });
+}
diff --git a/static/js/recap_parcours.js b/static/js/recap_parcours.js
new file mode 100644
index 0000000000000000000000000000000000000000..8f9329d38192d18a307ec74347bce5e31d00a377
--- /dev/null
+++ b/static/js/recap_parcours.js
@@ -0,0 +1,59 @@
+// Affichage parcours etudiant
+// (uses jQuery)
+
+function toggle_vis(e, new_state) { // change visibility of tr (UE in tr and next tr)
+    // e is the span containg the clicked +/- icon
+    var formsemestre_class = e.classList[1];
+    var tr = e.parentNode.parentNode;
+    if (new_state == undefined) {
+	    // current state: use alt attribute of current image
+	    if (e.childNodes[0].alt == '+') {
+            new_state=false;
+	    } else {
+            new_state=true;
+	    }
+    }
+    if (new_state) {
+        new_tr_display = 'none';
+    } else {
+        new_tr_display = 'table-row';
+    }
+    $("tr."+formsemestre_class+":not(.rcp_l1)").css('display', new_tr_display)
+    
+    // find next tr in siblings (xxx legacy code, could be optimized)
+    var sibl = tr.nextSibling;
+    while ((sibl != null) && sibl.nodeType != 1 && sibl.tagName != 'TR') {
+        sibl = sibl.nextSibling;
+    }
+    if (sibl) {
+        var td_disp = 'none';
+        if (new_state) {
+            e.innerHTML = '<img width="13" height="13" border="0" title="" alt="+" src="/ScoDoc/static/icons/plus_img.png"/>';
+        } else {
+            e.innerHTML = '<img width="13" height="13" border="0" title="" alt="-" src="/ScoDoc/static/icons/minus_img.png"/>';
+            td_disp = 'inline';
+        }
+        // acronymes d'UE
+        sibl = e.parentNode.nextSibling;
+        while (sibl != null) {
+            if (sibl.nodeType == 1 && sibl.className == 'ue_acro')
+                sibl.childNodes[0].style.display = td_disp; 
+            sibl = sibl.nextSibling;
+        }
+    }
+}
+
+var sems_state = false;
+
+function toggle_all_sems(e) {
+    var elems = $("span.toggle_sem"); 
+    for (var i=0; i < elems.length; i++) {
+	toggle_vis(elems[i], sems_state);
+    }
+    sems_state = !sems_state;
+    if (sems_state) {
+	e.innerHTML = '<img width="18" height="18" border="0" title="" alt="-" src="/ScoDoc/static/icons/minus18_img.png"/>';
+    } else {
+	e.innerHTML = '<img width="18" height="18" border="0" title="" alt="+" src="/ScoDoc/static/icons/plus18_img.png"/>';
+    }
+}
\ No newline at end of file
diff --git a/static/js/saisie_notes.js b/static/js/saisie_notes.js
new file mode 100644
index 0000000000000000000000000000000000000000..3fc029d5bbc444e30ed16e00977efcf2d22ea129
--- /dev/null
+++ b/static/js/saisie_notes.js
@@ -0,0 +1,113 @@
+// Formulaire saisie des notes
+
+$().ready(function(){
+    
+    $("#formnotes .note").bind("blur", valid_note);
+
+    $("#formnotes input").bind("paste", paste_text);
+    
+});
+
+function is_valid_note(v) {
+    if (!v)
+        return true; 
+    
+    var note_min = parseFloat($("#eval_note_min").text());
+    var note_max = parseFloat($("#eval_note_max").text());
+    
+    if (! v.match("^-?[0-9.]*$")) {
+        return (v=="ABS")||(v=="EXC")||(v=="SUPR")||(v=="ATT")||(v=="DEM");
+    } else {
+        var x = parseFloat(v);
+        return (x >= note_min) && (x <= note_max);
+    }
+}
+
+function valid_note(e) {
+    var v = this.value.trim().toUpperCase().replace(",", ".");
+    if (is_valid_note(v)) {
+        if (v && (v != $(this).attr('data-last-saved-value'))) {
+            this.className = "note_valid_new";
+            var etudid = $(this).attr('data-etudid');
+            save_note(this, v, etudid);
+        }
+    } else {
+        /* Saisie invalide */
+        this.className = "note_invalid";
+    }
+}
+
+function save_note(elem, v, etudid) {
+    var evaluation_id = $("#formnotes_evaluation_id").attr("value");
+    var formsemestre_id = $("#formnotes_formsemestre_id").attr("value");
+    $('#sco_msg').html("en cours...").show();
+    $.post( 'save_note',
+            { 
+                'etudid' : etudid,
+                'evaluation_id' : evaluation_id,
+                'value' : v,
+                'comment' : $("#formnotes_comment").attr("value")
+            },
+            function(result) {
+                sco_message("enregistré");
+                elem.className = "note_saved";                
+                if (result['nbchanged'] > 0) {
+                    // il y avait une decision de jury ?
+                    if (result.existing_decisions[0] == etudid) {
+                        if (v != $(elem).attr('data-orig-value')) {
+                            $("#jurylink_"+etudid).html('<a href="formsemestre_validation_etud_form?formsemestre_id=' + formsemestre_id + '&etudid=' + etudid + '">mettre à jour décision de jury</a>');
+                        } else {
+                            $("#jurylink_"+etudid).html('');
+                        }
+                    }                    
+                    // mise a jour menu historique
+                    if (result['history_menu']) {
+                        $("#hist_"+etudid).html(result['history_menu']);
+                    }
+                }
+                $(elem).attr('data-last-saved-value', v)
+            }
+          );
+}
+
+function change_history(e) {
+    var opt = e.selectedOptions[0];
+    var val = $(opt).attr("data-note");
+    var etudid = $(e).attr('data-etudid');
+    // le input associé a ce menu:
+    var input_elem = e.parentElement.parentElement.parentElement.childNodes[0];
+    input_elem.value = val;
+    save_note(input_elem, val, etudid);
+}
+
+// Contribution S.L.: copier/coller des notes
+
+
+function paste_text(e) {
+    var event = e.originalEvent;
+    event.stopPropagation();
+    event.preventDefault();
+    var clipb = e.originalEvent.clipboardData;
+    var data = clipb.getData('Text');
+    var list = data.split(/\r\n|\r|\n|\t| /g);
+    var currentInput = event.currentTarget;
+	
+    for(var i=0 ; i<list.length ; i++){
+	currentInput.value = list[i];
+	var evt = document.createEvent("HTMLEvents");
+	evt.initEvent("blur", false, true);
+	currentInput.dispatchEvent(evt);
+	var sibbling = currentInput.parentElement.parentElement.nextElementSibling;
+	while (sibbling && sibbling.style.display == "none") {
+            sibbling = sibbling.nextElementSibling;
+        }
+	if (sibbling) {
+	    currentInput = sibbling.querySelector("input");
+	    if(!currentInput){
+		return;
+	    }
+	} else {
+	    return;
+	}	    
+    }
+}		
diff --git a/static/js/sco_ue_external.js b/static/js/sco_ue_external.js
new file mode 100644
index 0000000000000000000000000000000000000000..24cd2157c398a19105c472dbfec51b8ac8fe3ac5
--- /dev/null
+++ b/static/js/sco_ue_external.js
@@ -0,0 +1,30 @@
+// Gestion formulaire UE externes
+
+function toggle_new_ue_form(state) {
+    // active/desactive le formulaire "nouvelle UE"
+    var text_color;
+    if (state) {
+        text_color = 'rgb(180,160,160)';
+    } else {
+        text_color = 'rgb(0,0,0)';
+    }
+
+    $("#tf_extue_titre td:eq(1) input").prop( "disabled", state );
+    $("#tf_extue_titre td:eq(1) input").css('color', text_color)
+
+    $("#tf_extue_acronyme td:eq(1) input").prop( "disabled", state );
+    $("#tf_extue_acronyme td:eq(1) input").css('color', text_color)
+
+    $("#tf_extue_ects td:eq(1) input").prop( "disabled", state );
+    $("#tf_extue_ects td:eq(1) input").css('color', text_color)
+}
+
+
+function update_external_ue_form() {
+    var state = (tf.existing_ue.value != "")
+    toggle_new_ue_form(state);
+}
+
+
+
+
diff --git a/static/js/scodoc.js b/static/js/scodoc.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ae653eee0a693fa52272a2720f5cbf221375ad2
--- /dev/null
+++ b/static/js/scodoc.js
@@ -0,0 +1,120 @@
+// JS for all ScoDoc pages (using jQuery UI)
+
+
+$(function() {
+    // Autocomplete recherche etudiants par nom
+    $("#in-expnom").autocomplete(
+        {
+            delay: 300, // wait 300ms before suggestions
+            minLength: 2, // min nb of chars before suggest
+            position: { collision: 'flip' }, // automatic menu position up/down
+            source: "search_etud_by_name",
+            select: function(event, ui) { 
+                $("#form-chercheetud").submit(); 
+            }
+        });    
+    
+    // Date picker
+    $(".datepicker").datepicker({
+        showOn: 'button', 
+        buttonImage: '/ScoDoc/static/icons/calendar_img.png', 
+        buttonImageOnly: true,
+        dateFormat: 'dd/mm/yy',   
+        duration : 'fast',                   
+    });
+    $('.datepicker').datepicker('option', $.extend({showMonthAfterYear: false},
+                                                   $.datepicker.regional['fr']));
+
+    /* Barre menu */
+    var sco_menu_position = {my: "left top", at: "left bottom"};
+    $("#sco_menu").menu({
+        position: sco_menu_position,
+        blur: function() {
+            $(this).menu("option", "position", sco_menu_position);
+        },
+        focus: function(e, ui) {
+            if ($("#sco_menu").get(0) !== $(ui).get(0).item.parent().get(0)) {
+                $(this).menu("option", "position", {my: "left top", at: "right top"});
+            }
+        }
+        }).mouseleave(function(x, y) {
+            $( "#sco_menu" ).menu('collapseAll');
+        });
+
+    $("#sco_menu > li > a > span").switchClass("ui-icon-carat-1-e", "ui-icon-carat-1-s");
+
+    /* Les menus isoles dropdown */
+    $(".sco_dropdown_menu").menu({
+        position: sco_menu_position
+    }).mouseleave(function(x, y) {
+        $( ".sco_dropdown_menu" ).menu('collapseAll');
+    }
+                 );
+    $(".sco_dropdown_menu > li > a > span").switchClass("ui-icon-carat-1-e", "ui-icon-carat-1-s");
+    
+});
+
+
+// Affiche un message transitoire
+function sco_message(msg, color) {
+    if (color === undefined) {
+        color = "green";
+    }
+    $('#sco_msg').html(msg).show();
+    if (color) {
+        $('#sco_msg').css('color', color);
+    }
+    setTimeout(
+        function() {  
+            $('#sco_msg').fadeOut(
+                'slow',
+                function() { 
+                    $('#sco_msg').html('');
+                }
+            );  
+        }, 
+        2000 // <-- duree affichage en milliseconds  
+    );
+}
+
+
+function get_query_args() {
+    var s = window.location.search; // eg "?x=1&y=2"
+    var vars = {};
+    s.replace( 
+		    /[?&]+([^=&]+)=?([^&]*)?/gi, // regexp
+		function( m, key, value ) { // callback
+			vars[key] = value !== undefined ? value : '';
+		}
+    );
+    return vars;
+}
+
+
+// Tables (gen_tables)
+$(function() {
+    $('table.gt_table').DataTable( {
+        "paging" : false,
+        "searching" : false,
+        "info" : false,
+        /* "autoWidth" : false, */
+        "fixedHeader" : {
+            "header": true,
+            "footer": true
+        },
+        "orderCellsTop": true, // cellules ligne 1 pour tri 
+        "aaSorting": [ ], // Prevent initial sorting
+    } );
+});
+
+
+// Show tags (readonly)
+function readOnlyTags(nodes) {
+    // nodes are textareas, hide them and create a span showing tags
+    for (var i = 0; i < nodes.length; i++) {
+	var node = $(nodes[i]);
+	node.hide();
+	var tags = nodes[i].value.split(',');
+	node.after('<span class="ro_tags"><span class="ro_tag">' + tags.join('</span><span class="ro_tag">') + '</span></span>');
+    }
+}
diff --git a/static/js/trombino.js b/static/js/trombino.js
new file mode 100644
index 0000000000000000000000000000000000000000..874567bdd336d95c90f101548825cfca8d6b41ca
--- /dev/null
+++ b/static/js/trombino.js
@@ -0,0 +1,11 @@
+// Affichage progressif du trombinoscope html
+
+$().ready(function(){
+    var spans = $(".unloaded_img");
+    for (var i=0; i < spans.size(); i++) {
+	var sp = spans[i];
+	var etudid = sp.id;
+	$(sp).load('etud_photo_html?etudid='+etudid);
+    }
+});
+
diff --git a/static/js/ue_list.js b/static/js/ue_list.js
new file mode 100644
index 0000000000000000000000000000000000000000..f6e9ea757877cf9e9b1123d73d08e7973058273e
--- /dev/null
+++ b/static/js/ue_list.js
@@ -0,0 +1,5 @@
+// Edition elements programme "en place"
+
+$(function() {
+    $('.span_apo_edit').jinplace();
+});
\ No newline at end of file
diff --git a/static/js/validate_previous_ue.js b/static/js/validate_previous_ue.js
new file mode 100644
index 0000000000000000000000000000000000000000..aae02f7d09165cb58beb89386a6fc3eb6e124c12
--- /dev/null
+++ b/static/js/validate_previous_ue.js
@@ -0,0 +1,31 @@
+// Affiche et met a jour la liste des UE partageant le meme code
+
+$().ready(function(){
+    update_ue_validations();
+    update_ue_list();
+    $("#tf_ue_id").bind("change", update_ue_list);
+    $("#tf_ue_id").bind("change", update_ue_validations);
+});
+
+
+function update_ue_list() {
+    var ue_id = $("#tf_ue_id")[0].value;
+    if (ue_id) {
+	var query = "ue_sharing_code?ue_id=" + ue_id;
+	$.get( query, '',  function(data){ 
+	    $("#ue_list_code").html(data);
+	});
+    }
+} 
+
+function update_ue_validations() {
+    var etudid = $("#tf_etudid")[0].value;
+    var ue_id = $("#tf_ue_id")[0].value;
+    var formsemestre_id = $("#tf_formsemestre_id")[0].value;
+    if (ue_id) {
+	var query = "get_etud_ue_cap_html?ue_id="+ue_id+"&etudid="+etudid+"&formsemestre_id="+formsemestre_id;
+	$.get( query, '',  function(data){ 
+	    $("#ue_list_etud_validations").html(data);
+	});
+    }
+}
\ No newline at end of file
diff --git a/static/libjs/AutoSuggest.js b/static/libjs/AutoSuggest.js
new file mode 120000
index 0000000000000000000000000000000000000000..3f0fa014129cd3ef05b523c05329403ac87b0565
--- /dev/null
+++ b/static/libjs/AutoSuggest.js
@@ -0,0 +1 @@
+AutoSuggest_2.1.3_comp.js
\ No newline at end of file
diff --git a/static/libjs/AutoSuggest_2.1.3_comp.js b/static/libjs/AutoSuggest_2.1.3_comp.js
new file mode 100644
index 0000000000000000000000000000000000000000..a7a83c77aa624fd913f112dad0bf28c7e590dba3
--- /dev/null
+++ b/static/libjs/AutoSuggest_2.1.3_comp.js
@@ -0,0 +1 @@
+if(typeof(bsn)=="undefined")_b=bsn={};if(typeof(_b.Autosuggest)=="undefined")_b.Autosuggest={};else alert("Autosuggest is already set!");_b.AutoSuggest=function(b,c){if(!document.getElementById)return 0;this.fld=_b.DOM.gE(b);if(!this.fld)return 0;this.sInp="";this.nInpC=0;this.aSug=[];this.iHigh=0;this.oP=c?c:{};var k,def={minchars:1,meth:"get",varname:"input",className:"autosuggest",timeout:2500,delay:500,offsety:-5,shownoresults:true,noresults:"No results!",maxheight:250,cache:true,maxentries:25};for(k in def){if(typeof(this.oP[k])!=typeof(def[k]))this.oP[k]=def[k]}var p=this;this.fld.onkeypress=function(a){return p.onKeyPress(a)};this.fld.onkeyup=function(a){return p.onKeyUp(a)};this.fld.setAttribute("autocomplete","off")};_b.AutoSuggest.prototype.onKeyPress=function(a){var b=(window.event)?window.event.keyCode:a.keyCode;var c=13;var d=9;var e=27;var f=1;switch(b){case c:this.setHighlightedValue();f=0;return false;break;case e:this.clearSuggestions();break}return f};_b.AutoSuggest.prototype.onKeyUp=function(a){var b=(window.event)?window.event.keyCode:a.keyCode;var c=38;var d=40;var e=1;switch(b){case c:this.changeHighlight(b);e=0;break;case d:this.changeHighlight(b);e=0;break;default:this.getSuggestions(this.fld.value)}return e};_b.AutoSuggest.prototype.getSuggestions=function(a){if(a==this.sInp)return 0;_b.DOM.remE(this.idAs);this.sInp=a;if(a.length<this.oP.minchars){this.aSug=[];this.nInpC=a.length;return 0}var b=this.nInpC;this.nInpC=a.length?a.length:0;var l=this.aSug.length;if(this.nInpC>b&&l&&l<this.oP.maxentries&&this.oP.cache){var c=[];for(var i=0;i<l;i++){if(this.aSug[i].value.substr(0,a.length).toLowerCase()==a.toLowerCase())c.push(this.aSug[i])}this.aSug=c;this.createList(this.aSug);return false}else{var d=this;var e=this.sInp;clearTimeout(this.ajID);this.ajID=setTimeout(function(){d.doAjaxRequest(e)},this.oP.delay)}return false};_b.AutoSuggest.prototype.doAjaxRequest=function(b){if(b!=this.fld.value)return false;var c=this;if(typeof(this.oP.script)=="function")var d=this.oP.script(encodeURIComponent(this.sInp));else var d=this.oP.script+this.oP.varname+"="+encodeURIComponent(this.sInp);if(!d)return false;var e=this.oP.meth;var b=this.sInp;var f=function(a){c.setSuggestions(a,b)};var g=function(a){alert("AJAX error: "+a)};var h=new _b.Ajax();h.makeRequest(d,e,f,g)};_b.AutoSuggest.prototype.setSuggestions=function(a,b){if(b!=this.fld.value)return false;this.aSug=[];if(this.oP.json){var c=eval('('+a.responseText+')');for(var i=0;i<c.results.length;i++){this.aSug.push({'id':c.results[i].id,'value':c.results[i].value,'info':c.results[i].info})}}else{var d=a.responseXML;var e=d.getElementsByTagName('results')[0].childNodes;for(var i=0;i<e.length;i++){if(e[i].hasChildNodes())this.aSug.push({'id':e[i].getAttribute('id'),'value':e[i].childNodes[0].nodeValue,'info':e[i].getAttribute('info')})}}this.idAs="as_"+this.fld.id;this.createList(this.aSug)};_b.AutoSuggest.prototype.createList=function(b){var c=this;_b.DOM.remE(this.idAs);this.killTimeout();if(b.length==0&&!this.oP.shownoresults)return false;var d=_b.DOM.cE("div",{id:this.idAs,className:this.oP.className});var e=_b.DOM.cE("div",{className:"as_corner"});var f=_b.DOM.cE("div",{className:"as_bar"});var g=_b.DOM.cE("div",{className:"as_header"});g.appendChild(e);g.appendChild(f);d.appendChild(g);var h=_b.DOM.cE("ul",{id:"as_ul"});for(var i=0;i<b.length;i++){var j=b[i].value;var k=j.toLowerCase().indexOf(this.sInp.toLowerCase());var l=j.substring(0,k)+"<em>"+j.substring(k,k+this.sInp.length)+"</em>"+j.substring(k+this.sInp.length);var m=_b.DOM.cE("span",{},l,true);if(b[i].info!=""){var n=_b.DOM.cE("br",{});m.appendChild(n);var o=_b.DOM.cE("small",{},b[i].info);m.appendChild(o)}var a=_b.DOM.cE("a",{href:"#"});var p=_b.DOM.cE("span",{className:"tl"}," ");var q=_b.DOM.cE("span",{className:"tr"}," ");a.appendChild(p);a.appendChild(q);a.appendChild(m);a.name=i+1;a.onclick=function(){c.setHighlightedValue();return false};a.onmouseover=function(){c.setHighlight(this.name)};var r=_b.DOM.cE("li",{},a);h.appendChild(r)}if(b.length==0&&this.oP.shownoresults){var r=_b.DOM.cE("li",{className:"as_warning"},this.oP.noresults);h.appendChild(r)}d.appendChild(h);var s=_b.DOM.cE("div",{className:"as_corner"});var t=_b.DOM.cE("div",{className:"as_bar"});var u=_b.DOM.cE("div",{className:"as_footer"});u.appendChild(s);u.appendChild(t);d.appendChild(u);var v=_b.DOM.getPos(this.fld);d.style.left=v.x+"px";d.style.top=(v.y+this.fld.offsetHeight+this.oP.offsety)+"px";d.style.width=this.fld.offsetWidth+"px";d.onmouseover=function(){c.killTimeout()};d.onmouseout=function(){c.resetTimeout()};document.getElementsByTagName("body")[0].appendChild(d);this.iHigh=0;var c=this;this.toID=setTimeout(function(){c.clearSuggestions()},this.oP.timeout)};_b.AutoSuggest.prototype.changeHighlight=function(a){var b=_b.DOM.gE("as_ul");if(!b)return false;var n;if(a==40)n=this.iHigh+1;else if(a==38)n=this.iHigh-1;if(n>b.childNodes.length)n=b.childNodes.length;if(n<1)n=1;this.setHighlight(n)};_b.AutoSuggest.prototype.setHighlight=function(n){var a=_b.DOM.gE("as_ul");if(!a)return false;if(this.iHigh>0)this.clearHighlight();this.iHigh=Number(n);a.childNodes[this.iHigh-1].className="as_highlight";this.killTimeout()};_b.AutoSuggest.prototype.clearHighlight=function(){var a=_b.DOM.gE("as_ul");if(!a)return false;if(this.iHigh>0){a.childNodes[this.iHigh-1].className="";this.iHigh=0}};_b.AutoSuggest.prototype.setHighlightedValue=function(){if(this.iHigh){this.sInp=this.fld.value=this.aSug[this.iHigh-1].value;this.fld.focus();if(this.fld.selectionStart)this.fld.setSelectionRange(this.sInp.length,this.sInp.length);this.clearSuggestions();if(typeof(this.oP.callback)=="function")this.oP.callback(this.aSug[this.iHigh-1])}};_b.AutoSuggest.prototype.killTimeout=function(){clearTimeout(this.toID)};_b.AutoSuggest.prototype.resetTimeout=function(){clearTimeout(this.toID);var a=this;this.toID=setTimeout(function(){a.clearSuggestions()},1000)};_b.AutoSuggest.prototype.clearSuggestions=function(){this.killTimeout();var a=_b.DOM.gE(this.idAs);var b=this;if(a){var c=new _b.Fader(a,1,0,250,function(){_b.DOM.remE(b.idAs)})}};if(typeof(_b.Ajax)=="undefined")_b.Ajax={};_b.Ajax=function(){this.req={};this.isIE=false};_b.Ajax.prototype.makeRequest=function(a,b,c,d){if(b!="POST")b="GET";this.onComplete=c;this.onError=d;var e=this;if(window.XMLHttpRequest){this.req=new XMLHttpRequest();this.req.onreadystatechange=function(){e.processReqChange()};this.req.open("GET",a,true);this.req.send(null)}else if(window.ActiveXObject){this.req=new ActiveXObject("Microsoft.XMLHTTP");if(this.req){this.req.onreadystatechange=function(){e.processReqChange()};this.req.open(b,a,true);this.req.send()}}};_b.Ajax.prototype.processReqChange=function(){if(this.req.readyState==4){if(this.req.status==200){this.onComplete(this.req)}else{this.onError(this.req.status)}}};if(typeof(_b.DOM)=="undefined")_b.DOM={};_b.DOM.cE=function(b,c,d,e){var f=document.createElement(b);if(!f)return 0;for(var a in c)f[a]=c[a];var t=typeof(d);if(t=="string"&&!e)f.appendChild(document.createTextNode(d));else if(t=="string"&&e)f.innerHTML=d;else if(t=="object")f.appendChild(d);return f};_b.DOM.gE=function(e){var t=typeof(e);if(t=="undefined")return 0;else if(t=="string"){var a=document.getElementById(e);if(!a)return 0;else if(typeof(a.appendChild)!="undefined")return a;else return 0}else if(typeof(e.appendChild)!="undefined")return e;else return 0};_b.DOM.remE=function(a){var e=this.gE(a);if(!e)return 0;else if(e.parentNode.removeChild(e))return true;else return 0};_b.DOM.getPos=function(e){var e=this.gE(e);var a=e;var b=0;if(a.offsetParent){while(a.offsetParent){b+=a.offsetLeft;a=a.offsetParent}}else if(a.x)b+=a.x;var a=e;var c=0;if(a.offsetParent){while(a.offsetParent){c+=a.offsetTop;a=a.offsetParent}}else if(a.y)c+=a.y;return{x:b,y:c}};if(typeof(_b.Fader)=="undefined")_b.Fader={};_b.Fader=function(a,b,c,d,e){if(!a)return 0;this.e=a;this.from=b;this.to=c;this.cb=e;this.nDur=d;this.nInt=50;this.nTime=0;var p=this;this.nID=setInterval(function(){p._fade()},this.nInt)};_b.Fader.prototype._fade=function(){this.nTime+=this.nInt;var a=Math.round(this._tween(this.nTime,this.from,this.to,this.nDur)*100);var b=a/100;if(this.e.filters){try{this.e.filters.item("DXImageTransform.Microsoft.Alpha").opacity=a}catch(e){this.e.style.filter='progid:DXImageTransform.Microsoft.Alpha(opacity='+a+')'}}else{this.e.style.opacity=b}if(this.nTime==this.nDur){clearInterval(this.nID);if(this.cb!=undefined)this.cb()}};_b.Fader.prototype._tween=function(t,b,c,d){return b+((c-b)*(t/d))};
\ No newline at end of file
diff --git a/static/libjs/bootstrap-3.1.1-dist/._css b/static/libjs/bootstrap-3.1.1-dist/._css
new file mode 100755
index 0000000000000000000000000000000000000000..9f1360166b24cc6c41372f2d6c4c63d8f0fcabae
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/._css differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/._fonts b/static/libjs/bootstrap-3.1.1-dist/._fonts
new file mode 100755
index 0000000000000000000000000000000000000000..8884c95fa83cfc33b5391ec532c8402f08ada9b4
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/._fonts differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/._js b/static/libjs/bootstrap-3.1.1-dist/._js
new file mode 100755
index 0000000000000000000000000000000000000000..32df7440c0711813292b3997e9365905f274601e
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/._js differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap-theme.css b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap-theme.css
new file mode 100644
index 0000000000000000000000000000000000000000..5d2c244e57fe283b31b8a7efbe2e8df71b8f453b
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap-theme.css differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap-theme.css.map b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap-theme.css.map
new file mode 100644
index 0000000000000000000000000000000000000000..8f3ee4d48a6abf40c73d71197fe9dc3cecd60690
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap-theme.css.map differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap-theme.min.css b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap-theme.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..7f7a855a23b27b86dde8be20f06542d4660287c6
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap-theme.min.css differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap.css b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap.css
new file mode 100644
index 0000000000000000000000000000000000000000..c0b1c0dfaced8c24912435f45a6f9380e9b71c42
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap.css differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap.css.map b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap.css.map
new file mode 100644
index 0000000000000000000000000000000000000000..491b8ba57ce83a792ae9800994473dd3eedcd448
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap.css.map differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap.min.css b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..45b6abda4f6cbef0fbfb4341e984e221b410070f
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/css/._bootstrap.min.css differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.css b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.css
new file mode 100644
index 0000000000000000000000000000000000000000..a4069929bceb661eacbd4b1eb21306cfa5a1c8f9
--- /dev/null
+++ b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.css
@@ -0,0 +1,347 @@
+/*!
+ * Bootstrap v3.1.1 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+.btn-default,
+.btn-primary,
+.btn-success,
+.btn-info,
+.btn-warning,
+.btn-danger {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
+}
+.btn-default:active,
+.btn-primary:active,
+.btn-success:active,
+.btn-info:active,
+.btn-warning:active,
+.btn-danger:active,
+.btn-default.active,
+.btn-primary.active,
+.btn-success.active,
+.btn-info.active,
+.btn-warning.active,
+.btn-danger.active {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+}
+.btn:active,
+.btn.active {
+  background-image: none;
+}
+.btn-default {
+  text-shadow: 0 1px 0 #fff;
+  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
+  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #dbdbdb;
+  border-color: #ccc;
+}
+.btn-default:hover,
+.btn-default:focus {
+  background-color: #e0e0e0;
+  background-position: 0 -15px;
+}
+.btn-default:active,
+.btn-default.active {
+  background-color: #e0e0e0;
+  border-color: #dbdbdb;
+}
+.btn-primary {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
+  background-image:         linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #2b669a;
+}
+.btn-primary:hover,
+.btn-primary:focus {
+  background-color: #2d6ca2;
+  background-position: 0 -15px;
+}
+.btn-primary:active,
+.btn-primary.active {
+  background-color: #2d6ca2;
+  border-color: #2b669a;
+}
+.btn-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #3e8f3e;
+}
+.btn-success:hover,
+.btn-success:focus {
+  background-color: #419641;
+  background-position: 0 -15px;
+}
+.btn-success:active,
+.btn-success.active {
+  background-color: #419641;
+  border-color: #3e8f3e;
+}
+.btn-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #28a4c9;
+}
+.btn-info:hover,
+.btn-info:focus {
+  background-color: #2aabd2;
+  background-position: 0 -15px;
+}
+.btn-info:active,
+.btn-info.active {
+  background-color: #2aabd2;
+  border-color: #28a4c9;
+}
+.btn-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #e38d13;
+}
+.btn-warning:hover,
+.btn-warning:focus {
+  background-color: #eb9316;
+  background-position: 0 -15px;
+}
+.btn-warning:active,
+.btn-warning.active {
+  background-color: #eb9316;
+  border-color: #e38d13;
+}
+.btn-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #b92c28;
+}
+.btn-danger:hover,
+.btn-danger:focus {
+  background-color: #c12e2a;
+  background-position: 0 -15px;
+}
+.btn-danger:active,
+.btn-danger.active {
+  background-color: #c12e2a;
+  border-color: #b92c28;
+}
+.thumbnail,
+.img-thumbnail {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+}
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  background-color: #e8e8e8;
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+  background-repeat: repeat-x;
+}
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  background-color: #357ebd;
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image:         linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
+  background-repeat: repeat-x;
+}
+.navbar-default {
+  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
+  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
+}
+.navbar-default .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
+  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
+  background-repeat: repeat-x;
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
+}
+.navbar-brand,
+.navbar-nav > li > a {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
+}
+.navbar-inverse {
+  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
+  background-image:         linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+}
+.navbar-inverse .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%);
+  background-image:         linear-gradient(to bottom, #222 0%, #282828 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
+  background-repeat: repeat-x;
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
+}
+.navbar-inverse .navbar-brand,
+.navbar-inverse .navbar-nav > li > a {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
+}
+.navbar-static-top,
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  border-radius: 0;
+}
+.alert {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
+}
+.alert-success {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #b2dba1;
+}
+.alert-info {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #9acfea;
+}
+.alert-warning {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #f5e79e;
+}
+.alert-danger {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #dca7a7;
+}
+.progress {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
+  background-image:         linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
+  background-repeat: repeat-x;
+}
+.list-group {
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+}
+.list-group-item.active,
+.list-group-item.active:hover,
+.list-group-item.active:focus {
+  text-shadow: 0 -1px 0 #3071a9;
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
+  background-image:         linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #3278b3;
+}
+.panel {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
+}
+.panel-default > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-primary > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image:         linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-success > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-info > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-warning > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-danger > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
+  background-repeat: repeat-x;
+}
+.well {
+  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image:         linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #dcdcdc;
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
+          box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
+}
+/*# sourceMappingURL=bootstrap-theme.css.map */
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.css.map b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.css.map
new file mode 100644
index 0000000000000000000000000000000000000000..b36fc9a4970e41d7a3bfdb67780e93ab18a68faf
--- /dev/null
+++ b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["less/theme.less","less/mixins.less"],"names":[],"mappings":"AAeA;AACA;AACA;AACA;AACA;AACA;EACE,wCAAA;ECoGA,2FAAA;EACQ,mFAAA;;ADhGR,YAAC;AAAD,YAAC;AAAD,YAAC;AAAD,SAAC;AAAD,YAAC;AAAD,WAAC;AACD,YAAC;AAAD,YAAC;AAAD,YAAC;AAAD,SAAC;AAAD,YAAC;AAAD,WAAC;EC8FD,wDAAA;EACQ,gDAAA;;ADnER,IAAC;AACD,IAAC;EACC,sBAAA;;AAKJ;EC4PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;EAyB2C,yBAAA;EAA2B,kBAAA;;AAvBtE,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAeJ;EC2PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAgBJ;EC0PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAiBJ;ECyPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,SAAC;AACD,SAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,SAAC;AACD,SAAC;EACC,yBAAA;EACA,qBAAA;;AAkBJ;ECwPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAmBJ;ECuPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,WAAC;AACD,WAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,WAAC;AACD,WAAC;EACC,yBAAA;EACA,qBAAA;;AA2BJ;AACA;EC6CE,kDAAA;EACQ,0CAAA;;ADpCV,cAAe,KAAK,IAAG;AACvB,cAAe,KAAK,IAAG;ECmOnB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EDpOF,yBAAA;;AAEF,cAAe,UAAU;AACzB,cAAe,UAAU,IAAG;AAC5B,cAAe,UAAU,IAAG;EC6NxB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED9NF,yBAAA;;AAUF;ECiNI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EAoCF,mEAAA;EDrPA,kBAAA;ECaA,2FAAA;EACQ,mFAAA;;ADjBV,eAOE,YAAY,UAAU;EC0MpB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EApMF,wDAAA;EACQ,gDAAA;;ADLV;AACA,WAAY,KAAK;EACf,8CAAA;;AAIF;EC+LI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EAoCF,mEAAA;;ADtOF,eAIE,YAAY,UAAU;EC2LpB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EApMF,uDAAA;EACQ,+CAAA;;ADCV,eASE;AATF,eAUE,YAAY,KAAK;EACf,yCAAA;;AAKJ;AACA;AACA;EACE,gBAAA;;AAUF;EACE,6CAAA;EChCA,0FAAA;EACQ,kFAAA;;AD2CV;ECqJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAKF;ECoJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAMF;ECmJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAOF;ECkJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAgBF;ECyII,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADlIJ;EC+HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADjIJ;EC8HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADhIJ;EC6HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD/HJ;EC4HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD9HJ;EC2HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADtHJ;EACE,kBAAA;EC/EA,kDAAA;EACQ,0CAAA;;ADiFV,gBAAgB;AAChB,gBAAgB,OAAO;AACvB,gBAAgB,OAAO;EACrB,6BAAA;EC4GE,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED7GF,qBAAA;;AAUF;ECjGE,iDAAA;EACQ,yCAAA;;AD0GV,cAAe;ECsFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADxFJ,cAAe;ECqFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADvFJ,cAAe;ECoFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADtFJ,WAAY;ECmFR,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADrFJ,cAAe;ECkFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADpFJ,aAAc;ECiFV,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD5EJ;ECyEI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED1EF,qBAAA;EC1HA,yFAAA;EACQ,iFAAA","sourcesContent":["\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n  text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n  @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n  .box-shadow(@shadow);\n\n  // Reset the shadow\n  &:active,\n  &.active {\n    .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n  }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n  #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n  .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners\n  background-repeat: repeat-x;\n  border-color: darken(@btn-color, 14%);\n\n  &:hover,\n  &:focus  {\n    background-color: darken(@btn-color, 12%);\n    background-position: 0 -15px;\n  }\n\n  &:active,\n  &.active {\n    background-color: darken(@btn-color, 12%);\n    border-color: darken(@btn-color, 14%);\n  }\n}\n\n// Common styles\n.btn {\n  // Remove the gradient for the pressed/active state\n  &:active,\n  &.active {\n    background-image: none;\n  }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info    { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger  { .btn-styles(@btn-danger-bg); }\n\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n  .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n  #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n  background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n  #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n  background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n  #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n  .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n  border-radius: @navbar-border-radius;\n  @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n  .box-shadow(@shadow);\n\n  .navbar-nav > .active > a {\n    #gradient > .vertical(@start-color: darken(@navbar-default-bg, 5%); @end-color: darken(@navbar-default-bg, 2%));\n    .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n  }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n  text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n  #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n  .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n\n  .navbar-nav > .active > a {\n    #gradient > .vertical(@start-color: @navbar-inverse-bg; @end-color: lighten(@navbar-inverse-bg, 2.5%));\n    .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n  }\n\n  .navbar-brand,\n  .navbar-nav > li > a {\n    text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n  }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  border-radius: 0;\n}\n\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n  text-shadow: 0 1px 0 rgba(255,255,255,.2);\n  @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n  .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n  #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n  border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success    { .alert-styles(@alert-success-bg); }\n.alert-info       { .alert-styles(@alert-info-bg); }\n.alert-warning    { .alert-styles(@alert-warning-bg); }\n.alert-danger     { .alert-styles(@alert-danger-bg); }\n\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n  #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n  #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar            { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success    { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info       { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning    { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger     { .progress-bar-styles(@progress-bar-danger-bg); }\n\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n  border-radius: @border-radius-base;\n  .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n  text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n  #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n  border-color: darken(@list-group-active-border, 7.5%);\n}\n\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n  .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n  #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading   { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading   { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading   { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading      { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading   { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading    { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n  #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n  border-color: darken(@well-bg, 10%);\n  @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n  .box-shadow(@shadow);\n}\n","//\n// Mixins\n// --------------------------------------------------\n\n\n// Utilities\n// -------------------------\n\n// Clearfix\n// Source: http://nicolasgallagher.com/micro-clearfix-hack/\n//\n// For modern browsers\n// 1. The space content is one way to avoid an Opera bug when the\n//    contenteditable attribute is included anywhere else in the document.\n//    Otherwise it causes space to appear at the top and bottom of elements\n//    that are clearfixed.\n// 2. The use of `table` rather than `block` is only necessary if using\n//    `:before` to contain the top-margins of child elements.\n.clearfix() {\n  &:before,\n  &:after {\n    content: \" \"; // 1\n    display: table; // 2\n  }\n  &:after {\n    clear: both;\n  }\n}\n\n// WebKit-style focus\n.tab-focus() {\n  // Default\n  outline: thin dotted;\n  // WebKit\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\n\n// Center-align a block level element\n.center-block() {\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n// Sizing shortcuts\n.size(@width; @height) {\n  width: @width;\n  height: @height;\n}\n.square(@size) {\n  .size(@size; @size);\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n  &::-moz-placeholder           { color: @color;   // Firefox\n                                  opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526\n  &:-ms-input-placeholder       { color: @color; } // Internet Explorer 10+\n  &::-webkit-input-placeholder  { color: @color; } // Safari and Chrome\n}\n\n// Text overflow\n// Requires inline-block or block for proper styling\n.text-overflow() {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n// CSS image replacement\n//\n// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for\n// mixins being reused as classes with the same name, this doesn't hold up. As\n// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. Note\n// that we cannot chain the mixins together in Less, so they are repeated.\n//\n// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757\n\n// Deprecated as of v3.0.1 (will be removed in v4)\n.hide-text() {\n  font: ~\"0/0\" a;\n  color: transparent;\n  text-shadow: none;\n  background-color: transparent;\n  border: 0;\n}\n// New mixin to use as of v3.0.1\n.text-hide() {\n  .hide-text();\n}\n\n\n\n// CSS3 PROPERTIES\n// --------------------------------------------------\n\n// Single side border-radius\n.border-top-radius(@radius) {\n  border-top-right-radius: @radius;\n   border-top-left-radius: @radius;\n}\n.border-right-radius(@radius) {\n  border-bottom-right-radius: @radius;\n     border-top-right-radius: @radius;\n}\n.border-bottom-radius(@radius) {\n  border-bottom-right-radius: @radius;\n   border-bottom-left-radius: @radius;\n}\n.border-left-radius(@radius) {\n  border-bottom-left-radius: @radius;\n     border-top-left-radius: @radius;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n//   supported browsers that have box shadow capabilities now support the\n//   standard `box-shadow` property.\n.box-shadow(@shadow) {\n  -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n          box-shadow: @shadow;\n}\n\n// Transitions\n.transition(@transition) {\n  -webkit-transition: @transition;\n          transition: @transition;\n}\n.transition-property(@transition-property) {\n  -webkit-transition-property: @transition-property;\n          transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n  -webkit-transition-delay: @transition-delay;\n          transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n  -webkit-transition-duration: @transition-duration;\n          transition-duration: @transition-duration;\n}\n.transition-transform(@transition) {\n  -webkit-transition: -webkit-transform @transition;\n     -moz-transition: -moz-transform @transition;\n       -o-transition: -o-transform @transition;\n          transition: transform @transition;\n}\n\n// Transformations\n.rotate(@degrees) {\n  -webkit-transform: rotate(@degrees);\n      -ms-transform: rotate(@degrees); // IE9 only\n          transform: rotate(@degrees);\n}\n.scale(@ratio; @ratio-y...) {\n  -webkit-transform: scale(@ratio, @ratio-y);\n      -ms-transform: scale(@ratio, @ratio-y); // IE9 only\n          transform: scale(@ratio, @ratio-y);\n}\n.translate(@x; @y) {\n  -webkit-transform: translate(@x, @y);\n      -ms-transform: translate(@x, @y); // IE9 only\n          transform: translate(@x, @y);\n}\n.skew(@x; @y) {\n  -webkit-transform: skew(@x, @y);\n      -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n          transform: skew(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n  -webkit-transform: translate3d(@x, @y, @z);\n          transform: translate3d(@x, @y, @z);\n}\n\n.rotateX(@degrees) {\n  -webkit-transform: rotateX(@degrees);\n      -ms-transform: rotateX(@degrees); // IE9 only\n          transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n  -webkit-transform: rotateY(@degrees);\n      -ms-transform: rotateY(@degrees); // IE9 only\n          transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n  -webkit-perspective: @perspective;\n     -moz-perspective: @perspective;\n          perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n  -webkit-perspective-origin: @perspective;\n     -moz-perspective-origin: @perspective;\n          perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n  -webkit-transform-origin: @origin;\n     -moz-transform-origin: @origin;\n      -ms-transform-origin: @origin; // IE9 only\n          transform-origin: @origin;\n}\n\n// Animations\n.animation(@animation) {\n  -webkit-animation: @animation;\n          animation: @animation;\n}\n.animation-name(@name) {\n  -webkit-animation-name: @name;\n          animation-name: @name;\n}\n.animation-duration(@duration) {\n  -webkit-animation-duration: @duration;\n          animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n  -webkit-animation-timing-function: @timing-function;\n          animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n  -webkit-animation-delay: @delay;\n          animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n  -webkit-animation-iteration-count: @iteration-count;\n          animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n  -webkit-animation-direction: @direction;\n          animation-direction: @direction;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n.backface-visibility(@visibility){\n  -webkit-backface-visibility: @visibility;\n     -moz-backface-visibility: @visibility;\n          backface-visibility: @visibility;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n  -webkit-box-sizing: @boxmodel;\n     -moz-box-sizing: @boxmodel;\n          box-sizing: @boxmodel;\n}\n\n// User select\n// For selecting text on the page\n.user-select(@select) {\n  -webkit-user-select: @select;\n     -moz-user-select: @select;\n      -ms-user-select: @select; // IE10+\n          user-select: @select;\n}\n\n// Resize anything\n.resizable(@direction) {\n  resize: @direction; // Options: horizontal, vertical, both\n  overflow: auto; // Safari fix\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n  -webkit-column-count: @column-count;\n     -moz-column-count: @column-count;\n          column-count: @column-count;\n  -webkit-column-gap: @column-gap;\n     -moz-column-gap: @column-gap;\n          column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n  word-wrap: break-word;\n  -webkit-hyphens: @mode;\n     -moz-hyphens: @mode;\n      -ms-hyphens: @mode; // IE10+\n       -o-hyphens: @mode;\n          hyphens: @mode;\n}\n\n// Opacity\n.opacity(@opacity) {\n  opacity: @opacity;\n  // IE8 filter\n  @opacity-ie: (@opacity * 100);\n  filter: ~\"alpha(opacity=@{opacity-ie})\";\n}\n\n\n\n// GRADIENTS\n// --------------------------------------------------\n\n#gradient {\n\n  // Horizontal gradient, from left to right\n  //\n  // Creates two color stops, start and end, by specifying a color and position for each color stop.\n  // Color stops are not available in IE9 and below.\n  .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n    background-image: -webkit-linear-gradient(left, color-stop(@start-color @start-percent), color-stop(@end-color @end-percent)); // Safari 5.1-6, Chrome 10+\n    background-image:  linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n    background-repeat: repeat-x;\n    filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n  }\n\n  // Vertical gradient, from top to bottom\n  //\n  // Creates two color stops, start and end, by specifying a color and position for each color stop.\n  // Color stops are not available in IE9 and below.\n  .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n    background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent);  // Safari 5.1-6, Chrome 10+\n    background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n    background-repeat: repeat-x;\n    filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n  }\n\n  .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n    background-repeat: repeat-x;\n    background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n    background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n  }\n  .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n    background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n    background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n    background-repeat: no-repeat;\n    filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n  }\n  .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n    background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n    background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n    background-repeat: no-repeat;\n    filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n  }\n  .radial(@inner-color: #555; @outer-color: #333) {\n    background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n    background-image: radial-gradient(circle, @inner-color, @outer-color);\n    background-repeat: no-repeat;\n  }\n  .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n    background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n    background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n  }\n}\n\n// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n.reset-filter() {\n  filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n\n\n\n// Retina images\n//\n// Short retina mixin for setting background-image and -size\n\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n  background-image: url(\"@{file-1x}\");\n\n  @media\n  only screen and (-webkit-min-device-pixel-ratio: 2),\n  only screen and (   min--moz-device-pixel-ratio: 2),\n  only screen and (     -o-min-device-pixel-ratio: 2/1),\n  only screen and (        min-device-pixel-ratio: 2),\n  only screen and (                min-resolution: 192dpi),\n  only screen and (                min-resolution: 2dppx) {\n    background-image: url(\"@{file-2x}\");\n    background-size: @width-1x @height-1x;\n  }\n}\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n.img-responsive(@display: block) {\n  display: @display;\n  max-width: 100%; // Part 1: Set a maximum relative to the parent\n  height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// COMPONENT MIXINS\n// --------------------------------------------------\n\n// Horizontal dividers\n// -------------------------\n// Dividers (basically an hr) within dropdowns and nav lists\n.nav-divider(@color: #e5e5e5) {\n  height: 1px;\n  margin: ((@line-height-computed / 2) - 1) 0;\n  overflow: hidden;\n  background-color: @color;\n}\n\n// Panels\n// -------------------------\n.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) {\n  border-color: @border;\n\n  & > .panel-heading {\n    color: @heading-text-color;\n    background-color: @heading-bg-color;\n    border-color: @heading-border;\n\n    + .panel-collapse .panel-body {\n      border-top-color: @border;\n    }\n  }\n  & > .panel-footer {\n    + .panel-collapse .panel-body {\n      border-bottom-color: @border;\n    }\n  }\n}\n\n// Alerts\n// -------------------------\n.alert-variant(@background; @border; @text-color) {\n  background-color: @background;\n  border-color: @border;\n  color: @text-color;\n\n  hr {\n    border-top-color: darken(@border, 5%);\n  }\n  .alert-link {\n    color: darken(@text-color, 10%);\n  }\n}\n\n// Tables\n// -------------------------\n.table-row-variant(@state; @background) {\n  // Exact selectors below required to override `.table-striped` and prevent\n  // inheritance to nested tables.\n  .table > thead > tr,\n  .table > tbody > tr,\n  .table > tfoot > tr {\n    > td.@{state},\n    > th.@{state},\n    &.@{state} > td,\n    &.@{state} > th {\n      background-color: @background;\n    }\n  }\n\n  // Hover states for `.table-hover`\n  // Note: this is not available for cells or rows within `thead` or `tfoot`.\n  .table-hover > tbody > tr {\n    > td.@{state}:hover,\n    > th.@{state}:hover,\n    &.@{state}:hover > td,\n    &.@{state}:hover > th {\n      background-color: darken(@background, 5%);\n    }\n  }\n}\n\n// List Groups\n// -------------------------\n.list-group-item-variant(@state; @background; @color) {\n  .list-group-item-@{state} {\n    color: @color;\n    background-color: @background;\n\n    a& {\n      color: @color;\n\n      .list-group-item-heading { color: inherit; }\n\n      &:hover,\n      &:focus {\n        color: @color;\n        background-color: darken(@background, 5%);\n      }\n      &.active,\n      &.active:hover,\n      &.active:focus {\n        color: #fff;\n        background-color: @color;\n        border-color: @color;\n      }\n    }\n  }\n}\n\n// Button variants\n// -------------------------\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n.button-variant(@color; @background; @border) {\n  color: @color;\n  background-color: @background;\n  border-color: @border;\n\n  &:hover,\n  &:focus,\n  &:active,\n  &.active,\n  .open .dropdown-toggle& {\n    color: @color;\n    background-color: darken(@background, 8%);\n        border-color: darken(@border, 12%);\n  }\n  &:active,\n  &.active,\n  .open .dropdown-toggle& {\n    background-image: none;\n  }\n  &.disabled,\n  &[disabled],\n  fieldset[disabled] & {\n    &,\n    &:hover,\n    &:focus,\n    &:active,\n    &.active {\n      background-color: @background;\n          border-color: @border;\n    }\n  }\n\n  .badge {\n    color: @background;\n    background-color: @color;\n  }\n}\n\n// Button sizes\n// -------------------------\n.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n  padding: @padding-vertical @padding-horizontal;\n  font-size: @font-size;\n  line-height: @line-height;\n  border-radius: @border-radius;\n}\n\n// Pagination\n// -------------------------\n.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @border-radius) {\n  > li {\n    > a,\n    > span {\n      padding: @padding-vertical @padding-horizontal;\n      font-size: @font-size;\n    }\n    &:first-child {\n      > a,\n      > span {\n        .border-left-radius(@border-radius);\n      }\n    }\n    &:last-child {\n      > a,\n      > span {\n        .border-right-radius(@border-radius);\n      }\n    }\n  }\n}\n\n// Labels\n// -------------------------\n.label-variant(@color) {\n  background-color: @color;\n  &[href] {\n    &:hover,\n    &:focus {\n      background-color: darken(@color, 10%);\n    }\n  }\n}\n\n// Contextual backgrounds\n// -------------------------\n.bg-variant(@color) {\n  background-color: @color;\n  a&:hover {\n    background-color: darken(@color, 10%);\n  }\n}\n\n// Typography\n// -------------------------\n.text-emphasis-variant(@color) {\n  color: @color;\n  a&:hover {\n    color: darken(@color, 10%);\n  }\n}\n\n// Navbar vertical align\n// -------------------------\n// Vertically center elements in the navbar.\n// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin.\n.navbar-vertical-align(@element-height) {\n  margin-top: ((@navbar-height - @element-height) / 2);\n  margin-bottom: ((@navbar-height - @element-height) / 2);\n}\n\n// Progress bars\n// -------------------------\n.progress-bar-variant(@color) {\n  background-color: @color;\n  .progress-striped & {\n    #gradient > .striped();\n  }\n}\n\n// Responsive utilities\n// -------------------------\n// More easily include all the states for responsive-utilities.less.\n.responsive-visibility() {\n  display: block !important;\n  table&  { display: table; }\n  tr&     { display: table-row !important; }\n  th&,\n  td&     { display: table-cell !important; }\n}\n\n.responsive-invisibility() {\n  display: none !important;\n}\n\n\n// Grid System\n// -----------\n\n// Centered container element\n.container-fixed() {\n  margin-right: auto;\n  margin-left: auto;\n  padding-left:  (@grid-gutter-width / 2);\n  padding-right: (@grid-gutter-width / 2);\n  &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n  margin-left:  (@gutter / -2);\n  margin-right: (@gutter / -2);\n  &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n  position: relative;\n  float: left;\n  width: percentage((@columns / @grid-columns));\n  min-height: 1px;\n  padding-left:  (@gutter / 2);\n  padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n  @media (min-width: @screen-xs-min) {\n    margin-left: percentage((@columns / @grid-columns));\n  }\n}\n.make-xs-column-push(@columns) {\n  @media (min-width: @screen-xs-min) {\n    left: percentage((@columns / @grid-columns));\n  }\n}\n.make-xs-column-pull(@columns) {\n  @media (min-width: @screen-xs-min) {\n    right: percentage((@columns / @grid-columns));\n  }\n}\n\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n  position: relative;\n  min-height: 1px;\n  padding-left:  (@gutter / 2);\n  padding-right: (@gutter / 2);\n\n  @media (min-width: @screen-sm-min) {\n    float: left;\n    width: percentage((@columns / @grid-columns));\n  }\n}\n.make-sm-column-offset(@columns) {\n  @media (min-width: @screen-sm-min) {\n    margin-left: percentage((@columns / @grid-columns));\n  }\n}\n.make-sm-column-push(@columns) {\n  @media (min-width: @screen-sm-min) {\n    left: percentage((@columns / @grid-columns));\n  }\n}\n.make-sm-column-pull(@columns) {\n  @media (min-width: @screen-sm-min) {\n    right: percentage((@columns / @grid-columns));\n  }\n}\n\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n  position: relative;\n  min-height: 1px;\n  padding-left:  (@gutter / 2);\n  padding-right: (@gutter / 2);\n\n  @media (min-width: @screen-md-min) {\n    float: left;\n    width: percentage((@columns / @grid-columns));\n  }\n}\n.make-md-column-offset(@columns) {\n  @media (min-width: @screen-md-min) {\n    margin-left: percentage((@columns / @grid-columns));\n  }\n}\n.make-md-column-push(@columns) {\n  @media (min-width: @screen-md-min) {\n    left: percentage((@columns / @grid-columns));\n  }\n}\n.make-md-column-pull(@columns) {\n  @media (min-width: @screen-md-min) {\n    right: percentage((@columns / @grid-columns));\n  }\n}\n\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n  position: relative;\n  min-height: 1px;\n  padding-left:  (@gutter / 2);\n  padding-right: (@gutter / 2);\n\n  @media (min-width: @screen-lg-min) {\n    float: left;\n    width: percentage((@columns / @grid-columns));\n  }\n}\n.make-lg-column-offset(@columns) {\n  @media (min-width: @screen-lg-min) {\n    margin-left: percentage((@columns / @grid-columns));\n  }\n}\n.make-lg-column-push(@columns) {\n  @media (min-width: @screen-lg-min) {\n    left: percentage((@columns / @grid-columns));\n  }\n}\n.make-lg-column-pull(@columns) {\n  @media (min-width: @screen-lg-min) {\n    right: percentage((@columns / @grid-columns));\n  }\n}\n\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n  // Common styles for all sizes of grid columns, widths 1-12\n  .col(@index) when (@index = 1) { // initial\n    @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n    .col((@index + 1), @item);\n  }\n  .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n    @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n    .col((@index + 1), ~\"@{list}, @{item}\");\n  }\n  .col(@index, @list) when (@index > @grid-columns) { // terminal\n    @{list} {\n      position: relative;\n      // Prevent columns from collapsing when empty\n      min-height: 1px;\n      // Inner gutter via padding\n      padding-left:  (@grid-gutter-width / 2);\n      padding-right: (@grid-gutter-width / 2);\n    }\n  }\n  .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n  .col(@index) when (@index = 1) { // initial\n    @item: ~\".col-@{class}-@{index}\";\n    .col((@index + 1), @item);\n  }\n  .col(@index, @list) when (@index =< @grid-columns) { // general\n    @item: ~\".col-@{class}-@{index}\";\n    .col((@index + 1), ~\"@{list}, @{item}\");\n  }\n  .col(@index, @list) when (@index > @grid-columns) { // terminal\n    @{list} {\n      float: left;\n    }\n  }\n  .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n  .col-@{class}-@{index} {\n    width: percentage((@index / @grid-columns));\n  }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) {\n  .col-@{class}-push-@{index} {\n    left: percentage((@index / @grid-columns));\n  }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) {\n  .col-@{class}-pull-@{index} {\n    right: percentage((@index / @grid-columns));\n  }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n  .col-@{class}-offset-@{index} {\n    margin-left: percentage((@index / @grid-columns));\n  }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n  .calc-grid-column(@index, @class, @type);\n  // next iteration\n  .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n  .float-grid-columns(@class);\n  .loop-grid-columns(@grid-columns, @class, width);\n  .loop-grid-columns(@grid-columns, @class, pull);\n  .loop-grid-columns(@grid-columns, @class, push);\n  .loop-grid-columns(@grid-columns, @class, offset);\n}\n\n// Form validation states\n//\n// Used in forms.less to generate the form validation CSS for warnings, errors,\n// and successes.\n\n.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) {\n  // Color the label and help text\n  .help-block,\n  .control-label,\n  .radio,\n  .checkbox,\n  .radio-inline,\n  .checkbox-inline  {\n    color: @text-color;\n  }\n  // Set the border and box shadow on specific inputs to match\n  .form-control {\n    border-color: @border-color;\n    .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work\n    &:focus {\n      border-color: darken(@border-color, 10%);\n      @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%);\n      .box-shadow(@shadow);\n    }\n  }\n  // Set validation states also for addons\n  .input-group-addon {\n    color: @text-color;\n    border-color: @border-color;\n    background-color: @background-color;\n  }\n  // Optional feedback icon\n  .form-control-feedback {\n    color: @text-color;\n  }\n}\n\n// Form control focus state\n//\n// Generate a customized focus state and for any input with the specified color,\n// which defaults to the `@input-focus-border` variable.\n//\n// We highly encourage you to not customize the default value, but instead use\n// this to tweak colors on an as-needed basis. This aesthetic change is based on\n// WebKit's default styles, but applicable to a wider range of browsers. Its\n// usability and accessibility should be taken into account with any change.\n//\n// Example usage: change the default blue border and shadow to white for better\n// contrast against a dark gray background.\n\n.form-control-focus(@color: @input-border-focus) {\n  @color-rgba: rgba(red(@color), green(@color), blue(@color), .6);\n  &:focus {\n    border-color: @color;\n    outline: 0;\n    .box-shadow(~\"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}\");\n  }\n}\n\n// Form control sizing\n//\n// Relative text size, padding, and border-radii changes for form controls. For\n// horizontal sizing, wrap controls in the predefined grid classes. `<select>`\n// element gets special love because it's special, and that's a fact!\n\n.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n  height: @input-height;\n  padding: @padding-vertical @padding-horizontal;\n  font-size: @font-size;\n  line-height: @line-height;\n  border-radius: @border-radius;\n\n  select& {\n    height: @input-height;\n    line-height: @input-height;\n  }\n\n  textarea&,\n  select[multiple]& {\n    height: auto;\n  }\n}\n"]}
\ No newline at end of file
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..ba4bd28ae51616917024b5d4a8d2d20b969a9e31
--- /dev/null
+++ b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v3.1.1 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:active,.btn.active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-color:#357ebd}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0)}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)}
\ No newline at end of file
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/bootstrap.css b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap.css
new file mode 100644
index 0000000000000000000000000000000000000000..7f36651961ed5bc42a712042c6db5493b4ce99e9
--- /dev/null
+++ b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap.css
@@ -0,0 +1,5785 @@
+/*!
+ * Bootstrap v3.1.1 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+/*! normalize.css v3.0.0 | MIT License | git.io/normalize */
+html {
+  font-family: sans-serif;
+  -webkit-text-size-adjust: 100%;
+      -ms-text-size-adjust: 100%;
+}
+body {
+  margin: 0;
+}
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section,
+summary {
+  display: block;
+}
+audio,
+canvas,
+progress,
+video {
+  display: inline-block;
+  vertical-align: baseline;
+}
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+[hidden],
+template {
+  display: none;
+}
+a {
+  background: transparent;
+}
+a:active,
+a:hover {
+  outline: 0;
+}
+abbr[title] {
+  border-bottom: 1px dotted;
+}
+b,
+strong {
+  font-weight: bold;
+}
+dfn {
+  font-style: italic;
+}
+h1 {
+  margin: .67em 0;
+  font-size: 2em;
+}
+mark {
+  color: #000;
+  background: #ff0;
+}
+small {
+  font-size: 80%;
+}
+sub,
+sup {
+  position: relative;
+  font-size: 75%;
+  line-height: 0;
+  vertical-align: baseline;
+}
+sup {
+  top: -.5em;
+}
+sub {
+  bottom: -.25em;
+}
+img {
+  border: 0;
+}
+svg:not(:root) {
+  overflow: hidden;
+}
+figure {
+  margin: 1em 40px;
+}
+hr {
+  height: 0;
+  -moz-box-sizing: content-box;
+       box-sizing: content-box;
+}
+pre {
+  overflow: auto;
+}
+code,
+kbd,
+pre,
+samp {
+  font-family: monospace, monospace;
+  font-size: 1em;
+}
+button,
+input,
+optgroup,
+select,
+textarea {
+  margin: 0;
+  font: inherit;
+  color: inherit;
+}
+button {
+  overflow: visible;
+}
+button,
+select {
+  text-transform: none;
+}
+button,
+html input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+  -webkit-appearance: button;
+  cursor: pointer;
+}
+button[disabled],
+html input[disabled] {
+  cursor: default;
+}
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+  padding: 0;
+  border: 0;
+}
+input {
+  line-height: normal;
+}
+input[type="checkbox"],
+input[type="radio"] {
+  box-sizing: border-box;
+  padding: 0;
+}
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+input[type="search"] {
+  -webkit-box-sizing: content-box;
+     -moz-box-sizing: content-box;
+          box-sizing: content-box;
+  -webkit-appearance: textfield;
+}
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+fieldset {
+  padding: .35em .625em .75em;
+  margin: 0 2px;
+  border: 1px solid #c0c0c0;
+}
+legend {
+  padding: 0;
+  border: 0;
+}
+textarea {
+  overflow: auto;
+}
+optgroup {
+  font-weight: bold;
+}
+table {
+  border-spacing: 0;
+  border-collapse: collapse;
+}
+td,
+th {
+  padding: 0;
+}
+@media print {
+  * {
+    color: #000 !important;
+    text-shadow: none !important;
+    background: transparent !important;
+    box-shadow: none !important;
+  }
+  a,
+  a:visited {
+    text-decoration: underline;
+  }
+  a[href]:after {
+    content: " (" attr(href) ")";
+  }
+  abbr[title]:after {
+    content: " (" attr(title) ")";
+  }
+  a[href^="javascript:"]:after,
+  a[href^="#"]:after {
+    content: "";
+  }
+  pre,
+  blockquote {
+    border: 1px solid #999;
+
+    page-break-inside: avoid;
+  }
+  thead {
+    display: table-header-group;
+  }
+  tr,
+  img {
+    page-break-inside: avoid;
+  }
+  img {
+    max-width: 100% !important;
+  }
+  p,
+  h2,
+  h3 {
+    orphans: 3;
+    widows: 3;
+  }
+  h2,
+  h3 {
+    page-break-after: avoid;
+  }
+  select {
+    background: #fff !important;
+  }
+  .navbar {
+    display: none;
+  }
+  .table td,
+  .table th {
+    background-color: #fff !important;
+  }
+  .btn > .caret,
+  .dropup > .btn > .caret {
+    border-top-color: #000 !important;
+  }
+  .label {
+    border: 1px solid #000;
+  }
+  .table {
+    border-collapse: collapse !important;
+  }
+  .table-bordered th,
+  .table-bordered td {
+    border: 1px solid #ddd !important;
+  }
+}
+* {
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
+}
+*:before,
+*:after {
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
+}
+html {
+  font-size: 62.5%;
+
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+body {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 14px;
+  line-height: 1.42857143;
+  color: #333;
+  background-color: #fff;
+}
+input,
+button,
+select,
+textarea {
+  font-family: inherit;
+  font-size: inherit;
+  line-height: inherit;
+}
+a {
+  color: #428bca;
+  text-decoration: none;
+}
+a:hover,
+a:focus {
+  color: #2a6496;
+  text-decoration: underline;
+}
+a:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+figure {
+  margin: 0;
+}
+img {
+  vertical-align: middle;
+}
+.img-responsive,
+.thumbnail > img,
+.thumbnail a > img,
+.carousel-inner > .item > img,
+.carousel-inner > .item > a > img {
+  display: block;
+  max-width: 100%;
+  height: auto;
+}
+.img-rounded {
+  border-radius: 6px;
+}
+.img-thumbnail {
+  display: inline-block;
+  max-width: 100%;
+  height: auto;
+  padding: 4px;
+  line-height: 1.42857143;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  -webkit-transition: all .2s ease-in-out;
+          transition: all .2s ease-in-out;
+}
+.img-circle {
+  border-radius: 50%;
+}
+hr {
+  margin-top: 20px;
+  margin-bottom: 20px;
+  border: 0;
+  border-top: 1px solid #eee;
+}
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  border: 0;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+.h1,
+.h2,
+.h3,
+.h4,
+.h5,
+.h6 {
+  font-family: inherit;
+  font-weight: 500;
+  line-height: 1.1;
+  color: inherit;
+}
+h1 small,
+h2 small,
+h3 small,
+h4 small,
+h5 small,
+h6 small,
+.h1 small,
+.h2 small,
+.h3 small,
+.h4 small,
+.h5 small,
+.h6 small,
+h1 .small,
+h2 .small,
+h3 .small,
+h4 .small,
+h5 .small,
+h6 .small,
+.h1 .small,
+.h2 .small,
+.h3 .small,
+.h4 .small,
+.h5 .small,
+.h6 .small {
+  font-weight: normal;
+  line-height: 1;
+  color: #999;
+}
+h1,
+.h1,
+h2,
+.h2,
+h3,
+.h3 {
+  margin-top: 20px;
+  margin-bottom: 10px;
+}
+h1 small,
+.h1 small,
+h2 small,
+.h2 small,
+h3 small,
+.h3 small,
+h1 .small,
+.h1 .small,
+h2 .small,
+.h2 .small,
+h3 .small,
+.h3 .small {
+  font-size: 65%;
+}
+h4,
+.h4,
+h5,
+.h5,
+h6,
+.h6 {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+h4 small,
+.h4 small,
+h5 small,
+.h5 small,
+h6 small,
+.h6 small,
+h4 .small,
+.h4 .small,
+h5 .small,
+.h5 .small,
+h6 .small,
+.h6 .small {
+  font-size: 75%;
+}
+h1,
+.h1 {
+  font-size: 36px;
+}
+h2,
+.h2 {
+  font-size: 30px;
+}
+h3,
+.h3 {
+  font-size: 24px;
+}
+h4,
+.h4 {
+  font-size: 18px;
+}
+h5,
+.h5 {
+  font-size: 14px;
+}
+h6,
+.h6 {
+  font-size: 12px;
+}
+p {
+  margin: 0 0 10px;
+}
+.lead {
+  margin-bottom: 20px;
+  font-size: 16px;
+  font-weight: 200;
+  line-height: 1.4;
+}
+@media (min-width: 768px) {
+  .lead {
+    font-size: 21px;
+  }
+}
+small,
+.small {
+  font-size: 85%;
+}
+cite {
+  font-style: normal;
+}
+.text-left {
+  text-align: left;
+}
+.text-right {
+  text-align: right;
+}
+.text-center {
+  text-align: center;
+}
+.text-justify {
+  text-align: justify;
+}
+.text-muted {
+  color: #999;
+}
+.text-primary {
+  color: #428bca;
+}
+a.text-primary:hover {
+  color: #3071a9;
+}
+.text-success {
+  color: #3c763d;
+}
+a.text-success:hover {
+  color: #2b542c;
+}
+.text-info {
+  color: #31708f;
+}
+a.text-info:hover {
+  color: #245269;
+}
+.text-warning {
+  color: #8a6d3b;
+}
+a.text-warning:hover {
+  color: #66512c;
+}
+.text-danger {
+  color: #a94442;
+}
+a.text-danger:hover {
+  color: #843534;
+}
+.bg-primary {
+  color: #fff;
+  background-color: #428bca;
+}
+a.bg-primary:hover {
+  background-color: #3071a9;
+}
+.bg-success {
+  background-color: #dff0d8;
+}
+a.bg-success:hover {
+  background-color: #c1e2b3;
+}
+.bg-info {
+  background-color: #d9edf7;
+}
+a.bg-info:hover {
+  background-color: #afd9ee;
+}
+.bg-warning {
+  background-color: #fcf8e3;
+}
+a.bg-warning:hover {
+  background-color: #f7ecb5;
+}
+.bg-danger {
+  background-color: #f2dede;
+}
+a.bg-danger:hover {
+  background-color: #e4b9b9;
+}
+.page-header {
+  padding-bottom: 9px;
+  margin: 40px 0 20px;
+  border-bottom: 1px solid #eee;
+}
+ul,
+ol {
+  margin-top: 0;
+  margin-bottom: 10px;
+}
+ul ul,
+ol ul,
+ul ol,
+ol ol {
+  margin-bottom: 0;
+}
+.list-unstyled {
+  padding-left: 0;
+  list-style: none;
+}
+.list-inline {
+  padding-left: 0;
+  margin-left: -5px;
+  list-style: none;
+}
+.list-inline > li {
+  display: inline-block;
+  padding-right: 5px;
+  padding-left: 5px;
+}
+dl {
+  margin-top: 0;
+  margin-bottom: 20px;
+}
+dt,
+dd {
+  line-height: 1.42857143;
+}
+dt {
+  font-weight: bold;
+}
+dd {
+  margin-left: 0;
+}
+@media (min-width: 768px) {
+  .dl-horizontal dt {
+    float: left;
+    width: 160px;
+    overflow: hidden;
+    clear: left;
+    text-align: right;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+  .dl-horizontal dd {
+    margin-left: 180px;
+  }
+}
+abbr[title],
+abbr[data-original-title] {
+  cursor: help;
+  border-bottom: 1px dotted #999;
+}
+.initialism {
+  font-size: 90%;
+  text-transform: uppercase;
+}
+blockquote {
+  padding: 10px 20px;
+  margin: 0 0 20px;
+  font-size: 17.5px;
+  border-left: 5px solid #eee;
+}
+blockquote p:last-child,
+blockquote ul:last-child,
+blockquote ol:last-child {
+  margin-bottom: 0;
+}
+blockquote footer,
+blockquote small,
+blockquote .small {
+  display: block;
+  font-size: 80%;
+  line-height: 1.42857143;
+  color: #999;
+}
+blockquote footer:before,
+blockquote small:before,
+blockquote .small:before {
+  content: '\2014 \00A0';
+}
+.blockquote-reverse,
+blockquote.pull-right {
+  padding-right: 15px;
+  padding-left: 0;
+  text-align: right;
+  border-right: 5px solid #eee;
+  border-left: 0;
+}
+.blockquote-reverse footer:before,
+blockquote.pull-right footer:before,
+.blockquote-reverse small:before,
+blockquote.pull-right small:before,
+.blockquote-reverse .small:before,
+blockquote.pull-right .small:before {
+  content: '';
+}
+.blockquote-reverse footer:after,
+blockquote.pull-right footer:after,
+.blockquote-reverse small:after,
+blockquote.pull-right small:after,
+.blockquote-reverse .small:after,
+blockquote.pull-right .small:after {
+  content: '\00A0 \2014';
+}
+blockquote:before,
+blockquote:after {
+  content: "";
+}
+address {
+  margin-bottom: 20px;
+  font-style: normal;
+  line-height: 1.42857143;
+}
+code,
+kbd,
+pre,
+samp {
+  font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
+}
+code {
+  padding: 2px 4px;
+  font-size: 90%;
+  color: #c7254e;
+  white-space: nowrap;
+  background-color: #f9f2f4;
+  border-radius: 4px;
+}
+kbd {
+  padding: 2px 4px;
+  font-size: 90%;
+  color: #fff;
+  background-color: #333;
+  border-radius: 3px;
+  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);
+}
+pre {
+  display: block;
+  padding: 9.5px;
+  margin: 0 0 10px;
+  font-size: 13px;
+  line-height: 1.42857143;
+  color: #333;
+  word-break: break-all;
+  word-wrap: break-word;
+  background-color: #f5f5f5;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+}
+pre code {
+  padding: 0;
+  font-size: inherit;
+  color: inherit;
+  white-space: pre-wrap;
+  background-color: transparent;
+  border-radius: 0;
+}
+.pre-scrollable {
+  max-height: 340px;
+  overflow-y: scroll;
+}
+.container {
+  padding-right: 15px;
+  padding-left: 15px;
+  margin-right: auto;
+  margin-left: auto;
+}
+@media (min-width: 768px) {
+  .container {
+    width: 750px;
+  }
+}
+@media (min-width: 992px) {
+  .container {
+    width: 970px;
+  }
+}
+@media (min-width: 1200px) {
+  .container {
+    width: 1170px;
+  }
+}
+.container-fluid {
+  padding-right: 15px;
+  padding-left: 15px;
+  margin-right: auto;
+  margin-left: auto;
+}
+.row {
+  margin-right: -15px;
+  margin-left: -15px;
+}
+.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
+  position: relative;
+  min-height: 1px;
+  padding-right: 15px;
+  padding-left: 15px;
+}
+.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
+  float: left;
+}
+.col-xs-12 {
+  width: 100%;
+}
+.col-xs-11 {
+  width: 91.66666667%;
+}
+.col-xs-10 {
+  width: 83.33333333%;
+}
+.col-xs-9 {
+  width: 75%;
+}
+.col-xs-8 {
+  width: 66.66666667%;
+}
+.col-xs-7 {
+  width: 58.33333333%;
+}
+.col-xs-6 {
+  width: 50%;
+}
+.col-xs-5 {
+  width: 41.66666667%;
+}
+.col-xs-4 {
+  width: 33.33333333%;
+}
+.col-xs-3 {
+  width: 25%;
+}
+.col-xs-2 {
+  width: 16.66666667%;
+}
+.col-xs-1 {
+  width: 8.33333333%;
+}
+.col-xs-pull-12 {
+  right: 100%;
+}
+.col-xs-pull-11 {
+  right: 91.66666667%;
+}
+.col-xs-pull-10 {
+  right: 83.33333333%;
+}
+.col-xs-pull-9 {
+  right: 75%;
+}
+.col-xs-pull-8 {
+  right: 66.66666667%;
+}
+.col-xs-pull-7 {
+  right: 58.33333333%;
+}
+.col-xs-pull-6 {
+  right: 50%;
+}
+.col-xs-pull-5 {
+  right: 41.66666667%;
+}
+.col-xs-pull-4 {
+  right: 33.33333333%;
+}
+.col-xs-pull-3 {
+  right: 25%;
+}
+.col-xs-pull-2 {
+  right: 16.66666667%;
+}
+.col-xs-pull-1 {
+  right: 8.33333333%;
+}
+.col-xs-pull-0 {
+  right: 0;
+}
+.col-xs-push-12 {
+  left: 100%;
+}
+.col-xs-push-11 {
+  left: 91.66666667%;
+}
+.col-xs-push-10 {
+  left: 83.33333333%;
+}
+.col-xs-push-9 {
+  left: 75%;
+}
+.col-xs-push-8 {
+  left: 66.66666667%;
+}
+.col-xs-push-7 {
+  left: 58.33333333%;
+}
+.col-xs-push-6 {
+  left: 50%;
+}
+.col-xs-push-5 {
+  left: 41.66666667%;
+}
+.col-xs-push-4 {
+  left: 33.33333333%;
+}
+.col-xs-push-3 {
+  left: 25%;
+}
+.col-xs-push-2 {
+  left: 16.66666667%;
+}
+.col-xs-push-1 {
+  left: 8.33333333%;
+}
+.col-xs-push-0 {
+  left: 0;
+}
+.col-xs-offset-12 {
+  margin-left: 100%;
+}
+.col-xs-offset-11 {
+  margin-left: 91.66666667%;
+}
+.col-xs-offset-10 {
+  margin-left: 83.33333333%;
+}
+.col-xs-offset-9 {
+  margin-left: 75%;
+}
+.col-xs-offset-8 {
+  margin-left: 66.66666667%;
+}
+.col-xs-offset-7 {
+  margin-left: 58.33333333%;
+}
+.col-xs-offset-6 {
+  margin-left: 50%;
+}
+.col-xs-offset-5 {
+  margin-left: 41.66666667%;
+}
+.col-xs-offset-4 {
+  margin-left: 33.33333333%;
+}
+.col-xs-offset-3 {
+  margin-left: 25%;
+}
+.col-xs-offset-2 {
+  margin-left: 16.66666667%;
+}
+.col-xs-offset-1 {
+  margin-left: 8.33333333%;
+}
+.col-xs-offset-0 {
+  margin-left: 0;
+}
+@media (min-width: 768px) {
+  .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
+    float: left;
+  }
+  .col-sm-12 {
+    width: 100%;
+  }
+  .col-sm-11 {
+    width: 91.66666667%;
+  }
+  .col-sm-10 {
+    width: 83.33333333%;
+  }
+  .col-sm-9 {
+    width: 75%;
+  }
+  .col-sm-8 {
+    width: 66.66666667%;
+  }
+  .col-sm-7 {
+    width: 58.33333333%;
+  }
+  .col-sm-6 {
+    width: 50%;
+  }
+  .col-sm-5 {
+    width: 41.66666667%;
+  }
+  .col-sm-4 {
+    width: 33.33333333%;
+  }
+  .col-sm-3 {
+    width: 25%;
+  }
+  .col-sm-2 {
+    width: 16.66666667%;
+  }
+  .col-sm-1 {
+    width: 8.33333333%;
+  }
+  .col-sm-pull-12 {
+    right: 100%;
+  }
+  .col-sm-pull-11 {
+    right: 91.66666667%;
+  }
+  .col-sm-pull-10 {
+    right: 83.33333333%;
+  }
+  .col-sm-pull-9 {
+    right: 75%;
+  }
+  .col-sm-pull-8 {
+    right: 66.66666667%;
+  }
+  .col-sm-pull-7 {
+    right: 58.33333333%;
+  }
+  .col-sm-pull-6 {
+    right: 50%;
+  }
+  .col-sm-pull-5 {
+    right: 41.66666667%;
+  }
+  .col-sm-pull-4 {
+    right: 33.33333333%;
+  }
+  .col-sm-pull-3 {
+    right: 25%;
+  }
+  .col-sm-pull-2 {
+    right: 16.66666667%;
+  }
+  .col-sm-pull-1 {
+    right: 8.33333333%;
+  }
+  .col-sm-pull-0 {
+    right: 0;
+  }
+  .col-sm-push-12 {
+    left: 100%;
+  }
+  .col-sm-push-11 {
+    left: 91.66666667%;
+  }
+  .col-sm-push-10 {
+    left: 83.33333333%;
+  }
+  .col-sm-push-9 {
+    left: 75%;
+  }
+  .col-sm-push-8 {
+    left: 66.66666667%;
+  }
+  .col-sm-push-7 {
+    left: 58.33333333%;
+  }
+  .col-sm-push-6 {
+    left: 50%;
+  }
+  .col-sm-push-5 {
+    left: 41.66666667%;
+  }
+  .col-sm-push-4 {
+    left: 33.33333333%;
+  }
+  .col-sm-push-3 {
+    left: 25%;
+  }
+  .col-sm-push-2 {
+    left: 16.66666667%;
+  }
+  .col-sm-push-1 {
+    left: 8.33333333%;
+  }
+  .col-sm-push-0 {
+    left: 0;
+  }
+  .col-sm-offset-12 {
+    margin-left: 100%;
+  }
+  .col-sm-offset-11 {
+    margin-left: 91.66666667%;
+  }
+  .col-sm-offset-10 {
+    margin-left: 83.33333333%;
+  }
+  .col-sm-offset-9 {
+    margin-left: 75%;
+  }
+  .col-sm-offset-8 {
+    margin-left: 66.66666667%;
+  }
+  .col-sm-offset-7 {
+    margin-left: 58.33333333%;
+  }
+  .col-sm-offset-6 {
+    margin-left: 50%;
+  }
+  .col-sm-offset-5 {
+    margin-left: 41.66666667%;
+  }
+  .col-sm-offset-4 {
+    margin-left: 33.33333333%;
+  }
+  .col-sm-offset-3 {
+    margin-left: 25%;
+  }
+  .col-sm-offset-2 {
+    margin-left: 16.66666667%;
+  }
+  .col-sm-offset-1 {
+    margin-left: 8.33333333%;
+  }
+  .col-sm-offset-0 {
+    margin-left: 0;
+  }
+}
+@media (min-width: 992px) {
+  .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
+    float: left;
+  }
+  .col-md-12 {
+    width: 100%;
+  }
+  .col-md-11 {
+    width: 91.66666667%;
+  }
+  .col-md-10 {
+    width: 83.33333333%;
+  }
+  .col-md-9 {
+    width: 75%;
+  }
+  .col-md-8 {
+    width: 66.66666667%;
+  }
+  .col-md-7 {
+    width: 58.33333333%;
+  }
+  .col-md-6 {
+    width: 50%;
+  }
+  .col-md-5 {
+    width: 41.66666667%;
+  }
+  .col-md-4 {
+    width: 33.33333333%;
+  }
+  .col-md-3 {
+    width: 25%;
+  }
+  .col-md-2 {
+    width: 16.66666667%;
+  }
+  .col-md-1 {
+    width: 8.33333333%;
+  }
+  .col-md-pull-12 {
+    right: 100%;
+  }
+  .col-md-pull-11 {
+    right: 91.66666667%;
+  }
+  .col-md-pull-10 {
+    right: 83.33333333%;
+  }
+  .col-md-pull-9 {
+    right: 75%;
+  }
+  .col-md-pull-8 {
+    right: 66.66666667%;
+  }
+  .col-md-pull-7 {
+    right: 58.33333333%;
+  }
+  .col-md-pull-6 {
+    right: 50%;
+  }
+  .col-md-pull-5 {
+    right: 41.66666667%;
+  }
+  .col-md-pull-4 {
+    right: 33.33333333%;
+  }
+  .col-md-pull-3 {
+    right: 25%;
+  }
+  .col-md-pull-2 {
+    right: 16.66666667%;
+  }
+  .col-md-pull-1 {
+    right: 8.33333333%;
+  }
+  .col-md-pull-0 {
+    right: 0;
+  }
+  .col-md-push-12 {
+    left: 100%;
+  }
+  .col-md-push-11 {
+    left: 91.66666667%;
+  }
+  .col-md-push-10 {
+    left: 83.33333333%;
+  }
+  .col-md-push-9 {
+    left: 75%;
+  }
+  .col-md-push-8 {
+    left: 66.66666667%;
+  }
+  .col-md-push-7 {
+    left: 58.33333333%;
+  }
+  .col-md-push-6 {
+    left: 50%;
+  }
+  .col-md-push-5 {
+    left: 41.66666667%;
+  }
+  .col-md-push-4 {
+    left: 33.33333333%;
+  }
+  .col-md-push-3 {
+    left: 25%;
+  }
+  .col-md-push-2 {
+    left: 16.66666667%;
+  }
+  .col-md-push-1 {
+    left: 8.33333333%;
+  }
+  .col-md-push-0 {
+    left: 0;
+  }
+  .col-md-offset-12 {
+    margin-left: 100%;
+  }
+  .col-md-offset-11 {
+    margin-left: 91.66666667%;
+  }
+  .col-md-offset-10 {
+    margin-left: 83.33333333%;
+  }
+  .col-md-offset-9 {
+    margin-left: 75%;
+  }
+  .col-md-offset-8 {
+    margin-left: 66.66666667%;
+  }
+  .col-md-offset-7 {
+    margin-left: 58.33333333%;
+  }
+  .col-md-offset-6 {
+    margin-left: 50%;
+  }
+  .col-md-offset-5 {
+    margin-left: 41.66666667%;
+  }
+  .col-md-offset-4 {
+    margin-left: 33.33333333%;
+  }
+  .col-md-offset-3 {
+    margin-left: 25%;
+  }
+  .col-md-offset-2 {
+    margin-left: 16.66666667%;
+  }
+  .col-md-offset-1 {
+    margin-left: 8.33333333%;
+  }
+  .col-md-offset-0 {
+    margin-left: 0;
+  }
+}
+@media (min-width: 1200px) {
+  .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
+    float: left;
+  }
+  .col-lg-12 {
+    width: 100%;
+  }
+  .col-lg-11 {
+    width: 91.66666667%;
+  }
+  .col-lg-10 {
+    width: 83.33333333%;
+  }
+  .col-lg-9 {
+    width: 75%;
+  }
+  .col-lg-8 {
+    width: 66.66666667%;
+  }
+  .col-lg-7 {
+    width: 58.33333333%;
+  }
+  .col-lg-6 {
+    width: 50%;
+  }
+  .col-lg-5 {
+    width: 41.66666667%;
+  }
+  .col-lg-4 {
+    width: 33.33333333%;
+  }
+  .col-lg-3 {
+    width: 25%;
+  }
+  .col-lg-2 {
+    width: 16.66666667%;
+  }
+  .col-lg-1 {
+    width: 8.33333333%;
+  }
+  .col-lg-pull-12 {
+    right: 100%;
+  }
+  .col-lg-pull-11 {
+    right: 91.66666667%;
+  }
+  .col-lg-pull-10 {
+    right: 83.33333333%;
+  }
+  .col-lg-pull-9 {
+    right: 75%;
+  }
+  .col-lg-pull-8 {
+    right: 66.66666667%;
+  }
+  .col-lg-pull-7 {
+    right: 58.33333333%;
+  }
+  .col-lg-pull-6 {
+    right: 50%;
+  }
+  .col-lg-pull-5 {
+    right: 41.66666667%;
+  }
+  .col-lg-pull-4 {
+    right: 33.33333333%;
+  }
+  .col-lg-pull-3 {
+    right: 25%;
+  }
+  .col-lg-pull-2 {
+    right: 16.66666667%;
+  }
+  .col-lg-pull-1 {
+    right: 8.33333333%;
+  }
+  .col-lg-pull-0 {
+    right: 0;
+  }
+  .col-lg-push-12 {
+    left: 100%;
+  }
+  .col-lg-push-11 {
+    left: 91.66666667%;
+  }
+  .col-lg-push-10 {
+    left: 83.33333333%;
+  }
+  .col-lg-push-9 {
+    left: 75%;
+  }
+  .col-lg-push-8 {
+    left: 66.66666667%;
+  }
+  .col-lg-push-7 {
+    left: 58.33333333%;
+  }
+  .col-lg-push-6 {
+    left: 50%;
+  }
+  .col-lg-push-5 {
+    left: 41.66666667%;
+  }
+  .col-lg-push-4 {
+    left: 33.33333333%;
+  }
+  .col-lg-push-3 {
+    left: 25%;
+  }
+  .col-lg-push-2 {
+    left: 16.66666667%;
+  }
+  .col-lg-push-1 {
+    left: 8.33333333%;
+  }
+  .col-lg-push-0 {
+    left: 0;
+  }
+  .col-lg-offset-12 {
+    margin-left: 100%;
+  }
+  .col-lg-offset-11 {
+    margin-left: 91.66666667%;
+  }
+  .col-lg-offset-10 {
+    margin-left: 83.33333333%;
+  }
+  .col-lg-offset-9 {
+    margin-left: 75%;
+  }
+  .col-lg-offset-8 {
+    margin-left: 66.66666667%;
+  }
+  .col-lg-offset-7 {
+    margin-left: 58.33333333%;
+  }
+  .col-lg-offset-6 {
+    margin-left: 50%;
+  }
+  .col-lg-offset-5 {
+    margin-left: 41.66666667%;
+  }
+  .col-lg-offset-4 {
+    margin-left: 33.33333333%;
+  }
+  .col-lg-offset-3 {
+    margin-left: 25%;
+  }
+  .col-lg-offset-2 {
+    margin-left: 16.66666667%;
+  }
+  .col-lg-offset-1 {
+    margin-left: 8.33333333%;
+  }
+  .col-lg-offset-0 {
+    margin-left: 0;
+  }
+}
+table {
+  max-width: 100%;
+  background-color: transparent;
+}
+th {
+  text-align: left;
+}
+.table {
+  width: 100%;
+  margin-bottom: 20px;
+}
+.table > thead > tr > th,
+.table > tbody > tr > th,
+.table > tfoot > tr > th,
+.table > thead > tr > td,
+.table > tbody > tr > td,
+.table > tfoot > tr > td {
+  padding: 8px;
+  line-height: 1.42857143;
+  vertical-align: top;
+  border-top: 1px solid #ddd;
+}
+.table > thead > tr > th {
+  vertical-align: bottom;
+  border-bottom: 2px solid #ddd;
+}
+.table > caption + thead > tr:first-child > th,
+.table > colgroup + thead > tr:first-child > th,
+.table > thead:first-child > tr:first-child > th,
+.table > caption + thead > tr:first-child > td,
+.table > colgroup + thead > tr:first-child > td,
+.table > thead:first-child > tr:first-child > td {
+  border-top: 0;
+}
+.table > tbody + tbody {
+  border-top: 2px solid #ddd;
+}
+.table .table {
+  background-color: #fff;
+}
+.table-condensed > thead > tr > th,
+.table-condensed > tbody > tr > th,
+.table-condensed > tfoot > tr > th,
+.table-condensed > thead > tr > td,
+.table-condensed > tbody > tr > td,
+.table-condensed > tfoot > tr > td {
+  padding: 5px;
+}
+.table-bordered {
+  border: 1px solid #ddd;
+}
+.table-bordered > thead > tr > th,
+.table-bordered > tbody > tr > th,
+.table-bordered > tfoot > tr > th,
+.table-bordered > thead > tr > td,
+.table-bordered > tbody > tr > td,
+.table-bordered > tfoot > tr > td {
+  border: 1px solid #ddd;
+}
+.table-bordered > thead > tr > th,
+.table-bordered > thead > tr > td {
+  border-bottom-width: 2px;
+}
+.table-striped > tbody > tr:nth-child(odd) > td,
+.table-striped > tbody > tr:nth-child(odd) > th {
+  background-color: #f9f9f9;
+}
+.table-hover > tbody > tr:hover > td,
+.table-hover > tbody > tr:hover > th {
+  background-color: #f5f5f5;
+}
+table col[class*="col-"] {
+  position: static;
+  display: table-column;
+  float: none;
+}
+table td[class*="col-"],
+table th[class*="col-"] {
+  position: static;
+  display: table-cell;
+  float: none;
+}
+.table > thead > tr > td.active,
+.table > tbody > tr > td.active,
+.table > tfoot > tr > td.active,
+.table > thead > tr > th.active,
+.table > tbody > tr > th.active,
+.table > tfoot > tr > th.active,
+.table > thead > tr.active > td,
+.table > tbody > tr.active > td,
+.table > tfoot > tr.active > td,
+.table > thead > tr.active > th,
+.table > tbody > tr.active > th,
+.table > tfoot > tr.active > th {
+  background-color: #f5f5f5;
+}
+.table-hover > tbody > tr > td.active:hover,
+.table-hover > tbody > tr > th.active:hover,
+.table-hover > tbody > tr.active:hover > td,
+.table-hover > tbody > tr.active:hover > th {
+  background-color: #e8e8e8;
+}
+.table > thead > tr > td.success,
+.table > tbody > tr > td.success,
+.table > tfoot > tr > td.success,
+.table > thead > tr > th.success,
+.table > tbody > tr > th.success,
+.table > tfoot > tr > th.success,
+.table > thead > tr.success > td,
+.table > tbody > tr.success > td,
+.table > tfoot > tr.success > td,
+.table > thead > tr.success > th,
+.table > tbody > tr.success > th,
+.table > tfoot > tr.success > th {
+  background-color: #dff0d8;
+}
+.table-hover > tbody > tr > td.success:hover,
+.table-hover > tbody > tr > th.success:hover,
+.table-hover > tbody > tr.success:hover > td,
+.table-hover > tbody > tr.success:hover > th {
+  background-color: #d0e9c6;
+}
+.table > thead > tr > td.info,
+.table > tbody > tr > td.info,
+.table > tfoot > tr > td.info,
+.table > thead > tr > th.info,
+.table > tbody > tr > th.info,
+.table > tfoot > tr > th.info,
+.table > thead > tr.info > td,
+.table > tbody > tr.info > td,
+.table > tfoot > tr.info > td,
+.table > thead > tr.info > th,
+.table > tbody > tr.info > th,
+.table > tfoot > tr.info > th {
+  background-color: #d9edf7;
+}
+.table-hover > tbody > tr > td.info:hover,
+.table-hover > tbody > tr > th.info:hover,
+.table-hover > tbody > tr.info:hover > td,
+.table-hover > tbody > tr.info:hover > th {
+  background-color: #c4e3f3;
+}
+.table > thead > tr > td.warning,
+.table > tbody > tr > td.warning,
+.table > tfoot > tr > td.warning,
+.table > thead > tr > th.warning,
+.table > tbody > tr > th.warning,
+.table > tfoot > tr > th.warning,
+.table > thead > tr.warning > td,
+.table > tbody > tr.warning > td,
+.table > tfoot > tr.warning > td,
+.table > thead > tr.warning > th,
+.table > tbody > tr.warning > th,
+.table > tfoot > tr.warning > th {
+  background-color: #fcf8e3;
+}
+.table-hover > tbody > tr > td.warning:hover,
+.table-hover > tbody > tr > th.warning:hover,
+.table-hover > tbody > tr.warning:hover > td,
+.table-hover > tbody > tr.warning:hover > th {
+  background-color: #faf2cc;
+}
+.table > thead > tr > td.danger,
+.table > tbody > tr > td.danger,
+.table > tfoot > tr > td.danger,
+.table > thead > tr > th.danger,
+.table > tbody > tr > th.danger,
+.table > tfoot > tr > th.danger,
+.table > thead > tr.danger > td,
+.table > tbody > tr.danger > td,
+.table > tfoot > tr.danger > td,
+.table > thead > tr.danger > th,
+.table > tbody > tr.danger > th,
+.table > tfoot > tr.danger > th {
+  background-color: #f2dede;
+}
+.table-hover > tbody > tr > td.danger:hover,
+.table-hover > tbody > tr > th.danger:hover,
+.table-hover > tbody > tr.danger:hover > td,
+.table-hover > tbody > tr.danger:hover > th {
+  background-color: #ebcccc;
+}
+@media (max-width: 767px) {
+  .table-responsive {
+    width: 100%;
+    margin-bottom: 15px;
+    overflow-x: scroll;
+    overflow-y: hidden;
+    -webkit-overflow-scrolling: touch;
+    -ms-overflow-style: -ms-autohiding-scrollbar;
+    border: 1px solid #ddd;
+  }
+  .table-responsive > .table {
+    margin-bottom: 0;
+  }
+  .table-responsive > .table > thead > tr > th,
+  .table-responsive > .table > tbody > tr > th,
+  .table-responsive > .table > tfoot > tr > th,
+  .table-responsive > .table > thead > tr > td,
+  .table-responsive > .table > tbody > tr > td,
+  .table-responsive > .table > tfoot > tr > td {
+    white-space: nowrap;
+  }
+  .table-responsive > .table-bordered {
+    border: 0;
+  }
+  .table-responsive > .table-bordered > thead > tr > th:first-child,
+  .table-responsive > .table-bordered > tbody > tr > th:first-child,
+  .table-responsive > .table-bordered > tfoot > tr > th:first-child,
+  .table-responsive > .table-bordered > thead > tr > td:first-child,
+  .table-responsive > .table-bordered > tbody > tr > td:first-child,
+  .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+    border-left: 0;
+  }
+  .table-responsive > .table-bordered > thead > tr > th:last-child,
+  .table-responsive > .table-bordered > tbody > tr > th:last-child,
+  .table-responsive > .table-bordered > tfoot > tr > th:last-child,
+  .table-responsive > .table-bordered > thead > tr > td:last-child,
+  .table-responsive > .table-bordered > tbody > tr > td:last-child,
+  .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+    border-right: 0;
+  }
+  .table-responsive > .table-bordered > tbody > tr:last-child > th,
+  .table-responsive > .table-bordered > tfoot > tr:last-child > th,
+  .table-responsive > .table-bordered > tbody > tr:last-child > td,
+  .table-responsive > .table-bordered > tfoot > tr:last-child > td {
+    border-bottom: 0;
+  }
+}
+fieldset {
+  min-width: 0;
+  padding: 0;
+  margin: 0;
+  border: 0;
+}
+legend {
+  display: block;
+  width: 100%;
+  padding: 0;
+  margin-bottom: 20px;
+  font-size: 21px;
+  line-height: inherit;
+  color: #333;
+  border: 0;
+  border-bottom: 1px solid #e5e5e5;
+}
+label {
+  display: inline-block;
+  margin-bottom: 5px;
+  font-weight: bold;
+}
+input[type="search"] {
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
+}
+input[type="radio"],
+input[type="checkbox"] {
+  margin: 4px 0 0;
+  margin-top: 1px \9;
+  /* IE8-9 */
+  line-height: normal;
+}
+input[type="file"] {
+  display: block;
+}
+input[type="range"] {
+  display: block;
+  width: 100%;
+}
+select[multiple],
+select[size] {
+  height: auto;
+}
+input[type="file"]:focus,
+input[type="radio"]:focus,
+input[type="checkbox"]:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+output {
+  display: block;
+  padding-top: 7px;
+  font-size: 14px;
+  line-height: 1.42857143;
+  color: #555;
+}
+.form-control {
+  display: block;
+  width: 100%;
+  height: 34px;
+  padding: 6px 12px;
+  font-size: 14px;
+  line-height: 1.42857143;
+  color: #555;
+  background-color: #fff;
+  background-image: none;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+  -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+          transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+}
+.form-control:focus {
+  border-color: #66afe9;
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
+          box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
+}
+.form-control::-moz-placeholder {
+  color: #999;
+  opacity: 1;
+}
+.form-control:-ms-input-placeholder {
+  color: #999;
+}
+.form-control::-webkit-input-placeholder {
+  color: #999;
+}
+.form-control[disabled],
+.form-control[readonly],
+fieldset[disabled] .form-control {
+  cursor: not-allowed;
+  background-color: #eee;
+  opacity: 1;
+}
+textarea.form-control {
+  height: auto;
+}
+input[type="search"] {
+  -webkit-appearance: none;
+}
+input[type="date"] {
+  line-height: 34px;
+}
+.form-group {
+  margin-bottom: 15px;
+}
+.radio,
+.checkbox {
+  display: block;
+  min-height: 20px;
+  padding-left: 20px;
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+.radio label,
+.checkbox label {
+  display: inline;
+  font-weight: normal;
+  cursor: pointer;
+}
+.radio input[type="radio"],
+.radio-inline input[type="radio"],
+.checkbox input[type="checkbox"],
+.checkbox-inline input[type="checkbox"] {
+  float: left;
+  margin-left: -20px;
+}
+.radio + .radio,
+.checkbox + .checkbox {
+  margin-top: -5px;
+}
+.radio-inline,
+.checkbox-inline {
+  display: inline-block;
+  padding-left: 20px;
+  margin-bottom: 0;
+  font-weight: normal;
+  vertical-align: middle;
+  cursor: pointer;
+}
+.radio-inline + .radio-inline,
+.checkbox-inline + .checkbox-inline {
+  margin-top: 0;
+  margin-left: 10px;
+}
+input[type="radio"][disabled],
+input[type="checkbox"][disabled],
+.radio[disabled],
+.radio-inline[disabled],
+.checkbox[disabled],
+.checkbox-inline[disabled],
+fieldset[disabled] input[type="radio"],
+fieldset[disabled] input[type="checkbox"],
+fieldset[disabled] .radio,
+fieldset[disabled] .radio-inline,
+fieldset[disabled] .checkbox,
+fieldset[disabled] .checkbox-inline {
+  cursor: not-allowed;
+}
+.input-sm {
+  height: 30px;
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+select.input-sm {
+  height: 30px;
+  line-height: 30px;
+}
+textarea.input-sm,
+select[multiple].input-sm {
+  height: auto;
+}
+.input-lg {
+  height: 46px;
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+select.input-lg {
+  height: 46px;
+  line-height: 46px;
+}
+textarea.input-lg,
+select[multiple].input-lg {
+  height: auto;
+}
+.has-feedback {
+  position: relative;
+}
+.has-feedback .form-control {
+  padding-right: 42.5px;
+}
+.has-feedback .form-control-feedback {
+  position: absolute;
+  top: 25px;
+  right: 0;
+  display: block;
+  width: 34px;
+  height: 34px;
+  line-height: 34px;
+  text-align: center;
+}
+.has-success .help-block,
+.has-success .control-label,
+.has-success .radio,
+.has-success .checkbox,
+.has-success .radio-inline,
+.has-success .checkbox-inline {
+  color: #3c763d;
+}
+.has-success .form-control {
+  border-color: #3c763d;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+}
+.has-success .form-control:focus {
+  border-color: #2b542c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168;
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168;
+}
+.has-success .input-group-addon {
+  color: #3c763d;
+  background-color: #dff0d8;
+  border-color: #3c763d;
+}
+.has-success .form-control-feedback {
+  color: #3c763d;
+}
+.has-warning .help-block,
+.has-warning .control-label,
+.has-warning .radio,
+.has-warning .checkbox,
+.has-warning .radio-inline,
+.has-warning .checkbox-inline {
+  color: #8a6d3b;
+}
+.has-warning .form-control {
+  border-color: #8a6d3b;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+}
+.has-warning .form-control:focus {
+  border-color: #66512c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b;
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b;
+}
+.has-warning .input-group-addon {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border-color: #8a6d3b;
+}
+.has-warning .form-control-feedback {
+  color: #8a6d3b;
+}
+.has-error .help-block,
+.has-error .control-label,
+.has-error .radio,
+.has-error .checkbox,
+.has-error .radio-inline,
+.has-error .checkbox-inline {
+  color: #a94442;
+}
+.has-error .form-control {
+  border-color: #a94442;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+}
+.has-error .form-control:focus {
+  border-color: #843534;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;
+}
+.has-error .input-group-addon {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #a94442;
+}
+.has-error .form-control-feedback {
+  color: #a94442;
+}
+.form-control-static {
+  margin-bottom: 0;
+}
+.help-block {
+  display: block;
+  margin-top: 5px;
+  margin-bottom: 10px;
+  color: #737373;
+}
+@media (min-width: 768px) {
+  .form-inline .form-group {
+    display: inline-block;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .form-inline .form-control {
+    display: inline-block;
+    width: auto;
+    vertical-align: middle;
+  }
+  .form-inline .input-group > .form-control {
+    width: 100%;
+  }
+  .form-inline .control-label {
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .form-inline .radio,
+  .form-inline .checkbox {
+    display: inline-block;
+    padding-left: 0;
+    margin-top: 0;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .form-inline .radio input[type="radio"],
+  .form-inline .checkbox input[type="checkbox"] {
+    float: none;
+    margin-left: 0;
+  }
+  .form-inline .has-feedback .form-control-feedback {
+    top: 0;
+  }
+}
+.form-horizontal .control-label,
+.form-horizontal .radio,
+.form-horizontal .checkbox,
+.form-horizontal .radio-inline,
+.form-horizontal .checkbox-inline {
+  padding-top: 7px;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+.form-horizontal .radio,
+.form-horizontal .checkbox {
+  min-height: 27px;
+}
+.form-horizontal .form-group {
+  margin-right: -15px;
+  margin-left: -15px;
+}
+.form-horizontal .form-control-static {
+  padding-top: 7px;
+}
+@media (min-width: 768px) {
+  .form-horizontal .control-label {
+    text-align: right;
+  }
+}
+.form-horizontal .has-feedback .form-control-feedback {
+  top: 0;
+  right: 15px;
+}
+.btn {
+  display: inline-block;
+  padding: 6px 12px;
+  margin-bottom: 0;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 1.42857143;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: middle;
+  cursor: pointer;
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+          user-select: none;
+  background-image: none;
+  border: 1px solid transparent;
+  border-radius: 4px;
+}
+.btn:focus,
+.btn:active:focus,
+.btn.active:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+.btn:hover,
+.btn:focus {
+  color: #333;
+  text-decoration: none;
+}
+.btn:active,
+.btn.active {
+  background-image: none;
+  outline: 0;
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+}
+.btn.disabled,
+.btn[disabled],
+fieldset[disabled] .btn {
+  pointer-events: none;
+  cursor: not-allowed;
+  filter: alpha(opacity=65);
+  -webkit-box-shadow: none;
+          box-shadow: none;
+  opacity: .65;
+}
+.btn-default {
+  color: #333;
+  background-color: #fff;
+  border-color: #ccc;
+}
+.btn-default:hover,
+.btn-default:focus,
+.btn-default:active,
+.btn-default.active,
+.open .dropdown-toggle.btn-default {
+  color: #333;
+  background-color: #ebebeb;
+  border-color: #adadad;
+}
+.btn-default:active,
+.btn-default.active,
+.open .dropdown-toggle.btn-default {
+  background-image: none;
+}
+.btn-default.disabled,
+.btn-default[disabled],
+fieldset[disabled] .btn-default,
+.btn-default.disabled:hover,
+.btn-default[disabled]:hover,
+fieldset[disabled] .btn-default:hover,
+.btn-default.disabled:focus,
+.btn-default[disabled]:focus,
+fieldset[disabled] .btn-default:focus,
+.btn-default.disabled:active,
+.btn-default[disabled]:active,
+fieldset[disabled] .btn-default:active,
+.btn-default.disabled.active,
+.btn-default[disabled].active,
+fieldset[disabled] .btn-default.active {
+  background-color: #fff;
+  border-color: #ccc;
+}
+.btn-default .badge {
+  color: #fff;
+  background-color: #333;
+}
+.btn-primary {
+  color: #fff;
+  background-color: #428bca;
+  border-color: #357ebd;
+}
+.btn-primary:hover,
+.btn-primary:focus,
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+  color: #fff;
+  background-color: #3276b1;
+  border-color: #285e8e;
+}
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+  background-image: none;
+}
+.btn-primary.disabled,
+.btn-primary[disabled],
+fieldset[disabled] .btn-primary,
+.btn-primary.disabled:hover,
+.btn-primary[disabled]:hover,
+fieldset[disabled] .btn-primary:hover,
+.btn-primary.disabled:focus,
+.btn-primary[disabled]:focus,
+fieldset[disabled] .btn-primary:focus,
+.btn-primary.disabled:active,
+.btn-primary[disabled]:active,
+fieldset[disabled] .btn-primary:active,
+.btn-primary.disabled.active,
+.btn-primary[disabled].active,
+fieldset[disabled] .btn-primary.active {
+  background-color: #428bca;
+  border-color: #357ebd;
+}
+.btn-primary .badge {
+  color: #428bca;
+  background-color: #fff;
+}
+.btn-success {
+  color: #fff;
+  background-color: #5cb85c;
+  border-color: #4cae4c;
+}
+.btn-success:hover,
+.btn-success:focus,
+.btn-success:active,
+.btn-success.active,
+.open .dropdown-toggle.btn-success {
+  color: #fff;
+  background-color: #47a447;
+  border-color: #398439;
+}
+.btn-success:active,
+.btn-success.active,
+.open .dropdown-toggle.btn-success {
+  background-image: none;
+}
+.btn-success.disabled,
+.btn-success[disabled],
+fieldset[disabled] .btn-success,
+.btn-success.disabled:hover,
+.btn-success[disabled]:hover,
+fieldset[disabled] .btn-success:hover,
+.btn-success.disabled:focus,
+.btn-success[disabled]:focus,
+fieldset[disabled] .btn-success:focus,
+.btn-success.disabled:active,
+.btn-success[disabled]:active,
+fieldset[disabled] .btn-success:active,
+.btn-success.disabled.active,
+.btn-success[disabled].active,
+fieldset[disabled] .btn-success.active {
+  background-color: #5cb85c;
+  border-color: #4cae4c;
+}
+.btn-success .badge {
+  color: #5cb85c;
+  background-color: #fff;
+}
+.btn-info {
+  color: #fff;
+  background-color: #5bc0de;
+  border-color: #46b8da;
+}
+.btn-info:hover,
+.btn-info:focus,
+.btn-info:active,
+.btn-info.active,
+.open .dropdown-toggle.btn-info {
+  color: #fff;
+  background-color: #39b3d7;
+  border-color: #269abc;
+}
+.btn-info:active,
+.btn-info.active,
+.open .dropdown-toggle.btn-info {
+  background-image: none;
+}
+.btn-info.disabled,
+.btn-info[disabled],
+fieldset[disabled] .btn-info,
+.btn-info.disabled:hover,
+.btn-info[disabled]:hover,
+fieldset[disabled] .btn-info:hover,
+.btn-info.disabled:focus,
+.btn-info[disabled]:focus,
+fieldset[disabled] .btn-info:focus,
+.btn-info.disabled:active,
+.btn-info[disabled]:active,
+fieldset[disabled] .btn-info:active,
+.btn-info.disabled.active,
+.btn-info[disabled].active,
+fieldset[disabled] .btn-info.active {
+  background-color: #5bc0de;
+  border-color: #46b8da;
+}
+.btn-info .badge {
+  color: #5bc0de;
+  background-color: #fff;
+}
+.btn-warning {
+  color: #fff;
+  background-color: #f0ad4e;
+  border-color: #eea236;
+}
+.btn-warning:hover,
+.btn-warning:focus,
+.btn-warning:active,
+.btn-warning.active,
+.open .dropdown-toggle.btn-warning {
+  color: #fff;
+  background-color: #ed9c28;
+  border-color: #d58512;
+}
+.btn-warning:active,
+.btn-warning.active,
+.open .dropdown-toggle.btn-warning {
+  background-image: none;
+}
+.btn-warning.disabled,
+.btn-warning[disabled],
+fieldset[disabled] .btn-warning,
+.btn-warning.disabled:hover,
+.btn-warning[disabled]:hover,
+fieldset[disabled] .btn-warning:hover,
+.btn-warning.disabled:focus,
+.btn-warning[disabled]:focus,
+fieldset[disabled] .btn-warning:focus,
+.btn-warning.disabled:active,
+.btn-warning[disabled]:active,
+fieldset[disabled] .btn-warning:active,
+.btn-warning.disabled.active,
+.btn-warning[disabled].active,
+fieldset[disabled] .btn-warning.active {
+  background-color: #f0ad4e;
+  border-color: #eea236;
+}
+.btn-warning .badge {
+  color: #f0ad4e;
+  background-color: #fff;
+}
+.btn-danger {
+  color: #fff;
+  background-color: #d9534f;
+  border-color: #d43f3a;
+}
+.btn-danger:hover,
+.btn-danger:focus,
+.btn-danger:active,
+.btn-danger.active,
+.open .dropdown-toggle.btn-danger {
+  color: #fff;
+  background-color: #d2322d;
+  border-color: #ac2925;
+}
+.btn-danger:active,
+.btn-danger.active,
+.open .dropdown-toggle.btn-danger {
+  background-image: none;
+}
+.btn-danger.disabled,
+.btn-danger[disabled],
+fieldset[disabled] .btn-danger,
+.btn-danger.disabled:hover,
+.btn-danger[disabled]:hover,
+fieldset[disabled] .btn-danger:hover,
+.btn-danger.disabled:focus,
+.btn-danger[disabled]:focus,
+fieldset[disabled] .btn-danger:focus,
+.btn-danger.disabled:active,
+.btn-danger[disabled]:active,
+fieldset[disabled] .btn-danger:active,
+.btn-danger.disabled.active,
+.btn-danger[disabled].active,
+fieldset[disabled] .btn-danger.active {
+  background-color: #d9534f;
+  border-color: #d43f3a;
+}
+.btn-danger .badge {
+  color: #d9534f;
+  background-color: #fff;
+}
+.btn-link {
+  font-weight: normal;
+  color: #428bca;
+  cursor: pointer;
+  border-radius: 0;
+}
+.btn-link,
+.btn-link:active,
+.btn-link[disabled],
+fieldset[disabled] .btn-link {
+  background-color: transparent;
+  -webkit-box-shadow: none;
+          box-shadow: none;
+}
+.btn-link,
+.btn-link:hover,
+.btn-link:focus,
+.btn-link:active {
+  border-color: transparent;
+}
+.btn-link:hover,
+.btn-link:focus {
+  color: #2a6496;
+  text-decoration: underline;
+  background-color: transparent;
+}
+.btn-link[disabled]:hover,
+fieldset[disabled] .btn-link:hover,
+.btn-link[disabled]:focus,
+fieldset[disabled] .btn-link:focus {
+  color: #999;
+  text-decoration: none;
+}
+.btn-lg,
+.btn-group-lg > .btn {
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+.btn-sm,
+.btn-group-sm > .btn {
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+.btn-xs,
+.btn-group-xs > .btn {
+  padding: 1px 5px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+.btn-block {
+  display: block;
+  width: 100%;
+  padding-right: 0;
+  padding-left: 0;
+}
+.btn-block + .btn-block {
+  margin-top: 5px;
+}
+input[type="submit"].btn-block,
+input[type="reset"].btn-block,
+input[type="button"].btn-block {
+  width: 100%;
+}
+.fade {
+  opacity: 0;
+  -webkit-transition: opacity .15s linear;
+          transition: opacity .15s linear;
+}
+.fade.in {
+  opacity: 1;
+}
+.collapse {
+  display: none;
+}
+.collapse.in {
+  display: block;
+}
+.collapsing {
+  position: relative;
+  height: 0;
+  overflow: hidden;
+  -webkit-transition: height .35s ease;
+          transition: height .35s ease;
+}
+@font-face {
+  font-family: 'Glyphicons Halflings';
+
+  src: url('../fonts/glyphicons-halflings-regular.eot');
+  src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
+}
+.glyphicon {
+  position: relative;
+  top: 1px;
+  display: inline-block;
+  font-family: 'Glyphicons Halflings';
+  font-style: normal;
+  font-weight: normal;
+  line-height: 1;
+
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+.glyphicon-asterisk:before {
+  content: "\2a";
+}
+.glyphicon-plus:before {
+  content: "\2b";
+}
+.glyphicon-euro:before {
+  content: "\20ac";
+}
+.glyphicon-minus:before {
+  content: "\2212";
+}
+.glyphicon-cloud:before {
+  content: "\2601";
+}
+.glyphicon-envelope:before {
+  content: "\2709";
+}
+.glyphicon-pencil:before {
+  content: "\270f";
+}
+.glyphicon-glass:before {
+  content: "\e001";
+}
+.glyphicon-music:before {
+  content: "\e002";
+}
+.glyphicon-search:before {
+  content: "\e003";
+}
+.glyphicon-heart:before {
+  content: "\e005";
+}
+.glyphicon-star:before {
+  content: "\e006";
+}
+.glyphicon-star-empty:before {
+  content: "\e007";
+}
+.glyphicon-user:before {
+  content: "\e008";
+}
+.glyphicon-film:before {
+  content: "\e009";
+}
+.glyphicon-th-large:before {
+  content: "\e010";
+}
+.glyphicon-th:before {
+  content: "\e011";
+}
+.glyphicon-th-list:before {
+  content: "\e012";
+}
+.glyphicon-ok:before {
+  content: "\e013";
+}
+.glyphicon-remove:before {
+  content: "\e014";
+}
+.glyphicon-zoom-in:before {
+  content: "\e015";
+}
+.glyphicon-zoom-out:before {
+  content: "\e016";
+}
+.glyphicon-off:before {
+  content: "\e017";
+}
+.glyphicon-signal:before {
+  content: "\e018";
+}
+.glyphicon-cog:before {
+  content: "\e019";
+}
+.glyphicon-trash:before {
+  content: "\e020";
+}
+.glyphicon-home:before {
+  content: "\e021";
+}
+.glyphicon-file:before {
+  content: "\e022";
+}
+.glyphicon-time:before {
+  content: "\e023";
+}
+.glyphicon-road:before {
+  content: "\e024";
+}
+.glyphicon-download-alt:before {
+  content: "\e025";
+}
+.glyphicon-download:before {
+  content: "\e026";
+}
+.glyphicon-upload:before {
+  content: "\e027";
+}
+.glyphicon-inbox:before {
+  content: "\e028";
+}
+.glyphicon-play-circle:before {
+  content: "\e029";
+}
+.glyphicon-repeat:before {
+  content: "\e030";
+}
+.glyphicon-refresh:before {
+  content: "\e031";
+}
+.glyphicon-list-alt:before {
+  content: "\e032";
+}
+.glyphicon-lock:before {
+  content: "\e033";
+}
+.glyphicon-flag:before {
+  content: "\e034";
+}
+.glyphicon-headphones:before {
+  content: "\e035";
+}
+.glyphicon-volume-off:before {
+  content: "\e036";
+}
+.glyphicon-volume-down:before {
+  content: "\e037";
+}
+.glyphicon-volume-up:before {
+  content: "\e038";
+}
+.glyphicon-qrcode:before {
+  content: "\e039";
+}
+.glyphicon-barcode:before {
+  content: "\e040";
+}
+.glyphicon-tag:before {
+  content: "\e041";
+}
+.glyphicon-tags:before {
+  content: "\e042";
+}
+.glyphicon-book:before {
+  content: "\e043";
+}
+.glyphicon-bookmark:before {
+  content: "\e044";
+}
+.glyphicon-print:before {
+  content: "\e045";
+}
+.glyphicon-camera:before {
+  content: "\e046";
+}
+.glyphicon-font:before {
+  content: "\e047";
+}
+.glyphicon-bold:before {
+  content: "\e048";
+}
+.glyphicon-italic:before {
+  content: "\e049";
+}
+.glyphicon-text-height:before {
+  content: "\e050";
+}
+.glyphicon-text-width:before {
+  content: "\e051";
+}
+.glyphicon-align-left:before {
+  content: "\e052";
+}
+.glyphicon-align-center:before {
+  content: "\e053";
+}
+.glyphicon-align-right:before {
+  content: "\e054";
+}
+.glyphicon-align-justify:before {
+  content: "\e055";
+}
+.glyphicon-list:before {
+  content: "\e056";
+}
+.glyphicon-indent-left:before {
+  content: "\e057";
+}
+.glyphicon-indent-right:before {
+  content: "\e058";
+}
+.glyphicon-facetime-video:before {
+  content: "\e059";
+}
+.glyphicon-picture:before {
+  content: "\e060";
+}
+.glyphicon-map-marker:before {
+  content: "\e062";
+}
+.glyphicon-adjust:before {
+  content: "\e063";
+}
+.glyphicon-tint:before {
+  content: "\e064";
+}
+.glyphicon-edit:before {
+  content: "\e065";
+}
+.glyphicon-share:before {
+  content: "\e066";
+}
+.glyphicon-check:before {
+  content: "\e067";
+}
+.glyphicon-move:before {
+  content: "\e068";
+}
+.glyphicon-step-backward:before {
+  content: "\e069";
+}
+.glyphicon-fast-backward:before {
+  content: "\e070";
+}
+.glyphicon-backward:before {
+  content: "\e071";
+}
+.glyphicon-play:before {
+  content: "\e072";
+}
+.glyphicon-pause:before {
+  content: "\e073";
+}
+.glyphicon-stop:before {
+  content: "\e074";
+}
+.glyphicon-forward:before {
+  content: "\e075";
+}
+.glyphicon-fast-forward:before {
+  content: "\e076";
+}
+.glyphicon-step-forward:before {
+  content: "\e077";
+}
+.glyphicon-eject:before {
+  content: "\e078";
+}
+.glyphicon-chevron-left:before {
+  content: "\e079";
+}
+.glyphicon-chevron-right:before {
+  content: "\e080";
+}
+.glyphicon-plus-sign:before {
+  content: "\e081";
+}
+.glyphicon-minus-sign:before {
+  content: "\e082";
+}
+.glyphicon-remove-sign:before {
+  content: "\e083";
+}
+.glyphicon-ok-sign:before {
+  content: "\e084";
+}
+.glyphicon-question-sign:before {
+  content: "\e085";
+}
+.glyphicon-info-sign:before {
+  content: "\e086";
+}
+.glyphicon-screenshot:before {
+  content: "\e087";
+}
+.glyphicon-remove-circle:before {
+  content: "\e088";
+}
+.glyphicon-ok-circle:before {
+  content: "\e089";
+}
+.glyphicon-ban-circle:before {
+  content: "\e090";
+}
+.glyphicon-arrow-left:before {
+  content: "\e091";
+}
+.glyphicon-arrow-right:before {
+  content: "\e092";
+}
+.glyphicon-arrow-up:before {
+  content: "\e093";
+}
+.glyphicon-arrow-down:before {
+  content: "\e094";
+}
+.glyphicon-share-alt:before {
+  content: "\e095";
+}
+.glyphicon-resize-full:before {
+  content: "\e096";
+}
+.glyphicon-resize-small:before {
+  content: "\e097";
+}
+.glyphicon-exclamation-sign:before {
+  content: "\e101";
+}
+.glyphicon-gift:before {
+  content: "\e102";
+}
+.glyphicon-leaf:before {
+  content: "\e103";
+}
+.glyphicon-fire:before {
+  content: "\e104";
+}
+.glyphicon-eye-open:before {
+  content: "\e105";
+}
+.glyphicon-eye-close:before {
+  content: "\e106";
+}
+.glyphicon-warning-sign:before {
+  content: "\e107";
+}
+.glyphicon-plane:before {
+  content: "\e108";
+}
+.glyphicon-calendar:before {
+  content: "\e109";
+}
+.glyphicon-random:before {
+  content: "\e110";
+}
+.glyphicon-comment:before {
+  content: "\e111";
+}
+.glyphicon-magnet:before {
+  content: "\e112";
+}
+.glyphicon-chevron-up:before {
+  content: "\e113";
+}
+.glyphicon-chevron-down:before {
+  content: "\e114";
+}
+.glyphicon-retweet:before {
+  content: "\e115";
+}
+.glyphicon-shopping-cart:before {
+  content: "\e116";
+}
+.glyphicon-folder-close:before {
+  content: "\e117";
+}
+.glyphicon-folder-open:before {
+  content: "\e118";
+}
+.glyphicon-resize-vertical:before {
+  content: "\e119";
+}
+.glyphicon-resize-horizontal:before {
+  content: "\e120";
+}
+.glyphicon-hdd:before {
+  content: "\e121";
+}
+.glyphicon-bullhorn:before {
+  content: "\e122";
+}
+.glyphicon-bell:before {
+  content: "\e123";
+}
+.glyphicon-certificate:before {
+  content: "\e124";
+}
+.glyphicon-thumbs-up:before {
+  content: "\e125";
+}
+.glyphicon-thumbs-down:before {
+  content: "\e126";
+}
+.glyphicon-hand-right:before {
+  content: "\e127";
+}
+.glyphicon-hand-left:before {
+  content: "\e128";
+}
+.glyphicon-hand-up:before {
+  content: "\e129";
+}
+.glyphicon-hand-down:before {
+  content: "\e130";
+}
+.glyphicon-circle-arrow-right:before {
+  content: "\e131";
+}
+.glyphicon-circle-arrow-left:before {
+  content: "\e132";
+}
+.glyphicon-circle-arrow-up:before {
+  content: "\e133";
+}
+.glyphicon-circle-arrow-down:before {
+  content: "\e134";
+}
+.glyphicon-globe:before {
+  content: "\e135";
+}
+.glyphicon-wrench:before {
+  content: "\e136";
+}
+.glyphicon-tasks:before {
+  content: "\e137";
+}
+.glyphicon-filter:before {
+  content: "\e138";
+}
+.glyphicon-briefcase:before {
+  content: "\e139";
+}
+.glyphicon-fullscreen:before {
+  content: "\e140";
+}
+.glyphicon-dashboard:before {
+  content: "\e141";
+}
+.glyphicon-paperclip:before {
+  content: "\e142";
+}
+.glyphicon-heart-empty:before {
+  content: "\e143";
+}
+.glyphicon-link:before {
+  content: "\e144";
+}
+.glyphicon-phone:before {
+  content: "\e145";
+}
+.glyphicon-pushpin:before {
+  content: "\e146";
+}
+.glyphicon-usd:before {
+  content: "\e148";
+}
+.glyphicon-gbp:before {
+  content: "\e149";
+}
+.glyphicon-sort:before {
+  content: "\e150";
+}
+.glyphicon-sort-by-alphabet:before {
+  content: "\e151";
+}
+.glyphicon-sort-by-alphabet-alt:before {
+  content: "\e152";
+}
+.glyphicon-sort-by-order:before {
+  content: "\e153";
+}
+.glyphicon-sort-by-order-alt:before {
+  content: "\e154";
+}
+.glyphicon-sort-by-attributes:before {
+  content: "\e155";
+}
+.glyphicon-sort-by-attributes-alt:before {
+  content: "\e156";
+}
+.glyphicon-unchecked:before {
+  content: "\e157";
+}
+.glyphicon-expand:before {
+  content: "\e158";
+}
+.glyphicon-collapse-down:before {
+  content: "\e159";
+}
+.glyphicon-collapse-up:before {
+  content: "\e160";
+}
+.glyphicon-log-in:before {
+  content: "\e161";
+}
+.glyphicon-flash:before {
+  content: "\e162";
+}
+.glyphicon-log-out:before {
+  content: "\e163";
+}
+.glyphicon-new-window:before {
+  content: "\e164";
+}
+.glyphicon-record:before {
+  content: "\e165";
+}
+.glyphicon-save:before {
+  content: "\e166";
+}
+.glyphicon-open:before {
+  content: "\e167";
+}
+.glyphicon-saved:before {
+  content: "\e168";
+}
+.glyphicon-import:before {
+  content: "\e169";
+}
+.glyphicon-export:before {
+  content: "\e170";
+}
+.glyphicon-send:before {
+  content: "\e171";
+}
+.glyphicon-floppy-disk:before {
+  content: "\e172";
+}
+.glyphicon-floppy-saved:before {
+  content: "\e173";
+}
+.glyphicon-floppy-remove:before {
+  content: "\e174";
+}
+.glyphicon-floppy-save:before {
+  content: "\e175";
+}
+.glyphicon-floppy-open:before {
+  content: "\e176";
+}
+.glyphicon-credit-card:before {
+  content: "\e177";
+}
+.glyphicon-transfer:before {
+  content: "\e178";
+}
+.glyphicon-cutlery:before {
+  content: "\e179";
+}
+.glyphicon-header:before {
+  content: "\e180";
+}
+.glyphicon-compressed:before {
+  content: "\e181";
+}
+.glyphicon-earphone:before {
+  content: "\e182";
+}
+.glyphicon-phone-alt:before {
+  content: "\e183";
+}
+.glyphicon-tower:before {
+  content: "\e184";
+}
+.glyphicon-stats:before {
+  content: "\e185";
+}
+.glyphicon-sd-video:before {
+  content: "\e186";
+}
+.glyphicon-hd-video:before {
+  content: "\e187";
+}
+.glyphicon-subtitles:before {
+  content: "\e188";
+}
+.glyphicon-sound-stereo:before {
+  content: "\e189";
+}
+.glyphicon-sound-dolby:before {
+  content: "\e190";
+}
+.glyphicon-sound-5-1:before {
+  content: "\e191";
+}
+.glyphicon-sound-6-1:before {
+  content: "\e192";
+}
+.glyphicon-sound-7-1:before {
+  content: "\e193";
+}
+.glyphicon-copyright-mark:before {
+  content: "\e194";
+}
+.glyphicon-registration-mark:before {
+  content: "\e195";
+}
+.glyphicon-cloud-download:before {
+  content: "\e197";
+}
+.glyphicon-cloud-upload:before {
+  content: "\e198";
+}
+.glyphicon-tree-conifer:before {
+  content: "\e199";
+}
+.glyphicon-tree-deciduous:before {
+  content: "\e200";
+}
+.caret {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  margin-left: 2px;
+  vertical-align: middle;
+  border-top: 4px solid;
+  border-right: 4px solid transparent;
+  border-left: 4px solid transparent;
+}
+.dropdown {
+  position: relative;
+}
+.dropdown-toggle:focus {
+  outline: 0;
+}
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 1000;
+  display: none;
+  float: left;
+  min-width: 160px;
+  padding: 5px 0;
+  margin: 2px 0 0;
+  font-size: 14px;
+  list-style: none;
+  background-color: #fff;
+  background-clip: padding-box;
+  border: 1px solid #ccc;
+  border: 1px solid rgba(0, 0, 0, .15);
+  border-radius: 4px;
+  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
+          box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
+}
+.dropdown-menu.pull-right {
+  right: 0;
+  left: auto;
+}
+.dropdown-menu .divider {
+  height: 1px;
+  margin: 9px 0;
+  overflow: hidden;
+  background-color: #e5e5e5;
+}
+.dropdown-menu > li > a {
+  display: block;
+  padding: 3px 20px;
+  clear: both;
+  font-weight: normal;
+  line-height: 1.42857143;
+  color: #333;
+  white-space: nowrap;
+}
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  color: #262626;
+  text-decoration: none;
+  background-color: #f5f5f5;
+}
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  color: #fff;
+  text-decoration: none;
+  background-color: #428bca;
+  outline: 0;
+}
+.dropdown-menu > .disabled > a,
+.dropdown-menu > .disabled > a:hover,
+.dropdown-menu > .disabled > a:focus {
+  color: #999;
+}
+.dropdown-menu > .disabled > a:hover,
+.dropdown-menu > .disabled > a:focus {
+  text-decoration: none;
+  cursor: not-allowed;
+  background-color: transparent;
+  background-image: none;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+}
+.open > .dropdown-menu {
+  display: block;
+}
+.open > a {
+  outline: 0;
+}
+.dropdown-menu-right {
+  right: 0;
+  left: auto;
+}
+.dropdown-menu-left {
+  right: auto;
+  left: 0;
+}
+.dropdown-header {
+  display: block;
+  padding: 3px 20px;
+  font-size: 12px;
+  line-height: 1.42857143;
+  color: #999;
+}
+.dropdown-backdrop {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 990;
+}
+.pull-right > .dropdown-menu {
+  right: 0;
+  left: auto;
+}
+.dropup .caret,
+.navbar-fixed-bottom .dropdown .caret {
+  content: "";
+  border-top: 0;
+  border-bottom: 4px solid;
+}
+.dropup .dropdown-menu,
+.navbar-fixed-bottom .dropdown .dropdown-menu {
+  top: auto;
+  bottom: 100%;
+  margin-bottom: 1px;
+}
+@media (min-width: 768px) {
+  .navbar-right .dropdown-menu {
+    right: 0;
+    left: auto;
+  }
+  .navbar-right .dropdown-menu-left {
+    right: auto;
+    left: 0;
+  }
+}
+.btn-group,
+.btn-group-vertical {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+}
+.btn-group > .btn,
+.btn-group-vertical > .btn {
+  position: relative;
+  float: left;
+}
+.btn-group > .btn:hover,
+.btn-group-vertical > .btn:hover,
+.btn-group > .btn:focus,
+.btn-group-vertical > .btn:focus,
+.btn-group > .btn:active,
+.btn-group-vertical > .btn:active,
+.btn-group > .btn.active,
+.btn-group-vertical > .btn.active {
+  z-index: 2;
+}
+.btn-group > .btn:focus,
+.btn-group-vertical > .btn:focus {
+  outline: none;
+}
+.btn-group .btn + .btn,
+.btn-group .btn + .btn-group,
+.btn-group .btn-group + .btn,
+.btn-group .btn-group + .btn-group {
+  margin-left: -1px;
+}
+.btn-toolbar {
+  margin-left: -5px;
+}
+.btn-toolbar .btn-group,
+.btn-toolbar .input-group {
+  float: left;
+}
+.btn-toolbar > .btn,
+.btn-toolbar > .btn-group,
+.btn-toolbar > .input-group {
+  margin-left: 5px;
+}
+.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
+  border-radius: 0;
+}
+.btn-group > .btn:first-child {
+  margin-left: 0;
+}
+.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+.btn-group > .btn:last-child:not(:first-child),
+.btn-group > .dropdown-toggle:not(:first-child) {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+.btn-group > .btn-group {
+  float: left;
+}
+.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+.btn-group > .btn-group:first-child > .btn:last-child,
+.btn-group > .btn-group:first-child > .dropdown-toggle {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+.btn-group > .btn-group:last-child > .btn:first-child {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+.btn-group .dropdown-toggle:active,
+.btn-group.open .dropdown-toggle {
+  outline: 0;
+}
+.btn-group > .btn + .dropdown-toggle {
+  padding-right: 8px;
+  padding-left: 8px;
+}
+.btn-group > .btn-lg + .dropdown-toggle {
+  padding-right: 12px;
+  padding-left: 12px;
+}
+.btn-group.open .dropdown-toggle {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+}
+.btn-group.open .dropdown-toggle.btn-link {
+  -webkit-box-shadow: none;
+          box-shadow: none;
+}
+.btn .caret {
+  margin-left: 0;
+}
+.btn-lg .caret {
+  border-width: 5px 5px 0;
+  border-bottom-width: 0;
+}
+.dropup .btn-lg .caret {
+  border-width: 0 5px 5px;
+}
+.btn-group-vertical > .btn,
+.btn-group-vertical > .btn-group,
+.btn-group-vertical > .btn-group > .btn {
+  display: block;
+  float: none;
+  width: 100%;
+  max-width: 100%;
+}
+.btn-group-vertical > .btn-group > .btn {
+  float: none;
+}
+.btn-group-vertical > .btn + .btn,
+.btn-group-vertical > .btn + .btn-group,
+.btn-group-vertical > .btn-group + .btn,
+.btn-group-vertical > .btn-group + .btn-group {
+  margin-top: -1px;
+  margin-left: 0;
+}
+.btn-group-vertical > .btn:not(:first-child):not(:last-child) {
+  border-radius: 0;
+}
+.btn-group-vertical > .btn:first-child:not(:last-child) {
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+.btn-group-vertical > .btn:last-child:not(:first-child) {
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+  border-bottom-left-radius: 4px;
+}
+.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,
+.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+.btn-group-justified {
+  display: table;
+  width: 100%;
+  table-layout: fixed;
+  border-collapse: separate;
+}
+.btn-group-justified > .btn,
+.btn-group-justified > .btn-group {
+  display: table-cell;
+  float: none;
+  width: 1%;
+}
+.btn-group-justified > .btn-group .btn {
+  width: 100%;
+}
+[data-toggle="buttons"] > .btn > input[type="radio"],
+[data-toggle="buttons"] > .btn > input[type="checkbox"] {
+  display: none;
+}
+.input-group {
+  position: relative;
+  display: table;
+  border-collapse: separate;
+}
+.input-group[class*="col-"] {
+  float: none;
+  padding-right: 0;
+  padding-left: 0;
+}
+.input-group .form-control {
+  position: relative;
+  z-index: 2;
+  float: left;
+  width: 100%;
+  margin-bottom: 0;
+}
+.input-group-lg > .form-control,
+.input-group-lg > .input-group-addon,
+.input-group-lg > .input-group-btn > .btn {
+  height: 46px;
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+select.input-group-lg > .form-control,
+select.input-group-lg > .input-group-addon,
+select.input-group-lg > .input-group-btn > .btn {
+  height: 46px;
+  line-height: 46px;
+}
+textarea.input-group-lg > .form-control,
+textarea.input-group-lg > .input-group-addon,
+textarea.input-group-lg > .input-group-btn > .btn,
+select[multiple].input-group-lg > .form-control,
+select[multiple].input-group-lg > .input-group-addon,
+select[multiple].input-group-lg > .input-group-btn > .btn {
+  height: auto;
+}
+.input-group-sm > .form-control,
+.input-group-sm > .input-group-addon,
+.input-group-sm > .input-group-btn > .btn {
+  height: 30px;
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+select.input-group-sm > .form-control,
+select.input-group-sm > .input-group-addon,
+select.input-group-sm > .input-group-btn > .btn {
+  height: 30px;
+  line-height: 30px;
+}
+textarea.input-group-sm > .form-control,
+textarea.input-group-sm > .input-group-addon,
+textarea.input-group-sm > .input-group-btn > .btn,
+select[multiple].input-group-sm > .form-control,
+select[multiple].input-group-sm > .input-group-addon,
+select[multiple].input-group-sm > .input-group-btn > .btn {
+  height: auto;
+}
+.input-group-addon,
+.input-group-btn,
+.input-group .form-control {
+  display: table-cell;
+}
+.input-group-addon:not(:first-child):not(:last-child),
+.input-group-btn:not(:first-child):not(:last-child),
+.input-group .form-control:not(:first-child):not(:last-child) {
+  border-radius: 0;
+}
+.input-group-addon,
+.input-group-btn {
+  width: 1%;
+  white-space: nowrap;
+  vertical-align: middle;
+}
+.input-group-addon {
+  padding: 6px 12px;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 1;
+  color: #555;
+  text-align: center;
+  background-color: #eee;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+}
+.input-group-addon.input-sm {
+  padding: 5px 10px;
+  font-size: 12px;
+  border-radius: 3px;
+}
+.input-group-addon.input-lg {
+  padding: 10px 16px;
+  font-size: 18px;
+  border-radius: 6px;
+}
+.input-group-addon input[type="radio"],
+.input-group-addon input[type="checkbox"] {
+  margin-top: 0;
+}
+.input-group .form-control:first-child,
+.input-group-addon:first-child,
+.input-group-btn:first-child > .btn,
+.input-group-btn:first-child > .btn-group > .btn,
+.input-group-btn:first-child > .dropdown-toggle,
+.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),
+.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+.input-group-addon:first-child {
+  border-right: 0;
+}
+.input-group .form-control:last-child,
+.input-group-addon:last-child,
+.input-group-btn:last-child > .btn,
+.input-group-btn:last-child > .btn-group > .btn,
+.input-group-btn:last-child > .dropdown-toggle,
+.input-group-btn:first-child > .btn:not(:first-child),
+.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+.input-group-addon:last-child {
+  border-left: 0;
+}
+.input-group-btn {
+  position: relative;
+  font-size: 0;
+  white-space: nowrap;
+}
+.input-group-btn > .btn {
+  position: relative;
+}
+.input-group-btn > .btn + .btn {
+  margin-left: -1px;
+}
+.input-group-btn > .btn:hover,
+.input-group-btn > .btn:focus,
+.input-group-btn > .btn:active {
+  z-index: 2;
+}
+.input-group-btn:first-child > .btn,
+.input-group-btn:first-child > .btn-group {
+  margin-right: -1px;
+}
+.input-group-btn:last-child > .btn,
+.input-group-btn:last-child > .btn-group {
+  margin-left: -1px;
+}
+.nav {
+  padding-left: 0;
+  margin-bottom: 0;
+  list-style: none;
+}
+.nav > li {
+  position: relative;
+  display: block;
+}
+.nav > li > a {
+  position: relative;
+  display: block;
+  padding: 10px 15px;
+}
+.nav > li > a:hover,
+.nav > li > a:focus {
+  text-decoration: none;
+  background-color: #eee;
+}
+.nav > li.disabled > a {
+  color: #999;
+}
+.nav > li.disabled > a:hover,
+.nav > li.disabled > a:focus {
+  color: #999;
+  text-decoration: none;
+  cursor: not-allowed;
+  background-color: transparent;
+}
+.nav .open > a,
+.nav .open > a:hover,
+.nav .open > a:focus {
+  background-color: #eee;
+  border-color: #428bca;
+}
+.nav .nav-divider {
+  height: 1px;
+  margin: 9px 0;
+  overflow: hidden;
+  background-color: #e5e5e5;
+}
+.nav > li > a > img {
+  max-width: none;
+}
+.nav-tabs {
+  border-bottom: 1px solid #ddd;
+}
+.nav-tabs > li {
+  float: left;
+  margin-bottom: -1px;
+}
+.nav-tabs > li > a {
+  margin-right: 2px;
+  line-height: 1.42857143;
+  border: 1px solid transparent;
+  border-radius: 4px 4px 0 0;
+}
+.nav-tabs > li > a:hover {
+  border-color: #eee #eee #ddd;
+}
+.nav-tabs > li.active > a,
+.nav-tabs > li.active > a:hover,
+.nav-tabs > li.active > a:focus {
+  color: #555;
+  cursor: default;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  border-bottom-color: transparent;
+}
+.nav-tabs.nav-justified {
+  width: 100%;
+  border-bottom: 0;
+}
+.nav-tabs.nav-justified > li {
+  float: none;
+}
+.nav-tabs.nav-justified > li > a {
+  margin-bottom: 5px;
+  text-align: center;
+}
+.nav-tabs.nav-justified > .dropdown .dropdown-menu {
+  top: auto;
+  left: auto;
+}
+@media (min-width: 768px) {
+  .nav-tabs.nav-justified > li {
+    display: table-cell;
+    width: 1%;
+  }
+  .nav-tabs.nav-justified > li > a {
+    margin-bottom: 0;
+  }
+}
+.nav-tabs.nav-justified > li > a {
+  margin-right: 0;
+  border-radius: 4px;
+}
+.nav-tabs.nav-justified > .active > a,
+.nav-tabs.nav-justified > .active > a:hover,
+.nav-tabs.nav-justified > .active > a:focus {
+  border: 1px solid #ddd;
+}
+@media (min-width: 768px) {
+  .nav-tabs.nav-justified > li > a {
+    border-bottom: 1px solid #ddd;
+    border-radius: 4px 4px 0 0;
+  }
+  .nav-tabs.nav-justified > .active > a,
+  .nav-tabs.nav-justified > .active > a:hover,
+  .nav-tabs.nav-justified > .active > a:focus {
+    border-bottom-color: #fff;
+  }
+}
+.nav-pills > li {
+  float: left;
+}
+.nav-pills > li > a {
+  border-radius: 4px;
+}
+.nav-pills > li + li {
+  margin-left: 2px;
+}
+.nav-pills > li.active > a,
+.nav-pills > li.active > a:hover,
+.nav-pills > li.active > a:focus {
+  color: #fff;
+  background-color: #428bca;
+}
+.nav-stacked > li {
+  float: none;
+}
+.nav-stacked > li + li {
+  margin-top: 2px;
+  margin-left: 0;
+}
+.nav-justified {
+  width: 100%;
+}
+.nav-justified > li {
+  float: none;
+}
+.nav-justified > li > a {
+  margin-bottom: 5px;
+  text-align: center;
+}
+.nav-justified > .dropdown .dropdown-menu {
+  top: auto;
+  left: auto;
+}
+@media (min-width: 768px) {
+  .nav-justified > li {
+    display: table-cell;
+    width: 1%;
+  }
+  .nav-justified > li > a {
+    margin-bottom: 0;
+  }
+}
+.nav-tabs-justified {
+  border-bottom: 0;
+}
+.nav-tabs-justified > li > a {
+  margin-right: 0;
+  border-radius: 4px;
+}
+.nav-tabs-justified > .active > a,
+.nav-tabs-justified > .active > a:hover,
+.nav-tabs-justified > .active > a:focus {
+  border: 1px solid #ddd;
+}
+@media (min-width: 768px) {
+  .nav-tabs-justified > li > a {
+    border-bottom: 1px solid #ddd;
+    border-radius: 4px 4px 0 0;
+  }
+  .nav-tabs-justified > .active > a,
+  .nav-tabs-justified > .active > a:hover,
+  .nav-tabs-justified > .active > a:focus {
+    border-bottom-color: #fff;
+  }
+}
+.tab-content > .tab-pane {
+  display: none;
+}
+.tab-content > .active {
+  display: block;
+}
+.nav-tabs .dropdown-menu {
+  margin-top: -1px;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+.navbar {
+  position: relative;
+  min-height: 50px;
+  margin-bottom: 20px;
+  border: 1px solid transparent;
+}
+@media (min-width: 768px) {
+  .navbar {
+    border-radius: 4px;
+  }
+}
+@media (min-width: 768px) {
+  .navbar-header {
+    float: left;
+  }
+}
+.navbar-collapse {
+  max-height: 340px;
+  padding-right: 15px;
+  padding-left: 15px;
+  overflow-x: visible;
+  -webkit-overflow-scrolling: touch;
+  border-top: 1px solid transparent;
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);
+}
+.navbar-collapse.in {
+  overflow-y: auto;
+}
+@media (min-width: 768px) {
+  .navbar-collapse {
+    width: auto;
+    border-top: 0;
+    box-shadow: none;
+  }
+  .navbar-collapse.collapse {
+    display: block !important;
+    height: auto !important;
+    padding-bottom: 0;
+    overflow: visible !important;
+  }
+  .navbar-collapse.in {
+    overflow-y: visible;
+  }
+  .navbar-fixed-top .navbar-collapse,
+  .navbar-static-top .navbar-collapse,
+  .navbar-fixed-bottom .navbar-collapse {
+    padding-right: 0;
+    padding-left: 0;
+  }
+}
+.container > .navbar-header,
+.container-fluid > .navbar-header,
+.container > .navbar-collapse,
+.container-fluid > .navbar-collapse {
+  margin-right: -15px;
+  margin-left: -15px;
+}
+@media (min-width: 768px) {
+  .container > .navbar-header,
+  .container-fluid > .navbar-header,
+  .container > .navbar-collapse,
+  .container-fluid > .navbar-collapse {
+    margin-right: 0;
+    margin-left: 0;
+  }
+}
+.navbar-static-top {
+  z-index: 1000;
+  border-width: 0 0 1px;
+}
+@media (min-width: 768px) {
+  .navbar-static-top {
+    border-radius: 0;
+  }
+}
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  position: fixed;
+  right: 0;
+  left: 0;
+  z-index: 1030;
+}
+@media (min-width: 768px) {
+  .navbar-fixed-top,
+  .navbar-fixed-bottom {
+    border-radius: 0;
+  }
+}
+.navbar-fixed-top {
+  top: 0;
+  border-width: 0 0 1px;
+}
+.navbar-fixed-bottom {
+  bottom: 0;
+  margin-bottom: 0;
+  border-width: 1px 0 0;
+}
+.navbar-brand {
+  float: left;
+  height: 50px;
+  padding: 15px 15px;
+  font-size: 18px;
+  line-height: 20px;
+}
+.navbar-brand:hover,
+.navbar-brand:focus {
+  text-decoration: none;
+}
+@media (min-width: 768px) {
+  .navbar > .container .navbar-brand,
+  .navbar > .container-fluid .navbar-brand {
+    margin-left: -15px;
+  }
+}
+.navbar-toggle {
+  position: relative;
+  float: right;
+  padding: 9px 10px;
+  margin-top: 8px;
+  margin-right: 15px;
+  margin-bottom: 8px;
+  background-color: transparent;
+  background-image: none;
+  border: 1px solid transparent;
+  border-radius: 4px;
+}
+.navbar-toggle:focus {
+  outline: none;
+}
+.navbar-toggle .icon-bar {
+  display: block;
+  width: 22px;
+  height: 2px;
+  border-radius: 1px;
+}
+.navbar-toggle .icon-bar + .icon-bar {
+  margin-top: 4px;
+}
+@media (min-width: 768px) {
+  .navbar-toggle {
+    display: none;
+  }
+}
+.navbar-nav {
+  margin: 7.5px -15px;
+}
+.navbar-nav > li > a {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  line-height: 20px;
+}
+@media (max-width: 767px) {
+  .navbar-nav .open .dropdown-menu {
+    position: static;
+    float: none;
+    width: auto;
+    margin-top: 0;
+    background-color: transparent;
+    border: 0;
+    box-shadow: none;
+  }
+  .navbar-nav .open .dropdown-menu > li > a,
+  .navbar-nav .open .dropdown-menu .dropdown-header {
+    padding: 5px 15px 5px 25px;
+  }
+  .navbar-nav .open .dropdown-menu > li > a {
+    line-height: 20px;
+  }
+  .navbar-nav .open .dropdown-menu > li > a:hover,
+  .navbar-nav .open .dropdown-menu > li > a:focus {
+    background-image: none;
+  }
+}
+@media (min-width: 768px) {
+  .navbar-nav {
+    float: left;
+    margin: 0;
+  }
+  .navbar-nav > li {
+    float: left;
+  }
+  .navbar-nav > li > a {
+    padding-top: 15px;
+    padding-bottom: 15px;
+  }
+  .navbar-nav.navbar-right:last-child {
+    margin-right: -15px;
+  }
+}
+@media (min-width: 768px) {
+  .navbar-left {
+    float: left !important;
+  }
+  .navbar-right {
+    float: right !important;
+  }
+}
+.navbar-form {
+  padding: 10px 15px;
+  margin-top: 8px;
+  margin-right: -15px;
+  margin-bottom: 8px;
+  margin-left: -15px;
+  border-top: 1px solid transparent;
+  border-bottom: 1px solid transparent;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);
+}
+@media (min-width: 768px) {
+  .navbar-form .form-group {
+    display: inline-block;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .navbar-form .form-control {
+    display: inline-block;
+    width: auto;
+    vertical-align: middle;
+  }
+  .navbar-form .input-group > .form-control {
+    width: 100%;
+  }
+  .navbar-form .control-label {
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .navbar-form .radio,
+  .navbar-form .checkbox {
+    display: inline-block;
+    padding-left: 0;
+    margin-top: 0;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .navbar-form .radio input[type="radio"],
+  .navbar-form .checkbox input[type="checkbox"] {
+    float: none;
+    margin-left: 0;
+  }
+  .navbar-form .has-feedback .form-control-feedback {
+    top: 0;
+  }
+}
+@media (max-width: 767px) {
+  .navbar-form .form-group {
+    margin-bottom: 5px;
+  }
+}
+@media (min-width: 768px) {
+  .navbar-form {
+    width: auto;
+    padding-top: 0;
+    padding-bottom: 0;
+    margin-right: 0;
+    margin-left: 0;
+    border: 0;
+    -webkit-box-shadow: none;
+            box-shadow: none;
+  }
+  .navbar-form.navbar-right:last-child {
+    margin-right: -15px;
+  }
+}
+.navbar-nav > li > .dropdown-menu {
+  margin-top: 0;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+.navbar-btn {
+  margin-top: 8px;
+  margin-bottom: 8px;
+}
+.navbar-btn.btn-sm {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+.navbar-btn.btn-xs {
+  margin-top: 14px;
+  margin-bottom: 14px;
+}
+.navbar-text {
+  margin-top: 15px;
+  margin-bottom: 15px;
+}
+@media (min-width: 768px) {
+  .navbar-text {
+    float: left;
+    margin-right: 15px;
+    margin-left: 15px;
+  }
+  .navbar-text.navbar-right:last-child {
+    margin-right: 0;
+  }
+}
+.navbar-default {
+  background-color: #f8f8f8;
+  border-color: #e7e7e7;
+}
+.navbar-default .navbar-brand {
+  color: #777;
+}
+.navbar-default .navbar-brand:hover,
+.navbar-default .navbar-brand:focus {
+  color: #5e5e5e;
+  background-color: transparent;
+}
+.navbar-default .navbar-text {
+  color: #777;
+}
+.navbar-default .navbar-nav > li > a {
+  color: #777;
+}
+.navbar-default .navbar-nav > li > a:hover,
+.navbar-default .navbar-nav > li > a:focus {
+  color: #333;
+  background-color: transparent;
+}
+.navbar-default .navbar-nav > .active > a,
+.navbar-default .navbar-nav > .active > a:hover,
+.navbar-default .navbar-nav > .active > a:focus {
+  color: #555;
+  background-color: #e7e7e7;
+}
+.navbar-default .navbar-nav > .disabled > a,
+.navbar-default .navbar-nav > .disabled > a:hover,
+.navbar-default .navbar-nav > .disabled > a:focus {
+  color: #ccc;
+  background-color: transparent;
+}
+.navbar-default .navbar-toggle {
+  border-color: #ddd;
+}
+.navbar-default .navbar-toggle:hover,
+.navbar-default .navbar-toggle:focus {
+  background-color: #ddd;
+}
+.navbar-default .navbar-toggle .icon-bar {
+  background-color: #888;
+}
+.navbar-default .navbar-collapse,
+.navbar-default .navbar-form {
+  border-color: #e7e7e7;
+}
+.navbar-default .navbar-nav > .open > a,
+.navbar-default .navbar-nav > .open > a:hover,
+.navbar-default .navbar-nav > .open > a:focus {
+  color: #555;
+  background-color: #e7e7e7;
+}
+@media (max-width: 767px) {
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a {
+    color: #777;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
+    color: #333;
+    background-color: transparent;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a,
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #555;
+    background-color: #e7e7e7;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+    color: #ccc;
+    background-color: transparent;
+  }
+}
+.navbar-default .navbar-link {
+  color: #777;
+}
+.navbar-default .navbar-link:hover {
+  color: #333;
+}
+.navbar-inverse {
+  background-color: #222;
+  border-color: #080808;
+}
+.navbar-inverse .navbar-brand {
+  color: #999;
+}
+.navbar-inverse .navbar-brand:hover,
+.navbar-inverse .navbar-brand:focus {
+  color: #fff;
+  background-color: transparent;
+}
+.navbar-inverse .navbar-text {
+  color: #999;
+}
+.navbar-inverse .navbar-nav > li > a {
+  color: #999;
+}
+.navbar-inverse .navbar-nav > li > a:hover,
+.navbar-inverse .navbar-nav > li > a:focus {
+  color: #fff;
+  background-color: transparent;
+}
+.navbar-inverse .navbar-nav > .active > a,
+.navbar-inverse .navbar-nav > .active > a:hover,
+.navbar-inverse .navbar-nav > .active > a:focus {
+  color: #fff;
+  background-color: #080808;
+}
+.navbar-inverse .navbar-nav > .disabled > a,
+.navbar-inverse .navbar-nav > .disabled > a:hover,
+.navbar-inverse .navbar-nav > .disabled > a:focus {
+  color: #444;
+  background-color: transparent;
+}
+.navbar-inverse .navbar-toggle {
+  border-color: #333;
+}
+.navbar-inverse .navbar-toggle:hover,
+.navbar-inverse .navbar-toggle:focus {
+  background-color: #333;
+}
+.navbar-inverse .navbar-toggle .icon-bar {
+  background-color: #fff;
+}
+.navbar-inverse .navbar-collapse,
+.navbar-inverse .navbar-form {
+  border-color: #101010;
+}
+.navbar-inverse .navbar-nav > .open > a,
+.navbar-inverse .navbar-nav > .open > a:hover,
+.navbar-inverse .navbar-nav > .open > a:focus {
+  color: #fff;
+  background-color: #080808;
+}
+@media (max-width: 767px) {
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {
+    border-color: #080808;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu .divider {
+    background-color: #080808;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {
+    color: #999;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {
+    color: #fff;
+    background-color: transparent;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #fff;
+    background-color: #080808;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+    color: #444;
+    background-color: transparent;
+  }
+}
+.navbar-inverse .navbar-link {
+  color: #999;
+}
+.navbar-inverse .navbar-link:hover {
+  color: #fff;
+}
+.breadcrumb {
+  padding: 8px 15px;
+  margin-bottom: 20px;
+  list-style: none;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+}
+.breadcrumb > li {
+  display: inline-block;
+}
+.breadcrumb > li + li:before {
+  padding: 0 5px;
+  color: #ccc;
+  content: "/\00a0";
+}
+.breadcrumb > .active {
+  color: #999;
+}
+.pagination {
+  display: inline-block;
+  padding-left: 0;
+  margin: 20px 0;
+  border-radius: 4px;
+}
+.pagination > li {
+  display: inline;
+}
+.pagination > li > a,
+.pagination > li > span {
+  position: relative;
+  float: left;
+  padding: 6px 12px;
+  margin-left: -1px;
+  line-height: 1.42857143;
+  color: #428bca;
+  text-decoration: none;
+  background-color: #fff;
+  border: 1px solid #ddd;
+}
+.pagination > li:first-child > a,
+.pagination > li:first-child > span {
+  margin-left: 0;
+  border-top-left-radius: 4px;
+  border-bottom-left-radius: 4px;
+}
+.pagination > li:last-child > a,
+.pagination > li:last-child > span {
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 4px;
+}
+.pagination > li > a:hover,
+.pagination > li > span:hover,
+.pagination > li > a:focus,
+.pagination > li > span:focus {
+  color: #2a6496;
+  background-color: #eee;
+  border-color: #ddd;
+}
+.pagination > .active > a,
+.pagination > .active > span,
+.pagination > .active > a:hover,
+.pagination > .active > span:hover,
+.pagination > .active > a:focus,
+.pagination > .active > span:focus {
+  z-index: 2;
+  color: #fff;
+  cursor: default;
+  background-color: #428bca;
+  border-color: #428bca;
+}
+.pagination > .disabled > span,
+.pagination > .disabled > span:hover,
+.pagination > .disabled > span:focus,
+.pagination > .disabled > a,
+.pagination > .disabled > a:hover,
+.pagination > .disabled > a:focus {
+  color: #999;
+  cursor: not-allowed;
+  background-color: #fff;
+  border-color: #ddd;
+}
+.pagination-lg > li > a,
+.pagination-lg > li > span {
+  padding: 10px 16px;
+  font-size: 18px;
+}
+.pagination-lg > li:first-child > a,
+.pagination-lg > li:first-child > span {
+  border-top-left-radius: 6px;
+  border-bottom-left-radius: 6px;
+}
+.pagination-lg > li:last-child > a,
+.pagination-lg > li:last-child > span {
+  border-top-right-radius: 6px;
+  border-bottom-right-radius: 6px;
+}
+.pagination-sm > li > a,
+.pagination-sm > li > span {
+  padding: 5px 10px;
+  font-size: 12px;
+}
+.pagination-sm > li:first-child > a,
+.pagination-sm > li:first-child > span {
+  border-top-left-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+.pagination-sm > li:last-child > a,
+.pagination-sm > li:last-child > span {
+  border-top-right-radius: 3px;
+  border-bottom-right-radius: 3px;
+}
+.pager {
+  padding-left: 0;
+  margin: 20px 0;
+  text-align: center;
+  list-style: none;
+}
+.pager li {
+  display: inline;
+}
+.pager li > a,
+.pager li > span {
+  display: inline-block;
+  padding: 5px 14px;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  border-radius: 15px;
+}
+.pager li > a:hover,
+.pager li > a:focus {
+  text-decoration: none;
+  background-color: #eee;
+}
+.pager .next > a,
+.pager .next > span {
+  float: right;
+}
+.pager .previous > a,
+.pager .previous > span {
+  float: left;
+}
+.pager .disabled > a,
+.pager .disabled > a:hover,
+.pager .disabled > a:focus,
+.pager .disabled > span {
+  color: #999;
+  cursor: not-allowed;
+  background-color: #fff;
+}
+.label {
+  display: inline;
+  padding: .2em .6em .3em;
+  font-size: 75%;
+  font-weight: bold;
+  line-height: 1;
+  color: #fff;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: baseline;
+  border-radius: .25em;
+}
+.label[href]:hover,
+.label[href]:focus {
+  color: #fff;
+  text-decoration: none;
+  cursor: pointer;
+}
+.label:empty {
+  display: none;
+}
+.btn .label {
+  position: relative;
+  top: -1px;
+}
+.label-default {
+  background-color: #999;
+}
+.label-default[href]:hover,
+.label-default[href]:focus {
+  background-color: #808080;
+}
+.label-primary {
+  background-color: #428bca;
+}
+.label-primary[href]:hover,
+.label-primary[href]:focus {
+  background-color: #3071a9;
+}
+.label-success {
+  background-color: #5cb85c;
+}
+.label-success[href]:hover,
+.label-success[href]:focus {
+  background-color: #449d44;
+}
+.label-info {
+  background-color: #5bc0de;
+}
+.label-info[href]:hover,
+.label-info[href]:focus {
+  background-color: #31b0d5;
+}
+.label-warning {
+  background-color: #f0ad4e;
+}
+.label-warning[href]:hover,
+.label-warning[href]:focus {
+  background-color: #ec971f;
+}
+.label-danger {
+  background-color: #d9534f;
+}
+.label-danger[href]:hover,
+.label-danger[href]:focus {
+  background-color: #c9302c;
+}
+.badge {
+  display: inline-block;
+  min-width: 10px;
+  padding: 3px 7px;
+  font-size: 12px;
+  font-weight: bold;
+  line-height: 1;
+  color: #fff;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: baseline;
+  background-color: #999;
+  border-radius: 10px;
+}
+.badge:empty {
+  display: none;
+}
+.btn .badge {
+  position: relative;
+  top: -1px;
+}
+.btn-xs .badge {
+  top: 0;
+  padding: 1px 5px;
+}
+a.badge:hover,
+a.badge:focus {
+  color: #fff;
+  text-decoration: none;
+  cursor: pointer;
+}
+a.list-group-item.active > .badge,
+.nav-pills > .active > a > .badge {
+  color: #428bca;
+  background-color: #fff;
+}
+.nav-pills > li > a > .badge {
+  margin-left: 3px;
+}
+.jumbotron {
+  padding: 30px;
+  margin-bottom: 30px;
+  color: inherit;
+  background-color: #eee;
+}
+.jumbotron h1,
+.jumbotron .h1 {
+  color: inherit;
+}
+.jumbotron p {
+  margin-bottom: 15px;
+  font-size: 21px;
+  font-weight: 200;
+}
+.container .jumbotron {
+  border-radius: 6px;
+}
+.jumbotron .container {
+  max-width: 100%;
+}
+@media screen and (min-width: 768px) {
+  .jumbotron {
+    padding-top: 48px;
+    padding-bottom: 48px;
+  }
+  .container .jumbotron {
+    padding-right: 60px;
+    padding-left: 60px;
+  }
+  .jumbotron h1,
+  .jumbotron .h1 {
+    font-size: 63px;
+  }
+}
+.thumbnail {
+  display: block;
+  padding: 4px;
+  margin-bottom: 20px;
+  line-height: 1.42857143;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  -webkit-transition: all .2s ease-in-out;
+          transition: all .2s ease-in-out;
+}
+.thumbnail > img,
+.thumbnail a > img {
+  margin-right: auto;
+  margin-left: auto;
+}
+a.thumbnail:hover,
+a.thumbnail:focus,
+a.thumbnail.active {
+  border-color: #428bca;
+}
+.thumbnail .caption {
+  padding: 9px;
+  color: #333;
+}
+.alert {
+  padding: 15px;
+  margin-bottom: 20px;
+  border: 1px solid transparent;
+  border-radius: 4px;
+}
+.alert h4 {
+  margin-top: 0;
+  color: inherit;
+}
+.alert .alert-link {
+  font-weight: bold;
+}
+.alert > p,
+.alert > ul {
+  margin-bottom: 0;
+}
+.alert > p + p {
+  margin-top: 5px;
+}
+.alert-dismissable {
+  padding-right: 35px;
+}
+.alert-dismissable .close {
+  position: relative;
+  top: -2px;
+  right: -21px;
+  color: inherit;
+}
+.alert-success {
+  color: #3c763d;
+  background-color: #dff0d8;
+  border-color: #d6e9c6;
+}
+.alert-success hr {
+  border-top-color: #c9e2b3;
+}
+.alert-success .alert-link {
+  color: #2b542c;
+}
+.alert-info {
+  color: #31708f;
+  background-color: #d9edf7;
+  border-color: #bce8f1;
+}
+.alert-info hr {
+  border-top-color: #a6e1ec;
+}
+.alert-info .alert-link {
+  color: #245269;
+}
+.alert-warning {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border-color: #faebcc;
+}
+.alert-warning hr {
+  border-top-color: #f7e1b5;
+}
+.alert-warning .alert-link {
+  color: #66512c;
+}
+.alert-danger {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #ebccd1;
+}
+.alert-danger hr {
+  border-top-color: #e4b9c0;
+}
+.alert-danger .alert-link {
+  color: #843534;
+}
+@-webkit-keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
+}
+@keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
+}
+.progress {
+  height: 20px;
+  margin-bottom: 20px;
+  overflow: hidden;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
+          box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
+}
+.progress-bar {
+  float: left;
+  width: 0;
+  height: 100%;
+  font-size: 12px;
+  line-height: 20px;
+  color: #fff;
+  text-align: center;
+  background-color: #428bca;
+  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);
+          box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);
+  -webkit-transition: width .6s ease;
+          transition: width .6s ease;
+}
+.progress-striped .progress-bar {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-size: 40px 40px;
+}
+.progress.active .progress-bar {
+  -webkit-animation: progress-bar-stripes 2s linear infinite;
+          animation: progress-bar-stripes 2s linear infinite;
+}
+.progress-bar-success {
+  background-color: #5cb85c;
+}
+.progress-striped .progress-bar-success {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+}
+.progress-bar-info {
+  background-color: #5bc0de;
+}
+.progress-striped .progress-bar-info {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+}
+.progress-bar-warning {
+  background-color: #f0ad4e;
+}
+.progress-striped .progress-bar-warning {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+}
+.progress-bar-danger {
+  background-color: #d9534f;
+}
+.progress-striped .progress-bar-danger {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+}
+.media,
+.media-body {
+  overflow: hidden;
+  zoom: 1;
+}
+.media,
+.media .media {
+  margin-top: 15px;
+}
+.media:first-child {
+  margin-top: 0;
+}
+.media-object {
+  display: block;
+}
+.media-heading {
+  margin: 0 0 5px;
+}
+.media > .pull-left {
+  margin-right: 10px;
+}
+.media > .pull-right {
+  margin-left: 10px;
+}
+.media-list {
+  padding-left: 0;
+  list-style: none;
+}
+.list-group {
+  padding-left: 0;
+  margin-bottom: 20px;
+}
+.list-group-item {
+  position: relative;
+  display: block;
+  padding: 10px 15px;
+  margin-bottom: -1px;
+  background-color: #fff;
+  border: 1px solid #ddd;
+}
+.list-group-item:first-child {
+  border-top-left-radius: 4px;
+  border-top-right-radius: 4px;
+}
+.list-group-item:last-child {
+  margin-bottom: 0;
+  border-bottom-right-radius: 4px;
+  border-bottom-left-radius: 4px;
+}
+.list-group-item > .badge {
+  float: right;
+}
+.list-group-item > .badge + .badge {
+  margin-right: 5px;
+}
+a.list-group-item {
+  color: #555;
+}
+a.list-group-item .list-group-item-heading {
+  color: #333;
+}
+a.list-group-item:hover,
+a.list-group-item:focus {
+  text-decoration: none;
+  background-color: #f5f5f5;
+}
+a.list-group-item.active,
+a.list-group-item.active:hover,
+a.list-group-item.active:focus {
+  z-index: 2;
+  color: #fff;
+  background-color: #428bca;
+  border-color: #428bca;
+}
+a.list-group-item.active .list-group-item-heading,
+a.list-group-item.active:hover .list-group-item-heading,
+a.list-group-item.active:focus .list-group-item-heading {
+  color: inherit;
+}
+a.list-group-item.active .list-group-item-text,
+a.list-group-item.active:hover .list-group-item-text,
+a.list-group-item.active:focus .list-group-item-text {
+  color: #e1edf7;
+}
+.list-group-item-success {
+  color: #3c763d;
+  background-color: #dff0d8;
+}
+a.list-group-item-success {
+  color: #3c763d;
+}
+a.list-group-item-success .list-group-item-heading {
+  color: inherit;
+}
+a.list-group-item-success:hover,
+a.list-group-item-success:focus {
+  color: #3c763d;
+  background-color: #d0e9c6;
+}
+a.list-group-item-success.active,
+a.list-group-item-success.active:hover,
+a.list-group-item-success.active:focus {
+  color: #fff;
+  background-color: #3c763d;
+  border-color: #3c763d;
+}
+.list-group-item-info {
+  color: #31708f;
+  background-color: #d9edf7;
+}
+a.list-group-item-info {
+  color: #31708f;
+}
+a.list-group-item-info .list-group-item-heading {
+  color: inherit;
+}
+a.list-group-item-info:hover,
+a.list-group-item-info:focus {
+  color: #31708f;
+  background-color: #c4e3f3;
+}
+a.list-group-item-info.active,
+a.list-group-item-info.active:hover,
+a.list-group-item-info.active:focus {
+  color: #fff;
+  background-color: #31708f;
+  border-color: #31708f;
+}
+.list-group-item-warning {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+}
+a.list-group-item-warning {
+  color: #8a6d3b;
+}
+a.list-group-item-warning .list-group-item-heading {
+  color: inherit;
+}
+a.list-group-item-warning:hover,
+a.list-group-item-warning:focus {
+  color: #8a6d3b;
+  background-color: #faf2cc;
+}
+a.list-group-item-warning.active,
+a.list-group-item-warning.active:hover,
+a.list-group-item-warning.active:focus {
+  color: #fff;
+  background-color: #8a6d3b;
+  border-color: #8a6d3b;
+}
+.list-group-item-danger {
+  color: #a94442;
+  background-color: #f2dede;
+}
+a.list-group-item-danger {
+  color: #a94442;
+}
+a.list-group-item-danger .list-group-item-heading {
+  color: inherit;
+}
+a.list-group-item-danger:hover,
+a.list-group-item-danger:focus {
+  color: #a94442;
+  background-color: #ebcccc;
+}
+a.list-group-item-danger.active,
+a.list-group-item-danger.active:hover,
+a.list-group-item-danger.active:focus {
+  color: #fff;
+  background-color: #a94442;
+  border-color: #a94442;
+}
+.list-group-item-heading {
+  margin-top: 0;
+  margin-bottom: 5px;
+}
+.list-group-item-text {
+  margin-bottom: 0;
+  line-height: 1.3;
+}
+.panel {
+  margin-bottom: 20px;
+  background-color: #fff;
+  border: 1px solid transparent;
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
+          box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
+}
+.panel-body {
+  padding: 15px;
+}
+.panel-heading {
+  padding: 10px 15px;
+  border-bottom: 1px solid transparent;
+  border-top-left-radius: 3px;
+  border-top-right-radius: 3px;
+}
+.panel-heading > .dropdown .dropdown-toggle {
+  color: inherit;
+}
+.panel-title {
+  margin-top: 0;
+  margin-bottom: 0;
+  font-size: 16px;
+  color: inherit;
+}
+.panel-title > a {
+  color: inherit;
+}
+.panel-footer {
+  padding: 10px 15px;
+  background-color: #f5f5f5;
+  border-top: 1px solid #ddd;
+  border-bottom-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+.panel > .list-group {
+  margin-bottom: 0;
+}
+.panel > .list-group .list-group-item {
+  border-width: 1px 0;
+  border-radius: 0;
+}
+.panel > .list-group:first-child .list-group-item:first-child {
+  border-top: 0;
+  border-top-left-radius: 3px;
+  border-top-right-radius: 3px;
+}
+.panel > .list-group:last-child .list-group-item:last-child {
+  border-bottom: 0;
+  border-bottom-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+.panel-heading + .list-group .list-group-item:first-child {
+  border-top-width: 0;
+}
+.panel > .table,
+.panel > .table-responsive > .table {
+  margin-bottom: 0;
+}
+.panel > .table:first-child,
+.panel > .table-responsive:first-child > .table:first-child {
+  border-top-left-radius: 3px;
+  border-top-right-radius: 3px;
+}
+.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,
+.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,
+.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,
+.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {
+  border-top-left-radius: 3px;
+}
+.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,
+.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,
+.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,
+.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,
+.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,
+.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {
+  border-top-right-radius: 3px;
+}
+.panel > .table:last-child,
+.panel > .table-responsive:last-child > .table:last-child {
+  border-bottom-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,
+.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,
+.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,
+.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {
+  border-bottom-left-radius: 3px;
+}
+.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,
+.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,
+.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,
+.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,
+.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,
+.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {
+  border-bottom-right-radius: 3px;
+}
+.panel > .panel-body + .table,
+.panel > .panel-body + .table-responsive {
+  border-top: 1px solid #ddd;
+}
+.panel > .table > tbody:first-child > tr:first-child th,
+.panel > .table > tbody:first-child > tr:first-child td {
+  border-top: 0;
+}
+.panel > .table-bordered,
+.panel > .table-responsive > .table-bordered {
+  border: 0;
+}
+.panel > .table-bordered > thead > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,
+.panel > .table-bordered > tbody > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,
+.panel > .table-bordered > tfoot > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,
+.panel > .table-bordered > thead > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,
+.panel > .table-bordered > tbody > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,
+.panel > .table-bordered > tfoot > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+  border-left: 0;
+}
+.panel > .table-bordered > thead > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,
+.panel > .table-bordered > tbody > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,
+.panel > .table-bordered > tfoot > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,
+.panel > .table-bordered > thead > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,
+.panel > .table-bordered > tbody > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,
+.panel > .table-bordered > tfoot > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+  border-right: 0;
+}
+.panel > .table-bordered > thead > tr:first-child > td,
+.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,
+.panel > .table-bordered > tbody > tr:first-child > td,
+.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,
+.panel > .table-bordered > thead > tr:first-child > th,
+.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,
+.panel > .table-bordered > tbody > tr:first-child > th,
+.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {
+  border-bottom: 0;
+}
+.panel > .table-bordered > tbody > tr:last-child > td,
+.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,
+.panel > .table-bordered > tfoot > tr:last-child > td,
+.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,
+.panel > .table-bordered > tbody > tr:last-child > th,
+.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,
+.panel > .table-bordered > tfoot > tr:last-child > th,
+.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {
+  border-bottom: 0;
+}
+.panel > .table-responsive {
+  margin-bottom: 0;
+  border: 0;
+}
+.panel-group {
+  margin-bottom: 20px;
+}
+.panel-group .panel {
+  margin-bottom: 0;
+  overflow: hidden;
+  border-radius: 4px;
+}
+.panel-group .panel + .panel {
+  margin-top: 5px;
+}
+.panel-group .panel-heading {
+  border-bottom: 0;
+}
+.panel-group .panel-heading + .panel-collapse .panel-body {
+  border-top: 1px solid #ddd;
+}
+.panel-group .panel-footer {
+  border-top: 0;
+}
+.panel-group .panel-footer + .panel-collapse .panel-body {
+  border-bottom: 1px solid #ddd;
+}
+.panel-default {
+  border-color: #ddd;
+}
+.panel-default > .panel-heading {
+  color: #333;
+  background-color: #f5f5f5;
+  border-color: #ddd;
+}
+.panel-default > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #ddd;
+}
+.panel-default > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #ddd;
+}
+.panel-primary {
+  border-color: #428bca;
+}
+.panel-primary > .panel-heading {
+  color: #fff;
+  background-color: #428bca;
+  border-color: #428bca;
+}
+.panel-primary > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #428bca;
+}
+.panel-primary > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #428bca;
+}
+.panel-success {
+  border-color: #d6e9c6;
+}
+.panel-success > .panel-heading {
+  color: #3c763d;
+  background-color: #dff0d8;
+  border-color: #d6e9c6;
+}
+.panel-success > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #d6e9c6;
+}
+.panel-success > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #d6e9c6;
+}
+.panel-info {
+  border-color: #bce8f1;
+}
+.panel-info > .panel-heading {
+  color: #31708f;
+  background-color: #d9edf7;
+  border-color: #bce8f1;
+}
+.panel-info > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #bce8f1;
+}
+.panel-info > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #bce8f1;
+}
+.panel-warning {
+  border-color: #faebcc;
+}
+.panel-warning > .panel-heading {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border-color: #faebcc;
+}
+.panel-warning > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #faebcc;
+}
+.panel-warning > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #faebcc;
+}
+.panel-danger {
+  border-color: #ebccd1;
+}
+.panel-danger > .panel-heading {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #ebccd1;
+}
+.panel-danger > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #ebccd1;
+}
+.panel-danger > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #ebccd1;
+}
+.well {
+  min-height: 20px;
+  padding: 19px;
+  margin-bottom: 20px;
+  background-color: #f5f5f5;
+  border: 1px solid #e3e3e3;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
+}
+.well blockquote {
+  border-color: #ddd;
+  border-color: rgba(0, 0, 0, .15);
+}
+.well-lg {
+  padding: 24px;
+  border-radius: 6px;
+}
+.well-sm {
+  padding: 9px;
+  border-radius: 3px;
+}
+.close {
+  float: right;
+  font-size: 21px;
+  font-weight: bold;
+  line-height: 1;
+  color: #000;
+  text-shadow: 0 1px 0 #fff;
+  filter: alpha(opacity=20);
+  opacity: .2;
+}
+.close:hover,
+.close:focus {
+  color: #000;
+  text-decoration: none;
+  cursor: pointer;
+  filter: alpha(opacity=50);
+  opacity: .5;
+}
+button.close {
+  -webkit-appearance: none;
+  padding: 0;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+}
+.modal-open {
+  overflow: hidden;
+}
+.modal {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1050;
+  display: none;
+  overflow: auto;
+  overflow-y: scroll;
+  -webkit-overflow-scrolling: touch;
+  outline: 0;
+}
+.modal.fade .modal-dialog {
+  -webkit-transition: -webkit-transform .3s ease-out;
+     -moz-transition:    -moz-transform .3s ease-out;
+       -o-transition:      -o-transform .3s ease-out;
+          transition:         transform .3s ease-out;
+  -webkit-transform: translate(0, -25%);
+      -ms-transform: translate(0, -25%);
+          transform: translate(0, -25%);
+}
+.modal.in .modal-dialog {
+  -webkit-transform: translate(0, 0);
+      -ms-transform: translate(0, 0);
+          transform: translate(0, 0);
+}
+.modal-dialog {
+  position: relative;
+  width: auto;
+  margin: 10px;
+}
+.modal-content {
+  position: relative;
+  background-color: #fff;
+  background-clip: padding-box;
+  border: 1px solid #999;
+  border: 1px solid rgba(0, 0, 0, .2);
+  border-radius: 6px;
+  outline: none;
+  -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5);
+          box-shadow: 0 3px 9px rgba(0, 0, 0, .5);
+}
+.modal-backdrop {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1040;
+  background-color: #000;
+}
+.modal-backdrop.fade {
+  filter: alpha(opacity=0);
+  opacity: 0;
+}
+.modal-backdrop.in {
+  filter: alpha(opacity=50);
+  opacity: .5;
+}
+.modal-header {
+  min-height: 16.42857143px;
+  padding: 15px;
+  border-bottom: 1px solid #e5e5e5;
+}
+.modal-header .close {
+  margin-top: -2px;
+}
+.modal-title {
+  margin: 0;
+  line-height: 1.42857143;
+}
+.modal-body {
+  position: relative;
+  padding: 20px;
+}
+.modal-footer {
+  padding: 19px 20px 20px;
+  margin-top: 15px;
+  text-align: right;
+  border-top: 1px solid #e5e5e5;
+}
+.modal-footer .btn + .btn {
+  margin-bottom: 0;
+  margin-left: 5px;
+}
+.modal-footer .btn-group .btn + .btn {
+  margin-left: -1px;
+}
+.modal-footer .btn-block + .btn-block {
+  margin-left: 0;
+}
+@media (min-width: 768px) {
+  .modal-dialog {
+    width: 600px;
+    margin: 30px auto;
+  }
+  .modal-content {
+    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
+            box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
+  }
+  .modal-sm {
+    width: 300px;
+  }
+}
+@media (min-width: 992px) {
+  .modal-lg {
+    width: 900px;
+  }
+}
+.tooltip {
+  position: absolute;
+  z-index: 1030;
+  display: block;
+  font-size: 12px;
+  line-height: 1.4;
+  visibility: visible;
+  filter: alpha(opacity=0);
+  opacity: 0;
+}
+.tooltip.in {
+  filter: alpha(opacity=90);
+  opacity: .9;
+}
+.tooltip.top {
+  padding: 5px 0;
+  margin-top: -3px;
+}
+.tooltip.right {
+  padding: 0 5px;
+  margin-left: 3px;
+}
+.tooltip.bottom {
+  padding: 5px 0;
+  margin-top: 3px;
+}
+.tooltip.left {
+  padding: 0 5px;
+  margin-left: -3px;
+}
+.tooltip-inner {
+  max-width: 200px;
+  padding: 3px 8px;
+  color: #fff;
+  text-align: center;
+  text-decoration: none;
+  background-color: #000;
+  border-radius: 4px;
+}
+.tooltip-arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+.tooltip.top .tooltip-arrow {
+  bottom: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-width: 5px 5px 0;
+  border-top-color: #000;
+}
+.tooltip.top-left .tooltip-arrow {
+  bottom: 0;
+  left: 5px;
+  border-width: 5px 5px 0;
+  border-top-color: #000;
+}
+.tooltip.top-right .tooltip-arrow {
+  right: 5px;
+  bottom: 0;
+  border-width: 5px 5px 0;
+  border-top-color: #000;
+}
+.tooltip.right .tooltip-arrow {
+  top: 50%;
+  left: 0;
+  margin-top: -5px;
+  border-width: 5px 5px 5px 0;
+  border-right-color: #000;
+}
+.tooltip.left .tooltip-arrow {
+  top: 50%;
+  right: 0;
+  margin-top: -5px;
+  border-width: 5px 0 5px 5px;
+  border-left-color: #000;
+}
+.tooltip.bottom .tooltip-arrow {
+  top: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-width: 0 5px 5px;
+  border-bottom-color: #000;
+}
+.tooltip.bottom-left .tooltip-arrow {
+  top: 0;
+  left: 5px;
+  border-width: 0 5px 5px;
+  border-bottom-color: #000;
+}
+.tooltip.bottom-right .tooltip-arrow {
+  top: 0;
+  right: 5px;
+  border-width: 0 5px 5px;
+  border-bottom-color: #000;
+}
+.popover {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 1010;
+  display: none;
+  max-width: 276px;
+  padding: 1px;
+  text-align: left;
+  white-space: normal;
+  background-color: #fff;
+  background-clip: padding-box;
+  border: 1px solid #ccc;
+  border: 1px solid rgba(0, 0, 0, .2);
+  border-radius: 6px;
+  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
+          box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
+}
+.popover.top {
+  margin-top: -10px;
+}
+.popover.right {
+  margin-left: 10px;
+}
+.popover.bottom {
+  margin-top: 10px;
+}
+.popover.left {
+  margin-left: -10px;
+}
+.popover-title {
+  padding: 8px 14px;
+  margin: 0;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 18px;
+  background-color: #f7f7f7;
+  border-bottom: 1px solid #ebebeb;
+  border-radius: 5px 5px 0 0;
+}
+.popover-content {
+  padding: 9px 14px;
+}
+.popover > .arrow,
+.popover > .arrow:after {
+  position: absolute;
+  display: block;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+.popover > .arrow {
+  border-width: 11px;
+}
+.popover > .arrow:after {
+  content: "";
+  border-width: 10px;
+}
+.popover.top > .arrow {
+  bottom: -11px;
+  left: 50%;
+  margin-left: -11px;
+  border-top-color: #999;
+  border-top-color: rgba(0, 0, 0, .25);
+  border-bottom-width: 0;
+}
+.popover.top > .arrow:after {
+  bottom: 1px;
+  margin-left: -10px;
+  content: " ";
+  border-top-color: #fff;
+  border-bottom-width: 0;
+}
+.popover.right > .arrow {
+  top: 50%;
+  left: -11px;
+  margin-top: -11px;
+  border-right-color: #999;
+  border-right-color: rgba(0, 0, 0, .25);
+  border-left-width: 0;
+}
+.popover.right > .arrow:after {
+  bottom: -10px;
+  left: 1px;
+  content: " ";
+  border-right-color: #fff;
+  border-left-width: 0;
+}
+.popover.bottom > .arrow {
+  top: -11px;
+  left: 50%;
+  margin-left: -11px;
+  border-top-width: 0;
+  border-bottom-color: #999;
+  border-bottom-color: rgba(0, 0, 0, .25);
+}
+.popover.bottom > .arrow:after {
+  top: 1px;
+  margin-left: -10px;
+  content: " ";
+  border-top-width: 0;
+  border-bottom-color: #fff;
+}
+.popover.left > .arrow {
+  top: 50%;
+  right: -11px;
+  margin-top: -11px;
+  border-right-width: 0;
+  border-left-color: #999;
+  border-left-color: rgba(0, 0, 0, .25);
+}
+.popover.left > .arrow:after {
+  right: 1px;
+  bottom: -10px;
+  content: " ";
+  border-right-width: 0;
+  border-left-color: #fff;
+}
+.carousel {
+  position: relative;
+}
+.carousel-inner {
+  position: relative;
+  width: 100%;
+  overflow: hidden;
+}
+.carousel-inner > .item {
+  position: relative;
+  display: none;
+  -webkit-transition: .6s ease-in-out left;
+          transition: .6s ease-in-out left;
+}
+.carousel-inner > .item > img,
+.carousel-inner > .item > a > img {
+  line-height: 1;
+}
+.carousel-inner > .active,
+.carousel-inner > .next,
+.carousel-inner > .prev {
+  display: block;
+}
+.carousel-inner > .active {
+  left: 0;
+}
+.carousel-inner > .next,
+.carousel-inner > .prev {
+  position: absolute;
+  top: 0;
+  width: 100%;
+}
+.carousel-inner > .next {
+  left: 100%;
+}
+.carousel-inner > .prev {
+  left: -100%;
+}
+.carousel-inner > .next.left,
+.carousel-inner > .prev.right {
+  left: 0;
+}
+.carousel-inner > .active.left {
+  left: -100%;
+}
+.carousel-inner > .active.right {
+  left: 100%;
+}
+.carousel-control {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  width: 15%;
+  font-size: 20px;
+  color: #fff;
+  text-align: center;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
+  filter: alpha(opacity=50);
+  opacity: .5;
+}
+.carousel-control.left {
+  background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, .5) 0%), color-stop(rgba(0, 0, 0, .0001) 100%));
+  background-image:         linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);
+  background-repeat: repeat-x;
+}
+.carousel-control.right {
+  right: 0;
+  left: auto;
+  background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, .0001) 0%), color-stop(rgba(0, 0, 0, .5) 100%));
+  background-image:         linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);
+  background-repeat: repeat-x;
+}
+.carousel-control:hover,
+.carousel-control:focus {
+  color: #fff;
+  text-decoration: none;
+  filter: alpha(opacity=90);
+  outline: none;
+  opacity: .9;
+}
+.carousel-control .icon-prev,
+.carousel-control .icon-next,
+.carousel-control .glyphicon-chevron-left,
+.carousel-control .glyphicon-chevron-right {
+  position: absolute;
+  top: 50%;
+  z-index: 5;
+  display: inline-block;
+}
+.carousel-control .icon-prev,
+.carousel-control .glyphicon-chevron-left {
+  left: 50%;
+}
+.carousel-control .icon-next,
+.carousel-control .glyphicon-chevron-right {
+  right: 50%;
+}
+.carousel-control .icon-prev,
+.carousel-control .icon-next {
+  width: 20px;
+  height: 20px;
+  margin-top: -10px;
+  margin-left: -10px;
+  font-family: serif;
+}
+.carousel-control .icon-prev:before {
+  content: '\2039';
+}
+.carousel-control .icon-next:before {
+  content: '\203a';
+}
+.carousel-indicators {
+  position: absolute;
+  bottom: 10px;
+  left: 50%;
+  z-index: 15;
+  width: 60%;
+  padding-left: 0;
+  margin-left: -30%;
+  text-align: center;
+  list-style: none;
+}
+.carousel-indicators li {
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  margin: 1px;
+  text-indent: -999px;
+  cursor: pointer;
+  background-color: #000 \9;
+  background-color: rgba(0, 0, 0, 0);
+  border: 1px solid #fff;
+  border-radius: 10px;
+}
+.carousel-indicators .active {
+  width: 12px;
+  height: 12px;
+  margin: 0;
+  background-color: #fff;
+}
+.carousel-caption {
+  position: absolute;
+  right: 15%;
+  bottom: 20px;
+  left: 15%;
+  z-index: 10;
+  padding-top: 20px;
+  padding-bottom: 20px;
+  color: #fff;
+  text-align: center;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
+}
+.carousel-caption .btn {
+  text-shadow: none;
+}
+@media screen and (min-width: 768px) {
+  .carousel-control .glyphicon-chevron-left,
+  .carousel-control .glyphicon-chevron-right,
+  .carousel-control .icon-prev,
+  .carousel-control .icon-next {
+    width: 30px;
+    height: 30px;
+    margin-top: -15px;
+    margin-left: -15px;
+    font-size: 30px;
+  }
+  .carousel-caption {
+    right: 20%;
+    left: 20%;
+    padding-bottom: 30px;
+  }
+  .carousel-indicators {
+    bottom: 20px;
+  }
+}
+.clearfix:before,
+.clearfix:after,
+.container:before,
+.container:after,
+.container-fluid:before,
+.container-fluid:after,
+.row:before,
+.row:after,
+.form-horizontal .form-group:before,
+.form-horizontal .form-group:after,
+.btn-toolbar:before,
+.btn-toolbar:after,
+.btn-group-vertical > .btn-group:before,
+.btn-group-vertical > .btn-group:after,
+.nav:before,
+.nav:after,
+.navbar:before,
+.navbar:after,
+.navbar-header:before,
+.navbar-header:after,
+.navbar-collapse:before,
+.navbar-collapse:after,
+.pager:before,
+.pager:after,
+.panel-body:before,
+.panel-body:after,
+.modal-footer:before,
+.modal-footer:after {
+  display: table;
+  content: " ";
+}
+.clearfix:after,
+.container:after,
+.container-fluid:after,
+.row:after,
+.form-horizontal .form-group:after,
+.btn-toolbar:after,
+.btn-group-vertical > .btn-group:after,
+.nav:after,
+.navbar:after,
+.navbar-header:after,
+.navbar-collapse:after,
+.pager:after,
+.panel-body:after,
+.modal-footer:after {
+  clear: both;
+}
+.center-block {
+  display: block;
+  margin-right: auto;
+  margin-left: auto;
+}
+.pull-right {
+  float: right !important;
+}
+.pull-left {
+  float: left !important;
+}
+.hide {
+  display: none !important;
+}
+.show {
+  display: block !important;
+}
+.invisible {
+  visibility: hidden;
+}
+.text-hide {
+  font: 0/0 a;
+  color: transparent;
+  text-shadow: none;
+  background-color: transparent;
+  border: 0;
+}
+.hidden {
+  display: none !important;
+  visibility: hidden !important;
+}
+.affix {
+  position: fixed;
+}
+@-ms-viewport {
+  width: device-width;
+}
+.visible-xs,
+.visible-sm,
+.visible-md,
+.visible-lg {
+  display: none !important;
+}
+@media (max-width: 767px) {
+  .visible-xs {
+    display: block !important;
+  }
+  table.visible-xs {
+    display: table;
+  }
+  tr.visible-xs {
+    display: table-row !important;
+  }
+  th.visible-xs,
+  td.visible-xs {
+    display: table-cell !important;
+  }
+}
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-sm {
+    display: block !important;
+  }
+  table.visible-sm {
+    display: table;
+  }
+  tr.visible-sm {
+    display: table-row !important;
+  }
+  th.visible-sm,
+  td.visible-sm {
+    display: table-cell !important;
+  }
+}
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-md {
+    display: block !important;
+  }
+  table.visible-md {
+    display: table;
+  }
+  tr.visible-md {
+    display: table-row !important;
+  }
+  th.visible-md,
+  td.visible-md {
+    display: table-cell !important;
+  }
+}
+@media (min-width: 1200px) {
+  .visible-lg {
+    display: block !important;
+  }
+  table.visible-lg {
+    display: table;
+  }
+  tr.visible-lg {
+    display: table-row !important;
+  }
+  th.visible-lg,
+  td.visible-lg {
+    display: table-cell !important;
+  }
+}
+@media (max-width: 767px) {
+  .hidden-xs {
+    display: none !important;
+  }
+}
+@media (min-width: 768px) and (max-width: 991px) {
+  .hidden-sm {
+    display: none !important;
+  }
+}
+@media (min-width: 992px) and (max-width: 1199px) {
+  .hidden-md {
+    display: none !important;
+  }
+}
+@media (min-width: 1200px) {
+  .hidden-lg {
+    display: none !important;
+  }
+}
+.visible-print {
+  display: none !important;
+}
+@media print {
+  .visible-print {
+    display: block !important;
+  }
+  table.visible-print {
+    display: table;
+  }
+  tr.visible-print {
+    display: table-row !important;
+  }
+  th.visible-print,
+  td.visible-print {
+    display: table-cell !important;
+  }
+}
+@media print {
+  .hidden-print {
+    display: none !important;
+  }
+}
+/*# sourceMappingURL=bootstrap.css.map */
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/bootstrap.css.map b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap.css.map
new file mode 100644
index 0000000000000000000000000000000000000000..6bc5a2dc75413860670f9e657d62b64cf15fd195
--- /dev/null
+++ b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["less/normalize.less","less/print.less","less/scaffolding.less","less/mixins.less","less/variables.less","less/thumbnails.less","less/carousel.less","less/type.less","less/code.less","less/grid.less","less/tables.less","less/forms.less","less/buttons.less","less/button-groups.less","less/component-animations.less","less/glyphicons.less","less/dropdowns.less","less/input-groups.less","less/navs.less","less/navbar.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/pager.less","less/labels.less","less/badges.less","less/jumbotron.less","less/alerts.less","less/progress-bars.less","less/media.less","less/list-group.less","less/panels.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/popovers.less","less/responsive-utilities.less"],"names":[],"mappings":";AAQA;EACE,uBAAA;EACA,0BAAA;EACA,8BAAA;;AAOF;EACE,SAAA;;AAUF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACE,cAAA;;AAQF;AACA;AACA;AACA;EACE,qBAAA;EACA,wBAAA;;AAQF,KAAK,IAAI;EACP,aAAA;EACA,SAAA;;AAQF;AACA;EACE,aAAA;;AAUF;EACE,uBAAA;;AAOF,CAAC;AACD,CAAC;EACC,UAAA;;AAUF,IAAI;EACF,yBAAA;;AAOF;AACA;EACE,iBAAA;;AAOF;EACE,kBAAA;;AAQF;EACE,cAAA;EACA,gBAAA;;AAOF;EACE,gBAAA;EACA,WAAA;;AAOF;EACE,cAAA;;AAOF;AACA;EACE,cAAA;EACA,cAAA;EACA,kBAAA;EACA,wBAAA;;AAGF;EACE,WAAA;;AAGF;EACE,eAAA;;AAUF;EACE,SAAA;;AAOF,GAAG,IAAI;EACL,gBAAA;;AAUF;EACE,gBAAA;;AAOF;EACE,4BAAA;EACA,uBAAA;EACA,SAAA;;AAOF;EACE,cAAA;;AAOF;AACA;AACA;AACA;EACE,iCAAA;EACA,cAAA;;AAkBF;AACA;AACA;AACA;AACA;EACE,cAAA;EACA,aAAA;EACA,SAAA;;AAOF;EACE,iBAAA;;AAUF;AACA;EACE,oBAAA;;AAWF;AACA,IAAK,MAAK;AACV,KAAK;AACL,KAAK;EACH,0BAAA;EACA,eAAA;;AAOF,MAAM;AACN,IAAK,MAAK;EACR,eAAA;;AAOF,MAAM;AACN,KAAK;EACH,SAAA;EACA,UAAA;;AAQF;EACE,mBAAA;;AAWF,KAAK;AACL,KAAK;EACH,sBAAA;EACA,UAAA;;AASF,KAAK,eAAe;AACpB,KAAK,eAAe;EAClB,YAAA;;AASF,KAAK;EACH,6BAAA;EACA,4BAAA;EACA,+BAAA;EACA,uBAAA;;AASF,KAAK,eAAe;AACpB,KAAK,eAAe;EAClB,wBAAA;;AAOF;EACE,yBAAA;EACA,aAAA;EACA,8BAAA;;AAQF;EACE,SAAA;EACA,UAAA;;AAOF;EACE,cAAA;;AAQF;EACE,iBAAA;;AAUF;EACE,yBAAA;EACA,iBAAA;;AAGF;AACA;EACE,UAAA;;AChUF;EA9FE;IACE,4BAAA;IACA,sBAAA;IACA,kCAAA;IACA,2BAAA;;EAGF;EACA,CAAC;IACC,0BAAA;;EAGF,CAAC,MAAM;IACL,SAAS,KAAK,WAAW,GAAzB;;EAGF,IAAI,OAAO;IACT,SAAS,KAAK,YAAY,GAA1B;;EAIF,CAAC,qBAAqB;EACtB,CAAC,WAAW;IACV,SAAS,EAAT;;EAGF;EACA;IACE,sBAAA;IACA,wBAAA;;EAGF;IACE,2BAAA;;EAGF;EACA;IACE,wBAAA;;EAGF;IACE,0BAAA;;EAGF;EACA;EACA;IACE,UAAA;IACA,SAAA;;EAGF;EACA;IACE,uBAAA;;EAKF;IACE,2BAAA;;EAIF;IACE,aAAA;;EAEF,MACE;EADF,MAEE;IACE,iCAAA;;EAGJ,IAEE;EADF,OAAQ,OACN;IACE,iCAAA;;EAGJ;IACE,sBAAA;;EAGF;IACE,oCAAA;;EAEF,eACE;EADF,eAEE;IACE,iCAAA;;;ACtFN;ECyOE,8BAAA;EACG,2BAAA;EACK,sBAAA;;ADxOV,CAAC;AACD,CAAC;ECqOC,8BAAA;EACG,2BAAA;EACK,sBAAA;;ADhOV;EACE,gBAAA;EACA,6CAAA;;AAGF;EACE,aEcwB,8CFdxB;EACA,eAAA;EACA,uBAAA;EACA,cAAA;EACA,yBAAA;;AAIF;AACA;AACA;AACA;EACE,oBAAA;EACA,kBAAA;EACA,oBAAA;;AAMF;EACE,cAAA;EACA,qBAAA;;AAEA,CAAC;AACD,CAAC;EACC,cAAA;EACA,0BAAA;;AAGF,CAAC;ECzBD,oBAAA;EAEA,0CAAA;EACA,oBAAA;;ADiCF;EACE,SAAA;;AAMF;EACE,sBAAA;;AAIF;AG1EA,UAUE;AAVF,UAWE,EAAE;ACPJ,eAKE,QAME;AAXJ,eAKE,QAOE,IAAI;EHyWN,cAAA;EACA,eAAA;EACA,YAAA;;AD5SF;EACE,kBAAA;;AAMF;EACE,YAAA;EACA,uBAAA;EACA,yBAAA;EACA,yBAAA;EACA,kBAAA;EC8BA,wCAAA;EACQ,gCAAA;EA+PR,qBAAA;EACA,eAAA;EACA,YAAA;;ADxRF;EACE,kBAAA;;AAMF;EACE,gBAAA;EACA,mBAAA;EACA,SAAA;EACA,6BAAA;;AAQF;EACE,kBAAA;EACA,UAAA;EACA,WAAA;EACA,YAAA;EACA,UAAA;EACA,gBAAA;EACA,MAAM,gBAAN;EACA,SAAA;;AK5HF;AAAI;AAAI;AAAI;AAAI;AAAI;AACpB;AAAK;AAAK;AAAK;AAAK;AAAK;EACvB,oBAAA;EACA,gBAAA;EACA,gBAAA;EACA,cAAA;;AALF,EAOE;AAPE,EAOF;AAPM,EAON;AAPU,EAOV;AAPc,EAOd;AAPkB,EAOlB;AANF,GAME;AANG,GAMH;AANQ,GAMR;AANa,GAMb;AANkB,GAMlB;AANuB,GAMvB;AAPF,EAQE;AARE,EAQF;AARM,EAQN;AARU,EAQV;AARc,EAQd;AARkB,EAQlB;AAPF,GAOE;AAPG,GAOH;AAPQ,GAOR;AAPa,GAOb;AAPkB,GAOlB;AAPuB,GAOvB;EACE,mBAAA;EACA,cAAA;EACA,cAAA;;AAIJ;AAAI;AACJ;AAAI;AACJ;AAAI;EACF,gBAAA;EACA,mBAAA;;AAJF,EAME;AANE,GAMF;AALF,EAKE;AALE,GAKF;AAJF,EAIE;AAJE,GAIF;AANF,EAOE;AAPE,GAOF;AANF,EAME;AANE,GAMF;AALF,EAKE;AALE,GAKF;EACE,cAAA;;AAGJ;AAAI;AACJ;AAAI;AACJ;AAAI;EACF,gBAAA;EACA,mBAAA;;AAJF,EAME;AANE,GAMF;AALF,EAKE;AALE,GAKF;AAJF,EAIE;AAJE,GAIF;AANF,EAOE;AAPE,GAOF;AANF,EAME;AANE,GAMF;AALF,EAKE;AALE,GAKF;EACE,cAAA;;AAIJ;AAAI;EAAM,eAAA;;AACV;AAAI;EAAM,eAAA;;AACV;AAAI;EAAM,eAAA;;AACV;AAAI;EAAM,eAAA;;AACV;AAAI;EAAM,eAAA;;AACV;AAAI;EAAM,eAAA;;AAMV;EACE,gBAAA;;AAGF;EACE,mBAAA;EACA,eAAA;EACA,gBAAA;EACA,gBAAA;;AAKF,QAHqC;EAGrC;IAFI,eAAA;;;AASJ;AACA;EAAU,cAAA;;AAGV;EAAU,kBAAA;;AAGV;EAAuB,gBAAA;;AACvB;EAAuB,iBAAA;;AACvB;EAAuB,kBAAA;;AACvB;EAAuB,mBAAA;;AAGvB;EACE,cAAA;;AAEF;EJofE,cAAA;;AACA,CAAC,aAAC;EACA,cAAA;;AInfJ;EJifE,cAAA;;AACA,CAAC,aAAC;EACA,cAAA;;AIhfJ;EJ8eE,cAAA;;AACA,CAAC,UAAC;EACA,cAAA;;AI7eJ;EJ2eE,cAAA;;AACA,CAAC,aAAC;EACA,cAAA;;AI1eJ;EJweE,cAAA;;AACA,CAAC,YAAC;EACA,cAAA;;AIneJ;EAGE,WAAA;EJqdA,yBAAA;;AACA,CAAC,WAAC;EACA,yBAAA;;AIpdJ;EJkdE,yBAAA;;AACA,CAAC,WAAC;EACA,yBAAA;;AIjdJ;EJ+cE,yBAAA;;AACA,CAAC,QAAC;EACA,yBAAA;;AI9cJ;EJ4cE,yBAAA;;AACA,CAAC,WAAC;EACA,yBAAA;;AI3cJ;EJycE,yBAAA;;AACA,CAAC,UAAC;EACA,yBAAA;;AIncJ;EACE,mBAAA;EACA,mBAAA;EACA,gCAAA;;AAQF;AACA;EACE,aAAA;EACA,mBAAA;;AAHF,EAIE;AAHF,EAGE;AAJF,EAKE;AAJF,EAIE;EACE,gBAAA;;AAOJ;EACE,eAAA;EACA,gBAAA;;AAIF;EALE,eAAA;EACA,gBAAA;EAMA,iBAAA;;AAFF,YAIE;EACE,qBAAA;EACA,iBAAA;EACA,kBAAA;;AAKJ;EACE,aAAA;EACA,mBAAA;;AAEF;AACA;EACE,uBAAA;;AAEF;EACE,iBAAA;;AAEF;EACE,cAAA;;AAwBF,QAhB2C;EACzC,cACE;IACE,WAAA;IACA,YAAA;IACA,WAAA;IACA,iBAAA;IJ1IJ,gBAAA;IACA,uBAAA;IACA,mBAAA;;EImIA,cAQE;IACE,kBAAA;;;AAUN,IAAI;AAEJ,IAAI;EACF,YAAA;EACA,iCAAA;;AAEF;EACE,cAAA;EACA,yBAAA;;AAIF;EACE,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,8BAAA;;AAKE,UAHF,EAGG;AAAD,UAFF,GAEG;AAAD,UADF,GACG;EACC,gBAAA;;AAVN,UAgBE;AAhBF,UAiBE;AAjBF,UAkBE;EACE,cAAA;EACA,cAAA;EACA,uBAAA;EACA,cAAA;;AAEA,UARF,OAQG;AAAD,UAPF,MAOG;AAAD,UANF,OAMG;EACC,SAAS,aAAT;;AAQN;AACA,UAAU;EACR,mBAAA;EACA,eAAA;EACA,+BAAA;EACA,cAAA;EACA,iBAAA;;AAME,mBAHF,OAGG;AAAD,UAXM,WAQR,OAGG;AAAD,mBAFF,MAEG;AAAD,UAXM,WASR,MAEG;AAAD,mBADF,OACG;AAAD,UAXM,WAUR,OACG;EAAU,SAAS,EAAT;;AACX,mBAJF,OAIG;AAAD,UAZM,WAQR,OAIG;AAAD,mBAHF,MAGG;AAAD,UAZM,WASR,MAGG;AAAD,mBAFF,OAEG;AAAD,UAZM,WAUR,OAEG;EACC,SAAS,aAAT;;AAMN,UAAU;AACV,UAAU;EACR,SAAS,EAAT;;AAIF;EACE,mBAAA;EACA,kBAAA;EACA,uBAAA;;AC7RF;AACA;AACA;AACA;EACE,sCJkCiD,wBIlCjD;;AAIF;EACE,gBAAA;EACA,cAAA;EACA,cAAA;EACA,yBAAA;EACA,mBAAA;EACA,kBAAA;;AAIF;EACE,gBAAA;EACA,cAAA;EACA,cAAA;EACA,yBAAA;EACA,kBAAA;EACA,8CAAA;;AAIF;EACE,cAAA;EACA,cAAA;EACA,gBAAA;EACA,eAAA;EACA,uBAAA;EACA,qBAAA;EACA,qBAAA;EACA,cAAA;EACA,yBAAA;EACA,yBAAA;EACA,kBAAA;;AAXF,GAcE;EACE,UAAA;EACA,kBAAA;EACA,cAAA;EACA,qBAAA;EACA,6BAAA;EACA,gBAAA;;AAKJ;EACE,iBAAA;EACA,kBAAA;;ACpDF;ENqnBE,kBAAA;EACA,iBAAA;EACA,kBAAA;EACA,mBAAA;;AMlnBA,QAHmC;EAGnC;IAFE,YAAA;;;AAKF,QAHmC;EAGnC;IAFE,YAAA;;;AAKJ,QAHqC;EAGrC;IAFI,aAAA;;;AAUJ;ENimBE,kBAAA;EACA,iBAAA;EACA,kBAAA;EACA,mBAAA;;AM3lBF;ENimBE,kBAAA;EACA,mBAAA;;AAqIE;EACE,kBAAA;EAEA,eAAA;EAEA,kBAAA;EACA,mBAAA;;AAgBF;EACE,WAAA;;AAOJ,KAAK,EAAQ,CAAC;EACZ,WAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,mBAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,mBAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,UAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,mBAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,mBAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,UAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,mBAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,mBAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,UAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,mBAAA;;AADF,KAAK,EAAQ,CAAC;EACZ,kBAAA;;AASF,KAAK,EAAQ,MAAM;EACjB,WAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,mBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,mBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,UAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,mBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,mBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,UAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,mBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,mBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,UAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,mBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,kBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,SAAA;;AANF,KAAK,EAAQ,MAAM;EACjB,UAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,kBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,kBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,SAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,kBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,kBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,SAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,kBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,kBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,SAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,kBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,iBAAA;;AADF,KAAK,EAAQ,MAAM;EACjB,QAAA;;AASF,KAAK,EAAQ,QAAQ;EACnB,iBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,yBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,yBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,gBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,yBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,yBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,gBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,yBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,yBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,gBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,yBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,wBAAA;;AADF,KAAK,EAAQ,QAAQ;EACnB,eAAA;;AMvvBJ,QALmC;ENouB/B;IACE,WAAA;;EAOJ,KAAK,EAAQ,CAAC;IACZ,WAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,UAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,UAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,UAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,kBAAA;;EASF,KAAK,EAAQ,MAAM;IACjB,WAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EANF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,iBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,QAAA;;EASF,KAAK,EAAQ,QAAQ;IACnB,iBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,gBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,gBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,gBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,wBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,eAAA;;;AM9uBJ,QALmC;EN2tB/B;IACE,WAAA;;EAOJ,KAAK,EAAQ,CAAC;IACZ,WAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,UAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,UAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,UAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,kBAAA;;EASF,KAAK,EAAQ,MAAM;IACjB,WAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EANF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,iBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,QAAA;;EASF,KAAK,EAAQ,QAAQ;IACnB,iBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,gBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,gBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,gBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,wBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,eAAA;;;AMvuBJ,QAHmC;ENktB/B;IACE,WAAA;;EAOJ,KAAK,EAAQ,CAAC;IACZ,WAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,UAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,UAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,UAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,mBAAA;;EADF,KAAK,EAAQ,CAAC;IACZ,kBAAA;;EASF,KAAK,EAAQ,MAAM;IACjB,WAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,mBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EANF,KAAK,EAAQ,MAAM;IACjB,UAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,SAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,kBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,iBAAA;;EADF,KAAK,EAAQ,MAAM;IACjB,QAAA;;EASF,KAAK,EAAQ,QAAQ;IACnB,iBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,gBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,gBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,gBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,yBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,wBAAA;;EADF,KAAK,EAAQ,QAAQ;IACnB,eAAA;;;AOtzBJ;EACE,eAAA;EACA,6BAAA;;AAEF;EACE,gBAAA;;AAMF;EACE,WAAA;EACA,mBAAA;;AAFF,MAIE,QAGE,KACE;AARN,MAKE,QAEE,KACE;AARN,MAME,QACE,KACE;AARN,MAIE,QAGE,KAEE;AATN,MAKE,QAEE,KAEE;AATN,MAME,QACE,KAEE;EACE,YAAA;EACA,uBAAA;EACA,mBAAA;EACA,6BAAA;;AAbR,MAkBE,QAAQ,KAAK;EACX,sBAAA;EACA,gCAAA;;AApBJ,MAuBE,UAAU,QAGR,KAAI,YACF;AA3BN,MAwBE,WAAW,QAET,KAAI,YACF;AA3BN,MAyBE,QAAO,YACL,KAAI,YACF;AA3BN,MAuBE,UAAU,QAGR,KAAI,YAEF;AA5BN,MAwBE,WAAW,QAET,KAAI,YAEF;AA5BN,MAyBE,QAAO,YACL,KAAI,YAEF;EACE,aAAA;;AA7BR,MAkCE,QAAQ;EACN,6BAAA;;AAnCJ,MAuCE;EACE,yBAAA;;AAOJ,gBACE,QAGE,KACE;AALN,gBAEE,QAEE,KACE;AALN,gBAGE,QACE,KACE;AALN,gBACE,QAGE,KAEE;AANN,gBAEE,QAEE,KAEE;AANN,gBAGE,QACE,KAEE;EACE,YAAA;;AAWR;EACE,yBAAA;;AADF,eAEE,QAGE,KACE;AANN,eAGE,QAEE,KACE;AANN,eAIE,QACE,KACE;AANN,eAEE,QAGE,KAEE;AAPN,eAGE,QAEE,KAEE;AAPN,eAIE,QACE,KAEE;EACE,yBAAA;;AARR,eAYE,QAAQ,KACN;AAbJ,eAYE,QAAQ,KAEN;EACE,wBAAA;;AAUN,cACE,QAAQ,KAAI,UAAU,KACpB;AAFJ,cACE,QAAQ,KAAI,UAAU,KAEpB;EACE,yBAAA;;AAUN,YACE,QAAQ,KAAI,MACV;AAFJ,YACE,QAAQ,KAAI,MAEV;EACE,yBAAA;;AAUN,KAAM,IAAG;EACP,gBAAA;EACA,WAAA;EACA,qBAAA;;AAKE,KAFF,GAEG;AAAD,KADF,GACG;EACC,gBAAA;EACA,WAAA;EACA,mBAAA;;AP0SJ,MAAO,QAAQ,KAGb,KAAI,CAAC;AAFP,MAAO,QAAQ,KAEb,KAAI,CAAC;AADP,MAAO,QAAQ,KACb,KAAI,CAAC;AAHP,MAAO,QAAQ,KAIb,KAAI,CAAC;AAHP,MAAO,QAAQ,KAGb,KAAI,CAAC;AAFP,MAAO,QAAQ,KAEb,KAAI,CAAC;AACL,MALK,QAAQ,KAKZ,CAAC,MAAS;AAAX,MAJK,QAAQ,KAIZ,CAAC,MAAS;AAAX,MAHK,QAAQ,KAGZ,CAAC,MAAS;AACX,MANK,QAAQ,KAMZ,CAAC,MAAS;AAAX,MALK,QAAQ,KAKZ,CAAC,MAAS;AAAX,MAJK,QAAQ,KAIZ,CAAC,MAAS;EACT,yBAAA;;AAMJ,YAAa,QAAQ,KACnB,KAAI,CAAC,MAAQ;AADf,YAAa,QAAQ,KAEnB,KAAI,CAAC,MAAQ;AACb,YAHW,QAAQ,KAGlB,CAAC,MAAQ,MAAO;AACjB,YAJW,QAAQ,KAIlB,CAAC,MAAQ,MAAO;EACf,yBAAA;;AAlBJ,MAAO,QAAQ,KAGb,KAAI,CAAC;AAFP,MAAO,QAAQ,KAEb,KAAI,CAAC;AADP,MAAO,QAAQ,KACb,KAAI,CAAC;AAHP,MAAO,QAAQ,KAIb,KAAI,CAAC;AAHP,MAAO,QAAQ,KAGb,KAAI,CAAC;AAFP,MAAO,QAAQ,KAEb,KAAI,CAAC;AACL,MALK,QAAQ,KAKZ,CAAC,OAAS;AAAX,MAJK,QAAQ,KAIZ,CAAC,OAAS;AAAX,MAHK,QAAQ,KAGZ,CAAC,OAAS;AACX,MANK,QAAQ,KAMZ,CAAC,OAAS;AAAX,MALK,QAAQ,KAKZ,CAAC,OAAS;AAAX,MAJK,QAAQ,KAIZ,CAAC,OAAS;EACT,yBAAA;;AAMJ,YAAa,QAAQ,KACnB,KAAI,CAAC,OAAQ;AADf,YAAa,QAAQ,KAEnB,KAAI,CAAC,OAAQ;AACb,YAHW,QAAQ,KAGlB,CAAC,OAAQ,MAAO;AACjB,YAJW,QAAQ,KAIlB,CAAC,OAAQ,MAAO;EACf,yBAAA;;AAlBJ,MAAO,QAAQ,KAGb,KAAI,CAAC;AAFP,MAAO,QAAQ,KAEb,KAAI,CAAC;AADP,MAAO,QAAQ,KACb,KAAI,CAAC;AAHP,MAAO,QAAQ,KAIb,KAAI,CAAC;AAHP,MAAO,QAAQ,KAGb,KAAI,CAAC;AAFP,MAAO,QAAQ,KAEb,KAAI,CAAC;AACL,MALK,QAAQ,KAKZ,CAAC,IAAS;AAAX,MAJK,QAAQ,KAIZ,CAAC,IAAS;AAAX,MAHK,QAAQ,KAGZ,CAAC,IAAS;AACX,MANK,QAAQ,KAMZ,CAAC,IAAS;AAAX,MALK,QAAQ,KAKZ,CAAC,IAAS;AAAX,MAJK,QAAQ,KAIZ,CAAC,IAAS;EACT,yBAAA;;AAMJ,YAAa,QAAQ,KACnB,KAAI,CAAC,IAAQ;AADf,YAAa,QAAQ,KAEnB,KAAI,CAAC,IAAQ;AACb,YAHW,QAAQ,KAGlB,CAAC,IAAQ,MAAO;AACjB,YAJW,QAAQ,KAIlB,CAAC,IAAQ,MAAO;EACf,yBAAA;;AAlBJ,MAAO,QAAQ,KAGb,KAAI,CAAC;AAFP,MAAO,QAAQ,KAEb,KAAI,CAAC;AADP,MAAO,QAAQ,KACb,KAAI,CAAC;AAHP,MAAO,QAAQ,KAIb,KAAI,CAAC;AAHP,MAAO,QAAQ,KAGb,KAAI,CAAC;AAFP,MAAO,QAAQ,KAEb,KAAI,CAAC;AACL,MALK,QAAQ,KAKZ,CAAC,OAAS;AAAX,MAJK,QAAQ,KAIZ,CAAC,OAAS;AAAX,MAHK,QAAQ,KAGZ,CAAC,OAAS;AACX,MANK,QAAQ,KAMZ,CAAC,OAAS;AAAX,MALK,QAAQ,KAKZ,CAAC,OAAS;AAAX,MAJK,QAAQ,KAIZ,CAAC,OAAS;EACT,yBAAA;;AAMJ,YAAa,QAAQ,KACnB,KAAI,CAAC,OAAQ;AADf,YAAa,QAAQ,KAEnB,KAAI,CAAC,OAAQ;AACb,YAHW,QAAQ,KAGlB,CAAC,OAAQ,MAAO;AACjB,YAJW,QAAQ,KAIlB,CAAC,OAAQ,MAAO;EACf,yBAAA;;AAlBJ,MAAO,QAAQ,KAGb,KAAI,CAAC;AAFP,MAAO,QAAQ,KAEb,KAAI,CAAC;AADP,MAAO,QAAQ,KACb,KAAI,CAAC;AAHP,MAAO,QAAQ,KAIb,KAAI,CAAC;AAHP,MAAO,QAAQ,KAGb,KAAI,CAAC;AAFP,MAAO,QAAQ,KAEb,KAAI,CAAC;AACL,MALK,QAAQ,KAKZ,CAAC,MAAS;AAAX,MAJK,QAAQ,KAIZ,CAAC,MAAS;AAAX,MAHK,QAAQ,KAGZ,CAAC,MAAS;AACX,MANK,QAAQ,KAMZ,CAAC,MAAS;AAAX,MALK,QAAQ,KAKZ,CAAC,MAAS;AAAX,MAJK,QAAQ,KAIZ,CAAC,MAAS;EACT,yBAAA;;AAMJ,YAAa,QAAQ,KACnB,KAAI,CAAC,MAAQ;AADf,YAAa,QAAQ,KAEnB,KAAI,CAAC,MAAQ;AACb,YAHW,QAAQ,KAGlB,CAAC,MAAQ,MAAO;AACjB,YAJW,QAAQ,KAIlB,CAAC,MAAQ,MAAO;EACf,yBAAA;;AOpON,QA/DmC;EACjC;IACE,WAAA;IACA,mBAAA;IACA,kBAAA;IACA,kBAAA;IACA,4CAAA;IACA,yBAAA;IACA,iCAAA;;EAPF,iBAUE;IACE,gBAAA;;EAXJ,iBAUE,SAIE,QAGE,KACE;EAlBR,iBAUE,SAKE,QAEE,KACE;EAlBR,iBAUE,SAME,QACE,KACE;EAlBR,iBAUE,SAIE,QAGE,KAEE;EAnBR,iBAUE,SAKE,QAEE,KAEE;EAnBR,iBAUE,SAME,QACE,KAEE;IACE,mBAAA;;EApBV,iBA2BE;IACE,SAAA;;EA5BJ,iBA2BE,kBAIE,QAGE,KACE,KAAI;EAnCZ,iBA2BE,kBAKE,QAEE,KACE,KAAI;EAnCZ,iBA2BE,kBAME,QACE,KACE,KAAI;EAnCZ,iBA2BE,kBAIE,QAGE,KAEE,KAAI;EApCZ,iBA2BE,kBAKE,QAEE,KAEE,KAAI;EApCZ,iBA2BE,kBAME,QACE,KAEE,KAAI;IACF,cAAA;;EArCV,iBA2BE,kBAIE,QAGE,KAKE,KAAI;EAvCZ,iBA2BE,kBAKE,QAEE,KAKE,KAAI;EAvCZ,iBA2BE,kBAME,QACE,KAKE,KAAI;EAvCZ,iBA2BE,kBAIE,QAGE,KAME,KAAI;EAxCZ,iBA2BE,kBAKE,QAEE,KAME,KAAI;EAxCZ,iBA2BE,kBAME,QACE,KAME,KAAI;IACF,eAAA;;EAzCV,iBA2BE,kBAsBE,QAEE,KAAI,WACF;EApDR,iBA2BE,kBAuBE,QACE,KAAI,WACF;EApDR,iBA2BE,kBAsBE,QAEE,KAAI,WAEF;EArDR,iBA2BE,kBAuBE,QACE,KAAI,WAEF;IACE,gBAAA;;;ACxNZ;EACE,UAAA;EACA,SAAA;EACA,SAAA;EAIA,YAAA;;AAGF;EACE,cAAA;EACA,WAAA;EACA,UAAA;EACA,mBAAA;EACA,eAAA;EACA,oBAAA;EACA,cAAA;EACA,SAAA;EACA,gCAAA;;AAGF;EACE,qBAAA;EACA,kBAAA;EACA,iBAAA;;AAWF,KAAK;ERsMH,8BAAA;EACG,2BAAA;EACK,sBAAA;;AQnMV,KAAK;AACL,KAAK;EACH,eAAA;EACA,kBAAA;;EACA,mBAAA;;AAIF,KAAK;EACH,cAAA;;AAIF,KAAK;EACH,cAAA;EACA,WAAA;;AAIF,MAAM;AACN,MAAM;EACJ,YAAA;;AAIF,KAAK,aAAa;AAClB,KAAK,cAAc;AACnB,KAAK,iBAAiB;ER7CpB,oBAAA;EAEA,0CAAA;EACA,oBAAA;;AQ+CF;EACE,cAAA;EACA,gBAAA;EACA,eAAA;EACA,uBAAA;EACA,cAAA;;AA0BF;EACE,cAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;EACA,eAAA;EACA,uBAAA;EACA,cAAA;EACA,yBAAA;EACA,sBAAA;EACA,yBAAA;EACA,kBAAA;ERHA,wDAAA;EACQ,gDAAA;EAKR,8EAAA;EACQ,sEAAA;;AAmwBR,aAAC;EACC,qBAAA;EACA,UAAA;EA5wBF,sFAAA;EACQ,8EAAA;;AAlER,aAAC;EAA+B,cAAA;EACA,UAAA;;AAChC,aAAC;EAA+B,cAAA;;AAChC,aAAC;EAA+B,cAAA;;AQgFhC,aAAC;AACD,aAAC;AACD,QAAQ,UAAW;EACjB,mBAAA;EACA,yBAAA;EACA,UAAA;;AAIF,QAAQ;EACN,YAAA;;AAYJ,KAAK;EACH,wBAAA;;AASF,KAAK;EACH,iBAAA;;AASF;EACE,mBAAA;;AAQF;AACA;EACE,cAAA;EACA,gBAAA;EACA,gBAAA;EACA,mBAAA;EACA,kBAAA;;AANF,MAOE;AANF,SAME;EACE,eAAA;EACA,mBAAA;EACA,eAAA;;AAGJ,MAAO,MAAK;AACZ,aAAc,MAAK;AACnB,SAAU,MAAK;AACf,gBAAiB,MAAK;EACpB,WAAA;EACA,kBAAA;;AAEF,MAAO;AACP,SAAU;EACR,gBAAA;;AAIF;AACA;EACE,qBAAA;EACA,kBAAA;EACA,gBAAA;EACA,sBAAA;EACA,mBAAA;EACA,eAAA;;AAEF,aAAc;AACd,gBAAiB;EACf,aAAA;EACA,iBAAA;;AAYA,KANG,cAMF;AAAD,KALG,iBAKF;AAAD,MAAC;AAAD,aAAC;AAAD,SAAC;AAAD,gBAAC;AACD,QAAQ,UAAW,MAPhB;AAOH,QAAQ,UAAW,MANhB;AAMH,QAAQ,UAAW;AAAnB,QAAQ,UAAW;AAAnB,QAAQ,UAAW;AAAnB,QAAQ,UAAW;EACjB,mBAAA;;AAUJ;ERqpBE,YAAA;EACA,iBAAA;EACA,eAAA;EACA,gBAAA;EACA,kBAAA;;AAEA,MAAM;EACJ,YAAA;EACA,iBAAA;;AAGF,QAAQ;AACR,MAAM,UAAU;EACd,YAAA;;AQ9pBJ;ERipBE,YAAA;EACA,kBAAA;EACA,eAAA;EACA,iBAAA;EACA,kBAAA;;AAEA,MAAM;EACJ,YAAA;EACA,iBAAA;;AAGF,QAAQ;AACR,MAAM,UAAU;EACd,YAAA;;AQrpBJ;EAEE,kBAAA;;AAFF,aAKE;EACE,qBAAA;;AANJ,aAUE;EACE,kBAAA;EACA,SAAA;EACA,QAAA;EACA,cAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;;AAKJ,YRsjBE;AQtjBF,YRujBE;AQvjBF,YRwjBE;AQxjBF,YRyjBE;AQzjBF,YR0jBE;AQ1jBF,YR2jBE;EACE,cAAA;;AQ5jBJ,YR+jBE;EACE,qBAAA;EAvuBF,wDAAA;EACQ,gDAAA;;AAwuBN,YAHF,cAGG;EACC,qBAAA;EA1uBJ,yEAAA;EACQ,iEAAA;;AQsKV,YRykBE;EACE,cAAA;EACA,qBAAA;EACA,yBAAA;;AQ5kBJ,YR+kBE;EACE,cAAA;;AQ7kBJ,YRmjBE;AQnjBF,YRojBE;AQpjBF,YRqjBE;AQrjBF,YRsjBE;AQtjBF,YRujBE;AQvjBF,YRwjBE;EACE,cAAA;;AQzjBJ,YR4jBE;EACE,qBAAA;EAvuBF,wDAAA;EACQ,gDAAA;;AAwuBN,YAHF,cAGG;EACC,qBAAA;EA1uBJ,yEAAA;EACQ,iEAAA;;AQyKV,YRskBE;EACE,cAAA;EACA,qBAAA;EACA,yBAAA;;AQzkBJ,YR4kBE;EACE,cAAA;;AQ1kBJ,URgjBE;AQhjBF,URijBE;AQjjBF,URkjBE;AQljBF,URmjBE;AQnjBF,URojBE;AQpjBF,URqjBE;EACE,cAAA;;AQtjBJ,URyjBE;EACE,qBAAA;EAvuBF,wDAAA;EACQ,gDAAA;;AAwuBN,UAHF,cAGG;EACC,qBAAA;EA1uBJ,yEAAA;EACQ,iEAAA;;AQ4KV,URmkBE;EACE,cAAA;EACA,qBAAA;EACA,yBAAA;;AQtkBJ,URykBE;EACE,cAAA;;AQhkBJ;EACE,gBAAA;;AASF;EACE,cAAA;EACA,eAAA;EACA,mBAAA;EACA,cAAA;;AAoEF,QAjDqC;EAiDrC,YA/CI;IACE,qBAAA;IACA,gBAAA;IACA,sBAAA;;EA4CN,YAxCI;IACE,qBAAA;IACA,WAAA;IACA,sBAAA;;EAqCN,YAlCI,aAAa;IACX,WAAA;;EAiCN,YA9BI;IACE,gBAAA;IACA,sBAAA;;EA4BN,YAtBI;EAsBJ,YArBI;IACE,qBAAA;IACA,aAAA;IACA,gBAAA;IACA,eAAA;IACA,sBAAA;;EAgBN,YAdI,OAAO,MAAK;EAchB,YAbI,UAAU,MAAK;IACb,WAAA;IACA,cAAA;;EAWN,YAJI,cAAc;IACZ,MAAA;;;AAWN,gBAGE;AAHF,gBAIE;AAJF,gBAKE;AALF,gBAME;AANF,gBAOE;EACE,aAAA;EACA,gBAAA;EACA,gBAAA;;AAVJ,gBAcE;AAdF,gBAeE;EACE,gBAAA;;AAhBJ,gBAoBE;ERyOA,kBAAA;EACA,mBAAA;;AQ9PF,gBAwBE;EACE,gBAAA;;AAUF,QANmC;EAMnC,gBALE;IACE,iBAAA;;;AA/BN,gBAuCE,cAAc;EACZ,MAAA;EACA,WAAA;;AC3aJ;EACE,qBAAA;EACA,gBAAA;EACA,mBAAA;EACA,kBAAA;EACA,sBAAA;EACA,eAAA;EACA,sBAAA;EACA,6BAAA;EACA,mBAAA;ET0gBA,iBAAA;EACA,eAAA;EACA,uBAAA;EACA,kBAAA;EAnSA,yBAAA;EACG,sBAAA;EACC,qBAAA;EACI,iBAAA;;AStON,IAAC;AAAD,IAFD,OAEE;AAAD,IADD,OACE;ETQH,oBAAA;EAEA,0CAAA;EACA,oBAAA;;ASNA,IAAC;AACD,IAAC;EACC,cAAA;EACA,qBAAA;;AAGF,IAAC;AACD,IAAC;EACC,UAAA;EACA,sBAAA;ETmFF,wDAAA;EACQ,gDAAA;;AShFR,IAAC;AACD,IAAC;AACD,QAAQ,UAAW;EACjB,mBAAA;EACA,oBAAA;ET+OF,aAAA;EAGA,yBAAA;EAvKA,wBAAA;EACQ,gBAAA;;ASlEV;ET2bE,cAAA;EACA,yBAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;AACD,YAAC;AACD,YAAC;AACD,KAAM,iBAAgB;EACpB,cAAA;EACA,yBAAA;EACI,qBAAA;;AAEN,YAAC;AACD,YAAC;AACD,KAAM,iBAAgB;EACpB,sBAAA;;AAKA,YAHD;AAGC,YAFD;AAEC,QADM,UAAW;AAEjB,YAJD,SAIE;AAAD,YAHD,UAGE;AAAD,QAFM,UAAW,aAEhB;AACD,YALD,SAKE;AAAD,YAJD,UAIE;AAAD,QAHM,UAAW,aAGhB;AACD,YAND,SAME;AAAD,YALD,UAKE;AAAD,QAJM,UAAW,aAIhB;AACD,YAPD,SAOE;AAAD,YAND,UAME;AAAD,QALM,UAAW,aAKhB;EACC,yBAAA;EACI,qBAAA;;AStdV,YT0dE;EACE,cAAA;EACA,yBAAA;;ASzdJ;ETwbE,cAAA;EACA,yBAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;AACD,YAAC;AACD,YAAC;AACD,KAAM,iBAAgB;EACpB,cAAA;EACA,yBAAA;EACI,qBAAA;;AAEN,YAAC;AACD,YAAC;AACD,KAAM,iBAAgB;EACpB,sBAAA;;AAKA,YAHD;AAGC,YAFD;AAEC,QADM,UAAW;AAEjB,YAJD,SAIE;AAAD,YAHD,UAGE;AAAD,QAFM,UAAW,aAEhB;AACD,YALD,SAKE;AAAD,YAJD,UAIE;AAAD,QAHM,UAAW,aAGhB;AACD,YAND,SAME;AAAD,YALD,UAKE;AAAD,QAJM,UAAW,aAIhB;AACD,YAPD,SAOE;AAAD,YAND,UAME;AAAD,QALM,UAAW,aAKhB;EACC,yBAAA;EACI,qBAAA;;ASndV,YTudE;EACE,cAAA;EACA,yBAAA;;ASrdJ;ETobE,cAAA;EACA,yBAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;AACD,YAAC;AACD,YAAC;AACD,KAAM,iBAAgB;EACpB,cAAA;EACA,yBAAA;EACI,qBAAA;;AAEN,YAAC;AACD,YAAC;AACD,KAAM,iBAAgB;EACpB,sBAAA;;AAKA,YAHD;AAGC,YAFD;AAEC,QADM,UAAW;AAEjB,YAJD,SAIE;AAAD,YAHD,UAGE;AAAD,QAFM,UAAW,aAEhB;AACD,YALD,SAKE;AAAD,YAJD,UAIE;AAAD,QAHM,UAAW,aAGhB;AACD,YAND,SAME;AAAD,YALD,UAKE;AAAD,QAJM,UAAW,aAIhB;AACD,YAPD,SAOE;AAAD,YAND,UAME;AAAD,QALM,UAAW,aAKhB;EACC,yBAAA;EACI,qBAAA;;AS/cV,YTmdE;EACE,cAAA;EACA,yBAAA;;ASjdJ;ETgbE,cAAA;EACA,yBAAA;EACA,qBAAA;;AAEA,SAAC;AACD,SAAC;AACD,SAAC;AACD,SAAC;AACD,KAAM,iBAAgB;EACpB,cAAA;EACA,yBAAA;EACI,qBAAA;;AAEN,SAAC;AACD,SAAC;AACD,KAAM,iBAAgB;EACpB,sBAAA;;AAKA,SAHD;AAGC,SAFD;AAEC,QADM,UAAW;AAEjB,SAJD,SAIE;AAAD,SAHD,UAGE;AAAD,QAFM,UAAW,UAEhB;AACD,SALD,SAKE;AAAD,SAJD,UAIE;AAAD,QAHM,UAAW,UAGhB;AACD,SAND,SAME;AAAD,SALD,UAKE;AAAD,QAJM,UAAW,UAIhB;AACD,SAPD,SAOE;AAAD,SAND,UAME;AAAD,QALM,UAAW,UAKhB;EACC,yBAAA;EACI,qBAAA;;AS3cV,ST+cE;EACE,cAAA;EACA,yBAAA;;AS7cJ;ET4aE,cAAA;EACA,yBAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;AACD,YAAC;AACD,YAAC;AACD,KAAM,iBAAgB;EACpB,cAAA;EACA,yBAAA;EACI,qBAAA;;AAEN,YAAC;AACD,YAAC;AACD,KAAM,iBAAgB;EACpB,sBAAA;;AAKA,YAHD;AAGC,YAFD;AAEC,QADM,UAAW;AAEjB,YAJD,SAIE;AAAD,YAHD,UAGE;AAAD,QAFM,UAAW,aAEhB;AACD,YALD,SAKE;AAAD,YAJD,UAIE;AAAD,QAHM,UAAW,aAGhB;AACD,YAND,SAME;AAAD,YALD,UAKE;AAAD,QAJM,UAAW,aAIhB;AACD,YAPD,SAOE;AAAD,YAND,UAME;AAAD,QALM,UAAW,aAKhB;EACC,yBAAA;EACI,qBAAA;;ASvcV,YT2cE;EACE,cAAA;EACA,yBAAA;;ASzcJ;ETwaE,cAAA;EACA,yBAAA;EACA,qBAAA;;AAEA,WAAC;AACD,WAAC;AACD,WAAC;AACD,WAAC;AACD,KAAM,iBAAgB;EACpB,cAAA;EACA,yBAAA;EACI,qBAAA;;AAEN,WAAC;AACD,WAAC;AACD,KAAM,iBAAgB;EACpB,sBAAA;;AAKA,WAHD;AAGC,WAFD;AAEC,QADM,UAAW;AAEjB,WAJD,SAIE;AAAD,WAHD,UAGE;AAAD,QAFM,UAAW,YAEhB;AACD,WALD,SAKE;AAAD,WAJD,UAIE;AAAD,QAHM,UAAW,YAGhB;AACD,WAND,SAME;AAAD,WALD,UAKE;AAAD,QAJM,UAAW,YAIhB;AACD,WAPD,SAOE;AAAD,WAND,UAME;AAAD,QALM,UAAW,YAKhB;EACC,yBAAA;EACI,qBAAA;;ASncV,WTucE;EACE,cAAA;EACA,yBAAA;;AShcJ;EACE,cAAA;EACA,mBAAA;EACA,eAAA;EACA,gBAAA;;AAEA;AACA,SAAC;AACD,SAAC;AACD,QAAQ,UAAW;EACjB,6BAAA;ET2BF,wBAAA;EACQ,gBAAA;;ASzBR;AACA,SAAC;AACD,SAAC;AACD,SAAC;EACC,yBAAA;;AAEF,SAAC;AACD,SAAC;EACC,cAAA;EACA,0BAAA;EACA,6BAAA;;AAIA,SAFD,UAEE;AAAD,QADM,UAAW,UAChB;AACD,SAHD,UAGE;AAAD,QAFM,UAAW,UAEhB;EACC,cAAA;EACA,qBAAA;;AASN;ACvBA,aAAc;EVubZ,kBAAA;EACA,eAAA;EACA,iBAAA;EACA,kBAAA;;AS/ZF;AC5BA,aAAc;EVwbZ,iBAAA;EACA,eAAA;EACA,gBAAA;EACA,kBAAA;;AS3ZF;ACjCA,aAAc;EVybZ,gBAAA;EACA,eAAA;EACA,gBAAA;EACA,kBAAA;;ASnZF;EACE,cAAA;EACA,WAAA;EACA,eAAA;EACA,gBAAA;;AAIF,UAAW;EACT,eAAA;;AAOA,KAHG,eAGF;AAAD,KAFG,cAEF;AAAD,KADG,eACF;EACC,WAAA;;AEnJJ;EACE,UAAA;EXqHA,wCAAA;EACQ,gCAAA;;AWpHR,KAAC;EACC,UAAA;;AAIJ;EACE,aAAA;;AACA,SAAC;EACC,cAAA;;AAGJ;EACE,kBAAA;EACA,SAAA;EACA,gBAAA;EXqGA,qCAAA;EACQ,6BAAA;;AYtHV;EACE,aAAa,sBAAb;EACA,qDAAA;EACA,2TAAA;;AAOF;EACE,kBAAA;EACA,QAAA;EACA,qBAAA;EACA,aAAa,sBAAb;EACA,kBAAA;EACA,mBAAA;EACA,cAAA;EACA,mCAAA;EACA,kCAAA;;AAIkC,mBAAC;EAAU,SAAS,KAAT;;AACX,eAAC;EAAU,SAAS,KAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,aAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,aAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,cAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,cAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,cAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,wBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,yBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,wBAAC;EAAU,SAAS,OAAT;;AACX,wBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,wBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,wBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,wBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,2BAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,wBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,0BAAC;EAAU,SAAS,OAAT;;AACX,4BAAC;EAAU,SAAS,OAAT;;AACX,cAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,6BAAC;EAAU,SAAS,OAAT;;AACX,4BAAC;EAAU,SAAS,OAAT;;AACX,0BAAC;EAAU,SAAS,OAAT;;AACX,4BAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,cAAC;EAAU,SAAS,OAAT;;AACX,cAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,2BAAC;EAAU,SAAS,OAAT;;AACX,+BAAC;EAAU,SAAS,OAAT;;AACX,wBAAC;EAAU,SAAS,OAAT;;AACX,4BAAC;EAAU,SAAS,OAAT;;AACX,6BAAC;EAAU,SAAS,OAAT;;AACX,iCAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,wBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,eAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,wBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,kBAAC;EAAU,SAAS,OAAT;;AACX,iBAAC;EAAU,SAAS,OAAT;;AACX,qBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,gBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,mBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,sBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,oBAAC;EAAU,SAAS,OAAT;;AACX,yBAAC;EAAU,SAAS,OAAT;;AACX,4BAAC;EAAU,SAAS,OAAT;;AACX,yBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,uBAAC;EAAU,SAAS,OAAT;;AACX,yBAAC;EAAU,SAAS,OAAT;;AClO/C;EACE,qBAAA;EACA,QAAA;EACA,SAAA;EACA,gBAAA;EACA,sBAAA;EACA,qBAAA;EACA,mCAAA;EACA,kCAAA;;AAIF;EACE,kBAAA;;AAIF,gBAAgB;EACd,UAAA;;AAIF;EACE,kBAAA;EACA,SAAA;EACA,OAAA;EACA,aAAA;EACA,aAAA;EACA,WAAA;EACA,gBAAA;EACA,cAAA;EACA,eAAA;EACA,gBAAA;EACA,eAAA;EACA,yBAAA;EACA,yBAAA;EACA,qCAAA;EACA,kBAAA;Eb8EA,mDAAA;EACQ,2CAAA;Ea7ER,4BAAA;;AAKA,cAAC;EACC,QAAA;EACA,UAAA;;AAxBJ,cA4BE;EboVA,WAAA;EACA,aAAA;EACA,gBAAA;EACA,yBAAA;;AanXF,cAiCE,KAAK;EACH,cAAA;EACA,iBAAA;EACA,WAAA;EACA,mBAAA;EACA,uBAAA;EACA,cAAA;EACA,mBAAA;;AAMF,cADa,KAAK,IACjB;AACD,cAFa,KAAK,IAEjB;EACC,qBAAA;EACA,cAAA;EACA,yBAAA;;AAMF,cADa,UAAU;AAEvB,cAFa,UAAU,IAEtB;AACD,cAHa,UAAU,IAGtB;EACC,cAAA;EACA,qBAAA;EACA,UAAA;EACA,yBAAA;;AASF,cADa,YAAY;AAEzB,cAFa,YAAY,IAExB;AACD,cAHa,YAAY,IAGxB;EACC,cAAA;;AAKF,cADa,YAAY,IACxB;AACD,cAFa,YAAY,IAExB;EACC,qBAAA;EACA,6BAAA;EACA,sBAAA;EbkPF,mEAAA;EahPE,mBAAA;;AAKJ,KAEE;EACE,cAAA;;AAHJ,KAOE;EACE,UAAA;;AAQJ;EACE,UAAA;EACA,QAAA;;AAQF;EACE,OAAA;EACA,WAAA;;AAIF;EACE,cAAA;EACA,iBAAA;EACA,eAAA;EACA,uBAAA;EACA,cAAA;;AAIF;EACE,eAAA;EACA,OAAA;EACA,QAAA;EACA,SAAA;EACA,MAAA;EACA,YAAA;;AAIF,WAAY;EACV,QAAA;EACA,UAAA;;AAQF,OAGE;AAFF,oBAAqB,UAEnB;EACE,aAAA;EACA,wBAAA;EACA,SAAS,EAAT;;AANJ,OASE;AARF,oBAAqB,UAQnB;EACE,SAAA;EACA,YAAA;EACA,kBAAA;;AAsBJ,QAb2C;EACzC,aACE;IAnEF,UAAA;IACA,QAAA;;EAiEA,aAME;IA9DF,OAAA;IACA,WAAA;;;AH7IF;AACA;EACE,kBAAA;EACA,qBAAA;EACA,sBAAA;;AAJF,UAKE;AAJF,mBAIE;EACE,kBAAA;EACA,WAAA;;AAEA,UAJF,OAIG;AAAD,mBAJF,OAIG;AACD,UALF,OAKG;AAAD,mBALF,OAKG;AACD,UANF,OAMG;AAAD,mBANF,OAMG;AACD,UAPF,OAOG;AAAD,mBAPF,OAOG;EACC,UAAA;;AAEF,UAVF,OAUG;AAAD,mBAVF,OAUG;EAEC,aAAA;;AAMN,UACE,KAAK;AADP,UAEE,KAAK;AAFP,UAGE,WAAW;AAHb,UAIE,WAAW;EACT,iBAAA;;AAKJ;EACE,iBAAA;;AADF,YAIE;AAJF,YAKE;EACE,WAAA;;AANJ,YAQE;AARF,YASE;AATF,YAUE;EACE,gBAAA;;AAIJ,UAAW,OAAM,IAAI,cAAc,IAAI,aAAa,IAAI;EACtD,gBAAA;;AAIF,UAAW,OAAM;EACf,cAAA;;AACA,UAFS,OAAM,YAEd,IAAI,aAAa,IAAI;EV2CtB,6BAAA;EACG,0BAAA;;AUvCL,UAAW,OAAM,WAAW,IAAI;AAChC,UAAW,mBAAkB,IAAI;EV6C/B,4BAAA;EACG,yBAAA;;AUzCL,UAAW;EACT,WAAA;;AAEF,UAAW,aAAY,IAAI,cAAc,IAAI,aAAc;EACzD,gBAAA;;AAEF,UAAW,aAAY,YACrB,OAAM;AADR,UAAW,aAAY,YAErB;EVwBA,6BAAA;EACG,0BAAA;;AUrBL,UAAW,aAAY,WAAY,OAAM;EV4BvC,4BAAA;EACG,yBAAA;;AUxBL,UAAW,iBAAgB;AAC3B,UAAU,KAAM;EACd,UAAA;;AAiBF,UAAW,OAAO;EAChB,iBAAA;EACA,kBAAA;;AAEF,UAAW,UAAU;EACnB,kBAAA;EACA,mBAAA;;AAKF,UAAU,KAAM;EVGd,wDAAA;EACQ,gDAAA;;AUAR,UAJQ,KAAM,iBAIb;EVDD,wBAAA;EACQ,gBAAA;;AUOV,IAAK;EACH,cAAA;;AAGF,OAAQ;EACN,uBAAA;EACA,sBAAA;;AAGF,OAAQ,QAAQ;EACd,uBAAA;;AAOF,mBACE;AADF,mBAEE;AAFF,mBAGE,aAAa;EACX,cAAA;EACA,WAAA;EACA,WAAA;EACA,eAAA;;AAPJ,mBAWE,aAEE;EACE,WAAA;;AAdN,mBAkBE,OAAO;AAlBT,mBAmBE,OAAO;AAnBT,mBAoBE,aAAa;AApBf,mBAqBE,aAAa;EACX,gBAAA;EACA,cAAA;;AAKF,mBADkB,OACjB,IAAI,cAAc,IAAI;EACrB,gBAAA;;AAEF,mBAJkB,OAIjB,YAAY,IAAI;EACf,4BAAA;EVvEF,6BAAA;EACC,4BAAA;;AUyED,mBARkB,OAQjB,WAAW,IAAI;EACd,8BAAA;EVnFF,0BAAA;EACC,yBAAA;;AUsFH,mBAAoB,aAAY,IAAI,cAAc,IAAI,aAAc;EAClE,gBAAA;;AAEF,mBAAoB,aAAY,YAAY,IAAI,aAC9C,OAAM;AADR,mBAAoB,aAAY,YAAY,IAAI,aAE9C;EVpFA,6BAAA;EACC,4BAAA;;AUuFH,mBAAoB,aAAY,WAAW,IAAI,cAAe,OAAM;EVhGlE,0BAAA;EACC,yBAAA;;AUwGH;EACE,cAAA;EACA,WAAA;EACA,mBAAA;EACA,yBAAA;;AAJF,oBAKE;AALF,oBAME;EACE,WAAA;EACA,mBAAA;EACA,SAAA;;AATJ,oBAWE,aAAa;EACX,WAAA;;AAMJ,uBAAwB,OAAO,QAAO;AACtC,uBAAwB,OAAO,QAAO;EACpC,aAAA;;AI1NF;EACE,kBAAA;EACA,cAAA;EACA,yBAAA;;AAGA,YAAC;EACC,WAAA;EACA,eAAA;EACA,gBAAA;;AATJ,YAYE;EAGE,kBAAA;EACA,UAAA;EAKA,WAAA;EAEA,WAAA;EACA,gBAAA;;AASJ,eAAgB;AAChB,eAAgB;AAChB,eAAgB,mBAAmB;Edw2BjC,YAAA;EACA,kBAAA;EACA,eAAA;EACA,iBAAA;EACA,kBAAA;;AAEA,MAAM,ech3BQ;Adg3Bd,MAAM,ec/2BQ;Ad+2Bd,MAAM,ec92BQ,mBAAmB;Ed+2B/B,YAAA;EACA,iBAAA;;AAGF,QAAQ,ecr3BM;Adq3Bd,QAAQ,ecp3BM;Ado3Bd,QAAQ,ecn3BM,mBAAmB;Ado3BjC,MAAM,UAAU,ect3BF;Ads3Bd,MAAM,UAAU,ecr3BF;Adq3Bd,MAAM,UAAU,ecp3BF,mBAAmB;Edq3B/B,YAAA;;Acp3BJ,eAAgB;AAChB,eAAgB;AAChB,eAAgB,mBAAmB;Edq2BjC,YAAA;EACA,iBAAA;EACA,eAAA;EACA,gBAAA;EACA,kBAAA;;AAEA,MAAM,ec72BQ;Ad62Bd,MAAM,ec52BQ;Ad42Bd,MAAM,ec32BQ,mBAAmB;Ed42B/B,YAAA;EACA,iBAAA;;AAGF,QAAQ,ecl3BM;Adk3Bd,QAAQ,ecj3BM;Adi3Bd,QAAQ,ech3BM,mBAAmB;Adi3BjC,MAAM,UAAU,ecn3BF;Adm3Bd,MAAM,UAAU,ecl3BF;Adk3Bd,MAAM,UAAU,ecj3BF,mBAAmB;Edk3B/B,YAAA;;Ac72BJ;AACA;AACA,YAAa;EACX,mBAAA;;AAEA,kBAAC,IAAI,cAAc,IAAI;AAAvB,gBAAC,IAAI,cAAc,IAAI;AAAvB,YAHW,cAGV,IAAI,cAAc,IAAI;EACrB,gBAAA;;AAIJ;AACA;EACE,SAAA;EACA,mBAAA;EACA,sBAAA;;AAKF;EACE,iBAAA;EACA,eAAA;EACA,mBAAA;EACA,cAAA;EACA,cAAA;EACA,kBAAA;EACA,yBAAA;EACA,yBAAA;EACA,kBAAA;;AAGA,kBAAC;EACC,iBAAA;EACA,eAAA;EACA,kBAAA;;AAEF,kBAAC;EACC,kBAAA;EACA,eAAA;EACA,kBAAA;;AApBJ,kBAwBE,MAAK;AAxBP,kBAyBE,MAAK;EACH,aAAA;;AAKJ,YAAa,cAAa;AAC1B,kBAAkB;AAClB,gBAAgB,YAAa;AAC7B,gBAAgB,YAAa,aAAa;AAC1C,gBAAgB,YAAa;AAC7B,gBAAgB,WAAY,OAAM,IAAI,aAAa,IAAI;AACvD,gBAAgB,WAAY,aAAY,IAAI,aAAc;EdFxD,6BAAA;EACG,0BAAA;;AcIL,kBAAkB;EAChB,eAAA;;AAEF,YAAa,cAAa;AAC1B,kBAAkB;AAClB,gBAAgB,WAAY;AAC5B,gBAAgB,WAAY,aAAa;AACzC,gBAAgB,WAAY;AAC5B,gBAAgB,YAAa,OAAM,IAAI;AACvC,gBAAgB,YAAa,aAAY,IAAI,cAAe;EdN1D,4BAAA;EACG,yBAAA;;AcQL,kBAAkB;EAChB,cAAA;;AAKF;EACE,kBAAA;EAGA,YAAA;EACA,mBAAA;;AALF,gBASE;EACE,kBAAA;;AAVJ,gBASE,OAEE;EACE,iBAAA;;AAGF,gBANF,OAMG;AACD,gBAPF,OAOG;AACD,gBARF,OAQG;EACC,UAAA;;AAKJ,gBAAC,YACC;AADF,gBAAC,YAEC;EACE,kBAAA;;AAGJ,gBAAC,WACC;AADF,gBAAC,WAEC;EACE,iBAAA;;ACtJN;EACE,gBAAA;EACA,eAAA;EACA,gBAAA;;AAHF,IAME;EACE,kBAAA;EACA,cAAA;;AARJ,IAME,KAIE;EACE,kBAAA;EACA,cAAA;EACA,kBAAA;;AACA,IARJ,KAIE,IAIG;AACD,IATJ,KAIE,IAKG;EACC,qBAAA;EACA,yBAAA;;AAKJ,IAhBF,KAgBG,SAAU;EACT,cAAA;;AAEA,IAnBJ,KAgBG,SAAU,IAGR;AACD,IApBJ,KAgBG,SAAU,IAIR;EACC,cAAA;EACA,qBAAA;EACA,6BAAA;EACA,mBAAA;;AAOJ,IADF,MAAM;AAEJ,IAFF,MAAM,IAEH;AACD,IAHF,MAAM,IAGH;EACC,yBAAA;EACA,qBAAA;;AAzCN,IAkDE;EfkVA,WAAA;EACA,aAAA;EACA,gBAAA;EACA,yBAAA;;AevYF,IAyDE,KAAK,IAAI;EACP,eAAA;;AASJ;EACE,gCAAA;;AADF,SAEE;EACE,WAAA;EAEA,mBAAA;;AALJ,SAEE,KAME;EACE,iBAAA;EACA,uBAAA;EACA,6BAAA;EACA,0BAAA;;AACA,SAXJ,KAME,IAKG;EACC,qCAAA;;AAMF,SAlBJ,KAiBG,OAAQ;AAEP,SAnBJ,KAiBG,OAAQ,IAEN;AACD,SApBJ,KAiBG,OAAQ,IAGN;EACC,cAAA;EACA,yBAAA;EACA,yBAAA;EACA,gCAAA;EACA,eAAA;;AAKN,SAAC;EAqDD,WAAA;EA8BA,gBAAA;;AAnFA,SAAC,cAuDD;EACE,WAAA;;AAxDF,SAAC,cAuDD,KAEG;EACC,kBAAA;EACA,kBAAA;;AA3DJ,SAAC,cA+DD,YAAY;EACV,SAAA;EACA,UAAA;;AAYJ,QATqC;EASrC,SA7EG,cAqEC;IACE,mBAAA;IACA,SAAA;;EAMN,SA7EG,cAqEC,KAGE;IACE,gBAAA;;;AAzEN,SAAC,cAqFD,KAAK;EAEH,eAAA;EACA,kBAAA;;AAxFF,SAAC,cA2FD,UAAU;AA3FV,SAAC,cA4FD,UAAU,IAAG;AA5Fb,SAAC,cA6FD,UAAU,IAAG;EACX,yBAAA;;AAcJ,QAXqC;EAWrC,SA5GG,cAkGC,KAAK;IACH,gCAAA;IACA,0BAAA;;EAQN,SA5GG,cAsGC,UAAU;EAMd,SA5GG,cAuGC,UAAU,IAAG;EAKjB,SA5GG,cAwGC,UAAU,IAAG;IACX,4BAAA;;;AAhGN,UACE;EACE,WAAA;;AAFJ,UACE,KAIE;EACE,kBAAA;;AANN,UACE,KAOE;EACE,gBAAA;;AAKA,UAbJ,KAYG,OAAQ;AAEP,UAdJ,KAYG,OAAQ,IAEN;AACD,UAfJ,KAYG,OAAQ,IAGN;EACC,cAAA;EACA,yBAAA;;AAQR,YACE;EACE,WAAA;;AAFJ,YACE,KAEE;EACE,eAAA;EACA,cAAA;;AAYN;EACE,WAAA;;AADF,cAGE;EACE,WAAA;;AAJJ,cAGE,KAEG;EACC,kBAAA;EACA,kBAAA;;AAPN,cAWE,YAAY;EACV,SAAA;EACA,UAAA;;AAYJ,QATqC;EASrC,cARI;IACE,mBAAA;IACA,SAAA;;EAMN,cARI,KAGE;IACE,gBAAA;;;AASR;EACE,gBAAA;;AADF,mBAGE,KAAK;EAEH,eAAA;EACA,kBAAA;;AANJ,mBASE,UAAU;AATZ,mBAUE,UAAU,IAAG;AAVf,mBAWE,UAAU,IAAG;EACX,yBAAA;;AAcJ,QAXqC;EAWrC,mBAVI,KAAK;IACH,gCAAA;IACA,0BAAA;;EAQN,mBANI,UAAU;EAMd,mBALI,UAAU,IAAG;EAKjB,mBAJI,UAAU,IAAG;IACX,4BAAA;;;AAUN,YACE;EACE,aAAA;;AAFJ,YAIE;EACE,cAAA;;AASJ,SAAU;EAER,gBAAA;Ef3IA,0BAAA;EACC,yBAAA;;AgB1FH;EACE,kBAAA;EACA,gBAAA;EACA,mBAAA;EACA,6BAAA;;AAQF,QAH6C;EAG7C;IAFI,kBAAA;;;AAgBJ,QAH6C;EAG7C;IAFI,WAAA;;;AAeJ;EACE,iBAAA;EACA,mBAAA;EACA,mBAAA;EACA,kBAAA;EACA,iCAAA;EACA,kDAAA;EAEA,iCAAA;;AAEA,gBAAC;EACC,gBAAA;;AA4BJ,QAzB6C;EAyB7C;IAxBI,WAAA;IACA,aAAA;IACA,gBAAA;;EAEA,gBAAC;IACC,yBAAA;IACA,uBAAA;IACA,iBAAA;IACA,4BAAA;;EAGF,gBAAC;IACC,mBAAA;;EAKF,iBAAkB;EAClB,kBAAmB;EACnB,oBAAqB;IACnB,eAAA;IACA,gBAAA;;;AAUN,UAEE;AADF,gBACE;AAFF,UAGE;AAFF,gBAEE;EACE,mBAAA;EACA,kBAAA;;AAMF,QAJ6C;EAI7C,UATA;EASA,gBATA;EASA,UARA;EAQA,gBARA;IAKI,eAAA;IACA,cAAA;;;AAaN;EACE,aAAA;EACA,qBAAA;;AAKF,QAH6C;EAG7C;IAFI,gBAAA;;;AAKJ;AACA;EACE,eAAA;EACA,QAAA;EACA,OAAA;EACA,aAAA;;AAMF,QAH6C;EAG7C;EAAA;IAFI,gBAAA;;;AAGJ;EACE,MAAA;EACA,qBAAA;;AAEF;EACE,SAAA;EACA,gBAAA;EACA,qBAAA;;AAMF;EACE,WAAA;EACA,kBAAA;EACA,eAAA;EACA,iBAAA;EACA,YAAA;;AAEA,aAAC;AACD,aAAC;EACC,qBAAA;;AASJ,QAN6C;EACzC,OAAQ,aAAa;EACrB,OAAQ,mBAAmB;IACzB,kBAAA;;;AAWN;EACE,kBAAA;EACA,YAAA;EACA,kBAAA;EACA,iBAAA;EhBsaA,eAAA;EACA,kBAAA;EgBraA,6BAAA;EACA,sBAAA;EACA,6BAAA;EACA,kBAAA;;AAIA,cAAC;EACC,aAAA;;AAdJ,cAkBE;EACE,cAAA;EACA,WAAA;EACA,WAAA;EACA,kBAAA;;AAtBJ,cAwBE,UAAU;EACR,eAAA;;AAMJ,QAH6C;EAG7C;IAFI,aAAA;;;AAUJ;EACE,mBAAA;;AADF,WAGE,KAAK;EACH,iBAAA;EACA,oBAAA;EACA,iBAAA;;AA2BF,QAxB+C;EAwB/C,WAtBE,MAAM;IACJ,gBAAA;IACA,WAAA;IACA,WAAA;IACA,aAAA;IACA,6BAAA;IACA,SAAA;IACA,gBAAA;;EAeJ,WAtBE,MAAM,eAQJ,KAAK;EAcT,WAtBE,MAAM,eASJ;IACE,0BAAA;;EAYN,WAtBE,MAAM,eAYJ,KAAK;IACH,iBAAA;;EACA,WAdJ,MAAM,eAYJ,KAAK,IAEF;EACD,WAfJ,MAAM,eAYJ,KAAK,IAGF;IACC,sBAAA;;;AAuBV,QAhB6C;EAgB7C;IAfI,WAAA;IACA,SAAA;;EAcJ,WAZI;IACE,WAAA;;EAWN,WAZI,KAEE;IACE,iBAAA;IACA,oBAAA;;EAIJ,WAAC,aAAa;IACZ,mBAAA;;;AAkBN,QAN2C;EACzC;ICnQA,sBAAA;;EDoQA;ICvQA,uBAAA;;;ADgRF;EACE,kBAAA;EACA,mBAAA;EACA,kBAAA;EACA,iCAAA;EACA,oCAAA;EhB3KA,4FAAA;EACQ,oFAAA;EAkeR,eAAA;EACA,kBAAA;;AQ3NF,QAjDqC;EAiDrC,YA/CI;IACE,qBAAA;IACA,gBAAA;IACA,sBAAA;;EA4CN,YAxCI;IACE,qBAAA;IACA,WAAA;IACA,sBAAA;;EAqCN,YAlCI,aAAa;IACX,WAAA;;EAiCN,YA9BI;IACE,gBAAA;IACA,sBAAA;;EA4BN,YAtBI;EAsBJ,YArBI;IACE,qBAAA;IACA,aAAA;IACA,gBAAA;IACA,eAAA;IACA,sBAAA;;EAgBN,YAdI,OAAO,MAAK;EAchB,YAbI,UAAU,MAAK;IACb,WAAA;IACA,cAAA;;EAWN,YAJI,cAAc;IACZ,MAAA;;;AQhFJ,QAHiD;EAGjD,YAJA;IAEI,kBAAA;;;AAsBN,QAd6C;EAc7C;IAbI,WAAA;IACA,SAAA;IACA,cAAA;IACA,eAAA;IACA,cAAA;IACA,iBAAA;IhBlMF,wBAAA;IACQ,gBAAA;;EgBqMN,YAAC,aAAa;IACZ,mBAAA;;;AASN,WAAY,KAAK;EACf,aAAA;EhBvOA,0BAAA;EACC,yBAAA;;AgB0OH,oBAAqB,YAAY,KAAK;EhBnOpC,6BAAA;EACC,4BAAA;;AgB2OH;EhBqQE,eAAA;EACA,kBAAA;;AgBnQA,WAAC;EhBkQD,gBAAA;EACA,mBAAA;;AgBhQA,WAAC;EhB+PD,gBAAA;EACA,mBAAA;;AgBtPF;EhBqPE,gBAAA;EACA,mBAAA;;AgBzOF,QAV6C;EAU7C;IATI,WAAA;IACA,iBAAA;IACA,kBAAA;;EAGA,YAAC,aAAa;IACZ,eAAA;;;AASN;EACE,yBAAA;EACA,qBAAA;;AAFF,eAIE;EACE,cAAA;;AACA,eAFF,cAEG;AACD,eAHF,cAGG;EACC,cAAA;EACA,6BAAA;;AATN,eAaE;EACE,cAAA;;AAdJ,eAiBE,YACE,KAAK;EACH,cAAA;;AAEA,eAJJ,YACE,KAAK,IAGF;AACD,eALJ,YACE,KAAK,IAIF;EACC,cAAA;EACA,6BAAA;;AAIF,eAXJ,YAUE,UAAU;AAER,eAZJ,YAUE,UAAU,IAEP;AACD,eAbJ,YAUE,UAAU,IAGP;EACC,cAAA;EACA,yBAAA;;AAIF,eAnBJ,YAkBE,YAAY;AAEV,eApBJ,YAkBE,YAAY,IAET;AACD,eArBJ,YAkBE,YAAY,IAGT;EACC,cAAA;EACA,6BAAA;;AAxCR,eA6CE;EACE,qBAAA;;AACA,eAFF,eAEG;AACD,eAHF,eAGG;EACC,yBAAA;;AAjDN,eA6CE,eAME;EACE,yBAAA;;AApDN,eAwDE;AAxDF,eAyDE;EACE,qBAAA;;AAOE,eAHJ,YAEE,QAAQ;AAEN,eAJJ,YAEE,QAAQ,IAEL;AACD,eALJ,YAEE,QAAQ,IAGL;EACC,yBAAA;EACA,cAAA;;AAiCN,QA7BiD;EA6BjD,eAxCA,YAaI,MAAM,eACJ,KAAK;IACH,cAAA;;EACA,eAhBR,YAaI,MAAM,eACJ,KAAK,IAEF;EACD,eAjBR,YAaI,MAAM,eACJ,KAAK,IAGF;IACC,cAAA;IACA,6BAAA;;EAIF,eAvBR,YAaI,MAAM,eASJ,UAAU;EAER,eAxBR,YAaI,MAAM,eASJ,UAAU,IAEP;EACD,eAzBR,YAaI,MAAM,eASJ,UAAU,IAGP;IACC,cAAA;IACA,yBAAA;;EAIF,eA/BR,YAaI,MAAM,eAiBJ,YAAY;EAEV,eAhCR,YAaI,MAAM,eAiBJ,YAAY,IAET;EACD,eAjCR,YAaI,MAAM,eAiBJ,YAAY,IAGT;IACC,cAAA;IACA,6BAAA;;;AAjGZ,eA6GE;EACE,cAAA;;AACA,eAFF,aAEG;EACC,cAAA;;AAQN;EACE,yBAAA;EACA,qBAAA;;AAFF,eAIE;EACE,cAAA;;AACA,eAFF,cAEG;AACD,eAHF,cAGG;EACC,cAAA;EACA,6BAAA;;AATN,eAaE;EACE,cAAA;;AAdJ,eAiBE,YACE,KAAK;EACH,cAAA;;AAEA,eAJJ,YACE,KAAK,IAGF;AACD,eALJ,YACE,KAAK,IAIF;EACC,cAAA;EACA,6BAAA;;AAIF,eAXJ,YAUE,UAAU;AAER,eAZJ,YAUE,UAAU,IAEP;AACD,eAbJ,YAUE,UAAU,IAGP;EACC,cAAA;EACA,yBAAA;;AAIF,eAnBJ,YAkBE,YAAY;AAEV,eApBJ,YAkBE,YAAY,IAET;AACD,eArBJ,YAkBE,YAAY,IAGT;EACC,cAAA;EACA,6BAAA;;AAxCR,eA8CE;EACE,qBAAA;;AACA,eAFF,eAEG;AACD,eAHF,eAGG;EACC,yBAAA;;AAlDN,eA8CE,eAME;EACE,yBAAA;;AArDN,eAyDE;AAzDF,eA0DE;EACE,qBAAA;;AAME,eAFJ,YACE,QAAQ;AAEN,eAHJ,YACE,QAAQ,IAEL;AACD,eAJJ,YACE,QAAQ,IAGL;EACC,yBAAA;EACA,cAAA;;AAuCN,QAnCiD;EAmCjD,eA7CA,YAYI,MAAM,eACJ;IACE,qBAAA;;EA+BR,eA7CA,YAYI,MAAM,eAIJ;IACE,yBAAA;;EA4BR,eA7CA,YAYI,MAAM,eAOJ,KAAK;IACH,cAAA;;EACA,eArBR,YAYI,MAAM,eAOJ,KAAK,IAEF;EACD,eAtBR,YAYI,MAAM,eAOJ,KAAK,IAGF;IACC,cAAA;IACA,6BAAA;;EAIF,eA5BR,YAYI,MAAM,eAeJ,UAAU;EAER,eA7BR,YAYI,MAAM,eAeJ,UAAU,IAEP;EACD,eA9BR,YAYI,MAAM,eAeJ,UAAU,IAGP;IACC,cAAA;IACA,yBAAA;;EAIF,eApCR,YAYI,MAAM,eAuBJ,YAAY;EAEV,eArCR,YAYI,MAAM,eAuBJ,YAAY,IAET;EACD,eAtCR,YAYI,MAAM,eAuBJ,YAAY,IAGT;IACC,cAAA;IACA,6BAAA;;;AAvGZ,eA8GE;EACE,cAAA;;AACA,eAFF,aAEG;EACC,cAAA;;AE9lBN;EACE,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,yBAAA;EACA,kBAAA;;AALF,WAOE;EACE,qBAAA;;AARJ,WAOE,KAGE,KAAI;EACF,SAAS,QAAT;EACA,cAAA;EACA,cAAA;;AAbN,WAiBE;EACE,cAAA;;ACpBJ;EACE,qBAAA;EACA,eAAA;EACA,cAAA;EACA,kBAAA;;AAJF,WAME;EACE,eAAA;;AAPJ,WAME,KAEE;AARJ,WAME,KAGE;EACE,kBAAA;EACA,WAAA;EACA,iBAAA;EACA,uBAAA;EACA,qBAAA;EACA,cAAA;EACA,yBAAA;EACA,yBAAA;EACA,iBAAA;;AAEF,WAdF,KAcG,YACC;AADF,WAdF,KAcG,YAEC;EACE,cAAA;EnBqFN,8BAAA;EACG,2BAAA;;AmBlFD,WArBF,KAqBG,WACC;AADF,WArBF,KAqBG,WAEC;EnBuEJ,+BAAA;EACG,4BAAA;;AmBhED,WAFF,KAAK,IAEF;AAAD,WADF,KAAK,OACF;AACD,WAHF,KAAK,IAGF;AAAD,WAFF,KAAK,OAEF;EACC,cAAA;EACA,yBAAA;EACA,qBAAA;;AAMF,WAFF,UAAU;AAER,WADF,UAAU;AAER,WAHF,UAAU,IAGP;AAAD,WAFF,UAAU,OAEP;AACD,WAJF,UAAU,IAIP;AAAD,WAHF,UAAU,OAGP;EACC,UAAA;EACA,cAAA;EACA,yBAAA;EACA,qBAAA;EACA,eAAA;;AAtDN,WA0DE,YACE;AA3DJ,WA0DE,YAEE,OAAM;AA5DV,WA0DE,YAGE,OAAM;AA7DV,WA0DE,YAIE;AA9DJ,WA0DE,YAKE,IAAG;AA/DP,WA0DE,YAME,IAAG;EACD,cAAA;EACA,yBAAA;EACA,qBAAA;EACA,mBAAA;;AASN,cnBodE,KACE;AmBrdJ,cnBodE,KAEE;EACE,kBAAA;EACA,eAAA;;AAEF,cANF,KAMG,YACC;AADF,cANF,KAMG,YAEC;EA7bJ,8BAAA;EACG,2BAAA;;AAgcD,cAZF,KAYG,WACC;AADF,cAZF,KAYG,WAEC;EA3cJ,+BAAA;EACG,4BAAA;;AmBnBL,cnB+cE,KACE;AmBhdJ,cnB+cE,KAEE;EACE,iBAAA;EACA,eAAA;;AAEF,cANF,KAMG,YACC;AADF,cANF,KAMG,YAEC;EA7bJ,8BAAA;EACG,2BAAA;;AAgcD,cAZF,KAYG,WACC;AADF,cAZF,KAYG,WAEC;EA3cJ,+BAAA;EACG,4BAAA;;AoBnGL;EACE,eAAA;EACA,cAAA;EACA,gBAAA;EACA,kBAAA;;AAJF,MAME;EACE,eAAA;;AAPJ,MAME,GAEE;AARJ,MAME,GAGE;EACE,qBAAA;EACA,iBAAA;EACA,yBAAA;EACA,yBAAA;EACA,mBAAA;;AAdN,MAME,GAWE,IAAG;AAjBP,MAME,GAYE,IAAG;EACD,qBAAA;EACA,yBAAA;;AApBN,MAwBE,MACE;AAzBJ,MAwBE,MAEE;EACE,YAAA;;AA3BN,MA+BE,UACE;AAhCJ,MA+BE,UAEE;EACE,WAAA;;AAlCN,MAsCE,UACE;AAvCJ,MAsCE,UAEE,IAAG;AAxCP,MAsCE,UAGE,IAAG;AAzCP,MAsCE,UAIE;EACE,cAAA;EACA,yBAAA;EACA,mBAAA;;AC9CN;EACE,eAAA;EACA,uBAAA;EACA,cAAA;EACA,iBAAA;EACA,cAAA;EACA,cAAA;EACA,kBAAA;EACA,mBAAA;EACA,wBAAA;EACA,oBAAA;;AAIE,MADD,MACE;AACD,MAFD,MAEE;EACC,cAAA;EACA,qBAAA;EACA,eAAA;;AAKJ,MAAC;EACC,aAAA;;AAIF,IAAK;EACH,kBAAA;EACA,SAAA;;AAOJ;ErBmhBE,yBAAA;;AAEE,cADD,MACE;AACD,cAFD,MAEE;EACC,yBAAA;;AqBnhBN;ErB+gBE,yBAAA;;AAEE,cADD,MACE;AACD,cAFD,MAEE;EACC,yBAAA;;AqB/gBN;ErB2gBE,yBAAA;;AAEE,cADD,MACE;AACD,cAFD,MAEE;EACC,yBAAA;;AqB3gBN;ErBugBE,yBAAA;;AAEE,WADD,MACE;AACD,WAFD,MAEE;EACC,yBAAA;;AqBvgBN;ErBmgBE,yBAAA;;AAEE,cADD,MACE;AACD,cAFD,MAEE;EACC,yBAAA;;AqBngBN;ErB+fE,yBAAA;;AAEE,aADD,MACE;AACD,aAFD,MAEE;EACC,yBAAA;;AsB1jBN;EACE,qBAAA;EACA,eAAA;EACA,gBAAA;EACA,eAAA;EACA,iBAAA;EACA,cAAA;EACA,cAAA;EACA,wBAAA;EACA,mBAAA;EACA,kBAAA;EACA,yBAAA;EACA,mBAAA;;AAGA,MAAC;EACC,aAAA;;AAIF,IAAK;EACH,kBAAA;EACA,SAAA;;AAEF,OAAQ;EACN,MAAA;EACA,gBAAA;;AAMF,CADD,MACE;AACD,CAFD,MAEE;EACC,cAAA;EACA,qBAAA;EACA,eAAA;;AAKJ,CAAC,gBAAgB,OAAQ;AACzB,UAAW,UAAU,IAAI;EACvB,cAAA;EACA,yBAAA;;AAEF,UAAW,KAAK,IAAI;EAClB,gBAAA;;AChDF;EACE,aAAA;EACA,mBAAA;EACA,cAAA;EACA,yBAAA;;AAJF,UAME;AANF,UAOE;EACE,cAAA;;AARJ,UAUE;EACE,mBAAA;EACA,eAAA;EACA,gBAAA;;AAGF,UAAW;EACT,kBAAA;;AAjBJ,UAoBE;EACE,eAAA;;AAiBJ,mBAdgD;EAchD;IAbI,iBAAA;IACA,oBAAA;;EAEA,UAAW;IACT,kBAAA;IACA,mBAAA;;EAQN,UALI;EAKJ,UAJI;IACE,eAAA;;;ArBlCN;EACE,cAAA;EACA,YAAA;EACA,mBAAA;EACA,uBAAA;EACA,yBAAA;EACA,yBAAA;EACA,kBAAA;EFkHA,wCAAA;EACQ,gCAAA;;AE1HV,UAUE;AAVF,UAWE,EAAE;EAEA,iBAAA;EACA,kBAAA;;AAIF,CAAC,UAAC;AACF,CAAC,UAAC;AACF,CAAC,UAAC;EACA,qBAAA;;AArBJ,UAyBE;EACE,YAAA;EACA,cAAA;;AsBzBJ;EACE,aAAA;EACA,mBAAA;EACA,6BAAA;EACA,kBAAA;;AAJF,MAOE;EACE,aAAA;EAEA,cAAA;;AAVJ,MAaE;EACE,iBAAA;;AAdJ,MAkBE;AAlBF,MAmBE;EACE,gBAAA;;AApBJ,MAsBE,IAAI;EACF,eAAA;;AAQJ;EACC,mBAAA;;AADD,kBAIE;EACE,kBAAA;EACA,SAAA;EACA,YAAA;EACA,cAAA;;AAQJ;ExBmXE,yBAAA;EACA,qBAAA;EACA,cAAA;;AwBrXF,cxBuXE;EACE,yBAAA;;AwBxXJ,cxB0XE;EACE,cAAA;;AwBxXJ;ExBgXE,yBAAA;EACA,qBAAA;EACA,cAAA;;AwBlXF,WxBoXE;EACE,yBAAA;;AwBrXJ,WxBuXE;EACE,cAAA;;AwBrXJ;ExB6WE,yBAAA;EACA,qBAAA;EACA,cAAA;;AwB/WF,cxBiXE;EACE,yBAAA;;AwBlXJ,cxBoXE;EACE,cAAA;;AwBlXJ;ExB0WE,yBAAA;EACA,qBAAA;EACA,cAAA;;AwB5WF,axB8WE;EACE,yBAAA;;AwB/WJ,axBiXE;EACE,cAAA;;AyBzaJ;EACE;IAAQ,2BAAA;;EACR;IAAQ,wBAAA;;;AAIV;EACE;IAAQ,2BAAA;;EACR;IAAQ,wBAAA;;;AASV;EACE,gBAAA;EACA,YAAA;EACA,mBAAA;EACA,yBAAA;EACA,kBAAA;EzB0FA,sDAAA;EACQ,8CAAA;;AyBtFV;EACE,WAAA;EACA,SAAA;EACA,YAAA;EACA,eAAA;EACA,iBAAA;EACA,cAAA;EACA,kBAAA;EACA,yBAAA;EzB6EA,sDAAA;EACQ,8CAAA;EAKR,mCAAA;EACQ,2BAAA;;AyB9EV,iBAAkB;EzBqSd,kBAAkB,2LAAlB;EACA,kBAAkB,mLAAlB;EyBpSF,0BAAA;;AAIF,SAAS,OAAQ;EzBoJf,0DAAA;EACQ,kDAAA;;AyB5IV;EzBkiBE,yBAAA;;AACA,iBAAkB;EA7QhB,kBAAkB,2LAAlB;EACA,kBAAkB,mLAAlB;;AyBnRJ;EzB8hBE,yBAAA;;AACA,iBAAkB;EA7QhB,kBAAkB,2LAAlB;EACA,kBAAkB,mLAAlB;;AyB/QJ;EzB0hBE,yBAAA;;AACA,iBAAkB;EA7QhB,kBAAkB,2LAAlB;EACA,kBAAkB,mLAAlB;;AyB3QJ;EzBshBE,yBAAA;;AACA,iBAAkB;EA7QhB,kBAAkB,2LAAlB;EACA,kBAAkB,mLAAlB;;A0B/UJ;AACA;EACE,gBAAA;EACA,OAAA;;AAIF;AACA,MAAO;EACL,gBAAA;;AAEF,MAAM;EACJ,aAAA;;AAIF;EACE,cAAA;;AAIF;EACE,eAAA;;AAOF,MACE;EACE,kBAAA;;AAFJ,MAIE;EACE,iBAAA;;AASJ;EACE,eAAA;EACA,gBAAA;;AC7CF;EAEE,mBAAA;EACA,eAAA;;AAQF;EACE,kBAAA;EACA,cAAA;EACA,kBAAA;EAEA,mBAAA;EACA,yBAAA;EACA,yBAAA;;AAGA,gBAAC;E3BqED,4BAAA;EACC,2BAAA;;A2BnED,gBAAC;EACC,gBAAA;E3ByEF,+BAAA;EACC,8BAAA;;A2BxFH,gBAmBE;EACE,YAAA;;AApBJ,gBAsBE,SAAS;EACP,iBAAA;;AAUJ,CAAC;EACC,cAAA;;AADF,CAAC,gBAGC;EACE,cAAA;;AAIF,CARD,gBAQE;AACD,CATD,gBASE;EACC,qBAAA;EACA,yBAAA;;AAIF,CAfD,gBAeE;AACD,CAhBD,gBAgBE,OAAO;AACR,CAjBD,gBAiBE,OAAO;EACN,UAAA;EACA,cAAA;EACA,yBAAA;EACA,qBAAA;;AANF,CAfD,gBAeE,OASC;AARF,CAhBD,gBAgBE,OAAO,MAQN;AAPF,CAjBD,gBAiBE,OAAO,MAON;EACE,cAAA;;AAVJ,CAfD,gBAeE,OAYC;AAXF,CAhBD,gBAgBE,OAAO,MAWN;AAVF,CAjBD,gBAiBE,OAAO,MAUN;EACE,cAAA;;A3BoYJ,iBAAiB;EACf,cAAA;EACA,yBAAA;;AAEA,CAAC,iBAJc;EAKb,cAAA;;AADF,CAAC,iBAJc,OAOb;EAA2B,cAAA;;AAE3B,CALD,iBAJc,OASZ;AACD,CAND,iBAJc,OAUZ;EACC,cAAA;EACA,yBAAA;;AAEF,CAVD,iBAJc,OAcZ;AACD,CAXD,iBAJc,OAeZ,OAAO;AACR,CAZD,iBAJc,OAgBZ,OAAO;EACN,WAAA;EACA,yBAAA;EACA,qBAAA;;AAnBN,iBAAiB;EACf,cAAA;EACA,yBAAA;;AAEA,CAAC,iBAJc;EAKb,cAAA;;AADF,CAAC,iBAJc,IAOb;EAA2B,cAAA;;AAE3B,CALD,iBAJc,IASZ;AACD,CAND,iBAJc,IAUZ;EACC,cAAA;EACA,yBAAA;;AAEF,CAVD,iBAJc,IAcZ;AACD,CAXD,iBAJc,IAeZ,OAAO;AACR,CAZD,iBAJc,IAgBZ,OAAO;EACN,WAAA;EACA,yBAAA;EACA,qBAAA;;AAnBN,iBAAiB;EACf,cAAA;EACA,yBAAA;;AAEA,CAAC,iBAJc;EAKb,cAAA;;AADF,CAAC,iBAJc,OAOb;EAA2B,cAAA;;AAE3B,CALD,iBAJc,OASZ;AACD,CAND,iBAJc,OAUZ;EACC,cAAA;EACA,yBAAA;;AAEF,CAVD,iBAJc,OAcZ;AACD,CAXD,iBAJc,OAeZ,OAAO;AACR,CAZD,iBAJc,OAgBZ,OAAO;EACN,WAAA;EACA,yBAAA;EACA,qBAAA;;AAnBN,iBAAiB;EACf,cAAA;EACA,yBAAA;;AAEA,CAAC,iBAJc;EAKb,cAAA;;AADF,CAAC,iBAJc,MAOb;EAA2B,cAAA;;AAE3B,CALD,iBAJc,MASZ;AACD,CAND,iBAJc,MAUZ;EACC,cAAA;EACA,yBAAA;;AAEF,CAVD,iBAJc,MAcZ;AACD,CAXD,iBAJc,MAeZ,OAAO;AACR,CAZD,iBAJc,MAgBZ,OAAO;EACN,WAAA;EACA,yBAAA;EACA,qBAAA;;A2BlYR;EACE,aAAA;EACA,kBAAA;;AAEF;EACE,gBAAA;EACA,gBAAA;;ACtGF;EACE,mBAAA;EACA,yBAAA;EACA,6BAAA;EACA,kBAAA;E5B+GA,iDAAA;EACQ,yCAAA;;A4B3GV;EACE,aAAA;;AAKF;EACE,kBAAA;EACA,oCAAA;E5B4EA,4BAAA;EACC,2BAAA;;A4B/EH,cAKE,YAAY;EACV,cAAA;;AAKJ;EACE,aAAA;EACA,gBAAA;EACA,eAAA;EACA,cAAA;;AAJF,YAME;EACE,cAAA;;AAKJ;EACE,kBAAA;EACA,yBAAA;EACA,6BAAA;E5B4DA,+BAAA;EACC,8BAAA;;A4BnDH,MACE;EACE,gBAAA;;AAFJ,MACE,cAGE;EACE,mBAAA;EACA,gBAAA;;AAIF,MATF,cASG,YACC,iBAAgB;EACd,aAAA;E5B8BN,4BAAA;EACC,2BAAA;;A4B1BC,MAhBF,cAgBG,WACC,iBAAgB;EACd,gBAAA;E5B+BN,+BAAA;EACC,8BAAA;;A4BzBH,cAAe,cACb,iBAAgB;EACd,mBAAA;;AAUJ,MACE;AADF,MAEE,oBAAoB;EAClB,gBAAA;;AAHJ,MAME,SAAQ;AANV,MAOE,oBAAmB,YAAa,SAAQ;E5BHxC,4BAAA;EACC,2BAAA;;A4BLH,MAME,SAAQ,YAIN,QAAO,YAEL,KAAI,YACF,GAAE;AAbV,MAOE,oBAAmB,YAAa,SAAQ,YAGtC,QAAO,YAEL,KAAI,YACF,GAAE;AAbV,MAME,SAAQ,YAKN,QAAO,YACL,KAAI,YACF,GAAE;AAbV,MAOE,oBAAmB,YAAa,SAAQ,YAItC,QAAO,YACL,KAAI,YACF,GAAE;AAbV,MAME,SAAQ,YAIN,QAAO,YAEL,KAAI,YAEF,GAAE;AAdV,MAOE,oBAAmB,YAAa,SAAQ,YAGtC,QAAO,YAEL,KAAI,YAEF,GAAE;AAdV,MAME,SAAQ,YAKN,QAAO,YACL,KAAI,YAEF,GAAE;AAdV,MAOE,oBAAmB,YAAa,SAAQ,YAItC,QAAO,YACL,KAAI,YAEF,GAAE;EACA,2BAAA;;AAfV,MAME,SAAQ,YAIN,QAAO,YAEL,KAAI,YAKF,GAAE;AAjBV,MAOE,oBAAmB,YAAa,SAAQ,YAGtC,QAAO,YAEL,KAAI,YAKF,GAAE;AAjBV,MAME,SAAQ,YAKN,QAAO,YACL,KAAI,YAKF,GAAE;AAjBV,MAOE,oBAAmB,YAAa,SAAQ,YAItC,QAAO,YACL,KAAI,YAKF,GAAE;AAjBV,MAME,SAAQ,YAIN,QAAO,YAEL,KAAI,YAMF,GAAE;AAlBV,MAOE,oBAAmB,YAAa,SAAQ,YAGtC,QAAO,YAEL,KAAI,YAMF,GAAE;AAlBV,MAME,SAAQ,YAKN,QAAO,YACL,KAAI,YAMF,GAAE;AAlBV,MAOE,oBAAmB,YAAa,SAAQ,YAItC,QAAO,YACL,KAAI,YAMF,GAAE;EACA,4BAAA;;AAnBV,MAyBE,SAAQ;AAzBV,MA0BE,oBAAmB,WAAY,SAAQ;E5BdvC,+BAAA;EACC,8BAAA;;A4BbH,MAyBE,SAAQ,WAIN,QAAO,WAEL,KAAI,WACF,GAAE;AAhCV,MA0BE,oBAAmB,WAAY,SAAQ,WAGrC,QAAO,WAEL,KAAI,WACF,GAAE;AAhCV,MAyBE,SAAQ,WAKN,QAAO,WACL,KAAI,WACF,GAAE;AAhCV,MA0BE,oBAAmB,WAAY,SAAQ,WAIrC,QAAO,WACL,KAAI,WACF,GAAE;AAhCV,MAyBE,SAAQ,WAIN,QAAO,WAEL,KAAI,WAEF,GAAE;AAjCV,MA0BE,oBAAmB,WAAY,SAAQ,WAGrC,QAAO,WAEL,KAAI,WAEF,GAAE;AAjCV,MAyBE,SAAQ,WAKN,QAAO,WACL,KAAI,WAEF,GAAE;AAjCV,MA0BE,oBAAmB,WAAY,SAAQ,WAIrC,QAAO,WACL,KAAI,WAEF,GAAE;EACA,8BAAA;;AAlCV,MAyBE,SAAQ,WAIN,QAAO,WAEL,KAAI,WAKF,GAAE;AApCV,MA0BE,oBAAmB,WAAY,SAAQ,WAGrC,QAAO,WAEL,KAAI,WAKF,GAAE;AApCV,MAyBE,SAAQ,WAKN,QAAO,WACL,KAAI,WAKF,GAAE;AApCV,MA0BE,oBAAmB,WAAY,SAAQ,WAIrC,QAAO,WACL,KAAI,WAKF,GAAE;AApCV,MAyBE,SAAQ,WAIN,QAAO,WAEL,KAAI,WAMF,GAAE;AArCV,MA0BE,oBAAmB,WAAY,SAAQ,WAGrC,QAAO,WAEL,KAAI,WAMF,GAAE;AArCV,MAyBE,SAAQ,WAKN,QAAO,WACL,KAAI,WAMF,GAAE;AArCV,MA0BE,oBAAmB,WAAY,SAAQ,WAIrC,QAAO,WACL,KAAI,WAMF,GAAE;EACA,+BAAA;;AAtCV,MA2CE,cAAc;AA3ChB,MA4CE,cAAc;EACZ,6BAAA;;AA7CJ,MA+CE,SAAS,QAAO,YAAa,KAAI,YAAa;AA/ChD,MAgDE,SAAS,QAAO,YAAa,KAAI,YAAa;EAC5C,aAAA;;AAjDJ,MAmDE;AAnDF,MAoDE,oBAAoB;EAClB,SAAA;;AArDJ,MAmDE,kBAGE,QAGE,KACE,KAAI;AA1DZ,MAoDE,oBAAoB,kBAElB,QAGE,KACE,KAAI;AA1DZ,MAmDE,kBAIE,QAEE,KACE,KAAI;AA1DZ,MAoDE,oBAAoB,kBAGlB,QAEE,KACE,KAAI;AA1DZ,MAmDE,kBAKE,QACE,KACE,KAAI;AA1DZ,MAoDE,oBAAoB,kBAIlB,QACE,KACE,KAAI;AA1DZ,MAmDE,kBAGE,QAGE,KAEE,KAAI;AA3DZ,MAoDE,oBAAoB,kBAElB,QAGE,KAEE,KAAI;AA3DZ,MAmDE,kBAIE,QAEE,KAEE,KAAI;AA3DZ,MAoDE,oBAAoB,kBAGlB,QAEE,KAEE,KAAI;AA3DZ,MAmDE,kBAKE,QACE,KAEE,KAAI;AA3DZ,MAoDE,oBAAoB,kBAIlB,QACE,KAEE,KAAI;EACF,cAAA;;AA5DV,MAmDE,kBAGE,QAGE,KAKE,KAAI;AA9DZ,MAoDE,oBAAoB,kBAElB,QAGE,KAKE,KAAI;AA9DZ,MAmDE,kBAIE,QAEE,KAKE,KAAI;AA9DZ,MAoDE,oBAAoB,kBAGlB,QAEE,KAKE,KAAI;AA9DZ,MAmDE,kBAKE,QACE,KAKE,KAAI;AA9DZ,MAoDE,oBAAoB,kBAIlB,QACE,KAKE,KAAI;AA9DZ,MAmDE,kBAGE,QAGE,KAME,KAAI;AA/DZ,MAoDE,oBAAoB,kBAElB,QAGE,KAME,KAAI;AA/DZ,MAmDE,kBAIE,QAEE,KAME,KAAI;AA/DZ,MAoDE,oBAAoB,kBAGlB,QAEE,KAME,KAAI;AA/DZ,MAmDE,kBAKE,QACE,KAME,KAAI;AA/DZ,MAoDE,oBAAoB,kBAIlB,QACE,KAME,KAAI;EACF,eAAA;;AAhEV,MAmDE,kBAiBE,QAEE,KAAI,YACF;AAvER,MAoDE,oBAAoB,kBAgBlB,QAEE,KAAI,YACF;AAvER,MAmDE,kBAkBE,QACE,KAAI,YACF;AAvER,MAoDE,oBAAoB,kBAiBlB,QACE,KAAI,YACF;AAvER,MAmDE,kBAiBE,QAEE,KAAI,YAEF;AAxER,MAoDE,oBAAoB,kBAgBlB,QAEE,KAAI,YAEF;AAxER,MAmDE,kBAkBE,QACE,KAAI,YAEF;AAxER,MAoDE,oBAAoB,kBAiBlB,QACE,KAAI,YAEF;EACE,gBAAA;;AAzEV,MAmDE,kBA0BE,QAEE,KAAI,WACF;AAhFR,MAoDE,oBAAoB,kBAyBlB,QAEE,KAAI,WACF;AAhFR,MAmDE,kBA2BE,QACE,KAAI,WACF;AAhFR,MAoDE,oBAAoB,kBA0BlB,QACE,KAAI,WACF;AAhFR,MAmDE,kBA0BE,QAEE,KAAI,WAEF;AAjFR,MAoDE,oBAAoB,kBAyBlB,QAEE,KAAI,WAEF;AAjFR,MAmDE,kBA2BE,QACE,KAAI,WAEF;AAjFR,MAoDE,oBAAoB,kBA0BlB,QACE,KAAI,WAEF;EACE,gBAAA;;AAlFV,MAuFE;EACE,SAAA;EACA,gBAAA;;AAUJ;EACE,mBAAA;;AADF,YAIE;EACE,gBAAA;EACA,kBAAA;EACA,gBAAA;;AAPJ,YAIE,OAIE;EACE,eAAA;;AATN,YAaE;EACE,gBAAA;;AAdJ,YAaE,eAEE,kBAAkB;EAChB,6BAAA;;AAhBN,YAmBE;EACE,aAAA;;AApBJ,YAmBE,cAEE,kBAAkB;EAChB,gCAAA;;AAON;E5BsLE,qBAAA;;AAEA,cAAE;EACA,cAAA;EACA,yBAAA;EACA,qBAAA;;AAHF,cAAE,iBAKA,kBAAkB;EAChB,yBAAA;;AAGJ,cAAE,gBACA,kBAAkB;EAChB,4BAAA;;A4BhMN;E5BmLE,qBAAA;;AAEA,cAAE;EACA,cAAA;EACA,yBAAA;EACA,qBAAA;;AAHF,cAAE,iBAKA,kBAAkB;EAChB,yBAAA;;AAGJ,cAAE,gBACA,kBAAkB;EAChB,4BAAA;;A4B7LN;E5BgLE,qBAAA;;AAEA,cAAE;EACA,cAAA;EACA,yBAAA;EACA,qBAAA;;AAHF,cAAE,iBAKA,kBAAkB;EAChB,yBAAA;;AAGJ,cAAE,gBACA,kBAAkB;EAChB,4BAAA;;A4B1LN;E5B6KE,qBAAA;;AAEA,WAAE;EACA,cAAA;EACA,yBAAA;EACA,qBAAA;;AAHF,WAAE,iBAKA,kBAAkB;EAChB,yBAAA;;AAGJ,WAAE,gBACA,kBAAkB;EAChB,4BAAA;;A4BvLN;E5B0KE,qBAAA;;AAEA,cAAE;EACA,cAAA;EACA,yBAAA;EACA,qBAAA;;AAHF,cAAE,iBAKA,kBAAkB;EAChB,yBAAA;;AAGJ,cAAE,gBACA,kBAAkB;EAChB,4BAAA;;A4BpLN;E5BuKE,qBAAA;;AAEA,aAAE;EACA,cAAA;EACA,yBAAA;EACA,qBAAA;;AAHF,aAAE,iBAKA,kBAAkB;EAChB,yBAAA;;AAGJ,aAAE,gBACA,kBAAkB;EAChB,4BAAA;;A6B5ZN;EACE,gBAAA;EACA,aAAA;EACA,mBAAA;EACA,yBAAA;EACA,yBAAA;EACA,kBAAA;E7B6GA,uDAAA;EACQ,+CAAA;;A6BpHV,KAQE;EACE,kBAAA;EACA,iCAAA;;AAKJ;EACE,aAAA;EACA,kBAAA;;AAEF;EACE,YAAA;EACA,kBAAA;;ACtBF;EACE,YAAA;EACA,eAAA;EACA,iBAAA;EACA,cAAA;EACA,cAAA;EACA,4BAAA;E9BkRA,YAAA;EAGA,yBAAA;;A8BlRA,MAAC;AACD,MAAC;EACC,cAAA;EACA,qBAAA;EACA,eAAA;E9B2QF,YAAA;EAGA,yBAAA;;A8BvQA,MAAM;EACJ,UAAA;EACA,eAAA;EACA,uBAAA;EACA,SAAA;EACA,wBAAA;;ACpBJ;EACE,gBAAA;;AAIF;EACE,aAAA;EACA,cAAA;EACA,kBAAA;EACA,eAAA;EACA,MAAA;EACA,QAAA;EACA,SAAA;EACA,OAAA;EACA,aAAA;EACA,iCAAA;EAIA,UAAA;;AAGA,MAAC,KAAM;E/BiIP,mBAAmB,kBAAnB;EACI,eAAe,kBAAf;EACI,WAAW,kBAAX;EApBR,mDAAA;EACG,6CAAA;EACE,yCAAA;EACG,mCAAA;;A+B9GR,MAAC,GAAI;E/B6HL,mBAAmB,eAAnB;EACI,eAAe,eAAf;EACI,WAAW,eAAX;;A+B3HV;EACE,kBAAA;EACA,WAAA;EACA,YAAA;;AAIF;EACE,kBAAA;EACA,yBAAA;EACA,yBAAA;EACA,oCAAA;EACA,kBAAA;E/BqEA,gDAAA;EACQ,wCAAA;E+BpER,4BAAA;EAEA,aAAA;;AAIF;EACE,eAAA;EACA,MAAA;EACA,QAAA;EACA,SAAA;EACA,OAAA;EACA,aAAA;EACA,yBAAA;;AAEA,eAAC;E/BwND,UAAA;EAGA,wBAAA;;A+B1NA,eAAC;E/BuND,YAAA;EAGA,yBAAA;;A+BrNF;EACE,aAAA;EACA,gCAAA;EACA,yBAAA;;AAGF,aAAc;EACZ,gBAAA;;AAIF;EACE,SAAA;EACA,uBAAA;;AAKF;EACE,kBAAA;EACA,aAAA;;AAIF;EACE,gBAAA;EACA,uBAAA;EACA,iBAAA;EACA,6BAAA;;AAJF,aAQE,KAAK;EACH,gBAAA;EACA,gBAAA;;AAVJ,aAaE,WAAW,KAAK;EACd,iBAAA;;AAdJ,aAiBE,WAAW;EACT,cAAA;;AAmBJ,QAdmC;EAEjC;IACE,YAAA;IACA,iBAAA;;EAEF;I/BPA,iDAAA;IACQ,yCAAA;;E+BWR;IAAY,YAAA;;;AAMd,QAHmC;EACjC;IAAY,YAAA;;;ACnId;EACE,kBAAA;EACA,aAAA;EACA,cAAA;EACA,mBAAA;EACA,eAAA;EACA,gBAAA;EhCiRA,UAAA;EAGA,wBAAA;;AgCjRA,QAAC;EhC8QD,YAAA;EAGA,yBAAA;;AgChRA,QAAC;EAAU,gBAAA;EAAmB,cAAA;;AAC9B,QAAC;EAAU,gBAAA;EAAmB,cAAA;;AAC9B,QAAC;EAAU,eAAA;EAAmB,cAAA;;AAC9B,QAAC;EAAU,iBAAA;EAAmB,cAAA;;AAIhC;EACE,gBAAA;EACA,gBAAA;EACA,cAAA;EACA,kBAAA;EACA,qBAAA;EACA,yBAAA;EACA,kBAAA;;AAIF;EACE,kBAAA;EACA,QAAA;EACA,SAAA;EACA,yBAAA;EACA,mBAAA;;AAGA,QAAC,IAAK;EACJ,SAAA;EACA,SAAA;EACA,iBAAA;EACA,uBAAA;EACA,yBAAA;;AAEF,QAAC,SAAU;EACT,SAAA;EACA,SAAA;EACA,uBAAA;EACA,yBAAA;;AAEF,QAAC,UAAW;EACV,SAAA;EACA,UAAA;EACA,uBAAA;EACA,yBAAA;;AAEF,QAAC,MAAO;EACN,QAAA;EACA,OAAA;EACA,gBAAA;EACA,2BAAA;EACA,2BAAA;;AAEF,QAAC,KAAM;EACL,QAAA;EACA,QAAA;EACA,gBAAA;EACA,2BAAA;EACA,0BAAA;;AAEF,QAAC,OAAQ;EACP,MAAA;EACA,SAAA;EACA,iBAAA;EACA,uBAAA;EACA,4BAAA;;AAEF,QAAC,YAAa;EACZ,MAAA;EACA,SAAA;EACA,uBAAA;EACA,4BAAA;;AAEF,QAAC,aAAc;EACb,MAAA;EACA,UAAA;EACA,uBAAA;EACA,4BAAA;;ACvFJ;EACE,kBAAA;EACA,MAAA;EACA,OAAA;EACA,aAAA;EACA,aAAA;EACA,gBAAA;EACA,YAAA;EACA,gBAAA;EACA,yBAAA;EACA,4BAAA;EACA,yBAAA;EACA,oCAAA;EACA,kBAAA;EjCuGA,iDAAA;EACQ,yCAAA;EiCpGR,mBAAA;;AAGA,QAAC;EAAW,iBAAA;;AACZ,QAAC;EAAW,iBAAA;;AACZ,QAAC;EAAW,gBAAA;;AACZ,QAAC;EAAW,kBAAA;;AAGd;EACE,SAAA;EACA,iBAAA;EACA,eAAA;EACA,mBAAA;EACA,iBAAA;EACA,yBAAA;EACA,gCAAA;EACA,0BAAA;;AAGF;EACE,iBAAA;;AAQA,QADO;AAEP,QAFO,SAEN;EACC,kBAAA;EACA,cAAA;EACA,QAAA;EACA,SAAA;EACA,yBAAA;EACA,mBAAA;;AAGJ,QAAS;EACP,kBAAA;;AAEF,QAAS,SAAQ;EACf,kBAAA;EACA,SAAS,EAAT;;AAIA,QAAC,IAAK;EACJ,SAAA;EACA,kBAAA;EACA,sBAAA;EACA,yBAAA;EACA,qCAAA;EACA,aAAA;;AACA,QAPD,IAAK,SAOH;EACC,SAAS,GAAT;EACA,WAAA;EACA,kBAAA;EACA,sBAAA;EACA,yBAAA;;AAGJ,QAAC,MAAO;EACN,QAAA;EACA,WAAA;EACA,iBAAA;EACA,oBAAA;EACA,2BAAA;EACA,uCAAA;;AACA,QAPD,MAAO,SAOL;EACC,SAAS,GAAT;EACA,SAAA;EACA,aAAA;EACA,oBAAA;EACA,2BAAA;;AAGJ,QAAC,OAAQ;EACP,SAAA;EACA,kBAAA;EACA,mBAAA;EACA,4BAAA;EACA,wCAAA;EACA,UAAA;;AACA,QAPD,OAAQ,SAON;EACC,SAAS,GAAT;EACA,QAAA;EACA,kBAAA;EACA,mBAAA;EACA,4BAAA;;AAIJ,QAAC,KAAM;EACL,QAAA;EACA,YAAA;EACA,iBAAA;EACA,qBAAA;EACA,0BAAA;EACA,sCAAA;;AACA,QAPD,KAAM,SAOJ;EACC,SAAS,GAAT;EACA,UAAA;EACA,qBAAA;EACA,0BAAA;EACA,aAAA;;A9B1HN;EACE,kBAAA;;AAGF;EACE,kBAAA;EACA,gBAAA;EACA,WAAA;;AAHF,eAKE;EACE,aAAA;EACA,kBAAA;EH8GF,yCAAA;EACQ,iCAAA;;AGtHV,eAKE,QAME;AAXJ,eAKE,QAOE,IAAI;EAEF,cAAA;;AAdN,eAkBE;AAlBF,eAmBE;AAnBF,eAoBE;EAAU,cAAA;;AApBZ,eAsBE;EACE,OAAA;;AAvBJ,eA0BE;AA1BF,eA2BE;EACE,kBAAA;EACA,MAAA;EACA,WAAA;;AA9BJ,eAiCE;EACE,UAAA;;AAlCJ,eAoCE;EACE,WAAA;;AArCJ,eAuCE,QAAO;AAvCT,eAwCE,QAAO;EACL,OAAA;;AAzCJ,eA4CE,UAAS;EACP,WAAA;;AA7CJ,eA+CE,UAAS;EACP,UAAA;;AAQJ;EACE,kBAAA;EACA,MAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EHsNA,YAAA;EAGA,yBAAA;EGvNA,eAAA;EACA,cAAA;EACA,kBAAA;EACA,yCAAA;;AAKA,iBAAC;EH8NC,kBAAkB,8BAA8B,mCAAyC,uCAAzF;EACA,kBAAmB,4EAAnB;EACA,2BAAA;EACA,sHAAA;;AG9NF,iBAAC;EACC,UAAA;EACA,QAAA;EHyNA,kBAAkB,8BAA8B,sCAAyC,oCAAzF;EACA,kBAAmB,4EAAnB;EACA,2BAAA;EACA,sHAAA;;AGvNF,iBAAC;AACD,iBAAC;EACC,aAAA;EACA,cAAA;EACA,qBAAA;EH8LF,YAAA;EAGA,yBAAA;;AG9NF,iBAkCE;AAlCF,iBAmCE;AAnCF,iBAoCE;AApCF,iBAqCE;EACE,kBAAA;EACA,QAAA;EACA,UAAA;EACA,qBAAA;;AAzCJ,iBA2CE;AA3CF,iBA4CE;EACE,SAAA;;AA7CJ,iBA+CE;AA/CF,iBAgDE;EACE,UAAA;;AAjDJ,iBAmDE;AAnDF,iBAoDE;EACE,WAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;EACA,kBAAA;;AAIA,iBADF,WACG;EACC,SAAS,OAAT;;AAIF,iBADF,WACG;EACC,SAAS,OAAT;;AAUN;EACE,kBAAA;EACA,YAAA;EACA,SAAA;EACA,WAAA;EACA,UAAA;EACA,iBAAA;EACA,eAAA;EACA,gBAAA;EACA,kBAAA;;AATF,oBAWE;EACE,qBAAA;EACA,WAAA;EACA,YAAA;EACA,WAAA;EACA,mBAAA;EACA,yBAAA;EACA,mBAAA;EACA,eAAA;EAUA,yBAAA;EACA,kCAAA;;AA9BJ,oBAgCE;EACE,SAAA;EACA,WAAA;EACA,YAAA;EACA,yBAAA;;AAOJ;EACE,kBAAA;EACA,SAAA;EACA,UAAA;EACA,YAAA;EACA,WAAA;EACA,iBAAA;EACA,oBAAA;EACA,cAAA;EACA,kBAAA;EACA,yCAAA;;AACA,iBAAE;EACA,iBAAA;;AAkCJ,mBA5B8C;EAG5C,iBACE;EADF,iBAEE;EAFF,iBAGE;EAHF,iBAIE;IACE,WAAA;IACA,YAAA;IACA,iBAAA;IACA,kBAAA;IACA,eAAA;;EAKJ;IACE,SAAA;IACA,UAAA;IACA,oBAAA;;EAIF;IACE,YAAA;;;AHlNF,SAAC;AACD,SAAC;AMXH,UNUG;AMVH,UNWG;AMSH,gBNVG;AMUH,gBNTG;AMkBH,INnBG;AMmBH,INlBG;AQsXH,gBAoBE,YR3YC;AQuXH,gBAoBE,YR1YC;AUkBH,YVnBG;AUmBH,YVlBG;AU8HH,mBAWE,aV1IC;AU+HH,mBAWE,aVzIC;AeZH,IfWG;AeXH,IfYG;AgBVH,OhBSG;AgBTH,OhBUG;AgBUH,chBXG;AgBWH,chBVG;AgB6BH,gBhB9BG;AgB8BH,gBhB7BG;AoBfH,MpBcG;AoBdH,MpBeG;A4BLH,W5BIG;A4BJH,W5BKG;A+B+EH,a/BhFG;A+BgFH,a/B/EG;EACC,SAAS,GAAT;EACA,cAAA;;AAEF,SAAC;AMfH,UNeG;AMKH,gBNLG;AMcH,INdG;AQkXH,gBAoBE,YRtYC;AUcH,YVdG;AU0HH,mBAWE,aVrIC;AehBH,IfgBG;AgBdH,OhBcG;AgBMH,chBNG;AgByBH,gBhBzBG;AoBnBH,MpBmBG;A4BTH,W5BSG;A+B2EH,a/B3EG;EACC,WAAA;;AiBdJ;EjB6BE,cAAA;EACA,iBAAA;EACA,kBAAA;;AiB5BF;EACE,uBAAA;;AAEF;EACE,sBAAA;;AAQF;EACE,wBAAA;;AAEF;EACE,yBAAA;;AAEF;EACE,kBAAA;;AAEF;EjB8CE,WAAA;EACA,kBAAA;EACA,iBAAA;EACA,6BAAA;EACA,SAAA;;AiBzCF;EACE,wBAAA;EACA,6BAAA;;AAOF;EACE,eAAA;;AiBnCF;EACE,mBAAA;;AAKF;AACA;AACA;AACA;ElCylBE,wBAAA;;AkCjlBF,QAHqC;EAGrC;IlCykBE,yBAAA;;EACA,KAAK;IAAK,cAAA;;EACV,EAAE;IAAQ,kBAAA;;EACV,EAAE;EACF,EAAE;IAAQ,mBAAA;;;AkCxkBZ,QAHqC,uBAAgC;EAGrE;IlCokBE,yBAAA;;EACA,KAAK;IAAK,cAAA;;EACV,EAAE;IAAQ,kBAAA;;EACV,EAAE;EACF,EAAE;IAAQ,mBAAA;;;AkCnkBZ,QAHqC,uBAAgC;EAGrE;IlC+jBE,yBAAA;;EACA,KAAK;IAAK,cAAA;;EACV,EAAE;IAAQ,kBAAA;;EACV,EAAE;EACF,EAAE;IAAQ,mBAAA;;;AkC9jBZ,QAHqC;EAGrC;IlC0jBE,yBAAA;;EACA,KAAK;IAAK,cAAA;;EACV,EAAE;IAAQ,kBAAA;;EACV,EAAE;EACF,EAAE;IAAQ,mBAAA;;;AkCxjBZ,QAHqC;EAGrC;IlC4jBE,wBAAA;;;AkCvjBF,QAHqC,uBAAgC;EAGrE;IlCujBE,wBAAA;;;AkCljBF,QAHqC,uBAAgC;EAGrE;IlCkjBE,wBAAA;;;AkC7iBF,QAHqC;EAGrC;IlC6iBE,wBAAA;;;AkCtiBF;ElCsiBE,wBAAA;;AkChiBF;EAAA;IlCwhBE,yBAAA;;EACA,KAAK;IAAK,cAAA;;EACV,EAAE;IAAQ,kBAAA;;EACV,EAAE;EACF,EAAE;IAAQ,mBAAA;;;AkCthBZ;EAAA;IlC0hBE,wBAAA","sourcesContent":["/*! normalize.css v3.0.0 | MIT License | git.io/normalize */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS text size adjust after orientation change, without disabling\n//    user zoom.\n//\n\nhtml {\n  font-family: sans-serif; // 1\n  -ms-text-size-adjust: 100%; // 2\n  -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n  margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined in IE 8/9.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nnav,\nsection,\nsummary {\n  display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n  display: inline-block; // 1\n  vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n  display: none;\n  height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9.\n// Hide the `template` element in IE, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n  display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n  background: transparent;\n}\n\n//\n// Improve readability when focused and also mouse hovered in all browsers.\n//\n\na:active,\na:hover {\n  outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// Address styling not present in IE 8/9, Safari 5, and Chrome.\n//\n\nabbr[title] {\n  border-bottom: 1px dotted;\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome.\n//\n\nb,\nstrong {\n  font-weight: bold;\n}\n\n//\n// Address styling not present in Safari 5 and Chrome.\n//\n\ndfn {\n  font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari 5, and Chrome.\n//\n\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n  background: #ff0;\n  color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n  font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsup {\n  top: -0.5em;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9.\n//\n\nimg {\n  border: 0;\n}\n\n//\n// Correct overflow displayed oddly in IE 9.\n//\n\nsvg:not(:root) {\n  overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari 5.\n//\n\nfigure {\n  margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n  -moz-box-sizing: content-box;\n  box-sizing: content-box;\n  height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n  overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace, monospace;\n  font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n//    Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  color: inherit; // 1\n  font: inherit; // 2\n  margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10.\n//\n\nbutton {\n  overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8+, and Opera\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n  text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n//    and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n//    `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n  -webkit-appearance: button; // 2\n  cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n  cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n  border: 0;\n  padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n  line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n  box-sizing: border-box; // 1\n  padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome\n//    (include `-moz` to future-proof).\n//\n\ninput[type=\"search\"] {\n  -webkit-appearance: textfield; // 1\n  -moz-box-sizing: content-box;\n  -webkit-box-sizing: content-box; // 2\n  box-sizing: content-box;\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n  border: 1px solid #c0c0c0;\n  margin: 0 2px;\n  padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n  border: 0; // 1\n  padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9.\n//\n\ntextarea {\n  overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n  font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\n\ntd,\nth {\n  padding: 0;\n}","//\n// Basic print styles\n// --------------------------------------------------\n// Source: https://github.com/h5bp/html5-boilerplate/blob/master/css/main.css\n\n@media print {\n\n  * {\n    text-shadow: none !important;\n    color: #000 !important; // Black prints faster: h5bp.com/s\n    background: transparent !important;\n    box-shadow: none !important;\n  }\n\n  a,\n  a:visited {\n    text-decoration: underline;\n  }\n\n  a[href]:after {\n    content: \" (\" attr(href) \")\";\n  }\n\n  abbr[title]:after {\n    content: \" (\" attr(title) \")\";\n  }\n\n  // Don't show links for images, or javascript/internal links\n  a[href^=\"javascript:\"]:after,\n  a[href^=\"#\"]:after {\n    content: \"\";\n  }\n\n  pre,\n  blockquote {\n    border: 1px solid #999;\n    page-break-inside: avoid;\n  }\n\n  thead {\n    display: table-header-group; // h5bp.com/t\n  }\n\n  tr,\n  img {\n    page-break-inside: avoid;\n  }\n\n  img {\n    max-width: 100% !important;\n  }\n\n  p,\n  h2,\n  h3 {\n    orphans: 3;\n    widows: 3;\n  }\n\n  h2,\n  h3 {\n    page-break-after: avoid;\n  }\n\n  // Chrome (OSX) fix for https://github.com/twbs/bootstrap/issues/11245\n  // Once fixed, we can just straight up remove this.\n  select {\n    background: #fff !important;\n  }\n\n  // Bootstrap components\n  .navbar {\n    display: none;\n  }\n  .table {\n    td,\n    th {\n      background-color: #fff !important;\n    }\n  }\n  .btn,\n  .dropup > .btn {\n    > .caret {\n      border-top-color: #000 !important;\n    }\n  }\n  .label {\n    border: 1px solid #000;\n  }\n\n  .table {\n    border-collapse: collapse !important;\n  }\n  .table-bordered {\n    th,\n    td {\n      border: 1px solid #ddd !important;\n    }\n  }\n\n}\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// http://getbootstrap.com/getting-started/#third-box-sizing\n* {\n  .box-sizing(border-box);\n}\n*:before,\n*:after {\n  .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n  font-size: 62.5%;\n  -webkit-tap-highlight-color: rgba(0,0,0,0);\n}\n\nbody {\n  font-family: @font-family-base;\n  font-size: @font-size-base;\n  line-height: @line-height-base;\n  color: @text-color;\n  background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit;\n}\n\n\n// Links\n\na {\n  color: @link-color;\n  text-decoration: none;\n\n  &:hover,\n  &:focus {\n    color: @link-hover-color;\n    text-decoration: underline;\n  }\n\n  &:focus {\n    .tab-focus();\n  }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n  margin: 0;\n}\n\n\n// Images\n\nimg {\n  vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n  .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n  border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n  padding: @thumbnail-padding;\n  line-height: @line-height-base;\n  background-color: @thumbnail-bg;\n  border: 1px solid @thumbnail-border;\n  border-radius: @thumbnail-border-radius;\n  .transition(all .2s ease-in-out);\n\n  // Keep them at most 100% wide\n  .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n  border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n  margin-top:    @line-height-computed;\n  margin-bottom: @line-height-computed;\n  border: 0;\n  border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content/\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  margin: -1px;\n  padding: 0;\n  overflow: hidden;\n  clip: rect(0,0,0,0);\n  border: 0;\n}\n","//\n// Mixins\n// --------------------------------------------------\n\n\n// Utilities\n// -------------------------\n\n// Clearfix\n// Source: http://nicolasgallagher.com/micro-clearfix-hack/\n//\n// For modern browsers\n// 1. The space content is one way to avoid an Opera bug when the\n//    contenteditable attribute is included anywhere else in the document.\n//    Otherwise it causes space to appear at the top and bottom of elements\n//    that are clearfixed.\n// 2. The use of `table` rather than `block` is only necessary if using\n//    `:before` to contain the top-margins of child elements.\n.clearfix() {\n  &:before,\n  &:after {\n    content: \" \"; // 1\n    display: table; // 2\n  }\n  &:after {\n    clear: both;\n  }\n}\n\n// WebKit-style focus\n.tab-focus() {\n  // Default\n  outline: thin dotted;\n  // WebKit\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\n\n// Center-align a block level element\n.center-block() {\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n// Sizing shortcuts\n.size(@width; @height) {\n  width: @width;\n  height: @height;\n}\n.square(@size) {\n  .size(@size; @size);\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n  &::-moz-placeholder           { color: @color;   // Firefox\n                                  opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526\n  &:-ms-input-placeholder       { color: @color; } // Internet Explorer 10+\n  &::-webkit-input-placeholder  { color: @color; } // Safari and Chrome\n}\n\n// Text overflow\n// Requires inline-block or block for proper styling\n.text-overflow() {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n// CSS image replacement\n//\n// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for\n// mixins being reused as classes with the same name, this doesn't hold up. As\n// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. Note\n// that we cannot chain the mixins together in Less, so they are repeated.\n//\n// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757\n\n// Deprecated as of v3.0.1 (will be removed in v4)\n.hide-text() {\n  font: ~\"0/0\" a;\n  color: transparent;\n  text-shadow: none;\n  background-color: transparent;\n  border: 0;\n}\n// New mixin to use as of v3.0.1\n.text-hide() {\n  .hide-text();\n}\n\n\n\n// CSS3 PROPERTIES\n// --------------------------------------------------\n\n// Single side border-radius\n.border-top-radius(@radius) {\n  border-top-right-radius: @radius;\n   border-top-left-radius: @radius;\n}\n.border-right-radius(@radius) {\n  border-bottom-right-radius: @radius;\n     border-top-right-radius: @radius;\n}\n.border-bottom-radius(@radius) {\n  border-bottom-right-radius: @radius;\n   border-bottom-left-radius: @radius;\n}\n.border-left-radius(@radius) {\n  border-bottom-left-radius: @radius;\n     border-top-left-radius: @radius;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n//   supported browsers that have box shadow capabilities now support the\n//   standard `box-shadow` property.\n.box-shadow(@shadow) {\n  -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n          box-shadow: @shadow;\n}\n\n// Transitions\n.transition(@transition) {\n  -webkit-transition: @transition;\n          transition: @transition;\n}\n.transition-property(@transition-property) {\n  -webkit-transition-property: @transition-property;\n          transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n  -webkit-transition-delay: @transition-delay;\n          transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n  -webkit-transition-duration: @transition-duration;\n          transition-duration: @transition-duration;\n}\n.transition-transform(@transition) {\n  -webkit-transition: -webkit-transform @transition;\n     -moz-transition: -moz-transform @transition;\n       -o-transition: -o-transform @transition;\n          transition: transform @transition;\n}\n\n// Transformations\n.rotate(@degrees) {\n  -webkit-transform: rotate(@degrees);\n      -ms-transform: rotate(@degrees); // IE9 only\n          transform: rotate(@degrees);\n}\n.scale(@ratio; @ratio-y...) {\n  -webkit-transform: scale(@ratio, @ratio-y);\n      -ms-transform: scale(@ratio, @ratio-y); // IE9 only\n          transform: scale(@ratio, @ratio-y);\n}\n.translate(@x; @y) {\n  -webkit-transform: translate(@x, @y);\n      -ms-transform: translate(@x, @y); // IE9 only\n          transform: translate(@x, @y);\n}\n.skew(@x; @y) {\n  -webkit-transform: skew(@x, @y);\n      -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n          transform: skew(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n  -webkit-transform: translate3d(@x, @y, @z);\n          transform: translate3d(@x, @y, @z);\n}\n\n.rotateX(@degrees) {\n  -webkit-transform: rotateX(@degrees);\n      -ms-transform: rotateX(@degrees); // IE9 only\n          transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n  -webkit-transform: rotateY(@degrees);\n      -ms-transform: rotateY(@degrees); // IE9 only\n          transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n  -webkit-perspective: @perspective;\n     -moz-perspective: @perspective;\n          perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n  -webkit-perspective-origin: @perspective;\n     -moz-perspective-origin: @perspective;\n          perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n  -webkit-transform-origin: @origin;\n     -moz-transform-origin: @origin;\n      -ms-transform-origin: @origin; // IE9 only\n          transform-origin: @origin;\n}\n\n// Animations\n.animation(@animation) {\n  -webkit-animation: @animation;\n          animation: @animation;\n}\n.animation-name(@name) {\n  -webkit-animation-name: @name;\n          animation-name: @name;\n}\n.animation-duration(@duration) {\n  -webkit-animation-duration: @duration;\n          animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n  -webkit-animation-timing-function: @timing-function;\n          animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n  -webkit-animation-delay: @delay;\n          animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n  -webkit-animation-iteration-count: @iteration-count;\n          animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n  -webkit-animation-direction: @direction;\n          animation-direction: @direction;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n.backface-visibility(@visibility){\n  -webkit-backface-visibility: @visibility;\n     -moz-backface-visibility: @visibility;\n          backface-visibility: @visibility;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n  -webkit-box-sizing: @boxmodel;\n     -moz-box-sizing: @boxmodel;\n          box-sizing: @boxmodel;\n}\n\n// User select\n// For selecting text on the page\n.user-select(@select) {\n  -webkit-user-select: @select;\n     -moz-user-select: @select;\n      -ms-user-select: @select; // IE10+\n          user-select: @select;\n}\n\n// Resize anything\n.resizable(@direction) {\n  resize: @direction; // Options: horizontal, vertical, both\n  overflow: auto; // Safari fix\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n  -webkit-column-count: @column-count;\n     -moz-column-count: @column-count;\n          column-count: @column-count;\n  -webkit-column-gap: @column-gap;\n     -moz-column-gap: @column-gap;\n          column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n  word-wrap: break-word;\n  -webkit-hyphens: @mode;\n     -moz-hyphens: @mode;\n      -ms-hyphens: @mode; // IE10+\n       -o-hyphens: @mode;\n          hyphens: @mode;\n}\n\n// Opacity\n.opacity(@opacity) {\n  opacity: @opacity;\n  // IE8 filter\n  @opacity-ie: (@opacity * 100);\n  filter: ~\"alpha(opacity=@{opacity-ie})\";\n}\n\n\n\n// GRADIENTS\n// --------------------------------------------------\n\n#gradient {\n\n  // Horizontal gradient, from left to right\n  //\n  // Creates two color stops, start and end, by specifying a color and position for each color stop.\n  // Color stops are not available in IE9 and below.\n  .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n    background-image: -webkit-linear-gradient(left, color-stop(@start-color @start-percent), color-stop(@end-color @end-percent)); // Safari 5.1-6, Chrome 10+\n    background-image:  linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n    background-repeat: repeat-x;\n    filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n  }\n\n  // Vertical gradient, from top to bottom\n  //\n  // Creates two color stops, start and end, by specifying a color and position for each color stop.\n  // Color stops are not available in IE9 and below.\n  .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n    background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent);  // Safari 5.1-6, Chrome 10+\n    background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n    background-repeat: repeat-x;\n    filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n  }\n\n  .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n    background-repeat: repeat-x;\n    background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n    background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n  }\n  .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n    background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n    background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n    background-repeat: no-repeat;\n    filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n  }\n  .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n    background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n    background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n    background-repeat: no-repeat;\n    filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n  }\n  .radial(@inner-color: #555; @outer-color: #333) {\n    background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n    background-image: radial-gradient(circle, @inner-color, @outer-color);\n    background-repeat: no-repeat;\n  }\n  .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n    background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n    background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n  }\n}\n\n// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n.reset-filter() {\n  filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n\n\n\n// Retina images\n//\n// Short retina mixin for setting background-image and -size\n\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n  background-image: url(\"@{file-1x}\");\n\n  @media\n  only screen and (-webkit-min-device-pixel-ratio: 2),\n  only screen and (   min--moz-device-pixel-ratio: 2),\n  only screen and (     -o-min-device-pixel-ratio: 2/1),\n  only screen and (        min-device-pixel-ratio: 2),\n  only screen and (                min-resolution: 192dpi),\n  only screen and (                min-resolution: 2dppx) {\n    background-image: url(\"@{file-2x}\");\n    background-size: @width-1x @height-1x;\n  }\n}\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n.img-responsive(@display: block) {\n  display: @display;\n  max-width: 100%; // Part 1: Set a maximum relative to the parent\n  height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// COMPONENT MIXINS\n// --------------------------------------------------\n\n// Horizontal dividers\n// -------------------------\n// Dividers (basically an hr) within dropdowns and nav lists\n.nav-divider(@color: #e5e5e5) {\n  height: 1px;\n  margin: ((@line-height-computed / 2) - 1) 0;\n  overflow: hidden;\n  background-color: @color;\n}\n\n// Panels\n// -------------------------\n.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) {\n  border-color: @border;\n\n  & > .panel-heading {\n    color: @heading-text-color;\n    background-color: @heading-bg-color;\n    border-color: @heading-border;\n\n    + .panel-collapse .panel-body {\n      border-top-color: @border;\n    }\n  }\n  & > .panel-footer {\n    + .panel-collapse .panel-body {\n      border-bottom-color: @border;\n    }\n  }\n}\n\n// Alerts\n// -------------------------\n.alert-variant(@background; @border; @text-color) {\n  background-color: @background;\n  border-color: @border;\n  color: @text-color;\n\n  hr {\n    border-top-color: darken(@border, 5%);\n  }\n  .alert-link {\n    color: darken(@text-color, 10%);\n  }\n}\n\n// Tables\n// -------------------------\n.table-row-variant(@state; @background) {\n  // Exact selectors below required to override `.table-striped` and prevent\n  // inheritance to nested tables.\n  .table > thead > tr,\n  .table > tbody > tr,\n  .table > tfoot > tr {\n    > td.@{state},\n    > th.@{state},\n    &.@{state} > td,\n    &.@{state} > th {\n      background-color: @background;\n    }\n  }\n\n  // Hover states for `.table-hover`\n  // Note: this is not available for cells or rows within `thead` or `tfoot`.\n  .table-hover > tbody > tr {\n    > td.@{state}:hover,\n    > th.@{state}:hover,\n    &.@{state}:hover > td,\n    &.@{state}:hover > th {\n      background-color: darken(@background, 5%);\n    }\n  }\n}\n\n// List Groups\n// -------------------------\n.list-group-item-variant(@state; @background; @color) {\n  .list-group-item-@{state} {\n    color: @color;\n    background-color: @background;\n\n    a& {\n      color: @color;\n\n      .list-group-item-heading { color: inherit; }\n\n      &:hover,\n      &:focus {\n        color: @color;\n        background-color: darken(@background, 5%);\n      }\n      &.active,\n      &.active:hover,\n      &.active:focus {\n        color: #fff;\n        background-color: @color;\n        border-color: @color;\n      }\n    }\n  }\n}\n\n// Button variants\n// -------------------------\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n.button-variant(@color; @background; @border) {\n  color: @color;\n  background-color: @background;\n  border-color: @border;\n\n  &:hover,\n  &:focus,\n  &:active,\n  &.active,\n  .open .dropdown-toggle& {\n    color: @color;\n    background-color: darken(@background, 8%);\n        border-color: darken(@border, 12%);\n  }\n  &:active,\n  &.active,\n  .open .dropdown-toggle& {\n    background-image: none;\n  }\n  &.disabled,\n  &[disabled],\n  fieldset[disabled] & {\n    &,\n    &:hover,\n    &:focus,\n    &:active,\n    &.active {\n      background-color: @background;\n          border-color: @border;\n    }\n  }\n\n  .badge {\n    color: @background;\n    background-color: @color;\n  }\n}\n\n// Button sizes\n// -------------------------\n.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n  padding: @padding-vertical @padding-horizontal;\n  font-size: @font-size;\n  line-height: @line-height;\n  border-radius: @border-radius;\n}\n\n// Pagination\n// -------------------------\n.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @border-radius) {\n  > li {\n    > a,\n    > span {\n      padding: @padding-vertical @padding-horizontal;\n      font-size: @font-size;\n    }\n    &:first-child {\n      > a,\n      > span {\n        .border-left-radius(@border-radius);\n      }\n    }\n    &:last-child {\n      > a,\n      > span {\n        .border-right-radius(@border-radius);\n      }\n    }\n  }\n}\n\n// Labels\n// -------------------------\n.label-variant(@color) {\n  background-color: @color;\n  &[href] {\n    &:hover,\n    &:focus {\n      background-color: darken(@color, 10%);\n    }\n  }\n}\n\n// Contextual backgrounds\n// -------------------------\n.bg-variant(@color) {\n  background-color: @color;\n  a&:hover {\n    background-color: darken(@color, 10%);\n  }\n}\n\n// Typography\n// -------------------------\n.text-emphasis-variant(@color) {\n  color: @color;\n  a&:hover {\n    color: darken(@color, 10%);\n  }\n}\n\n// Navbar vertical align\n// -------------------------\n// Vertically center elements in the navbar.\n// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin.\n.navbar-vertical-align(@element-height) {\n  margin-top: ((@navbar-height - @element-height) / 2);\n  margin-bottom: ((@navbar-height - @element-height) / 2);\n}\n\n// Progress bars\n// -------------------------\n.progress-bar-variant(@color) {\n  background-color: @color;\n  .progress-striped & {\n    #gradient > .striped();\n  }\n}\n\n// Responsive utilities\n// -------------------------\n// More easily include all the states for responsive-utilities.less.\n.responsive-visibility() {\n  display: block !important;\n  table&  { display: table; }\n  tr&     { display: table-row !important; }\n  th&,\n  td&     { display: table-cell !important; }\n}\n\n.responsive-invisibility() {\n  display: none !important;\n}\n\n\n// Grid System\n// -----------\n\n// Centered container element\n.container-fixed() {\n  margin-right: auto;\n  margin-left: auto;\n  padding-left:  (@grid-gutter-width / 2);\n  padding-right: (@grid-gutter-width / 2);\n  &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n  margin-left:  (@gutter / -2);\n  margin-right: (@gutter / -2);\n  &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n  position: relative;\n  float: left;\n  width: percentage((@columns / @grid-columns));\n  min-height: 1px;\n  padding-left:  (@gutter / 2);\n  padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n  @media (min-width: @screen-xs-min) {\n    margin-left: percentage((@columns / @grid-columns));\n  }\n}\n.make-xs-column-push(@columns) {\n  @media (min-width: @screen-xs-min) {\n    left: percentage((@columns / @grid-columns));\n  }\n}\n.make-xs-column-pull(@columns) {\n  @media (min-width: @screen-xs-min) {\n    right: percentage((@columns / @grid-columns));\n  }\n}\n\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n  position: relative;\n  min-height: 1px;\n  padding-left:  (@gutter / 2);\n  padding-right: (@gutter / 2);\n\n  @media (min-width: @screen-sm-min) {\n    float: left;\n    width: percentage((@columns / @grid-columns));\n  }\n}\n.make-sm-column-offset(@columns) {\n  @media (min-width: @screen-sm-min) {\n    margin-left: percentage((@columns / @grid-columns));\n  }\n}\n.make-sm-column-push(@columns) {\n  @media (min-width: @screen-sm-min) {\n    left: percentage((@columns / @grid-columns));\n  }\n}\n.make-sm-column-pull(@columns) {\n  @media (min-width: @screen-sm-min) {\n    right: percentage((@columns / @grid-columns));\n  }\n}\n\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n  position: relative;\n  min-height: 1px;\n  padding-left:  (@gutter / 2);\n  padding-right: (@gutter / 2);\n\n  @media (min-width: @screen-md-min) {\n    float: left;\n    width: percentage((@columns / @grid-columns));\n  }\n}\n.make-md-column-offset(@columns) {\n  @media (min-width: @screen-md-min) {\n    margin-left: percentage((@columns / @grid-columns));\n  }\n}\n.make-md-column-push(@columns) {\n  @media (min-width: @screen-md-min) {\n    left: percentage((@columns / @grid-columns));\n  }\n}\n.make-md-column-pull(@columns) {\n  @media (min-width: @screen-md-min) {\n    right: percentage((@columns / @grid-columns));\n  }\n}\n\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n  position: relative;\n  min-height: 1px;\n  padding-left:  (@gutter / 2);\n  padding-right: (@gutter / 2);\n\n  @media (min-width: @screen-lg-min) {\n    float: left;\n    width: percentage((@columns / @grid-columns));\n  }\n}\n.make-lg-column-offset(@columns) {\n  @media (min-width: @screen-lg-min) {\n    margin-left: percentage((@columns / @grid-columns));\n  }\n}\n.make-lg-column-push(@columns) {\n  @media (min-width: @screen-lg-min) {\n    left: percentage((@columns / @grid-columns));\n  }\n}\n.make-lg-column-pull(@columns) {\n  @media (min-width: @screen-lg-min) {\n    right: percentage((@columns / @grid-columns));\n  }\n}\n\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n  // Common styles for all sizes of grid columns, widths 1-12\n  .col(@index) when (@index = 1) { // initial\n    @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n    .col((@index + 1), @item);\n  }\n  .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n    @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n    .col((@index + 1), ~\"@{list}, @{item}\");\n  }\n  .col(@index, @list) when (@index > @grid-columns) { // terminal\n    @{list} {\n      position: relative;\n      // Prevent columns from collapsing when empty\n      min-height: 1px;\n      // Inner gutter via padding\n      padding-left:  (@grid-gutter-width / 2);\n      padding-right: (@grid-gutter-width / 2);\n    }\n  }\n  .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n  .col(@index) when (@index = 1) { // initial\n    @item: ~\".col-@{class}-@{index}\";\n    .col((@index + 1), @item);\n  }\n  .col(@index, @list) when (@index =< @grid-columns) { // general\n    @item: ~\".col-@{class}-@{index}\";\n    .col((@index + 1), ~\"@{list}, @{item}\");\n  }\n  .col(@index, @list) when (@index > @grid-columns) { // terminal\n    @{list} {\n      float: left;\n    }\n  }\n  .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n  .col-@{class}-@{index} {\n    width: percentage((@index / @grid-columns));\n  }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) {\n  .col-@{class}-push-@{index} {\n    left: percentage((@index / @grid-columns));\n  }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) {\n  .col-@{class}-pull-@{index} {\n    right: percentage((@index / @grid-columns));\n  }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n  .col-@{class}-offset-@{index} {\n    margin-left: percentage((@index / @grid-columns));\n  }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n  .calc-grid-column(@index, @class, @type);\n  // next iteration\n  .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n  .float-grid-columns(@class);\n  .loop-grid-columns(@grid-columns, @class, width);\n  .loop-grid-columns(@grid-columns, @class, pull);\n  .loop-grid-columns(@grid-columns, @class, push);\n  .loop-grid-columns(@grid-columns, @class, offset);\n}\n\n// Form validation states\n//\n// Used in forms.less to generate the form validation CSS for warnings, errors,\n// and successes.\n\n.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) {\n  // Color the label and help text\n  .help-block,\n  .control-label,\n  .radio,\n  .checkbox,\n  .radio-inline,\n  .checkbox-inline  {\n    color: @text-color;\n  }\n  // Set the border and box shadow on specific inputs to match\n  .form-control {\n    border-color: @border-color;\n    .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work\n    &:focus {\n      border-color: darken(@border-color, 10%);\n      @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%);\n      .box-shadow(@shadow);\n    }\n  }\n  // Set validation states also for addons\n  .input-group-addon {\n    color: @text-color;\n    border-color: @border-color;\n    background-color: @background-color;\n  }\n  // Optional feedback icon\n  .form-control-feedback {\n    color: @text-color;\n  }\n}\n\n// Form control focus state\n//\n// Generate a customized focus state and for any input with the specified color,\n// which defaults to the `@input-focus-border` variable.\n//\n// We highly encourage you to not customize the default value, but instead use\n// this to tweak colors on an as-needed basis. This aesthetic change is based on\n// WebKit's default styles, but applicable to a wider range of browsers. Its\n// usability and accessibility should be taken into account with any change.\n//\n// Example usage: change the default blue border and shadow to white for better\n// contrast against a dark gray background.\n\n.form-control-focus(@color: @input-border-focus) {\n  @color-rgba: rgba(red(@color), green(@color), blue(@color), .6);\n  &:focus {\n    border-color: @color;\n    outline: 0;\n    .box-shadow(~\"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}\");\n  }\n}\n\n// Form control sizing\n//\n// Relative text size, padding, and border-radii changes for form controls. For\n// horizontal sizing, wrap controls in the predefined grid classes. `<select>`\n// element gets special love because it's special, and that's a fact!\n\n.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n  height: @input-height;\n  padding: @padding-vertical @padding-horizontal;\n  font-size: @font-size;\n  line-height: @line-height;\n  border-radius: @border-radius;\n\n  select& {\n    height: @input-height;\n    line-height: @input-height;\n  }\n\n  textarea&,\n  select[multiple]& {\n    height: auto;\n  }\n}\n","//\n// Variables\n// --------------------------------------------------\n\n\n//== Colors\n//\n//## Gray and brand colors for use across Bootstrap.\n\n@gray-darker:            lighten(#000, 13.5%); // #222\n@gray-dark:              lighten(#000, 20%);   // #333\n@gray:                   lighten(#000, 33.5%); // #555\n@gray-light:             lighten(#000, 60%);   // #999\n@gray-lighter:           lighten(#000, 93.5%); // #eee\n\n@brand-primary:         #428bca;\n@brand-success:         #5cb85c;\n@brand-info:            #5bc0de;\n@brand-warning:         #f0ad4e;\n@brand-danger:          #d9534f;\n\n\n//== Scaffolding\n//\n// ## Settings for some of the most global styles.\n\n//** Background color for `<body>`.\n@body-bg:               #fff;\n//** Global text color on `<body>`.\n@text-color:            @gray-dark;\n\n//** Global textual link color.\n@link-color:            @brand-primary;\n//** Link hover color set via `darken()` function.\n@link-hover-color:      darken(@link-color, 15%);\n\n\n//== Typography\n//\n//## Font, line-height, and color for body text, headings, and more.\n\n@font-family-sans-serif:  \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n@font-family-serif:       Georgia, \"Times New Roman\", Times, serif;\n//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.\n@font-family-monospace:   Menlo, Monaco, Consolas, \"Courier New\", monospace;\n@font-family-base:        @font-family-sans-serif;\n\n@font-size-base:          14px;\n@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px\n@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px\n\n@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px\n@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px\n@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px\n@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px\n@font-size-h5:            @font-size-base;\n@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px\n\n//** Unit-less `line-height` for use in components like buttons.\n@line-height-base:        1.428571429; // 20/14\n//** Computed \"line-height\" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.\n@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px\n\n//** By default, this inherits from the `<body>`.\n@headings-font-family:    inherit;\n@headings-font-weight:    500;\n@headings-line-height:    1.1;\n@headings-color:          inherit;\n\n\n//-- Iconography\n//\n//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.\n\n@icon-font-path:          \"../fonts/\";\n@icon-font-name:          \"glyphicons-halflings-regular\";\n@icon-font-svg-id:        \"glyphicons_halflingsregular\";\n\n//== Components\n//\n//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).\n\n@padding-base-vertical:     6px;\n@padding-base-horizontal:   12px;\n\n@padding-large-vertical:    10px;\n@padding-large-horizontal:  16px;\n\n@padding-small-vertical:    5px;\n@padding-small-horizontal:  10px;\n\n@padding-xs-vertical:       1px;\n@padding-xs-horizontal:     5px;\n\n@line-height-large:         1.33;\n@line-height-small:         1.5;\n\n@border-radius-base:        4px;\n@border-radius-large:       6px;\n@border-radius-small:       3px;\n\n//** Global color for active items (e.g., navs or dropdowns).\n@component-active-color:    #fff;\n//** Global background color for active items (e.g., navs or dropdowns).\n@component-active-bg:       @brand-primary;\n\n//** Width of the `border` for generating carets that indicator dropdowns.\n@caret-width-base:          4px;\n//** Carets increase slightly in size for larger components.\n@caret-width-large:         5px;\n\n\n//== Tables\n//\n//## Customizes the `.table` component with basic values, each used across all table variations.\n\n//** Padding for `<th>`s and `<td>`s.\n@table-cell-padding:            8px;\n//** Padding for cells in `.table-condensed`.\n@table-condensed-cell-padding:  5px;\n\n//** Default background color used for all tables.\n@table-bg:                      transparent;\n//** Background color used for `.table-striped`.\n@table-bg-accent:               #f9f9f9;\n//** Background color used for `.table-hover`.\n@table-bg-hover:                #f5f5f5;\n@table-bg-active:               @table-bg-hover;\n\n//** Border color for table and cell borders.\n@table-border-color:            #ddd;\n\n\n//== Buttons\n//\n//## For each of Bootstrap's buttons, define text, background and border color.\n\n@btn-font-weight:                normal;\n\n@btn-default-color:              #333;\n@btn-default-bg:                 #fff;\n@btn-default-border:             #ccc;\n\n@btn-primary-color:              #fff;\n@btn-primary-bg:                 @brand-primary;\n@btn-primary-border:             darken(@btn-primary-bg, 5%);\n\n@btn-success-color:              #fff;\n@btn-success-bg:                 @brand-success;\n@btn-success-border:             darken(@btn-success-bg, 5%);\n\n@btn-info-color:                 #fff;\n@btn-info-bg:                    @brand-info;\n@btn-info-border:                darken(@btn-info-bg, 5%);\n\n@btn-warning-color:              #fff;\n@btn-warning-bg:                 @brand-warning;\n@btn-warning-border:             darken(@btn-warning-bg, 5%);\n\n@btn-danger-color:               #fff;\n@btn-danger-bg:                  @brand-danger;\n@btn-danger-border:              darken(@btn-danger-bg, 5%);\n\n@btn-link-disabled-color:        @gray-light;\n\n\n//== Forms\n//\n//##\n\n//** `<input>` background color\n@input-bg:                       #fff;\n//** `<input disabled>` background color\n@input-bg-disabled:              @gray-lighter;\n\n//** Text color for `<input>`s\n@input-color:                    @gray;\n//** `<input>` border color\n@input-border:                   #ccc;\n//** `<input>` border radius\n@input-border-radius:            @border-radius-base;\n//** Border color for inputs on focus\n@input-border-focus:             #66afe9;\n\n//** Placeholder text color\n@input-color-placeholder:        @gray-light;\n\n//** Default `.form-control` height\n@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);\n//** Large `.form-control` height\n@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);\n//** Small `.form-control` height\n@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);\n\n@legend-color:                   @gray-dark;\n@legend-border-color:            #e5e5e5;\n\n//** Background color for textual input addons\n@input-group-addon-bg:           @gray-lighter;\n//** Border color for textual input addons\n@input-group-addon-border-color: @input-border;\n\n\n//== Dropdowns\n//\n//## Dropdown menu container and contents.\n\n//** Background for the dropdown menu.\n@dropdown-bg:                    #fff;\n//** Dropdown menu `border-color`.\n@dropdown-border:                rgba(0,0,0,.15);\n//** Dropdown menu `border-color` **for IE8**.\n@dropdown-fallback-border:       #ccc;\n//** Divider color for between dropdown items.\n@dropdown-divider-bg:            #e5e5e5;\n\n//** Dropdown link text color.\n@dropdown-link-color:            @gray-dark;\n//** Hover color for dropdown links.\n@dropdown-link-hover-color:      darken(@gray-dark, 5%);\n//** Hover background for dropdown links.\n@dropdown-link-hover-bg:         #f5f5f5;\n\n//** Active dropdown menu item text color.\n@dropdown-link-active-color:     @component-active-color;\n//** Active dropdown menu item background color.\n@dropdown-link-active-bg:        @component-active-bg;\n\n//** Disabled dropdown menu item background color.\n@dropdown-link-disabled-color:   @gray-light;\n\n//** Text color for headers within dropdown menus.\n@dropdown-header-color:          @gray-light;\n\n// Note: Deprecated @dropdown-caret-color as of v3.1.0\n@dropdown-caret-color:           #000;\n\n\n//-- Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n//\n// Note: These variables are not generated into the Customizer.\n\n@zindex-navbar:            1000;\n@zindex-dropdown:          1000;\n@zindex-popover:           1010;\n@zindex-tooltip:           1030;\n@zindex-navbar-fixed:      1030;\n@zindex-modal-background:  1040;\n@zindex-modal:             1050;\n\n\n//== Media queries breakpoints\n//\n//## Define the breakpoints at which your layout will change, adapting to different screen sizes.\n\n// Extra small screen / phone\n// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1\n@screen-xs:                  480px;\n@screen-xs-min:              @screen-xs;\n@screen-phone:               @screen-xs-min;\n\n// Small screen / tablet\n// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1\n@screen-sm:                  768px;\n@screen-sm-min:              @screen-sm;\n@screen-tablet:              @screen-sm-min;\n\n// Medium screen / desktop\n// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1\n@screen-md:                  992px;\n@screen-md-min:              @screen-md;\n@screen-desktop:             @screen-md-min;\n\n// Large screen / wide desktop\n// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1\n@screen-lg:                  1200px;\n@screen-lg-min:              @screen-lg;\n@screen-lg-desktop:          @screen-lg-min;\n\n// So media queries don't overlap when required, provide a maximum\n@screen-xs-max:              (@screen-sm-min - 1);\n@screen-sm-max:              (@screen-md-min - 1);\n@screen-md-max:              (@screen-lg-min - 1);\n\n\n//== Grid system\n//\n//## Define your custom responsive grid.\n\n//** Number of columns in the grid.\n@grid-columns:              12;\n//** Padding between columns. Gets divided in half for the left and right.\n@grid-gutter-width:         30px;\n// Navbar collapse\n//** Point at which the navbar becomes uncollapsed.\n@grid-float-breakpoint:     @screen-sm-min;\n//** Point at which the navbar begins collapsing.\n@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);\n\n\n//== Container sizes\n//\n//## Define the maximum width of `.container` for different screen sizes.\n\n// Small screen / tablet\n@container-tablet:             ((720px + @grid-gutter-width));\n//** For `@screen-sm-min` and up.\n@container-sm:                 @container-tablet;\n\n// Medium screen / desktop\n@container-desktop:            ((940px + @grid-gutter-width));\n//** For `@screen-md-min` and up.\n@container-md:                 @container-desktop;\n\n// Large screen / wide desktop\n@container-large-desktop:      ((1140px + @grid-gutter-width));\n//** For `@screen-lg-min` and up.\n@container-lg:                 @container-large-desktop;\n\n\n//== Navbar\n//\n//##\n\n// Basics of a navbar\n@navbar-height:                    50px;\n@navbar-margin-bottom:             @line-height-computed;\n@navbar-border-radius:             @border-radius-base;\n@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));\n@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);\n@navbar-collapse-max-height:       340px;\n\n@navbar-default-color:             #777;\n@navbar-default-bg:                #f8f8f8;\n@navbar-default-border:            darken(@navbar-default-bg, 6.5%);\n\n// Navbar links\n@navbar-default-link-color:                #777;\n@navbar-default-link-hover-color:          #333;\n@navbar-default-link-hover-bg:             transparent;\n@navbar-default-link-active-color:         #555;\n@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);\n@navbar-default-link-disabled-color:       #ccc;\n@navbar-default-link-disabled-bg:          transparent;\n\n// Navbar brand label\n@navbar-default-brand-color:               @navbar-default-link-color;\n@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);\n@navbar-default-brand-hover-bg:            transparent;\n\n// Navbar toggle\n@navbar-default-toggle-hover-bg:           #ddd;\n@navbar-default-toggle-icon-bar-bg:        #888;\n@navbar-default-toggle-border-color:       #ddd;\n\n\n// Inverted navbar\n// Reset inverted navbar basics\n@navbar-inverse-color:                      @gray-light;\n@navbar-inverse-bg:                         #222;\n@navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);\n\n// Inverted navbar links\n@navbar-inverse-link-color:                 @gray-light;\n@navbar-inverse-link-hover-color:           #fff;\n@navbar-inverse-link-hover-bg:              transparent;\n@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;\n@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);\n@navbar-inverse-link-disabled-color:        #444;\n@navbar-inverse-link-disabled-bg:           transparent;\n\n// Inverted navbar brand label\n@navbar-inverse-brand-color:                @navbar-inverse-link-color;\n@navbar-inverse-brand-hover-color:          #fff;\n@navbar-inverse-brand-hover-bg:             transparent;\n\n// Inverted navbar toggle\n@navbar-inverse-toggle-hover-bg:            #333;\n@navbar-inverse-toggle-icon-bar-bg:         #fff;\n@navbar-inverse-toggle-border-color:        #333;\n\n\n//== Navs\n//\n//##\n\n//=== Shared nav styles\n@nav-link-padding:                          10px 15px;\n@nav-link-hover-bg:                         @gray-lighter;\n\n@nav-disabled-link-color:                   @gray-light;\n@nav-disabled-link-hover-color:             @gray-light;\n\n@nav-open-link-hover-color:                 #fff;\n\n//== Tabs\n@nav-tabs-border-color:                     #ddd;\n\n@nav-tabs-link-hover-border-color:          @gray-lighter;\n\n@nav-tabs-active-link-hover-bg:             @body-bg;\n@nav-tabs-active-link-hover-color:          @gray;\n@nav-tabs-active-link-hover-border-color:   #ddd;\n\n@nav-tabs-justified-link-border-color:            #ddd;\n@nav-tabs-justified-active-link-border-color:     @body-bg;\n\n//== Pills\n@nav-pills-border-radius:                   @border-radius-base;\n@nav-pills-active-link-hover-bg:            @component-active-bg;\n@nav-pills-active-link-hover-color:         @component-active-color;\n\n\n//== Pagination\n//\n//##\n\n@pagination-color:                     @link-color;\n@pagination-bg:                        #fff;\n@pagination-border:                    #ddd;\n\n@pagination-hover-color:               @link-hover-color;\n@pagination-hover-bg:                  @gray-lighter;\n@pagination-hover-border:              #ddd;\n\n@pagination-active-color:              #fff;\n@pagination-active-bg:                 @brand-primary;\n@pagination-active-border:             @brand-primary;\n\n@pagination-disabled-color:            @gray-light;\n@pagination-disabled-bg:               #fff;\n@pagination-disabled-border:           #ddd;\n\n\n//== Pager\n//\n//##\n\n@pager-bg:                             @pagination-bg;\n@pager-border:                         @pagination-border;\n@pager-border-radius:                  15px;\n\n@pager-hover-bg:                       @pagination-hover-bg;\n\n@pager-active-bg:                      @pagination-active-bg;\n@pager-active-color:                   @pagination-active-color;\n\n@pager-disabled-color:                 @pagination-disabled-color;\n\n\n//== Jumbotron\n//\n//##\n\n@jumbotron-padding:              30px;\n@jumbotron-color:                inherit;\n@jumbotron-bg:                   @gray-lighter;\n@jumbotron-heading-color:        inherit;\n@jumbotron-font-size:            ceil((@font-size-base * 1.5));\n\n\n//== Form states and alerts\n//\n//## Define colors for form feedback states and, by default, alerts.\n\n@state-success-text:             #3c763d;\n@state-success-bg:               #dff0d8;\n@state-success-border:           darken(spin(@state-success-bg, -10), 5%);\n\n@state-info-text:                #31708f;\n@state-info-bg:                  #d9edf7;\n@state-info-border:              darken(spin(@state-info-bg, -10), 7%);\n\n@state-warning-text:             #8a6d3b;\n@state-warning-bg:               #fcf8e3;\n@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);\n\n@state-danger-text:              #a94442;\n@state-danger-bg:                #f2dede;\n@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);\n\n\n//== Tooltips\n//\n//##\n\n//** Tooltip max width\n@tooltip-max-width:           200px;\n//** Tooltip text color\n@tooltip-color:               #fff;\n//** Tooltip background color\n@tooltip-bg:                  #000;\n@tooltip-opacity:             .9;\n\n//** Tooltip arrow width\n@tooltip-arrow-width:         5px;\n//** Tooltip arrow color\n@tooltip-arrow-color:         @tooltip-bg;\n\n\n//== Popovers\n//\n//##\n\n//** Popover body background color\n@popover-bg:                          #fff;\n//** Popover maximum width\n@popover-max-width:                   276px;\n//** Popover border color\n@popover-border-color:                rgba(0,0,0,.2);\n//** Popover fallback border color\n@popover-fallback-border-color:       #ccc;\n\n//** Popover title background color\n@popover-title-bg:                    darken(@popover-bg, 3%);\n\n//** Popover arrow width\n@popover-arrow-width:                 10px;\n//** Popover arrow color\n@popover-arrow-color:                 #fff;\n\n//** Popover outer arrow width\n@popover-arrow-outer-width:           (@popover-arrow-width + 1);\n//** Popover outer arrow color\n@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);\n//** Popover outer arrow fallback color\n@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);\n\n\n//== Labels\n//\n//##\n\n//** Default label background color\n@label-default-bg:            @gray-light;\n//** Primary label background color\n@label-primary-bg:            @brand-primary;\n//** Success label background color\n@label-success-bg:            @brand-success;\n//** Info label background color\n@label-info-bg:               @brand-info;\n//** Warning label background color\n@label-warning-bg:            @brand-warning;\n//** Danger label background color\n@label-danger-bg:             @brand-danger;\n\n//** Default label text color\n@label-color:                 #fff;\n//** Default text color of a linked label\n@label-link-hover-color:      #fff;\n\n\n//== Modals\n//\n//##\n\n//** Padding applied to the modal body\n@modal-inner-padding:         20px;\n\n//** Padding applied to the modal title\n@modal-title-padding:         15px;\n//** Modal title line-height\n@modal-title-line-height:     @line-height-base;\n\n//** Background color of modal content area\n@modal-content-bg:                             #fff;\n//** Modal content border color\n@modal-content-border-color:                   rgba(0,0,0,.2);\n//** Modal content border color **for IE8**\n@modal-content-fallback-border-color:          #999;\n\n//** Modal backdrop background color\n@modal-backdrop-bg:           #000;\n//** Modal backdrop opacity\n@modal-backdrop-opacity:      .5;\n//** Modal header border color\n@modal-header-border-color:   #e5e5e5;\n//** Modal footer border color\n@modal-footer-border-color:   @modal-header-border-color;\n\n@modal-lg:                    900px;\n@modal-md:                    600px;\n@modal-sm:                    300px;\n\n\n//== Alerts\n//\n//## Define alert colors, border radius, and padding.\n\n@alert-padding:               15px;\n@alert-border-radius:         @border-radius-base;\n@alert-link-font-weight:      bold;\n\n@alert-success-bg:            @state-success-bg;\n@alert-success-text:          @state-success-text;\n@alert-success-border:        @state-success-border;\n\n@alert-info-bg:               @state-info-bg;\n@alert-info-text:             @state-info-text;\n@alert-info-border:           @state-info-border;\n\n@alert-warning-bg:            @state-warning-bg;\n@alert-warning-text:          @state-warning-text;\n@alert-warning-border:        @state-warning-border;\n\n@alert-danger-bg:             @state-danger-bg;\n@alert-danger-text:           @state-danger-text;\n@alert-danger-border:         @state-danger-border;\n\n\n//== Progress bars\n//\n//##\n\n//** Background color of the whole progress component\n@progress-bg:                 #f5f5f5;\n//** Progress bar text color\n@progress-bar-color:          #fff;\n\n//** Default progress bar color\n@progress-bar-bg:             @brand-primary;\n//** Success progress bar color\n@progress-bar-success-bg:     @brand-success;\n//** Warning progress bar color\n@progress-bar-warning-bg:     @brand-warning;\n//** Danger progress bar color\n@progress-bar-danger-bg:      @brand-danger;\n//** Info progress bar color\n@progress-bar-info-bg:        @brand-info;\n\n\n//== List group\n//\n//##\n\n//** Background color on `.list-group-item`\n@list-group-bg:                 #fff;\n//** `.list-group-item` border color\n@list-group-border:             #ddd;\n//** List group border radius\n@list-group-border-radius:      @border-radius-base;\n\n//** Background color of single list elements on hover\n@list-group-hover-bg:           #f5f5f5;\n//** Text color of active list elements\n@list-group-active-color:       @component-active-color;\n//** Background color of active list elements\n@list-group-active-bg:          @component-active-bg;\n//** Border color of active list elements\n@list-group-active-border:      @list-group-active-bg;\n@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);\n\n@list-group-link-color:         #555;\n@list-group-link-heading-color: #333;\n\n\n//== Panels\n//\n//##\n\n@panel-bg:                    #fff;\n@panel-body-padding:          15px;\n@panel-border-radius:         @border-radius-base;\n\n//** Border color for elements within panels\n@panel-inner-border:          #ddd;\n@panel-footer-bg:             #f5f5f5;\n\n@panel-default-text:          @gray-dark;\n@panel-default-border:        #ddd;\n@panel-default-heading-bg:    #f5f5f5;\n\n@panel-primary-text:          #fff;\n@panel-primary-border:        @brand-primary;\n@panel-primary-heading-bg:    @brand-primary;\n\n@panel-success-text:          @state-success-text;\n@panel-success-border:        @state-success-border;\n@panel-success-heading-bg:    @state-success-bg;\n\n@panel-info-text:             @state-info-text;\n@panel-info-border:           @state-info-border;\n@panel-info-heading-bg:       @state-info-bg;\n\n@panel-warning-text:          @state-warning-text;\n@panel-warning-border:        @state-warning-border;\n@panel-warning-heading-bg:    @state-warning-bg;\n\n@panel-danger-text:           @state-danger-text;\n@panel-danger-border:         @state-danger-border;\n@panel-danger-heading-bg:     @state-danger-bg;\n\n\n//== Thumbnails\n//\n//##\n\n//** Padding around the thumbnail image\n@thumbnail-padding:           4px;\n//** Thumbnail background color\n@thumbnail-bg:                @body-bg;\n//** Thumbnail border color\n@thumbnail-border:            #ddd;\n//** Thumbnail border radius\n@thumbnail-border-radius:     @border-radius-base;\n\n//** Custom text color for thumbnail captions\n@thumbnail-caption-color:     @text-color;\n//** Padding around the thumbnail caption\n@thumbnail-caption-padding:   9px;\n\n\n//== Wells\n//\n//##\n\n@well-bg:                     #f5f5f5;\n@well-border:                 darken(@well-bg, 7%);\n\n\n//== Badges\n//\n//##\n\n@badge-color:                 #fff;\n//** Linked badge text color on hover\n@badge-link-hover-color:      #fff;\n@badge-bg:                    @gray-light;\n\n//** Badge text color in active nav link\n@badge-active-color:          @link-color;\n//** Badge background color in active nav link\n@badge-active-bg:             #fff;\n\n@badge-font-weight:           bold;\n@badge-line-height:           1;\n@badge-border-radius:         10px;\n\n\n//== Breadcrumbs\n//\n//##\n\n@breadcrumb-padding-vertical:   8px;\n@breadcrumb-padding-horizontal: 15px;\n//** Breadcrumb background color\n@breadcrumb-bg:                 #f5f5f5;\n//** Breadcrumb text color\n@breadcrumb-color:              #ccc;\n//** Text color of current page in the breadcrumb\n@breadcrumb-active-color:       @gray-light;\n//** Textual separator for between breadcrumb elements\n@breadcrumb-separator:          \"/\";\n\n\n//== Carousel\n//\n//##\n\n@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);\n\n@carousel-control-color:                      #fff;\n@carousel-control-width:                      15%;\n@carousel-control-opacity:                    .5;\n@carousel-control-font-size:                  20px;\n\n@carousel-indicator-active-bg:                #fff;\n@carousel-indicator-border-color:             #fff;\n\n@carousel-caption-color:                      #fff;\n\n\n//== Close\n//\n//##\n\n@close-font-weight:           bold;\n@close-color:                 #000;\n@close-text-shadow:           0 1px 0 #fff;\n\n\n//== Code\n//\n//##\n\n@code-color:                  #c7254e;\n@code-bg:                     #f9f2f4;\n\n@kbd-color:                   #fff;\n@kbd-bg:                      #333;\n\n@pre-bg:                      #f5f5f5;\n@pre-color:                   @gray-dark;\n@pre-border-color:            #ccc;\n@pre-scrollable-max-height:   340px;\n\n\n//== Type\n//\n//##\n\n//** Text muted color\n@text-muted:                  @gray-light;\n//** Abbreviations and acronyms border color\n@abbr-border-color:           @gray-light;\n//** Headings small color\n@headings-small-color:        @gray-light;\n//** Blockquote small color\n@blockquote-small-color:      @gray-light;\n//** Blockquote font size\n@blockquote-font-size:        (@font-size-base * 1.25);\n//** Blockquote border color\n@blockquote-border-color:     @gray-lighter;\n//** Page header border color\n@page-header-border-color:    @gray-lighter;\n\n\n//== Miscellaneous\n//\n//##\n\n//** Horizontal line color.\n@hr-border:                   @gray-lighter;\n\n//** Horizontal offset for forms and lists.\n@component-offset-horizontal: 180px;\n","//\n// Thumbnails\n// --------------------------------------------------\n\n\n// Mixin and adjust the regular image class\n.thumbnail {\n  display: block;\n  padding: @thumbnail-padding;\n  margin-bottom: @line-height-computed;\n  line-height: @line-height-base;\n  background-color: @thumbnail-bg;\n  border: 1px solid @thumbnail-border;\n  border-radius: @thumbnail-border-radius;\n  .transition(all .2s ease-in-out);\n\n  > img,\n  a > img {\n    &:extend(.img-responsive);\n    margin-left: auto;\n    margin-right: auto;\n  }\n\n  // Add a hover state for linked versions only\n  a&:hover,\n  a&:focus,\n  a&.active {\n    border-color: @link-color;\n  }\n\n  // Image captions\n  .caption {\n    padding: @thumbnail-caption-padding;\n    color: @thumbnail-caption-color;\n  }\n}\n","//\n// Carousel\n// --------------------------------------------------\n\n\n// Wrapper for the slide container and indicators\n.carousel {\n  position: relative;\n}\n\n.carousel-inner {\n  position: relative;\n  overflow: hidden;\n  width: 100%;\n\n  > .item {\n    display: none;\n    position: relative;\n    .transition(.6s ease-in-out left);\n\n    // Account for jankitude on images\n    > img,\n    > a > img {\n      &:extend(.img-responsive);\n      line-height: 1;\n    }\n  }\n\n  > .active,\n  > .next,\n  > .prev { display: block; }\n\n  > .active {\n    left: 0;\n  }\n\n  > .next,\n  > .prev {\n    position: absolute;\n    top: 0;\n    width: 100%;\n  }\n\n  > .next {\n    left: 100%;\n  }\n  > .prev {\n    left: -100%;\n  }\n  > .next.left,\n  > .prev.right {\n    left: 0;\n  }\n\n  > .active.left {\n    left: -100%;\n  }\n  > .active.right {\n    left: 100%;\n  }\n\n}\n\n// Left/right controls for nav\n// ---------------------------\n\n.carousel-control {\n  position: absolute;\n  top: 0;\n  left: 0;\n  bottom: 0;\n  width: @carousel-control-width;\n  .opacity(@carousel-control-opacity);\n  font-size: @carousel-control-font-size;\n  color: @carousel-control-color;\n  text-align: center;\n  text-shadow: @carousel-text-shadow;\n  // We can't have this transition here because WebKit cancels the carousel\n  // animation if you trip this while in the middle of another animation.\n\n  // Set gradients for backgrounds\n  &.left {\n    #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001));\n  }\n  &.right {\n    left: auto;\n    right: 0;\n    #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5));\n  }\n\n  // Hover/focus state\n  &:hover,\n  &:focus {\n    outline: none;\n    color: @carousel-control-color;\n    text-decoration: none;\n    .opacity(.9);\n  }\n\n  // Toggles\n  .icon-prev,\n  .icon-next,\n  .glyphicon-chevron-left,\n  .glyphicon-chevron-right {\n    position: absolute;\n    top: 50%;\n    z-index: 5;\n    display: inline-block;\n  }\n  .icon-prev,\n  .glyphicon-chevron-left {\n    left: 50%;\n  }\n  .icon-next,\n  .glyphicon-chevron-right {\n    right: 50%;\n  }\n  .icon-prev,\n  .icon-next {\n    width:  20px;\n    height: 20px;\n    margin-top: -10px;\n    margin-left: -10px;\n    font-family: serif;\n  }\n\n  .icon-prev {\n    &:before {\n      content: '\\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039)\n    }\n  }\n  .icon-next {\n    &:before {\n      content: '\\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A)\n    }\n  }\n}\n\n// Optional indicator pips\n//\n// Add an unordered list with the following class and add a list item for each\n// slide your carousel holds.\n\n.carousel-indicators {\n  position: absolute;\n  bottom: 10px;\n  left: 50%;\n  z-index: 15;\n  width: 60%;\n  margin-left: -30%;\n  padding-left: 0;\n  list-style: none;\n  text-align: center;\n\n  li {\n    display: inline-block;\n    width:  10px;\n    height: 10px;\n    margin: 1px;\n    text-indent: -999px;\n    border: 1px solid @carousel-indicator-border-color;\n    border-radius: 10px;\n    cursor: pointer;\n\n    // IE8-9 hack for event handling\n    //\n    // Internet Explorer 8-9 does not support clicks on elements without a set\n    // `background-color`. We cannot use `filter` since that's not viewed as a\n    // background color by the browser. Thus, a hack is needed.\n    //\n    // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we\n    // set alpha transparency for the best results possible.\n    background-color: #000 \\9; // IE8\n    background-color: rgba(0,0,0,0); // IE9\n  }\n  .active {\n    margin: 0;\n    width:  12px;\n    height: 12px;\n    background-color: @carousel-indicator-active-bg;\n  }\n}\n\n// Optional captions\n// -----------------------------\n// Hidden by default for smaller viewports\n.carousel-caption {\n  position: absolute;\n  left: 15%;\n  right: 15%;\n  bottom: 20px;\n  z-index: 10;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  color: @carousel-caption-color;\n  text-align: center;\n  text-shadow: @carousel-text-shadow;\n  & .btn {\n    text-shadow: none; // No shadow for button elements in carousel-caption\n  }\n}\n\n\n// Scale up controls for tablets and up\n@media screen and (min-width: @screen-sm-min) {\n\n  // Scale up the controls a smidge\n  .carousel-control {\n    .glyphicon-chevron-left,\n    .glyphicon-chevron-right,\n    .icon-prev,\n    .icon-next {\n      width: 30px;\n      height: 30px;\n      margin-top: -15px;\n      margin-left: -15px;\n      font-size: 30px;\n    }\n  }\n\n  // Show and left align the captions\n  .carousel-caption {\n    left: 20%;\n    right: 20%;\n    padding-bottom: 30px;\n  }\n\n  // Move up the indicators\n  .carousel-indicators {\n    bottom: 20px;\n  }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n  font-family: @headings-font-family;\n  font-weight: @headings-font-weight;\n  line-height: @headings-line-height;\n  color: @headings-color;\n\n  small,\n  .small {\n    font-weight: normal;\n    line-height: 1;\n    color: @headings-small-color;\n  }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n  margin-top: @line-height-computed;\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 65%;\n  }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n  margin-top: (@line-height-computed / 2);\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 75%;\n  }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n  margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n  margin-bottom: @line-height-computed;\n  font-size: floor((@font-size-base * 1.15));\n  font-weight: 200;\n  line-height: 1.4;\n\n  @media (min-width: @screen-sm-min) {\n    font-size: (@font-size-base * 1.5);\n  }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: 14px base font * 85% = about 12px\nsmall,\n.small  { font-size: 85%; }\n\n// Undo browser default styling\ncite    { font-style: normal; }\n\n// Alignment\n.text-left           { text-align: left; }\n.text-right          { text-align: right; }\n.text-center         { text-align: center; }\n.text-justify        { text-align: justify; }\n\n// Contextual colors\n.text-muted {\n  color: @text-muted;\n}\n.text-primary {\n  .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n  .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n  .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n  .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n  .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n  // Given the contrast here, this is the only class to have its color inverted\n  // automatically.\n  color: #fff;\n  .bg-variant(@brand-primary);\n}\n.bg-success {\n  .bg-variant(@state-success-bg);\n}\n.bg-info {\n  .bg-variant(@state-info-bg);\n}\n.bg-warning {\n  .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n  .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n  padding-bottom: ((@line-height-computed / 2) - 1);\n  margin: (@line-height-computed * 2) 0 @line-height-computed;\n  border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// --------------------------------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n  margin-top: 0;\n  margin-bottom: (@line-height-computed / 2);\n  ul,\n  ol {\n    margin-bottom: 0;\n  }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n  .list-unstyled();\n  margin-left: -5px;\n\n  > li {\n    display: inline-block;\n    padding-left: 5px;\n    padding-right: 5px;\n  }\n}\n\n// Description Lists\ndl {\n  margin-top: 0; // Remove browser default\n  margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n  line-height: @line-height-base;\n}\ndt {\n  font-weight: bold;\n}\ndd {\n  margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n@media (min-width: @grid-float-breakpoint) {\n  .dl-horizontal {\n    dt {\n      float: left;\n      width: (@component-offset-horizontal - 20);\n      clear: left;\n      text-align: right;\n      .text-overflow();\n    }\n    dd {\n      margin-left: @component-offset-horizontal;\n      &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n    }\n  }\n}\n\n// MISC\n// ----\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n  cursor: help;\n  border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n  font-size: 90%;\n  text-transform: uppercase;\n}\n\n// Blockquotes\nblockquote {\n  padding: (@line-height-computed / 2) @line-height-computed;\n  margin: 0 0 @line-height-computed;\n  font-size: @blockquote-font-size;\n  border-left: 5px solid @blockquote-border-color;\n\n  p,\n  ul,\n  ol {\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  // Note: Deprecated small and .small as of v3.1.0\n  // Context: https://github.com/twbs/bootstrap/issues/11660\n  footer,\n  small,\n  .small {\n    display: block;\n    font-size: 80%; // back to default font-size\n    line-height: @line-height-base;\n    color: @blockquote-small-color;\n\n    &:before {\n      content: '\\2014 \\00A0'; // em dash, nbsp\n    }\n  }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n  padding-right: 15px;\n  padding-left: 0;\n  border-right: 5px solid @blockquote-border-color;\n  border-left: 0;\n  text-align: right;\n\n  // Account for citation\n  footer,\n  small,\n  .small {\n    &:before { content: ''; }\n    &:after {\n      content: '\\00A0 \\2014'; // nbsp, em dash\n    }\n  }\n}\n\n// Quotes\nblockquote:before,\nblockquote:after {\n  content: \"\";\n}\n\n// Addresses\naddress {\n  margin-bottom: @line-height-computed;\n  font-style: normal;\n  line-height: @line-height-base;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n  font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: @code-color;\n  background-color: @code-bg;\n  white-space: nowrap;\n  border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: @kbd-color;\n  background-color: @kbd-bg;\n  border-radius: @border-radius-small;\n  box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n}\n\n// Blocks of code\npre {\n  display: block;\n  padding: ((@line-height-computed - 1) / 2);\n  margin: 0 0 (@line-height-computed / 2);\n  font-size: (@font-size-base - 1); // 14px to 13px\n  line-height: @line-height-base;\n  word-break: break-all;\n  word-wrap: break-word;\n  color: @pre-color;\n  background-color: @pre-bg;\n  border: 1px solid @pre-border-color;\n  border-radius: @border-radius-base;\n\n  // Account for some code outputs that place code tags in pre tags\n  code {\n    padding: 0;\n    font-size: inherit;\n    color: inherit;\n    white-space: pre-wrap;\n    background-color: transparent;\n    border-radius: 0;\n  }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n  max-height: @pre-scrollable-max-height;\n  overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n  .container-fixed();\n\n  @media (min-width: @screen-sm-min) {\n    width: @container-sm;\n  }\n  @media (min-width: @screen-md-min) {\n    width: @container-md;\n  }\n  @media (min-width: @screen-lg-min) {\n    width: @container-lg;\n  }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n  .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n  .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n  .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n  .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n  .make-grid(lg);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n  max-width: 100%;\n  background-color: @table-bg;\n}\nth {\n  text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n  width: 100%;\n  margin-bottom: @line-height-computed;\n  // Cells\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        padding: @table-cell-padding;\n        line-height: @line-height-base;\n        vertical-align: top;\n        border-top: 1px solid @table-border-color;\n      }\n    }\n  }\n  // Bottom align for column headings\n  > thead > tr > th {\n    vertical-align: bottom;\n    border-bottom: 2px solid @table-border-color;\n  }\n  // Remove top border from thead by default\n  > caption + thead,\n  > colgroup + thead,\n  > thead:first-child {\n    > tr:first-child {\n      > th,\n      > td {\n        border-top: 0;\n      }\n    }\n  }\n  // Account for multiple tbody instances\n  > tbody + tbody {\n    border-top: 2px solid @table-border-color;\n  }\n\n  // Nesting\n  .table {\n    background-color: @body-bg;\n  }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        padding: @table-condensed-cell-padding;\n      }\n    }\n  }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n  border: 1px solid @table-border-color;\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        border: 1px solid @table-border-color;\n      }\n    }\n  }\n  > thead > tr {\n    > th,\n    > td {\n      border-bottom-width: 2px;\n    }\n  }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n  > tbody > tr:nth-child(odd) {\n    > td,\n    > th {\n      background-color: @table-bg-accent;\n    }\n  }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n  > tbody > tr:hover {\n    > td,\n    > th {\n      background-color: @table-bg-hover;\n    }\n  }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n  position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n  float: none;\n  display: table-column;\n}\ntable {\n  td,\n  th {\n    &[class*=\"col-\"] {\n      position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n      float: none;\n      display: table-cell;\n    }\n  }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n@media (max-width: @screen-xs-max) {\n  .table-responsive {\n    width: 100%;\n    margin-bottom: (@line-height-computed * 0.75);\n    overflow-y: hidden;\n    overflow-x: scroll;\n    -ms-overflow-style: -ms-autohiding-scrollbar;\n    border: 1px solid @table-border-color;\n    -webkit-overflow-scrolling: touch;\n\n    // Tighten up spacing\n    > .table {\n      margin-bottom: 0;\n\n      // Ensure the content doesn't wrap\n      > thead,\n      > tbody,\n      > tfoot {\n        > tr {\n          > th,\n          > td {\n            white-space: nowrap;\n          }\n        }\n      }\n    }\n\n    // Special overrides for the bordered tables\n    > .table-bordered {\n      border: 0;\n\n      // Nuke the appropriate borders so that the parent can handle them\n      > thead,\n      > tbody,\n      > tfoot {\n        > tr {\n          > th:first-child,\n          > td:first-child {\n            border-left: 0;\n          }\n          > th:last-child,\n          > td:last-child {\n            border-right: 0;\n          }\n        }\n      }\n\n      // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n      // chances are there will be only one `tr` in a `thead` and that would\n      // remove the border altogether.\n      > tbody,\n      > tfoot {\n        > tr:last-child {\n          > th,\n          > td {\n            border-bottom: 0;\n          }\n        }\n      }\n\n    }\n  }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n  padding: 0;\n  margin: 0;\n  border: 0;\n  // Chrome and Firefox set a `min-width: -webkit-min-content;` on fieldsets,\n  // so we reset that to ensure it behaves more like a standard block element.\n  // See https://github.com/twbs/bootstrap/issues/12359.\n  min-width: 0;\n}\n\nlegend {\n  display: block;\n  width: 100%;\n  padding: 0;\n  margin-bottom: @line-height-computed;\n  font-size: (@font-size-base * 1.5);\n  line-height: inherit;\n  color: @legend-color;\n  border: 0;\n  border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n  display: inline-block;\n  margin-bottom: 5px;\n  font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n  .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n  margin: 4px 0 0;\n  margin-top: 1px \\9; /* IE8-9 */\n  line-height: normal;\n}\n\n// Set the height of file controls to match text inputs\ninput[type=\"file\"] {\n  display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n  display: block;\n  width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n  height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n  .tab-focus();\n}\n\n// Adjust output element\noutput {\n  display: block;\n  padding-top: (@padding-base-vertical + 1);\n  font-size: @font-size-base;\n  line-height: @line-height-base;\n  color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n  display: block;\n  width: 100%;\n  height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n  padding: @padding-base-vertical @padding-base-horizontal;\n  font-size: @font-size-base;\n  line-height: @line-height-base;\n  color: @input-color;\n  background-color: @input-bg;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid @input-border;\n  border-radius: @input-border-radius;\n  .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n  .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n  // Customize the `:focus` state to imitate native WebKit styles.\n  .form-control-focus();\n\n  // Placeholder\n  .placeholder();\n\n  // Disabled and read-only inputs\n  //\n  // HTML5 says that controls under a fieldset > legend:first-child won't be\n  // disabled if the fieldset is disabled. Due to implementation difficulty, we\n  // don't honor that edge case; we style them as disabled anyway.\n  &[disabled],\n  &[readonly],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n    background-color: @input-bg-disabled;\n    opacity: 1; // iOS fix for unreadable disabled content\n  }\n\n  // Reset height for `textarea`s\n  textarea& {\n    height: auto;\n  }\n}\n\n\n// Search inputs in iOS\n//\n// This overrides the extra rounded corners on search inputs in iOS so that our\n// `.form-control` class can properly style them. Note that this cannot simply\n// be added to `.form-control` as it's not specific enough. For details, see\n// https://github.com/twbs/bootstrap/issues/11586.\n\ninput[type=\"search\"] {\n  -webkit-appearance: none;\n}\n\n\n// Special styles for iOS date input\n//\n// In Mobile Safari, date inputs require a pixel line-height that matches the\n// given height of the input.\n\ninput[type=\"date\"] {\n  line-height: @input-height-base;\n}\n\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n  margin-bottom: 15px;\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.radio,\n.checkbox {\n  display: block;\n  min-height: @line-height-computed; // clear the floating input if there is no label text\n  margin-top: 10px;\n  margin-bottom: 10px;\n  padding-left: 20px;\n  label {\n    display: inline;\n    font-weight: normal;\n    cursor: pointer;\n  }\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n  float: left;\n  margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n  margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing\n}\n\n// Radios and checkboxes on same line\n.radio-inline,\n.checkbox-inline {\n  display: inline-block;\n  padding-left: 20px;\n  margin-bottom: 0;\n  vertical-align: middle;\n  font-weight: normal;\n  cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n  margin-top: 0;\n  margin-left: 10px; // space out consecutive inline controls\n}\n\n// Apply same disabled cursor tweak as for inputs\n//\n// Note: Neither radios nor checkboxes can be readonly.\ninput[type=\"radio\"],\ninput[type=\"checkbox\"],\n.radio,\n.radio-inline,\n.checkbox,\n.checkbox-inline {\n  &[disabled],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n  }\n}\n\n\n// Form control sizing\n//\n// Build on `.form-control` with modifier classes to decrease or increase the\n// height and font-size of form controls.\n\n.input-sm {\n  .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n\n.input-lg {\n  .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n\n\n// Form control feedback states\n//\n// Apply contextual and semantic states to individual form controls.\n\n.has-feedback {\n  // Enable absolute positioning\n  position: relative;\n\n  // Ensure icons don't overlap text\n  .form-control {\n    padding-right: (@input-height-base * 1.25);\n  }\n\n  // Feedback icon (requires .glyphicon classes)\n  .form-control-feedback {\n    position: absolute;\n    top: (@line-height-computed + 5); // Height of the `label` and its margin\n    right: 0;\n    display: block;\n    width: @input-height-base;\n    height: @input-height-base;\n    line-height: @input-height-base;\n    text-align: center;\n  }\n}\n\n// Feedback states\n.has-success {\n  .form-control-validation(@state-success-text; @state-success-text; @state-success-bg);\n}\n.has-warning {\n  .form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg);\n}\n.has-error {\n  .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg);\n}\n\n\n// Static form control text\n//\n// Apply class to a `p` element to make any string of text align with labels in\n// a horizontal form layout.\n\n.form-control-static {\n  margin-bottom: 0; // Remove default margin from `p`\n}\n\n\n// Help text\n//\n// Apply to any element you wish to create light text for placement immediately\n// below a form control. Use for general help, formatting, or instructional text.\n\n.help-block {\n  display: block; // account for any element using help-block\n  margin-top: 5px;\n  margin-bottom: 10px;\n  color: lighten(@text-color, 25%); // lighten the text some for contrast\n}\n\n\n\n// Inline forms\n//\n// Make forms appear inline(-block) by adding the `.form-inline` class. Inline\n// forms begin stacked on extra small (mobile) devices and then go inline when\n// viewports reach <768px.\n//\n// Requires wrapping inputs and labels with `.form-group` for proper display of\n// default HTML form controls and our custom form controls (e.g., input groups).\n//\n// Heads up! This is mixin-ed into `.navbar-form` in navbars.less.\n\n.form-inline {\n\n  // Kick in the inline\n  @media (min-width: @screen-sm-min) {\n    // Inline-block all the things for \"inline\"\n    .form-group {\n      display: inline-block;\n      margin-bottom: 0;\n      vertical-align: middle;\n    }\n\n    // In navbar-form, allow folks to *not* use `.form-group`\n    .form-control {\n      display: inline-block;\n      width: auto; // Prevent labels from stacking above inputs in `.form-group`\n      vertical-align: middle;\n    }\n    // Input groups need that 100% width though\n    .input-group > .form-control {\n      width: 100%;\n    }\n\n    .control-label {\n      margin-bottom: 0;\n      vertical-align: middle;\n    }\n\n    // Remove default margin on radios/checkboxes that were used for stacking, and\n    // then undo the floating of radios and checkboxes to match (which also avoids\n    // a bug in WebKit: https://github.com/twbs/bootstrap/issues/1969).\n    .radio,\n    .checkbox {\n      display: inline-block;\n      margin-top: 0;\n      margin-bottom: 0;\n      padding-left: 0;\n      vertical-align: middle;\n    }\n    .radio input[type=\"radio\"],\n    .checkbox input[type=\"checkbox\"] {\n      float: none;\n      margin-left: 0;\n    }\n\n    // Validation states\n    //\n    // Reposition the icon because it's now within a grid column and columns have\n    // `position: relative;` on them. Also accounts for the grid gutter padding.\n    .has-feedback .form-control-feedback {\n      top: 0;\n    }\n  }\n}\n\n\n// Horizontal forms\n//\n// Horizontal forms are built on grid classes and allow you to create forms with\n// labels on the left and inputs on the right.\n\n.form-horizontal {\n\n  // Consistent vertical alignment of labels, radios, and checkboxes\n  .control-label,\n  .radio,\n  .checkbox,\n  .radio-inline,\n  .checkbox-inline {\n    margin-top: 0;\n    margin-bottom: 0;\n    padding-top: (@padding-base-vertical + 1); // Default padding plus a border\n  }\n  // Account for padding we're adding to ensure the alignment and of help text\n  // and other content below items\n  .radio,\n  .checkbox {\n    min-height: (@line-height-computed + (@padding-base-vertical + 1));\n  }\n\n  // Make form groups behave like rows\n  .form-group {\n    .make-row();\n  }\n\n  .form-control-static {\n    padding-top: (@padding-base-vertical + 1);\n  }\n\n  // Only right align form labels here when the columns stop stacking\n  @media (min-width: @screen-sm-min) {\n    .control-label {\n      text-align: right;\n    }\n  }\n\n  // Validation states\n  //\n  // Reposition the icon because it's now within a grid column and columns have\n  // `position: relative;` on them. Also accounts for the grid gutter padding.\n  .has-feedback .form-control-feedback {\n    top: 0;\n    right: (@grid-gutter-width / 2);\n  }\n}\n","//\n// Buttons\n// --------------------------------------------------\n\n\n// Base styles\n// --------------------------------------------------\n\n.btn {\n  display: inline-block;\n  margin-bottom: 0; // For input.btn\n  font-weight: @btn-font-weight;\n  text-align: center;\n  vertical-align: middle;\n  cursor: pointer;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid transparent;\n  white-space: nowrap;\n  .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base);\n  .user-select(none);\n\n  &,\n  &:active,\n  &.active {\n    &:focus {\n      .tab-focus();\n    }\n  }\n\n  &:hover,\n  &:focus {\n    color: @btn-default-color;\n    text-decoration: none;\n  }\n\n  &:active,\n  &.active {\n    outline: 0;\n    background-image: none;\n    .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n  }\n\n  &.disabled,\n  &[disabled],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n    pointer-events: none; // Future-proof disabling of clicks\n    .opacity(.65);\n    .box-shadow(none);\n  }\n}\n\n\n// Alternate buttons\n// --------------------------------------------------\n\n.btn-default {\n  .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border);\n}\n.btn-primary {\n  .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border);\n}\n// Success appears as green\n.btn-success {\n  .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border);\n}\n// Info appears as blue-green\n.btn-info {\n  .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border);\n}\n// Warning appears as orange\n.btn-warning {\n  .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border);\n}\n// Danger and error appear as red\n.btn-danger {\n  .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border);\n}\n\n\n// Link buttons\n// -------------------------\n\n// Make a button look and behave like a link\n.btn-link {\n  color: @link-color;\n  font-weight: normal;\n  cursor: pointer;\n  border-radius: 0;\n\n  &,\n  &:active,\n  &[disabled],\n  fieldset[disabled] & {\n    background-color: transparent;\n    .box-shadow(none);\n  }\n  &,\n  &:hover,\n  &:focus,\n  &:active {\n    border-color: transparent;\n  }\n  &:hover,\n  &:focus {\n    color: @link-hover-color;\n    text-decoration: underline;\n    background-color: transparent;\n  }\n  &[disabled],\n  fieldset[disabled] & {\n    &:hover,\n    &:focus {\n      color: @btn-link-disabled-color;\n      text-decoration: none;\n    }\n  }\n}\n\n\n// Button Sizes\n// --------------------------------------------------\n\n.btn-lg {\n  // line-height: ensure even-numbered height of button next to large input\n  .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n.btn-sm {\n  // line-height: ensure proper height of button next to small input\n  .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n.btn-xs {\n  .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n\n\n// Block button\n// --------------------------------------------------\n\n.btn-block {\n  display: block;\n  width: 100%;\n  padding-left: 0;\n  padding-right: 0;\n}\n\n// Vertically space out multiple block buttons\n.btn-block + .btn-block {\n  margin-top: 5px;\n}\n\n// Specificity overrides\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"] {\n  &.btn-block {\n    width: 100%;\n  }\n}\n","//\n// Button groups\n// --------------------------------------------------\n\n// Make the div behave like a button\n.btn-group,\n.btn-group-vertical {\n  position: relative;\n  display: inline-block;\n  vertical-align: middle; // match .btn alignment given font-size hack above\n  > .btn {\n    position: relative;\n    float: left;\n    // Bring the \"active\" button to the front\n    &:hover,\n    &:focus,\n    &:active,\n    &.active {\n      z-index: 2;\n    }\n    &:focus {\n      // Remove focus outline when dropdown JS adds it after closing the menu\n      outline: none;\n    }\n  }\n}\n\n// Prevent double borders when buttons are next to each other\n.btn-group {\n  .btn + .btn,\n  .btn + .btn-group,\n  .btn-group + .btn,\n  .btn-group + .btn-group {\n    margin-left: -1px;\n  }\n}\n\n// Optional: Group multiple button groups together for a toolbar\n.btn-toolbar {\n  margin-left: -5px; // Offset the first child's margin\n  &:extend(.clearfix all);\n\n  .btn-group,\n  .input-group {\n    float: left;\n  }\n  > .btn,\n  > .btn-group,\n  > .input-group {\n    margin-left: 5px;\n  }\n}\n\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n  border-radius: 0;\n}\n\n// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match\n.btn-group > .btn:first-child {\n  margin-left: 0;\n  &:not(:last-child):not(.dropdown-toggle) {\n    .border-right-radius(0);\n  }\n}\n// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n  .border-left-radius(0);\n}\n\n// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group)\n.btn-group > .btn-group {\n  float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group > .btn-group:first-child {\n  > .btn:last-child,\n  > .dropdown-toggle {\n    .border-right-radius(0);\n  }\n}\n.btn-group > .btn-group:last-child > .btn:first-child {\n  .border-left-radius(0);\n}\n\n// On active and open, don't show outline\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n  outline: 0;\n}\n\n\n// Sizing\n//\n// Remix the default button sizing classes into new ones for easier manipulation.\n\n.btn-group-xs > .btn { &:extend(.btn-xs); }\n.btn-group-sm > .btn { &:extend(.btn-sm); }\n.btn-group-lg > .btn { &:extend(.btn-lg); }\n\n\n// Split button dropdowns\n// ----------------------\n\n// Give the line between buttons some depth\n.btn-group > .btn + .dropdown-toggle {\n  padding-left: 8px;\n  padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n  padding-left: 12px;\n  padding-right: 12px;\n}\n\n// The clickable button for toggling the menu\n// Remove the gradient and set the same inset shadow as the :active state\n.btn-group.open .dropdown-toggle {\n  .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n\n  // Show no shadow for `.btn-link` since it has no other button styles.\n  &.btn-link {\n    .box-shadow(none);\n  }\n}\n\n\n// Reposition the caret\n.btn .caret {\n  margin-left: 0;\n}\n// Carets in other button sizes\n.btn-lg .caret {\n  border-width: @caret-width-large @caret-width-large 0;\n  border-bottom-width: 0;\n}\n// Upside down carets for .dropup\n.dropup .btn-lg .caret {\n  border-width: 0 @caret-width-large @caret-width-large;\n}\n\n\n// Vertical button groups\n// ----------------------\n\n.btn-group-vertical {\n  > .btn,\n  > .btn-group,\n  > .btn-group > .btn {\n    display: block;\n    float: none;\n    width: 100%;\n    max-width: 100%;\n  }\n\n  // Clear floats so dropdown menus can be properly placed\n  > .btn-group {\n    &:extend(.clearfix all);\n    > .btn {\n      float: none;\n    }\n  }\n\n  > .btn + .btn,\n  > .btn + .btn-group,\n  > .btn-group + .btn,\n  > .btn-group + .btn-group {\n    margin-top: -1px;\n    margin-left: 0;\n  }\n}\n\n.btn-group-vertical > .btn {\n  &:not(:first-child):not(:last-child) {\n    border-radius: 0;\n  }\n  &:first-child:not(:last-child) {\n    border-top-right-radius: @border-radius-base;\n    .border-bottom-radius(0);\n  }\n  &:last-child:not(:first-child) {\n    border-bottom-left-radius: @border-radius-base;\n    .border-top-radius(0);\n  }\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) {\n  > .btn:last-child,\n  > .dropdown-toggle {\n    .border-bottom-radius(0);\n  }\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  .border-top-radius(0);\n}\n\n\n\n// Justified button groups\n// ----------------------\n\n.btn-group-justified {\n  display: table;\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: separate;\n  > .btn,\n  > .btn-group {\n    float: none;\n    display: table-cell;\n    width: 1%;\n  }\n  > .btn-group .btn {\n    width: 100%;\n  }\n}\n\n\n// Checkbox and radio options\n[data-toggle=\"buttons\"] > .btn > input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn > input[type=\"checkbox\"] {\n  display: none;\n}\n","//\n// Component animations\n// --------------------------------------------------\n\n// Heads up!\n//\n// We don't use the `.opacity()` mixin here since it causes a bug with text\n// fields in IE7-8. Source: https://github.com/twitter/bootstrap/pull/3552.\n\n.fade {\n  opacity: 0;\n  .transition(opacity .15s linear);\n  &.in {\n    opacity: 1;\n  }\n}\n\n.collapse {\n  display: none;\n  &.in {\n    display: block;\n  }\n}\n.collapsing {\n  position: relative;\n  height: 0;\n  overflow: hidden;\n  .transition(height .35s ease);\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// <a href=\"#\"><span class=\"glyphicon glyphicon-star\"></span> Star</a>\n\n// Import the fonts\n@font-face {\n  font-family: 'Glyphicons Halflings';\n  src: ~\"url('@{icon-font-path}@{icon-font-name}.eot')\";\n  src: ~\"url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.woff') format('woff')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg')\";\n}\n\n// Catchall baseclass\n.glyphicon {\n  position: relative;\n  top: 1px;\n  display: inline-block;\n  font-family: 'Glyphicons Halflings';\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk               { &:before { content: \"\\2a\"; } }\n.glyphicon-plus                   { &:before { content: \"\\2b\"; } }\n.glyphicon-euro                   { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus                  { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud                  { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope               { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil                 { &:before { content: \"\\270f\"; } }\n.glyphicon-glass                  { &:before { content: \"\\e001\"; } }\n.glyphicon-music                  { &:before { content: \"\\e002\"; } }\n.glyphicon-search                 { &:before { content: \"\\e003\"; } }\n.glyphicon-heart                  { &:before { content: \"\\e005\"; } }\n.glyphicon-star                   { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty             { &:before { content: \"\\e007\"; } }\n.glyphicon-user                   { &:before { content: \"\\e008\"; } }\n.glyphicon-film                   { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large               { &:before { content: \"\\e010\"; } }\n.glyphicon-th                     { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list                { &:before { content: \"\\e012\"; } }\n.glyphicon-ok                     { &:before { content: \"\\e013\"; } }\n.glyphicon-remove                 { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in                { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out               { &:before { content: \"\\e016\"; } }\n.glyphicon-off                    { &:before { content: \"\\e017\"; } }\n.glyphicon-signal                 { &:before { content: \"\\e018\"; } }\n.glyphicon-cog                    { &:before { content: \"\\e019\"; } }\n.glyphicon-trash                  { &:before { content: \"\\e020\"; } }\n.glyphicon-home                   { &:before { content: \"\\e021\"; } }\n.glyphicon-file                   { &:before { content: \"\\e022\"; } }\n.glyphicon-time                   { &:before { content: \"\\e023\"; } }\n.glyphicon-road                   { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt           { &:before { content: \"\\e025\"; } }\n.glyphicon-download               { &:before { content: \"\\e026\"; } }\n.glyphicon-upload                 { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox                  { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle            { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat                 { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh                { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt               { &:before { content: \"\\e032\"; } }\n.glyphicon-lock                   { &:before { content: \"\\e033\"; } }\n.glyphicon-flag                   { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones             { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off             { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down            { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up              { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode                 { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode                { &:before { content: \"\\e040\"; } }\n.glyphicon-tag                    { &:before { content: \"\\e041\"; } }\n.glyphicon-tags                   { &:before { content: \"\\e042\"; } }\n.glyphicon-book                   { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark               { &:before { content: \"\\e044\"; } }\n.glyphicon-print                  { &:before { content: \"\\e045\"; } }\n.glyphicon-camera                 { &:before { content: \"\\e046\"; } }\n.glyphicon-font                   { &:before { content: \"\\e047\"; } }\n.glyphicon-bold                   { &:before { content: \"\\e048\"; } }\n.glyphicon-italic                 { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height            { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width             { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left             { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center           { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right            { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify          { &:before { content: \"\\e055\"; } }\n.glyphicon-list                   { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left            { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right           { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video         { &:before { content: \"\\e059\"; } }\n.glyphicon-picture                { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker             { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust                 { &:before { content: \"\\e063\"; } }\n.glyphicon-tint                   { &:before { content: \"\\e064\"; } }\n.glyphicon-edit                   { &:before { content: \"\\e065\"; } }\n.glyphicon-share                  { &:before { content: \"\\e066\"; } }\n.glyphicon-check                  { &:before { content: \"\\e067\"; } }\n.glyphicon-move                   { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward          { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward          { &:before { content: \"\\e070\"; } }\n.glyphicon-backward               { &:before { content: \"\\e071\"; } }\n.glyphicon-play                   { &:before { content: \"\\e072\"; } }\n.glyphicon-pause                  { &:before { content: \"\\e073\"; } }\n.glyphicon-stop                   { &:before { content: \"\\e074\"; } }\n.glyphicon-forward                { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward           { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward           { &:before { content: \"\\e077\"; } }\n.glyphicon-eject                  { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left           { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right          { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign              { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign             { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign            { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign                { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign          { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign              { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot             { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle          { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle              { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle             { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left             { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right            { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up               { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down             { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt              { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full            { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small           { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign       { &:before { content: \"\\e101\"; } }\n.glyphicon-gift                   { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf                   { &:before { content: \"\\e103\"; } }\n.glyphicon-fire                   { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open               { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close              { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign           { &:before { content: \"\\e107\"; } }\n.glyphicon-plane                  { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar               { &:before { content: \"\\e109\"; } }\n.glyphicon-random                 { &:before { content: \"\\e110\"; } }\n.glyphicon-comment                { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet                 { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up             { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down           { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet                { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart          { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close           { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open            { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical        { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal      { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd                    { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn               { &:before { content: \"\\e122\"; } }\n.glyphicon-bell                   { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate            { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up              { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down            { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right             { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left              { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up                { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down              { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right     { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left      { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up        { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down      { &:before { content: \"\\e134\"; } }\n.glyphicon-globe                  { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench                 { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks                  { &:before { content: \"\\e137\"; } }\n.glyphicon-filter                 { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase              { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen             { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard              { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip              { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty            { &:before { content: \"\\e143\"; } }\n.glyphicon-link                   { &:before { content: \"\\e144\"; } }\n.glyphicon-phone                  { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin                { &:before { content: \"\\e146\"; } }\n.glyphicon-usd                    { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp                    { &:before { content: \"\\e149\"; } }\n.glyphicon-sort                   { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet       { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt   { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order          { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt      { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes     { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked              { &:before { content: \"\\e157\"; } }\n.glyphicon-expand                 { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down          { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up            { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in                 { &:before { content: \"\\e161\"; } }\n.glyphicon-flash                  { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out                { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window             { &:before { content: \"\\e164\"; } }\n.glyphicon-record                 { &:before { content: \"\\e165\"; } }\n.glyphicon-save                   { &:before { content: \"\\e166\"; } }\n.glyphicon-open                   { &:before { content: \"\\e167\"; } }\n.glyphicon-saved                  { &:before { content: \"\\e168\"; } }\n.glyphicon-import                 { &:before { content: \"\\e169\"; } }\n.glyphicon-export                 { &:before { content: \"\\e170\"; } }\n.glyphicon-send                   { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk            { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved           { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove          { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save            { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open            { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card            { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer               { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery                { &:before { content: \"\\e179\"; } }\n.glyphicon-header                 { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed             { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone               { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt              { &:before { content: \"\\e183\"; } }\n.glyphicon-tower                  { &:before { content: \"\\e184\"; } }\n.glyphicon-stats                  { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video               { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video               { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles              { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo           { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby            { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1              { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1              { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1              { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark         { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark      { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download         { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload           { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer           { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous         { &:before { content: \"\\e200\"; } }\n","//\n// Dropdown menus\n// --------------------------------------------------\n\n\n// Dropdown arrow/caret\n.caret {\n  display: inline-block;\n  width: 0;\n  height: 0;\n  margin-left: 2px;\n  vertical-align: middle;\n  border-top:   @caret-width-base solid;\n  border-right: @caret-width-base solid transparent;\n  border-left:  @caret-width-base solid transparent;\n}\n\n// The dropdown wrapper (div)\n.dropdown {\n  position: relative;\n}\n\n// Prevent the focus on the dropdown toggle when closing dropdowns\n.dropdown-toggle:focus {\n  outline: 0;\n}\n\n// The dropdown menu (ul)\n.dropdown-menu {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: @zindex-dropdown;\n  display: none; // none by default, but block on \"open\" of the menu\n  float: left;\n  min-width: 160px;\n  padding: 5px 0;\n  margin: 2px 0 0; // override default ul\n  list-style: none;\n  font-size: @font-size-base;\n  background-color: @dropdown-bg;\n  border: 1px solid @dropdown-fallback-border; // IE8 fallback\n  border: 1px solid @dropdown-border;\n  border-radius: @border-radius-base;\n  .box-shadow(0 6px 12px rgba(0,0,0,.175));\n  background-clip: padding-box;\n\n  // Aligns the dropdown menu to right\n  //\n  // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`\n  &.pull-right {\n    right: 0;\n    left: auto;\n  }\n\n  // Dividers (basically an hr) within the dropdown\n  .divider {\n    .nav-divider(@dropdown-divider-bg);\n  }\n\n  // Links within the dropdown menu\n  > li > a {\n    display: block;\n    padding: 3px 20px;\n    clear: both;\n    font-weight: normal;\n    line-height: @line-height-base;\n    color: @dropdown-link-color;\n    white-space: nowrap; // prevent links from randomly breaking onto new lines\n  }\n}\n\n// Hover/Focus state\n.dropdown-menu > li > a {\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    color: @dropdown-link-hover-color;\n    background-color: @dropdown-link-hover-bg;\n  }\n}\n\n// Active state\n.dropdown-menu > .active > a {\n  &,\n  &:hover,\n  &:focus {\n    color: @dropdown-link-active-color;\n    text-decoration: none;\n    outline: 0;\n    background-color: @dropdown-link-active-bg;\n  }\n}\n\n// Disabled state\n//\n// Gray out text and ensure the hover/focus state remains gray\n\n.dropdown-menu > .disabled > a {\n  &,\n  &:hover,\n  &:focus {\n    color: @dropdown-link-disabled-color;\n  }\n}\n// Nuke hover/focus effects\n.dropdown-menu > .disabled > a {\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    background-color: transparent;\n    background-image: none; // Remove CSS gradient\n    .reset-filter();\n    cursor: not-allowed;\n  }\n}\n\n// Open state for the dropdown\n.open {\n  // Show the menu\n  > .dropdown-menu {\n    display: block;\n  }\n\n  // Remove the outline when :focus is triggered\n  > a {\n    outline: 0;\n  }\n}\n\n// Menu positioning\n//\n// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown\n// menu with the parent.\n.dropdown-menu-right {\n  left: auto; // Reset the default from `.dropdown-menu`\n  right: 0;\n}\n// With v3, we enabled auto-flipping if you have a dropdown within a right\n// aligned nav component. To enable the undoing of that, we provide an override\n// to restore the default dropdown menu alignment.\n//\n// This is only for left-aligning a dropdown menu within a `.navbar-right` or\n// `.pull-right` nav component.\n.dropdown-menu-left {\n  left: 0;\n  right: auto;\n}\n\n// Dropdown section headers\n.dropdown-header {\n  display: block;\n  padding: 3px 20px;\n  font-size: @font-size-small;\n  line-height: @line-height-base;\n  color: @dropdown-header-color;\n}\n\n// Backdrop to catch body clicks on mobile, etc.\n.dropdown-backdrop {\n  position: fixed;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  top: 0;\n  z-index: (@zindex-dropdown - 10);\n}\n\n// Right aligned dropdowns\n.pull-right > .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n//\n// Just add .dropup after the standard .dropdown class and you're set, bro.\n// TODO: abstract this so that the navbar fixed styles are not placed here?\n\n.dropup,\n.navbar-fixed-bottom .dropdown {\n  // Reverse the caret\n  .caret {\n    border-top: 0;\n    border-bottom: @caret-width-base solid;\n    content: \"\";\n  }\n  // Different positioning for bottom up menu\n  .dropdown-menu {\n    top: auto;\n    bottom: 100%;\n    margin-bottom: 1px;\n  }\n}\n\n\n// Component alignment\n//\n// Reiterate per navbar.less and the modified component alignment there.\n\n@media (min-width: @grid-float-breakpoint) {\n  .navbar-right {\n    .dropdown-menu {\n      .dropdown-menu-right();\n    }\n    // Necessary for overrides of the default right aligned menu.\n    // Will remove come v4 in all likelihood.\n    .dropdown-menu-left {\n      .dropdown-menu-left();\n    }\n  }\n}\n\n","//\n// Input groups\n// --------------------------------------------------\n\n// Base styles\n// -------------------------\n.input-group {\n  position: relative; // For dropdowns\n  display: table;\n  border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table\n\n  // Undo padding and float of grid classes\n  &[class*=\"col-\"] {\n    float: none;\n    padding-left: 0;\n    padding-right: 0;\n  }\n\n  .form-control {\n    // Ensure that the input is always above the *appended* addon button for\n    // proper border colors.\n    position: relative;\n    z-index: 2;\n\n    // IE9 fubars the placeholder attribute in text inputs and the arrows on\n    // select elements in input groups. To fix it, we float the input. Details:\n    // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855\n    float: left;\n\n    width: 100%;\n    margin-bottom: 0;\n  }\n}\n\n// Sizing options\n//\n// Remix the default form control sizing classes into new ones for easier\n// manipulation.\n\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn { .input-lg(); }\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn { .input-sm(); }\n\n\n// Display as table-cell\n// -------------------------\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n  display: table-cell;\n\n  &:not(:first-child):not(:last-child) {\n    border-radius: 0;\n  }\n}\n// Addon and addon wrapper for buttons\n.input-group-addon,\n.input-group-btn {\n  width: 1%;\n  white-space: nowrap;\n  vertical-align: middle; // Match the inputs\n}\n\n// Text input groups\n// -------------------------\n.input-group-addon {\n  padding: @padding-base-vertical @padding-base-horizontal;\n  font-size: @font-size-base;\n  font-weight: normal;\n  line-height: 1;\n  color: @input-color;\n  text-align: center;\n  background-color: @input-group-addon-bg;\n  border: 1px solid @input-group-addon-border-color;\n  border-radius: @border-radius-base;\n\n  // Sizing\n  &.input-sm {\n    padding: @padding-small-vertical @padding-small-horizontal;\n    font-size: @font-size-small;\n    border-radius: @border-radius-small;\n  }\n  &.input-lg {\n    padding: @padding-large-vertical @padding-large-horizontal;\n    font-size: @font-size-large;\n    border-radius: @border-radius-large;\n  }\n\n  // Nuke default margins from checkboxes and radios to vertically center within.\n  input[type=\"radio\"],\n  input[type=\"checkbox\"] {\n    margin-top: 0;\n  }\n}\n\n// Reset rounded corners\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n  .border-right-radius(0);\n}\n.input-group-addon:first-child {\n  border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n  .border-left-radius(0);\n}\n.input-group-addon:last-child {\n  border-left: 0;\n}\n\n// Button input groups\n// -------------------------\n.input-group-btn {\n  position: relative;\n  // Jankily prevent input button groups from wrapping with `white-space` and\n  // `font-size` in combination with `inline-block` on buttons.\n  font-size: 0;\n  white-space: nowrap;\n\n  // Negative margin for spacing, position for bringing hovered/focused/actived\n  // element above the siblings.\n  > .btn {\n    position: relative;\n    + .btn {\n      margin-left: -1px;\n    }\n    // Bring the \"active\" button to the front\n    &:hover,\n    &:focus,\n    &:active {\n      z-index: 2;\n    }\n  }\n\n  // Negative margin to only have a 1px border between the two\n  &:first-child {\n    > .btn,\n    > .btn-group {\n      margin-right: -1px;\n    }\n  }\n  &:last-child {\n    > .btn,\n    > .btn-group {\n      margin-left: -1px;\n    }\n  }\n}\n","//\n// Navs\n// --------------------------------------------------\n\n\n// Base class\n// --------------------------------------------------\n\n.nav {\n  margin-bottom: 0;\n  padding-left: 0; // Override default ul/ol\n  list-style: none;\n  &:extend(.clearfix all);\n\n  > li {\n    position: relative;\n    display: block;\n\n    > a {\n      position: relative;\n      display: block;\n      padding: @nav-link-padding;\n      &:hover,\n      &:focus {\n        text-decoration: none;\n        background-color: @nav-link-hover-bg;\n      }\n    }\n\n    // Disabled state sets text to gray and nukes hover/tab effects\n    &.disabled > a {\n      color: @nav-disabled-link-color;\n\n      &:hover,\n      &:focus {\n        color: @nav-disabled-link-hover-color;\n        text-decoration: none;\n        background-color: transparent;\n        cursor: not-allowed;\n      }\n    }\n  }\n\n  // Open dropdowns\n  .open > a {\n    &,\n    &:hover,\n    &:focus {\n      background-color: @nav-link-hover-bg;\n      border-color: @link-color;\n    }\n  }\n\n  // Nav dividers (deprecated with v3.0.1)\n  //\n  // This should have been removed in v3 with the dropping of `.nav-list`, but\n  // we missed it. We don't currently support this anywhere, but in the interest\n  // of maintaining backward compatibility in case you use it, it's deprecated.\n  .nav-divider {\n    .nav-divider();\n  }\n\n  // Prevent IE8 from misplacing imgs\n  //\n  // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989\n  > li > a > img {\n    max-width: none;\n  }\n}\n\n\n// Tabs\n// -------------------------\n\n// Give the tabs something to sit on\n.nav-tabs {\n  border-bottom: 1px solid @nav-tabs-border-color;\n  > li {\n    float: left;\n    // Make the list-items overlay the bottom border\n    margin-bottom: -1px;\n\n    // Actual tabs (as links)\n    > a {\n      margin-right: 2px;\n      line-height: @line-height-base;\n      border: 1px solid transparent;\n      border-radius: @border-radius-base @border-radius-base 0 0;\n      &:hover {\n        border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color;\n      }\n    }\n\n    // Active state, and its :hover to override normal :hover\n    &.active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @nav-tabs-active-link-hover-color;\n        background-color: @nav-tabs-active-link-hover-bg;\n        border: 1px solid @nav-tabs-active-link-hover-border-color;\n        border-bottom-color: transparent;\n        cursor: default;\n      }\n    }\n  }\n  // pulling this in mainly for less shorthand\n  &.nav-justified {\n    .nav-justified();\n    .nav-tabs-justified();\n  }\n}\n\n\n// Pills\n// -------------------------\n.nav-pills {\n  > li {\n    float: left;\n\n    // Links rendered as pills\n    > a {\n      border-radius: @nav-pills-border-radius;\n    }\n    + li {\n      margin-left: 2px;\n    }\n\n    // Active state\n    &.active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @nav-pills-active-link-hover-color;\n        background-color: @nav-pills-active-link-hover-bg;\n      }\n    }\n  }\n}\n\n\n// Stacked pills\n.nav-stacked {\n  > li {\n    float: none;\n    + li {\n      margin-top: 2px;\n      margin-left: 0; // no need for this gap between nav items\n    }\n  }\n}\n\n\n// Nav variations\n// --------------------------------------------------\n\n// Justified nav links\n// -------------------------\n\n.nav-justified {\n  width: 100%;\n\n  > li {\n    float: none;\n     > a {\n      text-align: center;\n      margin-bottom: 5px;\n    }\n  }\n\n  > .dropdown .dropdown-menu {\n    top: auto;\n    left: auto;\n  }\n\n  @media (min-width: @screen-sm-min) {\n    > li {\n      display: table-cell;\n      width: 1%;\n      > a {\n        margin-bottom: 0;\n      }\n    }\n  }\n}\n\n// Move borders to anchors instead of bottom of list\n//\n// Mixin for adding on top the shared `.nav-justified` styles for our tabs\n.nav-tabs-justified {\n  border-bottom: 0;\n\n  > li > a {\n    // Override margin from .nav-tabs\n    margin-right: 0;\n    border-radius: @border-radius-base;\n  }\n\n  > .active > a,\n  > .active > a:hover,\n  > .active > a:focus {\n    border: 1px solid @nav-tabs-justified-link-border-color;\n  }\n\n  @media (min-width: @screen-sm-min) {\n    > li > a {\n      border-bottom: 1px solid @nav-tabs-justified-link-border-color;\n      border-radius: @border-radius-base @border-radius-base 0 0;\n    }\n    > .active > a,\n    > .active > a:hover,\n    > .active > a:focus {\n      border-bottom-color: @nav-tabs-justified-active-link-border-color;\n    }\n  }\n}\n\n\n// Tabbable tabs\n// -------------------------\n\n// Hide tabbable panes to start, show them when `.active`\n.tab-content {\n  > .tab-pane {\n    display: none;\n  }\n  > .active {\n    display: block;\n  }\n}\n\n\n// Dropdowns\n// -------------------------\n\n// Specific dropdowns\n.nav-tabs .dropdown-menu {\n  // make dropdown border overlap tab border\n  margin-top: -1px;\n  // Remove the top rounded corners here since there is a hard edge above the menu\n  .border-top-radius(0);\n}\n","//\n// Navbars\n// --------------------------------------------------\n\n\n// Wrapper and base class\n//\n// Provide a static navbar from which we expand to create full-width, fixed, and\n// other navbar variations.\n\n.navbar {\n  position: relative;\n  min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode)\n  margin-bottom: @navbar-margin-bottom;\n  border: 1px solid transparent;\n\n  // Prevent floats from breaking the navbar\n  &:extend(.clearfix all);\n\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: @navbar-border-radius;\n  }\n}\n\n\n// Navbar heading\n//\n// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy\n// styling of responsive aspects.\n\n.navbar-header {\n  &:extend(.clearfix all);\n\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n  }\n}\n\n\n// Navbar collapse (body)\n//\n// Group your navbar content into this for easy collapsing and expanding across\n// various device sizes. By default, this content is collapsed when <768px, but\n// will expand past that for a horizontal display.\n//\n// To start (on mobile devices) the navbar links, forms, and buttons are stacked\n// vertically and include a `max-height` to overflow in case you have too much\n// content for the user's viewport.\n\n.navbar-collapse {\n  max-height: @navbar-collapse-max-height;\n  overflow-x: visible;\n  padding-right: @navbar-padding-horizontal;\n  padding-left:  @navbar-padding-horizontal;\n  border-top: 1px solid transparent;\n  box-shadow: inset 0 1px 0 rgba(255,255,255,.1);\n  &:extend(.clearfix all);\n  -webkit-overflow-scrolling: touch;\n\n  &.in {\n    overflow-y: auto;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    width: auto;\n    border-top: 0;\n    box-shadow: none;\n\n    &.collapse {\n      display: block !important;\n      height: auto !important;\n      padding-bottom: 0; // Override default setting\n      overflow: visible !important;\n    }\n\n    &.in {\n      overflow-y: visible;\n    }\n\n    // Undo the collapse side padding for navbars with containers to ensure\n    // alignment of right-aligned contents.\n    .navbar-fixed-top &,\n    .navbar-static-top &,\n    .navbar-fixed-bottom & {\n      padding-left: 0;\n      padding-right: 0;\n    }\n  }\n}\n\n\n// Both navbar header and collapse\n//\n// When a container is present, change the behavior of the header and collapse.\n\n.container,\n.container-fluid {\n  > .navbar-header,\n  > .navbar-collapse {\n    margin-right: -@navbar-padding-horizontal;\n    margin-left:  -@navbar-padding-horizontal;\n\n    @media (min-width: @grid-float-breakpoint) {\n      margin-right: 0;\n      margin-left:  0;\n    }\n  }\n}\n\n\n//\n// Navbar alignment options\n//\n// Display the navbar across the entirety of the page or fixed it to the top or\n// bottom of the page.\n\n// Static top (unfixed, but 100% wide) navbar\n.navbar-static-top {\n  z-index: @zindex-navbar;\n  border-width: 0 0 1px;\n\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: 0;\n  }\n}\n\n// Fix the top/bottom navbars when screen real estate supports it\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  position: fixed;\n  right: 0;\n  left: 0;\n  z-index: @zindex-navbar-fixed;\n\n  // Undo the rounded corners\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top {\n  top: 0;\n  border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n  bottom: 0;\n  margin-bottom: 0; // override .navbar defaults\n  border-width: 1px 0 0;\n}\n\n\n// Brand/project name\n\n.navbar-brand {\n  float: left;\n  padding: @navbar-padding-vertical @navbar-padding-horizontal;\n  font-size: @font-size-large;\n  line-height: @line-height-computed;\n  height: @navbar-height;\n\n  &:hover,\n  &:focus {\n    text-decoration: none;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    .navbar > .container &,\n    .navbar > .container-fluid & {\n      margin-left: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Navbar toggle\n//\n// Custom button for toggling the `.navbar-collapse`, powered by the collapse\n// JavaScript plugin.\n\n.navbar-toggle {\n  position: relative;\n  float: right;\n  margin-right: @navbar-padding-horizontal;\n  padding: 9px 10px;\n  .navbar-vertical-align(34px);\n  background-color: transparent;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid transparent;\n  border-radius: @border-radius-base;\n\n  // We remove the `outline` here, but later compensate by attaching `:hover`\n  // styles to `:focus`.\n  &:focus {\n    outline: none;\n  }\n\n  // Bars\n  .icon-bar {\n    display: block;\n    width: 22px;\n    height: 2px;\n    border-radius: 1px;\n  }\n  .icon-bar + .icon-bar {\n    margin-top: 4px;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    display: none;\n  }\n}\n\n\n// Navbar nav links\n//\n// Builds on top of the `.nav` components with its own modifier class to make\n// the nav the full height of the horizontal nav (above 768px).\n\n.navbar-nav {\n  margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal;\n\n  > li > a {\n    padding-top:    10px;\n    padding-bottom: 10px;\n    line-height: @line-height-computed;\n  }\n\n  @media (max-width: @grid-float-breakpoint-max) {\n    // Dropdowns get custom display when collapsed\n    .open .dropdown-menu {\n      position: static;\n      float: none;\n      width: auto;\n      margin-top: 0;\n      background-color: transparent;\n      border: 0;\n      box-shadow: none;\n      > li > a,\n      .dropdown-header {\n        padding: 5px 15px 5px 25px;\n      }\n      > li > a {\n        line-height: @line-height-computed;\n        &:hover,\n        &:focus {\n          background-image: none;\n        }\n      }\n    }\n  }\n\n  // Uncollapse the nav\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n    margin: 0;\n\n    > li {\n      float: left;\n      > a {\n        padding-top:    @navbar-padding-vertical;\n        padding-bottom: @navbar-padding-vertical;\n      }\n    }\n\n    &.navbar-right:last-child {\n      margin-right: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Component alignment\n//\n// Repurpose the pull utilities as their own navbar utilities to avoid specificity\n// issues with parents and chaining. Only do this when the navbar is uncollapsed\n// though so that navbar contents properly stack and align in mobile.\n\n@media (min-width: @grid-float-breakpoint) {\n  .navbar-left  { .pull-left(); }\n  .navbar-right { .pull-right(); }\n}\n\n\n// Navbar form\n//\n// Extension of the `.form-inline` with some extra flavor for optimum display in\n// our navbars.\n\n.navbar-form {\n  margin-left: -@navbar-padding-horizontal;\n  margin-right: -@navbar-padding-horizontal;\n  padding: 10px @navbar-padding-horizontal;\n  border-top: 1px solid transparent;\n  border-bottom: 1px solid transparent;\n  @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);\n  .box-shadow(@shadow);\n\n  // Mixin behavior for optimum display\n  .form-inline();\n\n  .form-group {\n    @media (max-width: @grid-float-breakpoint-max) {\n      margin-bottom: 5px;\n    }\n  }\n\n  // Vertically center in expanded, horizontal navbar\n  .navbar-vertical-align(@input-height-base);\n\n  // Undo 100% width for pull classes\n  @media (min-width: @grid-float-breakpoint) {\n    width: auto;\n    border: 0;\n    margin-left: 0;\n    margin-right: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n    .box-shadow(none);\n\n    // Outdent the form if last child to line up with content down the page\n    &.navbar-right:last-child {\n      margin-right: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Dropdown menus\n\n// Menu position and menu carets\n.navbar-nav > li > .dropdown-menu {\n  margin-top: 0;\n  .border-top-radius(0);\n}\n// Menu position and menu caret support for dropups via extra dropup class\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n  .border-bottom-radius(0);\n}\n\n\n// Buttons in navbars\n//\n// Vertically center a button within a navbar (when *not* in a form).\n\n.navbar-btn {\n  .navbar-vertical-align(@input-height-base);\n\n  &.btn-sm {\n    .navbar-vertical-align(@input-height-small);\n  }\n  &.btn-xs {\n    .navbar-vertical-align(22);\n  }\n}\n\n\n// Text in navbars\n//\n// Add a class to make any element properly align itself vertically within the navbars.\n\n.navbar-text {\n  .navbar-vertical-align(@line-height-computed);\n\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n    margin-left: @navbar-padding-horizontal;\n    margin-right: @navbar-padding-horizontal;\n\n    // Outdent the form if last child to line up with content down the page\n    &.navbar-right:last-child {\n      margin-right: 0;\n    }\n  }\n}\n\n// Alternate navbars\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n  background-color: @navbar-default-bg;\n  border-color: @navbar-default-border;\n\n  .navbar-brand {\n    color: @navbar-default-brand-color;\n    &:hover,\n    &:focus {\n      color: @navbar-default-brand-hover-color;\n      background-color: @navbar-default-brand-hover-bg;\n    }\n  }\n\n  .navbar-text {\n    color: @navbar-default-color;\n  }\n\n  .navbar-nav {\n    > li > a {\n      color: @navbar-default-link-color;\n\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-hover-color;\n        background-color: @navbar-default-link-hover-bg;\n      }\n    }\n    > .active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-active-color;\n        background-color: @navbar-default-link-active-bg;\n      }\n    }\n    > .disabled > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-disabled-color;\n        background-color: @navbar-default-link-disabled-bg;\n      }\n    }\n  }\n\n  .navbar-toggle {\n    border-color: @navbar-default-toggle-border-color;\n    &:hover,\n    &:focus {\n      background-color: @navbar-default-toggle-hover-bg;\n    }\n    .icon-bar {\n      background-color: @navbar-default-toggle-icon-bar-bg;\n    }\n  }\n\n  .navbar-collapse,\n  .navbar-form {\n    border-color: @navbar-default-border;\n  }\n\n  // Dropdown menu items\n  .navbar-nav {\n    // Remove background color from open dropdown\n    > .open > a {\n      &,\n      &:hover,\n      &:focus {\n        background-color: @navbar-default-link-active-bg;\n        color: @navbar-default-link-active-color;\n      }\n    }\n\n    @media (max-width: @grid-float-breakpoint-max) {\n      // Dropdowns get custom display when collapsed\n      .open .dropdown-menu {\n        > li > a {\n          color: @navbar-default-link-color;\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-hover-color;\n            background-color: @navbar-default-link-hover-bg;\n          }\n        }\n        > .active > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-active-color;\n            background-color: @navbar-default-link-active-bg;\n          }\n        }\n        > .disabled > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-disabled-color;\n            background-color: @navbar-default-link-disabled-bg;\n          }\n        }\n      }\n    }\n  }\n\n\n  // Links in navbars\n  //\n  // Add a class to ensure links outside the navbar nav are colored correctly.\n\n  .navbar-link {\n    color: @navbar-default-link-color;\n    &:hover {\n      color: @navbar-default-link-hover-color;\n    }\n  }\n\n}\n\n// Inverse navbar\n\n.navbar-inverse {\n  background-color: @navbar-inverse-bg;\n  border-color: @navbar-inverse-border;\n\n  .navbar-brand {\n    color: @navbar-inverse-brand-color;\n    &:hover,\n    &:focus {\n      color: @navbar-inverse-brand-hover-color;\n      background-color: @navbar-inverse-brand-hover-bg;\n    }\n  }\n\n  .navbar-text {\n    color: @navbar-inverse-color;\n  }\n\n  .navbar-nav {\n    > li > a {\n      color: @navbar-inverse-link-color;\n\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-hover-color;\n        background-color: @navbar-inverse-link-hover-bg;\n      }\n    }\n    > .active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-active-color;\n        background-color: @navbar-inverse-link-active-bg;\n      }\n    }\n    > .disabled > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-disabled-color;\n        background-color: @navbar-inverse-link-disabled-bg;\n      }\n    }\n  }\n\n  // Darken the responsive nav toggle\n  .navbar-toggle {\n    border-color: @navbar-inverse-toggle-border-color;\n    &:hover,\n    &:focus {\n      background-color: @navbar-inverse-toggle-hover-bg;\n    }\n    .icon-bar {\n      background-color: @navbar-inverse-toggle-icon-bar-bg;\n    }\n  }\n\n  .navbar-collapse,\n  .navbar-form {\n    border-color: darken(@navbar-inverse-bg, 7%);\n  }\n\n  // Dropdowns\n  .navbar-nav {\n    > .open > a {\n      &,\n      &:hover,\n      &:focus {\n        background-color: @navbar-inverse-link-active-bg;\n        color: @navbar-inverse-link-active-color;\n      }\n    }\n\n    @media (max-width: @grid-float-breakpoint-max) {\n      // Dropdowns get custom display\n      .open .dropdown-menu {\n        > .dropdown-header {\n          border-color: @navbar-inverse-border;\n        }\n        .divider {\n          background-color: @navbar-inverse-border;\n        }\n        > li > a {\n          color: @navbar-inverse-link-color;\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-hover-color;\n            background-color: @navbar-inverse-link-hover-bg;\n          }\n        }\n        > .active > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-active-color;\n            background-color: @navbar-inverse-link-active-bg;\n          }\n        }\n        > .disabled > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-disabled-color;\n            background-color: @navbar-inverse-link-disabled-bg;\n          }\n        }\n      }\n    }\n  }\n\n  .navbar-link {\n    color: @navbar-inverse-link-color;\n    &:hover {\n      color: @navbar-inverse-link-hover-color;\n    }\n  }\n\n}\n","//\n// Utility classes\n// --------------------------------------------------\n\n\n// Floats\n// -------------------------\n\n.clearfix {\n  .clearfix();\n}\n.center-block {\n  .center-block();\n}\n.pull-right {\n  float: right !important;\n}\n.pull-left {\n  float: left !important;\n}\n\n\n// Toggling content\n// -------------------------\n\n// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1\n.hide {\n  display: none !important;\n}\n.show {\n  display: block !important;\n}\n.invisible {\n  visibility: hidden;\n}\n.text-hide {\n  .text-hide();\n}\n\n\n// Hide from screenreaders and browsers\n//\n// Credit: HTML5 Boilerplate\n\n.hidden {\n  display: none !important;\n  visibility: hidden !important;\n}\n\n\n// For Affix plugin\n// -------------------------\n\n.affix {\n  position: fixed;\n}\n","//\n// Breadcrumbs\n// --------------------------------------------------\n\n\n.breadcrumb {\n  padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal;\n  margin-bottom: @line-height-computed;\n  list-style: none;\n  background-color: @breadcrumb-bg;\n  border-radius: @border-radius-base;\n\n  > li {\n    display: inline-block;\n\n    + li:before {\n      content: \"@{breadcrumb-separator}\\00a0\"; // Unicode space added since inline-block means non-collapsing white-space\n      padding: 0 5px;\n      color: @breadcrumb-color;\n    }\n  }\n\n  > .active {\n    color: @breadcrumb-active-color;\n  }\n}\n","//\n// Pagination (multiple pages)\n// --------------------------------------------------\n.pagination {\n  display: inline-block;\n  padding-left: 0;\n  margin: @line-height-computed 0;\n  border-radius: @border-radius-base;\n\n  > li {\n    display: inline; // Remove list-style and block-level defaults\n    > a,\n    > span {\n      position: relative;\n      float: left; // Collapse white-space\n      padding: @padding-base-vertical @padding-base-horizontal;\n      line-height: @line-height-base;\n      text-decoration: none;\n      color: @pagination-color;\n      background-color: @pagination-bg;\n      border: 1px solid @pagination-border;\n      margin-left: -1px;\n    }\n    &:first-child {\n      > a,\n      > span {\n        margin-left: 0;\n        .border-left-radius(@border-radius-base);\n      }\n    }\n    &:last-child {\n      > a,\n      > span {\n        .border-right-radius(@border-radius-base);\n      }\n    }\n  }\n\n  > li > a,\n  > li > span {\n    &:hover,\n    &:focus {\n      color: @pagination-hover-color;\n      background-color: @pagination-hover-bg;\n      border-color: @pagination-hover-border;\n    }\n  }\n\n  > .active > a,\n  > .active > span {\n    &,\n    &:hover,\n    &:focus {\n      z-index: 2;\n      color: @pagination-active-color;\n      background-color: @pagination-active-bg;\n      border-color: @pagination-active-border;\n      cursor: default;\n    }\n  }\n\n  > .disabled {\n    > span,\n    > span:hover,\n    > span:focus,\n    > a,\n    > a:hover,\n    > a:focus {\n      color: @pagination-disabled-color;\n      background-color: @pagination-disabled-bg;\n      border-color: @pagination-disabled-border;\n      cursor: not-allowed;\n    }\n  }\n}\n\n// Sizing\n// --------------------------------------------------\n\n// Large\n.pagination-lg {\n  .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @border-radius-large);\n}\n\n// Small\n.pagination-sm {\n  .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @border-radius-small);\n}\n","//\n// Pager pagination\n// --------------------------------------------------\n\n\n.pager {\n  padding-left: 0;\n  margin: @line-height-computed 0;\n  list-style: none;\n  text-align: center;\n  &:extend(.clearfix all);\n  li {\n    display: inline;\n    > a,\n    > span {\n      display: inline-block;\n      padding: 5px 14px;\n      background-color: @pager-bg;\n      border: 1px solid @pager-border;\n      border-radius: @pager-border-radius;\n    }\n\n    > a:hover,\n    > a:focus {\n      text-decoration: none;\n      background-color: @pager-hover-bg;\n    }\n  }\n\n  .next {\n    > a,\n    > span {\n      float: right;\n    }\n  }\n\n  .previous {\n    > a,\n    > span {\n      float: left;\n    }\n  }\n\n  .disabled {\n    > a,\n    > a:hover,\n    > a:focus,\n    > span {\n      color: @pager-disabled-color;\n      background-color: @pager-bg;\n      cursor: not-allowed;\n    }\n  }\n\n}\n","//\n// Labels\n// --------------------------------------------------\n\n.label {\n  display: inline;\n  padding: .2em .6em .3em;\n  font-size: 75%;\n  font-weight: bold;\n  line-height: 1;\n  color: @label-color;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: .25em;\n\n  // Add hover effects, but only for links\n  &[href] {\n    &:hover,\n    &:focus {\n      color: @label-link-hover-color;\n      text-decoration: none;\n      cursor: pointer;\n    }\n  }\n\n  // Empty labels collapse automatically (not available in IE8)\n  &:empty {\n    display: none;\n  }\n\n  // Quick fix for labels in buttons\n  .btn & {\n    position: relative;\n    top: -1px;\n  }\n}\n\n// Colors\n// Contextual variations (linked labels get darker on :hover)\n\n.label-default {\n  .label-variant(@label-default-bg);\n}\n\n.label-primary {\n  .label-variant(@label-primary-bg);\n}\n\n.label-success {\n  .label-variant(@label-success-bg);\n}\n\n.label-info {\n  .label-variant(@label-info-bg);\n}\n\n.label-warning {\n  .label-variant(@label-warning-bg);\n}\n\n.label-danger {\n  .label-variant(@label-danger-bg);\n}\n","//\n// Badges\n// --------------------------------------------------\n\n\n// Base classes\n.badge {\n  display: inline-block;\n  min-width: 10px;\n  padding: 3px 7px;\n  font-size: @font-size-small;\n  font-weight: @badge-font-weight;\n  color: @badge-color;\n  line-height: @badge-line-height;\n  vertical-align: baseline;\n  white-space: nowrap;\n  text-align: center;\n  background-color: @badge-bg;\n  border-radius: @badge-border-radius;\n\n  // Empty badges collapse automatically (not available in IE8)\n  &:empty {\n    display: none;\n  }\n\n  // Quick fix for badges in buttons\n  .btn & {\n    position: relative;\n    top: -1px;\n  }\n  .btn-xs & {\n    top: 0;\n    padding: 1px 5px;\n  }\n}\n\n// Hover state, but only for links\na.badge {\n  &:hover,\n  &:focus {\n    color: @badge-link-hover-color;\n    text-decoration: none;\n    cursor: pointer;\n  }\n}\n\n// Account for counters in navs\na.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n  color: @badge-active-color;\n  background-color: @badge-active-bg;\n}\n.nav-pills > li > a > .badge {\n  margin-left: 3px;\n}\n","//\n// Jumbotron\n// --------------------------------------------------\n\n\n.jumbotron {\n  padding: @jumbotron-padding;\n  margin-bottom: @jumbotron-padding;\n  color: @jumbotron-color;\n  background-color: @jumbotron-bg;\n\n  h1,\n  .h1 {\n    color: @jumbotron-heading-color;\n  }\n  p {\n    margin-bottom: (@jumbotron-padding / 2);\n    font-size: @jumbotron-font-size;\n    font-weight: 200;\n  }\n\n  .container & {\n    border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container\n  }\n\n  .container {\n    max-width: 100%;\n  }\n\n  @media screen and (min-width: @screen-sm-min) {\n    padding-top:    (@jumbotron-padding * 1.6);\n    padding-bottom: (@jumbotron-padding * 1.6);\n\n    .container & {\n      padding-left:  (@jumbotron-padding * 2);\n      padding-right: (@jumbotron-padding * 2);\n    }\n\n    h1,\n    .h1 {\n      font-size: (@font-size-base * 4.5);\n    }\n  }\n}\n","//\n// Alerts\n// --------------------------------------------------\n\n\n// Base styles\n// -------------------------\n\n.alert {\n  padding: @alert-padding;\n  margin-bottom: @line-height-computed;\n  border: 1px solid transparent;\n  border-radius: @alert-border-radius;\n\n  // Headings for larger alerts\n  h4 {\n    margin-top: 0;\n    // Specified for the h4 to prevent conflicts of changing @headings-color\n    color: inherit;\n  }\n  // Provide class for links that match alerts\n  .alert-link {\n    font-weight: @alert-link-font-weight;\n  }\n\n  // Improve alignment and spacing of inner content\n  > p,\n  > ul {\n    margin-bottom: 0;\n  }\n  > p + p {\n    margin-top: 5px;\n  }\n}\n\n// Dismissable alerts\n//\n// Expand the right padding and account for the close button's positioning.\n\n.alert-dismissable {\n padding-right: (@alert-padding + 20);\n\n  // Adjust close link position\n  .close {\n    position: relative;\n    top: -2px;\n    right: -21px;\n    color: inherit;\n  }\n}\n\n// Alternate styles\n//\n// Generate contextual modifier classes for colorizing the alert.\n\n.alert-success {\n  .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text);\n}\n.alert-info {\n  .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text);\n}\n.alert-warning {\n  .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text);\n}\n.alert-danger {\n  .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);\n}\n","//\n// Progress bars\n// --------------------------------------------------\n\n\n// Bar animations\n// -------------------------\n\n// WebKit\n@-webkit-keyframes progress-bar-stripes {\n  from  { background-position: 40px 0; }\n  to    { background-position: 0 0; }\n}\n\n// Spec and IE10+\n@keyframes progress-bar-stripes {\n  from  { background-position: 40px 0; }\n  to    { background-position: 0 0; }\n}\n\n\n\n// Bar itself\n// -------------------------\n\n// Outer container\n.progress {\n  overflow: hidden;\n  height: @line-height-computed;\n  margin-bottom: @line-height-computed;\n  background-color: @progress-bg;\n  border-radius: @border-radius-base;\n  .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));\n}\n\n// Bar of progress\n.progress-bar {\n  float: left;\n  width: 0%;\n  height: 100%;\n  font-size: @font-size-small;\n  line-height: @line-height-computed;\n  color: @progress-bar-color;\n  text-align: center;\n  background-color: @progress-bar-bg;\n  .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));\n  .transition(width .6s ease);\n}\n\n// Striped bars\n.progress-striped .progress-bar {\n  #gradient > .striped();\n  background-size: 40px 40px;\n}\n\n// Call animation for the active one\n.progress.active .progress-bar {\n  .animation(progress-bar-stripes 2s linear infinite);\n}\n\n\n\n// Variations\n// -------------------------\n\n.progress-bar-success {\n  .progress-bar-variant(@progress-bar-success-bg);\n}\n\n.progress-bar-info {\n  .progress-bar-variant(@progress-bar-info-bg);\n}\n\n.progress-bar-warning {\n  .progress-bar-variant(@progress-bar-warning-bg);\n}\n\n.progress-bar-danger {\n  .progress-bar-variant(@progress-bar-danger-bg);\n}\n","// Media objects\n// Source: http://stubbornella.org/content/?p=497\n// --------------------------------------------------\n\n\n// Common styles\n// -------------------------\n\n// Clear the floats\n.media,\n.media-body {\n  overflow: hidden;\n  zoom: 1;\n}\n\n// Proper spacing between instances of .media\n.media,\n.media .media {\n  margin-top: 15px;\n}\n.media:first-child {\n  margin-top: 0;\n}\n\n// For images and videos, set to block\n.media-object {\n  display: block;\n}\n\n// Reset margins on headings for tighter default spacing\n.media-heading {\n  margin: 0 0 5px;\n}\n\n\n// Media image alignment\n// -------------------------\n\n.media {\n  > .pull-left {\n    margin-right: 10px;\n  }\n  > .pull-right {\n    margin-left: 10px;\n  }\n}\n\n\n// Media list variation\n// -------------------------\n\n// Undo default ul/ol styles\n.media-list {\n  padding-left: 0;\n  list-style: none;\n}\n","//\n// List groups\n// --------------------------------------------------\n\n\n// Base class\n//\n// Easily usable on <ul>, <ol>, or <div>.\n\n.list-group {\n  // No need to set list-style: none; since .list-group-item is block level\n  margin-bottom: 20px;\n  padding-left: 0; // reset padding because ul and ol\n}\n\n\n// Individual list items\n//\n// Use on `li`s or `div`s within the `.list-group` parent.\n\n.list-group-item {\n  position: relative;\n  display: block;\n  padding: 10px 15px;\n  // Place the border on the list items and negative margin up for better styling\n  margin-bottom: -1px;\n  background-color: @list-group-bg;\n  border: 1px solid @list-group-border;\n\n  // Round the first and last items\n  &:first-child {\n    .border-top-radius(@list-group-border-radius);\n  }\n  &:last-child {\n    margin-bottom: 0;\n    .border-bottom-radius(@list-group-border-radius);\n  }\n\n  // Align badges within list items\n  > .badge {\n    float: right;\n  }\n  > .badge + .badge {\n    margin-right: 5px;\n  }\n}\n\n\n// Linked list items\n//\n// Use anchor elements instead of `li`s or `div`s to create linked list items.\n// Includes an extra `.active` modifier class for showing selected items.\n\na.list-group-item {\n  color: @list-group-link-color;\n\n  .list-group-item-heading {\n    color: @list-group-link-heading-color;\n  }\n\n  // Hover state\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    background-color: @list-group-hover-bg;\n  }\n\n  // Active class on item itself, not parent\n  &.active,\n  &.active:hover,\n  &.active:focus {\n    z-index: 2; // Place active items above their siblings for proper border styling\n    color: @list-group-active-color;\n    background-color: @list-group-active-bg;\n    border-color: @list-group-active-border;\n\n    // Force color to inherit for custom content\n    .list-group-item-heading {\n      color: inherit;\n    }\n    .list-group-item-text {\n      color: @list-group-active-text-color;\n    }\n  }\n}\n\n\n// Contextual variants\n//\n// Add modifier classes to change text and background color on individual items.\n// Organizationally, this must come after the `:hover` states.\n\n.list-group-item-variant(success; @state-success-bg; @state-success-text);\n.list-group-item-variant(info; @state-info-bg; @state-info-text);\n.list-group-item-variant(warning; @state-warning-bg; @state-warning-text);\n.list-group-item-variant(danger; @state-danger-bg; @state-danger-text);\n\n\n// Custom content options\n//\n// Extra classes for creating well-formatted content within `.list-group-item`s.\n\n.list-group-item-heading {\n  margin-top: 0;\n  margin-bottom: 5px;\n}\n.list-group-item-text {\n  margin-bottom: 0;\n  line-height: 1.3;\n}\n","//\n// Panels\n// --------------------------------------------------\n\n\n// Base class\n.panel {\n  margin-bottom: @line-height-computed;\n  background-color: @panel-bg;\n  border: 1px solid transparent;\n  border-radius: @panel-border-radius;\n  .box-shadow(0 1px 1px rgba(0,0,0,.05));\n}\n\n// Panel contents\n.panel-body {\n  padding: @panel-body-padding;\n  &:extend(.clearfix all);\n}\n\n// Optional heading\n.panel-heading {\n  padding: 10px 15px;\n  border-bottom: 1px solid transparent;\n  .border-top-radius((@panel-border-radius - 1));\n\n  > .dropdown .dropdown-toggle {\n    color: inherit;\n  }\n}\n\n// Within heading, strip any `h*` tag of its default margins for spacing.\n.panel-title {\n  margin-top: 0;\n  margin-bottom: 0;\n  font-size: ceil((@font-size-base * 1.125));\n  color: inherit;\n\n  > a {\n    color: inherit;\n  }\n}\n\n// Optional footer (stays gray in every modifier class)\n.panel-footer {\n  padding: 10px 15px;\n  background-color: @panel-footer-bg;\n  border-top: 1px solid @panel-inner-border;\n  .border-bottom-radius((@panel-border-radius - 1));\n}\n\n\n// List groups in panels\n//\n// By default, space out list group content from panel headings to account for\n// any kind of custom content between the two.\n\n.panel {\n  > .list-group {\n    margin-bottom: 0;\n\n    .list-group-item {\n      border-width: 1px 0;\n      border-radius: 0;\n    }\n\n    // Add border top radius for first one\n    &:first-child {\n      .list-group-item:first-child {\n        border-top: 0;\n        .border-top-radius((@panel-border-radius - 1));\n      }\n    }\n    // Add border bottom radius for last one\n    &:last-child {\n      .list-group-item:last-child {\n        border-bottom: 0;\n        .border-bottom-radius((@panel-border-radius - 1));\n      }\n    }\n  }\n}\n// Collapse space between when there's no additional content.\n.panel-heading + .list-group {\n  .list-group-item:first-child {\n    border-top-width: 0;\n  }\n}\n\n\n// Tables in panels\n//\n// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and\n// watch it go full width.\n\n.panel {\n  > .table,\n  > .table-responsive > .table {\n    margin-bottom: 0;\n  }\n  // Add border top radius for first one\n  > .table:first-child,\n  > .table-responsive:first-child > .table:first-child {\n    .border-top-radius((@panel-border-radius - 1));\n\n    > thead:first-child,\n    > tbody:first-child {\n      > tr:first-child {\n        td:first-child,\n        th:first-child {\n          border-top-left-radius: (@panel-border-radius - 1);\n        }\n        td:last-child,\n        th:last-child {\n          border-top-right-radius: (@panel-border-radius - 1);\n        }\n      }\n    }\n  }\n  // Add border bottom radius for last one\n  > .table:last-child,\n  > .table-responsive:last-child > .table:last-child {\n    .border-bottom-radius((@panel-border-radius - 1));\n\n    > tbody:last-child,\n    > tfoot:last-child {\n      > tr:last-child {\n        td:first-child,\n        th:first-child {\n          border-bottom-left-radius: (@panel-border-radius - 1);\n        }\n        td:last-child,\n        th:last-child {\n          border-bottom-right-radius: (@panel-border-radius - 1);\n        }\n      }\n    }\n  }\n  > .panel-body + .table,\n  > .panel-body + .table-responsive {\n    border-top: 1px solid @table-border-color;\n  }\n  > .table > tbody:first-child > tr:first-child th,\n  > .table > tbody:first-child > tr:first-child td {\n    border-top: 0;\n  }\n  > .table-bordered,\n  > .table-responsive > .table-bordered {\n    border: 0;\n    > thead,\n    > tbody,\n    > tfoot {\n      > tr {\n        > th:first-child,\n        > td:first-child {\n          border-left: 0;\n        }\n        > th:last-child,\n        > td:last-child {\n          border-right: 0;\n        }\n      }\n    }\n    > thead,\n    > tbody {\n      > tr:first-child {\n        > td,\n        > th {\n          border-bottom: 0;\n        }\n      }\n    }\n    > tbody,\n    > tfoot {\n      > tr:last-child {\n        > td,\n        > th {\n          border-bottom: 0;\n        }\n      }\n    }\n  }\n  > .table-responsive {\n    border: 0;\n    margin-bottom: 0;\n  }\n}\n\n\n// Collapsable panels (aka, accordion)\n//\n// Wrap a series of panels in `.panel-group` to turn them into an accordion with\n// the help of our collapse JavaScript plugin.\n\n.panel-group {\n  margin-bottom: @line-height-computed;\n\n  // Tighten up margin so it's only between panels\n  .panel {\n    margin-bottom: 0;\n    border-radius: @panel-border-radius;\n    overflow: hidden; // crop contents when collapsed\n    + .panel {\n      margin-top: 5px;\n    }\n  }\n\n  .panel-heading {\n    border-bottom: 0;\n    + .panel-collapse .panel-body {\n      border-top: 1px solid @panel-inner-border;\n    }\n  }\n  .panel-footer {\n    border-top: 0;\n    + .panel-collapse .panel-body {\n      border-bottom: 1px solid @panel-inner-border;\n    }\n  }\n}\n\n\n// Contextual variations\n.panel-default {\n  .panel-variant(@panel-default-border; @panel-default-text; @panel-default-heading-bg; @panel-default-border);\n}\n.panel-primary {\n  .panel-variant(@panel-primary-border; @panel-primary-text; @panel-primary-heading-bg; @panel-primary-border);\n}\n.panel-success {\n  .panel-variant(@panel-success-border; @panel-success-text; @panel-success-heading-bg; @panel-success-border);\n}\n.panel-info {\n  .panel-variant(@panel-info-border; @panel-info-text; @panel-info-heading-bg; @panel-info-border);\n}\n.panel-warning {\n  .panel-variant(@panel-warning-border; @panel-warning-text; @panel-warning-heading-bg; @panel-warning-border);\n}\n.panel-danger {\n  .panel-variant(@panel-danger-border; @panel-danger-text; @panel-danger-heading-bg; @panel-danger-border);\n}\n","//\n// Wells\n// --------------------------------------------------\n\n\n// Base class\n.well {\n  min-height: 20px;\n  padding: 19px;\n  margin-bottom: 20px;\n  background-color: @well-bg;\n  border: 1px solid @well-border;\n  border-radius: @border-radius-base;\n  .box-shadow(inset 0 1px 1px rgba(0,0,0,.05));\n  blockquote {\n    border-color: #ddd;\n    border-color: rgba(0,0,0,.15);\n  }\n}\n\n// Sizes\n.well-lg {\n  padding: 24px;\n  border-radius: @border-radius-large;\n}\n.well-sm {\n  padding: 9px;\n  border-radius: @border-radius-small;\n}\n","//\n// Close icons\n// --------------------------------------------------\n\n\n.close {\n  float: right;\n  font-size: (@font-size-base * 1.5);\n  font-weight: @close-font-weight;\n  line-height: 1;\n  color: @close-color;\n  text-shadow: @close-text-shadow;\n  .opacity(.2);\n\n  &:hover,\n  &:focus {\n    color: @close-color;\n    text-decoration: none;\n    cursor: pointer;\n    .opacity(.5);\n  }\n\n  // Additional properties for button version\n  // iOS requires the button element instead of an anchor tag.\n  // If you want the anchor version, it requires `href=\"#\"`.\n  button& {\n    padding: 0;\n    cursor: pointer;\n    background: transparent;\n    border: 0;\n    -webkit-appearance: none;\n  }\n}\n","//\n// Modals\n// --------------------------------------------------\n\n// .modal-open      - body class for killing the scroll\n// .modal           - container to scroll within\n// .modal-dialog    - positioning shell for the actual modal\n// .modal-content   - actual modal w/ bg and corners and shit\n\n// Kill the scroll on the body\n.modal-open {\n  overflow: hidden;\n}\n\n// Container that the modal scrolls within\n.modal {\n  display: none;\n  overflow: auto;\n  overflow-y: scroll;\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: @zindex-modal;\n  -webkit-overflow-scrolling: touch;\n\n  // Prevent Chrome on Windows from adding a focus outline. For details, see\n  // https://github.com/twbs/bootstrap/pull/10951.\n  outline: 0;\n\n  // When fading in the modal, animate it to slide down\n  &.fade .modal-dialog {\n    .translate(0, -25%);\n    .transition-transform(~\"0.3s ease-out\");\n  }\n  &.in .modal-dialog { .translate(0, 0)}\n}\n\n// Shell div to position the modal with bottom padding\n.modal-dialog {\n  position: relative;\n  width: auto;\n  margin: 10px;\n}\n\n// Actual modal\n.modal-content {\n  position: relative;\n  background-color: @modal-content-bg;\n  border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc)\n  border: 1px solid @modal-content-border-color;\n  border-radius: @border-radius-large;\n  .box-shadow(0 3px 9px rgba(0,0,0,.5));\n  background-clip: padding-box;\n  // Remove focus outline from opened modal\n  outline: none;\n}\n\n// Modal background\n.modal-backdrop {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: @zindex-modal-background;\n  background-color: @modal-backdrop-bg;\n  // Fade for backdrop\n  &.fade { .opacity(0); }\n  &.in { .opacity(@modal-backdrop-opacity); }\n}\n\n// Modal header\n// Top section of the modal w/ title and dismiss\n.modal-header {\n  padding: @modal-title-padding;\n  border-bottom: 1px solid @modal-header-border-color;\n  min-height: (@modal-title-padding + @modal-title-line-height);\n}\n// Close icon\n.modal-header .close {\n  margin-top: -2px;\n}\n\n// Title text within header\n.modal-title {\n  margin: 0;\n  line-height: @modal-title-line-height;\n}\n\n// Modal body\n// Where all modal content resides (sibling of .modal-header and .modal-footer)\n.modal-body {\n  position: relative;\n  padding: @modal-inner-padding;\n}\n\n// Footer (for actions)\n.modal-footer {\n  margin-top: 15px;\n  padding: (@modal-inner-padding - 1) @modal-inner-padding @modal-inner-padding;\n  text-align: right; // right align buttons\n  border-top: 1px solid @modal-footer-border-color;\n  &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons\n\n  // Properly space out buttons\n  .btn + .btn {\n    margin-left: 5px;\n    margin-bottom: 0; // account for input[type=\"submit\"] which gets the bottom margin like all other inputs\n  }\n  // but override that for button groups\n  .btn-group .btn + .btn {\n    margin-left: -1px;\n  }\n  // and override it for block buttons as well\n  .btn-block + .btn-block {\n    margin-left: 0;\n  }\n}\n\n// Scale up the modal\n@media (min-width: @screen-sm-min) {\n  // Automatically set modal's width for larger viewports\n  .modal-dialog {\n    width: @modal-md;\n    margin: 30px auto;\n  }\n  .modal-content {\n    .box-shadow(0 5px 15px rgba(0,0,0,.5));\n  }\n\n  // Modal sizes\n  .modal-sm { width: @modal-sm; }\n}\n\n@media (min-width: @screen-md-min) {\n  .modal-lg { width: @modal-lg; }\n}\n","//\n// Tooltips\n// --------------------------------------------------\n\n\n// Base class\n.tooltip {\n  position: absolute;\n  z-index: @zindex-tooltip;\n  display: block;\n  visibility: visible;\n  font-size: @font-size-small;\n  line-height: 1.4;\n  .opacity(0);\n\n  &.in     { .opacity(@tooltip-opacity); }\n  &.top    { margin-top:  -3px; padding: @tooltip-arrow-width 0; }\n  &.right  { margin-left:  3px; padding: 0 @tooltip-arrow-width; }\n  &.bottom { margin-top:   3px; padding: @tooltip-arrow-width 0; }\n  &.left   { margin-left: -3px; padding: 0 @tooltip-arrow-width; }\n}\n\n// Wrapper for the tooltip content\n.tooltip-inner {\n  max-width: @tooltip-max-width;\n  padding: 3px 8px;\n  color: @tooltip-color;\n  text-align: center;\n  text-decoration: none;\n  background-color: @tooltip-bg;\n  border-radius: @border-radius-base;\n}\n\n// Arrows\n.tooltip-arrow {\n  position: absolute;\n  width: 0;\n  height: 0;\n  border-color: transparent;\n  border-style: solid;\n}\n.tooltip {\n  &.top .tooltip-arrow {\n    bottom: 0;\n    left: 50%;\n    margin-left: -@tooltip-arrow-width;\n    border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n    border-top-color: @tooltip-arrow-color;\n  }\n  &.top-left .tooltip-arrow {\n    bottom: 0;\n    left: @tooltip-arrow-width;\n    border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n    border-top-color: @tooltip-arrow-color;\n  }\n  &.top-right .tooltip-arrow {\n    bottom: 0;\n    right: @tooltip-arrow-width;\n    border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n    border-top-color: @tooltip-arrow-color;\n  }\n  &.right .tooltip-arrow {\n    top: 50%;\n    left: 0;\n    margin-top: -@tooltip-arrow-width;\n    border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0;\n    border-right-color: @tooltip-arrow-color;\n  }\n  &.left .tooltip-arrow {\n    top: 50%;\n    right: 0;\n    margin-top: -@tooltip-arrow-width;\n    border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width;\n    border-left-color: @tooltip-arrow-color;\n  }\n  &.bottom .tooltip-arrow {\n    top: 0;\n    left: 50%;\n    margin-left: -@tooltip-arrow-width;\n    border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n    border-bottom-color: @tooltip-arrow-color;\n  }\n  &.bottom-left .tooltip-arrow {\n    top: 0;\n    left: @tooltip-arrow-width;\n    border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n    border-bottom-color: @tooltip-arrow-color;\n  }\n  &.bottom-right .tooltip-arrow {\n    top: 0;\n    right: @tooltip-arrow-width;\n    border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n    border-bottom-color: @tooltip-arrow-color;\n  }\n}\n","//\n// Popovers\n// --------------------------------------------------\n\n\n.popover {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: @zindex-popover;\n  display: none;\n  max-width: @popover-max-width;\n  padding: 1px;\n  text-align: left; // Reset given new insertion method\n  background-color: @popover-bg;\n  background-clip: padding-box;\n  border: 1px solid @popover-fallback-border-color;\n  border: 1px solid @popover-border-color;\n  border-radius: @border-radius-large;\n  .box-shadow(0 5px 10px rgba(0,0,0,.2));\n\n  // Overrides for proper insertion\n  white-space: normal;\n\n  // Offset the popover to account for the popover arrow\n  &.top     { margin-top: -@popover-arrow-width; }\n  &.right   { margin-left: @popover-arrow-width; }\n  &.bottom  { margin-top: @popover-arrow-width; }\n  &.left    { margin-left: -@popover-arrow-width; }\n}\n\n.popover-title {\n  margin: 0; // reset heading margin\n  padding: 8px 14px;\n  font-size: @font-size-base;\n  font-weight: normal;\n  line-height: 18px;\n  background-color: @popover-title-bg;\n  border-bottom: 1px solid darken(@popover-title-bg, 5%);\n  border-radius: 5px 5px 0 0;\n}\n\n.popover-content {\n  padding: 9px 14px;\n}\n\n// Arrows\n//\n// .arrow is outer, .arrow:after is inner\n\n.popover > .arrow {\n  &,\n  &:after {\n    position: absolute;\n    display: block;\n    width: 0;\n    height: 0;\n    border-color: transparent;\n    border-style: solid;\n  }\n}\n.popover > .arrow {\n  border-width: @popover-arrow-outer-width;\n}\n.popover > .arrow:after {\n  border-width: @popover-arrow-width;\n  content: \"\";\n}\n\n.popover {\n  &.top > .arrow {\n    left: 50%;\n    margin-left: -@popover-arrow-outer-width;\n    border-bottom-width: 0;\n    border-top-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n    border-top-color: @popover-arrow-outer-color;\n    bottom: -@popover-arrow-outer-width;\n    &:after {\n      content: \" \";\n      bottom: 1px;\n      margin-left: -@popover-arrow-width;\n      border-bottom-width: 0;\n      border-top-color: @popover-arrow-color;\n    }\n  }\n  &.right > .arrow {\n    top: 50%;\n    left: -@popover-arrow-outer-width;\n    margin-top: -@popover-arrow-outer-width;\n    border-left-width: 0;\n    border-right-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n    border-right-color: @popover-arrow-outer-color;\n    &:after {\n      content: \" \";\n      left: 1px;\n      bottom: -@popover-arrow-width;\n      border-left-width: 0;\n      border-right-color: @popover-arrow-color;\n    }\n  }\n  &.bottom > .arrow {\n    left: 50%;\n    margin-left: -@popover-arrow-outer-width;\n    border-top-width: 0;\n    border-bottom-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n    border-bottom-color: @popover-arrow-outer-color;\n    top: -@popover-arrow-outer-width;\n    &:after {\n      content: \" \";\n      top: 1px;\n      margin-left: -@popover-arrow-width;\n      border-top-width: 0;\n      border-bottom-color: @popover-arrow-color;\n    }\n  }\n\n  &.left > .arrow {\n    top: 50%;\n    right: -@popover-arrow-outer-width;\n    margin-top: -@popover-arrow-outer-width;\n    border-right-width: 0;\n    border-left-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n    border-left-color: @popover-arrow-outer-color;\n    &:after {\n      content: \" \";\n      right: 1px;\n      border-right-width: 0;\n      border-left-color: @popover-arrow-color;\n      bottom: -@popover-arrow-width;\n    }\n  }\n\n}\n","//\n// Responsive: Utility classes\n// --------------------------------------------------\n\n\n// IE10 in Windows (Phone) 8\n//\n// Support for responsive views via media queries is kind of borked in IE10, for\n// Surface/desktop in split view and for Windows Phone 8. This particular fix\n// must be accompanied by a snippet of JavaScript to sniff the user agent and\n// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at\n// our Getting Started page for more information on this bug.\n//\n// For more information, see the following:\n//\n// Issue: https://github.com/twbs/bootstrap/issues/10497\n// Docs: http://getbootstrap.com/getting-started/#browsers\n// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/\n\n@-ms-viewport {\n  width: device-width;\n}\n\n\n// Visibility utilities\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n  .responsive-invisibility();\n}\n\n.visible-xs {\n  @media (max-width: @screen-xs-max) {\n    .responsive-visibility();\n  }\n}\n.visible-sm {\n  @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n    .responsive-visibility();\n  }\n}\n.visible-md {\n  @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n    .responsive-visibility();\n  }\n}\n.visible-lg {\n  @media (min-width: @screen-lg-min) {\n    .responsive-visibility();\n  }\n}\n\n.hidden-xs {\n  @media (max-width: @screen-xs-max) {\n    .responsive-invisibility();\n  }\n}\n.hidden-sm {\n  @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n    .responsive-invisibility();\n  }\n}\n.hidden-md {\n  @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n    .responsive-invisibility();\n  }\n}\n.hidden-lg {\n  @media (min-width: @screen-lg-min) {\n    .responsive-invisibility();\n  }\n}\n\n\n// Print utilities\n//\n// Media queries are placed on the inside to be mixin-friendly.\n\n.visible-print {\n  .responsive-invisibility();\n\n  @media print {\n    .responsive-visibility();\n  }\n}\n\n.hidden-print {\n  @media print {\n    .responsive-invisibility();\n  }\n}\n"]}
\ No newline at end of file
diff --git a/static/libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..679272d25859d55e9931101ef56656e2c50e5ea5
--- /dev/null
+++ b/static/libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v3.1.1 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none!important;color:#000!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#999}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:7px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:gray}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5) 0),color-stop(rgba(0,0,0,.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001) 0),color-stop(rgba(0,0,0,.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}@media print{.hidden-print{display:none!important}}
\ No newline at end of file
diff --git a/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.eot b/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..5ef4e3864bd38582e315f1cb690ad57c329641c6
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.eot differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.svg b/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a505b66afafd2cd9b7cbb180b58719644a59996c
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.svg differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.ttf b/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..c65766115a63c70cddbc70de6b8cea165c18b4f1
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.ttf differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.woff b/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..06f5b73713f80add4aaa17fc927f82acd5feadcb
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/fonts/._glyphicons-halflings-regular.woff differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.eot b/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..4a4ca865d67e86f961bc6e2ef00bffa4e34bb9ed
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.eot differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.svg b/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e3e2dc739dd851f2d7d291be032e30b909e3e95f
--- /dev/null
+++ b/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.svg
@@ -0,0 +1,229 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
+<font-face units-per-em="1200" ascent="960" descent="-240" />
+<missing-glyph horiz-adv-x="500" />
+<glyph />
+<glyph />
+<glyph unicode="&#xd;" />
+<glyph unicode=" " />
+<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
+<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
+<glyph unicode="&#xa0;" />
+<glyph unicode="&#x2000;" horiz-adv-x="652" />
+<glyph unicode="&#x2001;" horiz-adv-x="1304" />
+<glyph unicode="&#x2002;" horiz-adv-x="652" />
+<glyph unicode="&#x2003;" horiz-adv-x="1304" />
+<glyph unicode="&#x2004;" horiz-adv-x="434" />
+<glyph unicode="&#x2005;" horiz-adv-x="326" />
+<glyph unicode="&#x2006;" horiz-adv-x="217" />
+<glyph unicode="&#x2007;" horiz-adv-x="217" />
+<glyph unicode="&#x2008;" horiz-adv-x="163" />
+<glyph unicode="&#x2009;" horiz-adv-x="260" />
+<glyph unicode="&#x200a;" horiz-adv-x="72" />
+<glyph unicode="&#x202f;" horiz-adv-x="260" />
+<glyph unicode="&#x205f;" horiz-adv-x="326" />
+<glyph unicode="&#x20ac;" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
+<glyph unicode="&#x2212;" d="M200 400h900v300h-900v-300z" />
+<glyph unicode="&#x25fc;" horiz-adv-x="500" d="M0 0z" />
+<glyph unicode="&#x2601;" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" />
+<glyph unicode="&#x2709;" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
+<glyph unicode="&#x270f;" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
+<glyph unicode="&#xe001;" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
+<glyph unicode="&#xe002;" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
+<glyph unicode="&#xe003;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
+<glyph unicode="&#xe005;" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
+<glyph unicode="&#xe006;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
+<glyph unicode="&#xe007;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
+<glyph unicode="&#xe008;" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
+<glyph unicode="&#xe009;" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
+<glyph unicode="&#xe010;" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe011;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe012;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe013;" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
+<glyph unicode="&#xe014;" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
+<glyph unicode="&#xe015;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
+<glyph unicode="&#xe016;" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
+<glyph unicode="&#xe017;" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
+<glyph unicode="&#xe018;" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
+<glyph unicode="&#xe019;" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
+<glyph unicode="&#xe020;" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
+<glyph unicode="&#xe021;" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
+<glyph unicode="&#xe022;" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
+<glyph unicode="&#xe023;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" />
+<glyph unicode="&#xe024;" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
+<glyph unicode="&#xe025;" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
+<glyph unicode="&#xe026;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" />
+<glyph unicode="&#xe027;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" />
+<glyph unicode="&#xe028;" d="M0 25v475l200 700h800l199 -700l1 -475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
+<glyph unicode="&#xe029;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" />
+<glyph unicode="&#xe030;" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
+<glyph unicode="&#xe031;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
+<glyph unicode="&#xe032;" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
+<glyph unicode="&#xe033;" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
+<glyph unicode="&#xe034;" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
+<glyph unicode="&#xe035;" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
+<glyph unicode="&#xe036;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
+<glyph unicode="&#xe037;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
+<glyph unicode="&#xe038;" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
+<glyph unicode="&#xe039;" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
+<glyph unicode="&#xe040;" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
+<glyph unicode="&#xe041;" d="M0 700l1 475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
+<glyph unicode="&#xe042;" d="M1 700l1 475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
+<glyph unicode="&#xe043;" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
+<glyph unicode="&#xe044;" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
+<glyph unicode="&#xe045;" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
+<glyph unicode="&#xe046;" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
+<glyph unicode="&#xe047;" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" />
+<glyph unicode="&#xe048;" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v71l471 -1q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
+<glyph unicode="&#xe049;" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
+<glyph unicode="&#xe050;" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
+<glyph unicode="&#xe051;" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
+<glyph unicode="&#xe052;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe053;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe054;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe055;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe056;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe057;" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe058;" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
+<glyph unicode="&#xe059;" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
+<glyph unicode="&#xe060;" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " />
+<glyph unicode="&#xe062;" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" />
+<glyph unicode="&#xe063;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" />
+<glyph unicode="&#xe064;" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 139t-64 210zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" />
+<glyph unicode="&#xe065;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" />
+<glyph unicode="&#xe066;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" />
+<glyph unicode="&#xe067;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q61 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l567 567l-137 137l-430 -431l-146 147z" />
+<glyph unicode="&#xe068;" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
+<glyph unicode="&#xe069;" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe070;" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe071;" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
+<glyph unicode="&#xe072;" d="M200 0l900 550l-900 550v-1100z" />
+<glyph unicode="&#xe073;" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
+<glyph unicode="&#xe074;" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
+<glyph unicode="&#xe075;" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
+<glyph unicode="&#xe076;" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" />
+<glyph unicode="&#xe077;" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" />
+<glyph unicode="&#xe078;" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" />
+<glyph unicode="&#xe079;" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" />
+<glyph unicode="&#xe080;" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" />
+<glyph unicode="&#xe081;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" />
+<glyph unicode="&#xe082;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h600v200h-600v-200z" />
+<glyph unicode="&#xe083;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141 z" />
+<glyph unicode="&#xe084;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" />
+<glyph unicode="&#xe085;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM364 700h143q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5 q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-50 0 -90.5 -12t-75 -38.5t-53.5 -74.5t-19 -114zM500 300h200v100h-200 v-100z" />
+<glyph unicode="&#xe086;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" />
+<glyph unicode="&#xe087;" d="M0 500v200h195q31 125 98.5 199.5t206.5 100.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200v-206 q149 48 201 206h-201v200h200q-25 74 -75.5 127t-124.5 77v-204h-200v203q-75 -23 -130 -77t-79 -126h209v-200h-210z" />
+<glyph unicode="&#xe088;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" />
+<glyph unicode="&#xe089;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" />
+<glyph unicode="&#xe090;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" />
+<glyph unicode="&#xe091;" d="M0 547l600 453v-300h600v-300h-600v-301z" />
+<glyph unicode="&#xe092;" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
+<glyph unicode="&#xe093;" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
+<glyph unicode="&#xe094;" d="M104 600h296v600h300v-600h298l-449 -600z" />
+<glyph unicode="&#xe095;" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" />
+<glyph unicode="&#xe096;" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
+<glyph unicode="&#xe097;" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" />
+<glyph unicode="&#xe101;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5h-207q-21 0 -33 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" />
+<glyph unicode="&#xe102;" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111q1 1 1 6.5t-1.5 15t-3.5 17.5l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6 h-111v-100zM100 0h400v400h-400v-400zM200 900q-3 0 14 48t36 96l18 47l213 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" />
+<glyph unicode="&#xe103;" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" />
+<glyph unicode="&#xe104;" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" />
+<glyph unicode="&#xe105;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" />
+<glyph unicode="&#xe106;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" />
+<glyph unicode="&#xe107;" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 34 -48 36.5t-48 -29.5l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" />
+<glyph unicode="&#xe108;" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -20 -13 -28.5t-32 0.5l-94 78h-222l-94 -78q-19 -9 -32 -0.5t-13 28.5 v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" />
+<glyph unicode="&#xe109;" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" />
+<glyph unicode="&#xe110;" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" />
+<glyph unicode="&#xe111;" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" />
+<glyph unicode="&#xe112;" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" />
+<glyph unicode="&#xe113;" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" />
+<glyph unicode="&#xe114;" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" />
+<glyph unicode="&#xe115;" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" />
+<glyph unicode="&#xe116;" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" />
+<glyph unicode="&#xe117;" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" />
+<glyph unicode="&#xe118;" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" />
+<glyph unicode="&#xe119;" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
+<glyph unicode="&#xe120;" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
+<glyph unicode="&#xe121;" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" />
+<glyph unicode="&#xe122;" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM100 500v250v8v8v7t0.5 7t1.5 5.5t2 5t3 4t4.5 3.5t6 1.5t7.5 0.5h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35 q-55 337 -55 351zM1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe123;" d="M74 350q0 21 13.5 35.5t33.5 14.5h18l117 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5q-18 -36 -18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-8 -3 -23 -8.5 t-65 -20t-103 -25t-132.5 -19.5t-158.5 -9q-125 0 -245.5 20.5t-178.5 40.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" />
+<glyph unicode="&#xe124;" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" />
+<glyph unicode="&#xe125;" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q124 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 213l100 212h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" />
+<glyph unicode="&#xe126;" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q124 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" />
+<glyph unicode="&#xe127;" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" />
+<glyph unicode="&#xe128;" d="M-101 651q0 72 54 110t139 38l302 -1l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 17 -10.5t26.5 -26t16.5 -36.5v-526q0 -13 -86 -93.5t-94 -80.5h-341q-16 0 -29.5 20t-19.5 41l-130 339h-107q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l107 89v502l-343 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM1000 201v600h200v-600h-200z" />
+<glyph unicode="&#xe129;" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6.5v7.5v6.5v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" />
+<glyph unicode="&#xe130;" d="M2 585q-16 -31 6 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85q0 -51 -0.5 -153.5t-0.5 -148.5q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM77 565l236 339h503 l89 -100v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" />
+<glyph unicode="&#xe131;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM298 701l2 -201h300l-2 -194l402 294l-402 298v-197h-300z" />
+<glyph unicode="&#xe132;" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l402 -294l-2 194h300l2 201h-300v197z" />
+<glyph unicode="&#xe133;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" />
+<glyph unicode="&#xe134;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" />
+<glyph unicode="&#xe135;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60 q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q104 -3 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5 t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5 q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 39 2 44q31 -13 58 -14.5t39 3.5l11 4q7 36 -16.5 53.5t-64.5 28.5t-56 23q-19 -3 -37 0 q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5zM518 916q3 12 16 30t16 25q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -24 17 -66.5t17 -43.5 q-9 2 -31 5t-36 5t-32 8t-30 14zM692 1003h1h-1z" />
+<glyph unicode="&#xe136;" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" />
+<glyph unicode="&#xe137;" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" />
+<glyph unicode="&#xe138;" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
+<glyph unicode="&#xe139;" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" />
+<glyph unicode="&#xe140;" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" />
+<glyph unicode="&#xe141;" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM514 609q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" />
+<glyph unicode="&#xe142;" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -78.5 -16.5t-67.5 -51.5l-389 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23 q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60 l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" />
+<glyph unicode="&#xe143;" d="M80 784q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100q-71 70 -104.5 105.5t-77 89.5t-61 99 t-17.5 91zM250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-105 48.5q-74 0 -132 -83l-118 -171l-114 174q-51 80 -123 80q-60 0 -109.5 -49.5t-49.5 -118.5z" />
+<glyph unicode="&#xe144;" d="M57 353q0 -95 66 -159l141 -142q68 -66 159 -66q93 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-8 9 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141q7 -7 19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -17q47 -49 77 -100l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" />
+<glyph unicode="&#xe145;" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" />
+<glyph unicode="&#xe146;" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" />
+<glyph unicode="&#xe148;" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335q-6 1 -15.5 4t-11.5 3q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5 v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5 zM700 237q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" />
+<glyph unicode="&#xe149;" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -28 16.5 -69.5t28 -62.5t41.5 -72h241v-100h-197q8 -50 -2.5 -115 t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q33 1 103 -16t103 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221z" />
+<glyph unicode="&#xe150;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
+<glyph unicode="&#xe151;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" />
+<glyph unicode="&#xe152;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" />
+<glyph unicode="&#xe153;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
+<glyph unicode="&#xe154;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
+<glyph unicode="&#xe155;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
+<glyph unicode="&#xe156;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
+<glyph unicode="&#xe157;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" />
+<glyph unicode="&#xe158;" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" />
+<glyph unicode="&#xe159;" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" />
+<glyph unicode="&#xe160;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" />
+<glyph unicode="&#xe161;" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" />
+<glyph unicode="&#xe162;" d="M217 519q8 -19 31 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8h9q14 0 26 15q11 13 274.5 321.5t264.5 308.5q14 19 5 36q-8 17 -31 17l-301 -1q1 4 78 219.5t79 227.5q2 15 -5 27l-9 9h-9q-15 0 -25 -16q-4 -6 -98 -111.5t-228.5 -257t-209.5 -237.5q-16 -19 -6 -41 z" />
+<glyph unicode="&#xe163;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " />
+<glyph unicode="&#xe164;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" />
+<glyph unicode="&#xe165;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" />
+<glyph unicode="&#xe166;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe167;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe168;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe169;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 400l697 1l3 699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe170;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l249 -237l-1 697zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe171;" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" />
+<glyph unicode="&#xe172;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" />
+<glyph unicode="&#xe173;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" />
+<glyph unicode="&#xe174;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" />
+<glyph unicode="&#xe175;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" />
+<glyph unicode="&#xe176;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" />
+<glyph unicode="&#xe177;" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" />
+<glyph unicode="&#xe178;" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" />
+<glyph unicode="&#xe179;" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -116q-25 -17 -43.5 -51.5t-18.5 -65.5v-359z" />
+<glyph unicode="&#xe180;" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" />
+<glyph unicode="&#xe181;" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" />
+<glyph unicode="&#xe182;" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q17 18 13.5 41t-22.5 37l-192 136q-19 14 -45 12t-42 -19l-118 -118q-142 101 -268 227t-227 268l118 118q17 17 20 41.5t-11 44.5 l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" />
+<glyph unicode="&#xe183;" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-20 0 -35 14.5t-15 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" />
+<glyph unicode="&#xe184;" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" />
+<glyph unicode="&#xe185;" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" />
+<glyph unicode="&#xe186;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
+<glyph unicode="&#xe187;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
+<glyph unicode="&#xe188;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" />
+<glyph unicode="&#xe189;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" />
+<glyph unicode="&#xe190;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" />
+<glyph unicode="&#xe191;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
+<glyph unicode="&#xe192;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
+<glyph unicode="&#xe193;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" />
+<glyph unicode="&#xe194;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" />
+<glyph unicode="&#xe195;" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" />
+<glyph unicode="&#xe197;" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300h200 l-300 -300z" />
+<glyph unicode="&#xe198;" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104.5t60.5 178.5q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" />
+<glyph unicode="&#xe199;" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
+<glyph unicode="&#xe200;" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -11.5t1 -11.5q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" />
+</font>
+</defs></svg> 
\ No newline at end of file
diff --git a/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.ttf b/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..67fa00bf83801d2fa568546b982c80d27f6ef74e
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.ttf differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.woff b/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..8c54182aa5d4d1ab3c9171976b615c1dcb1dc187
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/fonts/glyphicons-halflings-regular.woff differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/js/._bootstrap.js b/static/libjs/bootstrap-3.1.1-dist/js/._bootstrap.js
new file mode 100644
index 0000000000000000000000000000000000000000..1d3a6eaf6f9cb004b5796ea4b2d0b237ffbe797f
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/js/._bootstrap.js differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/js/._bootstrap.min.js b/static/libjs/bootstrap-3.1.1-dist/js/._bootstrap.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..1cdeb05cdf3bf9ca03376c0d7a0efc410a5eb69c
Binary files /dev/null and b/static/libjs/bootstrap-3.1.1-dist/js/._bootstrap.min.js differ
diff --git a/static/libjs/bootstrap-3.1.1-dist/js/bootstrap.js b/static/libjs/bootstrap-3.1.1-dist/js/bootstrap.js
new file mode 100644
index 0000000000000000000000000000000000000000..8ae571b6da5be9c7dcd95ba25896ae39e1917445
--- /dev/null
+++ b/static/libjs/bootstrap-3.1.1-dist/js/bootstrap.js
@@ -0,0 +1,1951 @@
+/*!
+ * Bootstrap v3.1.1 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+if (typeof jQuery === 'undefined') { throw new Error('Bootstrap\'s JavaScript requires jQuery') }
+
+/* ========================================================================
+ * Bootstrap: transition.js v3.1.1
+ * http://getbootstrap.com/javascript/#transitions
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
+  // ============================================================
+
+  function transitionEnd() {
+    var el = document.createElement('bootstrap')
+
+    var transEndEventNames = {
+      'WebkitTransition' : 'webkitTransitionEnd',
+      'MozTransition'    : 'transitionend',
+      'OTransition'      : 'oTransitionEnd otransitionend',
+      'transition'       : 'transitionend'
+    }
+
+    for (var name in transEndEventNames) {
+      if (el.style[name] !== undefined) {
+        return { end: transEndEventNames[name] }
+      }
+    }
+
+    return false // explicit for ie8 (  ._.)
+  }
+
+  // http://blog.alexmaccaw.com/css-transitions
+  $.fn.emulateTransitionEnd = function (duration) {
+    var called = false, $el = this
+    $(this).one($.support.transition.end, function () { called = true })
+    var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
+    setTimeout(callback, duration)
+    return this
+  }
+
+  $(function () {
+    $.support.transition = transitionEnd()
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: alert.js v3.1.1
+ * http://getbootstrap.com/javascript/#alerts
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // ALERT CLASS DEFINITION
+  // ======================
+
+  var dismiss = '[data-dismiss="alert"]'
+  var Alert   = function (el) {
+    $(el).on('click', dismiss, this.close)
+  }
+
+  Alert.prototype.close = function (e) {
+    var $this    = $(this)
+    var selector = $this.attr('data-target')
+
+    if (!selector) {
+      selector = $this.attr('href')
+      selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
+    }
+
+    var $parent = $(selector)
+
+    if (e) e.preventDefault()
+
+    if (!$parent.length) {
+      $parent = $this.hasClass('alert') ? $this : $this.parent()
+    }
+
+    $parent.trigger(e = $.Event('close.bs.alert'))
+
+    if (e.isDefaultPrevented()) return
+
+    $parent.removeClass('in')
+
+    function removeElement() {
+      $parent.trigger('closed.bs.alert').remove()
+    }
+
+    $.support.transition && $parent.hasClass('fade') ?
+      $parent
+        .one($.support.transition.end, removeElement)
+        .emulateTransitionEnd(150) :
+      removeElement()
+  }
+
+
+  // ALERT PLUGIN DEFINITION
+  // =======================
+
+  var old = $.fn.alert
+
+  $.fn.alert = function (option) {
+    return this.each(function () {
+      var $this = $(this)
+      var data  = $this.data('bs.alert')
+
+      if (!data) $this.data('bs.alert', (data = new Alert(this)))
+      if (typeof option == 'string') data[option].call($this)
+    })
+  }
+
+  $.fn.alert.Constructor = Alert
+
+
+  // ALERT NO CONFLICT
+  // =================
+
+  $.fn.alert.noConflict = function () {
+    $.fn.alert = old
+    return this
+  }
+
+
+  // ALERT DATA-API
+  // ==============
+
+  $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close)
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: button.js v3.1.1
+ * http://getbootstrap.com/javascript/#buttons
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // BUTTON PUBLIC CLASS DEFINITION
+  // ==============================
+
+  var Button = function (element, options) {
+    this.$element  = $(element)
+    this.options   = $.extend({}, Button.DEFAULTS, options)
+    this.isLoading = false
+  }
+
+  Button.DEFAULTS = {
+    loadingText: 'loading...'
+  }
+
+  Button.prototype.setState = function (state) {
+    var d    = 'disabled'
+    var $el  = this.$element
+    var val  = $el.is('input') ? 'val' : 'html'
+    var data = $el.data()
+
+    state = state + 'Text'
+
+    if (!data.resetText) $el.data('resetText', $el[val]())
+
+    $el[val](data[state] || this.options[state])
+
+    // push to event loop to allow forms to submit
+    setTimeout($.proxy(function () {
+      if (state == 'loadingText') {
+        this.isLoading = true
+        $el.addClass(d).attr(d, d)
+      } else if (this.isLoading) {
+        this.isLoading = false
+        $el.removeClass(d).removeAttr(d)
+      }
+    }, this), 0)
+  }
+
+  Button.prototype.toggle = function () {
+    var changed = true
+    var $parent = this.$element.closest('[data-toggle="buttons"]')
+
+    if ($parent.length) {
+      var $input = this.$element.find('input')
+      if ($input.prop('type') == 'radio') {
+        if ($input.prop('checked') && this.$element.hasClass('active')) changed = false
+        else $parent.find('.active').removeClass('active')
+      }
+      if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')
+    }
+
+    if (changed) this.$element.toggleClass('active')
+  }
+
+
+  // BUTTON PLUGIN DEFINITION
+  // ========================
+
+  var old = $.fn.button
+
+  $.fn.button = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.button')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.button', (data = new Button(this, options)))
+
+      if (option == 'toggle') data.toggle()
+      else if (option) data.setState(option)
+    })
+  }
+
+  $.fn.button.Constructor = Button
+
+
+  // BUTTON NO CONFLICT
+  // ==================
+
+  $.fn.button.noConflict = function () {
+    $.fn.button = old
+    return this
+  }
+
+
+  // BUTTON DATA-API
+  // ===============
+
+  $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) {
+    var $btn = $(e.target)
+    if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
+    $btn.button('toggle')
+    e.preventDefault()
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: carousel.js v3.1.1
+ * http://getbootstrap.com/javascript/#carousel
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // CAROUSEL CLASS DEFINITION
+  // =========================
+
+  var Carousel = function (element, options) {
+    this.$element    = $(element)
+    this.$indicators = this.$element.find('.carousel-indicators')
+    this.options     = options
+    this.paused      =
+    this.sliding     =
+    this.interval    =
+    this.$active     =
+    this.$items      = null
+
+    this.options.pause == 'hover' && this.$element
+      .on('mouseenter', $.proxy(this.pause, this))
+      .on('mouseleave', $.proxy(this.cycle, this))
+  }
+
+  Carousel.DEFAULTS = {
+    interval: 5000,
+    pause: 'hover',
+    wrap: true
+  }
+
+  Carousel.prototype.cycle =  function (e) {
+    e || (this.paused = false)
+
+    this.interval && clearInterval(this.interval)
+
+    this.options.interval
+      && !this.paused
+      && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
+
+    return this
+  }
+
+  Carousel.prototype.getActiveIndex = function () {
+    this.$active = this.$element.find('.item.active')
+    this.$items  = this.$active.parent().children()
+
+    return this.$items.index(this.$active)
+  }
+
+  Carousel.prototype.to = function (pos) {
+    var that        = this
+    var activeIndex = this.getActiveIndex()
+
+    if (pos > (this.$items.length - 1) || pos < 0) return
+
+    if (this.sliding)       return this.$element.one('slid.bs.carousel', function () { that.to(pos) })
+    if (activeIndex == pos) return this.pause().cycle()
+
+    return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
+  }
+
+  Carousel.prototype.pause = function (e) {
+    e || (this.paused = true)
+
+    if (this.$element.find('.next, .prev').length && $.support.transition) {
+      this.$element.trigger($.support.transition.end)
+      this.cycle(true)
+    }
+
+    this.interval = clearInterval(this.interval)
+
+    return this
+  }
+
+  Carousel.prototype.next = function () {
+    if (this.sliding) return
+    return this.slide('next')
+  }
+
+  Carousel.prototype.prev = function () {
+    if (this.sliding) return
+    return this.slide('prev')
+  }
+
+  Carousel.prototype.slide = function (type, next) {
+    var $active   = this.$element.find('.item.active')
+    var $next     = next || $active[type]()
+    var isCycling = this.interval
+    var direction = type == 'next' ? 'left' : 'right'
+    var fallback  = type == 'next' ? 'first' : 'last'
+    var that      = this
+
+    if (!$next.length) {
+      if (!this.options.wrap) return
+      $next = this.$element.find('.item')[fallback]()
+    }
+
+    if ($next.hasClass('active')) return this.sliding = false
+
+    var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction })
+    this.$element.trigger(e)
+    if (e.isDefaultPrevented()) return
+
+    this.sliding = true
+
+    isCycling && this.pause()
+
+    if (this.$indicators.length) {
+      this.$indicators.find('.active').removeClass('active')
+      this.$element.one('slid.bs.carousel', function () {
+        var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
+        $nextIndicator && $nextIndicator.addClass('active')
+      })
+    }
+
+    if ($.support.transition && this.$element.hasClass('slide')) {
+      $next.addClass(type)
+      $next[0].offsetWidth // force reflow
+      $active.addClass(direction)
+      $next.addClass(direction)
+      $active
+        .one($.support.transition.end, function () {
+          $next.removeClass([type, direction].join(' ')).addClass('active')
+          $active.removeClass(['active', direction].join(' '))
+          that.sliding = false
+          setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0)
+        })
+        .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000)
+    } else {
+      $active.removeClass('active')
+      $next.addClass('active')
+      this.sliding = false
+      this.$element.trigger('slid.bs.carousel')
+    }
+
+    isCycling && this.cycle()
+
+    return this
+  }
+
+
+  // CAROUSEL PLUGIN DEFINITION
+  // ==========================
+
+  var old = $.fn.carousel
+
+  $.fn.carousel = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.carousel')
+      var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option)
+      var action  = typeof option == 'string' ? option : options.slide
+
+      if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))
+      if (typeof option == 'number') data.to(option)
+      else if (action) data[action]()
+      else if (options.interval) data.pause().cycle()
+    })
+  }
+
+  $.fn.carousel.Constructor = Carousel
+
+
+  // CAROUSEL NO CONFLICT
+  // ====================
+
+  $.fn.carousel.noConflict = function () {
+    $.fn.carousel = old
+    return this
+  }
+
+
+  // CAROUSEL DATA-API
+  // =================
+
+  $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
+    var $this   = $(this), href
+    var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
+    var options = $.extend({}, $target.data(), $this.data())
+    var slideIndex = $this.attr('data-slide-to')
+    if (slideIndex) options.interval = false
+
+    $target.carousel(options)
+
+    if (slideIndex = $this.attr('data-slide-to')) {
+      $target.data('bs.carousel').to(slideIndex)
+    }
+
+    e.preventDefault()
+  })
+
+  $(window).on('load', function () {
+    $('[data-ride="carousel"]').each(function () {
+      var $carousel = $(this)
+      $carousel.carousel($carousel.data())
+    })
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: collapse.js v3.1.1
+ * http://getbootstrap.com/javascript/#collapse
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // COLLAPSE PUBLIC CLASS DEFINITION
+  // ================================
+
+  var Collapse = function (element, options) {
+    this.$element      = $(element)
+    this.options       = $.extend({}, Collapse.DEFAULTS, options)
+    this.transitioning = null
+
+    if (this.options.parent) this.$parent = $(this.options.parent)
+    if (this.options.toggle) this.toggle()
+  }
+
+  Collapse.DEFAULTS = {
+    toggle: true
+  }
+
+  Collapse.prototype.dimension = function () {
+    var hasWidth = this.$element.hasClass('width')
+    return hasWidth ? 'width' : 'height'
+  }
+
+  Collapse.prototype.show = function () {
+    if (this.transitioning || this.$element.hasClass('in')) return
+
+    var startEvent = $.Event('show.bs.collapse')
+    this.$element.trigger(startEvent)
+    if (startEvent.isDefaultPrevented()) return
+
+    var actives = this.$parent && this.$parent.find('> .panel > .in')
+
+    if (actives && actives.length) {
+      var hasData = actives.data('bs.collapse')
+      if (hasData && hasData.transitioning) return
+      actives.collapse('hide')
+      hasData || actives.data('bs.collapse', null)
+    }
+
+    var dimension = this.dimension()
+
+    this.$element
+      .removeClass('collapse')
+      .addClass('collapsing')
+      [dimension](0)
+
+    this.transitioning = 1
+
+    var complete = function () {
+      this.$element
+        .removeClass('collapsing')
+        .addClass('collapse in')
+        [dimension]('auto')
+      this.transitioning = 0
+      this.$element.trigger('shown.bs.collapse')
+    }
+
+    if (!$.support.transition) return complete.call(this)
+
+    var scrollSize = $.camelCase(['scroll', dimension].join('-'))
+
+    this.$element
+      .one($.support.transition.end, $.proxy(complete, this))
+      .emulateTransitionEnd(350)
+      [dimension](this.$element[0][scrollSize])
+  }
+
+  Collapse.prototype.hide = function () {
+    if (this.transitioning || !this.$element.hasClass('in')) return
+
+    var startEvent = $.Event('hide.bs.collapse')
+    this.$element.trigger(startEvent)
+    if (startEvent.isDefaultPrevented()) return
+
+    var dimension = this.dimension()
+
+    this.$element
+      [dimension](this.$element[dimension]())
+      [0].offsetHeight
+
+    this.$element
+      .addClass('collapsing')
+      .removeClass('collapse')
+      .removeClass('in')
+
+    this.transitioning = 1
+
+    var complete = function () {
+      this.transitioning = 0
+      this.$element
+        .trigger('hidden.bs.collapse')
+        .removeClass('collapsing')
+        .addClass('collapse')
+    }
+
+    if (!$.support.transition) return complete.call(this)
+
+    this.$element
+      [dimension](0)
+      .one($.support.transition.end, $.proxy(complete, this))
+      .emulateTransitionEnd(350)
+  }
+
+  Collapse.prototype.toggle = function () {
+    this[this.$element.hasClass('in') ? 'hide' : 'show']()
+  }
+
+
+  // COLLAPSE PLUGIN DEFINITION
+  // ==========================
+
+  var old = $.fn.collapse
+
+  $.fn.collapse = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.collapse')
+      var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
+
+      if (!data && options.toggle && option == 'show') option = !option
+      if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.collapse.Constructor = Collapse
+
+
+  // COLLAPSE NO CONFLICT
+  // ====================
+
+  $.fn.collapse.noConflict = function () {
+    $.fn.collapse = old
+    return this
+  }
+
+
+  // COLLAPSE DATA-API
+  // =================
+
+  $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) {
+    var $this   = $(this), href
+    var target  = $this.attr('data-target')
+        || e.preventDefault()
+        || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
+    var $target = $(target)
+    var data    = $target.data('bs.collapse')
+    var option  = data ? 'toggle' : $this.data()
+    var parent  = $this.attr('data-parent')
+    var $parent = parent && $(parent)
+
+    if (!data || !data.transitioning) {
+      if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed')
+      $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
+    }
+
+    $target.collapse(option)
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: dropdown.js v3.1.1
+ * http://getbootstrap.com/javascript/#dropdowns
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // DROPDOWN CLASS DEFINITION
+  // =========================
+
+  var backdrop = '.dropdown-backdrop'
+  var toggle   = '[data-toggle=dropdown]'
+  var Dropdown = function (element) {
+    $(element).on('click.bs.dropdown', this.toggle)
+  }
+
+  Dropdown.prototype.toggle = function (e) {
+    var $this = $(this)
+
+    if ($this.is('.disabled, :disabled')) return
+
+    var $parent  = getParent($this)
+    var isActive = $parent.hasClass('open')
+
+    clearMenus()
+
+    if (!isActive) {
+      if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
+        // if mobile we use a backdrop because click events don't delegate
+        $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus)
+      }
+
+      var relatedTarget = { relatedTarget: this }
+      $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))
+
+      if (e.isDefaultPrevented()) return
+
+      $parent
+        .toggleClass('open')
+        .trigger('shown.bs.dropdown', relatedTarget)
+
+      $this.focus()
+    }
+
+    return false
+  }
+
+  Dropdown.prototype.keydown = function (e) {
+    if (!/(38|40|27)/.test(e.keyCode)) return
+
+    var $this = $(this)
+
+    e.preventDefault()
+    e.stopPropagation()
+
+    if ($this.is('.disabled, :disabled')) return
+
+    var $parent  = getParent($this)
+    var isActive = $parent.hasClass('open')
+
+    if (!isActive || (isActive && e.keyCode == 27)) {
+      if (e.which == 27) $parent.find(toggle).focus()
+      return $this.click()
+    }
+
+    var desc = ' li:not(.divider):visible a'
+    var $items = $parent.find('[role=menu]' + desc + ', [role=listbox]' + desc)
+
+    if (!$items.length) return
+
+    var index = $items.index($items.filter(':focus'))
+
+    if (e.keyCode == 38 && index > 0)                 index--                        // up
+    if (e.keyCode == 40 && index < $items.length - 1) index++                        // down
+    if (!~index)                                      index = 0
+
+    $items.eq(index).focus()
+  }
+
+  function clearMenus(e) {
+    $(backdrop).remove()
+    $(toggle).each(function () {
+      var $parent = getParent($(this))
+      var relatedTarget = { relatedTarget: this }
+      if (!$parent.hasClass('open')) return
+      $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
+      if (e.isDefaultPrevented()) return
+      $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget)
+    })
+  }
+
+  function getParent($this) {
+    var selector = $this.attr('data-target')
+
+    if (!selector) {
+      selector = $this.attr('href')
+      selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+    }
+
+    var $parent = selector && $(selector)
+
+    return $parent && $parent.length ? $parent : $this.parent()
+  }
+
+
+  // DROPDOWN PLUGIN DEFINITION
+  // ==========================
+
+  var old = $.fn.dropdown
+
+  $.fn.dropdown = function (option) {
+    return this.each(function () {
+      var $this = $(this)
+      var data  = $this.data('bs.dropdown')
+
+      if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
+      if (typeof option == 'string') data[option].call($this)
+    })
+  }
+
+  $.fn.dropdown.Constructor = Dropdown
+
+
+  // DROPDOWN NO CONFLICT
+  // ====================
+
+  $.fn.dropdown.noConflict = function () {
+    $.fn.dropdown = old
+    return this
+  }
+
+
+  // APPLY TO STANDARD DROPDOWN ELEMENTS
+  // ===================================
+
+  $(document)
+    .on('click.bs.dropdown.data-api', clearMenus)
+    .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
+    .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
+    .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu], [role=listbox]', Dropdown.prototype.keydown)
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: modal.js v3.1.1
+ * http://getbootstrap.com/javascript/#modals
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // MODAL CLASS DEFINITION
+  // ======================
+
+  var Modal = function (element, options) {
+    this.options   = options
+    this.$element  = $(element)
+    this.$backdrop =
+    this.isShown   = null
+
+    if (this.options.remote) {
+      this.$element
+        .find('.modal-content')
+        .load(this.options.remote, $.proxy(function () {
+          this.$element.trigger('loaded.bs.modal')
+        }, this))
+    }
+  }
+
+  Modal.DEFAULTS = {
+    backdrop: true,
+    keyboard: true,
+    show: true
+  }
+
+  Modal.prototype.toggle = function (_relatedTarget) {
+    return this[!this.isShown ? 'show' : 'hide'](_relatedTarget)
+  }
+
+  Modal.prototype.show = function (_relatedTarget) {
+    var that = this
+    var e    = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
+
+    this.$element.trigger(e)
+
+    if (this.isShown || e.isDefaultPrevented()) return
+
+    this.isShown = true
+
+    this.escape()
+
+    this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
+
+    this.backdrop(function () {
+      var transition = $.support.transition && that.$element.hasClass('fade')
+
+      if (!that.$element.parent().length) {
+        that.$element.appendTo(document.body) // don't move modals dom position
+      }
+
+      that.$element
+        .show()
+        .scrollTop(0)
+
+      if (transition) {
+        that.$element[0].offsetWidth // force reflow
+      }
+
+      that.$element
+        .addClass('in')
+        .attr('aria-hidden', false)
+
+      that.enforceFocus()
+
+      var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
+
+      transition ?
+        that.$element.find('.modal-dialog') // wait for modal to slide in
+          .one($.support.transition.end, function () {
+            that.$element.focus().trigger(e)
+          })
+          .emulateTransitionEnd(300) :
+        that.$element.focus().trigger(e)
+    })
+  }
+
+  Modal.prototype.hide = function (e) {
+    if (e) e.preventDefault()
+
+    e = $.Event('hide.bs.modal')
+
+    this.$element.trigger(e)
+
+    if (!this.isShown || e.isDefaultPrevented()) return
+
+    this.isShown = false
+
+    this.escape()
+
+    $(document).off('focusin.bs.modal')
+
+    this.$element
+      .removeClass('in')
+      .attr('aria-hidden', true)
+      .off('click.dismiss.bs.modal')
+
+    $.support.transition && this.$element.hasClass('fade') ?
+      this.$element
+        .one($.support.transition.end, $.proxy(this.hideModal, this))
+        .emulateTransitionEnd(300) :
+      this.hideModal()
+  }
+
+  Modal.prototype.enforceFocus = function () {
+    $(document)
+      .off('focusin.bs.modal') // guard against infinite focus loop
+      .on('focusin.bs.modal', $.proxy(function (e) {
+        if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
+          this.$element.focus()
+        }
+      }, this))
+  }
+
+  Modal.prototype.escape = function () {
+    if (this.isShown && this.options.keyboard) {
+      this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) {
+        e.which == 27 && this.hide()
+      }, this))
+    } else if (!this.isShown) {
+      this.$element.off('keyup.dismiss.bs.modal')
+    }
+  }
+
+  Modal.prototype.hideModal = function () {
+    var that = this
+    this.$element.hide()
+    this.backdrop(function () {
+      that.removeBackdrop()
+      that.$element.trigger('hidden.bs.modal')
+    })
+  }
+
+  Modal.prototype.removeBackdrop = function () {
+    this.$backdrop && this.$backdrop.remove()
+    this.$backdrop = null
+  }
+
+  Modal.prototype.backdrop = function (callback) {
+    var animate = this.$element.hasClass('fade') ? 'fade' : ''
+
+    if (this.isShown && this.options.backdrop) {
+      var doAnimate = $.support.transition && animate
+
+      this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
+        .appendTo(document.body)
+
+      this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) {
+        if (e.target !== e.currentTarget) return
+        this.options.backdrop == 'static'
+          ? this.$element[0].focus.call(this.$element[0])
+          : this.hide.call(this)
+      }, this))
+
+      if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
+
+      this.$backdrop.addClass('in')
+
+      if (!callback) return
+
+      doAnimate ?
+        this.$backdrop
+          .one($.support.transition.end, callback)
+          .emulateTransitionEnd(150) :
+        callback()
+
+    } else if (!this.isShown && this.$backdrop) {
+      this.$backdrop.removeClass('in')
+
+      $.support.transition && this.$element.hasClass('fade') ?
+        this.$backdrop
+          .one($.support.transition.end, callback)
+          .emulateTransitionEnd(150) :
+        callback()
+
+    } else if (callback) {
+      callback()
+    }
+  }
+
+
+  // MODAL PLUGIN DEFINITION
+  // =======================
+
+  var old = $.fn.modal
+
+  $.fn.modal = function (option, _relatedTarget) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.modal')
+      var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
+
+      if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
+      if (typeof option == 'string') data[option](_relatedTarget)
+      else if (options.show) data.show(_relatedTarget)
+    })
+  }
+
+  $.fn.modal.Constructor = Modal
+
+
+  // MODAL NO CONFLICT
+  // =================
+
+  $.fn.modal.noConflict = function () {
+    $.fn.modal = old
+    return this
+  }
+
+
+  // MODAL DATA-API
+  // ==============
+
+  $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
+    var $this   = $(this)
+    var href    = $this.attr('href')
+    var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
+    var option  = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
+
+    if ($this.is('a')) e.preventDefault()
+
+    $target
+      .modal(option, this)
+      .one('hide', function () {
+        $this.is(':visible') && $this.focus()
+      })
+  })
+
+  $(document)
+    .on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') })
+    .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: tooltip.js v3.1.1
+ * http://getbootstrap.com/javascript/#tooltip
+ * Inspired by the original jQuery.tipsy by Jason Frame
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // TOOLTIP PUBLIC CLASS DEFINITION
+  // ===============================
+
+  var Tooltip = function (element, options) {
+    this.type       =
+    this.options    =
+    this.enabled    =
+    this.timeout    =
+    this.hoverState =
+    this.$element   = null
+
+    this.init('tooltip', element, options)
+  }
+
+  Tooltip.DEFAULTS = {
+    animation: true,
+    placement: 'top',
+    selector: false,
+    template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+    trigger: 'hover focus',
+    title: '',
+    delay: 0,
+    html: false,
+    container: false
+  }
+
+  Tooltip.prototype.init = function (type, element, options) {
+    this.enabled  = true
+    this.type     = type
+    this.$element = $(element)
+    this.options  = this.getOptions(options)
+
+    var triggers = this.options.trigger.split(' ')
+
+    for (var i = triggers.length; i--;) {
+      var trigger = triggers[i]
+
+      if (trigger == 'click') {
+        this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
+      } else if (trigger != 'manual') {
+        var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focusin'
+        var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
+
+        this.$element.on(eventIn  + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
+        this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
+      }
+    }
+
+    this.options.selector ?
+      (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
+      this.fixTitle()
+  }
+
+  Tooltip.prototype.getDefaults = function () {
+    return Tooltip.DEFAULTS
+  }
+
+  Tooltip.prototype.getOptions = function (options) {
+    options = $.extend({}, this.getDefaults(), this.$element.data(), options)
+
+    if (options.delay && typeof options.delay == 'number') {
+      options.delay = {
+        show: options.delay,
+        hide: options.delay
+      }
+    }
+
+    return options
+  }
+
+  Tooltip.prototype.getDelegateOptions = function () {
+    var options  = {}
+    var defaults = this.getDefaults()
+
+    this._options && $.each(this._options, function (key, value) {
+      if (defaults[key] != value) options[key] = value
+    })
+
+    return options
+  }
+
+  Tooltip.prototype.enter = function (obj) {
+    var self = obj instanceof this.constructor ?
+      obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
+
+    clearTimeout(self.timeout)
+
+    self.hoverState = 'in'
+
+    if (!self.options.delay || !self.options.delay.show) return self.show()
+
+    self.timeout = setTimeout(function () {
+      if (self.hoverState == 'in') self.show()
+    }, self.options.delay.show)
+  }
+
+  Tooltip.prototype.leave = function (obj) {
+    var self = obj instanceof this.constructor ?
+      obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
+
+    clearTimeout(self.timeout)
+
+    self.hoverState = 'out'
+
+    if (!self.options.delay || !self.options.delay.hide) return self.hide()
+
+    self.timeout = setTimeout(function () {
+      if (self.hoverState == 'out') self.hide()
+    }, self.options.delay.hide)
+  }
+
+  Tooltip.prototype.show = function () {
+    var e = $.Event('show.bs.' + this.type)
+
+    if (this.hasContent() && this.enabled) {
+      this.$element.trigger(e)
+
+      if (e.isDefaultPrevented()) return
+      var that = this;
+
+      var $tip = this.tip()
+
+      this.setContent()
+
+      if (this.options.animation) $tip.addClass('fade')
+
+      var placement = typeof this.options.placement == 'function' ?
+        this.options.placement.call(this, $tip[0], this.$element[0]) :
+        this.options.placement
+
+      var autoToken = /\s?auto?\s?/i
+      var autoPlace = autoToken.test(placement)
+      if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
+
+      $tip
+        .detach()
+        .css({ top: 0, left: 0, display: 'block' })
+        .addClass(placement)
+
+      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
+
+      var pos          = this.getPosition()
+      var actualWidth  = $tip[0].offsetWidth
+      var actualHeight = $tip[0].offsetHeight
+
+      if (autoPlace) {
+        var $parent = this.$element.parent()
+
+        var orgPlacement = placement
+        var docScroll    = document.documentElement.scrollTop || document.body.scrollTop
+        var parentWidth  = this.options.container == 'body' ? window.innerWidth  : $parent.outerWidth()
+        var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
+        var parentLeft   = this.options.container == 'body' ? 0 : $parent.offset().left
+
+        placement = placement == 'bottom' && pos.top   + pos.height  + actualHeight - docScroll > parentHeight  ? 'top'    :
+                    placement == 'top'    && pos.top   - docScroll   - actualHeight < 0                         ? 'bottom' :
+                    placement == 'right'  && pos.right + actualWidth > parentWidth                              ? 'left'   :
+                    placement == 'left'   && pos.left  - actualWidth < parentLeft                               ? 'right'  :
+                    placement
+
+        $tip
+          .removeClass(orgPlacement)
+          .addClass(placement)
+      }
+
+      var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
+
+      this.applyPlacement(calculatedOffset, placement)
+      this.hoverState = null
+
+      var complete = function() {
+        that.$element.trigger('shown.bs.' + that.type)
+      }
+
+      $.support.transition && this.$tip.hasClass('fade') ?
+        $tip
+          .one($.support.transition.end, complete)
+          .emulateTransitionEnd(150) :
+        complete()
+    }
+  }
+
+  Tooltip.prototype.applyPlacement = function (offset, placement) {
+    var replace
+    var $tip   = this.tip()
+    var width  = $tip[0].offsetWidth
+    var height = $tip[0].offsetHeight
+
+    // manually read margins because getBoundingClientRect includes difference
+    var marginTop = parseInt($tip.css('margin-top'), 10)
+    var marginLeft = parseInt($tip.css('margin-left'), 10)
+
+    // we must check for NaN for ie 8/9
+    if (isNaN(marginTop))  marginTop  = 0
+    if (isNaN(marginLeft)) marginLeft = 0
+
+    offset.top  = offset.top  + marginTop
+    offset.left = offset.left + marginLeft
+
+    // $.fn.offset doesn't round pixel values
+    // so we use setOffset directly with our own function B-0
+    $.offset.setOffset($tip[0], $.extend({
+      using: function (props) {
+        $tip.css({
+          top: Math.round(props.top),
+          left: Math.round(props.left)
+        })
+      }
+    }, offset), 0)
+
+    $tip.addClass('in')
+
+    // check to see if placing tip in new offset caused the tip to resize itself
+    var actualWidth  = $tip[0].offsetWidth
+    var actualHeight = $tip[0].offsetHeight
+
+    if (placement == 'top' && actualHeight != height) {
+      replace = true
+      offset.top = offset.top + height - actualHeight
+    }
+
+    if (/bottom|top/.test(placement)) {
+      var delta = 0
+
+      if (offset.left < 0) {
+        delta       = offset.left * -2
+        offset.left = 0
+
+        $tip.offset(offset)
+
+        actualWidth  = $tip[0].offsetWidth
+        actualHeight = $tip[0].offsetHeight
+      }
+
+      this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
+    } else {
+      this.replaceArrow(actualHeight - height, actualHeight, 'top')
+    }
+
+    if (replace) $tip.offset(offset)
+  }
+
+  Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
+    this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
+  }
+
+  Tooltip.prototype.setContent = function () {
+    var $tip  = this.tip()
+    var title = this.getTitle()
+
+    $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
+    $tip.removeClass('fade in top bottom left right')
+  }
+
+  Tooltip.prototype.hide = function () {
+    var that = this
+    var $tip = this.tip()
+    var e    = $.Event('hide.bs.' + this.type)
+
+    function complete() {
+      if (that.hoverState != 'in') $tip.detach()
+      that.$element.trigger('hidden.bs.' + that.type)
+    }
+
+    this.$element.trigger(e)
+
+    if (e.isDefaultPrevented()) return
+
+    $tip.removeClass('in')
+
+    $.support.transition && this.$tip.hasClass('fade') ?
+      $tip
+        .one($.support.transition.end, complete)
+        .emulateTransitionEnd(150) :
+      complete()
+
+    this.hoverState = null
+
+    return this
+  }
+
+  Tooltip.prototype.fixTitle = function () {
+    var $e = this.$element
+    if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
+      $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
+    }
+  }
+
+  Tooltip.prototype.hasContent = function () {
+    return this.getTitle()
+  }
+
+  Tooltip.prototype.getPosition = function () {
+    var el = this.$element[0]
+    return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
+      width: el.offsetWidth,
+      height: el.offsetHeight
+    }, this.$element.offset())
+  }
+
+  Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
+    return placement == 'bottom' ? { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2  } :
+           placement == 'top'    ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } :
+           placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
+        /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }
+  }
+
+  Tooltip.prototype.getTitle = function () {
+    var title
+    var $e = this.$element
+    var o  = this.options
+
+    title = $e.attr('data-original-title')
+      || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)
+
+    return title
+  }
+
+  Tooltip.prototype.tip = function () {
+    return this.$tip = this.$tip || $(this.options.template)
+  }
+
+  Tooltip.prototype.arrow = function () {
+    return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
+  }
+
+  Tooltip.prototype.validate = function () {
+    if (!this.$element[0].parentNode) {
+      this.hide()
+      this.$element = null
+      this.options  = null
+    }
+  }
+
+  Tooltip.prototype.enable = function () {
+    this.enabled = true
+  }
+
+  Tooltip.prototype.disable = function () {
+    this.enabled = false
+  }
+
+  Tooltip.prototype.toggleEnabled = function () {
+    this.enabled = !this.enabled
+  }
+
+  Tooltip.prototype.toggle = function (e) {
+    var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
+    self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
+  }
+
+  Tooltip.prototype.destroy = function () {
+    clearTimeout(this.timeout)
+    this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
+  }
+
+
+  // TOOLTIP PLUGIN DEFINITION
+  // =========================
+
+  var old = $.fn.tooltip
+
+  $.fn.tooltip = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.tooltip')
+      var options = typeof option == 'object' && option
+
+      if (!data && option == 'destroy') return
+      if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.tooltip.Constructor = Tooltip
+
+
+  // TOOLTIP NO CONFLICT
+  // ===================
+
+  $.fn.tooltip.noConflict = function () {
+    $.fn.tooltip = old
+    return this
+  }
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: popover.js v3.1.1
+ * http://getbootstrap.com/javascript/#popovers
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // POPOVER PUBLIC CLASS DEFINITION
+  // ===============================
+
+  var Popover = function (element, options) {
+    this.init('popover', element, options)
+  }
+
+  if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
+
+  Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
+    placement: 'right',
+    trigger: 'click',
+    content: '',
+    template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
+  })
+
+
+  // NOTE: POPOVER EXTENDS tooltip.js
+  // ================================
+
+  Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)
+
+  Popover.prototype.constructor = Popover
+
+  Popover.prototype.getDefaults = function () {
+    return Popover.DEFAULTS
+  }
+
+  Popover.prototype.setContent = function () {
+    var $tip    = this.tip()
+    var title   = this.getTitle()
+    var content = this.getContent()
+
+    $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
+    $tip.find('.popover-content')[ // we use append for html objects to maintain js events
+      this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
+    ](content)
+
+    $tip.removeClass('fade top bottom left right in')
+
+    // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
+    // this manually by checking the contents.
+    if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
+  }
+
+  Popover.prototype.hasContent = function () {
+    return this.getTitle() || this.getContent()
+  }
+
+  Popover.prototype.getContent = function () {
+    var $e = this.$element
+    var o  = this.options
+
+    return $e.attr('data-content')
+      || (typeof o.content == 'function' ?
+            o.content.call($e[0]) :
+            o.content)
+  }
+
+  Popover.prototype.arrow = function () {
+    return this.$arrow = this.$arrow || this.tip().find('.arrow')
+  }
+
+  Popover.prototype.tip = function () {
+    if (!this.$tip) this.$tip = $(this.options.template)
+    return this.$tip
+  }
+
+
+  // POPOVER PLUGIN DEFINITION
+  // =========================
+
+  var old = $.fn.popover
+
+  $.fn.popover = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.popover')
+      var options = typeof option == 'object' && option
+
+      if (!data && option == 'destroy') return
+      if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.popover.Constructor = Popover
+
+
+  // POPOVER NO CONFLICT
+  // ===================
+
+  $.fn.popover.noConflict = function () {
+    $.fn.popover = old
+    return this
+  }
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: scrollspy.js v3.1.1
+ * http://getbootstrap.com/javascript/#scrollspy
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // SCROLLSPY CLASS DEFINITION
+  // ==========================
+
+  function ScrollSpy(element, options) {
+    var href
+    var process  = $.proxy(this.process, this)
+
+    this.$element       = $(element).is('body') ? $(window) : $(element)
+    this.$body          = $('body')
+    this.$scrollElement = this.$element.on('scroll.bs.scroll-spy.data-api', process)
+    this.options        = $.extend({}, ScrollSpy.DEFAULTS, options)
+    this.selector       = (this.options.target
+      || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
+      || '') + ' .nav li > a'
+    this.offsets        = $([])
+    this.targets        = $([])
+    this.activeTarget   = null
+
+    this.refresh()
+    this.process()
+  }
+
+  ScrollSpy.DEFAULTS = {
+    offset: 10
+  }
+
+  ScrollSpy.prototype.refresh = function () {
+    var offsetMethod = this.$element[0] == window ? 'offset' : 'position'
+
+    this.offsets = $([])
+    this.targets = $([])
+
+    var self     = this
+    var $targets = this.$body
+      .find(this.selector)
+      .map(function () {
+        var $el   = $(this)
+        var href  = $el.data('target') || $el.attr('href')
+        var $href = /^#./.test(href) && $(href)
+
+        return ($href
+          && $href.length
+          && $href.is(':visible')
+          && [[ $href[offsetMethod]().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]]) || null
+      })
+      .sort(function (a, b) { return a[0] - b[0] })
+      .each(function () {
+        self.offsets.push(this[0])
+        self.targets.push(this[1])
+      })
+  }
+
+  ScrollSpy.prototype.process = function () {
+    var scrollTop    = this.$scrollElement.scrollTop() + this.options.offset
+    var scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight
+    var maxScroll    = scrollHeight - this.$scrollElement.height()
+    var offsets      = this.offsets
+    var targets      = this.targets
+    var activeTarget = this.activeTarget
+    var i
+
+    if (scrollTop >= maxScroll) {
+      return activeTarget != (i = targets.last()[0]) && this.activate(i)
+    }
+
+    if (activeTarget && scrollTop <= offsets[0]) {
+      return activeTarget != (i = targets[0]) && this.activate(i)
+    }
+
+    for (i = offsets.length; i--;) {
+      activeTarget != targets[i]
+        && scrollTop >= offsets[i]
+        && (!offsets[i + 1] || scrollTop <= offsets[i + 1])
+        && this.activate( targets[i] )
+    }
+  }
+
+  ScrollSpy.prototype.activate = function (target) {
+    this.activeTarget = target
+
+    $(this.selector)
+      .parentsUntil(this.options.target, '.active')
+      .removeClass('active')
+
+    var selector = this.selector +
+        '[data-target="' + target + '"],' +
+        this.selector + '[href="' + target + '"]'
+
+    var active = $(selector)
+      .parents('li')
+      .addClass('active')
+
+    if (active.parent('.dropdown-menu').length) {
+      active = active
+        .closest('li.dropdown')
+        .addClass('active')
+    }
+
+    active.trigger('activate.bs.scrollspy')
+  }
+
+
+  // SCROLLSPY PLUGIN DEFINITION
+  // ===========================
+
+  var old = $.fn.scrollspy
+
+  $.fn.scrollspy = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.scrollspy')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.scrollspy.Constructor = ScrollSpy
+
+
+  // SCROLLSPY NO CONFLICT
+  // =====================
+
+  $.fn.scrollspy.noConflict = function () {
+    $.fn.scrollspy = old
+    return this
+  }
+
+
+  // SCROLLSPY DATA-API
+  // ==================
+
+  $(window).on('load', function () {
+    $('[data-spy="scroll"]').each(function () {
+      var $spy = $(this)
+      $spy.scrollspy($spy.data())
+    })
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: tab.js v3.1.1
+ * http://getbootstrap.com/javascript/#tabs
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // TAB CLASS DEFINITION
+  // ====================
+
+  var Tab = function (element) {
+    this.element = $(element)
+  }
+
+  Tab.prototype.show = function () {
+    var $this    = this.element
+    var $ul      = $this.closest('ul:not(.dropdown-menu)')
+    var selector = $this.data('target')
+
+    if (!selector) {
+      selector = $this.attr('href')
+      selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+    }
+
+    if ($this.parent('li').hasClass('active')) return
+
+    var previous = $ul.find('.active:last a')[0]
+    var e        = $.Event('show.bs.tab', {
+      relatedTarget: previous
+    })
+
+    $this.trigger(e)
+
+    if (e.isDefaultPrevented()) return
+
+    var $target = $(selector)
+
+    this.activate($this.parent('li'), $ul)
+    this.activate($target, $target.parent(), function () {
+      $this.trigger({
+        type: 'shown.bs.tab',
+        relatedTarget: previous
+      })
+    })
+  }
+
+  Tab.prototype.activate = function (element, container, callback) {
+    var $active    = container.find('> .active')
+    var transition = callback
+      && $.support.transition
+      && $active.hasClass('fade')
+
+    function next() {
+      $active
+        .removeClass('active')
+        .find('> .dropdown-menu > .active')
+        .removeClass('active')
+
+      element.addClass('active')
+
+      if (transition) {
+        element[0].offsetWidth // reflow for transition
+        element.addClass('in')
+      } else {
+        element.removeClass('fade')
+      }
+
+      if (element.parent('.dropdown-menu')) {
+        element.closest('li.dropdown').addClass('active')
+      }
+
+      callback && callback()
+    }
+
+    transition ?
+      $active
+        .one($.support.transition.end, next)
+        .emulateTransitionEnd(150) :
+      next()
+
+    $active.removeClass('in')
+  }
+
+
+  // TAB PLUGIN DEFINITION
+  // =====================
+
+  var old = $.fn.tab
+
+  $.fn.tab = function ( option ) {
+    return this.each(function () {
+      var $this = $(this)
+      var data  = $this.data('bs.tab')
+
+      if (!data) $this.data('bs.tab', (data = new Tab(this)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.tab.Constructor = Tab
+
+
+  // TAB NO CONFLICT
+  // ===============
+
+  $.fn.tab.noConflict = function () {
+    $.fn.tab = old
+    return this
+  }
+
+
+  // TAB DATA-API
+  // ============
+
+  $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
+    e.preventDefault()
+    $(this).tab('show')
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: affix.js v3.1.1
+ * http://getbootstrap.com/javascript/#affix
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // AFFIX CLASS DEFINITION
+  // ======================
+
+  var Affix = function (element, options) {
+    this.options = $.extend({}, Affix.DEFAULTS, options)
+    this.$window = $(window)
+      .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
+      .on('click.bs.affix.data-api',  $.proxy(this.checkPositionWithEventLoop, this))
+
+    this.$element     = $(element)
+    this.affixed      =
+    this.unpin        =
+    this.pinnedOffset = null
+
+    this.checkPosition()
+  }
+
+  Affix.RESET = 'affix affix-top affix-bottom'
+
+  Affix.DEFAULTS = {
+    offset: 0
+  }
+
+  Affix.prototype.getPinnedOffset = function () {
+    if (this.pinnedOffset) return this.pinnedOffset
+    this.$element.removeClass(Affix.RESET).addClass('affix')
+    var scrollTop = this.$window.scrollTop()
+    var position  = this.$element.offset()
+    return (this.pinnedOffset = position.top - scrollTop)
+  }
+
+  Affix.prototype.checkPositionWithEventLoop = function () {
+    setTimeout($.proxy(this.checkPosition, this), 1)
+  }
+
+  Affix.prototype.checkPosition = function () {
+    if (!this.$element.is(':visible')) return
+
+    var scrollHeight = $(document).height()
+    var scrollTop    = this.$window.scrollTop()
+    var position     = this.$element.offset()
+    var offset       = this.options.offset
+    var offsetTop    = offset.top
+    var offsetBottom = offset.bottom
+
+    if (this.affixed == 'top') position.top += scrollTop
+
+    if (typeof offset != 'object')         offsetBottom = offsetTop = offset
+    if (typeof offsetTop == 'function')    offsetTop    = offset.top(this.$element)
+    if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element)
+
+    var affix = this.unpin   != null && (scrollTop + this.unpin <= position.top) ? false :
+                offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' :
+                offsetTop    != null && (scrollTop <= offsetTop) ? 'top' : false
+
+    if (this.affixed === affix) return
+    if (this.unpin) this.$element.css('top', '')
+
+    var affixType = 'affix' + (affix ? '-' + affix : '')
+    var e         = $.Event(affixType + '.bs.affix')
+
+    this.$element.trigger(e)
+
+    if (e.isDefaultPrevented()) return
+
+    this.affixed = affix
+    this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null
+
+    this.$element
+      .removeClass(Affix.RESET)
+      .addClass(affixType)
+      .trigger($.Event(affixType.replace('affix', 'affixed')))
+
+    if (affix == 'bottom') {
+      this.$element.offset({ top: scrollHeight - offsetBottom - this.$element.height() })
+    }
+  }
+
+
+  // AFFIX PLUGIN DEFINITION
+  // =======================
+
+  var old = $.fn.affix
+
+  $.fn.affix = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.affix')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.affix', (data = new Affix(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.affix.Constructor = Affix
+
+
+  // AFFIX NO CONFLICT
+  // =================
+
+  $.fn.affix.noConflict = function () {
+    $.fn.affix = old
+    return this
+  }
+
+
+  // AFFIX DATA-API
+  // ==============
+
+  $(window).on('load', function () {
+    $('[data-spy="affix"]').each(function () {
+      var $spy = $(this)
+      var data = $spy.data()
+
+      data.offset = data.offset || {}
+
+      if (data.offsetBottom) data.offset.bottom = data.offsetBottom
+      if (data.offsetTop)    data.offset.top    = data.offsetTop
+
+      $spy.affix(data)
+    })
+  })
+
+}(jQuery);
diff --git a/static/libjs/bootstrap-3.1.1-dist/js/bootstrap.min.js b/static/libjs/bootstrap-3.1.1-dist/js/bootstrap.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..b04a0e82fffee109e8cd5e48b3f3aa2a9b2aceff
--- /dev/null
+++ b/static/libjs/bootstrap-3.1.1-dist/js/bootstrap.min.js
@@ -0,0 +1,6 @@
+/*!
+ * Bootstrap v3.1.1 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.isLoading=!1};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",f.resetText||d.data("resetText",d[e]()),d[e](f[b]||this.options[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},b.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});return this.$element.trigger(j),j.isDefaultPrevented()?void 0:(this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this)};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("collapse in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);!e&&f.toggle&&"show"==c&&(c=!c),e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(b){a(d).remove(),a(e).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown",h),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=" li:not(.divider):visible a",i=f.find("[role=menu]"+h+", [role=listbox]"+h);if(i.length){var j=i.index(i.filter(":focus"));38==b.keyCode&&j>0&&j--,40==b.keyCode&&j<i.length-1&&j++,~j||(j=0),i.eq(j).focus()}}}};var g=a.fn.dropdown;a.fn.dropdown=function(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new f(this)),"string"==typeof b&&d[b].call(c)})},a.fn.dropdown.Constructor=f,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=g,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",e,f.prototype.toggle).on("keydown.bs.dropdown.data-api",e+", [role=menu], [role=listbox]",f.prototype.keydown)}(jQuery),+function(a){"use strict";var b=function(b,c){this.options=c,this.$element=a(b),this.$backdrop=this.isShown=null,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,a.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};b.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},b.prototype.toggle=function(a){return this[this.isShown?"hide":"show"](a)},b.prototype.show=function(b){var c=this,d=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(d),this.isShown||d.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.backdrop(function(){var d=a.support.transition&&c.$element.hasClass("fade");c.$element.parent().length||c.$element.appendTo(document.body),c.$element.show().scrollTop(0),d&&c.$element[0].offsetWidth,c.$element.addClass("in").attr("aria-hidden",!1),c.enforceFocus();var e=a.Event("shown.bs.modal",{relatedTarget:b});d?c.$element.find(".modal-dialog").one(a.support.transition.end,function(){c.$element.focus().trigger(e)}).emulateTransitionEnd(300):c.$element.focus().trigger(e)}))},b.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.bs.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one(a.support.transition.end,a.proxy(this.hideModal,this)).emulateTransitionEnd(300):this.hideModal())},b.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.focus()},this))},b.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keyup.dismiss.bs.modal")},b.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden.bs.modal")})},b.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},b.prototype.backdrop=function(b){var c=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var d=a.support.transition&&c;if(this.$backdrop=a('<div class="modal-backdrop '+c+'" />').appendTo(document.body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());c.is("a")&&b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this,d=this.tip();this.setContent(),this.options.animation&&d.addClass("fade");var e="function"==typeof this.options.placement?this.options.placement.call(this,d[0],this.$element[0]):this.options.placement,f=/\s?auto?\s?/i,g=f.test(e);g&&(e=e.replace(f,"")||"top"),d.detach().css({top:0,left:0,display:"block"}).addClass(e),this.options.container?d.appendTo(this.options.container):d.insertAfter(this.$element);var h=this.getPosition(),i=d[0].offsetWidth,j=d[0].offsetHeight;if(g){var k=this.$element.parent(),l=e,m=document.documentElement.scrollTop||document.body.scrollTop,n="body"==this.options.container?window.innerWidth:k.outerWidth(),o="body"==this.options.container?window.innerHeight:k.outerHeight(),p="body"==this.options.container?0:k.offset().left;e="bottom"==e&&h.top+h.height+j-m>o?"top":"top"==e&&h.top-m-j<0?"bottom":"right"==e&&h.right+i>n?"left":"left"==e&&h.left-i<p?"right":e,d.removeClass(l).addClass(e)}var q=this.getCalculatedOffset(e,h,i,j);this.applyPlacement(q,e),this.hoverState=null;var r=function(){c.$element.trigger("shown.bs."+c.type)};a.support.transition&&this.$tip.hasClass("fade")?d.one(a.support.transition.end,r).emulateTransitionEnd(150):r()}},b.prototype.applyPlacement=function(b,c){var d,e=this.tip(),f=e[0].offsetWidth,g=e[0].offsetHeight,h=parseInt(e.css("margin-top"),10),i=parseInt(e.css("margin-left"),10);isNaN(h)&&(h=0),isNaN(i)&&(i=0),b.top=b.top+h,b.left=b.left+i,a.offset.setOffset(e[0],a.extend({using:function(a){e.css({top:Math.round(a.top),left:Math.round(a.left)})}},b),0),e.addClass("in");var j=e[0].offsetWidth,k=e[0].offsetHeight;if("top"==c&&k!=g&&(d=!0,b.top=b.top+g-k),/bottom|top/.test(c)){var l=0;b.left<0&&(l=-2*b.left,b.left=0,e.offset(b),j=e[0].offsetWidth,k=e[0].offsetHeight),this.replaceArrow(l-f+j,j,"left")}else this.replaceArrow(k-g,k,"top");d&&e.offset(b)},b.prototype.replaceArrow=function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},b.prototype.hide=function(){function b(){"in"!=c.hoverState&&d.detach(),c.$element.trigger("hidden.bs."+c.type)}var c=this,d=this.tip(),e=a.Event("hide.bs."+this.type);return this.$element.trigger(e),e.isDefaultPrevented()?void 0:(d.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d.one(a.support.transition.end,b).emulateTransitionEnd(150):b(),this.hoverState=null,this)},b.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},b.prototype.hasContent=function(){return this.getTitle()},b.prototype.getPosition=function(){var b=this.$element[0];return a.extend({},"function"==typeof b.getBoundingClientRect?b.getBoundingClientRect():{width:b.offsetWidth,height:b.offsetHeight},this.$element.offset())},b.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},b.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},b.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},b.prototype.validate=function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},b.prototype.enable=function(){this.enabled=!0},b.prototype.disable=function(){this.enabled=!1},b.prototype.toggleEnabled=function(){this.enabled=!this.enabled},b.prototype.toggle=function(b){var c=b?a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type):this;c.tip().hasClass("in")?c.leave(c):c.enter(c)},b.prototype.destroy=function(){clearTimeout(this.timeout),this.hide().$element.off("."+this.type).removeData("bs."+this.type)};var c=a.fn.tooltip;a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof c&&c;(e||"destroy"!=c)&&(e||d.data("bs.tooltip",e=new b(this,f)),"string"==typeof c&&e[c]())})},a.fn.tooltip.Constructor=b,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=c,this}}(jQuery),+function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");b.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;(e||"destroy"!=c)&&(e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]())})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(a(c).is("body")?window:c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);{var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})}},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);if(g&&b<=e[0])return g!=(a=f[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parentsUntil(this.options.target,".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(b.RESET).addClass("affix");var a=this.$window.scrollTop(),c=this.$element.offset();return this.pinnedOffset=c.top-a},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"top"==this.affixed&&(e.top+=d),"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top(this.$element)),"function"==typeof h&&(h=f.bottom(this.$element));var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;if(this.affixed!==i){this.unpin&&this.$element.css("top","");var j="affix"+(i?"-"+i:""),k=a.Event(j+".bs.affix");this.$element.trigger(k),k.isDefaultPrevented()||(this.affixed=i,this.unpin="bottom"==i?this.getPinnedOffset():null,this.$element.removeClass(b.RESET).addClass(j).trigger(a.Event(j.replace("affix","affixed"))),"bottom"==i&&this.$element.offset({top:c-h-this.$element.height()}))}}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery);
\ No newline at end of file
diff --git a/static/libjs/bootstrap-multiselect/bootstrap-multiselect.css b/static/libjs/bootstrap-multiselect/bootstrap-multiselect.css
new file mode 100644
index 0000000000000000000000000000000000000000..e6b8da91169da92a17026572c24c27c73080d8ff
--- /dev/null
+++ b/static/libjs/bootstrap-multiselect/bootstrap-multiselect.css
@@ -0,0 +1 @@
+.multiselect-container{position:absolute;list-style-type:none;margin:0;padding:0}.multiselect-container .input-group{margin:5px}.multiselect-container>li{padding:0}.multiselect-container>li>a.multiselect-all label{font-weight:bold}.multiselect-container>li>label.multiselect-group{margin:0;padding:3px 20px 3px 20px;height:100%;font-weight:bold}.multiselect-container>li>a>label{margin:0;height:100%;cursor:pointer;font-weight:normal}.multiselect-container>li>a>label.radio,.multiselect-container>li>a>label.checkbox{margin:0}.multiselect-container>li>a>label>input[type="checkbox"]{margin-bottom:5px}.btn-group>.btn-group:nth-child(2)>.multiselect.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}
\ No newline at end of file
diff --git a/static/libjs/bootstrap-multiselect/bootstrap-multiselect.js b/static/libjs/bootstrap-multiselect/bootstrap-multiselect.js
new file mode 100644
index 0000000000000000000000000000000000000000..fd083953909da3d7e41b4498b22421b3b0f447b1
--- /dev/null
+++ b/static/libjs/bootstrap-multiselect/bootstrap-multiselect.js
@@ -0,0 +1,899 @@
+/**
+ * bootstrap-multiselect.js
+ * https://github.com/davidstutz/bootstrap-multiselect
+ *
+ * Copyright 2012, 2013 David Stutz
+ *
+ * Dual licensed under the BSD-3-Clause and the Apache License, Version 2.0.
+ */
+!function($) {
+
+    "use strict";// jshint ;_;
+
+    if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
+        ko.bindingHandlers.multiselect = {
+            init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {},
+            update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+
+               var config = ko.utils.unwrapObservable(valueAccessor());
+               var selectOptions = allBindingsAccessor().options;
+               var ms = $(element).data('multiselect');
+
+               if (!ms) {
+                  $(element).multiselect(config);
+               }
+               else {
+                  ms.updateOriginalOptions();
+                  if (selectOptions && selectOptions().length !== ms.originalOptions.length) {
+                     $(element).multiselect('rebuild');
+                  }
+               }
+            }
+        };
+    }
+
+    /**
+     * Constructor to create a new multiselect using the given select.
+     * 
+     * @param {jQuery} select
+     * @param {Object} options
+     * @returns {Multiselect}
+     */
+    function Multiselect(select, options) {
+
+        this.options = this.mergeOptions(options);
+        this.$select = $(select);
+
+        // Initialization.
+        // We have to clone to create a new reference.
+        this.originalOptions = this.$select.clone()[0].options;
+        this.query = '';
+        this.searchTimeout = null;
+
+        this.options.multiple = this.$select.attr('multiple') === "multiple";
+        this.options.onChange = $.proxy(this.options.onChange, this);
+        this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this);
+        this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this);
+
+        // Build select all if enabled.
+        this.buildContainer();
+        this.buildButton();
+        this.buildSelectAll();
+        this.buildDropdown();
+        this.buildDropdownOptions();
+        this.buildFilter();
+        
+        this.updateButtonText();
+        this.updateSelectAll();
+        
+        this.$select.hide().after(this.$container);
+    };
+
+    Multiselect.prototype = {
+
+        defaults: {
+            /**
+             * Default text function will either print 'None selected' in case no
+             * option is selected or a list of the selected options up to a length of 3 selected options.
+             * 
+             * @param {jQuery} options
+             * @param {jQuery} select
+             * @returns {String}
+             */
+            buttonText: function(options, select) {
+                if (options.length === 0) {
+                    return this.nonSelectedText + ' <b class="caret"></b>';
+                }
+                else {
+                    if (options.length > this.numberDisplayed) {
+                        return options.length + ' ' + this.nSelectedText + ' <b class="caret"></b>';
+                    }
+                    else {
+                        var selected = '';
+                        options.each(function() {
+                            var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).html();
+
+                            selected += label + ', ';
+                        });
+                        return selected.substr(0, selected.length - 2) + ' <b class="caret"></b>';
+                    }
+                }
+            },
+            /**
+             * Updates the title of the button similar to the buttonText function.
+             * @param {jQuery} options
+             * @param {jQuery} select
+             * @returns {@exp;selected@call;substr}
+             */
+            buttonTitle: function(options, select) {
+                if (options.length === 0) {
+                    return this.nonSelectedText;
+                }
+                else {
+                    var selected = '';
+                    options.each(function () {
+                        selected += $(this).text() + ', ';
+                    });
+                    return selected.substr(0, selected.length - 2);
+                }
+            },
+            /**
+             * Create a label.
+             * 
+             * @param {jQuery} element
+             * @returns {String}
+             */
+            label: function(element){
+                return $(element).attr('label') || $(element).html();
+            },
+            /**
+             * Triggered on change of the multiselect.
+             * Not triggered when selecting/deselecting options manually.
+             * 
+             * @param {jQuery} option
+             * @param {Boolean} checked
+             */
+            onChange : function(option, checked) {
+
+            },
+            /**
+             * Triggered when the dropdown is shown.
+             * 
+             * @param {jQuery} event
+             */
+            onDropdownShow: function(event) {
+
+            },
+            /**
+             * Triggered when the dropdown is hidden.
+             * 
+             * @param {jQuery} event
+             */
+            onDropdownHide: function(event) {
+
+            },
+            buttonClass: 'btn btn-default',
+            dropRight: false,
+            selectedClass: 'active',
+            buttonWidth: 'auto',
+            buttonContainer: '<div class="btn-group" />',
+            // Maximum height of the dropdown menu.
+            // If maximum height is exceeded a scrollbar will be displayed.
+            maxHeight: false,
+            includeSelectAllOption: false,
+            selectAllText: ' Select all',
+            selectAllValue: 'multiselect-all',
+            enableFiltering: false,
+            enableCaseInsensitiveFiltering: false,
+            filterPlaceholder: 'Search',
+            // possible options: 'text', 'value', 'both'
+            filterBehavior: 'text',
+            preventInputChangeEvent: false,
+            nonSelectedText: 'None selected',
+            nSelectedText: 'selected',
+            numberDisplayed: 3
+        },
+
+        templates: {
+            button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown"></button>',
+            ul: '<ul class="multiselect-container dropdown-menu"></ul>',
+            filter: '<div class="input-group"><span class="input-group-addon"><i class="glyphicon glyphicon-search"></i></span><input class="form-control multiselect-search" type="text"></div>',
+            li: '<li><a href="javascript:void(0);"><label></label></a></li>',
+            divider: '<li class="divider"></li>',
+            liGroup: '<li><label class="multiselect-group"></label></li>'
+        },
+
+        constructor: Multiselect,
+
+        /**
+         * Builds the container of the multiselect.
+         */
+        buildContainer: function() {
+            this.$container = $(this.options.buttonContainer);
+            this.$container.on('show.bs.dropdown', this.options.onDropdownShow);
+            this.$container.on('hide.bs.dropdown', this.options.onDropdownHide);
+        },
+
+        /**
+         * Builds the button of the multiselect.
+         */
+        buildButton: function() {
+            this.$button = $(this.templates.button).addClass(this.options.buttonClass);
+
+            // Adopt active state.
+            if (this.$select.prop('disabled')) {
+                this.disable();
+            }
+            else {
+                this.enable();
+            }
+
+            // Manually add button width if set.
+            if (this.options.buttonWidth && this.options.buttonWidth != 'auto') {
+                this.$button.css({
+                    'width' : this.options.buttonWidth
+                });
+            }
+
+            // Keep the tab index from the select.
+            var tabindex = this.$select.attr('tabindex');
+            if (tabindex) {
+                this.$button.attr('tabindex', tabindex);
+            }
+
+            this.$container.prepend(this.$button);
+        },
+
+        /**
+         * Builds the ul representing the dropdown menu.
+         */
+        buildDropdown: function() {
+
+            // Build ul.
+            this.$ul = $(this.templates.ul);
+
+            if (this.options.dropRight) {
+                this.$ul.addClass('pull-right');
+            }
+
+            // Set max height of dropdown menu to activate auto scrollbar.
+            if (this.options.maxHeight) {
+                // TODO: Add a class for this option to move the css declarations.
+                this.$ul.css({
+                    'max-height': this.options.maxHeight + 'px',
+                    'overflow-y': 'auto',
+                    'overflow-x': 'hidden'
+                });
+            }
+
+            this.$container.append(this.$ul);
+        },
+
+        /**
+         * Build the dropdown options and binds all nessecary events.
+         * Uses createDivider and createOptionValue to create the necessary options.
+         */
+        buildDropdownOptions: function() {
+
+            this.$select.children().each($.proxy(function(index, element) {
+                
+                // Support optgroups and options without a group simultaneously.
+                var tag = $(element).prop('tagName')
+                    .toLowerCase();
+
+                if (tag === 'optgroup') {
+                    this.createOptgroup(element);
+                }
+                else if (tag === 'option') {
+
+                    if ($(element).data('role') === 'divider') {
+                        this.createDivider();
+                    }
+                    else {
+                        this.createOptionValue(element);
+                    }
+
+                }
+                
+                // Other illegal tags will be ignored.
+            }, this));
+
+            // Bind the change event on the dropdown elements.
+            $('li input', this.$ul).on('change', $.proxy(function(event) {
+                var checked = $(event.target).prop('checked') || false;
+                var isSelectAllOption = $(event.target).val() === this.options.selectAllValue;
+
+                // Apply or unapply the configured selected class.
+                if (this.options.selectedClass) {
+                    if (checked) {
+                        $(event.target).parents('li')
+                            .addClass(this.options.selectedClass);
+                    }
+                    else {
+                        $(event.target).parents('li')
+                            .removeClass(this.options.selectedClass);
+                    }
+                }
+
+                // Get the corresponding option.
+                var value = $(event.target).val();
+                var $option = this.getOptionByValue(value);
+
+                var $optionsNotThis = $('option', this.$select).not($option);
+                var $checkboxesNotThis = $('input', this.$container).not($(event.target));
+
+                if (isSelectAllOption) {
+                    if (this.$select[0][0].value === this.options.selectAllValue) {
+                        var values = [];
+                        var options = $('option[value!="' + this.options.selectAllValue + '"]', this.$select);
+                        for (var i = 0; i < options.length; i++) {
+                            // Additionally check whether the option is visible within the dropcown.
+                            if (options[i].value !== this.options.selectAllValue && this.getInputByValue(options[i].value).is(':visible')) {
+                                values.push(options[i].value);
+                            }
+                        }
+
+                        if (checked) {
+                            this.select(values);
+                        }
+                        else {
+                            this.deselect(values);
+                        }
+                    }
+                }
+
+                if (checked) {
+                    $option.prop('selected', true);
+
+                    if (this.options.multiple) {
+                        // Simply select additional option.
+                        $option.prop('selected', true);
+                    }
+                    else {
+                        // Unselect all other options and corresponding checkboxes.
+                        if (this.options.selectedClass) {
+                            $($checkboxesNotThis).parents('li').removeClass(this.options.selectedClass);
+                        }
+
+                        $($checkboxesNotThis).prop('checked', false);
+                        $optionsNotThis.prop('selected', false);
+
+                        // It's a single selection, so close.
+                        this.$button.click();
+                    }
+
+                    if (this.options.selectedClass === "active") {
+                        $optionsNotThis.parents("a").css("outline", "");
+                    }
+                }
+                else {
+                    // Unselect option.
+                    $option.prop('selected', false);
+                }
+
+                this.$select.change();
+                this.options.onChange($option, checked);
+                
+                this.updateButtonText();
+                this.updateSelectAll();
+
+                if(this.options.preventInputChangeEvent) {
+                    return false;
+                }
+            }, this));
+
+            $('li a', this.$ul).on('touchstart click', function(event) {
+                event.stopPropagation();
+
+                if (event.shiftKey) {
+                    var checked = $(event.target).prop('checked') || false;
+
+                    if (checked) {
+                        var prev = $(event.target).parents('li:last')
+                            .siblings('li[class="active"]:first');
+
+                        var currentIdx = $(event.target).parents('li')
+                            .index();
+                        var prevIdx = prev.index();
+
+                        if (currentIdx > prevIdx) {
+                            $(event.target).parents("li:last").prevUntil(prev).each(
+                                function() {
+                                    $(this).find("input:first").prop("checked", true)
+                                        .trigger("change");
+                                }
+                            );
+                        }
+                        else {
+                            $(event.target).parents("li:last").nextUntil(prev).each(
+                                function() {
+                                    $(this).find("input:first").prop("checked", true)
+                                        .trigger("change");
+                                }
+                            );
+                        }
+                    }
+                }
+
+                $(event.target).blur();
+            });
+
+            // Keyboard support.
+            this.$container.on('keydown', $.proxy(function(event) {
+                if ($('input[type="text"]', this.$container).is(':focus')) {
+                    return;
+                }
+                if ((event.keyCode === 9 || event.keyCode === 27)
+                        && this.$container.hasClass('open')) {
+                    
+                    // Close on tab or escape.
+                    this.$button.click();
+                }
+                else {
+                    var $items = $(this.$container).find("li:not(.divider):visible a");
+
+                    if (!$items.length) {
+                        return;
+                    }
+
+                    var index = $items.index($items.filter(':focus'));
+
+                    // Navigation up.
+                    if (event.keyCode === 38 && index > 0) {
+                        index--;
+                    }
+                    // Navigate down.
+                    else if (event.keyCode === 40 && index < $items.length - 1) {
+                        index++;
+                    }
+                    else if (!~index) {
+                        index = 0;
+                    }
+
+                    var $current = $items.eq(index);
+                    $current.focus();
+
+                    if (event.keyCode === 32 || event.keyCode === 13) {
+                        var $checkbox = $current.find('input');
+
+                        $checkbox.prop("checked", !$checkbox.prop("checked"));
+                        $checkbox.change();
+                    }
+
+                    event.stopPropagation();
+                    event.preventDefault();
+                }
+            }, this));
+        },
+
+        /**
+         * Create an option using the given select option.
+         * 
+         * @param {jQuery} element
+         */
+        createOptionValue: function(element) {
+            if ($(element).is(':selected')) {
+                $(element).prop('selected', true);
+            }
+
+            // Support the label attribute on options.
+            var label = this.options.label(element);
+            var value = $(element).val();
+            var inputType = this.options.multiple ? "checkbox" : "radio";
+
+            var $li = $(this.templates.li);
+            $('label', $li).addClass(inputType);
+            $('label', $li).append('<input type="' + inputType + '" />');
+
+            var selected = $(element).prop('selected') || false;
+            var $checkbox = $('input', $li);
+            $checkbox.val(value);
+
+            if (value === this.options.selectAllValue) {
+                $checkbox.parent().parent()
+                    .addClass('multiselect-all');
+            }
+
+            $('label', $li).append(" " + label);
+
+            this.$ul.append($li);
+
+            if ($(element).is(':disabled')) {
+                $checkbox.attr('disabled', 'disabled')
+                    .prop('disabled', true)
+                    .parents('li')
+                    .addClass('disabled');
+            }
+
+            $checkbox.prop('checked', selected);
+
+            if (selected && this.options.selectedClass) {
+                $checkbox.parents('li')
+                    .addClass(this.options.selectedClass);
+            }
+        },
+
+        /**
+         * Creates a divider using the given select option.
+         * 
+         * @param {jQuery} element
+         */
+        createDivider: function(element) {
+            var $divider = $(this.templates.divider);
+            this.$ul.append($divider);
+        },
+
+        /**
+         * Creates an optgroup.
+         * 
+         * @param {jQuery} group
+         */
+        createOptgroup: function(group) {
+            var groupName = $(group).prop('label');
+
+            // Add a header for the group.
+            var $li = $(this.templates.liGroup);
+            $('label', $li).text(groupName);
+
+            this.$ul.append($li);
+
+            // Add the options of the group.
+            $('option', group).each($.proxy(function(index, element) {
+                this.createOptionValue(element);
+            }, this));
+        },
+
+        /**
+         * Build the selct all.
+         * Checks if a select all ahs already been created.
+         */
+        buildSelectAll: function() {
+            var alreadyHasSelectAll = this.hasSelectAll();
+            
+            // If options.includeSelectAllOption === true, add the include all checkbox.
+            if (this.options.includeSelectAllOption && this.options.multiple && !alreadyHasSelectAll) {
+                this.$select.prepend('<option value="' + this.options.selectAllValue + '">' + this.options.selectAllText + '</option>');
+            }
+        },
+
+        /**
+         * Builds the filter.
+         */
+        buildFilter: function() {
+
+            // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength.
+            if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
+                var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering);
+
+                if (this.$select.find('option').length >= enableFilterLength) {
+
+                    this.$filter = $(this.templates.filter);
+                    $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder);
+                    this.$ul.prepend(this.$filter);
+
+                    this.$filter.val(this.query).on('click', function(event) {
+                        event.stopPropagation();
+                    }).on('input keydown', $.proxy(function(event) {
+                        // This is useful to catch "keydown" events after the browser has updated the control.
+                        clearTimeout(this.searchTimeout);
+
+                        this.searchTimeout = this.asyncFunction($.proxy(function() {
+
+                            if (this.query !== event.target.value) {
+                                this.query = event.target.value;
+
+                                $.each($('li', this.$ul), $.proxy(function(index, element) {
+                                    var value = $('input', element).val();
+                                    var text = $('label', element).text();
+
+                                    if (value !== this.options.selectAllValue && text) {
+                                        // by default lets assume that element is not
+                                        // interesting for this search
+                                        var showElement = false;
+
+                                        var filterCandidate = '';
+                                        if ((this.options.filterBehavior === 'text' || this.options.filterBehavior === 'both')) {
+                                            filterCandidate = text;
+                                        }
+                                        if ((this.options.filterBehavior === 'value' || this.options.filterBehavior === 'both')) {
+                                            filterCandidate = value;
+                                        }
+
+                                        if (this.options.enableCaseInsensitiveFiltering && filterCandidate.toLowerCase().indexOf(this.query.toLowerCase()) > -1) {
+                                            showElement = true;
+                                        }
+                                        else if (filterCandidate.indexOf(this.query) > -1) {
+                                            showElement = true;
+                                        }
+
+                                        if (showElement) {
+                                            $(element).show();
+                                        }
+                                        else {
+                                            $(element).hide();
+                                        }
+                                    }
+                                }, this));
+                            }
+
+                            // TODO: check whether select all option needs to be updated.
+                        }, this), 300, this);
+                    }, this));
+                }
+            }
+        },
+
+        /**
+         * Unbinds the whole plugin.
+         */
+        destroy: function() {
+            this.$container.remove();
+            this.$select.show();
+        },
+
+        /**
+         * Refreshs the multiselect based on the selected options of the select.
+         */
+        refresh: function() {
+            $('option', this.$select).each($.proxy(function(index, element) {
+                var $input = $('li input', this.$ul).filter(function() {
+                    return $(this).val() === $(element).val();
+                });
+
+                if ($(element).is(':selected')) {
+                    $input.prop('checked', true);
+
+                    if (this.options.selectedClass) {
+                        $input.parents('li')
+                            .addClass(this.options.selectedClass);
+                    }
+                }
+                else {
+                    $input.prop('checked', false);
+
+                    if (this.options.selectedClass) {
+                        $input.parents('li')
+                            .removeClass(this.options.selectedClass);
+                    }
+                }
+
+                if ($(element).is(":disabled")) {
+                    $input.attr('disabled', 'disabled')
+                        .prop('disabled', true)
+                        .parents('li')
+                        .addClass('disabled');
+                }
+                else {
+                    $input.prop('disabled', false)
+                        .parents('li')
+                        .removeClass('disabled');
+                }
+            }, this));
+
+            this.updateButtonText();
+            this.updateSelectAll();
+        },
+
+        /**
+         * Select all options of the given values.
+         * 
+         * @param {Array} selectValues
+         */
+        select: function(selectValues) {
+            if(selectValues && !$.isArray(selectValues)) {
+                selectValues = [selectValues];
+            }
+
+            for (var i = 0; i < selectValues.length; i++) {
+                var value = selectValues[i];
+
+                var $option = this.getOptionByValue(value);
+                var $checkbox = this.getInputByValue(value);
+
+                if (this.options.selectedClass) {
+                    $checkbox.parents('li')
+                        .addClass(this.options.selectedClass);
+                }
+
+                $checkbox.prop('checked', true);
+                $option.prop('selected', true);
+            }
+
+            this.updateButtonText();
+        },
+
+        /**
+         * Deselects all options of the given values.
+         * 
+         * @param {Array} deselectValues
+         */
+        deselect: function(deselectValues) {
+            if(deselectValues && !$.isArray(deselectValues)) {
+                deselectValues = [deselectValues];
+            }
+
+            for (var i = 0; i < deselectValues.length; i++) {
+
+                var value = deselectValues[i];
+
+                var $option = this.getOptionByValue(value);
+                var $checkbox = this.getInputByValue(value);
+
+                if (this.options.selectedClass) {
+                    $checkbox.parents('li')
+                        .removeClass(this.options.selectedClass);
+                }
+
+                $checkbox.prop('checked', false);
+                $option.prop('selected', false);
+            }
+
+            this.updateButtonText();
+        },
+
+        /**
+         * Rebuild the plugin.
+         * Rebuilds the dropdown, the filter and the select all option.
+         */
+        rebuild: function() {
+            this.$ul.html('');
+
+            // Remove select all option in select.
+            $('option[value="' + this.options.selectAllValue + '"]', this.$select).remove();
+
+            // Important to distinguish between radios and checkboxes.
+            this.options.multiple = this.$select.attr('multiple') === "multiple";
+
+            this.buildSelectAll();
+            this.buildDropdownOptions();
+            this.buildFilter();
+            
+            this.updateButtonText();
+            this.updateSelectAll();
+        },
+
+        /**
+         * The provided data will be used to build the dropdown.
+         * 
+         * @param {Array} dataprovider
+         */
+        dataprovider: function(dataprovider) {
+            var optionDOM = "";
+            dataprovider.forEach(function (option) {
+                optionDOM += '<option value="' + option.value + '">' + option.label + '</option>';
+            });
+
+            this.$select.html(optionDOM);
+            this.rebuild();
+        },
+
+        /**
+         * Enable the multiselect.
+         */
+        enable: function() {
+            this.$select.prop('disabled', false);
+            this.$button.prop('disabled', false)
+                .removeClass('disabled');
+        },
+
+        /**
+         * Disable the multiselect.
+         */
+        disable: function() {
+            this.$select.prop('disabled', true);
+            this.$button.prop('disabled', true)
+                .addClass('disabled');
+        },
+
+        /**
+         * Set the options.
+         * 
+         * @param {Array} options
+         */
+        setOptions: function(options) {
+            this.options = this.mergeOptions(options);
+        },
+
+        /**
+         * Merges the given options with the default options.
+         * 
+         * @param {Array} options
+         * @returns {Array}
+         */
+        mergeOptions: function(options) {
+            return $.extend({}, this.defaults, options);
+        },
+        
+        /**
+         * Checks whether a select all option is present.
+         * 
+         * @returns {Boolean}
+         */
+        hasSelectAll: function() {
+            return this.$select[0][0] ? this.$select[0][0].value === this.options.selectAllValue : false;
+        },
+        
+        /**
+         * Updates the select all option based on the currently selected options.
+         */
+        updateSelectAll: function() {
+            if (this.hasSelectAll()) {
+                var selected = this.getSelected();
+                
+                if (selected.length === $('option', this.$select).length - 1) {
+                    this.select(this.options.selectAllValue);
+                }
+                else {
+                    this.deselect(this.options.selectAllValue);
+                }
+            }
+        },
+        
+        /**
+         * Update the button text and its title base don the currenty selected options.
+         */
+        updateButtonText: function() {
+            var options = this.getSelected();
+            
+            // First update the displayed button text.
+            $('button', this.$container).html(this.options.buttonText(options, this.$select));
+            
+            // Now update the title attribute of the button.
+            $('button', this.$container).attr('title', this.options.buttonTitle(options, this.$select));
+
+        },
+
+        /**
+         * Get all selected options.
+         * 
+         * @returns {jQUery}
+         */
+        getSelected: function() {
+            return $('option[value!="' + this.options.selectAllValue + '"]:selected', this.$select).filter(function() {
+                return $(this).prop('selected');
+            });
+        },
+
+        /**
+         * Gets a select option by its value.
+         * 
+         * @param {String} value
+         * @returns {jQuery}
+         */
+        getOptionByValue: function(value) {
+            return $('option', this.$select).filter(function() {
+                return $(this).val() === value;
+            });
+        },
+
+        /**
+         * Get the input (radio/checkbox) by its value.
+         * 
+         * @param {String} value
+         * @returns {jQuery}
+         */
+        getInputByValue: function(value) {
+            return $('li input', this.$ul).filter(function() {
+                return $(this).val() === value;
+            });
+        },
+
+        /**
+         * Used for knockout integration.
+         */
+        updateOriginalOptions: function() {
+            this.originalOptions = this.$select.clone()[0].options;
+        },
+
+        asyncFunction: function(callback, timeout, self) {
+            var args = Array.prototype.slice.call(arguments, 3);
+            return setTimeout(function() {
+                callback.apply(self || window, args);
+            }, timeout);
+        }
+    };
+
+    $.fn.multiselect = function(option, parameter) {
+        return this.each(function() {
+            var data = $(this).data('multiselect');
+            var options = typeof option === 'object' && option;
+
+            // Initialize the multiselect.
+            if (!data) {
+                $(this).data('multiselect', ( data = new Multiselect(this, options)));
+            }
+
+            // Call multiselect method.
+            if (typeof option === 'string') {
+                data[option](parameter);
+            }
+        });
+    };
+
+    $.fn.multiselect.Constructor = Multiselect;
+
+    $(function() {
+        $("select[data-role=multiselect]").multiselect();
+    });
+
+}(window.jQuery);
diff --git a/static/libjs/bubble.js b/static/libjs/bubble.js
new file mode 100644
index 0000000000000000000000000000000000000000..d0c0e24954ad63b2ecba5d80f6dd66142f24fd40
--- /dev/null
+++ b/static/libjs/bubble.js
@@ -0,0 +1,100 @@
+/* -*- mode: javascript -*-
+ *                 JavaScript for Help Bubbles (aka tooltips) 
+ */
+
+function enableTooltips(id){
+var links,i,h;
+if(!document.getElementById || !document.getElementsByTagName) return;
+// AddCss();
+h=document.createElement("span");
+h.id="btc";
+h.setAttribute("id","btc");
+h.style.position="absolute";
+document.getElementsByTagName("body")[0].appendChild(h);
+if(id==null) links=document.getElementsByTagName("a");
+else links=document.getElementById(id).getElementsByTagName("a");
+for(i=0;i<links.length;i++){
+    Prepare(links[i]);
+    }
+}
+
+function Prepare(el){
+  var tooltip,t,b,s,l;
+  t=el.getAttribute("title");
+  if(t==null || t.length==0) 
+    return; /* rien si pas de title. Was: t="link:"; */
+  el.removeAttribute("title");
+  tooltip=CreateEl("span","tooltip");
+  s=CreateEl("span","top");
+  s.appendChild(document.createTextNode(t));
+  tooltip.appendChild(s);
+
+  b=CreateEl("b","bottom");
+  /* url du lien:
+  l=el.getAttribute("href");
+  if(l.length>30) 
+    l=l.substr(0,27)+"...";
+  b.appendChild(document.createTextNode(l));
+  */
+  tooltip.appendChild(b);
+  setOpacity(tooltip);
+  el.tooltip=tooltip;
+  el.onmouseover=showTooltip;
+  el.onmouseout=hideTooltip;
+  el.onmousemove=Locate;
+}
+
+function showTooltip(e){
+  document.getElementById("btc").appendChild(this.tooltip);
+  Locate(e);
+}
+
+function hideTooltip(e){
+  var d=document.getElementById("btc");
+  if(d.childNodes.length>0) 
+    d.removeChild(d.firstChild);
+}
+
+function setOpacity(el){
+  el.style.filter="alpha(opacity:95)";
+  el.style.KHTMLOpacity="0.95";
+  el.style.MozOpacity="0.95";
+  el.style.opacity="0.95";
+}
+
+function CreateEl(t,c){
+  var x=document.createElement(t);
+  x.className=c;
+  x.style.display="block";
+  return(x);
+}
+
+function AddCss(){
+  var l=CreateEl("link");
+  l.setAttribute("type","text/css");
+  l.setAttribute("rel","stylesheet");
+  l.setAttribute("href","bubble.js");
+  l.setAttribute("media","screen");
+  document.getElementsByTagName("head")[0].appendChild(l);
+}
+
+function Locate(e){
+  var posx=0,posy=0;
+  if(e==null) 
+    e=window.event;
+  if(e.pageX || e.pageY){
+    posx=e.pageX; posy=e.pageY;
+  }
+  else if(e.clientX || e.clientY){
+    if(document.documentElement.scrollTop){
+        posx=e.clientX+document.documentElement.scrollLeft;
+        posy=e.clientY+document.documentElement.scrollTop;
+    }
+    else{
+      posx=e.clientX+document.body.scrollLeft;
+      posy=e.clientY+document.body.scrollTop;
+    }
+  }
+  document.getElementById("btc").style.top=(posy+10)+"px";
+  document.getElementById("btc").style.left=(posx-20)+"px";
+}
diff --git a/static/libjs/d3.v3.min.js b/static/libjs/d3.v3.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..8cfc9ef3f4917b76800f6783d33197cd8d618f33
--- /dev/null
+++ b/static/libjs/d3.v3.min.js
@@ -0,0 +1,5 @@
+!function(){function n(n){return null!=n&&!isNaN(n)}function t(n){return n.length}function e(n){for(var t=1;n*t%1;)t*=10;return t}function r(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function u(){}function i(n){return aa+n in this}function o(n){return n=aa+n,n in this&&delete this[n]}function a(){var n=[];return this.forEach(function(t){n.push(t)}),n}function c(){var n=0;for(var t in this)t.charCodeAt(0)===ca&&++n;return n}function s(){for(var n in this)if(n.charCodeAt(0)===ca)return!1;return!0}function l(){}function f(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function h(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=sa.length;r>e;++e){var u=sa[e]+t;if(u in n)return u}}function g(){}function p(){}function v(n){function t(){for(var t,r=e,u=-1,i=r.length;++u<i;)(t=r[u].on)&&t.apply(this,arguments);return n}var e=[],r=new u;return t.on=function(t,u){var i,o=r.get(t);return arguments.length<2?o&&o.on:(o&&(o.on=null,e=e.slice(0,i=e.indexOf(o)).concat(e.slice(i+1)),r.remove(t)),u&&e.push(r.set(t,{on:u})),n)},t}function d(){Xo.event.preventDefault()}function m(){for(var n,t=Xo.event;n=t.sourceEvent;)t=n;return t}function y(n){for(var t=new p,e=0,r=arguments.length;++e<r;)t[arguments[e]]=v(t);return t.of=function(e,r){return function(u){try{var i=u.sourceEvent=Xo.event;u.target=n,Xo.event=u,t[u.type].apply(e,r)}finally{Xo.event=i}}},t}function x(n){return fa(n,da),n}function M(n){return"function"==typeof n?n:function(){return ha(n,this)}}function _(n){return"function"==typeof n?n:function(){return ga(n,this)}}function b(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function u(){this.setAttribute(n,t)}function i(){this.setAttributeNS(n.space,n.local,t)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=Xo.ns.qualify(n),null==t?n.local?r:e:"function"==typeof t?n.local?a:o:n.local?i:u}function w(n){return n.trim().replace(/\s+/g," ")}function S(n){return new RegExp("(?:^|\\s+)"+Xo.requote(n)+"(?:\\s+|$)","g")}function k(n){return n.trim().split(/^|\s+/)}function E(n,t){function e(){for(var e=-1;++e<u;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<u;)n[e](this,r)}n=k(n).map(A);var u=n.length;return"function"==typeof t?r:e}function A(n){var t=S(n);return function(e,r){if(u=e.classList)return r?u.add(n):u.remove(n);var u=e.getAttribute("class")||"";r?(t.lastIndex=0,t.test(u)||e.setAttribute("class",w(u+" "+n))):e.setAttribute("class",w(u.replace(t," ")))}}function C(n,t,e){function r(){this.style.removeProperty(n)}function u(){this.style.setProperty(n,t,e)}function i(){var r=t.apply(this,arguments);null==r?this.style.removeProperty(n):this.style.setProperty(n,r,e)}return null==t?r:"function"==typeof t?i:u}function N(n,t){function e(){delete this[n]}function r(){this[n]=t}function u(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:"function"==typeof t?u:r}function L(n){return"function"==typeof n?n:(n=Xo.ns.qualify(n)).local?function(){return this.ownerDocument.createElementNS(n.space,n.local)}:function(){return this.ownerDocument.createElementNS(this.namespaceURI,n)}}function z(n){return{__data__:n}}function q(n){return function(){return va(this,n)}}function T(n){return arguments.length||(n=Xo.ascending),function(t,e){return t&&e?n(t.__data__,e.__data__):!t-!e}}function R(n,t){for(var e=0,r=n.length;r>e;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function D(n){return fa(n,ya),n}function P(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t<c;);return o}}function U(){var n=this.__transition__;n&&++n.active}function j(n,t,e){function r(){var t=this[o];t&&(this.removeEventListener(n,t,t.$),delete this[o])}function u(){var u=c(t,Bo(arguments));r.call(this),this.addEventListener(n,this[o]=u,u.$=e),u._=t}function i(){var t,e=new RegExp("^__on([^.]+)"+Xo.requote(n)+"$");for(var r in this)if(t=r.match(e)){var u=this[r];this.removeEventListener(t[1],u,u.$),delete this[r]}}var o="__on"+n,a=n.indexOf("."),c=H;a>0&&(n=n.substring(0,a));var s=Ma.get(n);return s&&(n=s,c=F),a?t?u:r:t?g:i}function H(n,t){return function(e){var r=Xo.event;Xo.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{Xo.event=r}}}function F(n,t){var e=H(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function O(){var n=".dragsuppress-"+ ++ba,t="click"+n,e=Xo.select(Go).on("touchmove"+n,d).on("dragstart"+n,d).on("selectstart"+n,d);if(_a){var r=Jo.style,u=r[_a];r[_a]="none"}return function(i){function o(){e.on(t,null)}e.on(n,null),_a&&(r[_a]=u),i&&(e.on(t,function(){d(),o()},!0),setTimeout(o,0))}}function Y(n,t){t.changedTouches&&(t=t.changedTouches[0]);var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();if(0>wa&&(Go.scrollX||Go.scrollY)){e=Xo.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var u=e[0][0].getScreenCTM();wa=!(u.f||u.e),e.remove()}return wa?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var i=n.getBoundingClientRect();return[t.clientX-i.left-n.clientLeft,t.clientY-i.top-n.clientTop]}function I(n){return n>0?1:0>n?-1:0}function Z(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function V(n){return n>1?0:-1>n?Sa:Math.acos(n)}function X(n){return n>1?Ea:-1>n?-Ea:Math.asin(n)}function $(n){return((n=Math.exp(n))-1/n)/2}function B(n){return((n=Math.exp(n))+1/n)/2}function W(n){return((n=Math.exp(2*n))-1)/(n+1)}function J(n){return(n=Math.sin(n/2))*n}function G(){}function K(n,t,e){return new Q(n,t,e)}function Q(n,t,e){this.h=n,this.s=t,this.l=e}function nt(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(o-i)*n/60:180>n?o:240>n?i+(o-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,i=2*e-o,gt(u(n+120),u(n),u(n-120))}function tt(n,t,e){return new et(n,t,e)}function et(n,t,e){this.h=n,this.c=t,this.l=e}function rt(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),ut(e,Math.cos(n*=Na)*t,Math.sin(n)*t)}function ut(n,t,e){return new it(n,t,e)}function it(n,t,e){this.l=n,this.a=t,this.b=e}function ot(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=ct(u)*Fa,r=ct(r)*Oa,i=ct(i)*Ya,gt(lt(3.2404542*u-1.5371385*r-.4985314*i),lt(-.969266*u+1.8760108*r+.041556*i),lt(.0556434*u-.2040259*r+1.0572252*i))}function at(n,t,e){return n>0?tt(Math.atan2(e,t)*La,Math.sqrt(t*t+e*e),n):tt(0/0,0/0,n)}function ct(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function st(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function lt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function ft(n){return gt(n>>16,255&n>>8,255&n)}function ht(n){return ft(n)+""}function gt(n,t,e){return new pt(n,t,e)}function pt(n,t,e){this.r=n,this.g=t,this.b=e}function vt(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function dt(n,t,e){var r,u,i,o=0,a=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(Mt(u[0]),Mt(u[1]),Mt(u[2]))}return(i=Va.get(n))?t(i.r,i.g,i.b):(null!=n&&"#"===n.charAt(0)&&(4===n.length?(o=n.charAt(1),o+=o,a=n.charAt(2),a+=a,c=n.charAt(3),c+=c):7===n.length&&(o=n.substring(1,3),a=n.substring(3,5),c=n.substring(5,7)),o=parseInt(o,16),a=parseInt(a,16),c=parseInt(c,16)),t(o,a,c))}function mt(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-i,c=(o+i)/2;return a?(u=.5>c?a/(o+i):a/(2-o-i),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=0/0,u=c>0&&1>c?0:r),K(r,u,c)}function yt(n,t,e){n=xt(n),t=xt(t),e=xt(e);var r=st((.4124564*n+.3575761*t+.1804375*e)/Fa),u=st((.2126729*n+.7151522*t+.072175*e)/Oa),i=st((.0193339*n+.119192*t+.9503041*e)/Ya);return ut(116*u-16,500*(r-u),200*(u-i))}function xt(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Mt(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function _t(n){return"function"==typeof n?n:function(){return n}}function bt(n){return n}function wt(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),St(t,e,n,r)}}function St(n,t,e,r){function u(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(i,c)}catch(r){return o.error.call(i,r),void 0}o.load.call(i,n)}else o.error.call(i,c)}var i={},o=Xo.dispatch("beforesend","progress","load","error"),a={},c=new XMLHttpRequest,s=null;return!Go.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"onload"in c?c.onload=c.onerror=u:c.onreadystatechange=function(){c.readyState>3&&u()},c.onprogress=function(n){var t=Xo.event;Xo.event=n;try{o.progress.call(i,c)}finally{Xo.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(s=n,i):s},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(Bo(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),c.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),c.setRequestHeader)for(var l in a)c.setRequestHeader(l,a[l]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=s&&(c.responseType=s),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),o.beforesend.call(i,c),c.send(null==r?null:r),i},i.abort=function(){return c.abort(),i},Xo.rebind(i,o,"on"),null==r?i:i.get(kt(r))}function kt(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Et(){var n=At(),t=Ct()-n;t>24?(isFinite(t)&&(clearTimeout(Wa),Wa=setTimeout(Et,t)),Ba=0):(Ba=1,Ga(Et))}function At(){var n=Date.now();for(Ja=Xa;Ja;)n>=Ja.t&&(Ja.f=Ja.c(n-Ja.t)),Ja=Ja.n;return n}function Ct(){for(var n,t=Xa,e=1/0;t;)t.f?t=n?n.n=t.n:Xa=t.n:(t.t<e&&(e=t.t),t=(n=t).n);return $a=n,e}function Nt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function Lt(n,t){var e=Math.pow(10,3*oa(8-t));return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function zt(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r?function(n){for(var t=n.length,u=[],i=0,o=r[0];t>0&&o>0;)u.push(n.substring(t-=o,t+o)),o=r[i=(i+1)%r.length];return u.reverse().join(e)}:bt;return function(n){var e=Qa.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"",c=e[4]||"",s=e[5],l=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1;switch(h&&(h=+h.substring(1)),(s||"0"===r&&"="===o)&&(s=r="0",o="=",f&&(l-=Math.floor((l-1)/4))),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===c&&(v="0"+g.toLowerCase());case"c":case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===c&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=nc.get(g)||qt;var y=s&&f;return function(n){if(m&&n%1)return"";var e=0>n||0===n&&0>1/n?(n=-n,"-"):a;if(0>p){var u=Xo.formatPrefix(n,h);n=u.scale(n),d=u.symbol}else n*=p;n=g(n,h);var c=n.lastIndexOf("."),x=0>c?n:n.substring(0,c),M=0>c?"":t+n.substring(c+1);!s&&f&&(x=i(x));var _=v.length+x.length+M.length+(y?0:e.length),b=l>_?new Array(_=l-_+1).join(r):"";return y&&(x=i(b+x)),e+=v,n=x+M,("<"===o?e+n+b:">"===o?b+e+n:"^"===o?b.substring(0,_>>=1)+e+n+b.substring(_):e+(y?n:b+n))+d}}}function qt(n){return n+""}function Tt(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function Rt(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new ec(e-1)),1),e}function i(n,e){return t(n=new ec(+n),e),n}function o(n,r,i){var o=u(n),a=[];if(i>1)for(;r>o;)e(o)%i||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{ec=Tt;var r=new Tt;return r._=n,o(r,t,e)}finally{ec=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=o;var c=n.utc=Dt(n);return c.floor=c,c.round=Dt(r),c.ceil=Dt(u),c.offset=Dt(i),c.range=a,n}function Dt(n){return function(t,e){try{ec=Tt;var r=new Tt;return r._=t,n(r,e)._}finally{ec=Date}}}function Pt(n){function t(n){function t(t){for(var e,u,i,o=[],a=-1,c=0;++a<r;)37===n.charCodeAt(a)&&(o.push(n.substring(c,a)),null!=(u=uc[e=n.charAt(++a)])&&(e=n.charAt(++a)),(i=C[e])&&(e=i(t,null==u?"e"===e?" ":"0":u)),o.push(e),c=a+1);return o.push(n.substring(c,a)),o.join("")}var r=n.length;return t.parse=function(t){var r={y:1900,m:0,d:1,H:0,M:0,S:0,L:0,Z:null},u=e(r,n,t,0);if(u!=t.length)return null;"p"in r&&(r.H=r.H%12+12*r.p);var i=null!=r.Z&&ec!==Tt,o=new(i?Tt:ec);return"j"in r?o.setFullYear(r.y,0,r.j):"w"in r&&("W"in r||"U"in r)?(o.setFullYear(r.y,0,1),o.setFullYear(r.y,0,"W"in r?(r.w+6)%7+7*r.W-(o.getDay()+5)%7:r.w+7*r.U-(o.getDay()+6)%7)):o.setFullYear(r.y,r.m,r.d),o.setHours(r.H+Math.floor(r.Z/100),r.M+r.Z%100,r.S,r.L),i?o._:o},t.toString=function(){return n},t}function e(n,t,e,r){for(var u,i,o,a=0,c=t.length,s=e.length;c>a;){if(r>=s)return-1;if(u=t.charCodeAt(a++),37===u){if(o=t.charAt(a++),i=N[o in uc?t.charAt(a++):o],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){b.lastIndex=0;var r=b.exec(t.substring(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){M.lastIndex=0;var r=M.exec(t.substring(e));return r?(n.w=_.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){E.lastIndex=0;var r=E.exec(t.substring(e));return r?(n.m=A.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.substring(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,C.c.toString(),t,r)}function c(n,t,r){return e(n,C.x.toString(),t,r)}function s(n,t,r){return e(n,C.X.toString(),t,r)}function l(n,t,e){var r=x.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{ec=Tt;var t=new ec;return t._=n,r(t)}finally{ec=Date}}var r=t(n);return e.parse=function(n){try{ec=Tt;var t=r.parse(n);return t&&t._}finally{ec=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ee;var x=Xo.map(),M=jt(v),_=Ht(v),b=jt(d),w=Ht(d),S=jt(m),k=Ht(m),E=jt(y),A=Ht(y);p.forEach(function(n,t){x.set(n.toLowerCase(),t)});var C={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return Ut(n.getDate(),t,2)},e:function(n,t){return Ut(n.getDate(),t,2)},H:function(n,t){return Ut(n.getHours(),t,2)},I:function(n,t){return Ut(n.getHours()%12||12,t,2)},j:function(n,t){return Ut(1+tc.dayOfYear(n),t,3)},L:function(n,t){return Ut(n.getMilliseconds(),t,3)},m:function(n,t){return Ut(n.getMonth()+1,t,2)},M:function(n,t){return Ut(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return Ut(n.getSeconds(),t,2)},U:function(n,t){return Ut(tc.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Ut(tc.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return Ut(n.getFullYear()%100,t,2)},Y:function(n,t){return Ut(n.getFullYear()%1e4,t,4)},Z:ne,"%":function(){return"%"}},N={a:r,A:u,b:i,B:o,c:a,d:Bt,e:Bt,H:Jt,I:Jt,j:Wt,L:Qt,m:$t,M:Gt,p:l,S:Kt,U:Ot,w:Ft,W:Yt,x:c,X:s,y:Zt,Y:It,Z:Vt,"%":te};return t}function Ut(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function jt(n){return new RegExp("^(?:"+n.map(Xo.requote).join("|")+")","i")}function Ht(n){for(var t=new u,e=-1,r=n.length;++e<r;)t.set(n[e].toLowerCase(),e);return t}function Ft(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+1));return r?(n.w=+r[0],e+r[0].length):-1}function Ot(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e));return r?(n.U=+r[0],e+r[0].length):-1}function Yt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e));return r?(n.W=+r[0],e+r[0].length):-1}function It(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+4));return r?(n.y=+r[0],e+r[0].length):-1}function Zt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.y=Xt(+r[0]),e+r[0].length):-1}function Vt(n,t,e){return/^[+-]\d{4}$/.test(t=t.substring(e,e+5))?(n.Z=+t,e+5):-1}function Xt(n){return n+(n>68?1900:2e3)}function $t(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Bt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function Wt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function Jt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function Gt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function Kt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function Qt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ne(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(oa(t)/60),u=oa(t)%60;return e+Ut(r,"0",2)+Ut(u,"0",2)}function te(n,t,e){oc.lastIndex=0;var r=oc.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function ee(n){for(var t=n.length,e=-1;++e<t;)n[e][0]=this(n[e][0]);return function(t){for(var e=0,r=n[e];!r[1](t);)r=n[++e];return r[0](t)}}function re(){}function ue(n,t,e){var r=e.s=n+t,u=r-n,i=r-u;e.t=n-i+(t-u)}function ie(n,t){n&&lc.hasOwnProperty(n.type)&&lc[n.type](n,t)}function oe(n,t,e){var r,u=-1,i=n.length-e;for(t.lineStart();++u<i;)r=n[u],t.point(r[0],r[1],r[2]);t.lineEnd()}function ae(n,t){var e=-1,r=n.length;for(t.polygonStart();++e<r;)oe(n[e],t,1);t.polygonEnd()}function ce(){function n(n,t){n*=Na,t=t*Na/2+Sa/4;var e=n-r,o=Math.cos(t),a=Math.sin(t),c=i*a,s=u*o+c*Math.cos(e),l=c*Math.sin(e);hc.add(Math.atan2(l,s)),r=n,u=o,i=a}var t,e,r,u,i;gc.point=function(o,a){gc.point=n,r=(t=o)*Na,u=Math.cos(a=(e=a)*Na/2+Sa/4),i=Math.sin(a)},gc.lineEnd=function(){n(t,e)}}function se(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function le(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function fe(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function he(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function ge(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function pe(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function ve(n){return[Math.atan2(n[1],n[0]),X(n[2])]}function de(n,t){return oa(n[0]-t[0])<Aa&&oa(n[1]-t[1])<Aa}function me(n,t){n*=Na;var e=Math.cos(t*=Na);ye(e*Math.cos(n),e*Math.sin(n),Math.sin(t))}function ye(n,t,e){++pc,dc+=(n-dc)/pc,mc+=(t-mc)/pc,yc+=(e-yc)/pc}function xe(){function n(n,u){n*=Na;var i=Math.cos(u*=Na),o=i*Math.cos(n),a=i*Math.sin(n),c=Math.sin(u),s=Math.atan2(Math.sqrt((s=e*c-r*a)*s+(s=r*o-t*c)*s+(s=t*a-e*o)*s),t*o+e*a+r*c);vc+=s,xc+=s*(t+(t=o)),Mc+=s*(e+(e=a)),_c+=s*(r+(r=c)),ye(t,e,r)}var t,e,r;kc.point=function(u,i){u*=Na;var o=Math.cos(i*=Na);t=o*Math.cos(u),e=o*Math.sin(u),r=Math.sin(i),kc.point=n,ye(t,e,r)}}function Me(){kc.point=me}function _e(){function n(n,t){n*=Na;var e=Math.cos(t*=Na),o=e*Math.cos(n),a=e*Math.sin(n),c=Math.sin(t),s=u*c-i*a,l=i*o-r*c,f=r*a-u*o,h=Math.sqrt(s*s+l*l+f*f),g=r*o+u*a+i*c,p=h&&-V(g)/h,v=Math.atan2(h,g);bc+=p*s,wc+=p*l,Sc+=p*f,vc+=v,xc+=v*(r+(r=o)),Mc+=v*(u+(u=a)),_c+=v*(i+(i=c)),ye(r,u,i)}var t,e,r,u,i;kc.point=function(o,a){t=o,e=a,kc.point=n,o*=Na;var c=Math.cos(a*=Na);r=c*Math.cos(o),u=c*Math.sin(o),i=Math.sin(a),ye(r,u,i)},kc.lineEnd=function(){n(t,e),kc.lineEnd=Me,kc.point=me}}function be(){return!0}function we(n,t,e,r,u){var i=[],o=[];if(n.forEach(function(n){if(!((t=n.length-1)<=0)){var t,e=n[0],r=n[t];if(de(e,r)){u.lineStart();for(var a=0;t>a;++a)u.point((e=n[a])[0],e[1]);return u.lineEnd(),void 0}var c=new ke(e,n,null,!0),s=new ke(e,null,c,!1);c.o=s,i.push(c),o.push(s),c=new ke(r,n,null,!1),s=new ke(r,null,c,!0),c.o=s,i.push(c),o.push(s)}}),o.sort(t),Se(i),Se(o),i.length){for(var a=0,c=e,s=o.length;s>a;++a)o[a].e=c=!c;for(var l,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;l=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var a=0,s=l.length;s>a;++a)u.point((f=l[a])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){l=g.p.z;for(var a=l.length-1;a>=0;--a)u.point((f=l[a])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,l=g.z,p=!p}while(!g.v);u.lineEnd()}}}function Se(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r<t;)u.n=e=n[r],e.p=u,u=e;u.n=e=n[0],e.p=u}}function ke(n,t,e,r){this.x=n,this.z=t,this.o=e,this.e=r,this.v=!1,this.n=this.p=null}function Ee(n,t,e,r){return function(u,i){function o(t,e){var r=u(t,e);n(t=r[0],e=r[1])&&i.point(t,e)}function a(n,t){var e=u(n,t);d.point(e[0],e[1])}function c(){y.point=a,d.lineStart()}function s(){y.point=o,d.lineEnd()}function l(n,t){v.push([n,t]);var e=u(n,t);M.point(e[0],e[1])}function f(){M.lineStart(),v=[]}function h(){l(v[0][0],v[0][1]),M.lineEnd();var n,t=M.clean(),e=x.buffer(),r=e.length;if(v.pop(),p.push(v),v=null,r){if(1&t){n=e[0];var u,r=n.length-1,o=-1;for(i.lineStart();++o<r;)i.point((u=n[o])[0],u[1]);return i.lineEnd(),void 0}r>1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Ae))}}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:o,lineStart:c,lineEnd:s,polygonStart:function(){y.point=l,y.lineStart=f,y.lineEnd=h,g=[],p=[],i.polygonStart()},polygonEnd:function(){y.point=o,y.lineStart=c,y.lineEnd=s,g=Xo.merge(g);var n=Le(m,p);g.length?we(g,Ne,n,e,i):n&&(i.lineStart(),e(null,null,1,i),i.lineEnd()),i.polygonEnd(),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},x=Ce(),M=t(x);return y}}function Ae(n){return n.length>1}function Ce(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:g,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ne(n,t){return((n=n.x)[0]<0?n[1]-Ea-Aa:Ea-n[1])-((t=t.x)[0]<0?t[1]-Ea-Aa:Ea-t[1])}function Le(n,t){var e=n[0],r=n[1],u=[Math.sin(e),-Math.cos(e),0],i=0,o=0;hc.reset();for(var a=0,c=t.length;c>a;++a){var s=t[a],l=s.length;if(l)for(var f=s[0],h=f[0],g=f[1]/2+Sa/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===l&&(d=0),n=s[d];var m=n[0],y=n[1]/2+Sa/4,x=Math.sin(y),M=Math.cos(y),_=m-h,b=oa(_)>Sa,w=p*x;if(hc.add(Math.atan2(w*Math.sin(_),v*M+w*Math.cos(_))),i+=b?_+(_>=0?ka:-ka):_,b^h>=e^m>=e){var S=fe(se(f),se(n));pe(S);var k=fe(u,S);pe(k);var E=(b^_>=0?-1:1)*X(k[2]);(r>E||r===E&&(S[0]||S[1]))&&(o+=b^_>=0?1:-1)}if(!d++)break;h=m,p=x,v=M,f=n}}return(-Aa>i||Aa>i&&0>hc)^1&o}function ze(n){var t,e=0/0,r=0/0,u=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(i,o){var a=i>0?Sa:-Sa,c=oa(i-e);oa(c-Sa)<Aa?(n.point(e,r=(r+o)/2>0?Ea:-Ea),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(i,r),t=0):u!==a&&c>=Sa&&(oa(e-u)<Aa&&(e-=u*Aa),oa(i-a)<Aa&&(i-=a*Aa),r=qe(e,r,i,o),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),t=0),n.point(e=i,r=o),u=a},lineEnd:function(){n.lineEnd(),e=r=0/0},clean:function(){return 2-t}}}function qe(n,t,e,r){var u,i,o=Math.sin(n-e);return oa(o)>Aa?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*o)):(t+r)/2}function Te(n,t,e,r){var u;if(null==n)u=e*Ea,r.point(-Sa,u),r.point(0,u),r.point(Sa,u),r.point(Sa,0),r.point(Sa,-u),r.point(0,-u),r.point(-Sa,-u),r.point(-Sa,0),r.point(-Sa,u);else if(oa(n[0]-t[0])>Aa){var i=n[0]<t[0]?Sa:-Sa;u=e*i/2,r.point(-i,u),r.point(0,u),r.point(i,u)}else r.point(t[0],t[1])}function Re(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,c,s,l;return{lineStart:function(){s=c=!1,l=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=o?v?0:u(f,h):v?u(f+(0>f?Sa:-Sa),h):0;if(!e&&(s=c=v)&&n.lineStart(),v!==c&&(g=r(e,p),(de(e,g)||de(p,g))&&(p[0]+=Aa,p[1]+=Aa,v=t(p[0],p[1]))),v!==c)l=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(a&&e&&o^v){var m;d&i||!(m=r(p,e,!0))||(l=0,o?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&de(e,p)||n.point(p[0],p[1]),e=p,c=v,i=d},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return l|(s&&c)<<1}}}function r(n,t,e){var r=se(n),u=se(t),o=[1,0,0],a=fe(r,u),c=le(a,a),s=a[0],l=c-s*s;if(!l)return!e&&n;var f=i*c/l,h=-i*s/l,g=fe(o,a),p=ge(o,f),v=ge(a,h);he(p,v);var d=g,m=le(p,d),y=le(d,d),x=m*m-y*(le(p,p)-1);if(!(0>x)){var M=Math.sqrt(x),_=ge(d,(-m-M)/y);if(he(_,p),_=ve(_),!e)return _;var b,w=n[0],S=t[0],k=n[1],E=t[1];w>S&&(b=w,w=S,S=b);var A=S-w,C=oa(A-Sa)<Aa,N=C||Aa>A;if(!C&&k>E&&(b=k,k=E,E=b),N?C?k+E>0^_[1]<(oa(_[0]-w)<Aa?k:E):k<=_[1]&&_[1]<=E:A>Sa^(w<=_[0]&&_[0]<=S)){var L=ge(d,(-m+M)/y);return he(L,p),[_,ve(L)]}}}function u(t,e){var r=o?n:Sa-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),o=i>0,a=oa(i)>Aa,c=cr(n,6*Na);return Ee(t,e,c,o?[0,-n]:[-Sa,n-Sa])}function De(n,t,e,r){return function(u){var i,o=u.a,a=u.b,c=o.x,s=o.y,l=a.x,f=a.y,h=0,g=1,p=l-c,v=f-s;if(i=n-c,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-c,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-s,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-s,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:c+h*p,y:s+h*v}),1>g&&(u.b={x:c+g*p,y:s+g*v}),u}}}}}}function Pe(n,t,e,r){function u(r,u){return oa(r[0]-n)<Aa?u>0?0:3:oa(r[0]-e)<Aa?u>0?2:1:oa(r[1]-t)<Aa?u>0?1:0:u>0?3:2}function i(n,t){return o(n.x,t.x)}function o(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function c(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,o=1,a=d[u],c=a.length,s=a[0];c>o;++o)i=a[o],s[1]<=r?i[1]>r&&Z(s,i,n)>0&&++t:i[1]<=r&&Z(s,i,n)<0&&--t,s=i;return 0!==t}function s(i,a,c,s){var l=0,f=0;if(null==i||(l=u(i,c))!==(f=u(a,c))||o(i,a)<0^c>0){do s.point(0===l||3===l?n:e,l>1?r:t);while((l=(l+c+4)%4)!==f)}else s.point(a[0],a[1])}function l(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){l(n,t)&&a.point(n,t)}function h(){N.point=p,d&&d.push(m=[]),S=!0,w=!1,_=b=0/0}function g(){v&&(p(y,x),M&&w&&A.rejoin(),v.push(A.buffer())),N.point=f,w&&a.lineEnd()}function p(n,t){n=Math.max(-Ac,Math.min(Ac,n)),t=Math.max(-Ac,Math.min(Ac,t));var e=l(n,t);if(d&&m.push([n,t]),S)y=n,x=t,M=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:_,y:b},b:{x:n,y:t}};C(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}_=n,b=t,w=e}var v,d,m,y,x,M,_,b,w,S,k,E=a,A=Ce(),C=De(n,t,e,r),N={point:f,lineStart:h,lineEnd:g,polygonStart:function(){a=A,v=[],d=[],k=!0},polygonEnd:function(){a=E,v=Xo.merge(v);var t=c([n,r]),e=k&&t,u=v.length;(e||u)&&(a.polygonStart(),e&&(a.lineStart(),s(null,null,1,a),a.lineEnd()),u&&we(v,i,t,s,a),a.polygonEnd()),v=d=m=null}};return N}}function Ue(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function je(n){var t=0,e=Sa/3,r=nr(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*Sa/180,e=n[1]*Sa/180):[180*(t/Sa),180*(e/Sa)]},u}function He(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),o-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),o=Math.sqrt(i)/u;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/u,X((i-(n*n+e*e)*u*u)/(2*u))]},e}function Fe(){function n(n,t){Nc+=u*n-r*t,r=n,u=t}var t,e,r,u;Rc.point=function(i,o){Rc.point=n,t=r=i,e=u=o},Rc.lineEnd=function(){n(t,e)}}function Oe(n,t){Lc>n&&(Lc=n),n>qc&&(qc=n),zc>t&&(zc=t),t>Tc&&(Tc=t)}function Ye(){function n(n,t){o.push("M",n,",",t,i)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function u(){o.push("Z")}var i=Ie(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return i=Ie(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Ie(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Ze(n,t){dc+=n,mc+=t,++yc}function Ve(){function n(n,r){var u=n-t,i=r-e,o=Math.sqrt(u*u+i*i);xc+=o*(t+n)/2,Mc+=o*(e+r)/2,_c+=o,Ze(t=n,e=r)}var t,e;Pc.point=function(r,u){Pc.point=n,Ze(t=r,e=u)}}function Xe(){Pc.point=Ze}function $e(){function n(n,t){var e=n-r,i=t-u,o=Math.sqrt(e*e+i*i);xc+=o*(r+n)/2,Mc+=o*(u+t)/2,_c+=o,o=u*n-r*t,bc+=o*(r+n),wc+=o*(u+t),Sc+=3*o,Ze(r=n,u=t)}var t,e,r,u;Pc.point=function(i,o){Pc.point=n,Ze(t=r=i,e=u=o)},Pc.lineEnd=function(){n(t,e)}}function Be(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,o,0,ka)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function u(){a.point=t}function i(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:u,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=u,a.point=t},pointRadius:function(n){return o=n,a},result:g};return a}function We(n){function t(n){return(a?r:e)(n)}function e(t){return Ke(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){x=0/0,S.point=i,t.lineStart()}function i(e,r){var i=se([e,r]),o=n(e,r);u(x,M,y,_,b,w,x=o[0],M=o[1],y=e,_=i[0],b=i[1],w=i[2],a,t),t.point(x,M)}function o(){S.point=e,t.lineEnd()}function c(){r(),S.point=s,S.lineEnd=l}function s(n,t){i(f=n,h=t),g=x,p=M,v=_,d=b,m=w,S.point=i}function l(){u(x,M,y,_,b,w,g,p,f,v,d,m,a,t),S.lineEnd=o,o()}var f,h,g,p,v,d,m,y,x,M,_,b,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=c},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,a,c,s,l,f,h,g,p,v,d,m){var y=l-t,x=f-e,M=y*y+x*x;if(M>4*i&&d--){var _=a+g,b=c+p,w=s+v,S=Math.sqrt(_*_+b*b+w*w),k=Math.asin(w/=S),E=oa(oa(w)-1)<Aa||oa(r-h)<Aa?(r+h)/2:Math.atan2(b,_),A=n(E,k),C=A[0],N=A[1],L=C-t,z=N-e,q=x*L-y*z;(q*q/M>i||oa((y*L+x*z)/M-.5)>.3||o>a*g+c*p+s*v)&&(u(t,e,r,a,c,s,C,N,E,_/=S,b/=S,w,d,m),m.point(C,N),u(C,N,E,_,b,w,l,f,h,g,p,v,d,m))}}var i=.5,o=Math.cos(30*Na),a=16;return t.precision=function(n){return arguments.length?(a=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function Je(n){var t=We(function(t,e){return n([t*La,e*La])});return function(n){return tr(t(n))}}function Ge(n){this.stream=n}function Ke(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function Qe(n){return nr(function(){return n})()}function nr(n){function t(n){return n=a(n[0]*Na,n[1]*Na),[n[0]*h+c,s-n[1]*h]}function e(n){return n=a.invert((n[0]-c)/h,(s-n[1])/h),n&&[n[0]*La,n[1]*La]}function r(){a=Ue(o=ur(m,y,x),i);var n=i(v,d);return c=g-n[0]*h,s=p+n[1]*h,u()}function u(){return l&&(l.valid=!1,l=null),t}var i,o,a,c,s,l,f=We(function(n,t){return n=i(n,t),[n[0]*h+c,s-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,y=0,x=0,M=Ec,_=bt,b=null,w=null;return t.stream=function(n){return l&&(l.valid=!1),l=tr(M(o,f(_(n)))),l.valid=!0,l},t.clipAngle=function(n){return arguments.length?(M=null==n?(b=n,Ec):Re((b=+n)*Na),u()):b
+},t.clipExtent=function(n){return arguments.length?(w=n,_=n?Pe(n[0][0],n[0][1],n[1][0],n[1][1]):bt,u()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Na,d=n[1]%360*Na,r()):[v*La,d*La]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Na,y=n[1]%360*Na,x=n.length>2?n[2]%360*Na:0,r()):[m*La,y*La,x*La]},Xo.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function tr(n){return Ke(n,function(t,e){n.point(t*Na,e*Na)})}function er(n,t){return[n,t]}function rr(n,t){return[n>Sa?n-ka:-Sa>n?n+ka:n,t]}function ur(n,t,e){return n?t||e?Ue(or(n),ar(t,e)):or(n):t||e?ar(t,e):rr}function ir(n){return function(t,e){return t+=n,[t>Sa?t-ka:-Sa>t?t+ka:t,e]}}function or(n){var t=ir(n);return t.invert=ir(-n),t}function ar(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*r+a*u;return[Math.atan2(c*i-l*o,a*r-s*u),X(l*i+c*o)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*i-c*o;return[Math.atan2(c*i+s*o,a*r+l*u),X(l*r-a*u)]},e}function cr(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,o,a){var c=o*t;null!=u?(u=sr(e,u),i=sr(e,i),(o>0?i>u:u>i)&&(u+=o*ka)):(u=n+o*ka,i=n-.5*c);for(var s,l=u;o>0?l>i:i>l;l-=c)a.point((s=ve([e,-r*Math.cos(l),-r*Math.sin(l)]))[0],s[1])}}function sr(n,t){var e=se(t);e[0]-=n,pe(e);var r=V(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Aa)%(2*Math.PI)}function lr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function fr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function hr(n){return n.source}function gr(n){return n.target}function pr(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),o=Math.cos(r),a=Math.sin(r),c=u*Math.cos(n),s=u*Math.sin(n),l=o*Math.cos(e),f=o*Math.sin(e),h=2*Math.asin(Math.sqrt(J(r-t)+u*o*J(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*l,u=e*s+t*f,o=e*i+t*a;return[Math.atan2(u,r)*La,Math.atan2(o,Math.sqrt(r*r+u*u))*La]}:function(){return[n*La,t*La]};return p.distance=h,p}function vr(){function n(n,u){var i=Math.sin(u*=Na),o=Math.cos(u),a=oa((n*=Na)-t),c=Math.cos(a);Uc+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*i-e*o*c)*a),e*i+r*o*c),t=n,e=i,r=o}var t,e,r;jc.point=function(u,i){t=u*Na,e=Math.sin(i*=Na),r=Math.cos(i),jc.point=n},jc.lineEnd=function(){jc.point=jc.lineEnd=g}}function dr(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),o=Math.cos(u);return[Math.atan2(n*i,r*o),Math.asin(r&&e*i/r)]},e}function mr(n,t){function e(n,t){var e=oa(oa(t)-Ea)<Aa?0:o/Math.pow(u(t),i);return[e*Math.sin(i*n),o-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(Sa/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),o=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=o-t,r=I(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(o/r,1/i))-Ea]},e):xr}function yr(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return oa(u)<Aa?er:(e.invert=function(n,t){var e=i-t;return[Math.atan2(n,e)/u,i-I(u)*Math.sqrt(n*n+e*e)]},e)}function xr(n,t){return[n,Math.log(Math.tan(Sa/4+t/2))]}function Mr(n){var t,e=Qe(n),r=e.scale,u=e.translate,i=e.clipExtent;return e.scale=function(){var n=r.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.translate=function(){var n=u.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.clipExtent=function(n){var o=i.apply(e,arguments);if(o===e){if(t=null==n){var a=Sa*r(),c=u();i([[c[0]-a,c[1]-a],[c[0]+a,c[1]+a]])}}else t&&(o=null);return o},e.clipExtent(null)}function _r(n,t){return[Math.log(Math.tan(Sa/4+t/2)),-n]}function br(n){return n[0]}function wr(n){return n[1]}function Sr(n){for(var t=n.length,e=[0,1],r=2,u=2;t>u;u++){for(;r>1&&Z(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function kr(n,t){return n[0]-t[0]||n[1]-t[1]}function Er(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Ar(n,t,e,r){var u=n[0],i=e[0],o=t[0]-u,a=r[0]-i,c=n[1],s=e[1],l=t[1]-c,f=r[1]-s,h=(a*(c-s)-f*(u-i))/(f*o-a*l);return[u+h*o,c+h*l]}function Cr(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Nr(){Jr(this),this.edge=this.site=this.circle=null}function Lr(n){var t=Jc.pop()||new Nr;return t.site=n,t}function zr(n){Or(n),$c.remove(n),Jc.push(n),Jr(n)}function qr(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,o=n.N,a=[n];zr(n);for(var c=i;c.circle&&oa(e-c.circle.x)<Aa&&oa(r-c.circle.cy)<Aa;)i=c.P,a.unshift(c),zr(c),c=i;a.unshift(c),Or(c);for(var s=o;s.circle&&oa(e-s.circle.x)<Aa&&oa(r-s.circle.cy)<Aa;)o=s.N,a.push(s),zr(s),s=o;a.push(s),Or(s);var l,f=a.length;for(l=1;f>l;++l)s=a[l],c=a[l-1],$r(s.edge,c.site,s.site,u);c=a[0],s=a[f-1],s.edge=Vr(c.site,s.site,null,u),Fr(c),Fr(s)}function Tr(n){for(var t,e,r,u,i=n.x,o=n.y,a=$c._;a;)if(r=Rr(a,o)-i,r>Aa)a=a.L;else{if(u=i-Dr(a,o),!(u>Aa)){r>-Aa?(t=a.P,e=a):u>-Aa?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var c=Lr(n);if($c.insert(t,c),t||e){if(t===e)return Or(t),e=Lr(t.site),$c.insert(c,e),c.edge=e.edge=Vr(t.site,c.site),Fr(t),Fr(e),void 0;if(!e)return c.edge=Vr(t.site,c.site),void 0;Or(t),Or(e);var s=t.site,l=s.x,f=s.y,h=n.x-l,g=n.y-f,p=e.site,v=p.x-l,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,x=v*v+d*d,M={x:(d*y-g*x)/m+l,y:(h*x-v*y)/m+f};$r(e.edge,s,p,M),c.edge=Vr(s,n,null,M),e.edge=Vr(n,p,null,M),Fr(t),Fr(e)}}function Rr(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var o=n.P;if(!o)return-1/0;e=o.site;var a=e.x,c=e.y,s=c-t;if(!s)return a;var l=a-r,f=1/i-1/s,h=l/s;return f?(-h+Math.sqrt(h*h-2*f*(l*l/(-2*s)-c+s/2+u-i/2)))/f+r:(r+a)/2}function Dr(n,t){var e=n.N;if(e)return Rr(e,t);var r=n.site;return r.y===t?r.x:1/0}function Pr(n){this.site=n,this.edges=[]}function Ur(n){for(var t,e,r,u,i,o,a,c,s,l,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=Xc,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(a=i.edges,c=a.length,o=0;c>o;)l=a[o].end(),r=l.x,u=l.y,s=a[++o%c].start(),t=s.x,e=s.y,(oa(r-t)>Aa||oa(u-e)>Aa)&&(a.splice(o,0,new Br(Xr(i.site,l,oa(r-f)<Aa&&p-u>Aa?{x:f,y:oa(t-f)<Aa?e:p}:oa(u-p)<Aa&&h-r>Aa?{x:oa(e-p)<Aa?t:h,y:p}:oa(r-h)<Aa&&u-g>Aa?{x:h,y:oa(t-h)<Aa?e:g}:oa(u-g)<Aa&&r-f>Aa?{x:oa(e-g)<Aa?t:f,y:g}:null),i.site,null)),++c)}function jr(n,t){return t.angle-n.angle}function Hr(){Jr(this),this.x=this.y=this.arc=this.site=this.cy=null}function Fr(n){var t=n.P,e=n.N;if(t&&e){var r=t.site,u=n.site,i=e.site;if(r!==i){var o=u.x,a=u.y,c=r.x-o,s=r.y-a,l=i.x-o,f=i.y-a,h=2*(c*f-s*l);if(!(h>=-Ca)){var g=c*c+s*s,p=l*l+f*f,v=(f*g-s*p)/h,d=(c*p-l*g)/h,f=d+a,m=Gc.pop()||new Hr;m.arc=n,m.site=u,m.x=v+o,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,x=Wc._;x;)if(m.y<x.y||m.y===x.y&&m.x<=x.x){if(!x.L){y=x.P;break}x=x.L}else{if(!x.R){y=x;break}x=x.R}Wc.insert(y,m),y||(Bc=m)}}}}function Or(n){var t=n.circle;t&&(t.P||(Bc=t.N),Wc.remove(t),Gc.push(t),Jr(t),n.circle=null)}function Yr(n){for(var t,e=Vc,r=De(n[0][0],n[0][1],n[1][0],n[1][1]),u=e.length;u--;)t=e[u],(!Ir(t,n)||!r(t)||oa(t.a.x-t.b.x)<Aa&&oa(t.a.y-t.b.y)<Aa)&&(t.a=t.b=null,e.splice(u,1))}function Ir(n,t){var e=n.b;if(e)return!0;var r,u,i=n.a,o=t[0][0],a=t[1][0],c=t[0][1],s=t[1][1],l=n.l,f=n.r,h=l.x,g=l.y,p=f.x,v=f.y,d=(h+p)/2,m=(g+v)/2;if(v===g){if(o>d||d>=a)return;if(h>p){if(i){if(i.y>=s)return}else i={x:d,y:c};e={x:d,y:s}}else{if(i){if(i.y<c)return}else i={x:d,y:s};e={x:d,y:c}}}else if(r=(h-p)/(v-g),u=m-r*d,-1>r||r>1)if(h>p){if(i){if(i.y>=s)return}else i={x:(c-u)/r,y:c};e={x:(s-u)/r,y:s}}else{if(i){if(i.y<c)return}else i={x:(s-u)/r,y:s};e={x:(c-u)/r,y:c}}else if(v>g){if(i){if(i.x>=a)return}else i={x:o,y:r*o+u};e={x:a,y:r*a+u}}else{if(i){if(i.x<o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}return n.a=i,n.b=e,!0}function Zr(n,t){this.l=n,this.r=t,this.a=this.b=null}function Vr(n,t,e,r){var u=new Zr(n,t);return Vc.push(u),e&&$r(u,n,t,e),r&&$r(u,t,n,r),Xc[n.i].edges.push(new Br(u,n,t)),Xc[t.i].edges.push(new Br(u,t,n)),u}function Xr(n,t,e){var r=new Zr(n,null);return r.a=t,r.b=e,Vc.push(r),r}function $r(n,t,e,r){n.a||n.b?n.l===e?n.b=r:n.a=r:(n.a=r,n.l=t,n.r=e)}function Br(n,t,e){var r=n.a,u=n.b;this.edge=n,this.site=t,this.angle=e?Math.atan2(e.y-t.y,e.x-t.x):n.l===t?Math.atan2(u.x-r.x,r.y-u.y):Math.atan2(r.x-u.x,u.y-r.y)}function Wr(){this._=null}function Jr(n){n.U=n.C=n.L=n.R=n.P=n.N=null}function Gr(n,t){var e=t,r=t.R,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.R=r.L,e.R&&(e.R.U=e),r.L=e}function Kr(n,t){var e=t,r=t.L,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.L=r.R,e.L&&(e.L.U=e),r.R=e}function Qr(n){for(;n.L;)n=n.L;return n}function nu(n,t){var e,r,u,i=n.sort(tu).pop();for(Vc=[],Xc=new Array(n.length),$c=new Wr,Wc=new Wr;;)if(u=Bc,i&&(!u||i.y<u.y||i.y===u.y&&i.x<u.x))(i.x!==e||i.y!==r)&&(Xc[i.i]=new Pr(i),Tr(i),e=i.x,r=i.y),i=n.pop();else{if(!u)break;qr(u.arc)}t&&(Yr(t),Ur(t));var o={cells:Xc,edges:Vc};return $c=Wc=Vc=Xc=null,o}function tu(n,t){return t.y-n.y||t.x-n.x}function eu(n,t,e){return(n.x-e.x)*(t.y-n.y)-(n.x-t.x)*(e.y-n.y)}function ru(n){return n.x}function uu(n){return n.y}function iu(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function ou(n,t,e,r,u,i){if(!n(t,e,r,u,i)){var o=.5*(e+u),a=.5*(r+i),c=t.nodes;c[0]&&ou(n,c[0],e,r,o,a),c[1]&&ou(n,c[1],o,r,u,a),c[2]&&ou(n,c[2],e,a,o,i),c[3]&&ou(n,c[3],o,a,u,i)}}function au(n,t){n=Xo.rgb(n),t=Xo.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,o=t.g-r,a=t.b-u;return function(n){return"#"+vt(Math.round(e+i*n))+vt(Math.round(r+o*n))+vt(Math.round(u+a*n))}}function cu(n,t){var e,r={},u={};for(e in n)e in t?r[e]=fu(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function su(n,t){return t-=n=+n,function(e){return n+t*e}}function lu(n,t){var e,r,u,i,o,a=0,c=0,s=[],l=[];for(n+="",t+="",Qc.lastIndex=0,r=0;e=Qc.exec(t);++r)e.index&&s.push(t.substring(a,c=e.index)),l.push({i:s.length,x:e[0]}),s.push(null),a=Qc.lastIndex;for(a<t.length&&s.push(t.substring(a)),r=0,i=l.length;(e=Qc.exec(n))&&i>r;++r)if(o=l[r],o.x==e[0]){if(o.i)if(null==s[o.i+1])for(s[o.i-1]+=o.x,s.splice(o.i,1),u=r+1;i>u;++u)l[u].i--;else for(s[o.i-1]+=o.x+s[o.i+1],s.splice(o.i,2),u=r+1;i>u;++u)l[u].i-=2;else if(null==s[o.i+1])s[o.i]=o.x;else for(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1),u=r+1;i>u;++u)l[u].i--;l.splice(r,1),i--,r--}else o.x=su(parseFloat(e[0]),parseFloat(o.x));for(;i>r;)o=l.pop(),null==s[o.i+1]?s[o.i]=o.x:(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1)),i--;return 1===s.length?null==s[0]?(o=l[0].x,function(n){return o(n)+""}):function(){return t}:function(n){for(r=0;i>r;++r)s[(o=l[r]).i]=o.x(n);return s.join("")}}function fu(n,t){for(var e,r=Xo.interpolators.length;--r>=0&&!(e=Xo.interpolators[r](n,t)););return e}function hu(n,t){var e,r=[],u=[],i=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(fu(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;o>e;++e)u[e]=t[e];return function(n){for(e=0;a>e;++e)u[e]=r[e](n);return u}}function gu(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function pu(n){return function(t){return 1-n(1-t)}}function vu(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function du(n){return n*n}function mu(n){return n*n*n}function yu(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function xu(n){return function(t){return Math.pow(t,n)}}function Mu(n){return 1-Math.cos(n*Ea)}function _u(n){return Math.pow(2,10*(n-1))}function bu(n){return 1-Math.sqrt(1-n*n)}function wu(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/ka*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*ka/t)}}function Su(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function ku(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Eu(n,t){n=Xo.hcl(n),t=Xo.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,o=t.c-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return rt(e+i*n,r+o*n,u+a*n)+""}}function Au(n,t){n=Xo.hsl(n),t=Xo.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,o=t.s-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return nt(e+i*n,r+o*n,u+a*n)+""}}function Cu(n,t){n=Xo.lab(n),t=Xo.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,o=t.a-r,a=t.b-u;return function(n){return ot(e+i*n,r+o*n,u+a*n)+""}}function Nu(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Lu(n){var t=[n.a,n.b],e=[n.c,n.d],r=qu(t),u=zu(t,e),i=qu(Tu(e,t,-u))||0;t[0]*e[1]<e[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,u*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-e[0],e[1]))*La,this.translate=[n.e,n.f],this.scale=[r,i],this.skew=i?Math.atan2(u,i)*La:0}function zu(n,t){return n[0]*t[0]+n[1]*t[1]}function qu(n){var t=Math.sqrt(zu(n,n));return t&&(n[0]/=t,n[1]/=t),t}function Tu(n,t,e){return n[0]+=e*t[0],n[1]+=e*t[1],n}function Ru(n,t){var e,r=[],u=[],i=Xo.transform(n),o=Xo.transform(t),a=i.translate,c=o.translate,s=i.rotate,l=o.rotate,f=i.skew,h=o.skew,g=i.scale,p=o.scale;return a[0]!=c[0]||a[1]!=c[1]?(r.push("translate(",null,",",null,")"),u.push({i:1,x:su(a[0],c[0])},{i:3,x:su(a[1],c[1])})):c[0]||c[1]?r.push("translate("+c+")"):r.push(""),s!=l?(s-l>180?l+=360:l-s>180&&(s+=360),u.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:su(s,l)})):l&&r.push(r.pop()+"rotate("+l+")"),f!=h?u.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:su(f,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),u.push({i:e-4,x:su(g[0],p[0])},{i:e-2,x:su(g[1],p[1])})):(1!=p[0]||1!=p[1])&&r.push(r.pop()+"scale("+p+")"),e=u.length,function(n){for(var t,i=-1;++i<e;)r[(t=u[i]).i]=t.x(n);return r.join("")}}function Du(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return(e-n)*t}}function Pu(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return Math.max(0,Math.min(1,(e-n)*t))}}function Uu(n){for(var t=n.source,e=n.target,r=Hu(t,e),u=[t];t!==r;)t=t.parent,u.push(t);for(var i=u.length;e!==r;)u.splice(i,0,e),e=e.parent;return u}function ju(n){for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Hu(n,t){if(n===t)return n;for(var e=ju(n),r=ju(t),u=e.pop(),i=r.pop(),o=null;u===i;)o=u,u=e.pop(),i=r.pop();return o}function Fu(n){n.fixed|=2}function Ou(n){n.fixed&=-7}function Yu(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Iu(n){n.fixed&=-5}function Zu(n,t,e){var r=0,u=0;if(n.charge=0,!n.leaf)for(var i,o=n.nodes,a=o.length,c=-1;++c<a;)i=o[c],null!=i&&(Zu(i,t,e),n.charge+=i.charge,r+=i.charge*i.cx,u+=i.charge*i.cy);if(n.point){n.leaf||(n.point.x+=Math.random()-.5,n.point.y+=Math.random()-.5);var s=t*e[n.point.index];n.charge+=n.pointCharge=s,r+=s*n.point.x,u+=s*n.point.y}n.cx=r/n.charge,n.cy=u/n.charge}function Vu(n,t){return Xo.rebind(n,t,"sort","children","value"),n.nodes=n,n.links=Wu,n}function Xu(n){return n.children}function $u(n){return n.value}function Bu(n,t){return t.value-n.value}function Wu(n){return Xo.merge(n.map(function(n){return(n.children||[]).map(function(t){return{source:n,target:t}})}))}function Ju(n){return n.x}function Gu(n){return n.y}function Ku(n,t,e){n.y0=t,n.y=e}function Qu(n){return Xo.range(n.length)}function ni(n){for(var t=-1,e=n[0].length,r=[];++t<e;)r[t]=0;return r}function ti(n){for(var t,e=1,r=0,u=n[0][1],i=n.length;i>e;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function ei(n){return n.reduce(ri,0)}function ri(n,t){return n+t[1]}function ui(n,t){return ii(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ii(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function oi(n){return[Xo.min(n),Xo.max(n)]}function ai(n,t){return n.parent==t.parent?1:2}function ci(n){var t=n.children;return t&&t.length?t[0]:n._tree.thread}function si(n){var t,e=n.children;return e&&(t=e.length)?e[t-1]:n._tree.thread}function li(n,t){var e=n.children;if(e&&(u=e.length))for(var r,u,i=-1;++i<u;)t(r=li(e[i],t),n)>0&&(n=r);return n}function fi(n,t){return n.x-t.x}function hi(n,t){return t.x-n.x}function gi(n,t){return n.depth-t.depth}function pi(n,t){function e(n,r){var u=n.children;if(u&&(o=u.length))for(var i,o,a=null,c=-1;++c<o;)i=u[c],e(i,a),a=i;t(n,r)}e(n,null)}function vi(n){for(var t,e=0,r=0,u=n.children,i=u.length;--i>=0;)t=u[i]._tree,t.prelim+=e,t.mod+=e,e+=t.shift+(r+=t.change)}function di(n,t,e){n=n._tree,t=t._tree;var r=e/(t.number-n.number);n.change+=r,t.change-=r,t.shift+=e,t.prelim+=e,t.mod+=e}function mi(n,t,e){return n._tree.ancestor.parent==t.parent?n._tree.ancestor:e}function yi(n,t){return n.value-t.value}function xi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Mi(n,t){n._pack_next=t,t._pack_prev=n}function _i(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function bi(n){function t(n){l=Math.min(n.x-n.r,l),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(s=e.length)){var e,r,u,i,o,a,c,s,l=1/0,f=-1/0,h=1/0,g=-1/0;if(e.forEach(wi),r=e[0],r.x=-r.r,r.y=0,t(r),s>1&&(u=e[1],u.x=u.r,u.y=0,t(u),s>2))for(i=e[2],Ei(r,u,i),t(i),xi(r,i),r._pack_prev=i,xi(i,u),u=r._pack_next,o=3;s>o;o++){Ei(r,u,i=e[o]);var p=0,v=1,d=1;for(a=u._pack_next;a!==u;a=a._pack_next,v++)if(_i(a,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==a._pack_prev&&!_i(c,i);c=c._pack_prev,d++);p?(d>v||v==d&&u.r<r.r?Mi(r,u=a):Mi(r=c,u),o--):(xi(r,i),u=i,t(i))}var m=(l+f)/2,y=(h+g)/2,x=0;for(o=0;s>o;o++)i=e[o],i.x-=m,i.y-=y,x=Math.max(x,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=x,e.forEach(Si)}}function wi(n){n._pack_next=n._pack_prev=n}function Si(n){delete n._pack_next,delete n._pack_prev}function ki(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,o=u.length;++i<o;)ki(u[i],t,e,r)}function Ei(n,t,e){var r=n.r+e.r,u=t.x-n.x,i=t.y-n.y;if(r&&(u||i)){var o=t.r+e.r,a=u*u+i*i;o*=o,r*=r;var c=.5+(r-o)/(2*a),s=Math.sqrt(Math.max(0,2*o*(r+a)-(r-=a)*r-o*o))/(2*a);e.x=n.x+c*u+s*i,e.y=n.y+c*i-s*u}else e.x=n.x+r,e.y=n.y}function Ai(n){return 1+Xo.max(n,function(n){return n.y})}function Ci(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Ni(n){var t=n.children;return t&&t.length?Ni(t[0]):n}function Li(n){var t,e=n.children;return e&&(t=e.length)?Li(e[t-1]):n}function zi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function qi(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Ti(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Ri(n){return n.rangeExtent?n.rangeExtent():Ti(n.range())}function Di(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Pi(n,t){var e,r=0,u=n.length-1,i=n[r],o=n[u];return i>o&&(e=r,r=u,u=e,e=i,i=o,o=e),n[r]=t.floor(i),n[u]=t.ceil(o),n}function Ui(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:ls}function ji(n,t,e,r){var u=[],i=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]<n[0]&&(n=n.slice().reverse(),t=t.slice().reverse());++o<=a;)u.push(e(n[o-1],n[o])),i.push(r(t[o-1],t[o]));return function(t){var e=Xo.bisect(n,t,1,a)-1;return i[e](u[e](t))}}function Hi(n,t,e,r){function u(){var u=Math.min(n.length,t.length)>2?ji:Di,c=r?Pu:Du;return o=u(n,t,c,e),a=u(t,n,c,fu),i}function i(n){return o(n)}var o,a;return i.invert=function(n){return a(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Nu)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Ii(n,t)},i.tickFormat=function(t,e){return Zi(n,t,e)},i.nice=function(t){return Oi(n,t),u()},i.copy=function(){return Hi(n,t,e,r)},u()}function Fi(n,t){return Xo.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Oi(n,t){return Pi(n,Ui(Yi(n,t)[2]))}function Yi(n,t){null==t&&(t=10);var e=Ti(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Ii(n,t){return Xo.range.apply(Xo,Yi(n,t))}function Zi(n,t,e){var r=Yi(n,t);return Xo.format(e?e.replace(Qa,function(n,t,e,u,i,o,a,c,s,l){return[t,e,u,i,o,a,c,s||"."+Xi(l,r),l].join("")}):",."+Vi(r[2])+"f")}function Vi(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function Xi(n,t){var e=Vi(t[2]);return n in fs?Math.abs(e-Vi(Math.max(Math.abs(t[0]),Math.abs(t[1]))))+ +("e"!==n):e-2*("%"===n)}function $i(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(u(t))}return o.invert=function(t){return i(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),o):t},o.nice=function(){var t=Pi(r.map(u),e?Math:gs);return n.domain(t),r=t.map(i),o},o.ticks=function(){var n=Ti(r),o=[],a=n[0],c=n[1],s=Math.floor(u(a)),l=Math.ceil(u(c)),f=t%1?2:t;if(isFinite(l-s)){if(e){for(;l>s;s++)for(var h=1;f>h;h++)o.push(i(s)*h);o.push(i(s))}else for(o.push(i(s));s++<l;)for(var h=f-1;h>0;h--)o.push(i(s)*h);for(s=0;o[s]<a;s++);for(l=o.length;o[l-1]>c;l--);o=o.slice(s,l)}return o},o.tickFormat=function(n,t){if(!arguments.length)return hs;arguments.length<2?t=hs:"function"!=typeof t&&(t=Xo.format(t));var r,a=Math.max(.1,n/o.ticks().length),c=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(c(u(n)+r))<=a?t(n):""}},o.copy=function(){return $i(n.copy(),t,e,r)},Fi(o,n)}function Bi(n,t,e){function r(t){return n(u(t))}var u=Wi(t),i=Wi(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Ii(e,n)},r.tickFormat=function(n,t){return Zi(e,n,t)},r.nice=function(n){return r.domain(Oi(e,n))},r.exponent=function(o){return arguments.length?(u=Wi(t=o),i=Wi(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return Bi(n.copy(),t,e)},Fi(r,n)}function Wi(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function Ji(n,t){function e(e){return o[((i.get(e)||"range"===t.t&&i.set(e,n.push(e)))-1)%o.length]}function r(t,e){return Xo.range(n.length).map(function(n){return t+e*n})}var i,o,a;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new u;for(var o,a=-1,c=r.length;++a<c;)i.has(o=r[a])||i.set(o,n.push(o));return e[t.t].apply(e,t.a)},e.range=function(n){return arguments.length?(o=n,a=0,t={t:"range",a:arguments},e):o},e.rangePoints=function(u,i){arguments.length<2&&(i=0);var c=u[0],s=u[1],l=(s-c)/(Math.max(1,n.length-1)+i);return o=r(n.length<2?(c+s)/2:c+l*i/2,l),a=0,t={t:"rangePoints",a:arguments},e},e.rangeBands=function(u,i,c){arguments.length<2&&(i=0),arguments.length<3&&(c=i);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=(f-l)/(n.length-i+2*c);return o=r(l+h*c,h),s&&o.reverse(),a=h*(1-i),t={t:"rangeBands",a:arguments},e},e.rangeRoundBands=function(u,i,c){arguments.length<2&&(i=0),arguments.length<3&&(c=i);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=Math.floor((f-l)/(n.length-i+2*c)),g=f-l-(n.length-i)*h;return o=r(l+Math.round(g/2),h),s&&o.reverse(),a=Math.round(h*(1-i)),t={t:"rangeRoundBands",a:arguments},e},e.rangeBand=function(){return a},e.rangeExtent=function(){return Ti(t.a[0])},e.copy=function(){return Ji(n,t)},e.domain(n)}function Gi(n,t){function e(){var e=0,i=t.length;for(u=[];++e<i;)u[e-1]=Xo.quantile(n,e/i);return r}function r(n){return isNaN(n=+n)?void 0:t[Xo.bisect(u,n)]}var u;return r.domain=function(t){return arguments.length?(n=t.filter(function(n){return!isNaN(n)}).sort(Xo.ascending),e()):n},r.range=function(n){return arguments.length?(t=n,e()):t},r.quantiles=function(){return u},r.invertExtent=function(e){return e=t.indexOf(e),0>e?[0/0,0/0]:[e>0?u[e-1]:n[0],e<u.length?u[e]:n[n.length-1]]},r.copy=function(){return Gi(n,t)},e()}function Ki(n,t,e){function r(t){return e[Math.max(0,Math.min(o,Math.floor(i*(t-n))))]}function u(){return i=e.length/(t-n),o=e.length-1,r}var i,o;return r.domain=function(e){return arguments.length?(n=+e[0],t=+e[e.length-1],u()):[n,t]},r.range=function(n){return arguments.length?(e=n,u()):e},r.invertExtent=function(t){return t=e.indexOf(t),t=0>t?0/0:t/i+n,[t,t+1/i]},r.copy=function(){return Ki(n,t,e)},u()}function Qi(n,t){function e(e){return e>=e?t[Xo.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return Qi(n,t)},e}function no(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Ii(n,t)},t.tickFormat=function(t,e){return Zi(n,t,e)},t.copy=function(){return no(n)},t}function to(n){return n.innerRadius}function eo(n){return n.outerRadius}function ro(n){return n.startAngle}function uo(n){return n.endAngle}function io(n){function t(t){function o(){s.push("M",i(n(l),a))}for(var c,s=[],l=[],f=-1,h=t.length,g=_t(e),p=_t(r);++f<h;)u.call(this,c=t[f],f)?l.push([+g.call(this,c,f),+p.call(this,c,f)]):l.length&&(o(),l=[]);return l.length&&o(),s.length?s.join(""):null}var e=br,r=wr,u=be,i=oo,o=i.key,a=.7;return t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.defined=function(n){return arguments.length?(u=n,t):u},t.interpolate=function(n){return arguments.length?(o="function"==typeof n?i=n:(i=Ms.get(n)||oo).key,t):o},t.tension=function(n){return arguments.length?(a=n,t):a},t}function oo(n){return n.join("L")}function ao(n){return oo(n)+"Z"}function co(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r[0]+(r=n[t])[0])/2,"V",r[1]);return e>1&&u.push("H",r[0]),u.join("")}function so(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("V",(r=n[t])[1],"H",r[0]);return u.join("")}function lo(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r=n[t])[0],"V",r[1]);return u.join("")}function fo(n,t){return n.length<4?oo(n):n[1]+po(n.slice(1,n.length-1),vo(n,t))}function ho(n,t){return n.length<3?oo(n):n[0]+po((n.push(n[0]),n),vo([n[n.length-2]].concat(n,[n[1]]),t))}function go(n,t){return n.length<3?oo(n):n[0]+po(n,vo(n,t))}function po(n,t){if(t.length<1||n.length!=t.length&&n.length!=t.length+2)return oo(n);var e=n.length!=t.length,r="",u=n[0],i=n[1],o=t[0],a=o,c=1;if(e&&(r+="Q"+(i[0]-2*o[0]/3)+","+(i[1]-2*o[1]/3)+","+i[0]+","+i[1],u=n[1],c=2),t.length>1){a=t[1],i=n[c],c++,r+="C"+(u[0]+o[0])+","+(u[1]+o[1])+","+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1];for(var s=2;s<t.length;s++,c++)i=n[c],a=t[s],r+="S"+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1]}if(e){var l=n[c];r+="Q"+(i[0]+2*a[0]/3)+","+(i[1]+2*a[1]/3)+","+l[0]+","+l[1]}return r}function vo(n,t){for(var e,r=[],u=(1-t)/2,i=n[0],o=n[1],a=1,c=n.length;++a<c;)e=i,i=o,o=n[a],r.push([u*(o[0]-e[0]),u*(o[1]-e[1])]);return r}function mo(n){if(n.length<3)return oo(n);var t=1,e=n.length,r=n[0],u=r[0],i=r[1],o=[u,u,u,(r=n[1])[0]],a=[i,i,i,r[1]],c=[u,",",i,"L",_o(ws,o),",",_o(ws,a)];for(n.push(n[e-1]);++t<=e;)r=n[t],o.shift(),o.push(r[0]),a.shift(),a.push(r[1]),bo(c,o,a);return n.pop(),c.push("L",r),c.join("")}function yo(n){if(n.length<4)return oo(n);for(var t,e=[],r=-1,u=n.length,i=[0],o=[0];++r<3;)t=n[r],i.push(t[0]),o.push(t[1]);for(e.push(_o(ws,i)+","+_o(ws,o)),--r;++r<u;)t=n[r],i.shift(),i.push(t[0]),o.shift(),o.push(t[1]),bo(e,i,o);return e.join("")}function xo(n){for(var t,e,r=-1,u=n.length,i=u+4,o=[],a=[];++r<4;)e=n[r%u],o.push(e[0]),a.push(e[1]);for(t=[_o(ws,o),",",_o(ws,a)],--r;++r<i;)e=n[r%u],o.shift(),o.push(e[0]),a.shift(),a.push(e[1]),bo(t,o,a);return t.join("")}function Mo(n,t){var e=n.length-1;if(e)for(var r,u,i=n[0][0],o=n[0][1],a=n[e][0]-i,c=n[e][1]-o,s=-1;++s<=e;)r=n[s],u=s/e,r[0]=t*r[0]+(1-t)*(i+u*a),r[1]=t*r[1]+(1-t)*(o+u*c);return mo(n)}function _o(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]+n[3]*t[3]}function bo(n,t,e){n.push("C",_o(_s,t),",",_o(_s,e),",",_o(bs,t),",",_o(bs,e),",",_o(ws,t),",",_o(ws,e))}function wo(n,t){return(t[1]-n[1])/(t[0]-n[0])}function So(n){for(var t=0,e=n.length-1,r=[],u=n[0],i=n[1],o=r[0]=wo(u,i);++t<e;)r[t]=(o+(o=wo(u=i,i=n[t+1])))/2;return r[t]=o,r}function ko(n){for(var t,e,r,u,i=[],o=So(n),a=-1,c=n.length-1;++a<c;)t=wo(n[a],n[a+1]),oa(t)<Aa?o[a]=o[a+1]=0:(e=o[a]/t,r=o[a+1]/t,u=e*e+r*r,u>9&&(u=3*t/Math.sqrt(u),o[a]=u*e,o[a+1]=u*r));for(a=-1;++a<=c;)u=(n[Math.min(c,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),i.push([u||0,o[a]*u||0]);return i}function Eo(n){return n.length<3?oo(n):n[0]+po(n,ko(n))}function Ao(n){for(var t,e,r,u=-1,i=n.length;++u<i;)t=n[u],e=t[0],r=t[1]+ys,t[0]=e*Math.cos(r),t[1]=e*Math.sin(r);return n}function Co(n){function t(t){function c(){v.push("M",a(n(m),f),l,s(n(d.reverse()),f),"Z")}for(var h,g,p,v=[],d=[],m=[],y=-1,x=t.length,M=_t(e),_=_t(u),b=e===r?function(){return g}:_t(r),w=u===i?function(){return p}:_t(i);++y<x;)o.call(this,h=t[y],y)?(d.push([g=+M.call(this,h,y),p=+_.call(this,h,y)]),m.push([+b.call(this,h,y),+w.call(this,h,y)])):d.length&&(c(),d=[],m=[]);return d.length&&c(),v.length?v.join(""):null}var e=br,r=br,u=0,i=wr,o=be,a=oo,c=a.key,s=a,l="L",f=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(u=i=n,t):i},t.y0=function(n){return arguments.length?(u=n,t):u},t.y1=function(n){return arguments.length?(i=n,t):i},t.defined=function(n){return arguments.length?(o=n,t):o},t.interpolate=function(n){return arguments.length?(c="function"==typeof n?a=n:(a=Ms.get(n)||oo).key,s=a.reverse||a,l=a.closed?"M":"L",t):c},t.tension=function(n){return arguments.length?(f=n,t):f},t}function No(n){return n.radius}function Lo(n){return[n.x,n.y]}function zo(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]+ys;return[e*Math.cos(r),e*Math.sin(r)]}}function qo(){return 64}function To(){return"circle"}function Ro(n){var t=Math.sqrt(n/Sa);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function Do(n,t){return fa(n,Ns),n.id=t,n}function Po(n,t,e,r){var u=n.id;return R(n,"function"==typeof e?function(n,i,o){n.__transition__[u].tween.set(t,r(e.call(n,n.__data__,i,o)))}:(e=r(e),function(n){n.__transition__[u].tween.set(t,e)}))}function Uo(n){return null==n&&(n=""),function(){this.textContent=n}}function jo(n,t,e,r){var i=n.__transition__||(n.__transition__={active:0,count:0}),o=i[e];if(!o){var a=r.time;o=i[e]={tween:new u,time:a,ease:r.ease,delay:r.delay,duration:r.duration},++i.count,Xo.timer(function(r){function u(r){return i.active>e?s():(i.active=e,o.event&&o.event.start.call(n,l,t),o.tween.forEach(function(e,r){(r=r.call(n,l,t))&&v.push(r)}),Xo.timer(function(){return p.c=c(r||1)?be:c,1},0,a),void 0)}function c(r){if(i.active!==e)return s();for(var u=r/g,a=f(u),c=v.length;c>0;)v[--c].call(n,a);return u>=1?(o.event&&o.event.end.call(n,l,t),s()):void 0}function s(){return--i.count?delete i[e]:delete n.__transition__,1}var l=n.__data__,f=o.ease,h=o.delay,g=o.duration,p=Ja,v=[];return p.t=h+a,r>=h?u(r-h):(p.c=u,void 0)},0,a)}}function Ho(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function Fo(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function Oo(n){return n.toISOString()}function Yo(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=Xo.bisect(js,u);return i==js.length?[t.year,Yi(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/js[i-1]<js[i]/u?i-1:i]:[Os,Yi(n,e)[2]]
+}return r.invert=function(t){return Io(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(Io)},r.nice=function(n,t){function e(e){return!isNaN(e)&&!n.range(e,Io(+e+1),t).length}var i=r.domain(),o=Ti(i),a=null==n?u(o,10):"number"==typeof n&&u(o,n);return a&&(n=a[0],t=a[1]),r.domain(Pi(i,t>1?{floor:function(t){for(;e(t=n.floor(t));)t=Io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=Io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Ti(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],Io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return Yo(n.copy(),t,e)},Fi(r,n)}function Io(n){return new Date(n)}function Zo(n){return JSON.parse(n.responseText)}function Vo(n){var t=Wo.createRange();return t.selectNode(Wo.body),t.createContextualFragment(n.responseText)}var Xo={version:"3.4.1"};Date.now||(Date.now=function(){return+new Date});var $o=[].slice,Bo=function(n){return $o.call(n)},Wo=document,Jo=Wo.documentElement,Go=window;try{Bo(Jo.childNodes)[0].nodeType}catch(Ko){Bo=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}try{Wo.createElement("div").style.setProperty("opacity",0,"")}catch(Qo){var na=Go.Element.prototype,ta=na.setAttribute,ea=na.setAttributeNS,ra=Go.CSSStyleDeclaration.prototype,ua=ra.setProperty;na.setAttribute=function(n,t){ta.call(this,n,t+"")},na.setAttributeNS=function(n,t,e){ea.call(this,n,t,e+"")},ra.setProperty=function(n,t,e){ua.call(this,n,t+"",e)}}Xo.ascending=function(n,t){return t>n?-1:n>t?1:n>=t?0:0/0},Xo.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},Xo.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&e>r&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&e>r&&(e=r)}return e},Xo.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&r>e&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&r>e&&(e=r)}return e},Xo.extent=function(n,t){var e,r,u,i=-1,o=n.length;if(1===arguments.length){for(;++i<o&&!(null!=(e=u=n[i])&&e>=e);)e=u=void 0;for(;++i<o;)null!=(r=n[i])&&(e>r&&(e=r),r>u&&(u=r))}else{for(;++i<o&&!(null!=(e=u=t.call(n,n[i],i))&&e>=e);)e=void 0;for(;++i<o;)null!=(r=t.call(n,n[i],i))&&(e>r&&(e=r),r>u&&(u=r))}return[e,u]},Xo.sum=function(n,t){var e,r=0,u=n.length,i=-1;if(1===arguments.length)for(;++i<u;)isNaN(e=+n[i])||(r+=e);else for(;++i<u;)isNaN(e=+t.call(n,n[i],i))||(r+=e);return r},Xo.mean=function(t,e){var r,u=t.length,i=0,o=-1,a=0;if(1===arguments.length)for(;++o<u;)n(r=t[o])&&(i+=(r-i)/++a);else for(;++o<u;)n(r=e.call(t,t[o],o))&&(i+=(r-i)/++a);return a?i:void 0},Xo.quantile=function(n,t){var e=(n.length-1)*t+1,r=Math.floor(e),u=+n[r-1],i=e-r;return i?u+i*(n[r]-u):u},Xo.median=function(t,e){return arguments.length>1&&(t=t.map(e)),t=t.filter(n),t.length?Xo.quantile(t.sort(Xo.ascending),.5):void 0},Xo.bisector=function(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n.call(t,t[i],i)<e?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;e<n.call(t,t[i],i)?u=i:r=i+1}return r}}};var ia=Xo.bisector(function(n){return n});Xo.bisectLeft=ia.left,Xo.bisect=Xo.bisectRight=ia.right,Xo.shuffle=function(n){for(var t,e,r=n.length;r;)e=0|Math.random()*r--,t=n[r],n[r]=n[e],n[e]=t;return n},Xo.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},Xo.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},Xo.zip=function(){if(!(u=arguments.length))return[];for(var n=-1,e=Xo.min(arguments,t),r=new Array(e);++n<e;)for(var u,i=-1,o=r[n]=new Array(u);++i<u;)o[i]=arguments[i][n];return r},Xo.transpose=function(n){return Xo.zip.apply(Xo,n)},Xo.keys=function(n){var t=[];for(var e in n)t.push(e);return t},Xo.values=function(n){var t=[];for(var e in n)t.push(n[e]);return t},Xo.entries=function(n){var t=[];for(var e in n)t.push({key:e,value:n[e]});return t},Xo.merge=function(n){for(var t,e,r,u=n.length,i=-1,o=0;++i<u;)o+=n[i].length;for(e=new Array(o);--u>=0;)for(r=n[u],t=r.length;--t>=0;)e[--o]=r[t];return e};var oa=Math.abs;Xo.range=function(n,t,r){if(arguments.length<3&&(r=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/r)throw new Error("infinite range");var u,i=[],o=e(oa(r)),a=-1;if(n*=o,t*=o,r*=o,0>r)for(;(u=n+r*++a)>t;)i.push(u/o);else for(;(u=n+r*++a)<t;)i.push(u/o);return i},Xo.map=function(n){var t=new u;if(n instanceof u)n.forEach(function(n,e){t.set(n,e)});else for(var e in n)t.set(e,n[e]);return t},r(u,{has:i,get:function(n){return this[aa+n]},set:function(n,t){return this[aa+n]=t},remove:o,keys:a,values:function(){var n=[];return this.forEach(function(t,e){n.push(e)}),n},entries:function(){var n=[];return this.forEach(function(t,e){n.push({key:t,value:e})}),n},size:c,empty:s,forEach:function(n){for(var t in this)t.charCodeAt(0)===ca&&n.call(this,t.substring(1),this[t])}});var aa="\x00",ca=aa.charCodeAt(0);Xo.nest=function(){function n(t,a,c){if(c>=o.length)return r?r.call(i,a):e?a.sort(e):a;for(var s,l,f,h,g=-1,p=a.length,v=o[c++],d=new u;++g<p;)(h=d.get(s=v(l=a[g])))?h.push(l):d.set(s,[l]);return t?(l=t(),f=function(e,r){l.set(e,n(t,r,c))}):(l={},f=function(e,r){l[e]=n(t,r,c)}),d.forEach(f),l}function t(n,e){if(e>=o.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,i={},o=[],a=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(Xo.map,e,0),0)},i.key=function(n){return o.push(n),i},i.sortKeys=function(n){return a[o.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},Xo.set=function(n){var t=new l;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},r(l,{has:i,add:function(n){return this[aa+n]=!0,n},remove:function(n){return n=aa+n,n in this&&delete this[n]},values:a,size:c,empty:s,forEach:function(n){for(var t in this)t.charCodeAt(0)===ca&&n.call(this,t.substring(1))}}),Xo.behavior={},Xo.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r<u;)n[e=arguments[r]]=f(n,t,t[e]);return n};var sa=["webkit","ms","moz","Moz","o","O"];Xo.dispatch=function(){for(var n=new p,t=-1,e=arguments.length;++t<e;)n[arguments[t]]=v(n);return n},p.prototype.on=function(n,t){var e=n.indexOf("."),r="";if(e>=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},Xo.event=null,Xo.requote=function(n){return n.replace(la,"\\$&")};var la=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,fa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ha=function(n,t){return t.querySelector(n)},ga=function(n,t){return t.querySelectorAll(n)},pa=Jo[h(Jo,"matchesSelector")],va=function(n,t){return pa.call(n,t)};"function"==typeof Sizzle&&(ha=function(n,t){return Sizzle(n,t)[0]||null},ga=function(n,t){return Sizzle.uniqueSort(Sizzle(n,t))},va=Sizzle.matchesSelector),Xo.selection=function(){return xa};var da=Xo.selection.prototype=[];da.select=function(n){var t,e,r,u,i=[];n=M(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]),t.parentNode=(r=this[o]).parentNode;for(var c=-1,s=r.length;++c<s;)(u=r[c])?(t.push(e=n.call(u,u.__data__,c,o)),e&&"__data__"in u&&(e.__data__=u.__data__)):t.push(null)}return x(i)},da.selectAll=function(n){var t,e,r=[];n=_(n);for(var u=-1,i=this.length;++u<i;)for(var o=this[u],a=-1,c=o.length;++a<c;)(e=o[a])&&(r.push(t=Bo(n.call(e,e.__data__,a,u))),t.parentNode=e);return x(r)};var ma={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};Xo.ns={prefix:ma,qualify:function(n){var t=n.indexOf(":"),e=n;return t>=0&&(e=n.substring(0,t),n=n.substring(t+1)),ma.hasOwnProperty(e)?{space:ma[e],local:n}:n}},da.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=Xo.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(b(t,n[t]));return this}return this.each(b(n,t))},da.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=k(n)).length,u=-1;if(t=e.classList){for(;++u<r;)if(!t.contains(n[u]))return!1}else for(t=e.getAttribute("class");++u<r;)if(!S(n[u]).test(t))return!1;return!0}for(t in n)this.each(E(t,n[t]));return this}return this.each(E(n,t))},da.style=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(C(e,n[e],t));return this}if(2>r)return Go.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(C(n,t,e))},da.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(N(t,n[t]));return this}return this.each(N(n,t))},da.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},da.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},da.append=function(n){return n=L(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},da.insert=function(n,t){return n=L(n),t=M(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},da.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},da.data=function(n,t){function e(n,e){var r,i,o,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new u,y=new u,x=[];for(r=-1;++r<a;)d=t.call(i=n[r],i.__data__,r),m.has(d)?v[r]=i:m.set(d,i),x.push(d);for(r=-1;++r<f;)d=t.call(e,o=e[r],r),(i=m.get(d))?(g[r]=i,i.__data__=o):y.has(d)||(p[r]=z(o)),y.set(d,o),m.remove(d);for(r=-1;++r<a;)m.has(x[r])&&(v[r]=n[r])}else{for(r=-1;++r<h;)i=n[r],o=e[r],i?(i.__data__=o,g[r]=i):p[r]=z(o);for(;f>r;++r)p[r]=z(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,c.push(p),s.push(g),l.push(v)}var r,i,o=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++o<a;)(i=r[o])&&(n[o]=i.__data__);return n}var c=D([]),s=x([]),l=x([]);if("function"==typeof n)for(;++o<a;)e(r=this[o],n.call(r,r.parentNode.__data__,o));else for(;++o<a;)e(r=this[o],n);return s.enter=function(){return c},s.exit=function(){return l},s},da.datum=function(n){return arguments.length?this.property("__data__",n):this.property("__data__")},da.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=q(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return x(u)},da.order=function(){for(var n=-1,t=this.length;++n<t;)for(var e,r=this[n],u=r.length-1,i=r[u];--u>=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},da.sort=function(n){n=T.apply(this,arguments);for(var t=-1,e=this.length;++t<e;)this[t].sort(n);return this.order()},da.each=function(n){return R(this,function(t,e,r){n.call(t,t.__data__,e,r)})},da.call=function(n){var t=Bo(arguments);return n.apply(t[0]=this,t),this},da.empty=function(){return!this.node()},da.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},da.size=function(){var n=0;return this.each(function(){++n}),n};var ya=[];Xo.selection.enter=D,Xo.selection.enter.prototype=ya,ya.append=da.append,ya.empty=da.empty,ya.node=da.node,ya.call=da.call,ya.size=da.size,ya.select=function(n){for(var t,e,r,u,i,o=[],a=-1,c=this.length;++a<c;){r=(u=this[a]).update,o.push(t=[]),t.parentNode=u.parentNode;for(var s=-1,l=u.length;++s<l;)(i=u[s])?(t.push(r[s]=e=n.call(u.parentNode,i.__data__,s,a)),e.__data__=i.__data__):t.push(null)}return x(o)},ya.insert=function(n,t){return arguments.length<2&&(t=P(this)),da.insert.call(this,n,t)},da.transition=function(){for(var n,t,e=ks||++Ls,r=[],u=Es||{time:Date.now(),ease:yu,delay:0,duration:250},i=-1,o=this.length;++i<o;){r.push(n=[]);for(var a=this[i],c=-1,s=a.length;++c<s;)(t=a[c])&&jo(t,c,e,u),n.push(t)}return Do(r,e)},da.interrupt=function(){return this.each(U)},Xo.select=function(n){var t=["string"==typeof n?ha(n,Wo):n];return t.parentNode=Jo,x([t])},Xo.selectAll=function(n){var t=Bo("string"==typeof n?ga(n,Wo):n);return t.parentNode=Jo,x([t])};var xa=Xo.select(Jo);da.on=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(j(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(j(n,t,e))};var Ma=Xo.map({mouseenter:"mouseover",mouseleave:"mouseout"});Ma.forEach(function(n){"on"+n in Wo&&Ma.remove(n)});var _a="onselectstart"in Wo?null:h(Jo.style,"userSelect"),ba=0;Xo.mouse=function(n){return Y(n,m())};var wa=/WebKit/.test(Go.navigator.userAgent)?-1:0;Xo.touches=function(n,t){return arguments.length<2&&(t=m().touches),t?Bo(t).map(function(t){var e=Y(n,t);return e.identifier=t.identifier,e}):[]},Xo.behavior.drag=function(){function n(){this.on("mousedown.drag",o).on("touchstart.drag",a)}function t(){return Xo.event.changedTouches[0].identifier}function e(n,t){return Xo.touches(n).filter(function(n){return n.identifier===t})[0]}function r(n,t,e,r){return function(){function o(){var n=t(l,g),e=n[0]-v[0],r=n[1]-v[1];d|=e|r,v=n,f({type:"drag",x:n[0]+c[0],y:n[1]+c[1],dx:e,dy:r})}function a(){m.on(e+"."+p,null).on(r+"."+p,null),y(d&&Xo.event.target===h),f({type:"dragend"})}var c,s=this,l=s.parentNode,f=u.of(s,arguments),h=Xo.event.target,g=n(),p=null==g?"drag":"drag-"+g,v=t(l,g),d=0,m=Xo.select(Go).on(e+"."+p,o).on(r+"."+p,a),y=O();i?(c=i.apply(s,arguments),c=[c.x-v[0],c.y-v[1]]):c=[0,0],f({type:"dragstart"})}}var u=y(n,"drag","dragstart","dragend"),i=null,o=r(g,Xo.mouse,"mousemove","mouseup"),a=r(t,e,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},Xo.rebind(n,u,"on")};var Sa=Math.PI,ka=2*Sa,Ea=Sa/2,Aa=1e-6,Ca=Aa*Aa,Na=Sa/180,La=180/Sa,za=Math.SQRT2,qa=2,Ta=4;Xo.interpolateZoom=function(n,t){function e(n){var t=n*y;if(m){var e=B(v),o=i/(qa*h)*(e*W(za*t+v)-$(v));return[r+o*s,u+o*l,i*e/B(za*t+v)]}return[r+n*s,u+n*l,i*Math.exp(za*t)]}var r=n[0],u=n[1],i=n[2],o=t[0],a=t[1],c=t[2],s=o-r,l=a-u,f=s*s+l*l,h=Math.sqrt(f),g=(c*c-i*i+Ta*f)/(2*i*qa*h),p=(c*c-i*i-Ta*f)/(2*c*qa*h),v=Math.log(Math.sqrt(g*g+1)-g),d=Math.log(Math.sqrt(p*p+1)-p),m=d-v,y=(m||Math.log(c/i))/za;return e.duration=1e3*y,e},Xo.behavior.zoom=function(){function n(n){n.on(A,s).on(Pa+".zoom",f).on(C,h).on("dblclick.zoom",g).on(L,l)}function t(n){return[(n[0]-S.x)/S.k,(n[1]-S.y)/S.k]}function e(n){return[n[0]*S.k+S.x,n[1]*S.k+S.y]}function r(n){S.k=Math.max(E[0],Math.min(E[1],n))}function u(n,t){t=e(t),S.x+=n[0]-t[0],S.y+=n[1]-t[1]}function i(){_&&_.domain(M.range().map(function(n){return(n-S.x)/S.k}).map(M.invert)),w&&w.domain(b.range().map(function(n){return(n-S.y)/S.k}).map(b.invert))}function o(n){n({type:"zoomstart"})}function a(n){i(),n({type:"zoom",scale:S.k,translate:[S.x,S.y]})}function c(n){n({type:"zoomend"})}function s(){function n(){l=1,u(Xo.mouse(r),g),a(i)}function e(){f.on(C,Go===r?h:null).on(N,null),p(l&&Xo.event.target===s),c(i)}var r=this,i=z.of(r,arguments),s=Xo.event.target,l=0,f=Xo.select(Go).on(C,n).on(N,e),g=t(Xo.mouse(r)),p=O();U.call(r),o(i)}function l(){function n(){var n=Xo.touches(g);return h=S.k,n.forEach(function(n){n.identifier in v&&(v[n.identifier]=t(n))}),n}function e(){for(var t=Xo.event.changedTouches,e=0,i=t.length;i>e;++e)v[t[e].identifier]=null;var o=n(),c=Date.now();if(1===o.length){if(500>c-x){var s=o[0],l=v[s.identifier];r(2*S.k),u(s,l),d(),a(p)}x=c}else if(o.length>1){var s=o[0],f=o[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function i(){for(var n,t,e,i,o=Xo.touches(g),c=0,s=o.length;s>c;++c,i=null)if(e=o[c],i=v[e.identifier]){if(t)break;n=e,t=i}if(i){var l=(l=e[0]-n[0])*l+(l=e[1]-n[1])*l,f=m&&Math.sqrt(l/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+i[0])/2,(t[1]+i[1])/2],r(f*h)}x=null,u(n,t),a(p)}function f(){if(Xo.event.touches.length){for(var t=Xo.event.changedTouches,e=0,r=t.length;r>e;++e)delete v[t[e].identifier];for(var u in v)return void n()}b.on(M,null).on(_,null),w.on(A,s).on(L,l),k(),c(p)}var h,g=this,p=z.of(g,arguments),v={},m=0,y=Xo.event.changedTouches[0].identifier,M="touchmove.zoom-"+y,_="touchend.zoom-"+y,b=Xo.select(Go).on(M,i).on(_,f),w=Xo.select(g).on(A,null).on(L,e),k=O();U.call(g),e(),o(p)}function f(){var n=z.of(this,arguments);m?clearTimeout(m):(U.call(this),o(n)),m=setTimeout(function(){m=null,c(n)},50),d();var e=v||Xo.mouse(this);p||(p=t(e)),r(Math.pow(2,.002*Ra())*S.k),u(e,p),a(n)}function h(){p=null}function g(){var n=z.of(this,arguments),e=Xo.mouse(this),i=t(e),s=Math.log(S.k)/Math.LN2;o(n),r(Math.pow(2,Xo.event.shiftKey?Math.ceil(s)-1:Math.floor(s)+1)),u(e,i),a(n),c(n)}var p,v,m,x,M,_,b,w,S={x:0,y:0,k:1},k=[960,500],E=Da,A="mousedown.zoom",C="mousemove.zoom",N="mouseup.zoom",L="touchstart.zoom",z=y(n,"zoomstart","zoom","zoomend");return n.event=function(n){n.each(function(){var n=z.of(this,arguments),t=S;ks?Xo.select(this).transition().each("start.zoom",function(){S=this.__chart__||{x:0,y:0,k:1},o(n)}).tween("zoom:zoom",function(){var e=k[0],r=k[1],u=e/2,i=r/2,o=Xo.interpolateZoom([(u-S.x)/S.k,(i-S.y)/S.k,e/S.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=o(t),c=e/r[2];this.__chart__=S={x:u-r[0]*c,y:i-r[1]*c,k:c},a(n)}}).each("end.zoom",function(){c(n)}):(this.__chart__=S,o(n),a(n),c(n))})},n.translate=function(t){return arguments.length?(S={x:+t[0],y:+t[1],k:S.k},i(),n):[S.x,S.y]},n.scale=function(t){return arguments.length?(S={x:S.x,y:S.y,k:+t},i(),n):S.k},n.scaleExtent=function(t){return arguments.length?(E=null==t?Da:[+t[0],+t[1]],n):E},n.center=function(t){return arguments.length?(v=t&&[+t[0],+t[1]],n):v},n.size=function(t){return arguments.length?(k=t&&[+t[0],+t[1]],n):k},n.x=function(t){return arguments.length?(_=t,M=t.copy(),S={x:0,y:0,k:1},n):_},n.y=function(t){return arguments.length?(w=t,b=t.copy(),S={x:0,y:0,k:1},n):w},Xo.rebind(n,z,"on")};var Ra,Da=[0,1/0],Pa="onwheel"in Wo?(Ra=function(){return-Xo.event.deltaY*(Xo.event.deltaMode?120:1)},"wheel"):"onmousewheel"in Wo?(Ra=function(){return Xo.event.wheelDelta},"mousewheel"):(Ra=function(){return-Xo.event.detail},"MozMousePixelScroll");G.prototype.toString=function(){return this.rgb()+""},Xo.hsl=function(n,t,e){return 1===arguments.length?n instanceof Q?K(n.h,n.s,n.l):dt(""+n,mt,K):K(+n,+t,+e)};var Ua=Q.prototype=new G;Ua.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,this.l/n)},Ua.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,n*this.l)},Ua.rgb=function(){return nt(this.h,this.s,this.l)},Xo.hcl=function(n,t,e){return 1===arguments.length?n instanceof et?tt(n.h,n.c,n.l):n instanceof it?at(n.l,n.a,n.b):at((n=yt((n=Xo.rgb(n)).r,n.g,n.b)).l,n.a,n.b):tt(+n,+t,+e)};var ja=et.prototype=new G;ja.brighter=function(n){return tt(this.h,this.c,Math.min(100,this.l+Ha*(arguments.length?n:1)))},ja.darker=function(n){return tt(this.h,this.c,Math.max(0,this.l-Ha*(arguments.length?n:1)))},ja.rgb=function(){return rt(this.h,this.c,this.l).rgb()},Xo.lab=function(n,t,e){return 1===arguments.length?n instanceof it?ut(n.l,n.a,n.b):n instanceof et?rt(n.l,n.c,n.h):yt((n=Xo.rgb(n)).r,n.g,n.b):ut(+n,+t,+e)};var Ha=18,Fa=.95047,Oa=1,Ya=1.08883,Ia=it.prototype=new G;Ia.brighter=function(n){return ut(Math.min(100,this.l+Ha*(arguments.length?n:1)),this.a,this.b)},Ia.darker=function(n){return ut(Math.max(0,this.l-Ha*(arguments.length?n:1)),this.a,this.b)},Ia.rgb=function(){return ot(this.l,this.a,this.b)},Xo.rgb=function(n,t,e){return 1===arguments.length?n instanceof pt?gt(n.r,n.g,n.b):dt(""+n,gt,nt):gt(~~n,~~t,~~e)};var Za=pt.prototype=new G;Za.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),gt(Math.min(255,~~(t/n)),Math.min(255,~~(e/n)),Math.min(255,~~(r/n)))):gt(u,u,u)},Za.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),gt(~~(n*this.r),~~(n*this.g),~~(n*this.b))},Za.hsl=function(){return mt(this.r,this.g,this.b)},Za.toString=function(){return"#"+vt(this.r)+vt(this.g)+vt(this.b)};var Va=Xo.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});Va.forEach(function(n,t){Va.set(n,ft(t))}),Xo.functor=_t,Xo.xhr=wt(bt),Xo.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var o=St(n,t,null==e?r:u(e),i);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:u(n)):e},o}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),c=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(l>=s)return o;if(u)return u=!1,i;var t=l;if(34===n.charCodeAt(t)){for(var e=t;e++<s;)if(34===n.charCodeAt(e)){if(34!==n.charCodeAt(e+1))break;++e}l=e+2;var r=n.charCodeAt(e+1);return 13===r?(u=!0,10===n.charCodeAt(e+2)&&++l):10===r&&(u=!0),n.substring(t+1,e).replace(/""/g,'"')}for(;s>l;){var r=n.charCodeAt(l++),a=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(l)&&(++l,++a);else if(r!==c)continue;return n.substring(t,l-a)}return n.substring(t)}for(var r,u,i={},o={},a=[],s=n.length,l=0,f=0;(r=e())!==o;){for(var h=[];r!==i&&r!==o;)h.push(r),r=e();(!t||(h=t(h,f++)))&&a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new l,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},Xo.csv=Xo.dsv(",","text/csv"),Xo.tsv=Xo.dsv("	","text/tab-separated-values");var Xa,$a,Ba,Wa,Ja,Ga=Go[h(Go,"requestAnimationFrame")]||function(n){setTimeout(n,17)};Xo.timer=function(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,f:!1,n:null};$a?$a.n=i:Xa=i,$a=i,Ba||(Wa=clearTimeout(Wa),Ba=1,Ga(Et))},Xo.timer.flush=function(){At(),Ct()},Xo.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var Ka=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Lt);Xo.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=Xo.round(n,Nt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((0>=e?e+1:e-1)/3)))),Ka[8+e/3]};var Qa=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,nc=Xo.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=Xo.round(n,Nt(n,t))).toFixed(Math.max(0,Math.min(20,Nt(n*(1+1e-15),t))))}}),tc=Xo.time={},ec=Date;Tt.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){rc.setUTCDate.apply(this._,arguments)},setDay:function(){rc.setUTCDay.apply(this._,arguments)},setFullYear:function(){rc.setUTCFullYear.apply(this._,arguments)},setHours:function(){rc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){rc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){rc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){rc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){rc.setUTCSeconds.apply(this._,arguments)},setTime:function(){rc.setTime.apply(this._,arguments)}};var rc=Date.prototype;tc.year=Rt(function(n){return n=tc.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),tc.years=tc.year.range,tc.years.utc=tc.year.utc.range,tc.day=Rt(function(n){var t=new ec(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),tc.days=tc.day.range,tc.days.utc=tc.day.utc.range,tc.dayOfYear=function(n){var t=tc.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=tc[n]=Rt(function(n){return(n=tc.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});tc[n+"s"]=e.range,tc[n+"s"].utc=e.utc.range,tc[n+"OfYear"]=function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)}}),tc.week=tc.sunday,tc.weeks=tc.sunday.range,tc.weeks.utc=tc.sunday.utc.range,tc.weekOfYear=tc.sundayOfYear;var uc={"-":"",_:" ",0:"0"},ic=/^\s*\d+/,oc=/^%/;Xo.locale=function(n){return{numberFormat:zt(n),timeFormat:Pt(n)}};var ac=Xo.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});Xo.format=ac.numberFormat,Xo.geo={},re.prototype={s:0,t:0,add:function(n){ue(n,this.t,cc),ue(cc.s,this.s,this),this.s?this.t+=cc.t:this.s=cc.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var cc=new re;Xo.geo.stream=function(n,t){n&&sc.hasOwnProperty(n.type)?sc[n.type](n,t):ie(n,t)};var sc={Feature:function(n,t){ie(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++r<u;)ie(e[r].geometry,t)}},lc={Sphere:function(n,t){t.sphere()},Point:function(n,t){n=n.coordinates,t.point(n[0],n[1],n[2])},MultiPoint:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)n=e[r],t.point(n[0],n[1],n[2])},LineString:function(n,t){oe(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)oe(e[r],t,0)},Polygon:function(n,t){ae(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)ae(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,u=e.length;++r<u;)ie(e[r],t)}};Xo.geo.area=function(n){return fc=0,Xo.geo.stream(n,gc),fc};var fc,hc=new re,gc={sphere:function(){fc+=4*Sa},point:g,lineStart:g,lineEnd:g,polygonStart:function(){hc.reset(),gc.lineStart=ce},polygonEnd:function(){var n=2*hc;fc+=0>n?4*Sa+n:n,gc.lineStart=gc.lineEnd=gc.point=g}};Xo.geo.bounds=function(){function n(n,t){x.push(M=[l=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=se([t*Na,e*Na]);if(m){var u=fe(m,r),i=[u[1],-u[0],0],o=fe(i,u);pe(o),o=ve(o);var c=t-p,s=c>0?1:-1,v=o[0]*La*s,d=oa(c)>180;if(d^(v>s*p&&s*t>v)){var y=o[1]*La;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>s*p&&s*t>v)){var y=-o[1]*La;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t):h>=l?(l>t&&(l=t),t>h&&(h=t)):t>p?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t)}else n(t,e);m=r,p=t}function e(){_.point=t}function r(){M[0]=l,M[1]=h,_.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=oa(r)>180?r+(r>0?360:-360):r}else v=n,d=e;gc.point(n,e),t(n,e)}function i(){gc.lineStart()}function o(){u(v,d),gc.lineEnd(),oa(y)>Aa&&(l=-(h=180)),M[0]=l,M[1]=h,m=null}function a(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function s(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}var l,f,h,g,p,v,d,m,y,x,M,_={point:n,lineStart:e,lineEnd:r,polygonStart:function(){_.point=u,_.lineStart=i,_.lineEnd=o,y=0,gc.polygonStart()},polygonEnd:function(){gc.polygonEnd(),_.point=n,_.lineStart=e,_.lineEnd=r,0>hc?(l=-(h=180),f=-(g=90)):y>Aa?g=90:-Aa>y&&(f=-90),M[0]=l,M[1]=h
+}};return function(n){g=h=-(l=f=1/0),x=[],Xo.geo.stream(n,_);var t=x.length;if(t){x.sort(c);for(var e,r=1,u=x[0],i=[u];t>r;++r)e=x[r],s(e[0],u)||s(e[1],u)?(a(u[0],e[1])>a(u[0],u[1])&&(u[1]=e[1]),a(e[0],u[1])>a(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var o,e,p=-1/0,t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(o=a(u[1],e[0]))>p&&(p=o,l=e[0],h=u[1])}return x=M=null,1/0===l||1/0===f?[[0/0,0/0],[0/0,0/0]]:[[l,f],[h,g]]}}(),Xo.geo.centroid=function(n){pc=vc=dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,kc);var t=bc,e=wc,r=Sc,u=t*t+e*e+r*r;return Ca>u&&(t=xc,e=Mc,r=_c,Aa>vc&&(t=dc,e=mc,r=yc),u=t*t+e*e+r*r,Ca>u)?[0/0,0/0]:[Math.atan2(e,t)*La,X(r/Math.sqrt(u))*La]};var pc,vc,dc,mc,yc,xc,Mc,_c,bc,wc,Sc,kc={sphere:g,point:me,lineStart:xe,lineEnd:Me,polygonStart:function(){kc.lineStart=_e},polygonEnd:function(){kc.lineStart=xe}},Ec=Ee(be,ze,Te,[-Sa,-Sa/2]),Ac=1e9;Xo.geo.clipExtent=function(){var n,t,e,r,u,i,o={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(a){return arguments.length?(i=Pe(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),u&&(u.valid=!1,u=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(Xo.geo.conicEqualArea=function(){return je(He)}).raw=He,Xo.geo.albers=function(){return Xo.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},Xo.geo.albersUsa=function(){function n(n){var i=n[0],o=n[1];return t=null,e(i,o),t||(r(i,o),t)||u(i,o),t}var t,e,r,u,i=Xo.geo.albers(),o=Xo.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=Xo.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?o:u>=.166&&.234>u&&r>=-.214&&-.115>r?a:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),o.precision(t),a.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),o.scale(.35*t),a.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var s=i.scale(),l=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[l-.455*s,f-.238*s],[l+.455*s,f+.238*s]]).stream(c).point,r=o.translate([l-.307*s,f+.201*s]).clipExtent([[l-.425*s+Aa,f+.12*s+Aa],[l-.214*s-Aa,f+.234*s-Aa]]).stream(c).point,u=a.translate([l-.205*s,f+.212*s]).clipExtent([[l-.214*s+Aa,f+.166*s+Aa],[l-.115*s-Aa,f+.234*s-Aa]]).stream(c).point,n},n.scale(1070)};var Cc,Nc,Lc,zc,qc,Tc,Rc={point:g,lineStart:g,lineEnd:g,polygonStart:function(){Nc=0,Rc.lineStart=Fe},polygonEnd:function(){Rc.lineStart=Rc.lineEnd=Rc.point=g,Cc+=oa(Nc/2)}},Dc={point:Oe,lineStart:g,lineEnd:g,polygonStart:g,polygonEnd:g},Pc={point:Ze,lineStart:Ve,lineEnd:Xe,polygonStart:function(){Pc.lineStart=$e},polygonEnd:function(){Pc.point=Ze,Pc.lineStart=Ve,Pc.lineEnd=Xe}};Xo.geo.path=function(){function n(n){return n&&("function"==typeof a&&i.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=u(i)),Xo.geo.stream(n,o)),i.result()}function t(){return o=null,n}var e,r,u,i,o,a=4.5;return n.area=function(n){return Cc=0,Xo.geo.stream(n,u(Rc)),Cc},n.centroid=function(n){return dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,u(Pc)),Sc?[bc/Sc,wc/Sc]:_c?[xc/_c,Mc/_c]:yc?[dc/yc,mc/yc]:[0/0,0/0]},n.bounds=function(n){return qc=Tc=-(Lc=zc=1/0),Xo.geo.stream(n,u(Dc)),[[Lc,zc],[qc,Tc]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||Je(n):bt,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Ye:new Be(n),"function"!=typeof a&&i.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(i.pointRadius(+t),+t),n):a},n.projection(Xo.geo.albersUsa()).context(null)},Xo.geo.transform=function(n){return{stream:function(t){var e=new Ge(t);for(var r in n)e[r]=n[r];return e}}},Ge.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},Xo.geo.projection=Qe,Xo.geo.projectionMutator=nr,(Xo.geo.equirectangular=function(){return Qe(er)}).raw=er.invert=er,Xo.geo.rotation=function(n){function t(t){return t=n(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t}return n=ur(n[0]%360*Na,n[1]*Na,n.length>2?n[2]*Na:0),t.invert=function(t){return t=n.invert(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t},t},rr.invert=er,Xo.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=ur(-n[0]*Na,-n[1]*Na,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=La,n[1]*=La}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=cr((t=+r)*Na,u*Na),n):t},n.precision=function(r){return arguments.length?(e=cr(t*Na,(u=+r)*Na),n):u},n.angle(90)},Xo.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Na,u=n[1]*Na,i=t[1]*Na,o=Math.sin(r),a=Math.cos(r),c=Math.sin(u),s=Math.cos(u),l=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*o)*e+(e=s*l-c*f*a)*e),c*l+s*f*a)},Xo.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return Xo.range(Math.ceil(i/d)*d,u,d).map(h).concat(Xo.range(Math.ceil(s/m)*m,c,m).map(g)).concat(Xo.range(Math.ceil(r/p)*p,e,p).filter(function(n){return oa(n%d)>Aa}).map(l)).concat(Xo.range(Math.ceil(a/v)*v,o,v).filter(function(n){return oa(n%m)>Aa}).map(f))}var e,r,u,i,o,a,c,s,l,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(c).slice(1),h(u).reverse().slice(1),g(s).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],s=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),s>c&&(t=s,s=c,c=t),n.precision(y)):[[i,s],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(y)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,l=lr(a,o,90),f=fr(r,e,y),h=lr(s,c,90),g=fr(i,u,y),n):y},n.majorExtent([[-180,-90+Aa],[180,90-Aa]]).minorExtent([[-180,-80-Aa],[180,80+Aa]])},Xo.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=hr,u=gr;return n.distance=function(){return Xo.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},Xo.geo.interpolate=function(n,t){return pr(n[0]*Na,n[1]*Na,t[0]*Na,t[1]*Na)},Xo.geo.length=function(n){return Uc=0,Xo.geo.stream(n,jc),Uc};var Uc,jc={sphere:g,point:g,lineStart:vr,lineEnd:g,polygonStart:g,polygonEnd:g},Hc=dr(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(Xo.geo.azimuthalEqualArea=function(){return Qe(Hc)}).raw=Hc;var Fc=dr(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},bt);(Xo.geo.azimuthalEquidistant=function(){return Qe(Fc)}).raw=Fc,(Xo.geo.conicConformal=function(){return je(mr)}).raw=mr,(Xo.geo.conicEquidistant=function(){return je(yr)}).raw=yr;var Oc=dr(function(n){return 1/n},Math.atan);(Xo.geo.gnomonic=function(){return Qe(Oc)}).raw=Oc,xr.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Ea]},(Xo.geo.mercator=function(){return Mr(xr)}).raw=xr;var Yc=dr(function(){return 1},Math.asin);(Xo.geo.orthographic=function(){return Qe(Yc)}).raw=Yc;var Ic=dr(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(Xo.geo.stereographic=function(){return Qe(Ic)}).raw=Ic,_r.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Ea]},(Xo.geo.transverseMercator=function(){var n=Mr(_r),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[-n[1],n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},n.rotate([0,0])}).raw=_r,Xo.geom={},Xo.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=_t(e),i=_t(r),o=n.length,a=[],c=[];for(t=0;o>t;t++)a.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(a.sort(kr),t=0;o>t;t++)c.push([a[t][0],-a[t][1]]);var s=Sr(a),l=Sr(c),f=l[0]===s[0],h=l[l.length-1]===s[s.length-1],g=[];for(t=s.length-1;t>=0;--t)g.push(n[a[s[t]][2]]);for(t=+f;t<l.length-h;++t)g.push(n[a[l[t]][2]]);return g}var e=br,r=wr;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t)},Xo.geom.polygon=function(n){return fa(n,Zc),n};var Zc=Xo.geom.polygon.prototype=[];Zc.area=function(){for(var n,t=-1,e=this.length,r=this[e-1],u=0;++t<e;)n=r,r=this[t],u+=n[1]*r[0]-n[0]*r[1];return.5*u},Zc.centroid=function(n){var t,e,r=-1,u=this.length,i=0,o=0,a=this[u-1];for(arguments.length||(n=-1/(6*this.area()));++r<u;)t=a,a=this[r],e=t[0]*a[1]-a[0]*t[1],i+=(t[0]+a[0])*e,o+=(t[1]+a[1])*e;return[i*n,o*n]},Zc.clip=function(n){for(var t,e,r,u,i,o,a=Cr(n),c=-1,s=this.length-Cr(this),l=this[s-1];++c<s;){for(t=n.slice(),n.length=0,u=this[c],i=t[(r=t.length-a)-1],e=-1;++e<r;)o=t[e],Er(o,l,u)?(Er(i,l,u)||n.push(Ar(i,o,l,u)),n.push(o)):Er(i,l,u)&&n.push(Ar(i,o,l,u)),i=o;a&&n.push(n[0]),l=u}return n};var Vc,Xc,$c,Bc,Wc,Jc=[],Gc=[];Pr.prototype.prepare=function(){for(var n,t=this.edges,e=t.length;e--;)n=t[e].edge,n.b&&n.a||t.splice(e,1);return t.sort(jr),t.length},Br.prototype={start:function(){return this.edge.l===this.site?this.edge.a:this.edge.b},end:function(){return this.edge.l===this.site?this.edge.b:this.edge.a}},Wr.prototype={insert:function(n,t){var e,r,u;if(n){if(t.P=n,t.N=n.N,n.N&&(n.N.P=t),n.N=t,n.R){for(n=n.R;n.L;)n=n.L;n.L=t}else n.R=t;e=n}else this._?(n=Qr(this._),t.P=null,t.N=n,n.P=n.L=t,e=n):(t.P=t.N=null,this._=t,e=null);for(t.L=t.R=null,t.U=e,t.C=!0,n=t;e&&e.C;)r=e.U,e===r.L?(u=r.R,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.R&&(Gr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Kr(this,r))):(u=r.L,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.L&&(Kr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Gr(this,r))),e=n.U;this._.C=!1},remove:function(n){n.N&&(n.N.P=n.P),n.P&&(n.P.N=n.N),n.N=n.P=null;var t,e,r,u=n.U,i=n.L,o=n.R;if(e=i?o?Qr(o):i:o,u?u.L===n?u.L=e:u.R=e:this._=e,i&&o?(r=e.C,e.C=n.C,e.L=i,i.U=e,e!==o?(u=e.U,e.U=n.U,n=e.R,u.L=n,e.R=o,o.U=e):(e.U=u,u=e,n=e.R)):(r=n.C,n=e),n&&(n.U=u),!r){if(n&&n.C)return n.C=!1,void 0;do{if(n===this._)break;if(n===u.L){if(t=u.R,t.C&&(t.C=!1,u.C=!0,Gr(this,u),t=u.R),t.L&&t.L.C||t.R&&t.R.C){t.R&&t.R.C||(t.L.C=!1,t.C=!0,Kr(this,t),t=u.R),t.C=u.C,u.C=t.R.C=!1,Gr(this,u),n=this._;break}}else if(t=u.L,t.C&&(t.C=!1,u.C=!0,Kr(this,u),t=u.L),t.L&&t.L.C||t.R&&t.R.C){t.L&&t.L.C||(t.R.C=!1,t.C=!0,Gr(this,t),t=u.L),t.C=u.C,u.C=t.L.C=!1,Kr(this,u),n=this._;break}t.C=!0,n=u,u=u.U}while(!n.C);n&&(n.C=!1)}}},Xo.geom.voronoi=function(n){function t(n){var t=new Array(n.length),r=a[0][0],u=a[0][1],i=a[1][0],o=a[1][1];return nu(e(n),a).cells.forEach(function(e,a){var c=e.edges,s=e.site,l=t[a]=c.length?c.map(function(n){var t=n.start();return[t.x,t.y]}):s.x>=r&&s.x<=i&&s.y>=u&&s.y<=o?[[r,o],[i,o],[i,u],[r,u]]:[];l.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Aa)*Aa,y:Math.round(o(n,t)/Aa)*Aa,i:t}})}var r=br,u=wr,i=r,o=u,a=Kc;return n?t(n):(t.links=function(n){return nu(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return nu(e(n)).cells.forEach(function(e,r){for(var u,i,o=e.site,a=e.edges.sort(jr),c=-1,s=a.length,l=a[s-1].edge,f=l.l===o?l.r:l.l;++c<s;)u=l,i=f,l=a[c].edge,f=l.l===o?l.r:l.l,r<i.i&&r<f.i&&eu(o,i,f)<0&&t.push([n[r],n[i.i],n[f.i]])}),t},t.x=function(n){return arguments.length?(i=_t(r=n),t):r},t.y=function(n){return arguments.length?(o=_t(u=n),t):u},t.clipExtent=function(n){return arguments.length?(a=null==n?Kc:n,t):a===Kc?null:a},t.size=function(n){return arguments.length?t.clipExtent(n&&[[0,0],n]):a===Kc?null:a&&a[1]},t)};var Kc=[[-1e6,-1e6],[1e6,1e6]];Xo.geom.delaunay=function(n){return Xo.geom.voronoi().triangles(n)},Xo.geom.quadtree=function(n,t,e,r,u){function i(n){function i(n,t,e,r,u,i,o,a){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,l=n.y;if(null!=c)if(oa(c-e)+oa(l-r)<.01)s(n,t,e,r,u,i,o,a);else{var f=n.point;n.x=n.y=n.point=null,s(n,f,c,l,u,i,o,a),s(n,t,e,r,u,i,o,a)}else n.x=e,n.y=r,n.point=t}else s(n,t,e,r,u,i,o,a)}function s(n,t,e,r,u,o,a,c){var s=.5*(u+a),l=.5*(o+c),f=e>=s,h=r>=l,g=(h<<1)+f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=iu()),f?u=s:a=s,h?o=l:c=l,i(n,t,e,r,u,o,a,c)}var l,f,h,g,p,v,d,m,y,x=_t(a),M=_t(c);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,o)for(g=0;p>g;++g)l=n[g],l.x<v&&(v=l.x),l.y<d&&(d=l.y),l.x>m&&(m=l.x),l.y>y&&(y=l.y),f.push(l.x),h.push(l.y);else for(g=0;p>g;++g){var _=+x(l=n[g],g),b=+M(l,g);v>_&&(v=_),d>b&&(d=b),_>m&&(m=_),b>y&&(y=b),f.push(_),h.push(b)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=iu();if(k.add=function(n){i(k,n,+x(n,++g),+M(n,g),v,d,m,y)},k.visit=function(n){ou(n,k,v,d,m,y)},g=-1,null==t){for(;++g<p;)i(k,n[g],f[g],h[g],v,d,m,y);--g}else n.forEach(k.add);return f=h=n=l=null,k}var o,a=br,c=wr;return(o=arguments.length)?(a=ru,c=uu,3===o&&(u=e,r=t,e=t=0),i(n)):(i.x=function(n){return arguments.length?(a=n,i):a},i.y=function(n){return arguments.length?(c=n,i):c},i.extent=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=+n[0][0],e=+n[0][1],r=+n[1][0],u=+n[1][1]),i):null==t?null:[[t,e],[r,u]]},i.size=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=e=0,r=+n[0],u=+n[1]),i):null==t?null:[r-t,u-e]},i)},Xo.interpolateRgb=au,Xo.interpolateObject=cu,Xo.interpolateNumber=su,Xo.interpolateString=lu;var Qc=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;Xo.interpolate=fu,Xo.interpolators=[function(n,t){var e=typeof t;return("string"===e?Va.has(t)||/^(#|rgb\(|hsl\()/.test(t)?au:lu:t instanceof G?au:"object"===e?Array.isArray(t)?hu:cu:su)(n,t)}],Xo.interpolateArray=hu;var ns=function(){return bt},ts=Xo.map({linear:ns,poly:xu,quad:function(){return du},cubic:function(){return mu},sin:function(){return Mu},exp:function(){return _u},circle:function(){return bu},elastic:wu,back:Su,bounce:function(){return ku}}),es=Xo.map({"in":bt,out:pu,"in-out":vu,"out-in":function(n){return vu(pu(n))}});Xo.ease=function(n){var t=n.indexOf("-"),e=t>=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=ts.get(e)||ns,r=es.get(r)||bt,gu(r(e.apply(null,$o.call(arguments,1))))},Xo.interpolateHcl=Eu,Xo.interpolateHsl=Au,Xo.interpolateLab=Cu,Xo.interpolateRound=Nu,Xo.transform=function(n){var t=Wo.createElementNS(Xo.ns.prefix.svg,"g");return(Xo.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Lu(e?e.matrix:rs)})(n)},Lu.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var rs={a:1,b:0,c:0,d:1,e:0,f:0};Xo.interpolateTransform=Ru,Xo.layout={},Xo.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e<r;)t.push(Uu(n[e]));return t}},Xo.layout.chord=function(){function n(){var n,s,f,h,g,p={},v=[],d=Xo.range(i),m=[];for(e=[],r=[],n=0,h=-1;++h<i;){for(s=0,g=-1;++g<i;)s+=u[h][g];v.push(s),m.push(Xo.range(i)),n+=s}for(o&&d.sort(function(n,t){return o(v[n],v[t])}),a&&m.forEach(function(n,t){n.sort(function(n,e){return a(u[t][n],u[t][e])})}),n=(ka-l*i)/n,s=0,h=-1;++h<i;){for(f=s,g=-1;++g<i;){var y=d[h],x=m[y][g],M=u[y][x],_=s,b=s+=M*n;p[y+"-"+x]={index:y,subindex:x,startAngle:_,endAngle:b,value:M}}r[y]={index:y,startAngle:f,endAngle:s,value:(s-f)/n},s+=l}for(h=-1;++h<i;)for(g=h-1;++g<i;){var w=p[h+"-"+g],S=p[g+"-"+h];(w.value||S.value)&&e.push(w.value<S.value?{source:S,target:w}:{source:w,target:S})}c&&t()}function t(){e.sort(function(n,t){return c((n.source.value+n.target.value)/2,(t.source.value+t.target.value)/2)})}var e,r,u,i,o,a,c,s={},l=0;return s.matrix=function(n){return arguments.length?(i=(u=n)&&u.length,e=r=null,s):u},s.padding=function(n){return arguments.length?(l=n,e=r=null,s):l},s.sortGroups=function(n){return arguments.length?(o=n,e=r=null,s):o},s.sortSubgroups=function(n){return arguments.length?(a=n,e=null,s):a},s.sortChords=function(n){return arguments.length?(c=n,e&&t(),s):c},s.chords=function(){return e||n(),e},s.groups=function(){return r||n(),r},s},Xo.layout.force=function(){function n(n){return function(t,e,r,u){if(t.point!==n){var i=t.cx-n.x,o=t.cy-n.y,a=u-e,c=i*i+o*o;if(c>a*a/d){if(p>c){var s=t.charge/c;n.px-=i*s,n.py-=o*s}return!0}if(t.point&&c&&p>c){var s=t.pointCharge/c;n.px-=i*s,n.py-=o*s}}return!t.charge}}function t(n){n.px=Xo.event.x,n.py=Xo.event.y,a.resume()}var e,r,u,i,o,a={},c=Xo.dispatch("start","tick","end"),s=[1,1],l=.9,f=us,h=is,g=-30,p=os,v=.1,d=.64,m=[],y=[];return a.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,a,f,h,p,d,x,M,_=m.length,b=y.length;for(e=0;b>e;++e)a=y[e],f=a.source,h=a.target,x=h.x-f.x,M=h.y-f.y,(p=x*x+M*M)&&(p=r*i[e]*((p=Math.sqrt(p))-u[e])/p,x*=p,M*=p,h.x-=x*(d=f.weight/(h.weight+f.weight)),h.y-=M*d,f.x+=x*(d=1-d),f.y+=M*d);if((d=r*v)&&(x=s[0]/2,M=s[1]/2,e=-1,d))for(;++e<_;)a=m[e],a.x+=(x-a.x)*d,a.y+=(M-a.y)*d;if(g)for(Zu(t=Xo.geom.quadtree(m),r,o),e=-1;++e<_;)(a=m[e]).fixed||t.visit(n(a));for(e=-1;++e<_;)a=m[e],a.fixed?(a.x=a.px,a.y=a.py):(a.x-=(a.px-(a.px=a.x))*l,a.y-=(a.py-(a.py=a.y))*l);c.tick({type:"tick",alpha:r})},a.nodes=function(n){return arguments.length?(m=n,a):m},a.links=function(n){return arguments.length?(y=n,a):y},a.size=function(n){return arguments.length?(s=n,a):s},a.linkDistance=function(n){return arguments.length?(f="function"==typeof n?n:+n,a):f},a.distance=a.linkDistance,a.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,a):h},a.friction=function(n){return arguments.length?(l=+n,a):l},a.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,a):g},a.chargeDistance=function(n){return arguments.length?(p=n*n,a):Math.sqrt(p)},a.gravity=function(n){return arguments.length?(v=+n,a):v},a.theta=function(n){return arguments.length?(d=n*n,a):Math.sqrt(d)},a.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),Xo.timer(a.tick)),a):r},a.start=function(){function n(n,r){if(!e){for(e=new Array(c),a=0;c>a;++a)e[a]=[];for(a=0;s>a;++a){var u=y[a];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var i,o=e[t],a=-1,s=o.length;++a<s;)if(!isNaN(i=o[a][n]))return i;return Math.random()*r}var t,e,r,c=m.length,l=y.length,p=s[0],v=s[1];for(t=0;c>t;++t)(r=m[t]).index=t,r.weight=0;for(t=0;l>t;++t)r=y[t],"number"==typeof r.source&&(r.source=m[r.source]),"number"==typeof r.target&&(r.target=m[r.target]),++r.source.weight,++r.target.weight;for(t=0;c>t;++t)r=m[t],isNaN(r.x)&&(r.x=n("x",p)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof f)for(t=0;l>t;++t)u[t]=+f.call(this,y[t],t);else for(t=0;l>t;++t)u[t]=f;if(i=[],"function"==typeof h)for(t=0;l>t;++t)i[t]=+h.call(this,y[t],t);else for(t=0;l>t;++t)i[t]=h;if(o=[],"function"==typeof g)for(t=0;c>t;++t)o[t]=+g.call(this,m[t],t);else for(t=0;c>t;++t)o[t]=g;return a.resume()},a.resume=function(){return a.alpha(.1)},a.stop=function(){return a.alpha(0)},a.drag=function(){return e||(e=Xo.behavior.drag().origin(bt).on("dragstart.force",Fu).on("drag.force",t).on("dragend.force",Ou)),arguments.length?(this.on("mouseover.force",Yu).on("mouseout.force",Iu).call(e),void 0):e},Xo.rebind(a,c,"on")};var us=20,is=1,os=1/0;Xo.layout.hierarchy=function(){function n(t,o,a){var c=u.call(e,t,o);if(t.depth=o,a.push(t),c&&(s=c.length)){for(var s,l,f=-1,h=t.children=new Array(s),g=0,p=o+1;++f<s;)l=h[f]=n(c[f],p,a),l.parent=t,g+=l.value;r&&h.sort(r),i&&(t.value=g)}else delete t.children,i&&(t.value=+i.call(e,t,o)||0);return t}function t(n,r){var u=n.children,o=0;if(u&&(a=u.length))for(var a,c=-1,s=r+1;++c<a;)o+=t(u[c],s);else i&&(o=+i.call(e,n,r)||0);return i&&(n.value=o),o}function e(t){var e=[];return n(t,0,e),e}var r=Bu,u=Xu,i=$u;return e.sort=function(n){return arguments.length?(r=n,e):r},e.children=function(n){return arguments.length?(u=n,e):u},e.value=function(n){return arguments.length?(i=n,e):i},e.revalue=function(n){return t(n,0),n},e},Xo.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(o=i.length)){var o,a,c,s=-1;for(r=t.value?r/t.value:0;++s<o;)n(a=i[s],e,c=a.value*r,u),e+=c}}function t(n){var e=n.children,r=0;if(e&&(u=e.length))for(var u,i=-1;++i<u;)r=Math.max(r,t(e[i]));return 1+r}function e(e,i){var o=r.call(this,e,i);return n(o[0],0,u[0],u[1]/t(o[0])),o}var r=Xo.layout.hierarchy(),u=[1,1];return e.size=function(n){return arguments.length?(u=n,e):u},Vu(e,r)},Xo.layout.pie=function(){function n(i){var o=i.map(function(e,r){return+t.call(n,e,r)}),a=+("function"==typeof r?r.apply(this,arguments):r),c=(("function"==typeof u?u.apply(this,arguments):u)-a)/Xo.sum(o),s=Xo.range(i.length);null!=e&&s.sort(e===as?function(n,t){return o[t]-o[n]}:function(n,t){return e(i[n],i[t])});var l=[];return s.forEach(function(n){var t;l[n]={data:i[n],value:t=o[n],startAngle:a,endAngle:a+=t*c}}),l}var t=Number,e=as,r=0,u=ka;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n};var as={};Xo.layout.stack=function(){function n(a,c){var s=a.map(function(e,r){return t.call(n,e,r)}),l=s.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),o.call(n,t,e)]})}),f=e.call(n,l,c);s=Xo.permute(s,f),l=Xo.permute(l,f);var h,g,p,v=r.call(n,l,c),d=s.length,m=s[0].length;for(g=0;m>g;++g)for(u.call(n,s[0][g],p=v[g],l[0][g][1]),h=1;d>h;++h)u.call(n,s[h][g],p+=l[h-1][g][1],l[h][g][1]);return a}var t=bt,e=Qu,r=ni,u=Ku,i=Ju,o=Gu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:cs.get(t)||Qu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:ss.get(t)||ni,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(u=t,n):u},n};var cs=Xo.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(ti),i=n.map(ei),o=Xo.range(r).sort(function(n,t){return u[n]-u[t]}),a=0,c=0,s=[],l=[];for(t=0;r>t;++t)e=o[t],c>a?(a+=i[e],s.push(e)):(c+=i[e],l.push(e));return l.reverse().concat(s)},reverse:function(n){return Xo.range(n.length).reverse()},"default":Qu}),ss=Xo.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,o=[],a=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;i>e;++e)c[e]=(a-o[e])/2;return c},wiggle:function(n){var t,e,r,u,i,o,a,c,s,l=n.length,f=n[0],h=f.length,g=[];for(g[0]=c=s=0,e=1;h>e;++e){for(t=0,u=0;l>t;++t)u+=n[t][e][1];for(t=0,i=0,a=f[e][0]-f[e-1][0];l>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;i+=o*n[t][e][1]}g[e]=c-=u?i/u*a:0,s>c&&(s=c)}for(e=0;h>e;++e)g[e]-=s;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,o=1/u,a=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=o}for(e=0;i>e;++e)a[e]=0;return a},zero:ni});Xo.layout.histogram=function(){function n(n,i){for(var o,a,c=[],s=n.map(e,this),l=r.call(this,s,i),f=u.call(this,l,s,i),i=-1,h=s.length,g=f.length-1,p=t?1:1/h;++i<g;)o=c[i]=[],o.dx=f[i+1]-(o.x=f[i]),o.y=0;if(g>0)for(i=-1;++i<h;)a=s[i],a>=l[0]&&a<=l[1]&&(o=c[Xo.bisect(f,a,1,g)-1],o.y+=p,o.push(n[i]));return c}var t=!0,e=Number,r=oi,u=ui;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=_t(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return ii(n,t)}:_t(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},Xo.layout.tree=function(){function n(n,i){function o(n,t){var r=n.children,u=n._tree;if(r&&(i=r.length)){for(var i,a,s,l=r[0],f=l,h=-1;++h<i;)s=r[h],o(s,a),f=c(s,a,f),a=s;vi(n);var g=.5*(l._tree.prelim+s._tree.prelim);t?(u.prelim=t._tree.prelim+e(n,t),u.mod=u.prelim-g):u.prelim=g}else t&&(u.prelim=t._tree.prelim+e(n,t))}function a(n,t){n.x=n._tree.prelim+t;var e=n.children;if(e&&(r=e.length)){var r,u=-1;for(t+=n._tree.mod;++u<r;)a(e[u],t)}}function c(n,t,r){if(t){for(var u,i=n,o=n,a=t,c=n.parent.children[0],s=i._tree.mod,l=o._tree.mod,f=a._tree.mod,h=c._tree.mod;a=si(a),i=ci(i),a&&i;)c=ci(c),o=si(o),o._tree.ancestor=n,u=a._tree.prelim+f-i._tree.prelim-s+e(a,i),u>0&&(di(mi(a,n,r),n,u),s+=u,l+=u),f+=a._tree.mod,s+=i._tree.mod,h+=c._tree.mod,l+=o._tree.mod;a&&!si(o)&&(o._tree.thread=a,o._tree.mod+=f-l),i&&!ci(c)&&(c._tree.thread=i,c._tree.mod+=s-h,r=n)}return r}var s=t.call(this,n,i),l=s[0];pi(l,function(n,t){n._tree={ancestor:n,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),o(l),a(l,-l._tree.prelim);var f=li(l,hi),h=li(l,fi),g=li(l,gi),p=f.x-e(f,h)/2,v=h.x+e(h,f)/2,d=g.depth||1;return pi(l,u?function(n){n.x*=r[0],n.y=n.depth*r[1],delete n._tree}:function(n){n.x=(n.x-p)/(v-p)*r[0],n.y=n.depth/d*r[1],delete n._tree}),s}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.pack=function(){function n(n,i){var o=e.call(this,n,i),a=o[0],c=u[0],s=u[1],l=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,pi(a,function(n){n.r=+l(n.value)}),pi(a,bi),r){var f=r*(t?1:Math.max(2*a.r/c,2*a.r/s))/2;pi(a,function(n){n.r+=f}),pi(a,bi),pi(a,function(n){n.r-=f})}return ki(a,c/2,s/2,t?1:1/Math.max(2*a.r/c,2*a.r/s)),o}var t,e=Xo.layout.hierarchy().sort(yi),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Vu(n,e)},Xo.layout.cluster=function(){function n(n,i){var o,a=t.call(this,n,i),c=a[0],s=0;pi(c,function(n){var t=n.children;t&&t.length?(n.x=Ci(t),n.y=Ai(t)):(n.x=o?s+=e(n,o):0,n.y=0,o=n)});var l=Ni(c),f=Li(c),h=l.x-e(l,f)/2,g=f.x+e(f,l)/2;return pi(c,u?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),a}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++u<i;)r=(e=n[u]).value*(0>t?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var o,a,c,s=f(e),l=[],h=i.slice(),p=1/0,v="slice"===g?s.dx:"dice"===g?s.dy:"slice-dice"===g?1&e.depth?s.dy:s.dx:Math.min(s.dx,s.dy);for(n(h,s.dx*s.dy/e.value),l.area=0;(c=h.length)>0;)l.push(o=h[c-1]),l.area+=o.area,"squarify"!==g||(a=r(l,v))<=p?(h.pop(),p=a):(l.area-=l.pop().area,u(l,v,s,!1),v=Math.min(s.dx,s.dy),l.length=l.area=0,p=1/0);l.length&&(u(l,v,s,!0),l.length=l.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,o=f(t),a=r.slice(),c=[];for(n(a,o.dx*o.dy/t.value),c.area=0;i=a.pop();)c.push(i),c.area+=i.area,null!=i.z&&(u(c,i.z?o.dx:o.dy,o,!a.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,o=-1,a=n.length;++o<a;)(e=n[o].area)&&(i>e&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,o=n.length,a=e.x,s=e.y,l=t?c(n.area/t):0;if(t==e.dx){for((r||l>e.dy)&&(l=e.dy);++i<o;)u=n[i],u.x=a,u.y=s,u.dy=l,a+=u.dx=Math.min(e.x+e.dx-a,l?c(u.area/l):0);u.z=!0,u.dx+=e.x+e.dx-a,e.y+=l,e.dy-=l}else{for((r||l>e.dx)&&(l=e.dx);++i<o;)u=n[i],u.x=a,u.y=s,u.dx=l,s+=u.dy=Math.min(e.y+e.dy-s,l?c(u.area/l):0);u.z=!1,u.dy+=e.y+e.dy-s,e.x+=l,e.dx-=l}}function i(r){var u=o||a(r),i=u[0];return i.x=0,i.y=0,i.dx=s[0],i.dy=s[1],o&&a.revalue(i),n([i],i.dx*i.dy/i.value),(o?e:t)(i),h&&(o=u),u}var o,a=Xo.layout.hierarchy(),c=Math.round,s=[1,1],l=null,f=zi,h=!1,g="squarify",p=.5*(1+Math.sqrt(5));return i.size=function(n){return arguments.length?(s=n,i):s},i.padding=function(n){function t(t){var e=n.call(i,t,t.depth);return null==e?zi(t):qi(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return qi(t,n)}if(!arguments.length)return l;var r;return f=null==(l=n)?zi:"function"==(r=typeof n)?t:"number"===r?(n=[n,n,n,n],e):e,i},i.round=function(n){return arguments.length?(c=n?Math.round:Number,i):c!=Number},i.sticky=function(n){return arguments.length?(h=n,o=null,i):h},i.ratio=function(n){return arguments.length?(p=n,i):p},i.mode=function(n){return arguments.length?(g=n+"",i):g},Vu(i,a)},Xo.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=Xo.random.normal.apply(Xo,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=Xo.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},Xo.scale={};var ls={floor:bt,ceil:bt};Xo.scale.linear=function(){return Hi([0,1],[0,1],fu,!1)};var fs={s:1,g:1,p:1,r:1,e:1};Xo.scale.log=function(){return $i(Xo.scale.linear().domain([0,1]),10,!0,[1,10])};var hs=Xo.format(".0e"),gs={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};Xo.scale.pow=function(){return Bi(Xo.scale.linear(),1,[0,1])},Xo.scale.sqrt=function(){return Xo.scale.pow().exponent(.5)},Xo.scale.ordinal=function(){return Ji([],{t:"range",a:[[]]})},Xo.scale.category10=function(){return Xo.scale.ordinal().range(ps)},Xo.scale.category20=function(){return Xo.scale.ordinal().range(vs)},Xo.scale.category20b=function(){return Xo.scale.ordinal().range(ds)},Xo.scale.category20c=function(){return Xo.scale.ordinal().range(ms)};var ps=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(ht),vs=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(ht),ds=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(ht),ms=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(ht);Xo.scale.quantile=function(){return Gi([],[])
+},Xo.scale.quantize=function(){return Ki(0,1,[0,1])},Xo.scale.threshold=function(){return Qi([.5],[0,1])},Xo.scale.identity=function(){return no([0,1])},Xo.svg={},Xo.svg.arc=function(){function n(){var n=t.apply(this,arguments),i=e.apply(this,arguments),o=r.apply(this,arguments)+ys,a=u.apply(this,arguments)+ys,c=(o>a&&(c=o,o=a,a=c),a-o),s=Sa>c?"0":"1",l=Math.cos(o),f=Math.sin(o),h=Math.cos(a),g=Math.sin(a);return c>=xs?n?"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"Z":n?"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+s+",0 "+n*l+","+n*f+"Z":"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L0,0"+"Z"}var t=to,e=eo,r=ro,u=uo;return n.innerRadius=function(e){return arguments.length?(t=_t(e),n):t},n.outerRadius=function(t){return arguments.length?(e=_t(t),n):e},n.startAngle=function(t){return arguments.length?(r=_t(t),n):r},n.endAngle=function(t){return arguments.length?(u=_t(t),n):u},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,i=(r.apply(this,arguments)+u.apply(this,arguments))/2+ys;return[Math.cos(i)*n,Math.sin(i)*n]},n};var ys=-Ea,xs=ka-Aa;Xo.svg.line=function(){return io(bt)};var Ms=Xo.map({linear:oo,"linear-closed":ao,step:co,"step-before":so,"step-after":lo,basis:mo,"basis-open":yo,"basis-closed":xo,bundle:Mo,cardinal:go,"cardinal-open":fo,"cardinal-closed":ho,monotone:Eo});Ms.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var _s=[0,2/3,1/3,0],bs=[0,1/3,2/3,0],ws=[0,1/6,2/3,1/6];Xo.svg.line.radial=function(){var n=io(Ao);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},so.reverse=lo,lo.reverse=so,Xo.svg.area=function(){return Co(bt)},Xo.svg.area.radial=function(){var n=Co(Ao);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},Xo.svg.chord=function(){function n(n,a){var c=t(this,i,n,a),s=t(this,o,n,a);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,s)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,s.r,s.p0)+r(s.r,s.p1,s.a1-s.a0)+u(s.r,s.p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=a.call(n,u,r),o=c.call(n,u,r)+ys,l=s.call(n,u,r)+ys;return{r:i,a0:o,a1:l,p0:[i*Math.cos(o),i*Math.sin(o)],p1:[i*Math.cos(l),i*Math.sin(l)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Sa)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=hr,o=gr,a=No,c=ro,s=uo;return n.radius=function(t){return arguments.length?(a=_t(t),n):a},n.source=function(t){return arguments.length?(i=_t(t),n):i},n.target=function(t){return arguments.length?(o=_t(t),n):o},n.startAngle=function(t){return arguments.length?(c=_t(t),n):c},n.endAngle=function(t){return arguments.length?(s=_t(t),n):s},n},Xo.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),o=e.call(this,n,u),a=(i.y+o.y)/2,c=[i,{x:i.x,y:a},{x:o.x,y:a},o];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=hr,e=gr,r=Lo;return n.source=function(e){return arguments.length?(t=_t(e),n):t},n.target=function(t){return arguments.length?(e=_t(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},Xo.svg.diagonal.radial=function(){var n=Xo.svg.diagonal(),t=Lo,e=n.projection;return n.projection=function(n){return arguments.length?e(zo(t=n)):t},n},Xo.svg.symbol=function(){function n(n,r){return(Ss.get(t.call(this,n,r))||Ro)(e.call(this,n,r))}var t=To,e=qo;return n.type=function(e){return arguments.length?(t=_t(e),n):t},n.size=function(t){return arguments.length?(e=_t(t),n):e},n};var Ss=Xo.map({circle:Ro,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Cs)),e=t*Cs;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});Xo.svg.symbolTypes=Ss.keys();var ks,Es,As=Math.sqrt(3),Cs=Math.tan(30*Na),Ns=[],Ls=0;Ns.call=da.call,Ns.empty=da.empty,Ns.node=da.node,Ns.size=da.size,Xo.transition=function(n){return arguments.length?ks?n.transition():n:xa.transition()},Xo.transition.prototype=Ns,Ns.select=function(n){var t,e,r,u=this.id,i=[];n=M(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]);for(var c=this[o],s=-1,l=c.length;++s<l;)(r=c[s])&&(e=n.call(r,r.__data__,s,o))?("__data__"in r&&(e.__data__=r.__data__),jo(e,s,u,r.__transition__[u]),t.push(e)):t.push(null)}return Do(i,u)},Ns.selectAll=function(n){var t,e,r,u,i,o=this.id,a=[];n=_(n);for(var c=-1,s=this.length;++c<s;)for(var l=this[c],f=-1,h=l.length;++f<h;)if(r=l[f]){i=r.__transition__[o],e=n.call(r,r.__data__,f,c),a.push(t=[]);for(var g=-1,p=e.length;++g<p;)(u=e[g])&&jo(u,g,o,i),t.push(u)}return Do(a,o)},Ns.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=q(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]);for(var e=this[i],a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return Do(u,this.id)},Ns.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):R(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Ns.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?Ru:fu,a=Xo.ns.qualify(n);return Po(this,"attr."+n,t,a.local?i:u)},Ns.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=Xo.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Ns.style=function(n,t,e){function r(){this.style.removeProperty(n)}function u(t){return null==t?r:(t+="",function(){var r,u=Go.getComputedStyle(this,null).getPropertyValue(n);return u!==t&&(r=fu(u,t),function(t){this.style.setProperty(n,r(t),e)})})}var i=arguments.length;if(3>i){if("string"!=typeof n){2>i&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}return Po(this,"style."+n,t,u)},Ns.styleTween=function(n,t,e){function r(r,u){var i=t.call(this,r,u,Go.getComputedStyle(this,null).getPropertyValue(n));return i&&function(t){this.style.setProperty(n,i(t),e)}}return arguments.length<3&&(e=""),this.tween("style."+n,r)},Ns.text=function(n){return Po(this,"text",n,Uo)},Ns.remove=function(){return this.each("end.transition",function(){var n;this.__transition__.count<2&&(n=this.parentNode)&&n.removeChild(this)})},Ns.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=Xo.ease.apply(Xo,arguments)),R(this,function(e){e.__transition__[t].ease=n}))},Ns.delay=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].delay=+n.call(e,e.__data__,r,u)}:(n=+n,function(e){e.__transition__[t].delay=n}))},Ns.duration=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,u))}:(n=Math.max(1,n),function(e){e.__transition__[t].duration=n}))},Ns.each=function(n,t){var e=this.id;if(arguments.length<2){var r=Es,u=ks;ks=e,R(this,function(t,r,u){Es=t.__transition__[e],n.call(t,t.__data__,r,u)}),Es=r,ks=u}else R(this,function(r){var u=r.__transition__[e];(u.event||(u.event=Xo.dispatch("start","end"))).on(n,t)});return this},Ns.transition=function(){for(var n,t,e,r,u=this.id,i=++Ls,o=[],a=0,c=this.length;c>a;a++){o.push(n=[]);for(var t=this[a],s=0,l=t.length;l>s;s++)(e=t[s])&&(r=Object.create(e.__transition__[u]),r.delay+=r.duration,jo(e,s,i,r)),n.push(e)}return Do(o,i)},Xo.svg.axis=function(){function n(n){n.each(function(){var n,s=Xo.select(this),l=this.__chart__||e,f=this.__chart__=e.copy(),h=null==c?f.ticks?f.ticks.apply(f,a):f.domain():c,g=null==t?f.tickFormat?f.tickFormat.apply(f,a):bt:t,p=s.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Aa),d=Xo.transition(p.exit()).style("opacity",Aa).remove(),m=Xo.transition(p).style("opacity",1),y=Ri(f),x=s.selectAll(".domain").data([0]),M=(x.enter().append("path").attr("class","domain"),Xo.transition(x));v.append("line"),v.append("text");var _=v.select("line"),b=m.select("line"),w=p.select("text").text(g),S=v.select("text"),k=m.select("text");switch(r){case"bottom":n=Ho,_.attr("y2",u),S.attr("y",Math.max(u,0)+o),b.attr("x2",0).attr("y2",u),k.attr("x",0).attr("y",Math.max(u,0)+o),w.attr("dy",".71em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+i+"V0H"+y[1]+"V"+i);break;case"top":n=Ho,_.attr("y2",-u),S.attr("y",-(Math.max(u,0)+o)),b.attr("x2",0).attr("y2",-u),k.attr("x",0).attr("y",-(Math.max(u,0)+o)),w.attr("dy","0em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+-i+"V0H"+y[1]+"V"+-i);break;case"left":n=Fo,_.attr("x2",-u),S.attr("x",-(Math.max(u,0)+o)),b.attr("x2",-u).attr("y2",0),k.attr("x",-(Math.max(u,0)+o)).attr("y",0),w.attr("dy",".32em").style("text-anchor","end"),M.attr("d","M"+-i+","+y[0]+"H0V"+y[1]+"H"+-i);break;case"right":n=Fo,_.attr("x2",u),S.attr("x",Math.max(u,0)+o),b.attr("x2",u).attr("y2",0),k.attr("x",Math.max(u,0)+o).attr("y",0),w.attr("dy",".32em").style("text-anchor","start"),M.attr("d","M"+i+","+y[0]+"H0V"+y[1]+"H"+i)}if(f.rangeBand){var E=f,A=E.rangeBand()/2;l=f=function(n){return E(n)+A}}else l.rangeBand?l=f:d.call(n,f);v.call(n,l),m.call(n,f)})}var t,e=Xo.scale.linear(),r=zs,u=6,i=6,o=3,a=[10],c=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in qs?t+"":zs,n):r},n.ticks=function(){return arguments.length?(a=arguments,n):a},n.tickValues=function(t){return arguments.length?(c=t,n):c},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var zs="bottom",qs={top:1,right:1,bottom:1,left:1};Xo.svg.brush=function(){function n(i){i.each(function(){var i=Xo.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=i.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),i.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=i.selectAll(".resize").data(p,bt);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Ts[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,f=Xo.transition(i),h=Xo.transition(o);c&&(l=Ri(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),e(f)),s&&(l=Ri(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),r(f)),t(f)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+l[+/e$/.test(n)]+","+f[+/^s/.test(n)]+")"})}function e(n){n.select(".extent").attr("x",l[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",l[1]-l[0])}function r(n){n.select(".extent").attr("y",f[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",f[1]-f[0])}function u(){function u(){32==Xo.event.keyCode&&(C||(x=null,L[0]-=l[1],L[1]-=f[1],C=2),d())}function p(){32==Xo.event.keyCode&&2==C&&(L[0]+=l[1],L[1]+=f[1],C=0,d())}function v(){var n=Xo.mouse(_),u=!1;M&&(n[0]+=M[0],n[1]+=M[1]),C||(Xo.event.altKey?(x||(x=[(l[0]+l[1])/2,(f[0]+f[1])/2]),L[0]=l[+(n[0]<x[0])],L[1]=f[+(n[1]<x[1])]):x=null),E&&m(n,c,0)&&(e(S),u=!0),A&&m(n,s,1)&&(r(S),u=!0),u&&(t(S),w({type:"brush",mode:C?"move":"resize"}))}function m(n,t,e){var r,u,a=Ri(t),c=a[0],s=a[1],p=L[e],v=e?f:l,d=v[1]-v[0];return C&&(c-=p,s-=d+p),r=(e?g:h)?Math.max(c,Math.min(s,n[e])):n[e],C?u=(r+=p)+d:(x&&(p=Math.max(c,Math.min(s,2*x[e]-r))),r>p?(u=r,r=p):u=p),v[0]!=r||v[1]!=u?(e?o=null:i=null,v[0]=r,v[1]=u,!0):void 0}function y(){v(),S.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),Xo.select("body").style("cursor",null),z.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),N(),w({type:"brushend"})}var x,M,_=this,b=Xo.select(Xo.event.target),w=a.of(_,arguments),S=Xo.select(_),k=b.datum(),E=!/^(n|s)$/.test(k)&&c,A=!/^(e|w)$/.test(k)&&s,C=b.classed("extent"),N=O(),L=Xo.mouse(_),z=Xo.select(Go).on("keydown.brush",u).on("keyup.brush",p);if(Xo.event.changedTouches?z.on("touchmove.brush",v).on("touchend.brush",y):z.on("mousemove.brush",v).on("mouseup.brush",y),S.interrupt().selectAll("*").interrupt(),C)L[0]=l[0]-L[0],L[1]=f[0]-L[1];else if(k){var q=+/w$/.test(k),T=+/^n/.test(k);M=[l[1-q]-L[0],f[1-T]-L[1]],L[0]=l[q],L[1]=f[T]}else Xo.event.altKey&&(x=L.slice());S.style("pointer-events","none").selectAll(".resize").style("display",null),Xo.select("body").style("cursor",b.style("cursor")),w({type:"brushstart"}),v()}var i,o,a=y(n,"brushstart","brush","brushend"),c=null,s=null,l=[0,0],f=[0,0],h=!0,g=!0,p=Rs[0];return n.event=function(n){n.each(function(){var n=a.of(this,arguments),t={x:l,y:f,i:i,j:o},e=this.__chart__||t;this.__chart__=t,ks?Xo.select(this).transition().each("start.brush",function(){i=e.i,o=e.j,l=e.x,f=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=hu(l,t.x),r=hu(f,t.y);return i=o=null,function(u){l=t.x=e(u),f=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){i=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,p=Rs[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,p=Rs[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(h=!!t[0],g=!!t[1]):c?h=!!t:s&&(g=!!t),n):c&&s?[h,g]:c?h:s?g:null},n.extent=function(t){var e,r,u,a,h;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),i=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(h=e,e=r,r=h),(e!=l[0]||r!=l[1])&&(l=[e,r])),s&&(u=t[0],a=t[1],c&&(u=u[1],a=a[1]),o=[u,a],s.invert&&(u=s(u),a=s(a)),u>a&&(h=u,u=a,a=h),(u!=f[0]||a!=f[1])&&(f=[u,a])),n):(c&&(i?(e=i[0],r=i[1]):(e=l[0],r=l[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(h=e,e=r,r=h))),s&&(o?(u=o[0],a=o[1]):(u=f[0],a=f[1],s.invert&&(u=s.invert(u),a=s.invert(a)),u>a&&(h=u,u=a,a=h))),c&&s?[[e,u],[r,a]]:c?[e,r]:s&&[u,a])},n.clear=function(){return n.empty()||(l=[0,0],f=[0,0],i=o=null),n},n.empty=function(){return!!c&&l[0]==l[1]||!!s&&f[0]==f[1]},Xo.rebind(n,a,"on")};var Ts={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Rs=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Ds=tc.format=ac.timeFormat,Ps=Ds.utc,Us=Ps("%Y-%m-%dT%H:%M:%S.%LZ");Ds.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Oo:Us,Oo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},Oo.toString=Us.toString,tc.second=Rt(function(n){return new ec(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),tc.seconds=tc.second.range,tc.seconds.utc=tc.second.utc.range,tc.minute=Rt(function(n){return new ec(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),tc.minutes=tc.minute.range,tc.minutes.utc=tc.minute.utc.range,tc.hour=Rt(function(n){var t=n.getTimezoneOffset()/60;return new ec(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),tc.hours=tc.hour.range,tc.hours.utc=tc.hour.utc.range,tc.month=Rt(function(n){return n=tc.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),tc.months=tc.month.range,tc.months.utc=tc.month.utc.range;var js=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Hs=[[tc.second,1],[tc.second,5],[tc.second,15],[tc.second,30],[tc.minute,1],[tc.minute,5],[tc.minute,15],[tc.minute,30],[tc.hour,1],[tc.hour,3],[tc.hour,6],[tc.hour,12],[tc.day,1],[tc.day,2],[tc.week,1],[tc.month,1],[tc.month,3],[tc.year,1]],Fs=Ds.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",be]]),Os={range:function(n,t,e){return Xo.range(+n,+t,e).map(Io)},floor:bt,ceil:bt};Hs.year=tc.year,tc.scale=function(){return Yo(Xo.scale.linear(),Hs,Fs)};var Ys=Hs.map(function(n){return[n[0].utc,n[1]]}),Is=Ps.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",be]]);Ys.year=tc.year.utc,tc.scale.utc=function(){return Yo(Xo.scale.linear(),Ys,Is)},Xo.text=wt(function(n){return n.responseText}),Xo.json=function(n,t){return St(n,"application/json",Zo,t)},Xo.html=function(n,t){return St(n,"text/html",Vo,t)},Xo.xml=wt(function(n){return n.responseXML}),"function"==typeof define&&define.amd?define(Xo):"object"==typeof module&&module.exports?module.exports=Xo:this.d3=Xo}();
\ No newline at end of file
diff --git a/static/libjs/fullcalendar/THIS_IS_VERSION_2.0.0-beta2 b/static/libjs/fullcalendar/THIS_IS_VERSION_2.0.0-beta2
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/static/libjs/fullcalendar/fullcalendar.css b/static/libjs/fullcalendar/fullcalendar.css
new file mode 100644
index 0000000000000000000000000000000000000000..ce74f7737d265573ff1af356d0d723596e72b240
--- /dev/null
+++ b/static/libjs/fullcalendar/fullcalendar.css
@@ -0,0 +1,601 @@
+/*!
+ * FullCalendar v2.0.0-beta2 Stylesheet
+ * Docs & License: http://arshaw.com/fullcalendar/
+ * (c) 2013 Adam Shaw
+ */
+
+
+.fc {
+	direction: ltr;
+	text-align: left;
+	}
+	
+.fc table {
+	border-collapse: collapse;
+	border-spacing: 0;
+	}
+	
+html .fc,
+.fc table {
+	font-size: 1em;
+	}
+	
+.fc td,
+.fc th {
+	padding: 0;
+	vertical-align: top;
+	}
+
+
+
+/* Header
+------------------------------------------------------------------------*/
+
+.fc-header td {
+	white-space: nowrap;
+	}
+
+.fc-header-left {
+	width: 25%;
+	text-align: left;
+	}
+	
+.fc-header-center {
+	text-align: center;
+	}
+	
+.fc-header-right {
+	width: 25%;
+	text-align: right;
+	}
+	
+.fc-header-title {
+	display: inline-block;
+	vertical-align: top;
+	}
+	
+.fc-header-title h2 {
+	margin-top: 0;
+	white-space: nowrap;
+	}
+	
+.fc .fc-header-space {
+	padding-left: 10px;
+	}
+	
+.fc-header .fc-button {
+	margin-bottom: 1em;
+	vertical-align: top;
+	}
+	
+/* buttons edges butting together */
+
+.fc-header .fc-button {
+	margin-right: -1px;
+	}
+	
+.fc-header .fc-corner-right,  /* non-theme */
+.fc-header .ui-corner-right { /* theme */
+	margin-right: 0; /* back to normal */
+	}
+	
+/* button layering (for border precedence) */
+	
+.fc-header .fc-state-hover,
+.fc-header .ui-state-hover {
+	z-index: 2;
+	}
+	
+.fc-header .fc-state-down {
+	z-index: 3;
+	}
+
+.fc-header .fc-state-active,
+.fc-header .ui-state-active {
+	z-index: 4;
+	}
+	
+	
+	
+/* Content
+------------------------------------------------------------------------*/
+	
+.fc-content {
+	position: relative;
+	z-index: 1; /* scopes all other z-index's to be inside this container */
+	clear: both;
+	zoom: 1; /* for IE7, gives accurate coordinates for [un]freezeContentHeight */
+	}
+	
+.fc-view {
+	position: relative;
+	width: 100%;
+	overflow: hidden;
+	}
+	
+	
+
+/* Cell Styles
+------------------------------------------------------------------------*/
+
+.fc-widget-header,    /* <th>, usually */
+.fc-widget-content {  /* <td>, usually */
+	border: 1px solid #ddd;
+	}
+	
+.fc-state-highlight { /* <td> today cell */ /* TODO: add .fc-today to <th> */
+	background: #fcf8e3;
+	}
+	
+.fc-cell-overlay { /* semi-transparent rectangle while dragging */
+	background: #bce8f1;
+	opacity: .3;
+	filter: alpha(opacity=30); /* for IE */
+	}
+	
+
+
+/* Buttons
+------------------------------------------------------------------------*/
+
+.fc-button {
+	position: relative;
+	display: inline-block;
+	padding: 0 .6em;
+	overflow: hidden;
+	height: 1.9em;
+	line-height: 1.9em;
+	white-space: nowrap;
+	cursor: pointer;
+	}
+	
+.fc-state-default { /* non-theme */
+	border: 1px solid;
+	}
+
+.fc-state-default.fc-corner-left { /* non-theme */
+	border-top-left-radius: 4px;
+	border-bottom-left-radius: 4px;
+	}
+
+.fc-state-default.fc-corner-right { /* non-theme */
+	border-top-right-radius: 4px;
+	border-bottom-right-radius: 4px;
+	}
+
+/*
+	Our default prev/next buttons use HTML entities like &lsaquo; &rsaquo; &laquo; &raquo;
+	and we'll try to make them look good cross-browser.
+*/
+
+.fc-button .fc-icon {
+	margin: 0 .1em;
+	font-size: 2em;
+	font-family: "Courier New", Courier, monospace;
+	vertical-align: baseline; /* for IE7 */
+	}
+
+.fc-icon-left-single-arrow:after {
+	content: "\02039";
+	font-weight: bold;
+	}
+
+.fc-icon-right-single-arrow:after {
+	content: "\0203A";
+	font-weight: bold;
+	}
+
+.fc-icon-left-double-arrow:after {
+	content: "\000AB";
+	}
+
+.fc-icon-right-double-arrow:after {
+	content: "\000BB";
+	}
+	
+/* icon (for jquery ui) */
+
+.fc-button .ui-icon {
+	position: relative;
+	top: 50%;
+	float: left;
+	margin-top: -8px; /* we know jqui icons are always 16px tall */
+	}
+	
+/*
+  button states
+  borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/)
+*/
+
+.fc-state-default {
+	background-color: #f5f5f5;
+	background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
+	background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
+	background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
+	background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
+	background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
+	background-repeat: repeat-x;
+	border-color: #e6e6e6 #e6e6e6 #bfbfbf;
+	border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+	color: #333;
+	text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+	box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+	}
+
+.fc-state-hover,
+.fc-state-down,
+.fc-state-active,
+.fc-state-disabled {
+	color: #333333;
+	background-color: #e6e6e6;
+	}
+
+.fc-state-hover {
+	color: #333333;
+	text-decoration: none;
+	background-position: 0 -15px;
+	-webkit-transition: background-position 0.1s linear;
+	   -moz-transition: background-position 0.1s linear;
+	     -o-transition: background-position 0.1s linear;
+	        transition: background-position 0.1s linear;
+	}
+
+.fc-state-down,
+.fc-state-active {
+	background-color: #cccccc;
+	background-image: none;
+	outline: 0;
+	box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+	}
+
+.fc-state-disabled {
+	cursor: default;
+	background-image: none;
+	opacity: 0.65;
+	filter: alpha(opacity=65);
+	box-shadow: none;
+	}
+
+	
+
+/* Global Event Styles
+------------------------------------------------------------------------*/
+
+.fc-event-container > * {
+	z-index: 8;
+	}
+
+.fc-event-container > .ui-draggable-dragging,
+.fc-event-container > .ui-resizable-resizing {
+	z-index: 9;
+	}
+	 
+.fc-event {
+	border: 1px solid #3a87ad; /* default BORDER color */
+	background-color: #3a87ad; /* default BACKGROUND color */
+	color: #fff;               /* default TEXT color */
+	font-size: .85em;
+	cursor: default;
+	}
+
+a.fc-event {
+	text-decoration: none;
+	}
+	
+a.fc-event,
+.fc-event-draggable {
+	cursor: pointer;
+	}
+	
+.fc-rtl .fc-event {
+	text-align: right;
+	}
+
+.fc-event-inner {
+	width: 100%;
+	height: 100%;
+	overflow: hidden;
+	}
+	
+.fc-event-time,
+.fc-event-title {
+	padding: 0 1px;
+	}
+	
+.fc .ui-resizable-handle {
+	display: block;
+	position: absolute;
+	z-index: 99999;
+	overflow: hidden; /* hacky spaces (IE6/7) */
+	font-size: 300%;  /* */
+	line-height: 50%; /* */
+	}
+	
+	
+	
+/* Horizontal Events
+------------------------------------------------------------------------*/
+
+.fc-event-hori {
+	border-width: 1px 0;
+	margin-bottom: 1px;
+	}
+
+.fc-ltr .fc-event-hori.fc-event-start,
+.fc-rtl .fc-event-hori.fc-event-end {
+	border-left-width: 1px;
+	border-top-left-radius: 3px;
+	border-bottom-left-radius: 3px;
+	}
+
+.fc-ltr .fc-event-hori.fc-event-end,
+.fc-rtl .fc-event-hori.fc-event-start {
+	border-right-width: 1px;
+	border-top-right-radius: 3px;
+	border-bottom-right-radius: 3px;
+	}
+	
+/* resizable */
+	
+.fc-event-hori .ui-resizable-e {
+	top: 0           !important; /* importants override pre jquery ui 1.7 styles */
+	right: -3px      !important;
+	width: 7px       !important;
+	height: 100%     !important;
+	cursor: e-resize;
+	}
+	
+.fc-event-hori .ui-resizable-w {
+	top: 0           !important;
+	left: -3px       !important;
+	width: 7px       !important;
+	height: 100%     !important;
+	cursor: w-resize;
+	}
+	
+.fc-event-hori .ui-resizable-handle {
+	_padding-bottom: 14px; /* IE6 had 0 height */
+	}
+	
+	
+	
+/* Reusable Separate-border Table
+------------------------------------------------------------*/
+
+table.fc-border-separate {
+	border-collapse: separate;
+	}
+	
+.fc-border-separate th,
+.fc-border-separate td {
+	border-width: 1px 0 0 1px;
+	}
+	
+.fc-border-separate th.fc-last,
+.fc-border-separate td.fc-last {
+	border-right-width: 1px;
+	}
+	
+.fc-border-separate tr.fc-last th,
+.fc-border-separate tr.fc-last td {
+	border-bottom-width: 1px;
+	}
+	
+.fc-border-separate tbody tr.fc-first td,
+.fc-border-separate tbody tr.fc-first th {
+	border-top-width: 0;
+	}
+	
+	
+
+/* Month View, Basic Week View, Basic Day View
+------------------------------------------------------------------------*/
+
+.fc-grid th {
+	text-align: center;
+	}
+
+.fc .fc-week-number {
+	width: 22px;
+	text-align: center;
+	}
+
+.fc .fc-week-number div {
+	padding: 0 2px;
+	}
+	
+.fc-grid .fc-day-number {
+	float: right;
+	padding: 0 2px;
+	}
+	
+.fc-grid .fc-other-month .fc-day-number {
+	opacity: 0.3;
+	filter: alpha(opacity=30); /* for IE */
+	/* opacity with small font can sometimes look too faded
+	   might want to set the 'color' property instead
+	   making day-numbers bold also fixes the problem */
+	}
+	
+.fc-grid .fc-day-content {
+	clear: both;
+	padding: 2px 2px 1px; /* distance between events and day edges */
+	}
+	
+/* event styles */
+	
+.fc-grid .fc-event-time {
+	font-weight: bold;
+	}
+	
+/* right-to-left */
+	
+.fc-rtl .fc-grid .fc-day-number {
+	float: left;
+	}
+	
+.fc-rtl .fc-grid .fc-event-time {
+	float: right;
+	}
+	
+	
+
+/* Agenda Week View, Agenda Day View
+------------------------------------------------------------------------*/
+
+.fc-agenda table {
+	border-collapse: separate;
+	}
+	
+.fc-agenda-days th {
+	text-align: center;
+	}
+	
+.fc-agenda .fc-agenda-axis {
+	width: 50px;
+	padding: 0 4px;
+	vertical-align: middle;
+	text-align: right;
+	font-weight: normal;
+	}
+
+.fc-agenda-slots .fc-agenda-axis {
+	white-space: nowrap;
+	}
+
+.fc-agenda .fc-week-number {
+	font-weight: bold;
+	}
+	
+.fc-agenda .fc-day-content {
+	padding: 2px 2px 1px;
+	}
+	
+/* make axis border take precedence */
+	
+.fc-agenda-days .fc-agenda-axis {
+	border-right-width: 1px;
+	}
+	
+.fc-agenda-days .fc-col0 {
+	border-left-width: 0;
+	}
+	
+/* all-day area */
+	
+.fc-agenda-allday th {
+	border-width: 0 1px;
+	}
+	
+.fc-agenda-allday .fc-day-content {
+	min-height: 34px; /* TODO: doesnt work well in quirksmode */
+	_height: 34px;
+	}
+	
+/* divider (between all-day and slots) */
+	
+.fc-agenda-divider-inner {
+	height: 2px;
+	overflow: hidden;
+	}
+	
+.fc-widget-header .fc-agenda-divider-inner {
+	background: #eee;
+	}
+	
+/* slot rows */
+	
+.fc-agenda-slots th {
+	border-width: 1px 1px 0;
+	}
+	
+.fc-agenda-slots td {
+	border-width: 1px 0 0;
+	background: none;
+	}
+	
+.fc-agenda-slots td div {
+	height: 20px;
+	}
+	
+.fc-agenda-slots tr.fc-slot0 th,
+.fc-agenda-slots tr.fc-slot0 td {
+	border-top-width: 0;
+	}
+
+.fc-agenda-slots tr.fc-minor th,
+.fc-agenda-slots tr.fc-minor td {
+	border-top-style: dotted;
+	}
+	
+.fc-agenda-slots tr.fc-minor th.ui-widget-header {
+	*border-top-style: solid; /* doesn't work with background in IE6/7 */
+	}
+	
+
+
+/* Vertical Events
+------------------------------------------------------------------------*/
+
+.fc-event-vert {
+	border-width: 0 1px;
+	}
+
+.fc-event-vert.fc-event-start {
+	border-top-width: 1px;
+	border-top-left-radius: 3px;
+	border-top-right-radius: 3px;
+	}
+
+.fc-event-vert.fc-event-end {
+	border-bottom-width: 1px;
+	border-bottom-left-radius: 3px;
+	border-bottom-right-radius: 3px;
+	}
+	
+.fc-event-vert .fc-event-time {
+	white-space: nowrap;
+	font-size: 10px;
+	}
+
+.fc-event-vert .fc-event-inner {
+	position: relative;
+	z-index: 2;
+	}
+	
+.fc-event-vert .fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay  */
+	position: absolute;
+	z-index: 1;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	background: #fff;
+	opacity: .25;
+	filter: alpha(opacity=25);
+	}
+	
+.fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */
+.fc-select-helper .fc-event-bg {
+	display: none\9; /* for IE6/7/8. nested opacity filters while dragging don't work */
+	}
+	
+/* resizable */
+	
+.fc-event-vert .ui-resizable-s {
+	bottom: 0        !important; /* importants override pre jquery ui 1.7 styles */
+	width: 100%      !important;
+	height: 8px      !important;
+	overflow: hidden !important;
+	line-height: 8px !important;
+	font-size: 11px  !important;
+	font-family: monospace;
+	text-align: center;
+	cursor: s-resize;
+	}
+	
+.fc-agenda .ui-resizable-resizing { /* TODO: better selector */
+	_overflow: hidden;
+	}
+	
+	
diff --git a/static/libjs/fullcalendar/fullcalendar.min.js b/static/libjs/fullcalendar/fullcalendar.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..fd05901e56cefd4f35fa50f1cc85c0dcd7854b03
--- /dev/null
+++ b/static/libjs/fullcalendar/fullcalendar.min.js
@@ -0,0 +1,7 @@
+/*!
+ * FullCalendar v2.0.0-beta2
+ * Docs & License: http://arshaw.com/fullcalendar/
+ * (c) 2013 Adam Shaw
+ */
+(function(t){"function"==typeof define&&define.amd?define(["jquery","moment"],t):t(jQuery,moment)})(function(t,e){function n(t,e){return e.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"t")}function r(t,e){var n=e.longDateFormat("L");return n=n.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g,""),t.isRTL?n+=" ddd":n="ddd "+n,n}function a(t){o(Me,t)}function o(e){function n(n,r){t.isPlainObject(r)&&t.isPlainObject(e[n])&&!i(n)?e[n]=o({},e[n],r):void 0!==r&&(e[n]=r)}for(var r=1;arguments.length>r;r++)t.each(arguments[r],n);return e}function i(t){return/(Time|Duration)$/.test(t)}function s(n,r){function a(t){oe?f()&&(b(),m(t)):i()}function i(){ie=te.theme?"ui":"fc",n.addClass("fc"),te.isRTL?n.addClass("fc-rtl"):n.addClass("fc-ltr"),te.theme&&n.addClass("ui-widget"),oe=t("<div class='fc-content' />").prependTo(n),re=new l(J,te),ae=re.render(),ae&&n.prepend(ae),h(te.defaultView),te.handleWindowResize&&t(window).resize(D),v()||s()}function s(){setTimeout(function(){!se.start&&v()&&g()},0)}function d(){se&&(Q("viewDestroy",se,se,se.element),se.triggerEventDestroy()),t(window).unbind("resize",D),re.destroy(),oe.remove(),n.removeClass("fc fc-rtl ui-widget")}function f(){return n.is(":visible")}function v(){return t("body").is(":visible")}function h(t){se&&t==se.name||p(t)}function p(e){ge++,se&&(Q("viewDestroy",se,se,se.element),H(),se.triggerEventDestroy(),$(),se.element.remove(),re.deactivateButton(se.name)),re.activateButton(e),se=new Fe[e](t("<div class='fc-view fc-view-"+e+"' />").appendTo(oe),J),g(),V(),ge--}function g(t){se.start&&!t&&de.isWithin(se.intervalStart,se.intervalEnd)||f()&&m(t)}function m(t){ge++,se.start&&(Q("viewDestroy",se,se,se.element),H(),x()),$(),t&&(de=se.incrementDate(de,t)),se.render(de.clone()),w(),V(),(se.afterRender||k)(),F(),N(),Q("viewRender",se,se,se.element),ge--,M()}function y(){f()&&(H(),x(),b(),w(),S())}function b(){ce=te.contentHeight?te.contentHeight:te.height?te.height-(ae?ae.height():0)-T(oe):Math.round(oe.width()/Math.max(te.aspectRatio,.5))}function w(){void 0===ce&&b(),ge++,se.setHeight(ce),se.setWidth(oe.width()),ge--,le=n.outerWidth()}function D(){if(!ge)if(se.start){var t=++pe;setTimeout(function(){t==pe&&!ge&&f()&&le!=(le=n.outerWidth())&&(ge++,y(),se.trigger("windowResize",he),ge--)},200)}else s()}function C(){x(),z()}function E(t){x(),S(t)}function S(t){f()&&(se.renderEvents(me,t),se.trigger("eventAfterAllRender"))}function x(){se.triggerEventDestroy(),se.clearEvents(),se.clearEventData()}function M(){!te.lazyFetching||fe(se.start,se.end)?z():S()}function z(){ve(se.start,se.end)}function R(t){me=t,S()}function _(t){E(t)}function F(){re.updateTitle(se.title)}function N(){var t=J.getNow();t.isWithin(se.intervalStart,se.intervalEnd)?re.disableButton("today"):re.enableButton("today")}function Y(t,e){se.select(t,e)}function H(){se&&se.unselect()}function O(){g(-1)}function A(){g(1)}function W(){de.add("years",-1),g()}function L(){de.add("years",1),g()}function Z(){de=J.getNow(),g()}function P(t){de=J.moment(t),g()}function j(){de.add.apply(de,arguments),g()}function I(){return de.clone()}function $(){oe.css({width:"100%",height:oe.height(),overflow:"hidden"})}function V(){oe.css({width:"",height:"",overflow:""})}function X(){return J}function U(){return se}function G(t,e){return void 0===e?te[t]:(("height"==t||"contentHeight"==t||"aspectRatio"==t)&&(te[t]=e,y()),void 0)}function Q(t,e){return te[t]?te[t].apply(e||he,Array.prototype.slice.call(arguments,2)):void 0}var J=this;r=r||{};var K,te=o({},Me,r);K=te.lang in ze?ze[te.lang]:ze[Me.lang],K&&(te=o({},Me,K,r)),te.isRTL&&(te=o({},Me,Re,K||{},r)),J.options=te,J.render=a,J.destroy=d,J.refetchEvents=C,J.reportEvents=R,J.reportEventChange=_,J.rerenderEvents=E,J.changeView=h,J.select=Y,J.unselect=H,J.prev=O,J.next=A,J.prevYear=W,J.nextYear=L,J.today=Z,J.gotoDate=P,J.incrementDate=j,J.getDate=I,J.getCalendar=X,J.getView=U,J.option=G,J.trigger=Q;var ee=u(e.langData(te.lang));if(te.monthNames&&(ee._months=te.monthNames),te.monthNamesShort&&(ee._monthsShort=te.monthNamesShort),te.dayNames&&(ee._weekdays=te.dayNames),te.dayNamesShort&&(ee._weekdaysShort=te.dayNamesShort),te.firstDay){var ne=u(ee._week);ne.dow=te.firstDay,ee._week=ne}J.defaultAllDayEventDuration=e.duration(te.defaultAllDayEventDuration),J.defaultTimedEventDuration=e.duration(te.defaultTimedEventDuration),J.moment=function(){var t;return t="local"===te.timezone?_e.moment.apply(null,arguments):"UTC"===te.timezone?_e.moment.utc.apply(null,arguments):_e.moment.parseZone.apply(null,arguments),t._lang=ee,t},J.getIsAmbigTimezone=function(){return"local"!==te.timezone&&"UTC"!==te.timezone},J.rezoneDate=function(t){return J.moment(t.toArray())},J.getNow=function(){var t=te.now;return"function"==typeof t&&(t=t()),J.moment(t)},J.calculateWeekNumber=function(t){var e=te.weekNumberCalculation;return"function"==typeof e?e(t):"local"===e?t.week():"ISO"===e.toUpperCase()?t.isoWeek():void 0},J.getEventEnd=function(t){return t.end?t.end.clone():J.getDefaultEventEnd(t.allDay,t.start)},J.getDefaultEventEnd=function(t,e){var n=e.clone();return t?n.stripTime().add(J.defaultAllDayEventDuration):n.add(J.defaultTimedEventDuration),J.getIsAmbigTimezone()&&n.stripZone(),n},J.formatRange=function(t,e,n){return"function"==typeof n&&(n=n.call(J,te,ee)),q(t,e,n,null,te.isRTL)},J.formatDate=function(t,e){return"function"==typeof e&&(e=e.call(J,te,ee)),B(t,e)},c.call(J,te);var re,ae,oe,ie,se,le,ce,de,ue,fe=J.isFetchNeeded,ve=J.fetchEvents,he=n[0],pe=0,ge=0,me=[];de=null!=te.defaultDate?J.moment(te.defaultDate):J.getNow(),te.droppable&&t(document).bind("dragstart",function(e,n){var r=e.target,a=t(r);if(!a.parents(".fc").length){var o=te.dropAccept;(t.isFunction(o)?o.call(r,a):a.is(o))&&(ue=r,se.dragStart(ue,e,n))}}).bind("dragstop",function(t,e){ue&&(se.dragStop(ue,t,e),ue=null)})}function l(e,n){function r(){f=n.theme?"ui":"fc";var e=n.header;return e?v=t("<table class='fc-header' style='width:100%'/>").append(t("<tr/>").append(o("left")).append(o("center")).append(o("right"))):void 0}function a(){v.remove()}function o(r){var a=t("<td class='fc-header-"+r+"'/>"),o=n.header[r];return o&&t.each(o.split(" "),function(r){r>0&&a.append("<span class='fc-header-space'/>");var o;t.each(this.split(","),function(r,i){if("title"==i)a.append("<span class='fc-header-title'><h2>&nbsp;</h2></span>"),o&&o.addClass(f+"-corner-right"),o=null;else{var s;if(e[i]?s=e[i]:Fe[i]&&(s=function(){v.removeClass(f+"-state-hover"),e.changeView(i)}),s){var l,c=z(n.themeButtonIcons,i),d=z(n.buttonIcons,i),u=z(n.buttonText,i);l=c&&n.theme?"<span class='ui-icon ui-icon-"+c+"'></span>":d&&!n.theme?"<span class='fc-icon fc-icon-"+d+"'></span>":R(u||i);var v=t("<span class='fc-button fc-button-"+i+" "+f+"-state-default'>"+l+"</span>").click(function(){v.hasClass(f+"-state-disabled")||s()}).mousedown(function(){v.not("."+f+"-state-active").not("."+f+"-state-disabled").addClass(f+"-state-down")}).mouseup(function(){v.removeClass(f+"-state-down")}).hover(function(){v.not("."+f+"-state-active").not("."+f+"-state-disabled").addClass(f+"-state-hover")},function(){v.removeClass(f+"-state-hover").removeClass(f+"-state-down")}).appendTo(a);F(v),o||v.addClass(f+"-corner-left"),o=v}}}),o&&o.addClass(f+"-corner-right")}),a}function i(t){v.find("h2").html(t)}function s(t){v.find("span.fc-button-"+t).addClass(f+"-state-active")}function l(t){v.find("span.fc-button-"+t).removeClass(f+"-state-active")}function c(t){v.find("span.fc-button-"+t).addClass(f+"-state-disabled")}function d(t){v.find("span.fc-button-"+t).removeClass(f+"-state-disabled")}var u=this;u.render=r,u.destroy=a,u.updateTitle=i,u.activateButton=s,u.deactivateButton=l,u.disableButton=c,u.enableButton=d;var f,v=t([])}function c(e){function n(t,e){return!S||t.clone().stripZone()<S.clone().stripZone()||e.clone().stripZone()>k.clone().stripZone()}function r(t,e){S=t,k=e,W=[];var n=++N,r=F.length;Y=r;for(var o=0;r>o;o++)a(F[o],n)}function a(t,e){o(t,function(n){if(e==N){if(n)for(var r=0;n.length>r;r++){var a=y(n[r],t);a&&W.push(a)}Y--,Y||z(W)}})}function o(n,r){var a,i,s=_e.sourceFetchers;for(a=0;s.length>a;a++){if(i=s[a].call(E,n,S.clone(),k.clone(),e.timezone,r),i===!0)return;if("object"==typeof i)return o(i,r),void 0}var l=n.events;if(l)t.isFunction(l)?(g(),l.call(E,S.clone(),k.clone(),e.timezone,function(t){r(t),m()})):t.isArray(l)?r(l):r();else{var c=n.url;if(c){var d,u=n.success,f=n.error,v=n.complete;d=t.isFunction(n.data)?n.data():n.data;var h=t.extend({},d||{}),p=O(n.startParam,e.startParam),y=O(n.endParam,e.endParam),b=O(n.timezoneParam,e.timezoneParam);p&&(h[p]=S.format()),y&&(h[y]=k.format()),e.timezone&&"local"!=e.timezone&&(h[b]=e.timezone),g(),t.ajax(t.extend({},Ne,n,{data:h,success:function(e){e=e||[];var n=H(u,this,arguments);t.isArray(n)&&(e=n),r(e)},error:function(){H(f,this,arguments),r()},complete:function(){H(v,this,arguments),m()}}))}else r()}}function i(t){t=s(t),t&&(Y++,a(t,N))}function s(e){return t.isFunction(e)||t.isArray(e)?e={events:e}:"string"==typeof e&&(e={url:e}),"object"==typeof e?(D(e),F.push(e),e):void 0}function l(e){F=t.grep(F,function(t){return!T(t,e)}),W=t.grep(W,function(t){return!T(t.source,e)}),z(W)}function c(t){b(t),u(t),z(W)}function u(t){var e,n,r,a;for(e=0;W.length>e;e++)if(n=W[e],n._id==t._id&&n!==t)for(r=0;P.length>r;r++)a=P[r],void 0!==t[a]&&(n[a]=t[a])}function f(t,e){var n=y(t);n&&(n.source||(e&&(_.events.push(n),n.source=_),W.push(n)),z(W))}function h(e){var n;if(e){if(!t.isFunction(e)){var r=e+"";e=function(t){return t._id==r}}for(W=t.grep(W,e,!0),n=0;F.length>n;n++)t.isArray(F[n].events)&&(F[n].events=t.grep(F[n].events,e,!0))}else for(W=[],n=0;F.length>n;n++)t.isArray(F[n].events)&&(F[n].events=[]);z(W)}function p(e){return t.isFunction(e)?t.grep(W,e):e?(e+="",t.grep(W,function(t){return t._id==e})):W}function g(){A++||x("loading",null,!0,M())}function m(){--A||x("loading",null,!1,M())}function y(n,r){var a,o,i,s,l={};return e.eventDataTransform&&(n=e.eventDataTransform(n)),r&&r.eventDataTransform&&(n=r.eventDataTransform(n)),a=E.moment(n.start||n.date),a.isValid()&&(o=null,!n.end||(o=E.moment(n.end),o.isValid()))?(i=n.allDay,void 0===i&&(s=O(r?r.allDayDefault:void 0,e.allDayDefault),i=void 0!==s?s:!(a.hasTime()||o&&o.hasTime())),i?(a.hasTime()&&a.stripTime(),o&&o.hasTime()&&o.stripTime()):(a.hasTime()||(a=E.rezoneDate(a)),o&&!o.hasTime()&&(o=E.rezoneDate(o))),t.extend(l,n),r&&(l.source=r),l._id=n._id||(void 0===n.id?"_fc"+Ye++:n.id+""),l.className=n.className?"string"==typeof n.className?n.className.split(/\s+/):n.className:[],l.allDay=i,l.start=a,l.end=o,e.forceEventDuration&&!l.end&&(l.end=R(l)),d(l),l):void 0}function b(t,e,n){var r,a,o,i=t._allDay,s=t._start,l=t._end,c=!1;return e||n||(e=t.start,n=t.end),r=t.allDay!=i?t.allDay:!(e||n).hasTime(),r&&(e&&(e=e.clone().stripTime()),n&&(n=n.clone().stripTime())),e&&(a=r?v(e,s.clone().stripTime()):v(e,s)),r!=i?c=!0:n&&(o=v(n||E.getDefaultEventEnd(r,e||s),e||s).subtract(v(l||E.getDefaultEventEnd(i,s),s))),w(p(t._id),c,r,a,o)}function w(n,r,a,o,i){var s=E.getIsAmbigTimezone(),l=[];return t.each(n,function(t,n){var c=n._allDay,u=n._start,f=n._end,v=null!=a?a:c,h=u.clone(),p=!r&&f?f.clone():null;v?(h.stripTime(),p&&p.stripTime()):(h.hasTime()||(h=E.rezoneDate(h)),p&&!p.hasTime()&&(p=E.rezoneDate(p))),p||!e.forceEventDuration&&!+i||(p=E.getDefaultEventEnd(v,h)),h.add(o),p&&p.add(o).add(i),s&&(+o&&h.stripZone(),p&&(+o||+i)&&p.stripZone()),n.allDay=v,n.start=h,n.end=p,d(n),l.push(function(){n.allDay=c,n.start=u,n.end=f,d(n)})}),function(){for(var t=0;l.length>t;t++)l[t]()}}function D(t){t.className?"string"==typeof t.className&&(t.className=t.className.split(/\s+/)):t.className=[];for(var e=_e.sourceNormalizers,n=0;e.length>n;n++)e[n].call(E,t)}function T(t,e){return t&&e&&C(t)==C(e)}function C(t){return("object"==typeof t?t.events||t.url:"")||t}var E=this;E.isFetchNeeded=n,E.fetchEvents=r,E.addEventSource=i,E.removeEventSource=l,E.updateEvent=c,E.renderEvent=f,E.removeEvents=h,E.clientEvents=p,E.mutateEvent=b;var S,k,x=E.trigger,M=E.getView,z=E.reportEvents,R=E.getEventEnd,_={events:[]},F=[_],N=0,Y=0,A=0,W=[],L=e.eventSources||[];e.events&&L.push(e.events);for(var Z=0;L.length>Z;Z++)s(L[Z]);var P=["title","url","allDay","className","editable","color","backgroundColor","borderColor","textColor"]}function d(t){t._allDay=t.allDay,t._start=t.start.clone(),t._end=t.end?t.end.clone():null}function u(t){var e=function(){};return e.prototype=t,new e}function f(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])}function v(t,n){return e.duration({days:t.clone().stripTime().diff(n.clone().stripTime(),"days"),ms:t.time()-n.time()})}function h(t){return"[object Date]"===Object.prototype.toString.call(t)||t instanceof Date}function p(e,n,r){e.unbind("mouseover").mouseover(function(e){for(var a,o,i,s=e.target;s!=this;)a=s,s=s.parentNode;void 0!==(o=a._fci)&&(a._fci=void 0,i=n[o],r(i.event,i.element,i),t(e.target).trigger(e)),e.stopPropagation()})}function g(e,n,r){for(var a,o=0;e.length>o;o++)a=t(e[o]),a.width(Math.max(0,n-y(a,r)))}function m(e,n,r){for(var a,o=0;e.length>o;o++)a=t(e[o]),a.height(Math.max(0,n-T(a,r)))}function y(t,e){return b(t)+D(t)+(e?w(t):0)}function b(e){return(parseFloat(t.css(e[0],"paddingLeft",!0))||0)+(parseFloat(t.css(e[0],"paddingRight",!0))||0)}function w(e){return(parseFloat(t.css(e[0],"marginLeft",!0))||0)+(parseFloat(t.css(e[0],"marginRight",!0))||0)}function D(e){return(parseFloat(t.css(e[0],"borderLeftWidth",!0))||0)+(parseFloat(t.css(e[0],"borderRightWidth",!0))||0)}function T(t,e){return C(t)+S(t)+(e?E(t):0)}function C(e){return(parseFloat(t.css(e[0],"paddingTop",!0))||0)+(parseFloat(t.css(e[0],"paddingBottom",!0))||0)}function E(e){return(parseFloat(t.css(e[0],"marginTop",!0))||0)+(parseFloat(t.css(e[0],"marginBottom",!0))||0)}function S(e){return(parseFloat(t.css(e[0],"borderTopWidth",!0))||0)+(parseFloat(t.css(e[0],"borderBottomWidth",!0))||0)}function k(){}function x(t,e){return t-e}function M(t){return Math.max.apply(Math,t)}function z(t,e){if(void 0!==t[e])return t[e];for(var n,r=e.split(/(?=[A-Z])/),a=r.length-1;a>=0;a--)if(n=t[r[a].toLowerCase()],void 0!==n)return n;return t["default"]}function R(t){return(t+"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/'/g,"&#039;").replace(/"/g,"&quot;").replace(/\n/g,"<br />")}function _(t){return t.replace(/&.*?;/g,"")}function F(t){t.attr("unselectable","on").css("MozUserSelect","none").bind("selectstart.ui",function(){return!1})}function N(t){t.children().removeClass("fc-first fc-last").filter(":first-child").addClass("fc-first").end().filter(":last-child").addClass("fc-last")}function Y(t,e){var n=t.source||{},r=t.color,a=n.color,o=e("eventColor"),i=t.backgroundColor||r||n.backgroundColor||a||e("eventBackgroundColor")||o,s=t.borderColor||r||n.borderColor||a||e("eventBorderColor")||o,l=t.textColor||n.textColor||e("eventTextColor"),c=[];return i&&c.push("background-color:"+i),s&&c.push("border-color:"+s),l&&c.push("color:"+l),c.join(";")}function H(e,n,r){if(t.isFunction(e)&&(e=[e]),e){var a,o;for(a=0;e.length>a;a++)o=e[a].apply(n,r)||o;return o}}function O(){for(var t=0;arguments.length>t;t++)if(void 0!==arguments[t])return arguments[t]}function A(n,r,a){var o,i,s=n[0],l=1==n.length&&"string"==typeof s,c=!1,d=!1;return l?Oe.test(s)?(s+="-01",c=!0,d=!0):(o=Ae.exec(s))&&(c=!o[5],d=!0):t.isArray(s)&&(d=!0),i=r||a||c?e.utc.apply(e,n):e.apply(null,n),e.isMoment(s)&&L(s,i),c&&(i._ambigTime=!0,i._ambigZone=!0),a&&(d?i._ambigZone=!0:l?i.zone(s):(h(s)||void 0===s)&&i.local()),new W(i)}function W(t){f(this,t)}function L(t,e){t._ambigTime?e._ambigTime=!0:e._ambigTime&&delete e._ambigTime,t._ambigZone?e._ambigZone=!0:e._ambigZone&&delete e._ambigZone}function Z(t){var e,n=[],r=!1,a=!1;for(e=0;t.length>e;e++)n.push(_e.moment(t[e])),r=r||n[e]._ambigTime,a=a||n[e]._ambigZone;for(e=0;n.length>e;e++)r?n[e].stripTime():a&&n[e].stripZone();return n}function P(t,n){return e.fn.format.call(t,n)}function B(t,e){return j(t,X(e))}function j(t,e){var n,r="";for(n=0;e.length>n;n++)r+=I(t,e[n]);return r}function I(t,e){var n,r;return"string"==typeof e?e:(n=e.token)?We[n]?We[n](t):P(t,n):e.maybe&&(r=j(t,e.maybe),r.match(/[1-9]/))?r:""}function q(t,e,n,r,a){return n=t.lang().longDateFormat(n)||n,r=r||" - ",$(t,e,X(n),r,a)}function $(t,e,n,r,a){var o,i,s,l,c="",d="",u="",f="",v="";for(i=0;n.length>i&&(o=V(t,e,n[i]),o!==!1);i++)c+=o;for(s=n.length-1;s>i&&(o=V(t,e,n[s]),o!==!1);s--)d=o+d;for(l=i;s>=l;l++)u+=I(t,n[l]),f+=I(e,n[l]);return(u||f)&&(v=a?f+r+u:u+r+f),c+v+d}function V(t,e,n){var r,a;return"string"==typeof n?n:(r=n.token)&&(a=Le[r.charAt(0)],a&&t.isSame(e,a))?P(t,r):!1}function X(t){return t in Ze?Ze[t]:Ze[t]=U(t)}function U(t){for(var e,n=[],r=/\[([^\]]*)\]|\(([^\)]*)\)|((\w)\4*o?T?)|([^\w\[\(]+)/g;e=r.exec(t);)e[1]?n.push(e[1]):e[2]?n.push({maybe:U(e[2])}):e[3]?n.push({token:e[3]}):e[5]&&n.push(e[5]);return n}function G(t,e){function n(t,e){return t.clone().stripTime().add("months",e).startOf("month")}function r(t){a.intervalStart=t.clone().stripTime().startOf("month"),a.intervalEnd=a.intervalStart.clone().add("months",1),a.start=a.intervalStart.clone().startOf("week"),a.start=a.skipHiddenDays(a.start),a.end=a.intervalEnd.clone().add("days",(7-a.intervalEnd.weekday())%7),a.end=a.skipHiddenDays(a.end,-1,!0);var n=Math.ceil(a.end.diff(a.start,"weeks",!0));"fixed"==a.opt("weekMode")&&(a.end.add("weeks",6-n),n=6),a.title=e.formatDate(a.intervalStart,a.opt("titleFormat")),a.renderBasic(n,a.getCellsPerWeek(),!0)}var a=this;a.incrementDate=n,a.render=r,K.call(a,t,e,"month")}function Q(t,e){function n(t,e){return t.clone().stripTime().add("weeks",e).startOf("week")}function r(t){a.intervalStart=t.clone().stripTime().startOf("week"),a.intervalEnd=a.intervalStart.clone().add("weeks",1),a.start=a.skipHiddenDays(a.intervalStart),a.end=a.skipHiddenDays(a.intervalEnd,-1,!0),a.title=e.formatRange(a.start,a.end.clone().subtract(1),a.opt("titleFormat")," — "),a.renderBasic(1,a.getCellsPerWeek(),!1)}var a=this;a.incrementDate=n,a.render=r,K.call(a,t,e,"basicWeek")}function J(t,e){function n(t,e){var n=t.clone().stripTime().add("days",e);return n=a.skipHiddenDays(n,0>e?-1:1)}function r(t){a.start=a.intervalStart=t.clone().stripTime(),a.end=a.intervalEnd=a.start.clone().add("days",1),a.title=e.formatDate(a.start,a.opt("titleFormat")),a.renderBasic(1,1,!1)}var a=this;a.incrementDate=n,a.render=r,K.call(a,t,e,"basicDay")}function K(e,n,r){function a(t,e,n){U=t,G=e,Q=n,o(),W||i(),s()}function o(){re=ie("theme")?"ui":"fc",ae=ie("columnFormat"),oe=ie("weekNumbers")}function i(){I=t("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(e)}function s(){var n=l();H&&H.remove(),H=t(n).appendTo(e),O=H.find("thead"),A=O.find(".fc-day-header"),W=H.find("tbody"),L=W.find("tr"),Z=W.find(".fc-day"),P=L.find("td:first-child"),B=L.eq(0).find(".fc-day > div"),j=L.eq(0).find(".fc-day-content > div"),N(O.add(O.find("tr"))),N(L),L.eq(0).addClass("fc-first"),L.filter(":last").addClass("fc-last"),Z.each(function(e,n){var r=ue(Math.floor(e/G),e%G);se("dayRender",Y,r,t(n))}),h(Z)}function l(){var t="<table class='fc-border-separate' style='width:100%' cellspacing='0'>"+c()+d()+"</table>";return t}function c(){var t,e,n=re+"-widget-header",r="";for(r+="<thead><tr>",oe&&(r+="<th class='fc-week-number "+n+"'>"+R(ie("weekNumberTitle"))+"</th>"),t=0;G>t;t++)e=ue(0,t),r+="<th class='fc-day-header fc-"+He[e.day()]+" "+n+"'>"+R(he(e,ae))+"</th>";return r+="</tr></thead>"}function d(){var t,e,n,r=re+"-widget-content",a="";for(a+="<tbody>",t=0;U>t;t++){for(a+="<tr class='fc-week'>",oe&&(n=ue(t,0),a+="<td class='fc-week-number "+r+"'>"+"<div>"+R(pe(n))+"</div>"+"</td>"),e=0;G>e;e++)n=ue(t,e),a+=u(n);a+="</tr>"}return a+="</tbody>"}function u(t){var e=Y.intervalStart.month(),r=n.getNow().stripTime(),a="",o=re+"-widget-content",i=["fc-day","fc-"+He[t.day()],o];return t.month()!=e&&i.push("fc-other-month"),t.isSame(r,"day")?i.push("fc-today",re+"-state-highlight"):r>t?i.push("fc-past"):i.push("fc-future"),a+="<td class='"+i.join(" ")+"'"+" data-date='"+t.format()+"'"+">"+"<div>",Q&&(a+="<div class='fc-day-number'>"+t.date()+"</div>"),a+="<div class='fc-day-content'><div style='position:relative'>&nbsp;</div></div></div></td>"}function f(e){$=e;var n,r,a,o=Math.max($-O.height(),0);"variable"==ie("weekMode")?n=r=Math.floor(o/(1==U?2:6)):(n=Math.floor(o/U),r=o-n*(U-1)),P.each(function(e,o){U>e&&(a=t(o),a.find("> div").css("min-height",(e==U-1?r:n)-T(a)))})}function v(t){q=t,ee.clear(),ne.clear(),X=0,oe&&(X=O.find("th.fc-week-number").outerWidth()),V=Math.floor((q-X)/G),g(A.slice(0,-1),V)}function h(t){t.click(p).mousedown(de)}function p(e){if(!ie("selectable")){var r=n.moment(t(this).data("date"));se("dayClick",this,r,e)}}function m(t,e,n){n&&J.build();for(var r=ve(t,e),a=0;r.length>a;a++){var o=r[a];h(y(o.row,o.leftCol,o.row,o.rightCol))}}function y(t,n,r,a){var o=J.rect(t,n,r,a,e);return le(o,e)}function b(t){return t.clone().stripTime().add("days",1)}function w(t,e){m(t,e,!0)}function D(){ce()}function C(t,e){var n=fe(t),r=Z[n.row*G+n.col];se("dayClick",r,t,e)}function E(t,e){K.start(function(t){if(ce(),t){var e=ue(t),r=e.clone().add(n.defaultAllDayEventDuration);m(e,r)}},e)}function S(t,e,n){var r=K.stop();ce(),r&&se("drop",t,ue(r),e,n)}function k(t){return ee.left(t)}function x(t){return ee.right(t)}function M(t){return ne.left(t)}function z(t){return ne.right(t)}function _(t){return L.eq(t)}var Y=this;Y.renderBasic=a,Y.setHeight=f,Y.setWidth=v,Y.renderDayOverlay=m,Y.defaultSelectionEnd=b,Y.renderSelection=w,Y.clearSelection=D,Y.reportDayClick=C,Y.dragStart=E,Y.dragStop=S,Y.getHoverListener=function(){return K},Y.colLeft=k,Y.colRight=x,Y.colContentLeft=M,Y.colContentRight=z,Y.getIsCellAllDay=function(){return!0},Y.allDayRow=_,Y.getRowCnt=function(){return U},Y.getColCnt=function(){return G},Y.getColWidth=function(){return V},Y.getDaySegmentContainer=function(){return I},me.call(Y,e,n,r),Ce.call(Y),Te.call(Y),te.call(Y);var H,O,A,W,L,Z,P,B,j,I,q,$,V,X,U,G,Q,J,K,ee,ne,re,ae,oe,ie=Y.opt,se=Y.trigger,le=Y.renderOverlay,ce=Y.clearOverlays,de=Y.daySelectionMousedown,ue=Y.cellToDate,fe=Y.dateToCell,ve=Y.rangeToSegments,he=n.formatDate,pe=n.calculateWeekNumber;F(e.addClass("fc-grid")),J=new Ee(function(e,n){var r,a,o;A.each(function(e,i){r=t(i),a=r.offset().left,e&&(o[1]=a),o=[a],n[e]=o}),o[1]=a+r.outerWidth(),L.each(function(n,i){U>n&&(r=t(i),a=r.offset().top,n&&(o[1]=a),o=[a],e[n]=o)}),o[1]=a+r.outerHeight()}),K=new Se(J),ee=new xe(function(t){return B.eq(t)}),ne=new xe(function(t){return j.eq(t)})}function te(){function t(t,e){n.renderDayEvents(t,e)}function e(){n.getDaySegmentContainer().empty()}var n=this;n.renderEvents=t,n.clearEvents=e,ye.call(n)}function ee(t,e){function n(t,e){return t.clone().stripTime().add("weeks",e).startOf("week")}function r(t){a.intervalStart=t.clone().stripTime().startOf("week"),a.intervalEnd=a.intervalStart.clone().add("weeks",1),a.start=a.skipHiddenDays(a.intervalStart),a.end=a.skipHiddenDays(a.intervalEnd,-1,!0),a.title=e.formatRange(a.start,a.end.clone().subtract(1),a.opt("titleFormat")," — "),a.renderAgenda(a.getCellsPerWeek())}var a=this;a.incrementDate=n,a.render=r,oe.call(a,t,e,"agendaWeek")}function ne(t,e){function n(t,e){var n=t.clone().stripTime().add("days",e);return n=a.skipHiddenDays(n,0>e?-1:1)}function r(t){a.start=a.intervalStart=t.clone().stripTime(),a.end=a.intervalEnd=a.start.clone().add("days",1),a.title=e.formatDate(a.start,a.opt("titleFormat")),a.renderAgenda(1)}var a=this;a.incrementDate=n,a.render=r,oe.call(a,t,e,"agendaDay")}function re(t,e){return e.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"a")}function ae(t,e){return e.longDateFormat("LT").replace(/\s*a$/i,"")}function oe(n,r,a){function o(t){ke=t,i(),$?l():s()}function i(){Ne=Le("theme")?"ui":"fc",Ye=Le("isRTL"),We=Le("columnFormat"),Oe=e.duration(Le("minTime")),Ae=e.duration(Le("maxTime")),ge=e.duration(Le("slotDuration")),be=Le("snapDuration"),be=be?e.duration(be):ge}function s(){var r,a,o,i,s=Ne+"-widget-header",c=Ne+"-widget-content",d=0===ge.asMinutes()%15;for(l(),ee=t("<div style='position:absolute;z-index:2;left:0;width:100%'/>").appendTo(n),Le("allDaySlot")?(ne=t("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(ee),r="<table style='width:100%' class='fc-agenda-allday' cellspacing='0'><tr><th class='"+s+" fc-agenda-axis'>"+(Le("allDayHTML")||R(Le("allDayText")))+"</th>"+"<td>"+"<div class='fc-day-content'><div style='position:relative'/></div>"+"</td>"+"<th class='"+s+" fc-agenda-gutter'>&nbsp;</th>"+"</tr>"+"</table>",re=t(r).appendTo(ee),ae=re.find("tr"),y(ae.find("td")),ee.append("<div class='fc-agenda-divider "+s+"'>"+"<div class='fc-agenda-divider-inner'/>"+"</div>")):ne=t([]),oe=t("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>").appendTo(ee),se=t("<div style='position:relative;width:100%;overflow:hidden'/>").appendTo(oe),le=t("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(se),r="<table class='fc-agenda-slots' style='width:100%' cellspacing='0'><tbody>",a=e.duration(+Oe),Me=0;Ae>a;)o=q.start.clone().time(a),i=o.minutes(),r+="<tr class='fc-slot"+Me+" "+(i?"fc-minor":"")+"'>"+"<th class='fc-agenda-axis "+s+"'>"+(d&&i?"&nbsp;":R(Ge(o,Le("axisFormat"))))+"</th>"+"<td class='"+c+"'>"+"<div style='position:relative'>&nbsp;</div>"+"</td>"+"</tr>",a.add(ge),Me++;r+="</tbody></table>",ce=t(r).appendTo(se),b(ce.find("td"))}function l(){var e=c();$&&$.remove(),$=t(e).appendTo(n),V=$.find("thead"),X=V.find("th").slice(1,-1),U=$.find("tbody"),G=U.find("td").slice(0,-1),Q=G.find("> div"),J=G.find(".fc-day-content > div"),K=G.eq(0),te=Q.eq(0),N(V.add(V.find("tr"))),N(U.add(U.find("tr")))}function c(){var t="<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>"+d()+u()+"</table>";return t}function d(){var t,e,n,r=Ne+"-widget-header",a="";for(a+="<thead><tr>",Le("weekNumbers")?(t=Ve(0,0),e=Qe(t),Ye?e+=Le("weekNumberTitle"):e=Le("weekNumberTitle")+e,a+="<th class='fc-agenda-axis fc-week-number "+r+"'>"+R(e)+"</th>"):a+="<th class='fc-agenda-axis "+r+"'>&nbsp;</th>",n=0;ke>n;n++)t=Ve(0,n),a+="<th class='fc-"+He[t.day()]+" fc-col"+n+" "+r+"'>"+R(Ge(t,We))+"</th>";return a+="<th class='fc-agenda-gutter "+r+"'>&nbsp;</th>"+"</tr>"+"</thead>"}function u(){var t,e,n,a,o,i=Ne+"-widget-header",s=Ne+"-widget-content",l=r.getNow().stripTime(),c="";for(c+="<tbody><tr><th class='fc-agenda-axis "+i+"'>&nbsp;</th>",n="",e=0;ke>e;e++)t=Ve(0,e),o=["fc-col"+e,"fc-"+He[t.day()],s],t.isSame(l,"day")?o.push(Ne+"-state-highlight","fc-today"):l>t?o.push("fc-past"):o.push("fc-future"),a="<td class='"+o.join(" ")+"'>"+"<div>"+"<div class='fc-day-content'>"+"<div style='position:relative'>&nbsp;</div>"+"</div>"+"</div>"+"</td>",n+=a;return c+=n,c+="<td class='fc-agenda-gutter "+s+"'>&nbsp;</td>"+"</tr>"+"</tbody>"}function f(t){void 0===t&&(t=fe),fe=t,Je={};var e=U.position().top,n=oe.position().top,r=Math.min(t-e,ce.height()+n+1);te.height(r-T(K)),ee.css("top",e),oe.height(r-n-1);var a=ce.find("tr:first").height()+1,o=ce.find("tr:eq(1)").height();ye=(a+o)/2,we=ge/be,De=ye/we}function v(e){ue=e,_e.clear(),Fe.clear();var n=V.find("th:first");re&&(n=n.add(re.find("th:first"))),n=n.add(ce.find("th:first")),ve=0,g(n.width("").each(function(e,n){ve=Math.max(ve,t(n).outerWidth())}),ve);var r=$.find(".fc-agenda-gutter");re&&(r=r.add(re.find("th.fc-agenda-gutter")));var a=oe[0].clientWidth;pe=oe.width()-a,pe?(g(r,pe),r.show().prev().removeClass("fc-last")):r.hide().prev().addClass("fc-last"),he=Math.floor((a-ve)/ke),g(X.slice(0,-1),he)}function h(){function t(){oe.scrollTop(n)}var n=O(e.duration(Le("scrollTime")))+1;t(),setTimeout(t,0)}function p(){h()}function y(t){t.click(w).mousedown(qe)}function b(t){t.click(w).mousedown(P)}function w(t){if(!Le("selectable")){var e=Math.min(ke-1,Math.floor((t.pageX-$.offset().left-ve)/he)),n=Ve(0,e),a=this.parentNode.className.match(/fc-slot(\d+)/);if(a){var o=parseInt(a[1]);n.add(Oe+o*ge),n=r.rezoneDate(n),Ze("dayClick",G[e],n,t)}else Ze("dayClick",G[e],n,t)}}function D(t,e,n){n&&ze.build();for(var r=Ue(t,e),a=0;r.length>a;a++){var o=r[a];y(C(o.row,o.leftCol,o.row,o.rightCol))}}function C(t,e,n,r){var a=ze.rect(t,e,n,r,ee);return Pe(a,ee)}function E(t,e){t=t.clone().stripZone(),e=e.clone().stripZone();for(var n=0;ke>n;n++){var r=Ve(0,n),a=r.clone().add("days",1),o=t>r?t:r,i=e>a?a:e;if(i>o){var s=ze.rect(0,n,0,n,se),l=H(o,r),c=H(i,r);s.top=l,s.height=c-l,b(Pe(s,se))}}}function S(t){return _e.left(t)}function k(t){return Fe.left(t)}function M(t){return _e.right(t)}function z(t){return Fe.right(t)}function _(t){return Le("allDaySlot")&&!t.row}function Y(t){var n=Ve(0,t.col),a=t.row;return Le("allDaySlot")&&a--,a>=0&&(n.time(e.duration(Oe+a*ge)),n=r.rezoneDate(n)),n}function H(t,n){return O(e.duration(t.clone().stripZone()-n.clone().stripTime()))}function O(t){if(Oe>t)return 0;if(t>=Ae)return ce.height();var e=(t-Oe)/ge,n=Math.floor(e),r=e-n,a=Je[n];void 0===a&&(a=Je[n]=ce.find("tr").eq(n).find("td div")[0].offsetTop);var o=a-1+r*ye;return o=Math.max(o,0)}function A(t){return t.hasTime()?t.clone().add(ge):t.clone().add("days",1)}function W(t,e){t.hasTime()||e.hasTime()?L(t,e):Le("allDaySlot")&&D(t,e,!0)}function L(e,n){var r=Le("selectHelper");if(ze.build(),r){var a=Xe(e).col;if(a>=0&&ke>a){var o=ze.rect(0,a,0,a,se),i=H(e,e),s=H(n,e);if(s>i){if(o.top=i,o.height=s-i,o.left+=2,o.width-=5,t.isFunction(r)){var l=r(e,n);l&&(o.position="absolute",de=t(l).css(o).appendTo(se))}else o.isStart=!0,o.isEnd=!0,de=t($e({title:"",start:e,end:n,className:["fc-select-helper"],editable:!1},o)),de.css("opacity",Le("dragOpacity"));de&&(b(de),se.append(de),g(de,o.width,!0),m(de,o.height,!0))}}}else E(e,n)}function Z(){Be(),de&&(de.remove(),de=null)}function P(e){if(1==e.which&&Le("selectable")){Ie(e);var n;Re.start(function(t,e){if(Z(),t&&t.col==e.col&&!_(t)){var r=Y(e),a=Y(t);n=[r,r.clone().add(be),a,a.clone().add(be)].sort(x),L(n[0],n[3])}else n=null},e),t(document).one("mouseup",function(t){Re.stop(),n&&(+n[0]==+n[1]&&B(n[0],t),je(n[0],n[3],t))})}}function B(t,e){Ze("dayClick",G[Xe(t).col],t,e)}function j(t,e){Re.start(function(t){if(Be(),t){var e=Y(t),n=e.clone();e.hasTime()?(n.add(r.defaultTimedEventDuration),E(e,n)):(n.add(r.defaultAllDayEventDuration),D(e,n))}},e)}function I(t,e,n){var r=Re.stop();Be(),r&&Ze("drop",t,Y(r),e,n)}var q=this;q.renderAgenda=o,q.setWidth=v,q.setHeight=f,q.afterRender=p,q.computeDateTop=H,q.getIsCellAllDay=_,q.allDayRow=function(){return ae},q.getCoordinateGrid=function(){return ze},q.getHoverListener=function(){return Re},q.colLeft=S,q.colRight=M,q.colContentLeft=k,q.colContentRight=z,q.getDaySegmentContainer=function(){return ne},q.getSlotSegmentContainer=function(){return le},q.getSlotContainer=function(){return se},q.getRowCnt=function(){return 1},q.getColCnt=function(){return ke},q.getColWidth=function(){return he},q.getSnapHeight=function(){return De},q.getSnapDuration=function(){return be},q.getSlotHeight=function(){return ye},q.getSlotDuration=function(){return ge},q.getMinTime=function(){return Oe},q.getMaxTime=function(){return Ae},q.defaultSelectionEnd=A,q.renderDayOverlay=D,q.renderSelection=W,q.clearSelection=Z,q.reportDayClick=B,q.dragStart=j,q.dragStop=I,me.call(q,n,r,a),Ce.call(q),Te.call(q),ie.call(q);var $,V,X,U,G,Q,J,K,te,ee,ne,re,ae,oe,se,le,ce,de,ue,fe,ve,he,pe,ge,ye,be,we,De,ke,Me,ze,Re,_e,Fe,Ne,Ye,Oe,Ae,We,Le=q.opt,Ze=q.trigger,Pe=q.renderOverlay,Be=q.clearOverlays,je=q.reportSelection,Ie=q.unselect,qe=q.daySelectionMousedown,$e=q.slotSegHtml,Ve=q.cellToDate,Xe=q.dateToCell,Ue=q.rangeToSegments,Ge=r.formatDate,Qe=r.calculateWeekNumber,Je={};F(n.addClass("fc-agenda")),ze=new Ee(function(e,n){function r(t){return Math.max(l,Math.min(c,t))}var a,o,i;X.each(function(e,r){a=t(r),o=a.offset().left,e&&(i[1]=o),i=[o],n[e]=i}),i[1]=o+a.outerWidth(),Le("allDaySlot")&&(a=ae,o=a.offset().top,e[0]=[o,o+a.outerHeight()]);for(var s=se.offset().top,l=oe.offset().top,c=l+oe.outerHeight(),d=0;Me*we>d;d++)e.push([r(s+De*d),r(s+De*(d+1))])
+}),Re=new Se(ze),_e=new xe(function(t){return Q.eq(t)}),Fe=new xe(function(t){return J.eq(t)})}function ie(){function n(t,e){var n,r=t.length,o=[],s=[];for(n=0;r>n;n++)t[n].allDay?o.push(t[n]):s.push(t[n]);v("allDaySlot")&&(V(o,e),D()),i(a(s),e)}function r(){C().empty(),E().empty()}function a(t){var e,n,r,a,i,s=F(),l=X(),c=U(),d=[];for(n=0;s>n;n++)for(e=_(0,n),i=o(t,e.clone().time(l),e.clone().time(c)),i=se(i),r=0;i.length>r;r++)a=i[r],a.col=n,d.push(a);return d}function o(t,e,n){e=e.clone().stripZone(),n=n.clone().stripZone();var r,a,o,i,s,l,c,d,u=[],f=t.length;for(r=0;f>r;r++)a=t[r],o=a.start.clone().stripZone(),i=K(a).stripZone(),i>e&&n>o&&(e>o?(s=e.clone(),c=!1):(s=o,c=!0),i>n?(l=n.clone(),d=!1):(l=i,d=!0),u.push({event:a,start:s,end:l,isStart:c,isEnd:d}));return u.sort(ge)}function i(e,n){var r,a,o,i,c,d,u,f,g,m,b,w,D,C,S,x,R=e.length,_="",F=E(),N=v("isRTL");for(r=0;R>r;r++)a=e[r],o=a.event,i=k(a.start,a.start),c=k(a.end,a.start),d=M(a.col),u=z(a.col),f=u-d,u-=.025*f,f=u-d,g=f*(a.forwardCoord-a.backwardCoord),v("slotEventOverlap")&&(g=Math.max(2*(g-10),g)),N?(b=u-a.backwardCoord*f,m=b-g):(m=d+a.backwardCoord*f,b=m+g),m=Math.max(m,d),b=Math.min(b,u),g=b-m,a.top=i,a.left=m,a.outerWidth=g,a.outerHeight=c-i,_+=s(o,a);for(F[0].innerHTML=_,w=F.children(),r=0;R>r;r++)a=e[r],o=a.event,D=t(w[r]),C=h("eventRender",o,o,D),C===!1?D.remove():(C&&C!==!0&&(D.remove(),D=t(C).css({position:"absolute",top:a.top,left:a.left}).appendTo(F)),a.element=D,o._id===n?l(o,D,a):D[0]._fci=r,Z(o,D));for(p(F,e,l),r=0;R>r;r++)a=e[r],(D=a.element)&&(a.vsides=T(D,!0),a.hsides=y(D,!0),S=D.find(".fc-event-title"),S.length&&(a.contentTop=S[0].offsetTop));for(r=0;R>r;r++)a=e[r],(D=a.element)&&(D[0].style.width=Math.max(0,a.outerWidth-a.hsides)+"px",x=Math.max(0,a.outerHeight-a.vsides),D[0].style.height=x+"px",o=a.event,void 0!==a.contentTop&&10>x-a.contentTop&&(D.find("div.fc-event-time").text(Q(o.start,v("timeFormat"))+" - "+o.title),D.find("div.fc-event-title").remove()),h("eventAfterRender",o,o,D))}function s(t,e){var n="<",r=t.url,a=Y(t,v),o=["fc-event","fc-event-vert"];return g(t)&&o.push("fc-event-draggable"),e.isStart&&o.push("fc-event-start"),e.isEnd&&o.push("fc-event-end"),o=o.concat(t.className),t.source&&(o=o.concat(t.source.className||[])),n+=r?"a href='"+R(t.url)+"'":"div",n+=" class='"+o.join(" ")+"'"+" style="+"'"+"position:absolute;"+"top:"+e.top+"px;"+"left:"+e.left+"px;"+a+"'"+">"+"<div class='fc-event-inner'>"+"<div class='fc-event-time'>",n+=t.end?R(J(t.start,t.end,v("timeFormat"))):R(Q(t.start,v("timeFormat"))),n+="</div><div class='fc-event-title'>"+R(t.title||"")+"</div>"+"</div>"+"<div class='fc-event-bg'></div>",e.isEnd&&b(t)&&(n+="<div class='ui-resizable-handle ui-resizable-s'>=</div>"),n+="</"+(r?"a":"div")+">"}function l(t,e,n){var r=e.find("div.fc-event-time");g(t)&&d(t,e,r),n.isEnd&&b(t)&&u(t,e,r),w(t,e)}function c(t,n,r){function a(){c||(n.width(o).height("").draggable("option","grid",null),c=!0)}var o,i,s,l=r.isStart,c=!0,d=S(),u=N(),f=X(),p=W(),g=A(),y=O(),b=H();n.draggable({opacity:v("dragOpacity","month"),revertDuration:v("dragRevertDuration"),start:function(e,r){h("eventDragStart",n,t,e,r),B(t,n),o=n.width(),d.start(function(e,r){if($(),e){i=!1;var o=_(0,r.col),d=_(0,e.col);s=d.diff(o,"days"),e.row?l?c&&(n.width(u-10),m(n,G.defaultTimedEventDuration/p*g),n.draggable("option","grid",[u,1]),c=!1):i=!0:(q(t.start.clone().add("days",s),K(t).add("days",s)),a()),i=i||c&&!s}else a(),i=!0;n.draggable("option","revert",i)},e,"drag")},stop:function(r,o){if(d.stop(),$(),h("eventDragStop",n,t,r,o),i)a(),n.css("filter",""),P(t,n);else{var l,u,v=t.start.clone().add("days",s);c||(u=Math.round((n.offset().top-L().offset().top)/b),l=e.duration(f+u*y),v=G.rezoneDate(v.clone().time(l))),j(this,t,v,r,o)}}})}function d(t,e,n){function r(){$(),s&&(c?(n.hide(),e.draggable("option","grid",null),q(b,w)):(a(),n.css("display",""),e.draggable("option","grid",[C,E])))}function a(){var e;b&&(e=t.end?J(b,w,v("timeFormat")):Q(b,v("timeFormat")),n.text(e))}var o,i,s,l,c,d,u,p,g,m,y,b,w,D=f.getCoordinateGrid(),T=F(),C=N(),E=H(),S=O();e.draggable({scroll:!1,grid:[C,E],axis:1==T?"y":!1,opacity:v("dragOpacity"),revertDuration:v("dragRevertDuration"),start:function(n,r){h("eventDragStart",e,t,n,r),B(t,e),D.build(),o=e.position(),i=D.cell(n.pageX,n.pageY),s=l=!0,c=d=x(i),u=p=0,g=0,m=y=0,b=null,w=null},drag:function(n,a){var f=D.cell(n.pageX,n.pageY);if(s=!!f){if(c=x(f),u=Math.round((a.position.left-o.left)/C),u!=p){var v=_(0,i.col),h=i.col+u;h=Math.max(0,h),h=Math.min(T-1,h);var k=_(0,h);g=k.diff(v,"days")}c||(m=Math.round((a.position.top-o.top)/E))}(s!=l||c!=d||u!=p||m!=y)&&(c?(b=t.start.clone().stripTime().add("days",g),w=b.clone().add(G.defaultAllDayEventDuration)):(b=t.start.clone().add(m*S).add("days",g),w=K(t).add(m*S).add("days",g)),r(),l=s,d=c,p=u,y=m),e.draggable("option","revert",!s)},stop:function(n,a){$(),h("eventDragStop",e,t,n,a),s&&(c||g||m)?j(this,t,b,n,a):(s=!0,c=!1,u=0,g=0,m=0,r(),e.css("filter",""),e.css(o),P(t,e))}})}function u(t,e,n){var r,a,o,i=H(),s=O();e.resizable({handles:{s:".ui-resizable-handle"},grid:i,start:function(n,o){r=a=0,B(t,e),h("eventResizeStart",this,t,n,o)},resize:function(l,c){if(r=Math.round((Math.max(i,e.height())-c.originalSize.height)/i),r!=a){o=K(t).add(s*r);var d;d=r||t.end?J(t.start,o,v("timeFormat")):Q(t.start,v("timeFormat")),n.text(d),a=r}},stop:function(n,a){h("eventResizeStop",this,t,n,a),r?I(this,t,o,n,a):P(t,e)}})}var f=this;f.renderEvents=n,f.clearEvents=r,f.slotSegHtml=s,ye.call(f);var v=f.opt,h=f.trigger,g=f.isEventDraggable,b=f.isEventResizable,w=f.eventElementHandlers,D=f.setHeight,C=f.getDaySegmentContainer,E=f.getSlotSegmentContainer,S=f.getHoverListener,k=f.computeDateTop,x=f.getIsCellAllDay,M=f.colContentLeft,z=f.colContentRight,_=f.cellToDate,F=f.getColCnt,N=f.getColWidth,H=f.getSnapHeight,O=f.getSnapDuration,A=f.getSlotHeight,W=f.getSlotDuration,L=f.getSlotContainer,Z=f.reportEventElement,P=f.showEvents,B=f.hideEvents,j=f.eventDrop,I=f.eventResize,q=f.renderDayOverlay,$=f.clearOverlays,V=f.renderDayEvents,X=f.getMinTime,U=f.getMaxTime,G=f.calendar,Q=G.formatDate,J=G.formatRange,K=G.getEventEnd;f.draggableDayEvent=c}function se(t){var e,n=le(t),r=n[0];if(ce(n),r){for(e=0;r.length>e;e++)de(r[e]);for(e=0;r.length>e;e++)ue(r[e],0,0)}return fe(n)}function le(t){var e,n,r,a=[];for(e=0;t.length>e;e++){for(n=t[e],r=0;a.length>r&&ve(n,a[r]).length;r++);(a[r]||(a[r]=[])).push(n)}return a}function ce(t){var e,n,r,a,o;for(e=0;t.length>e;e++)for(n=t[e],r=0;n.length>r;r++)for(a=n[r],a.forwardSegs=[],o=e+1;t.length>o;o++)ve(a,t[o],a.forwardSegs)}function de(t){var e,n,r=t.forwardSegs,a=0;if(void 0===t.forwardPressure){for(e=0;r.length>e;e++)n=r[e],de(n),a=Math.max(a,1+n.forwardPressure);t.forwardPressure=a}}function ue(t,e,n){var r,a=t.forwardSegs;if(void 0===t.forwardCoord)for(a.length?(a.sort(pe),ue(a[0],e+1,n),t.forwardCoord=a[0].backwardCoord):t.forwardCoord=1,t.backwardCoord=t.forwardCoord-(t.forwardCoord-n)/(e+1),r=0;a.length>r;r++)ue(a[r],0,t.forwardCoord)}function fe(t){var e,n,r,a=[];for(e=0;t.length>e;e++)for(n=t[e],r=0;n.length>r;r++)a.push(n[r]);return a}function ve(t,e,n){n=n||[];for(var r=0;e.length>r;r++)he(t,e[r])&&n.push(e[r]);return n}function he(t,e){return t.end>e.start&&t.start<e.end}function pe(t,e){return e.forwardPressure-t.forwardPressure||(t.backwardCoord||0)-(e.backwardCoord||0)||ge(t,e)}function ge(t,e){return t.start-e.start||e.end-e.start-(t.end-t.start)||(t.event.title||"").localeCompare(e.event.title)}function me(n,r,a){function o(e,n){var r=A[e];return t.isPlainObject(r)&&!i(e)?z(r,n||a):r}function s(t,e){return r.trigger.apply(r,[t,e||F].concat(Array.prototype.slice.call(arguments,2),[F]))}function l(t){var e=t.source||{};return O(t.startEditable,e.startEditable,o("eventStartEditable"),t.editable,e.editable,o("editable"))}function c(t){var e=t.source||{};return O(t.durationEditable,e.durationEditable,o("eventDurationEditable"),t.editable,e.editable,o("editable"))}function d(){Y={},H=[]}function u(t,e){H.push({event:t,element:e}),Y[t._id]?Y[t._id].push(e):Y[t._id]=[e]}function f(){t.each(H,function(t,e){F.trigger("eventDestroy",e.event,e.event,e.element)})}function v(t,e){e.click(function(n){return e.hasClass("ui-draggable-dragging")||e.hasClass("ui-resizable-resizing")?void 0:s("eventClick",this,t,n)}).hover(function(e){s("eventMouseover",this,t,e)},function(e){s("eventMouseout",this,t,e)})}function h(t,e){g(t,e,"show")}function p(t,e){g(t,e,"hide")}function g(t,e,n){var r,a=Y[t._id],o=a.length;for(r=0;o>r;r++)e&&a[r][0]==e[0]||a[r][n]()}function m(t,e,n,a,o){var i=r.mutateEvent(e,n,null);s("eventDrop",t,e,function(){i(),N(e._id)},a,o),N(e._id)}function y(t,e,n,a,o){var i=r.mutateEvent(e,null,n);s("eventResize",t,e,function(){i(),N(e._id)},a,o),N(e._id)}function b(t){return e.isMoment(t)&&(t=t.day()),P[t]}function w(){return L}function D(t,e,n){var r=t.clone();for(e=e||1;P[(r.day()+(n?e:0)+7)%7];)r.add("days",e);return r}function T(){var t=C.apply(null,arguments),e=E(t),n=S(e);return n}function C(t,e){var n=F.getColCnt(),r=I?-1:1,a=I?n-1:0;"object"==typeof t&&(e=t.col,t=t.row);var o=t*n+(e*r+a);return o}function E(t){var e=F.start.day();return t+=B[e],7*Math.floor(t/L)+j[(t%L+L)%L]-e}function S(t){return F.start.clone().add("days",t)}function k(t){var e=x(t),n=M(e),r=R(n);return r}function x(t){return t.clone().stripTime().diff(F.start,"days")}function M(t){var e=F.start.day();return t+=e,Math.floor(t/7)*L+B[(t%7+7)%7]-B[e]}function R(t){var e=F.getColCnt(),n=I?-1:1,r=I?e-1:0,a=Math.floor(t/e),o=(t%e+e)%e*n+r;return{row:a,col:o}}function _(t,e){var n=F.getRowCnt(),r=F.getColCnt(),a=[],o=x(t),i=x(e),s=+e.time();s&&s>=W&&i++,i=Math.max(i,o+1);for(var l=M(o),c=M(i)-1,d=0;n>d;d++){var u=d*r,f=u+r-1,v=Math.max(l,u),h=Math.min(c,f);if(h>=v){var p=R(v),g=R(h),m=[p.col,g.col].sort(),y=E(v)==o,b=E(h)+1==i;a.push({row:d,leftCol:m[0],rightCol:m[1],isStart:y,isEnd:b})}}return a}var F=this;F.element=n,F.calendar=r,F.name=a,F.opt=o,F.trigger=s,F.isEventDraggable=l,F.isEventResizable=c,F.clearEventData=d,F.reportEventElement=u,F.triggerEventDestroy=f,F.eventElementHandlers=v,F.showEvents=h,F.hideEvents=p,F.eventDrop=m,F.eventResize=y;var N=r.reportEventChange,Y={},H=[],A=r.options,W=e.duration(A.nextDayThreshold);F.isHiddenDay=b,F.skipHiddenDays=D,F.getCellsPerWeek=w,F.dateToCell=k,F.dateToDayOffset=x,F.dayOffsetToCellOffset=M,F.cellOffsetToCell=R,F.cellToDate=T,F.cellToCellOffset=C,F.cellOffsetToDayOffset=E,F.dayOffsetToDate=S,F.rangeToSegments=_;var L,Z=o("hiddenDays")||[],P=[],B=[],j=[],I=o("isRTL");(function(){o("weekends")===!1&&Z.push(0,6);for(var e=0,n=0;7>e;e++)B[e]=n,P[e]=-1!=t.inArray(e,Z),P[e]||(j[n]=e,n++);if(L=n,!L)throw"invalid hiddenDays"})()}function ye(){function e(t,e){var n=r(t,!1,!0);we(n,function(t,e){x(t.event,e)}),m(n,e),we(n,function(t,e){E("eventAfterRender",t.event,t.event,e)})}function n(t,e,n){var a=r([t],!0,!1),o=[];return we(a,function(t,r){t.row===e&&r.css("top",n),o.push(r[0])}),o}function r(e,n,r){var o,l,u=I(),f=n?t("<div/>"):u,v=a(e);return i(v),o=s(v),f[0].innerHTML=o,l=f.children(),n&&u.append(l),c(v,l),we(v,function(t,e){t.hsides=y(e,!0)}),we(v,function(t,e){e.width(Math.max(0,t.outerWidth-t.hsides))}),we(v,function(t,e){t.outerHeight=e.outerHeight(!0)}),d(v,r),v}function a(t){for(var e=[],n=0;t.length>n;n++){var r=o(t[n]);e.push.apply(e,r)}return e}function o(t){for(var e=U(t.start,ne(t)),n=0;e.length>n;n++)e[n].event=t;return e}function i(t){for(var e=C("isRTL"),n=0;t.length>n;n++){var r=t[n],a=(e?r.isEnd:r.isStart)?B:Z,o=(e?r.isStart:r.isEnd)?j:P,i=a(r.leftCol),s=o(r.rightCol);r.left=i,r.outerWidth=s-i}}function s(t){for(var e="",n=0;t.length>n;n++)e+=l(t[n]);return e}function l(t){var e="",n=C("isRTL"),r=t.event,a=r.url,o=["fc-event","fc-event-hori"];S(r)&&o.push("fc-event-draggable"),t.isStart&&o.push("fc-event-start"),t.isEnd&&o.push("fc-event-end"),o=o.concat(r.className),r.source&&(o=o.concat(r.source.className||[]));var i=Y(r,C);return e+=a?"<a href='"+R(a)+"'":"<div",e+=" class='"+o.join(" ")+"'"+" style="+"'"+"position:absolute;"+"left:"+t.left+"px;"+i+"'"+">"+"<div class='fc-event-inner'>",!r.allDay&&t.isStart&&(e+="<span class='fc-event-time'>"+R(re(r.start,C("timeFormat")))+"</span>"),e+="<span class='fc-event-title'>"+R(r.title||"")+"</span>"+"</div>",r.allDay&&t.isEnd&&k(r)&&(e+="<div class='ui-resizable-handle ui-resizable-"+(n?"w":"e")+"'>"+"&nbsp;&nbsp;&nbsp;"+"</div>"),e+="</"+(a?"a":"div")+">"}function c(e,n){for(var r=0;e.length>r;r++){var a=e[r],o=a.event,i=n.eq(r),s=E("eventRender",o,o,i);s===!1?i.remove():(s&&s!==!0&&(s=t(s).css({position:"absolute",left:a.left}),i.replaceWith(s),i=s),a.element=i)}}function d(t,e){var n,r=u(t),a=g(),o=[];if(e)for(n=0;a.length>n;n++)a[n].height(r[n]);for(n=0;a.length>n;n++)o.push(a[n].position().top);we(t,function(t,e){e.css("top",o[t.row]+t.top)})}function u(t){for(var e,n=A(),r=W(),a=[],o=f(t),i=0;n>i;i++){var s=o[i],l=[];for(e=0;r>e;e++)l.push(0);for(var c=0;s.length>c;c++){var d=s[c];for(d.top=M(l.slice(d.leftCol,d.rightCol+1)),e=d.leftCol;d.rightCol>=e;e++)l[e]=d.top+d.outerHeight}a.push(M(l))}return a}function f(t){var e,n,r,a=A(),o=[];for(e=0;t.length>e;e++)n=t[e],r=n.row,n.element&&(o[r]?o[r].push(n):o[r]=[n]);for(r=0;a>r;r++)o[r]=v(o[r]||[]);return o}function v(t){for(var e=[],n=h(t),r=0;n.length>r;r++)e.push.apply(e,n[r]);return e}function h(t){t.sort(De);for(var e=[],n=0;t.length>n;n++){for(var r=t[n],a=0;e.length>a&&be(r,e[a]);a++);e[a]?e[a].push(r):e[a]=[r]}return e}function g(){var t,e=A(),n=[];for(t=0;e>t;t++)n[t]=L(t).find("div.fc-day-content > div");return n}function m(t,e){var n=I();we(t,function(t,n,r){var a=t.event;a._id===e?b(a,n,t):n[0]._fci=r}),p(n,t,b)}function b(t,e,n){S(t)&&T.draggableDayEvent(t,e,n),t.allDay&&n.isEnd&&k(t)&&T.resizableDayEvent(t,e,n),z(t,e)}function w(t,e){var n,r,a=X();e.draggable({delay:50,opacity:C("dragOpacity"),revertDuration:C("dragRevertDuration"),start:function(o,i){E("eventDragStart",e,t,o,i),N(t,e),a.start(function(a,o,i,s){if(e.draggable("option","revert",!a||!i&&!s),$(),a){var l=G(o),c=G(a);n=c.diff(l,"days"),r=t.start.clone().add("days",n),q(r,ne(t).add("days",n))}else n=0},o,"drag")},stop:function(o,i){a.stop(),$(),E("eventDragStop",e,t,o,i),n?H(this,t,r,o,i):(e.css("filter",""),_(t,e))}})}function D(e,r,a){var o=C("isRTL"),i=o?"w":"e",s=r.find(".ui-resizable-"+i),l=!1;F(r),r.mousedown(function(t){t.preventDefault()}).click(function(t){l&&(t.preventDefault(),t.stopImmediatePropagation())}),s.mousedown(function(o){function s(n){E("eventResizeStop",this,e,n),t("body").css("cursor",""),f.stop(),$(),c&&O(this,e,d,n),setTimeout(function(){l=!1},0)}if(1==o.which){l=!0;var c,d,u,f=X(),v=r.css("top"),h=t.extend({},e),p=te(K(e.start));V(),t("body").css("cursor",i+"-resize").one("mouseup",s),E("eventResizeStart",this,e,o),f.start(function(r,o){if(r){var s=Q(o),l=Q(r);if(l=Math.max(l,p),c=J(l)-J(s),d=ne(e).add("days",c),c){h.end=d;var f=u;u=n(h,a.row,v),u=t(u),u.find("*").css("cursor",i+"-resize"),f&&f.remove(),N(e)}else u&&(_(e),u.remove(),u=null);$(),q(e.start,d)}},o)}})}var T=this;T.renderDayEvents=e,T.draggableDayEvent=w,T.resizableDayEvent=D;var C=T.opt,E=T.trigger,S=T.isEventDraggable,k=T.isEventResizable,x=T.reportEventElement,z=T.eventElementHandlers,_=T.showEvents,N=T.hideEvents,H=T.eventDrop,O=T.eventResize,A=T.getRowCnt,W=T.getColCnt,L=T.allDayRow,Z=T.colLeft,P=T.colRight,B=T.colContentLeft,j=T.colContentRight,I=T.getDaySegmentContainer,q=T.renderDayOverlay,$=T.clearOverlays,V=T.clearSelection,X=T.getHoverListener,U=T.rangeToSegments,G=T.cellToDate,Q=T.cellToCellOffset,J=T.cellOffsetToDayOffset,K=T.dateToDayOffset,te=T.dayOffsetToCellOffset,ee=T.calendar,ne=ee.getEventEnd,re=ee.formatDate}function be(t,e){for(var n=0;e.length>n;n++){var r=e[n];if(r.leftCol<=t.rightCol&&r.rightCol>=t.leftCol)return!0}return!1}function we(t,e){for(var n=0;t.length>n;n++){var r=t[n],a=r.element;a&&e(r,a,n)}}function De(t,e){return e.rightCol-e.leftCol-(t.rightCol-t.leftCol)||e.event.allDay-t.event.allDay||t.event.start-e.event.start||(t.event.title||"").localeCompare(e.event.title)}function Te(){function e(t,e){n(),t=i.moment(t),e=e?i.moment(e):c(t),d(t,e),r(t,e)}function n(t){f&&(f=!1,u(),l("unselect",null,t))}function r(t,e,n){f=!0,l("select",null,t,e,n)}function a(e){var a=o.cellToDate,i=o.getIsCellAllDay,l=o.getHoverListener(),c=o.reportDayClick;if(1==e.which&&s("selectable")){n(e);var f;l.start(function(t,e){u(),t&&i(t)?(f=[a(e),a(t)].sort(x),d(f[0],f[1].clone().add("days",1))):f=null},e),t(document).one("mouseup",function(t){l.stop(),f&&(+f[0]==+f[1]&&c(f[0],t),r(f[0],f[1].clone().add("days",1),t))})}}var o=this;o.select=e,o.unselect=n,o.reportSelection=r,o.daySelectionMousedown=a;var i=o.calendar,s=o.opt,l=o.trigger,c=o.defaultSelectionEnd,d=o.renderSelection,u=o.clearSelection,f=!1;s("selectable")&&s("unselectAuto")&&t(document).mousedown(function(e){var r=s("unselectCancel");r&&t(e.target).parents(r).length||n(e)})}function Ce(){function e(e,n){var r=o.shift();return r||(r=t("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>")),r[0].parentNode!=n[0]&&r.appendTo(n),a.push(r.css(e).show()),r}function n(){for(var t;t=a.shift();)o.push(t.hide().unbind())}var r=this;r.renderOverlay=e,r.clearOverlays=n;var a=[],o=[]}function Ee(t){var e,n,r=this;r.build=function(){e=[],n=[],t(e,n)},r.cell=function(t,r){var a,o=e.length,i=n.length,s=-1,l=-1;for(a=0;o>a;a++)if(r>=e[a][0]&&e[a][1]>r){s=a;break}for(a=0;i>a;a++)if(t>=n[a][0]&&n[a][1]>t){l=a;break}return s>=0&&l>=0?{row:s,col:l}:null},r.rect=function(t,r,a,o,i){var s=i.offset();return{top:e[t][0]-s.top,left:n[r][0]-s.left,width:n[o][1]-n[r][0],height:e[a][1]-e[t][0]}}}function Se(e){function n(t){ke(t);var n=e.cell(t.pageX,t.pageY);(Boolean(n)!==Boolean(i)||n&&(n.row!=i.row||n.col!=i.col))&&(n?(o||(o=n),a(n,o,n.row-o.row,n.col-o.col)):a(n,o),i=n)}var r,a,o,i,s=this;s.start=function(s,l,c){a=s,o=i=null,e.build(),n(l),r=c||"mousemove",t(document).bind(r,n)},s.stop=function(){return t(document).unbind(r,n),i}}function ke(t){void 0===t.pageX&&(t.pageX=t.originalEvent.pageX,t.pageY=t.originalEvent.pageY)}function xe(t){function e(e){return r[e]=r[e]||t(e)}var n=this,r={},a={},o={};n.left=function(t){return a[t]=void 0===a[t]?e(t).position().left:a[t]},n.right=function(t){return o[t]=void 0===o[t]?n.left(t)+e(t).width():o[t]},n.clear=function(){r={},a={},o={}}}var Me={lang:"en",defaultTimedEventDuration:"02:00:00",defaultAllDayEventDuration:{days:1},forceEventDuration:!1,nextDayThreshold:"09:00:00",defaultView:"month",aspectRatio:1.35,header:{left:"title",center:"",right:"today prev,next"},weekends:!0,weekNumbers:!1,weekNumberTitle:"W",weekNumberCalculation:"local",lazyFetching:!0,startParam:"start",endParam:"end",timezoneParam:"timezone",titleFormat:{month:"MMMM YYYY",week:"ll",day:"LL"},columnFormat:{month:"ddd",week:r,day:"dddd"},timeFormat:{"default":n},isRTL:!1,buttonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",today:"today",month:"month",week:"week",day:"day"},buttonIcons:{prev:"left-single-arrow",next:"right-single-arrow",prevYear:"left-double-arrow",nextYear:"right-double-arrow"},theme:!1,themeButtonIcons:{prev:"circle-triangle-w",next:"circle-triangle-e",prevYear:"seek-prev",nextYear:"seek-next"},unselectAuto:!0,dropAccept:"*",handleWindowResize:!0},ze={en:{columnFormat:{week:"ddd M/D"}}},Re={header:{left:"next,prev today",center:"",right:"title"},buttonIcons:{prev:"right-single-arrow",next:"left-single-arrow",prevYear:"right-double-arrow",nextYear:"left-double-arrow"},themeButtonIcons:{prev:"circle-triangle-e",next:"circle-triangle-w",nextYear:"seek-prev",prevYear:"seek-next"}},_e=t.fullCalendar={version:"2.0.0-beta2"},Fe=_e.views={};t.fn.fullCalendar=function(e){var n=Array.prototype.slice.call(arguments,1),r=this;return this.each(function(a,o){var i,l=t(o),c=l.data("fullCalendar");"string"==typeof e?c&&t.isFunction(c[e])&&(i=c[e].apply(c,n),a||(r=i),"destroy"===e&&l.removeData("fullCalendar")):c||(c=new s(l,e),l.data("fullCalendar",c),c.render())}),r},_e.langs=ze,_e.datepickerLang=function(e,n,r){var a=ze[e];a||(a=ze[e]={}),o(a,{isRTL:r.isRTL,weekNumberTitle:r.weekHeader,titleFormat:{month:r.showMonthAfterYear?"YYYY["+r.yearSuffix+"] MMMM":"MMMM YYYY["+r.yearSuffix+"]"},buttonText:{prev:_(r.prevText),next:_(r.nextText),today:_(r.currentText)}}),t.datepicker&&(t.datepicker.regional[n]=t.datepicker.regional[e]=r,t.datepicker.regional.en=t.datepicker.regional[""],t.datepicker.setDefaults(r))},_e.lang=function(t,e){var n;e&&(n=ze[t],n||(n=ze[t]={}),o(n,e||{})),Me.lang=t},_e.sourceNormalizers=[],_e.sourceFetchers=[];var Ne={dataType:"json",cache:!1},Ye=1;_e.applyAll=H;var He=["sun","mon","tue","wed","thu","fri","sat"],Oe=/^\s*\d{4}-\d\d$/,Ae=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;_e.moment=function(){return A(arguments)},_e.moment.utc=function(){return A(arguments,!0)},_e.moment.parseZone=function(){return A(arguments,!0,!0)},W.prototype=u(e.fn),W.prototype.clone=function(){return A([this])},W.prototype.time=function(t){return null==t?e.duration({hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()}):(delete this._ambigTime,e.isDuration(t)||e.isMoment(t)||(t=e.duration(t)),this.hours(t.hours()+24*t.days()).minutes(t.minutes()).seconds(t.seconds()).milliseconds(t.milliseconds()))},W.prototype.stripTime=function(){var t=this.toArray();return e.fn.utc.call(this),this._ambigTime=!0,this._ambigZone=!0,this.year(t[0]).month(t[1]).date(t[2]).hours(0).minutes(0).seconds(0).milliseconds(0),this},W.prototype.hasTime=function(){return!this._ambigTime},W.prototype.stripZone=function(){var t=this.toArray();return e.fn.utc.call(this),this._ambigZone=!0,this.year(t[0]).month(t[1]).date(t[2]).hours(t[3]).minutes(t[4]).seconds(t[5]).milliseconds(t[6]),this},W.prototype.hasZone=function(){return!this._ambigZone},W.prototype.zone=function(t){return null!=t&&delete this._ambigZone,e.fn.zone.apply(this,arguments)},W.prototype.local=function(){return delete this._ambigZone,e.fn.local.apply(this,arguments)},W.prototype.utc=function(){return delete this._ambigZone,e.fn.utc.apply(this,arguments)},W.prototype.format=function(){return arguments[0]?B(this,arguments[0]):this._ambigTime?P(this,"YYYY-MM-DD"):this._ambigZone?P(this,"YYYY-MM-DD[T]HH:mm:ss"):P(this)},W.prototype.toISOString=function(){return this._ambigTime?P(this,"YYYY-MM-DD"):this._ambigZone?P(this,"YYYY-MM-DD[T]HH:mm:ss"):e.fn.toISOString.apply(this,arguments)},W.prototype.isWithin=function(t,e){var n=Z([this,t,e]);return n[0]>=n[1]&&n[0]<n[2]},t.each(["isBefore","isAfter","isSame"],function(t,n){W.prototype[n]=function(t,r){var a=Z([this,t]);return e.fn[n].call(a[0],a[1],r)}});var We={t:function(t){return P(t,"a").charAt(0)},T:function(t){return P(t,"A").charAt(0)}};_e.formatRange=q;var Le={Y:"year",M:"month",D:"day",d:"day"},Ze={};Fe.month=G,Fe.basicWeek=Q,Fe.basicDay=J,a({weekMode:"fixed"}),Fe.agendaWeek=ee,Fe.agendaDay=ne,a({allDaySlot:!0,allDayText:"all-day",scrollTime:"06:00:00",slotDuration:"00:30:00",axisFormat:re,timeFormat:{agenda:ae},dragOpacity:{agenda:.5},minTime:"00:00:00",maxTime:"24:00:00",slotEventOverlap:!0})});
\ No newline at end of file
diff --git a/static/libjs/fullcalendar/fullcalendar.print.css b/static/libjs/fullcalendar/fullcalendar.print.css
new file mode 100644
index 0000000000000000000000000000000000000000..90f9abd8222f06293b668e399efdb74acce9fd2b
--- /dev/null
+++ b/static/libjs/fullcalendar/fullcalendar.print.css
@@ -0,0 +1,32 @@
+/*!
+ * FullCalendar v2.0.0-beta2 Print Stylesheet
+ * Docs & License: http://arshaw.com/fullcalendar/
+ * (c) 2013 Adam Shaw
+ */
+
+/*
+ * Include this stylesheet on your page to get a more printer-friendly calendar.
+ * When including this stylesheet, use the media='print' attribute of the <link> tag.
+ * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
+ */
+ 
+ 
+ /* Events
+-----------------------------------------------------*/
+ 
+.fc-event {
+	background: #fff !important;
+	color: #000 !important;
+	}
+	
+/* for vertical events */
+	
+.fc-event-bg {
+	display: none !important;
+	}
+	
+.fc-event .ui-resizable-handle {
+	display: none !important;
+	}
+	
+	
diff --git a/static/libjs/jQuery-tagEditor/bower.json b/static/libjs/jQuery-tagEditor/bower.json
new file mode 100644
index 0000000000000000000000000000000000000000..8d5ae21d3e64b6da6f88b964894f237dac6dcbfd
--- /dev/null
+++ b/static/libjs/jQuery-tagEditor/bower.json
@@ -0,0 +1,33 @@
+{
+    "name": "jquery-tag-editor",
+	"description": "A powerful and lightweight tag editor plugin for jQuery.",
+    "version": "1.0.20",
+    "dependencies": {
+        "jquery": ">=1.7",
+        "caret": null
+    },
+    "homepage": "https://github.com/Pixabay/jQuery-tagEditor",
+    "authors": [{
+        "name": "Simon Steinberger",
+        "url": "https://pixabay.com/users/Simon/",
+        "email": "simon@pixabay.com"
+    }],
+    "keywords": [
+        "tags",
+        "keywords",
+        "editor",
+        "drag and drop",
+        "editable",
+        "edit"
+    ],
+    "licenses": [{
+        "type": "MIT",
+        "url": "http://www.opensource.org/licenses/mit-license.php"
+    }],
+    "ignore": [
+        "bower.json",
+        "demo.html",
+        "readme.md",
+        "tag-editor.jquery.json"
+    ]
+}
diff --git a/static/libjs/jQuery-tagEditor/demo.html b/static/libjs/jQuery-tagEditor/demo.html
new file mode 100644
index 0000000000000000000000000000000000000000..73f1f259f9ce1e5ee57344750a6044c5fa233ca8
--- /dev/null
+++ b/static/libjs/jQuery-tagEditor/demo.html
@@ -0,0 +1,461 @@
+<!DOCTYPE html><html lang="en">
+<head>
+    <meta charset="utf-8">
+    <title>jQuery tagEditor Plugin</title>
+    <meta name="description" content="A lightweight and sophisticated tag editor for jQuery. Sortable, editable tags with cursor navigation, autocomplete, and callbacks.">
+    <link rel="shortcut icon" href="https://pixabay.com/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300">
+    <link rel="stylesheet" href="https://cdn.rawgit.com/yahoo/pure-release/v0.6.0/pure-min.css">
+    <style>
+        body { margin: 0; padding: 0; border: 0; min-width: 320px; color: #777; }
+        html, button, input, select, textarea, .pure-g [class *= "pure-u"] { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1.02em; }
+        p, td { line-height: 1.5; }
+        ul { padding: 0 0 0 20px; }
+
+        th { background: #eee; white-space: nowrap; }
+        th, td { padding: 10px; text-align: left; vertical-align: top; font-size: .9em; font-weight: normal; border-right: 1px solid #fff; }
+        td:first-child { white-space: nowrap; color: #008000; width: 1%; font-style: italic; }
+
+        h1, h2, h3 { color: #4b4b4b; font-family: "Source Sans Pro", sans-serif; font-weight: 300; margin: 0 0 1.2em; }
+        h1 { font-size: 4.5em; color: #1f8dd6; margin: 0 0 .4em; }
+        h2 { font-size: 2em; color: #636363; }
+        h3 { font-size: 1.8em; color: #4b4b4b; margin: 1.8em 0 .8em }
+        h4 { font: bold 1em sans-serif; color: #636363; margin: 4em 0 1em; }
+        a { color: #4e99c7; text-decoration: none; }
+        a:hover { text-decoration: underline; }
+        p, pre { margin: 0 0 1.2em; }
+        ::selection { color: #fff; background: #328efd; }
+        ::-moz-selection { color: #fff; background: #328efd; }
+
+        @media (max-width:480px) {
+            h1 { font-size: 3em; }
+            h2 { font-size: 1.8em; }
+            h3 { font-size: 1.5em; }
+            td:first-child { white-space: normal; }
+        }
+
+        .inline-code { padding: 1px 5px; background: #eee; border-radius: 2px; }
+        pre { padding: 15px 10px; font-size: .9em; color: #555; background: #edf3f8; }
+        pre i { color: #aaa; } /* comments */
+        pre b { font-weight: normal; color: #cf4b25; } /* strings */
+        pre em { color: #0c59e9; } /* numeric */
+
+        /* Pure CSS */
+        .pure-button { margin: 5px 0; text-decoration: none !important; }
+        .button-lg { margin: 5px 0; padding: .65em 1.6em; font-size: 105%; }
+        .button-sm { font-size: 85%; }
+
+        textarea {
+            width: 100%; height: 29px; padding: .3em .5em; border: 1px solid #ddd; font-size: .9em;
+            box-sizing: border-box; margin: 0 0 20px;
+        }
+        textarea[readonly] { color: #aaa; background: #f7f7f7; }
+
+        #response {
+            margin: 0 0 1.2em; padding: 10px; background: #f3f3f3; color: #777;
+            font-size: .9em; max-height: 150px; overflow: hidden; overflow-y: auto;
+        }
+        #response i { font-style: normal; color: #cf4b25; }
+        #response hr { margin: 2px 0; border: 0; border-top: 1px solid #eee; border-bottom: 1px solid #fdfdfd; }
+
+        /* overwrite default CSS for tiny, dark tags in demo5 */
+        #demo5+.tag-editor { background: #fafafa; font-size: 12px; }
+        #demo5+.tag-editor .tag-editor-tag { color: #fff; background: #555; border-radius: 2px; }
+        #demo5+.tag-editor .tag-editor-spacer { width: 7px; }
+        #demo5+.tag-editor .tag-editor-delete { display: none; }
+
+        /* color tags */
+        .tag-editor .red-tag .tag-editor-tag { color: #c65353; background: #ffd7d7; }
+        .tag-editor .red-tag .tag-editor-delete { background-color: #ffd7d7; }
+        .tag-editor .green-tag .tag-editor-tag { color: #45872c; background: #e1f3da; }
+        .tag-editor .green-tag .tag-editor-delete { background-color: #e1f3da; }
+    </style>
+    <link rel="stylesheet" href="jquery.tag-editor.css">
+</head>
+<body>
+    <div style="max-width:900px;padding:0 10px;margin:40px auto;text-align:center">
+        <h1>tagEditor</h1>
+        <h2>A powerful and lightweight tag editor plugin for jQuery.</h2>
+        <a href="https://github.com/Pixabay/jQuery-tagEditor/archive/master.zip" class="pure-button pure-button-primary button-lg">Download</a>
+        &nbsp;
+        <a href="https://github.com/Pixabay/jQuery-tagEditor" class="pure-button button-lg">View on GitHub</a>
+    </div>
+    <div style="border-top: 1px solid #eee;border-bottom:1px solid #eee;background:#fafafa;margin:30px 0;padding:20px 5px">
+        <div style="padding :0 7px 0 5px;max-width:900px;margin:auto">
+            <textarea id="hero-demo">example tags, sortable, autocomplete, edit in place, tab/cursor navigation, duplicate check, callbacks, copy-paste, placeholder, public methods, custom delimiter, graceful degradation</textarea>
+        </div>
+    </div>
+    <div style="max-width:900px;margin:auto;padding:0 10px 50px">
+        <h3>Overview and Features</h3>
+        <p>
+            Released under the <a href="http://www.opensource.org/licenses/mit-license.php">MIT License</a>.
+            Source on <a href="https://github.com/Pixabay/jQuery-tagEditor">Github</a> (<a href="https://github.com/Pixabay/jQuery-tagEditor#changelog">changelog</a>).
+            Compatible with jQuery 1.7.0+ in Firefox, Safari, Chrome, Opera, Internet Explorer 8+. IE7 technically works, but no care has gone into CSS/layout bugs.
+            tagEditor depends on accursoft's <a href="https://github.com/accursoft/caret">caret plugin</a> (1.1 kB minified).
+        </p>
+        <ul>
+            <li>Lightweight: 8.5 kB of JavaScript - less than 3.2 kB gzipped</li>
+            <li>Edit in place tags</li>
+            <li>Intuitive navigation between tags with cursor keys, Tab, Shift+Tab, Enter, Pos1, End, Backspace, Del, and ESC</li>
+            <li>Optional jQuery UI sortable</li>
+            <li>Optional jQuery UI autocomplete</li>
+            <li>Copy-paste or delete multiple selected tags</li>
+            <li>Duplicate tags check</li>
+            <li>Custom delimiter/s</li>
+            <li>Placeholder</li>
+            <li>Custom style for faulty tags</li>
+            <li>Public methods for reading, adding and removing tags + destroy function</li>
+            <li>Callbacks</li>
+            <li>Allows tabindex for form navigation</li>
+            <li>Graceful degradation if JavaScript is disabled</li>
+        </ul>
+        <p>
+            This plugin was developed by and for <a href="https://pixabay.com/">Pixabay.com</a> - an international repository for free Public Domain images.
+            We have implemented this piece of software in production and we share it - in the spirit of Pixabay - freely with others.
+        </p>
+
+        <h3>Usage</h3>
+        <p>
+            Include the stylesheet <span class="inline-code">jquery.tag-editor.css</span> in the <span class="inline-code">&lt;head&gt;</span> section of your HTML document - and the JavaScript file <span class="inline-code">jquery.tag-editor.min.js</span> after loading jQuery and optional jQuery UI sortable/autocomplete.
+            Make sure to also load accursoft's <a href="http://code.accursoft.com/caret">caret plugin</a> (1.1 kB minified).
+            tagEditor accepts settings from an object of key/value pairs, and can be assigned to any text input field or textarea.
+        </p>
+        <pre>
+$(selector).tagEditor({key1: value1, key2: value2});
+
+<i>// examples</i>
+
+<i>// assign tag editor to textarea - existing text will be used as initial tags</i>
+$(<b>'textarea'</b>).tagEditor();
+
+<i>// assign tag editor to text input with initial tags</i>
+$(<b>'input[type="text"]'</b>).tagEditor({ initialTags: [<b>'tag1'</b>, <b>'tag2'</b>, <b>'tag3'</b>] });
+
+<i>// use jQuery UI autocomplete</i>
+$(<b>'#my_textarea'</b>).tagEditor({ autocomplete: { <b>'source'</b>: <b>'/url/'</b>, minLength: <em>3</em> } });</pre>
+
+        <h3>Settings</h3>
+        <table>
+            <tr><th>Property</th><th>Default</th><th>Description</th></tr>
+            <tr><td>initialTags</td><td>[]</td><td>Initial tags as an array of strings.</td></tr>
+            <tr><td>maxTags</td><td><i>null</i></td><td>Maximum number of allowed tags.</td></tr>
+            <tr><td>maxLength</td><td>50</td><td><span class="inline-code">maxlength</span> attribute of the tag input field.</td></tr>
+            <tr>
+                <td>delimiter</td><td style="white-space:nowrap">',;'</td>
+                <td>
+                    <p>
+                        Required string of delimiters - characters for separating tags.
+                        The first character is used as default delimiter in the (hidden) original field.
+                    </p>
+                </td>
+            </tr>
+            <tr><td>placeholder</td><td>''</td><td>Placeholder text for empty tag editor.</td></tr>
+            <tr><td>forceLowercase</td><td><i>true</i></td><td>Lowercase all tags.</td></tr>
+            <tr><td>removeDuplicates</td><td><i>true</i></td><td>Automatically remove duplicate tags.</td></tr>
+            <tr><td>clickDelete</td><td><i>false</i></td><td>Delete tags on right click and on Ctrl+click.</td></tr>
+            <tr><td>animateDelete</td><td><i>175</i></td><td>Animate duration for deletion of tags in milliseconds. Set to 0 for non-animated removal.</td></tr>
+            <tr><td>sortable</td><td><i>true</i></td><td>If <a href="https://jqueryui.com/sortable/">jQuery UI sortable</a> is available and this option is set to <span class="inline-code">true</span>, tags are sortable by drag and drop.</td></tr>
+            <tr><td>autocomplete</td><td><i>null</i></td><td><a href="https://jqueryui.com/autocomplete/">jQuery UI autocomplete</a> options as key/value pairs object. If provided, jQuery UI autocomplete must be loaded additionally.</td></tr>
+
+            <tr><td colspan="3">&nbsp;</td></tr>
+            <tr><th>Callbacks</th><th colspan="2"></th></tr>
+            <tr><td>onChange(field, editor, tags)</td><td colspan="2">Callback that fires after tags are changed. <span class="inline-code">field</span> is the (hidden) original field, <span class="inline-code">editor</span> is the editor's DOM element (an &lt;ul&gt; list of tag elements), and <span class="inline-code">tags</span> contains the list of current tags.</td></tr>
+            <tr><td>beforeTagSave(field, editor, tags, tag, val)</td><td colspan="2">Callback that fires before a tag is saved. <span class="inline-code">field</span> is the (hidden) original field, <span class="inline-code">editor</span> is the editor's DOM element. <span class="inline-code">tags</span> contains the list of current tags, <span class="inline-code">tag</span> is the value that is about to get overwritten (empty string, unless an existing tag gets changed), and <span class="inline-code">val</span> is the new value to be saved. <span class="inline-code">beforeTagSave()</span> may return a string for overwriting the saved tag. Return <span class="inline-code">false</span> for reverting to the tag's previous value (or to skip this tag value in the case of copy-paste insertion).</td></tr>
+            <tr><td>beforeTagDelete(field, editor, tags, val)</td><td colspan="2">Callback that fires before a tag is deleted. <span class="inline-code">field</span> is the (hidden) original field, <span class="inline-code">editor</span> is the editor's DOM element. <span class="inline-code">tags</span> contains the list of current tags, <span class="inline-code">val</span> is the tag that is about to get deleted. Return <span class="inline-code">false</span> to prevent this action.</td></tr>
+
+            <tr><td colspan="3">&nbsp;</td></tr>
+            <tr><th>Public Methods</th><th colspan="2"></th></tr>
+            <tr><td>getTags</td><td colspan="2">
+                Returns a list of objects in the following format:
+                <br>[{ field: <i>selected input/textarea</i>, editor: <i>editor instance for field</i>, tags: <i>current tags</i> }]
+            </td></tr>
+            <tr><td>addTag(val, blur)</td><td colspan="2">Adds <span class="inline-code">val</span> as a new tag. Set <span class="inline-code">blur</span> to <span class="inline-code">true</span> if focus should not be set automatically into an empty, new tag after this action.</td></tr>
+            <tr><td>removeTag(val, blur)</td><td colspan="2">Removes <span class="inline-code">val</span> as tag. Set <span class="inline-code">blur</span> to <span class="inline-code">true</span> if focus should not be set automatically into an empty, new tag after this action.</td></tr>
+            <tr><td>destroy</td><td colspan="2">Removes the tag editor instance an restores visibility of the original text field or textarea.</td></tr>
+        </table>
+
+        <h3 style="margin-top:.8em;border-top:1px solid #eee;padding-top:1.8em">Demos</h3>
+
+        <h4 style="margin-top:.5em">Basic settings</h4>
+        <pre>
+$(<b>'#demo1'</b>).tagEditor({
+    initialTags: [<b>'Hello'</b>, <b>'World'</b>, <b>'Example'</b>, <b>'Tags'</b>],
+    delimiter: <b>', '</b>, <i>/* space and comma */</i>
+    placeholder: <b>'Enter tags ...'</b>
+});</pre>
+        <div style="margin:0 0 1.2em">
+            <p>The original field - textarea or text input - is normally hidden automatically. We show it here to make value changes visible:</p>
+            <textarea id="demo1"></textarea>
+        </div>
+        <p>
+            The placeholder is visible when all tags are deleted and the editor looses focus.
+            jQuery UI is already loaded on this page - and by default, tags are then sortable via drag and drop.
+        </p>
+
+        <h4>Autocomplete</h4>
+        <p>
+            For enabling tag autocompletion, make sure to have <a href="https://jqueryui.com/autocomplete/">jQuery UI autocomplete</a> readily loaded.
+            You can then pass <i>any</i> options that work with UI autocomplete to your tagEditor settings.
+        </p>
+        <pre>
+$(<b>'#demo2'</b>).tagEditor({
+    autocomplete: {
+        delay: 0, <i>// show suggestions immediately</i>
+        position: { collision: 'flip' }, <i>// automatic menu position up/down</i>
+        source: [<b>'ActionScript'</b>, <b>'AppleScript'</b>, <b>'Asp'</b>, ... <b>'Python'</b>, <b>'Ruby'</b>]
+    },
+    forceLowercase: <em>false</em>,
+    placeholder: <b>'Programming languages ...'</b>
+});</pre>
+        <div style="margin:0 0 1.2em"><textarea id="demo2"></textarea></div>
+
+        <h4>Public methods</h4>
+        <pre>
+$(<b>'#demo3'</b>).tagEditor({
+    initialTags: [<b>'Hello'</b>, <b>'World'</b>],
+    placeholder: <b>'Enter tags ...'</b>
+});</pre>
+        <div style="margin:0 0 1.2em"><textarea id="demo3"></textarea></div>
+        <p>
+            <span onclick="alert($('#demo3').tagEditor('getTags')[0].tags);" class="ed_on pure-button button-sm">getTags</span>
+            <span onclick="$('#demo3').tagEditor('addTag', 'example');" class="ed_on pure-button button-sm">addTag 'example'</span>
+            <span onclick="$('#demo3').tagEditor('removeTag', 'example', true);" class="ed_on pure-button button-sm">removeTag 'example'</span>
+            <span id="remove_all_tags" class="ed_on pure-button button-sm">Remove all tags</span>
+            <span onclick="$('#demo3').tagEditor('destroy');$('.ed_on').hide();$('.ed_off').show();" class="ed_on pure-button button-sm">destroy</span>
+            <span onclick="$('#demo3').tagEditor({ placeholder: 'Enter tags ...' });$('.ed_off').hide();$('.ed_on').show();" class="ed_off pure-button button-sm" style="display:none">Init editor</span>
+        </p>
+        <pre>
+<i>// actions on button clicks</i>
+
+<i>// getTags</i>
+alert( $(<b>'#demo3'</b>).tagEditor(<b>'getTags'</b>)[0].tags );
+
+<i>// addTag</i>
+$(<b>'#demo3'</b>).tagEditor(<b>'addTag'</b>, <b>'example'</b>);
+
+<i>// removeTag</i>
+$(<b>'#demo3'</b>).tagEditor(<b>'removeTag'</b>, <b>'example'</b>);
+
+<i>// Remove all tags</i>
+function() {
+    var tags = $(<b>'#demo3'</b>).tagEditor(<b>'getTags'</b>)[0].tags;
+    for (i = 0; i &lt; tags.length; i++) { $(<b>'#demo3'</b>).tagEditor(<b>'removeTag'</b>, tags[i]); }
+}
+<i>// working shortcut for removing all tags
+// $('#demo3').next('.tag-editor').find('.tag-editor-delete').click();</i>
+
+<i>// destroy</i>
+$(<b>'#demo3'</b>).tagEditor(<b>'destroy'</b>);
+
+<i>// re-init editor</i>
+$(<b>'#demo3'</b>).tagEditor({ placeholder: <b>'Enter tags ...'</b> });</pre>
+
+        <h4>Callbacks</h4>
+        <pre>
+$(<b>'#demo4'</b>).tagEditor({
+    initialTags: [<b>'Hello'</b>, <b>'World'</b>],
+    placeholder: <b>'Enter tags ...'</b>,
+    onChange: function(field, editor, tags) {
+        $(<b>'#response'</b>).prepend(
+            <b>'Tags changed to: '</b> + (tags.length ? tags.join(<b>', '</b>) : <b>'----'</b>) + <b>'&lt;hr&gt;'</b>
+        );
+    },
+    beforeTagSave: function(field, editor, tags, tag, val) {
+        $(<b>'#response'</b>).prepend(<b>'Tag '</b> + val + <b>' saved'</b> + (tag ? <b>' over '</b> + tag : <b>''</b>) + <b>'.'</b>);
+    },
+    beforeTagDelete: function(field, editor, tags, val) {
+        var q = confirm(<b>'Remove tag "'</b> + val + <b>'"?'</b>);
+        if (q) $(<b>'#response'</b>).prepend(<b>'Tag '</b> + val + <b>' deleted.'</b>);
+        else $(<b>'#response'</b>).prepend(<b>'Removal of '</b> + val + <b>' discarded.'</b>);
+        return q;
+    }
+});</pre>
+        <div style="margin:0 0 1.2em"><textarea id="demo4"></textarea></div>
+        <p style="font-size:.9em;margin:0 0 .2em">Callback response:</p>
+        <div id="response">Starting tags: <i>hello, world</i></div>
+
+        <h4>Custom style and clickDelete</h4>
+        <p>
+            Use right mouse click or Ctrl+left click to delete tags.
+        </p>
+        <pre>
+$(<b>'#demo5'</b>).tagEditor({
+    clickDelete: true,
+    initialTags: [ ... ],
+    placeholder: <b>'Enter tags ...'</b>
+});</pre>
+        <div style="margin:0 0 1.2em"><textarea id="demo5"></textarea></div>
+        <pre>
+<i>/* overwrite default CSS for tiny, dark tags */</i>
+
+<b>#demo5+.tag-editor</b> { background: <em>#fafafa</em>; font-size: <em>12px</em>; }
+<b>#demo5+.tag-editor .tag-editor-tag</b> {
+    color: <em>#fff</em>; background: <em>#555</em>;
+    border-radius: <em>2px</em>;
+}
+<b>#demo5+.tag-editor .tag-editor-spacer</b> { width: <em>7px</em>; }
+<b>#demo5+.tag-editor .tag-editor-delete</b> { display: <em>none</em>; }</pre>
+        <p>
+            This jQuery plugin was designed with custom styling in mind. In this example we've enabled the <span class="inline-code">clickDelete</span> feature while hiding all delete icons. Both options may be used at the same time, as well.
+            By fiddling around with the default stylesheet, you can achieve almost any desired look for your tag Editor.
+            Comments inside the CSS file will help you understand what rule controls which object inside the editor.
+        </p>
+
+        <h4>Custom CSS classes for tags</h4>
+        <p>
+            Using the onChange callback for adding custom CSS classes to specific tags.
+        </p>
+        <pre>
+$(<b>'#demo6'</b>).tagEditor({
+    initialTags: [<b>'custom'</b>, <b>'class'</b>, <b>'red'</b>, <b>'green'</b>, <b>'demo'</b>],
+    onChange: tag_classes
+});
+
+function tag_classes(field, editor, tags) {
+    $(<b>'li'</b>, editor).each(function(){
+        var li = $(this);
+        if (li.find(<b>'.tag-editor-tag'</b>).html() == <b>'red'</b>) li.addClass(<b>'red-tag'</b>);
+        else if (li.find(<b>'.tag-editor-tag'</b>).html() == <b>'green'</b>) li.addClass(<b>'green-tag'</b>)
+        else li.removeClass(<b>'red-tag green-tag'</b>);
+    });
+}
+
+<i>// first assign tag classes after initializing tagEditor; onChange is not called on init</i>
+tag_classes(null, $('#demo6').tagEditor('getTags')[0].editor);</pre>
+        <div style="margin:0 0 1.2em"><textarea id="demo6"></textarea></div>
+        <p>
+            In the onChange callback we iterate over all tags and assign custom CSS classes where appropriate.
+            The DOM structure of the editor looks like this:
+        </p>
+        <pre>&lt;ul&gt;
+    &lt;li&gt;
+        &lt;div class=<b>&quot;tag-editor-spacer&quot;</b>&gt;&lt;/div&gt;
+        &lt;div class=<b>&quot;tag-editor-tag&quot;</b>&gt;Tag content&lt;/div&gt;
+        &lt;div class=<b>&quot;tag-editor-delete&quot;</b>&gt;&lt;i&gt;&lt;/i&gt;&lt;/div&gt;
+    &lt;/li&gt;
+    [...]
+&lt;/ul&gt;</pre>
+
+        <p>
+            In the example, we simply add CSS classes to the <span class="inline-code">&lt;li&gt;</span> elements.
+            This is just an exampe of what the onChange callback may be used for. Inside of it, <span class="inline-code">addTag</span> and <span class="inline-code">removeTag</span> may be called to dynamically change the current list of tags.
+        </p>
+
+        <div style="margin:40px 0;overflow:hidden">
+            <span id="github_social"></span>
+            <div style="float:left;margin-right:35px">
+                <a href="#" data-width="70" class="twitter-share-button" data-text="jQuery tagEditor Plugin"></a>
+            </div>
+            <div style="float:left">
+                <div class="g-plusone" data-size="medium"></div>
+            </div>
+            <div style="float:left;width:140px" class="fb-like" data-send="false" data-layout="button_count" data-width="140" data-show-faces="false"></div>
+        </div>
+
+        <p style="border-top:1px solid #eee;padding-top:30px">Please report any bugs and issues at the <a href="https://github.com/Pixabay/jQuery-tagEditor">GitHub repositiory</a>.</p>
+        <p>This software is released as Open Source under the <a href="http://www.opensource.org/licenses/mit-license.php">MIT License</a> by <a href="https://pixabay.com/users/Simon/">Simon Steinberger / Pixabay.com</a>.</p>
+
+    </div>
+
+    <div style="background:#fafafa;border-top:1px solid #eee;padding:15px;font-size:.9em">
+        <div style="max-width:900px;margin:auto;padding:0 10px">
+            <a style="float:right;margin-left:20px" href="https://pixabay.com/en/service/about/">About Us</a>
+            <a style="float:right;margin-left:20px" href="https://pixabay.com/en/blog/">Blog</a>
+            <a style="float:right;margin-left:20px" href="https://goodies.pixabay.com/">More Goodies</a>
+            © <a href="https://pixabay.com/">Pixabay.com</a> / Simon Steinberger / Hans Braxmeier
+        </div>
+    </div>
+
+    <div id="fb-root"></div>
+    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
+    <script src="https://code.jquery.com/ui/1.10.2/jquery-ui.min.js"></script>
+    <script src="jquery.caret.min.js"></script>
+    <script src="jquery.tag-editor.js"></script>
+    <script>
+        // jQuery UI autocomplete extension - suggest labels may contain HTML tags
+        // github.com/scottgonzalez/jquery-ui-extensions/blob/master/src/autocomplete/jquery.ui.autocomplete.html.js
+        (function($){var proto=$.ui.autocomplete.prototype,initSource=proto._initSource;function filter(array,term){var matcher=new RegExp($.ui.autocomplete.escapeRegex(term),"i");return $.grep(array,function(value){return matcher.test($("<div>").html(value.label||value.value||value).text());});}$.extend(proto,{_initSource:function(){if(this.options.html&&$.isArray(this.options.source)){this.source=function(request,response){response(filter(this.options.source,request.term));};}else{initSource.call(this);}},_renderItem:function(ul,item){return $("<li></li>").data("item.autocomplete",item).append($("<a></a>")[this.options.html?"html":"text"](item.label)).appendTo(ul);}});})(jQuery);
+
+        var cache = {};
+        function googleSuggest(request, response) {
+            var term = request.term;
+            if (term in cache) { response(cache[term]); return; }
+            $.ajax({
+                url: 'https://query.yahooapis.com/v1/public/yql',
+                dataType: 'JSONP',
+                data: { format: 'json', q: 'select * from xml where url="http://google.com/complete/search?output=toolbar&q='+term+'"' },
+                success: function(data) {
+                    var suggestions = [];
+                    try { var results = data.query.results.toplevel.CompleteSuggestion; } catch(e) { var results = []; }
+                    $.each(results, function() {
+                        try {
+                            var s = this.suggestion.data.toLowerCase();
+                            suggestions.push({label: s.replace(term, '<b>'+term+'</b>'), value: s});
+                        } catch(e){}
+                    });
+                    cache[term] = suggestions;
+                    response(suggestions);
+                }
+            });
+        }
+
+        $(function() {
+            $('#hero-demo').tagEditor({
+                placeholder: 'Enter tags ...',
+                autocomplete: { source: googleSuggest, minLength: 3, delay: 250, html: true, position: { collision: 'flip' } }
+            });
+
+            $('#demo1').tagEditor({ initialTags: ['Hello', 'World', 'Example', 'Tags'], delimiter: ', ', placeholder: 'Enter tags ...' }).css('display', 'block').attr('readonly', true);
+
+            $('#demo2').tagEditor({
+                autocomplete: { delay: 0, position: { collision: 'flip' }, source: ['ActionScript', 'AppleScript', 'Asp', 'BASIC', 'C', 'C++', 'CSS', 'Clojure', 'COBOL', 'ColdFusion', 'Erlang', 'Fortran', 'Groovy', 'Haskell', 'HTML', 'Java', 'JavaScript', 'Lisp', 'Perl', 'PHP', 'Python', 'Ruby', 'Scala', 'Scheme'] },
+                forceLowercase: false,
+                placeholder: 'Programming languages ...'
+            });
+
+            $('#demo3').tagEditor({ initialTags: ['Hello', 'World'], placeholder: 'Enter tags ...' });
+            $('#remove_all_tags').click(function() {
+                var tags = $('#demo3').tagEditor('getTags')[0].tags;
+                for (i=0;i<tags.length;i++){ $('#demo3').tagEditor('removeTag', tags[i]); }
+            });
+
+            $('#demo4').tagEditor({
+                initialTags: ['Hello', 'World'],
+                placeholder: 'Enter tags ...',
+                onChange: function(field, editor, tags) { $('#response').prepend('Tags changed to: <i>'+(tags.length ? tags.join(', ') : '----')+'</i><hr>'); },
+                beforeTagSave: function(field, editor, tags, tag, val) { $('#response').prepend('Tag <i>'+val+'</i> saved'+(tag ? ' over <i>'+tag+'</i>' : '')+'.<hr>'); },
+                beforeTagDelete: function(field, editor, tags, val) {
+                    var q = confirm('Remove tag "'+val+'"?');
+                    if (q) $('#response').prepend('Tag <i>'+val+'</i> deleted.<hr>');
+                    else $('#response').prepend('Removal of <i>'+val+'</i> discarded.<hr>');
+                    return q;
+                }
+            });
+
+            $('#demo5').tagEditor({ clickDelete: true, initialTags: ['custom style', 'dark tags', 'delete on click', 'no delete icon', 'hello', 'world'], placeholder: 'Enter tags ...' });
+
+            function tag_classes(field, editor, tags) {
+                $('li', editor).each(function(){
+                    var li = $(this);
+                    if (li.find('.tag-editor-tag').html() == 'red') li.addClass('red-tag');
+                    else if (li.find('.tag-editor-tag').html() == 'green') li.addClass('green-tag')
+                    else li.removeClass('red-tag green-tag');
+                });
+            }
+            $('#demo6').tagEditor({ initialTags: ['custom', 'class', 'red', 'green', 'demo'], onChange: tag_classes });
+            tag_classes(null, $('#demo6').tagEditor('getTags')[0].editor); // or editor == $('#demo6').next()
+        });
+
+        if (~window.location.href.indexOf('http')) {
+            (function() {var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;po.src = 'https://apis.google.com/js/plusone.js';var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);})();
+            (function(d, s, id) {var js, fjs = d.getElementsByTagName(s)[0];if (d.getElementById(id)) return;js = d.createElement(s); js.id = id;js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.4&appId=114593902037957";fjs.parentNode.insertBefore(js, fjs);}(document, 'script', 'facebook-jssdk'));
+            !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
+            $('#github_social').html('\
+                <iframe style="float:left;margin-right:15px" src="//ghbtns.com/github-btn.html?user=Pixabay&repo=jQuery-tagEditor&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20"></iframe>\
+                <iframe style="float:left;margin-right:15px" src="//ghbtns.com/github-btn.html?user=Pixabay&repo=jQuery-tagEditor&type=fork&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20"></iframe>\
+            ');
+        }
+    </script>
+</body>
+</html>
diff --git a/static/libjs/jQuery-tagEditor/jquery.caret.min.js b/static/libjs/jQuery-tagEditor/jquery.caret.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..0f9ef48d6e8008076a01323f837636e4d31008ee
--- /dev/null
+++ b/static/libjs/jQuery-tagEditor/jquery.caret.min.js
@@ -0,0 +1,2 @@
+// http://code.accursoft.com/caret - 1.3.3
+!function(e){e.fn.caret=function(e){var t=this[0],n="true"===t.contentEditable;if(0==arguments.length){if(window.getSelection){if(n){t.focus();var o=window.getSelection().getRangeAt(0),r=o.cloneRange();return r.selectNodeContents(t),r.setEnd(o.endContainer,o.endOffset),r.toString().length}return t.selectionStart}if(document.selection){if(t.focus(),n){var o=document.selection.createRange(),r=document.body.createTextRange();return r.moveToElementText(t),r.setEndPoint("EndToEnd",o),r.text.length}var e=0,c=t.createTextRange(),r=document.selection.createRange().duplicate(),a=r.getBookmark();for(c.moveToBookmark(a);0!==c.moveStart("character",-1);)e++;return e}return t.selectionStart?t.selectionStart:0}if(-1==e&&(e=this[n?"text":"val"]().length),window.getSelection)n?(t.focus(),window.getSelection().collapse(t.firstChild,e)):t.setSelectionRange(e,e);else if(document.body.createTextRange)if(n){var c=document.body.createTextRange();c.moveToElementText(t),c.moveStart("character",e),c.collapse(!0),c.select()}else{var c=t.createTextRange();c.move("character",e),c.select()}return n||t.focus(),e}}(jQuery);
diff --git a/static/libjs/jQuery-tagEditor/jquery.tag-editor.css b/static/libjs/jQuery-tagEditor/jquery.tag-editor.css
new file mode 100644
index 0000000000000000000000000000000000000000..949995d259bb970481662c15426e9730ea049648
--- /dev/null
+++ b/static/libjs/jQuery-tagEditor/jquery.tag-editor.css
@@ -0,0 +1,45 @@
+/* surrounding tag container */
+.tag-editor {
+    list-style-type: none; padding: 0 5px 0 0; margin: 0; overflow: hidden; border: 1px solid #eee; cursor: text;
+    font: normal 14px sans-serif; color: #555; background: #fff; line-height: 20px;
+}
+
+/* core styles usually need no change */
+.tag-editor li { display: block; float: left; overflow: hidden; margin: 3px 0; }
+.tag-editor div { float: left; padding: 0 4px; }
+.tag-editor .placeholder { padding: 0 8px; color: #bbb; }
+.tag-editor .tag-editor-spacer { padding: 0; width: 8px; overflow: hidden; color: transparent; background: none; }
+.tag-editor input {
+    vertical-align: inherit; border: 0; outline: none; padding: 0; margin: 0; cursor: text;
+    font-family: inherit; font-weight: inherit; font-size: inherit; font-style: inherit;
+    box-shadow: none; background: none; color: #444;
+}
+/* hide original input field or textarea visually to allow tab navigation */
+.tag-editor-hidden-src { position: absolute !important; left: -99999px; }
+/* hide IE10 "clear field" X */
+.tag-editor ::-ms-clear { display: none; }
+
+/* tag style */
+.tag-editor .tag-editor-tag {
+    padding-left: 5px; color: #46799b; background: #e0eaf1; white-space: nowrap;
+    overflow: hidden; cursor: pointer; border-radius: 2px 0 0 2px;
+}
+
+/* delete icon */
+.tag-editor .tag-editor-delete { background: #e0eaf1; cursor: pointer; border-radius: 0 2px 2px 0; padding-left: 3px; padding-right: 4px; }
+.tag-editor .tag-editor-delete i { line-height: 18px; display: inline-block; }
+.tag-editor .tag-editor-delete i:before { font-size: 16px; color: #8ba7ba; content: "×"; font-style: normal; }
+.tag-editor .tag-editor-delete:hover i:before { color: #d65454; }
+.tag-editor .tag-editor-tag.active+.tag-editor-delete, .tag-editor .tag-editor-tag.active+.tag-editor-delete i { visibility: hidden; cursor: text; }
+
+.tag-editor .tag-editor-tag.active { background: none !important; }
+
+/* jQuery UI autocomplete - code.jquery.com/ui/1.10.2/themes/smoothness/jquery-ui.css */
+.ui-autocomplete { position: absolute; top: 0; left: 0; cursor: default; font-size: 14px; }
+.ui-front { z-index: 9999; }
+.ui-menu { list-style: none; padding: 1px; margin: 0; display: block; outline: none; }
+.ui-menu .ui-menu-item a { text-decoration: none; display: block; padding: 2px .4em; line-height: 1.4; min-height: 0; /* support: IE7 */ }
+.ui-widget-content { border: 1px solid #bbb; background: #fff; color: #555; }
+.ui-widget-content a { color: #46799b; }
+.ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { background: #e0eaf1; }
+.ui-helper-hidden-accessible { display: none; }
diff --git a/static/libjs/jQuery-tagEditor/jquery.tag-editor.js b/static/libjs/jQuery-tagEditor/jquery.tag-editor.js
new file mode 100644
index 0000000000000000000000000000000000000000..048f5384ac33fcf3f6c44cc9356c403567eb58d0
--- /dev/null
+++ b/static/libjs/jQuery-tagEditor/jquery.tag-editor.js
@@ -0,0 +1,370 @@
+/*
+	jQuery tagEditor v1.0.20
+    Copyright (c) 2014 Simon Steinberger / Pixabay
+    GitHub: https://github.com/Pixabay/jQuery-tagEditor
+	License: http://www.opensource.org/licenses/mit-license.php
+*/
+
+(function($){
+    // auto grow input (stackoverflow.com/questions/931207)
+    $.fn.tagEditorInput=function(){var t=" ",e=$(this),n=parseInt(e.css("fontSize")),i=$("<span/>").css({position:"absolute",top:-9999,left:-9999,width:"auto",fontSize:e.css("fontSize"),fontFamily:e.css("fontFamily"),fontWeight:e.css("fontWeight"),letterSpacing:e.css("letterSpacing"),whiteSpace:"nowrap"}),s=function(){if(t!==(t=e.val())){i.text(t);var s=i.width()+n;20>s&&(s=20),s!=e.width()&&e.width(s)}};return i.insertAfter(e),e.bind("keyup keydown focus",s)};
+
+    // plugin with val as parameter for public methods
+    $.fn.tagEditor = function(options, val, blur){
+
+        // helper
+        function escape(tag) {
+            return tag.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
+        }
+
+        // build options dictionary with default values
+        var blur_result, o = $.extend({}, $.fn.tagEditor.defaults, options), selector = this;
+
+        // store regex and default delimiter in options for later use
+        o.dregex = new RegExp('['+o.delimiter.replace('-', '\-')+']', 'g');
+
+        // public methods
+        if (typeof options == 'string') {
+            // depending on selector, response may contain tag lists of multiple editor instances
+            var response = [];
+            selector.each(function(){
+                // the editor is the next sibling to the hidden, original field
+                var el = $(this), o = el.data('options'), ed = el.next('.tag-editor');
+                if (options == 'getTags')
+                    response.push({field: el[0], editor: ed, tags: ed.data('tags')});
+                else if (options == 'addTag') {
+                    if (o.maxTags && ed.data('tags').length >= o.maxTags) return false;
+                    // insert new tag
+                    $('<li><div class="tag-editor-spacer">&nbsp;'+o.delimiter[0]+'</div><div class="tag-editor-tag"></div><div class="tag-editor-delete"><i></i></div></li>').appendTo(ed).find('.tag-editor-tag')
+                        .html('<input type="text" maxlength="'+o.maxLength+'">').addClass('active').find('input').val(val).blur();
+                    if (!blur) ed.click();
+                    else $('.placeholder', ed).remove();
+                } else if (options == 'removeTag') {
+                    // trigger delete on matching tag, then click editor to create a new tag
+                    $('.tag-editor-tag', ed).filter(function(){return $(this).text()==val;}).closest('li').find('.tag-editor-delete').click();
+                    if (!blur) ed.click();
+                } else if (options == 'destroy') {
+                    el.removeClass('tag-editor-hidden-src').removeData('options').off('focus.tag-editor').next('.tag-editor').remove();
+                }
+            });
+            return options == 'getTags' ? response : this;
+        }
+
+        // delete selected tags on backspace, delete, ctrl+x
+        if (window.getSelection) $(document).off('keydown.tag-editor').on('keydown.tag-editor', function(e){
+            if (e.which == 8 || e.which == 46 || e.ctrlKey && e.which == 88) {
+                try {
+                    var sel = getSelection(), el = document.activeElement.tagName == 'BODY' ? $(sel.getRangeAt(0).startContainer.parentNode).closest('.tag-editor') : 0;
+                } catch(e){ el = 0; }
+                if (sel.rangeCount > 0 && el && el.length) {
+                    var tags = [], splits = sel.toString().split(el.prev().data('options').dregex);
+                    for (i=0; i<splits.length; i++){ var tag = $.trim(splits[i]); if (tag) tags.push(tag); }
+                    $('.tag-editor-tag', el).each(function(){
+                        if (~$.inArray($(this).text(), tags)) $(this).closest('li').find('.tag-editor-delete').click();
+                    });
+                    return false;
+                }
+            }
+        });
+
+        return selector.each(function(){
+            var el = $(this), tag_list = []; // cache current tags
+
+            // create editor (ed) instance
+            var ed = $('<ul '+(o.clickDelete ? 'oncontextmenu="return false;" ' : '')+'class="tag-editor"></ul>').insertAfter(el);
+            el.addClass('tag-editor-hidden-src') // hide original field
+                .data('options', o) // set data on hidden field
+                .on('focus.tag-editor', function(){ ed.click(); }); // simulate tabindex
+
+            // add dummy item for min-height on empty editor
+            ed.append('<li style="width:1px">&nbsp;</li>');
+
+            // markup for new tag
+            var new_tag = '<li><div class="tag-editor-spacer">&nbsp;'+o.delimiter[0]+'</div><div class="tag-editor-tag"></div><div class="tag-editor-delete"><i></i></div></li>';
+
+            // helper: update global data
+            function set_placeholder(){
+                if (o.placeholder && !tag_list.length && !$('.deleted, .placeholder, input', ed).length)
+                    ed.append('<li class="placeholder"><div>'+o.placeholder+'</div></li>');
+            }
+
+            // helper: update global data
+            function update_globals(init){
+                var old_tags = tag_list.toString();
+                tag_list = $('.tag-editor-tag:not(.deleted)', ed).map(function(i, e) {
+                    var val = $.trim($(this).hasClass('active') ? $(this).find('input').val() : $(e).text());
+                    if (val) return val;
+                }).get();
+                ed.data('tags', tag_list);
+                el.val(tag_list.join(o.delimiter[0]));
+                // change callback except for plugin init
+                if (!init) if (old_tags != tag_list.toString()) o.onChange(el, ed, tag_list);
+                set_placeholder();
+            }
+
+            ed.click(function(e, closest_tag){
+                var d, dist = 99999, loc;
+
+                // do not create tag when user selects tags by text selection
+                if (window.getSelection && getSelection() != '') return;
+
+                if (o.maxTags && ed.data('tags').length >= o.maxTags) { ed.find('input').blur(); return false; }
+
+                blur_result = true
+                $('input:focus', ed).blur();
+                if (!blur_result) return false;
+                blur_result = true
+
+                // always remove placeholder on click
+                $('.placeholder', ed).remove();
+                if (closest_tag && closest_tag.length)
+                    loc = 'before';
+                else {
+                    // calculate tag closest to click position
+                    $('.tag-editor-tag', ed).each(function(){
+                        var tag = $(this), to = tag.offset(), tag_x = to.left, tag_y = to.top;
+                        if (e.pageY >= tag_y && e.pageY <= tag_y+tag.height()) {
+                            if (e.pageX < tag_x) loc = 'before', d = tag_x - e.pageX;
+                            else loc = 'after', d = e.pageX - tag_x - tag.width();
+                            if (d < dist) dist = d, closest_tag = tag;
+                        }
+                    });
+                }
+
+                if (loc == 'before') {
+                    $(new_tag).insertBefore(closest_tag.closest('li')).find('.tag-editor-tag').click();
+                } else if (loc == 'after')
+                    $(new_tag).insertAfter(closest_tag.closest('li')).find('.tag-editor-tag').click();
+                else // empty editor
+                    $(new_tag).appendTo(ed).find('.tag-editor-tag').click();
+                return false;
+            });
+
+            ed.on('click', '.tag-editor-delete', function(e){
+                // delete icon is hidden when input is visible; place cursor near invisible delete icon on click
+                if ($(this).prev().hasClass('active')) { $(this).closest('li').find('input').caret(-1); return false; }
+
+                var li = $(this).closest('li'), tag = li.find('.tag-editor-tag');
+                if (o.beforeTagDelete(el, ed, tag_list, tag.text()) === false) return false;
+                tag.addClass('deleted').animate({width: 0}, o.animateDelete, function(){ li.remove(); set_placeholder(); });
+                update_globals();
+                return false;
+            });
+
+            // delete on right mouse click or ctrl+click
+            if (o.clickDelete)
+                ed.on('mousedown', '.tag-editor-tag', function(e){
+                    if (e.ctrlKey || e.which > 1) {
+                        var li = $(this).closest('li'), tag = li.find('.tag-editor-tag');
+                        if (o.beforeTagDelete(el, ed, tag_list, tag.text()) === false) return false;
+                        tag.addClass('deleted').animate({width: 0}, o.animateDelete, function(){ li.remove(); set_placeholder(); });
+                        update_globals();
+                        return false;
+                    }
+                });
+
+            ed.on('click', '.tag-editor-tag', function(e){
+                // delete on right click or ctrl+click -> exit
+                if (o.clickDelete && (e.ctrlKey || e.which > 1)) return false;
+
+                if (!$(this).hasClass('active')) {
+                    var tag = $(this).text();
+                    // guess cursor position in text input
+                    var left_percent = Math.abs(($(this).offset().left - e.pageX)/$(this).width()), caret_pos = parseInt(tag.length*left_percent),
+                        input = $(this).html('<input type="text" maxlength="'+o.maxLength+'" value="'+escape(tag)+'">').addClass('active').find('input');
+                        input.data('old_tag', tag).tagEditorInput().focus().caret(caret_pos);
+                    if (o.autocomplete) {
+                        var aco = $.extend({}, o.autocomplete);
+                        // extend user provided autocomplete select method
+                        var ac_select = 'select'  in aco ? o.autocomplete.select : '';
+                        aco.select = function(e, ui){ if (ac_select) ac_select(e, ui); setTimeout(function(){
+                            ed.trigger('click', [$('.active', ed).find('input').closest('li').next('li').find('.tag-editor-tag')]);
+                        }, 20); };
+                        input.autocomplete(aco);
+                    }
+                }
+                return false;
+            });
+
+            // helper: split into multiple tags, e.g. after paste
+            function split_cleanup(input){
+                var li = input.closest('li'), sub_tags = input.val().replace(/ +/, ' ').split(o.dregex),
+                    old_tag = input.data('old_tag'), old_tags = tag_list.slice(0), exceeded = false, cb_val; // copy tag_list
+                for (var i=0; i<sub_tags.length; i++) {
+                    tag = $.trim(sub_tags[i]).slice(0, o.maxLength);
+                    if (o.forceLowercase) tag = tag.toLowerCase();
+                    cb_val = o.beforeTagSave(el, ed, old_tags, old_tag, tag);
+                    tag = cb_val || tag;
+                    if (cb_val === false || !tag) continue;
+                    // remove duplicates
+                    if (o.removeDuplicates && ~$.inArray(tag, old_tags))
+                        $('.tag-editor-tag', ed).each(function(){ if ($(this).text() == tag) $(this).closest('li').remove(); });
+                    old_tags.push(tag);
+                    li.before('<li><div class="tag-editor-spacer">&nbsp;'+o.delimiter[0]+'</div><div class="tag-editor-tag">'+escape(tag)+'</div><div class="tag-editor-delete"><i></i></div></li>');
+                    if (o.maxTags && old_tags.length >= o.maxTags) { exceeded = true; break; }
+                }
+                input.attr('maxlength', o.maxLength).removeData('old_tag').val('')
+                if (exceeded) input.blur(); else input.focus();
+                update_globals();
+            }
+
+            ed.on('blur', 'input', function(e){
+                e.stopPropagation();
+                var input = $(this), old_tag = input.data('old_tag'), tag = $.trim(input.val().replace(/ +/, ' ').replace(o.dregex, o.delimiter[0]));
+                if (!tag) {
+                    if (old_tag && o.beforeTagDelete(el, ed, tag_list, old_tag) === false) {
+                        input.val(old_tag).focus();
+                        blur_result = false;
+                        update_globals();
+                        return;
+                    }
+                    try { input.closest('li').remove(); } catch(e){}
+                    if (old_tag) update_globals();
+                }
+                else if (tag.indexOf(o.delimiter[0])>=0) { split_cleanup(input); return; }
+                else if (tag != old_tag) {
+                    if (o.forceLowercase) tag = tag.toLowerCase();
+                    cb_val = o.beforeTagSave(el, ed, tag_list, old_tag, tag);
+                    tag = cb_val || tag;
+                    if (cb_val === false) {
+                        if (old_tag) {
+                            input.val(old_tag).focus();
+                            blur_result = false;
+                            update_globals();
+                            return;
+                        }
+                        try { input.closest('li').remove(); } catch(e){}
+                        if (old_tag) update_globals();
+                    }
+                    // remove duplicates
+                    else if (o.removeDuplicates)
+                        $('.tag-editor-tag:not(.active)', ed).each(function(){ if ($(this).text() == tag) $(this).closest('li').remove(); });
+                }
+                input.parent().html(escape(tag)).removeClass('active');
+                if (tag != old_tag) update_globals();
+                set_placeholder();
+            });
+
+            var pasted_content;
+            ed.on('paste', 'input', function(e){
+                $(this).removeAttr('maxlength');
+                pasted_content = $(this);
+                setTimeout(function(){ split_cleanup(pasted_content); }, 30);
+            });
+
+            // keypress delimiter
+            var inp;
+            ed.on('keypress', 'input', function(e){
+                if (o.delimiter.indexOf(String.fromCharCode(e.which))>=0) {
+                    inp = $(this);
+                    setTimeout(function(){ split_cleanup(inp); }, 20);
+                }
+            });
+
+            ed.on('keydown', 'input', function(e){
+                var $t = $(this);
+
+                // left/up key + backspace key on empty field
+                if ((e.which == 37 || !o.autocomplete && e.which == 38) && !$t.caret() || e.which == 8 && !$t.val()) {
+                    var prev_tag = $t.closest('li').prev('li').find('.tag-editor-tag');
+                    if (prev_tag.length) prev_tag.click().find('input').caret(-1);
+                    else if ($t.val() && !(o.maxTags && ed.data('tags').length >= o.maxTags)) $(new_tag).insertBefore($t.closest('li')).find('.tag-editor-tag').click();
+                    return false;
+                }
+                // right/down key
+                else if ((e.which == 39 || !o.autocomplete && e.which == 40) && ($t.caret() == $t.val().length)) {
+                    var next_tag = $t.closest('li').next('li').find('.tag-editor-tag');
+                    if (next_tag.length) next_tag.click().find('input').caret(0);
+                    else if ($t.val()) ed.click();
+                    return false;
+                }
+                // tab key
+                else if (e.which == 9) {
+                    // shift+tab
+                    if (e.shiftKey) {
+                        var prev_tag = $t.closest('li').prev('li').find('.tag-editor-tag');
+                        if (prev_tag.length) prev_tag.click().find('input').caret(0);
+                        else if ($t.val() && !(o.maxTags && ed.data('tags').length >= o.maxTags)) $(new_tag).insertBefore($t.closest('li')).find('.tag-editor-tag').click();
+                        // allow tabbing to previous element
+                        else {
+                            el.attr('disabled', 'disabled');
+                            setTimeout(function(){ el.removeAttr('disabled'); }, 30);
+                            return;
+                        }
+                        return false;
+                    // tab
+                    } else {
+                        var next_tag = $t.closest('li').next('li').find('.tag-editor-tag');
+                        if (next_tag.length) next_tag.click().find('input').caret(0);
+                        else if ($t.val()) ed.click();
+                        else return; // allow tabbing to next element
+                        return false;
+                    }
+                }
+                // del key
+                else if (e.which == 46 && (!$.trim($t.val()) || ($t.caret() == $t.val().length))) {
+                    var next_tag = $t.closest('li').next('li').find('.tag-editor-tag');
+                    if (next_tag.length) next_tag.click().find('input').caret(0);
+                    else if ($t.val()) ed.click();
+                    return false;
+                }
+                // enter key
+                else if (e.which == 13) {
+                    ed.trigger('click', [$t.closest('li').next('li').find('.tag-editor-tag')]);
+
+                    // trigger blur if maxTags limit is reached
+                    if (o.maxTags && ed.data('tags').length >= o.maxTags) ed.find('input').blur();
+
+                    return false;
+                }
+                // pos1
+                else if (e.which == 36 && !$t.caret()) ed.find('.tag-editor-tag').first().click();
+                // end
+                else if (e.which == 35 && $t.caret() == $t.val().length) ed.find('.tag-editor-tag').last().click();
+                // esc
+                else if (e.which == 27) {
+                    $t.val($t.data('old_tag') ? $t.data('old_tag') : '').blur();
+                    return false;
+                }
+            });
+
+            // create initial tags
+            var tags = o.initialTags.length ? o.initialTags : el.val().split(o.dregex);
+            for (var i=0; i<tags.length; i++) {
+                if (o.maxTags && i >= o.maxTags) break;
+                var tag = $.trim(tags[i].replace(/ +/, ' '));
+                if (tag) {
+                    if (o.forceLowercase) tag = tag.toLowerCase();
+                    tag_list.push(tag);
+                    ed.append('<li><div class="tag-editor-spacer">&nbsp;'+o.delimiter[0]+'</div><div class="tag-editor-tag">'+escape(tag)+'</div><div class="tag-editor-delete"><i></i></div></li>');
+                }
+            }
+            update_globals(true); // true -> no onChange callback
+
+            // init sortable
+            if (o.sortable && $.fn.sortable) ed.sortable({
+                distance: 5, cancel: '.tag-editor-spacer, input', helper: 'clone',
+                update: function(){ update_globals(); }
+            });
+        });
+    };
+
+    $.fn.tagEditor.defaults = {
+        initialTags: [],
+        maxTags: 0,
+        maxLength: 50,
+        delimiter: ',;',
+        placeholder: '',
+        forceLowercase: true,
+        removeDuplicates: true,
+        clickDelete: false,
+        animateDelete: 175,
+        sortable: true, // jQuery UI sortable
+        autocomplete: null, // options dict for jQuery UI autocomplete
+
+        // callbacks
+        onChange: function(){},
+        beforeTagSave: function(){},
+        beforeTagDelete: function(){}
+    };
+}(jQuery));
diff --git a/static/libjs/jQuery-tagEditor/jquery.tag-editor.min.js b/static/libjs/jQuery-tagEditor/jquery.tag-editor.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..1d083d242607ca7b2f7b8f386242dcc49918eace
--- /dev/null
+++ b/static/libjs/jQuery-tagEditor/jquery.tag-editor.min.js
@@ -0,0 +1,3 @@
+// jQuery tagEditor v1.0.20
+// https://github.com/Pixabay/jQuery-tagEditor
+!function(t){t.fn.tagEditorInput=function(){var e=" ",i=t(this),a=parseInt(i.css("fontSize")),r=t("<span/>").css({position:"absolute",top:-9999,left:-9999,width:"auto",fontSize:i.css("fontSize"),fontFamily:i.css("fontFamily"),fontWeight:i.css("fontWeight"),letterSpacing:i.css("letterSpacing"),whiteSpace:"nowrap"}),l=function(){if(e!==(e=i.val())){r.text(e);var t=r.width()+a;20>t&&(t=20),t!=i.width()&&i.width(t)}};return r.insertAfter(i),i.bind("keyup keydown focus",l)},t.fn.tagEditor=function(e,a,r){function l(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}var n,o=t.extend({},t.fn.tagEditor.defaults,e),c=this;if(o.dregex=new RegExp("["+o.delimiter.replace("-","-")+"]","g"),"string"==typeof e){var s=[];return c.each(function(){var i=t(this),l=i.data("options"),n=i.next(".tag-editor");if("getTags"==e)s.push({field:i[0],editor:n,tags:n.data("tags")});else if("addTag"==e){if(l.maxTags&&n.data("tags").length>=l.maxTags)return!1;t('<li><div class="tag-editor-spacer">&nbsp;'+l.delimiter[0]+'</div><div class="tag-editor-tag"></div><div class="tag-editor-delete"><i></i></div></li>').appendTo(n).find(".tag-editor-tag").html('<input type="text" maxlength="'+l.maxLength+'">').addClass("active").find("input").val(a).blur(),r?t(".placeholder",n).remove():n.click()}else"removeTag"==e?(t(".tag-editor-tag",n).filter(function(){return t(this).text()==a}).closest("li").find(".tag-editor-delete").click(),r||n.click()):"destroy"==e&&i.removeClass("tag-editor-hidden-src").removeData("options").off("focus.tag-editor").next(".tag-editor").remove()}),"getTags"==e?s:this}return window.getSelection&&t(document).off("keydown.tag-editor").on("keydown.tag-editor",function(e){if(8==e.which||46==e.which||e.ctrlKey&&88==e.which){try{var a=getSelection(),r="BODY"==document.activeElement.tagName?t(a.getRangeAt(0).startContainer.parentNode).closest(".tag-editor"):0}catch(e){r=0}if(a.rangeCount>0&&r&&r.length){var l=[],n=a.toString().split(r.prev().data("options").dregex);for(i=0;i<n.length;i++){var o=t.trim(n[i]);o&&l.push(o)}return t(".tag-editor-tag",r).each(function(){~t.inArray(t(this).text(),l)&&t(this).closest("li").find(".tag-editor-delete").click()}),!1}}}),c.each(function(){function e(){!o.placeholder||c.length||t(".deleted, .placeholder, input",s).length||s.append('<li class="placeholder"><div>'+o.placeholder+"</div></li>")}function i(i){var a=c.toString();c=t(".tag-editor-tag:not(.deleted)",s).map(function(e,i){var a=t.trim(t(this).hasClass("active")?t(this).find("input").val():t(i).text());return a?a:void 0}).get(),s.data("tags",c),r.val(c.join(o.delimiter[0])),i||a!=c.toString()&&o.onChange(r,s,c),e()}function a(e){for(var a,n=e.closest("li"),d=e.val().replace(/ +/," ").split(o.dregex),g=e.data("old_tag"),f=c.slice(0),h=!1,u=0;u<d.length;u++)if(v=t.trim(d[u]).slice(0,o.maxLength),o.forceLowercase&&(v=v.toLowerCase()),a=o.beforeTagSave(r,s,f,g,v),v=a||v,a!==!1&&v&&(o.removeDuplicates&&~t.inArray(v,f)&&t(".tag-editor-tag",s).each(function(){t(this).text()==v&&t(this).closest("li").remove()}),f.push(v),n.before('<li><div class="tag-editor-spacer">&nbsp;'+o.delimiter[0]+'</div><div class="tag-editor-tag">'+l(v)+'</div><div class="tag-editor-delete"><i></i></div></li>'),o.maxTags&&f.length>=o.maxTags)){h=!0;break}e.attr("maxlength",o.maxLength).removeData("old_tag").val(""),h?e.blur():e.focus(),i()}var r=t(this),c=[],s=t("<ul "+(o.clickDelete?'oncontextmenu="return false;" ':"")+'class="tag-editor"></ul>').insertAfter(r);r.addClass("tag-editor-hidden-src").data("options",o).on("focus.tag-editor",function(){s.click()}),s.append('<li style="width:1px">&nbsp;</li>');var d='<li><div class="tag-editor-spacer">&nbsp;'+o.delimiter[0]+'</div><div class="tag-editor-tag"></div><div class="tag-editor-delete"><i></i></div></li>';s.click(function(e,i){var a,r,l=99999;if(!window.getSelection||""==getSelection())return o.maxTags&&s.data("tags").length>=o.maxTags?(s.find("input").blur(),!1):(n=!0,t("input:focus",s).blur(),n?(n=!0,t(".placeholder",s).remove(),i&&i.length?r="before":t(".tag-editor-tag",s).each(function(){var n=t(this),o=n.offset(),c=o.left,s=o.top;e.pageY>=s&&e.pageY<=s+n.height()&&(e.pageX<c?(r="before",a=c-e.pageX):(r="after",a=e.pageX-c-n.width()),l>a&&(l=a,i=n))}),"before"==r?t(d).insertBefore(i.closest("li")).find(".tag-editor-tag").click():"after"==r?t(d).insertAfter(i.closest("li")).find(".tag-editor-tag").click():t(d).appendTo(s).find(".tag-editor-tag").click(),!1):!1)}),s.on("click",".tag-editor-delete",function(){if(t(this).prev().hasClass("active"))return t(this).closest("li").find("input").caret(-1),!1;var a=t(this).closest("li"),l=a.find(".tag-editor-tag");return o.beforeTagDelete(r,s,c,l.text())===!1?!1:(l.addClass("deleted").animate({width:0},o.animateDelete,function(){a.remove(),e()}),i(),!1)}),o.clickDelete&&s.on("mousedown",".tag-editor-tag",function(a){if(a.ctrlKey||a.which>1){var l=t(this).closest("li"),n=l.find(".tag-editor-tag");return o.beforeTagDelete(r,s,c,n.text())===!1?!1:(n.addClass("deleted").animate({width:0},o.animateDelete,function(){l.remove(),e()}),i(),!1)}}),s.on("click",".tag-editor-tag",function(e){if(o.clickDelete&&(e.ctrlKey||e.which>1))return!1;if(!t(this).hasClass("active")){var i=t(this).text(),a=Math.abs((t(this).offset().left-e.pageX)/t(this).width()),r=parseInt(i.length*a),n=t(this).html('<input type="text" maxlength="'+o.maxLength+'" value="'+l(i)+'">').addClass("active").find("input");if(n.data("old_tag",i).tagEditorInput().focus().caret(r),o.autocomplete){var c=t.extend({},o.autocomplete),d="select"in c?o.autocomplete.select:"";c.select=function(e,i){d&&d(e,i),setTimeout(function(){s.trigger("click",[t(".active",s).find("input").closest("li").next("li").find(".tag-editor-tag")])},20)},n.autocomplete(c)}}return!1}),s.on("blur","input",function(d){d.stopPropagation();var g=t(this),f=g.data("old_tag"),h=t.trim(g.val().replace(/ +/," ").replace(o.dregex,o.delimiter[0]));if(h){if(h.indexOf(o.delimiter[0])>=0)return void a(g);if(h!=f)if(o.forceLowercase&&(h=h.toLowerCase()),cb_val=o.beforeTagSave(r,s,c,f,h),h=cb_val||h,cb_val===!1){if(f)return g.val(f).focus(),n=!1,void i();try{g.closest("li").remove()}catch(d){}f&&i()}else o.removeDuplicates&&t(".tag-editor-tag:not(.active)",s).each(function(){t(this).text()==h&&t(this).closest("li").remove()})}else{if(f&&o.beforeTagDelete(r,s,c,f)===!1)return g.val(f).focus(),n=!1,void i();try{g.closest("li").remove()}catch(d){}f&&i()}g.parent().html(l(h)).removeClass("active"),h!=f&&i(),e()});var g;s.on("paste","input",function(){t(this).removeAttr("maxlength"),g=t(this),setTimeout(function(){a(g)},30)});var f;s.on("keypress","input",function(e){o.delimiter.indexOf(String.fromCharCode(e.which))>=0&&(f=t(this),setTimeout(function(){a(f)},20))}),s.on("keydown","input",function(e){var i=t(this);if((37==e.which||!o.autocomplete&&38==e.which)&&!i.caret()||8==e.which&&!i.val()){var a=i.closest("li").prev("li").find(".tag-editor-tag");return a.length?a.click().find("input").caret(-1):!i.val()||o.maxTags&&s.data("tags").length>=o.maxTags||t(d).insertBefore(i.closest("li")).find(".tag-editor-tag").click(),!1}if((39==e.which||!o.autocomplete&&40==e.which)&&i.caret()==i.val().length){var l=i.closest("li").next("li").find(".tag-editor-tag");return l.length?l.click().find("input").caret(0):i.val()&&s.click(),!1}if(9==e.which){if(e.shiftKey){var a=i.closest("li").prev("li").find(".tag-editor-tag");if(a.length)a.click().find("input").caret(0);else{if(!i.val()||o.maxTags&&s.data("tags").length>=o.maxTags)return r.attr("disabled","disabled"),void setTimeout(function(){r.removeAttr("disabled")},30);t(d).insertBefore(i.closest("li")).find(".tag-editor-tag").click()}return!1}var l=i.closest("li").next("li").find(".tag-editor-tag");if(l.length)l.click().find("input").caret(0);else{if(!i.val())return;s.click()}return!1}if(!(46!=e.which||t.trim(i.val())&&i.caret()!=i.val().length)){var l=i.closest("li").next("li").find(".tag-editor-tag");return l.length?l.click().find("input").caret(0):i.val()&&s.click(),!1}if(13==e.which)return s.trigger("click",[i.closest("li").next("li").find(".tag-editor-tag")]),o.maxTags&&s.data("tags").length>=o.maxTags&&s.find("input").blur(),!1;if(36!=e.which||i.caret()){if(35==e.which&&i.caret()==i.val().length)s.find(".tag-editor-tag").last().click();else if(27==e.which)return i.val(i.data("old_tag")?i.data("old_tag"):"").blur(),!1}else s.find(".tag-editor-tag").first().click()});for(var h=o.initialTags.length?o.initialTags:r.val().split(o.dregex),u=0;u<h.length&&!(o.maxTags&&u>=o.maxTags);u++){var v=t.trim(h[u].replace(/ +/," "));v&&(o.forceLowercase&&(v=v.toLowerCase()),c.push(v),s.append('<li><div class="tag-editor-spacer">&nbsp;'+o.delimiter[0]+'</div><div class="tag-editor-tag">'+l(v)+'</div><div class="tag-editor-delete"><i></i></div></li>'))}i(!0),o.sortable&&t.fn.sortable&&s.sortable({distance:5,cancel:".tag-editor-spacer, input",helper:"clone",update:function(){i()}})})},t.fn.tagEditor.defaults={initialTags:[],maxTags:0,maxLength:50,delimiter:",;",placeholder:"",forceLowercase:!0,removeDuplicates:!0,clickDelete:!1,animateDelete:175,sortable:!0,autocomplete:null,onChange:function(){},beforeTagSave:function(){},beforeTagDelete:function(){}}}(jQuery);
\ No newline at end of file
diff --git a/static/libjs/jQuery-tagEditor/readme.md b/static/libjs/jQuery-tagEditor/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..8990f89df3558a9e0f657aa7cecdf6299e014693
--- /dev/null
+++ b/static/libjs/jQuery-tagEditor/readme.md
@@ -0,0 +1,119 @@
+jQuery-tagEditor
+================
+
+A powerful and lightweight tag editor plugin for jQuery.
+
+Compatible with jQuery 1.7.0+ in Firefox, Safari, Chrome, Opera, Internet Explorer 8+. IE7 technically works, but no care has gone into CSS/layout bugs.
+Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+This plugin was developed by and for [Pixabay.com](https://pixabay.com/) - an international repository for sharing free public domain images.
+We have implemented this plugin in production and we share this piece of software - in the spirit of Pixabay - freely with others.
+
+## Demo and Documentation
+
+https://goodies.pixabay.com/jquery/tag-editor/demo.html
+
+## Features
+
+* Lightweight: 8.5 kB of JavaScript - less than 3.2 kB gzipped
+* Edit in place tags
+* Intuitive navigation between tags with cursor keys, Tab, Shift+Tab, Enter, Pos1, End, Backspace, Del, and ESC
+* Optional jQuery UI sortable
+* Optional jQuery UI autocomplete
+* Copy-paste or delete multiple selected tags
+* Duplicate tags check
+* Custom delimiter/s
+* Placeholder
+* Custom style for faulty tags
+* Public methods for reading, adding and removing tags + destroy function
+* Callbacks
+* Allows tabindex for form navigation
+* Graceful degradation if JavaScript is disabled
+
+## Changelog
+
+### Version 1.0.20 - 2016/01/30
+
+* Fixed #62: tagEditor is blocking key events on other input and textarea elements on page.
+
+### Version 1.0.19 - 2015/12/02
+
+* Fixed #60: Tag editor fails to handle HTML operator chars.
+
+### Version 1.0.18 - 2015/08/12
+
+* Pull #43: Escape HTML special characters on input.
+
+### Version 1.0.17 - 2015/07/14
+
+* Allow beforeTagSave() to return `false` for discarding certain tag values.
+
+### Version 1.0.16 - 2015/07/01
+
+* Fix #5, #35, #37, #38: "TypeError: owner is null" backspace browser history issue.
+
+### Version 1.0.15 - 2015/05/24
+
+* Fix #31, #33, #34: Added maxTags, removeDuplicates, and animateDelete options.
+
+### Version 1.0.14 - 2015/04/05
+
+* Fix #24: Auto-close tag after selecting autocomplete suggestion by mouse click.
+
+### Version 1.0.13 - 2015/01/26
+
+* Fix #9: Added bower support.
+
+### Version 1.0.12 - 2015/01/16
+
+* Fix #17: Make use of tabindex for form navigation.
+
+### Version 1.0.11 - 2015/01/08
+
+* Use beforeTagSave return value for overwriting new tags.
+
+### Version 1.0.10 - 2015/01/04
+
+* Fix for IE8
+
+### Version 1.0.9 - 2014/12/17
+
+* Optimized internal input autogrow function.
+
+### Version 1.0.8 - 2014/12/14
+
+* Added bower.json file.
+
+### Version 1.0.7 - 2014/11/26
+
+* Removing accursoft's caret plugin (http://code.accursoft.com/caret) from tagEditor source (and adding caret as a dependency).
+
+### Version 1.0.6 - 2014/10/22
+
+* Fixed: Detection for selected field (.tag-editor) on backspace/delete keypress failed in some cases.
+
+### Version 1.0.5 - 2014/09/30
+
+* Merged pull - Added logic for selected field to be .tag-editor only:
+  https://github.com/jegarts/jQuery-tagEditor/commit/498435b562d72c3e502863312b0b2ccbb9e80cab
+
+### Version 1.0.4 - 2014/09/24
+
+* Merged pull stop aco.select form calling itself:
+  https://github.com/jegarts/jQuery-tagEditor/commit/fd0340ba46272290cedc8991f58769945d0fc2c2
+
+### Version 1.0.3 - 2014/09/13
+
+* Removed unnecessary vendor prefixes in CSS stylesheet.
+
+### Version 1.0.2 - 2014/07/16
+
+* Fixed removal of placeholder after calling addTags.
+
+### Version 1.0.1 - 2014/07/16
+
+* Fixed tagEditor for IE8 and IE7. IE7 still has some obvious layout alignment bugs, that can be fixed by conditional CSS rules.
+
+### Version 1.0.0-beta - 2014/07/15
+
+* First release
diff --git a/static/libjs/jQuery-tagEditor/tag-editor.jquery.json b/static/libjs/jQuery-tagEditor/tag-editor.jquery.json
new file mode 100644
index 0000000000000000000000000000000000000000..b39c0017f34c8ece9b54bc7a139ac02fc78302cb
--- /dev/null
+++ b/static/libjs/jQuery-tagEditor/tag-editor.jquery.json
@@ -0,0 +1,31 @@
+{
+	"name": "tag-editor",
+	"title": "tagEditor",
+	"description": "A powerful and lightweight tag editor plugin for jQuery.",
+	"version": "1.0.20",
+	"dependencies": {
+		"jquery": ">=1.7",
+		"caret": ">=1.3.2"
+	},
+	"keywords": [
+		"tags",
+		"keywords",
+		"editor",
+		"ui",
+		"tagging",
+		"jQuery"
+	],
+	"author": {
+		"name": "Simon Steinberger",
+		"url": "https://pixabay.com/users/Simon/",
+		"email": "simon@pixabay.com"
+	},
+	"licenses": [
+		{
+			"type": "MIT",
+			"url": "http://www.opensource.org/licenses/mit-license.php"
+		}
+	],
+	"homepage": "https://goodies.pixabay.com/jquery/tag-editor/demo.html",
+	"demo": "https://goodies.pixabay.com/jquery/tag-editor/demo.html"
+}
\ No newline at end of file
diff --git a/static/libjs/jinplace-1.2.1.min.js b/static/libjs/jinplace-1.2.1.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..30f81fa380784648d1bce3bb53d855ed216933b4
--- /dev/null
+++ b/static/libjs/jinplace-1.2.1.min.js
@@ -0,0 +1,12 @@
+/*
+ Copyright ? 2013, 2014 the jinplace team and contributors.
+ MIT Licence */
+(function(d,s,n,g){function k(b,a){var c=this.element=d(b),c=this.elementOptions(c);c.activator=d(c.activator||b);this.opts=c=d.extend({},d.fn.jinplace.defaults,a,c);this.bindElement(c)}var p="type url data loadurl elementId object attribute okButton cancelButton inputClass activator textOnly placeholder submitFunction".split(" ");k.prototype={elementOptions:function(b){function a(a){return"-"+a.toLowerCase()}var c={};d.each(p,function(d,f){c[f]=b.attr("data-"+f.replace(/[A-Z]/g,a))});c.elementId=
+b.attr("id");c.textOnly&&(c.textOnly="false"!==c.textOnly);return c},bindElement:function(b){b.activator.off("click.jip").on("click.jip",d.proxy(this.clickHandler,this));var a=this.element;""==d.trim(a.html())&&(a.html(b.placeholder),b.placeholder=a.html())},clickHandler:function(b){b.preventDefault();b.stopPropagation();d(b.currentTarget).off("click.jip").on("click.jip",function(a){a.preventDefault()});var a=this,c=a.opts,e=d.extend({},l,d.fn.jinplace.editors[c.type]);a.origValue=a.element.html();
+a.fetchData(c).done(function(b){b=e.makeField(a.element,b);e.inputField||(e.inputField=b);b.addClass(c.inputClass);var d=q(c,b,e.buttonsAllowed);a.element.html(d);d.on("jip:submit submit",function(b){a.submit(e,c);return!1}).on("jip:cancel",function(b){a.cancel(e);return!1}).on("keyup",function(b){27==b.keyCode&&a.cancel(e)});e.activate(d,b);e.blurEvent(b,d,e.blurAction||(c.okButton?c.cancelButton?g:"jip:cancel":"submit"))})},fetchData:function(b){var a;a=b.data?b.data:b.loadurl?b.loadFunction(b):
+b.textOnly?d.trim(this.element.text()):d.trim(this.element.html().replace(/&amp;/gi,"&"));var c=function(a){return a==b.placeholder?"":a};a=d.when(a);return a.pipe?a.pipe(c):a.then(c)},cancel:function(b){this.element.html(this.origValue);b.finish();this.bindElement(this.opts)},submit:function(b,a){var c=this,e,f=d.Deferred().reject();try{e=a.submitFunction.call(g,a,b.value()),e===g&&(e=f)}catch(t){e=f}d.when(e).done(function(d,e,f){c.element.trigger("jinplace:done",[d,e,f]);c.onUpdate(b,a,d)}).fail(function(a,
+d,e){c.element.trigger("jinplace:fail",[a,d,e]);c.cancel(b)}).always(function(a,b,d){c.element.trigger("jinplace:always",[a,b,d])})},onUpdate:function(b,a,c){this.setContent(c);b.finish();this.bindElement(a)},setContent:function(b){var a=this.element;b?this.opts.textOnly?a.text(b):a.html(b):a.html(this.opts.placeholder)}};var m=function(b,a){var c={id:b.elementId,object:b.object,attribute:b.attribute};d.isPlainObject(a)?d.extend(c,a):a!==g&&(c.value=a);return c};d.fn.jinplace=function(b){return this.each(function(){d.data(this,
+"plugin_jinplace")||d.data(this,"plugin_jinplace",new k(this,b))})};d.fn.jinplace.defaults={url:n.location.pathname,type:"input",textOnly:!0,placeholder:"[ --- ]",submitFunction:function(b,a){return d.ajax(b.url,{type:"post",data:m(b,a),dataType:"text",headers:{"Cache-Control":"no-cache"}})},loadFunction:function(b){return d.ajax(b.loadurl,{data:m(b)})}};var q=function(b,a,c){a=d("<form>").attr("style","display: inline;").attr("action","javascript:void(0);").append(a);c&&r(a,b);return a},r=function(b,
+a){var c=function(a,c){b.append(a);a.one("click",function(a){a.stopPropagation();b.trigger(c)})},e=a.okButton;e&&(e=d("<input>").attr("type","button").attr("value",e).addClass("jip-button jip-ok-button"),c(e,"submit"));if(e=a.cancelButton)e=d("<input>").attr("type","button").attr("value",e).addClass("jip-button jip-cancel-button"),c(e,"jip:cancel")},l={makeField:function(b,a){return d("<input>").attr("type","text").val(a)},activate:function(b,a){a.focus()},value:function(){return this.inputField.val()},
+blurEvent:function(b,a,c){if(c&&"ignore"!=c)b.on("blur",function(d){var f=setTimeout(function(){b.trigger(c)},300);a.on("click",function(){clearTimeout(f)})})},finish:function(){}};d.fn.jinplace.editorBase=l;d.fn.jinplace.editors={input:{buttonsAllowed:!0},textarea:{buttonsAllowed:!0,makeField:function(b,a){return d("<textarea>").css({"min-width":b.width(),"min-height":b.height()}).val(a)},activate:function(b,a){a.focus();a.elastic&&a.elastic()}},select:{makeField:function(b,a){var c=d("<select>"),
+e=d.parseJSON(a),f=!1,g=null;d.each(e,function(a,e){var h=d("<option>").val(e[0]).html(e[1]);e[2]&&(h.attr("selected","1"),f=!0);e[1]==b.text()&&(g=h);c.append(h)});!f&&g&&g.attr("selected","1");return c},activate:function(b,a){a.focus();a.on("change",function(){a.trigger("jip:submit")})}}}})(jQuery,window,document);
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/animated-overlay.gif b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/animated-overlay.gif
new file mode 100644
index 0000000000000000000000000000000000000000..d441f75ebfbdf26a265dfccd670120d25c0a341c
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/animated-overlay.gif differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100644
index 0000000000000000000000000000000000000000..b389fea0c22c03db4b4040dbf2d289b95a3a6cd7
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100644
index 0000000000000000000000000000000000000000..217cace76acebb838a74623cd2548d2808084c31
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ce34daad75dd7307d3a94f16772ce1b21c5d82f
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100644
index 0000000000000000000000000000000000000000..dbefb42bb828fa399d748850081a356c2c826e7b
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png
new file mode 100644
index 0000000000000000000000000000000000000000..c1224d098225737995905c72207caacccc1044bc
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb449cb018350769792631c824b6f9ca15287994
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png
new file mode 100644
index 0000000000000000000000000000000000000000..be9926b733fb00a3f5badc9290f4bf7eba6f86ba
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png
new file mode 100644
index 0000000000000000000000000000000000000000..5498f8588a54356fa0db53be7699802d7ffcbb9d
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_222222_256x240.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_222222_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..c1cb1170c8b3795835b8831ab81fa9ae63b606b1
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_222222_256x240.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_2e83ff_256x240.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_2e83ff_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..84b601bf0f726bf95801da487deaf2344a32e4b8
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_2e83ff_256x240.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_454545_256x240.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_454545_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6db1acdd433be80a472b045018f25c7f2cf7e08
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_454545_256x240.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_888888_256x240.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_888888_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..feea0e20264c4649b2ef03fe6705d69b4937c04e
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_888888_256x240.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_cd0a0a_256x240.png b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_cd0a0a_256x240.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed5b6b0930f672fa08e9b9bdbe5e55370fd1dc30
Binary files /dev/null and b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/images/ui-icons_cd0a0a_256x240.png differ
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.css b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.css
new file mode 100644
index 0000000000000000000000000000000000000000..4cdd22a8ce187ca66cb55ce12ac57cd25ef18986
--- /dev/null
+++ b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.css
@@ -0,0 +1,1178 @@
+/*! jQuery UI - v1.10.4 - 2014-02-10
+* http://jqueryui.com
+* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css, jquery.ui.theme.css
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden {
+	display: none;
+}
+.ui-helper-hidden-accessible {
+	border: 0;
+	clip: rect(0 0 0 0);
+	height: 1px;
+	margin: -1px;
+	overflow: hidden;
+	padding: 0;
+	position: absolute;
+	width: 1px;
+}
+.ui-helper-reset {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	outline: 0;
+	line-height: 1.3;
+	text-decoration: none;
+	font-size: 100%;
+	list-style: none;
+}
+.ui-helper-clearfix:before,
+.ui-helper-clearfix:after {
+	content: "";
+	display: table;
+	border-collapse: collapse;
+}
+.ui-helper-clearfix:after {
+	clear: both;
+}
+.ui-helper-clearfix {
+	min-height: 0; /* support: IE7 */
+}
+.ui-helper-zfix {
+	width: 100%;
+	height: 100%;
+	top: 0;
+	left: 0;
+	position: absolute;
+	opacity: 0;
+	filter:Alpha(Opacity=0);
+}
+
+.ui-front {
+	z-index: 100;
+}
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled {
+	cursor: default !important;
+}
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon {
+	display: block;
+	text-indent: -99999px;
+	overflow: hidden;
+	background-repeat: no-repeat;
+}
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+}
+.ui-resizable {
+	position: relative;
+}
+.ui-resizable-handle {
+	position: absolute;
+	font-size: 0.1px;
+	display: block;
+}
+.ui-resizable-disabled .ui-resizable-handle,
+.ui-resizable-autohide .ui-resizable-handle {
+	display: none;
+}
+.ui-resizable-n {
+	cursor: n-resize;
+	height: 7px;
+	width: 100%;
+	top: -5px;
+	left: 0;
+}
+.ui-resizable-s {
+	cursor: s-resize;
+	height: 7px;
+	width: 100%;
+	bottom: -5px;
+	left: 0;
+}
+.ui-resizable-e {
+	cursor: e-resize;
+	width: 7px;
+	right: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-w {
+	cursor: w-resize;
+	width: 7px;
+	left: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-se {
+	cursor: se-resize;
+	width: 12px;
+	height: 12px;
+	right: 1px;
+	bottom: 1px;
+}
+.ui-resizable-sw {
+	cursor: sw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	bottom: -5px;
+}
+.ui-resizable-nw {
+	cursor: nw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	top: -5px;
+}
+.ui-resizable-ne {
+	cursor: ne-resize;
+	width: 9px;
+	height: 9px;
+	right: -5px;
+	top: -5px;
+}
+.ui-selectable-helper {
+	position: absolute;
+	z-index: 100;
+	border: 1px dotted black;
+}
+.ui-accordion .ui-accordion-header {
+	display: block;
+	cursor: pointer;
+	position: relative;
+	margin-top: 2px;
+	padding: .5em .5em .5em .7em;
+	min-height: 0; /* support: IE7 */
+}
+.ui-accordion .ui-accordion-icons {
+	padding-left: 2.2em;
+}
+.ui-accordion .ui-accordion-noicons {
+	padding-left: .7em;
+}
+.ui-accordion .ui-accordion-icons .ui-accordion-icons {
+	padding-left: 2.2em;
+}
+.ui-accordion .ui-accordion-header .ui-accordion-header-icon {
+	position: absolute;
+	left: .5em;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-accordion .ui-accordion-content {
+	padding: 1em 2.2em;
+	border-top: 0;
+	overflow: auto;
+}
+.ui-autocomplete {
+	position: absolute;
+	top: 0;
+	left: 0;
+	cursor: default;
+}
+.ui-button {
+	display: inline-block;
+	position: relative;
+	padding: 0;
+	line-height: normal;
+	margin-right: .1em;
+	cursor: pointer;
+	vertical-align: middle;
+	text-align: center;
+	overflow: visible; /* removes extra width in IE */
+}
+.ui-button,
+.ui-button:link,
+.ui-button:visited,
+.ui-button:hover,
+.ui-button:active {
+	text-decoration: none;
+}
+/* to make room for the icon, a width needs to be set here */
+.ui-button-icon-only {
+	width: 2.2em;
+}
+/* button elements seem to need a little more width */
+button.ui-button-icon-only {
+	width: 2.4em;
+}
+.ui-button-icons-only {
+	width: 3.4em;
+}
+button.ui-button-icons-only {
+	width: 3.7em;
+}
+
+/* button text element */
+.ui-button .ui-button-text {
+	display: block;
+	line-height: normal;
+}
+.ui-button-text-only .ui-button-text {
+	padding: .4em 1em;
+}
+.ui-button-icon-only .ui-button-text,
+.ui-button-icons-only .ui-button-text {
+	padding: .4em;
+	text-indent: -9999999px;
+}
+.ui-button-text-icon-primary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+	padding: .4em 1em .4em 2.1em;
+}
+.ui-button-text-icon-secondary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+	padding: .4em 2.1em .4em 1em;
+}
+.ui-button-text-icons .ui-button-text {
+	padding-left: 2.1em;
+	padding-right: 2.1em;
+}
+/* no icon support for input elements, provide padding by default */
+input.ui-button {
+	padding: .4em 1em;
+}
+
+/* button icon element(s) */
+.ui-button-icon-only .ui-icon,
+.ui-button-text-icon-primary .ui-icon,
+.ui-button-text-icon-secondary .ui-icon,
+.ui-button-text-icons .ui-icon,
+.ui-button-icons-only .ui-icon {
+	position: absolute;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-button-icon-only .ui-icon {
+	left: 50%;
+	margin-left: -8px;
+}
+.ui-button-text-icon-primary .ui-button-icon-primary,
+.ui-button-text-icons .ui-button-icon-primary,
+.ui-button-icons-only .ui-button-icon-primary {
+	left: .5em;
+}
+.ui-button-text-icon-secondary .ui-button-icon-secondary,
+.ui-button-text-icons .ui-button-icon-secondary,
+.ui-button-icons-only .ui-button-icon-secondary {
+	right: .5em;
+}
+
+/* button sets */
+.ui-buttonset {
+	margin-right: 7px;
+}
+.ui-buttonset .ui-button {
+	margin-left: 0;
+	margin-right: -.3em;
+}
+
+/* workarounds */
+/* reset extra padding in Firefox, see h5bp.com/l */
+input.ui-button::-moz-focus-inner,
+button.ui-button::-moz-focus-inner {
+	border: 0;
+	padding: 0;
+}
+.ui-datepicker {
+	width: 17em;
+	padding: .2em .2em 0;
+	display: none;
+}
+.ui-datepicker .ui-datepicker-header {
+	position: relative;
+	padding: .2em 0;
+}
+.ui-datepicker .ui-datepicker-prev,
+.ui-datepicker .ui-datepicker-next {
+	position: absolute;
+	top: 2px;
+	width: 1.8em;
+	height: 1.8em;
+}
+.ui-datepicker .ui-datepicker-prev-hover,
+.ui-datepicker .ui-datepicker-next-hover {
+	top: 1px;
+}
+.ui-datepicker .ui-datepicker-prev {
+	left: 2px;
+}
+.ui-datepicker .ui-datepicker-next {
+	right: 2px;
+}
+.ui-datepicker .ui-datepicker-prev-hover {
+	left: 1px;
+}
+.ui-datepicker .ui-datepicker-next-hover {
+	right: 1px;
+}
+.ui-datepicker .ui-datepicker-prev span,
+.ui-datepicker .ui-datepicker-next span {
+	display: block;
+	position: absolute;
+	left: 50%;
+	margin-left: -8px;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-datepicker .ui-datepicker-title {
+	margin: 0 2.3em;
+	line-height: 1.8em;
+	text-align: center;
+}
+.ui-datepicker .ui-datepicker-title select {
+	font-size: 1em;
+	margin: 1px 0;
+}
+.ui-datepicker select.ui-datepicker-month,
+.ui-datepicker select.ui-datepicker-year {
+	width: 49%;
+}
+.ui-datepicker table {
+	width: 100%;
+	font-size: .9em;
+	border-collapse: collapse;
+	margin: 0 0 .4em;
+}
+.ui-datepicker th {
+	padding: .7em .3em;
+	text-align: center;
+	font-weight: bold;
+	border: 0;
+}
+.ui-datepicker td {
+	border: 0;
+	padding: 1px;
+}
+.ui-datepicker td span,
+.ui-datepicker td a {
+	display: block;
+	padding: .2em;
+	text-align: right;
+	text-decoration: none;
+}
+.ui-datepicker .ui-datepicker-buttonpane {
+	background-image: none;
+	margin: .7em 0 0 0;
+	padding: 0 .2em;
+	border-left: 0;
+	border-right: 0;
+	border-bottom: 0;
+}
+.ui-datepicker .ui-datepicker-buttonpane button {
+	float: right;
+	margin: .5em .2em .4em;
+	cursor: pointer;
+	padding: .2em .6em .3em .6em;
+	width: auto;
+	overflow: visible;
+}
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
+	float: left;
+}
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi {
+	width: auto;
+}
+.ui-datepicker-multi .ui-datepicker-group {
+	float: left;
+}
+.ui-datepicker-multi .ui-datepicker-group table {
+	width: 95%;
+	margin: 0 auto .4em;
+}
+.ui-datepicker-multi-2 .ui-datepicker-group {
+	width: 50%;
+}
+.ui-datepicker-multi-3 .ui-datepicker-group {
+	width: 33.3%;
+}
+.ui-datepicker-multi-4 .ui-datepicker-group {
+	width: 25%;
+}
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
+	border-left-width: 0;
+}
+.ui-datepicker-multi .ui-datepicker-buttonpane {
+	clear: left;
+}
+.ui-datepicker-row-break {
+	clear: both;
+	width: 100%;
+	font-size: 0;
+}
+
+/* RTL support */
+.ui-datepicker-rtl {
+	direction: rtl;
+}
+.ui-datepicker-rtl .ui-datepicker-prev {
+	right: 2px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next {
+	left: 2px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-prev:hover {
+	right: 1px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next:hover {
+	left: 1px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane {
+	clear: right;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button {
+	float: left;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
+.ui-datepicker-rtl .ui-datepicker-group {
+	float: right;
+}
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
+	border-right-width: 0;
+	border-left-width: 1px;
+}
+.ui-dialog {
+	overflow: hidden;
+	position: absolute;
+	top: 0;
+	left: 0;
+	padding: .2em;
+	outline: 0;
+}
+.ui-dialog .ui-dialog-titlebar {
+	padding: .4em 1em;
+	position: relative;
+}
+.ui-dialog .ui-dialog-title {
+	float: left;
+	margin: .1em 0;
+	white-space: nowrap;
+	width: 90%;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+.ui-dialog .ui-dialog-titlebar-close {
+	position: absolute;
+	right: .3em;
+	top: 50%;
+	width: 20px;
+	margin: -10px 0 0 0;
+	padding: 1px;
+	height: 20px;
+}
+.ui-dialog .ui-dialog-content {
+	position: relative;
+	border: 0;
+	padding: .5em 1em;
+	background: none;
+	overflow: auto;
+}
+.ui-dialog .ui-dialog-buttonpane {
+	text-align: left;
+	border-width: 1px 0 0 0;
+	background-image: none;
+	margin-top: .5em;
+	padding: .3em 1em .5em .4em;
+}
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
+	float: right;
+}
+.ui-dialog .ui-dialog-buttonpane button {
+	margin: .5em .4em .5em 0;
+	cursor: pointer;
+}
+.ui-dialog .ui-resizable-se {
+	width: 12px;
+	height: 12px;
+	right: -5px;
+	bottom: -5px;
+	background-position: 16px 16px;
+}
+.ui-draggable .ui-dialog-titlebar {
+	cursor: move;
+}
+.ui-menu {
+	list-style: none;
+	padding: 2px;
+	margin: 0;
+	display: block;
+	outline: none;
+}
+.ui-menu .ui-menu {
+	margin-top: -3px;
+	position: absolute;
+}
+.ui-menu .ui-menu-item {
+	margin: 0;
+	padding: 0;
+	width: 100%;
+	/* support: IE10, see #8844 */
+	list-style-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);
+}
+.ui-menu .ui-menu-divider {
+	margin: 5px -2px 5px -2px;
+	height: 0;
+	font-size: 0;
+	line-height: 0;
+	border-width: 1px 0 0 0;
+}
+.ui-menu .ui-menu-item a {
+	text-decoration: none;
+	display: block;
+	padding: 2px .4em;
+	line-height: 1.5;
+	min-height: 0; /* support: IE7 */
+	font-weight: normal;
+}
+.ui-menu .ui-menu-item a.ui-state-focus,
+.ui-menu .ui-menu-item a.ui-state-active {
+	font-weight: normal;
+	margin: -1px;
+}
+
+.ui-menu .ui-state-disabled {
+	font-weight: normal;
+	margin: .4em 0 .2em;
+	line-height: 1.5;
+}
+.ui-menu .ui-state-disabled a {
+	cursor: default;
+}
+
+/* icon support */
+.ui-menu-icons {
+	position: relative;
+}
+.ui-menu-icons .ui-menu-item a {
+	position: relative;
+	padding-left: 2em;
+}
+
+/* left-aligned */
+.ui-menu .ui-icon {
+	position: absolute;
+	top: .2em;
+	left: .2em;
+}
+
+/* right-aligned */
+.ui-menu .ui-menu-icon {
+	position: static;
+	float: right;
+}
+.ui-progressbar {
+	height: 2em;
+	text-align: left;
+	overflow: hidden;
+}
+.ui-progressbar .ui-progressbar-value {
+	margin: -1px;
+	height: 100%;
+}
+.ui-progressbar .ui-progressbar-overlay {
+	background: url("images/animated-overlay.gif");
+	height: 100%;
+	filter: alpha(opacity=25);
+	opacity: 0.25;
+}
+.ui-progressbar-indeterminate .ui-progressbar-value {
+	background-image: none;
+}
+.ui-slider {
+	position: relative;
+	text-align: left;
+}
+.ui-slider .ui-slider-handle {
+	position: absolute;
+	z-index: 2;
+	width: 1.2em;
+	height: 1.2em;
+	cursor: default;
+}
+.ui-slider .ui-slider-range {
+	position: absolute;
+	z-index: 1;
+	font-size: .7em;
+	display: block;
+	border: 0;
+	background-position: 0 0;
+}
+
+/* For IE8 - See #6727 */
+.ui-slider.ui-state-disabled .ui-slider-handle,
+.ui-slider.ui-state-disabled .ui-slider-range {
+	filter: inherit;
+}
+
+.ui-slider-horizontal {
+	height: .8em;
+}
+.ui-slider-horizontal .ui-slider-handle {
+	top: -.3em;
+	margin-left: -.6em;
+}
+.ui-slider-horizontal .ui-slider-range {
+	top: 0;
+	height: 100%;
+}
+.ui-slider-horizontal .ui-slider-range-min {
+	left: 0;
+}
+.ui-slider-horizontal .ui-slider-range-max {
+	right: 0;
+}
+
+.ui-slider-vertical {
+	width: .8em;
+	height: 100px;
+}
+.ui-slider-vertical .ui-slider-handle {
+	left: -.3em;
+	margin-left: 0;
+	margin-bottom: -.6em;
+}
+.ui-slider-vertical .ui-slider-range {
+	left: 0;
+	width: 100%;
+}
+.ui-slider-vertical .ui-slider-range-min {
+	bottom: 0;
+}
+.ui-slider-vertical .ui-slider-range-max {
+	top: 0;
+}
+.ui-spinner {
+	position: relative;
+	display: inline-block;
+	overflow: hidden;
+	padding: 0;
+	vertical-align: middle;
+}
+.ui-spinner-input {
+	border: none;
+	background: none;
+	color: inherit;
+	padding: 0;
+	margin: .2em 0;
+	vertical-align: middle;
+	margin-left: .4em;
+	margin-right: 22px;
+}
+.ui-spinner-button {
+	width: 16px;
+	height: 50%;
+	font-size: .5em;
+	padding: 0;
+	margin: 0;
+	text-align: center;
+	position: absolute;
+	cursor: default;
+	display: block;
+	overflow: hidden;
+	right: 0;
+}
+/* more specificity required here to override default borders */
+.ui-spinner a.ui-spinner-button {
+	border-top: none;
+	border-bottom: none;
+	border-right: none;
+}
+/* vertically center icon */
+.ui-spinner .ui-icon {
+	position: absolute;
+	margin-top: -8px;
+	top: 50%;
+	left: 0;
+}
+.ui-spinner-up {
+	top: 0;
+}
+.ui-spinner-down {
+	bottom: 0;
+}
+
+/* TR overrides */
+.ui-spinner .ui-icon-triangle-1-s {
+	/* need to fix icons sprite */
+	background-position: -65px -16px;
+}
+.ui-tabs {
+	position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+	padding: .2em;
+}
+.ui-tabs .ui-tabs-nav {
+	margin: 0;
+	padding: .2em .2em 0;
+}
+.ui-tabs .ui-tabs-nav li {
+	list-style: none;
+	float: left;
+	position: relative;
+	top: 0;
+	margin: 1px .2em 0 0;
+	border-bottom-width: 0;
+	padding: 0;
+	white-space: nowrap;
+}
+.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
+	float: left;
+	padding: .5em 1em;
+	text-decoration: none;
+}
+.ui-tabs .ui-tabs-nav li.ui-tabs-active {
+	margin-bottom: -1px;
+	padding-bottom: 1px;
+}
+.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,
+.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,
+.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {
+	cursor: text;
+}
+.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {
+	cursor: pointer;
+}
+.ui-tabs .ui-tabs-panel {
+	display: block;
+	border-width: 0;
+	padding: 1em 1.4em;
+	background: none;
+}
+.ui-tooltip {
+	padding: 8px;
+	position: absolute;
+	z-index: 9999;
+	max-width: 300px;
+	-webkit-box-shadow: 0 0 5px #aaa;
+	box-shadow: 0 0 5px #aaa;
+}
+body .ui-tooltip {
+	border-width: 2px;
+}
+
+/* Component containers
+----------------------------------*/
+.ui-widget {
+	font-family: Verdana,Arial,sans-serif;
+	font-size: 1.1em;
+}
+.ui-widget .ui-widget {
+	font-size: 1em;
+}
+.ui-widget input,
+.ui-widget select,
+.ui-widget textarea,
+.ui-widget button {
+	font-family: Verdana,Arial,sans-serif;
+	font-size: 1em;
+}
+.ui-widget-content {
+	border: 1px solid #aaaaaa;
+	background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;
+	color: #222222;
+}
+.ui-widget-content a {
+	color: #222222;
+}
+.ui-widget-header {
+	border: 1px solid #aaaaaa;
+	background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;
+	color: #222222;
+	font-weight: bold;
+}
+.ui-widget-header a {
+	color: #222222;
+}
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default,
+.ui-widget-content .ui-state-default,
+.ui-widget-header .ui-state-default {
+	border: 1px solid #d3d3d3;
+	background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;
+	font-weight: normal;
+	color: #555555;
+}
+.ui-state-default a,
+.ui-state-default a:link,
+.ui-state-default a:visited {
+	color: #555555;
+	text-decoration: none;
+}
+.ui-state-hover,
+.ui-widget-content .ui-state-hover,
+.ui-widget-header .ui-state-hover,
+.ui-state-focus,
+.ui-widget-content .ui-state-focus,
+.ui-widget-header .ui-state-focus {
+	border: 1px solid #999999;
+	background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;
+	font-weight: normal;
+	color: #212121;
+}
+.ui-state-hover a,
+.ui-state-hover a:hover,
+.ui-state-hover a:link,
+.ui-state-hover a:visited,
+.ui-state-focus a,
+.ui-state-focus a:hover,
+.ui-state-focus a:link,
+.ui-state-focus a:visited {
+	color: #212121;
+	text-decoration: none;
+}
+.ui-state-active,
+.ui-widget-content .ui-state-active,
+.ui-widget-header .ui-state-active {
+	border: 1px solid #aaaaaa;
+	background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;
+	font-weight: normal;
+	color: #212121;
+}
+.ui-state-active a,
+.ui-state-active a:link,
+.ui-state-active a:visited {
+	color: #212121;
+	text-decoration: none;
+}
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight,
+.ui-widget-content .ui-state-highlight,
+.ui-widget-header .ui-state-highlight {
+	border: 1px solid #fcefa1;
+	background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;
+	color: #363636;
+}
+.ui-state-highlight a,
+.ui-widget-content .ui-state-highlight a,
+.ui-widget-header .ui-state-highlight a {
+	color: #363636;
+}
+.ui-state-error,
+.ui-widget-content .ui-state-error,
+.ui-widget-header .ui-state-error {
+	border: 1px solid #cd0a0a;
+	background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;
+	color: #cd0a0a;
+}
+.ui-state-error a,
+.ui-widget-content .ui-state-error a,
+.ui-widget-header .ui-state-error a {
+	color: #cd0a0a;
+}
+.ui-state-error-text,
+.ui-widget-content .ui-state-error-text,
+.ui-widget-header .ui-state-error-text {
+	color: #cd0a0a;
+}
+.ui-priority-primary,
+.ui-widget-content .ui-priority-primary,
+.ui-widget-header .ui-priority-primary {
+	font-weight: bold;
+}
+.ui-priority-secondary,
+.ui-widget-content .ui-priority-secondary,
+.ui-widget-header .ui-priority-secondary {
+	opacity: .7;
+	filter:Alpha(Opacity=70);
+	font-weight: normal;
+}
+.ui-state-disabled,
+.ui-widget-content .ui-state-disabled,
+.ui-widget-header .ui-state-disabled {
+	opacity: .35;
+	filter:Alpha(Opacity=35);
+	background-image: none;
+}
+.ui-state-disabled .ui-icon {
+	filter:Alpha(Opacity=35); /* For IE8 - See #6059 */
+}
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon {
+	width: 16px;
+	height: 16px;
+}
+.ui-icon,
+.ui-widget-content .ui-icon {
+	background-image: url(images/ui-icons_222222_256x240.png);
+}
+.ui-widget-header .ui-icon {
+	background-image: url(images/ui-icons_222222_256x240.png);
+}
+.ui-state-default .ui-icon {
+	background-image: url(images/ui-icons_888888_256x240.png);
+}
+.ui-state-hover .ui-icon,
+.ui-state-focus .ui-icon {
+	background-image: url(images/ui-icons_454545_256x240.png);
+}
+.ui-state-active .ui-icon {
+	background-image: url(images/ui-icons_454545_256x240.png);
+}
+.ui-state-highlight .ui-icon {
+	background-image: url(images/ui-icons_2e83ff_256x240.png);
+}
+.ui-state-error .ui-icon,
+.ui-state-error-text .ui-icon {
+	background-image: url(images/ui-icons_cd0a0a_256x240.png);
+}
+
+/* positioning */
+.ui-icon-blank { background-position: 16px 16px; }
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-on { background-position: -96px -144px; }
+.ui-icon-radio-off { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all,
+.ui-corner-top,
+.ui-corner-left,
+.ui-corner-tl {
+	border-top-left-radius: 4px;
+}
+.ui-corner-all,
+.ui-corner-top,
+.ui-corner-right,
+.ui-corner-tr {
+	border-top-right-radius: 4px;
+}
+.ui-corner-all,
+.ui-corner-bottom,
+.ui-corner-left,
+.ui-corner-bl {
+	border-bottom-left-radius: 4px;
+}
+.ui-corner-all,
+.ui-corner-bottom,
+.ui-corner-right,
+.ui-corner-br {
+	border-bottom-right-radius: 4px;
+}
+
+/* Overlays */
+.ui-widget-overlay {
+	background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;
+	opacity: .3;
+	filter: Alpha(Opacity=30);
+}
+.ui-widget-shadow {
+	margin: -8px 0 0 -8px;
+	padding: 8px;
+	background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;
+	opacity: .3;
+	filter: Alpha(Opacity=30);
+	border-radius: 8px;
+}
diff --git a/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..172edd9917363684ae871a3010ba77de6097de8a
--- /dev/null
+++ b/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css
@@ -0,0 +1,7 @@
+/*! jQuery UI - v1.10.4 - 2014-02-10
+* http://jqueryui.com
+* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css, jquery.ui.theme.css
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin-top:2px;padding:.5em .5em .5em .7em;min-height:0}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-noicons{padding-left:.7em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:49%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:none}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;width:100%;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;min-height:0;font-weight:normal}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:normal;margin:-1px}.ui-menu .ui-state-disabled{font-weight:normal;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("images/animated-overlay.gif");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_888888_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px}
\ No newline at end of file
diff --git a/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js b/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..ce3dc7ddbd8e4e07a444176c2313ae47b7d93d37
--- /dev/null
+++ b/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js
@@ -0,0 +1,7 @@
+/*! jQuery UI - v1.10.4 - 2014-02-09
+* http://jqueryui.com
+* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.position.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.resizable.js, jquery.ui.selectable.js, jquery.ui.sortable.js, jquery.ui.accordion.js, jquery.ui.autocomplete.js, jquery.ui.button.js, jquery.ui.datepicker.js, jquery.ui.dialog.js, jquery.ui.menu.js, jquery.ui.progressbar.js, jquery.ui.slider.js, jquery.ui.spinner.js, jquery.ui.tabs.js, jquery.ui.tooltip.js, jquery.ui.effect.js, jquery.ui.effect-blind.js, jquery.ui.effect-bounce.js, jquery.ui.effect-clip.js, jquery.ui.effect-drop.js, jquery.ui.effect-explode.js, jquery.ui.effect-fade.js, jquery.ui.effect-fold.js, jquery.ui.effect-highlight.js, jquery.ui.effect-pulsate.js, jquery.ui.effect-scale.js, jquery.ui.effect-shake.js, jquery.ui.effect-slide.js, jquery.ui.effect-transfer.js
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+(function(e,t){function i(t,i){var s,a,o,r=t.nodeName.toLowerCase();return"area"===r?(s=t.parentNode,a=s.name,t.href&&a&&"map"===s.nodeName.toLowerCase()?(o=e("img[usemap=#"+a+"]")[0],!!o&&n(o)):!1):(/input|select|textarea|button|object/.test(r)?!t.disabled:"a"===r?t.href||i:i)&&n(t)}function n(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var s=0,a=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.4",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,n){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),n&&n.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var n,s,a=e(this[0]);a.length&&a[0]!==document;){if(n=a.css("position"),("absolute"===n||"relative"===n||"fixed"===n)&&(s=parseInt(a.css("zIndex"),10),!isNaN(s)&&0!==s))return s;a=a.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++s)})},removeUniqueId:function(){return this.each(function(){a.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,n){return!!e.data(t,n[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),s=isNaN(n);return(s||n>=0)&&i(t,!s)}}),e("<a>").outerWidth(1).jquery||e.each(["Width","Height"],function(i,n){function s(t,i,n,s){return e.each(a,function(){i-=parseFloat(e.css(t,"padding"+this))||0,n&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var a="Width"===n?["Left","Right"]:["Top","Bottom"],o=n.toLowerCase(),r={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+n]=function(i){return i===t?r["inner"+n].call(this):this.each(function(){e(this).css(o,s(this,i)+"px")})},e.fn["outer"+n]=function(t,i){return"number"!=typeof t?r["outer"+n].call(this,t):this.each(function(){e(this).css(o,s(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("<a>").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,n){var s,a=e.ui[t].prototype;for(s in n)a.plugins[s]=a.plugins[s]||[],a.plugins[s].push([i,n[s]])},call:function(e,t,i){var n,s=e.plugins[t];if(s&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(n=0;s.length>n;n++)e.options[s[n][0]]&&s[n][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var n=i&&"left"===i?"scrollLeft":"scrollTop",s=!1;return t[n]>0?!0:(t[n]=1,s=t[n]>0,t[n]=0,s)}})})(jQuery);(function(t,e){var i=0,s=Array.prototype.slice,n=t.cleanData;t.cleanData=function(e){for(var i,s=0;null!=(i=e[s]);s++)try{t(i).triggerHandler("remove")}catch(o){}n(e)},t.widget=function(i,s,n){var o,a,r,h,l={},c=i.split(".")[0];i=i.split(".")[1],o=c+"-"+i,n||(n=s,s=t.Widget),t.expr[":"][o.toLowerCase()]=function(e){return!!t.data(e,o)},t[c]=t[c]||{},a=t[c][i],r=t[c][i]=function(t,i){return this._createWidget?(arguments.length&&this._createWidget(t,i),e):new r(t,i)},t.extend(r,a,{version:n.version,_proto:t.extend({},n),_childConstructors:[]}),h=new s,h.options=t.widget.extend({},h.options),t.each(n,function(i,n){return t.isFunction(n)?(l[i]=function(){var t=function(){return s.prototype[i].apply(this,arguments)},e=function(t){return s.prototype[i].apply(this,t)};return function(){var i,s=this._super,o=this._superApply;return this._super=t,this._superApply=e,i=n.apply(this,arguments),this._super=s,this._superApply=o,i}}(),e):(l[i]=n,e)}),r.prototype=t.widget.extend(h,{widgetEventPrefix:a?h.widgetEventPrefix||i:i},l,{constructor:r,namespace:c,widgetName:i,widgetFullName:o}),a?(t.each(a._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,r,i._proto)}),delete a._childConstructors):s._childConstructors.push(r),t.widget.bridge(i,r)},t.widget.extend=function(i){for(var n,o,a=s.call(arguments,1),r=0,h=a.length;h>r;r++)for(n in a[r])o=a[r][n],a[r].hasOwnProperty(n)&&o!==e&&(i[n]=t.isPlainObject(o)?t.isPlainObject(i[n])?t.widget.extend({},i[n],o):t.widget.extend({},o):o);return i},t.widget.bridge=function(i,n){var o=n.prototype.widgetFullName||i;t.fn[i]=function(a){var r="string"==typeof a,h=s.call(arguments,1),l=this;return a=!r&&h.length?t.widget.extend.apply(null,[a].concat(h)):a,r?this.each(function(){var s,n=t.data(this,o);return n?t.isFunction(n[a])&&"_"!==a.charAt(0)?(s=n[a].apply(n,h),s!==n&&s!==e?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):e):t.error("no such method '"+a+"' for "+i+" widget instance"):t.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var e=t.data(this,o);e?e.option(a||{})._init():t.data(this,o,new n(a,this))}),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this.bindings=t(),this.hoverable=t(),this.focusable=t(),s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(i,s){var n,o,a,r=i;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof i)if(r={},n=i.split("."),i=n.shift(),n.length){for(o=r[i]=t.widget.extend({},this.options[i]),a=0;n.length-1>a;a++)o[n[a]]=o[n[a]]||{},o=o[n[a]];if(i=n.pop(),1===arguments.length)return o[i]===e?null:o[i];o[i]=s}else{if(1===arguments.length)return this.options[i]===e?null:this.options[i];r[i]=s}return this._setOptions(r),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!e).attr("aria-disabled",e),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var o,a=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=o=t(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,o=this.widget()),t.each(n,function(n,r){function h(){return i||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof r?a[r]:r).apply(a,arguments):e}"string"!=typeof r&&(h.guid=r.guid=r.guid||h.guid||t.guid++);var l=n.match(/^(\w+)\s*(.*)$/),c=l[1]+a.eventNamespace,u=l[2];u?o.delegate(u,c,h):s.bind(c,h)})},_off:function(t,e){e=(e||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(e).undelegate(e)},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}})})(jQuery);(function(t){var e=!1;t(document).mouseup(function(){e=!1}),t.widget("ui.mouse",{version:"1.10.4",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.bind("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).bind("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):undefined}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(i){if(!e){this._mouseStarted&&this._mouseUp(i),this._mouseDownEvent=i;var s=this,n=1===i.which,a="string"==typeof this.options.cancel&&i.target.nodeName?t(i.target).closest(this.options.cancel).length:!1;return n&&!a&&this._mouseCapture(i)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){s.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(i)&&this._mouseDelayMet(i)&&(this._mouseStarted=this._mouseStart(i)!==!1,!this._mouseStarted)?(i.preventDefault(),!0):(!0===t.data(i.target,this.widgetName+".preventClickEvent")&&t.removeData(i.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return s._mouseMove(t)},this._mouseUpDelegate=function(t){return s._mouseUp(t)},t(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),i.preventDefault(),e=!0,!0)):!0}},_mouseMove:function(e){return t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button?this._mouseUp(e):this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){return t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),!1},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}})})(jQuery);(function(t,e){function i(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function s(e,i){return parseInt(t.css(e,i),10)||0}function n(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var a,o=Math.max,r=Math.abs,l=Math.round,h=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(a!==e)return a;var i,s,n=t("<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>"),o=n.children()[0];return t("body").append(n),i=o.offsetWidth,n.css("overflow","scroll"),s=o.offsetWidth,i===s&&(s=n[0].clientWidth),n.remove(),a=i-s},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.width<e.element[0].scrollWidth,a="scroll"===s||"auto"===s&&e.height<e.element[0].scrollHeight;return{width:a?t.position.scrollbarWidth():0,height:n?t.position.scrollbarWidth():0}},getWithinInfo:function(e){var i=t(e||window),s=t.isWindow(i[0]),n=!!i[0]&&9===i[0].nodeType;return{element:i,isWindow:s,isDocument:n,offset:i.offset()||{left:0,top:0},scrollLeft:i.scrollLeft(),scrollTop:i.scrollTop(),width:s?i.width():i.outerWidth(),height:s?i.height():i.outerHeight()}}},t.fn.position=function(e){if(!e||!e.of)return f.apply(this,arguments);e=t.extend({},e);var a,p,g,m,v,_,b=t(e.of),y=t.position.getWithinInfo(e.within),k=t.position.getScrollInfo(y),w=(e.collision||"flip").split(" "),D={};return _=n(b),b[0].preventDefault&&(e.at="left top"),p=_.width,g=_.height,m=_.offset,v=t.extend({},m),t.each(["my","at"],function(){var t,i,s=(e[this]||"").split(" ");1===s.length&&(s=h.test(s[0])?s.concat(["center"]):c.test(s[0])?["center"].concat(s):["center","center"]),s[0]=h.test(s[0])?s[0]:"center",s[1]=c.test(s[1])?s[1]:"center",t=u.exec(s[0]),i=u.exec(s[1]),D[this]=[t?t[0]:0,i?i[0]:0],e[this]=[d.exec(s[0])[0],d.exec(s[1])[0]]}),1===w.length&&(w[1]=w[0]),"right"===e.at[0]?v.left+=p:"center"===e.at[0]&&(v.left+=p/2),"bottom"===e.at[1]?v.top+=g:"center"===e.at[1]&&(v.top+=g/2),a=i(D.at,p,g),v.left+=a[0],v.top+=a[1],this.each(function(){var n,h,c=t(this),u=c.outerWidth(),d=c.outerHeight(),f=s(this,"marginLeft"),_=s(this,"marginTop"),x=u+f+s(this,"marginRight")+k.width,C=d+_+s(this,"marginBottom")+k.height,M=t.extend({},v),T=i(D.my,c.outerWidth(),c.outerHeight());"right"===e.my[0]?M.left-=u:"center"===e.my[0]&&(M.left-=u/2),"bottom"===e.my[1]?M.top-=d:"center"===e.my[1]&&(M.top-=d/2),M.left+=T[0],M.top+=T[1],t.support.offsetFractions||(M.left=l(M.left),M.top=l(M.top)),n={marginLeft:f,marginTop:_},t.each(["left","top"],function(i,s){t.ui.position[w[i]]&&t.ui.position[w[i]][s](M,{targetWidth:p,targetHeight:g,elemWidth:u,elemHeight:d,collisionPosition:n,collisionWidth:x,collisionHeight:C,offset:[a[0]+T[0],a[1]+T[1]],my:e.my,at:e.at,within:y,elem:c})}),e.using&&(h=function(t){var i=m.left-M.left,s=i+p-u,n=m.top-M.top,a=n+g-d,l={target:{element:b,left:m.left,top:m.top,width:p,height:g},element:{element:c,left:M.left,top:M.top,width:u,height:d},horizontal:0>s?"left":i>0?"right":"center",vertical:0>a?"top":n>0?"bottom":"middle"};u>p&&p>r(i+s)&&(l.horizontal="center"),d>g&&g>r(n+a)&&(l.vertical="middle"),l.important=o(r(i),r(s))>o(r(n),r(a))?"horizontal":"vertical",e.using.call(this,t,l)}),c.offset(t.extend(M,{using:h}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-a-n;e.collisionWidth>a?l>0&&0>=h?(i=t.left+l+e.collisionWidth-a-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+a-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-a-n;e.collisionHeight>a?l>0&&0>=h?(i=t.top+l+e.collisionHeight-a-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+a-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,a=n.offset.left+n.scrollLeft,o=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-o-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-o-a,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,a=n.offset.top+n.scrollTop,o=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-o-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-o-a,t.top+p+f+g>c&&(0>s||r(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,t.top+p+f+g>u&&(i>0||u>r(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}},function(){var e,i,s,n,a,o=document.getElementsByTagName("body")[0],r=document.createElement("div");e=document.createElement(o?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&t.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(a in s)e.style[a]=s[a];e.appendChild(r),i=o||document.documentElement,i.insertBefore(e,i.firstChild),r.style.cssText="position: absolute; left: 10.7432222px;",n=t(r).offset().left,t.support.offsetFractions=n>10&&11>n,e.innerHTML="",i.removeChild(e)}()})(jQuery);(function(t){t.widget("ui.draggable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"!==this.options.helper||/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},_destroy:function(){this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy()},_mouseCapture:function(e){var i=this.options;return this.helper||i.disabled||t(e.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(e),this.handle?(t(i.iframeFix===!0?"iframe":i.iframeFix).each(function(){t("<div class='ui-draggable-iframeFix' style='background: #fff;'></div>").css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(t(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(e){var i=this.options;return this.helper=this._createHelper(e),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),t.ui.ddmanager&&(t.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offsetParent=this.helper.offsetParent(),this.offsetParentCssPosition=this.offsetParent.css("position"),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},this.offset.scroll=!1,t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",e)===!1?(this._clear(),!1):(this._cacheHelperProportions(),t.ui.ddmanager&&!i.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this._mouseDrag(e,!0),t.ui.ddmanager&&t.ui.ddmanager.dragStart(this,e),!0)},_mouseDrag:function(e,i){if("fixed"===this.offsetParentCssPosition&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",e,s)===!1)return this._mouseUp({}),!1;this.position=s.position}return this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),!1},_mouseStop:function(e){var i=this,s=!1;return t.ui.ddmanager&&!this.options.dropBehaviour&&(s=t.ui.ddmanager.drop(this,e)),this.dropped&&(s=this.dropped,this.dropped=!1),"original"!==this.options.helper||t.contains(this.element[0].ownerDocument,this.element[0])?("invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||t.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?t(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",e)!==!1&&i._clear()}):this._trigger("stop",e)!==!1&&this._clear(),!1):!1},_mouseUp:function(e){return t("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),t.ui.ddmanager&&t.ui.ddmanager.dragStop(this,e),t.ui.mouse.prototype._mouseUp.call(this,e)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(e){return this.options.handle?!!t(e.target).closest(this.element.find(this.options.handle)).length:!0},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return s.parents("body").length||s.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s[0]===this.element[0]||/(fixed|absolute)/.test(s.css("position"))||s.css("position","absolute"),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.element.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;return n.containment?"window"===n.containment?(this.containment=[t(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,t(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,t(window).scrollLeft()+t(window).width()-this.helperProportions.width-this.margins.left,t(window).scrollTop()+(t(window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):"document"===n.containment?(this.containment=[0,0,t(document).width()-this.helperProportions.width-this.margins.left,(t(document).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):n.containment.constructor===Array?(this.containment=n.containment,undefined):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=t(n.containment),s=i[0],s&&(e="hidden"!==i.css("overflow"),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(e?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(e?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=i),undefined):(this.containment=null,undefined)},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent;return this.offset.scroll||(this.offset.scroll={top:n.scrollTop(),left:n.scrollLeft()}),{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top)*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)*s}},_generatePosition:function(e){var i,s,n,a,o=this.options,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,l=e.pageX,h=e.pageY;return this.offset.scroll||(this.offset.scroll={top:r.scrollTop(),left:r.scrollLeft()}),this.originalPosition&&(this.containment&&(this.relative_container?(s=this.relative_container.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,e.pageX-this.offset.click.left<i[0]&&(l=i[0]+this.offset.click.left),e.pageY-this.offset.click.top<i[1]&&(h=i[1]+this.offset.click.top),e.pageX-this.offset.click.left>i[2]&&(l=i[2]+this.offset.click.left),e.pageY-this.offset.click.top>i[3]&&(h=i[3]+this.offset.click.top)),o.grid&&(n=o.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/o.grid[1])*o.grid[1]:this.originalPageY,h=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-o.grid[1]:n+o.grid[1]:n,a=o.grid[0]?this.originalPageX+Math.round((l-this.originalPageX)/o.grid[0])*o.grid[0]:this.originalPageX,l=i?a-this.offset.click.left>=i[0]||a-this.offset.click.left>i[2]?a:a-this.offset.click.left>=i[0]?a-o.grid[0]:a+o.grid[0]:a)),{top:h-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top),left:l-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(e,i,s){return s=s||this._uiHash(),t.ui.plugin.call(this,e,[i,s]),"drag"===e&&(this.positionAbs=this._convertPositionTo("absolute")),t.Widget.prototype._trigger.call(this,e,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),t.ui.plugin.add("draggable","connectToSortable",{start:function(e,i){var s=t(this).data("ui-draggable"),n=s.options,a=t.extend({},i,{item:s.element});s.sortables=[],t(n.connectToSortable).each(function(){var i=t.data(this,"ui-sortable");i&&!i.options.disabled&&(s.sortables.push({instance:i,shouldRevert:i.options.revert}),i.refreshPositions(),i._trigger("activate",e,a))})},stop:function(e,i){var s=t(this).data("ui-draggable"),n=t.extend({},i,{item:s.element});t.each(s.sortables,function(){this.instance.isOver?(this.instance.isOver=0,s.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=this.shouldRevert),this.instance._mouseStop(e),this.instance.options.helper=this.instance.options._helper,"original"===s.options.helper&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",e,n))})},drag:function(e,i){var s=t(this).data("ui-draggable"),n=this;t.each(s.sortables,function(){var a=!1,o=this;this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this.instance._intersectsWith(this.instance.containerCache)&&(a=!0,t.each(s.sortables,function(){return this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this!==o&&this.instance._intersectsWith(this.instance.containerCache)&&t.contains(o.instance.element[0],this.instance.element[0])&&(a=!1),a})),a?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=t(n).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item",!0),this.instance.options._helper=this.instance.options.helper,this.instance.options.helper=function(){return i.helper[0]},e.target=this.instance.currentItem[0],this.instance._mouseCapture(e,!0),this.instance._mouseStart(e,!0,!0),this.instance.offset.click.top=s.offset.click.top,this.instance.offset.click.left=s.offset.click.left,this.instance.offset.parent.left-=s.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=s.offset.parent.top-this.instance.offset.parent.top,s._trigger("toSortable",e),s.dropped=this.instance.element,s.currentItem=s.element,this.instance.fromOutside=s),this.instance.currentItem&&this.instance._mouseDrag(e)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",e,this.instance._uiHash(this.instance)),this.instance._mouseStop(e,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeholder.remove(),s._trigger("fromSortable",e),s.dropped=!1)})}}),t.ui.plugin.add("draggable","cursor",{start:function(){var e=t("body"),i=t(this).data("ui-draggable").options;e.css("cursor")&&(i._cursor=e.css("cursor")),e.css("cursor",i.cursor)},stop:function(){var e=t(this).data("ui-draggable").options;e._cursor&&t("body").css("cursor",e._cursor)}}),t.ui.plugin.add("draggable","opacity",{start:function(e,i){var s=t(i.helper),n=t(this).data("ui-draggable").options;s.css("opacity")&&(n._opacity=s.css("opacity")),s.css("opacity",n.opacity)},stop:function(e,i){var s=t(this).data("ui-draggable").options;s._opacity&&t(i.helper).css("opacity",s._opacity)}}),t.ui.plugin.add("draggable","scroll",{start:function(){var e=t(this).data("ui-draggable");e.scrollParent[0]!==document&&"HTML"!==e.scrollParent[0].tagName&&(e.overflowOffset=e.scrollParent.offset())},drag:function(e){var i=t(this).data("ui-draggable"),s=i.options,n=!1;i.scrollParent[0]!==document&&"HTML"!==i.scrollParent[0].tagName?(s.axis&&"x"===s.axis||(i.overflowOffset.top+i.scrollParent[0].offsetHeight-e.pageY<s.scrollSensitivity?i.scrollParent[0].scrollTop=n=i.scrollParent[0].scrollTop+s.scrollSpeed:e.pageY-i.overflowOffset.top<s.scrollSensitivity&&(i.scrollParent[0].scrollTop=n=i.scrollParent[0].scrollTop-s.scrollSpeed)),s.axis&&"y"===s.axis||(i.overflowOffset.left+i.scrollParent[0].offsetWidth-e.pageX<s.scrollSensitivity?i.scrollParent[0].scrollLeft=n=i.scrollParent[0].scrollLeft+s.scrollSpeed:e.pageX-i.overflowOffset.left<s.scrollSensitivity&&(i.scrollParent[0].scrollLeft=n=i.scrollParent[0].scrollLeft-s.scrollSpeed))):(s.axis&&"x"===s.axis||(e.pageY-t(document).scrollTop()<s.scrollSensitivity?n=t(document).scrollTop(t(document).scrollTop()-s.scrollSpeed):t(window).height()-(e.pageY-t(document).scrollTop())<s.scrollSensitivity&&(n=t(document).scrollTop(t(document).scrollTop()+s.scrollSpeed))),s.axis&&"y"===s.axis||(e.pageX-t(document).scrollLeft()<s.scrollSensitivity?n=t(document).scrollLeft(t(document).scrollLeft()-s.scrollSpeed):t(window).width()-(e.pageX-t(document).scrollLeft())<s.scrollSensitivity&&(n=t(document).scrollLeft(t(document).scrollLeft()+s.scrollSpeed)))),n!==!1&&t.ui.ddmanager&&!s.dropBehaviour&&t.ui.ddmanager.prepareOffsets(i,e)}}),t.ui.plugin.add("draggable","snap",{start:function(){var e=t(this).data("ui-draggable"),i=e.options;e.snapElements=[],t(i.snap.constructor!==String?i.snap.items||":data(ui-draggable)":i.snap).each(function(){var i=t(this),s=i.offset();this!==e.element[0]&&e.snapElements.push({item:this,width:i.outerWidth(),height:i.outerHeight(),top:s.top,left:s.left})})},drag:function(e,i){var s,n,a,o,r,l,h,c,u,d,p=t(this).data("ui-draggable"),g=p.options,f=g.snapTolerance,m=i.offset.left,_=m+p.helperProportions.width,v=i.offset.top,b=v+p.helperProportions.height;for(u=p.snapElements.length-1;u>=0;u--)r=p.snapElements[u].left,l=r+p.snapElements[u].width,h=p.snapElements[u].top,c=h+p.snapElements[u].height,r-f>_||m>l+f||h-f>b||v>c+f||!t.contains(p.snapElements[u].item.ownerDocument,p.snapElements[u].item)?(p.snapElements[u].snapping&&p.options.snap.release&&p.options.snap.release.call(p.element,e,t.extend(p._uiHash(),{snapItem:p.snapElements[u].item})),p.snapElements[u].snapping=!1):("inner"!==g.snapMode&&(s=f>=Math.abs(h-b),n=f>=Math.abs(c-v),a=f>=Math.abs(r-_),o=f>=Math.abs(l-m),s&&(i.position.top=p._convertPositionTo("relative",{top:h-p.helperProportions.height,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:c,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r-p.helperProportions.width}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:l}).left-p.margins.left)),d=s||n||a||o,"outer"!==g.snapMode&&(s=f>=Math.abs(h-v),n=f>=Math.abs(c-b),a=f>=Math.abs(r-m),o=f>=Math.abs(l-_),s&&(i.position.top=p._convertPositionTo("relative",{top:h,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:c-p.helperProportions.height,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:l-p.helperProportions.width}).left-p.margins.left)),!p.snapElements[u].snapping&&(s||n||a||o||d)&&p.options.snap.snap&&p.options.snap.snap.call(p.element,e,t.extend(p._uiHash(),{snapItem:p.snapElements[u].item})),p.snapElements[u].snapping=s||n||a||o||d)}}),t.ui.plugin.add("draggable","stack",{start:function(){var e,i=this.data("ui-draggable").options,s=t.makeArray(t(i.stack)).sort(function(e,i){return(parseInt(t(e).css("zIndex"),10)||0)-(parseInt(t(i).css("zIndex"),10)||0)});s.length&&(e=parseInt(t(s[0]).css("zIndex"),10)||0,t(s).each(function(i){t(this).css("zIndex",e+i)}),this.css("zIndex",e+s.length))}}),t.ui.plugin.add("draggable","zIndex",{start:function(e,i){var s=t(i.helper),n=t(this).data("ui-draggable").options;s.css("zIndex")&&(n._zIndex=s.css("zIndex")),s.css("zIndex",n.zIndex)},stop:function(e,i){var s=t(this).data("ui-draggable").options;s._zIndex&&t(i.helper).css("zIndex",s._zIndex)}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}t.widget("ui.droppable",{version:"1.10.4",widgetEventPrefix:"drop",options:{accept:"*",activeClass:!1,addClasses:!0,greedy:!1,hoverClass:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var e,i=this.options,s=i.accept;this.isover=!1,this.isout=!0,this.accept=t.isFunction(s)?s:function(t){return t.is(s)},this.proportions=function(){return arguments.length?(e=arguments[0],undefined):e?e:e={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight}},t.ui.ddmanager.droppables[i.scope]=t.ui.ddmanager.droppables[i.scope]||[],t.ui.ddmanager.droppables[i.scope].push(this),i.addClasses&&this.element.addClass("ui-droppable")},_destroy:function(){for(var e=0,i=t.ui.ddmanager.droppables[this.options.scope];i.length>e;e++)i[e]===this&&i.splice(e,1);this.element.removeClass("ui-droppable ui-droppable-disabled")},_setOption:function(e,i){"accept"===e&&(this.accept=t.isFunction(i)?i:function(t){return t.is(i)}),t.Widget.prototype._setOption.apply(this,arguments)},_activate:function(e){var i=t.ui.ddmanager.current;this.options.activeClass&&this.element.addClass(this.options.activeClass),i&&this._trigger("activate",e,this.ui(i))},_deactivate:function(e){var i=t.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass),i&&this._trigger("deactivate",e,this.ui(i))},_over:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.addClass(this.options.hoverClass),this._trigger("over",e,this.ui(i)))},_out:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("out",e,this.ui(i)))},_drop:function(e,i){var s=i||t.ui.ddmanager.current,n=!1;return s&&(s.currentItem||s.element)[0]!==this.element[0]?(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var e=t.data(this,"ui-droppable");return e.options.greedy&&!e.options.disabled&&e.options.scope===s.options.scope&&e.accept.call(e.element[0],s.currentItem||s.element)&&t.ui.intersect(s,t.extend(e,{offset:e.element.offset()}),e.options.tolerance)?(n=!0,!1):undefined}),n?!1:this.accept.call(this.element[0],s.currentItem||s.element)?(this.options.activeClass&&this.element.removeClass(this.options.activeClass),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("drop",e,this.ui(s)),this.element):!1):!1},ui:function(t){return{draggable:t.currentItem||t.element,helper:t.helper,position:t.position,offset:t.positionAbs}}}),t.ui.intersect=function(t,i,s){if(!i.offset)return!1;var n,a,o=(t.positionAbs||t.position.absolute).left,r=(t.positionAbs||t.position.absolute).top,l=o+t.helperProportions.width,h=r+t.helperProportions.height,c=i.offset.left,u=i.offset.top,d=c+i.proportions().width,p=u+i.proportions().height;switch(s){case"fit":return o>=c&&d>=l&&r>=u&&p>=h;case"intersect":return o+t.helperProportions.width/2>c&&d>l-t.helperProportions.width/2&&r+t.helperProportions.height/2>u&&p>h-t.helperProportions.height/2;case"pointer":return n=(t.positionAbs||t.position.absolute).left+(t.clickOffset||t.offset.click).left,a=(t.positionAbs||t.position.absolute).top+(t.clickOffset||t.offset.click).top,e(a,u,i.proportions().height)&&e(n,c,i.proportions().width);case"touch":return(r>=u&&p>=r||h>=u&&p>=h||u>r&&h>p)&&(o>=c&&d>=o||l>=c&&d>=l||c>o&&l>d);default:return!1}},t.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,i){var s,n,a=t.ui.ddmanager.droppables[e.options.scope]||[],o=i?i.type:null,r=(e.currentItem||e.element).find(":data(ui-droppable)").addBack();t:for(s=0;a.length>s;s++)if(!(a[s].options.disabled||e&&!a[s].accept.call(a[s].element[0],e.currentItem||e.element))){for(n=0;r.length>n;n++)if(r[n]===a[s].element[0]){a[s].proportions().height=0;continue t}a[s].visible="none"!==a[s].element.css("display"),a[s].visible&&("mousedown"===o&&a[s]._activate.call(a[s],i),a[s].offset=a[s].element.offset(),a[s].proportions({width:a[s].element[0].offsetWidth,height:a[s].element[0].offsetHeight}))}},drop:function(e,i){var s=!1;return t.each((t.ui.ddmanager.droppables[e.options.scope]||[]).slice(),function(){this.options&&(!this.options.disabled&&this.visible&&t.ui.intersect(e,this,this.options.tolerance)&&(s=this._drop.call(this,i)||s),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],e.currentItem||e.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,i)))}),s},dragStart:function(e,i){e.element.parentsUntil("body").bind("scroll.droppable",function(){e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)})},drag:function(e,i){e.options.refreshPositions&&t.ui.ddmanager.prepareOffsets(e,i),t.each(t.ui.ddmanager.droppables[e.options.scope]||[],function(){if(!this.options.disabled&&!this.greedyChild&&this.visible){var s,n,a,o=t.ui.intersect(e,this,this.options.tolerance),r=!o&&this.isover?"isout":o&&!this.isover?"isover":null;r&&(this.options.greedy&&(n=this.options.scope,a=this.element.parents(":data(ui-droppable)").filter(function(){return t.data(this,"ui-droppable").options.scope===n}),a.length&&(s=t.data(a[0],"ui-droppable"),s.greedyChild="isover"===r)),s&&"isover"===r&&(s.isover=!1,s.isout=!0,s._out.call(s,i)),this[r]=!0,this["isout"===r?"isover":"isout"]=!1,this["isover"===r?"_over":"_out"].call(this,i),s&&"isout"===r&&(s.isout=!1,s.isover=!0,s._over.call(s,i)))}})},dragStop:function(e,i){e.element.parentsUntil("body").unbind("scroll.droppable"),e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)}}})(jQuery);(function(t){function e(t){return parseInt(t,10)||0}function i(t){return!isNaN(parseInt(t,10))}t.widget("ui.resizable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_create:function(){var e,i,s,n,a,o=this,r=this.options;if(this.element.addClass("ui-resizable"),t.extend(this,{_aspectRatio:!!r.aspectRatio,aspectRatio:r.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:r.helper||r.ghost||r.animate?r.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(t("<div class='ui-wrapper' style='overflow: hidden;'></div>").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.data("ui-resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=r.handles||(t(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),e=this.handles.split(","),this.handles={},i=0;e.length>i;i++)s=t.trim(e[i]),a="ui-resizable-"+s,n=t("<div class='ui-resizable-handle "+a+"'></div>"),n.css({zIndex:r.zIndex}),"se"===s&&n.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[s]=".ui-resizable-"+s,this.element.append(n);this._renderAxis=function(e){var i,s,n,a;e=e||this.element;for(i in this.handles)this.handles[i].constructor===String&&(this.handles[i]=t(this.handles[i],this.element).show()),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)&&(s=t(this.handles[i],this.element),a=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),e.css(n,a),this._proportionallyResize()),t(this.handles[i]).length},this._renderAxis(this.element),this._handles=t(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){o.resizing||(this.className&&(n=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),o.axis=n&&n[1]?n[1]:"se")}),r.autoHide&&(this._handles.hide(),t(this.element).addClass("ui-resizable-autohide").mouseenter(function(){r.disabled||(t(this).removeClass("ui-resizable-autohide"),o._handles.show())}).mouseleave(function(){r.disabled||o.resizing||(t(this).addClass("ui-resizable-autohide"),o._handles.hide())})),this._mouseInit()},_destroy:function(){this._mouseDestroy();var e,i=function(e){t(e).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_mouseCapture:function(e){var i,s,n=!1;for(i in this.handles)s=t(this.handles[i])[0],(s===e.target||t.contains(s,e.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(i){var s,n,a,o=this.options,r=this.element.position(),h=this.element;return this.resizing=!0,/absolute/.test(h.css("position"))?h.css({position:"absolute",top:h.css("top"),left:h.css("left")}):h.is(".ui-draggable")&&h.css({position:"absolute",top:r.top,left:r.left}),this._renderProxy(),s=e(this.helper.css("left")),n=e(this.helper.css("top")),o.containment&&(s+=t(o.containment).scrollLeft()||0,n+=t(o.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:s,top:n},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:h.width(),height:h.height()},this.originalSize=this._helper?{width:h.outerWidth(),height:h.outerHeight()}:{width:h.width(),height:h.height()},this.originalPosition={left:s,top:n},this.sizeDiff={width:h.outerWidth()-h.width(),height:h.outerHeight()-h.height()},this.originalMousePosition={left:i.pageX,top:i.pageY},this.aspectRatio="number"==typeof o.aspectRatio?o.aspectRatio:this.originalSize.width/this.originalSize.height||1,a=t(".ui-resizable-"+this.axis).css("cursor"),t("body").css("cursor","auto"===a?this.axis+"-resize":a),h.addClass("ui-resizable-resizing"),this._propagate("start",i),!0},_mouseDrag:function(e){var i,s=this.helper,n={},a=this.originalMousePosition,o=this.axis,r=this.position.top,h=this.position.left,l=this.size.width,c=this.size.height,u=e.pageX-a.left||0,d=e.pageY-a.top||0,p=this._change[o];return p?(i=p.apply(this,[e,u,d]),this._updateVirtualBoundaries(e.shiftKey),(this._aspectRatio||e.shiftKey)&&(i=this._updateRatio(i,e)),i=this._respectSize(i,e),this._updateCache(i),this._propagate("resize",e),this.position.top!==r&&(n.top=this.position.top+"px"),this.position.left!==h&&(n.left=this.position.left+"px"),this.size.width!==l&&(n.width=this.size.width+"px"),this.size.height!==c&&(n.height=this.size.height+"px"),s.css(n),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),t.isEmptyObject(n)||this._trigger("resize",e,this.ui()),!1):!1},_mouseStop:function(e){this.resizing=!1;var i,s,n,a,o,r,h,l=this.options,c=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&t.ui.hasScroll(i[0],"left")?0:c.sizeDiff.height,a=s?0:c.sizeDiff.width,o={width:c.helper.width()-a,height:c.helper.height()-n},r=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null,h=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null,l.animate||this.element.css(t.extend(o,{top:h,left:r})),c.helper.height(c.size.height),c.helper.width(c.size.width),this._helper&&!l.animate&&this._proportionallyResize()),t("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resizing"),this._propagate("stop",e),this._helper&&this.helper.remove(),!1},_updateVirtualBoundaries:function(t){var e,s,n,a,o,r=this.options;o={minWidth:i(r.minWidth)?r.minWidth:0,maxWidth:i(r.maxWidth)?r.maxWidth:1/0,minHeight:i(r.minHeight)?r.minHeight:0,maxHeight:i(r.maxHeight)?r.maxHeight:1/0},(this._aspectRatio||t)&&(e=o.minHeight*this.aspectRatio,n=o.minWidth/this.aspectRatio,s=o.maxHeight*this.aspectRatio,a=o.maxWidth/this.aspectRatio,e>o.minWidth&&(o.minWidth=e),n>o.minHeight&&(o.minHeight=n),o.maxWidth>s&&(o.maxWidth=s),o.maxHeight>a&&(o.maxHeight=a)),this._vBoundaries=o},_updateCache:function(t){this.offset=this.helper.offset(),i(t.left)&&(this.position.left=t.left),i(t.top)&&(this.position.top=t.top),i(t.height)&&(this.size.height=t.height),i(t.width)&&(this.size.width=t.width)},_updateRatio:function(t){var e=this.position,s=this.size,n=this.axis;return i(t.height)?t.width=t.height*this.aspectRatio:i(t.width)&&(t.height=t.width/this.aspectRatio),"sw"===n&&(t.left=e.left+(s.width-t.width),t.top=null),"nw"===n&&(t.top=e.top+(s.height-t.height),t.left=e.left+(s.width-t.width)),t},_respectSize:function(t){var e=this._vBoundaries,s=this.axis,n=i(t.width)&&e.maxWidth&&e.maxWidth<t.width,a=i(t.height)&&e.maxHeight&&e.maxHeight<t.height,o=i(t.width)&&e.minWidth&&e.minWidth>t.width,r=i(t.height)&&e.minHeight&&e.minHeight>t.height,h=this.originalPosition.left+this.originalSize.width,l=this.position.top+this.size.height,c=/sw|nw|w/.test(s),u=/nw|ne|n/.test(s);return o&&(t.width=e.minWidth),r&&(t.height=e.minHeight),n&&(t.width=e.maxWidth),a&&(t.height=e.maxHeight),o&&c&&(t.left=h-e.minWidth),n&&c&&(t.left=h-e.maxWidth),r&&u&&(t.top=l-e.minHeight),a&&u&&(t.top=l-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_proportionallyResize:function(){if(this._proportionallyResizeElements.length){var t,e,i,s,n,a=this.helper||this.element;for(t=0;this._proportionallyResizeElements.length>t;t++){if(n=this._proportionallyResizeElements[t],!this.borderDif)for(this.borderDif=[],i=[n.css("borderTopWidth"),n.css("borderRightWidth"),n.css("borderBottomWidth"),n.css("borderLeftWidth")],s=[n.css("paddingTop"),n.css("paddingRight"),n.css("paddingBottom"),n.css("paddingLeft")],e=0;i.length>e;e++)this.borderDif[e]=(parseInt(i[e],10)||0)+(parseInt(s[e],10)||0);n.css({height:a.height()-this.borderDif[0]-this.borderDif[2]||0,width:a.width()-this.borderDif[1]-this.borderDif[3]||0})}}},_renderProxy:function(){var e=this.element,i=this.options;this.elementOffset=e.offset(),this._helper?(this.helper=this.helper||t("<div style='overflow:hidden;'></div>"),this.helper.addClass(this._helper).css({width:this.element.outerWidth()-1,height:this.element.outerHeight()-1,position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize,s=this.originalPosition;return{left:s.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},sw:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[e,i,s]))},ne:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},nw:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[e,i,s]))}},_propagate:function(e,i){t.ui.plugin.call(this,e,[i,this.ui()]),"resize"!==e&&this._trigger(e,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),t.ui.plugin.add("resizable","animate",{stop:function(e){var i=t(this).data("ui-resizable"),s=i.options,n=i._proportionallyResizeElements,a=n.length&&/textarea/i.test(n[0].nodeName),o=a&&t.ui.hasScroll(n[0],"left")?0:i.sizeDiff.height,r=a?0:i.sizeDiff.width,h={width:i.size.width-r,height:i.size.height-o},l=parseInt(i.element.css("left"),10)+(i.position.left-i.originalPosition.left)||null,c=parseInt(i.element.css("top"),10)+(i.position.top-i.originalPosition.top)||null;i.element.animate(t.extend(h,c&&l?{top:c,left:l}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseInt(i.element.css("width"),10),height:parseInt(i.element.css("height"),10),top:parseInt(i.element.css("top"),10),left:parseInt(i.element.css("left"),10)};n&&n.length&&t(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",e)}})}}),t.ui.plugin.add("resizable","containment",{start:function(){var i,s,n,a,o,r,h,l=t(this).data("ui-resizable"),c=l.options,u=l.element,d=c.containment,p=d instanceof t?d.get(0):/parent/.test(d)?u.parent().get(0):d;p&&(l.containerElement=t(p),/document/.test(d)||d===document?(l.containerOffset={left:0,top:0},l.containerPosition={left:0,top:0},l.parentData={element:t(document),left:0,top:0,width:t(document).width(),height:t(document).height()||document.body.parentNode.scrollHeight}):(i=t(p),s=[],t(["Top","Right","Left","Bottom"]).each(function(t,n){s[t]=e(i.css("padding"+n))}),l.containerOffset=i.offset(),l.containerPosition=i.position(),l.containerSize={height:i.innerHeight()-s[3],width:i.innerWidth()-s[1]},n=l.containerOffset,a=l.containerSize.height,o=l.containerSize.width,r=t.ui.hasScroll(p,"left")?p.scrollWidth:o,h=t.ui.hasScroll(p)?p.scrollHeight:a,l.parentData={element:p,left:n.left,top:n.top,width:r,height:h}))},resize:function(e){var i,s,n,a,o=t(this).data("ui-resizable"),r=o.options,h=o.containerOffset,l=o.position,c=o._aspectRatio||e.shiftKey,u={top:0,left:0},d=o.containerElement;d[0]!==document&&/static/.test(d.css("position"))&&(u=h),l.left<(o._helper?h.left:0)&&(o.size.width=o.size.width+(o._helper?o.position.left-h.left:o.position.left-u.left),c&&(o.size.height=o.size.width/o.aspectRatio),o.position.left=r.helper?h.left:0),l.top<(o._helper?h.top:0)&&(o.size.height=o.size.height+(o._helper?o.position.top-h.top:o.position.top),c&&(o.size.width=o.size.height*o.aspectRatio),o.position.top=o._helper?h.top:0),o.offset.left=o.parentData.left+o.position.left,o.offset.top=o.parentData.top+o.position.top,i=Math.abs((o._helper?o.offset.left-u.left:o.offset.left-u.left)+o.sizeDiff.width),s=Math.abs((o._helper?o.offset.top-u.top:o.offset.top-h.top)+o.sizeDiff.height),n=o.containerElement.get(0)===o.element.parent().get(0),a=/relative|absolute/.test(o.containerElement.css("position")),n&&a&&(i-=Math.abs(o.parentData.left)),i+o.size.width>=o.parentData.width&&(o.size.width=o.parentData.width-i,c&&(o.size.height=o.size.width/o.aspectRatio)),s+o.size.height>=o.parentData.height&&(o.size.height=o.parentData.height-s,c&&(o.size.width=o.size.height*o.aspectRatio))},stop:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.containerOffset,n=e.containerPosition,a=e.containerElement,o=t(e.helper),r=o.offset(),h=o.outerWidth()-e.sizeDiff.width,l=o.outerHeight()-e.sizeDiff.height;e._helper&&!i.animate&&/relative/.test(a.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l}),e._helper&&!i.animate&&/static/.test(a.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l})}}),t.ui.plugin.add("resizable","alsoResize",{start:function(){var e=t(this).data("ui-resizable"),i=e.options,s=function(e){t(e).each(function(){var e=t(this);e.data("ui-resizable-alsoresize",{width:parseInt(e.width(),10),height:parseInt(e.height(),10),left:parseInt(e.css("left"),10),top:parseInt(e.css("top"),10)})})};"object"!=typeof i.alsoResize||i.alsoResize.parentNode?s(i.alsoResize):i.alsoResize.length?(i.alsoResize=i.alsoResize[0],s(i.alsoResize)):t.each(i.alsoResize,function(t){s(t)})},resize:function(e,i){var s=t(this).data("ui-resizable"),n=s.options,a=s.originalSize,o=s.originalPosition,r={height:s.size.height-a.height||0,width:s.size.width-a.width||0,top:s.position.top-o.top||0,left:s.position.left-o.left||0},h=function(e,s){t(e).each(function(){var e=t(this),n=t(this).data("ui-resizable-alsoresize"),a={},o=s&&s.length?s:e.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];t.each(o,function(t,e){var i=(n[e]||0)+(r[e]||0);i&&i>=0&&(a[e]=i||null)}),e.css(a)})};"object"!=typeof n.alsoResize||n.alsoResize.nodeType?h(n.alsoResize):t.each(n.alsoResize,function(t,e){h(t,e)})},stop:function(){t(this).removeData("resizable-alsoresize")}}),t.ui.plugin.add("resizable","ghost",{start:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.size;e.ghost=e.originalElement.clone(),e.ghost.css({opacity:.25,display:"block",position:"relative",height:s.height,width:s.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass("string"==typeof i.ghost?i.ghost:""),e.ghost.appendTo(e.helper)},resize:function(){var e=t(this).data("ui-resizable");e.ghost&&e.ghost.css({position:"relative",height:e.size.height,width:e.size.width})},stop:function(){var e=t(this).data("ui-resizable");e.ghost&&e.helper&&e.helper.get(0).removeChild(e.ghost.get(0))}}),t.ui.plugin.add("resizable","grid",{resize:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.size,n=e.originalSize,a=e.originalPosition,o=e.axis,r="number"==typeof i.grid?[i.grid,i.grid]:i.grid,h=r[0]||1,l=r[1]||1,c=Math.round((s.width-n.width)/h)*h,u=Math.round((s.height-n.height)/l)*l,d=n.width+c,p=n.height+u,f=i.maxWidth&&d>i.maxWidth,g=i.maxHeight&&p>i.maxHeight,m=i.minWidth&&i.minWidth>d,v=i.minHeight&&i.minHeight>p;i.grid=r,m&&(d+=h),v&&(p+=l),f&&(d-=h),g&&(p-=l),/^(se|s|e)$/.test(o)?(e.size.width=d,e.size.height=p):/^(ne)$/.test(o)?(e.size.width=d,e.size.height=p,e.position.top=a.top-u):/^(sw)$/.test(o)?(e.size.width=d,e.size.height=p,e.position.left=a.left-c):(p-l>0?(e.size.height=p,e.position.top=a.top-u):(e.size.height=l,e.position.top=a.top+n.height-l),d-h>0?(e.size.width=d,e.position.left=a.left-c):(e.size.width=h,e.position.left=a.left+n.width-h))}})})(jQuery);(function(t){t.widget("ui.selectable",t.ui.mouse,{version:"1.10.4",options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch",selected:null,selecting:null,start:null,stop:null,unselected:null,unselecting:null},_create:function(){var e,i=this;this.element.addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){e=t(i.options.filter,i.element[0]),e.addClass("ui-selectee"),e.each(function(){var e=t(this),i=e.offset();t.data(this,"selectable-item",{element:this,$element:e,left:i.left,top:i.top,right:i.left+e.outerWidth(),bottom:i.top+e.outerHeight(),startselected:!1,selected:e.hasClass("ui-selected"),selecting:e.hasClass("ui-selecting"),unselecting:e.hasClass("ui-unselecting")})})},this.refresh(),this.selectees=e.addClass("ui-selectee"),this._mouseInit(),this.helper=t("<div class='ui-selectable-helper'></div>")},_destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item"),this.element.removeClass("ui-selectable ui-selectable-disabled"),this._mouseDestroy()},_mouseStart:function(e){var i=this,s=this.options;this.opos=[e.pageX,e.pageY],this.options.disabled||(this.selectees=t(s.filter,this.element[0]),this._trigger("start",e),t(s.appendTo).append(this.helper),this.helper.css({left:e.pageX,top:e.pageY,width:0,height:0}),s.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var s=t.data(this,"selectable-item");s.startselected=!0,e.metaKey||e.ctrlKey||(s.$element.removeClass("ui-selected"),s.selected=!1,s.$element.addClass("ui-unselecting"),s.unselecting=!0,i._trigger("unselecting",e,{unselecting:s.element}))}),t(e.target).parents().addBack().each(function(){var s,n=t.data(this,"selectable-item");return n?(s=!e.metaKey&&!e.ctrlKey||!n.$element.hasClass("ui-selected"),n.$element.removeClass(s?"ui-unselecting":"ui-selected").addClass(s?"ui-selecting":"ui-unselecting"),n.unselecting=!s,n.selecting=s,n.selected=s,s?i._trigger("selecting",e,{selecting:n.element}):i._trigger("unselecting",e,{unselecting:n.element}),!1):undefined}))},_mouseDrag:function(e){if(this.dragged=!0,!this.options.disabled){var i,s=this,n=this.options,a=this.opos[0],o=this.opos[1],r=e.pageX,l=e.pageY;return a>r&&(i=r,r=a,a=i),o>l&&(i=l,l=o,o=i),this.helper.css({left:a,top:o,width:r-a,height:l-o}),this.selectees.each(function(){var i=t.data(this,"selectable-item"),h=!1;i&&i.element!==s.element[0]&&("touch"===n.tolerance?h=!(i.left>r||a>i.right||i.top>l||o>i.bottom):"fit"===n.tolerance&&(h=i.left>a&&r>i.right&&i.top>o&&l>i.bottom),h?(i.selected&&(i.$element.removeClass("ui-selected"),i.selected=!1),i.unselecting&&(i.$element.removeClass("ui-unselecting"),i.unselecting=!1),i.selecting||(i.$element.addClass("ui-selecting"),i.selecting=!0,s._trigger("selecting",e,{selecting:i.element}))):(i.selecting&&((e.metaKey||e.ctrlKey)&&i.startselected?(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.$element.addClass("ui-selected"),i.selected=!0):(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.startselected&&(i.$element.addClass("ui-unselecting"),i.unselecting=!0),s._trigger("unselecting",e,{unselecting:i.element}))),i.selected&&(e.metaKey||e.ctrlKey||i.startselected||(i.$element.removeClass("ui-selected"),i.selected=!1,i.$element.addClass("ui-unselecting"),i.unselecting=!0,s._trigger("unselecting",e,{unselecting:i.element})))))}),!1}},_mouseStop:function(e){var i=this;return this.dragged=!1,t(".ui-unselecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");s.$element.removeClass("ui-unselecting"),s.unselecting=!1,s.startselected=!1,i._trigger("unselected",e,{unselected:s.element})}),t(".ui-selecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");s.$element.removeClass("ui-selecting").addClass("ui-selected"),s.selecting=!1,s.selected=!0,s.startselected=!0,i._trigger("selected",e,{selected:s.element})}),this._trigger("stop",e),this.helper.remove(),!1}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}function i(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))}t.widget("ui.sortable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_create:function(){var t=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?"x"===t.axis||i(this.items[0].item):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},_destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_setOption:function(e,i){"disabled"===e?(this.options[e]=i,this.widget().toggleClass("ui-sortable-disabled",!!i)):t.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(e,i){var s=null,n=!1,o=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,o.widgetName+"-item")===o?(s=t(this),!1):undefined}),t.data(e.target,o.widgetName+"-item")===o&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,o,a=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,a.cursorAt&&this._adjustOffsetFromHelper(a.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),a.containment&&this._setContainment(),a.cursor&&"auto"!==a.cursor&&(o=this.document.find("body"),this.storedCursor=o.css("cursor"),o.css("cursor",a.cursor),this.storedStylesheet=t("<style>*{ cursor: "+a.cursor+" !important; }</style>").appendTo(o)),a.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",a.opacity)),a.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",a.zIndex)),this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,o,a=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY<a.scrollSensitivity?this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop+a.scrollSpeed:e.pageY-this.overflowOffset.top<a.scrollSensitivity&&(this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop-a.scrollSpeed),this.overflowOffset.left+this.scrollParent[0].offsetWidth-e.pageX<a.scrollSensitivity?this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft+a.scrollSpeed:e.pageX-this.overflowOffset.left<a.scrollSensitivity&&(this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft-a.scrollSpeed)):(e.pageY-t(document).scrollTop()<a.scrollSensitivity?r=t(document).scrollTop(t(document).scrollTop()-a.scrollSpeed):t(window).height()-(e.pageY-t(document).scrollTop())<a.scrollSensitivity&&(r=t(document).scrollTop(t(document).scrollTop()+a.scrollSpeed)),e.pageX-t(document).scrollLeft()<a.scrollSensitivity?r=t(document).scrollLeft(t(document).scrollLeft()-a.scrollSpeed):t(window).width()-(e.pageX-t(document).scrollLeft())<a.scrollSensitivity&&(r=t(document).scrollLeft(t(document).scrollLeft()+a.scrollSpeed))),r!==!1&&t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e)),this.positionAbs=this._convertPositionTo("absolute"),this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),i=this.items.length-1;i>=0;i--)if(s=this.items[i],n=s.item[0],o=this._intersectsWithPointer(s),o&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===o?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===o?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),o=this.options.axis,a={};o&&"x"!==o||(a.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollLeft)),o&&"y"!==o||(a.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(a,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,o=t.left,a=o+t.width,r=t.top,h=r+t.height,l=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+l>r&&h>s+l,d="y"===this.options.axis||e+c>o&&a>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>o&&a>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&h>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var i="x"===this.options.axis||e(this.positionAbs.top+this.offset.click.top,t.top,t.height),s="y"===this.options.axis||e(this.positionAbs.left+this.offset.click.left,t.left,t.width),n=i&&s,o=this._getDragVerticalDirection(),a=this._getDragHorizontalDirection();return n?this.floating?a&&"right"===a||"down"===o?2:1:o&&("down"===o?2:1):!1},_intersectsWithSides:function(t){var i=e(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),s=e(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),n=this._getDragVerticalDirection(),o=this._getDragHorizontalDirection();return this.floating&&o?"right"===o&&s||"left"===o&&!s:n&&("down"===n&&i||"up"===n&&!i)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,o,a,r=[],h=[],l=this._connectWith();if(l&&e)for(s=l.length-1;s>=0;s--)for(o=t(l[s]),n=o.length-1;n>=0;n--)a=t.data(o[n],this.widgetFullName),a&&a!==this&&!a.options.disabled&&h.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(h.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=h.length-1;s>=0;s--)h[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,o,a,r,h,l,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i]),s=n.length-1;s>=0;s--)o=t.data(n[s],this.widgetFullName),o&&o!==this&&!o.options.disabled&&(u.push([t.isFunction(o.options.items)?o.options.items.call(o.element[0],e,{item:this.currentItem}):t(o.options.items,o.element),o]),this.containers.push(o));for(i=u.length-1;i>=0;i--)for(a=u[i][1],r=u[i][0],s=0,l=r.length;l>s;s++)h=t(r[s]),h.data(this.widgetName+"-item",a),c.push({item:h,instance:a,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,o;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),o=n.offset(),s.left=o.left,s.top=o.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)o=this.containers[i].element.offset(),this.containers[i].containerCache.left=o.left,this.containers[i].containerCache.top=o.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]).addClass(i||e.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper");return"tr"===s?e.currentItem.children().each(function(){t("<td>&#160;</td>",e.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(n)}):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_contactContainers:function(s){var n,o,a,r,h,l,c,u,d,p,f=null,g=null;for(n=this.containers.length-1;n>=0;n--)if(!t.contains(this.currentItem[0],this.containers[n].element[0]))if(this._intersectsWith(this.containers[n].containerCache)){if(f&&t.contains(this.containers[n].element[0],f.element[0]))continue;f=this.containers[n],g=n}else this.containers[n].containerCache.over&&(this.containers[n]._trigger("out",s,this._uiHash(this)),this.containers[n].containerCache.over=0);if(f)if(1===this.containers.length)this.containers[g].containerCache.over||(this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1);else{for(a=1e4,r=null,p=f.floating||i(this.currentItem),h=p?"left":"top",l=p?"width":"height",c=this.positionAbs[h]+this.offset.click[h],o=this.items.length-1;o>=0;o--)t.contains(this.containers[g].element[0],this.items[o].item[0])&&this.items[o].item[0]!==this.currentItem[0]&&(!p||e(this.positionAbs.top+this.offset.click.top,this.items[o].top,this.items[o].height))&&(u=this.items[o].item.offset()[h],d=!1,Math.abs(u-c)>Math.abs(u+this.items[o][l]-c)&&(d=!0,u+=this.items[o][l]),a>Math.abs(u-c)&&(a=Math.abs(u-c),r=this.items[o],this.direction=d?"up":"down"));if(!r&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[g])return;r?this._rearrange(s,r,null,!0):this._rearrange(s,null,this.containers[g].element,!0),this._trigger("change",s,this._uiHash()),this.containers[g]._trigger("change",s,this._uiHash(this)),this.currentContainer=this.containers[g],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,t("document"===n.containment?document:window).width()-this.helperProportions.width-this.margins.left,(t("document"===n.containment?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,o=e.pageX,a=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==document&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.left<this.containment[0]&&(o=this.containment[0]+this.offset.click.left),e.pageY-this.offset.click.top<this.containment[1]&&(a=this.containment[1]+this.offset.click.top),e.pageX-this.offset.click.left>this.containment[2]&&(o=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(a=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((a-this.originalPageY)/n.grid[1])*n.grid[1],a=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((o-this.originalPageX)/n.grid[0])*n.grid[0],o=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,this.cancelHelperRemoval){if(!e){for(this._trigger("beforeStop",t,this._uiHash()),s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!1}if(e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null,!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})})(jQuery);(function(e){var t=0,i={},a={};i.height=i.paddingTop=i.paddingBottom=i.borderTopWidth=i.borderBottomWidth="hide",a.height=a.paddingTop=a.paddingBottom=a.borderTopWidth=a.borderBottomWidth="show",e.widget("ui.accordion",{version:"1.10.4",options:{active:0,animate:{},collapsible:!1,event:"click",header:"> li > :first-child,> :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},_create:function(){var t=this.options;this.prevShow=this.prevHide=e(),this.element.addClass("ui-accordion ui-widget ui-helper-reset").attr("role","tablist"),t.collapsible||t.active!==!1&&null!=t.active||(t.active=0),this._processPanels(),0>t.active&&(t.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():e(),content:this.active.length?this.active.next():e()}},_createIcons:function(){var t=this.options.icons;t&&(e("<span>").addClass("ui-accordion-header-icon ui-icon "+t.header).prependTo(this.headers),this.active.children(".ui-accordion-header-icon").removeClass(t.header).addClass(t.activeHeader),this.headers.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.removeClass("ui-accordion-icons").children(".ui-accordion-header-icon").remove()},_destroy:function(){var e;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.removeClass("ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("aria-controls").removeAttr("tabIndex").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),this._destroyIcons(),e=this.headers.next().css("display","").removeAttr("role").removeAttr("aria-hidden").removeAttr("aria-labelledby").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),"content"!==this.options.heightStyle&&e.css("height","")},_setOption:function(e,t){return"active"===e?(this._activate(t),undefined):("event"===e&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(t)),this._super(e,t),"collapsible"!==e||t||this.options.active!==!1||this._activate(0),"icons"===e&&(this._destroyIcons(),t&&this._createIcons()),"disabled"===e&&this.headers.add(this.headers.next()).toggleClass("ui-state-disabled",!!t),undefined)},_keydown:function(t){if(!t.altKey&&!t.ctrlKey){var i=e.ui.keyCode,a=this.headers.length,s=this.headers.index(t.target),n=!1;switch(t.keyCode){case i.RIGHT:case i.DOWN:n=this.headers[(s+1)%a];break;case i.LEFT:case i.UP:n=this.headers[(s-1+a)%a];break;case i.SPACE:case i.ENTER:this._eventHandler(t);break;case i.HOME:n=this.headers[0];break;case i.END:n=this.headers[a-1]}n&&(e(t.target).attr("tabIndex",-1),e(n).attr("tabIndex",0),n.focus(),t.preventDefault())}},_panelKeyDown:function(t){t.keyCode===e.ui.keyCode.UP&&t.ctrlKey&&e(t.currentTarget).prev().focus()},refresh:function(){var t=this.options;this._processPanels(),t.active===!1&&t.collapsible===!0||!this.headers.length?(t.active=!1,this.active=e()):t.active===!1?this._activate(0):this.active.length&&!e.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(t.active=!1,this.active=e()):this._activate(Math.max(0,t.active-1)):t.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){this.headers=this.element.find(this.options.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all"),this.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom").filter(":not(.ui-accordion-content-active)").hide()},_refresh:function(){var i,a=this.options,s=a.heightStyle,n=this.element.parent(),r=this.accordionId="ui-accordion-"+(this.element.attr("id")||++t);this.active=this._findActive(a.active).addClass("ui-accordion-header-active ui-state-active ui-corner-top").removeClass("ui-corner-all"),this.active.next().addClass("ui-accordion-content-active").show(),this.headers.attr("role","tab").each(function(t){var i=e(this),a=i.attr("id"),s=i.next(),n=s.attr("id");a||(a=r+"-header-"+t,i.attr("id",a)),n||(n=r+"-panel-"+t,s.attr("id",n)),i.attr("aria-controls",n),s.attr("aria-labelledby",a)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(a.event),"fill"===s?(i=n.height(),this.element.siblings(":visible").each(function(){var t=e(this),a=t.css("position");"absolute"!==a&&"fixed"!==a&&(i-=t.outerHeight(!0))}),this.headers.each(function(){i-=e(this).outerHeight(!0)}),this.headers.next().each(function(){e(this).height(Math.max(0,i-e(this).innerHeight()+e(this).height()))}).css("overflow","auto")):"auto"===s&&(i=0,this.headers.next().each(function(){i=Math.max(i,e(this).css("height","").height())}).height(i))},_activate:function(t){var i=this._findActive(t)[0];i!==this.active[0]&&(i=i||this.active[0],this._eventHandler({target:i,currentTarget:i,preventDefault:e.noop}))},_findActive:function(t){return"number"==typeof t?this.headers.eq(t):e()},_setupEvents:function(t){var i={keydown:"_keydown"};t&&e.each(t.split(" "),function(e,t){i[t]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(t){var i=this.options,a=this.active,s=e(t.currentTarget),n=s[0]===a[0],r=n&&i.collapsible,o=r?e():s.next(),h=a.next(),d={oldHeader:a,oldPanel:h,newHeader:r?e():s,newPanel:o};t.preventDefault(),n&&!i.collapsible||this._trigger("beforeActivate",t,d)===!1||(i.active=r?!1:this.headers.index(s),this.active=n?e():s,this._toggle(d),a.removeClass("ui-accordion-header-active ui-state-active"),i.icons&&a.children(".ui-accordion-header-icon").removeClass(i.icons.activeHeader).addClass(i.icons.header),n||(s.removeClass("ui-corner-all").addClass("ui-accordion-header-active ui-state-active ui-corner-top"),i.icons&&s.children(".ui-accordion-header-icon").removeClass(i.icons.header).addClass(i.icons.activeHeader),s.next().addClass("ui-accordion-content-active")))},_toggle:function(t){var i=t.newPanel,a=this.prevShow.length?this.prevShow:t.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=i,this.prevHide=a,this.options.animate?this._animate(i,a,t):(a.hide(),i.show(),this._toggleComplete(t)),a.attr({"aria-hidden":"true"}),a.prev().attr("aria-selected","false"),i.length&&a.length?a.prev().attr({tabIndex:-1,"aria-expanded":"false"}):i.length&&this.headers.filter(function(){return 0===e(this).attr("tabIndex")}).attr("tabIndex",-1),i.attr("aria-hidden","false").prev().attr({"aria-selected":"true",tabIndex:0,"aria-expanded":"true"})},_animate:function(e,t,s){var n,r,o,h=this,d=0,c=e.length&&(!t.length||e.index()<t.index()),l=this.options.animate||{},u=c&&l.down||l,v=function(){h._toggleComplete(s)};return"number"==typeof u&&(o=u),"string"==typeof u&&(r=u),r=r||u.easing||l.easing,o=o||u.duration||l.duration,t.length?e.length?(n=e.show().outerHeight(),t.animate(i,{duration:o,easing:r,step:function(e,t){t.now=Math.round(e)}}),e.hide().animate(a,{duration:o,easing:r,complete:v,step:function(e,i){i.now=Math.round(e),"height"!==i.prop?d+=i.now:"content"!==h.options.heightStyle&&(i.now=Math.round(n-t.outerHeight()-d),d=0)}}),undefined):t.animate(i,o,r,v):e.animate(a,o,r,v)},_toggleComplete:function(e){var t=e.oldPanel;t.removeClass("ui-accordion-content-active").prev().removeClass("ui-corner-top").addClass("ui-corner-all"),t.length&&(t.parent()[0].className=t.parent()[0].className),this._trigger("activate",null,e)}})})(jQuery);(function(e){e.widget("ui.autocomplete",{version:"1.10.4",defaultElement:"<input>",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var t,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return t=!0,s=!0,i=!0,undefined;t=!1,s=!1,i=!1;var a=e.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:t=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:t=!0,this._move("nextPage",n);break;case a.UP:t=!0,this._keyEvent("previous",n);break;case a.DOWN:t=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(t=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(t)return t=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=e.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(e){return s?(s=!1,e.preventDefault(),undefined):(this._searchTimeout(e),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(e),this._change(e),undefined)}}),this._initSource(),this.menu=e("<ul>").addClass("ui-autocomplete ui-front").appendTo(this._appendTo()).menu({role:null}).hide().data("ui-menu"),this._on(this.menu.element,{mousedown:function(t){t.preventDefault(),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur});var i=this.menu.element[0];e(t.target).closest(".ui-menu-item").length||this._delay(function(){var t=this;this.document.one("mousedown",function(s){s.target===t.element[0]||s.target===i||e.contains(i,s.target)||t.close()})})},menufocus:function(t,i){if(this.isNewMenu&&(this.isNewMenu=!1,t.originalEvent&&/^mouse/.test(t.originalEvent.type)))return this.menu.blur(),this.document.one("mousemove",function(){e(t.target).trigger(t.originalEvent)}),undefined;var s=i.item.data("ui-autocomplete-item");!1!==this._trigger("focus",t,{item:s})?t.originalEvent&&/^key/.test(t.originalEvent.type)&&this._value(s.value):this.liveRegion.text(s.value)},menuselect:function(e,t){var i=t.item.data("ui-autocomplete-item"),s=this.previous;this.element[0]!==this.document[0].activeElement&&(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s,this.selectedItem=i})),!1!==this._trigger("select",e,{item:i})&&this._value(i.value),this.term=this._value(),this.close(e),this.selectedItem=i}}),this.liveRegion=e("<span>",{role:"status","aria-live":"polite"}).addClass("ui-helper-hidden-accessible").insertBefore(this.element),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(e,t){this._super(e,t),"source"===e&&this._initSource(),"appendTo"===e&&this.menu.element.appendTo(this._appendTo()),"disabled"===e&&t&&this.xhr&&this.xhr.abort()},_appendTo:function(){var t=this.options.appendTo;return t&&(t=t.jquery||t.nodeType?e(t):this.document.find(t).eq(0)),t||(t=this.element.closest(".ui-front")),t.length||(t=this.document[0].body),t},_initSource:function(){var t,i,s=this;e.isArray(this.options.source)?(t=this.options.source,this.source=function(i,s){s(e.ui.autocomplete.filter(t,i.term))}):"string"==typeof this.options.source?(i=this.options.source,this.source=function(t,n){s.xhr&&s.xhr.abort(),s.xhr=e.ajax({url:i,data:t,dataType:"json",success:function(e){n(e)},error:function(){n([])}})}):this.source=this.options.source},_searchTimeout:function(e){clearTimeout(this.searching),this.searching=this._delay(function(){this.term!==this._value()&&(this.selectedItem=null,this.search(null,e))},this.options.delay)},search:function(e,t){return e=null!=e?e:this._value(),this.term=this._value(),e.length<this.options.minLength?this.close(t):this._trigger("search",t)!==!1?this._search(e):undefined},_search:function(e){this.pending++,this.element.addClass("ui-autocomplete-loading"),this.cancelSearch=!1,this.source({term:e},this._response())},_response:function(){var t=++this.requestIndex;return e.proxy(function(e){t===this.requestIndex&&this.__response(e),this.pending--,this.pending||this.element.removeClass("ui-autocomplete-loading")},this)},__response:function(e){e&&(e=this._normalize(e)),this._trigger("response",null,{content:e}),!this.options.disabled&&e&&e.length&&!this.cancelSearch?(this._suggest(e),this._trigger("open")):this._close()},close:function(e){this.cancelSearch=!0,this._close(e)},_close:function(e){this.menu.element.is(":visible")&&(this.menu.element.hide(),this.menu.blur(),this.isNewMenu=!0,this._trigger("close",e))},_change:function(e){this.previous!==this._value()&&this._trigger("change",e,{item:this.selectedItem})},_normalize:function(t){return t.length&&t[0].label&&t[0].value?t:e.map(t,function(t){return"string"==typeof t?{label:t,value:t}:e.extend({label:t.label||t.value,value:t.value||t.label},t)})},_suggest:function(t){var i=this.menu.element.empty();this._renderMenu(i,t),this.isNewMenu=!0,this.menu.refresh(),i.show(),this._resizeMenu(),i.position(e.extend({of:this.element},this.options.position)),this.options.autoFocus&&this.menu.next()},_resizeMenu:function(){var e=this.menu.element;e.outerWidth(Math.max(e.width("").outerWidth()+1,this.element.outerWidth()))},_renderMenu:function(t,i){var s=this;e.each(i,function(e,i){s._renderItemData(t,i)})},_renderItemData:function(e,t){return this._renderItem(e,t).data("ui-autocomplete-item",t)},_renderItem:function(t,i){return e("<li>").append(e("<a>").text(i.label)).appendTo(t)},_move:function(e,t){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(e)||this.menu.isLastItem()&&/^next/.test(e)?(this._value(this.term),this.menu.blur(),undefined):(this.menu[e](t),undefined):(this.search(null,t),undefined)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(e,t){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(e,t),t.preventDefault())}}),e.extend(e.ui.autocomplete,{escapeRegex:function(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(t,i){var s=RegExp(e.ui.autocomplete.escapeRegex(i),"i");return e.grep(t,function(e){return s.test(e.label||e.value||e)})}}),e.widget("ui.autocomplete",e.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(e){return e+(e>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var t;this._superApply(arguments),this.options.disabled||this.cancelSearch||(t=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.text(t))}})})(jQuery);(function(e){var t,i="ui-button ui-widget ui-state-default ui-corner-all",n="ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",s=function(){var t=e(this);setTimeout(function(){t.find(":ui-button").button("refresh")},1)},a=function(t){var i=t.name,n=t.form,s=e([]);return i&&(i=i.replace(/'/g,"\\'"),s=n?e(n).find("[name='"+i+"']"):e("[name='"+i+"']",t.ownerDocument).filter(function(){return!this.form})),s};e.widget("ui.button",{version:"1.10.4",defaultElement:"<button>",options:{disabled:null,text:!0,label:null,icons:{primary:null,secondary:null}},_create:function(){this.element.closest("form").unbind("reset"+this.eventNamespace).bind("reset"+this.eventNamespace,s),"boolean"!=typeof this.options.disabled?this.options.disabled=!!this.element.prop("disabled"):this.element.prop("disabled",this.options.disabled),this._determineButtonType(),this.hasTitle=!!this.buttonElement.attr("title");var n=this,o=this.options,r="checkbox"===this.type||"radio"===this.type,h=r?"":"ui-state-active";null===o.label&&(o.label="input"===this.type?this.buttonElement.val():this.buttonElement.html()),this._hoverable(this.buttonElement),this.buttonElement.addClass(i).attr("role","button").bind("mouseenter"+this.eventNamespace,function(){o.disabled||this===t&&e(this).addClass("ui-state-active")}).bind("mouseleave"+this.eventNamespace,function(){o.disabled||e(this).removeClass(h)}).bind("click"+this.eventNamespace,function(e){o.disabled&&(e.preventDefault(),e.stopImmediatePropagation())}),this._on({focus:function(){this.buttonElement.addClass("ui-state-focus")},blur:function(){this.buttonElement.removeClass("ui-state-focus")}}),r&&this.element.bind("change"+this.eventNamespace,function(){n.refresh()}),"checkbox"===this.type?this.buttonElement.bind("click"+this.eventNamespace,function(){return o.disabled?!1:undefined}):"radio"===this.type?this.buttonElement.bind("click"+this.eventNamespace,function(){if(o.disabled)return!1;e(this).addClass("ui-state-active"),n.buttonElement.attr("aria-pressed","true");var t=n.element[0];a(t).not(t).map(function(){return e(this).button("widget")[0]}).removeClass("ui-state-active").attr("aria-pressed","false")}):(this.buttonElement.bind("mousedown"+this.eventNamespace,function(){return o.disabled?!1:(e(this).addClass("ui-state-active"),t=this,n.document.one("mouseup",function(){t=null}),undefined)}).bind("mouseup"+this.eventNamespace,function(){return o.disabled?!1:(e(this).removeClass("ui-state-active"),undefined)}).bind("keydown"+this.eventNamespace,function(t){return o.disabled?!1:((t.keyCode===e.ui.keyCode.SPACE||t.keyCode===e.ui.keyCode.ENTER)&&e(this).addClass("ui-state-active"),undefined)}).bind("keyup"+this.eventNamespace+" blur"+this.eventNamespace,function(){e(this).removeClass("ui-state-active")}),this.buttonElement.is("a")&&this.buttonElement.keyup(function(t){t.keyCode===e.ui.keyCode.SPACE&&e(this).click()})),this._setOption("disabled",o.disabled),this._resetButton()},_determineButtonType:function(){var e,t,i;this.type=this.element.is("[type=checkbox]")?"checkbox":this.element.is("[type=radio]")?"radio":this.element.is("input")?"input":"button","checkbox"===this.type||"radio"===this.type?(e=this.element.parents().last(),t="label[for='"+this.element.attr("id")+"']",this.buttonElement=e.find(t),this.buttonElement.length||(e=e.length?e.siblings():this.element.siblings(),this.buttonElement=e.filter(t),this.buttonElement.length||(this.buttonElement=e.find(t))),this.element.addClass("ui-helper-hidden-accessible"),i=this.element.is(":checked"),i&&this.buttonElement.addClass("ui-state-active"),this.buttonElement.prop("aria-pressed",i)):this.buttonElement=this.element},widget:function(){return this.buttonElement},_destroy:function(){this.element.removeClass("ui-helper-hidden-accessible"),this.buttonElement.removeClass(i+" ui-state-active "+n).removeAttr("role").removeAttr("aria-pressed").html(this.buttonElement.find(".ui-button-text").html()),this.hasTitle||this.buttonElement.removeAttr("title")},_setOption:function(e,t){return this._super(e,t),"disabled"===e?(this.element.prop("disabled",!!t),t&&this.buttonElement.removeClass("ui-state-focus"),undefined):(this._resetButton(),undefined)},refresh:function(){var t=this.element.is("input, button")?this.element.is(":disabled"):this.element.hasClass("ui-button-disabled");t!==this.options.disabled&&this._setOption("disabled",t),"radio"===this.type?a(this.element[0]).each(function(){e(this).is(":checked")?e(this).button("widget").addClass("ui-state-active").attr("aria-pressed","true"):e(this).button("widget").removeClass("ui-state-active").attr("aria-pressed","false")}):"checkbox"===this.type&&(this.element.is(":checked")?this.buttonElement.addClass("ui-state-active").attr("aria-pressed","true"):this.buttonElement.removeClass("ui-state-active").attr("aria-pressed","false"))},_resetButton:function(){if("input"===this.type)return this.options.label&&this.element.val(this.options.label),undefined;var t=this.buttonElement.removeClass(n),i=e("<span></span>",this.document[0]).addClass("ui-button-text").html(this.options.label).appendTo(t.empty()).text(),s=this.options.icons,a=s.primary&&s.secondary,o=[];s.primary||s.secondary?(this.options.text&&o.push("ui-button-text-icon"+(a?"s":s.primary?"-primary":"-secondary")),s.primary&&t.prepend("<span class='ui-button-icon-primary ui-icon "+s.primary+"'></span>"),s.secondary&&t.append("<span class='ui-button-icon-secondary ui-icon "+s.secondary+"'></span>"),this.options.text||(o.push(a?"ui-button-icons-only":"ui-button-icon-only"),this.hasTitle||t.attr("title",e.trim(i)))):o.push("ui-button-text-only"),t.addClass(o.join(" "))}}),e.widget("ui.buttonset",{version:"1.10.4",options:{items:"button, input[type=button], input[type=submit], input[type=reset], input[type=checkbox], input[type=radio], a, :data(ui-button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(e,t){"disabled"===e&&this.buttons.button("option",e,t),this._super(e,t)},refresh:function(){var t="rtl"===this.element.css("direction");this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return e(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(t?"ui-corner-right":"ui-corner-left").end().filter(":last").addClass(t?"ui-corner-left":"ui-corner-right").end().end()},_destroy:function(){this.element.removeClass("ui-buttonset"),this.buttons.map(function(){return e(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy")}})})(jQuery);(function(e,t){function i(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},e.extend(this._defaults,this.regional[""]),this.dpDiv=a(e("<div id='"+this._mainDivId+"' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>"))}function a(t){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return t.delegate(i,"mouseout",function(){e(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).removeClass("ui-datepicker-next-hover")}).delegate(i,"mouseover",function(){e.datepicker._isDisabledDatepicker(n.inline?t.parent()[0]:n.input[0])||(e(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),e(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).addClass("ui-datepicker-next-hover"))})}function s(t,i){e.extend(t,i);for(var a in i)null==i[a]&&(t[a]=i[a]);return t}e.extend(e.ui,{datepicker:{version:"1.10.4"}});var n,r="datepicker";e.extend(i.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(e){return s(this._defaults,e||{}),this},_attachDatepicker:function(t,i){var a,s,n;a=t.nodeName.toLowerCase(),s="div"===a||"span"===a,t.id||(this.uuid+=1,t.id="dp"+this.uuid),n=this._newInst(e(t),s),n.settings=e.extend({},i||{}),"input"===a?this._connectDatepicker(t,n):s&&this._inlineDatepicker(t,n)},_newInst:function(t,i){var s=t[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1");return{id:s,input:t,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:i,dpDiv:i?a(e("<div class='"+this._inlineClass+" ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")):this.dpDiv}},_connectDatepicker:function(t,i){var a=e(t);i.append=e([]),i.trigger=e([]),a.hasClass(this.markerClassName)||(this._attachments(a,i),a.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp),this._autoSize(i),e.data(t,r,i),i.settings.disabled&&this._disableDatepicker(t))},_attachments:function(t,i){var a,s,n,r=this._get(i,"appendText"),o=this._get(i,"isRTL");i.append&&i.append.remove(),r&&(i.append=e("<span class='"+this._appendClass+"'>"+r+"</span>"),t[o?"before":"after"](i.append)),t.unbind("focus",this._showDatepicker),i.trigger&&i.trigger.remove(),a=this._get(i,"showOn"),("focus"===a||"both"===a)&&t.focus(this._showDatepicker),("button"===a||"both"===a)&&(s=this._get(i,"buttonText"),n=this._get(i,"buttonImage"),i.trigger=e(this._get(i,"buttonImageOnly")?e("<img/>").addClass(this._triggerClass).attr({src:n,alt:s,title:s}):e("<button type='button'></button>").addClass(this._triggerClass).html(n?e("<img/>").attr({src:n,alt:s,title:s}):s)),t[o?"before":"after"](i.trigger),i.trigger.click(function(){return e.datepicker._datepickerShowing&&e.datepicker._lastInput===t[0]?e.datepicker._hideDatepicker():e.datepicker._datepickerShowing&&e.datepicker._lastInput!==t[0]?(e.datepicker._hideDatepicker(),e.datepicker._showDatepicker(t[0])):e.datepicker._showDatepicker(t[0]),!1}))},_autoSize:function(e){if(this._get(e,"autoSize")&&!e.inline){var t,i,a,s,n=new Date(2009,11,20),r=this._get(e,"dateFormat");r.match(/[DM]/)&&(t=function(e){for(i=0,a=0,s=0;e.length>s;s++)e[s].length>i&&(i=e[s].length,a=s);return a},n.setMonth(t(this._get(e,r.match(/MM/)?"monthNames":"monthNamesShort"))),n.setDate(t(this._get(e,r.match(/DD/)?"dayNames":"dayNamesShort"))+20-n.getDay())),e.input.attr("size",this._formatDate(e,n).length)}},_inlineDatepicker:function(t,i){var a=e(t);a.hasClass(this.markerClassName)||(a.addClass(this.markerClassName).append(i.dpDiv),e.data(t,r,i),this._setDate(i,this._getDefaultDate(i),!0),this._updateDatepicker(i),this._updateAlternate(i),i.settings.disabled&&this._disableDatepicker(t),i.dpDiv.css("display","block"))},_dialogDatepicker:function(t,i,a,n,o){var u,c,h,l,d,p=this._dialogInst;return p||(this.uuid+=1,u="dp"+this.uuid,this._dialogInput=e("<input type='text' id='"+u+"' style='position: absolute; top: -100px; width: 0px;'/>"),this._dialogInput.keydown(this._doKeyDown),e("body").append(this._dialogInput),p=this._dialogInst=this._newInst(this._dialogInput,!1),p.settings={},e.data(this._dialogInput[0],r,p)),s(p.settings,n||{}),i=i&&i.constructor===Date?this._formatDate(p,i):i,this._dialogInput.val(i),this._pos=o?o.length?o:[o.pageX,o.pageY]:null,this._pos||(c=document.documentElement.clientWidth,h=document.documentElement.clientHeight,l=document.documentElement.scrollLeft||document.body.scrollLeft,d=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[c/2-100+l,h/2-150+d]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),p.settings.onSelect=a,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),e.blockUI&&e.blockUI(this.dpDiv),e.data(this._dialogInput[0],r,p),this},_destroyDatepicker:function(t){var i,a=e(t),s=e.data(t,r);a.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),e.removeData(t,r),"input"===i?(s.append.remove(),s.trigger.remove(),a.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):("div"===i||"span"===i)&&a.removeClass(this.markerClassName).empty())},_enableDatepicker:function(t){var i,a,s=e(t),n=e.data(t,r);s.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!1,n.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(a=s.children("."+this._inlineClass),a.children().removeClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=e.map(this._disabledInputs,function(e){return e===t?null:e}))},_disableDatepicker:function(t){var i,a,s=e(t),n=e.data(t,r);s.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!0,n.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):("div"===i||"span"===i)&&(a=s.children("."+this._inlineClass),a.children().addClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=e.map(this._disabledInputs,function(e){return e===t?null:e}),this._disabledInputs[this._disabledInputs.length]=t)},_isDisabledDatepicker:function(e){if(!e)return!1;for(var t=0;this._disabledInputs.length>t;t++)if(this._disabledInputs[t]===e)return!0;return!1},_getInst:function(t){try{return e.data(t,r)}catch(i){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(i,a,n){var r,o,u,c,h=this._getInst(i);return 2===arguments.length&&"string"==typeof a?"defaults"===a?e.extend({},e.datepicker._defaults):h?"all"===a?e.extend({},h.settings):this._get(h,a):null:(r=a||{},"string"==typeof a&&(r={},r[a]=n),h&&(this._curInst===h&&this._hideDatepicker(),o=this._getDateDatepicker(i,!0),u=this._getMinMaxDate(h,"min"),c=this._getMinMaxDate(h,"max"),s(h.settings,r),null!==u&&r.dateFormat!==t&&r.minDate===t&&(h.settings.minDate=this._formatDate(h,u)),null!==c&&r.dateFormat!==t&&r.maxDate===t&&(h.settings.maxDate=this._formatDate(h,c)),"disabled"in r&&(r.disabled?this._disableDatepicker(i):this._enableDatepicker(i)),this._attachments(e(i),h),this._autoSize(h),this._setDate(h,o),this._updateAlternate(h),this._updateDatepicker(h)),t)},_changeDatepicker:function(e,t,i){this._optionDatepicker(e,t,i)},_refreshDatepicker:function(e){var t=this._getInst(e);t&&this._updateDatepicker(t)},_setDateDatepicker:function(e,t){var i=this._getInst(e);i&&(this._setDate(i,t),this._updateDatepicker(i),this._updateAlternate(i))},_getDateDatepicker:function(e,t){var i=this._getInst(e);return i&&!i.inline&&this._setDateFromField(i,t),i?this._getDate(i):null},_doKeyDown:function(t){var i,a,s,n=e.datepicker._getInst(t.target),r=!0,o=n.dpDiv.is(".ui-datepicker-rtl");if(n._keyEvent=!0,e.datepicker._datepickerShowing)switch(t.keyCode){case 9:e.datepicker._hideDatepicker(),r=!1;break;case 13:return s=e("td."+e.datepicker._dayOverClass+":not(."+e.datepicker._currentClass+")",n.dpDiv),s[0]&&e.datepicker._selectDay(t.target,n.selectedMonth,n.selectedYear,s[0]),i=e.datepicker._get(n,"onSelect"),i?(a=e.datepicker._formatDate(n),i.apply(n.input?n.input[0]:null,[a,n])):e.datepicker._hideDatepicker(),!1;case 27:e.datepicker._hideDatepicker();break;case 33:e.datepicker._adjustDate(t.target,t.ctrlKey?-e.datepicker._get(n,"stepBigMonths"):-e.datepicker._get(n,"stepMonths"),"M");break;case 34:e.datepicker._adjustDate(t.target,t.ctrlKey?+e.datepicker._get(n,"stepBigMonths"):+e.datepicker._get(n,"stepMonths"),"M");break;case 35:(t.ctrlKey||t.metaKey)&&e.datepicker._clearDate(t.target),r=t.ctrlKey||t.metaKey;break;case 36:(t.ctrlKey||t.metaKey)&&e.datepicker._gotoToday(t.target),r=t.ctrlKey||t.metaKey;break;case 37:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,o?1:-1,"D"),r=t.ctrlKey||t.metaKey,t.originalEvent.altKey&&e.datepicker._adjustDate(t.target,t.ctrlKey?-e.datepicker._get(n,"stepBigMonths"):-e.datepicker._get(n,"stepMonths"),"M");break;case 38:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,-7,"D"),r=t.ctrlKey||t.metaKey;break;case 39:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,o?-1:1,"D"),r=t.ctrlKey||t.metaKey,t.originalEvent.altKey&&e.datepicker._adjustDate(t.target,t.ctrlKey?+e.datepicker._get(n,"stepBigMonths"):+e.datepicker._get(n,"stepMonths"),"M");break;case 40:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,7,"D"),r=t.ctrlKey||t.metaKey;break;default:r=!1}else 36===t.keyCode&&t.ctrlKey?e.datepicker._showDatepicker(this):r=!1;r&&(t.preventDefault(),t.stopPropagation())},_doKeyPress:function(i){var a,s,n=e.datepicker._getInst(i.target);return e.datepicker._get(n,"constrainInput")?(a=e.datepicker._possibleChars(e.datepicker._get(n,"dateFormat")),s=String.fromCharCode(null==i.charCode?i.keyCode:i.charCode),i.ctrlKey||i.metaKey||" ">s||!a||a.indexOf(s)>-1):t},_doKeyUp:function(t){var i,a=e.datepicker._getInst(t.target);if(a.input.val()!==a.lastVal)try{i=e.datepicker.parseDate(e.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,e.datepicker._getFormatConfig(a)),i&&(e.datepicker._setDateFromField(a),e.datepicker._updateAlternate(a),e.datepicker._updateDatepicker(a))}catch(s){}return!0},_showDatepicker:function(t){if(t=t.target||t,"input"!==t.nodeName.toLowerCase()&&(t=e("input",t.parentNode)[0]),!e.datepicker._isDisabledDatepicker(t)&&e.datepicker._lastInput!==t){var i,a,n,r,o,u,c;i=e.datepicker._getInst(t),e.datepicker._curInst&&e.datepicker._curInst!==i&&(e.datepicker._curInst.dpDiv.stop(!0,!0),i&&e.datepicker._datepickerShowing&&e.datepicker._hideDatepicker(e.datepicker._curInst.input[0])),a=e.datepicker._get(i,"beforeShow"),n=a?a.apply(t,[t,i]):{},n!==!1&&(s(i.settings,n),i.lastVal=null,e.datepicker._lastInput=t,e.datepicker._setDateFromField(i),e.datepicker._inDialog&&(t.value=""),e.datepicker._pos||(e.datepicker._pos=e.datepicker._findPos(t),e.datepicker._pos[1]+=t.offsetHeight),r=!1,e(t).parents().each(function(){return r|="fixed"===e(this).css("position"),!r}),o={left:e.datepicker._pos[0],top:e.datepicker._pos[1]},e.datepicker._pos=null,i.dpDiv.empty(),i.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),e.datepicker._updateDatepicker(i),o=e.datepicker._checkOffset(i,o,r),i.dpDiv.css({position:e.datepicker._inDialog&&e.blockUI?"static":r?"fixed":"absolute",display:"none",left:o.left+"px",top:o.top+"px"}),i.inline||(u=e.datepicker._get(i,"showAnim"),c=e.datepicker._get(i,"duration"),i.dpDiv.zIndex(e(t).zIndex()+1),e.datepicker._datepickerShowing=!0,e.effects&&e.effects.effect[u]?i.dpDiv.show(u,e.datepicker._get(i,"showOptions"),c):i.dpDiv[u||"show"](u?c:null),e.datepicker._shouldFocusInput(i)&&i.input.focus(),e.datepicker._curInst=i))}},_updateDatepicker:function(t){this.maxRows=4,n=t,t.dpDiv.empty().append(this._generateHTML(t)),this._attachHandlers(t),t.dpDiv.find("."+this._dayOverClass+" a").mouseover();var i,a=this._getNumberOfMonths(t),s=a[1],r=17;t.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),s>1&&t.dpDiv.addClass("ui-datepicker-multi-"+s).css("width",r*s+"em"),t.dpDiv[(1!==a[0]||1!==a[1]?"add":"remove")+"Class"]("ui-datepicker-multi"),t.dpDiv[(this._get(t,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),t===e.datepicker._curInst&&e.datepicker._datepickerShowing&&e.datepicker._shouldFocusInput(t)&&t.input.focus(),t.yearshtml&&(i=t.yearshtml,setTimeout(function(){i===t.yearshtml&&t.yearshtml&&t.dpDiv.find("select.ui-datepicker-year:first").replaceWith(t.yearshtml),i=t.yearshtml=null},0))},_shouldFocusInput:function(e){return e.input&&e.input.is(":visible")&&!e.input.is(":disabled")&&!e.input.is(":focus")},_checkOffset:function(t,i,a){var s=t.dpDiv.outerWidth(),n=t.dpDiv.outerHeight(),r=t.input?t.input.outerWidth():0,o=t.input?t.input.outerHeight():0,u=document.documentElement.clientWidth+(a?0:e(document).scrollLeft()),c=document.documentElement.clientHeight+(a?0:e(document).scrollTop());return i.left-=this._get(t,"isRTL")?s-r:0,i.left-=a&&i.left===t.input.offset().left?e(document).scrollLeft():0,i.top-=a&&i.top===t.input.offset().top+o?e(document).scrollTop():0,i.left-=Math.min(i.left,i.left+s>u&&u>s?Math.abs(i.left+s-u):0),i.top-=Math.min(i.top,i.top+n>c&&c>n?Math.abs(n+o):0),i},_findPos:function(t){for(var i,a=this._getInst(t),s=this._get(a,"isRTL");t&&("hidden"===t.type||1!==t.nodeType||e.expr.filters.hidden(t));)t=t[s?"previousSibling":"nextSibling"];return i=e(t).offset(),[i.left,i.top]},_hideDatepicker:function(t){var i,a,s,n,o=this._curInst;!o||t&&o!==e.data(t,r)||this._datepickerShowing&&(i=this._get(o,"showAnim"),a=this._get(o,"duration"),s=function(){e.datepicker._tidyDialog(o)},e.effects&&(e.effects.effect[i]||e.effects[i])?o.dpDiv.hide(i,e.datepicker._get(o,"showOptions"),a,s):o.dpDiv["slideDown"===i?"slideUp":"fadeIn"===i?"fadeOut":"hide"](i?a:null,s),i||s(),this._datepickerShowing=!1,n=this._get(o,"onClose"),n&&n.apply(o.input?o.input[0]:null,[o.input?o.input.val():"",o]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),e.blockUI&&(e.unblockUI(),e("body").append(this.dpDiv))),this._inDialog=!1)},_tidyDialog:function(e){e.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(t){if(e.datepicker._curInst){var i=e(t.target),a=e.datepicker._getInst(i[0]);(i[0].id!==e.datepicker._mainDivId&&0===i.parents("#"+e.datepicker._mainDivId).length&&!i.hasClass(e.datepicker.markerClassName)&&!i.closest("."+e.datepicker._triggerClass).length&&e.datepicker._datepickerShowing&&(!e.datepicker._inDialog||!e.blockUI)||i.hasClass(e.datepicker.markerClassName)&&e.datepicker._curInst!==a)&&e.datepicker._hideDatepicker()}},_adjustDate:function(t,i,a){var s=e(t),n=this._getInst(s[0]);this._isDisabledDatepicker(s[0])||(this._adjustInstDate(n,i+("M"===a?this._get(n,"showCurrentAtPos"):0),a),this._updateDatepicker(n))},_gotoToday:function(t){var i,a=e(t),s=this._getInst(a[0]);this._get(s,"gotoCurrent")&&s.currentDay?(s.selectedDay=s.currentDay,s.drawMonth=s.selectedMonth=s.currentMonth,s.drawYear=s.selectedYear=s.currentYear):(i=new Date,s.selectedDay=i.getDate(),s.drawMonth=s.selectedMonth=i.getMonth(),s.drawYear=s.selectedYear=i.getFullYear()),this._notifyChange(s),this._adjustDate(a)},_selectMonthYear:function(t,i,a){var s=e(t),n=this._getInst(s[0]);n["selected"+("M"===a?"Month":"Year")]=n["draw"+("M"===a?"Month":"Year")]=parseInt(i.options[i.selectedIndex].value,10),this._notifyChange(n),this._adjustDate(s)},_selectDay:function(t,i,a,s){var n,r=e(t);e(s).hasClass(this._unselectableClass)||this._isDisabledDatepicker(r[0])||(n=this._getInst(r[0]),n.selectedDay=n.currentDay=e("a",s).html(),n.selectedMonth=n.currentMonth=i,n.selectedYear=n.currentYear=a,this._selectDate(t,this._formatDate(n,n.currentDay,n.currentMonth,n.currentYear)))},_clearDate:function(t){var i=e(t);this._selectDate(i,"")},_selectDate:function(t,i){var a,s=e(t),n=this._getInst(s[0]);i=null!=i?i:this._formatDate(n),n.input&&n.input.val(i),this._updateAlternate(n),a=this._get(n,"onSelect"),a?a.apply(n.input?n.input[0]:null,[i,n]):n.input&&n.input.trigger("change"),n.inline?this._updateDatepicker(n):(this._hideDatepicker(),this._lastInput=n.input[0],"object"!=typeof n.input[0]&&n.input.focus(),this._lastInput=null)},_updateAlternate:function(t){var i,a,s,n=this._get(t,"altField");n&&(i=this._get(t,"altFormat")||this._get(t,"dateFormat"),a=this._getDate(t),s=this.formatDate(i,a,this._getFormatConfig(t)),e(n).each(function(){e(this).val(s)}))},noWeekends:function(e){var t=e.getDay();return[t>0&&6>t,""]},iso8601Week:function(e){var t,i=new Date(e.getTime());return i.setDate(i.getDate()+4-(i.getDay()||7)),t=i.getTime(),i.setMonth(0),i.setDate(1),Math.floor(Math.round((t-i)/864e5)/7)+1},parseDate:function(i,a,s){if(null==i||null==a)throw"Invalid arguments";if(a="object"==typeof a?""+a:a+"",""===a)return null;var n,r,o,u,c=0,h=(s?s.shortYearCutoff:null)||this._defaults.shortYearCutoff,l="string"!=typeof h?h:(new Date).getFullYear()%100+parseInt(h,10),d=(s?s.dayNamesShort:null)||this._defaults.dayNamesShort,p=(s?s.dayNames:null)||this._defaults.dayNames,g=(s?s.monthNamesShort:null)||this._defaults.monthNamesShort,m=(s?s.monthNames:null)||this._defaults.monthNames,f=-1,_=-1,v=-1,k=-1,y=!1,b=function(e){var t=i.length>n+1&&i.charAt(n+1)===e;return t&&n++,t},D=function(e){var t=b(e),i="@"===e?14:"!"===e?20:"y"===e&&t?4:"o"===e?3:2,s=RegExp("^\\d{1,"+i+"}"),n=a.substring(c).match(s);if(!n)throw"Missing number at position "+c;return c+=n[0].length,parseInt(n[0],10)},w=function(i,s,n){var r=-1,o=e.map(b(i)?n:s,function(e,t){return[[t,e]]}).sort(function(e,t){return-(e[1].length-t[1].length)});if(e.each(o,function(e,i){var s=i[1];return a.substr(c,s.length).toLowerCase()===s.toLowerCase()?(r=i[0],c+=s.length,!1):t}),-1!==r)return r+1;throw"Unknown name at position "+c},M=function(){if(a.charAt(c)!==i.charAt(n))throw"Unexpected literal at position "+c;c++};for(n=0;i.length>n;n++)if(y)"'"!==i.charAt(n)||b("'")?M():y=!1;else switch(i.charAt(n)){case"d":v=D("d");break;case"D":w("D",d,p);break;case"o":k=D("o");break;case"m":_=D("m");break;case"M":_=w("M",g,m);break;case"y":f=D("y");break;case"@":u=new Date(D("@")),f=u.getFullYear(),_=u.getMonth()+1,v=u.getDate();break;case"!":u=new Date((D("!")-this._ticksTo1970)/1e4),f=u.getFullYear(),_=u.getMonth()+1,v=u.getDate();break;case"'":b("'")?M():y=!0;break;default:M()}if(a.length>c&&(o=a.substr(c),!/^\s+/.test(o)))throw"Extra/unparsed characters found in date: "+o;if(-1===f?f=(new Date).getFullYear():100>f&&(f+=(new Date).getFullYear()-(new Date).getFullYear()%100+(l>=f?0:-100)),k>-1)for(_=1,v=k;;){if(r=this._getDaysInMonth(f,_-1),r>=v)break;_++,v-=r}if(u=this._daylightSavingAdjust(new Date(f,_-1,v)),u.getFullYear()!==f||u.getMonth()+1!==_||u.getDate()!==v)throw"Invalid date";return u},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:1e7*60*60*24*(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925)),formatDate:function(e,t,i){if(!t)return"";var a,s=(i?i.dayNamesShort:null)||this._defaults.dayNamesShort,n=(i?i.dayNames:null)||this._defaults.dayNames,r=(i?i.monthNamesShort:null)||this._defaults.monthNamesShort,o=(i?i.monthNames:null)||this._defaults.monthNames,u=function(t){var i=e.length>a+1&&e.charAt(a+1)===t;return i&&a++,i},c=function(e,t,i){var a=""+t;if(u(e))for(;i>a.length;)a="0"+a;return a},h=function(e,t,i,a){return u(e)?a[t]:i[t]},l="",d=!1;if(t)for(a=0;e.length>a;a++)if(d)"'"!==e.charAt(a)||u("'")?l+=e.charAt(a):d=!1;else switch(e.charAt(a)){case"d":l+=c("d",t.getDate(),2);break;case"D":l+=h("D",t.getDay(),s,n);break;case"o":l+=c("o",Math.round((new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime()-new Date(t.getFullYear(),0,0).getTime())/864e5),3);break;case"m":l+=c("m",t.getMonth()+1,2);break;case"M":l+=h("M",t.getMonth(),r,o);break;case"y":l+=u("y")?t.getFullYear():(10>t.getYear()%100?"0":"")+t.getYear()%100;break;case"@":l+=t.getTime();break;case"!":l+=1e4*t.getTime()+this._ticksTo1970;break;case"'":u("'")?l+="'":d=!0;break;default:l+=e.charAt(a)}return l},_possibleChars:function(e){var t,i="",a=!1,s=function(i){var a=e.length>t+1&&e.charAt(t+1)===i;return a&&t++,a};for(t=0;e.length>t;t++)if(a)"'"!==e.charAt(t)||s("'")?i+=e.charAt(t):a=!1;else switch(e.charAt(t)){case"d":case"m":case"y":case"@":i+="0123456789";break;case"D":case"M":return null;case"'":s("'")?i+="'":a=!0;break;default:i+=e.charAt(t)}return i},_get:function(e,i){return e.settings[i]!==t?e.settings[i]:this._defaults[i]},_setDateFromField:function(e,t){if(e.input.val()!==e.lastVal){var i=this._get(e,"dateFormat"),a=e.lastVal=e.input?e.input.val():null,s=this._getDefaultDate(e),n=s,r=this._getFormatConfig(e);try{n=this.parseDate(i,a,r)||s}catch(o){a=t?"":a}e.selectedDay=n.getDate(),e.drawMonth=e.selectedMonth=n.getMonth(),e.drawYear=e.selectedYear=n.getFullYear(),e.currentDay=a?n.getDate():0,e.currentMonth=a?n.getMonth():0,e.currentYear=a?n.getFullYear():0,this._adjustInstDate(e)}},_getDefaultDate:function(e){return this._restrictMinMax(e,this._determineDate(e,this._get(e,"defaultDate"),new Date))},_determineDate:function(t,i,a){var s=function(e){var t=new Date;return t.setDate(t.getDate()+e),t},n=function(i){try{return e.datepicker.parseDate(e.datepicker._get(t,"dateFormat"),i,e.datepicker._getFormatConfig(t))}catch(a){}for(var s=(i.toLowerCase().match(/^c/)?e.datepicker._getDate(t):null)||new Date,n=s.getFullYear(),r=s.getMonth(),o=s.getDate(),u=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,c=u.exec(i);c;){switch(c[2]||"d"){case"d":case"D":o+=parseInt(c[1],10);break;case"w":case"W":o+=7*parseInt(c[1],10);break;case"m":case"M":r+=parseInt(c[1],10),o=Math.min(o,e.datepicker._getDaysInMonth(n,r));break;case"y":case"Y":n+=parseInt(c[1],10),o=Math.min(o,e.datepicker._getDaysInMonth(n,r))}c=u.exec(i)}return new Date(n,r,o)},r=null==i||""===i?a:"string"==typeof i?n(i):"number"==typeof i?isNaN(i)?a:s(i):new Date(i.getTime());return r=r&&"Invalid Date"==""+r?a:r,r&&(r.setHours(0),r.setMinutes(0),r.setSeconds(0),r.setMilliseconds(0)),this._daylightSavingAdjust(r)},_daylightSavingAdjust:function(e){return e?(e.setHours(e.getHours()>12?e.getHours()+2:0),e):null},_setDate:function(e,t,i){var a=!t,s=e.selectedMonth,n=e.selectedYear,r=this._restrictMinMax(e,this._determineDate(e,t,new Date));e.selectedDay=e.currentDay=r.getDate(),e.drawMonth=e.selectedMonth=e.currentMonth=r.getMonth(),e.drawYear=e.selectedYear=e.currentYear=r.getFullYear(),s===e.selectedMonth&&n===e.selectedYear||i||this._notifyChange(e),this._adjustInstDate(e),e.input&&e.input.val(a?"":this._formatDate(e))},_getDate:function(e){var t=!e.currentYear||e.input&&""===e.input.val()?null:this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return t},_attachHandlers:function(t){var i=this._get(t,"stepMonths"),a="#"+t.id.replace(/\\\\/g,"\\");t.dpDiv.find("[data-handler]").map(function(){var t={prev:function(){e.datepicker._adjustDate(a,-i,"M")},next:function(){e.datepicker._adjustDate(a,+i,"M")},hide:function(){e.datepicker._hideDatepicker()},today:function(){e.datepicker._gotoToday(a)},selectDay:function(){return e.datepicker._selectDay(a,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return e.datepicker._selectMonthYear(a,this,"M"),!1},selectYear:function(){return e.datepicker._selectMonthYear(a,this,"Y"),!1}};e(this).bind(this.getAttribute("data-event"),t[this.getAttribute("data-handler")])})},_generateHTML:function(e){var t,i,a,s,n,r,o,u,c,h,l,d,p,g,m,f,_,v,k,y,b,D,w,M,C,x,I,N,T,A,E,S,Y,F,P,O,j,K,R,H=new Date,W=this._daylightSavingAdjust(new Date(H.getFullYear(),H.getMonth(),H.getDate())),L=this._get(e,"isRTL"),U=this._get(e,"showButtonPanel"),B=this._get(e,"hideIfNoPrevNext"),z=this._get(e,"navigationAsDateFormat"),q=this._getNumberOfMonths(e),G=this._get(e,"showCurrentAtPos"),J=this._get(e,"stepMonths"),Q=1!==q[0]||1!==q[1],V=this._daylightSavingAdjust(e.currentDay?new Date(e.currentYear,e.currentMonth,e.currentDay):new Date(9999,9,9)),$=this._getMinMaxDate(e,"min"),X=this._getMinMaxDate(e,"max"),Z=e.drawMonth-G,et=e.drawYear;if(0>Z&&(Z+=12,et--),X)for(t=this._daylightSavingAdjust(new Date(X.getFullYear(),X.getMonth()-q[0]*q[1]+1,X.getDate())),t=$&&$>t?$:t;this._daylightSavingAdjust(new Date(et,Z,1))>t;)Z--,0>Z&&(Z=11,et--);for(e.drawMonth=Z,e.drawYear=et,i=this._get(e,"prevText"),i=z?this.formatDate(i,this._daylightSavingAdjust(new Date(et,Z-J,1)),this._getFormatConfig(e)):i,a=this._canAdjustMonth(e,-1,et,Z)?"<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"e":"w")+"'>"+i+"</span></a>":B?"":"<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"e":"w")+"'>"+i+"</span></a>",s=this._get(e,"nextText"),s=z?this.formatDate(s,this._daylightSavingAdjust(new Date(et,Z+J,1)),this._getFormatConfig(e)):s,n=this._canAdjustMonth(e,1,et,Z)?"<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click' title='"+s+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"w":"e")+"'>"+s+"</span></a>":B?"":"<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+s+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"w":"e")+"'>"+s+"</span></a>",r=this._get(e,"currentText"),o=this._get(e,"gotoCurrent")&&e.currentDay?V:W,r=z?this.formatDate(r,o,this._getFormatConfig(e)):r,u=e.inline?"":"<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>"+this._get(e,"closeText")+"</button>",c=U?"<div class='ui-datepicker-buttonpane ui-widget-content'>"+(L?u:"")+(this._isInRange(e,o)?"<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'>"+r+"</button>":"")+(L?"":u)+"</div>":"",h=parseInt(this._get(e,"firstDay"),10),h=isNaN(h)?0:h,l=this._get(e,"showWeek"),d=this._get(e,"dayNames"),p=this._get(e,"dayNamesMin"),g=this._get(e,"monthNames"),m=this._get(e,"monthNamesShort"),f=this._get(e,"beforeShowDay"),_=this._get(e,"showOtherMonths"),v=this._get(e,"selectOtherMonths"),k=this._getDefaultDate(e),y="",D=0;q[0]>D;D++){for(w="",this.maxRows=4,M=0;q[1]>M;M++){if(C=this._daylightSavingAdjust(new Date(et,Z,e.selectedDay)),x=" ui-corner-all",I="",Q){if(I+="<div class='ui-datepicker-group",q[1]>1)switch(M){case 0:I+=" ui-datepicker-group-first",x=" ui-corner-"+(L?"right":"left");break;case q[1]-1:I+=" ui-datepicker-group-last",x=" ui-corner-"+(L?"left":"right");break;default:I+=" ui-datepicker-group-middle",x=""}I+="'>"}for(I+="<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix"+x+"'>"+(/all|left/.test(x)&&0===D?L?n:a:"")+(/all|right/.test(x)&&0===D?L?a:n:"")+this._generateMonthYearHeader(e,Z,et,$,X,D>0||M>0,g,m)+"</div><table class='ui-datepicker-calendar'><thead>"+"<tr>",N=l?"<th class='ui-datepicker-week-col'>"+this._get(e,"weekHeader")+"</th>":"",b=0;7>b;b++)T=(b+h)%7,N+="<th"+((b+h+6)%7>=5?" class='ui-datepicker-week-end'":"")+">"+"<span title='"+d[T]+"'>"+p[T]+"</span></th>";for(I+=N+"</tr></thead><tbody>",A=this._getDaysInMonth(et,Z),et===e.selectedYear&&Z===e.selectedMonth&&(e.selectedDay=Math.min(e.selectedDay,A)),E=(this._getFirstDayOfMonth(et,Z)-h+7)%7,S=Math.ceil((E+A)/7),Y=Q?this.maxRows>S?this.maxRows:S:S,this.maxRows=Y,F=this._daylightSavingAdjust(new Date(et,Z,1-E)),P=0;Y>P;P++){for(I+="<tr>",O=l?"<td class='ui-datepicker-week-col'>"+this._get(e,"calculateWeek")(F)+"</td>":"",b=0;7>b;b++)j=f?f.apply(e.input?e.input[0]:null,[F]):[!0,""],K=F.getMonth()!==Z,R=K&&!v||!j[0]||$&&$>F||X&&F>X,O+="<td class='"+((b+h+6)%7>=5?" ui-datepicker-week-end":"")+(K?" ui-datepicker-other-month":"")+(F.getTime()===C.getTime()&&Z===e.selectedMonth&&e._keyEvent||k.getTime()===F.getTime()&&k.getTime()===C.getTime()?" "+this._dayOverClass:"")+(R?" "+this._unselectableClass+" ui-state-disabled":"")+(K&&!_?"":" "+j[1]+(F.getTime()===V.getTime()?" "+this._currentClass:"")+(F.getTime()===W.getTime()?" ui-datepicker-today":""))+"'"+(K&&!_||!j[2]?"":" title='"+j[2].replace(/'/g,"&#39;")+"'")+(R?"":" data-handler='selectDay' data-event='click' data-month='"+F.getMonth()+"' data-year='"+F.getFullYear()+"'")+">"+(K&&!_?"&#xa0;":R?"<span class='ui-state-default'>"+F.getDate()+"</span>":"<a class='ui-state-default"+(F.getTime()===W.getTime()?" ui-state-highlight":"")+(F.getTime()===V.getTime()?" ui-state-active":"")+(K?" ui-priority-secondary":"")+"' href='#'>"+F.getDate()+"</a>")+"</td>",F.setDate(F.getDate()+1),F=this._daylightSavingAdjust(F);I+=O+"</tr>"}Z++,Z>11&&(Z=0,et++),I+="</tbody></table>"+(Q?"</div>"+(q[0]>0&&M===q[1]-1?"<div class='ui-datepicker-row-break'></div>":""):""),w+=I}y+=w}return y+=c,e._keyEvent=!1,y},_generateMonthYearHeader:function(e,t,i,a,s,n,r,o){var u,c,h,l,d,p,g,m,f=this._get(e,"changeMonth"),_=this._get(e,"changeYear"),v=this._get(e,"showMonthAfterYear"),k="<div class='ui-datepicker-title'>",y="";if(n||!f)y+="<span class='ui-datepicker-month'>"+r[t]+"</span>";else{for(u=a&&a.getFullYear()===i,c=s&&s.getFullYear()===i,y+="<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>",h=0;12>h;h++)(!u||h>=a.getMonth())&&(!c||s.getMonth()>=h)&&(y+="<option value='"+h+"'"+(h===t?" selected='selected'":"")+">"+o[h]+"</option>");y+="</select>"}if(v||(k+=y+(!n&&f&&_?"":"&#xa0;")),!e.yearshtml)if(e.yearshtml="",n||!_)k+="<span class='ui-datepicker-year'>"+i+"</span>";else{for(l=this._get(e,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(e){var t=e.match(/c[+\-].*/)?i+parseInt(e.substring(1),10):e.match(/[+\-].*/)?d+parseInt(e,10):parseInt(e,10);
+return isNaN(t)?d:t},g=p(l[0]),m=Math.max(g,p(l[1]||"")),g=a?Math.max(g,a.getFullYear()):g,m=s?Math.min(m,s.getFullYear()):m,e.yearshtml+="<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>";m>=g;g++)e.yearshtml+="<option value='"+g+"'"+(g===i?" selected='selected'":"")+">"+g+"</option>";e.yearshtml+="</select>",k+=e.yearshtml,e.yearshtml=null}return k+=this._get(e,"yearSuffix"),v&&(k+=(!n&&f&&_?"":"&#xa0;")+y),k+="</div>"},_adjustInstDate:function(e,t,i){var a=e.drawYear+("Y"===i?t:0),s=e.drawMonth+("M"===i?t:0),n=Math.min(e.selectedDay,this._getDaysInMonth(a,s))+("D"===i?t:0),r=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(a,s,n)));e.selectedDay=r.getDate(),e.drawMonth=e.selectedMonth=r.getMonth(),e.drawYear=e.selectedYear=r.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(e)},_restrictMinMax:function(e,t){var i=this._getMinMaxDate(e,"min"),a=this._getMinMaxDate(e,"max"),s=i&&i>t?i:t;return a&&s>a?a:s},_notifyChange:function(e){var t=this._get(e,"onChangeMonthYear");t&&t.apply(e.input?e.input[0]:null,[e.selectedYear,e.selectedMonth+1,e])},_getNumberOfMonths:function(e){var t=this._get(e,"numberOfMonths");return null==t?[1,1]:"number"==typeof t?[1,t]:t},_getMinMaxDate:function(e,t){return this._determineDate(e,this._get(e,t+"Date"),null)},_getDaysInMonth:function(e,t){return 32-this._daylightSavingAdjust(new Date(e,t,32)).getDate()},_getFirstDayOfMonth:function(e,t){return new Date(e,t,1).getDay()},_canAdjustMonth:function(e,t,i,a){var s=this._getNumberOfMonths(e),n=this._daylightSavingAdjust(new Date(i,a+(0>t?t:s[0]*s[1]),1));return 0>t&&n.setDate(this._getDaysInMonth(n.getFullYear(),n.getMonth())),this._isInRange(e,n)},_isInRange:function(e,t){var i,a,s=this._getMinMaxDate(e,"min"),n=this._getMinMaxDate(e,"max"),r=null,o=null,u=this._get(e,"yearRange");return u&&(i=u.split(":"),a=(new Date).getFullYear(),r=parseInt(i[0],10),o=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(r+=a),i[1].match(/[+\-].*/)&&(o+=a)),(!s||t.getTime()>=s.getTime())&&(!n||t.getTime()<=n.getTime())&&(!r||t.getFullYear()>=r)&&(!o||o>=t.getFullYear())},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return t="string"!=typeof t?t:(new Date).getFullYear()%100+parseInt(t,10),{shortYearCutoff:t,dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,i,a){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);var s=t?"object"==typeof t?t:this._daylightSavingAdjust(new Date(a,i,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return this.formatDate(this._get(e,"dateFormat"),s,this._getFormatConfig(e))}}),e.fn.datepicker=function(t){if(!this.length)return this;e.datepicker.initialized||(e(document).mousedown(e.datepicker._checkExternalClick),e.datepicker.initialized=!0),0===e("#"+e.datepicker._mainDivId).length&&e("body").append(e.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof t||"isDisabled"!==t&&"getDate"!==t&&"widget"!==t?"option"===t&&2===arguments.length&&"string"==typeof arguments[1]?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof t?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this].concat(i)):e.datepicker._attachDatepicker(this,t)}):e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i))},e.datepicker=new i,e.datepicker.initialized=!1,e.datepicker.uuid=(new Date).getTime(),e.datepicker.version="1.10.4"})(jQuery);(function(e){var t={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},i={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0};e.widget("ui.dialog",{version:"1.10.4",options:{appendTo:"body",autoOpen:!0,buttons:[],closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:null,maxWidth:null,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(t){var i=e(this).css(t).offset().top;0>i&&e(this).css("top",t.top-i)}},resizable:!0,show:null,title:null,width:300,beforeClose:null,close:null,drag:null,dragStart:null,dragStop:null,focus:null,open:null,resize:null,resizeStart:null,resizeStop:null},_create:function(){this.originalCss={display:this.element[0].style.display,width:this.element[0].style.width,minHeight:this.element[0].style.minHeight,maxHeight:this.element[0].style.maxHeight,height:this.element[0].style.height},this.originalPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.originalTitle=this.element.attr("title"),this.options.title=this.options.title||this.originalTitle,this._createWrapper(),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(this.uiDialog),this._createTitlebar(),this._createButtonPane(),this.options.draggable&&e.fn.draggable&&this._makeDraggable(),this.options.resizable&&e.fn.resizable&&this._makeResizable(),this._isOpen=!1},_init:function(){this.options.autoOpen&&this.open()},_appendTo:function(){var t=this.options.appendTo;return t&&(t.jquery||t.nodeType)?e(t):this.document.find(t||"body").eq(0)},_destroy:function(){var e,t=this.originalPosition;this._destroyOverlay(),this.element.removeUniqueId().removeClass("ui-dialog-content ui-widget-content").css(this.originalCss).detach(),this.uiDialog.stop(!0,!0).remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),e=t.parent.children().eq(t.index),e.length&&e[0]!==this.element[0]?e.before(this.element):t.parent.append(this.element)},widget:function(){return this.uiDialog},disable:e.noop,enable:e.noop,close:function(t){var i,a=this;if(this._isOpen&&this._trigger("beforeClose",t)!==!1){if(this._isOpen=!1,this._destroyOverlay(),!this.opener.filter(":focusable").focus().length)try{i=this.document[0].activeElement,i&&"body"!==i.nodeName.toLowerCase()&&e(i).blur()}catch(s){}this._hide(this.uiDialog,this.options.hide,function(){a._trigger("close",t)})}},isOpen:function(){return this._isOpen},moveToTop:function(){this._moveToTop()},_moveToTop:function(e,t){var i=!!this.uiDialog.nextAll(":visible").insertBefore(this.uiDialog).length;return i&&!t&&this._trigger("focus",e),i},open:function(){var t=this;return this._isOpen?(this._moveToTop()&&this._focusTabbable(),undefined):(this._isOpen=!0,this.opener=e(this.document[0].activeElement),this._size(),this._position(),this._createOverlay(),this._moveToTop(null,!0),this._show(this.uiDialog,this.options.show,function(){t._focusTabbable(),t._trigger("focus")}),this._trigger("open"),undefined)},_focusTabbable:function(){var e=this.element.find("[autofocus]");e.length||(e=this.element.find(":tabbable")),e.length||(e=this.uiDialogButtonPane.find(":tabbable")),e.length||(e=this.uiDialogTitlebarClose.filter(":tabbable")),e.length||(e=this.uiDialog),e.eq(0).focus()},_keepFocus:function(t){function i(){var t=this.document[0].activeElement,i=this.uiDialog[0]===t||e.contains(this.uiDialog[0],t);i||this._focusTabbable()}t.preventDefault(),i.call(this),this._delay(i)},_createWrapper:function(){this.uiDialog=e("<div>").addClass("ui-dialog ui-widget ui-widget-content ui-corner-all ui-front "+this.options.dialogClass).hide().attr({tabIndex:-1,role:"dialog"}).appendTo(this._appendTo()),this._on(this.uiDialog,{keydown:function(t){if(this.options.closeOnEscape&&!t.isDefaultPrevented()&&t.keyCode&&t.keyCode===e.ui.keyCode.ESCAPE)return t.preventDefault(),this.close(t),undefined;if(t.keyCode===e.ui.keyCode.TAB){var i=this.uiDialog.find(":tabbable"),a=i.filter(":first"),s=i.filter(":last");t.target!==s[0]&&t.target!==this.uiDialog[0]||t.shiftKey?t.target!==a[0]&&t.target!==this.uiDialog[0]||!t.shiftKey||(s.focus(1),t.preventDefault()):(a.focus(1),t.preventDefault())}},mousedown:function(e){this._moveToTop(e)&&this._focusTabbable()}}),this.element.find("[aria-describedby]").length||this.uiDialog.attr({"aria-describedby":this.element.uniqueId().attr("id")})},_createTitlebar:function(){var t;this.uiDialogTitlebar=e("<div>").addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(this.uiDialog),this._on(this.uiDialogTitlebar,{mousedown:function(t){e(t.target).closest(".ui-dialog-titlebar-close")||this.uiDialog.focus()}}),this.uiDialogTitlebarClose=e("<button type='button'></button>").button({label:this.options.closeText,icons:{primary:"ui-icon-closethick"},text:!1}).addClass("ui-dialog-titlebar-close").appendTo(this.uiDialogTitlebar),this._on(this.uiDialogTitlebarClose,{click:function(e){e.preventDefault(),this.close(e)}}),t=e("<span>").uniqueId().addClass("ui-dialog-title").prependTo(this.uiDialogTitlebar),this._title(t),this.uiDialog.attr({"aria-labelledby":t.attr("id")})},_title:function(e){this.options.title||e.html("&#160;"),e.text(this.options.title)},_createButtonPane:function(){this.uiDialogButtonPane=e("<div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),this.uiButtonSet=e("<div>").addClass("ui-dialog-buttonset").appendTo(this.uiDialogButtonPane),this._createButtons()},_createButtons:function(){var t=this,i=this.options.buttons;return this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),e.isEmptyObject(i)||e.isArray(i)&&!i.length?(this.uiDialog.removeClass("ui-dialog-buttons"),undefined):(e.each(i,function(i,a){var s,n;a=e.isFunction(a)?{click:a,text:i}:a,a=e.extend({type:"button"},a),s=a.click,a.click=function(){s.apply(t.element[0],arguments)},n={icons:a.icons,text:a.showText},delete a.icons,delete a.showText,e("<button></button>",a).button(n).appendTo(t.uiButtonSet)}),this.uiDialog.addClass("ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog),undefined)},_makeDraggable:function(){function t(e){return{position:e.position,offset:e.offset}}var i=this,a=this.options;this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(a,s){e(this).addClass("ui-dialog-dragging"),i._blockFrames(),i._trigger("dragStart",a,t(s))},drag:function(e,a){i._trigger("drag",e,t(a))},stop:function(s,n){a.position=[n.position.left-i.document.scrollLeft(),n.position.top-i.document.scrollTop()],e(this).removeClass("ui-dialog-dragging"),i._unblockFrames(),i._trigger("dragStop",s,t(n))}})},_makeResizable:function(){function t(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}var i=this,a=this.options,s=a.resizable,n=this.uiDialog.css("position"),r="string"==typeof s?s:"n,e,s,w,se,sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:a.maxWidth,maxHeight:a.maxHeight,minWidth:a.minWidth,minHeight:this._minHeight(),handles:r,start:function(a,s){e(this).addClass("ui-dialog-resizing"),i._blockFrames(),i._trigger("resizeStart",a,t(s))},resize:function(e,a){i._trigger("resize",e,t(a))},stop:function(s,n){a.height=e(this).height(),a.width=e(this).width(),e(this).removeClass("ui-dialog-resizing"),i._unblockFrames(),i._trigger("resizeStop",s,t(n))}}).css("position",n)},_minHeight:function(){var e=this.options;return"auto"===e.height?e.minHeight:Math.min(e.minHeight,e.height)},_position:function(){var e=this.uiDialog.is(":visible");e||this.uiDialog.show(),this.uiDialog.position(this.options.position),e||this.uiDialog.hide()},_setOptions:function(a){var s=this,n=!1,r={};e.each(a,function(e,a){s._setOption(e,a),e in t&&(n=!0),e in i&&(r[e]=a)}),n&&(this._size(),this._position()),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option",r)},_setOption:function(e,t){var i,a,s=this.uiDialog;"dialogClass"===e&&s.removeClass(this.options.dialogClass).addClass(t),"disabled"!==e&&(this._super(e,t),"appendTo"===e&&this.uiDialog.appendTo(this._appendTo()),"buttons"===e&&this._createButtons(),"closeText"===e&&this.uiDialogTitlebarClose.button({label:""+t}),"draggable"===e&&(i=s.is(":data(ui-draggable)"),i&&!t&&s.draggable("destroy"),!i&&t&&this._makeDraggable()),"position"===e&&this._position(),"resizable"===e&&(a=s.is(":data(ui-resizable)"),a&&!t&&s.resizable("destroy"),a&&"string"==typeof t&&s.resizable("option","handles",t),a||t===!1||this._makeResizable()),"title"===e&&this._title(this.uiDialogTitlebar.find(".ui-dialog-title")))},_size:function(){var e,t,i,a=this.options;this.element.show().css({width:"auto",minHeight:0,maxHeight:"none",height:0}),a.minWidth>a.width&&(a.width=a.minWidth),e=this.uiDialog.css({height:"auto",width:a.width}).outerHeight(),t=Math.max(0,a.minHeight-e),i="number"==typeof a.maxHeight?Math.max(0,a.maxHeight-e):"none","auto"===a.height?this.element.css({minHeight:t,maxHeight:i,height:"auto"}):this.element.height(Math.max(0,a.height-e)),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())},_blockFrames:function(){this.iframeBlocks=this.document.find("iframe").map(function(){var t=e(this);return e("<div>").css({position:"absolute",width:t.outerWidth(),height:t.outerHeight()}).appendTo(t.parent()).offset(t.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_allowInteraction:function(t){return e(t.target).closest(".ui-dialog").length?!0:!!e(t.target).closest(".ui-datepicker").length},_createOverlay:function(){if(this.options.modal){var t=this,i=this.widgetFullName;e.ui.dialog.overlayInstances||this._delay(function(){e.ui.dialog.overlayInstances&&this.document.bind("focusin.dialog",function(a){t._allowInteraction(a)||(a.preventDefault(),e(".ui-dialog:visible:last .ui-dialog-content").data(i)._focusTabbable())})}),this.overlay=e("<div>").addClass("ui-widget-overlay ui-front").appendTo(this._appendTo()),this._on(this.overlay,{mousedown:"_keepFocus"}),e.ui.dialog.overlayInstances++}},_destroyOverlay:function(){this.options.modal&&this.overlay&&(e.ui.dialog.overlayInstances--,e.ui.dialog.overlayInstances||this.document.unbind("focusin.dialog"),this.overlay.remove(),this.overlay=null)}}),e.ui.dialog.overlayInstances=0,e.uiBackCompat!==!1&&e.widget("ui.dialog",e.ui.dialog,{_position:function(){var t,i=this.options.position,a=[],s=[0,0];i?(("string"==typeof i||"object"==typeof i&&"0"in i)&&(a=i.split?i.split(" "):[i[0],i[1]],1===a.length&&(a[1]=a[0]),e.each(["left","top"],function(e,t){+a[e]===a[e]&&(s[e]=a[e],a[e]=t)}),i={my:a[0]+(0>s[0]?s[0]:"+"+s[0])+" "+a[1]+(0>s[1]?s[1]:"+"+s[1]),at:a.join(" ")}),i=e.extend({},e.ui.dialog.prototype.options.position,i)):i=e.ui.dialog.prototype.options.position,t=this.uiDialog.is(":visible"),t||this.uiDialog.show(),this.uiDialog.position(i),t||this.uiDialog.hide()}})})(jQuery);(function(t){t.widget("ui.menu",{version:"1.10.4",defaultElement:"<ul>",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,t.proxy(function(t){this.options.disabled&&t.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(t){t.preventDefault()},"click .ui-state-disabled > a":function(t){t.preventDefault()},"click .ui-menu-item:has(a)":function(e){var i=t(e.target).closest(".ui-menu-item");!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&t(this.document[0].activeElement).closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){var i=t(e.currentTarget);i.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(e,i)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.children(".ui-menu-item").eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){t.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(e){t(e.target).closest(".ui-menu").length||this.collapseAll(e),this.mouseHandled=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var e=t(this);e.data("ui-menu-submenu-carat")&&e.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(e){function i(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var s,n,a,o,r,l=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:l=!1,n=this.previousFilter||"",a=String.fromCharCode(e.keyCode),o=!1,clearTimeout(this.filterTimer),a===n?o=!0:a=n+a,r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())}),s=o&&-1!==s.index(this.active.next())?this.active.nextAll(".ui-menu-item"):s,s.length||(a=String.fromCharCode(e.keyCode),r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())})),s.length?(this.focus(e,s),s.length>1?(this.previousFilter=a,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}l&&e.preventDefault()},_activate:function(t){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i=this.options.icons.submenu,s=this.element.find(this.options.menus);this.element.toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length),s.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),s=e.prev("a"),n=t("<span>").addClass("ui-menu-icon ui-icon "+i).data("ui-menu-submenu-carat",!0);s.attr("aria-haspopup","true").prepend(n),e.attr("aria-labelledby",s.attr("id"))}),e=s.add(this.element),e.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),e.children(":not(.ui-menu-item)").each(function(){var e=t(this);/[^\-\u2014\u2013\s]/.test(e.text())||e.addClass("ui-widget-content ui-menu-divider")}),e.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){"icons"===t&&this.element.find(".ui-menu-icon").removeClass(this.options.icons.submenu).addClass(e.submenu),this._super(t,e)},focus:function(t,e){var i,s;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,a,o,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,a=this.activeMenu.scrollTop(),o=this.activeMenu.height(),r=e.height(),0>n?this.activeMenu.scrollTop(a+n):n+r>o&&this.activeMenu.scrollTop(a+n-o+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",t,{item:this.active}))},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.children(".ui-menu-item")[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())),undefined):(this.next(e),undefined)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item").first())),undefined):(this.next(e),undefined)},_hasScroll:function(){return this.element.outerHeight()<this.element.prop("scrollHeight")},select:function(e){this.active=this.active||t(e.target).closest(".ui-menu-item");var i={item:this.active};this.active.has(".ui-menu").length||this.collapseAll(e,!0),this._trigger("select",e,i)}})})(jQuery);(function(t,e){t.widget("ui.progressbar",{version:"1.10.4",options:{max:100,value:0,change:null,complete:null},min:0,_create:function(){this.oldValue=this.options.value=this._constrainedValue(),this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min}),this.valueDiv=t("<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>").appendTo(this.element),this._refreshValue()},_destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove()},value:function(t){return t===e?this.options.value:(this.options.value=this._constrainedValue(t),this._refreshValue(),e)},_constrainedValue:function(t){return t===e&&(t=this.options.value),this.indeterminate=t===!1,"number"!=typeof t&&(t=0),this.indeterminate?!1:Math.min(this.options.max,Math.max(this.min,t))},_setOptions:function(t){var e=t.value;delete t.value,this._super(t),this.options.value=this._constrainedValue(e),this._refreshValue()},_setOption:function(t,e){"max"===t&&(e=Math.max(this.min,e)),this._super(t,e)},_percentage:function(){return this.indeterminate?100:100*(this.options.value-this.min)/(this.options.max-this.min)},_refreshValue:function(){var e=this.options.value,i=this._percentage();this.valueDiv.toggle(this.indeterminate||e>this.min).toggleClass("ui-corner-right",e===this.options.max).width(i.toFixed(0)+"%"),this.element.toggleClass("ui-progressbar-indeterminate",this.indeterminate),this.indeterminate?(this.element.removeAttr("aria-valuenow"),this.overlayDiv||(this.overlayDiv=t("<div class='ui-progressbar-overlay'></div>").appendTo(this.valueDiv))):(this.element.attr({"aria-valuemax":this.options.max,"aria-valuenow":e}),this.overlayDiv&&(this.overlayDiv.remove(),this.overlayDiv=null)),this.oldValue!==e&&(this.oldValue=e,this._trigger("change")),e===this.options.max&&this._trigger("complete")}})})(jQuery);(function(t){var e=5;t.widget("ui.slider",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null,change:null,slide:null,start:null,stop:null},_create:function(){this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"),this._refresh(),this._setOption("disabled",this.options.disabled),this._animateOff=!1},_refresh:function(){this._createRange(),this._createHandles(),this._setupEvents(),this._refreshValue()},_createHandles:function(){var e,i,s=this.options,n=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),a="<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>",o=[];for(i=s.values&&s.values.length||1,n.length>i&&(n.slice(i).remove(),n=n.slice(0,i)),e=n.length;i>e;e++)o.push(a);this.handles=n.add(t(o.join("")).appendTo(this.element)),this.handle=this.handles.eq(0),this.handles.each(function(e){t(this).data("ui-slider-handle-index",e)})},_createRange:function(){var e=this.options,i="";e.range?(e.range===!0&&(e.values?e.values.length&&2!==e.values.length?e.values=[e.values[0],e.values[0]]:t.isArray(e.values)&&(e.values=e.values.slice(0)):e.values=[this._valueMin(),this._valueMin()]),this.range&&this.range.length?this.range.removeClass("ui-slider-range-min ui-slider-range-max").css({left:"",bottom:""}):(this.range=t("<div></div>").appendTo(this.element),i="ui-slider-range ui-widget-header ui-corner-all"),this.range.addClass(i+("min"===e.range||"max"===e.range?" ui-slider-range-"+e.range:""))):(this.range&&this.range.remove(),this.range=null)},_setupEvents:function(){var t=this.handles.add(this.range).filter("a");this._off(t),this._on(t,this._handleEvents),this._hoverable(t),this._focusable(t)},_destroy:function(){this.handles.remove(),this.range&&this.range.remove(),this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-widget ui-widget-content ui-corner-all"),this._mouseDestroy()},_mouseCapture:function(e){var i,s,n,a,o,r,l,h,u=this,c=this.options;return c.disabled?!1:(this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()},this.elementOffset=this.element.offset(),i={x:e.pageX,y:e.pageY},s=this._normValueFromMouse(i),n=this._valueMax()-this._valueMin()+1,this.handles.each(function(e){var i=Math.abs(s-u.values(e));(n>i||n===i&&(e===u._lastChangedValue||u.values(e)===c.min))&&(n=i,a=t(this),o=e)}),r=this._start(e,o),r===!1?!1:(this._mouseSliding=!0,this._handleIndex=o,a.addClass("ui-state-active").focus(),l=a.offset(),h=!t(e.target).parents().addBack().is(".ui-slider-handle"),this._clickOffset=h?{left:0,top:0}:{left:e.pageX-l.left-a.width()/2,top:e.pageY-l.top-a.height()/2-(parseInt(a.css("borderTopWidth"),10)||0)-(parseInt(a.css("borderBottomWidth"),10)||0)+(parseInt(a.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(e,o,s),this._animateOff=!0,!0))},_mouseStart:function(){return!0},_mouseDrag:function(t){var e={x:t.pageX,y:t.pageY},i=this._normValueFromMouse(e);return this._slide(t,this._handleIndex,i),!1},_mouseStop:function(t){return this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(t,this._handleIndex),this._change(t,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1,!1},_detectOrientation:function(){this.orientation="vertical"===this.options.orientation?"vertical":"horizontal"},_normValueFromMouse:function(t){var e,i,s,n,a;return"horizontal"===this.orientation?(e=this.elementSize.width,i=t.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(e=this.elementSize.height,i=t.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),s=i/e,s>1&&(s=1),0>s&&(s=0),"vertical"===this.orientation&&(s=1-s),n=this._valueMax()-this._valueMin(),a=this._valueMin()+s*n,this._trimAlignValue(a)},_start:function(t,e){var i={handle:this.handles[e],value:this.value()};return this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._trigger("start",t,i)},_slide:function(t,e,i){var s,n,a;this.options.values&&this.options.values.length?(s=this.values(e?0:1),2===this.options.values.length&&this.options.range===!0&&(0===e&&i>s||1===e&&s>i)&&(i=s),i!==this.values(e)&&(n=this.values(),n[e]=i,a=this._trigger("slide",t,{handle:this.handles[e],value:i,values:n}),s=this.values(e?0:1),a!==!1&&this.values(e,i))):i!==this.value()&&(a=this._trigger("slide",t,{handle:this.handles[e],value:i}),a!==!1&&this.value(i))},_stop:function(t,e){var i={handle:this.handles[e],value:this.value()};this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._trigger("stop",t,i)},_change:function(t,e){if(!this._keySliding&&!this._mouseSliding){var i={handle:this.handles[e],value:this.value()};this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._lastChangedValue=e,this._trigger("change",t,i)}},value:function(t){return arguments.length?(this.options.value=this._trimAlignValue(t),this._refreshValue(),this._change(null,0),undefined):this._value()},values:function(e,i){var s,n,a;if(arguments.length>1)return this.options.values[e]=this._trimAlignValue(i),this._refreshValue(),this._change(null,e),undefined;if(!arguments.length)return this._values();if(!t.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(e):this.value();for(s=this.options.values,n=arguments[0],a=0;s.length>a;a+=1)s[a]=this._trimAlignValue(n[a]),this._change(null,a);this._refreshValue()},_setOption:function(e,i){var s,n=0;switch("range"===e&&this.options.range===!0&&("min"===i?(this.options.value=this._values(0),this.options.values=null):"max"===i&&(this.options.value=this._values(this.options.values.length-1),this.options.values=null)),t.isArray(this.options.values)&&(n=this.options.values.length),t.Widget.prototype._setOption.apply(this,arguments),e){case"orientation":this._detectOrientation(),this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation),this._refreshValue();break;case"value":this._animateOff=!0,this._refreshValue(),this._change(null,0),this._animateOff=!1;break;case"values":for(this._animateOff=!0,this._refreshValue(),s=0;n>s;s+=1)this._change(null,s);this._animateOff=!1;break;case"min":case"max":this._animateOff=!0,this._refreshValue(),this._animateOff=!1;break;case"range":this._animateOff=!0,this._refresh(),this._animateOff=!1}},_value:function(){var t=this.options.value;return t=this._trimAlignValue(t)},_values:function(t){var e,i,s;if(arguments.length)return e=this.options.values[t],e=this._trimAlignValue(e);if(this.options.values&&this.options.values.length){for(i=this.options.values.slice(),s=0;i.length>s;s+=1)i[s]=this._trimAlignValue(i[s]);return i}return[]},_trimAlignValue:function(t){if(this._valueMin()>=t)return this._valueMin();if(t>=this._valueMax())return this._valueMax();var e=this.options.step>0?this.options.step:1,i=(t-this._valueMin())%e,s=t-i;return 2*Math.abs(i)>=e&&(s+=i>0?e:-e),parseFloat(s.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var e,i,s,n,a,o=this.options.range,r=this.options,l=this,h=this._animateOff?!1:r.animate,u={};this.options.values&&this.options.values.length?this.handles.each(function(s){i=100*((l.values(s)-l._valueMin())/(l._valueMax()-l._valueMin())),u["horizontal"===l.orientation?"left":"bottom"]=i+"%",t(this).stop(1,1)[h?"animate":"css"](u,r.animate),l.options.range===!0&&("horizontal"===l.orientation?(0===s&&l.range.stop(1,1)[h?"animate":"css"]({left:i+"%"},r.animate),1===s&&l.range[h?"animate":"css"]({width:i-e+"%"},{queue:!1,duration:r.animate})):(0===s&&l.range.stop(1,1)[h?"animate":"css"]({bottom:i+"%"},r.animate),1===s&&l.range[h?"animate":"css"]({height:i-e+"%"},{queue:!1,duration:r.animate}))),e=i}):(s=this.value(),n=this._valueMin(),a=this._valueMax(),i=a!==n?100*((s-n)/(a-n)):0,u["horizontal"===this.orientation?"left":"bottom"]=i+"%",this.handle.stop(1,1)[h?"animate":"css"](u,r.animate),"min"===o&&"horizontal"===this.orientation&&this.range.stop(1,1)[h?"animate":"css"]({width:i+"%"},r.animate),"max"===o&&"horizontal"===this.orientation&&this.range[h?"animate":"css"]({width:100-i+"%"},{queue:!1,duration:r.animate}),"min"===o&&"vertical"===this.orientation&&this.range.stop(1,1)[h?"animate":"css"]({height:i+"%"},r.animate),"max"===o&&"vertical"===this.orientation&&this.range[h?"animate":"css"]({height:100-i+"%"},{queue:!1,duration:r.animate}))},_handleEvents:{keydown:function(i){var s,n,a,o,r=t(i.target).data("ui-slider-handle-index");switch(i.keyCode){case t.ui.keyCode.HOME:case t.ui.keyCode.END:case t.ui.keyCode.PAGE_UP:case t.ui.keyCode.PAGE_DOWN:case t.ui.keyCode.UP:case t.ui.keyCode.RIGHT:case t.ui.keyCode.DOWN:case t.ui.keyCode.LEFT:if(i.preventDefault(),!this._keySliding&&(this._keySliding=!0,t(i.target).addClass("ui-state-active"),s=this._start(i,r),s===!1))return}switch(o=this.options.step,n=a=this.options.values&&this.options.values.length?this.values(r):this.value(),i.keyCode){case t.ui.keyCode.HOME:a=this._valueMin();break;case t.ui.keyCode.END:a=this._valueMax();break;case t.ui.keyCode.PAGE_UP:a=this._trimAlignValue(n+(this._valueMax()-this._valueMin())/e);break;case t.ui.keyCode.PAGE_DOWN:a=this._trimAlignValue(n-(this._valueMax()-this._valueMin())/e);break;case t.ui.keyCode.UP:case t.ui.keyCode.RIGHT:if(n===this._valueMax())return;a=this._trimAlignValue(n+o);break;case t.ui.keyCode.DOWN:case t.ui.keyCode.LEFT:if(n===this._valueMin())return;a=this._trimAlignValue(n-o)}this._slide(i,r,a)},click:function(t){t.preventDefault()},keyup:function(e){var i=t(e.target).data("ui-slider-handle-index");this._keySliding&&(this._keySliding=!1,this._stop(e,i),this._change(e,i),t(e.target).removeClass("ui-state-active"))}}})})(jQuery);(function(t){function e(t){return function(){var e=this.element.val();t.apply(this,arguments),this._refresh(),e!==this.element.val()&&this._trigger("change")}}t.widget("ui.spinner",{version:"1.10.4",defaultElement:"<input>",widgetEventPrefix:"spin",options:{culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),""!==this.value()&&this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var e={},i=this.element;return t.each(["min","max","step"],function(t,s){var n=i.attr(s);void 0!==n&&n.length&&(e[s]=n)}),e},_events:{keydown:function(t){this._start(t)&&this._keydown(t)&&t.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(this._stop(),this._refresh(),this.previous!==this.element.val()&&this._trigger("change",t),void 0)},mousewheel:function(t,e){if(e){if(!this.spinning&&!this._start(t))return!1;this._spin((e>0?1:-1)*this.options.step,t),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(t)},100),t.preventDefault()}},"mousedown .ui-spinner-button":function(e){function i(){var t=this.element[0]===this.document[0].activeElement;t||(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s}))}var s;s=this.element[0]===this.document[0].activeElement?this.previous:this.element.val(),e.preventDefault(),i.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,i.call(this)}),this._start(e)!==!1&&this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(e){return t(e.currentTarget).hasClass("ui-state-active")?this._start(e)===!1?!1:(this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e),void 0):void 0},"mouseleave .ui-spinner-button":"_stop"},_draw:function(){var t=this.uiSpinner=this.element.addClass("ui-spinner-input").attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml());this.element.attr("role","spinbutton"),this.buttons=t.find(".ui-spinner-button").attr("tabIndex",-1).button().removeClass("ui-corner-all"),this.buttons.height()>Math.ceil(.5*t.height())&&t.height()>0&&t.height(t.height()),this.options.disabled&&this.disable()},_keydown:function(e){var i=this.options,s=t.ui.keyCode;switch(e.keyCode){case s.UP:return this._repeat(null,1,e),!0;case s.DOWN:return this._repeat(null,-1,e),!0;case s.PAGE_UP:return this._repeat(null,i.page,e),!0;case s.PAGE_DOWN:return this._repeat(null,-i.page,e),!0}return!1},_uiSpinnerHtml:function(){return"<span class='ui-spinner ui-widget ui-widget-content ui-corner-all'></span>"},_buttonHtml:function(){return"<a class='ui-spinner-button ui-spinner-up ui-corner-tr'><span class='ui-icon "+this.options.icons.up+"'>&#9650;</span>"+"</a>"+"<a class='ui-spinner-button ui-spinner-down ui-corner-br'>"+"<span class='ui-icon "+this.options.icons.down+"'>&#9660;</span>"+"</a>"},_start:function(t){return this.spinning||this._trigger("start",t)!==!1?(this.counter||(this.counter=1),this.spinning=!0,!0):!1},_repeat:function(t,e,i){t=t||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,e,i)},t),this._spin(e*this.options.step,i)},_spin:function(t,e){var i=this.value()||0;this.counter||(this.counter=1),i=this._adjustValue(i+t*this._increment(this.counter)),this.spinning&&this._trigger("spin",e,{value:i})===!1||(this._value(i),this.counter++)},_increment:function(e){var i=this.options.incremental;return i?t.isFunction(i)?i(e):Math.floor(e*e*e/5e4-e*e/500+17*e/200+1):1},_precision:function(){var t=this._precisionOf(this.options.step);return null!==this.options.min&&(t=Math.max(t,this._precisionOf(this.options.min))),t},_precisionOf:function(t){var e=""+t,i=e.indexOf(".");return-1===i?0:e.length-i-1},_adjustValue:function(t){var e,i,s=this.options;return e=null!==s.min?s.min:0,i=t-e,i=Math.round(i/s.step)*s.step,t=e+i,t=parseFloat(t.toFixed(this._precision())),null!==s.max&&t>s.max?s.max:null!==s.min&&s.min>t?s.min:t},_stop:function(t){this.spinning&&(clearTimeout(this.timer),clearTimeout(this.mousewheelTimer),this.counter=0,this.spinning=!1,this._trigger("stop",t))},_setOption:function(t,e){if("culture"===t||"numberFormat"===t){var i=this._parse(this.element.val());return this.options[t]=e,this.element.val(this._format(i)),void 0}("max"===t||"min"===t||"step"===t)&&"string"==typeof e&&(e=this._parse(e)),"icons"===t&&(this.buttons.first().find(".ui-icon").removeClass(this.options.icons.up).addClass(e.up),this.buttons.last().find(".ui-icon").removeClass(this.options.icons.down).addClass(e.down)),this._super(t,e),"disabled"===t&&(e?(this.element.prop("disabled",!0),this.buttons.button("disable")):(this.element.prop("disabled",!1),this.buttons.button("enable")))},_setOptions:e(function(t){this._super(t),this._value(this.element.val())}),_parse:function(t){return"string"==typeof t&&""!==t&&(t=window.Globalize&&this.options.numberFormat?Globalize.parseFloat(t,10,this.options.culture):+t),""===t||isNaN(t)?null:t},_format:function(t){return""===t?"":window.Globalize&&this.options.numberFormat?Globalize.format(t,this.options.numberFormat,this.options.culture):t},_refresh:function(){this.element.attr({"aria-valuemin":this.options.min,"aria-valuemax":this.options.max,"aria-valuenow":this._parse(this.element.val())})},_value:function(t,e){var i;""!==t&&(i=this._parse(t),null!==i&&(e||(i=this._adjustValue(i)),t=this._format(i))),this.element.val(t),this._refresh()},_destroy:function(){this.element.removeClass("ui-spinner-input").prop("disabled",!1).removeAttr("autocomplete").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.uiSpinner.replaceWith(this.element)},stepUp:e(function(t){this._stepUp(t)}),_stepUp:function(t){this._start()&&(this._spin((t||1)*this.options.step),this._stop())},stepDown:e(function(t){this._stepDown(t)}),_stepDown:function(t){this._start()&&(this._spin((t||1)*-this.options.step),this._stop())},pageUp:e(function(t){this._stepUp((t||1)*this.options.page)}),pageDown:e(function(t){this._stepDown((t||1)*this.options.page)}),value:function(t){return arguments.length?(e(this._value).call(this,t),void 0):this._parse(this.element.val())},widget:function(){return this.uiSpinner}})})(jQuery);(function(t,e){function i(){return++n}function s(t){return t=t.cloneNode(!1),t.hash.length>1&&decodeURIComponent(t.href.replace(a,""))===decodeURIComponent(location.href.replace(a,""))}var n=0,a=/#.*$/;t.widget("ui.tabs",{version:"1.10.4",delay:300,options:{active:null,collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_create:function(){var e=this,i=this.options;this.running=!1,this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all").toggleClass("ui-tabs-collapsible",i.collapsible).delegate(".ui-tabs-nav > li","mousedown"+this.eventNamespace,function(e){t(this).is(".ui-state-disabled")&&e.preventDefault()}).delegate(".ui-tabs-anchor","focus"+this.eventNamespace,function(){t(this).closest("li").is(".ui-state-disabled")&&this.blur()}),this._processTabs(),i.active=this._initialActive(),t.isArray(i.disabled)&&(i.disabled=t.unique(i.disabled.concat(t.map(this.tabs.filter(".ui-state-disabled"),function(t){return e.tabs.index(t)}))).sort()),this.active=this.options.active!==!1&&this.anchors.length?this._findActive(i.active):t(),this._refresh(),this.active.length&&this.load(i.active)},_initialActive:function(){var i=this.options.active,s=this.options.collapsible,n=location.hash.substring(1);return null===i&&(n&&this.tabs.each(function(s,a){return t(a).attr("aria-controls")===n?(i=s,!1):e}),null===i&&(i=this.tabs.index(this.tabs.filter(".ui-tabs-active"))),(null===i||-1===i)&&(i=this.tabs.length?0:!1)),i!==!1&&(i=this.tabs.index(this.tabs.eq(i)),-1===i&&(i=s?!1:0)),!s&&i===!1&&this.anchors.length&&(i=0),i},_getCreateEventData:function(){return{tab:this.active,panel:this.active.length?this._getPanelForTab(this.active):t()}},_tabKeydown:function(i){var s=t(this.document[0].activeElement).closest("li"),n=this.tabs.index(s),a=!0;if(!this._handlePageNav(i)){switch(i.keyCode){case t.ui.keyCode.RIGHT:case t.ui.keyCode.DOWN:n++;break;case t.ui.keyCode.UP:case t.ui.keyCode.LEFT:a=!1,n--;break;case t.ui.keyCode.END:n=this.anchors.length-1;break;case t.ui.keyCode.HOME:n=0;break;case t.ui.keyCode.SPACE:return i.preventDefault(),clearTimeout(this.activating),this._activate(n),e;case t.ui.keyCode.ENTER:return i.preventDefault(),clearTimeout(this.activating),this._activate(n===this.options.active?!1:n),e;default:return}i.preventDefault(),clearTimeout(this.activating),n=this._focusNextTab(n,a),i.ctrlKey||(s.attr("aria-selected","false"),this.tabs.eq(n).attr("aria-selected","true"),this.activating=this._delay(function(){this.option("active",n)},this.delay))}},_panelKeydown:function(e){this._handlePageNav(e)||e.ctrlKey&&e.keyCode===t.ui.keyCode.UP&&(e.preventDefault(),this.active.focus())},_handlePageNav:function(i){return i.altKey&&i.keyCode===t.ui.keyCode.PAGE_UP?(this._activate(this._focusNextTab(this.options.active-1,!1)),!0):i.altKey&&i.keyCode===t.ui.keyCode.PAGE_DOWN?(this._activate(this._focusNextTab(this.options.active+1,!0)),!0):e},_findNextTab:function(e,i){function s(){return e>n&&(e=0),0>e&&(e=n),e}for(var n=this.tabs.length-1;-1!==t.inArray(s(),this.options.disabled);)e=i?e+1:e-1;return e},_focusNextTab:function(t,e){return t=this._findNextTab(t,e),this.tabs.eq(t).focus(),t},_setOption:function(t,i){return"active"===t?(this._activate(i),e):"disabled"===t?(this._setupDisabled(i),e):(this._super(t,i),"collapsible"===t&&(this.element.toggleClass("ui-tabs-collapsible",i),i||this.options.active!==!1||this._activate(0)),"event"===t&&this._setupEvents(i),"heightStyle"===t&&this._setupHeightStyle(i),e)},_tabId:function(t){return t.attr("aria-controls")||"ui-tabs-"+i()},_sanitizeSelector:function(t){return t?t.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g,"\\$&"):""},refresh:function(){var e=this.options,i=this.tablist.children(":has(a[href])");e.disabled=t.map(i.filter(".ui-state-disabled"),function(t){return i.index(t)}),this._processTabs(),e.active!==!1&&this.anchors.length?this.active.length&&!t.contains(this.tablist[0],this.active[0])?this.tabs.length===e.disabled.length?(e.active=!1,this.active=t()):this._activate(this._findNextTab(Math.max(0,e.active-1),!1)):e.active=this.tabs.index(this.active):(e.active=!1,this.active=t()),this._refresh()},_refresh:function(){this._setupDisabled(this.options.disabled),this._setupEvents(this.options.event),this._setupHeightStyle(this.options.heightStyle),this.tabs.not(this.active).attr({"aria-selected":"false",tabIndex:-1}),this.panels.not(this._getPanelForTab(this.active)).hide().attr({"aria-expanded":"false","aria-hidden":"true"}),this.active.length?(this.active.addClass("ui-tabs-active ui-state-active").attr({"aria-selected":"true",tabIndex:0}),this._getPanelForTab(this.active).show().attr({"aria-expanded":"true","aria-hidden":"false"})):this.tabs.eq(0).attr("tabIndex",0)},_processTabs:function(){var e=this;this.tablist=this._getList().addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").attr("role","tablist"),this.tabs=this.tablist.find("> li:has(a[href])").addClass("ui-state-default ui-corner-top").attr({role:"tab",tabIndex:-1}),this.anchors=this.tabs.map(function(){return t("a",this)[0]}).addClass("ui-tabs-anchor").attr({role:"presentation",tabIndex:-1}),this.panels=t(),this.anchors.each(function(i,n){var a,o,r,h=t(n).uniqueId().attr("id"),l=t(n).closest("li"),c=l.attr("aria-controls");s(n)?(a=n.hash,o=e.element.find(e._sanitizeSelector(a))):(r=e._tabId(l),a="#"+r,o=e.element.find(a),o.length||(o=e._createPanel(r),o.insertAfter(e.panels[i-1]||e.tablist)),o.attr("aria-live","polite")),o.length&&(e.panels=e.panels.add(o)),c&&l.data("ui-tabs-aria-controls",c),l.attr({"aria-controls":a.substring(1),"aria-labelledby":h}),o.attr("aria-labelledby",h)}),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").attr("role","tabpanel")},_getList:function(){return this.tablist||this.element.find("ol,ul").eq(0)},_createPanel:function(e){return t("<div>").attr("id",e).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").data("ui-tabs-destroy",!0)},_setupDisabled:function(e){t.isArray(e)&&(e.length?e.length===this.anchors.length&&(e=!0):e=!1);for(var i,s=0;i=this.tabs[s];s++)e===!0||-1!==t.inArray(s,e)?t(i).addClass("ui-state-disabled").attr("aria-disabled","true"):t(i).removeClass("ui-state-disabled").removeAttr("aria-disabled");this.options.disabled=e},_setupEvents:function(e){var i={click:function(t){t.preventDefault()}};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(this.anchors,i),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(e){var i,s=this.element.parent();"fill"===e?(i=s.height(),i-=this.element.outerHeight()-this.element.height(),this.element.siblings(":visible").each(function(){var e=t(this),s=e.css("position");"absolute"!==s&&"fixed"!==s&&(i-=e.outerHeight(!0))}),this.element.children().not(this.panels).each(function(){i-=t(this).outerHeight(!0)}),this.panels.each(function(){t(this).height(Math.max(0,i-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===e&&(i=0,this.panels.each(function(){i=Math.max(i,t(this).height("").height())}).height(i))},_eventHandler:function(e){var i=this.options,s=this.active,n=t(e.currentTarget),a=n.closest("li"),o=a[0]===s[0],r=o&&i.collapsible,h=r?t():this._getPanelForTab(a),l=s.length?this._getPanelForTab(s):t(),c={oldTab:s,oldPanel:l,newTab:r?t():a,newPanel:h};e.preventDefault(),a.hasClass("ui-state-disabled")||a.hasClass("ui-tabs-loading")||this.running||o&&!i.collapsible||this._trigger("beforeActivate",e,c)===!1||(i.active=r?!1:this.tabs.index(a),this.active=o?t():a,this.xhr&&this.xhr.abort(),l.length||h.length||t.error("jQuery UI Tabs: Mismatching fragment identifier."),h.length&&this.load(this.tabs.index(a),e),this._toggle(e,c))},_toggle:function(e,i){function s(){a.running=!1,a._trigger("activate",e,i)}function n(){i.newTab.closest("li").addClass("ui-tabs-active ui-state-active"),o.length&&a.options.show?a._show(o,a.options.show,s):(o.show(),s())}var a=this,o=i.newPanel,r=i.oldPanel;this.running=!0,r.length&&this.options.hide?this._hide(r,this.options.hide,function(){i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),n()}):(i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),r.hide(),n()),r.attr({"aria-expanded":"false","aria-hidden":"true"}),i.oldTab.attr("aria-selected","false"),o.length&&r.length?i.oldTab.attr("tabIndex",-1):o.length&&this.tabs.filter(function(){return 0===t(this).attr("tabIndex")}).attr("tabIndex",-1),o.attr({"aria-expanded":"true","aria-hidden":"false"}),i.newTab.attr({"aria-selected":"true",tabIndex:0})},_activate:function(e){var i,s=this._findActive(e);s[0]!==this.active[0]&&(s.length||(s=this.active),i=s.find(".ui-tabs-anchor")[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return e===!1?t():this.tabs.eq(e)},_getIndex:function(t){return"string"==typeof t&&(t=this.anchors.index(this.anchors.filter("[href$='"+t+"']"))),t},_destroy:function(){this.xhr&&this.xhr.abort(),this.element.removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible"),this.tablist.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").removeAttr("role"),this.anchors.removeClass("ui-tabs-anchor").removeAttr("role").removeAttr("tabIndex").removeUniqueId(),this.tabs.add(this.panels).each(function(){t.data(this,"ui-tabs-destroy")?t(this).remove():t(this).removeClass("ui-state-default ui-state-active ui-state-disabled ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel").removeAttr("tabIndex").removeAttr("aria-live").removeAttr("aria-busy").removeAttr("aria-selected").removeAttr("aria-labelledby").removeAttr("aria-hidden").removeAttr("aria-expanded").removeAttr("role")}),this.tabs.each(function(){var e=t(this),i=e.data("ui-tabs-aria-controls");i?e.attr("aria-controls",i).removeData("ui-tabs-aria-controls"):e.removeAttr("aria-controls")}),this.panels.show(),"content"!==this.options.heightStyle&&this.panels.css("height","")},enable:function(i){var s=this.options.disabled;s!==!1&&(i===e?s=!1:(i=this._getIndex(i),s=t.isArray(s)?t.map(s,function(t){return t!==i?t:null}):t.map(this.tabs,function(t,e){return e!==i?e:null})),this._setupDisabled(s))},disable:function(i){var s=this.options.disabled;if(s!==!0){if(i===e)s=!0;else{if(i=this._getIndex(i),-1!==t.inArray(i,s))return;s=t.isArray(s)?t.merge([i],s).sort():[i]}this._setupDisabled(s)}},load:function(e,i){e=this._getIndex(e);var n=this,a=this.tabs.eq(e),o=a.find(".ui-tabs-anchor"),r=this._getPanelForTab(a),h={tab:a,panel:r};s(o[0])||(this.xhr=t.ajax(this._ajaxSettings(o,i,h)),this.xhr&&"canceled"!==this.xhr.statusText&&(a.addClass("ui-tabs-loading"),r.attr("aria-busy","true"),this.xhr.success(function(t){setTimeout(function(){r.html(t),n._trigger("load",i,h)},1)}).complete(function(t,e){setTimeout(function(){"abort"===e&&n.panels.stop(!1,!0),a.removeClass("ui-tabs-loading"),r.removeAttr("aria-busy"),t===n.xhr&&delete n.xhr},1)})))},_ajaxSettings:function(e,i,s){var n=this;return{url:e.attr("href"),beforeSend:function(e,a){return n._trigger("beforeLoad",i,t.extend({jqXHR:e,ajaxSettings:a},s))}}},_getPanelForTab:function(e){var i=t(e).attr("aria-controls");return this.element.find(this._sanitizeSelector("#"+i))}})})(jQuery);(function(t){function e(e,i){var s=(e.attr("aria-describedby")||"").split(/\s+/);s.push(i),e.data("ui-tooltip-id",i).attr("aria-describedby",t.trim(s.join(" ")))}function i(e){var i=e.data("ui-tooltip-id"),s=(e.attr("aria-describedby")||"").split(/\s+/),n=t.inArray(i,s);-1!==n&&s.splice(n,1),e.removeData("ui-tooltip-id"),s=t.trim(s.join(" ")),s?e.attr("aria-describedby",s):e.removeAttr("aria-describedby")}var s=0;t.widget("ui.tooltip",{version:"1.10.4",options:{content:function(){var e=t(this).attr("title")||"";return t("<a>").text(e).html()},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,tooltipClass:null,track:!1,close:null,open:null},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.options.disabled&&this._disable()},_setOption:function(e,i){var s=this;return"disabled"===e?(this[i?"_disable":"_enable"](),this.options[e]=i,void 0):(this._super(e,i),"content"===e&&t.each(this.tooltips,function(t,e){s._updateContent(e)}),void 0)},_disable:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur");n.target=n.currentTarget=s[0],e.close(n,!0)}),this.element.find(this.options.items).addBack().each(function(){var e=t(this);e.is("[title]")&&e.data("ui-tooltip-title",e.attr("title")).attr("title","")})},_enable:function(){this.element.find(this.options.items).addBack().each(function(){var e=t(this);e.data("ui-tooltip-title")&&e.attr("title",e.data("ui-tooltip-title"))})},open:function(e){var i=this,s=t(e?e.target:this.element).closest(this.options.items);s.length&&!s.data("ui-tooltip-id")&&(s.attr("title")&&s.data("ui-tooltip-title",s.attr("title")),s.data("ui-tooltip-open",!0),e&&"mouseover"===e.type&&s.parents().each(function(){var e,s=t(this);s.data("ui-tooltip-open")&&(e=t.Event("blur"),e.target=e.currentTarget=this,i.close(e,!0)),s.attr("title")&&(s.uniqueId(),i.parents[this.id]={element:this,title:s.attr("title")},s.attr("title",""))}),this._updateContent(s,e))},_updateContent:function(t,e){var i,s=this.options.content,n=this,o=e?e.type:null;return"string"==typeof s?this._open(e,t,s):(i=s.call(t[0],function(i){t.data("ui-tooltip-open")&&n._delay(function(){e&&(e.type=o),this._open(e,t,i)})}),i&&this._open(e,t,i),void 0)},_open:function(i,s,n){function o(t){l.of=t,a.is(":hidden")||a.position(l)}var a,r,h,l=t.extend({},this.options.position);if(n){if(a=this._find(s),a.length)return a.find(".ui-tooltip-content").html(n),void 0;s.is("[title]")&&(i&&"mouseover"===i.type?s.attr("title",""):s.removeAttr("title")),a=this._tooltip(s),e(s,a.attr("id")),a.find(".ui-tooltip-content").html(n),this.options.track&&i&&/^mouse/.test(i.type)?(this._on(this.document,{mousemove:o}),o(i)):a.position(t.extend({of:s},this.options.position)),a.hide(),this._show(a,this.options.show),this.options.show&&this.options.show.delay&&(h=this.delayedShow=setInterval(function(){a.is(":visible")&&(o(l.of),clearInterval(h))},t.fx.interval)),this._trigger("open",i,{tooltip:a}),r={keyup:function(e){if(e.keyCode===t.ui.keyCode.ESCAPE){var i=t.Event(e);i.currentTarget=s[0],this.close(i,!0)}},remove:function(){this._removeTooltip(a)}},i&&"mouseover"!==i.type||(r.mouseleave="close"),i&&"focusin"!==i.type||(r.focusout="close"),this._on(!0,s,r)}},close:function(e){var s=this,n=t(e?e.currentTarget:this.element),o=this._find(n);this.closing||(clearInterval(this.delayedShow),n.data("ui-tooltip-title")&&n.attr("title",n.data("ui-tooltip-title")),i(n),o.stop(!0),this._hide(o,this.options.hide,function(){s._removeTooltip(t(this))}),n.removeData("ui-tooltip-open"),this._off(n,"mouseleave focusout keyup"),n[0]!==this.element[0]&&this._off(n,"remove"),this._off(this.document,"mousemove"),e&&"mouseleave"===e.type&&t.each(this.parents,function(e,i){t(i.element).attr("title",i.title),delete s.parents[e]}),this.closing=!0,this._trigger("close",e,{tooltip:o}),this.closing=!1)},_tooltip:function(e){var i="ui-tooltip-"+s++,n=t("<div>").attr({id:i,role:"tooltip"}).addClass("ui-tooltip ui-widget ui-corner-all ui-widget-content "+(this.options.tooltipClass||""));return t("<div>").addClass("ui-tooltip-content").appendTo(n),n.appendTo(this.document[0].body),this.tooltips[i]=e,n},_find:function(e){var i=e.data("ui-tooltip-id");return i?t("#"+i):t()},_removeTooltip:function(t){t.remove(),delete this.tooltips[t.attr("id")]},_destroy:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur");n.target=n.currentTarget=s[0],e.close(n,!0),t("#"+i).remove(),s.data("ui-tooltip-title")&&(s.attr("title",s.data("ui-tooltip-title")),s.removeData("ui-tooltip-title"))})}})})(jQuery);(function(t,e){var i="ui-effects-";t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=h(),n=s._rgba=[];return i=i.toLowerCase(),f(l,function(t,a){var o,r=a.re.exec(i),l=r&&a.parse(r),h=a.space||"rgba";return l?(o=s[h](l),s[c[h].cache]=o[c[h].cache],n=s._rgba=o._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,a.transparent),s):a[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var a,o="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,l=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],h=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=h.support={},p=t("<p>")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),h.fn=t.extend(h.prototype,{parse:function(n,o,r,l){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(o),o=e);var u=this,d=t.type(n),p=this._rgba=[];return o!==e&&(n=[n,o,r,l],d="array"),"string"===d?this.parse(s(n)||a._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof h?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var a=s.cache;f(s.props,function(t,e){if(!u[a]&&s.to){if("alpha"===t||null==n[t])return;u[a]=s.to(u._rgba)}u[a][e.idx]=i(n[t],e,!0)}),u[a]&&0>t.inArray(null,u[a].slice(0,3))&&(u[a][3]=1,s.from&&(u._rgba=s.from(u[a])))}),this):e},is:function(t){var i=h(t),s=!0,n=this;return f(c,function(t,a){var o,r=i[a.cache];return r&&(o=n[a.cache]||a.to&&a.to(n._rgba)||[],f(a.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===o[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=h(t),n=s._space(),a=c[n],o=0===this.alpha()?h("transparent"):this,r=o[a.cache]||a.to(o._rgba),l=r.slice();return s=s[a.cache],f(a.props,function(t,n){var a=n.idx,o=r[a],h=s[a],c=u[n.type]||{};null!==h&&(null===o?l[a]=h:(c.mod&&(h-o>c.mod/2?o+=c.mod:o-h>c.mod/2&&(o-=c.mod)),l[a]=i((h-o)*e+o,n)))}),this[n](l)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=h(e)._rgba;return h(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),h.fn.parse.prototype=h.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,a=t[2]/255,o=t[3],r=Math.max(s,n,a),l=Math.min(s,n,a),h=r-l,c=r+l,u=.5*c;return e=l===r?0:s===r?60*(n-a)/h+360:n===r?60*(a-s)/h+120:60*(s-n)/h+240,i=0===h?0:.5>=u?h/c:h/(2-c),[Math.round(e)%360,i,u,null==o?1:o]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],a=t[3],o=.5>=s?s*(1+i):s+i-s*i,r=2*s-o;return[Math.round(255*n(r,o,e+1/3)),Math.round(255*n(r,o,e)),Math.round(255*n(r,o,e-1/3)),a]},f(c,function(s,n){var a=n.props,o=n.cache,l=n.to,c=n.from;h.fn[s]=function(s){if(l&&!this[o]&&(this[o]=l(this._rgba)),s===e)return this[o].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[o].slice();return f(a,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=h(c(d)),n[o]=d,n):h(d)},f(a,function(e,i){h.fn[e]||(h.fn[e]=function(n){var a,o=t.type(n),l="alpha"===e?this._hsla?"hsla":"rgba":s,h=this[l](),c=h[i.idx];return"undefined"===o?c:("function"===o&&(n=n.call(this,c),o=t.type(n)),null==n&&i.empty?this:("string"===o&&(a=r.exec(n),a&&(n=c+parseFloat(a[2])*("+"===a[1]?1:-1))),h[i.idx]=n,this[l](h)))})})}),h.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var a,o,r="";if("transparent"!==n&&("string"!==t.type(n)||(a=s(n)))){if(n=h(a||n),!d.rgba&&1!==n._rgba[3]){for(o="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&o&&o.style;)try{r=t.css(o,"backgroundColor"),o=o.parentNode}catch(l){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(l){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=h(e.elem,i),e.end=h(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},h.hook(o),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},a=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(jQuery),function(){function i(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,a={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(a[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(a[i]=n[i]);return a}function s(e,i){var s,n,o={};for(s in i)n=i[s],e[s]!==n&&(a[s]||(t.fx.step[s]||!isNaN(parseFloat(n)))&&(o[s]=n));return o}var n=["add","remove","toggle"],a={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(jQuery.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(e,a,o,r){var l=t.speed(a,o,r);return this.queue(function(){var a,o=t(this),r=o.attr("class")||"",h=l.children?o.find("*").addBack():o;h=h.map(function(){var e=t(this);return{el:e,start:i(this)}}),a=function(){t.each(n,function(t,i){e[i]&&o[i+"Class"](e[i])})},a(),h=h.map(function(){return this.end=i(this.el[0]),this.diff=s(this.start,this.end),this}),o.attr("class",r),h=h.map(function(){var e=this,i=t.Deferred(),s=t.extend({},l,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,h.get()).done(function(){a(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),l.complete.call(o[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,a){return s?t.effects.animateClass.call(this,{add:i},s,n,a):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,a){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,a):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(i){return function(s,n,a,o,r){return"boolean"==typeof n||n===e?a?t.effects.animateClass.call(this,n?{add:s}:{remove:s},a,o,r):i.apply(this,arguments):t.effects.animateClass.call(this,{toggle:s},n,a,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,a){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,a)}})}(),function(){function s(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function n(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}t.extend(t.effects,{version:"1.10.4",save:function(t,e){for(var s=0;e.length>s;s++)null!==e[s]&&t.data(i+e[s],t[0].style[e[s]])},restore:function(t,s){var n,a;for(a=0;s.length>a;a++)null!==s[a]&&(n=t.data(i+s[a]),n===e&&(n=""),t.css(s[a],n))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("<div></div>").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},a=document.activeElement;try{a.id}catch(o){a=document.body}return e.wrap(s),(e[0]===a||t.contains(e[0],a))&&t(a).focus(),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).focus()),e},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var a=e.cssUnit(i);a[0]>0&&(n[i]=a[0]*s+a[1])}),n}}),t.fn.extend({effect:function(){function e(e){function s(){t.isFunction(a)&&a.call(n[0]),t.isFunction(e)&&e()}var n=t(this),a=i.complete,r=i.mode;(n.is(":hidden")?"hide"===r:"show"===r)?(n[r](),s()):o.call(n[0],i,s)}var i=s.apply(this,arguments),n=i.mode,a=i.queue,o=t.effects.effect[i.effect];return t.fx.off||!o?n?this[n](i.duration,i.complete):this.each(function(){i.complete&&i.complete.call(this)}):a===!1?this.each(e):this.queue(a||"fx",e)},show:function(t){return function(e){if(n(e))return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="show",this.effect.call(this,i)}}(t.fn.show),hide:function(t){return function(e){if(n(e))return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="hide",this.effect.call(this,i)}}(t.fn.hide),toggle:function(t){return function(e){if(n(e)||"boolean"==typeof e)return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="toggle",this.effect.call(this,i)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s}})}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}()})(jQuery);(function(t){var e=/up|down|vertical/,i=/up|left|vertical|horizontal/;t.effects.effect.blind=function(s,n){var a,o,r,l=t(this),h=["position","top","bottom","left","right","height","width"],c=t.effects.setMode(l,s.mode||"hide"),u=s.direction||"up",d=e.test(u),p=d?"height":"width",f=d?"top":"left",g=i.test(u),m={},v="show"===c;l.parent().is(".ui-effects-wrapper")?t.effects.save(l.parent(),h):t.effects.save(l,h),l.show(),a=t.effects.createWrapper(l).css({overflow:"hidden"}),o=a[p](),r=parseFloat(a.css(f))||0,m[p]=v?o:0,g||(l.css(d?"bottom":"right",0).css(d?"top":"left","auto").css({position:"absolute"}),m[f]=v?r:o+r),v&&(a.css(p,0),g||a.css(f,r+o)),a.animate(m,{duration:s.duration,easing:s.easing,queue:!1,complete:function(){"hide"===c&&l.hide(),t.effects.restore(l,h),t.effects.removeWrapper(l),n()}})}})(jQuery);(function(t){t.effects.effect.bounce=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","height","width"],l=t.effects.setMode(o,e.mode||"effect"),h="hide"===l,c="show"===l,u=e.direction||"up",d=e.distance,p=e.times||5,f=2*p+(c||h?1:0),g=e.duration/f,m=e.easing,v="up"===u||"down"===u?"top":"left",_="up"===u||"left"===u,b=o.queue(),y=b.length;for((c||h)&&r.push("opacity"),t.effects.save(o,r),o.show(),t.effects.createWrapper(o),d||(d=o["top"===v?"outerHeight":"outerWidth"]()/3),c&&(a={opacity:1},a[v]=0,o.css("opacity",0).css(v,_?2*-d:2*d).animate(a,g,m)),h&&(d/=Math.pow(2,p-1)),a={},a[v]=0,s=0;p>s;s++)n={},n[v]=(_?"-=":"+=")+d,o.animate(n,g,m).animate(a,g,m),d=h?2*d:d/2;h&&(n={opacity:0},n[v]=(_?"-=":"+=")+d,o.animate(n,g,m)),o.queue(function(){h&&o.hide(),t.effects.restore(o,r),t.effects.removeWrapper(o),i()}),y>1&&b.splice.apply(b,[1,0].concat(b.splice(y,f+1))),o.dequeue()}})(jQuery);(function(t){t.effects.effect.clip=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","height","width"],l=t.effects.setMode(o,e.mode||"hide"),h="show"===l,c=e.direction||"vertical",u="vertical"===c,d=u?"height":"width",p=u?"top":"left",f={};t.effects.save(o,r),o.show(),s=t.effects.createWrapper(o).css({overflow:"hidden"}),n="IMG"===o[0].tagName?s:o,a=n[d](),h&&(n.css(d,0),n.css(p,a/2)),f[d]=h?a:0,f[p]=h?0:a/2,n.animate(f,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){h||o.hide(),t.effects.restore(o,r),t.effects.removeWrapper(o),i()}})}})(jQuery);(function(t){t.effects.effect.drop=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","opacity","height","width"],o=t.effects.setMode(n,e.mode||"hide"),r="show"===o,l=e.direction||"left",h="up"===l||"down"===l?"top":"left",c="up"===l||"left"===l?"pos":"neg",u={opacity:r?1:0};t.effects.save(n,a),n.show(),t.effects.createWrapper(n),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0)/2,r&&n.css("opacity",0).css(h,"pos"===c?-s:s),u[h]=(r?"pos"===c?"+=":"-=":"pos"===c?"-=":"+=")+s,n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}})}})(jQuery);(function(t){t.effects.effect.explode=function(e,i){function s(){b.push(this),b.length===u*d&&n()}function n(){p.css({visibility:"visible"}),t(b).remove(),g||p.hide(),i()}var a,o,r,l,h,c,u=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=u,p=t(this),f=t.effects.setMode(p,e.mode||"hide"),g="show"===f,m=p.show().css("visibility","hidden").offset(),v=Math.ceil(p.outerWidth()/d),_=Math.ceil(p.outerHeight()/u),b=[];for(a=0;u>a;a++)for(l=m.top+a*_,c=a-(u-1)/2,o=0;d>o;o++)r=m.left+o*v,h=o-(d-1)/2,p.clone().appendTo("body").wrap("<div></div>").css({position:"absolute",visibility:"visible",left:-o*v,top:-a*_}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:v,height:_,left:r+(g?h*v:0),top:l+(g?c*_:0),opacity:g?0:1}).animate({left:r+(g?0:h*v),top:l+(g?0:c*_),opacity:g?1:0},e.duration||500,e.easing,s)}})(jQuery);(function(t){t.effects.effect.fade=function(e,i){var s=t(this),n=t.effects.setMode(s,e.mode||"toggle");s.animate({opacity:n},{queue:!1,duration:e.duration,easing:e.easing,complete:i})}})(jQuery);(function(t){t.effects.effect.fold=function(e,i){var s,n,a=t(this),o=["position","top","bottom","left","right","height","width"],r=t.effects.setMode(a,e.mode||"hide"),l="show"===r,h="hide"===r,c=e.size||15,u=/([0-9]+)%/.exec(c),d=!!e.horizFirst,p=l!==d,f=p?["width","height"]:["height","width"],g=e.duration/2,m={},v={};t.effects.save(a,o),a.show(),s=t.effects.createWrapper(a).css({overflow:"hidden"}),n=p?[s.width(),s.height()]:[s.height(),s.width()],u&&(c=parseInt(u[1],10)/100*n[h?0:1]),l&&s.css(d?{height:0,width:c}:{height:c,width:0}),m[f[0]]=l?n[0]:c,v[f[1]]=l?n[1]:0,s.animate(m,g,e.easing).animate(v,g,e.easing,function(){h&&a.hide(),t.effects.restore(a,o),t.effects.removeWrapper(a),i()})}})(jQuery);(function(t){t.effects.effect.highlight=function(e,i){var s=t(this),n=["backgroundImage","backgroundColor","opacity"],a=t.effects.setMode(s,e.mode||"show"),o={backgroundColor:s.css("backgroundColor")};"hide"===a&&(o.opacity=0),t.effects.save(s,n),s.show().css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(o,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===a&&s.hide(),t.effects.restore(s,n),i()}})}})(jQuery);(function(t){t.effects.effect.pulsate=function(e,i){var s,n=t(this),a=t.effects.setMode(n,e.mode||"show"),o="show"===a,r="hide"===a,l=o||"hide"===a,h=2*(e.times||5)+(l?1:0),c=e.duration/h,u=0,d=n.queue(),p=d.length;for((o||!n.is(":visible"))&&(n.css("opacity",0).show(),u=1),s=1;h>s;s++)n.animate({opacity:u},c,e.easing),u=1-u;n.animate({opacity:u},c,e.easing),n.queue(function(){r&&n.hide(),i()}),p>1&&d.splice.apply(d,[1,0].concat(d.splice(p,h+1))),n.dequeue()}})(jQuery);(function(t){t.effects.effect.puff=function(e,i){var s=t(this),n=t.effects.setMode(s,e.mode||"hide"),a="hide"===n,o=parseInt(e.percent,10)||150,r=o/100,l={height:s.height(),width:s.width(),outerHeight:s.outerHeight(),outerWidth:s.outerWidth()};t.extend(e,{effect:"scale",queue:!1,fade:!0,mode:n,complete:i,percent:a?o:100,from:a?l:{height:l.height*r,width:l.width*r,outerHeight:l.outerHeight*r,outerWidth:l.outerWidth*r}}),s.effect(e)},t.effects.effect.scale=function(e,i){var s=t(this),n=t.extend(!0,{},e),a=t.effects.setMode(s,e.mode||"effect"),o=parseInt(e.percent,10)||(0===parseInt(e.percent,10)?0:"hide"===a?0:100),r=e.direction||"both",l=e.origin,h={height:s.height(),width:s.width(),outerHeight:s.outerHeight(),outerWidth:s.outerWidth()},c={y:"horizontal"!==r?o/100:1,x:"vertical"!==r?o/100:1};n.effect="size",n.queue=!1,n.complete=i,"effect"!==a&&(n.origin=l||["middle","center"],n.restore=!0),n.from=e.from||("show"===a?{height:0,width:0,outerHeight:0,outerWidth:0}:h),n.to={height:h.height*c.y,width:h.width*c.x,outerHeight:h.outerHeight*c.y,outerWidth:h.outerWidth*c.x},n.fade&&("show"===a&&(n.from.opacity=0,n.to.opacity=1),"hide"===a&&(n.from.opacity=1,n.to.opacity=0)),s.effect(n)},t.effects.effect.size=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","width","height","overflow","opacity"],l=["position","top","bottom","left","right","overflow","opacity"],h=["width","height","overflow"],c=["fontSize"],u=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],d=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],p=t.effects.setMode(o,e.mode||"effect"),f=e.restore||"effect"!==p,g=e.scale||"both",m=e.origin||["middle","center"],v=o.css("position"),_=f?r:l,b={height:0,width:0,outerHeight:0,outerWidth:0};"show"===p&&o.show(),s={height:o.height(),width:o.width(),outerHeight:o.outerHeight(),outerWidth:o.outerWidth()},"toggle"===e.mode&&"show"===p?(o.from=e.to||b,o.to=e.from||s):(o.from=e.from||("show"===p?b:s),o.to=e.to||("hide"===p?b:s)),a={from:{y:o.from.height/s.height,x:o.from.width/s.width},to:{y:o.to.height/s.height,x:o.to.width/s.width}},("box"===g||"both"===g)&&(a.from.y!==a.to.y&&(_=_.concat(u),o.from=t.effects.setTransition(o,u,a.from.y,o.from),o.to=t.effects.setTransition(o,u,a.to.y,o.to)),a.from.x!==a.to.x&&(_=_.concat(d),o.from=t.effects.setTransition(o,d,a.from.x,o.from),o.to=t.effects.setTransition(o,d,a.to.x,o.to))),("content"===g||"both"===g)&&a.from.y!==a.to.y&&(_=_.concat(c).concat(h),o.from=t.effects.setTransition(o,c,a.from.y,o.from),o.to=t.effects.setTransition(o,c,a.to.y,o.to)),t.effects.save(o,_),o.show(),t.effects.createWrapper(o),o.css("overflow","hidden").css(o.from),m&&(n=t.effects.getBaseline(m,s),o.from.top=(s.outerHeight-o.outerHeight())*n.y,o.from.left=(s.outerWidth-o.outerWidth())*n.x,o.to.top=(s.outerHeight-o.to.outerHeight)*n.y,o.to.left=(s.outerWidth-o.to.outerWidth)*n.x),o.css(o.from),("content"===g||"both"===g)&&(u=u.concat(["marginTop","marginBottom"]).concat(c),d=d.concat(["marginLeft","marginRight"]),h=r.concat(u).concat(d),o.find("*[width]").each(function(){var i=t(this),s={height:i.height(),width:i.width(),outerHeight:i.outerHeight(),outerWidth:i.outerWidth()};f&&t.effects.save(i,h),i.from={height:s.height*a.from.y,width:s.width*a.from.x,outerHeight:s.outerHeight*a.from.y,outerWidth:s.outerWidth*a.from.x},i.to={height:s.height*a.to.y,width:s.width*a.to.x,outerHeight:s.height*a.to.y,outerWidth:s.width*a.to.x},a.from.y!==a.to.y&&(i.from=t.effects.setTransition(i,u,a.from.y,i.from),i.to=t.effects.setTransition(i,u,a.to.y,i.to)),a.from.x!==a.to.x&&(i.from=t.effects.setTransition(i,d,a.from.x,i.from),i.to=t.effects.setTransition(i,d,a.to.x,i.to)),i.css(i.from),i.animate(i.to,e.duration,e.easing,function(){f&&t.effects.restore(i,h)})})),o.animate(o.to,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){0===o.to.opacity&&o.css("opacity",o.from.opacity),"hide"===p&&o.hide(),t.effects.restore(o,_),f||("static"===v?o.css({position:"relative",top:o.to.top,left:o.to.left}):t.each(["top","left"],function(t,e){o.css(e,function(e,i){var s=parseInt(i,10),n=t?o.to.left:o.to.top;return"auto"===i?n+"px":s+n+"px"})})),t.effects.removeWrapper(o),i()}})}})(jQuery);(function(t){t.effects.effect.shake=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","height","width"],o=t.effects.setMode(n,e.mode||"effect"),r=e.direction||"left",l=e.distance||20,h=e.times||3,c=2*h+1,u=Math.round(e.duration/c),d="up"===r||"down"===r?"top":"left",p="up"===r||"left"===r,f={},g={},m={},v=n.queue(),_=v.length;for(t.effects.save(n,a),n.show(),t.effects.createWrapper(n),f[d]=(p?"-=":"+=")+l,g[d]=(p?"+=":"-=")+2*l,m[d]=(p?"-=":"+=")+2*l,n.animate(f,u,e.easing),s=1;h>s;s++)n.animate(g,u,e.easing).animate(m,u,e.easing);n.animate(g,u,e.easing).animate(f,u/2,e.easing).queue(function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}),_>1&&v.splice.apply(v,[1,0].concat(v.splice(_,c+1))),n.dequeue()}})(jQuery);(function(t){t.effects.effect.slide=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","width","height"],o=t.effects.setMode(n,e.mode||"show"),r="show"===o,l=e.direction||"left",h="up"===l||"down"===l?"top":"left",c="up"===l||"left"===l,u={};t.effects.save(n,a),n.show(),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0),t.effects.createWrapper(n).css({overflow:"hidden"}),r&&n.css(h,c?isNaN(s)?"-"+s:-s:s),u[h]=(r?c?"+=":"-=":c?"-=":"+=")+s,n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}})}})(jQuery);(function(t){t.effects.effect.transfer=function(e,i){var s=t(this),n=t(e.to),a="fixed"===n.css("position"),o=t("body"),r=a?o.scrollTop():0,l=a?o.scrollLeft():0,h=n.offset(),c={top:h.top-r,left:h.left-l,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("<div class='ui-effects-transfer'></div>").appendTo(document.body).addClass(e.className).css({top:u.top-r,left:u.left-l,height:s.innerHeight(),width:s.innerWidth(),position:a?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),i()})}})(jQuery);
\ No newline at end of file
diff --git a/static/libjs/jquery.field.min.js b/static/libjs/jquery.field.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..4da5cb7d5e5e58cad74f207dc5e042de80437871
--- /dev/null
+++ b/static/libjs/jquery.field.min.js
@@ -0,0 +1,12 @@
+/*
+ * jQuery Field Plug-in
+ *
+ * Copyright (c) 2011 Dan G. Switzer, II
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *   http://www.gnu.org/licenses/gpl.html
+ *
+ * Version: 0.9.6
+*/
+(function($){var defaults={delimiter:",",checkboxRangeKeyBinding:"shiftKey",useArray:false};$.Field={version:"0.9.6",setDefaults:function(options){$.extend(defaults,options)},setProperty:function(prop,value){defaults[prop]=value},getProperty:function(prop){return defaults[prop]}};$.fn.fieldArray=function(v){var t=$type(v);if(t=="undefined")return getValue(this);if(t=="string"||t=="number"){v=v.toString().split(defaults.delimiter);t="array"}if(t=="array")return setValue(this,v);return this};$.fn.getValue=function(){return getValue(this).join(defaults.delimiter)};var getValue=function(jq){var v=[];jq.each(function(lc){var t=getType(this);switch(t){case"checkbox":case"radio":if(this.checked)v.push(this.value);break;case"select":if(this.type=="select-one"){v.push((this.selectedIndex==-1)?"":getOptionVal(this[this.selectedIndex]))}else{for(var i=0;i<this.length;i++){if(this[i].selected){v.push(getOptionVal(this[i]))}}}break;case"text":v.push(this.value);break}});return v};$.fn.setValue=function(v){return setValue(this,((!v&&(v!==0))?[""]:v.toString().split(defaults.delimiter)))};var setValue=function(jq,v){jq.each(function(lc){var t=getType(this),x;switch(t){case"checkbox":case"radio":if(valueExists(v,this.value))this.checked=true;else this.checked=false;break;case"select":var bSelectOne=(this.type=="select-one");var bKeepLooking=true;for(var i=0;i<this.length;i++){x=getOptionVal(this[i]);bSelectItem=valueExists(v,x);if(bSelectItem){this[i].selected=true;if(bSelectOne){bKeepLooking=false;break}}else if(!bSelectOne)this[i].selected=false}if(bSelectOne&&bKeepLooking&&!!this[0]){this[0].selected=true}break;case"text":this.value=v.join(defaults.delimiter);break}});return jq};$.fn.formHash=function(map,clear){var isGet=(arguments.length==0);var hash={};this.filter("form").each(function(){var els=this.elements,el,n,fields={},$el;for(var i=0,elsMax=els.length;i<elsMax;i++){el=els[i];n=el.name;if(!n||fields[n])continue;var $el=$(el.form[n]);if(isGet){hash[n]=$el[defaults.useArray?"fieldArray":"getValue"]()}else if(n in map){$el[defaults.useArray?"fieldArray":"setValue"](map[n])}else if(clear===true){$el[defaults.useArray?"fieldArray":"setValue"]("")}fields[n]=true}});return(isGet)?hash:this};$.fn.fieldHash=function(map,clear){var isGet=!(map&&typeof map=="object");var hash={},fields={};this.filter(":input").each(function(){var el=this,n=el.name;if(!n||fields[n])return;var $el=$(el.form[n]);if(isGet){hash[n]=$el[defaults.useArray?"fieldArray":"getValue"]()}else if(n in map){$el[defaults.useArray?"fieldArray":"setValue"](map[n])}else if(clear===true){$el[defaults.useArray?"fieldArray":"setValue"]("")}fields[n]=true});return(isGet)?hash:this};$.fn.autoAdvance=function(callback){return this.find(":text,:password,textarea").bind("keyup.autoAdvance",function(e){var $field=$(this),iMaxLength=parseInt($field.attr("maxlength"),10);if(isNaN(iMaxLength)||("|9|16|37|38|39|40|".indexOf("|"+e.keyCode+"|")>-1))return true;if($field.getValue().length>=$field.attr("maxlength")){var $next=$field.moveNext().select();if($.isFunction(callback))callback.apply($field,[$next])}})};$.fn.moveNext=function(){return this.moveIndex("next")};$.fn.movePrev=function(){return this.moveIndex("prev")};$.fn.moveIndex=function(i){var pos=getFieldPosition(this);if(i=="next")i=pos[0]+1;else if(i=="prev")i=pos[0]-1;if(i<0)i=pos[1].length-1;else if(i>=pos[1].length)i=0;return $(pos[1][i]).trigger("focus")};$.fn.getTabIndex=function(){return getFieldPosition(this)[0]};var getFieldPosition=function(jq){var $field=jq.filter("input, select, textarea").get(0),tabIndex=[],posIndex=[];if(!$field)return[-1,[]];$.each($field.form.elements,function(i,o){if(o.tagName!="FIELDSET"&&!o.disabled&&jQuery(o).is(":visible")){if(o.tabIndex>0){tabIndex.push(o)}else{posIndex.push(o)}}});tabIndex.sort(function(a,b){return a.tabIndex-b.tabIndex});tabIndex=$.merge(tabIndex,posIndex);for(var i=0;i<tabIndex.length;i++){if(tabIndex[i]==$field)return[i,tabIndex]}return[-1,tabIndex]};$.fn.limitSelection=function(limit,options){var opt=jQuery.extend((limit&&limit.constructor==Object?limit:{limit:limit,onsuccess:function(limit){return true},onfailure:function(limit){alert("You can only select a maximum a of "+limit+" items.");return false}}),options);var self=this;var getCount=function(el){if(el.type=="select-multiple")return $("option:selected",self).length;else if(el.type=="checkbox")return self.filter(":checked").length;return 0};var undoSelect=function(){setValue(self,getValue(self).slice(0,opt.limit));return opt.onfailure.apply(self,[opt.limit])};return this.bind((!!self[0]&&self[0].type=="select-multiple")?"change.limitSelection":"click.limitSelection",function(){if(getCount(this)>opt.limit){return(this.type=="select-multiple")?undoSelect():opt.onfailure.apply(self,[opt.limit])}opt.onsuccess.apply(self,[opt.limit]);return true})};$.fn.createCheckboxRange=function(callback){var opt=jQuery.extend((callback&&callback.constructor==Object?callback:{bind:defaults.checkboxRangeKeyBinding,click:callback}),callback);var iLastSelection=0,self=this,bCallback=$.isFunction(opt.click);if(bCallback)this.each(function(){opt.click.apply(this,[$.event.fix({type:null}),$(this).is(":checked")])});return this.each(function(){if(this.type!="checkbox")return false;var el=this;var updateLastCheckbox=function(e){iLastSelection=self.index(e.target)};var checkboxClicked=function(e){var bSetChecked=this.checked,current=self.index(e.target),low=Math.min(iLastSelection,current),high=Math.max(iLastSelection+1,current);if(bCallback)opt.click.apply(this,[e,bSetChecked]);if(!e[opt.bind])return;for(var i=low;i<high;i++){var item=self.eq(i).attr("checked",bSetChecked).trigger("change");if(bCallback)opt.click.apply(item[0],[e,bSetChecked])}return true};$(this).unbind("click.createCheckboxRange").bind("click.createCheckboxRange",checkboxClicked).bind("click.createCheckboxRange",updateLastCheckbox);return true})};var getType=function(el){var t=el.type;switch(t){case"select":case"select-one":case"select-multiple":t="select";break;case"text":case"hidden":case"textarea":case"password":case"button":case"submit":case"submit":case"file":t="text";break;case"checkbox":case"radio":t=t;break}return t};var getOptionVal=function(el){return el.value||((el.attributes&&el.attributes['value']&&!(el.attributes['value'].specified))?el.text:null)||""};var valueExists=function(a,v){return($.inArray(v,a)>-1)};var $type=function(o){var t=(typeof o).toLowerCase();if(t=="object"){if(o instanceof Array)t="array";else if(o instanceof Date)t="date"}return t};var $isType=function(o,v){return($type(o)==String(v).toLowerCase())}})(jQuery);
\ No newline at end of file
diff --git a/static/libjs/jquery.ui.map.full.min.js b/static/libjs/jquery.ui.map.full.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..0c8962736aeb9a6cb2ce20824a6064f82c277e75
--- /dev/null
+++ b/static/libjs/jquery.ui.map.full.min.js
@@ -0,0 +1,2 @@
+/*! http://code.google.com/p/jquery-ui-map/ | Johan S�ll Larsson */
+eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('(3(d){d.a=3(a,b){k c=a.u(".")[0],a=a.u(".")[1];d[c]=d[c]||{};d[c][a]=3(a,b){J.N&&2.13(a,b)};d[c][a].G=d.p({1w:c,1F:a},b);d.1h[a]=3(b){k g="1E"===1G b,f=Q.G.14.15(J,1),i=2;l(g&&"1I"===b.1H(0,1))4 i;2.1A(3(){k h=d.R(2,a);h||(h=d.R(2,a,j d[c][a](b,2)));g&&(i=h[b].18(h,f))});4 i}};d.a("1z.1B",{m:{1D:"1C",1P:5},1O:3(a,b){b&&(2.m[a]=b,2.6("9").C(2.m));4 2.m[a]},13:3(a,b){2.D=b;o.p(2.m,a);2.m.U=2.E(2.m.U);2.Y();2.V&&2.V()},Y:3(){k a=2;a.q={9:j 8.7.1Q(a.D,a.m),H:[],s:[],r:[],S:j 8.7.1S};8.7.x.1R(a.q.9,"1K",3(){d(a.D).1p("1J",a.q.9)});a.y(a.m.1N,a.q.9)},16:3(a){2.6("M",j 8.7.1M).p(2.E(a));2.6("9").1q(2.6("M"));4 2},1r:3(a){k b=2.6("9").1s();4 b?b.1t(a.17()):!1},1x:3(a,b){2.6("9").1y[b].L(2.z(a));4 2},1u:3(a,b,c){a.9=2.6("9");a.W=2.E(a.W);k c=j(c||8.7.1v)(a),e=2.6("H");c.T?e[c.T]=c:e.L(c);c.M&&2.16(c.17());2.y(b,a.9,c);4 d(c)},w:3(a){2.A(2.6(a));2.O(a,[]);4 2},A:3(a){P(k b 10 a)a.Z(b)&&(a[b]t 8.7.1c?(8.7.x.2c(a[b]),a[b].F&&a[b].F(v)):a[b]t Q&&2.A(a[b]),a[b]=v)},2b:3(a,b,c){k a=2.6(a),e;P(e 10 a)a.Z(e)&&c(a[e],b.11&&a[e][b.I]?-1<d.2e(b.12,a[e][b.I].u(b.11)):a[e][b.I]===b.12);4 2},6:3(a,b){k c=2.q;l(!c[a]){l(-1<a.2d(">")){P(k e=a.X(/ /g,"").u(">"),d=0;d<e.N;d++){l(!c[e[d]])l(b)c[e[d]]=d+1<e.N?[]:b;1l 4 v;c=c[e[d]]}4 c}b&&!c[a]&&2.O(a,b)}4 c[a]},27:3(a,b,c){k d=2.6("S");d.C(a);d.2a(2.6("9"),2.z(b));2.y(c,d);4 2},O:3(a,b){2.q[a]=b;4 2},2f:3(){k a=2.6("9"),b=a.2j();d(a).1m("2g");a.2h(b);4 2},2i:3(){2.w("H");2.w("r");2.w("s");2.A(2.q);o.1Z(2.D,2.1T)},y:3(a){a&&d.1V(a)&&a.18(2,Q.G.14.15(J,1))},E:3(a){l(!a)4 j 8.7.K(0,0);l(a t 8.7.K)4 a;a=a.X(/ /g,"").u(",");4 j 8.7.K(a[0],a[1])},z:3(a){l(a){l(a t o)4 a[0];l(a t 1W)4 a}1l 4 v;4 d("#"+a)[0]},22:3(a,b){4 d(2.6("s > "+a,[]).L(j 8.7[a](o.p({9:2.6("9")},b))))},21:3(a,b){(!b?2.6("s > B",j 8.7.B):2.6("s > B",j 8.7.B(b,a))).C(o.p({9:2.6("9")},a))},20:3(a,b,c){2.6("s > "+a,j 8.7.23(b,o.p({9:2.6("9")},c)))},26:3(a,b,c){k d=2,g=2.6("r > 1n",j 8.7.1n),f=2.6("r > 1i",j 8.7.1i);b&&f.C(b);g.25(a,3(a,b){"24"===b?(f.1U(a),f.F(d.6("9"))):f.F(v);c(a,b)})},1Y:3(a,b){2.6("9").1X(2.6("r > 1e",j 8.7.1e(2.z(a),b)))},29:3(a,b){2.6("r > 1f",j 8.7.1f).28(a,b)}});o.1h.p({1d:3(a,b){4 2.n("1d",a,b)},1a:3(a){4 2.n("1a",a)},19:3(a,b){4 2.n("19",a,b)},1b:3(a,b){4 2.n("1b",a,b)},1o:3(a,b){4 2.n("1o",a,b)},1k:3(a){4 2.n("1k",a)},1j:3(a){4 2.n("1j",a)},1m:3(a){8.7.x.1p(2[0],a)},n:3(a,b,c){8.7&&2[0]t 8.7.1c?8.7.x.1L(2[0],a,b):c?2.1g(a,b,c):2.1g(a,b);4 2}})})(o);',62,144,'||this|function|return||get|maps|google|map||||||||||new|var|if|options|addEventListener|jQuery|extend|instance|services|overlays|instanceof|split|null|clear|event|_call|_unwrap|_c|FusionTablesLayer|setOptions|el|_latLng|setMap|prototype|markers|property|arguments|LatLng|push|bounds|length|set|for|Array|data|iw|id|center|_init|position|replace|_create|hasOwnProperty|in|delimiter|value|_setup|slice|call|addBounds|getPosition|apply|dblclick|rightclick|mouseover|MVCObject|click|StreetViewPanorama|Geocoder|bind|fn|DirectionsRenderer|dragend|drag|else|triggerEvent|DirectionsService|mouseout|trigger|fitBounds|inViewport|getBounds|contains|addMarker|Marker|namespace|addControl|controls|ui|each|gmap|roadmap|mapTypeId|string|pluginName|typeof|substring|_|init|bounds_changed|addListener|LatLngBounds|callback|option|zoom|Map|addListenerOnce|InfoWindow|name|setDirections|isFunction|Object|setStreetView|displayStreetView|removeData|loadKML|loadFusion|addShape|KmlLayer|OK|route|displayDirections|openInfoWindow|geocode|search|open|find|clearInstanceListeners|indexOf|inArray|refresh|resize|setCenter|destroy|getCenter'.split('|'),0,{}))
\ No newline at end of file
diff --git a/static/libjs/menu.js b/static/libjs/menu.js
new file mode 100644
index 0000000000000000000000000000000000000000..07ca8bf443635eea04f8052a26db739da3b590a6
--- /dev/null
+++ b/static/libjs/menu.js
@@ -0,0 +1,63 @@
+/* -*- mode: javascript -*-
+ */
+
+function getMouseXY(e) // works on IE6,FF,Moz,Opera7
+{ 
+  if (!e) e = window.event; // works on IE, but not NS (we rely on NS passing us the event)
+
+  if (e)
+  { 
+    if (e.pageY)
+    { // this doesn't work on IE6!! (works on FF,Moz,Opera7)
+      mousey = e.pageY;
+      algor = '[e.pageX]';
+      if (e.clientX || e.clientY) algor += ' [e.clientX] '
+    }
+    else if (e.clientY)
+    { // works on IE6,FF,Moz,Opera7
+if ( document.documentElement && document.documentElement.scrollTop )	
+	{
+      mousey = e.clientY + document.documentElement.scrollTop;
+	}
+	
+	else
+	{
+      mousey = e.clientY + document.body.scrollTop;
+	}
+      algor = '[e.clientX]';
+      if (e.pageX || e.pageY) algor += ' [e.pageX] '
+    }
+  }
+}
+
+var menu_firefox_flicker = false ;
+
+
+var mousey = 0
+
+function MenuDisplay(l_element)
+	{
+	getMouseXY()
+	if ( ! menu_firefox_flicker )
+		{
+		l_element.childNodes[1].style.display = 'block' ;
+		if ( mousey > 600 )
+			{
+			l_element.childNodes[1].style.left = '0px' ;
+			l_element.childNodes[1].style.display = 'block' ;
+			l_element.childNodes[1].style.top = - l_element.childNodes[1].offsetHeight + 'px' ;
+			}
+		}
+	else if ( mousey > 600 )
+		{
+		l_element.childNodes[1].style.top = - l_element.childNodes[1].offsetHeight + 'px' ;
+		}
+	}
+	
+function MenuHide(l_element)
+	{
+	if ( ! menu_firefox_flicker )
+		{
+		l_element.childNodes[1].style.display = 'none'
+		}
+	}
diff --git a/static/libjs/moment.min.js b/static/libjs/moment.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..29e1cef97a407ad776704f4df216a8774191ec10
--- /dev/null
+++ b/static/libjs/moment.min.js
@@ -0,0 +1,6 @@
+//! moment.js
+//! version : 2.5.1
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+(function(a){function b(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function c(a,b){return function(c){return k(a.call(this,c),b)}}function d(a,b){return function(c){return this.lang().ordinal(a.call(this,c),b)}}function e(){}function f(a){w(a),h(this,a)}function g(a){var b=q(a),c=b.year||0,d=b.month||0,e=b.week||0,f=b.day||0,g=b.hour||0,h=b.minute||0,i=b.second||0,j=b.millisecond||0;this._milliseconds=+j+1e3*i+6e4*h+36e5*g,this._days=+f+7*e,this._months=+d+12*c,this._data={},this._bubble()}function h(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return b.hasOwnProperty("toString")&&(a.toString=b.toString),b.hasOwnProperty("valueOf")&&(a.valueOf=b.valueOf),a}function i(a){var b,c={};for(b in a)a.hasOwnProperty(b)&&qb.hasOwnProperty(b)&&(c[b]=a[b]);return c}function j(a){return 0>a?Math.ceil(a):Math.floor(a)}function k(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.length<b;)d="0"+d;return(e?c?"+":"":"-")+d}function l(a,b,c,d){var e,f,g=b._milliseconds,h=b._days,i=b._months;g&&a._d.setTime(+a._d+g*c),(h||i)&&(e=a.minute(),f=a.hour()),h&&a.date(a.date()+h*c),i&&a.month(a.month()+i*c),g&&!d&&db.updateOffset(a),(h||i)&&(a.minute(e),a.hour(f))}function m(a){return"[object Array]"===Object.prototype.toString.call(a)}function n(a){return"[object Date]"===Object.prototype.toString.call(a)||a instanceof Date}function o(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&s(a[d])!==s(b[d]))&&g++;return g+f}function p(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=Tb[a]||Ub[b]||b}return a}function q(a){var b,c,d={};for(c in a)a.hasOwnProperty(c)&&(b=p(c),b&&(d[b]=a[c]));return d}function r(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}db[b]=function(e,f){var g,h,i=db.fn._lang[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=db().utc().set(d,a);return i.call(db.fn._lang,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function s(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function t(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function u(a){return v(a)?366:365}function v(a){return a%4===0&&a%100!==0||a%400===0}function w(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[jb]<0||a._a[jb]>11?jb:a._a[kb]<1||a._a[kb]>t(a._a[ib],a._a[jb])?kb:a._a[lb]<0||a._a[lb]>23?lb:a._a[mb]<0||a._a[mb]>59?mb:a._a[nb]<0||a._a[nb]>59?nb:a._a[ob]<0||a._a[ob]>999?ob:-1,a._pf._overflowDayOfYear&&(ib>b||b>kb)&&(b=kb),a._pf.overflow=b)}function x(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function y(a){return a?a.toLowerCase().replace("_","-"):a}function z(a,b){return b._isUTC?db(a).zone(b._offset||0):db(a).local()}function A(a,b){return b.abbr=a,pb[a]||(pb[a]=new e),pb[a].set(b),pb[a]}function B(a){delete pb[a]}function C(a){var b,c,d,e,f=0,g=function(a){if(!pb[a]&&rb)try{require("./lang/"+a)}catch(b){}return pb[a]};if(!a)return db.fn._lang;if(!m(a)){if(c=g(a))return c;a=[a]}for(;f<a.length;){for(e=y(a[f]).split("-"),b=e.length,d=y(a[f+1]),d=d?d.split("-"):null;b>0;){if(c=g(e.slice(0,b).join("-")))return c;if(d&&d.length>=b&&o(e,d,!0)>=b-1)break;b--}f++}return db.fn._lang}function D(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function E(a){var b,c,d=a.match(vb);for(b=0,c=d.length;c>b;b++)d[b]=Yb[d[b]]?Yb[d[b]]:D(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function F(a,b){return a.isValid()?(b=G(b,a.lang()),Vb[b]||(Vb[b]=E(b)),Vb[b](a)):a.lang().invalidDate()}function G(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(wb.lastIndex=0;d>=0&&wb.test(a);)a=a.replace(wb,c),wb.lastIndex=0,d-=1;return a}function H(a,b){var c,d=b._strict;switch(a){case"DDDD":return Ib;case"YYYY":case"GGGG":case"gggg":return d?Jb:zb;case"Y":case"G":case"g":return Lb;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?Kb:Ab;case"S":if(d)return Gb;case"SS":if(d)return Hb;case"SSS":if(d)return Ib;case"DDD":return yb;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Cb;case"a":case"A":return C(b._l)._meridiemParse;case"X":return Fb;case"Z":case"ZZ":return Db;case"T":return Eb;case"SSSS":return Bb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?Hb:xb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return xb;default:return c=new RegExp(P(O(a.replace("\\","")),"i"))}}function I(a){a=a||"";var b=a.match(Db)||[],c=b[b.length-1]||[],d=(c+"").match(Qb)||["-",0,0],e=+(60*d[1])+s(d[2]);return"+"===d[0]?-e:e}function J(a,b,c){var d,e=c._a;switch(a){case"M":case"MM":null!=b&&(e[jb]=s(b)-1);break;case"MMM":case"MMMM":d=C(c._l).monthsParse(b),null!=d?e[jb]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[kb]=s(b));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=s(b));break;case"YY":e[ib]=s(b)+(s(b)>68?1900:2e3);break;case"YYYY":case"YYYYY":case"YYYYYY":e[ib]=s(b);break;case"a":case"A":c._isPm=C(c._l).isPM(b);break;case"H":case"HH":case"h":case"hh":e[lb]=s(b);break;case"m":case"mm":e[mb]=s(b);break;case"s":case"ss":e[nb]=s(b);break;case"S":case"SS":case"SSS":case"SSSS":e[ob]=s(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=I(b);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":a=a.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=b)}}function K(a){var b,c,d,e,f,g,h,i,j,k,l=[];if(!a._d){for(d=M(a),a._w&&null==a._a[kb]&&null==a._a[jb]&&(f=function(b){var c=parseInt(b,10);return b?b.length<3?c>68?1900+c:2e3+c:c:null==a._a[ib]?db().weekYear():a._a[ib]},g=a._w,null!=g.GG||null!=g.W||null!=g.E?h=Z(f(g.GG),g.W||1,g.E,4,1):(i=C(a._l),j=null!=g.d?V(g.d,i):null!=g.e?parseInt(g.e,10)+i._week.dow:0,k=parseInt(g.w,10)||1,null!=g.d&&j<i._week.dow&&k++,h=Z(f(g.gg),k,j,i._week.doy,i._week.dow)),a._a[ib]=h.year,a._dayOfYear=h.dayOfYear),a._dayOfYear&&(e=null==a._a[ib]?d[ib]:a._a[ib],a._dayOfYear>u(e)&&(a._pf._overflowDayOfYear=!0),c=U(e,0,a._dayOfYear),a._a[jb]=c.getUTCMonth(),a._a[kb]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=l[b]=d[b];for(;7>b;b++)a._a[b]=l[b]=null==a._a[b]?2===b?1:0:a._a[b];l[lb]+=s((a._tzm||0)/60),l[mb]+=s((a._tzm||0)%60),a._d=(a._useUTC?U:T).apply(null,l)}}function L(a){var b;a._d||(b=q(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],K(a))}function M(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function N(a){a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=C(a._l),h=""+a._i,i=h.length,j=0;for(d=G(a._f,g).match(vb)||[],b=0;b<d.length;b++)e=d[b],c=(h.match(H(e,a))||[])[0],c&&(f=h.substr(0,h.indexOf(c)),f.length>0&&a._pf.unusedInput.push(f),h=h.slice(h.indexOf(c)+c.length),j+=c.length),Yb[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),J(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=i-j,h.length>0&&a._pf.unusedInput.push(h),a._isPm&&a._a[lb]<12&&(a._a[lb]+=12),a._isPm===!1&&12===a._a[lb]&&(a._a[lb]=0),K(a),w(a)}function O(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function P(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function Q(a){var c,d,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,a._d=new Date(0/0),void 0;for(f=0;f<a._f.length;f++)g=0,c=h({},a),c._pf=b(),c._f=a._f[f],N(c),x(c)&&(g+=c._pf.charsLeftOver,g+=10*c._pf.unusedTokens.length,c._pf.score=g,(null==e||e>g)&&(e=g,d=c));h(a,d||c)}function R(a){var b,c,d=a._i,e=Mb.exec(d);if(e){for(a._pf.iso=!0,b=0,c=Ob.length;c>b;b++)if(Ob[b][1].exec(d)){a._f=Ob[b][0]+(e[6]||" ");break}for(b=0,c=Pb.length;c>b;b++)if(Pb[b][1].exec(d)){a._f+=Pb[b][0];break}d.match(Db)&&(a._f+="Z"),N(a)}else a._d=new Date(d)}function S(b){var c=b._i,d=sb.exec(c);c===a?b._d=new Date:d?b._d=new Date(+d[1]):"string"==typeof c?R(b):m(c)?(b._a=c.slice(0),K(b)):n(c)?b._d=new Date(+c):"object"==typeof c?L(b):b._d=new Date(c)}function T(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function U(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function V(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function W(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function X(a,b,c){var d=hb(Math.abs(a)/1e3),e=hb(d/60),f=hb(e/60),g=hb(f/24),h=hb(g/365),i=45>d&&["s",d]||1===e&&["m"]||45>e&&["mm",e]||1===f&&["h"]||22>f&&["hh",f]||1===g&&["d"]||25>=g&&["dd",g]||45>=g&&["M"]||345>g&&["MM",hb(g/30)]||1===h&&["y"]||["yy",h];return i[2]=b,i[3]=a>0,i[4]=c,W.apply({},i)}function Y(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=db(a).add("d",f),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function Z(a,b,c,d,e){var f,g,h=U(a,0,1).getUTCDay();return c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:u(a-1)+g}}function $(a){var b=a._i,c=a._f;return null===b?db.invalid({nullInput:!0}):("string"==typeof b&&(a._i=b=C().preparse(b)),db.isMoment(b)?(a=i(b),a._d=new Date(+b._d)):c?m(c)?Q(a):N(a):S(a),new f(a))}function _(a,b){db.fn[a]=db.fn[a+"s"]=function(a){var c=this._isUTC?"UTC":"";return null!=a?(this._d["set"+c+b](a),db.updateOffset(this),this):this._d["get"+c+b]()}}function ab(a){db.duration.fn[a]=function(){return this._data[a]}}function bb(a,b){db.duration.fn["as"+a]=function(){return+this/b}}function cb(a){var b=!1,c=db;"undefined"==typeof ender&&(a?(gb.moment=function(){return!b&&console&&console.warn&&(b=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),c.apply(null,arguments)},h(gb.moment,c)):gb.moment=db)}for(var db,eb,fb="2.5.1",gb=this,hb=Math.round,ib=0,jb=1,kb=2,lb=3,mb=4,nb=5,ob=6,pb={},qb={_isAMomentObject:null,_i:null,_f:null,_l:null,_strict:null,_isUTC:null,_offset:null,_pf:null,_lang:null},rb="undefined"!=typeof module&&module.exports&&"undefined"!=typeof require,sb=/^\/?Date\((\-?\d+)/i,tb=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,ub=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,vb=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,wb=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,xb=/\d\d?/,yb=/\d{1,3}/,zb=/\d{1,4}/,Ab=/[+\-]?\d{1,6}/,Bb=/\d+/,Cb=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Db=/Z|[\+\-]\d\d:?\d\d/gi,Eb=/T/i,Fb=/[\+\-]?\d+(\.\d{1,3})?/,Gb=/\d/,Hb=/\d\d/,Ib=/\d{3}/,Jb=/\d{4}/,Kb=/[+-]?\d{6}/,Lb=/[+-]?\d+/,Mb=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Nb="YYYY-MM-DDTHH:mm:ssZ",Ob=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],Pb=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Qb=/([\+\-]|\d\d)/gi,Rb="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),Sb={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},Tb={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},Ub={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Vb={},Wb="DDD w W M D d".split(" "),Xb="M D H h m s w W".split(" "),Yb={M:function(){return this.month()+1},MMM:function(a){return this.lang().monthsShort(this,a)},MMMM:function(a){return this.lang().months(this,a)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(a){return this.lang().weekdaysMin(this,a)},ddd:function(a){return this.lang().weekdaysShort(this,a)},dddd:function(a){return this.lang().weekdays(this,a)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return k(this.year()%100,2)},YYYY:function(){return k(this.year(),4)},YYYYY:function(){return k(this.year(),5)},YYYYYY:function(){var a=this.year(),b=a>=0?"+":"-";return b+k(Math.abs(a),6)},gg:function(){return k(this.weekYear()%100,2)},gggg:function(){return k(this.weekYear(),4)},ggggg:function(){return k(this.weekYear(),5)},GG:function(){return k(this.isoWeekYear()%100,2)},GGGG:function(){return k(this.isoWeekYear(),4)},GGGGG:function(){return k(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return s(this.milliseconds()/100)},SS:function(){return k(s(this.milliseconds()/10),2)},SSS:function(){return k(this.milliseconds(),3)},SSSS:function(){return k(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+k(s(a/60),2)+":"+k(s(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+k(s(a/60),2)+k(s(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},Zb=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];Wb.length;)eb=Wb.pop(),Yb[eb+"o"]=d(Yb[eb],eb);for(;Xb.length;)eb=Xb.pop(),Yb[eb+eb]=c(Yb[eb],2);for(Yb.DDDD=c(Yb.DDD,3),h(e.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=db.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=db([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return Y(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),db=function(c,d,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._i=c,g._f=d,g._l=e,g._strict=f,g._isUTC=!1,g._pf=b(),$(g)},db.utc=function(c,d,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._useUTC=!0,g._isUTC=!0,g._l=e,g._i=c,g._f=d,g._strict=f,g._pf=b(),$(g).utc()},db.unix=function(a){return db(1e3*a)},db.duration=function(a,b){var c,d,e,f=a,h=null;return db.isDuration(a)?f={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(f={},b?f[b]=a:f.milliseconds=a):(h=tb.exec(a))?(c="-"===h[1]?-1:1,f={y:0,d:s(h[kb])*c,h:s(h[lb])*c,m:s(h[mb])*c,s:s(h[nb])*c,ms:s(h[ob])*c}):(h=ub.exec(a))&&(c="-"===h[1]?-1:1,e=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*c},f={y:e(h[2]),M:e(h[3]),d:e(h[4]),h:e(h[5]),m:e(h[6]),s:e(h[7]),w:e(h[8])}),d=new g(f),db.isDuration(a)&&a.hasOwnProperty("_lang")&&(d._lang=a._lang),d},db.version=fb,db.defaultFormat=Nb,db.updateOffset=function(){},db.lang=function(a,b){var c;return a?(b?A(y(a),b):null===b?(B(a),a="en"):pb[a]||C(a),c=db.duration.fn._lang=db.fn._lang=C(a),c._abbr):db.fn._lang._abbr},db.langData=function(a){return a&&a._lang&&a._lang._abbr&&(a=a._lang._abbr),C(a)},db.isMoment=function(a){return a instanceof f||null!=a&&a.hasOwnProperty("_isAMomentObject")},db.isDuration=function(a){return a instanceof g},eb=Zb.length-1;eb>=0;--eb)r(Zb[eb]);for(db.normalizeUnits=function(a){return p(a)},db.invalid=function(a){var b=db.utc(0/0);return null!=a?h(b._pf,a):b._pf.userInvalidated=!0,b},db.parseZone=function(a){return db(a).parseZone()},h(db.fn=f.prototype,{clone:function(){return db(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=db(this).utc();return 0<a.year()&&a.year()<=9999?F(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):F(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var a=this;return[a.year(),a.month(),a.date(),a.hours(),a.minutes(),a.seconds(),a.milliseconds()]},isValid:function(){return x(this)},isDSTShifted:function(){return this._a?this.isValid()&&o(this._a,(this._isUTC?db.utc(this._a):db(this._a)).toArray())>0:!1},parsingFlags:function(){return h({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(a){var b=F(this,a||db.defaultFormat);return this.lang().postformat(b)},add:function(a,b){var c;return c="string"==typeof a?db.duration(+b,a):db.duration(a,b),l(this,c,1),this},subtract:function(a,b){var c;return c="string"==typeof a?db.duration(+b,a):db.duration(a,b),l(this,c,-1),this},diff:function(a,b,c){var d,e,f=z(a,this),g=6e4*(this.zone()-f.zone());return b=p(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-db(this).startOf("month")-(f-db(f).startOf("month")))/d,e-=6e4*(this.zone()-db(this).startOf("month").zone()-(f.zone()-db(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:j(e)},from:function(a,b){return db.duration(this.diff(a)).lang(this.lang()._abbr).humanize(!b)},fromNow:function(a){return this.from(db(),a)},calendar:function(){var a=z(db(),this).startOf("day"),b=this.diff(a,"days",!0),c=-6>b?"sameElse":-1>b?"lastWeek":0>b?"lastDay":1>b?"sameDay":2>b?"nextDay":7>b?"nextWeek":"sameElse";return this.format(this.lang().calendar(c,this))},isLeapYear:function(){return v(this.year())},isDST:function(){return this.zone()<this.clone().month(0).zone()||this.zone()<this.clone().month(5).zone()},day:function(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=V(a,this.lang()),this.add({d:a-b})):b},month:function(a){var b,c=this._isUTC?"UTC":"";return null!=a?"string"==typeof a&&(a=this.lang().monthsParse(a),"number"!=typeof a)?this:(b=this.date(),this.date(1),this._d["set"+c+"Month"](a),this.date(Math.min(b,this.daysInMonth())),db.updateOffset(this),this):this._d["get"+c+"Month"]()},startOf:function(a){switch(a=p(a)){case"year":this.month(0);case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a?this.weekday(0):"isoWeek"===a&&this.isoWeekday(1),this},endOf:function(a){return a=p(a),this.startOf(a).add("isoWeek"===a?"week":a,1).subtract("ms",1)},isAfter:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)>+db(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+db(a).startOf(b)},isSame:function(a,b){return b=b||"ms",+this.clone().startOf(b)===+z(a,this).startOf(b)},min:function(a){return a=db.apply(null,arguments),this>a?this:a},max:function(a){return a=db.apply(null,arguments),a>this?this:a},zone:function(a){var b=this._offset||0;return null==a?this._isUTC?b:this._d.getTimezoneOffset():("string"==typeof a&&(a=I(a)),Math.abs(a)<16&&(a=60*a),this._offset=a,this._isUTC=!0,b!==a&&l(this,db.duration(b-a,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?db(a).zone():0,(this.zone()-a)%60===0},daysInMonth:function(){return t(this.year(),this.month())},dayOfYear:function(a){var b=hb((db(this).startOf("day")-db(this).startOf("year"))/864e5)+1;return null==a?b:this.add("d",a-b)},quarter:function(){return Math.ceil((this.month()+1)/3)},weekYear:function(a){var b=Y(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==a?b:this.add("y",a-b)},isoWeekYear:function(a){var b=Y(this,1,4).year;return null==a?b:this.add("y",a-b)},week:function(a){var b=this.lang().week(this);return null==a?b:this.add("d",7*(a-b))},isoWeek:function(a){var b=Y(this,1,4).week;return null==a?b:this.add("d",7*(a-b))},weekday:function(a){var b=(this.day()+7-this.lang()._week.dow)%7;return null==a?b:this.add("d",a-b)},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},get:function(a){return a=p(a),this[a]()},set:function(a,b){return a=p(a),"function"==typeof this[a]&&this[a](b),this},lang:function(b){return b===a?this._lang:(this._lang=C(b),this)}}),eb=0;eb<Rb.length;eb++)_(Rb[eb].toLowerCase().replace(/s$/,""),Rb[eb]);_("year","FullYear"),db.fn.days=db.fn.day,db.fn.months=db.fn.month,db.fn.weeks=db.fn.week,db.fn.isoWeeks=db.fn.isoWeek,db.fn.toJSON=db.fn.toISOString,h(db.duration.fn=g.prototype,{_bubble:function(){var a,b,c,d,e=this._milliseconds,f=this._days,g=this._months,h=this._data;h.milliseconds=e%1e3,a=j(e/1e3),h.seconds=a%60,b=j(a/60),h.minutes=b%60,c=j(b/60),h.hours=c%24,f+=j(c/24),h.days=f%30,g+=j(f/30),h.months=g%12,d=j(g/12),h.years=d},weeks:function(){return j(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*s(this._months/12)},humanize:function(a){var b=+this,c=X(b,!a,this.lang());return a&&(c=this.lang().pastFuture(b,c)),this.lang().postformat(c)},add:function(a,b){var c=db.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=db.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=p(a),this[a.toLowerCase()+"s"]()},as:function(a){return a=p(a),this["as"+a.charAt(0).toUpperCase()+a.slice(1)+"s"]()},lang:db.fn.lang,toIsoString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"}});for(eb in Sb)Sb.hasOwnProperty(eb)&&(bb(eb,Sb[eb]),ab(eb.toLowerCase()));bb("Weeks",6048e5),db.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},db.lang("en",{ordinal:function(a){var b=a%10,c=1===s(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),rb?(module.exports=db,cb(!0)):"function"==typeof define&&define.amd?define("moment",function(b,c,d){return d.config&&d.config()&&d.config().noGlobal!==!0&&cb(d.config().noGlobal===a),db}):cb()}).call(this);
\ No newline at end of file
diff --git a/static/libjs/purl.js b/static/libjs/purl.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5799c6cf4c4014566de2b89f9093306b59c48d5
--- /dev/null
+++ b/static/libjs/purl.js
@@ -0,0 +1,267 @@
+/*
+ * Purl (A JavaScript URL parser) v2.3.1
+ * Developed and maintanined by Mark Perkins, mark@allmarkedup.com
+ * Source repository: https://github.com/allmarkedup/jQuery-URL-Parser
+ * Licensed under an MIT-style license. See https://github.com/allmarkedup/jQuery-URL-Parser/blob/master/LICENSE for details.
+ */
+
+;(function(factory) {
+    if (typeof define === 'function' && define.amd) {
+        define(factory);
+    } else {
+        window.purl = factory();
+    }
+})(function() {
+
+    var tag2attr = {
+            a       : 'href',
+            img     : 'src',
+            form    : 'action',
+            base    : 'href',
+            script  : 'src',
+            iframe  : 'src',
+            link    : 'href',
+            embed   : 'src',
+            object  : 'data'
+        },
+
+        key = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'fragment'], // keys available to query
+
+        aliases = { 'anchor' : 'fragment' }, // aliases for backwards compatability
+
+        parser = {
+            strict : /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,  //less intuitive, more accurate to the specs
+            loose :  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ // more intuitive, fails on relative paths and deviates from specs
+        },
+
+        isint = /^[0-9]+$/;
+
+    function parseUri( url, strictMode ) {
+        var str = decodeURI( url ),
+        res   = parser[ strictMode || false ? 'strict' : 'loose' ].exec( str ),
+        uri = { attr : {}, param : {}, seg : {} },
+        i   = 14;
+
+        while ( i-- ) {
+            uri.attr[ key[i] ] = res[i] || '';
+        }
+
+        // build query and fragment parameters
+        uri.param['query'] = parseString(uri.attr['query']);
+        uri.param['fragment'] = parseString(uri.attr['fragment']);
+
+        // split path and fragement into segments
+        uri.seg['path'] = uri.attr.path.replace(/^\/+|\/+$/g,'').split('/');
+        uri.seg['fragment'] = uri.attr.fragment.replace(/^\/+|\/+$/g,'').split('/');
+
+        // compile a 'base' domain attribute
+        uri.attr['base'] = uri.attr.host ? (uri.attr.protocol ?  uri.attr.protocol+'://'+uri.attr.host : uri.attr.host) + (uri.attr.port ? ':'+uri.attr.port : '') : '';
+
+        return uri;
+    }
+
+    function getAttrName( elm ) {
+        var tn = elm.tagName;
+        if ( typeof tn !== 'undefined' ) return tag2attr[tn.toLowerCase()];
+        return tn;
+    }
+
+    function promote(parent, key) {
+        if (parent[key].length === 0) return parent[key] = {};
+        var t = {};
+        for (var i in parent[key]) t[i] = parent[key][i];
+        parent[key] = t;
+        return t;
+    }
+
+    function parse(parts, parent, key, val) {
+        var part = parts.shift();
+        if (!part) {
+            if (isArray(parent[key])) {
+                parent[key].push(val);
+            } else if ('object' == typeof parent[key]) {
+                parent[key] = val;
+            } else if ('undefined' == typeof parent[key]) {
+                parent[key] = val;
+            } else {
+                parent[key] = [parent[key], val];
+            }
+        } else {
+            var obj = parent[key] = parent[key] || [];
+            if (']' == part) {
+                if (isArray(obj)) {
+                    if ('' !== val) obj.push(val);
+                } else if ('object' == typeof obj) {
+                    obj[keys(obj).length] = val;
+                } else {
+                    obj = parent[key] = [parent[key], val];
+                }
+            } else if (~part.indexOf(']')) {
+                part = part.substr(0, part.length - 1);
+                if (!isint.test(part) && isArray(obj)) obj = promote(parent, key);
+                parse(parts, obj, part, val);
+                // key
+            } else {
+                if (!isint.test(part) && isArray(obj)) obj = promote(parent, key);
+                parse(parts, obj, part, val);
+            }
+        }
+    }
+
+    function merge(parent, key, val) {
+        if (~key.indexOf(']')) {
+            var parts = key.split('[');
+            parse(parts, parent, 'base', val);
+        } else {
+            if (!isint.test(key) && isArray(parent.base)) {
+                var t = {};
+                for (var k in parent.base) t[k] = parent.base[k];
+                parent.base = t;
+            }
+            if (key !== '') {
+                set(parent.base, key, val);
+            }
+        }
+        return parent;
+    }
+
+    function parseString(str) {
+        return reduce(String(str).split(/&|;/), function(ret, pair) {
+            try {
+                pair = decodeURIComponent(pair.replace(/\+/g, ' '));
+            } catch(e) {
+                // ignore
+            }
+            var eql = pair.indexOf('='),
+                brace = lastBraceInKey(pair),
+                key = pair.substr(0, brace || eql),
+                val = pair.substr(brace || eql, pair.length);
+
+            val = val.substr(val.indexOf('=') + 1, val.length);
+
+            if (key === '') {
+                key = pair;
+                val = '';
+            }
+
+            return merge(ret, key, val);
+        }, { base: {} }).base;
+    }
+
+    function set(obj, key, val) {
+        var v = obj[key];
+        if (typeof v === 'undefined') {
+            obj[key] = val;
+        } else if (isArray(v)) {
+            v.push(val);
+        } else {
+            obj[key] = [v, val];
+        }
+    }
+
+    function lastBraceInKey(str) {
+        var len = str.length,
+            brace,
+            c;
+        for (var i = 0; i < len; ++i) {
+            c = str[i];
+            if (']' == c) brace = false;
+            if ('[' == c) brace = true;
+            if ('=' == c && !brace) return i;
+        }
+    }
+
+    function reduce(obj, accumulator){
+        var i = 0,
+            l = obj.length >> 0,
+            curr = arguments[2];
+        while (i < l) {
+            if (i in obj) curr = accumulator.call(undefined, curr, obj[i], i, obj);
+            ++i;
+        }
+        return curr;
+    }
+
+    function isArray(vArg) {
+        return Object.prototype.toString.call(vArg) === "[object Array]";
+    }
+
+    function keys(obj) {
+        var key_array = [];
+        for ( var prop in obj ) {
+            if ( obj.hasOwnProperty(prop) ) key_array.push(prop);
+        }
+        return key_array;
+    }
+
+    function purl( url, strictMode ) {
+        if ( arguments.length === 1 && url === true ) {
+            strictMode = true;
+            url = undefined;
+        }
+        strictMode = strictMode || false;
+        url = url || window.location.toString();
+
+        return {
+
+            data : parseUri(url, strictMode),
+
+            // get various attributes from the URI
+            attr : function( attr ) {
+                attr = aliases[attr] || attr;
+                return typeof attr !== 'undefined' ? this.data.attr[attr] : this.data.attr;
+            },
+
+            // return query string parameters
+            param : function( param ) {
+                return typeof param !== 'undefined' ? this.data.param.query[param] : this.data.param.query;
+            },
+
+            // return fragment parameters
+            fparam : function( param ) {
+                return typeof param !== 'undefined' ? this.data.param.fragment[param] : this.data.param.fragment;
+            },
+
+            // return path segments
+            segment : function( seg ) {
+                if ( typeof seg === 'undefined' ) {
+                    return this.data.seg.path;
+                } else {
+                    seg = seg < 0 ? this.data.seg.path.length + seg : seg - 1; // negative segments count from the end
+                    return this.data.seg.path[seg];
+                }
+            },
+
+            // return fragment segments
+            fsegment : function( seg ) {
+                if ( typeof seg === 'undefined' ) {
+                    return this.data.seg.fragment;
+                } else {
+                    seg = seg < 0 ? this.data.seg.fragment.length + seg : seg - 1; // negative segments count from the end
+                    return this.data.seg.fragment[seg];
+                }
+            }
+
+        };
+
+    }
+    
+    purl.jQuery = function($){
+        if ($ != null) {
+            $.fn.url = function( strictMode ) {
+                var url = '';
+                if ( this.length ) {
+                    url = $(this).attr( getAttrName(this[0]) ) || '';
+                }
+                return purl( url, strictMode );
+            };
+
+            $.url = purl;
+        }
+    };
+
+    purl.jQuery(window.jQuery);
+
+    return purl;
+
+});
diff --git a/static/libjs/qtip/LICENSE b/static/libjs/qtip/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..e9cd5ecb5b66da2d53420906c4b3d6635900c41b
--- /dev/null
+++ b/static/libjs/qtip/LICENSE
@@ -0,0 +1,7 @@
+Copyright © 2009 Craig Thompson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
\ No newline at end of file
diff --git a/static/libjs/qtip/jquery.qtip-3.0.3.min.css b/static/libjs/qtip/jquery.qtip-3.0.3.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..89782933f2ab97d36941e4db5ae9c4c73edf010c
--- /dev/null
+++ b/static/libjs/qtip/jquery.qtip-3.0.3.min.css
@@ -0,0 +1 @@
+#qtip-overlay.blurs,.qtip-close{cursor:pointer}.qtip{position:absolute;left:-28000px;top:-28000px;display:none;max-width:280px;min-width:50px;font-size:10.5px;line-height:12px;direction:ltr;box-shadow:none;padding:0}.qtip-content,.qtip-titlebar{position:relative;overflow:hidden}.qtip-content{padding:5px 9px;text-align:left;word-wrap:break-word}.qtip-titlebar{padding:5px 35px 5px 10px;border-width:0 0 1px;font-weight:700}.qtip-titlebar+.qtip-content{border-top-width:0!important}.qtip-close{position:absolute;right:-9px;top:-9px;z-index:11;outline:0;border:1px solid transparent}.qtip-titlebar .qtip-close{right:4px;top:50%;margin-top:-9px}* html .qtip-titlebar .qtip-close{top:16px}.qtip-icon .ui-icon,.qtip-titlebar .ui-icon{display:block;text-indent:-1000em;direction:ltr}.qtip-icon,.qtip-icon .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;text-decoration:none}.qtip-icon .ui-icon{width:18px;height:14px;line-height:14px;text-align:center;text-indent:0;font:normal 700 10px/13px Tahoma,sans-serif;color:inherit;background:-100em -100em no-repeat}.qtip-default{border:1px solid #F1D031;background-color:#FFFFA3;color:#555}.qtip-default .qtip-titlebar{background-color:#FFEF93}.qtip-default .qtip-icon{border-color:#CCC;background:#F1F1F1;color:#777}.qtip-default .qtip-titlebar .qtip-close{border-color:#AAA;color:#111}.qtip-light{background-color:#fff;border-color:#E2E2E2;color:#454545}.qtip-light .qtip-titlebar{background-color:#f1f1f1}.qtip-dark{background-color:#505050;border-color:#303030;color:#f3f3f3}.qtip-dark .qtip-titlebar{background-color:#404040}.qtip-dark .qtip-icon{border-color:#444}.qtip-dark .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-cream{background-color:#FBF7AA;border-color:#F9E98E;color:#A27D35}.qtip-red,.qtip-red .qtip-icon,.qtip-red .qtip-titlebar .ui-state-hover{border-color:#D95252}.qtip-cream .qtip-titlebar{background-color:#F0DE7D}.qtip-cream .qtip-close .qtip-icon{background-position:-82px 0}.qtip-red{background-color:#F78B83;color:#912323}.qtip-red .qtip-titlebar{background-color:#F06D65}.qtip-red .qtip-close .qtip-icon{background-position:-102px 0}.qtip-green{background-color:#CAED9E;border-color:#90D93F;color:#3F6219}.qtip-green .qtip-titlebar{background-color:#B0DE78}.qtip-green .qtip-close .qtip-icon{background-position:-42px 0}.qtip-blue{background-color:#E5F6FE;border-color:#ADD9ED;color:#5E99BD}.qtip-blue .qtip-titlebar{background-color:#D0E9F5}.qtip-blue .qtip-close .qtip-icon{background-position:-2px 0}.qtip-shadow{-webkit-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);-moz-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);box-shadow:1px 1px 3px 1px rgba(0,0,0,.15)}.qtip-bootstrap,.qtip-rounded,.qtip-tipsy{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.qtip-rounded .qtip-titlebar{-moz-border-radius:4px 4px 0 0;-webkit-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.qtip-youtube{-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 3px #333;-moz-box-shadow:0 0 3px #333;box-shadow:0 0 3px #333;color:#fff;border:0 solid transparent;background:#4A4A4A;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0,#4A4A4A),color-stop(100%,#000));background-image:-webkit-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-moz-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-ms-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-o-linear-gradient(top,#4A4A4A 0,#000 100%)}.qtip-youtube .qtip-titlebar{background-color:#4A4A4A;background-color:rgba(0,0,0,0)}.qtip-youtube .qtip-content{padding:.75em;font:12px arial,sans-serif;filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);"}.qtip-youtube .qtip-icon{border-color:#222}.qtip-youtube .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-jtools{background:#232323;background:rgba(0,0,0,.7);background-image:-webkit-gradient(linear,left top,left bottom,from(#717171),to(#232323));background-image:-moz-linear-gradient(top,#717171,#232323);background-image:-webkit-linear-gradient(top,#717171,#232323);background-image:-ms-linear-gradient(top,#717171,#232323);background-image:-o-linear-gradient(top,#717171,#232323);border:2px solid #ddd;border:2px solid rgba(241,241,241,1);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 12px #333;-moz-box-shadow:0 0 12px #333;box-shadow:0 0 12px #333}.qtip-jtools .qtip-titlebar{background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4A4A4A);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)"}.qtip-jtools .qtip-content{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A, endColorstr=#232323);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)"}.qtip-jtools .qtip-content,.qtip-jtools .qtip-titlebar{background:0 0;color:#fff;border:0 dashed transparent}.qtip-jtools .qtip-icon{border-color:#555}.qtip-jtools .qtip-titlebar .ui-state-hover{border-color:#333}.qtip-cluetip{-webkit-box-shadow:4px 4px 5px rgba(0,0,0,.4);-moz-box-shadow:4px 4px 5px rgba(0,0,0,.4);box-shadow:4px 4px 5px rgba(0,0,0,.4);background-color:#D9D9C2;color:#111;border:0 dashed transparent}.qtip-cluetip .qtip-titlebar{background-color:#87876A;color:#fff;border:0 dashed transparent}.qtip-cluetip .qtip-icon{border-color:#808064}.qtip-cluetip .qtip-titlebar .ui-state-hover{border-color:#696952;color:#696952}.qtip-tipsy{background:#000;background:rgba(0,0,0,.87);color:#fff;border:0 solid transparent;font-size:11px;font-family:'Lucida Grande',sans-serif;font-weight:700;line-height:16px;text-shadow:0 1px #000}.qtip-tipsy .qtip-titlebar{padding:6px 35px 0 10px;background-color:transparent}.qtip-tipsy .qtip-content{padding:6px 10px}.qtip-tipsy .qtip-icon{border-color:#222;text-shadow:none}.qtip-tipsy .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-tipped{border:3px solid #959FA9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-color:#F9F9F9;color:#454545;font-weight:400;font-family:serif}.qtip-tipped .qtip-titlebar{border-bottom-width:0;color:#fff;background:#3A79B8;background-image:-webkit-gradient(linear,left top,left bottom,from(#3A79B8),to(#2E629D));background-image:-webkit-linear-gradient(top,#3A79B8,#2E629D);background-image:-moz-linear-gradient(top,#3A79B8,#2E629D);background-image:-ms-linear-gradient(top,#3A79B8,#2E629D);background-image:-o-linear-gradient(top,#3A79B8,#2E629D);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8, endColorstr=#2E629D);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)"}.qtip-tipped .qtip-icon{border:2px solid #285589;background:#285589}.qtip-tipped .qtip-icon .ui-icon{background-color:#FBFBFB;color:#555}.qtip-bootstrap{font-size:14px;line-height:20px;color:#333;padding:1px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.qtip-bootstrap .qtip-titlebar{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.qtip-bootstrap .qtip-titlebar .qtip-close{right:11px;top:45%;border-style:none}.qtip-bootstrap .qtip-content{padding:9px 14px}.qtip-bootstrap .qtip-icon{background:0 0}.qtip-bootstrap .qtip-icon .ui-icon{width:auto;height:auto;float:right;font-size:20px;font-weight:700;line-height:18px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}#qtip-overlay,#qtip-overlay div{left:0;top:0;width:100%;height:100%}.qtip-bootstrap .qtip-icon .ui-icon:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}.qtip:not(.ie9haxors) div.qtip-content,.qtip:not(.ie9haxors) div.qtip-titlebar{filter:none;-ms-filter:none}#qtip-overlay{position:fixed}#qtip-overlay div{position:absolute;background-color:#000;opacity:.7;filter:alpha(opacity=70);-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"}
\ No newline at end of file
diff --git a/static/libjs/qtip/jquery.qtip-3.0.3.min.js b/static/libjs/qtip/jquery.qtip-3.0.3.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..0c5f7bff6aa6216b8c22d30f99bbb9ed3d0b93b4
--- /dev/null
+++ b/static/libjs/qtip/jquery.qtip-3.0.3.min.js
@@ -0,0 +1,5 @@
+/* qtip2 v3.0.3 | Plugins: viewport svg modal | Styles: core basic css3 | qtip2.com | Licensed MIT | Fri May 13 2016 08:21:03 */
+
+!function(a,b,c){!function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):jQuery&&!jQuery.fn.qtip&&a(jQuery)}(function(d){"use strict";function e(a,b,c,e){this.id=c,this.target=a,this.tooltip=A,this.elements={target:a},this._id=N+"-"+c,this.timers={img:{}},this.options=b,this.plugins={},this.cache={event:{},target:d(),disabled:z,attr:e,onTooltip:z,lastClass:""},this.rendered=this.destroyed=this.disabled=this.waiting=this.hiddenDuringWait=this.positioning=this.triggering=z}function f(a){return a===A||"object"!==d.type(a)}function g(a){return!(d.isFunction(a)||a&&a.attr||a.length||"object"===d.type(a)&&(a.jquery||a.then))}function h(a){var b,c,e,h;return f(a)?z:(f(a.metadata)&&(a.metadata={type:a.metadata}),"content"in a&&(b=a.content,f(b)||b.jquery||b.done?(c=g(b)?z:b,b=a.content={text:c}):c=b.text,"ajax"in b&&(e=b.ajax,h=e&&e.once!==z,delete b.ajax,b.text=function(a,b){var f=c||d(this).attr(b.options.content.attr)||"Loading...",g=d.ajax(d.extend({},e,{context:b})).then(e.success,A,e.error).then(function(a){return a&&h&&b.set("content.text",a),a},function(a,c,d){b.destroyed||0===a.status||b.set("content.text",c+": "+d)});return h?f:(b.set("content.text",f),g)}),"title"in b&&(d.isPlainObject(b.title)&&(b.button=b.title.button,b.title=b.title.text),g(b.title||z)&&(b.title=z))),"position"in a&&f(a.position)&&(a.position={my:a.position,at:a.position}),"show"in a&&f(a.show)&&(a.show=a.show.jquery?{target:a.show}:a.show===y?{ready:y}:{event:a.show}),"hide"in a&&f(a.hide)&&(a.hide=a.hide.jquery?{target:a.hide}:{event:a.hide}),"style"in a&&f(a.style)&&(a.style={classes:a.style}),d.each(M,function(){this.sanitize&&this.sanitize(a)}),a)}function i(a,b){for(var c,d=0,e=a,f=b.split(".");e=e[f[d++]];)d<f.length&&(c=e);return[c||a,f.pop()]}function j(a,b){var c,d,e;for(c in this.checks)if(this.checks.hasOwnProperty(c))for(d in this.checks[c])this.checks[c].hasOwnProperty(d)&&(e=new RegExp(d,"i").exec(a))&&(b.push(e),("builtin"===c||this.plugins[c])&&this.checks[c][d].apply(this.plugins[c]||this,b))}function k(a){return Q.concat("").join(a?"-"+a+" ":" ")}function l(a,b){return b>0?setTimeout(d.proxy(a,this),b):void a.call(this)}function m(a){this.tooltip.hasClass(X)||(clearTimeout(this.timers.show),clearTimeout(this.timers.hide),this.timers.show=l.call(this,function(){this.toggle(y,a)},this.options.show.delay))}function n(a){if(!this.tooltip.hasClass(X)&&!this.destroyed){var b=d(a.relatedTarget),c=b.closest(R)[0]===this.tooltip[0],e=b[0]===this.options.show.target[0];if(clearTimeout(this.timers.show),clearTimeout(this.timers.hide),this!==b[0]&&"mouse"===this.options.position.target&&c||this.options.hide.fixed&&/mouse(out|leave|move)/.test(a.type)&&(c||e))try{a.preventDefault(),a.stopImmediatePropagation()}catch(f){}else this.timers.hide=l.call(this,function(){this.toggle(z,a)},this.options.hide.delay,this)}}function o(a){!this.tooltip.hasClass(X)&&this.options.hide.inactive&&(clearTimeout(this.timers.inactive),this.timers.inactive=l.call(this,function(){this.hide(a)},this.options.hide.inactive))}function p(a){this.rendered&&this.tooltip[0].offsetWidth>0&&this.reposition(a)}function q(a,c,e){d(b.body).delegate(a,(c.split?c:c.join("."+N+" "))+"."+N,function(){var a=t.api[d.attr(this,P)];a&&!a.disabled&&e.apply(a,arguments)})}function r(a,c,f){var g,i,j,k,l,m=d(b.body),n=a[0]===b?m:a,o=a.metadata?a.metadata(f.metadata):A,p="html5"===f.metadata.type&&o?o[f.metadata.name]:A,q=a.data(f.metadata.name||"qtipopts");try{q="string"==typeof q?d.parseJSON(q):q}catch(r){}if(k=d.extend(y,{},t.defaults,f,"object"==typeof q?h(q):A,h(p||o)),i=k.position,k.id=c,"boolean"==typeof k.content.text){if(j=a.attr(k.content.attr),k.content.attr===z||!j)return z;k.content.text=j}if(i.container.length||(i.container=m),i.target===z&&(i.target=n),k.show.target===z&&(k.show.target=n),k.show.solo===y&&(k.show.solo=i.container.closest("body")),k.hide.target===z&&(k.hide.target=n),k.position.viewport===y&&(k.position.viewport=i.container),i.container=i.container.eq(0),i.at=new v(i.at,y),i.my=new v(i.my),a.data(N))if(k.overwrite)a.qtip("destroy",!0);else if(k.overwrite===z)return z;return a.attr(O,c),k.suppress&&(l=a.attr("title"))&&a.removeAttr("title").attr(Z,l).attr("title",""),g=new e(a,k,c,!!j),a.data(N,g),g}function s(a,b){this.options=b,this._ns="-modal",this.qtip=a,this.init(a)}var t,u,v,w,x,y=!0,z=!1,A=null,B="x",C="y",D="width",E="height",F="top",G="left",H="bottom",I="right",J="center",K="flipinvert",L="shift",M={},N="qtip",O="data-hasqtip",P="data-qtip-id",Q=["ui-widget","ui-tooltip"],R="."+N,S="click dblclick mousedown mouseup mousemove mouseleave mouseenter".split(" "),T=N+"-fixed",U=N+"-default",V=N+"-focus",W=N+"-hover",X=N+"-disabled",Y="_replacedByqTip",Z="oldtitle",$={ie:function(){var a,c;for(a=4,c=b.createElement("div");(c.innerHTML="<!--[if gt IE "+a+"]><i></i><![endif]-->")&&c.getElementsByTagName("i")[0];a+=1);return a>4?a:NaN}(),iOS:parseFloat((""+(/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent)||[0,""])[1]).replace("undefined","3_2").replace("_",".").replace("_",""))||z};u=e.prototype,u._when=function(a){return d.when.apply(d,a)},u.render=function(a){if(this.rendered||this.destroyed)return this;var b=this,c=this.options,e=this.cache,f=this.elements,g=c.content.text,h=c.content.title,i=c.content.button,j=c.position,k=[];return d.attr(this.target[0],"aria-describedby",this._id),e.posClass=this._createPosClass((this.position={my:j.my,at:j.at}).my),this.tooltip=f.tooltip=d("<div/>",{id:this._id,"class":[N,U,c.style.classes,e.posClass].join(" "),width:c.style.width||"",height:c.style.height||"",tracking:"mouse"===j.target&&j.adjust.mouse,role:"alert","aria-live":"polite","aria-atomic":z,"aria-describedby":this._id+"-content","aria-hidden":y}).toggleClass(X,this.disabled).attr(P,this.id).data(N,this).appendTo(j.container).append(f.content=d("<div />",{"class":N+"-content",id:this._id+"-content","aria-atomic":y})),this.rendered=-1,this.positioning=y,h&&(this._createTitle(),d.isFunction(h)||k.push(this._updateTitle(h,z))),i&&this._createButton(),d.isFunction(g)||k.push(this._updateContent(g,z)),this.rendered=y,this._setWidget(),d.each(M,function(a){var c;"render"===this.initialize&&(c=this(b))&&(b.plugins[a]=c)}),this._unassignEvents(),this._assignEvents(),this._when(k).then(function(){b._trigger("render"),b.positioning=z,b.hiddenDuringWait||!c.show.ready&&!a||b.toggle(y,e.event,z),b.hiddenDuringWait=z}),t.api[this.id]=this,this},u.destroy=function(a){function b(){if(!this.destroyed){this.destroyed=y;var a,b=this.target,c=b.attr(Z);this.rendered&&this.tooltip.stop(1,0).find("*").remove().end().remove(),d.each(this.plugins,function(){this.destroy&&this.destroy()});for(a in this.timers)this.timers.hasOwnProperty(a)&&clearTimeout(this.timers[a]);b.removeData(N).removeAttr(P).removeAttr(O).removeAttr("aria-describedby"),this.options.suppress&&c&&b.attr("title",c).removeAttr(Z),this._unassignEvents(),this.options=this.elements=this.cache=this.timers=this.plugins=this.mouse=A,delete t.api[this.id]}}return this.destroyed?this.target:(a===y&&"hide"!==this.triggering||!this.rendered?b.call(this):(this.tooltip.one("tooltiphidden",d.proxy(b,this)),!this.triggering&&this.hide()),this.target)},w=u.checks={builtin:{"^id$":function(a,b,c,e){var f=c===y?t.nextid:c,g=N+"-"+f;f!==z&&f.length>0&&!d("#"+g).length?(this._id=g,this.rendered&&(this.tooltip[0].id=this._id,this.elements.content[0].id=this._id+"-content",this.elements.title[0].id=this._id+"-title")):a[b]=e},"^prerender":function(a,b,c){c&&!this.rendered&&this.render(this.options.show.ready)},"^content.text$":function(a,b,c){this._updateContent(c)},"^content.attr$":function(a,b,c,d){this.options.content.text===this.target.attr(d)&&this._updateContent(this.target.attr(c))},"^content.title$":function(a,b,c){return c?(c&&!this.elements.title&&this._createTitle(),void this._updateTitle(c)):this._removeTitle()},"^content.button$":function(a,b,c){this._updateButton(c)},"^content.title.(text|button)$":function(a,b,c){this.set("content."+b,c)},"^position.(my|at)$":function(a,b,c){"string"==typeof c&&(this.position[b]=a[b]=new v(c,"at"===b))},"^position.container$":function(a,b,c){this.rendered&&this.tooltip.appendTo(c)},"^show.ready$":function(a,b,c){c&&(!this.rendered&&this.render(y)||this.toggle(y))},"^style.classes$":function(a,b,c,d){this.rendered&&this.tooltip.removeClass(d).addClass(c)},"^style.(width|height)":function(a,b,c){this.rendered&&this.tooltip.css(b,c)},"^style.widget|content.title":function(){this.rendered&&this._setWidget()},"^style.def":function(a,b,c){this.rendered&&this.tooltip.toggleClass(U,!!c)},"^events.(render|show|move|hide|focus|blur)$":function(a,b,c){this.rendered&&this.tooltip[(d.isFunction(c)?"":"un")+"bind"]("tooltip"+b,c)},"^(show|hide|position).(event|target|fixed|inactive|leave|distance|viewport|adjust)":function(){if(this.rendered){var a=this.options.position;this.tooltip.attr("tracking","mouse"===a.target&&a.adjust.mouse),this._unassignEvents(),this._assignEvents()}}}},u.get=function(a){if(this.destroyed)return this;var b=i(this.options,a.toLowerCase()),c=b[0][b[1]];return c.precedance?c.string():c};var _=/^position\.(my|at|adjust|target|container|viewport)|style|content|show\.ready/i,aa=/^prerender|show\.ready/i;u.set=function(a,b){if(this.destroyed)return this;var c,e=this.rendered,f=z,g=this.options;return"string"==typeof a?(c=a,a={},a[c]=b):a=d.extend({},a),d.each(a,function(b,c){if(e&&aa.test(b))return void delete a[b];var h,j=i(g,b.toLowerCase());h=j[0][j[1]],j[0][j[1]]=c&&c.nodeType?d(c):c,f=_.test(b)||f,a[b]=[j[0],j[1],c,h]}),h(g),this.positioning=y,d.each(a,d.proxy(j,this)),this.positioning=z,this.rendered&&this.tooltip[0].offsetWidth>0&&f&&this.reposition("mouse"===g.position.target?A:this.cache.event),this},u._update=function(a,b){var c=this,e=this.cache;return this.rendered&&a?(d.isFunction(a)&&(a=a.call(this.elements.target,e.event,this)||""),d.isFunction(a.then)?(e.waiting=y,a.then(function(a){return e.waiting=z,c._update(a,b)},A,function(a){return c._update(a,b)})):a===z||!a&&""!==a?z:(a.jquery&&a.length>0?b.empty().append(a.css({display:"block",visibility:"visible"})):b.html(a),this._waitForContent(b).then(function(a){c.rendered&&c.tooltip[0].offsetWidth>0&&c.reposition(e.event,!a.length)}))):z},u._waitForContent=function(a){var b=this.cache;return b.waiting=y,(d.fn.imagesLoaded?a.imagesLoaded():(new d.Deferred).resolve([])).done(function(){b.waiting=z}).promise()},u._updateContent=function(a,b){this._update(a,this.elements.content,b)},u._updateTitle=function(a,b){this._update(a,this.elements.title,b)===z&&this._removeTitle(z)},u._createTitle=function(){var a=this.elements,b=this._id+"-title";a.titlebar&&this._removeTitle(),a.titlebar=d("<div />",{"class":N+"-titlebar "+(this.options.style.widget?k("header"):"")}).append(a.title=d("<div />",{id:b,"class":N+"-title","aria-atomic":y})).insertBefore(a.content).delegate(".qtip-close","mousedown keydown mouseup keyup mouseout",function(a){d(this).toggleClass("ui-state-active ui-state-focus","down"===a.type.substr(-4))}).delegate(".qtip-close","mouseover mouseout",function(a){d(this).toggleClass("ui-state-hover","mouseover"===a.type)}),this.options.content.button&&this._createButton()},u._removeTitle=function(a){var b=this.elements;b.title&&(b.titlebar.remove(),b.titlebar=b.title=b.button=A,a!==z&&this.reposition())},u._createPosClass=function(a){return N+"-pos-"+(a||this.options.position.my).abbrev()},u.reposition=function(c,e){if(!this.rendered||this.positioning||this.destroyed)return this;this.positioning=y;var f,g,h,i,j=this.cache,k=this.tooltip,l=this.options.position,m=l.target,n=l.my,o=l.at,p=l.viewport,q=l.container,r=l.adjust,s=r.method.split(" "),t=k.outerWidth(z),u=k.outerHeight(z),v=0,w=0,x=k.css("position"),A={left:0,top:0},B=k[0].offsetWidth>0,C=c&&"scroll"===c.type,D=d(a),E=q[0].ownerDocument,K=this.mouse;if(d.isArray(m)&&2===m.length)o={x:G,y:F},A={left:m[0],top:m[1]};else if("mouse"===m)o={x:G,y:F},(!r.mouse||this.options.hide.distance)&&j.origin&&j.origin.pageX?c=j.origin:!c||c&&("resize"===c.type||"scroll"===c.type)?c=j.event:K&&K.pageX&&(c=K),"static"!==x&&(A=q.offset()),E.body.offsetWidth!==(a.innerWidth||E.documentElement.clientWidth)&&(g=d(b.body).offset()),A={left:c.pageX-A.left+(g&&g.left||0),top:c.pageY-A.top+(g&&g.top||0)},r.mouse&&C&&K&&(A.left-=(K.scrollX||0)-D.scrollLeft(),A.top-=(K.scrollY||0)-D.scrollTop());else{if("event"===m?c&&c.target&&"scroll"!==c.type&&"resize"!==c.type?j.target=d(c.target):c.target||(j.target=this.elements.target):"event"!==m&&(j.target=d(m.jquery?m:this.elements.target)),m=j.target,m=d(m).eq(0),0===m.length)return this;m[0]===b||m[0]===a?(v=$.iOS?a.innerWidth:m.width(),w=$.iOS?a.innerHeight:m.height(),m[0]===a&&(A={top:(p||m).scrollTop(),left:(p||m).scrollLeft()})):M.imagemap&&m.is("area")?f=M.imagemap(this,m,o,M.viewport?s:z):M.svg&&m&&m[0].ownerSVGElement?f=M.svg(this,m,o,M.viewport?s:z):(v=m.outerWidth(z),w=m.outerHeight(z),A=m.offset()),f&&(v=f.width,w=f.height,g=f.offset,A=f.position),A=this.reposition.offset(m,A,q),($.iOS>3.1&&$.iOS<4.1||$.iOS>=4.3&&$.iOS<4.33||!$.iOS&&"fixed"===x)&&(A.left-=D.scrollLeft(),A.top-=D.scrollTop()),(!f||f&&f.adjustable!==z)&&(A.left+=o.x===I?v:o.x===J?v/2:0,A.top+=o.y===H?w:o.y===J?w/2:0)}return A.left+=r.x+(n.x===I?-t:n.x===J?-t/2:0),A.top+=r.y+(n.y===H?-u:n.y===J?-u/2:0),M.viewport?(h=A.adjusted=M.viewport(this,A,l,v,w,t,u),g&&h.left&&(A.left+=g.left),g&&h.top&&(A.top+=g.top),h.my&&(this.position.my=h.my)):A.adjusted={left:0,top:0},j.posClass!==(i=this._createPosClass(this.position.my))&&(j.posClass=i,k.removeClass(j.posClass).addClass(i)),this._trigger("move",[A,p.elem||p],c)?(delete A.adjusted,e===z||!B||isNaN(A.left)||isNaN(A.top)||"mouse"===m||!d.isFunction(l.effect)?k.css(A):d.isFunction(l.effect)&&(l.effect.call(k,this,d.extend({},A)),k.queue(function(a){d(this).css({opacity:"",height:""}),$.ie&&this.style.removeAttribute("filter"),a()})),this.positioning=z,this):this},u.reposition.offset=function(a,c,e){function f(a,b){c.left+=b*a.scrollLeft(),c.top+=b*a.scrollTop()}if(!e[0])return c;var g,h,i,j,k=d(a[0].ownerDocument),l=!!$.ie&&"CSS1Compat"!==b.compatMode,m=e[0];do"static"!==(h=d.css(m,"position"))&&("fixed"===h?(i=m.getBoundingClientRect(),f(k,-1)):(i=d(m).position(),i.left+=parseFloat(d.css(m,"borderLeftWidth"))||0,i.top+=parseFloat(d.css(m,"borderTopWidth"))||0),c.left-=i.left+(parseFloat(d.css(m,"marginLeft"))||0),c.top-=i.top+(parseFloat(d.css(m,"marginTop"))||0),g||"hidden"===(j=d.css(m,"overflow"))||"visible"===j||(g=d(m)));while(m=m.offsetParent);return g&&(g[0]!==k[0]||l)&&f(g,1),c};var ba=(v=u.reposition.Corner=function(a,b){a=(""+a).replace(/([A-Z])/," $1").replace(/middle/gi,J).toLowerCase(),this.x=(a.match(/left|right/i)||a.match(/center/)||["inherit"])[0].toLowerCase(),this.y=(a.match(/top|bottom|center/i)||["inherit"])[0].toLowerCase(),this.forceY=!!b;var c=a.charAt(0);this.precedance="t"===c||"b"===c?C:B}).prototype;ba.invert=function(a,b){this[a]=this[a]===G?I:this[a]===I?G:b||this[a]},ba.string=function(a){var b=this.x,c=this.y,d=b!==c?"center"===b||"center"!==c&&(this.precedance===C||this.forceY)?[c,b]:[b,c]:[b];return a!==!1?d.join(" "):d},ba.abbrev=function(){var a=this.string(!1);return a[0].charAt(0)+(a[1]&&a[1].charAt(0)||"")},ba.clone=function(){return new v(this.string(),this.forceY)},u.toggle=function(a,c){var e=this.cache,f=this.options,g=this.tooltip;if(c){if(/over|enter/.test(c.type)&&e.event&&/out|leave/.test(e.event.type)&&f.show.target.add(c.target).length===f.show.target.length&&g.has(c.relatedTarget).length)return this;e.event=d.event.fix(c)}if(this.waiting&&!a&&(this.hiddenDuringWait=y),!this.rendered)return a?this.render(1):this;if(this.destroyed||this.disabled)return this;var h,i,j,k=a?"show":"hide",l=this.options[k],m=this.options.position,n=this.options.content,o=this.tooltip.css("width"),p=this.tooltip.is(":visible"),q=a||1===l.target.length,r=!c||l.target.length<2||e.target[0]===c.target;return(typeof a).search("boolean|number")&&(a=!p),h=!g.is(":animated")&&p===a&&r,i=h?A:!!this._trigger(k,[90]),this.destroyed?this:(i!==z&&a&&this.focus(c),!i||h?this:(d.attr(g[0],"aria-hidden",!a),a?(this.mouse&&(e.origin=d.event.fix(this.mouse)),d.isFunction(n.text)&&this._updateContent(n.text,z),d.isFunction(n.title)&&this._updateTitle(n.title,z),!x&&"mouse"===m.target&&m.adjust.mouse&&(d(b).bind("mousemove."+N,this._storeMouse),x=y),o||g.css("width",g.outerWidth(z)),this.reposition(c,arguments[2]),o||g.css("width",""),l.solo&&("string"==typeof l.solo?d(l.solo):d(R,l.solo)).not(g).not(l.target).qtip("hide",new d.Event("tooltipsolo"))):(clearTimeout(this.timers.show),delete e.origin,x&&!d(R+'[tracking="true"]:visible',l.solo).not(g).length&&(d(b).unbind("mousemove."+N),x=z),this.blur(c)),j=d.proxy(function(){a?($.ie&&g[0].style.removeAttribute("filter"),g.css("overflow",""),"string"==typeof l.autofocus&&d(this.options.show.autofocus,g).focus(),this.options.show.target.trigger("qtip-"+this.id+"-inactive")):g.css({display:"",visibility:"",opacity:"",left:"",top:""}),this._trigger(a?"visible":"hidden")},this),l.effect===z||q===z?(g[k](),j()):d.isFunction(l.effect)?(g.stop(1,1),l.effect.call(g,this),g.queue("fx",function(a){j(),a()})):g.fadeTo(90,a?1:0,j),a&&l.target.trigger("qtip-"+this.id+"-inactive"),this))},u.show=function(a){return this.toggle(y,a)},u.hide=function(a){return this.toggle(z,a)},u.focus=function(a){if(!this.rendered||this.destroyed)return this;var b=d(R),c=this.tooltip,e=parseInt(c[0].style.zIndex,10),f=t.zindex+b.length;return c.hasClass(V)||this._trigger("focus",[f],a)&&(e!==f&&(b.each(function(){this.style.zIndex>e&&(this.style.zIndex=this.style.zIndex-1)}),b.filter("."+V).qtip("blur",a)),c.addClass(V)[0].style.zIndex=f),this},u.blur=function(a){return!this.rendered||this.destroyed?this:(this.tooltip.removeClass(V),this._trigger("blur",[this.tooltip.css("zIndex")],a),this)},u.disable=function(a){return this.destroyed?this:("toggle"===a?a=!(this.rendered?this.tooltip.hasClass(X):this.disabled):"boolean"!=typeof a&&(a=y),this.rendered&&this.tooltip.toggleClass(X,a).attr("aria-disabled",a),this.disabled=!!a,this)},u.enable=function(){return this.disable(z)},u._createButton=function(){var a=this,b=this.elements,c=b.tooltip,e=this.options.content.button,f="string"==typeof e,g=f?e:"Close tooltip";b.button&&b.button.remove(),e.jquery?b.button=e:b.button=d("<a />",{"class":"qtip-close "+(this.options.style.widget?"":N+"-icon"),title:g,"aria-label":g}).prepend(d("<span />",{"class":"ui-icon ui-icon-close",html:"&times;"})),b.button.appendTo(b.titlebar||c).attr("role","button").click(function(b){return c.hasClass(X)||a.hide(b),z})},u._updateButton=function(a){if(!this.rendered)return z;var b=this.elements.button;a?this._createButton():b.remove()},u._setWidget=function(){var a=this.options.style.widget,b=this.elements,c=b.tooltip,d=c.hasClass(X);c.removeClass(X),X=a?"ui-state-disabled":"qtip-disabled",c.toggleClass(X,d),c.toggleClass("ui-helper-reset "+k(),a).toggleClass(U,this.options.style.def&&!a),b.content&&b.content.toggleClass(k("content"),a),b.titlebar&&b.titlebar.toggleClass(k("header"),a),b.button&&b.button.toggleClass(N+"-icon",!a)},u._storeMouse=function(a){return(this.mouse=d.event.fix(a)).type="mousemove",this},u._bind=function(a,b,c,e,f){if(a&&c&&b.length){var g="."+this._id+(e?"-"+e:"");return d(a).bind((b.split?b:b.join(g+" "))+g,d.proxy(c,f||this)),this}},u._unbind=function(a,b){return a&&d(a).unbind("."+this._id+(b?"-"+b:"")),this},u._trigger=function(a,b,c){var e=new d.Event("tooltip"+a);return e.originalEvent=c&&d.extend({},c)||this.cache.event||A,this.triggering=a,this.tooltip.trigger(e,[this].concat(b||[])),this.triggering=z,!e.isDefaultPrevented()},u._bindEvents=function(a,b,c,e,f,g){var h=c.filter(e).add(e.filter(c)),i=[];h.length&&(d.each(b,function(b,c){var e=d.inArray(c,a);e>-1&&i.push(a.splice(e,1)[0])}),i.length&&(this._bind(h,i,function(a){var b=this.rendered?this.tooltip[0].offsetWidth>0:!1;(b?g:f).call(this,a)}),c=c.not(h),e=e.not(h))),this._bind(c,a,f),this._bind(e,b,g)},u._assignInitialEvents=function(a){function b(a){return this.disabled||this.destroyed?z:(this.cache.event=a&&d.event.fix(a),this.cache.target=a&&d(a.target),clearTimeout(this.timers.show),void(this.timers.show=l.call(this,function(){this.render("object"==typeof a||c.show.ready)},c.prerender?0:c.show.delay)))}var c=this.options,e=c.show.target,f=c.hide.target,g=c.show.event?d.trim(""+c.show.event).split(" "):[],h=c.hide.event?d.trim(""+c.hide.event).split(" "):[];this._bind(this.elements.target,["remove","removeqtip"],function(){this.destroy(!0)},"destroy"),/mouse(over|enter)/i.test(c.show.event)&&!/mouse(out|leave)/i.test(c.hide.event)&&h.push("mouseleave"),this._bind(e,"mousemove",function(a){this._storeMouse(a),this.cache.onTarget=y}),this._bindEvents(g,h,e,f,b,function(){return this.timers?void clearTimeout(this.timers.show):z}),(c.show.ready||c.prerender)&&b.call(this,a)},u._assignEvents=function(){var c=this,e=this.options,f=e.position,g=this.tooltip,h=e.show.target,i=e.hide.target,j=f.container,k=f.viewport,l=d(b),q=d(a),r=e.show.event?d.trim(""+e.show.event).split(" "):[],s=e.hide.event?d.trim(""+e.hide.event).split(" "):[];d.each(e.events,function(a,b){c._bind(g,"toggle"===a?["tooltipshow","tooltiphide"]:["tooltip"+a],b,null,g)}),/mouse(out|leave)/i.test(e.hide.event)&&"window"===e.hide.leave&&this._bind(l,["mouseout","blur"],function(a){/select|option/.test(a.target.nodeName)||a.relatedTarget||this.hide(a)}),e.hide.fixed?i=i.add(g.addClass(T)):/mouse(over|enter)/i.test(e.show.event)&&this._bind(i,"mouseleave",function(){clearTimeout(this.timers.show)}),(""+e.hide.event).indexOf("unfocus")>-1&&this._bind(j.closest("html"),["mousedown","touchstart"],function(a){var b=d(a.target),c=this.rendered&&!this.tooltip.hasClass(X)&&this.tooltip[0].offsetWidth>0,e=b.parents(R).filter(this.tooltip[0]).length>0;b[0]===this.target[0]||b[0]===this.tooltip[0]||e||this.target.has(b[0]).length||!c||this.hide(a)}),"number"==typeof e.hide.inactive&&(this._bind(h,"qtip-"+this.id+"-inactive",o,"inactive"),this._bind(i.add(g),t.inactiveEvents,o)),this._bindEvents(r,s,h,i,m,n),this._bind(h.add(g),"mousemove",function(a){if("number"==typeof e.hide.distance){var b=this.cache.origin||{},c=this.options.hide.distance,d=Math.abs;(d(a.pageX-b.pageX)>=c||d(a.pageY-b.pageY)>=c)&&this.hide(a)}this._storeMouse(a)}),"mouse"===f.target&&f.adjust.mouse&&(e.hide.event&&this._bind(h,["mouseenter","mouseleave"],function(a){return this.cache?void(this.cache.onTarget="mouseenter"===a.type):z}),this._bind(l,"mousemove",function(a){this.rendered&&this.cache.onTarget&&!this.tooltip.hasClass(X)&&this.tooltip[0].offsetWidth>0&&this.reposition(a)})),(f.adjust.resize||k.length)&&this._bind(d.event.special.resize?k:q,"resize",p),f.adjust.scroll&&this._bind(q.add(f.container),"scroll",p)},u._unassignEvents=function(){var c=this.options,e=c.show.target,f=c.hide.target,g=d.grep([this.elements.target[0],this.rendered&&this.tooltip[0],c.position.container[0],c.position.viewport[0],c.position.container.closest("html")[0],a,b],function(a){return"object"==typeof a});e&&e.toArray&&(g=g.concat(e.toArray())),f&&f.toArray&&(g=g.concat(f.toArray())),this._unbind(g)._unbind(g,"destroy")._unbind(g,"inactive")},d(function(){q(R,["mouseenter","mouseleave"],function(a){var b="mouseenter"===a.type,c=d(a.currentTarget),e=d(a.relatedTarget||a.target),f=this.options;b?(this.focus(a),c.hasClass(T)&&!c.hasClass(X)&&clearTimeout(this.timers.hide)):"mouse"===f.position.target&&f.position.adjust.mouse&&f.hide.event&&f.show.target&&!e.closest(f.show.target[0]).length&&this.hide(a),c.toggleClass(W,b)}),q("["+P+"]",S,o)}),t=d.fn.qtip=function(a,b,e){var f=(""+a).toLowerCase(),g=A,i=d.makeArray(arguments).slice(1),j=i[i.length-1],k=this[0]?d.data(this[0],N):A;return!arguments.length&&k||"api"===f?k:"string"==typeof a?(this.each(function(){var a=d.data(this,N);if(!a)return y;if(j&&j.timeStamp&&(a.cache.event=j),!b||"option"!==f&&"options"!==f)a[f]&&a[f].apply(a,i);else{if(e===c&&!d.isPlainObject(b))return g=a.get(b),z;a.set(b,e)}}),g!==A?g:this):"object"!=typeof a&&arguments.length?void 0:(k=h(d.extend(y,{},a)),this.each(function(a){var b,c;return c=d.isArray(k.id)?k.id[a]:k.id,c=!c||c===z||c.length<1||t.api[c]?t.nextid++:c,b=r(d(this),c,k),b===z?y:(t.api[c]=b,d.each(M,function(){"initialize"===this.initialize&&this(b)}),void b._assignInitialEvents(j))}))},d.qtip=e,t.api={},d.each({attr:function(a,b){if(this.length){var c=this[0],e="title",f=d.data(c,"qtip");if(a===e&&f&&f.options&&"object"==typeof f&&"object"==typeof f.options&&f.options.suppress)return arguments.length<2?d.attr(c,Z):(f&&f.options.content.attr===e&&f.cache.attr&&f.set("content.text",b),this.attr(Z,b))}return d.fn["attr"+Y].apply(this,arguments)},clone:function(a){var b=d.fn["clone"+Y].apply(this,arguments);return a||b.filter("["+Z+"]").attr("title",function(){return d.attr(this,Z)}).removeAttr(Z),b}},function(a,b){if(!b||d.fn[a+Y])return y;var c=d.fn[a+Y]=d.fn[a];d.fn[a]=function(){return b.apply(this,arguments)||c.apply(this,arguments)}}),d.ui||(d["cleanData"+Y]=d.cleanData,d.cleanData=function(a){for(var b,c=0;(b=d(a[c])).length;c++)if(b.attr(O))try{b.triggerHandler("removeqtip")}catch(e){}d["cleanData"+Y].apply(this,arguments)}),t.version="3.0.3",t.nextid=0,t.inactiveEvents=S,t.zindex=15e3,t.defaults={prerender:z,id:z,overwrite:y,suppress:y,content:{text:y,attr:"title",title:z,button:z},position:{my:"top left",at:"bottom right",target:z,container:z,viewport:z,adjust:{x:0,y:0,mouse:y,scroll:y,resize:y,method:"flipinvert flipinvert"},effect:function(a,b){d(this).animate(b,{duration:200,queue:z})}},show:{target:z,event:"mouseenter",effect:y,delay:90,solo:z,ready:z,autofocus:z},hide:{target:z,event:"mouseleave",effect:y,delay:0,fixed:z,inactive:z,leave:"window",distance:z},style:{classes:"",widget:z,width:z,height:z,def:y},events:{render:A,move:A,show:A,hide:A,toggle:A,visible:A,hidden:A,focus:A,blur:A}},M.viewport=function(c,d,e,f,g,h,i){function j(a,b,c,e,f,g,h,i,j){var k=d[f],s=u[a],t=v[a],w=c===L,x=s===f?j:s===g?-j:-j/2,y=t===f?i:t===g?-i:-i/2,z=q[f]+r[f]-(n?0:m[f]),A=z-k,B=k+j-(h===D?o:p)-z,C=x-(u.precedance===a||s===u[b]?y:0)-(t===J?i/2:0);return w?(C=(s===f?1:-1)*x,d[f]+=A>0?A:B>0?-B:0,d[f]=Math.max(-m[f]+r[f],k-C,Math.min(Math.max(-m[f]+r[f]+(h===D?o:p),k+C),d[f],"center"===s?k-x:1e9))):(e*=c===K?2:0,A>0&&(s!==f||B>0)?(d[f]-=C+e,l.invert(a,f)):B>0&&(s!==g||A>0)&&(d[f]-=(s===J?-C:C)+e,l.invert(a,g)),d[f]<q[f]&&-d[f]>B&&(d[f]=k,l=u.clone())),d[f]-k}var k,l,m,n,o,p,q,r,s=e.target,t=c.elements.tooltip,u=e.my,v=e.at,w=e.adjust,x=w.method.split(" "),y=x[0],A=x[1]||x[0],M=e.viewport,N=e.container,O={left:0,top:0};return M.jquery&&s[0]!==a&&s[0]!==b.body&&"none"!==w.method?(m=N.offset()||O,n="static"===N.css("position"),k="fixed"===t.css("position"),o=M[0]===a?M.width():M.outerWidth(z),p=M[0]===a?M.height():M.outerHeight(z),q={left:k?0:M.scrollLeft(),top:k?0:M.scrollTop()},r=M.offset()||O,"shift"===y&&"shift"===A||(l=u.clone()),O={left:"none"!==y?j(B,C,y,w.x,G,I,D,f,h):0,top:"none"!==A?j(C,B,A,w.y,F,H,E,g,i):0,my:l}):O},M.polys={polygon:function(a,b){var c,d,e,f={width:0,height:0,position:{top:1e10,right:0,bottom:0,left:1e10},adjustable:z},g=0,h=[],i=1,j=1,k=0,l=0;for(g=a.length;g--;)c=[parseInt(a[--g],10),parseInt(a[g+1],10)],c[0]>f.position.right&&(f.position.right=c[0]),c[0]<f.position.left&&(f.position.left=c[0]),c[1]>f.position.bottom&&(f.position.bottom=c[1]),c[1]<f.position.top&&(f.position.top=c[1]),h.push(c);if(d=f.width=Math.abs(f.position.right-f.position.left),e=f.height=Math.abs(f.position.bottom-f.position.top),"c"===b.abbrev())f.position={left:f.position.left+f.width/2,top:f.position.top+f.height/2};else{for(;d>0&&e>0&&i>0&&j>0;)for(d=Math.floor(d/2),e=Math.floor(e/2),b.x===G?i=d:b.x===I?i=f.width-d:i+=Math.floor(d/2),b.y===F?j=e:b.y===H?j=f.height-e:j+=Math.floor(e/2),g=h.length;g--&&!(h.length<2);)k=h[g][0]-f.position.left,l=h[g][1]-f.position.top,(b.x===G&&k>=i||b.x===I&&i>=k||b.x===J&&(i>k||k>f.width-i)||b.y===F&&l>=j||b.y===H&&j>=l||b.y===J&&(j>l||l>f.height-j))&&h.splice(g,1);f.position={left:h[0][0],top:h[0][1]}}return f},rect:function(a,b,c,d){return{width:Math.abs(c-a),height:Math.abs(d-b),position:{left:Math.min(a,c),top:Math.min(b,d)}}},_angles:{tc:1.5,tr:7/4,tl:5/4,bc:.5,br:.25,bl:.75,rc:2,lc:1,c:0},ellipse:function(a,b,c,d,e){var f=M.polys._angles[e.abbrev()],g=0===f?0:c*Math.cos(f*Math.PI),h=d*Math.sin(f*Math.PI);return{width:2*c-Math.abs(g),height:2*d-Math.abs(h),position:{left:a+g,top:b+h},adjustable:z}},circle:function(a,b,c,d){return M.polys.ellipse(a,b,c,c,d)}},M.svg=function(a,c,e){for(var f,g,h,i,j,k,l,m,n,o=c[0],p=d(o.ownerSVGElement),q=o.ownerDocument,r=(parseInt(c.css("stroke-width"),10)||0)/2;!o.getBBox;)o=o.parentNode;if(!o.getBBox||!o.parentNode)return z;switch(o.nodeName){case"ellipse":case"circle":m=M.polys.ellipse(o.cx.baseVal.value,o.cy.baseVal.value,(o.rx||o.r).baseVal.value+r,(o.ry||o.r).baseVal.value+r,e);break;case"line":case"polygon":case"polyline":for(l=o.points||[{x:o.x1.baseVal.value,y:o.y1.baseVal.value},{x:o.x2.baseVal.value,y:o.y2.baseVal.value}],m=[],k=-1,i=l.numberOfItems||l.length;++k<i;)j=l.getItem?l.getItem(k):l[k],m.push.apply(m,[j.x,j.y]);m=M.polys.polygon(m,e);break;default:m=o.getBBox(),m={width:m.width,height:m.height,position:{left:m.x,top:m.y}}}return n=m.position,p=p[0],p.createSVGPoint&&(g=o.getScreenCTM(),l=p.createSVGPoint(),l.x=n.left,l.y=n.top,h=l.matrixTransform(g),n.left=h.x,n.top=h.y),q!==b&&"mouse"!==a.position.target&&(f=d((q.defaultView||q.parentWindow).frameElement).offset(),f&&(n.left+=f.left,n.top+=f.top)),q=d(q),n.left+=q.scrollLeft(),n.top+=q.scrollTop(),m};var ca,da,ea="qtip-modal",fa="."+ea;da=function(){function a(a){if(d.expr[":"].focusable)return d.expr[":"].focusable;var b,c,e,f=!isNaN(d.attr(a,"tabindex")),g=a.nodeName&&a.nodeName.toLowerCase();return"area"===g?(b=a.parentNode,c=b.name,a.href&&c&&"map"===b.nodeName.toLowerCase()?(e=d("img[usemap=#"+c+"]")[0],!!e&&e.is(":visible")):!1):/input|select|textarea|button|object/.test(g)?!a.disabled:"a"===g?a.href||f:f}function c(a){j.length<1&&a.length?a.not("body").blur():j.first().focus()}function e(a){if(h.is(":visible")){var b,e=d(a.target),g=f.tooltip,i=e.closest(R);b=i.length<1?z:parseInt(i[0].style.zIndex,10)>parseInt(g[0].style.zIndex,10),b||e.closest(R)[0]===g[0]||c(e)}}var f,g,h,i=this,j={};d.extend(i,{init:function(){return h=i.elem=d("<div />",{id:"qtip-overlay",html:"<div></div>",mousedown:function(){return z}}).hide(),d(b.body).bind("focusin"+fa,e),d(b).bind("keydown"+fa,function(a){f&&f.options.show.modal.escape&&27===a.keyCode&&f.hide(a)}),h.bind("click"+fa,function(a){f&&f.options.show.modal.blur&&f.hide(a)}),i},update:function(b){f=b,j=b.options.show.modal.stealfocus!==z?b.tooltip.find("*").filter(function(){return a(this)}):[]},toggle:function(a,e,j){var k=a.tooltip,l=a.options.show.modal,m=l.effect,n=e?"show":"hide",o=h.is(":visible"),p=d(fa).filter(":visible:not(:animated)").not(k);return i.update(a),e&&l.stealfocus!==z&&c(d(":focus")),h.toggleClass("blurs",l.blur),e&&h.appendTo(b.body),h.is(":animated")&&o===e&&g!==z||!e&&p.length?i:(h.stop(y,z),d.isFunction(m)?m.call(h,e):m===z?h[n]():h.fadeTo(parseInt(j,10)||90,e?1:0,function(){e||h.hide()}),e||h.queue(function(a){h.css({left:"",top:""}),d(fa).length||h.detach(),a()}),g=e,f.destroyed&&(f=A),i)}}),i.init()},da=new da,d.extend(s.prototype,{init:function(a){var b=a.tooltip;return this.options.on?(a.elements.overlay=da.elem,b.addClass(ea).css("z-index",t.modal_zindex+d(fa).length),a._bind(b,["tooltipshow","tooltiphide"],function(a,c,e){var f=a.originalEvent;if(a.target===b[0])if(f&&"tooltiphide"===a.type&&/mouse(leave|enter)/.test(f.type)&&d(f.relatedTarget).closest(da.elem[0]).length)try{a.preventDefault()}catch(g){}else(!f||f&&"tooltipsolo"!==f.type)&&this.toggle(a,"tooltipshow"===a.type,e)},this._ns,this),a._bind(b,"tooltipfocus",function(a,c){if(!a.isDefaultPrevented()&&a.target===b[0]){
+var e=d(fa),f=t.modal_zindex+e.length,g=parseInt(b[0].style.zIndex,10);da.elem[0].style.zIndex=f-1,e.each(function(){this.style.zIndex>g&&(this.style.zIndex-=1)}),e.filter("."+V).qtip("blur",a.originalEvent),b.addClass(V)[0].style.zIndex=f,da.update(c);try{a.preventDefault()}catch(h){}}},this._ns,this),void a._bind(b,"tooltiphide",function(a){a.target===b[0]&&d(fa).filter(":visible").not(b).last().qtip("focus",a)},this._ns,this)):this},toggle:function(a,b,c){return a&&a.isDefaultPrevented()?this:void da.toggle(this.qtip,!!b,c)},destroy:function(){this.qtip.tooltip.removeClass(ea),this.qtip._unbind(this.qtip.tooltip,this._ns),da.toggle(this.qtip,z),delete this.qtip.elements.overlay}}),ca=M.modal=function(a){return new s(a,a.options.show.modal)},ca.sanitize=function(a){a.show&&("object"!=typeof a.show.modal?a.show.modal={on:!!a.show.modal}:"undefined"==typeof a.show.modal.on&&(a.show.modal.on=y))},t.modal_zindex=t.zindex-200,ca.initialize="render",w.modal={"^show.modal.(on|blur)$":function(){this.destroy(),this.init(),this.qtip.elems.overlay.toggle(this.qtip.tooltip[0].offsetWidth>0)}},d.extend(y,t.defaults,{show:{modal:{on:z,effect:y,blur:y,stealfocus:y,escape:y}}})})}(window,document);
+//# sourceMappingURL=jquery.qtip.min.map
\ No newline at end of file
diff --git a/static/libjs/rickshaw.min.js b/static/libjs/rickshaw.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..e9924a43afa2734bb98a9075b83114d939ec21a7
--- /dev/null
+++ b/static/libjs/rickshaw.min.js
@@ -0,0 +1,3 @@
+var Rickshaw={namespace:function(namespace,obj){var parts=namespace.split(".");var parent=Rickshaw;for(var i=1,length=parts.length;i<length;i++){var currentPart=parts[i];parent[currentPart]=parent[currentPart]||{};parent=parent[currentPart]}return parent},keys:function(obj){var keys=[];for(var key in obj)keys.push(key);return keys},extend:function(destination,source){for(var property in source){destination[property]=source[property]}return destination},clone:function(obj){return JSON.parse(JSON.stringify(obj))}};if(typeof module!=="undefined"&&module.exports){var d3=require("d3");module.exports=Rickshaw}(function(globalContext){var _toString=Object.prototype.toString,NULL_TYPE="Null",UNDEFINED_TYPE="Undefined",BOOLEAN_TYPE="Boolean",NUMBER_TYPE="Number",STRING_TYPE="String",OBJECT_TYPE="Object",FUNCTION_CLASS="[object Function]";function isFunction(object){return _toString.call(object)===FUNCTION_CLASS}function extend(destination,source){for(var property in source)if(source.hasOwnProperty(property))destination[property]=source[property];return destination}function keys(object){if(Type(object)!==OBJECT_TYPE){throw new TypeError}var results=[];for(var property in object){if(object.hasOwnProperty(property)){results.push(property)}}return results}function Type(o){switch(o){case null:return NULL_TYPE;case void 0:return UNDEFINED_TYPE}var type=typeof o;switch(type){case"boolean":return BOOLEAN_TYPE;case"number":return NUMBER_TYPE;case"string":return STRING_TYPE}return OBJECT_TYPE}function isUndefined(object){return typeof object==="undefined"}var slice=Array.prototype.slice;function argumentNames(fn){var names=fn.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1].replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g,"").replace(/\s+/g,"").split(",");return names.length==1&&!names[0]?[]:names}function wrap(fn,wrapper){var __method=fn;return function(){var a=update([bind(__method,this)],arguments);return wrapper.apply(this,a)}}function update(array,args){var arrayLength=array.length,length=args.length;while(length--)array[arrayLength+length]=args[length];return array}function merge(array,args){array=slice.call(array,0);return update(array,args)}function bind(fn,context){if(arguments.length<2&&isUndefined(arguments[0]))return this;var __method=fn,args=slice.call(arguments,2);return function(){var a=merge(args,arguments);return __method.apply(context,a)}}var emptyFunction=function(){};var Class=function(){var IS_DONTENUM_BUGGY=function(){for(var p in{toString:1}){if(p==="toString")return false}return true}();function subclass(){}function create(){var parent=null,properties=[].slice.apply(arguments);if(isFunction(properties[0]))parent=properties.shift();function klass(){this.initialize.apply(this,arguments)}extend(klass,Class.Methods);klass.superclass=parent;klass.subclasses=[];if(parent){subclass.prototype=parent.prototype;klass.prototype=new subclass;try{parent.subclasses.push(klass)}catch(e){}}for(var i=0,length=properties.length;i<length;i++)klass.addMethods(properties[i]);if(!klass.prototype.initialize)klass.prototype.initialize=emptyFunction;klass.prototype.constructor=klass;return klass}function addMethods(source){var ancestor=this.superclass&&this.superclass.prototype,properties=keys(source);if(IS_DONTENUM_BUGGY){if(source.toString!=Object.prototype.toString)properties.push("toString");if(source.valueOf!=Object.prototype.valueOf)properties.push("valueOf")}for(var i=0,length=properties.length;i<length;i++){var property=properties[i],value=source[property];if(ancestor&&isFunction(value)&&argumentNames(value)[0]=="$super"){var method=value;value=wrap(function(m){return function(){return ancestor[m].apply(this,arguments)}}(property),method);value.valueOf=bind(method.valueOf,method);value.toString=bind(method.toString,method)}this.prototype[property]=value}return this}return{create:create,Methods:{addMethods:addMethods}}}();if(globalContext.exports){globalContext.exports.Class=Class}else{globalContext.Class=Class}})(Rickshaw);Rickshaw.namespace("Rickshaw.Compat.ClassList");Rickshaw.Compat.ClassList=function(){if(typeof document!=="undefined"&&!("classList"in document.createElement("a"))){(function(view){"use strict";var classListProp="classList",protoProp="prototype",elemCtrProto=(view.HTMLElement||view.Element)[protoProp],objCtr=Object,strTrim=String[protoProp].trim||function(){return this.replace(/^\s+|\s+$/g,"")},arrIndexOf=Array[protoProp].indexOf||function(item){var i=0,len=this.length;for(;i<len;i++){if(i in this&&this[i]===item){return i}}return-1},DOMEx=function(type,message){this.name=type;this.code=DOMException[type];this.message=message},checkTokenAndGetIndex=function(classList,token){if(token===""){throw new DOMEx("SYNTAX_ERR","An invalid or illegal string was specified")}if(/\s/.test(token)){throw new DOMEx("INVALID_CHARACTER_ERR","String contains an invalid character")}return arrIndexOf.call(classList,token)},ClassList=function(elem){var trimmedClasses=strTrim.call(elem.className),classes=trimmedClasses?trimmedClasses.split(/\s+/):[],i=0,len=classes.length;for(;i<len;i++){this.push(classes[i])}this._updateClassName=function(){elem.className=this.toString()}},classListProto=ClassList[protoProp]=[],classListGetter=function(){return new ClassList(this)};DOMEx[protoProp]=Error[protoProp];classListProto.item=function(i){return this[i]||null};classListProto.contains=function(token){token+="";return checkTokenAndGetIndex(this,token)!==-1};classListProto.add=function(token){token+="";if(checkTokenAndGetIndex(this,token)===-1){this.push(token);this._updateClassName()}};classListProto.remove=function(token){token+="";var index=checkTokenAndGetIndex(this,token);if(index!==-1){this.splice(index,1);this._updateClassName()}};classListProto.toggle=function(token){token+="";if(checkTokenAndGetIndex(this,token)===-1){this.add(token)}else{this.remove(token)}};classListProto.toString=function(){return this.join(" ")};if(objCtr.defineProperty){var classListPropDesc={get:classListGetter,enumerable:true,configurable:true};try{objCtr.defineProperty(elemCtrProto,classListProp,classListPropDesc)}catch(ex){if(ex.number===-2146823252){classListPropDesc.enumerable=false;objCtr.defineProperty(elemCtrProto,classListProp,classListPropDesc)}}}else if(objCtr[protoProp].__defineGetter__){elemCtrProto.__defineGetter__(classListProp,classListGetter)}})(window)}};if(typeof RICKSHAW_NO_COMPAT!=="undefined"&&!RICKSHAW_NO_COMPAT||typeof RICKSHAW_NO_COMPAT==="undefined"){new Rickshaw.Compat.ClassList}Rickshaw.namespace("Rickshaw.Graph");Rickshaw.Graph=function(args){var self=this;this.initialize=function(args){if(!args.element)throw"Rickshaw.Graph needs a reference to an element";if(args.element.nodeType!==1)throw"Rickshaw.Graph element was defined but not an HTML element";this.element=args.element;this.series=args.series;this.window={};this.updateCallbacks=[];this.configureCallbacks=[];this.defaults={interpolation:"cardinal",offset:"zero",min:undefined,max:undefined,preserve:false,xScale:undefined,yScale:undefined};this._loadRenderers();this.configure(args);this.validateSeries(args.series);this.series.active=function(){return self.series.filter(function(s){return!s.disabled})};this.setSize({width:args.width,height:args.height});this.element.classList.add("rickshaw_graph");this.vis=d3.select(this.element).append("svg:svg").attr("width",this.width).attr("height",this.height);this.discoverRange()};this._loadRenderers=function(){for(var name in Rickshaw.Graph.Renderer){if(!name||!Rickshaw.Graph.Renderer.hasOwnProperty(name))continue;var r=Rickshaw.Graph.Renderer[name];if(!r||!r.prototype||!r.prototype.render)continue;self.registerRenderer(new r({graph:self}))}};this.validateSeries=function(series){if(!Array.isArray(series)&&!(series instanceof Rickshaw.Series)){var seriesSignature=Object.prototype.toString.apply(series);throw"series is not an array: "+seriesSignature}var pointsCount;series.forEach(function(s){if(!(s instanceof Object)){throw"series element is not an object: "+s}if(!s.data){throw"series has no data: "+JSON.stringify(s)}if(!Array.isArray(s.data)){throw"series data is not an array: "+JSON.stringify(s.data)}var x=s.data[0].x;var y=s.data[0].y;if(typeof x!="number"||typeof y!="number"&&y!==null){throw"x and y properties of points should be numbers instead of "+typeof x+" and "+typeof y}if(s.data.length>=3){if(s.data[2].x<s.data[1].x||s.data[1].x<s.data[0].x||s.data[s.data.length-1].x<s.data[0].x){throw"series data needs to be sorted on x values for series name: "+s.name}}},this)};this.dataDomain=function(){var data=this.series.map(function(s){return s.data});var min=d3.min(data.map(function(d){return d[0].x}));var max=d3.max(data.map(function(d){return d[d.length-1].x}));return[min,max]};this.discoverRange=function(){var domain=this.renderer.domain();this.x=(this.xScale||d3.scale.linear()).domain(domain.x).range([0,this.width]);this.y=(this.yScale||d3.scale.linear()).domain(domain.y).range([this.height,0]);this.y.magnitude=d3.scale.linear().domain([domain.y[0]-domain.y[0],domain.y[1]-domain.y[0]]).range([0,this.height])};this.render=function(){var stackedData=this.stackData();this.discoverRange();this.renderer.render();this.updateCallbacks.forEach(function(callback){callback()})};this.update=this.render;this.stackData=function(){var data=this.series.active().map(function(d){return d.data}).map(function(d){return d.filter(function(d){return this._slice(d)},this)},this);var preserve=this.preserve;if(!preserve){this.series.forEach(function(series){if(series.scale){preserve=true}})}data=preserve?Rickshaw.clone(data):data;this.series.active().forEach(function(series,index){if(series.scale){var seriesData=data[index];if(seriesData){seriesData.forEach(function(d){d.y=series.scale(d.y)})}}});this.stackData.hooks.data.forEach(function(entry){data=entry.f.apply(self,[data])});var stackedData;if(!this.renderer.unstack){this._validateStackable();var layout=d3.layout.stack();layout.offset(self.offset);stackedData=layout(data)}stackedData=stackedData||data;if(this.renderer.unstack){stackedData.forEach(function(seriesData){seriesData.forEach(function(d){d.y0=d.y0===undefined?0:d.y0})})}this.stackData.hooks.after.forEach(function(entry){stackedData=entry.f.apply(self,[data])});var i=0;this.series.forEach(function(series){if(series.disabled)return;series.stack=stackedData[i++]});this.stackedData=stackedData;return stackedData};this._validateStackable=function(){var series=this.series;var pointsCount;series.forEach(function(s){pointsCount=pointsCount||s.data.length;if(pointsCount&&s.data.length!=pointsCount){throw"stacked series cannot have differing numbers of points: "+pointsCount+" vs "+s.data.length+"; see Rickshaw.Series.fill()"}},this)};this.stackData.hooks={data:[],after:[]};this._slice=function(d){if(this.window.xMin||this.window.xMax){var isInRange=true;if(this.window.xMin&&d.x<this.window.xMin)isInRange=false;if(this.window.xMax&&d.x>this.window.xMax)isInRange=false;return isInRange}return true};this.onUpdate=function(callback){this.updateCallbacks.push(callback)};this.onConfigure=function(callback){this.configureCallbacks.push(callback)};this.registerRenderer=function(renderer){this._renderers=this._renderers||{};this._renderers[renderer.name]=renderer};this.configure=function(args){this.config=this.config||{};if(args.width||args.height){this.setSize(args)}Rickshaw.keys(this.defaults).forEach(function(k){this.config[k]=k in args?args[k]:k in this?this[k]:this.defaults[k]},this);Rickshaw.keys(this.config).forEach(function(k){this[k]=this.config[k]},this);var renderer=args.renderer||this.renderer&&this.renderer.name||"stack";this.setRenderer(renderer,args);this.configureCallbacks.forEach(function(callback){callback(args)})};this.setRenderer=function(r,args){if(typeof r=="function"){this.renderer=new r({graph:self});this.registerRenderer(this.renderer)}else{if(!this._renderers[r]){throw"couldn't find renderer "+r}this.renderer=this._renderers[r]}if(typeof args=="object"){this.renderer.configure(args)}};this.setSize=function(args){args=args||{};if(typeof window!==undefined){var style=window.getComputedStyle(this.element,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);var elementHeight=parseInt(style.getPropertyValue("height"),10)}this.width=args.width||elementWidth||400;this.height=args.height||elementHeight||250;this.vis&&this.vis.attr("width",this.width).attr("height",this.height)};this.initialize(args)};Rickshaw.namespace("Rickshaw.Fixtures.Color");Rickshaw.Fixtures.Color=function(){this.schemes={};this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse();this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"];this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"];this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse();this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"};this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse();this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"];this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]};Rickshaw.namespace("Rickshaw.Fixtures.RandomData");Rickshaw.Fixtures.RandomData=function(timeInterval){var addData;timeInterval=timeInterval||1;var lastRandomValue=200;var timeBase=Math.floor((new Date).getTime()/1e3);this.addData=function(data){var randomValue=Math.random()*100+15+lastRandomValue;var index=data[0].length;var counter=1;data.forEach(function(series){var randomVariance=Math.random()*20;var v=randomValue/25+counter++ +(Math.cos(index*counter*11/960)+2)*15+(Math.cos(index/7)+2)*7+(Math.cos(index/17)+2)*1;series.push({x:index*timeInterval+timeBase,y:v+randomVariance})});lastRandomValue=randomValue*.85};this.removeData=function(data){data.forEach(function(series){series.shift()});timeBase+=timeInterval}};Rickshaw.namespace("Rickshaw.Fixtures.Time");Rickshaw.Fixtures.Time=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getUTCFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getUTCFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getUTCMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getUTCDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getUTCMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getUTCMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toUTCString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="month"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),date.getUTCMonth())/1e3;if(floor==time)return time;year=date.getUTCFullYear();var month=date.getUTCMonth();if(month==11){month=0;year=year+1}else{month+=1}return Date.UTC(year,month)/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),0)/1e3;if(floor==time)return time;year=date.getUTCFullYear()+1;return Date.UTC(year,0)/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Time.Local");Rickshaw.Fixtures.Time.Local=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="day"){var nearFuture=new Date((time+unit.seconds-1)*1e3);var rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(nearFuture.getDate());rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="month"){date=new Date(time*1e3);floor=new Date(date.getFullYear(),date.getMonth()).getTime()/1e3;if(floor==time)return time;year=date.getFullYear();var month=date.getMonth();if(month==11){month=0;year=year+1}else{month+=1}return new Date(year,month).getTime()/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=new Date(date.getUTCFullYear(),0).getTime()/1e3;if(floor==time)return time;year=date.getFullYear()+1;return new Date(year,0).getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Number");Rickshaw.Fixtures.Number.formatKMBT=function(y){var abs_y=Math.abs(y);if(abs_y>=1e12){return y/1e12+"T"}else if(abs_y>=1e9){return y/1e9+"B"}else if(abs_y>=1e6){return y/1e6+"M"}else if(abs_y>=1e3){return y/1e3+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(y){var abs_y=Math.abs(y);if(abs_y>=0x4000000000000){return y/0x4000000000000+"P"}else if(abs_y>=1099511627776){return y/1099511627776+"T"}else if(abs_y>=1073741824){return y/1073741824+"G"}else if(abs_y>=1048576){return y/1048576+"M"}else if(abs_y>=1024){return y/1024+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.namespace("Rickshaw.Color.Palette");Rickshaw.Color.Palette=function(args){var color=new Rickshaw.Fixtures.Color;args=args||{};this.schemes={};this.scheme=color.schemes[args.scheme]||args.scheme||color.schemes.colorwheel;this.runningIndex=0;this.generatorIndex=0;if(args.interpolatedStopCount){var schemeCount=this.scheme.length-1;var i,j,scheme=[];for(i=0;i<schemeCount;i++){scheme.push(this.scheme[i]);var generator=d3.interpolateHsl(this.scheme[i],this.scheme[i+1]);for(j=1;j<args.interpolatedStopCount;j++){scheme.push(generator(1/args.interpolatedStopCount*j))}}scheme.push(this.scheme[this.scheme.length-1]);this.scheme=scheme}this.rotateCount=this.scheme.length;this.color=function(key){return this.scheme[key]||this.scheme[this.runningIndex++]||this.interpolateColor()||"#808080"};this.interpolateColor=function(){if(!Array.isArray(this.scheme))return;var color;if(this.generatorIndex==this.rotateCount*2-1){color=d3.interpolateHsl(this.scheme[this.generatorIndex],this.scheme[0])(.5);this.generatorIndex=0;this.rotateCount*=2}else{color=d3.interpolateHsl(this.scheme[this.generatorIndex],this.scheme[this.generatorIndex+1])(.5);this.generatorIndex++}this.scheme.push(color);return color}};Rickshaw.namespace("Rickshaw.Graph.Ajax");Rickshaw.Graph.Ajax=Rickshaw.Class.create({initialize:function(args){this.dataURL=args.dataURL;this.onData=args.onData||function(d){return d};this.onComplete=args.onComplete||function(){};this.onError=args.onError||function(){};this.args=args;this.request()},request:function(){$.ajax({url:this.dataURL,dataType:"json",success:this.success.bind(this),error:this.error.bind(this)})},error:function(){console.log("error loading dataURL: "+this.dataURL);this.onError(this)},success:function(data,status){data=this.onData(data);this.args.series=this._splice({data:data,series:this.args.series});this.graph=this.graph||new Rickshaw.Graph(this.args);this.graph.render();this.onComplete(this)},_splice:function(args){var data=args.data;var series=args.series;if(!args.series)return data;series.forEach(function(s){var seriesKey=s.key||s.name;if(!seriesKey)throw"series needs a key or a name";data.forEach(function(d){var dataKey=d.key||d.name;if(!dataKey)throw"data needs a key or a name";if(seriesKey==dataKey){var properties=["color","name","data"];properties.forEach(function(p){if(d[p])s[p]=d[p]})}})});return series}});Rickshaw.namespace("Rickshaw.Graph.Annotate");Rickshaw.Graph.Annotate=function(args){var graph=this.graph=args.graph;this.elements={timeline:args.element};var self=this;this.data={};this.elements.timeline.classList.add("rickshaw_annotation_timeline");this.add=function(time,content,end_time){self.data[time]=self.data[time]||{boxes:[]};self.data[time].boxes.push({content:content,end:end_time})};this.update=function(){Rickshaw.keys(self.data).forEach(function(time){var annotation=self.data[time];var left=self.graph.x(time);if(left<0||left>self.graph.x.range()[1]){if(annotation.element){annotation.line.classList.add("offscreen");annotation.element.style.display="none"}annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.add("offscreen")});return}if(!annotation.element){var element=annotation.element=document.createElement("div");element.classList.add("annotation");this.elements.timeline.appendChild(element);element.addEventListener("click",function(e){element.classList.toggle("active");annotation.line.classList.toggle("active");annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.toggle("active")})},false)}annotation.element.style.left=left+"px";annotation.element.style.display="block";annotation.boxes.forEach(function(box){var element=box.element;if(!element){element=box.element=document.createElement("div");element.classList.add("content");element.innerHTML=box.content;annotation.element.appendChild(element);annotation.line=document.createElement("div");annotation.line.classList.add("annotation_line");self.graph.element.appendChild(annotation.line);if(box.end){box.rangeElement=document.createElement("div");box.rangeElement.classList.add("annotation_range");self.graph.element.appendChild(box.rangeElement)}}if(box.end){var annotationRangeStart=left;var annotationRangeEnd=Math.min(self.graph.x(box.end),self.graph.x.range()[1]);if(annotationRangeStart>annotationRangeEnd){annotationRangeEnd=left;annotationRangeStart=Math.max(self.graph.x(box.end),self.graph.x.range()[0])}var annotationRangeWidth=annotationRangeEnd-annotationRangeStart;box.rangeElement.style.left=annotationRangeStart+"px";box.rangeElement.style.width=annotationRangeWidth+"px";box.rangeElement.classList.remove("offscreen")}annotation.line.classList.remove("offscreen");annotation.line.style.left=left+"px"})},this)};this.graph.onUpdate(function(){self.update()})};Rickshaw.namespace("Rickshaw.Graph.Axis.Time");Rickshaw.Graph.Axis.Time=function(args){var self=this;this.graph=args.graph;this.elements=[];this.ticksTreatment=args.ticksTreatment||"plain";this.fixedTimeUnit=args.timeUnit;var time=args.timeFixture||new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var unit;var units=time.units;var domain=this.graph.x.domain();var rangeSeconds=domain[1]-domain[0];units.forEach(function(u){if(Math.floor(rangeSeconds/u.seconds)>=2){unit=unit||u}});return unit||time.units[time.units.length-1]};this.tickOffsets=function(){var domain=this.graph.x.domain();var unit=this.fixedTimeUnit||this.appropriateTimeUnit();var count=Math.ceil((domain[1]-domain[0])/unit.seconds);var runningTick=domain[0];var offsets=[];for(var i=0;i<count;i++){var tickValue=time.ceil(runningTick,unit);runningTick=tickValue+unit.seconds/2;offsets.push({value:tickValue,unit:unit})}return offsets};this.render=function(){this.elements.forEach(function(e){e.parentNode.removeChild(e)});this.elements=[];var offsets=this.tickOffsets();offsets.forEach(function(o){if(self.graph.x(o.value)>self.graph.x.range()[1])return;var element=document.createElement("div");element.style.left=self.graph.x(o.value)+"px";element.classList.add("x_tick");element.classList.add(self.ticksTreatment);var title=document.createElement("div");title.classList.add("title");title.innerHTML=o.unit.formatter(new Date(o.value*1e3));element.appendChild(title);self.graph.element.appendChild(element);self.elements.push(element)})};this.graph.onUpdate(function(){self.render()})};Rickshaw.namespace("Rickshaw.Graph.Axis.X");Rickshaw.Graph.Axis.X=function(args){var self=this;var berthRate=.1;this.initialize=function(args){this.graph=args.graph;this.orientation=args.orientation||"top";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";if(args.element){this.element=args.element;this._discoverSize(args.element,args);this.vis=d3.select(args.element).append("svg:svg").attr("height",this.height).attr("width",this.width).attr("class","rickshaw_graph x_axis_d3");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}this.graph.onUpdate(function(){self.render()})};this.setSize=function(args){args=args||{};if(!this.element)return;this._discoverSize(this.element.parentNode,args);this.vis.attr("height",this.height).attr("width",this.width*(1+berthRate));var berth=Math.floor(this.width*berthRate/2);this.element.style.left=-1*berth+"px"};this.render=function(){if(this._renderWidth!==undefined&&this.graph.width!==this._renderWidth)this.setSize({auto:true});var axis=d3.svg.axis().scale(this.graph.x).orient(this.orientation);axis.tickFormat(args.tickFormat||function(x){return x});if(this.tickValues)axis.tickValues(this.tickValues);this.ticks=this.staticTicks||Math.floor(this.graph.width/this.pixelsPerTick);var berth=Math.floor(this.width*berthRate/2)||0;var transform;if(this.orientation=="top"){var yOffset=this.height||this.graph.height;transform="translate("+berth+","+yOffset+")"}else{transform="translate("+berth+", 0)"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["x_ticks_d3",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var gridSize=(this.orientation=="bottom"?1:-1)*this.graph.height;this.graph.vis.append("svg:g").attr("class","x_grid_d3").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-x-value",this.textContent)});this._renderHeight=this.graph.height};this._discoverSize=function(element,args){if(typeof window!=="undefined"){var style=window.getComputedStyle(element,null);var elementHeight=parseInt(style.getPropertyValue("height"),10);if(!args.auto){var elementWidth=parseInt(style.getPropertyValue("width"),10)}}this.width=(args.width||elementWidth||this.graph.width)*(1+berthRate);this.height=args.height||elementHeight||40};this.initialize(args)};Rickshaw.namespace("Rickshaw.Graph.Axis.Y");Rickshaw.Graph.Axis.Y=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.orientation=args.orientation||"right";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";this.tickFormat=args.tickFormat||function(y){return y};this.berthRate=.1;if(args.element){this.element=args.element;this.vis=d3.select(args.element).append("svg:svg").attr("class","rickshaw_graph y_axis");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}var self=this;this.graph.onUpdate(function(){self.render()})},setSize:function(args){args=args||{};if(!this.element)return;if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element.parentNode,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);if(!args.auto){var elementHeight=parseInt(style.getPropertyValue("height"),10)}}this.width=args.width||elementWidth||this.graph.width*this.berthRate;this.height=args.height||elementHeight||this.graph.height;this.vis.attr("width",this.width).attr("height",this.height*(1+this.berthRate));var berth=this.height*this.berthRate;if(this.orientation=="left"){this.element.style.top=-1*berth+"px"}},render:function(){if(this._renderHeight!==undefined&&this.graph.height!==this._renderHeight)this.setSize({auto:true});this.ticks=this.staticTicks||Math.floor(this.graph.height/this.pixelsPerTick);var axis=this._drawAxis(this.graph.y);this._drawGrid(axis);this._renderHeight=this.graph.height},_drawAxis:function(scale){var axis=d3.svg.axis().scale(scale).orient(this.orientation);axis.tickFormat(this.tickFormat);if(this.tickValues)axis.tickValues(this.tickValues);if(this.orientation=="left"){var berth=this.height*this.berthRate;var transform="translate("+this.width+", "+berth+")"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));return axis},_drawGrid:function(axis){var gridSize=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-y-value",this.textContent)})}});Rickshaw.namespace("Rickshaw.Graph.Axis.Y.Scaled");Rickshaw.Graph.Axis.Y.Scaled=Rickshaw.Class.create(Rickshaw.Graph.Axis.Y,{initialize:function($super,args){if(typeof args.scale==="undefined"){throw new Error("Scaled requires scale")}this.scale=args.scale;if(typeof args.grid==="undefined"){this.grid=true
+}else{this.grid=args.grid}$super(args)},_drawAxis:function($super,scale){var domain=this.scale.domain();var renderDomain=this.graph.renderer.domain().y;var extents=[Math.min.apply(Math,domain),Math.max.apply(Math,domain)];var extentMap=d3.scale.linear().domain([0,1]).range(extents);var adjExtents=[extentMap(renderDomain[0]),extentMap(renderDomain[1])];var adjustment=d3.scale.linear().domain(extents).range(adjExtents);var adjustedScale=this.scale.copy().domain(domain.map(adjustment)).range(scale.range());return $super(adjustedScale)},_drawGrid:function($super,axis){if(this.grid){$super(axis)}}});Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight");Rickshaw.Graph.Behavior.Series.Highlight=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;var colorSafe={};var activeLine=null;var disabledColor=args.disabledColor||function(seriesColor){return d3.interpolateRgb(seriesColor,d3.rgb("#d8d8d8"))(.8).toString()};this.addHighlightEvents=function(l){l.element.addEventListener("mouseover",function(e){if(activeLine)return;else activeLine=l;self.legend.lines.forEach(function(line){if(l===line){if(self.graph.renderer.unstack&&(line.series.renderer?line.series.renderer.unstack:true)){var seriesIndex=self.graph.series.indexOf(line.series);line.originalIndex=seriesIndex;var series=self.graph.series.splice(seriesIndex,1)[0];self.graph.series.push(series)}return}colorSafe[line.series.name]=colorSafe[line.series.name]||line.series.color;line.series.color=disabledColor(line.series.color)});self.graph.update()},false);l.element.addEventListener("mouseout",function(e){if(!activeLine)return;else activeLine=null;self.legend.lines.forEach(function(line){if(l===line&&line.hasOwnProperty("originalIndex")){var series=self.graph.series.pop();self.graph.series.splice(line.originalIndex,0,series);delete line.originalIndex}if(colorSafe[line.series.name]){line.series.color=colorSafe[line.series.name]}});self.graph.update()},false)};if(this.legend){this.legend.lines.forEach(function(l){self.addHighlightEvents(l)})}};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order");Rickshaw.Graph.Behavior.Series.Order=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;if(typeof window.$=="undefined"){throw"couldn't find jQuery at window.$"}if(typeof window.$.ui=="undefined"){throw"couldn't find jQuery UI at window.$.ui"}$(function(){$(self.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(event,ui){var series=[];$(self.legend.list).find("li").each(function(index,item){if(!item.series)return;series.push(item.series)});for(var i=self.graph.series.length-1;i>=0;i--){self.graph.series[i]=series.shift()}self.graph.update()}});$(self.legend.list).disableSelection()});this.graph.onUpdate(function(){var h=window.getComputedStyle(self.legend.element).height;self.legend.element.style.height=h})};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle");Rickshaw.Graph.Behavior.Series.Toggle=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;this.addAnchor=function(line){var anchor=document.createElement("a");anchor.innerHTML="&#10004;";anchor.classList.add("action");line.element.insertBefore(anchor,line.element.firstChild);anchor.onclick=function(e){if(line.series.disabled){line.series.enable();line.element.classList.remove("disabled")}else{if(this.graph.series.filter(function(s){return!s.disabled}).length<=1)return;line.series.disable();line.element.classList.add("disabled")}}.bind(this);var label=line.element.getElementsByTagName("span")[0];label.onclick=function(e){var disableAllOtherLines=line.series.disabled;if(!disableAllOtherLines){for(var i=0;i<self.legend.lines.length;i++){var l=self.legend.lines[i];if(line.series===l.series){}else if(l.series.disabled){}else{disableAllOtherLines=true;break}}}if(disableAllOtherLines){line.series.enable();line.element.classList.remove("disabled");self.legend.lines.forEach(function(l){if(line.series===l.series){}else{l.series.disable();l.element.classList.add("disabled")}})}else{self.legend.lines.forEach(function(l){l.series.enable();l.element.classList.remove("disabled")})}}};if(this.legend){if(typeof $!="undefined"&&$(this.legend.list).sortable){$(this.legend.list).sortable({start:function(event,ui){ui.item.bind("no.onclick",function(event){event.preventDefault()})},stop:function(event,ui){setTimeout(function(){ui.item.unbind("no.onclick")},250)}})}this.legend.lines.forEach(function(l){self.addAnchor(l)})}this._addBehavior=function(){this.graph.series.forEach(function(s){s.disable=function(){if(self.graph.series.length<=1){throw"only one series left"}s.disabled=true;self.graph.update()};s.enable=function(){s.disabled=false;self.graph.update()}})};this._addBehavior();this.updateBehaviour=function(){this._addBehavior()}};Rickshaw.namespace("Rickshaw.Graph.HoverDetail");Rickshaw.Graph.HoverDetail=Rickshaw.Class.create({initialize:function(args){var graph=this.graph=args.graph;this.xFormatter=args.xFormatter||function(x){return new Date(x*1e3).toUTCString()};this.yFormatter=args.yFormatter||function(y){return y===null?y:y.toFixed(2)};var element=this.element=document.createElement("div");element.className="detail";this.visible=true;graph.element.appendChild(element);this.lastEvent=null;this._addListeners();this.onShow=args.onShow;this.onHide=args.onHide;this.onRender=args.onRender;this.formatter=args.formatter||this.formatter},formatter:function(series,x,y,formattedX,formattedY,d){return series.name+":&nbsp;"+formattedY},update:function(e){e=e||this.lastEvent;if(!e)return;this.lastEvent=e;if(!e.target.nodeName.match(/^(path|svg|rect|circle)$/))return;var graph=this.graph;var eventX=e.offsetX||e.layerX;var eventY=e.offsetY||e.layerY;var j=0;var points=[];var nearestPoint;this.graph.series.active().forEach(function(series){var data=this.graph.stackedData[j++];if(!data.length)return;var domainX=graph.x.invert(eventX);var domainIndexScale=d3.scale.linear().domain([data[0].x,data.slice(-1)[0].x]).range([0,data.length-1]);var approximateIndex=Math.round(domainIndexScale(domainX));if(approximateIndex==data.length-1)approximateIndex--;var dataIndex=Math.min(approximateIndex||0,data.length-1);for(var i=approximateIndex;i<data.length-1;){if(!data[i]||!data[i+1])break;if(data[i].x<=domainX&&data[i+1].x>domainX){dataIndex=Math.abs(domainX-data[i].x)<Math.abs(domainX-data[i+1].x)?i:i+1;break}if(data[i+1].x<=domainX){i++}else{i--}}if(dataIndex<0)dataIndex=0;var value=data[dataIndex];var distance=Math.sqrt(Math.pow(Math.abs(graph.x(value.x)-eventX),2)+Math.pow(Math.abs(graph.y(value.y+value.y0)-eventY),2));var xFormatter=series.xFormatter||this.xFormatter;var yFormatter=series.yFormatter||this.yFormatter;var point={formattedXValue:xFormatter(value.x),formattedYValue:yFormatter(series.scale?series.scale.invert(value.y):value.y),series:series,value:value,distance:distance,order:j,name:series.name};if(!nearestPoint||distance<nearestPoint.distance){nearestPoint=point}points.push(point)},this);if(!nearestPoint)return;nearestPoint.active=true;var domainX=nearestPoint.value.x;var formattedXValue=nearestPoint.formattedXValue;this.element.innerHTML="";this.element.style.left=graph.x(domainX)+"px";this.visible&&this.render({points:points,detail:points,mouseX:eventX,mouseY:eventY,formattedXValue:formattedXValue,domainX:domainX})},hide:function(){this.visible=false;this.element.classList.add("inactive");if(typeof this.onHide=="function"){this.onHide()}},show:function(){this.visible=true;this.element.classList.remove("inactive");if(typeof this.onShow=="function"){this.onShow()}},render:function(args){var graph=this.graph;var points=args.points;var point=points.filter(function(p){return p.active}).shift();if(point.value.y===null)return;var formattedXValue=point.formattedXValue;var formattedYValue=point.formattedYValue;this.element.innerHTML="";this.element.style.left=graph.x(point.value.x)+"px";var xLabel=document.createElement("div");xLabel.className="x_label";xLabel.innerHTML=formattedXValue;this.element.appendChild(xLabel);var item=document.createElement("div");item.className="item";var series=point.series;var actualY=series.scale?series.scale.invert(point.value.y):point.value.y;item.innerHTML=this.formatter(series,point.value.x,actualY,formattedXValue,formattedYValue,point);item.style.top=this.graph.y(point.value.y0+point.value.y)+"px";this.element.appendChild(item);var dot=document.createElement("div");dot.className="dot";dot.style.top=item.style.top;dot.style.borderColor=series.color;this.element.appendChild(dot);if(point.active){item.classList.add("active");dot.classList.add("active")}var alignables=[xLabel,item];alignables.forEach(function(el){el.classList.add("left")});this.show();var leftAlignError=this._calcLayoutError(alignables);if(leftAlignError>0){alignables.forEach(function(el){el.classList.remove("left");el.classList.add("right")});var rightAlignError=this._calcLayoutError(alignables);if(rightAlignError>leftAlignError){alignables.forEach(function(el){el.classList.remove("right");el.classList.add("left")})}}if(typeof this.onRender=="function"){this.onRender(args)}},_calcLayoutError:function(alignables){var parentRect=this.element.parentNode.getBoundingClientRect();var error=0;var alignRight=alignables.forEach(function(el){var rect=el.getBoundingClientRect();if(!rect.width){return}if(rect.right>parentRect.right){error+=rect.right-parentRect.right}if(rect.left<parentRect.left){error+=parentRect.left-rect.left}});return error},_addListeners:function(){this.graph.element.addEventListener("mousemove",function(e){this.visible=true;this.update(e)}.bind(this),false);this.graph.onUpdate(function(){this.update()}.bind(this));this.graph.element.addEventListener("mouseout",function(e){if(e.relatedTarget&&!(e.relatedTarget.compareDocumentPosition(this.graph.element)&Node.DOCUMENT_POSITION_CONTAINS)){this.hide()}}.bind(this),false)}});Rickshaw.namespace("Rickshaw.Graph.JSONP");Rickshaw.Graph.JSONP=Rickshaw.Class.create(Rickshaw.Graph.Ajax,{request:function(){$.ajax({url:this.dataURL,dataType:"jsonp",success:this.success.bind(this),error:this.error.bind(this)})}});Rickshaw.namespace("Rickshaw.Graph.Legend");Rickshaw.Graph.Legend=Rickshaw.Class.create({className:"rickshaw_legend",initialize:function(args){this.element=args.element;this.graph=args.graph;this.naturalOrder=args.naturalOrder;this.element.classList.add(this.className);this.list=document.createElement("ul");this.element.appendChild(this.list);this.render();this.graph.onUpdate(function(){})},render:function(){var self=this;while(this.list.firstChild){this.list.removeChild(this.list.firstChild)}this.lines=[];var series=this.graph.series.map(function(s){return s});if(!this.naturalOrder){series=series.reverse()}series.forEach(function(s){self.addLine(s)})},addLine:function(series){var line=document.createElement("li");line.className="line";if(series.disabled){line.className+=" disabled"}if(series.className){d3.select(line).classed(series.className,true)}var swatch=document.createElement("div");swatch.className="swatch";swatch.style.backgroundColor=series.color;line.appendChild(swatch);var label=document.createElement("span");label.className="label";label.innerHTML=series.name;line.appendChild(label);this.list.appendChild(line);line.series=series;if(series.noLegend){line.style.display="none"}var _line={element:line,series:series};if(this.shelving){this.shelving.addAnchor(_line);this.shelving.updateBehaviour()}if(this.highlighter){this.highlighter.addHighlightEvents(_line)}this.lines.push(_line);return line}});Rickshaw.namespace("Rickshaw.Graph.RangeSlider");Rickshaw.Graph.RangeSlider=Rickshaw.Class.create({initialize:function(args){var element=this.element=args.element;var graph=this.graph=args.graph;this.build();graph.onUpdate(function(){this.update()}.bind(this))},build:function(){var element=this.element;var graph=this.graph;var domain=graph.dataDomain();$(function(){$(element).slider({range:true,min:domain[0],max:domain[1],values:[domain[0],domain[1]],slide:function(event,ui){if(ui.values[1]<=ui.values[0])return;graph.window.xMin=ui.values[0];graph.window.xMax=ui.values[1];graph.update();var domain=graph.dataDomain();if(domain[0]==ui.values[0]){graph.window.xMin=undefined}if(domain[1]==ui.values[1]){graph.window.xMax=undefined}}})});$(element)[0].style.width=graph.width+"px"},update:function(){var element=this.element;var graph=this.graph;var values=$(element).slider("option","values");var domain=graph.dataDomain();$(element).slider("option","min",domain[0]);$(element).slider("option","max",domain[1]);if(graph.window.xMin==null){values[0]=domain[0]}if(graph.window.xMax==null){values[1]=domain[1]}$(element).slider("option","values",values)}});Rickshaw.namespace("Rickshaw.Graph.RangeSlider.Preview");Rickshaw.Graph.RangeSlider.Preview=Rickshaw.Class.create({initialize:function(args){if(!args.element)throw"Rickshaw.Graph.RangeSlider.Preview needs a reference to an element";if(!args.graph&&!args.graphs)throw"Rickshaw.Graph.RangeSlider.Preview needs a reference to an graph or an array of graphs";this.element=args.element;this.graphs=args.graph?[args.graph]:args.graphs;this.defaults={height:75,width:400,gripperColor:undefined,frameTopThickness:3,frameHandleThickness:10,frameColor:"#d4d4d4",frameOpacity:1,minimumFrameWidth:0};this.defaults.gripperColor=d3.rgb(this.defaults.frameColor).darker().toString();this.configureCallbacks=[];this.previews=[];args.width=args.width||this.graphs[0].width||this.defaults.width;args.height=args.height||this.graphs[0].height/5||this.defaults.height;this.configure(args);this.render()},onConfigure:function(callback){this.configureCallbacks.push(callback)},configure:function(args){this.config={};this.configureCallbacks.forEach(function(callback){callback(args)});Rickshaw.keys(this.defaults).forEach(function(k){this.config[k]=k in args?args[k]:k in this.config?this.config[k]:this.defaults[k]},this);if(args.width){this.previews.forEach(function(preview){var width=args.width-this.config.frameHandleThickness*2;preview.setSize({width:width})},this)}if(args.height){this.previews.forEach(function(preview){var height=this.previewHeight/this.graphs.length;preview.setSize({height:height})},this)}},render:function(){var self=this;this.svg=d3.select(this.element).selectAll("svg.rickshaw_range_slider_preview").data([null]);this.previewHeight=this.config.height-this.config.frameTopThickness*2;this.previewWidth=this.config.width-this.config.frameHandleThickness*2;this.currentFrame=[0,this.previewWidth];var buildGraph=function(parent,index){var graphArgs=Rickshaw.extend({},parent.config);var height=self.previewHeight/self.graphs.length;Rickshaw.extend(graphArgs,{element:this.appendChild(document.createElement("div")),height:height,width:self.previewWidth,series:parent.series});var graph=new Rickshaw.Graph(graphArgs);self.previews.push(graph);parent.onUpdate(function(){graph.render();self.render()});parent.onConfigure(function(args){delete args.height;graph.configure(args);graph.render()});graph.render()};var graphContainer=d3.select(this.element).selectAll("div.rickshaw_range_slider_preview_container").data(this.graphs);var translateCommand="translate("+this.config.frameHandleThickness+"px, "+this.config.frameTopThickness+"px)";graphContainer.enter().append("div").classed("rickshaw_range_slider_preview_container",true).style("-webkit-transform",translateCommand).style("-moz-transform",translateCommand).style("-ms-transform",translateCommand).style("transform",translateCommand).each(buildGraph);graphContainer.exit().remove();var masterGraph=this.graphs[0];var domainScale=d3.scale.linear().domain([0,this.previewWidth]).range(masterGraph.dataDomain());var currentWindow=[masterGraph.window.xMin,masterGraph.window.xMax];this.currentFrame[0]=currentWindow[0]===undefined?0:Math.round(domainScale.invert(currentWindow[0]));if(this.currentFrame[0]<0)this.currentFrame[0]=0;this.currentFrame[1]=currentWindow[1]===undefined?this.previewWidth:domainScale.invert(currentWindow[1]);if(this.currentFrame[1]-this.currentFrame[0]<self.config.minimumFrameWidth){this.currentFrame[1]=(this.currentFrame[0]||0)+self.config.minimumFrameWidth}this.svg.enter().append("svg").classed("rickshaw_range_slider_preview",true).style("height",this.config.height+"px").style("width",this.config.width+"px").style("position","relative").style("top",-this.previewHeight+"px");this._renderDimming();this._renderFrame();this._renderGrippers();this._renderHandles();this._renderMiddle();this._registerMouseEvents()},_renderDimming:function(){var element=this.svg.selectAll("path.dimming").data([null]);element.enter().append("path").attr("fill","white").attr("fill-opacity","0.7").attr("fill-rule","evenodd").classed("dimming",true);var path="";path+=" M "+this.config.frameHandleThickness+" "+this.config.frameTopThickness;path+=" h "+this.previewWidth;path+=" v "+this.previewHeight;path+=" h "+-this.previewWidth;path+=" z ";path+=" M "+Math.max(this.currentFrame[0],this.config.frameHandleThickness)+" "+this.config.frameTopThickness;path+=" H "+Math.min(this.currentFrame[1]+this.config.frameHandleThickness*2,this.previewWidth+this.config.frameHandleThickness);path+=" v "+this.previewHeight;path+=" H "+Math.max(this.currentFrame[0],this.config.frameHandleThickness);path+=" z";element.attr("d",path)},_renderFrame:function(){var element=this.svg.selectAll("path.frame").data([null]);element.enter().append("path").attr("stroke","white").attr("stroke-width","1px").attr("stroke-linejoin","round").attr("fill",this.config.frameColor).attr("fill-opacity",this.config.frameOpacity).attr("fill-rule","evenodd").classed("frame",true);var path="";path+=" M "+this.currentFrame[0]+" 0";path+=" H "+(this.currentFrame[1]+this.config.frameHandleThickness*2);path+=" V "+this.config.height;path+=" H "+this.currentFrame[0];path+=" z";path+=" M "+(this.currentFrame[0]+this.config.frameHandleThickness)+" "+this.config.frameTopThickness;path+=" H "+(this.currentFrame[1]+this.config.frameHandleThickness);path+=" v "+this.previewHeight;path+=" H "+(this.currentFrame[0]+this.config.frameHandleThickness);path+=" z";element.attr("d",path)},_renderGrippers:function(){var gripper=this.svg.selectAll("path.gripper").data([null]);gripper.enter().append("path").attr("stroke",this.config.gripperColor).classed("gripper",true);var path="";[.4,.6].forEach(function(spacing){path+=" M "+Math.round(this.currentFrame[0]+this.config.frameHandleThickness*spacing)+" "+Math.round(this.config.height*.3);path+=" V "+Math.round(this.config.height*.7);path+=" M "+Math.round(this.currentFrame[1]+this.config.frameHandleThickness*(1+spacing))+" "+Math.round(this.config.height*.3);path+=" V "+Math.round(this.config.height*.7)}.bind(this));gripper.attr("d",path)},_renderHandles:function(){var leftHandle=this.svg.selectAll("rect.left_handle").data([null]);leftHandle.enter().append("rect").attr("width",this.config.frameHandleThickness).attr("height",this.config.height).style("cursor","ew-resize").style("fill-opacity","0").classed("left_handle",true);leftHandle.attr("x",this.currentFrame[0]);var rightHandle=this.svg.selectAll("rect.right_handle").data([null]);rightHandle.enter().append("rect").attr("width",this.config.frameHandleThickness).attr("height",this.config.height).style("cursor","ew-resize").style("fill-opacity","0").classed("right_handle",true);rightHandle.attr("x",this.currentFrame[1]+this.config.frameHandleThickness)},_renderMiddle:function(){var middleHandle=this.svg.selectAll("rect.middle_handle").data([null]);middleHandle.enter().append("rect").attr("height",this.config.height).style("cursor","move").style("fill-opacity","0").classed("middle_handle",true);middleHandle.attr("width",Math.max(0,this.currentFrame[1]-this.currentFrame[0])).attr("x",this.currentFrame[0]+this.config.frameHandleThickness)},_registerMouseEvents:function(){var element=d3.select(this.element);var drag={target:null,start:null,stop:null,left:false,right:false,rigid:false};var self=this;function onMousemove(datum,index){drag.stop=self._getClientXFromEvent(d3.event,drag);var distanceTraveled=drag.stop-drag.start;var frameAfterDrag=self.frameBeforeDrag.slice(0);var minimumFrameWidth=self.config.minimumFrameWidth;if(drag.rigid){minimumFrameWidth=self.frameBeforeDrag[1]-self.frameBeforeDrag[0]}if(drag.left){frameAfterDrag[0]=Math.max(frameAfterDrag[0]+distanceTraveled,0)}if(drag.right){frameAfterDrag[1]=Math.min(frameAfterDrag[1]+distanceTraveled,self.previewWidth)}var currentFrameWidth=frameAfterDrag[1]-frameAfterDrag[0];if(currentFrameWidth<=minimumFrameWidth){if(drag.left){frameAfterDrag[0]=frameAfterDrag[1]-minimumFrameWidth}if(drag.right){frameAfterDrag[1]=frameAfterDrag[0]+minimumFrameWidth}if(frameAfterDrag[0]<=0){frameAfterDrag[1]-=frameAfterDrag[0];frameAfterDrag[0]=0}if(frameAfterDrag[1]>=self.previewWidth){frameAfterDrag[0]-=frameAfterDrag[1]-self.previewWidth;frameAfterDrag[1]=self.previewWidth}}self.graphs.forEach(function(graph){var domainScale=d3.scale.linear().interpolate(d3.interpolateRound).domain([0,self.previewWidth]).range(graph.dataDomain());var windowAfterDrag=[domainScale(frameAfterDrag[0]),domainScale(frameAfterDrag[1])];if(frameAfterDrag[0]===0){windowAfterDrag[0]=undefined}if(frameAfterDrag[1]===self.previewWidth){windowAfterDrag[1]=undefined}graph.window.xMin=windowAfterDrag[0];graph.window.xMax=windowAfterDrag[1];graph.update()})}function onMousedown(){drag.target=d3.event.target;drag.start=self._getClientXFromEvent(d3.event,drag);self.frameBeforeDrag=self.currentFrame.slice();d3.event.preventDefault?d3.event.preventDefault():d3.event.returnValue=false;d3.select(document).on("mousemove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("mouseup.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchmove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("touchend.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",onMouseup)}function onMousedownLeftHandle(datum,index){drag.left=true;onMousedown()}function onMousedownRightHandle(datum,index){drag.right=true;onMousedown()}function onMousedownMiddleHandle(datum,index){drag.left=true;drag.right=true;drag.rigid=true;onMousedown()}function onMouseup(datum,index){d3.select(document).on("mousemove.rickshaw_range_slider_preview",null);d3.select(document).on("mouseup.rickshaw_range_slider_preview",null);d3.select(document).on("touchmove.rickshaw_range_slider_preview",null);d3.select(document).on("touchend.rickshaw_range_slider_preview",null);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",null);delete self.frameBeforeDrag;drag.left=false;drag.right=false;drag.rigid=false}element.select("rect.left_handle").on("mousedown",onMousedownLeftHandle);element.select("rect.right_handle").on("mousedown",onMousedownRightHandle);element.select("rect.middle_handle").on("mousedown",onMousedownMiddleHandle);element.select("rect.left_handle").on("touchstart",onMousedownLeftHandle);element.select("rect.right_handle").on("touchstart",onMousedownRightHandle);element.select("rect.middle_handle").on("touchstart",onMousedownMiddleHandle)},_getClientXFromEvent:function(event,drag){switch(event.type){case"touchstart":case"touchmove":var touchList=event.changedTouches;var touch=null;for(var touchIndex=0;touchIndex<touchList.length;touchIndex++){if(touchList[touchIndex].target===drag.target){touch=touchList[touchIndex];break}}return touch!==null?touch.clientX:undefined;default:return event.clientX}}});Rickshaw.namespace("Rickshaw.Graph.Renderer");Rickshaw.Graph.Renderer=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.tension=args.tension||this.tension;this.configure(args)},seriesPathFactory:function(){},seriesStrokeFactory:function(){},defaults:function(){return{tension:.8,strokeWidth:2,unstack:true,padding:{top:.01,right:0,bottom:.01,left:0},stroke:false,fill:false}},domain:function(data){var stackedData=data||this.graph.stackedData||this.graph.stackData();var firstPoint=stackedData[0][0];if(firstPoint===undefined){return{x:[null,null],y:[null,null]}}var xMin=firstPoint.x;var xMax=firstPoint.x;var yMin=firstPoint.y+firstPoint.y0;var yMax=firstPoint.y+firstPoint.y0;stackedData.forEach(function(series){series.forEach(function(d){if(d.y==null)return;var y=d.y+d.y0;if(y<yMin)yMin=y;if(y>yMax)yMax=y});if(!series.length)return;if(series[0].x<xMin)xMin=series[0].x;if(series[series.length-1].x>xMax)xMax=series[series.length-1].x});xMin-=(xMax-xMin)*this.padding.left;xMax+=(xMax-xMin)*this.padding.right;yMin=this.graph.min==="auto"?yMin:this.graph.min||0;yMax=this.graph.max===undefined?yMax:this.graph.max;if(this.graph.min==="auto"||yMin<0){yMin-=(yMax-yMin)*this.padding.bottom}if(this.graph.max===undefined){yMax+=(yMax-yMin)*this.padding.top}return{x:[xMin,xMax],y:[yMin,yMax]}},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var pathNodes=vis.selectAll("path.path").data(data).enter().append("svg:path").classed("path",true).attr("d",this.seriesPathFactory());if(this.stroke){var strokeNodes=vis.selectAll("path.stroke").data(data).enter().append("svg:path").classed("stroke",true).attr("d",this.seriesStrokeFactory())}var i=0;series.forEach(function(series){if(series.disabled)return;series.path=pathNodes[0][i];if(this.stroke)series.stroke=strokeNodes[0][i];this._styleSeries(series);i++},this)},_styleSeries:function(series){var fill=this.fill?series.color:"none";var stroke=this.stroke?series.color:"none";series.path.setAttribute("fill",fill);series.path.setAttribute("stroke",stroke);series.path.setAttribute("stroke-width",this.strokeWidth);if(series.className){d3.select(series.path).classed(series.className,true)}if(series.className&&this.stroke){d3.select(series.stroke).classed(series.className,true)}},configure:function(args){args=args||{};Rickshaw.keys(this.defaults()).forEach(function(key){if(!args.hasOwnProperty(key)){this[key]=this[key]||this.graph[key]||this.defaults()[key];return}if(typeof this.defaults()[key]=="object"){Rickshaw.keys(this.defaults()[key]).forEach(function(k){this[key][k]=args[key][k]!==undefined?args[key][k]:this[key][k]!==undefined?this[key][k]:this.defaults()[key][k]},this)}else{this[key]=args[key]!==undefined?args[key]:this[key]!==undefined?this[key]:this.graph[key]!==undefined?this.graph[key]:this.defaults()[key]}},this)},setStrokeWidth:function(strokeWidth){if(strokeWidth!==undefined){this.strokeWidth=strokeWidth}},setTension:function(tension){if(tension!==undefined){this.tension=tension}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Line");Rickshaw.Graph.Renderer.Line=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"line",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Stack");Rickshaw.Graph.Renderer.Stack=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"stack",defaults:function($super){return Rickshaw.extend($super(),{fill:true,stroke:false,unstack:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Bar");Rickshaw.Graph.Renderer.Bar=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"bar",defaults:function($super){var defaults=Rickshaw.extend($super(),{gapSize:.05,unstack:false});delete defaults.tension;return defaults},initialize:function($super,args){args=args||{};this.gapSize=args.gapSize||this.gapSize;$super(args)},domain:function($super){var domain=$super();var frequentInterval=this._frequentInterval(this.graph.stackedData.slice(-1).shift());domain.x[1]+=Number(frequentInterval.magnitude);return domain},barWidth:function(series){var frequentInterval=this._frequentInterval(series.stack);var barWidth=this.graph.x(series.stack[0].x+frequentInterval.magnitude*(1-this.gapSize));return barWidth},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var barWidth=this.barWidth(series.active()[0]);var barXOffset=0;var activeSeriesCount=series.filter(function(s){return!s.disabled}).length;var seriesBarWidth=this.unstack?barWidth/activeSeriesCount:barWidth;var transform=function(d){var matrix=[1,0,0,d.y<0?-1:1,0,d.y<0?graph.y.magnitude(Math.abs(d.y))*2:0];return"matrix("+matrix.join(",")+")"};series.forEach(function(series){if(series.disabled)return;var barWidth=this.barWidth(series);var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:rect").attr("x",function(d){return graph.x(d.x)+barXOffset}).attr("y",function(d){return graph.y(d.y0+Math.abs(d.y))*(d.y<0?-1:1)}).attr("width",seriesBarWidth).attr("height",function(d){return graph.y.magnitude(Math.abs(d.y))}).attr("transform",transform);Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)});if(this.unstack)barXOffset+=seriesBarWidth},this)},_frequentInterval:function(data){var intervalCounts={};for(var i=0;i<data.length-1;i++){var interval=data[i+1].x-data[i].x;intervalCounts[interval]=intervalCounts[interval]||0;intervalCounts[interval]++}var frequentInterval={count:0,magnitude:1};Rickshaw.keys(intervalCounts).forEach(function(i){if(frequentInterval.count<intervalCounts[i]){frequentInterval={count:intervalCounts[i],magnitude:i}}});return frequentInterval}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Area");Rickshaw.Graph.Renderer.Area=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"area",defaults:function($super){return Rickshaw.extend($super(),{unstack:false,fill:false,stroke:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory},seriesStrokeFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y+d.y0)}).interpolate(graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var method=this.unstack?"append":"insert";var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var nodes=vis.selectAll("path").data(data).enter()[method]("svg:g","g");nodes.append("svg:path").attr("d",this.seriesPathFactory()).attr("class","area");if(this.stroke){nodes.append("svg:path").attr("d",this.seriesStrokeFactory()).attr("class","line")}var i=0;series.forEach(function(series){if(series.disabled)return;series.path=nodes[0][i++];this._styleSeries(series)},this)},_styleSeries:function(series){if(!series.path)return;d3.select(series.path).select(".area").attr("fill",series.color);if(this.stroke){d3.select(series.path).select(".line").attr("fill","none").attr("stroke",series.stroke||d3.interpolateRgb(series.color,"black")(.125)).attr("stroke-width",this.strokeWidth)}if(series.className){series.path.setAttribute("class",series.className)}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.ScatterPlot");
+Rickshaw.Graph.Renderer.ScatterPlot=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"scatterplot",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:true,stroke:false,padding:{top:.01,right:.01,bottom:.01,left:.01},dotSize:4})},initialize:function($super,args){$super(args)},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;var dotSize=this.dotSize;vis.selectAll("*").remove();series.forEach(function(series){if(series.disabled)return;var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:circle").attr("cx",function(d){return graph.x(d.x)}).attr("cy",function(d){return graph.y(d.y)}).attr("r",function(d){return"r"in d?d.r:dotSize});if(series.className){nodes.classed(series.className,true)}Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)})},this)}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Multi");Rickshaw.Graph.Renderer.Multi=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"multi",initialize:function($super,args){$super(args)},defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},configure:function($super,args){args=args||{};this.config=args;$super(args)},domain:function($super){this.graph.stackData();var domains=[];var groups=this._groups();this._stack(groups);groups.forEach(function(group){var data=group.series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});if(!data.length)return;var domain=$super(data);domains.push(domain)});var xMin=d3.min(domains.map(function(d){return d.x[0]}));var xMax=d3.max(domains.map(function(d){return d.x[1]}));var yMin=d3.min(domains.map(function(d){return d.y[0]}));var yMax=d3.max(domains.map(function(d){return d.y[1]}));return{x:[xMin,xMax],y:[yMin,yMax]}},_groups:function(){var graph=this.graph;var renderGroups={};graph.series.forEach(function(series){if(series.disabled)return;if(!renderGroups[series.renderer]){var ns="http://www.w3.org/2000/svg";var vis=document.createElementNS(ns,"g");graph.vis[0][0].appendChild(vis);var renderer=graph._renderers[series.renderer];var config={};var defaults=[this.defaults(),renderer.defaults(),this.config,this.graph];defaults.forEach(function(d){Rickshaw.extend(config,d)});renderer.configure(config);renderGroups[series.renderer]={renderer:renderer,series:[],vis:d3.select(vis)}}renderGroups[series.renderer].series.push(series)},this);var groups=[];Object.keys(renderGroups).forEach(function(key){var group=renderGroups[key];groups.push(group)});return groups},_stack:function(groups){groups.forEach(function(group){var series=group.series.filter(function(series){return!series.disabled});var data=series.map(function(series){return series.stack});if(!group.renderer.unstack){var layout=d3.layout.stack();var stackedData=Rickshaw.clone(layout(data));series.forEach(function(series,index){series._stack=Rickshaw.clone(stackedData[index])})}},this);return groups},render:function(){this.graph.series.forEach(function(series){if(!series.renderer){throw new Error("Each series needs a renderer for graph 'multi' renderer")}});this.graph.vis.selectAll("*").remove();var groups=this._groups();groups=this._stack(groups);groups.forEach(function(group){var series=group.series.filter(function(series){return!series.disabled});series.active=function(){return series};group.renderer.render({series:series,vis:group.vis});series.forEach(function(s){s.stack=s._stack||s.stack||s.data})})}});Rickshaw.namespace("Rickshaw.Graph.Renderer.LinePlot");Rickshaw.Graph.Renderer.LinePlot=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"lineplot",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true,padding:{top:.01,right:.01,bottom:.01,left:.01},dotSize:3,strokeWidth:2})},initialize:function($super,args){$super(args)},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory},_renderDots:function(){var graph=this.graph;graph.series.forEach(function(series){if(series.disabled)return;var nodes=graph.vis.selectAll("x").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:circle").attr("cx",function(d){return graph.x(d.x)}).attr("cy",function(d){return graph.y(d.y)}).attr("r",function(d){return"r"in d?d.r:graph.renderer.dotSize});Array.prototype.forEach.call(nodes[0],function(n){if(!n)return;n.setAttribute("data-color",series.color);n.setAttribute("fill","white");n.setAttribute("stroke",series.color);n.setAttribute("stroke-width",this.strokeWidth)}.bind(this))},this)},_renderLines:function(){var graph=this.graph;var nodes=graph.vis.selectAll("path").data(this.graph.stackedData).enter().append("svg:path").attr("d",this.seriesPathFactory());var i=0;graph.series.forEach(function(series){if(series.disabled)return;series.path=nodes[0][i++];this._styleSeries(series)},this)},render:function(){var graph=this.graph;graph.vis.selectAll("*").remove();this._renderLines();this._renderDots()}});Rickshaw.namespace("Rickshaw.Graph.Smoother");Rickshaw.Graph.Smoother=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.element=args.element;this.aggregationScale=1;this.build();this.graph.stackData.hooks.data.push({name:"smoother",orderPosition:50,f:this.transformer.bind(this)})},build:function(){var self=this;if(this.element){$(function(){$(self.element).slider({min:1,max:100,slide:function(event,ui){self.setScale(ui.value);self.graph.update()}})})}},setScale:function(scale){if(scale<1){throw"scale out of range: "+scale}this.aggregationScale=scale;this.graph.update()},transformer:function(data){if(this.aggregationScale==1)return data;var aggregatedData=[];data.forEach(function(seriesData){var aggregatedSeriesData=[];while(seriesData.length){var avgX=0,avgY=0;var slice=seriesData.splice(0,this.aggregationScale);slice.forEach(function(d){avgX+=d.x/slice.length;avgY+=d.y/slice.length});aggregatedSeriesData.push({x:avgX,y:avgY})}aggregatedData.push(aggregatedSeriesData)}.bind(this));return aggregatedData}});Rickshaw.namespace("Rickshaw.Graph.Socketio");Rickshaw.Graph.Socketio=Rickshaw.Class.create(Rickshaw.Graph.Ajax,{request:function(){var socket=io.connect(this.dataURL);var self=this;socket.on("rickshaw",function(data){self.success(data)})}});Rickshaw.namespace("Rickshaw.Series");Rickshaw.Series=Rickshaw.Class.create(Array,{initialize:function(data,palette,options){options=options||{};this.palette=new Rickshaw.Color.Palette(palette);this.timeBase=typeof options.timeBase==="undefined"?Math.floor((new Date).getTime()/1e3):options.timeBase;var timeInterval=typeof options.timeInterval=="undefined"?1e3:options.timeInterval;this.setTimeInterval(timeInterval);if(data&&typeof data=="object"&&Array.isArray(data)){data.forEach(function(item){this.addItem(item)},this)}},addItem:function(item){if(typeof item.name==="undefined"){throw"addItem() needs a name"}item.color=item.color||this.palette.color(item.name);item.data=item.data||[];if(item.data.length===0&&this.length&&this.getIndex()>0){this[0].data.forEach(function(plot){item.data.push({x:plot.x,y:0})})}else if(item.data.length===0){item.data.push({x:this.timeBase-(this.timeInterval||0),y:0})}this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},addData:function(data,x){var index=this.getIndex();Rickshaw.keys(data).forEach(function(name){if(!this.itemByName(name)){this.addItem({name:name})}},this);this.forEach(function(item){item.data.push({x:x||(index*this.timeInterval||1)+this.timeBase,y:data[item.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(name){for(var i=0;i<this.length;i++){if(this[i].name==name)return this[i]}},setTimeInterval:function(iv){this.timeInterval=iv/1e3},setTimeBase:function(t){this.timeBase=t},dump:function(){var data={timeBase:this.timeBase,timeInterval:this.timeInterval,items:[]};this.forEach(function(item){var newItem={color:item.color,name:item.name,data:[]};item.data.forEach(function(plot){newItem.data.push({x:plot.x,y:plot.y})});data.items.push(newItem)});return data},load:function(data){if(data.timeInterval){this.timeInterval=data.timeInterval}if(data.timeBase){this.timeBase=data.timeBase}if(data.items){data.items.forEach(function(item){this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},this)}}});Rickshaw.Series.zeroFill=function(series){Rickshaw.Series.fill(series,0)};Rickshaw.Series.fill=function(series,fill){var x;var i=0;var data=series.map(function(s){return s.data});while(i<Math.max.apply(null,data.map(function(d){return d.length}))){x=Math.min.apply(null,data.filter(function(d){return d[i]}).map(function(d){return d[i].x}));data.forEach(function(d){if(!d[i]||d[i].x!=x){d.splice(i,0,{x:x,y:fill})}});i++}};Rickshaw.namespace("Rickshaw.Series.FixedDuration");Rickshaw.Series.FixedDuration=Rickshaw.Class.create(Rickshaw.Series,{initialize:function(data,palette,options){options=options||{};if(typeof options.timeInterval==="undefined"){throw new Error("FixedDuration series requires timeInterval")}if(typeof options.maxDataPoints==="undefined"){throw new Error("FixedDuration series requires maxDataPoints")}this.palette=new Rickshaw.Color.Palette(palette);this.timeBase=typeof options.timeBase==="undefined"?Math.floor((new Date).getTime()/1e3):options.timeBase;this.setTimeInterval(options.timeInterval);if(this[0]&&this[0].data&&this[0].data.length){this.currentSize=this[0].data.length;this.currentIndex=this[0].data.length}else{this.currentSize=0;this.currentIndex=0}this.maxDataPoints=options.maxDataPoints;if(data&&typeof data=="object"&&Array.isArray(data)){data.forEach(function(item){this.addItem(item)},this);this.currentSize+=1;this.currentIndex+=1}this.timeBase-=(this.maxDataPoints-this.currentSize)*this.timeInterval;if(typeof this.maxDataPoints!=="undefined"&&this.currentSize<this.maxDataPoints){for(var i=this.maxDataPoints-this.currentSize-1;i>1;i--){this.currentSize+=1;this.currentIndex+=1;this.forEach(function(item){item.data.unshift({x:((i-1)*this.timeInterval||1)+this.timeBase,y:0,i:i})},this)}}},addData:function($super,data,x){$super(data,x);this.currentSize+=1;this.currentIndex+=1;if(this.maxDataPoints!==undefined){while(this.currentSize>this.maxDataPoints){this.dropData()}}},dropData:function(){this.forEach(function(item){item.data.splice(0,1)});this.currentSize-=1},getIndex:function(){return this.currentIndex}});
\ No newline at end of file
diff --git a/static/libjs/sorttable.js b/static/libjs/sorttable.js
new file mode 100644
index 0000000000000000000000000000000000000000..41f9cb0e4951cce757154d27bddcf3f19bfa151b
--- /dev/null
+++ b/static/libjs/sorttable.js
@@ -0,0 +1,244 @@
+/* -*- mode: javascript -*-
+ */
+
+addEvent(window, "load", sortables_init);
+
+var SORT_COLUMN_INDEX;
+
+function sortables_init() {
+    // Find all tables with class sortable and make them sortable
+    if (!document.getElementsByTagName) return;
+    var tbls = document.getElementsByTagName("table");
+    for (var ti=0;ti<tbls.length;ti++) {
+        var thisTbl = tbls[ti];
+        if (((' '+thisTbl.className+' ').indexOf("sortable") != -1) && (thisTbl.id)) {
+            //initTable(thisTbl.id);
+            ts_makeSortable(thisTbl);
+        }
+    }
+}
+
+function ts_makeSortable(table) {
+    if (table.rows && table.rows.length > 0) {
+        var firstRow = table.rows[0];
+    }
+    if (!firstRow) return;
+    
+    // We have a first row: assume it's the header, and make its contents clickable links
+    for (var i=0;i<firstRow.cells.length;i++) {
+        var cell = firstRow.cells[i];
+        var txt = ts_getInnerText(cell);
+        cell.innerHTML = '<a href="#" class="sortheader" '+ 
+        'onclick="ts_resortTable(this, '+i+');return false;">' + 
+        txt+'<span class="sortarrow">&nbsp;&nbsp;&nbsp;</span></a>';
+    }
+}
+
+function ts_getInnerText(el) {
+	if (typeof el == "string") return el;
+	if (typeof el == "undefined") { return el };
+	if (el.innerText) return el.innerText;	//Not needed but it is faster
+	var str = "";
+	
+	var cs = el.childNodes;
+	var l = cs.length;
+	for (var i = 0; i < l; i++) {
+		switch (cs[i].nodeType) {
+			case 1: //ELEMENT_NODE
+				str += ts_getInnerText(cs[i]);
+				break;
+			case 3:	//TEXT_NODE
+				str += cs[i].nodeValue;
+				break;
+		}
+	}
+	return str;
+}
+
+// Check ifan element contains a css class
+// Uses classList  Introduced in Gecko 1.9.2 (FF 3.6) or regexp
+var containsClass = function (elm, className) {
+    if (document.documentElement.classList) {
+        containsClass = function (elm, className) {
+            return elm.classList.contains(className);
+        }
+    } else {
+        containsClass = function (elm, className) {
+            if (!elm || !elm.className) {
+                return false;
+            }
+            var re = new RegExp('(^|\\s)' + className + '(\\s|$)');
+            return elm.className.match(re);
+        }
+    }
+    return containsClass(elm, className);
+}
+
+
+function ts_resortTable(lnk,clid) {
+    // get the span
+    var span;
+    for (var ci=0;ci<lnk.childNodes.length;ci++) {
+        if (lnk.childNodes[ci].tagName && lnk.childNodes[ci].tagName.toLowerCase() == 'span') span = lnk.childNodes[ci];
+    }
+    var spantext = ts_getInnerText(span);
+    var td = lnk.parentNode;
+    var column = clid || td.cellIndex;
+    var table = getParent(td,'TABLE');
+    
+    // Work out a type for the column
+    if (table.rows.length <= 1) return;
+    
+    isNumeric = false;
+    if (containsClass(table.rows[0].cells[column], 'sortnumeric'))  {
+	isNumeric = true;
+    }
+
+    var itm = ts_getInnerText(table.rows[1].cells[column]);
+    sortfn = ts_sort_caseinsensitive;
+    if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) sortfn = ts_sort_date;
+    if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) sortfn = ts_sort_date;
+    if (itm.match(/^[�$]/)) sortfn = ts_sort_currency;
+    if (itm.match(/^[\d\.]+$/) || isNumeric) sortfn = ts_sort_numeric;
+    SORT_COLUMN_INDEX = column;
+    var firstRow = new Array();
+    var newRows = new Array();
+    var botRows = new Array();
+    var topRows = new Array();
+    var ir = 0;
+    var ib = 0;
+    var it = 0;
+    for (i=0;i<table.rows[0].length;i++) { firstRow[i] = table.rows[0][i]; }
+    for (j=1;j<table.rows.length;j++) { 
+      if (!table.rows[j].className) {
+	newRows[ir] = table.rows[j];
+	ir += 1;
+      } else {
+	if (table.rows[j].className.indexOf('sortbottom') != -1) {
+	  botRows[ib] = table.rows[j];
+	  ib += 1;
+	} else {
+	  if (table.rows[j].className.indexOf('sorttop') != -1) {
+	    topRows[it] = table.rows[j];
+	    it += 1;
+	  } else {
+	    newRows[ir] = table.rows[j];
+	    ir += 1;
+	  }
+	}
+      }
+    }
+
+    newRows.sort(sortfn);
+
+    if (span.getAttribute("sortdir") == 'down') {
+        ARROW = '&nbsp;&nbsp;&uarr;';
+        newRows.reverse();
+        span.setAttribute('sortdir','up');
+    } else {
+        ARROW = '&nbsp;&nbsp;&darr;';
+        span.setAttribute('sortdir','down');
+    }
+    
+    // We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
+    // place sorttop rows at first:
+    for (i=0; i < topRows.length; i++) {
+      table.tBodies[0].appendChild(topRows[i]);
+    }
+    // standard (sorted) rows:
+    for (i=0; i < newRows.length;i++) { 
+      table.tBodies[0].appendChild(newRows[i]);
+    }
+    // do sortbottom rows only
+    for (i=0; i < botRows.length;i++) {
+      table.tBodies[0].appendChild(botRows[i]);
+    }
+    // Delete any other arrows there may be showing
+    var allspans = document.getElementsByTagName("span");
+    for (var ci=0;ci<allspans.length;ci++) {
+        if (allspans[ci].className == 'sortarrow') {
+            if (getParent(allspans[ci],"table") == getParent(lnk,"table")) { // in the same table as us?
+                allspans[ci].innerHTML = '&nbsp;&nbsp;&nbsp;';
+            }
+        }
+    }
+        
+    span.innerHTML = ARROW;
+}
+
+function getParent(el, pTagName) {
+	if (el == null) return null;
+	else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase())	// Gecko bug, supposed to be uppercase
+		return el;
+	else
+		return getParent(el.parentNode, pTagName);
+}
+function ts_sort_date(a,b) {
+    // y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
+    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
+    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
+    if (aa.length == 10) {
+        dt1 = aa.substr(6,4)+aa.substr(3,2)+aa.substr(0,2);
+    } else {
+        yr = aa.substr(6,2);
+        if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
+        dt1 = yr+aa.substr(3,2)+aa.substr(0,2);
+    }
+    if (bb.length == 10) {
+        dt2 = bb.substr(6,4)+bb.substr(3,2)+bb.substr(0,2);
+    } else {
+        yr = bb.substr(6,2);
+        if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
+        dt2 = yr+bb.substr(3,2)+bb.substr(0,2);
+    }
+    if (dt1==dt2) return 0;
+    if (dt1<dt2) return -1;
+    return 1;
+}
+
+function ts_sort_currency(a,b) { 
+    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
+    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
+    return parseFloat(aa) - parseFloat(bb);
+}
+
+function ts_sort_numeric(a,b) { 
+    aa = parseFloat(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
+    if (isNaN(aa)) aa = 0;
+    bb = parseFloat(ts_getInnerText(b.cells[SORT_COLUMN_INDEX])); 
+    if (isNaN(bb)) bb = 0;
+    return aa-bb;
+}
+
+function ts_sort_caseinsensitive(a,b) {
+    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
+    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
+    if (aa==bb) return 0;
+    if (aa<bb) return -1;
+    return 1;
+}
+
+function ts_sort_default(a,b) {
+    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
+    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
+    if (aa==bb) return 0;
+    if (aa<bb) return -1;
+    return 1;
+}
+
+
+function addEvent(elm, evType, fn, useCapture)
+// addEvent and removeEvent
+// cross-browser event handling for IE5+,  NS6 and Mozilla
+// By Scott Andrew
+{
+  if (elm.addEventListener){
+    elm.addEventListener(evType, fn, useCapture);
+    return true;
+  } else if (elm.attachEvent){
+    var r = elm.attachEvent("on"+evType, fn);
+    return r;
+  } else {
+    alert("Handler could not be removed");
+  }
+} 
diff --git a/static/robots.txt b/static/robots.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1f53798bb4fe33c86020be7f10c44f29486fd190
--- /dev/null
+++ b/static/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/test_jurype.py b/test_jurype.py
new file mode 100644
index 0000000000000000000000000000000000000000..99f1c649d9d561907337c02012c3e784560ca1f9
--- /dev/null
+++ b/test_jurype.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+
+"""
+ A executer apres s'etre place dans /opt/scodoc/Products/ScoDoc
+ et en lancant l'interpreteur avec /opt/scodoc/bin/zopectl debug
+
+ execfile("test_jurype.py")
+
+
+@author: barasc
+Juin 2017
+"""
+import pprint
+
+from debug import *
+
+import sco_utils
+import sco_formsemestre
+import sco_codes_parcours
+
+import pe_tools
+
+reload(pe_tools)  # inutile sauf en debug interactif
+import pe_tagtable
+
+reload(pe_tagtable)
+import pe_semestretag
+
+reload(pe_semestretag)
+import pe_settag
+
+reload(pe_settag)
+import pe_jurype
+
+reload(pe_jurype)
+import codecs
+import pe_avislatex
+
+reload(pe_avislatex)
+
+# ****************************************************************************
+# Initialisations generales
+# ****************************************************************************
+context = go_dept(app, "RT")  # se place dans le departement RT
+authuser = app.acl_users.getUserById("admin")
+sems = (
+    context.Notes.formsemestre_list()
+)  # Renvoie la liste de tous les semestres de la BDD
+
+qui = "emmanuel"
+if qui == "cleo":
+    fid = "SEM9045"  # Choix du semestre sur lequel sera fait le calcul du jury (ici promo 2016 avec tous les résultats)
+    # fid = 'SEM14440' # Cas d'une promo dans laquelle manque au moins le S4
+else:
+    fid = "SEM27456"  # RT S3 jan 2016
+
+print("Semestre=", fid)
+
+if qui == "cleo":
+    nom = "BRUN"  # Choix du nom de l'étudiant dont les résultats seront affichés sur la console
+else:
+    nom = "ROSSE"
+
+# paramètres des avis Latex
+REPERTOIRE_MODELES = "tmp/avis/modeles/"
+REPERTOIRE_ECRITURE_AVIS = "tmp/avis/"
+
+# class = "avisPE.cls"
+# main = "avis.tex" # Fichier principal de compilation
+modele = "un_avis.tex"  # Modele à actualiser au regard des résultats de l'étudiant
+
+# *****************************************************************************
+# Calcul du Jury PE
+# *****************************************************************************
+# semsDUT = [sem for sem in sems if 1 <= sem['semestre_id'] <= 4 ]
+semBase = sco_formsemestre.get_formsemestre(context, fid)
+jury = pe_jurype.JuryPE(context, semBase)
+
+
+# *****************************************************************************
+# Affichage des résultats (texte)
+# *****************************************************************************
+etudid = [
+    etudid for etudid in jury.syntheseJury if jury.syntheseJury[etudid]["nom"] == nom
+][0]
+resEtudiant = jury.syntheseJury[etudid]  # Résultat de l'étudiant au jury PE
+
+# Parcours
+parcours = resEtudiant["parcours"][::-1]
+print("Parcours de %s %s" % (resEtudiant["prenom"], resEtudiant["nom"]))
+for (no, sem) in enumerate(parcours):
+    print("  %d) %s" % (no + 1, sem))
+
+# Bilan synthétique par semestre et par tag
+for sem in ["S1", "S2", "S3", "S4", "1A", "2A", "3S", "4S"]:
+    if sem in ["S1", "S2", "S3", "S4"]:
+        print("Semestre %s" % (sem))
+    elif sem in ["1A", "2A"]:
+        print("Annee %s" % (sem))
+    elif sem == "3S":
+        print("Fusion S1/S2/S3")
+    else:
+        print("Fusion S1/S2/S3/S4")
+
+    allTags = resEtudiant[sem]["groupe"].keys()
+    allTags.extend(resEtudiant[sem]["promo"].keys())
+    allTags = sorted(list(set(allTags)))  # tous les tags du groupe et de la promo
+
+    restxt = ""
+    for tag in allTags:
+        chaine = "    * %20s\t" % (tag)
+
+        for ensembleRes in [resEtudiant[sem]["groupe"], resEtudiant[sem]["promo"]]:
+            if tag in ensembleRes:
+                (note, coeff, classement, nb_inscrits, vmoy, vmax, vmin) = ensembleRes[
+                    tag
+                ]
+                note = "%2.2f" % note if isinstance(note, float) else str(note)
+                classement = "None" if classement == None else classement
+                nb_inscrits = (
+                    ("%d" % nb_inscrits)
+                    if isinstance(nb_inscrits, int)
+                    else str(nb_inscrits)
+                )
+                vmin = "%2.2f" % vmin if isinstance(vmin, float) else str(vmin)
+                vmoy = "%2.2f" % vmoy if isinstance(vmoy, float) else str(vmoy)
+                vmax = "%2.2f" % vmax if isinstance(vmax, float) else str(vmax)
+
+                chaine += "%5s\t%5s/%2s\t%5s/%5s/%5s\t" % (
+                    note,
+                    classement,
+                    nb_inscrits,
+                    vmin,
+                    vmoy,
+                    vmax,
+                )
+            else:
+                chaine += "miss.\t" * 3
+
+        restxt += chaine + "\n"
+    print(restxt)
+
+# Sauvegarde du zip contenant les CSV:
+filename = "/tmp/test_pe.zip"
+print("Enregistrement du ZIP: ", filename)
+f = open(filename, "w")
+f.write(jury.get_zipped_data())
+f.close()
+
+# *****************************************************************************
+# Genere l'avis latex
+# *****************************************************************************
+# Choix d'un modele d'avis PE
+
+
+# Genere l'avis PE
+print("Avis PE de %s (%s)" % (jury.syntheseJury[etudid]["nom"], etudid))
+un_avis_latex = pe_avislatex.get_code_latex_from_modele(
+    REPERTOIRE_MODELES + "un_avis.tex"
+)
+code_latex = pe_avislatex.get_code_latex_avis_etudiant(
+    jury.syntheseJury[etudid], un_avis_latex
+)
+
+# Sauvegarde l'avis
+fid = codecs.open(REPERTOIRE_ECRITURE_AVIS + modele, "w", encoding="utf-8")
+fid.write(code_latex)
+fid.close()
diff --git a/tests/conn_info.py b/tests/conn_info.py
new file mode 100644
index 0000000000000000000000000000000000000000..986953654ab6445b03e2b6378be35c4d2c02d87c
--- /dev/null
+++ b/tests/conn_info.py
@@ -0,0 +1,9 @@
+
+"""Put here the informations neeede to connect to your development test user
+"""
+
+SCODOC='https://scodoc.example.com/'
+USER = 'tester'
+PASSWD = 'XXXXXXXXXXXX'
+
+
diff --git a/tests/demo/demo_reset_noms.py b/tests/demo/demo_reset_noms.py
new file mode 100755
index 0000000000000000000000000000000000000000..ef5b589a159153a44e7e293fd3d04c327a0b954f
--- /dev/null
+++ b/tests/demo/demo_reset_noms.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""Outils pour environnements de démo.
+
+Change aléatoirement les identites (nom, prenom) d'un ensemble d'étudiants.
+"""
+
+import sys
+import random
+import psycopg2
+
+DBCNXSTRING='dbname=SCODEMO'
+
+# Noms et prénoms les plus fréquents en France:
+NOMS = [ x.strip() for x in open('noms.txt').readlines() ]
+PRENOMS_H = [ x.strip() for x in open('prenoms-h.txt').readlines() ]
+PRENOMS_F = [ x.strip() for x in open('prenoms-f.txt').readlines() ]
+
+def nomprenom(sexe):
+    """un nom et un prenom au hasard"""
+    if 'e' in sexe.lower():
+        prenom = random.choice(PRENOMS_F)
+    else:
+        prenom = random.choice(PRENOMS_H)
+    return random.choice(NOMS), prenom
+
+def usage():
+    print(f'Usage: {sys.argv[0]} formsemestre_id')
+    sys.exit(1)
+
+if len(sys.argv) != 2:
+    usage()
+formsemestre_id = sys.argv[1]
+
+# Liste des etudiants inscrits à ce semestre
+cnx = psycopg2.connect( DBCNXSTRING )
+cursor = cnx.cursor()
+cursor.execute( """select i.etudid
+    from identite i, notes_formsemestre_inscription ins 
+    where i.etudid=ins.etudid and ins.formsemestre_id=%(formsemestre_id)s
+    """, 
+    { 'formsemestre_id' : formsemestre_id})
+
+wcursor = cnx.cursor()
+for (etudid,) in cursor:
+    sexe = random.choice( ('M.', 'MME') )
+    nom, prenom = nomprenom(sexe)
+    print(f'{etudid}: {nom}\t{prenom}')
+    args = { 'nom' : nom, 'prenom' : prenom, 'sexe' : sexe, 'etudid' : etudid }
+    req = "update identite set nom=%(nom)s, prenom=%(prenom)s, sexe=%(sexe)s where etudid=%(etudid)s"
+    #print( req % args)
+    wcursor.execute( req, args )
+    
+
+cnx.commit()
+cnx.close()
+
diff --git a/tests/demo/noms.txt b/tests/demo/noms.txt
new file mode 100644
index 0000000000000000000000000000000000000000..eb6ea355fc3981c052b5fd54d7b43fdeb0d3f51c
--- /dev/null
+++ b/tests/demo/noms.txt
@@ -0,0 +1,1051 @@
+Martin
+Bernard
+Thomas
+Petit
+Robert
+Richard
+Dubois
+Durand
+Moreau
+Laurent
+Simon
+Michel
+Lefebvre
+Leroy
+David
+Roux
+Morel
+Bertrand
+Fournier
+Girard
+Fontaine
+Lambert
+Dupont
+Bonnet
+Rousseau
+Vincent
+Muller
+Lefevre
+Faure
+Andre
+Mercier
+Guerin
+Garcia
+Boyer
+Blanc
+Garnier
+Chevalier
+Francois
+Legrand
+Gauthier
+Perrin
+Robin
+Clement
+Morin
+Henry
+Nicolas
+Roussel
+Gautier
+Mathieu
+Masson
+Duval
+Marchand
+Denis
+Lemaire
+Dumont
+Marie
+Noel
+Meyer
+Dufour
+Meunier
+Martinez
+Blanchard
+Brun
+Riviere
+Lucas
+Joly
+Giraud
+Brunet
+Gaillard
+Barbier
+Gerard
+Arnaud
+Renard
+Roche
+Schmitt
+Roy
+Leroux
+Caron
+Colin
+Vidal
+Picard
+Roger
+Fabre
+Aubert
+Lemoine
+Renaud
+Dumas
+Payet
+Olivier
+Lacroix
+Philippe
+Pierre
+Bourgeois
+Lopez
+Benoit
+Leclerc
+Rey
+Leclercq
+Sanchez
+Lecomte
+Rolland
+Guillaume
+Jean
+Hubert
+Dupuy
+Carpentier
+Guillot
+Berger
+Perez
+Dupuis
+Louis
+Moulin
+Deschamps
+Vasseur
+Huet
+Boucher
+Fernandez
+Fleury
+Adam
+Royer
+Paris
+Jacquet
+Klein
+Poirier
+Charles
+Aubry
+Guyot
+Carre
+Renault
+Menard
+Maillard
+Charpentier
+Marty
+Bertin
+Baron
+Da Silva
+Bailly
+Herve
+Schneider
+Le Gall
+Collet
+Leger
+Bouvier
+Julien
+Prevost
+Millet
+Le Roux
+Daniel
+Perrot
+Cousin
+Germain
+Breton
+Rodriguez
+Langlois
+Remy
+Besson
+Leveque
+Le Goff
+Pelletier
+Leblanc
+Barre
+Lebrun
+Grondin
+Perrier
+Marchal
+Weber
+Boulanger
+Mallet
+Hamon
+Jacob
+Monnier
+Michaud
+Guichard
+Poulain
+Etienne
+Gillet
+Hoarau
+Tessier
+Chevallier
+Collin
+Lemaitre
+Benard
+Chauvin
+Bouchet
+Marechal
+Gay
+Humbert
+Gonzalez
+Antoine
+Perret
+Reynaud
+Cordier
+Lejeune
+Barthelemy
+Delaunay
+Carlier
+Pichon
+Pasquier
+Lamy
+Gilbert
+Pereira
+Maillot
+Briand
+Alexandre
+Laporte
+Ollivier
+Buisson
+Gros
+Legros
+Besnard
+Georges
+Guillou
+Delattre
+Coulon
+Hebert
+Albert
+Launay
+Lesage
+Camus
+Voisin
+Pons
+Didier
+Ferreira
+Blanchet
+Vallee
+Jacques
+Martel
+Bigot
+Barbe
+Coste
+Charrier
+Sauvage
+Bousquet
+Guillet
+Leduc
+Pascal
+Maury
+Raynaud
+Verdier
+Mahe
+Lebon
+Lelievre
+Lebreton
+Gregoire
+Joubert
+Masse
+Pineau
+Gomez
+Morvan
+Delmas
+Gaudin
+Tanguy
+Colas
+Paul
+Raymond
+Guillon
+Regnier
+Hardy
+Imbert
+Brunel
+Devaux
+Courtois
+Ferrand
+Blondel
+Bodin
+Allard
+Lenoir
+Laine
+Delorme
+Berthelot
+Chauvet
+Seguin
+Bonneau
+Rodrigues
+Clerc
+Riou
+Thibault
+Hoareau
+Lacombe
+Dos Santos
+Godard
+Vaillant
+Lagarde
+Couturier
+Valentin
+Bruneau
+Turpin
+Marin
+Blin
+Jourdan
+Lombard
+Fernandes
+Rossi
+Evrard
+Mary
+Guilbert
+Texier
+Baudry
+Marion
+Pages
+Allain
+Maurice
+Dupre
+Bourdon
+Lefort
+Legendre
+Duhamel
+Chartier
+Gilles
+Loiseau
+Laroche
+Lacoste
+Goncalves
+Toussaint
+Hernandez
+Rousset
+Fischer
+Normand
+Maillet
+Wagner
+Guibert
+Labbe
+Bazin
+Leconte
+Rocher
+Bonnin
+Grenier
+Pruvost
+Jacquot
+Peltier
+Descamps
+Merle
+Auger
+Valette
+Pottier
+Vallet
+Parent
+Potier
+Delahaye
+Joseph
+Boutin
+Martineau
+Chauveau
+Peron
+Neveu
+Lemonnier
+Blot
+Vial
+Ruiz
+Delage
+Petitjean
+Maurin
+Faivre
+Chretien
+Levy
+Guyon
+Fouquet
+Mace
+Becker
+Foucher
+Lafon
+Favre
+Cros
+Bouvet
+Gallet
+Salmon
+Bernier
+Serre
+Dijoux
+Le Corre
+Begue
+Charbonnier
+Delannoy
+Martins
+Rossignol
+Jourdain
+Lecoq
+Cornu
+Thierry
+Prigent
+Weiss
+Parmentier
+Girault
+Andrieu
+Boulay
+Samson
+Castel
+Guy
+Maurel
+Stephan
+Laborde
+Duclos
+Gervais
+Gras
+Hamel
+Chapuis
+Poncet
+Doucet
+Chambon
+Prevot
+Letellier
+Gosselin
+Bonhomme
+Sabatier
+Besse
+Leblond
+Leleu
+Berthier
+Grandjean
+Bellanger
+Benoist
+Favier
+Grand
+Martinet
+Comte
+Rault
+Billard
+Boulet
+Poisson
+Drouet
+Forestier
+Blondeau
+Geoffroy
+Pommier
+Maire
+Navarro
+Ricard
+Ledoux
+Roques
+Gueguen
+Leonard
+Huguet
+Mounier
+Ferre
+Picot
+Morand
+Fortin
+Combes
+Brossard
+Dubreuil
+Hoffmann
+Techer
+Jeanne
+Merlin
+Rigaud
+Bauer
+Brault
+Prat
+Bocquet
+Granger
+Mouton
+Laval
+Le Roy
+Marquet
+Marc
+Claude
+Levasseur
+Chatelain
+Constant
+Guillemin
+Lallemand
+Lavigne
+Pujol
+Lacour
+Tellier
+Jung
+Rose
+Provost
+Da Costa
+Basset
+Salaun
+Jamet
+Lepage
+Costa
+Gibert
+Grange
+Bouquet
+Walter
+Keller
+Jolly
+Lelong
+Leon
+Dujardin
+Papin
+Bataille
+Tournier
+Guillard
+Cartier
+Cadet
+Le Guen
+Flament
+Champion
+Dumoulin
+Lopes
+Schmidt
+Husson
+Nguyen
+Le Bihan
+Bourdin
+Millot
+Gicquel
+Marques
+Ferry
+Lasserre
+Barret
+Kieffer
+Mas
+Bureau
+Mangin
+Leray
+Mignot
+Bouchard
+Savary
+Foulon
+Soulier
+Gimenez
+Barreau
+Fort
+Guillemot
+Blaise
+Felix
+Cohen
+Thiebaut
+Binet
+Jolivet
+Lafont
+Armand
+Lefrancois
+Saunier
+Jullien
+Montagne
+Berard
+Lauret
+Payen
+Vacher
+Sellier
+Jouan
+Dupin
+Andrieux
+Lecocq
+Berthet
+Lagrange
+Lebeau
+Cornet
+Zimmermann
+Schwartz
+Esnault
+Godefroy
+Ducrocq
+Poulet
+Lang
+Gomes
+Vivier
+Lemarchand
+Terrier
+Lalanne
+Vigneron
+Lavergne
+Combe
+Granier
+Bon
+Dore
+Le Borgne
+Thiery
+Sarrazin
+Bayle
+Ribeiro
+Aubin
+Thery
+Prieur
+Jacquemin
+Lamotte
+Arnould
+Vernet
+Corre
+Le Floch
+Bordes
+Jouve
+Bastien
+Faucher
+Cochet
+Chevrier
+Duchene
+Quere
+Couderc
+Magnier
+Prost
+Bois
+Villard
+Lefranc
+Caillaud
+Monier
+Beaumont
+Tardy
+Grosjean
+Le Gal
+Bonnard
+Guignard
+Moreno
+Rouxel
+Le Berre
+Lesueur
+Lefeuvre
+Tissier
+Crepin
+Sergent
+Wolff
+Lecuyer
+Thebault
+Alves
+Bonin
+Tavernier
+Roth
+Le Bris
+Pasquet
+Cardon
+Josse
+Jarry
+Guegan
+Duret
+Labat
+Bonnefoy
+Goujon
+Abadie
+Grimaud
+Villain
+Ragot
+Chollet
+Salles
+Pollet
+Oger
+Baudin
+Baudouin
+Forest
+Beaufils
+Boutet
+Godin
+Deshayes
+Diaz
+Derrien
+Avril
+Maitre
+Baudet
+Rigal
+Brochard
+Hue
+Monnet
+Boudet
+Bouche
+Lassalle
+Pierron
+Morice
+Tissot
+Godet
+Lepretre
+Delaporte
+Beck
+Bourguignon
+Arnoux
+Vannier
+Boivin
+Guitton
+Rio
+Sicard
+Pain
+Belin
+Michon
+Carrere
+Magne
+Chabert
+Bailleul
+Porte
+Piquet
+Berton
+Le Meur
+Parisot
+Boisson
+Foucault
+Le Bras
+George
+Pelissier
+Leclere
+Salomon
+Pepin
+Thuillier
+Galland
+Rambaud
+Proust
+Jacquin
+Monteil
+Torres
+Gonthier
+Rivet
+Roland
+Guilbaud
+Borel
+Raynal
+Clain
+Guiraud
+Simonet
+Louvet
+Marais
+Froment
+Vigier
+Pouget
+Baud
+Mauger
+Barriere
+Moine
+Lapeyre
+Thevenin
+Pinel
+Saulnier
+Astier
+Senechal
+Courtin
+Renou
+Coudert
+Carriere
+Gabriel
+Blandin
+Tisserand
+Gillot
+Munier
+Bourgoin
+Perron
+Paquet
+Puech
+Thevenet
+Romain
+Munoz
+Pellerin
+Jan
+Mayer
+Lebas
+Charlot
+Geffroy
+Garreau
+Bontemps
+Gaubert
+Varin
+Duchemin
+Rondeau
+Caillet
+Jaouen
+Tison
+Verger
+Billon
+Bruno
+Dubourg
+Duchesne
+Alix
+Bisson
+Chopin
+Hamelin
+Rougier
+Fayolle
+Charlet
+Pichard
+Villeneuve
+Duprat
+Marteau
+Dejean
+Briere
+Chabot
+Nicolle
+Chateau
+Diot
+Ferrier
+Boisseau
+Burel
+Verrier
+Pinto
+Babin
+Auvray
+Berthe
+Landais
+Simonin
+De Oliveira
+Dubos
+Lapierre
+Ferrari
+Baudoin
+Veron
+Chiron
+Capelle
+Costes
+Gil
+Fourcade
+Charron
+Viaud
+Teyssier
+Baptiste
+Michelet
+Dupouy
+Page
+Quentin
+Larcher
+Portier
+Genin
+Clavel
+Redon
+Manceau
+Devos
+Le Breton
+Brousse
+Janvier
+Prudhomme
+Bresson
+Bardet
+Constantin
+Metayer
+Christophe
+Chemin
+Bruyere
+Gross
+Bour
+Bossard
+Cheron
+Gobert
+Paillard
+Soulie
+Vigouroux
+Robinet
+Herault
+Larue
+Mille
+Caille
+Bastide
+Calvet
+Schaeffer
+Fuchs
+Bouyer
+Jeannin
+Mathis
+Vergne
+Cariou
+Auffret
+Delarue
+Cormier
+Soulard
+De Sousa
+Darras
+Lhomme
+Choquet
+Gourdon
+Tixier
+Drouin
+Bosc
+Guilloux
+Latour
+Simonnet
+Loisel
+Fritsch
+Beauvais
+Teixeira
+Coutant
+Delpech
+Ollier
+Besnier
+Delhaye
+Koch
+Menager
+Bardin
+Chapelle
+Merlet
+Barthe
+Renaudin
+Faye
+Dubus
+Honore
+Crouzet
+Gosset
+Gasnier
+Miquel
+Lacaze
+Delcroix
+Loison
+Lamarque
+Jardin
+Carton
+Malet
+Laplace
+Lamour
+Gaultier
+Dias
+Goupil
+Serres
+Cellier
+Peter
+Gallois
+Durieux
+Jegou
+Pichot
+Leloup
+Billet
+Braun
+Fauvel
+Rousselle
+Claudel
+Bouillon
+Viard
+Barraud
+Langlais
+Reboul
+Lecerf
+Fevrier
+Lepine
+Janin
+Poirot
+Guyard
+Rollet
+Legay
+Olive
+Boulard
+Laffont
+Cottin
+Chardon
+Lavaud
+Magnin
+Mahieu
+Viala
+Philippon
+Delcourt
+Langlet
+Lemercier
+Piot
+Hemery
+Genet
+Beau
+Bidault
+Collignon
+Devillers
+Villette
+Rouault
+Raimbault
+Moret
+Rollin
+Dauphin
+Desbois
+Lafond
+Cuny
+Bellet
+Brisset
+Conte
+Cazenave
+Marcel
+Casanova
+Delamare
+Garcin
+Toutain
+Lahaye
+Michaux
+Baillet
+Collard
+Remond
+Santiago
+Ouvrard
+Landry
+Varlet
+James
+Bonnot
+Mazet
+Capron
+Frey
+Demay
+Genty
+Auge
+Prunier
+Bachelet
+Damour
+Soler
+Lalande
+Gaucher
+Guery
+Hilaire
+Huard
+Pernot
+Ramos
+Le Brun
+Bellec
+Charton
+Duriez
+Dubost
+Claverie
+Demange
+Murat
+Trouve
+Rodier
+Nedelec
+Lehmann
+Deville
+Amiot
+Lenfant
+Pinson
+Achard
+Ferrer
+Abraham
+Renaux
+Le Moal
+Conan
+Lallement
+Sorin
+Peyre
+Haas
+Lienard
+Traore
+Georget
+Esteve
+Viel
+Thibaut
+Billaud
+Leriche
+Pierrot
+Salle
+Bach
+Raffin
+Cosson
+Even
+Fremont
+Forget
+Barrier
+Barbet
+Cuvelier
+Arnal
+Diallo
+Lafitte
+Bonnaud
+Bouchez
+Beraud
+Pierson
+Bourbon
+Fraisse
+Bret
+Thibaud
+Bouton
+Hugon
+Paulin
+Porcher
+Mathe
+Reymond
+Chaillou
+Madec
+Bouet
+Busson
+Lenormand
+Duc
+Naudin
+Froger
+Vernier
+Jeannot
diff --git a/tests/demo/prenoms-f.txt b/tests/demo/prenoms-f.txt
new file mode 100644
index 0000000000000000000000000000000000000000..cfda8438fe0fef78ac1230cb491c351984eed7c9
--- /dev/null
+++ b/tests/demo/prenoms-f.txt
@@ -0,0 +1,32 @@
+Marie
+Jeanne
+Françoise
+Rose
+Catherine
+Nathalie
+Isabelle
+Jacqueline
+Anne
+Sylvie
+Martine
+Madeleine
+Nicole
+Suzanne
+Hélène
+Christine
+Marguerite
+Denise
+Louise
+Claire
+Lucie
+Valérie
+Sophie
+Stéphanie
+Julien
+Céline
+Véronique
+Chantal
+Frédérique
+Alice
+Julie
+Simone
diff --git a/tests/demo/prenoms-h.txt b/tests/demo/prenoms-h.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b3ddbb618c7b68c8b7c2d1ba2d86533a0f7c71d7
--- /dev/null
+++ b/tests/demo/prenoms-h.txt
@@ -0,0 +1,32 @@
+Jean
+Pierre
+Michel
+André
+Philippe
+René
+Louis
+Alain
+Jacques
+Bernard
+Marcel
+Daniel
+Roger
+Robert
+Paul
+Claude
+Bruno
+Henri
+Georges
+Nicolas
+François
+Patrick
+Gérard
+Emmanuel
+Joseph
+Julien
+Maurice
+Laurent
+Frédéric
+Eric
+David
+
diff --git a/tests/demo/prenoms.txt b/tests/demo/prenoms.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4996ffbdcd2bf6dba4bcfe8dc42aa46f4de2174d
--- /dev/null
+++ b/tests/demo/prenoms.txt
@@ -0,0 +1,61 @@
+Marie
+Jean
+Pierre
+Jeanne
+Michel
+Françoise
+André
+Rose
+Philippe
+Catherine
+René
+Nathalie
+Louis
+Isabelle
+Alain
+Jacqueline
+Jacques
+Anne
+Bernard
+Sylvie
+Marcel
+Martine
+Daniel
+Madeleine
+Roger
+Nicole
+Robert
+Suzanne
+Paul
+Hélène
+Claude
+Christine
+Bruno
+Marguerite
+Henri
+Denise
+Georges
+Louise
+Nicolas
+Claire
+François
+Lucie
+Patrick
+Valérie
+Gérard
+Sophie
+Emmanuel
+Joseph
+Stéphanie
+Julien
+Céline
+Maurice
+Véronique
+Laurent
+Chantal
+Frédéric
+Alice
+Eric
+Julie
+David
+Simone
diff --git a/tests/splinter/README.md b/tests/splinter/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..56f36ef18f382d85c05f5d33ba9927b6866bee22
--- /dev/null
+++ b/tests/splinter/README.md
@@ -0,0 +1,38 @@
+
+# Tests avec splinter (expérimental)
+
+<http://splinter.cobrateam.info/docs/tutorial.html>
+
+## Installation de Splinter
+
+```
+apt-get install python-dev
+apt-get install libxslt-dev
+apt-get install libxml2-dev
+apt-get install python-lxml python-cssselect
+
+
+/opt/zope213/bin/easy_install zope.testbrowser
+/opt/zope213/bin/easy_install cssselect
+/opt/zope213/bin/easy_install splinter
+```
+
+
+J'ai du hacker `_mechanize.py`, ligne 218
+
+```
+vi +218 /opt/zope213/lib/python2.7/site-packages/mechanize-0.2.5-py2.7.egg/mechanize/_mechanize.py
+
+url = _rfc3986.urljoin(self._response.geturl()+'/', url)
+```
+
+(ajouter le + '/')
+
+### Essais:
+    /opt/zope213/bin/python common.py  
+
+ne doit pas déclencher d'erreur.
+
+
+
+
diff --git a/tests/splinter/common.py b/tests/splinter/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..788551cdc33f2f3b0e3af99b28636c5dce964f4a
--- /dev/null
+++ b/tests/splinter/common.py
@@ -0,0 +1,49 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Partie commune:
+
+se connecte et accede a la page d'accueil du premier departement
+"""
+from splinter import Browser
+import re, sys, time
+import urlparse
+import pdb
+from optparse import OptionParser
+
+from conn_info import *
+
+parser = OptionParser()
+parser.add_option("-d", "--dept", dest="dept_index", default=0, help="indice du departement")
+options, args = parser.parse_args()
+
+dept_index = int(options.dept_index)
+
+t0 = time.time()
+browser = Browser('zope.testbrowser')
+browser._browser.mech_browser.set_handle_robots(False) # must ignore ScoDoc robots.txt
+browser.visit(SCODOC)
+print 'Start: title:', browser.title
+print 'URL: ', browser.url
+# print browser.html
+
+links = browser.find_link_by_partial_text('Scolarit')
+print '%d departements' % len(links)
+
+links[dept_index].click() # va sur le premier departement
+
+# ---- Formulaire authentification
+print 'Authentification: ', browser.url
+
+browser.fill('__ac_name', USER)
+browser.fill('__ac_password', PASSWD)
+button = browser.find_by_id('submit')
+button[0].click()
+
+# ---- Page accueil Dept
+print browser.url
+
+links = browser.find_link_by_partial_text('DUT')
+links[0].click()
+print 'Starting test from %s' % browser.url
+print browser.title
diff --git a/tests/splinter/conn_info.py b/tests/splinter/conn_info.py
new file mode 100644
index 0000000000000000000000000000000000000000..d827308d9d982af16373a77c1408f1964114412f
--- /dev/null
+++ b/tests/splinter/conn_info.py
@@ -0,0 +1,10 @@
+
+"""Put here the informations neeede to connect to your development test user
+"""
+
+#SCODOC='https://scodoc.example.com/'
+SCODOC='https://scodoc.viennet.net/'
+USER = 'tester'
+#PASSWD = 'XXXXXXXXXXXX'
+PASSWD = '67un^:653'
+
diff --git a/tests/splinter/test-0.py b/tests/splinter/test-0.py
new file mode 100644
index 0000000000000000000000000000000000000000..eb49864df3782c82edff059bfffd64174617c5d2
--- /dev/null
+++ b/tests/splinter/test-0.py
@@ -0,0 +1,51 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Essais de base:
+
+se connecte, accede a un semestre puis a un module,
+et modifie les notes existantes dans la premiere evaluation
+
+"""
+
+from common import *
+# ici on est sur la page d'accueil du departement !
+
+links = browser.find_link_by_partial_text('DUT informatique en FI')
+links[0].click()
+
+# ---- Tableau bord semestre
+print browser.url
+# va dans module AP2 saisir des notes (dans la p1ere evaluation):
+browser.find_link_by_partial_text('AP1').first.click()
+browser.find_link_by_partial_text('Saisir notes').first.click()
+
+# ---- Ici c'est complique car le bouton submit est disabled
+# on construit l'url a la main:
+url = browser.find_by_id('gr')[0]["action"]
+evaluation_id = browser.find_by_name('evaluation_id').value
+group_id = re.search( r'value="(.*?)".*?tous', browser.html ).group(1)
+dest = urlparse.urljoin(url, 'notes_evaluation_formnotes?evaluation_id='+evaluation_id+'&group_ids:list='+group_id+'&note_method=form')
+browser.visit(dest)
+
+# ---- Change une note:
+# browser.fill('note_EID3835', '15')
+etudids = re.findall( r'name="note_(.*?)"', browser.html )[1:]
+note_max = float(re.search( r'notes sur ([0-9]+?)</span>\)', browser.html ).group(1))
+for etudid in etudids:
+    # essaie d'ajouter 1 à la note !
+    old_val = browser.find_by_name('note_%s' % etudid).value
+    try:
+        val = min(float(old_val) + 1, note_max)
+        browser.fill('note_%s'%etudid, str(val))
+        print etudid, old_val, '->', val
+    except:
+        pass
+
+# ... et met la derniere au max (pour tester)
+browser.fill('note_%s'%etudids[-1], str(note_max))
+print etudids[-1], '->', note_max
+
+# ---- Validation formulaire saisie notes:
+browser.find_by_id('tf_submit').click()
+browser.find_by_id('tf_submit').click()
diff --git a/tests/splinter/test-intensive-changes.py b/tests/splinter/test-intensive-changes.py
new file mode 100755
index 0000000000000000000000000000000000000000..812eca5f678118902cd2e63ebfa721f6445f8987
--- /dev/null
+++ b/tests/splinter/test-intensive-changes.py
@@ -0,0 +1,66 @@
+#!/opt/zope213/bin/python
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Essais de changements intensifs des notes
+   (pour faire des tests en parallele)
+
+se connecte, accede a un semestre puis a un module,
+et modifie les notes existantes dans la premiere evaluation
+
+ajoute puis soustrait 1 aux notes valides, N fois
+
+"""
+import time
+
+from common import *
+# -> ici on est sur la page d'accueil du departement !
+
+links = browser.find_link_by_partial_text('DUT')
+links[0].click() # va sur le 1er semestre de DUT trouve
+
+# ---- Tableau bord semestre
+print browser.url
+# va dans module M1101 saisir des notes (dans la p1ere evaluation):
+browser.find_link_by_partial_text('M1101').first.click()
+browser.find_link_by_partial_text('Saisir notes').first.click()
+
+# ---- Ici c'est complique car le bouton submit est disabled
+# on construit l'url a la main:
+url = browser.find_by_id('gr')[0]["action"]
+evaluation_id = browser.find_by_name('evaluation_id').value
+group_id = re.search( r'value="(.*?)".*?tous', browser.html ).group(1)
+url_form = urlparse.urljoin(url, 'notes_evaluation_formnotes?evaluation_id='+evaluation_id+'&group_ids:list='+group_id+'&note_method=form')
+
+
+# ---- Ajoute une constante aux notes valides:
+# le browser doit etre sur le formulaire saisie note
+def add_to_notes(increment):
+    etudids = re.findall( r'name="note_(.*?)"', browser.html )[1:]
+    note_max = float(re.search( r'notes sur ([0-9]+?)</span>\)', browser.html ).group(1))
+    print 'add_to_notes: %d etudiants' % len(etudids)
+    for etudid in etudids:
+        # essaie d'ajouter 1 a la note !
+        old_val = browser.find_by_name('note_%s' % etudid).value
+        try:
+            val = max(0,min(float(old_val) + increment, note_max))
+            browser.fill('note_%s'%etudid, str(val))
+            print etudid, old_val, '->', val
+        except:
+            pass
+    
+    # ---- Validation formulaire saisie notes:
+    browser.find_by_id('tf_submit').click()
+    browser.find_by_id('tf_submit').click()
+
+
+for i in range(10):
+    browser.visit(url_form) # va sur form saisie notes
+    add_to_notes(1)
+    #time.sleep(1)
+    browser.visit(url_form) # va sur form saisie notes
+    add_to_notes(-1)
+    #time.sleep(1)
+
+t1 = time.time()
+print 'done in %gs' % (t1-t0)
diff --git a/tests/splinter/test-jury.py b/tests/splinter/test-jury.py
new file mode 100644
index 0000000000000000000000000000000000000000..63c8f9d72a32bc29a110d9c93a48fc36eb9c60f8
--- /dev/null
+++ b/tests/splinter/test-jury.py
@@ -0,0 +1,44 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Modification decision de jury
+"""
+from common import *
+# -> ici on est sur la page d'accueil du departement !
+DeptURL = browser.url
+
+# Cherche un formsemestre_id:
+links = browser.find_link_by_partial_text('DUT')
+u = links[0]['href']
+formsemestre_id = re.search( r'formsemestre_id=(SEM[0-9]*)', u ).group(1)
+
+# Cherche les etudids
+browser.visit( urlparse.urljoin(DeptURL, 'formsemestre_recapcomplet?modejury=1&hidemodules=1&formsemestre_id=' + formsemestre_id) )
+
+#u = browser.find_link_by_partial_href('formsemestre_bulletinetud')[0]['href']
+#etudid = re.search( r'etudid=([A-Za-z0-9]*)', u ).group(1)
+
+L = browser.find_link_by_partial_href('formsemestre_bulletinetud')
+etudids = [ re.search(r'etudid=([A-Za-z0-9_]*)', x['href']).group(1) for x in L ]
+
+def suppress_then_set( etudid, formsemestre_id, code='ADM' ):
+    """Supprime decision de jury pour cet étudiant dans ce semestre
+    puis saisie de la decision (manuelle) indiquée par code
+    """
+    # Suppression décision existante
+    browser.visit( urlparse.urljoin(DeptURL, 'formsemestre_validation_suppress_etud?etudid=%s&formsemestre_id=%s&dialog_confirmed=1' % (etudid, formsemestre_id)))
+    
+    # Saisie décision
+    browser.visit( urlparse.urljoin(DeptURL, 'formsemestre_validation_etud_form?etudid=%s&formsemestre_id=%s' % (etudid, formsemestre_id)))
+    browser.fill('code_etat', [code])
+    browser.find_by_name('formvalidmanu_submit').first.click()
+    # pas de verification de la page résultat
+
+# Change decisions de jury de tous les étudiants:
+for etudid in etudids:
+    print 'decision pour %s' % etudid
+    suppress_then_set( etudid, formsemestre_id, code='ADM')
+
+t1 = time.time()
+print '%d etudiants traites en %gs' % (len(etudids),t1-t0)
+
diff --git a/tests/test-all-moys.py b/tests/test-all-moys.py
new file mode 100644
index 0000000000000000000000000000000000000000..50bfddc97c6a7959de01bd16aa262cff62e7929b
--- /dev/null
+++ b/tests/test-all-moys.py
@@ -0,0 +1,45 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""
+Enregistre les moyennes générales de tous les étudinats de tous les
+semestres.
+A utiliser avec debug.py (côté serveur).
+"""
+
+
+from __future__ import print_function
+from debug import *
+import time
+
+DeptName = 'CJ'
+context = go_dept(app, DeptName)
+
+sems = context.Notes.formsemestre_list()
+
+print( '%d semestres' % len(sems) )
+
+L=[]
+n=0
+for sem in sems:
+    formsemestre_id = sem['formsemestre_id']
+    nt = context.Notes._getNotesCache().get_NotesTable(context.Notes, formsemestre_id)
+    etudids = nt.get_etudids()
+    use_ue_coef = context.get_preference( 'use_ue_coefs', formsemestre_id )
+    n += 1
+    print("%d %s (%d) use_ue_coef=%s" % (n, formsemestre_id, len(etudids), use_ue_coef))
+    for etudid in etudids:
+        mg = nt.get_etud_moy_gen(etudid)
+        L.append( (formsemestre_id, str(use_ue_coef), etudid, str(mg)) )
+
+print("Done: %s moys computed" % len(L))
+
+filename='/opt/tests/%s-%s' % (DeptName, time.strftime('%Y-%m-%dT%H:%M:%S'))
+print("Writing file '%s'..." % filename)
+f = open(filename, 'w')
+for l in L:
+    f.write('\t'.join(l) + '\n')
+
+f.close()
+
+
diff --git a/tests/test-apo-csv.py b/tests/test-apo-csv.py
new file mode 100644
index 0000000000000000000000000000000000000000..1743d2d09407dda71585fa9f5a5a882205cd707e
--- /dev/null
+++ b/tests/test-apo-csv.py
@@ -0,0 +1,64 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Petits essais sur les fichiers CSV Apogée
+
+Utiliser avec 
+     /opt/scodoc/bin/zopectl debug 
+
+"""
+from __future__ import print_function
+from debug import *
+
+import sco_apogee_csv
+import sco_apogee_compare
+
+#data = open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2018-2/2019-09-23-15-46-40/V2RT2!116.csv', 'r').read()
+#data = open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2018-1/2019-02-20-11-53-05/V2RT!116.csv', 'r').read()
+data = open('/tmp/V2RT116.csv', 'r').read()
+A = sco_apogee_csv.ApoData(data)
+data = open('/tmp/V2RT116-modif.csv', 'r').read()
+B = sco_apogee_csv.ApoData(data)
+sco_apogee_compare.compare_etuds_res(A, B)
+
+
+
+
+A.col_ids
+# -> ['apoL_a01_code', 'apoL_a02_nom', 'apoL_a03_prenom', 'apoL_a04_naissance', 'apoL_c0001', 'apoL_c0002', 'apoL_c0003', 'apoL_c0004']
+
+e = A.etuds[0]
+pp(e.cols)
+# {'apoL_a01_code': '11809768',
+#  'apoL_a02_nom': 'AKYOL',
+#  'apoL_a03_prenom': 'OLIVIER',
+#  'apoL_a04_naissance': ' 31/01/1999',
+#  'apoL_c0001': '',
+#  'apoL_c0002': '',
+#  ... }
+
+A.apo_elts.keys()
+# ['VRTW4', 'VRTW3', 'VRTU42', 'VRTU41', 'VRTU32', ... ]
+elt = A.apo_elts['VRT3101']
+
+elt.code # 'VRT3102'
+
+B = sco_apogee_csv.ApoData( open('/opt/tests/V2RT-modif.csv').read() )
+
+# les colonnes de l'élément
+col_ids = [ ec['apoL_a01_code'] for ec in elt.cols ]
+e.cols['apoL_c0033']
+
+common_nips = set([e["nip"] for e in A.etuds])
+A.etud_by_nip.keys()
+
+B_etud_by_nip = { e["nip"] : e for e in B.etuds }
+
+d = build_etud_res(B.etuds[0], B)
+
+
+    
+    
+
+
+
diff --git a/tests/test-apo-export-impairs.py b/tests/test-apo-export-impairs.py
new file mode 100644
index 0000000000000000000000000000000000000000..9025045f294bb961c7f8c1d198f7fd068f92120d
--- /dev/null
+++ b/tests/test-apo-export-impairs.py
@@ -0,0 +1,46 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+"""Essai export des semestres impairs avec VET seulement pour les diplômés
+
+Utiliser avec 
+     /opt/scodoc/bin/zopectl debug 
+"""
+
+from __future__ import print_function
+from debug import *
+
+import sco_apogee_csv
+import sco_parcours_dut
+
+context = go_dept(app, 'RT').Notes
+
+etudid='EID33751'
+formsemestre_id='SEM37099'
+
+etud = context.getEtudInfo(etudid=etudid, filled=True)[0]
+Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id)
+
+print(Se.all_other_validated())
+
+
+data = open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2019-1/2020-07-07-17-24-18/V2RT!117.csv', 'r').read()
+
+apo_data = sco_apogee_csv.ApoData(data, periode=1, export_res_etape=False)
+
+apo_data.setup(context)
+
+ix = [ x['nom'] for x in apo_data.etuds ].index('HAMILA')
+
+e  = apo_data.etuds[ix]
+e.lookup_scodoc(context, apo_data.etape_formsemestre_ids)
+
+e.associate_sco(context, apo_data)
+
+pp({ k :  e.new_cols[k] for k in e.new_cols if e.new_cols[k] != '' })
+
+    
+    
+
+
+
diff --git a/tests/testpyexcelerator.py b/tests/testpyexcelerator.py
new file mode 100644
index 0000000000000000000000000000000000000000..543829f788efbd2f26a083a6f0ae9d0f07ebf0e9
--- /dev/null
+++ b/tests/testpyexcelerator.py
@@ -0,0 +1,19 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+import sys
+from pyExcelerator import *
+
+#UnicodeUtils.DEFAULT_ENCODING = 'utf-8'
+
+wb = Workbook()
+
+title = "Essai"
+
+ws = wb.add_sheet(u"çelé")
+
+ws.write(1,1, "çeci où".decode('utf-8'))
+ws.write(2,2, "Hélène".decode('utf-8'))
+
+wb.save("toto.xls")
+